Skip to content

Commit

Permalink
Add memory efficient polyline decoder
Browse files Browse the repository at this point in the history
  • Loading branch information
kmadsen committed Nov 23, 2022
1 parent 6b63f16 commit 4576fb4
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.mapbox.geojson.utils;

import com.mapbox.geojson.Point;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;

/**
* Decodes an encoded path string as an iterator of {@link Point}.
* This is a memory efficient version of {@link PolylineUtils#decode}.
*
* @see <a href="https://github.com/mapbox/polyline/blob/master/src/polyline.js">Part of algorithm came from this source</a>
* @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>
* @since 6.10.0
*/
public class PolylineDecoder implements Iterator<Point>, Closeable {

private final InputStream inputStream;

// OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
private final double factor;

// For speed we preallocate to an upper bound on the final length, then
// truncate the array before returning.
private int lat = 0;
private int lng = 0;
private int data = -1;

/**
* Decodes an encoded input stream into a sequence of {@link Point}.
*
* @param inputStream InputStream that reads a String as bytes
* @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5
*/
public PolylineDecoder(InputStream inputStream, int precision) {
this.inputStream = inputStream;
this.factor = Math.pow(10, precision);
loadNext();
}

@Override
public boolean hasNext() {
return data != -1;
}

@Override
public Point next() {
int result = 1;
int shift = 0;
int temp;
do {
temp = data - 63 - 1;
loadNext();
result += temp << shift;
shift += 5;
}
while (temp >= 0x1f);
lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

result = 1;
shift = 0;
do {
temp = data - 63 - 1;
loadNext();
result += temp << shift;
shift += 5;
}
while (temp >= 0x1f);
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

return Point.fromLngLat(lng / factor, lat / factor);
}

@Override
public void close() {
try {
inputStream.close();
} catch (IOException exception) {
// Safe close
}
}

private void loadNext() throws RuntimeException {
try {
this.data = inputStream.read();
} catch (IOException exception) {
this.data = -1;
throw new RuntimeException("Failed to read the encoded path", exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import androidx.annotation.NonNull;
import com.mapbox.geojson.Point;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

Expand Down Expand Up @@ -37,43 +39,13 @@ private PolylineUtils() {
*/
@NonNull
public static List<Point> decode(@NonNull final String encodedPath, int precision) {
int len = encodedPath.length();

// OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
double factor = Math.pow(10, precision);

// For speed we preallocate to an upper bound on the final length, then
// truncate the array before returning.
final List<Point> path = new ArrayList<>();
int index = 0;
int lat = 0;
int lng = 0;

while (index < len) {
int result = 1;
int shift = 0;
int temp;
do {
temp = encodedPath.charAt(index++) - 63 - 1;
result += temp << shift;
shift += 5;
}
while (temp >= 0x1f);
lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

result = 1;
shift = 0;
do {
temp = encodedPath.charAt(index++) - 63 - 1;
result += temp << shift;
shift += 5;
InputStream inputStream = new ByteArrayInputStream(encodedPath.getBytes());
try (PolylineDecoder polylineDecoder = new PolylineDecoder(inputStream, precision)) {
while (polylineDecoder.hasNext()) {
path.add(polylineDecoder.next());
}
while (temp >= 0x1f);
lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

path.add(Point.fromLngLat(lng / factor, lat / factor));
}

return path;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ public void testDecodePath() {
expectNearNumber(-122.41488, lastPoint.longitude(), 1e-6);
}

@Test
public void testDecodeLargeFile() throws IOException {
final String testLine = loadJsonFixture("polyline-sfo-nyc.txt");
List<Point> latLngs = decode(testLine, PRECISION_6);
assertEquals(34102, latLngs.size());
}

@Test
public void testDecodeCoordinates() {
List<Point> latLngs = decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@", PRECISION_5);
assertEquals(3, latLngs.size());
assertEquals(38.5, latLngs.get(0).latitude(), 0.0);
assertEquals(-120.2, latLngs.get(0).longitude(), 0.0);
assertEquals(40.7, latLngs.get(1).latitude(), 0.0);
assertEquals(-120.95, latLngs.get(1).longitude(), 0.0);
assertEquals(43.252, latLngs.get(2).latitude(), 0.0);
assertEquals(-126.453, latLngs.get(2).longitude(), 0.0);
}

@Test
public void testEncodePath5() {
List<Point> path = decode(TEST_LINE, PRECISION_5);
Expand Down Expand Up @@ -90,7 +109,6 @@ public void testFromPolylineAndDecode() {
}
}


@Test
public void testEncodeDecodePath6() {
List<Point> originalPath = Arrays.asList(
Expand Down
1 change: 1 addition & 0 deletions services-geojson/src/test/resources/polyline-sfo-nyc.txt

Large diffs are not rendered by default.

0 comments on commit 4576fb4

Please sign in to comment.