From d7244938e7c7dc31dd398a042ab5d44acd3217e3 Mon Sep 17 00:00:00 2001 From: Sean Barbeau Date: Thu, 17 Sep 2015 17:23:47 -0400 Subject: [PATCH] Add Douglas-Peucker poly simplification algorithm as PolyUtil.simplify() * Douglas-Peucker line simplification implementation ported from the Google MyTracks project (https://code.google.com/p/mytracks/source/browse/MyTracks/src/com/google/android/apps/mytracks/util/LocationUtils.java#81), (c) Google 2008, licensed under Apache v2.0. * Add support for simplifying polygons to the PolyUtil.simplify() implementation * Add demo activity for PolyUtil.simplify() - shows the original line in black, as well as simplified versions of the line with multiple tolerances in different colors. Also shows origin polygons in blue, with simplified polygons in yellow * Add unit tests for PolyUtil.simplify() (covering polylines and polygons), PolyUtil.distanceToLine(), and PolyUtil.isClosedPolygon() in PolyUtilTest --- demo/AndroidManifest.xml | 1 + .../maps/android/utils/demo/MainActivity.java | 1 + .../utils/demo/PolySimplifyDemoActivity.java | 128 ++++++++++ .../src/com/google/maps/android/PolyUtil.java | 156 +++++++++++- .../com/google/maps/android/PolyUtilTest.java | 236 +++++++++++++++++- 5 files changed, 515 insertions(+), 7 deletions(-) create mode 100644 demo/src/com/google/maps/android/utils/demo/PolySimplifyDemoActivity.java diff --git a/demo/AndroidManifest.xml b/demo/AndroidManifest.xml index f022f4ada..ecb7cac7e 100644 --- a/demo/AndroidManifest.xml +++ b/demo/AndroidManifest.xml @@ -49,6 +49,7 @@ + diff --git a/demo/src/com/google/maps/android/utils/demo/MainActivity.java b/demo/src/com/google/maps/android/utils/demo/MainActivity.java index 09fdc9d10..7f8a7f7fe 100644 --- a/demo/src/com/google/maps/android/utils/demo/MainActivity.java +++ b/demo/src/com/google/maps/android/utils/demo/MainActivity.java @@ -39,6 +39,7 @@ protected void onCreate(Bundle savedInstanceState) { addDemo("Clustering: Custom Look", CustomMarkerClusteringDemoActivity.class); addDemo("Clustering: 2K markers", BigClusteringDemoActivity.class); addDemo("PolyUtil.decode", PolyDecodeDemoActivity.class); + addDemo("PolyUtil.simplify", PolySimplifyDemoActivity.class); addDemo("IconGenerator", IconGeneratorDemoActivity.class); addDemo("SphericalUtil.computeDistanceBetween", DistanceDemoActivity.class); addDemo("Generating tiles", TileProviderAndProjectionDemo.class); diff --git a/demo/src/com/google/maps/android/utils/demo/PolySimplifyDemoActivity.java b/demo/src/com/google/maps/android/utils/demo/PolySimplifyDemoActivity.java new file mode 100644 index 000000000..58ca5aca3 --- /dev/null +++ b/demo/src/com/google/maps/android/utils/demo/PolySimplifyDemoActivity.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015 Sean J. Barbeau + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.utils.demo; + +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.PolygonOptions; +import com.google.android.gms.maps.model.PolylineOptions; +import com.google.maps.android.PolyUtil; + +import android.graphics.Color; + +import java.util.ArrayList; +import java.util.List; + +public class PolySimplifyDemoActivity extends BaseDemoActivity { + + private final static String LINE = "elfjD~a}uNOnFN~Em@fJv@tEMhGDjDe@hG^nF??@lA?n@IvAC`Ay@A{@DwCA{CF_EC{CEi@PBTFDJBJ?V?n@?D@?A@?@?F?F?LAf@?n@@`@@T@~@FpA?fA?p@?r@?vAH`@OR@^ETFJCLD?JA^?J?P?fAC`B@d@?b@A\\@`@Ad@@\\?`@?f@?V?H?DD@DDBBDBD?D?B?B@B@@@B@B@B@D?D?JAF@H@FCLADBDBDCFAN?b@Af@@x@@"; + private final static String OVAL_POLYGON = "}wgjDxw_vNuAd@}AN{A]w@_Au@kAUaA?{@Ke@@_@C]D[FULWFOLSNMTOVOXO\\I\\CX?VJXJTDTNXTVVLVJ`@FXA\\AVLZBTATBZ@ZAT?\\?VFT@XGZAP"; + private final static int ALPHA_ADJUSTMENT = 0x77000000; + + @Override + protected void startDemo() { + GoogleMap mMap = getMap(); + + // Original line + List line = PolyUtil.decode(LINE); + mMap.addPolyline(new PolylineOptions() + .addAll(line) + .color(Color.BLACK)); + + getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(28.05870, -82.4090), 15)); + + List simplifiedLine; + + /** + * Simplified lines - increasing the tolerance will result in fewer points in the simplified + * line + */ + double tolerance = 5; // meters + simplifiedLine = PolyUtil.simplify(line, tolerance); + mMap.addPolyline(new PolylineOptions() + .addAll(simplifiedLine) + .color(Color.RED - ALPHA_ADJUSTMENT)); + + tolerance = 20; // meters + simplifiedLine = PolyUtil.simplify(line, tolerance); + mMap.addPolyline(new PolylineOptions() + .addAll(simplifiedLine) + .color(Color.GREEN - ALPHA_ADJUSTMENT)); + + tolerance = 50; // meters + simplifiedLine = PolyUtil.simplify(line, tolerance); + mMap.addPolyline(new PolylineOptions() + .addAll(simplifiedLine) + .color(Color.MAGENTA - ALPHA_ADJUSTMENT)); + + tolerance = 500; // meters + simplifiedLine = PolyUtil.simplify(line, tolerance); + mMap.addPolyline(new PolylineOptions() + .addAll(simplifiedLine) + .color(Color.YELLOW - ALPHA_ADJUSTMENT)); + + tolerance = 1000; // meters + simplifiedLine = PolyUtil.simplify(line, tolerance); + mMap.addPolyline(new PolylineOptions() + .addAll(simplifiedLine) + .color(Color.BLUE - ALPHA_ADJUSTMENT)); + + + // Triangle polygon - the polygon should be closed + ArrayList triangle = new ArrayList<>(); + triangle.add(new LatLng(28.06025,-82.41030)); // Should match last point + triangle.add(new LatLng(28.06129,-82.40945)); + triangle.add(new LatLng(28.06206,-82.40917)); + triangle.add(new LatLng(28.06125,-82.40850)); + triangle.add(new LatLng(28.06035,-82.40834)); + triangle.add(new LatLng(28.06038, -82.40924)); + triangle.add(new LatLng(28.06025,-82.41030)); // Should match first point + + mMap.addPolygon(new PolygonOptions() + .addAll(triangle) + .fillColor(Color.BLUE - ALPHA_ADJUSTMENT) + .strokeColor(Color.BLUE) + .strokeWidth(5)); + + // Simplified triangle polygon + tolerance = 88; // meters + List simplifiedTriangle = PolyUtil.simplify(triangle, tolerance); + mMap.addPolygon(new PolygonOptions() + .addAll(simplifiedTriangle) + .fillColor(Color.YELLOW - ALPHA_ADJUSTMENT) + .strokeColor(Color.YELLOW) + .strokeWidth(5)); + + // Oval polygon - the polygon should be closed + List oval = PolyUtil.decode(OVAL_POLYGON); + mMap.addPolygon(new PolygonOptions() + .addAll(oval) + .fillColor(Color.BLUE - ALPHA_ADJUSTMENT) + .strokeColor(Color.BLUE) + .strokeWidth(5)); + + // Simplified oval polygon + tolerance = 10; // meters + List simplifiedOval= PolyUtil.simplify(oval, tolerance); + mMap.addPolygon(new PolygonOptions() + .addAll(simplifiedOval) + .fillColor(Color.YELLOW - ALPHA_ADJUSTMENT) + .strokeColor(Color.YELLOW) + .strokeWidth(5)); + } +} diff --git a/library/src/com/google/maps/android/PolyUtil.java b/library/src/com/google/maps/android/PolyUtil.java index 6ab7957b6..0d521a374 100644 --- a/library/src/com/google/maps/android/PolyUtil.java +++ b/library/src/com/google/maps/android/PolyUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Google Inc. + * Copyright 2008, 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import java.util.List; import java.util.ArrayList; +import java.util.Stack; +import static com.google.maps.android.SphericalUtil.*; import static java.lang.Math.*; import static com.google.maps.android.MathUtil.*; @@ -38,11 +40,11 @@ private static double tanLatGC(double lat1, double lat2, double lng2, double lng /** * Returns mercator(latitude-at-lng3) on the Rhumb line (lat1, lng1) to (lat2, lng2). lng1==0. - */ + */ private static double mercatorLatRhumb(double lat1, double lat2, double lng2, double lng3) { return (mercator(lat1) * (lng2 - lng3) + mercator(lat2) * lng3) / lng2; } - + /** * Computes whether the vertical segment (lat3, lng3) to South Pole intersects the segment * (lat1, lng1) to (lat2, lng2). @@ -84,7 +86,7 @@ private static boolean intersects(double lat1, double lat2, double lng2, tan(lat3) >= tanLatGC(lat1, lat2, lng2, lng3) : mercator(lat3) >= mercatorLatRhumb(lat1, lat2, lng2, lng3); } - + /** * Computes whether the given point lies inside the specified polygon. * The polygon is always cosidered closed, regardless of whether the last point equals @@ -201,7 +203,7 @@ private static boolean isLocationOnEdgeOrPath(LatLng point, List poly, b for (LatLng point2 : poly) { double lat2 = toRadians(point2.latitude); double y2 = mercator(lat2); - double lng2 = toRadians(point2.longitude); + double lng2 = toRadians(point2.longitude); if (max(lat1, lat2) >= minAcceptable && min(lat1, lat2) <= maxAcceptable) { // We offset longitudes by -lng1; the implicit x1 is 0. double x2 = wrap(lng2 - lng1, -PI, PI); @@ -283,6 +285,150 @@ private static boolean isOnSegmentGC(double lat1, double lng1, double lat2, doub return sinSumAlongTrack > 0; // Compare with half-circle == PI using sign of sin(). } + /** + * Simplifies the given poly (polyline or polygon) using the Douglas-Peucker decimation + * algorithm. Increasing the tolerance will result in fewer points in the simplified polyline + * or polygon. + * + * When the providing a polygon as input, the first and last point of the list MUST have the + * same latitude and longitude (i.e., the polygon must be closed). If the input polygon is not + * closed, the resulting polygon may not be fully simplified. + * + * The time complexity of Douglas-Peucker is O(n^2), so take care that you do not call this + * algorithm too frequently in your code. + * + * @param poly polyline or polygon to be simplified. Polygon should be closed (i.e., + * first and last points should have the same latitude and longitude). + * @param tolerance in meters. Increasing the tolerance will result in fewer points in the + * simplified poly. + * @return a simplified poly produced by the Douglas-Peucker algorithm + */ + public static List simplify(List poly, double tolerance) { + final int n = poly.size(); + if (n < 1) { + throw new IllegalArgumentException("Polyline must have at least 1 point"); + } + if (tolerance <= 0) { + throw new IllegalArgumentException("Tolerance must be greater than zero"); + } + + boolean closedPolygon = isClosedPolygon(poly); + + // Check if the provided poly is a closed polygon + if (closedPolygon) { + // Add a small offset to the last point for Douglas-Peucker on polygons (see #201) + final double OFFSET = 0.00000000001; + LatLng lastPoint = poly.get(poly.size() - 1); + // LatLng.latitude and .longitude are immutable, so replace the last point + poly.remove(poly.size() - 1); + poly.add(new LatLng(lastPoint.latitude + OFFSET, lastPoint.longitude + OFFSET)); + } + + int idx; + int maxIdx = 0; + Stack stack = new Stack<>(); + double[] dists = new double[n]; + dists[0] = 1; + dists[n - 1] = 1; + double maxDist; + double dist = 0.0; + int[] current; + + if (n > 2) { + int[] stackVal = new int[]{0, (n - 1)}; + stack.push(stackVal); + while (stack.size() > 0) { + current = stack.pop(); + maxDist = 0; + for (idx = current[0] + 1; idx < current[1]; ++idx) { + dist = distanceToLine(poly.get(idx), poly.get(current[0]), + poly.get(current[1])); + if (dist > maxDist) { + maxDist = dist; + maxIdx = idx; + } + } + if (maxDist > tolerance) { + dists[maxIdx] = maxDist; + int[] stackValCurMax = {current[0], maxIdx}; + stack.push(stackValCurMax); + int[] stackValMaxCur = {maxIdx, current[1]}; + stack.push(stackValMaxCur); + } + } + } + + if (closedPolygon) { + // Replace last point of input (w/ offset) with a copy of first to re-close the polygon + poly.remove(poly.size() - 1); + poly.add(new LatLng(poly.get(0).latitude, poly.get(0).longitude)); + } + + // Generate the simplified line + idx = 0; + ArrayList simplifiedLine = new ArrayList<>(); + for (LatLng l : poly) { + if (dists[idx] != 0) { + simplifiedLine.add(l); + } + idx++; + } + + return simplifiedLine; + } + + /** + * Returns true if the provided list of points is a closed polygon (i.e., the first and last + * points are the same), and false if it is not + * @param poly polyline or polygon + * @return true if the provided list of points is a closed polygon (i.e., the first and last + * points are the same), and false if it is not + */ + public static boolean isClosedPolygon(List poly) { + LatLng firstPoint = poly.get(0); + LatLng lastPoint = poly.get(poly.size()-1); + if (firstPoint.equals(lastPoint)) { + return true; + } else { + return false; + } + } + + /** + * Computes the distance on the sphere between the point p and the line segment start to end. + * + * @param p the point to be measured + * @param start the beginning of the line segment + * @param end the end of the line segment + * @return the distance in meters (assuming spherical earth) + */ + public static double distanceToLine(final LatLng p, final LatLng start, final LatLng end) { + if (start.equals(end)) { + computeDistanceBetween(end, p); + } + + final double s0lat = toRadians(p.latitude); + final double s0lng = toRadians(p.longitude); + final double s1lat = toRadians(start.latitude); + final double s1lng = toRadians(start.longitude); + final double s2lat = toRadians(end.latitude); + final double s2lng = toRadians(end.longitude); + + double s2s1lat = s2lat - s1lat; + double s2s1lng = s2lng - s1lng; + final double u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng) + / (s2s1lat * s2s1lat + s2s1lng * s2s1lng); + if (u <= 0) { + return computeDistanceBetween(p, start); + } + if (u >= 1) { + return computeDistanceBetween(p, end); + } + LatLng sa = new LatLng(p.latitude - start.latitude, p.longitude - start.longitude); + LatLng sb = new LatLng(u * (end.latitude - start.latitude), u * (end.longitude - start.longitude)); + return computeDistanceBetween(sa, sb); + } + /** * Decodes an encoded path string into a sequence of LatLngs. */ diff --git a/library/tests/src/com/google/maps/android/PolyUtilTest.java b/library/tests/src/com/google/maps/android/PolyUtilTest.java index 828f8e3ab..3b825658e 100644 --- a/library/tests/src/com/google/maps/android/PolyUtilTest.java +++ b/library/tests/src/com/google/maps/android/PolyUtilTest.java @@ -17,14 +17,13 @@ package com.google.maps.android; import com.google.android.gms.maps.model.LatLng; -import com.google.maps.android.PolyUtil; import junit.framework.Assert; import junit.framework.TestCase; import java.lang.String; +import java.lang.reflect.Array; import java.util.List; -import java.util.Arrays; import java.util.ArrayList; public class PolyUtilTest extends TestCase { @@ -162,6 +161,239 @@ public void testContainsLocation() { makeList(2.5, 10, 1, 0), makeList(15, 10, 0, -15, 0, 25, -1, 0)); } + + public void testSimplify() { + /** + * Polyline + */ + final String LINE = "elfjD~a}uNOnFN~Em@fJv@tEMhGDjDe@hG^nF??@lA?n@IvAC`Ay@A{@DwCA{CF_EC{CEi@PBTFDJBJ?V?n@?D@?A@?@?F?F?LAf@?n@@`@@T@~@FpA?fA?p@?r@?vAH`@OR@^ETFJCLD?JA^?J?P?fAC`B@d@?b@A\\@`@Ad@@\\?`@?f@?V?H?DD@DDBBDBD?D?B?B@B@@@B@B@B@D?D?JAF@H@FCLADBDBDCFAN?b@Af@@x@@"; + List line = PolyUtil.decode(LINE); + assertEquals(95, line.size()); + + List simplifiedLine; + List copy; + + double tolerance = 5; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(21, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + tolerance = 10; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(14, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + tolerance = 15; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(10, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + tolerance = 20; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(8, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + tolerance = 50; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(6, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + tolerance = 500; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(3, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + tolerance = 1000; // meters + copy = copyList(line); + simplifiedLine = PolyUtil.simplify(line, tolerance); + assertEquals(2, simplifiedLine.size()); + assertEndPoints(line, simplifiedLine); + assertSimplifiedPointsFromLine(line, simplifiedLine); + assertLineLength(line, simplifiedLine); + assertInputUnchanged(line, copy); + + /** + * Polygons + */ + // Open triangle + ArrayList triangle = new ArrayList<>(); + triangle.add(new LatLng(28.06025,-82.41030)); + triangle.add(new LatLng(28.06129, -82.40945)); + triangle.add(new LatLng(28.06206, -82.40917)); + triangle.add(new LatLng(28.06125, -82.40850)); + triangle.add(new LatLng(28.06035, -82.40834)); + triangle.add(new LatLng(28.06038, -82.40924)); + assertFalse(PolyUtil.isClosedPolygon(triangle)); + + copy = copyList(triangle); + tolerance = 88; // meters + List simplifiedTriangle = PolyUtil.simplify(triangle, tolerance); + assertEquals(4, simplifiedTriangle.size()); + assertEndPoints(triangle, simplifiedTriangle); + assertSimplifiedPointsFromLine(triangle, simplifiedTriangle); + assertLineLength(triangle, simplifiedTriangle); + assertInputUnchanged(triangle, copy); + + // Close the triangle + LatLng p = triangle.get(0); + LatLng closePoint = new LatLng(p.latitude, p.longitude); + triangle.add(closePoint); + assertTrue(PolyUtil.isClosedPolygon(triangle)); + + copy = copyList(triangle); + tolerance = 88; // meters + simplifiedTriangle = PolyUtil.simplify(triangle, tolerance); + assertEquals(4, simplifiedTriangle.size()); + assertEndPoints(triangle, simplifiedTriangle); + assertSimplifiedPointsFromLine(triangle, simplifiedTriangle); + assertLineLength(triangle, simplifiedTriangle); + assertInputUnchanged(triangle, copy); + + // Open oval + final String OVAL_POLYGON = "}wgjDxw_vNuAd@}AN{A]w@_Au@kAUaA?{@Ke@@_@C]D[FULWFOLSNMTOVOXO\\I\\CX?VJXJTDTNXTVVLVJ`@FXA\\AVLZBTATBZ@ZAT?\\?VFT@XGZ"; + List oval = PolyUtil.decode(OVAL_POLYGON); + assertFalse(PolyUtil.isClosedPolygon(oval)); + + copy = copyList(oval); + tolerance = 10; // meters + List simplifiedOval= PolyUtil.simplify(oval, tolerance); + assertEquals(13, simplifiedOval.size()); + assertEndPoints(oval, simplifiedOval); + assertSimplifiedPointsFromLine(oval, simplifiedOval); + assertLineLength(oval, simplifiedOval); + assertInputUnchanged(oval, copy); + + // Close the oval + p = oval.get(0); + closePoint = new LatLng(p.latitude, p.longitude); + oval.add(closePoint); + assertTrue(PolyUtil.isClosedPolygon(oval)); + + copy = copyList(oval); + tolerance = 10; // meters + simplifiedOval= PolyUtil.simplify(oval, tolerance); + assertEquals(13, simplifiedOval.size()); + assertEndPoints(oval, simplifiedOval); + assertSimplifiedPointsFromLine(oval, simplifiedOval); + assertLineLength(oval, simplifiedOval); + assertInputUnchanged(oval, copy); + } + + /** + * Asserts that the beginning point of the original line matches the beginning point of the + * simplified line, and that the end point of the original line matches the end point of the + * simplified line. + * @param line original line + * @param simplifiedLine simplified line + */ + private void assertEndPoints(List line, List simplifiedLine) { + assertEquals(line.get(0), simplifiedLine.get(0)); + assertEquals(line.get(line.size() - 1), simplifiedLine.get(simplifiedLine.size() - 1)); + } + + /** + * Asserts that the simplified line is composed of points from the original line. + * @param line original line + * @param simplifiedLine simplified line + */ + private void assertSimplifiedPointsFromLine(List line, List simplifiedLine) { + for (LatLng l : simplifiedLine) { + assertTrue(line.contains(l)); + } + } + + /** + * Asserts that the length of the simplified line is always equal to or less than the length of + * the original line, if simplification has eliminated any points from the original line + * @param line original line + * @param simplifiedLine simplified line + */ + private void assertLineLength(List line, List simplifiedLine) { + if (line.size() == simplifiedLine.size()) { + // If no points were eliminated, then the length of both lines should be the same + assertTrue(SphericalUtil.computeLength(simplifiedLine) == SphericalUtil.computeLength(line)); + } else { + assertTrue(simplifiedLine.size() < line.size()); + // If points were eliminated, then the simplified line should always be shorter + assertTrue(SphericalUtil.computeLength(simplifiedLine) < SphericalUtil.computeLength(line)); + } + } + + /** + * Returns a copy of the LatLng objects contained in one list to another list. LatLng.latitude + * and LatLng.longitude are immutable, so having references to the same LatLng object is + * sufficient to guarantee that the contents are the same. + * @param original original list + * @return a copy of the original list, containing references to the same LatLng elements in + * the same order. + */ + private List copyList(List original) { + ArrayList copy = new ArrayList<>(original.size()); + for (LatLng l : original) { + copy.add(l); + } + return copy; + } + + /** + * Asserts that the contents of the original List passed into the PolyUtil.simplify() method + * doesn't change after the method is executed. We test for this because the poly is modified + * (a small offset is added to the last point) to allow for polygon simplification. + * @param afterInput the list passed into PolyUtil.simplify(), after PolyUtil.simplify() has + * finished executing + * @param beforeInput a copy of the list before it is passed into PolyUtil.simplify() + */ + private void assertInputUnchanged(List afterInput, List beforeInput) { + assertEquals(beforeInput, afterInput); + } + + public void testIsClosedPolygon() { + ArrayList poly = new ArrayList<>(); + poly.add(new LatLng(28.06025, -82.41030)); + poly.add(new LatLng(28.06129, -82.40945)); + poly.add(new LatLng(28.06206, -82.40917)); + poly.add(new LatLng(28.06125, -82.40850)); + poly.add(new LatLng(28.06035, -82.40834)); + + assertFalse(PolyUtil.isClosedPolygon(poly)); + + // Add the closing point that's same as the first + poly.add(new LatLng(28.06025, -82.41030)); + assertTrue(PolyUtil.isClosedPolygon(poly)); + } + + public void testDistanceToLine() { + LatLng startLine = new LatLng(28.05359, -82.41632); + LatLng endLine = new LatLng(28.05310, -82.41634); + LatLng p = new LatLng(28.05342, -82.41594); + + double distance = PolyUtil.distanceToLine(p, startLine, endLine); + expectNearNumber(42.989894, distance, 1e-6); + } public void testDecodePath() { List latLngs = PolyUtil.decode(TEST_LINE);