diff --git a/src/main/java/com/onthegomap/flatmap/LayerStats.java b/src/main/java/com/onthegomap/flatmap/LayerStats.java index 8df858ab..4d0eb4c6 100644 --- a/src/main/java/com/onthegomap/flatmap/LayerStats.java +++ b/src/main/java/com/onthegomap/flatmap/LayerStats.java @@ -1,5 +1,6 @@ package com.onthegomap.flatmap; +import com.onthegomap.flatmap.render.RenderedFeature; import com.onthegomap.flatmap.write.Mbtiles; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/com/onthegomap/flatmap/OpenMapTilesMain.java b/src/main/java/com/onthegomap/flatmap/OpenMapTilesMain.java index 996fefe0..3c2864e4 100644 --- a/src/main/java/com/onthegomap/flatmap/OpenMapTilesMain.java +++ b/src/main/java/com/onthegomap/flatmap/OpenMapTilesMain.java @@ -100,8 +100,7 @@ public class OpenMapTilesMain { stats.time("sort", featureDb::sort); - stats.time("mbtiles", - () -> MbtilesWriter.writeOutput(featureMap, output, profile, config, stats)); + stats.time("mbtiles", () -> MbtilesWriter.writeOutput(featureMap, output, profile, config, stats)); stats.stopTimer("import"); diff --git a/src/main/java/com/onthegomap/flatmap/collections/FeatureGroup.java b/src/main/java/com/onthegomap/flatmap/collections/FeatureGroup.java index 74daf2f4..af356b4b 100644 --- a/src/main/java/com/onthegomap/flatmap/collections/FeatureGroup.java +++ b/src/main/java/com/onthegomap/flatmap/collections/FeatureGroup.java @@ -4,10 +4,10 @@ import com.carrotsearch.hppc.LongLongHashMap; import com.graphhopper.coll.GHLongLongHashMap; import com.onthegomap.flatmap.LayerStats; import com.onthegomap.flatmap.Profile; -import com.onthegomap.flatmap.RenderedFeature; import com.onthegomap.flatmap.VectorTileEncoder; import com.onthegomap.flatmap.geo.TileCoord; import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.render.RenderedFeature; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java b/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java index 4bc91fff..6e0049eb 100644 --- a/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java +++ b/src/main/java/com/onthegomap/flatmap/geo/GeoUtils.java @@ -11,6 +11,7 @@ import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.TopologyException; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; import org.locationtech.jts.geom.util.GeometryTransformer; import org.locationtech.jts.io.WKBReader; @@ -198,4 +199,35 @@ public class GeoUtils { public static Geometry createMultiLineString(List lineStrings) { return JTS_FACTORY.createMultiLineString(lineStrings.toArray(EMPTY_LINE_STRING_ARRAY)); } + + public static Geometry fixPolygon(Geometry geom, int maxAttempts) throws GeometryException { + try { + int attempts; + for (attempts = 0; attempts < maxAttempts && !geom.isValid(); attempts++) { + geom = geom.buffer(0); + } + + if (attempts == maxAttempts) { + throw new GeometryException("Geometry still invalid after 2 buffers"); + } + return geom; + } catch (TopologyException e) { + throw new GeometryException("Unable to fix polygon: " + e); + } + } + + private static double wrapDouble(double value, double max) { + value %= max; + if (value < 0) { + value += max; + } + return value; + } + + public static long labelGridId(int tilesAtZoom, double labelGridTileSize, Coordinate coord) { + return GeoUtils.longPair( + (int) Math.floor(wrapDouble(coord.getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize), + (int) Math.floor((coord.getY() * tilesAtZoom) / labelGridTileSize) + ); + } } diff --git a/src/main/java/com/onthegomap/flatmap/geo/GeometryException.java b/src/main/java/com/onthegomap/flatmap/geo/GeometryException.java new file mode 100644 index 00000000..ca3620e2 --- /dev/null +++ b/src/main/java/com/onthegomap/flatmap/geo/GeometryException.java @@ -0,0 +1,17 @@ +package com.onthegomap.flatmap.geo; + +public class GeometryException extends Exception { + + public GeometryException(Throwable cause) { + super(cause); + } + + public GeometryException(String message, Throwable cause) { + super(message, cause); + } + + public GeometryException(String message) { + super(message); + } + +} diff --git a/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java b/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java index ee1ea0a4..97eb9b01 100644 --- a/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java +++ b/src/main/java/com/onthegomap/flatmap/read/OpenStreetMapReader.java @@ -12,7 +12,6 @@ import com.graphhopper.reader.ReaderRelation; import com.graphhopper.reader.ReaderWay; import com.onthegomap.flatmap.CommonParams; import com.onthegomap.flatmap.FeatureCollector; -import com.onthegomap.flatmap.FeatureRenderer; import com.onthegomap.flatmap.MemoryEstimator; import com.onthegomap.flatmap.Profile; import com.onthegomap.flatmap.SourceFeature; @@ -23,6 +22,7 @@ import com.onthegomap.flatmap.collections.LongLongMultimap; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.monitoring.ProgressLoggers; import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.render.FeatureRenderer; import com.onthegomap.flatmap.worker.Topology; import java.io.Closeable; import java.io.IOException; diff --git a/src/main/java/com/onthegomap/flatmap/read/Reader.java b/src/main/java/com/onthegomap/flatmap/read/Reader.java index 0175be24..ee786978 100644 --- a/src/main/java/com/onthegomap/flatmap/read/Reader.java +++ b/src/main/java/com/onthegomap/flatmap/read/Reader.java @@ -2,13 +2,13 @@ package com.onthegomap.flatmap.read; import com.onthegomap.flatmap.CommonParams; import com.onthegomap.flatmap.FeatureCollector; -import com.onthegomap.flatmap.FeatureRenderer; import com.onthegomap.flatmap.Profile; import com.onthegomap.flatmap.SourceFeature; import com.onthegomap.flatmap.collections.FeatureGroup; import com.onthegomap.flatmap.collections.FeatureSort; import com.onthegomap.flatmap.monitoring.ProgressLoggers; import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.render.FeatureRenderer; import com.onthegomap.flatmap.worker.Topology; import java.io.Closeable; import java.util.concurrent.atomic.AtomicLong; diff --git a/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java b/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java new file mode 100644 index 00000000..073a6ff3 --- /dev/null +++ b/src/main/java/com/onthegomap/flatmap/render/CoordinateSequenceExtractor.java @@ -0,0 +1,105 @@ +package com.onthegomap.flatmap.render; + +import com.onthegomap.flatmap.FeatureCollector; +import com.onthegomap.flatmap.geo.GeoUtils; +import com.onthegomap.flatmap.geo.TileCoord; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.locationtech.jts.algorithm.Area; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequences; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.Polygon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CoordinateSequenceExtractor { + + private static final Logger LOGGER = LoggerFactory.getLogger(CoordinateSequenceExtractor.class); + + static Geometry reassembleLineString(List> geoms) { + Geometry geom; + List lineStrings = new ArrayList<>(); + for (List inner : geoms) { + for (CoordinateSequence coordinateSequence : inner) { + lineStrings.add(GeoUtils.JTS_FACTORY.createLineString(coordinateSequence)); + } + } + geom = GeoUtils.createMultiLineString(lineStrings); + return geom; + } + + @NotNull + static Geometry reassemblePolygon(FeatureCollector.Feature feature, TileCoord tile, + List> geoms) { + Geometry geom; + int numGeoms = geoms.size(); + Polygon[] polygons = new Polygon[numGeoms]; + for (int i = 0; i < numGeoms; i++) { + List group = geoms.get(i); + 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); + } + polygons[i] = GeoUtils.JTS_FACTORY.createPolygon(first, rest); + } + geom = GeoUtils.JTS_FACTORY.createMultiPolygon(polygons); + return geom; + } + + static List> extractGroups(Geometry geom, double minSize) { + List> result = new ArrayList<>(); + extractGroups(geom, result, minSize); + return result; + } + + private static void extractGroups(Geometry geom, List> groups, double minSize) { + if (geom.isEmpty()) { + // ignore + } else if (geom instanceof GeometryCollection) { + for (int i = 0; i < geom.getNumGeometries(); i++) { + extractGroups(geom.getGeometryN(i), groups, minSize); + } + } else if (geom instanceof Polygon polygon) { + extractGroupsFromPolygon(groups, minSize, polygon); + } else if (geom instanceof LinearRing linearRing) { + extractGroups(GeoUtils.JTS_FACTORY.createPolygon(linearRing), groups, minSize); + } else if (geom instanceof LineString lineString) { + if (lineString.getLength() >= minSize) { + groups.add(List.of(lineString.getCoordinateSequence())); + } + } else { + throw new RuntimeException("unrecognized geometry type: " + geom.getGeometryType()); + } + } + + private static void extractGroupsFromPolygon(List> groups, double minSize, Polygon polygon) { + CoordinateSequence outer = polygon.getExteriorRing().getCoordinateSequence(); + double outerArea = Area.ofRingSigned(outer); + if (outerArea > 0) { + CoordinateSequences.reverse(outer); + } + if (Math.abs(outerArea) >= minSize) { + List group = new ArrayList<>(1 + polygon.getNumInteriorRing()); + groups.add(group); + group.add(outer); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { + CoordinateSequence inner = polygon.getInteriorRingN(i).getCoordinateSequence(); + double innerArea = Area.ofRingSigned(inner); + if (innerArea > 0) { + CoordinateSequences.reverse(inner); + } + if (Math.abs(innerArea) >= minSize) { + group.add(inner); + } + } + } + } +} diff --git a/src/main/java/com/onthegomap/flatmap/FeatureRenderer.java b/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java similarity index 59% rename from src/main/java/com/onthegomap/flatmap/FeatureRenderer.java rename to src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java index 2ad01beb..a5daac9b 100644 --- a/src/main/java/com/onthegomap/flatmap/FeatureRenderer.java +++ b/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java @@ -1,8 +1,12 @@ -package com.onthegomap.flatmap; +package com.onthegomap.flatmap.render; +import com.onthegomap.flatmap.CommonParams; +import com.onthegomap.flatmap.FeatureCollector; +import com.onthegomap.flatmap.TileExtents; +import com.onthegomap.flatmap.VectorTileEncoder; 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.HashMap; import java.util.HashSet; import java.util.List; @@ -11,16 +15,12 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; -import org.jetbrains.annotations.NotNull; -import org.locationtech.jts.algorithm.Area; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.CoordinateSequence; -import org.locationtech.jts.geom.CoordinateSequences; import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.LinearRing; import org.locationtech.jts.geom.MultiLineString; import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.MultiPolygon; @@ -65,14 +65,6 @@ public class FeatureRenderer { return value; } - private static double wrapDouble(double value, double max) { - value %= max; - if (value < 0) { - value += max; - } - return value; - } - public void renderFeature(FeatureCollector.Feature feature) { renderGeometry(feature.getGeometry(), feature); } @@ -113,6 +105,7 @@ public class FeatureRenderer { TileCoord tile = TileCoord.ofXYZ(wrapInt(x, tilesAtZoom), y, zoom); double tileY = worldY - y; Coordinate outCoordinate = new CoordinateXY(tileX * 256, tileY * 256); + tilePrecision.makePrecise(outCoordinate); output.computeIfAbsent(tile, t -> new HashSet<>()).add(outCoordinate); } } @@ -120,6 +113,7 @@ public class FeatureRenderer { private void addPointFeature(FeatureCollector.Feature feature, Coordinate... coords) { long id = idGen.incrementAndGet(); + boolean hasLabelGrid = feature.hasLabelGrid(); for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) { Map> sliced = new HashMap<>(); Map attrs = feature.getAttrsAtZoom(zoom); @@ -131,12 +125,12 @@ public class FeatureRenderer { } RenderedFeature.Group groupInfo = null; - if (feature.hasLabelGrid() && coords.length == 1) { + if (hasLabelGrid && coords.length == 1) { double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d; - groupInfo = labelGridTileSize >= 1d / 4096d ? new RenderedFeature.Group(GeoUtils.longPair( - (int) Math.floor(wrapDouble(coords[0].getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize), - (int) Math.floor((coords[0].getY() * tilesAtZoom) / labelGridTileSize) - ), feature.getLabelGridLimitAtZoom(zoom)) : null; + groupInfo = labelGridTileSize < 1d / 4096d ? null : new RenderedFeature.Group( + GeoUtils.labelGridId(tilesAtZoom, labelGridTileSize, coords[0]), + feature.getLabelGridLimitAtZoom(zoom) + ); } for (var entry : sliced.entrySet()) { @@ -145,17 +139,13 @@ public class FeatureRenderer { Geometry geom = value.size() == 1 ? GeoUtils.point(value.iterator().next()) : GeoUtils.multiPoint(value); // TODO stats // TODO writeTileFeatures - emitFeature(feature, id, attrs, groupInfo, tile, geom); + emitFeature(feature, id, attrs, tile, geom, groupInfo); } } } - private void emitFeature(FeatureCollector.Feature feature, long id, TileCoord tile, Geometry geom) { - emitFeature(feature, id, feature.getAttrsAtZoom(tile.z()), null, tile, geom); - } - - private void emitFeature(FeatureCollector.Feature feature, long id, Map attrs, - RenderedFeature.Group groupInfo, TileCoord tile, Geometry geom) { + private void emitFeature(FeatureCollector.Feature feature, long id, Map attrs, TileCoord tile, + Geometry geom, RenderedFeature.Group groupInfo) { consumer.accept(new RenderedFeature( tile, new VectorTileEncoder.Feature( @@ -206,8 +196,7 @@ public class FeatureRenderer { simplifier.setDistanceTolerance(tolerance); geom = simplifier.getResultGeometry(); - List> groups = new ArrayList<>(); - extractGroups(geom, groups, minSize); + List> groups = CoordinateSequenceExtractor.extractGroups(geom, minSize); double buffer = feature.getBufferPixelsAtZoom(z); TileExtents.ForZoom extents = config.extents().getForZoom(z); TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents); @@ -216,29 +205,35 @@ public class FeatureRenderer { } private void writeTileFeatures(long id, FeatureCollector.Feature feature, TiledGeometry sliced) { + Map attrs = feature.getAttrsAtZoom(sliced.zoomLevel()); for (var entry : sliced.getTileData()) { TileCoord tile = entry.getKey(); - List> geoms = entry.getValue(); - - Geometry geom; - if (feature.area()) { - geom = reassemblePolygon(feature, tile, geoms); - } else { - geom = reassembleLineString(geoms); - } - try { - geom = GeometryPrecisionReducer.reduce(geom, tilePrecision); - } catch (IllegalArgumentException e) { - LOGGER.warn("Error reducing precision of " + feature + " on " + tile + ": " + e); - } + List> geoms = entry.getValue(); - if (!geom.isEmpty()) { - // JTS utilities "fix" the geometry to be clockwise outer/CCW inner + Geometry geom; if (feature.area()) { - geom = geom.reverse(); + geom = CoordinateSequenceExtractor.reassemblePolygon(feature, tile, geoms); + geom = GeoUtils.fixPolygon(geom, 2); + } else { + geom = CoordinateSequenceExtractor.reassembleLineString(geoms); } - emitFeature(feature, id, tile, geom); + + try { + geom = GeometryPrecisionReducer.reduce(geom, tilePrecision); + } catch (IllegalArgumentException e) { + throw new GeometryException("Error reducing precision"); + } + + 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) { + LOGGER.warn(e.getMessage() + ": " + tile + " " + feature); } } @@ -251,110 +246,23 @@ public class FeatureRenderer { private void emitFilledTiles(long id, FeatureCollector.Feature feature, TiledGeometry sliced) { /* * Optimization: large input polygons that generate many filled interior tiles (ie. the ocean), the encoder avoids - * re-encoding if groupInfo and vector tile feature are == to previous values. The feature can have different - * attributes at different zoom levels though, so need to cache each vector tile feature instance by zoom level. + * re-encoding if groupInfo and vector tile feature are == to previous values. */ Optional groupInfo = Optional.empty(); - VectorTileEncoder.Feature cachedFeature = null; - int lastZoom = Integer.MIN_VALUE; + VectorTileEncoder.Feature vectorTileFeature = new VectorTileEncoder.Feature( + feature.getLayer(), + id, + FILL, + feature.getAttrsAtZoom(sliced.zoomLevel()) + ); - for (TileCoord tile : sliced.getFilledTilesOrderedByZXY()) { - int zoom = tile.z(); - if (zoom != lastZoom) { - cachedFeature = new VectorTileEncoder.Feature(feature.getLayer(), id, FILL, feature.getAttrsAtZoom(zoom)); - lastZoom = zoom; - } + for (TileCoord tile : sliced.getFilledTiles()) { consumer.accept(new RenderedFeature( tile, - cachedFeature, + vectorTileFeature, feature.getZorder(), groupInfo )); } } - - private Geometry reassembleLineString(List> geoms) { - Geometry geom; - List lineStrings = new ArrayList<>(); - for (List inner : geoms) { - for (CoordinateSequence coordinateSequence : inner) { - lineStrings.add(GeoUtils.JTS_FACTORY.createLineString(coordinateSequence)); - } - } - geom = GeoUtils.createMultiLineString(lineStrings); - return geom; - } - - @NotNull - private Geometry reassemblePolygon(FeatureCollector.Feature feature, TileCoord tile, - List> geoms) { - Geometry geom; - int numGeoms = geoms.size(); - Polygon[] polygons = new Polygon[numGeoms]; - for (int i = 0; i < numGeoms; i++) { - List group = geoms.get(i); - 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); - } - polygons[i] = GeoUtils.JTS_FACTORY.createPolygon(first, rest); - } - geom = GeoUtils.JTS_FACTORY.createMultiPolygon(polygons); - if (!geom.isValid()) { - geom = geom.buffer(0); - if (!geom.isValid()) { - geom = geom.buffer(0); - if (!geom.isValid()) { - LOGGER.warn("Geometry still invalid after 2 buffers " + feature + " on " + tile); - } - } - } - return geom; - } - - private void extractGroups(Geometry geom, List> groups, double minSize) { - if (geom.isEmpty()) { - // ignore - } else if (geom instanceof GeometryCollection) { - for (int i = 0; i < geom.getNumGeometries(); i++) { - extractGroups(geom.getGeometryN(i), groups, minSize); - } - } else if (geom instanceof Polygon polygon) { - extractGroupsFromPolygon(groups, minSize, polygon); - } else if (geom instanceof LinearRing linearRing) { - extractGroups(GeoUtils.JTS_FACTORY.createPolygon(linearRing), groups, minSize); - } else if (geom instanceof LineString lineString) { - if (lineString.getLength() >= minSize) { - groups.add(List.of(lineString.getCoordinateSequence())); - } - } else { - throw new RuntimeException("unrecognized geometry type: " + geom.getGeometryType()); - } - } - - private void extractGroupsFromPolygon(List> groups, double minSize, Polygon polygon) { - CoordinateSequence outer = polygon.getExteriorRing().getCoordinateSequence(); - double outerArea = Area.ofRingSigned(outer); - if (outerArea > 0) { - CoordinateSequences.reverse(outer); - } - if (Math.abs(outerArea) >= minSize) { - List group = new ArrayList<>(1 + polygon.getNumInteriorRing()); - groups.add(group); - group.add(outer); - for (int i = 0; i < polygon.getNumInteriorRing(); i++) { - CoordinateSequence inner = polygon.getInteriorRingN(i).getCoordinateSequence(); - double innerArea = Area.ofRingSigned(inner); - if (innerArea > 0) { - CoordinateSequences.reverse(inner); - } - if (Math.abs(innerArea) >= minSize) { - group.add(inner); - } - } - } - } } diff --git a/src/main/java/com/onthegomap/flatmap/RenderedFeature.java b/src/main/java/com/onthegomap/flatmap/render/RenderedFeature.java similarity index 79% rename from src/main/java/com/onthegomap/flatmap/RenderedFeature.java rename to src/main/java/com/onthegomap/flatmap/render/RenderedFeature.java index 2e8f0e45..4a8c5ff7 100644 --- a/src/main/java/com/onthegomap/flatmap/RenderedFeature.java +++ b/src/main/java/com/onthegomap/flatmap/render/RenderedFeature.java @@ -1,5 +1,6 @@ -package com.onthegomap.flatmap; +package com.onthegomap.flatmap.render; +import com.onthegomap.flatmap.VectorTileEncoder; import com.onthegomap.flatmap.geo.TileCoord; import java.util.Optional; diff --git a/src/main/java/com/onthegomap/flatmap/TiledGeometry.java b/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java similarity index 92% rename from src/main/java/com/onthegomap/flatmap/TiledGeometry.java rename to src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java index 118da73f..ea3c4707 100644 --- a/src/main/java/com/onthegomap/flatmap/TiledGeometry.java +++ b/src/main/java/com/onthegomap/flatmap/render/TiledGeometry.java @@ -12,12 +12,13 @@ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ -package com.onthegomap.flatmap; +package com.onthegomap.flatmap.render; import com.carrotsearch.hppc.IntObjectMap; import com.carrotsearch.hppc.cursors.IntCursor; import com.carrotsearch.hppc.cursors.IntObjectCursor; import com.graphhopper.coll.GHIntObjectHashMap; +import com.onthegomap.flatmap.TileExtents; import com.onthegomap.flatmap.collections.IntRange; import com.onthegomap.flatmap.collections.MutableCoordinateSequence; import com.onthegomap.flatmap.geo.TileCoord; @@ -26,10 +27,7 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; import java.util.TreeSet; -import org.jetbrains.annotations.NotNull; import org.locationtech.jts.geom.CoordinateSequence; import org.locationtech.jts.geom.impl.PackedCoordinateSequence; import org.slf4j.Logger; @@ -39,12 +37,12 @@ import org.slf4j.LoggerFactory; * This class is adapted from the stripe clipping algorithm in https://github.com/mapbox/geojson-vt/ and modified so * that it eagerly produces all sliced tiles at a zoom level for each input geometry. */ -public class TiledGeometry { +class TiledGeometry { private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class); private final Map>> tileContents = new HashMap<>(); - private final SortedMap filledRanges = new TreeMap<>(); + private final Map filledRanges = new HashMap<>(); private final TileExtents.ForZoom extents; private final double buffer; private final int z; @@ -59,6 +57,10 @@ public class TiledGeometry { this.max = 1 << z; } + public int zoomLevel() { + return z; + } + public static TiledGeometry sliceIntoTiles(List> groups, double buffer, boolean area, int z, TileExtents.ForZoom extents) { int worldExtent = 1 << z; @@ -73,11 +75,10 @@ public class TiledGeometry { return result; } - public Iterable getFilledTilesOrderedByZXY() { + public Iterable getFilledTiles() { return () -> filledRanges.entrySet().stream() .mapMulti((entry, next) -> { - Column column = entry.getKey(); - int x = column.x, z = column.z; + int x = entry.getKey(); for (int y : entry.getValue()) { TileCoord coord = TileCoord.ofXYZ(x, y, z); if (!tileContents.containsKey(coord)) { @@ -141,12 +142,12 @@ public class TiledGeometry { overflow.add(Direction.LEFT); } else { for (CoordinateSequence stripeSegment : xCursor.value) { - IntRange filled = sliceY(stripeSegment, x, outer, inProgressShapes); - if (area && filled != null) { + IntRange filledYRange = sliceY(stripeSegment, x, outer, inProgressShapes); + if (area && filledYRange != null) { if (outer) { - addFilledRange(z, x, filled); + addFilledRange(x, filledYRange); } else { - removeFilledRange(z, x, filled); + removeFilledRange(x, filledYRange); } } } @@ -428,38 +429,27 @@ public class TiledGeometry { return rightFilled != null ? rightFilled.intersect(leftFilled) : null; } - private void addFilledRange(int z, int x, IntRange range) { - if (range == null) { + private void addFilledRange(int x, IntRange yRange) { + if (yRange == null) { return; } - Column key = new Column(z, x); - IntRange existing = filledRanges.get(key); + IntRange existing = filledRanges.get(x); if (existing == null) { - filledRanges.put(key, range); + filledRanges.put(x, yRange); } else { - existing.addAll(range); + existing.addAll(yRange); } } - private void removeFilledRange(int z, int x, IntRange range) { - if (range == null) { + private void removeFilledRange(int x, IntRange yRange) { + if (yRange == null) { return; } - Column key = new Column(z, x); - IntRange existing = filledRanges.get(key); + IntRange existing = filledRanges.get(x); if (existing != null) { - existing.removeAll(range); + existing.removeAll(yRange); } } private enum Direction {RIGHT, LEFT} - - private static record Column(int z, int x) implements Comparable { - - @Override - public int compareTo(@NotNull Column o) { - int result = Integer.compare(z, o.z); - return result == 0 ? Integer.compare(x, o.x) : result; - } - } } diff --git a/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java b/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java index 90cddb18..7fb55fee 100644 --- a/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java +++ b/src/test/java/com/onthegomap/flatmap/LayerStatsTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.geo.TileCoord; +import com.onthegomap.flatmap.render.RenderedFeature; import com.onthegomap.flatmap.write.Mbtiles; import java.util.Map; import java.util.Optional; diff --git a/src/test/java/com/onthegomap/flatmap/TestUtils.java b/src/test/java/com/onthegomap/flatmap/TestUtils.java index 20e4aa08..96696c99 100644 --- a/src/test/java/com/onthegomap/flatmap/TestUtils.java +++ b/src/test/java/com/onthegomap/flatmap/TestUtils.java @@ -29,10 +29,12 @@ import org.locationtech.jts.geom.CoordinateXY; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryCollection; import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Lineal; import org.locationtech.jts.geom.MultiPoint; import org.locationtech.jts.geom.MultiPolygon; import org.locationtech.jts.geom.Point; import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.Puntal; import org.locationtech.jts.geom.util.AffineTransformation; import org.locationtech.jts.geom.util.GeometryTransformer; @@ -228,15 +230,17 @@ public class TestUtils { public static Map toMap(FeatureCollector.Feature feature, int zoom) { TreeMap result = new TreeMap<>(feature.getAttrsAtZoom(zoom)); + Geometry geom = feature.getGeometry(); result.put("_minzoom", feature.getMinZoom()); result.put("_maxzoom", feature.getMaxZoom()); result.put("_buffer", feature.getBufferPixelsAtZoom(zoom)); result.put("_layer", feature.getLayer()); result.put("_zorder", feature.getZorder()); - result.put("_geom", new NormGeometry(feature.getGeometry())); + result.put("_geom", new NormGeometry(geom)); result.put("_labelgrid_limit", feature.getLabelGridLimitAtZoom(zoom)); result.put("_labelgrid_size", feature.getLabelGridPixelSizeAtZoom(zoom)); result.put("_minpixelsize", feature.getMinPixelSize(zoom)); + result.put("_type", geom instanceof Puntal ? "point" : geom instanceof Lineal ? "line" : "polygon"); return result; } diff --git a/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java b/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java index 9ab95057..c238ba9f 100644 --- a/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java +++ b/src/test/java/com/onthegomap/flatmap/collections/FeatureGroupTest.java @@ -7,10 +7,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import com.onthegomap.flatmap.Profile; -import com.onthegomap.flatmap.RenderedFeature; import com.onthegomap.flatmap.VectorTileEncoder; import com.onthegomap.flatmap.geo.TileCoord; import com.onthegomap.flatmap.monitoring.Stats; +import com.onthegomap.flatmap.render.RenderedFeature; import java.util.ArrayList; import java.util.Collections; import java.util.List; diff --git a/src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java b/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java similarity index 98% rename from src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java rename to src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java index 2bae7b5c..1b45a2ee 100644 --- a/src/test/java/com/onthegomap/flatmap/FeatureRendererTest.java +++ b/src/test/java/com/onthegomap/flatmap/render/FeatureRendererTest.java @@ -1,4 +1,4 @@ -package com.onthegomap.flatmap; +package com.onthegomap.flatmap.render; import static com.onthegomap.flatmap.TestUtils.assertSameNormalizedFeatures; import static com.onthegomap.flatmap.TestUtils.emptyGeometry; @@ -8,6 +8,8 @@ import static com.onthegomap.flatmap.TestUtils.newPoint; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import com.onthegomap.flatmap.CommonParams; +import com.onthegomap.flatmap.FeatureCollector; import com.onthegomap.flatmap.geo.GeoUtils; import com.onthegomap.flatmap.geo.TileCoord; import com.onthegomap.flatmap.read.ReaderFeature;