Source code of a graphical tool for drawing and computing distances over Google maps.
Run Tool | index.html | main.css | formatters.js | geoCircle.js | geoCode.js | geo.js | index.js | mapControls.js | tableManager.js | util.js | wayPoint.js | wayPointsManager.js
// Copyright 2006-2008 (c) Paul Demers <paul@acscdg.com>
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA., or visit one
// of the links here:
// http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
// http://www.acscdg.com/LICENSE.txt
//////////////////////////////////////////////////////////////////
//
// JavaScript geography functions.
// Computes great circle courses, rhumb line courses, and
// rhumb line approximations to great circles.
// Web site with this code running: http://www.acscdg.com/
//
// Dependency on other modules:
// Google maps (for geo point object).
// index.js for _optsNotClickable shared object.
// Angle constants.
var piOver4 = Math.PI / 4.0;
var twoPi = Math.PI * 2.0;
var piOver2 = Math.PI / 2.0;
var radiansToDegrees = 180.0 / Math.PI;
// Distance constants. From Bowditch.
var kilometersPerNM = 1.852;
var milesPerNM = 1.150779448;
// Drawing settings.
var vectorColor = "#ff0000";
var vectorWeight = 3; // Was 4
var vectorOpacity = 0.8; // Default is supposed to be 1.0
//
//// Enumerations to pass around the user's distance measure preference.
var NAUTICAL_MILES_UNITS = 0;
var MILES_UNITS = 1;
var KILOMETERS_UNITS = 2;
//
//// Converts distance measure enumeration to actual units.
function getDistanceMultiplier(distanceUnits)
{
var distanceMultiplier = 1.0;
if (distanceUnits == MILES_UNITS)
distanceMultiplier = milesPerNM;
else if (distanceUnits == KILOMETERS_UNITS)
distanceMultiplier = kilometersPerNM;
return distanceMultiplier;
}
//
//// Reduce an angle to (-PI/2, PI/2), for latitudes.
function reduceLat(lat) // old name: fng(x)
{
var reducedLat = lat - Math.PI * Math.floor((lat + piOver2) / Math.PI);
return reducedLat;
}
//
//// Reduce and angle to (-PI, PI), for longitudes.
function reduceLng(lng) // old name: fnl(x)
{
var reducedLng = lng - twoPi * Math.floor((lng + Math.PI ) / twoPi);
return reducedLng;
}
//
//// Reduce an angle to (0, 2*PI), for direction and azimuth.
function reduceAzimuth(azimuth)
{
var reducedAzimuth = azimuth - twoPi * Math.floor(azimuth / twoPi);
return reducedAzimuth;
}
////////////////// Earth Vector Object /////////////////////
// Debugging method.
//function earthVectorToString()
//{
// return new String("Distance (NM): " + this.distanceNM + ", Azimuth: " + this.azimuthDegrees);
//}
//
//// A vector object contains distance and azimuth.
//// Distance is in nautical miles, azimuth in radians (north is 0, east is PI/2)
function EarthVector(distanceNM, azimuth)
{
this.distanceNM = distanceNM;
this.azimuth = reduceAzimuth(azimuth);
this.azimuthDegrees = azimuth * radiansToDegrees;
// this.toString = earthVectorToString;
}
//////////////////////////
//
//// Utility getter method.
function getVector()
{
return this.vector;
}
//
//// Computes rhumbline course and distance. Round Earth formula, for speed.
//// Coordinate system: west is negative, south is negative.
//// Also called the rhumb line direct solution.
function computeRhumbLineVector()
{
this.deltaLng = this.endPoint.lngRadians() - this.startPoint.lngRadians();
var gb = Math.tan(piOver4 + (this.endPoint.latRadians() / 2.0));
var ga = Math.tan(piOver4 + (this.startPoint.latRadians() /2.0));
var X = Math.log(gb) - Math.log(ga);
var azimuth = Math.atan2(this.deltaLng, X);
var distanceNM = 0.0;
var cosineAzimuth = Math.cos(azimuth);
if (Math.abs(cosineAzimuth) > 0.0001)
{
var deltaLatDegrees = (this.endPoint.lat() - this.startPoint.lat());
distanceNM = 60.0 * deltaLatDegrees / cosineAzimuth; // NMi
}
else // Too close to straight east or straight west to use cosine.
{
var deltaLngDegrees = Math.abs(this.endPoint.lng() - this.startPoint.lng());
distanceNM = 60.0 * deltaLngDegrees * Math.cos(this.startPoint.lat());
}
this.vector = new EarthVector(distanceNM, azimuth);
}
//
//// Creates a polyline overlay object of a rhumbline course.
//// Assumes the line will overlay a mercator map.
function rhumbLineLine()
{
var rhLine = new GPolyline( [this.startPoint, this.endPoint],
vectorColor, vectorWeight, vectorOpacity, _optsNotClickable);
return rhLine;
}
//
//// Constructor for a rhumbline course object.
function RhumbLineCourse(startPoint, endPoint)
{
this.startPoint = startPoint;
this.endPoint = endPoint;
this.computeRhumbLineVector = computeRhumbLineVector;
this.getLine = rhumbLineLine;
this.getVector = getVector;
this.computeRhumbLineVector();
}
//
//// Computes great circle course and distance.
//// Round Earth formula, for speed.
//// Coordinate system: west is negative, south is negative.
//// Also called the great circle indirect solution.
function computeGreatCircleVector()
{
this.deltaLng = reduceLng(this.endPoint.lngRadians() - this.startPoint.lngRadians());
var sinDeltaLng = Math.sin(this.deltaLng);
var cosDeltaLng = Math.cos(this.deltaLng);
this.sinStartLat = Math.sin(this.startPoint.latRadians());
this.cosStartLat = Math.cos(this.startPoint.latRadians());
this.sinEndLat = Math.sin(this.endPoint.latRadians());
this.cosEndLat = Math.cos(this.endPoint.latRadians());
// This is the fastest formula for great circles, but is not the best.
// The best is the Haversine formula. But is much more computation.
this.distanceRadians = Math.acos(this.sinStartLat*this.sinEndLat +
this.cosStartLat*this.cosEndLat*cosDeltaLng);
this.distanceDegrees = radiansToDegrees * this.distanceRadians;
var distanceNM = 60.0 * this.distanceDegrees;
var Y = sinDeltaLng * this.cosEndLat;
var X = this.cosStartLat * this.sinEndLat - this.sinStartLat * this.cosEndLat * cosDeltaLng;
azimuth = Math.atan2(Y, X);
this.vector = new EarthVector(distanceNM, azimuth);
}
//
//// Get latitude from longitude along a great circle course
//// Needed to compute way points along a rhumbline approximation to a great circle.
//// Returns latitude (in radians)
function getLatFromLng(givenLng)
{
var deltaLng = givenLng - this.startPoint.lngRadians() ;
var sinDeltaLng = Math.sin(deltaLng);
var cosDeltaLng = Math.cos(deltaLng);
var Y = this.sinStartLat * cosDeltaLng * this.sinAzimuth + sinDeltaLng * this.cosAzimuth;
var X = this.cosStartLat * this.sinAzimuth;
var foundLat = reduceLat(Math.atan2(Y, X));
return(foundLat);
}
//
//// Computes the vertex of a great circle course.
//// The vertex is the point furthest north (or south).
//// Returns a Google LatLng object.
//// TODO: This doesn't always work. The vertex is on the wrong side
//// of the international date line when crossing from east to west.
function computeVertex()
{
if (this.sinAzimuth == 0.0)
return(new GLatLng(twoPi, this.startPoint.lngRadians()));
var vertexLng = reduceLng(
Math.atan(1.0/this.sinStartLat/Math.tan(this.vector.azimuth)) + this.startPoint.lngRadians());
var vertexLat = this.getLatFromLng(vertexLng);
this.vertex = new GLatLng(vertexLat*radiansToDegrees, vertexLng*radiansToDegrees);
//GLog.write("start: " + this.startPoint + " vertext: " + this.vertex);
}
//
//// Determines the points of the rhumbline approximation to a great circle course.
//// It works by moving a fixed number of longitude degrees, then computing the latitude
//// along the great circle at that longitude.
function createWayPoints()
{
this.sinAzimuth = Math.sin(this.vector.azimuth);
this.cosAzimuth = Math.cos(this.vector.azimuth);
this.longitudeIncrementRadians = this.longitudeIncrement/radiansToDegrees;
var numberOfPointsD = Math.abs(this.deltaLng)/this.longitudeIncrementRadians;
var numberOfPointsI = Math.floor(numberOfPointsD);
var wayPointsList = new Array();
wayPointsList.push(this.startPoint);
if (numberOfPointsI > 1)
{
if (this.deltaLng < 0.0)
this.longitudeIncrementRadians *= -1.0; // Always move west to east.
var nextPointNumber;
var lastLng = this.startPoint.lngRadians();
for (nextPointNumber = 0; nextPointNumber < numberOfPointsI; nextPointNumber++)
{
var nextLng = reduceLng(lastLng + this.longitudeIncrementRadians);
var nextLat = this.getLatFromLng(nextLng);
var nextPoint = new GLatLng(nextLat*radiansToDegrees, nextLng*radiansToDegrees);
wayPointsList.push(nextPoint);
lastLng = nextLng;
} // For loop.
} // If number of points > 1.
wayPointsList.push(this.endPoint);
var wayPointsLine = new GPolyline(wayPointsList,
vectorColor, vectorWeight, vectorOpacity, _optsNotClickable) ;
return(wayPointsLine);
}
//
//// TODO: Combine with rhumb line object. Have an abstract base, and two sub classes.
function GreatCircleCourse(startPoint, endPoint)
{
this.longitudeIncrement = 3; // TODO: Perhaps base increment on map scale?
this.startPoint = startPoint;
this.endPoint = endPoint;
this.computeGreatCircleVector = computeGreatCircleVector;
this.getLatFromLng = getLatFromLng;
this.computeVertex = computeVertex;
this.getLine = createWayPoints;
this.getVector = getVector;
this.computeGreatCircleVector();
}
////////////////////////// Best Course Object //////////////////////////
// The idea of best course is to use different algorithms based on the
// distance between the points. For example, the haversine formula is
// more accurate for small distances.
//
////
function bestCourseGetLine()
{
if (this.courseLine == null)
this.courseLine = this.course.getLine();
return(this.courseLine);
}
//
////
function bestCourseGetVector()
{
return(this.course.vector);
}
//
//// Chooses the best algorithm, based on distance between points.
function BestCourse(startPoint, endPoint)
{
this.getLine = bestCourseGetLine;
this.getVector = bestCourseGetVector;
// var minDeltaLng = 3;
// var deltaLng = reduceLng(startPoint.lngRadians() - endPoint.lngRadians());
// var deltaLngDegrees = Math.abs(deltaLng * radiansToDegrees);
// if (deltaLngDegrees < minDeltaLng)
// {
// The great circle formula used is very fast, but inaccurate
// for short distances.
// }
// else
// {
this.course = new GreatCircleCourse(startPoint, endPoint);
// }
}
//
//// Compute the great circle direct solution.
//// That is, given a start point, a distance, and a course (azimith),
//// compute the end point.
//// Returns a Google GLatLng object with the end point.
function directSolution(startPoint, azimuthR, distanceNM)
{
var distanceR = (distanceNM / 60) / radiansToDegrees;
var sinStartLat = Math.sin(startPoint.latRadians());
var cosStartLat = Math.cos(startPoint.latRadians());
var sinAzimuth = Math.sin(azimuthR);
var cosAzimuth = Math.cos(azimuthR);
var sinDistance = Math.sin(distanceR);
var cosDistance = Math.cos(distanceR);
var X = (cosStartLat * cosDistance) - (sinStartLat * cosAzimuth * sinDistance);
var Y = sinDistance * sinAzimuth;
var endLat = Math.asin((sinStartLat * cosDistance) + (cosStartLat * sinDistance * cosAzimuth));
var endLng = startPoint.lngRadians() - Math.atan2(Y, X);
// The Google contructor doesn't need reduced angles.
// endLat = reduceLat(endLat);
// endLng = reduceLng(endLng);
return(new GLatLng(endLat*radiansToDegrees, endLng*radiansToDegrees));
}