package net.thewebsitedoctor.gmap; import java.util.*; import org.w3c.dom.*; import org.apache.xerces.dom.*; /** * Support functions for Google Maps. * *

Provides static methods that can be called from * Xalan as a * Java extension * to support XSLT scripts * that generate data for input to the * Google Maps API.

* *

Copyright 2007 Walter O. Haas

* *

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 St, Fifth Floor, Boston, MA * 02110-1301 USA

* * @author Walter O. Haas * @version $Id: GMap2.java,v 1.2 2007/05/08 13:46:10 haas Exp $ * @see GMap2Test */ public class GMap2 { /** * minimum increment */ private static final double verySmall = 0.00001; private static double[] zoomLevelBreaks; /** * Stores one point on a polyline. */ static private class Point { /** * Latitude of the point on the earth's surface */ double lat; /** * Longitude of the point on the earth's surface */ double lon; /** * Distance from the point's enclosing line segment as * calculated by the Douglas-Peucker algorithm in * {@link GMap2#dpEncode} */ double dist = Double.NaN; /** * Constructor * @param lat Latitude * @param lon Longitude */ Point(double lat, double lon) { this.lat = lat; this.lon = lon; } } // private class Point /** * Defines a segment of a polyline as the indexes of the two * endpoints of the segment. */ static private class Segment { /** * First point of pair. Value from zero to last point in * line. */ int first; /** * Last point of pair. Value from first+1 to * last point in the line */ int last; /** * Constructor */ Segment(int first, int last) { this.first = first; this.last = last; } } // private class Segment /** * Encode polyline from a DOM tree. * *

This is a Java version of * Mark McClure's JavaScript program, * modified to be called as a Xalan Java extension. * The DOM tree format is *

          <path>
            <coordinates>lng,lat</coordinates>
            <coordinates>lng,lat</coordinates>
            <coordinates>lng,lat</coordinates>
            ...
          </path>
        
* where lng,lat is a longitude, latitude pair * describing a point on the earth's surface.

* @param nList DOM tree containing coordinates for polyline * @param zoomFactor Zoom size between levels * @param numLevels Number of zoom levels * @return DOM tree containing encoded points and levels */ public static Node encodePolyline(NodeList nList, int zoomFactor, int numLevels) throws GMap2BadArgException { Node root = nList.item(0); if (root == null) { throw new GMap2BadArgException(); } ArrayList points = new ArrayList(); // Table of level breakpoints zoomLevelBreaks = new double[numLevels]; for (int i=0; i < numLevels; i++) { zoomLevelBreaks[i] = verySmall * Math.pow(zoomFactor, numLevels-i-1); } // Walk the input DOM tree to find the coordinates of each // point of the polyline and store the coordinates in "points" if (root.hasChildNodes()) { NodeList nl = root.getChildNodes(); for (int i=0; i 0) { // End points get an arbitrarily large distance which will // cause them to be visible at any zoom level points.get(0).dist = verySmall * Math.pow(zoomFactor, numLevels); points.get(points.size()-1).dist = verySmall * Math.pow(zoomFactor, numLevels); // Compute the zoom level for every other point to be visible // usiing the DP algorithm dpEncode(points); // Generate the encoded level string from computed zoom levels for (int i=0; iDouglas-Peucker algorithm * adapted for encoding. Rather than eliminating points, we record * their distance from the segment which occurs at that recursive * step. These distances are converted to zoom levels later. * @param points Polyline to be encoded. At entry, Point.dist * is NaN for each point of the polyline except * the end points. At return, the value in * Point.dist has been set to select the zoom * level at which the point is displayed; if * Point.dist is still NaN for some point, that * point is never displayed. */ private static void dpEncode(ArrayList points) { double maxDist = 0.0; double temp; ArrayDeque stack = new ArrayDeque(); Segment current; int maxLoc = 0; // A line two points long is the degenerate case, there is // no point eligible for reduction if (points.size() > 2) { // The line has more than two points, so push the whole // line onto the stack stack.push(new Segment(0, points.size()-1)); // Find that point most distant from the line, then if it // is more distant than a very small amount, recursively // find the most distant point from each of the line // segments before and after that point. while (stack.size() > 0) { // Find that point which is most distant from the // line segment now on top of the stack current = stack.pop(); maxDist = 0; // If there are only two points in the line segment, // this loop falls through immediately leaving // maxDist == 0 so no recursion will occur. for (int i = current.first+1; i maxDist) { maxDist = temp; maxLoc = i; } } // If the most distant point is more than a very // small distance from the line segment, recurse to // the two line segments before and after. if (maxDist > verySmall) { // Remember the distance value computed for this // point. It will control the zoom level at // which this point is displayed. points.get(maxLoc).dist = maxDist; stack.push(new Segment(current.first, maxLoc)); stack.push(new Segment(maxLoc, current.last)); } } } } // private static void dpEncode() /** * Compute distance between a point and a line segment. * @param p0 Point * @param p1 One end of a line segment * @param p2 The other end of a line segment * @return distance from point p0 to line segment [p1,p2] */ private static double distance(Point p0, Point p1, Point p2) { double u; double out = 0.0; if ( (p1.lat == p2.lat) && (p1.lon == p2.lon) ) { // Zero-length line segment, ie. the end points are identical out = Math.sqrt(Math.pow(p2.lat-p0.lat,2) + Math.pow(p2.lon-p0.lon,2)); } else { // Non-zero-length line segment. u = ((p0.lat-p1.lat)*(p2.lat-p1.lat) + (p0.lon-p1.lon)*(p2.lon-p1.lon)) /(Math.pow(p2.lat-p1.lat,2) + Math.pow(p2.lon-p1.lon,2)); if(u <= 0) { out = Math.sqrt(Math.pow(p0.lat - p1.lat,2) + Math.pow(p0.lon - p1.lon,2)); } if(u >= 1) { out = Math.sqrt(Math.pow(p0.lat - p2.lat,2) + Math.pow(p0.lon - p2.lon,2)); } if(0 < u && u < 1) { out = Math.sqrt(Math.pow(p0.lat-p1.lat-u*(p2.lat-p1.lat),2) + Math.pow(p0.lon-p1.lon-u*(p2.lon-p1.lon),2)); } } return out; } // private static void distance() /** * Compute zoom level of a point. * * Compute the appropriate zoom level of a point in terms of it's * distance from the relevant segment in the DP algorithm. */ private static int computeLevel(double dd) { int lev = 0; if (dd > verySmall) { while (dd < zoomLevelBreaks[lev]) { lev++; } } return lev; } // private static char computeLevel() /** * Encode points. * * This function is very similar to Google's * * polyline.js * The key difference is that not all points are encoded, * since some were eliminated by Douglas-Peucker. */ private static String createEncodings(ArrayList points) { Point point; long plat = 0; long plng = 0; StringBuilder encoded_points = new StringBuilder(); for(int i = 0; i < points.size(); i++) { point = points.get(i); if (!Double.isNaN(point.dist)) { double lat = point.lat; double lng = point.lon; long late5 = (long)Math.floor(lat * 1e5); long lnge5 = (long)Math.floor(lng * 1e5); long dlat = late5 - plat; long dlng = lnge5 - plng; plat = late5; plng = lnge5; encoded_points.append(encodeSignedNumber(dlat) + encodeSignedNumber(dlng)); } } return encoded_points.toString(); } // private static void createEncodings() /** * Encode a 32-bit signed binary number in little-endian base64. * * See the Google Maps API documentation * of how this works * @param number Number to encode * @return Number encoded in base64 */ private static String encodeSignedNumber(long number) { number = number << 1; if (number < 0) { number = number ^ -1; } return encodeNumber(number); } // private String encodeSignedNumber() /** * Encode a 32-bit positive binary number in little-endian base64. * * See the Google Maps API documentation of how this works * @param number Number to encode * @return Number encoded in base64 */ private static String encodeNumber(long number) { int[] chunks = new int[6]; chunks[0] = ((int)(number & 0x0000001F) ); chunks[1] = ((int)(number & 0x000003E0) >> 5); chunks[2] = ((int)(number & 0x00007C00) >> 10); chunks[3] = ((int)(number & 0x000F8000) >> 15); chunks[4] = ((int)(number & 0x01F00000) >> 20); chunks[5] = ((int)(number & 0x3E000000) >> 25); // Find which chunks are significant (from 1 to 6 will be) int last_chunk; for (last_chunk = 5; last_chunk > 0; last_chunk--) { if (chunks[last_chunk] != 0) { break; } } for (int i = 0; i < last_chunk; i++) { chunks[i] |= 0x20; } char[] enc_chunk = new char[2]; StringBuilder result = new StringBuilder(); for (int i = 0; i <= last_chunk; i++) { enc_chunk = Character.toChars(chunks[i] + 63); result.append(enc_chunk[0]); } return result.toString(); } // private String encodeNumber() } // public class GMap2