Skip to content

Commit 9b67c33

Browse files
committed
Add memory efficient polyline decoder
1 parent 6b63f16 commit 9b67c33

File tree

4 files changed

+120
-36
lines changed

4 files changed

+120
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.mapbox.geojson.utils;
2+
3+
import com.mapbox.geojson.Point;
4+
5+
import java.io.Closeable;
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.util.Iterator;
9+
10+
/**
11+
* Decodes an encoded path string as an iterator of {@link Point}. This is a memory efficient version of
12+
* {@link PolylineUtils#decode}.
13+
*
14+
* @see <a href="https://github.com/mapbox/polyline/blob/master/src/polyline.js">Part of algorithm came from this source</a>
15+
* @see <a href="https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/PolyUtil.java">Part of algorithm came from this source.</a>
16+
* @since 6.10.0
17+
*/
18+
public class PolylineDecoder implements Iterator<Point>, Closeable {
19+
20+
private final InputStream inputStream;
21+
22+
// OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
23+
private final double factor;
24+
25+
// For speed we preallocate to an upper bound on the final length, then
26+
// truncate the array before returning.
27+
private int lat = 0;
28+
private int lng = 0;
29+
private int data = -1;
30+
31+
/**
32+
* Decodes an encoded input stream into a sequence of {@link Point}.
33+
*
34+
* @param inputStream InputStream that reads a String as bytes
35+
* @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5
36+
*/
37+
public PolylineDecoder(InputStream inputStream, int precision) {
38+
this.inputStream = inputStream;
39+
this.factor = Math.pow(10, precision);
40+
loadNext();
41+
}
42+
43+
@Override
44+
public boolean hasNext() {
45+
return data != -1;
46+
}
47+
48+
@Override
49+
public Point next() {
50+
int result = 1;
51+
int shift = 0;
52+
int temp;
53+
do {
54+
temp = data - 63 - 1;
55+
loadNext();
56+
result += temp << shift;
57+
shift += 5;
58+
}
59+
while (temp >= 0x1f);
60+
lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
61+
62+
result = 1;
63+
shift = 0;
64+
do {
65+
temp = data - 63 - 1;
66+
loadNext();
67+
result += temp << shift;
68+
shift += 5;
69+
}
70+
while (temp >= 0x1f);
71+
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
72+
73+
return Point.fromLngLat(lng / factor, lat / factor);
74+
}
75+
76+
@Override
77+
public void close() {
78+
try {
79+
inputStream.close();
80+
} catch (IOException exception) {
81+
// Safe close
82+
}
83+
}
84+
85+
private void loadNext() throws RuntimeException {
86+
try {
87+
this.data = inputStream.read();
88+
} catch (IOException exception) {
89+
this.data = -1;
90+
throw new RuntimeException("Failed to read the encoded path");
91+
}
92+
}
93+
}

services-geojson/src/main/java/com/mapbox/geojson/utils/PolylineUtils.java

+7-35
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import androidx.annotation.NonNull;
44
import com.mapbox.geojson.Point;
55

6+
import java.io.ByteArrayInputStream;
7+
import java.io.InputStream;
68
import java.util.ArrayList;
79
import java.util.List;
810

@@ -37,43 +39,13 @@ private PolylineUtils() {
3739
*/
3840
@NonNull
3941
public static List<Point> decode(@NonNull final String encodedPath, int precision) {
40-
int len = encodedPath.length();
41-
42-
// OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
43-
double factor = Math.pow(10, precision);
44-
45-
// For speed we preallocate to an upper bound on the final length, then
46-
// truncate the array before returning.
4742
final List<Point> path = new ArrayList<>();
48-
int index = 0;
49-
int lat = 0;
50-
int lng = 0;
51-
52-
while (index < len) {
53-
int result = 1;
54-
int shift = 0;
55-
int temp;
56-
do {
57-
temp = encodedPath.charAt(index++) - 63 - 1;
58-
result += temp << shift;
59-
shift += 5;
60-
}
61-
while (temp >= 0x1f);
62-
lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
63-
64-
result = 1;
65-
shift = 0;
66-
do {
67-
temp = encodedPath.charAt(index++) - 63 - 1;
68-
result += temp << shift;
69-
shift += 5;
70-
}
71-
while (temp >= 0x1f);
72-
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);
73-
74-
path.add(Point.fromLngLat(lng / factor, lat / factor));
43+
InputStream inputStream = new ByteArrayInputStream(encodedPath.getBytes());
44+
PolylineDecoder polylineDecoder = new PolylineDecoder(inputStream, precision);
45+
while (polylineDecoder.hasNext()) {
46+
path.add(polylineDecoder.next());
7547
}
76-
48+
polylineDecoder.close();
7749
return path;
7850
}
7951

services-geojson/src/test/java/com/mapbox/geojson/utils/PolylineUtilsTest.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ public void testDecodePath() {
4747
expectNearNumber(-122.41488, lastPoint.longitude(), 1e-6);
4848
}
4949

50+
@Test
51+
public void testDecodeLargeFile() throws IOException {
52+
final String testLine = loadJsonFixture("polyline-sfo-nyc.txt");
53+
List<Point> latLngs = decode(testLine, PRECISION_6);
54+
assertEquals(34102, latLngs.size());
55+
}
56+
57+
@Test
58+
public void testDecodeCoordinates() {
59+
List<Point> latLngs = decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@", PRECISION_5);
60+
assertEquals(3, latLngs.size());
61+
assertEquals(38.5, latLngs.get(0).latitude(), 0.0);
62+
assertEquals(-120.2, latLngs.get(0).longitude(), 0.0);
63+
assertEquals(40.7, latLngs.get(1).latitude(), 0.0);
64+
assertEquals(-120.95, latLngs.get(1).longitude(), 0.0);
65+
assertEquals(43.252, latLngs.get(2).latitude(), 0.0);
66+
assertEquals(-126.453, latLngs.get(2).longitude(), 0.0);
67+
}
68+
5069
@Test
5170
public void testEncodePath5() {
5271
List<Point> path = decode(TEST_LINE, PRECISION_5);
@@ -90,7 +109,6 @@ public void testFromPolylineAndDecode() {
90109
}
91110
}
92111

93-
94112
@Test
95113
public void testEncodeDecodePath6() {
96114
List<Point> originalPath = Arrays.asList(

services-geojson/src/test/resources/polyline-sfo-nyc.txt

+1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)