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)); }