From 2800c339f865c82a8d7f6a9fb908d20e8bfc0ebd Mon Sep 17 00:00:00 2001 From: Mike Barry Date: Sun, 23 May 2021 15:06:26 -0400 Subject: [PATCH] better invalid geometry handling --- .../onthegomap/flatmap/VectorTileEncoder.java | 264 +++++++++--------- .../render/CoordinateSequenceExtractor.java | 27 +- .../flatmap/render/FeatureRenderer.java | 6 +- .../flatmap/render/TiledGeometry.java | 46 +-- .../com/onthegomap/flatmap/TestUtils.java | 13 +- .../flatmap/VectorTileEncoderTest.java | 10 +- .../flatmap/collections/FeatureGroupTest.java | 5 +- .../flatmap/render/FeatureRendererTest.java | 20 +- 8 files changed, 219 insertions(+), 172 deletions(-) diff --git a/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java b/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java index 1221381e..b8b83c33 100644 --- a/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java +++ b/src/main/java/com/onthegomap/flatmap/VectorTileEncoder.java @@ -23,6 +23,8 @@ import com.carrotsearch.hppc.IntArrayList; import com.google.common.primitives.Ints; import com.google.protobuf.InvalidProtocolBufferException; import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; +import com.onthegomap.flatmap.geo.TileCoord; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -98,137 +100,145 @@ public class VectorTileEncoder { return ((n >> 1) ^ (-(n & 1))); } - private static Geometry decodeCommands(byte geomTypeByte, int[] commands) { - VectorTile.Tile.GeomType geomType = Objects.requireNonNull(VectorTile.Tile.GeomType.forNumber(geomTypeByte)); - GeometryFactory gf = GeoUtils.JTS_FACTORY; - int x = 0; - int y = 0; + private static Geometry decodeCommands(byte geomTypeByte, int[] commands) throws GeometryException { + try { + VectorTile.Tile.GeomType geomType = Objects.requireNonNull(VectorTile.Tile.GeomType.forNumber(geomTypeByte)); + GeometryFactory gf = GeoUtils.JTS_FACTORY; + int x = 0; + int y = 0; - List coordsList = new ArrayList<>(); - DoubleArrayList coords = null; + List coordsList = new ArrayList<>(); + DoubleArrayList coords = null; - int geometryCount = commands.length; - int length = 0; - int command = 0; - int i = 0; - while (i < geometryCount) { + int geometryCount = commands.length; + int length = 0; + int command = 0; + int i = 0; + while (i < geometryCount) { - if (length <= 0) { - length = commands[i++]; - command = length & ((1 << 3) - 1); - length = length >> 3; - } - - if (length > 0) { - - if (command == Command.MOVE_TO.value) { - coords = new DoubleArrayList(); - coordsList.add(coords); - } else { - Objects.requireNonNull(coords); + if (length <= 0) { + length = commands[i++]; + command = length & ((1 << 3) - 1); + length = length >> 3; } - if (command == Command.CLOSE_PATH.value) { - if (geomType != VectorTile.Tile.GeomType.POINT && !coords.isEmpty()) { - coords.add(coords.get(0), coords.get(1)); + if (length > 0) { + + if (command == Command.MOVE_TO.value) { + coords = new DoubleArrayList(); + coordsList.add(coords); + } else { + Objects.requireNonNull(coords); } + + if (command == Command.CLOSE_PATH.value) { + if (geomType != VectorTile.Tile.GeomType.POINT && !coords.isEmpty()) { + coords.add(coords.get(0), coords.get(1)); + } + length--; + continue; + } + + int dx = commands[i++]; + int dy = commands[i++]; + length--; - continue; + + dx = zigZagDecode(dx); + dy = zigZagDecode(dy); + + x = x + dx; + y = y + dy; + + coords.add(x / SCALE, y / SCALE); } - int dx = commands[i++]; - int dy = commands[i++]; - - length--; - - dx = zigZagDecode(dx); - dy = zigZagDecode(dy); - - x = x + dx; - y = y + dy; - - coords.add(x / SCALE, y / SCALE); } + Geometry geometry = null; + boolean outerCCW = false; + + switch (geomType) { + case LINESTRING: + List lineStrings = new ArrayList<>(coordsList.size()); + for (DoubleArrayList cs : coordsList) { + if (cs.size() <= 2) { + continue; + } + lineStrings.add(gf.createLineString(toCs(cs))); + } + if (lineStrings.size() == 1) { + geometry = lineStrings.get(0); + } else if (lineStrings.size() > 1) { + geometry = gf.createMultiLineString(lineStrings.toArray(new LineString[0])); + } + break; + case POINT: + CoordinateSequence cs = new PackedCoordinateSequence.Double(coordsList.size(), 2, 0); + for (int j = 0; j < coordsList.size(); j++) { + cs.setOrdinate(j, 0, coordsList.get(j).get(0)); + cs.setOrdinate(j, 1, coordsList.get(j).get(1)); + } + if (cs.size() == 1) { + geometry = gf.createPoint(cs); + } else if (cs.size() > 1) { + geometry = gf.createMultiPoint(cs); + } + break; + case POLYGON: + List> polygonRings = new ArrayList<>(); + List ringsForCurrentPolygon = new ArrayList<>(); + boolean first = true; + for (DoubleArrayList clist : coordsList) { + // skip hole with too few coordinates + if (ringsForCurrentPolygon.size() > 0 && clist.size() < 4) { + continue; + } + LinearRing ring = gf.createLinearRing(toCs(clist)); + boolean ccw = Orientation.isCCW(ring.getCoordinates()); + if (first) { + first = false; + outerCCW = ccw; + assert outerCCW; + } + if (ccw == outerCCW) { + ringsForCurrentPolygon = new ArrayList<>(); + polygonRings.add(ringsForCurrentPolygon); + } + ringsForCurrentPolygon.add(ring); + } + List polygons = new ArrayList<>(); + for (List rings : polygonRings) { + LinearRing shell = rings.get(0); + LinearRing[] holes = rings.subList(1, rings.size()).toArray(new LinearRing[rings.size() - 1]); + polygons.add(gf.createPolygon(shell, holes)); + } + if (polygons.size() == 1) { + geometry = polygons.get(0); + } + if (polygons.size() > 1) { + geometry = gf.createMultiPolygon(GeometryFactory.toPolygonArray(polygons)); + } + break; + default: + break; + } + + if (geometry == null) { + geometry = gf.createGeometryCollection(new Geometry[0]); + } + + return geometry; + } catch (IllegalArgumentException e) { + throw new GeometryException("Unable to decode geometry", e); } - - Geometry geometry = null; - boolean outerCCW = false; - - switch (geomType) { - case LINESTRING: - List lineStrings = new ArrayList<>(coordsList.size()); - for (DoubleArrayList cs : coordsList) { - if (cs.size() <= 2) { - continue; - } - lineStrings.add(gf.createLineString(toCs(cs))); - } - if (lineStrings.size() == 1) { - geometry = lineStrings.get(0); - } else if (lineStrings.size() > 1) { - geometry = gf.createMultiLineString(lineStrings.toArray(new LineString[0])); - } - break; - case POINT: - CoordinateSequence cs = new PackedCoordinateSequence.Double(coordsList.size(), 2, 0); - for (int j = 0; j < coordsList.size(); j++) { - cs.setOrdinate(j, 0, coordsList.get(j).get(0)); - cs.setOrdinate(j, 1, coordsList.get(j).get(1)); - } - if (cs.size() == 1) { - geometry = gf.createPoint(cs); - } else if (cs.size() > 1) { - geometry = gf.createMultiPoint(cs); - } - break; - case POLYGON: - List> polygonRings = new ArrayList<>(); - List ringsForCurrentPolygon = new ArrayList<>(); - boolean first = true; - for (DoubleArrayList clist : coordsList) { - // skip hole with too few coordinates - if (ringsForCurrentPolygon.size() > 0 && clist.size() < 4) { - continue; - } - LinearRing ring = gf.createLinearRing(toCs(clist)); - boolean ccw = Orientation.isCCW(ring.getCoordinates()); - if (first) { - first = false; - outerCCW = ccw; - assert outerCCW; - } - if (ccw == outerCCW) { - ringsForCurrentPolygon = new ArrayList<>(); - polygonRings.add(ringsForCurrentPolygon); - } - ringsForCurrentPolygon.add(ring); - } - List polygons = new ArrayList<>(); - for (List rings : polygonRings) { - LinearRing shell = rings.get(0); - LinearRing[] holes = rings.subList(1, rings.size()).toArray(new LinearRing[rings.size() - 1]); - polygons.add(gf.createPolygon(shell, holes)); - } - if (polygons.size() == 1) { - geometry = polygons.get(0); - } - if (polygons.size() > 1) { - geometry = gf.createMultiPolygon(GeometryFactory.toPolygonArray(polygons)); - } - break; - default: - break; - } - - if (geometry == null) { - geometry = gf.createGeometryCollection(new Geometry[0]); - } - - return geometry; } public static List decode(byte[] encoded) { + return decode(TileCoord.ofXYZ(0, 0, 0), encoded); + } + + public static List decode(TileCoord tileID, byte[] encoded) { try { VectorTile.Tile tile = VectorTile.Tile.parseFrom(encoded); List features = new ArrayList<>(); @@ -267,13 +277,17 @@ public class VectorTileEncoder { Object value = values.get(feature.getTags(tagIdx++)); attrs.put(key, value); } - Geometry geometry = decodeCommands(feature.getType(), feature.getGeometryList()); - features.add(new Feature( - layerName, - feature.getId(), - encodeGeometry(geometry), - attrs - )); + try { + Geometry geometry = decodeCommands(feature.getType(), feature.getGeometryList()); + features.add(new Feature( + layerName, + feature.getId(), + encodeGeometry(geometry), + attrs + )); + } catch (GeometryException e) { + LOGGER.warn("Error decoding " + tileID + ": " + e); + } } } return features; @@ -282,7 +296,7 @@ public class VectorTileEncoder { } } - private static Geometry decodeCommands(GeomType type, List geometryList) { + private static Geometry decodeCommands(GeomType type, List geometryList) throws GeometryException { return decodeCommands((byte) type.getNumber(), geometryList.stream().mapToInt(i -> i).toArray()); } @@ -387,7 +401,7 @@ public class VectorTileEncoder { public static record VectorGeometry(int[] commands, byte geomType) { - public Geometry decode() { + public Geometry decode() throws GeometryException { return decodeCommands(geomType, commands); } diff --git a/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java b/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java index a7c4d1cc..eadac865 100644 --- a/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java +++ b/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java @@ -1,6 +1,7 @@ package com.onthegomap.flatmap.render; import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; @@ -73,14 +74,16 @@ class CoordinateSequenceExtractor { List lineStrings = new ArrayList<>(); for (List inner : geoms) { for (CoordinateSequence coordinateSequence : inner) { - lineStrings.add(GeoUtils.JTS_FACTORY.createLineString(coordinateSequence)); + if (coordinateSequence.size() > 1) { + lineStrings.add(GeoUtils.JTS_FACTORY.createLineString(coordinateSequence)); + } } } return lineStrings.size() == 1 ? lineStrings.get(0) : GeoUtils.createMultiLineString(lineStrings); } @NotNull - static Geometry reassemblePolygons(List> groups) { + static Geometry reassemblePolygons(List> groups) throws GeometryException { int numGeoms = groups.size(); if (numGeoms == 1) { return reassemblePolygon(groups.get(0)); @@ -93,15 +96,19 @@ class CoordinateSequenceExtractor { } } - private static Polygon reassemblePolygon(List group) { - LinearRing first = GeoUtils.JTS_FACTORY.createLinearRing(group.get(0)); - LinearRing[] rest = new LinearRing[group.size() - 1]; - for (int j = 1; j < group.size(); j++) { - CoordinateSequence seq = group.get(j); - CoordinateSequences.reverse(seq); - rest[j - 1] = GeoUtils.JTS_FACTORY.createLinearRing(seq); + private static Polygon reassemblePolygon(List group) throws GeometryException { + try { + LinearRing first = GeoUtils.JTS_FACTORY.createLinearRing(group.get(0)); + LinearRing[] rest = new LinearRing[group.size() - 1]; + for (int j = 1; j < group.size(); j++) { + CoordinateSequence seq = group.get(j); + CoordinateSequences.reverse(seq); + rest[j - 1] = GeoUtils.JTS_FACTORY.createLinearRing(seq); + } + return GeoUtils.JTS_FACTORY.createPolygon(first, rest); + } catch (IllegalArgumentException e) { + throw new GeometryException("Could not build polygon", e); } - return GeoUtils.JTS_FACTORY.createPolygon(first, rest); } static Geometry reassemblePoints(List> result) { diff --git a/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java b/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java index aff906bc..f6db8337 100644 --- a/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java +++ b/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java @@ -172,15 +172,13 @@ public class FeatureRenderer { if (feature.area()) { geom = CoordinateSequenceExtractor.reassemblePolygons(geoms); geom = GeoUtils.snapAndFixPolygon(geom, tilePrecision); + // JTS utilities "fix" the geometry to be clockwise outer/CCW inner + geom = geom.reverse(); } else { geom = CoordinateSequenceExtractor.reassembleLineStrings(geoms); } if (!geom.isEmpty()) { - // JTS utilities "fix" the geometry to be clockwise outer/CCW inner - if (feature.area()) { - geom = geom.reverse(); - } emitFeature(feature, id, attrs, tile, geom, null); } } catch (GeometryException e) { diff --git a/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java b/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java index 11f051d6..173f3857 100644 --- a/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java +++ b/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java @@ -231,20 +231,20 @@ class TiledGeometry { IntObjectMap xSlices = new GHIntObjectHashMap<>(); int end = segment.size() - 1; for (int i = 0; i < end; i++) { - double _ax = segment.getX(i); + double ax = segment.getX(i); double ay = segment.getY(i); - double _bx = segment.getX(i + 1); + double bx = segment.getX(i + 1); double by = segment.getY(i + 1); - double minX = Math.min(_ax, _bx); - double maxX = Math.max(_ax, _bx); + double minX = Math.min(ax, bx); + double maxX = Math.max(ax, bx); int startX = (int) Math.floor(minX - neighborBuffer); int endX = (int) Math.floor(maxX + neighborBuffer); for (int x = startX; x <= endX; x++) { - double ax = _ax - x; - double bx = _bx - x; + double axTile = ax - x; + double bxTile = bx - x; MutableCoordinateSequence slice = xSlices.get(x); if (slice == null) { xSlices.put(x, slice = new MutableCoordinateSequence()); @@ -257,27 +257,27 @@ class TiledGeometry { boolean exited = false; - if (ax < k1) { + if (axTile < k1) { // ---|--> | (line enters the clip region from the left) - if (bx > k1) { - intersectX(slice, ax, ay, bx, by, k1); + if (bxTile > k1) { + intersectX(slice, axTile, ay, bxTile, by, k1); } - } else if (ax > k2) { + } else if (axTile > k2) { // | <--|--- (line enters the clip region from the right) - if (bx < k2) { - intersectX(slice, ax, ay, bx, by, k2); + if (bxTile < k2) { + intersectX(slice, axTile, ay, bxTile, by, k2); } } else { - slice.addPoint(ax, ay); + slice.addPoint(axTile, ay); } - if (bx < k1 && ax >= k1) { + if (bxTile < k1 && axTile >= k1) { // <--|--- | or <--|-----|--- (line exits the clip region on the left) - intersectX(slice, ax, ay, bx, by, k1); + intersectX(slice, axTile, ay, bxTile, by, k1); exited = true; } - if (bx > k2 && ax <= k2) { + if (bxTile > k2 && axTile <= k2) { // | ---|--> or ---|-----|--> (line exits the clip region on the right) - intersectX(slice, ax, ay, bx, by, k2); + intersectX(slice, axTile, ay, bxTile, by, k2); exited = true; } @@ -287,16 +287,16 @@ class TiledGeometry { } } // add the last point - double _ax = segment.getX(segment.size() - 1); + double ax = segment.getX(segment.size() - 1); double ay = segment.getY(segment.size() - 1); - int startX = (int) Math.floor(_ax - neighborBuffer); - int endX = (int) Math.floor(_ax + neighborBuffer); + int startX = (int) Math.floor(ax - neighborBuffer); + int endX = (int) Math.floor(ax + neighborBuffer); for (int x = startX - 1; x <= endX + 1; x++) { - double ax = _ax - x; + double axTile = ax - x; MutableCoordinateSequence slice = xSlices.get(x); - if (slice != null && ax >= k1 && ax <= k2) { - slice.addPoint(ax, ay); + if (slice != null && axTile >= k1 && axTile <= k2) { + slice.addPoint(axTile, ay); } } diff --git a/src/test/java/com/onthegomap/flatmap/TestUtils.java b/src/test/java/com/onthegomap/flatmap/TestUtils.java index dc92275e..6af041f9 100644 --- a/src/test/java/com/onthegomap/flatmap/TestUtils.java +++ b/src/test/java/com/onthegomap/flatmap/TestUtils.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.fail; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.GeometryException; import com.onthegomap.flatmap.geo.TileCoord; import com.onthegomap.flatmap.write.Mbtiles; import java.io.ByteArrayInputStream; @@ -176,13 +177,21 @@ public class TestUtils { Map> tiles = new TreeMap<>(); for (var tile : getAllTiles(db)) { var bytes = gunzip(tile.bytes()); - var decoded = VectorTileEncoder.decode(bytes).stream() - .map(feature -> feature(feature.geometry().decode(), feature.attrs())).toList(); + var decoded = VectorTileEncoder.decode(tile.tile(), bytes).stream() + .map(feature -> feature(decodeSilently(feature.geometry()), feature.attrs())).toList(); tiles.put(tile.tile(), decoded); } return tiles; } + public static Geometry decodeSilently(VectorTileEncoder.VectorGeometry geom) { + try { + return geom.decode(); + } catch (GeometryException e) { + throw new RuntimeException(e); + } + } + public static Set getAllTiles(Mbtiles db) throws SQLException { Set result = new HashSet<>(); try (Statement statement = db.connection().createStatement()) { diff --git a/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java b/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java index 68b6ba6b..c35dc582 100644 --- a/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java +++ b/src/test/java/com/onthegomap/flatmap/VectorTileEncoderTest.java @@ -19,6 +19,7 @@ package com.onthegomap.flatmap; import static com.onthegomap.flatmap.TestUtils.TRANSFORM_TO_TILE; +import static com.onthegomap.flatmap.TestUtils.decodeSilently; import static com.onthegomap.flatmap.TestUtils.newGeometryCollection; import static com.onthegomap.flatmap.TestUtils.newMultiPoint; import static com.onthegomap.flatmap.TestUtils.newMultiPolygon; @@ -30,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.primitives.Ints; +import com.onthegomap.flatmap.geo.TileCoord; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -118,7 +120,7 @@ public class VectorTileEncoderTest { byte[] encoded = vtm.encode(); assertNotSame(0, encoded.length); - var decoded = VectorTileEncoder.decode(encoded); + var decoded = VectorTileEncoder.decode(TileCoord.ofXYZ(0, 0, 0), encoded); assertEquals(List .of(new VectorTileEncoder.Feature("DEPCNT", 1, VectorTileEncoder.encodeGeometry(newPoint(3, 6)), Map.of( "key1", "value1", @@ -209,7 +211,7 @@ public class VectorTileEncoderTest { var features = VectorTileEncoder.decode(encoded); assertEquals(1, features.size()); - MultiPolygon mp2 = (MultiPolygon) features.get(0).geometry().decode(); + MultiPolygon mp2 = (MultiPolygon) decodeSilently(features.get(0).geometry()); assertEquals(mp.getNumGeometries(), mp2.getNumGeometries()); } @@ -339,7 +341,7 @@ public class VectorTileEncoderTest { private void testRoundTrip(Geometry input, String layer, Map attrs, long id) { VectorTileEncoder.VectorGeometry encodedGeom = VectorTileEncoder.encodeGeometry(input); - Geometry output = encodedGeom.decode(); + Geometry output = decodeSilently(encodedGeom); assertTrue(input.equalsExact(output), "\n" + input + "\n!=\n" + output); byte[] encoded = new VectorTileEncoder().addLayerFeatures(layer, List.of( @@ -354,6 +356,6 @@ public class VectorTileEncoderTest { } private void assertSameGeometries(List expected, List actual) { - assertEquals(expected, actual.stream().map(d -> d.geometry().decode()).toList()); + assertEquals(expected, actual.stream().map(d -> decodeSilently(d.geometry())).toList()); } } diff --git a/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java b/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java index c238ba9f..8193efee 100644 --- a/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java +++ b/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java @@ -1,5 +1,6 @@ package com.onthegomap.flatmap.collections; +import static com.onthegomap.flatmap.TestUtils.decodeSilently; import static com.onthegomap.flatmap.TestUtils.newPoint; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -69,10 +70,10 @@ public class FeatureGroupTest { private Map>> getFeatures() { Map>> map = new TreeMap<>(); for (FeatureGroup.TileFeatures tile : features) { - for (var feature : VectorTileEncoder.decode(tile.getTile().encode())) { + for (var feature : VectorTileEncoder.decode(tile.coord(), tile.getTile().encode())) { map.computeIfAbsent(tile.coord().encoded(), (i) -> new TreeMap<>()) .computeIfAbsent(feature.layer(), l -> new ArrayList<>()) - .add(new Feature(feature.attrs(), feature.geometry().decode())); + .add(new Feature(feature.attrs(), decodeSilently(feature.geometry()))); } } return map; diff --git a/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java b/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java index ecaaf7df..de3b5ca3 100644 --- a/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java +++ b/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java @@ -2,6 +2,7 @@ package com.onthegomap.flatmap.render; import static com.onthegomap.flatmap.TestUtils.assertExactSameFeatures; import static com.onthegomap.flatmap.TestUtils.assertSameNormalizedFeatures; +import static com.onthegomap.flatmap.TestUtils.decodeSilently; import static com.onthegomap.flatmap.TestUtils.emptyGeometry; import static com.onthegomap.flatmap.TestUtils.newLineString; import static com.onthegomap.flatmap.TestUtils.newMultiLineString; @@ -60,7 +61,7 @@ public class FeatureRendererTest { private Map> renderGeometry(FeatureCollector.Feature feature) { Map> result = new TreeMap<>(); new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>()) - .add(rendered.vectorTileFeature().geometry().decode())).renderFeature(feature); + .add(decodeSilently(rendered.vectorTileFeature().geometry()))).renderFeature(feature); result.values().forEach(gs -> gs.forEach(TestUtils::validateGeometry)); return result; } @@ -70,7 +71,7 @@ public class FeatureRendererTest { new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>()) .add(rendered)).renderFeature(feature); result.values() - .forEach(gs -> gs.forEach(f -> TestUtils.validateGeometry(f.vectorTileFeature().geometry().decode()))); + .forEach(gs -> gs.forEach(f -> TestUtils.validateGeometry(decodeSilently(f.vectorTileFeature().geometry())))); return result; } @@ -456,6 +457,21 @@ public class FeatureRendererTest { ), renderGeometry(feature)); } + @Test + public void testLineStringCollapsesToPointWithRounding() { + var eps = Z14_WIDTH / 4096; + var pixel = Z14_WIDTH / 256; + var feature = lineFeature(newLineString( + 0.5 + pixel * 10, 0.5 + pixel * 10, + 0.5 + pixel * 10 + eps / 3, 0.5 + pixel * 10 + )) + .setMinPixelSize(1) + .setZoomRange(14, 14) + .setBufferPixels(0) + .setPixelToleranceAtAllZooms(0); + assertExactSameFeatures(Map.of(), renderGeometry(feature)); + } + /* * POLYGON TESTS */