Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add memory efficient polyline decoder #1518

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.mapbox.geojson.utils;

import androidx.annotation.Nullable;
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;
private Point current;

/**
* 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public PolylineDecoder(InputStream inputStream, int precision) {
public PolylineDecoder(@NonNull InputStream inputStream, int precision) {

this.inputStream = inputStream;
this.factor = Math.pow(10, precision);
loadNext();
}

/**
* Returns the current [Point] for the iterator. Every call to [next] will update the [current].
*/
@Nullable
public Point getCurrent() {
return current;
}

/**
* Returns true if the geometry has more points.
*/
@Override
public boolean hasNext() {
return data != -1;
}

/**
* Returns the next point in the geometry.
*/
@Override
public Point next() {
kmadsen marked this conversation as resolved.
Show resolved Hide resolved
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);

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

@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
@@ -0,0 +1,65 @@
package com.mapbox.geojson.utils;

import com.mapbox.geojson.Point;
import com.mapbox.geojson.TestUtils;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

public class PolylineDecoderTest extends TestUtils {

@Test
public void testHasNext() {
String geometry = "_p~iF~ps|U_ulLnnqC_mqNvxq`@";

InputStream inputStream = new ByteArrayInputStream(geometry.getBytes());
PolylineDecoder polylineDecoder = new PolylineDecoder(inputStream, 5);

assertTrue(polylineDecoder.hasNext());
polylineDecoder.next();
assertTrue(polylineDecoder.hasNext());
polylineDecoder.next();
assertTrue(polylineDecoder.hasNext());
polylineDecoder.next();
assertFalse(polylineDecoder.hasNext());

polylineDecoder.close();
}

@Test
public void testNextValues() {
String geometry = "_p~iF~ps|U_ulLnnqC_mqNvxq`@";

InputStream inputStream = new ByteArrayInputStream(geometry.getBytes());
PolylineDecoder polylineDecoder = new PolylineDecoder(inputStream, 5);

Point point = polylineDecoder.next();
assertEquals(38.5, point.latitude(), 0.0);
assertEquals(-120.2, point.longitude(), 0.0);
point = polylineDecoder.next();
assertEquals(40.7, point.latitude(), 0.0);
assertEquals(-120.95, point.longitude(), 0.0);
point = polylineDecoder.next();
assertEquals(43.252, point.latitude(), 0.0);
assertEquals(-126.453, point.longitude(), 0.0);
}

@Test
public void testCurrentValue() {
String geometry = "_p~iF~ps|U_ulLnnqC_mqNvxq`@";

InputStream inputStream = new ByteArrayInputStream(geometry.getBytes());
PolylineDecoder polylineDecoder = new PolylineDecoder(inputStream, 5);

assertNull(polylineDecoder.getCurrent());
assertEquals(polylineDecoder.next(), polylineDecoder.getCurrent());
assertEquals(polylineDecoder.next(), polylineDecoder.getCurrent());
assertEquals(polylineDecoder.next(), polylineDecoder.getCurrent());
}
}
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.