From e4a40fe7974462369533722a20d252b753a55347 Mon Sep 17 00:00:00 2001 From: Osana Babayan <32496536+osana@users.noreply.github.com> Date: Thu, 15 Feb 2018 12:02:00 -0500 Subject: [android] bounds can go over the antimeridian / date line. (#10892) --- .../mapboxsdk/constants/GeometryConstants.java | 14 +++ .../mapbox/mapboxsdk/geometry/LatLngBounds.java | 112 ++++++++++++++---- .../mapboxsdk/geometry/LatLngBoundsTest.java | 131 +++++++++++++++++++++ 3 files changed, 236 insertions(+), 21 deletions(-) (limited to 'platform') diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java index 1a7544d33a..7a17e500ca 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/constants/GeometryConstants.java @@ -36,6 +36,20 @@ public class GeometryConstants { */ public static final double MIN_LATITUDE = -90; + /** + * This constant represents the latitude span when representing a geolocation. + * + * @since 6.0.0 + */ + public static final double LATITUDE_SPAN = 180; + + /** + * This constant represents the longitude span when representing a geolocation. + * + * @since 6.0.0 + */ + public static final double LONGITUDE_SPAN = 360; + /** * This constant represents the highest latitude value available to represent a geolocation. * diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java index cf647224ae..fc8d2ec8f0 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/geometry/LatLngBounds.java @@ -29,6 +29,10 @@ public class LatLngBounds implements Parcelable { * Construct a new LatLngBounds based on its corners, given in NESW * order. * + * If eastern longitude is smaller than the western one, bounds will include antimeridian. + * For example, if the NE point is (10, -170) and the SW point is (-10, 170), then bounds will span over 20 degrees + * and cross the antimeridian. + * * @param northLatitude Northern Latitude * @param eastLongitude Eastern Longitude * @param southLatitude Southern Latitude @@ -48,10 +52,9 @@ public class LatLngBounds implements Parcelable { * @return the bounds representing the world */ public static LatLngBounds world() { - return new LatLngBounds.Builder() - .include(new LatLng(GeometryConstants.MAX_LATITUDE, GeometryConstants.MAX_LONGITUDE)) - .include(new LatLng(GeometryConstants.MIN_LATITUDE, GeometryConstants.MIN_LONGITUDE)) - .build(); + return LatLngBounds.from( + GeometryConstants.MAX_LATITUDE, GeometryConstants.MAX_LONGITUDE, + GeometryConstants.MIN_LATITUDE, GeometryConstants.MIN_LONGITUDE); } /** @@ -61,8 +64,21 @@ public class LatLngBounds implements Parcelable { * @return LatLng center of this LatLngBounds */ public LatLng getCenter() { - return new LatLng((this.latitudeNorth + this.latitudeSouth) / 2, - (this.longitudeEast + this.longitudeWest) / 2); + double latCenter = (this.latitudeNorth + this.latitudeSouth) / 2.0; + double longCenter; + + if (this.longitudeEast > this.longitudeWest) { + longCenter = (this.longitudeEast + this.longitudeWest) / 2; + } else { + double halfSpan = (GeometryConstants.LONGITUDE_SPAN + this.longitudeEast - this.longitudeWest) / 2.0; + longCenter = this.longitudeWest + halfSpan; + if (longCenter >= GeometryConstants.MAX_LONGITUDE) { + longCenter = this.longitudeEast - halfSpan; + } + return new LatLng(latCenter, longCenter); + } + + return new LatLng(latCenter, longCenter); } /** @@ -163,10 +179,26 @@ public class LatLngBounds implements Parcelable { * @return Span distance */ public double getLongitudeSpan() { - return Math.abs(this.longitudeEast - this.longitudeWest); + double longSpan = Math.abs(this.longitudeEast - this.longitudeWest); + if (this.longitudeEast > this.longitudeWest) { + return longSpan; + } + + // shortest span contains antimeridian + return GeometryConstants.LONGITUDE_SPAN - longSpan; } + static double getLongitudeSpan(final double longEast, final double longWest) { + double longSpan = Math.abs(longEast - longWest); + if (longEast > longWest) { + return longSpan; + } + + // shortest span contains antimeridian + return GeometryConstants.LONGITUDE_SPAN - longSpan; + } + /** * Validate if LatLngBounds is empty, determined if absolute distance is * @@ -196,21 +228,44 @@ public class LatLngBounds implements Parcelable { */ static LatLngBounds fromLatLngs(final List latLngs) { double minLat = GeometryConstants.MAX_LATITUDE; - double minLon = GeometryConstants.MAX_LONGITUDE; double maxLat = GeometryConstants.MIN_LATITUDE; - double maxLon = GeometryConstants.MIN_LONGITUDE; + + double eastLon = latLngs.get(0).getLongitude(); + double westLon = latLngs.get(1).getLongitude(); + double lonSpan = Math.abs(eastLon - westLon); + if (lonSpan < GeometryConstants.LONGITUDE_SPAN / 2) { + if (eastLon < westLon) { + double temp = eastLon; + eastLon = westLon; + westLon = temp; + } + } else { + lonSpan = GeometryConstants.LONGITUDE_SPAN - lonSpan; + if (westLon < eastLon) { + double temp = eastLon; + eastLon = westLon; + westLon = temp; + } + } for (final ILatLng gp : latLngs) { final double latitude = gp.getLatitude(); - final double longitude = gp.getLongitude(); - minLat = Math.min(minLat, latitude); - minLon = Math.min(minLon, longitude); maxLat = Math.max(maxLat, latitude); - maxLon = Math.max(maxLon, longitude); + + final double longitude = gp.getLongitude(); + if (!containsLongitude(eastLon, westLon, longitude)) { + final double eastSpan = getLongitudeSpan(longitude, westLon); + final double westSpan = getLongitudeSpan(eastLon, longitude); + if (eastSpan <= westSpan) { + eastLon = longitude; + } else { + westLon = longitude; + } + } } - return new LatLngBounds(maxLat, maxLon, minLat, minLon); + return new LatLngBounds(maxLat, eastLon, minLat, westLon); } /** @@ -322,6 +377,24 @@ public class LatLngBounds implements Parcelable { return false; } + + private boolean containsLatitude(final double latitude) { + return (latitude <= this.latitudeNorth) + && (latitude >= this.latitudeSouth); + } + + private boolean containsLongitude(final double longitude) { + return containsLongitude(this.longitudeEast, this.longitudeWest, longitude); + } + + static boolean containsLongitude(final double eastLon, final double westLon, final double longitude) { + if (eastLon > westLon) { + return (longitude <= eastLon) + && (longitude >= westLon); + } + return (longitude < eastLon) || (longitude > westLon); + } + /** * Determines whether this LatLngBounds contains a point. * @@ -329,12 +402,8 @@ public class LatLngBounds implements Parcelable { * @return true, if the point is contained within the bounds */ public boolean contains(final ILatLng latLng) { - final double latitude = latLng.getLatitude(); - final double longitude = latLng.getLongitude(); - return ((latitude <= this.latitudeNorth) - && (latitude >= this.latitudeSouth)) - && ((longitude <= this.longitudeEast) - && (longitude >= this.longitudeWest)); + return containsLatitude(latLng.getLatitude()) + && containsLongitude(latLng.getLongitude()); } /** @@ -344,7 +413,8 @@ public class LatLngBounds implements Parcelable { * @return true, if the bounds is contained within the bounds */ public boolean contains(final LatLngBounds other) { - return contains(other.getNorthEast()) && contains(other.getSouthWest()); + return contains(other.getNorthEast()) + && contains(other.getSouthWest()); } /** diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java index e6c1fdd0cf..f03bbdb11c 100644 --- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/geometry/LatLngBoundsTest.java @@ -70,6 +70,82 @@ public class LatLngBoundsTest { assertEquals("LatLngSpan should be the same", new LatLngSpan(2, 2), latLngSpan); } + @Test + public void dateLineSpanBuilder1() { + latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, -170)) + .include(new LatLng(-10, 170)) + .build(); + + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be shortest distance", new LatLngSpan(20, 20), + latLngSpan); + } + + @Test + public void dateLineSpanBuilder2() { + latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(-10, -170)) + .include(new LatLng(10, 170)) + .build(); + + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be shortest distance", new LatLngSpan(20, 20), + latLngSpan); + } + + @Test + public void dateLineSpanFrom1() { + latLngBounds = LatLngBounds.from(10, -170, -10, 170); + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be shortest distance", new LatLngSpan(20, 20), + latLngSpan); + } + + @Test + public void dateLineSpanFrom2() { + latLngBounds = LatLngBounds.from(10, 170, -10, -170); + LatLngSpan latLngSpan = latLngBounds.getSpan(); + assertEquals("LatLngSpan should be shortest distance", new LatLngSpan(20, 340), + latLngSpan); + } + + @Test + public void nearDateLineCenter1() { + latLngBounds = LatLngBounds.from(10, -175, -10, 165); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 175), center); + } + + @Test + public void nearDateLineCenter2() { + latLngBounds = LatLngBounds.from(10, -165, -10, 175); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, -175), center); + } + + @Test + public void nearDateLineCenter3() { + latLngBounds = LatLngBounds.from(10, -170, -10, 170); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, -180), center); + } + + @Test + public void nearDateLineCenter4() { + latLngBounds = LatLngBounds.from(10, -180, -10, 0); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 90), center); + } + + @Test + public void nearDateLineCenter5() { + latLngBounds = LatLngBounds.from(10, 180, -10, 0); + LatLng center = latLngBounds.getCenter(); + assertEquals("Center should match", new LatLng(0, 90), center); + } + + @Test public void center() { LatLng center = latLngBounds.getCenter(); @@ -120,6 +196,46 @@ public class LatLngBoundsTest { assertEquals("LatLngBounds should match", latLngBounds1, latLngBounds2); } + @Test + public void includesOverDateline1() { + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, -170)) + .include(new LatLng(-10, -175)) + .include(new LatLng(0, 170)) + .build(); + + assertEquals("LatLngSpan should be the same", + new LatLngSpan(20, 20), latLngBounds.getSpan()); + } + + @Test + public void includesOverDateline2() { + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, 170)) + .include(new LatLng(-10, 175)) + .include(new LatLng(0, -170)) + .build(); + + assertEquals("LatLngSpan should be the same", + new LatLngSpan(20, 20), latLngBounds.getSpan()); + } + + @Test + public void includesOverDateline3() { + + LatLngBounds latLngBounds = new LatLngBounds.Builder() + .include(new LatLng(10, 170)) + .include(new LatLng(-10, -170)) + .include(new LatLng(0, -180)) + .include(new LatLng(5, 180)) + .build(); + + assertEquals("LatLngSpan should be the same", + new LatLngSpan(20, 20), latLngBounds.getSpan()); + } + @Test public void containsNot() { assertFalse("LatLng should not be included", latLngBounds.contains(new LatLng(3, 1))); @@ -130,6 +246,21 @@ public class LatLngBoundsTest { assertTrue("LatLngBounds should be contained in the world", LatLngBounds.world().contains(latLngBounds)); } + @Test + public void worldSpan() { + assertEquals("LatLngBounds world span should be 180, 360", + GeometryConstants.LATITUDE_SPAN, LatLngBounds.world().getLatitudeSpan(), DELTA); + assertEquals("LatLngBounds world span should be 180, 360", + GeometryConstants.LONGITUDE_SPAN, LatLngBounds.world().getLongitudeSpan(), DELTA); + } + + @Test + public void emptySpan() { + LatLngBounds latLngBounds = LatLngBounds.from(GeometryConstants.MIN_LATITUDE, GeometryConstants.MAX_LONGITUDE, + GeometryConstants.MIN_LATITUDE, GeometryConstants.MAX_LONGITUDE); + assertTrue("LatLngBounds empty span", latLngBounds.isEmptySpan()); + } + @Test public void containsBounds() { LatLngBounds inner = new LatLngBounds.Builder() -- cgit v1.2.1