diff --git a/CHANGELOG.md b/CHANGELOG.md index 348bd8227..12dc4af43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Mapbox welcomes participation and contributions from everyone. ### master +- Added support for Turf area measurement method [#1079](https://github.com/mapbox/mapbox-java/pull/1079) + ### 4.9.0 - September 23, 2019 - Added intersection search support to MapboxGeocoding [#1074](https://github.com/mapbox/mapbox-java/pull/1074) - Added support for Turf polygonToLine method [#1075](https://github.com/mapbox/mapbox-java/pull/1075) diff --git a/docs/turf-port.md b/docs/turf-port.md index c0d2a16bb..9621e9909 100644 --- a/docs/turf-port.md +++ b/docs/turf-port.md @@ -8,7 +8,7 @@ Below's an on going list of the Turf functions which currently exist inside the ## Measurement - [x] turf-along -- [ ] turf-area +- [x] turf-area - [x] turf-bbox - [x] turf-bbox-polygon - [x] turf-bearing diff --git a/services-turf/src/main/java/com/mapbox/turf/TurfMeasurement.java b/services-turf/src/main/java/com/mapbox/turf/TurfMeasurement.java index d47b52762..2f40680bb 100644 --- a/services-turf/src/main/java/com/mapbox/turf/TurfMeasurement.java +++ b/services-turf/src/main/java/com/mapbox/turf/TurfMeasurement.java @@ -39,6 +39,11 @@ private TurfMeasurement() { throw new AssertionError("No Instances."); } + /** + * Earth's radius in meters. + */ + public static double EARTH_RADIUS = 6378137; + /** * Takes two {@link Point}s and finds the geographic bearing between them. * @@ -572,4 +577,123 @@ public static BoundingBox square(@NonNull BoundingBox boundingBox) { ); } } + + /** + * Takes one {@link Feature} and returns it's area in square meters. + * + * @param feature input {@link Feature} + * @return area in square meters + * @since 4.10.0 + */ + public static double area(@NonNull Feature feature) { + return feature.geometry() != null ? area(feature.geometry()) : 0.0f; + } + + /** + * Takes one {@link FeatureCollection} and returns it's area in square meters. + * + * @param featureCollection input {@link FeatureCollection} + * @return area in square meters + * @since 4.10.0 + */ + public static double area(@NonNull FeatureCollection featureCollection) { + List features = featureCollection.features(); + double total = 0.0f; + if (features != null) { + for (Feature feature : features) { + total += area(feature); + } + } + return total; + } + + /** + * Takes one {@link Geometry} and returns it's area in square meters. + * + * @param geometry input {@link Geometry} + * @return area in square meters + * @since 4.10.0 + */ + public static double area(@NonNull Geometry geometry) { + return calculateArea(geometry); + } + + private static double calculateArea(@NonNull Geometry geometry) { + double total = 0.0f; + if (geometry instanceof Polygon) { + return polygonArea(((Polygon) geometry).coordinates()); + } else if (geometry instanceof MultiPolygon) { + List>> coordinates = ((MultiPolygon) geometry).coordinates(); + for (int i = 0; i < coordinates.size(); i++) { + total += polygonArea(coordinates.get(i)); + } + return total; + } else { + // Area should be 0 for case Point, MultiPoint, LineString and MultiLineString + return 0.0f; + } + } + + private static double polygonArea(@NonNull List> coordinates) { + double total = 0.0f; + if (coordinates.size() > 0) { + total += Math.abs(ringArea(coordinates.get(0))); + for (int i = 1; i < coordinates.size(); i++) { + total -= Math.abs(ringArea(coordinates.get(i))); + } + } + return total; + } + + /** + * Calculate the approximate area of the polygon were it projected onto the earth. + * Note that this area will be positive if ring is oriented clockwise, otherwise + * it will be negative. + * + * Reference: + * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for Polygons on a Sphere", + * JPL Publication 07-03, Jet Propulsion + * Laboratory, Pasadena, CA, June 2007 https://trs.jpl.nasa.gov/handle/2014/41271 + * + * @param coordinates A list of {@link Point} of Ring Coordinates + * @return The approximate signed geodesic area of the polygon in square meters. + */ + private static double ringArea(@NonNull List coordinates) { + Point p1; + Point p2; + Point p3; + int lowerIndex; + int middleIndex; + int upperIndex; + double total = 0.0f; + final int coordsLength = coordinates.size(); + + if (coordsLength > 2) { + for (int i = 0; i < coordsLength; i++) { + if (i == coordsLength - 2) { // i = N-2 + lowerIndex = coordsLength - 2; + middleIndex = coordsLength - 1; + upperIndex = 0; + } else if (i == coordsLength - 1) { // i = N-1 + lowerIndex = coordsLength - 1; + middleIndex = 0; + upperIndex = 1; + } else { // i = 0 to N-3 + lowerIndex = i; + middleIndex = i + 1; + upperIndex = i + 2; + } + p1 = coordinates.get(lowerIndex); + p2 = coordinates.get(middleIndex); + p3 = coordinates.get(upperIndex); + total += (rad(p3.longitude()) - rad(p1.longitude())) * Math.sin(rad(p2.latitude())); + } + total = total * EARTH_RADIUS * EARTH_RADIUS / 2; + } + return total; + } + + private static double rad(double num) { + return num * Math.PI / 180; + } } diff --git a/services-turf/src/test/java/com/mapbox/turf/TurfMeasurementTest.java b/services-turf/src/test/java/com/mapbox/turf/TurfMeasurementTest.java index 691a416b9..4b0c5c381 100644 --- a/services-turf/src/test/java/com/mapbox/turf/TurfMeasurementTest.java +++ b/services-turf/src/test/java/com/mapbox/turf/TurfMeasurementTest.java @@ -44,6 +44,14 @@ public class TurfMeasurementTest extends TestUtils { private static final String TURF_ENVELOPE_FEATURE_COLLECTION = "turf-envelope/feature-collection.geojson"; private static final String LINE_DISTANCE_MULTILINESTRING = "turf-line-distance/multilinestring.geojson"; + private static final String TURF_AREA_POLYGON_GEOJSON = "turf-area/polygon.geojson"; + private static final String TURF_AREA_POLYGON_RESULT = "turf-area/polygon.json"; + private static final String TURF_AREA_MULTIPOLYGON_GEOJSON = "turf-area/multi-polygon.geojson"; + private static final String TURF_AREA_MULTIPOLYGON_RESULT = "turf-area/multi-polygon.json"; + private static final String TURF_AREA_GEOM_POLYGON_GEOJSON = "turf-area/geometry-polygon.geojson"; + private static final String TURF_AREA_GEOM_POLYGON_RESULT = "turf-area/geometry-polygon.json"; + private static final String TURF_AREA_FEATURECOLLECTION_POLYGON_GEOJSON = "turf-area/featurecollection-polygon.geojson"; + private static final String TURF_AREA_FEATURECOLLECTION_POLYGON_RESULT = "turf-area/featurecollection-polygon.json"; @Rule public ExpectedException thrown = ExpectedException.none(); @@ -474,4 +482,30 @@ public void square(){ assertEquals(BoundingBox.fromCoordinates(-2.5, 0, 7.5, 10), sq1); assertEquals(BoundingBox.fromCoordinates(0, -2.5, 10, 7.5), sq2); } + + @Test + public void areaPolygon() { + double expected = Double.valueOf(loadJsonFixture(TURF_AREA_POLYGON_RESULT)); + assertEquals(expected, TurfMeasurement.area(Feature.fromJson(loadJsonFixture(TURF_AREA_POLYGON_GEOJSON))), 1); + } + + @Test + public void areaMultiPolygon() { + double expected = Double.valueOf(loadJsonFixture(TURF_AREA_MULTIPOLYGON_RESULT)); + assertEquals(expected, TurfMeasurement.area(Feature.fromJson(loadJsonFixture(TURF_AREA_MULTIPOLYGON_GEOJSON))), 1); + } + + @Test + public void areaGeometry() { + double expected = Double.valueOf(loadJsonFixture(TURF_AREA_GEOM_POLYGON_RESULT)); + assertEquals(expected, TurfMeasurement.area(Polygon.fromJson(loadJsonFixture(TURF_AREA_GEOM_POLYGON_GEOJSON))), 1); + } + + @Test + public void areaFeatureCollection() { + double expected = Double.valueOf(loadJsonFixture(TURF_AREA_FEATURECOLLECTION_POLYGON_RESULT)); + assertEquals(expected, TurfMeasurement.area(FeatureCollection.fromJson(loadJsonFixture(TURF_AREA_FEATURECOLLECTION_POLYGON_GEOJSON))), 1); + } + + } diff --git a/services-turf/src/test/resources/turf-area/featurecollection-polygon.geojson b/services-turf/src/test/resources/turf-area/featurecollection-polygon.geojson new file mode 100644 index 000000000..e5bde0805 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/featurecollection-polygon.geojson @@ -0,0 +1,75 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -2.109375, + 47.040182144806664 + ], + [ + 4.5703125, + 44.59046718130883 + ], + [ + 7.03125, + 49.15296965617042 + ], + [ + -3.515625, + 49.83798245308484 + ], + [ + -2.109375, + 47.040182144806664 + ] + ] + ] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 9.64599609375, + 47.70976154266637 + ], + [ + 9.4482421875, + 47.73932336136857 + ], + [ + 8.8330078125, + 47.47266286861342 + ], + [ + 10.21728515625, + 46.604167162931844 + ], + [ + 11.755371093749998, + 46.81509864599243 + ], + [ + 11.865234375, + 47.90161354142077 + ], + [ + 9.64599609375, + 47.70976154266637 + ] + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/services-turf/src/test/resources/turf-area/featurecollection-polygon.json b/services-turf/src/test/resources/turf-area/featurecollection-polygon.json new file mode 100644 index 000000000..cf516b0df --- /dev/null +++ b/services-turf/src/test/resources/turf-area/featurecollection-polygon.json @@ -0,0 +1 @@ +294852371360 \ No newline at end of file diff --git a/services-turf/src/test/resources/turf-area/geometry-polygon.geojson b/services-turf/src/test/resources/turf-area/geometry-polygon.geojson new file mode 100644 index 000000000..b4a5e3606 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/geometry-polygon.geojson @@ -0,0 +1,10 @@ +{ + "type": "Polygon", + "coordinates": [[ + [-2.275543, 53.464547 ], + [-2.275543, 53.489271 ], + [-2.215118, 53.489271 ], + [-2.215118, 53.464547 ], + [-2.275543, 53.464547 ] + ]] +} diff --git a/services-turf/src/test/resources/turf-area/geometry-polygon.json b/services-turf/src/test/resources/turf-area/geometry-polygon.json new file mode 100644 index 000000000..abd6005e0 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/geometry-polygon.json @@ -0,0 +1 @@ +11017976 \ No newline at end of file diff --git a/services-turf/src/test/resources/turf-area/multi-polygon.geojson b/services-turf/src/test/resources/turf-area/multi-polygon.geojson new file mode 100644 index 000000000..0fc8fc5c1 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/multi-polygon.geojson @@ -0,0 +1,60 @@ +{ + "type": "Feature", + "properties": { + "stroke": "#F00", + "stroke-width": 6 + }, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + 102, + 2 + ], + [ + 103, + 2 + ], + [ + 103, + 3 + ], + [ + 102, + 3 + ], + [ + 102, + 2 + ] + ] + ], + [ + [ + [ + 100, + 0 + ], + [ + 101, + 0 + ], + [ + 101, + 1 + ], + [ + 100, + 1 + ], + [ + 100, + 0 + ] + ] + ] + ] + } +} \ No newline at end of file diff --git a/services-turf/src/test/resources/turf-area/multi-polygon.json b/services-turf/src/test/resources/turf-area/multi-polygon.json new file mode 100644 index 000000000..59e3bae95 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/multi-polygon.json @@ -0,0 +1 @@ +24771477332 \ No newline at end of file diff --git a/services-turf/src/test/resources/turf-area/polygon.geojson b/services-turf/src/test/resources/turf-area/polygon.geojson new file mode 100644 index 000000000..e9b1c8ce7 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/polygon.geojson @@ -0,0 +1,19 @@ +{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [125, -15], + [113, -22], + [117, -37], + [130, -33], + [148, -39], + [154, -27], + [144, -15], + [125, -15] + ] + ] + } +} \ No newline at end of file diff --git a/services-turf/src/test/resources/turf-area/polygon.json b/services-turf/src/test/resources/turf-area/polygon.json new file mode 100644 index 000000000..9a8d74ff6 --- /dev/null +++ b/services-turf/src/test/resources/turf-area/polygon.json @@ -0,0 +1 @@ +7766240997209