Skip to content

Commit 30d7171

Browse files
committed
introduce TurfSimplify
1 parent 0b9f41b commit 30d7171

19 files changed

Lines changed: 825 additions & 2 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Mapbox welcomes participation and contributions from everyone.
44

55
### main
6+
- Added `TurfSimplify#simplify` method to simplify `LineString` using Ramer-Douglas-Peucker algorithm. [1486](https://github.com/mapbox/mapbox-java/pull/1486)
67

78
### v6.8.0-beta.3 - September 1, 2022
89
- Added `subTypes` to `BannerComponents`. [#1485](https://github.com/mapbox/mapbox-java/pull/1485)

samples/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies {
2626
implementation project(":services-tilequery")
2727
implementation project(":services-directions-refresh")
2828
implementation project(":services-isochrone")
29+
implementation project(":services-turf")
2930
}
3031

3132
buildConfig {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.mapbox.samples;
2+
3+
import com.mapbox.api.directions.v5.DirectionsCriteria;
4+
import com.mapbox.api.directions.v5.models.DirectionsResponse;
5+
import com.mapbox.api.directions.v5.MapboxDirections;
6+
import com.mapbox.api.directions.v5.models.RouteOptions;
7+
import com.mapbox.core.constants.Constants;
8+
import com.mapbox.geojson.LineString;
9+
import com.mapbox.geojson.Point;
10+
import com.mapbox.sample.BuildConfig;
11+
import com.mapbox.turf.TurfSimplify;
12+
import retrofit2.Response;
13+
14+
import java.io.IOException;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
public class BasicTurfSimplify {
19+
20+
public static void main(String[] args) throws IOException {
21+
LineString complexLineString = generateLineString();
22+
LineString simplifiedLineString = LineString.fromLngLats(
23+
TurfSimplify.simplify(complexLineString.coordinates(), 0.001, true),
24+
complexLineString.bbox()
25+
);
26+
System.out.println("[Turf] complex: " + complexLineString.coordinates());
27+
System.out.println("[Turf] simplified: " + simplifiedLineString.coordinates());
28+
}
29+
30+
private static LineString generateLineString() throws IOException {
31+
MapboxDirections.Builder builder = MapboxDirections.builder();
32+
33+
// 1. Pass in all the required information to get a simple directions route.
34+
List<Point> coordinates = new ArrayList<>();
35+
coordinates.add(Point.fromLngLat(-95.6332, 29.7890));
36+
coordinates.add(Point.fromLngLat(-95.3591, 29.7576));
37+
RouteOptions routeOptions = RouteOptions.builder()
38+
.coordinatesList(coordinates)
39+
.profile(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC)
40+
.build();
41+
builder.routeOptions(routeOptions);
42+
builder.accessToken(BuildConfig.MAPBOX_ACCESS_TOKEN);
43+
44+
// 2. That's it! Now execute the command and get the response.
45+
Response<DirectionsResponse> response = builder.build().executeCall();
46+
return LineString.fromPolyline(response.body().routes().get(0).geometry(), Constants.PRECISION_6);
47+
}
48+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.mapbox.turf;
2+
3+
import androidx.annotation.NonNull;
4+
import com.mapbox.geojson.Point;
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
9+
/**
10+
* Use this class to simplify a complex line via Douglas-Peucker and radial algorithms.
11+
*/
12+
public class TurfSimplify {
13+
14+
/**
15+
* Simplifies provided list of coordinates points into a shorter list of coordinates.
16+
*
17+
* @param original the coordinates to simplify
18+
* @param tolerance tolerance in the same measurement as the point coordinates
19+
* @param highQuality true for using Douglas-Peucker, false for Radial-Distance algorithm
20+
* @return simplified coordinates
21+
*/
22+
public static List<Point> simplify(
23+
@NonNull List<Point> original,
24+
double tolerance,
25+
boolean highQuality
26+
) {
27+
if (original.size() <= 2) {
28+
return original;
29+
}
30+
double squaredTolerance = tolerance * tolerance;
31+
List<Point> dpInput = highQuality ? original : simplifyRadial(original, squaredTolerance);
32+
return simplifyDouglasPeucker(dpInput, squaredTolerance);
33+
}
34+
35+
private static List<Point> simplifyRadial(
36+
@NonNull List<Point> original,
37+
double squaredTolerance
38+
) {
39+
if (original.size() <= 2) {
40+
return original;
41+
}
42+
Point prevCoordinate = original.get(0);
43+
List<Point> newCoordinates = new ArrayList<>();
44+
newCoordinates.add(prevCoordinate);
45+
Point coordinate = original.get(1);
46+
47+
for (int i = 1; i < original.size(); i++) {
48+
coordinate = original.get(i);
49+
if (squaredDistance(coordinate, prevCoordinate) > squaredTolerance) {
50+
newCoordinates.add(coordinate);
51+
prevCoordinate = coordinate;
52+
}
53+
}
54+
55+
if (prevCoordinate != coordinate) {
56+
newCoordinates.add(coordinate);
57+
}
58+
59+
return newCoordinates;
60+
}
61+
62+
private static double squaredDistance(@NonNull Point p1, @NonNull Point p2) {
63+
double dx = p2.longitude() - p1.longitude();
64+
double dy = p2.latitude() - p1.latitude();
65+
return dx * dx + dy * dy;
66+
}
67+
68+
private static List<Point> simplifyDouglasPeucker(
69+
@NonNull List<Point> original,
70+
double tolerance
71+
) {
72+
if (original.size() <= 2) {
73+
return original;
74+
}
75+
76+
int lastPointIndex = original.size() - 1;
77+
List<Point> result = new ArrayList<>();
78+
result.add(original.get(0));
79+
simplifyDouglasPeuckerStep(original, 0, lastPointIndex, tolerance, result);
80+
result.add(original.get(lastPointIndex));
81+
return result;
82+
}
83+
84+
private static void simplifyDouglasPeuckerStep(
85+
@NonNull List<Point> original,
86+
int startIndex,
87+
int endIndex,
88+
double tolerance,
89+
@NonNull List<Point> simplified
90+
) {
91+
double maxSquaredDistance = tolerance;
92+
int index = 0;
93+
94+
for (int i = startIndex + 1; i < endIndex; i++) {
95+
double squaredDistance = squaredSegmentDistance(
96+
original.get(i),
97+
original.get(startIndex),
98+
original.get(endIndex)
99+
);
100+
if (squaredDistance > maxSquaredDistance) {
101+
index = i;
102+
maxSquaredDistance = squaredDistance;
103+
}
104+
}
105+
106+
if (maxSquaredDistance > tolerance) {
107+
if (index - startIndex > 1) {
108+
simplifyDouglasPeuckerStep(original, startIndex, index, tolerance, simplified);
109+
}
110+
simplified.add(original.get(index));
111+
if (endIndex - index > 1) {
112+
simplifyDouglasPeuckerStep(original, index, endIndex, tolerance, simplified);
113+
}
114+
}
115+
}
116+
117+
private static double squaredSegmentDistance(
118+
@NonNull Point point,
119+
@NonNull Point segmentStart,
120+
@NonNull Point segmentEnd
121+
) {
122+
double x = segmentStart.latitude();
123+
double y = segmentStart.longitude();
124+
double dx = segmentEnd.latitude() - x;
125+
double dy = segmentEnd.longitude() - y;
126+
127+
if (dx != 0 || dy != 0) {
128+
double t = ((point.latitude() - x) * dx + (point.longitude() - y) * dy) / (dx * dx + dy * dy);
129+
if (t > 1) {
130+
x = segmentEnd.latitude();
131+
y = segmentEnd.longitude();
132+
} else if (t > 0) {
133+
x += dx * t;
134+
y += dy * t;
135+
}
136+
}
137+
138+
dx = point.latitude() - x;
139+
dy = point.longitude() - y;
140+
141+
return dx * dx + dy * dy;
142+
}
143+
}

services-turf/src/test/java/com/mapbox/turf/TestUtils.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@ public void compareJson(String expectedJson, String actualJson) {
3333
}
3434

3535
protected String loadJsonFixture(String filename) {
36+
return readFile("src/test/resources/" + filename);
37+
}
38+
39+
public String readFile(String filepath) {
3640
try {
37-
String filepath = "src/test/resources/" + filename;
3841
byte[] encoded = Files.readAllBytes(Paths.get(filepath));
3942
return new String(encoded, UTF_8);
4043
} catch (IOException e) {
41-
fail("Unable to load " + filename);
44+
fail("Unable to load " + filepath);
4245
return "";
4346
}
4447
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.mapbox.turf;
2+
3+
import com.mapbox.geojson.Point;
4+
import org.junit.Test;
5+
6+
import java.util.ArrayList;
7+
import java.util.Arrays;
8+
import java.util.Collections;
9+
import java.util.List;
10+
11+
import static org.junit.Assert.assertEquals;
12+
13+
public class TurfSimplifySpecialCasesTest {
14+
15+
@Test
16+
public void emptyCoordinates() {
17+
List<Point> input = new ArrayList<>();
18+
List<Point> output = TurfSimplify.simplify(input, 0.01, true);
19+
assertEquals(0, output.size());
20+
}
21+
22+
@Test
23+
public void oneCoordinate() {
24+
List<Point> input = Collections.singletonList(Point.fromLngLat(1.0, 2.0));
25+
List<Point> output = TurfSimplify.simplify(input, 0.01, false);
26+
assertEquals(input, output);
27+
}
28+
29+
@Test
30+
public void twoCoordinates() {
31+
List<Point> input = Arrays.asList(
32+
Point.fromLngLat(1.0, 2.0),
33+
Point.fromLngLat(-56.12, 87.09)
34+
);
35+
List<Point> output = TurfSimplify.simplify(input, 0.01, true);
36+
assertEquals(input, output);
37+
}
38+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.mapbox.turf;
2+
3+
import com.google.gson.JsonArray;
4+
import com.google.gson.JsonObject;
5+
import com.google.gson.JsonParser;
6+
import com.mapbox.geojson.Point;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
import org.junit.runners.Parameterized;
10+
11+
import java.util.ArrayList;
12+
import java.util.Collection;
13+
import java.util.List;
14+
import java.util.stream.Collectors;
15+
16+
import static org.junit.Assert.assertEquals;
17+
18+
@RunWith(Parameterized.class)
19+
public class TurfSimplifyTest {
20+
21+
@Parameterized.Parameters(name = "{0}")
22+
public static Collection<Object[]> data() {
23+
return new TestUtils().getResourceFolderFileNames("turf-simplify/input")
24+
.stream()
25+
.map(s -> new Object[] { s })
26+
.collect(Collectors.toList());
27+
}
28+
29+
private final String fileName;
30+
private final TestUtils testUtils = new TestUtils();
31+
32+
public TurfSimplifyTest(String fileName) {
33+
this.fileName = fileName;
34+
}
35+
36+
@Test
37+
public void simplify() {
38+
String inputContent = testUtils.readFile("src/test/resources/turf-simplify/input/" + fileName);
39+
JsonObject json = JsonParser.parseString(inputContent).getAsJsonObject();
40+
JsonObject properties = json.getAsJsonObject("properties");
41+
double tolerance = properties.get("tolerance").getAsDouble();
42+
boolean highQuality = properties.get("highQuality").getAsBoolean();
43+
List<Point> input = parseCoordinates(json);
44+
String expectedContent = testUtils.readFile("src/test/resources/turf-simplify/output/" + fileName);
45+
List<Point> expected = parseCoordinates(JsonParser.parseString(expectedContent).getAsJsonObject());
46+
47+
List<Point> output = TurfSimplify.simplify(input, tolerance, highQuality);
48+
49+
checkPointsEquality(output, expected, 0.00001);
50+
}
51+
52+
private List<Point> parseCoordinates(JsonObject json) {
53+
List<Point> points = new ArrayList<>();
54+
JsonArray jsonArray = json.getAsJsonArray("coordinates");
55+
for (int i = 0; i < jsonArray.size(); i++) {
56+
JsonArray coord = jsonArray.get(i).getAsJsonArray();
57+
double longitude = coord.get(0).getAsDouble();
58+
double latitude = coord.get(1).getAsDouble();
59+
points.add(Point.fromLngLat(longitude, latitude));
60+
}
61+
return points;
62+
}
63+
64+
private void checkPointsEquality(List<Point> actual, List<Point> expected, double tolerance) {
65+
assertEquals(expected.size(), actual.size());
66+
for (int i = 0; i < expected.size(); i++) {
67+
assertEquals(expected.get(i).latitude(), actual.get(i).latitude(), tolerance);
68+
assertEquals(expected.get(i).longitude(), actual.get(i).longitude(), tolerance);
69+
}
70+
}
71+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"properties": {
3+
"tolerance": 0.01,
4+
"highQuality": false
5+
},
6+
"coordinates": [
7+
[27.977542877197266, -26.17500493262446],
8+
[27.975482940673828, -26.17870225771557],
9+
[27.969818115234375, -26.177931991326645],
10+
[27.967071533203125, -26.177623883345735],
11+
[27.966899871826172, -26.1810130263384],
12+
[27.967758178710938, -26.1853263385099],
13+
[27.97290802001953, -26.1853263385099],
14+
[27.97496795654297, -26.18270756087535],
15+
[27.97840118408203, -26.1810130263384],
16+
[27.98011779785156, -26.183323749143113],
17+
[27.98011779785156, -26.18655868408986],
18+
[27.978744506835938, -26.18933141398614],
19+
[27.97496795654297, -26.19025564262006],
20+
[27.97119140625, -26.19040968001282],
21+
[27.969303131103516, -26.1899475672235],
22+
[27.96741485595703, -26.189639491012183],
23+
[27.9656982421875, -26.187945057286793],
24+
[27.965354919433594, -26.18563442612686],
25+
[27.96432495117187, -26.183015655416536]
26+
]
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"properties": {
3+
"tolerance": 0.0001,
4+
"highQuality": false
5+
},
6+
"coordinates": [
7+
[27.977542877197266, -26.17500493262446],
8+
[27.975482940673828, -26.17870225771557],
9+
[27.969818115234375, -26.177931991326645],
10+
[27.967071533203125, -26.177623883345735],
11+
[27.966899871826172, -26.1810130263384],
12+
[27.967758178710938, -26.1853263385099],
13+
[27.97290802001953, -26.1853263385099],
14+
[27.97496795654297, -26.18270756087535],
15+
[27.97840118408203, -26.1810130263384],
16+
[27.98011779785156, -26.183323749143113],
17+
[27.98011779785156, -26.18655868408986],
18+
[27.978744506835938, -26.18933141398614],
19+
[27.97496795654297, -26.19025564262006],
20+
[27.97119140625, -26.19040968001282],
21+
[27.969303131103516, -26.1899475672235],
22+
[27.96741485595703, -26.189639491012183],
23+
[27.9656982421875, -26.187945057286793],
24+
[27.965354919433594, -26.18563442612686],
25+
[27.96432495117187, -26.183015655416536]
26+
]
27+
}

0 commit comments

Comments
 (0)