// PolylineEncoder.js copyright Mark McClure April/May 2007 // // This software is placed explicitly in the public // domain and may be freely distributed or modified. // No warranty express or implied is provided. // // History: // V 2.1 July 2007 // Minor modification in distance function to enhance // speed. Suggested by Joel Rosenberg. // V 2.0 May 2007. // Major revisions include: // Incorporation of Douglas-Peucker algorithm // Encapsulation into the PolylineEncoder package. // V 1.0 September 2006 // Original version based on simple vertex reduction // // This module defines a PolylineEncoder class to encode // polylines for use with Google Maps together with a few // auxiliary functions. Documentation at // http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/PolylineEncoder.html // // Google map reference including encoded polylines: // http://www.google.com/apis/maps/documentation/ // // Details on the algorithm used here: // http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/ // // Constructor: // polylineEncoder = new PolylineEncoder(numLevels, // zoomFactor, verySmall, forceEndpoints?); // where numLevels and zoomFactor indicate how many // different levels of magnification the polyline has // and the change in magnification between those levels, // verySmall indicates the length of a barely visible // object at the highest zoom level, forceEndpoints // indicates whether or not the endpoints should be // visible at all zoom levels. forceEndpoints is // optional with a default value of true. Probably // should stay true regardless. // // Main methods: // * PolylineEncoder.dpEncodeToPolyline(points, // color?, weight?, opacity?) // Accepts an array of latLng objects (see below) and // optional style specifications. Returns an encoded // polyline that may be directly overlayed on a Google // Map. Requires that the Google Maps API be loaded. // // * PolylineEncoder.dpEncodeToPolygon(pointsArray, // boundaryColor?, boundaryWeight?, boundaryOpacity?, // fillColor?, fillOpacity?, fill?, outline?) // Accepts an array of arrays latLng objects and // optional style specifications. Returns an encoded // polylgon that may be directly overlayed on a Google // Map. Requires that the Google Maps API be loaded. // // // Convenience classes and methods: // * PolylineEncoder.latLng // Constructor: // myLatLng = new PolylineEncoder.latLng(y,x); // The dpEncode* functions expect points in the // form of an object with lat and lng methods. A // GLatLng as defined by the Google Maps API does // quite nicely. If you're developing a javascript // without loading the API, however, you can use // a PolylineEncoder.latLng for this purpose. // // // PolylineEncoder.pointsToLatLngs // Sometimes your points are defined in terms of an // array of arrays, rather than an array of latLngs. // PolylineEncoder.pointsToLatLngs converts to an array // of arrays to an array of latLngs for use by the // dpEncode functions. // // // PolylineEncoder.pointsToGLatLngs // PolylineEncoder.pointsToGLatLngs is analagous to the // previous function, but it returns GLatLngs rather // than PolylineEncoder.latLngs. The first function may // be used independently of Google Maps. Use the second, // if you need to use the result in a Goole Map function. // // // Lower level methods // PolylineEncoder.dpEncodeToJSON(points, // color?, weight?, opacity?) // Returns a legal argument to GPolyline.fromEncoded. // // // PolylineEncoder.dpEncode(points); // This is where the real work is done. The return value // is a JSON object with properties named encodedLevels, // encdodedPoints and encodedPointsLiteral. These are // strings which are acceptable input to the points and // levels properties of the GPolyline.fromEncoded // function. The encodedPoints string should be used for // maps generated dynamically, while the // encodedPointsLiteral string should be copied into a // static document. // // The standard disclaimers, such as "use at your own risk, // since I really don't have any idea what I'm doing," apply. // The constructor PolylineEncoder = function(numLevels, zoomFactor, verySmall, forceEndpoints) { var i; if(!numLevels) { numLevels = 18; } if(!zoomFactor) { zoomFactor = 2; } if(!verySmall) { verySmall = 0.00001; } if(!forceEndpoints) { forceEndpoints = true; } this.numLevels = numLevels; this.zoomFactor = zoomFactor; this.verySmall = verySmall; this.forceEndpoints = forceEndpoints; this.zoomLevelBreaks = new Array(numLevels); for(i = 0; i < numLevels; i++) { this.zoomLevelBreaks[i] = verySmall*Math.pow(zoomFactor, numLevels-i-1); } } // The main function. Essentially the Douglas-Peucker // algorithm, adapted for encoding. Rather than simply // eliminating points, we record their from the // segment which occurs at that recursive step. These // distances are then easily converted to zoom levels. PolylineEncoder.prototype.dpEncode = function(points) { var absMaxDist = 0; var stack = []; var dists = new Array(points.length); var maxDist, maxLoc, temp, first, last, current; var i, encodedPoints, encodedLevels; var segmentLength; if(points.length > 2) { stack.push([0, points.length-1]); while(stack.length > 0) { current = stack.pop(); maxDist = 0; segmentLength = Math.pow(points[current[1]].lat()-points[current[0]].lat(),2) + Math.pow(points[current[1]].lng()-points[current[0]].lng(),2); for(i = current[0]+1; i < current[1]; i++) { temp = this.distance(points[i], points[current[0]], points[current[1]], segmentLength); if(temp > maxDist) { maxDist = temp; maxLoc = i; if(maxDist > absMaxDist) { absMaxDist = maxDist; } } } if(maxDist > this.verySmall) { dists[maxLoc] = maxDist; stack.push([current[0], maxLoc]); stack.push([maxLoc, current[1]]); } } } encodedPoints = this.createEncodings(points, dists); encodedLevels = this.encodeLevels(points, dists, absMaxDist); return { encodedPoints: encodedPoints, encodedLevels: encodedLevels, encodedPointsLiteral: encodedPoints.replace(/\\/g,"\\\\") } } PolylineEncoder.prototype.dpEncodeToJSON = function(points, color, weight, opacity) { var result; if(!opacity) { opacity = 0.9; } if(!weight) { weight = 3; } if(!color) { color = "#0000ff"; } result = this.dpEncode(points); return { color: color, weight: weight, opacity: opacity, points: result.encodedPoints, levels: result.encodedLevels, numLevels: this.numLevels, zoomFactor: this.zoomFactor } } PolylineEncoder.prototype.dpEncodeToGPolyline = function(points, color, weight, opacity) { if(!opacity) { opacity = 0.9; } if(!weight) { weight = 3; } if(!color) { color = "#0000ff"; } return new GPolyline.fromEncoded( this.dpEncodeToJSON(points, color, weight, opacity)); } PolylineEncoder.prototype.dpEncodeToGPolygon = function(pointsArray, boundaryColor, boundaryWeight, boundaryOpacity, fillColor, fillOpacity, fill, outline) { var i, boundaries; if(!boundaryColor) { boundaryColor = "#0000ff"; } if(!boundaryWeight) { boundaryWeight = 3; } if(!boundaryOpacity) { boundaryOpacity = 0.9; } if(!fillColor) { fillColor = boundaryColor; } if(!fillOpacity) { fillOpacity = boundaryOpacity/3; } if(fill==undefined) { fill = true; } if(outline==undefined) { outline = true; } boundaries = new Array(0); for(i=0; i= 1) { out = Math.sqrt(Math.pow(p0.lat() - p2.lat(),2) + Math.pow(p0.lng() - p2.lng(),2)); } if(0 < u && u < 1) { out = Math.sqrt(Math.pow(p0.lat()-p1.lat()-u*(p2.lat()-p1.lat()),2) + Math.pow(p0.lng()-p1.lng()-u*(p2.lng()-p1.lng()),2)); } } return out; } // The createEncodings function is very similar to Google's // http://www.google.com/apis/maps/documentation/polyline.js // The key difference is that not all points are encoded, // since some were eliminated by Douglas-Peucker. PolylineEncoder.prototype.createEncodings = function(points, dists) { var i, dlat, dlng; var plat = 0; var plng = 0; var encoded_points = ""; for(i = 0; i < points.length; i++) { if(dists[i] != undefined || i == 0 || i == points.length-1) { var point = points[i]; var lat = point.lat(); var lng = point.lng(); var late5 = Math.floor(lat * 1e5); var lnge5 = Math.floor(lng * 1e5); dlat = late5 - plat; dlng = lnge5 - plng; plat = late5; plng = lnge5; encoded_points += this.encodeSignedNumber(dlat) + this.encodeSignedNumber(dlng); } } return encoded_points; } // This computes the appropriate zoom level of a point in terms of it's // distance from the relevant segment in the DP algorithm. Could be done // in terms of a logarithm, but this approach makes it a bit easier to // ensure that the level is not too large. PolylineEncoder.prototype.computeLevel = function(dd) { var lev; if(dd > this.verySmall) { lev=0; while(dd < this.zoomLevelBreaks[lev]) { lev++; } return lev; } } // Now we can use the previous function to march down the list // of points and encode the levels. Like createEncodings, we // ignore points whose distance (in dists) is undefined. PolylineEncoder.prototype.encodeLevels = function(points, dists, absMaxDist) { var i; var encoded_levels = ""; if(this.forceEndpoints) { encoded_levels += this.encodeNumber(this.numLevels-1) } else { encoded_levels += this.encodeNumber( this.numLevels-this.computeLevel(absMaxDist)-1) } for(i=1; i < points.length-1; i++) { if(dists[i] != undefined) { encoded_levels += this.encodeNumber( this.numLevels-this.computeLevel(dists[i])-1); } } if(this.forceEndpoints) { encoded_levels += this.encodeNumber(this.numLevels-1) } else { encoded_levels += this.encodeNumber( this.numLevels-this.computeLevel(absMaxDist)-1) } return encoded_levels; } // This function is very similar to Google's, but I added // some stuff to deal with the double slash issue. PolylineEncoder.prototype.encodeNumber = function(num) { var encodeString = ""; var nextValue, finalValue; while (num >= 0x20) { nextValue = (0x20 | (num & 0x1f)) + 63; // if (nextValue == 92) { // encodeString += (String.fromCharCode(nextValue)); // } encodeString += (String.fromCharCode(nextValue)); num >>= 5; } finalValue = num + 63; // if (finalValue == 92) { // encodeString += (String.fromCharCode(finalValue)); // } encodeString += (String.fromCharCode(finalValue)); return encodeString; } // This one is Google's verbatim. PolylineEncoder.prototype.encodeSignedNumber = function(num) { var sgn_num = num << 1; if (num < 0) { sgn_num = ~(sgn_num); } return(this.encodeNumber(sgn_num)); } // The remaining code defines a few convenience utilities. // PolylineEncoder.latLng PolylineEncoder.latLng = function(y, x) { this.y = y; this.x = x; } PolylineEncoder.latLng.prototype.lat = function() { return this.y; } PolylineEncoder.latLng.prototype.lng = function() { return this.x; } // PolylineEncoder.pointsToLatLngs PolylineEncoder.pointsToLatLngs = function(points) { var i, latLngs; latLngs = new Array(0); for(i=0; i