package org.activityinfo.geoadmin.writer; import java.util.HashMap; import java.util.Map; import java.util.Stack; import com.google.common.collect.Maps; import com.vividsolutions.jts.geom.Coordinate; /** * Adapation of... Reimplementation of... Mark McClures Javascript * PolylineEncoder All the mathematical logic is more or less copied by McClure * * @author Mark Rambow * @e-mail markrambow[at]gmail[dot]com * @version 0.1 * * * http://facstaff.unca.edu/mcmcclur/GoogleMaps/EncodePolyline/ * */ public class GooglePolylineEncoder { private int numLevels = 18; private int zoomFactor = 2; private double verySmall = 0.00001; private boolean forceEndpoints = true; private double[] zoomLevelBreaks; private HashMap<String, Double> bounds; // constructor public GooglePolylineEncoder(int numLevels, int zoomFactor, double verySmall, boolean forceEndpoints) { this.numLevels = numLevels; this.zoomFactor = zoomFactor; this.verySmall = verySmall; this.forceEndpoints = forceEndpoints; this.zoomLevelBreaks = new double[numLevels]; for (int i = 0; i < numLevels; i++) { this.zoomLevelBreaks[i] = verySmall * Math.pow(this.zoomFactor, numLevels - i - 1); } } public GooglePolylineEncoder() { this.zoomLevelBreaks = new double[numLevels]; for (int i = 0; i < numLevels; i++) { this.zoomLevelBreaks[i] = verySmall * Math.pow(this.zoomFactor, numLevels - i - 1); } } public int getNumLevels() { return numLevels; } public int getZoomFactor() { return zoomFactor; } /** * Douglas-Peucker algorithm, adapted for encoding * * @return HashMap [EncodedPoints;EncodedLevels] * */ public PolylineEncoded dpEncode(Coordinate[] track) { int i, maxLoc = 0; Stack<int[]> stack = new Stack<int[]>(); double[] dists = new double[track.length]; double maxDist, absMaxDist = 0.0, temp = 0.0; int[] current; String encodedPoints, encodedLevels; if (track.length > 2) { int[] stackVal = new int[] { 0, (track.length - 1) }; stack.push(stackVal); while (stack.size() > 0) { current = stack.pop(); maxDist = 0; for (i = current[0] + 1; i < current[1]; i++) { temp = this.distance( track[i], track[current[0]], track[current[1]]); if (temp > maxDist) { maxDist = temp; maxLoc = i; if (maxDist > absMaxDist) { absMaxDist = maxDist; } } } if (maxDist > this.verySmall) { dists[maxLoc] = maxDist; int[] stackValCurMax = { current[0], maxLoc }; stack.push(stackValCurMax); int[] stackValMaxCur = { maxLoc, current[1] }; stack.push(stackValMaxCur); } } } encodedPoints = createEncodings(track, dists); encodedLevels = encodeLevels(track, dists, absMaxDist); return new PolylineEncoded(encodedPoints, encodedLevels); } /** * distance(p0, p1, p2) computes the distance between the point p0 and the * segment [p1,p2]. This could probably be replaced with something that is a * bit more numerically stable. * * @param p0 * @param p1 * @param p2 * @return */ public double distance(Coordinate p0, Coordinate p1, Coordinate p2) { double u, out = 0.0; if (p1.y == p2.y && p1.x == p2.x) { out = Math.sqrt(Math.pow(p2.y - p0.y, 2) + Math.pow(p2.x - p0.x, 2)); } else { u = ((p0.y - p1.y) * (p2.y - p1.y) + (p0 .x - p1.x) * (p2.x - p1.x)) / (Math.pow(p2.y - p1.y, 2) + Math .pow(p2.x - p1.x, 2)); if (u <= 0) { out = Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2)); } if (u >= 1) { out = Math.sqrt(Math.pow(p0.y - p2.y, 2) + Math.pow(p0.x - p2.x, 2)); } if (0 < u && u < 1) { out = Math.sqrt(Math.pow(p0.y - p1.y - u * (p2.y - p1.y), 2) + Math.pow(p0.x - p1.x - u * (p2.x - p1.x), 2)); } } return out; } private static int floor1e5(double coordinate) { return (int) Math.floor(coordinate * 1e5); } private static String encodeSignedNumber(int num) { int sgn_num = num << 1; if (num < 0) { sgn_num = ~(sgn_num); } return (encodeNumber(sgn_num)); } private static String encodeNumber(int num) { StringBuffer encodeString = new StringBuffer(); while (num >= 0x20) { int nextValue = (0x20 | (num & 0x1f)) + 63; encodeString.append((char) (nextValue)); num >>= 5; } num += 63; encodeString.append((char) (num)); return encodeString.toString(); } /** * 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. */ private String encodeLevels(Coordinate[] points, double[] dists, double absMaxDist) { int i; StringBuffer encoded_levels = new StringBuffer(); if (this.forceEndpoints) { encoded_levels.append(encodeNumber(this.numLevels - 1)); } else { encoded_levels.append(encodeNumber(this.numLevels - computeLevel(absMaxDist) - 1)); } for (i = 1; i < points.length - 1; i++) { if (dists[i] != 0) { encoded_levels.append(encodeNumber(this.numLevels - computeLevel(dists[i]) - 1)); } } if (this.forceEndpoints) { encoded_levels.append(encodeNumber(this.numLevels - 1)); } else { encoded_levels.append(encodeNumber(this.numLevels - computeLevel(absMaxDist) - 1)); } // System.out.println("encodedLevels: " + encoded_levels); return encoded_levels.toString(); } /** * 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. */ private int computeLevel(double absMaxDist) { int lev = 0; if (absMaxDist > this.verySmall) { lev = 0; while (absMaxDist < this.zoomLevelBreaks[lev]) { lev++; } return lev; } return lev; } private String createEncodings(Coordinate[] points, double[] dists) { StringBuffer encodedPoints = new StringBuffer(); double maxlat = 0, minlat = 0, maxlon = 0, minlon = 0; int plat = 0; int plng = 0; for (int i = 0; i < points.length; i++) { // determin bounds (max/min lat/lon) if (i == 0) { maxlat = minlat = points[i].y; maxlon = minlon = points[i].x; } else { if (points[i].y > maxlat) { maxlat = points[i].y; } else if (points[i].y < minlat) { minlat = points[i].y; } else if (points[i].x > maxlon) { maxlon = points[i].x; } else if (points[i].x < minlon) { minlon = points[i].x; } } if (dists[i] != 0 || i == 0 || i == points.length - 1) { Coordinate point = points[i]; int late5 = floor1e5(point.y); int lnge5 = floor1e5(point.x); int dlat = late5 - plat; int dlng = lnge5 - plng; plat = late5; plng = lnge5; encodedPoints.append(encodeSignedNumber(dlat)); encodedPoints.append(encodeSignedNumber(dlng)); } } HashMap<String, Double> bounds = new HashMap<String, Double>(); bounds.put("maxlat", new Double(maxlat)); bounds.put("minlat", new Double(minlat)); bounds.put("maxlon", new Double(maxlon)); bounds.put("minlon", new Double(minlon)); this.setBounds(bounds); return encodedPoints.toString(); } private void setBounds(HashMap<String, Double> bounds) { this.bounds = bounds; } public static Map<String, String> createEncodings(Coordinate[] track, int level, int step) { Map<String, String> resultMap = Maps.newHashMap(); StringBuffer encodedPoints = new StringBuffer(); StringBuffer encodedLevels = new StringBuffer(); int plat = 0; int plng = 0; int counter = 0; int listSize = track.length; Coordinate trackpoint; for (int i = 0; i < listSize; i += step) { counter++; trackpoint = track[i]; int late5 = floor1e5(trackpoint.y); int lnge5 = floor1e5(trackpoint.x); int dlat = late5 - plat; int dlng = lnge5 - plng; plat = late5; plng = lnge5; encodedPoints.append(encodeSignedNumber(dlat)).append( encodeSignedNumber(dlng)); encodedLevels.append(encodeNumber(level)); } System.out.println("listSize: " + listSize + " step: " + step + " counter: " + counter); resultMap.put("encodedPoints", encodedPoints.toString()); resultMap.put("encodedLevels", encodedLevels.toString()); return resultMap; } public HashMap<String, Double> getBounds() { return bounds; } }