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

StringCompressedPolyLine now supports WKB for incompressible polylines #200

Merged
merged 8 commits into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added Wkb fallback to StringCompressedPolyLine for when compression a…
…lgo fails
  • Loading branch information
lucaspcram committed Aug 20, 2018
commit 4f9faa3d88a700b42554030e89ec8599f65b1189
27 changes: 27 additions & 0 deletions src/main/java/org/openstreetmap/atlas/geography/PolyLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.openstreetmap.atlas.geography.Snapper.SnappedLocation;
import org.openstreetmap.atlas.geography.clipping.Clip;
import org.openstreetmap.atlas.geography.clipping.Clip.ClipType;
import org.openstreetmap.atlas.geography.converters.WkbLocationConverter;
import org.openstreetmap.atlas.geography.converters.WkbPolyLineConverter;
import org.openstreetmap.atlas.geography.converters.WktLocationConverter;
import org.openstreetmap.atlas.geography.converters.WktPolyLineConverter;
import org.openstreetmap.atlas.geography.geojson.GeoJsonBuilder;
Expand Down Expand Up @@ -87,6 +89,18 @@ public static void saveAsGeoJson(final Iterable<? extends Iterable<Location>> ge
}
}

/**
* Create a {@link PolyLine} from Well Known Binary
*
* @param wkb
* The Well Known Binary
* @return The {@link PolyLine}
*/
public static PolyLine wkb(final byte[] wkb)
{
return new WkbPolyLineConverter().backwardConvert(wkb);
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like this would not handle the single location polylines outlined below?

Copy link
Contributor Author

@lucaspcram lucaspcram Aug 21, 2018

Choose a reason for hiding this comment

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

Does it not already? This method is just a carbon copy of the already existing wkt method. The WkbPolyLineConverter.backwardConvert method used by wkb will just end up creating a singleton list to initialize the polyline.

If you take a look at the toWkb method I added (line 1126), you'll notice it does handle the case of a polyline with a single point (following the example set by toWkt). In fact I was actually wondering why this is necessary. Both of those functions call WkXPolyLineConverter.convert, and it looks like that method also handles the special case of a single point PolyLine. So is this check redundant, or am I missing something?

}

/**
* Create a {@link PolyLine} from Well Known Text
*
Expand Down Expand Up @@ -1106,6 +1120,19 @@ public String toString()
return toWkt();
}

/**
* @return This {@link PolyLine} as Well Known Binary
*/
public byte[] toWkb()
{
if (this.size() == 1)
{
// Handle a single location polyLine
return new WkbLocationConverter().convert(this.first());
}
return new WkbPolyLineConverter().convert(this);
}

/**
* @return This {@link PolyLine} as Well Known Text
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@
import java.util.List;

import org.openstreetmap.atlas.exception.CoreException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Derived from MapQuest's developer portal:
* https://developer.mapquest.com/documentation/common/encode-decode/
* Encode a {@link PolyLine} using an algorithm derived from the MapQuest variant of the Google
* Polyline Encoding Format. The encoding scheme falls back on WKB in edge cases when the algorithm
* fails (such as when two consecutive points in the polyline have a longitude difference of greater
* than 180 degrees).
*
* @see <a href="https://developer.mapquest.com/documentation/common/encode-decode">The MapQuest
* algorithm</a>
* @see <a href=
* "https://developers.google.com/maps/documentation/utilities/polylinealgorithm?csw=1">Google
* Polyline Encoding Format</a>
* @author matthieun
*/
public class StringCompressedPolyLine implements Serializable
Expand Down Expand Up @@ -42,7 +51,13 @@ public PolyLineCompressionException(final String message, final Object... items)
private static final long MAXIMUM_DELTA_LONGITUDE = (long) (MAXIMUM_DELTA_LONGITUDE_IN_DEGREES
* Math.pow(10, PRECISION));

private final byte[] encoding;
// if the first byte of the encoding array is this sentinel value, then the following encoding
// is WKB and not string compressed
private static final byte WKB_SENTINEL = 0;

private static final Logger logger = LoggerFactory.getLogger(StringCompressedPolyLine.class);

private byte[] encoding;

public StringCompressedPolyLine(final byte[] encoding)
{
Expand All @@ -55,25 +70,34 @@ public StringCompressedPolyLine(final PolyLine polyLine)
{
this.encoding = compress(polyLine, PRECISION).getBytes(ENCODING_NAME);
}
catch (final PolyLineCompressionException e)
catch (final PolyLineCompressionException exception)
{
throw e;
this.encoding = getWkbFallback(polyLine);
}
catch (final Exception e)
catch (final Exception exception)
{
throw new CoreException("Could not compress polyline.", e);
throw new CoreException("Could not compress polyline.", exception);
}
}

public PolyLine asPolyLine()
{
try
if (this.encoding[0] == WKB_SENTINEL)
{
return asPolyLine(new String(this.encoding, ENCODING_NAME), PRECISION);
final byte[] strippedEncoding = new byte[this.encoding.length - 1];
System.arraycopy(this.encoding, 1, strippedEncoding, 0, strippedEncoding.length);
return PolyLine.wkb(strippedEncoding);
}
catch (final Exception e)
else
{
throw new CoreException("Could not decompress polyline.", e);
try
{
return asPolyLine(new String(this.encoding, ENCODING_NAME), PRECISION);
}
catch (final Exception exception)
{
throw new CoreException("Could not decompress polyline.", exception);
}
}
}

