2021-12-23 10:42:24 +00:00
|
|
|
package com.onthegomap.planetiler.render;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-12-23 10:42:24 +00:00
|
|
|
import com.onthegomap.planetiler.FeatureCollector;
|
|
|
|
import com.onthegomap.planetiler.VectorTile;
|
|
|
|
import com.onthegomap.planetiler.config.PlanetilerConfig;
|
|
|
|
import com.onthegomap.planetiler.geo.DouglasPeuckerSimplifier;
|
|
|
|
import com.onthegomap.planetiler.geo.GeoUtils;
|
|
|
|
import com.onthegomap.planetiler.geo.GeometryException;
|
|
|
|
import com.onthegomap.planetiler.geo.TileCoord;
|
|
|
|
import com.onthegomap.planetiler.geo.TileExtents;
|
|
|
|
import com.onthegomap.planetiler.stats.Stats;
|
2022-04-26 10:26:05 +00:00
|
|
|
import java.io.Closeable;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.UncheckedIOException;
|
2021-06-25 11:06:55 +00:00
|
|
|
import java.util.HashMap;
|
2021-05-18 11:30:46 +00:00
|
|
|
import java.util.List;
|
2021-05-13 10:25:06 +00:00
|
|
|
import java.util.Map;
|
|
|
|
import java.util.Optional;
|
2021-04-11 20:10:28 +00:00
|
|
|
import java.util.function.Consumer;
|
2021-05-16 10:42:57 +00:00
|
|
|
import org.locationtech.jts.geom.Coordinate;
|
2021-05-18 11:30:46 +00:00
|
|
|
import org.locationtech.jts.geom.CoordinateSequence;
|
2021-04-12 12:53:53 +00:00
|
|
|
import org.locationtech.jts.geom.Geometry;
|
|
|
|
import org.locationtech.jts.geom.GeometryCollection;
|
|
|
|
import org.locationtech.jts.geom.LineString;
|
|
|
|
import org.locationtech.jts.geom.MultiLineString;
|
|
|
|
import org.locationtech.jts.geom.MultiPoint;
|
|
|
|
import org.locationtech.jts.geom.MultiPolygon;
|
|
|
|
import org.locationtech.jts.geom.Point;
|
|
|
|
import org.locationtech.jts.geom.Polygon;
|
2021-05-18 11:30:46 +00:00
|
|
|
import org.locationtech.jts.geom.Polygonal;
|
2021-05-19 10:44:28 +00:00
|
|
|
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
2021-05-18 11:30:46 +00:00
|
|
|
import org.locationtech.jts.geom.util.AffineTransformation;
|
2021-04-12 12:53:53 +00:00
|
|
|
import org.slf4j.Logger;
|
|
|
|
import org.slf4j.LoggerFactory;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/**
|
|
|
|
* Converts source features geometries to encoded vector tile features according to settings configured in the map
|
|
|
|
* profile (like zoom range, min pixel size, output attributes and their zoom ranges).
|
|
|
|
*/
|
2022-04-26 10:26:05 +00:00
|
|
|
public class FeatureRenderer implements Consumer<FeatureCollector.Feature>, Closeable {
|
2021-04-12 12:53:53 +00:00
|
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureRenderer.class);
|
2021-09-10 00:46:20 +00:00
|
|
|
private static final VectorTile.VectorGeometry FILL = VectorTile.encodeGeometry(GeoUtils.JTS_FACTORY
|
2021-05-19 10:44:28 +00:00
|
|
|
.createPolygon(GeoUtils.JTS_FACTORY.createLinearRing(new PackedCoordinateSequence.Double(new double[]{
|
|
|
|
-5, -5,
|
|
|
|
261, -5,
|
|
|
|
261, 261,
|
|
|
|
-5, 261,
|
|
|
|
-5, -5
|
|
|
|
}, 2, 0))));
|
2021-12-23 10:42:24 +00:00
|
|
|
private final PlanetilerConfig config;
|
2021-05-13 10:25:06 +00:00
|
|
|
private final Consumer<RenderedFeature> consumer;
|
2021-06-06 12:00:04 +00:00
|
|
|
private final Stats stats;
|
2022-04-26 10:26:05 +00:00
|
|
|
private final Closeable closeable;
|
2021-04-10 09:25:42 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
/** Constructs a new feature render that will send rendered features to {@code consumer}. */
|
2022-04-26 10:26:05 +00:00
|
|
|
public FeatureRenderer(PlanetilerConfig config, Consumer<RenderedFeature> consumer, Stats stats,
|
|
|
|
Closeable closeable) {
|
2021-04-12 12:53:53 +00:00
|
|
|
this.config = config;
|
2021-05-13 10:25:06 +00:00
|
|
|
this.consumer = consumer;
|
2021-06-06 12:00:04 +00:00
|
|
|
this.stats = stats;
|
2022-04-26 10:26:05 +00:00
|
|
|
this.closeable = closeable;
|
|
|
|
}
|
|
|
|
|
|
|
|
public FeatureRenderer(PlanetilerConfig config, Consumer<RenderedFeature> consumer, Stats stats) {
|
|
|
|
this(config, consumer, stats, null);
|
2021-04-10 09:25:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-28 10:08:13 +00:00
|
|
|
@Override
|
|
|
|
public void accept(FeatureCollector.Feature feature) {
|
2021-05-13 10:25:06 +00:00
|
|
|
renderGeometry(feature.getGeometry(), feature);
|
2021-04-12 12:53:53 +00:00
|
|
|
}
|
|
|
|
|
2021-05-18 10:53:12 +00:00
|
|
|
private void renderGeometry(Geometry geom, FeatureCollector.Feature feature) {
|
2021-05-16 10:42:57 +00:00
|
|
|
if (geom.isEmpty()) {
|
2022-04-26 10:26:05 +00:00
|
|
|
LOGGER.warn("Empty geometry {}", feature);
|
2021-05-18 10:53:12 +00:00
|
|
|
} else if (geom instanceof Point point) {
|
2021-05-27 09:54:45 +00:00
|
|
|
renderPoint(feature, point.getCoordinates());
|
2021-05-18 10:53:12 +00:00
|
|
|
} else if (geom instanceof MultiPoint points) {
|
2021-05-27 09:54:45 +00:00
|
|
|
renderPoint(feature, points);
|
2022-03-09 02:08:03 +00:00
|
|
|
} else if (geom instanceof Polygon || geom instanceof MultiPolygon || geom instanceof LineString ||
|
|
|
|
geom instanceof MultiLineString) {
|
2021-05-27 09:54:45 +00:00
|
|
|
renderLineOrPolygon(feature, geom);
|
2021-04-12 12:53:53 +00:00
|
|
|
} else if (geom instanceof GeometryCollection collection) {
|
|
|
|
for (int i = 0; i < collection.getNumGeometries(); i++) {
|
2021-05-13 10:25:06 +00:00
|
|
|
renderGeometry(collection.getGeometryN(i), feature);
|
2021-04-12 12:53:53 +00:00
|
|
|
}
|
|
|
|
} else {
|
2022-04-26 10:26:05 +00:00
|
|
|
LOGGER.warn("Unrecognized JTS geometry type for {}: {}", feature.getClass().getSimpleName(),
|
|
|
|
geom.getGeometryType());
|
2021-04-12 12:53:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-22 10:48:04 +00:00
|
|
|
private void renderPoint(FeatureCollector.Feature feature, Coordinate... origCoords) {
|
2022-01-10 11:41:15 +00:00
|
|
|
boolean hasLabelGrid = feature.hasLabelGrid();
|
2022-07-22 10:48:04 +00:00
|
|
|
Coordinate[] coords = new Coordinate[origCoords.length];
|
|
|
|
for (int i = 0; i < origCoords.length; i++) {
|
|
|
|
coords[i] = origCoords[i].copy();
|
|
|
|
}
|
2021-05-13 10:25:06 +00:00
|
|
|
for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) {
|
|
|
|
Map<String, Object> attrs = feature.getAttrsAtZoom(zoom);
|
2021-05-16 10:42:57 +00:00
|
|
|
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
|
|
|
|
int tilesAtZoom = 1 << zoom;
|
2022-07-22 10:48:04 +00:00
|
|
|
// scale coordinates for this zoom
|
|
|
|
for (int i = 0; i < coords.length; i++) {
|
|
|
|
var orig = origCoords[i];
|
|
|
|
coords[i].setX(orig.x * tilesAtZoom);
|
|
|
|
coords[i].setY(orig.y * tilesAtZoom);
|
|
|
|
}
|
|
|
|
|
2021-05-16 10:42:57 +00:00
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// for "label grid" point density limiting, compute the grid square that this point sits in
|
|
|
|
// only valid if not a multipoint
|
2021-05-19 10:44:28 +00:00
|
|
|
RenderedFeature.Group groupInfo = null;
|
2022-01-10 11:41:15 +00:00
|
|
|
if (hasLabelGrid && coords.length == 1) {
|
|
|
|
double labelGridTileSize = feature.getPointLabelGridPixelSizeAtZoom(zoom) / 256d;
|
|
|
|
groupInfo = labelGridTileSize < 1d / 4096d ? null : new RenderedFeature.Group(
|
|
|
|
GeoUtils.labelGridId(tilesAtZoom, labelGridTileSize, coords[0]),
|
|
|
|
feature.getPointLabelGridLimitAtZoom(zoom)
|
|
|
|
);
|
2021-05-16 10:42:57 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// compute the tile coordinate of every tile these points should show up in at the given buffer size
|
|
|
|
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(zoom);
|
2022-07-22 10:48:04 +00:00
|
|
|
TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords);
|
2021-06-06 12:00:04 +00:00
|
|
|
int emitted = 0;
|
2022-07-22 10:48:04 +00:00
|
|
|
for (var entry : tiled.getTileData().entrySet()) {
|
2021-05-19 10:44:28 +00:00
|
|
|
TileCoord tile = entry.getKey();
|
2021-05-22 11:07:34 +00:00
|
|
|
List<List<CoordinateSequence>> result = entry.getValue();
|
2021-09-10 00:46:20 +00:00
|
|
|
Geometry geom = GeometryCoordinateSequences.reassemblePoints(result);
|
2023-03-15 14:28:16 +00:00
|
|
|
encodeAndEmitFeature(feature, feature.getId(), attrs, tile, geom, groupInfo, 0);
|
2021-06-06 12:00:04 +00:00
|
|
|
emitted++;
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
2021-06-06 12:00:04 +00:00
|
|
|
stats.emittedFeatures(zoom, feature.getLayer(), emitted);
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
2021-06-06 12:00:04 +00:00
|
|
|
|
|
|
|
stats.processedElement("point", feature.getLayer());
|
2021-05-13 10:25:06 +00:00
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
private void encodeAndEmitFeature(FeatureCollector.Feature feature, long id, Map<String, Object> attrs,
|
2021-10-20 01:57:47 +00:00
|
|
|
TileCoord tile, Geometry geom, RenderedFeature.Group groupInfo, int scale) {
|
2021-05-19 10:44:28 +00:00
|
|
|
consumer.accept(new RenderedFeature(
|
|
|
|
tile,
|
2021-09-10 00:46:20 +00:00
|
|
|
new VectorTile.Feature(
|
2021-05-19 10:44:28 +00:00
|
|
|
feature.getLayer(),
|
|
|
|
id,
|
2021-10-20 01:57:47 +00:00
|
|
|
VectorTile.encodeGeometry(geom, scale),
|
2021-05-31 11:16:44 +00:00
|
|
|
attrs,
|
2021-09-10 00:46:20 +00:00
|
|
|
groupInfo == null ? VectorTile.Feature.NO_GROUP : groupInfo.group()
|
2021-05-19 10:44:28 +00:00
|
|
|
),
|
2021-09-18 00:18:06 +00:00
|
|
|
feature.getSortKey(),
|
2021-05-19 10:44:28 +00:00
|
|
|
Optional.ofNullable(groupInfo)
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2021-05-27 09:54:45 +00:00
|
|
|
private void renderPoint(FeatureCollector.Feature feature, MultiPoint points) {
|
2021-09-10 00:46:20 +00:00
|
|
|
/*
|
2021-09-18 00:18:06 +00:00
|
|
|
* Attempt to encode multipoints as a single feature sharing attributes and sort-key
|
2021-09-10 00:46:20 +00:00
|
|
|
* but if it has label grid data then need to fall back to separate features per point,
|
|
|
|
* so they can be filtered individually.
|
|
|
|
*/
|
2021-05-16 10:42:57 +00:00
|
|
|
if (feature.hasLabelGrid()) {
|
|
|
|
for (Coordinate coord : points.getCoordinates()) {
|
2021-05-27 09:54:45 +00:00
|
|
|
renderPoint(feature, coord);
|
2021-05-16 10:42:57 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-05-27 09:54:45 +00:00
|
|
|
renderPoint(feature, points.getCoordinates());
|
2021-05-16 10:42:57 +00:00
|
|
|
}
|
2021-04-12 12:53:53 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 09:54:45 +00:00
|
|
|
private void renderLineOrPolygon(FeatureCollector.Feature feature, Geometry input) {
|
2021-05-18 11:30:46 +00:00
|
|
|
boolean area = input instanceof Polygonal;
|
|
|
|
double worldLength = (area || input.getNumGeometries() > 1) ? 0 : input.getLength();
|
2021-06-25 11:06:55 +00:00
|
|
|
String numPointsAttr = feature.getNumPointsAttr();
|
2021-05-18 11:30:46 +00:00
|
|
|
for (int z = feature.getMaxZoom(); z >= feature.getMinZoom(); z--) {
|
2022-01-10 11:41:15 +00:00
|
|
|
double scale = 1 << z;
|
2021-09-10 00:46:20 +00:00
|
|
|
double tolerance = feature.getPixelToleranceAtZoom(z) / 256d;
|
|
|
|
double minSize = feature.getMinPixelSizeAtZoom(z) / 256d;
|
2021-05-18 11:30:46 +00:00
|
|
|
if (area) {
|
2021-09-10 00:46:20 +00:00
|
|
|
// treat minPixelSize as the edge of a square that defines minimum area for features
|
2021-05-18 11:30:46 +00:00
|
|
|
minSize *= minSize;
|
|
|
|
} else if (worldLength > 0 && worldLength * scale < minSize) {
|
|
|
|
// skip linestring, too short
|
|
|
|
continue;
|
|
|
|
}
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2023-02-24 18:14:50 +00:00
|
|
|
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
|
|
|
|
TileExtents.ForZoom extents = config.bounds().tileExtents().getForZoom(z);
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// TODO potential optimization: iteratively simplify z+1 to get z instead of starting with original geom each time
|
|
|
|
// simplify only takes 4-5 minutes of wall time when generating the planet though, so not a big deal
|
2023-02-24 18:14:50 +00:00
|
|
|
Geometry scaled = AffineTransformation.scaleInstance(scale, scale).transform(input);
|
|
|
|
TiledGeometry sliced;
|
|
|
|
Geometry geom = DouglasPeuckerSimplifier.simplify(scaled, tolerance);
|
2021-09-10 00:46:20 +00:00
|
|
|
List<List<CoordinateSequence>> groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
2023-02-24 18:14:50 +00:00
|
|
|
try {
|
|
|
|
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
|
|
|
} catch (GeometryException e) {
|
|
|
|
try {
|
|
|
|
geom = GeoUtils.fixPolygon(geom);
|
|
|
|
groups = GeometryCoordinateSequences.extractGroups(geom, minSize);
|
|
|
|
sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
|
|
|
} catch (GeometryException ex) {
|
|
|
|
ex.log(stats, "slice_line_or_polygon", "Error slicing feature at z" + z + ": " + feature);
|
|
|
|
// omit from this zoom level, but maybe the next will be better
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
2021-06-25 11:06:55 +00:00
|
|
|
Map<String, Object> attrs = feature.getAttrsAtZoom(sliced.zoomLevel());
|
|
|
|
if (numPointsAttr != null) {
|
2021-09-10 00:46:20 +00:00
|
|
|
// if profile wants the original number of points that the simplified but untiled geometry started with
|
2021-06-25 11:06:55 +00:00
|
|
|
attrs = new HashMap<>(attrs);
|
|
|
|
attrs.put(numPointsAttr, geom.getNumPoints());
|
|
|
|
}
|
2023-03-15 14:28:16 +00:00
|
|
|
writeTileFeatures(z, feature.getId(), feature, sliced, attrs);
|
2021-05-19 10:44:28 +00:00
|
|
|
}
|
2021-06-06 12:00:04 +00:00
|
|
|
|
|
|
|
stats.processedElement(area ? "polygon" : "line", feature.getLayer());
|
2021-05-19 10:44:28 +00:00
|
|
|
}
|
|
|
|
|
2021-06-25 11:06:55 +00:00
|
|
|
private void writeTileFeatures(int zoom, long id, FeatureCollector.Feature feature, TiledGeometry sliced,
|
2022-01-10 11:41:15 +00:00
|
|
|
Map<String, Object> attrs) {
|
2021-06-06 12:00:04 +00:00
|
|
|
int emitted = 0;
|
2022-07-22 10:48:04 +00:00
|
|
|
for (var entry : sliced.getTileData().entrySet()) {
|
2021-05-19 10:44:28 +00:00
|
|
|
TileCoord tile = entry.getKey();
|
|
|
|
try {
|
2021-05-20 09:59:18 +00:00
|
|
|
List<List<CoordinateSequence>> geoms = entry.getValue();
|
2021-05-19 10:44:28 +00:00
|
|
|
|
2021-05-20 09:59:18 +00:00
|
|
|
Geometry geom;
|
2021-10-20 01:57:47 +00:00
|
|
|
int scale = 0;
|
2021-09-10 00:46:20 +00:00
|
|
|
if (feature.isPolygon()) {
|
|
|
|
geom = GeometryCoordinateSequences.reassemblePolygons(geoms);
|
|
|
|
/*
|
|
|
|
* Use the very expensive, but necessary JTS Geometry#buffer(0) trick to repair invalid polygons (with self-
|
|
|
|
* intersections) and JTS GeometryPrecisionReducer utility to snap polygon nodes to the vector tile grid
|
|
|
|
* without introducing self-intersections.
|
|
|
|
*
|
|
|
|
* See https://docs.mapbox.com/vector-tiles/specification/#simplification for issues that can arise from naive
|
|
|
|
* coordinate rounding.
|
|
|
|
*/
|
|
|
|
geom = GeoUtils.snapAndFixPolygon(geom);
|
|
|
|
// JTS utilities "fix" the geometry to be clockwise outer/CCW inner but vector tiles flip Y coordinate,
|
|
|
|
// so we need outer CCW/inner clockwise
|
2021-05-23 19:06:26 +00:00
|
|
|
geom = geom.reverse();
|
2021-05-20 09:59:18 +00:00
|
|
|
} else {
|
2021-09-10 00:46:20 +00:00
|
|
|
geom = GeometryCoordinateSequences.reassembleLineStrings(geoms);
|
2021-10-20 01:57:47 +00:00
|
|
|
// Store lines with extra precision (2^scale) in intermediate feature storage so that
|
|
|
|
// rounding does not introduce artificial endpoint intersections and confuse line merge
|
|
|
|
// post-processing. Features need to be "unscaled" in FeatureGroup after line merging,
|
2023-01-17 12:05:45 +00:00
|
|
|
// and before emitting to the output archive.
|
2021-10-20 01:57:47 +00:00
|
|
|
scale = Math.max(config.maxzoom(), 14) - zoom;
|
2022-03-27 09:49:58 +00:00
|
|
|
// need 14 bits to represent tile coordinates (4096 * 2 for buffer * 2 for zigzag encoding)
|
2021-10-20 01:57:47 +00:00
|
|
|
// so cap the scale factor to avoid overflowing 32-bit integer space
|
|
|
|
scale = Math.min(31 - 14, scale);
|
2021-05-20 09:59:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!geom.isEmpty()) {
|
2022-01-10 11:41:15 +00:00
|
|
|
encodeAndEmitFeature(feature, id, attrs, tile, geom, null, scale);
|
2021-06-06 12:00:04 +00:00
|
|
|
emitted++;
|
2021-05-19 10:44:28 +00:00
|
|
|
}
|
2021-05-20 09:59:18 +00:00
|
|
|
} catch (GeometryException e) {
|
2021-07-18 12:17:58 +00:00
|
|
|
e.log(stats, "write_tile_features", "Error writing tile " + tile + " feature " + feature);
|
2021-05-19 10:44:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-10 00:46:20 +00:00
|
|
|
// polygons that span multiple tiles contain detail about the outer edges separate from the filled tiles, so emit
|
|
|
|
// filled tiles now
|
|
|
|
if (feature.isPolygon()) {
|
2021-06-06 12:00:04 +00:00
|
|
|
emitted += emitFilledTiles(id, feature, sliced);
|
2021-05-18 11:30:46 +00:00
|
|
|
}
|
2021-06-06 12:00:04 +00:00
|
|
|
|
|
|
|
stats.emittedFeatures(zoom, feature.getLayer(), emitted);
|
2021-04-10 09:25:42 +00:00
|
|
|
}
|
2021-05-18 10:53:12 +00:00
|
|
|
|
2021-06-06 12:00:04 +00:00
|
|
|
private int emitFilledTiles(long id, FeatureCollector.Feature feature, TiledGeometry sliced) {
|
2021-09-10 00:46:20 +00:00
|
|
|
Optional<RenderedFeature.Group> groupInfo = Optional.empty();
|
2021-05-19 10:44:28 +00:00
|
|
|
/*
|
2021-09-10 00:46:20 +00:00
|
|
|
* Optimization: large input polygons that generate many filled interior tiles (i.e. the ocean), the encoder avoids
|
|
|
|
* re-encoding if groupInfo and vector tile feature are == to previous values, so compute one instance at the start
|
|
|
|
* of each zoom level for this feature.
|
2021-05-19 10:44:28 +00:00
|
|
|
*/
|
2021-09-10 00:46:20 +00:00
|
|
|
VectorTile.Feature vectorTileFeature = new VectorTile.Feature(
|
2021-05-20 09:59:18 +00:00
|
|
|
feature.getLayer(),
|
|
|
|
id,
|
|
|
|
FILL,
|
|
|
|
feature.getAttrsAtZoom(sliced.zoomLevel())
|
|
|
|
);
|
2021-05-19 10:44:28 +00:00
|
|
|
|
2021-06-06 12:00:04 +00:00
|
|
|
int emitted = 0;
|
2021-05-20 09:59:18 +00:00
|
|
|
for (TileCoord tile : sliced.getFilledTiles()) {
|
2021-05-19 10:44:28 +00:00
|
|
|
consumer.accept(new RenderedFeature(
|
|
|
|
tile,
|
2021-05-20 09:59:18 +00:00
|
|
|
vectorTileFeature,
|
2021-09-18 00:18:06 +00:00
|
|
|
feature.getSortKey(),
|
2021-05-19 10:44:28 +00:00
|
|
|
groupInfo
|
|
|
|
));
|
2021-06-06 12:00:04 +00:00
|
|
|
emitted++;
|
2021-05-19 10:44:28 +00:00
|
|
|
}
|
2021-06-06 12:00:04 +00:00
|
|
|
return emitted;
|
2021-05-18 11:30:46 +00:00
|
|
|
}
|
2022-04-26 10:26:05 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() {
|
|
|
|
if (closeable != null) {
|
|
|
|
try {
|
|
|
|
closeable.close();
|
|
|
|
} catch (IOException e) {
|
|
|
|
throw new UncheckedIOException(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-10 09:25:42 +00:00
|
|
|
}
|