package de.blau.android.util.mapbox.utils; import java.util.ArrayList; import java.util.List; import de.blau.android.util.mapbox.models.Position; /** * Polyline utils class contains method that can decode/encode a polyline, simplify a line, and more. * * @since 1.0.0 */ public class PolylineUtils { // 1 by default (in the same metric as the point coordinates) private static final double SIMPLIFY_DEFAULT_TOLERANCE = 1; // False by default (excludes distance-based preprocessing step which leads to highest quality // simplification but runs slower) private static final boolean SIMPLIFY_DEFAULT_HIGHEST_QUALITY = false; /** * Decodes an encoded path string into a sequence of Positions. * * @param encodedPath {@link String} representing a path string. * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5. * @return List of {@link Position} making up the line. * @see <a href="https://github.com/mapbox/polyline/blob/master/src/polyline.js">Part of algorithm came from this source</a> * @see <a href="https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/PolyUtil.java">Part of algorithm came from this source.</a> * @since 1.0.0 */ public static List<Position> decode(final String encodedPath, int precision) { int len = encodedPath.length(); // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5 double factor = Math.pow(10, precision); // For speed we preallocate to an upper bound on the final length, then // truncate the array before returning. final List<Position> path = new ArrayList<>(); int index = 0; int lat = 0; int lng = 0; while (index < len) { int result = 1; int shift = 0; int b; do { b = encodedPath.charAt(index++) - 63 - 1; result += b << shift; shift += 5; } while (b >= 0x1f); lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1); result = 1; shift = 0; do { b = encodedPath.charAt(index++) - 63 - 1; result += b << shift; shift += 5; } while (b >= 0x1f); lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1); path.add(Position.fromCoordinates(lng / factor, lat / factor)); } return path; } /** * Encodes a sequence of Positions into an encoded path string. * * @param path List of {@link Position} making up the line. * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5. * @return {@link String} representing a path string. * @since 1.0.0 */ public static String encode(final List<Position> path, int precision) { long lastLat = 0; long lastLng = 0; final StringBuffer result = new StringBuffer(); // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5 double factor = Math.pow(10, precision); for (final Position point : path) { long lat = Math.round(point.getLatitude() * factor); long lng = Math.round(point.getLongitude() * factor); long dLat = lat - lastLat; long dLng = lng - lastLng; encode(dLat, result); encode(dLng, result); lastLat = lat; lastLng = lng; } return result.toString(); } private static void encode(long variable, StringBuffer result) { variable = variable < 0 ? ~(variable << 1) : variable << 1; while (variable >= 0x20) { result.append(Character.toChars((int) ((0x20 | (variable & 0x1f)) + 63))); variable >>= 5; } result.append(Character.toChars((int) (variable + 63))); } /* * Polyline simplification method. It's a direct port of simplify.js to Java. * See: https://github.com/mourner/simplify-js/blob/master/simplify.js */ /** * Square distance between 2 points */ private static double getSqDist(Position p1, Position p2) { double dx = p1.getLongitude() - p2.getLongitude(); double dy = p1.getLatitude() - p2.getLatitude(); return dx * dx + dy * dy; } /** * Square distance from a point to a segment */ private static double getSqSegDist(Position position, Position p1, Position p2) { double x = p1.getLongitude(); double y = p1.getLatitude(); double dx = p2.getLongitude() - x; double dy = p2.getLatitude() - y; if (dx != 0 || dy != 0) { double t = ((position.getLongitude() - x) * dx + (position.getLatitude() - y) * dy) / (dx * dx + dy * dy); if (t > 1) { x = p2.getLongitude(); y = p2.getLatitude(); } else if (t > 0) { x += dx * t; y += dy * t; } } dx = position.getLongitude() - x; dy = position.getLatitude() - y; return dx * dx + dy * dy; } /** * Basic distance-based simplification */ private static Position[] simplifyRadialDist(Position[] points, double sqTolerance) { Position prevPoint = points[0]; ArrayList<Position> newPoints = new ArrayList<>(); newPoints.add(prevPoint); Position point = null; for (int i = 1, len = points.length; i < len; i++) { point = points[i]; if (getSqDist(point, prevPoint) > sqTolerance) { newPoints.add(point); prevPoint = point; } } if (prevPoint != point) { newPoints.add(point); } return newPoints.toArray(new Position[newPoints.size()]); } private static List<Position> simplifyDpStep( Position[] points, int first, int last, double sqTolerance, List<Position> simplified) { double maxSqDist = sqTolerance; int index = 0; ArrayList<Position> stepList = new ArrayList<>(); for (int i = first + 1; i < last; i++) { double sqDist = getSqSegDist(points[i], points[first], points[last]); if (sqDist > maxSqDist) { index = i; maxSqDist = sqDist; } } if (maxSqDist > sqTolerance) { if (index - first > 1) { stepList.addAll(simplifyDpStep(points, first, index, sqTolerance, simplified)); } stepList.add(points[index]); if (last - index > 1) { stepList.addAll(simplifyDpStep(points, index, last, sqTolerance, simplified)); } } return stepList; } /** * Simplification using Ramer-Douglas-Peucker algorithm */ private static Position[] simplifyDouglasPeucker(Position[] points, double sqTolerance) { int last = points.length - 1; ArrayList<Position> simplified = new ArrayList<>(); simplified.add(points[0]); simplified.addAll(simplifyDpStep(points, 0, last, sqTolerance, simplified)); simplified.add(points[last]); return simplified.toArray(new Position[simplified.size()]); } /** * Reduces the number of points in a polyline while retaining its shape, giving a performance * boost when processing it and also reducing visual noise * * @param points an array of points * @return an array of simplified points * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a> * @since 1.2.0 */ public static Position[] simplify(Position[] points) { return simplify(points, SIMPLIFY_DEFAULT_TOLERANCE, SIMPLIFY_DEFAULT_HIGHEST_QUALITY); } /** * Reduces the number of points in a polyline while retaining its shape, giving a performance * boost when processing it and also reducing visual noise. * * @param points an array of points * @param tolerance affects the amount of simplification (in the same metric as the point coordinates) * @return an array of simplified points * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a> * @since 1.2.0 */ public static Position[] simplify(Position[] points, double tolerance) { return simplify(points, tolerance, SIMPLIFY_DEFAULT_HIGHEST_QUALITY); } /** * Reduces the number of points in a polyline while retaining its shape, giving a performance * boost when processing it and also reducing visual noise. * * @param points an array of points * @param highestQuality excludes distance-based preprocessing step which leads to highest quality simplification * @return an array of simplified points * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a> * @since 1.2.0 */ public static Position[] simplify(Position[] points, boolean highestQuality) { return simplify(points, SIMPLIFY_DEFAULT_TOLERANCE, highestQuality); } /** * Reduces the number of points in a polyline while retaining its shape, giving a performance * boost when processing it and also reducing visual noise. * * @param points an array of points * @param tolerance affects the amount of simplification (in the same metric as the point coordinates) * @param highestQuality excludes distance-based preprocessing step which leads to highest quality simplification * @return an array of simplified points * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a> * @since 1.2.0 */ private static Position[] simplify(Position[] points, double tolerance, boolean highestQuality) { if (points.length <= 2) { return points; } double sqTolerance = tolerance * tolerance; points = highestQuality ? points : simplifyRadialDist(points, sqTolerance); points = simplifyDouglasPeucker(points, sqTolerance); return points; } }