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);