Expand All @@ -85,13 +109,22 @@ public byte[] getEncoding()
@Override
public String toString()
{
try
if (this.encoding[0] == WKB_SENTINEL)
{
return new String(this.encoding, ENCODING_NAME);
final byte[] strippedEncoding = new byte[this.encoding.length - 1];
System.arraycopy(this.encoding, 1, strippedEncoding, 0, strippedEncoding.length);
return PolyLine.wkb(strippedEncoding).toWkt();
}
catch (final Exception e)
else
{
throw new CoreException("Could not stringify byte array.", e);
try
{
return new String(this.encoding, ENCODING_NAME);
}
catch (final Exception e)
{
throw new CoreException("Could not stringify byte array.", e);
}
}
}

Expand Down Expand Up @@ -152,6 +185,7 @@ private String compress(final PolyLine points, final int precision0)
final long deltaLongitude = longitude - oldLongitude;
if (Math.abs(deltaLongitude) > MAXIMUM_DELTA_LONGITUDE)
{
logger.warn("Unable to use string compression for {}", points.toWkt());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't that logging statement redundant if the below exception is thrown?

Copy link
Contributor Author

@lucaspcram lucaspcram Aug 21, 2018

Choose a reason for hiding this comment

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

Normally it would be but this specific exception is always caught on line 77 of StringCompressedPolyLine (which triggers the fallback), so I thought it would be worth logging. I believe catching the exception explicitly hides it from log output (since it is a checked exception and we are not dumping the stack trace in the catch block).

Please correct me if I am wrong, and I can remove the log statement.

throw new PolyLineCompressionException(
"Unable to compress the polyLine, two consecutive points ({} and {}) are too far apart in longitude: {} degrees.",
last, location, deltaLongitude / precision);
Expand Down Expand Up @@ -182,4 +216,13 @@ private String encodeNumber(final long number0)
encoded += String.valueOf(Character.toChars((int) number + ENCODING_OFFSET_MINUS_ONE));
return encoded;
}

private byte[] getWkbFallback(final PolyLine polyLine)
{
final byte[] wkbEncoding = polyLine.toWkb();
final byte[] finalEncoding = new byte[1 + wkbEncoding.length];
finalEncoding[0] = WKB_SENTINEL;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Such a waste of bytes!! ;p

System.arraycopy(wkbEncoding, 0, finalEncoding, 1, wkbEncoding.length);
return finalEncoding;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import org.junit.Assert;
import org.junit.Test;
import org.openstreetmap.atlas.geography.StringCompressedPolyLine.PolyLineCompressionException;
import org.openstreetmap.atlas.utilities.scalars.Distance;

/**
Expand All @@ -24,39 +23,26 @@ public void testCompressionDecompression()
Assert.assertEquals(polyLine, decompressed);
}

@Test(expected = PolyLineCompressionException.class)
public void testCompressionError1()
{
final Location location1 = new Location(Latitude.degrees(45.0), Longitude.degrees(-179.0));
final Location location2 = new Location(Latitude.degrees(45.0), Longitude.degrees(179.0));
final PolyLine line = new PolyLine(location1, location2);
final StringCompressedPolyLine compressedLine = new StringCompressedPolyLine(line);
}

@Test(expected = PolyLineCompressionException.class)
public void testCompressionError2()
{
final Location location1 = new Location(Latitude.degrees(45.0), Longitude.degrees(179.0));
final Location location2 = new Location(Latitude.degrees(45.0), Longitude.degrees(-179.0));
final PolyLine line = new PolyLine(location1, location2);
final StringCompressedPolyLine compressedLine = new StringCompressedPolyLine(line);
}

/**
* Here the delta longitude between loc2 and loc3 is more than 180 degrees, hence the
* compression exception.
* Here the delta longitude between loc1/loc2 and loc3/loc4 is more than 180 degrees, so the
* algorithm falls back on WKB.
*/
@Test(expected = PolyLineCompressionException.class)
public void testJumpyPolyLine()
@Test
public void testCompressionWkbFallback()
{
final Location loc1 = Location.forString("5.972761,-75.8373644");
final Location loc2 = Location.forString("5.9712693,-75.8363875");
final Location loc3 = Location.forString("18.6398595,157.7635783");
final Location loc4 = Location.forString("15.3405183,13.9936102");
final PolyLine polyLine = new PolyLine(loc1, loc2, loc3, loc4);
final Location location1 = new Location(Latitude.degrees(45.0), Longitude.degrees(-179.0));
final Location location2 = new Location(Latitude.degrees(45.0), Longitude.degrees(179.0));
final Location location3 = new Location(Latitude.degrees(45.0), Longitude.degrees(179.0));
final Location location4 = new Location(Latitude.degrees(45.0), Longitude.degrees(-179.0));
final PolyLine line1 = new PolyLine(location1, location2);
final PolyLine line2 = new PolyLine(location3, location4);
final StringCompressedPolyLine compressedLine1 = new StringCompressedPolyLine(line1);
final StringCompressedPolyLine compressedLine2 = new StringCompressedPolyLine(line2);

System.out.println(polyLine);
new StringCompressedPolyLine(polyLine);
// the toString method should return WKT since the compression is WKB instead of MapQuest
// string compression
Assert.assertEquals(compressedLine1.toString(), compressedLine1.asPolyLine().toWkt());
Assert.assertEquals(compressedLine2.toString(), compressedLine2.asPolyLine().toWkt());
}

@Test
Expand Down