/*
* Copyright 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.
* 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;
import com.google.android.gms.maps.model.LatLng;
import junit.framework.Assert;
import junit.framework.TestCase;
import java.lang.String;
import java.lang.reflect.Array;
import java.util.List;
import java.util.ArrayList;
public class PolyUtilTest extends TestCase {
private static final String TEST_LINE = "_cqeFf~cjVf@p@fA}AtAoB`ArAx@hA`GbIvDiFv@gAh@t@X\\|@z@`@Z\\Xf@Vf@VpA\\tATJ@NBBkC";
private static void expectNearNumber(double expected, double actual, double epsilon) {
Assert.assertTrue(String.format("Expected %f to be near %f", actual, expected),
Math.abs(expected - actual) <= epsilon);
}
private static List<LatLng> makeList(double... coords) {
int size = coords.length / 2;
ArrayList<LatLng> list = new ArrayList<LatLng>(size);
for (int i = 0; i < size; ++i) {
list.add(new LatLng(coords[i + i], coords[i + i + 1]));
}
return list;
}
private static void containsCase(List<LatLng> poly, List<LatLng> yes, List<LatLng> no) {
for (LatLng point : yes) {
Assert.assertTrue(PolyUtil.containsLocation(point, poly, true));
Assert.assertTrue(PolyUtil.containsLocation(point, poly, false));
}
for (LatLng point : no) {
Assert.assertFalse(PolyUtil.containsLocation(point, poly, true));
Assert.assertFalse(PolyUtil.containsLocation(point, poly, false));
}
}
private static void onEdgeCase(boolean geodesic,
List<LatLng> poly, List<LatLng> yes, List<LatLng> no) {
for (LatLng point : yes) {
Assert.assertTrue(PolyUtil.isLocationOnEdge(point, poly, geodesic));
Assert.assertTrue(PolyUtil.isLocationOnPath(point, poly, geodesic));
}
for (LatLng point : no) {
Assert.assertFalse(PolyUtil.isLocationOnEdge(point, poly, geodesic));
Assert.assertFalse(PolyUtil.isLocationOnPath(point, poly, geodesic));
}
}
private static void onEdgeCase(List<LatLng> poly, List<LatLng> yes, List<LatLng> no) {
onEdgeCase(true, poly, yes, no);
onEdgeCase(false, poly, yes, no);
}
public void testOnEdge() {
// Empty
onEdgeCase(makeList(), makeList(), makeList(0, 0));
final double small = 5e-7; // About 5cm on equator, half the default tolerance.
final double big = 2e-6; // About 10cm on equator, double the default tolerance.
// Endpoints
onEdgeCase(makeList(1, 2), makeList(1, 2), makeList(3, 5));
onEdgeCase(makeList(1, 2, 3, 5), makeList(1, 2, 3, 5), makeList(0, 0));
// On equator.
onEdgeCase(makeList(0, 90, 0, 180),
makeList(0, 90-small, 0, 90+small, 0-small, 90, 0, 135, small, 135),
makeList(0, 90-big, 0, 0, 0, -90, big, 135));
// Ends on same latitude.
onEdgeCase(makeList(-45, -180, -45, -small),
makeList(-45, 180+small, -45, 180-small, -45-small, 180-small, -45, 0),
makeList(-45, big, -45, 180-big, -45+big, -90, -45, 90));
// Meridian.
onEdgeCase(makeList(-10, 30, 45, 30),
makeList(10, 30-small, 20, 30+small, -10-small, 30+small),
makeList(-10-big, 30, 10, -150, 0, 30-big));
// Slanted close to meridian, close to North pole.
onEdgeCase(makeList(0, 0, 90-small, 0+big),
makeList(1, 0+small, 2, 0-small, 90-small, -90, 90-small, 10),
makeList(-big, 0, 90-big, 180, 10, big));
// Arc > 120 deg.
onEdgeCase(makeList(0, 0, 0, 179.999),
makeList(0, 90, 0, small, 0, 179, small, 90),
makeList(0, -90, small, -100, 0, 180, 0, -big, 90, 0, -90, 180));
onEdgeCase(makeList(10, 5, 30, 15),
makeList(10+2*big, 5+big, 10+big, 5+big/2, 30-2*big, 15-big),
makeList(20, 10, 10-big, 5-big/2, 30+2*big, 15+big, 10+2*big, 5, 10, 5+big));
onEdgeCase(makeList(90-small, 0, 0, 180-small/2),
makeList(big, -180+small/2, big, 180-small/4, big, 180-small),
makeList(-big, -180+small/2, -big, 180, -big, 180-small));
// Reaching close to North pole.
onEdgeCase(true, makeList(80, 0, 80, 180-small),
makeList(90-small, -90, 90, -135, 80-small, 0, 80+small, 0),
makeList(80, 90, 79, big));
onEdgeCase(false, makeList(80, 0, 80, 180-small),
makeList(80-small, 0, 80+small, 0, 80, 90),
makeList(79, big, 90-small, -90, 90, -135));
}
public void testContainsLocation() {
// Empty.
containsCase(makeList(),
makeList(),
makeList(0, 0));
// One point.
containsCase(makeList(1, 2),
makeList(1, 2),
makeList(0, 0));
// Two points.
containsCase(makeList(1, 2, 3, 5),
makeList(1, 2, 3, 5),
makeList(0, 0, 40, 4));
// Some arbitrary triangle.
containsCase(makeList(0., 0., 10., 12., 20., 5.),
makeList(10., 12., 10, 11, 19, 5),
makeList(0, 1, 11, 12, 30, 5, 0, -180, 0, 90));
// Around North Pole.
containsCase(makeList(89, 0, 89, 120, 89, -120),
makeList(90, 0, 90, 180, 90, -90),
makeList(-90, 0, 0, 0));
// Around South Pole.
containsCase(makeList(-89, 0, -89, 120, -89, -120),
makeList(90, 0, 90, 180, 90, -90, 0, 0),
makeList(-90, 0, -90, 90));
// Over/under segment on meridian and equator.
containsCase(makeList(5, 10, 10, 10, 0, 20, 0, -10),
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<LatLng> line = PolyUtil.decode(LINE);
assertEquals(95, line.size());
List<LatLng> simplifiedLine;
List<LatLng> 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<LatLng> 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<LatLng> 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<LatLng> line, List<LatLng> 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<LatLng> line, List<LatLng> 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<LatLng> line, List<LatLng> 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<LatLng> copyList(List<LatLng> original) {
ArrayList<LatLng> 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<LatLng> afterInput, List<LatLng> beforeInput) {
// Check values
assertEquals(beforeInput, afterInput);
// Check references
for (int i = 0; i < beforeInput.size(); i++) {
assertTrue(afterInput.get(i) == beforeInput.get(i));
}
}
public void testIsClosedPolygon() {
ArrayList<LatLng> 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<LatLng> latLngs = PolyUtil.decode(TEST_LINE);
int expectedLength = 21;
Assert.assertEquals("Wrong length.", expectedLength, latLngs.size());
LatLng lastPoint = latLngs.get(expectedLength - 1);
expectNearNumber(37.76953, lastPoint.latitude, 1e-6);
expectNearNumber(-122.41488, lastPoint.longitude, 1e-6);
}
public void testEncodePath() {
List<LatLng> path = PolyUtil.decode(TEST_LINE);
String encoded = PolyUtil.encode(path);
Assert.assertEquals(TEST_LINE, encoded);
}
}