package com.google.maps.android; import com.google.android.gms.maps.model.LatLng; import junit.framework.Assert; import junit.framework.TestCase; import java.util.Arrays; import java.util.Collections; import java.util.List; public class SphericalUtilTest extends TestCase { static final double EARTH_RADIUS = SphericalUtil.EARTH_RADIUS; // The vertices of an octahedron, for testing private final LatLng up = new LatLng(90, 0); private final LatLng down = new LatLng(-90, 0); private final LatLng front = new LatLng(0, 0); private final LatLng right = new LatLng(0, 90); private final LatLng back = new LatLng(0, -180); private final LatLng left = new LatLng(0, -90); private static void expectEq(Object expected, Object actual) { Assert.assertEquals(expected, actual); } /** * Tests for approximate equality. */ private static void expectLatLngApproxEquals(LatLng actual, LatLng expected) { expectNearNumber(actual.latitude, expected.latitude, 1e-6); // Account for the convergence of longitude lines at the poles double cosLat = Math.cos(Math.toRadians(actual.latitude)); expectNearNumber(cosLat * actual.longitude, cosLat * expected.longitude, 1e-6); } private static void expectNearNumber(double actual, double expected, double epsilon) { Assert.assertTrue(String.format("Expected %f to be near %f", actual, expected), Math.abs(expected - actual) <= epsilon); } public void testAngles() { // Same vertex expectNearNumber(SphericalUtil.computeAngleBetween(up, up), 0, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(down, down), 0, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(left, left), 0, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(right, right), 0, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(front, front), 0, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(back, back), 0, 1e-6); // Adjacent vertices expectNearNumber(SphericalUtil.computeAngleBetween(up, front), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(up, right), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(up, back), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(up, left), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(down, front), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(down, right), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(down, back), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(down, left), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(back, up), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(back, right), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(back, down), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(back, left), Math.PI / 2, 1e-6); // Opposite vertices expectNearNumber(SphericalUtil.computeAngleBetween(up, down), Math.PI, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(front, back), Math.PI, 1e-6); expectNearNumber(SphericalUtil.computeAngleBetween(left, right), Math.PI, 1e-6); } public void testDistances() { expectNearNumber(SphericalUtil.computeDistanceBetween(up, down), Math.PI * EARTH_RADIUS, 1e-6); } public void testHeadings() { // Opposing vertices for which there is a result expectNearNumber(SphericalUtil.computeHeading(up, down), -180, 1e-6); expectNearNumber(SphericalUtil.computeHeading(down, up), 0, 1e-6); // Adjacent vertices for which there is a result expectNearNumber(SphericalUtil.computeHeading(front, up), 0, 1e-6); expectNearNumber(SphericalUtil.computeHeading(right, up), 0, 1e-6); expectNearNumber(SphericalUtil.computeHeading(back, up), 0, 1e-6); expectNearNumber(SphericalUtil.computeHeading(down, up), 0, 1e-6); expectNearNumber(SphericalUtil.computeHeading(front, down), -180, 1e-6); expectNearNumber(SphericalUtil.computeHeading(right, down), -180, 1e-6); expectNearNumber(SphericalUtil.computeHeading(back, down), -180, 1e-6); expectNearNumber(SphericalUtil.computeHeading(left, down), -180, 1e-6); expectNearNumber(SphericalUtil.computeHeading(right, front), -90, 1e-6); expectNearNumber(SphericalUtil.computeHeading(left, front), 90, 1e-6); expectNearNumber(SphericalUtil.computeHeading(front, right), 90, 1e-6); expectNearNumber(SphericalUtil.computeHeading(back, right), -90, 1e-6); } public void testComputeOffset() { // From front expectLatLngApproxEquals( front, SphericalUtil.computeOffset(front, 0, 0)); expectLatLngApproxEquals( up, SphericalUtil.computeOffset(front, Math.PI * EARTH_RADIUS / 2, 0)); expectLatLngApproxEquals( down, SphericalUtil.computeOffset(front, Math.PI * EARTH_RADIUS / 2, 180)); expectLatLngApproxEquals( left, SphericalUtil.computeOffset(front, Math.PI * EARTH_RADIUS / 2, -90)); expectLatLngApproxEquals( right, SphericalUtil.computeOffset(front, Math.PI * EARTH_RADIUS / 2, 90)); expectLatLngApproxEquals( back, SphericalUtil.computeOffset(front, Math.PI * EARTH_RADIUS, 0)); expectLatLngApproxEquals( back, SphericalUtil.computeOffset(front, Math.PI * EARTH_RADIUS, 90)); // From left expectLatLngApproxEquals( left, SphericalUtil.computeOffset(left, 0, 0)); expectLatLngApproxEquals( up, SphericalUtil.computeOffset(left, Math.PI * EARTH_RADIUS / 2, 0)); expectLatLngApproxEquals( down, SphericalUtil.computeOffset(left, Math.PI * EARTH_RADIUS / 2, 180)); expectLatLngApproxEquals( front, SphericalUtil.computeOffset(left, Math.PI * EARTH_RADIUS / 2, 90)); expectLatLngApproxEquals( back, SphericalUtil.computeOffset(left, Math.PI * EARTH_RADIUS / 2, -90)); expectLatLngApproxEquals( right, SphericalUtil.computeOffset(left, Math.PI * EARTH_RADIUS, 0)); expectLatLngApproxEquals( right, SphericalUtil.computeOffset(left, Math.PI * EARTH_RADIUS, 90)); // NOTE(appleton): Heading is undefined at the poles, so we do not test // from up/down. } public void testComputeOffsetOrigin() { expectLatLngApproxEquals( front, SphericalUtil.computeOffsetOrigin(front, 0, 0)); expectLatLngApproxEquals( front, SphericalUtil.computeOffsetOrigin(new LatLng(0, 45), Math.PI * EARTH_RADIUS / 4, 90)); expectLatLngApproxEquals( front, SphericalUtil.computeOffsetOrigin(new LatLng(0, -45), Math.PI * EARTH_RADIUS / 4, -90)); expectLatLngApproxEquals( front, SphericalUtil.computeOffsetOrigin(new LatLng(45, 0), Math.PI * EARTH_RADIUS / 4, 0)); expectLatLngApproxEquals( front, SphericalUtil.computeOffsetOrigin(new LatLng(-45, 0), Math.PI * EARTH_RADIUS / 4, 180)); /*expectLatLngApproxEquals( front, SphericalUtil.computeOffsetOrigin(new LatLng(-45, 0), Math.PI / 4, 180, 1)); */ // Situations with no solution, should return null. // // First 'over' the pole. expectEq(null, SphericalUtil.computeOffsetOrigin(new LatLng(80, 0), Math.PI * EARTH_RADIUS / 4, 180)); // Second a distance that doesn't fit on the earth. expectEq(null, SphericalUtil.computeOffsetOrigin(new LatLng(80, 0), Math.PI * EARTH_RADIUS / 4, 90)); } public void testComputeOffsetAndBackToOrigin() { LatLng start = new LatLng(40, 40); double distance = 1e5; double heading = 15; LatLng end; // Some semi-random values to demonstrate going forward and backward yields // the same location. end = SphericalUtil.computeOffset(start, distance, heading); expectLatLngApproxEquals( start, SphericalUtil.computeOffsetOrigin(end, distance, heading)); heading = -37; end = SphericalUtil.computeOffset(start, distance, heading); expectLatLngApproxEquals( start, SphericalUtil.computeOffsetOrigin(end, distance, heading)); distance = 3.8e+7; end = SphericalUtil.computeOffset(start, distance, heading); expectLatLngApproxEquals( start, SphericalUtil.computeOffsetOrigin(end, distance, heading)); start = new LatLng(-21, -73); end = SphericalUtil.computeOffset(start, distance, heading); expectLatLngApproxEquals( start, SphericalUtil.computeOffsetOrigin(end, distance, heading)); // computeOffsetOrigin with multiple solutions, all we care about is that // going from there yields the requested result. // // First, for this particular situation the latitude is completely arbitrary. start = SphericalUtil.computeOffsetOrigin(new LatLng(0, 90), Math.PI * EARTH_RADIUS / 2, 90); expectLatLngApproxEquals( new LatLng(0, 90), SphericalUtil.computeOffset(start, Math.PI * EARTH_RADIUS / 2, 90)); // Second, for this particular situation the longitude is completely // arbitrary. start = SphericalUtil.computeOffsetOrigin(new LatLng(90, 0), Math.PI * EARTH_RADIUS / 4, 0); expectLatLngApproxEquals( new LatLng(90, 0), SphericalUtil.computeOffset(start, Math.PI * EARTH_RADIUS / 4, 0)); } public void testInterpolate() { // Same point expectLatLngApproxEquals( up, SphericalUtil.interpolate(up, up, 1 / 2.0)); expectLatLngApproxEquals( down, SphericalUtil.interpolate(down, down, 1 / 2.0)); expectLatLngApproxEquals( left, SphericalUtil.interpolate(left, left, 1 / 2.0)); // Between front and up expectLatLngApproxEquals( new LatLng(1, 0), SphericalUtil.interpolate(front, up, 1 / 90.0)); expectLatLngApproxEquals( new LatLng(1, 0), SphericalUtil.interpolate(up, front, 89 / 90.0)); expectLatLngApproxEquals( new LatLng(89, 0), SphericalUtil.interpolate(front, up, 89 / 90.0)); expectLatLngApproxEquals( new LatLng(89, 0), SphericalUtil.interpolate(up, front, 1 / 90.0)); // Between front and down expectLatLngApproxEquals( new LatLng(-1, 0), SphericalUtil.interpolate(front, down, 1 / 90.0)); expectLatLngApproxEquals( new LatLng(-1, 0), SphericalUtil.interpolate(down, front, 89 / 90.0)); expectLatLngApproxEquals( new LatLng(-89, 0), SphericalUtil.interpolate(front, down, 89 / 90.0)); expectLatLngApproxEquals( new LatLng(-89, 0), SphericalUtil.interpolate(down, front, 1 / 90.0)); // Between left and back expectLatLngApproxEquals( new LatLng(0, -91), SphericalUtil.interpolate(left, back, 1 / 90.0)); expectLatLngApproxEquals( new LatLng(0, -91), SphericalUtil.interpolate(back, left, 89 / 90.0)); expectLatLngApproxEquals( new LatLng(0, -179), SphericalUtil.interpolate(left, back, 89 / 90.0)); expectLatLngApproxEquals( new LatLng(0, -179), SphericalUtil.interpolate(back, left, 1 / 90.0)); // geodesic crosses pole expectLatLngApproxEquals( up, SphericalUtil.interpolate(new LatLng(45, 0), new LatLng(45, 180), 1 / 2.0)); expectLatLngApproxEquals( down, SphericalUtil.interpolate(new LatLng(-45, 0), new LatLng(-45, 180), 1 / 2.0)); } public void testComputeLength() { List<LatLng> latLngs; expectNearNumber(SphericalUtil.computeLength(Collections.<LatLng>emptyList()), 0, 1e-6); expectNearNumber(SphericalUtil.computeLength(Arrays.asList(new LatLng(0, 0))), 0, 1e-6); latLngs = Arrays.asList(new LatLng(0, 0), new LatLng(0.1, 0.1)); expectNearNumber(SphericalUtil.computeLength(latLngs), Math.toRadians(0.1) * Math.sqrt(2) * EARTH_RADIUS, 1); latLngs = Arrays.asList(new LatLng(0, 0), new LatLng(90, 0), new LatLng(0, 90)); expectNearNumber(SphericalUtil.computeLength(latLngs), Math.PI * EARTH_RADIUS, 1e-6); } public void testIsCCW() { // One face of the octahedron expectEq(1, SphericalUtil.isCCW(right, up, front)); expectEq(1, SphericalUtil.isCCW(up, front, right)); expectEq(1, SphericalUtil.isCCW(front, right, up)); expectEq(-1, SphericalUtil.isCCW(front, up, right)); expectEq(-1, SphericalUtil.isCCW(up, right, front)); expectEq(-1, SphericalUtil.isCCW(right, front, up)); } public void testComputeTriangleArea() { expectNearNumber(SphericalUtil.computeTriangleArea(right, up, front), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeTriangleArea(front, up, right), Math.PI / 2, 1e-6); // computeArea returns area of zero on small polys double area = SphericalUtil.computeTriangleArea( new LatLng(0, 0), new LatLng(0, Math.toDegrees(1E-6)), new LatLng(Math.toDegrees(1E-6), 0)); double expectedArea = 1E-12 / 2; Assert.assertTrue(Math.abs(expectedArea - area) < 1e-20); } public void testComputeSignedTriangleArea() { expectNearNumber( SphericalUtil.computeSignedTriangleArea( new LatLng(0, 0), new LatLng(0, 0.1), new LatLng(0.1, 0.1)), Math.toRadians(0.1) * Math.toRadians(0.1) / 2, 1e-6); expectNearNumber(SphericalUtil.computeSignedTriangleArea(right, up, front), Math.PI / 2, 1e-6); expectNearNumber(SphericalUtil.computeSignedTriangleArea(front, up, right), -Math.PI / 2, 1e-6); } public void testComputeArea() { expectNearNumber(SphericalUtil.computeArea(Arrays.asList(right, up, front, down, right)), Math.PI * EARTH_RADIUS * EARTH_RADIUS, .4); expectNearNumber(SphericalUtil.computeArea(Arrays.asList(right, down, front, up, right)), Math.PI * EARTH_RADIUS * EARTH_RADIUS, .4); } public void testComputeSignedArea() { List<LatLng> path = Arrays.asList(right, up, front, down, right); List<LatLng> pathReversed = Arrays.asList(right, down, front, up, right); expectEq(-SphericalUtil.computeSignedArea(path), SphericalUtil.computeSignedArea(pathReversed)); } }