planetiler/flatmap-core/src/main/java/com/onthegomap/flatmap/render/FeatureRenderer.java

237 wiersze
8.9 KiB
Java
Czysty Zwykły widok Historia

2021-05-20 09:59:18 +00:00
package com.onthegomap.flatmap.render;
2021-04-10 09:25:42 +00:00
2021-06-02 11:54:21 +00:00
import static com.onthegomap.flatmap.geo.GeoUtils.TILE_PRECISON;
2021-05-20 09:59:18 +00:00
import com.onthegomap.flatmap.FeatureCollector;
import com.onthegomap.flatmap.VectorTileEncoder;
2021-08-06 09:56:24 +00:00
import com.onthegomap.flatmap.config.CommonParams;
2021-07-30 09:32:10 +00:00
import com.onthegomap.flatmap.geo.DouglasPeuckerSimplifier;
2021-05-13 10:25:06 +00:00
import com.onthegomap.flatmap.geo.GeoUtils;
2021-05-20 09:59:18 +00:00
import com.onthegomap.flatmap.geo.GeometryException;
2021-05-13 10:25:06 +00:00
import com.onthegomap.flatmap.geo.TileCoord;
2021-08-06 09:56:24 +00:00
import com.onthegomap.flatmap.geo.TileExtents;
import com.onthegomap.flatmap.stats.Stats;
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-05-16 10:42:57 +00:00
import java.util.concurrent.atomic.AtomicLong;
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-05-28 10:08:13 +00:00
public class FeatureRenderer implements Consumer<FeatureCollector.Feature> {
2021-04-10 09:25:42 +00:00
2021-05-16 10:42:57 +00:00
private static final AtomicLong idGen = new AtomicLong(0);
2021-04-12 12:53:53 +00:00
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureRenderer.class);
2021-05-19 10:44:28 +00:00
private static final VectorTileEncoder.VectorGeometry FILL = VectorTileEncoder.encodeGeometry(GeoUtils.JTS_FACTORY
.createPolygon(GeoUtils.JTS_FACTORY.createLinearRing(new PackedCoordinateSequence.Double(new double[]{
-5, -5,
261, -5,
261, 261,
-5, 261,
-5, -5
}, 2, 0))));
2021-05-01 20:08:20 +00:00
private final CommonParams 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;
2021-04-10 09:25:42 +00:00
2021-06-06 12:00:04 +00:00
public FeatureRenderer(CommonParams config, Consumer<RenderedFeature> consumer, Stats stats) {
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;
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()) {
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);
2021-04-12 12:53:53 +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 {
2021-05-13 10:25:06 +00:00
LOGGER.warn(
"Unrecognized JTS geometry type for " + feature.getClass().getSimpleName() + ": " + geom.getGeometryType());
2021-04-12 12:53:53 +00:00
}
}
2021-05-27 09:54:45 +00:00
private void renderPoint(FeatureCollector.Feature feature, Coordinate... coords) {
2021-05-16 10:42:57 +00:00
long id = idGen.incrementAndGet();
2021-05-20 09:59:18 +00:00
boolean hasLabelGrid = feature.hasLabelGrid();
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;
2021-05-22 11:07:34 +00:00
TileExtents.ForZoom extents = config.extents().getForZoom(zoom);
2021-06-23 01:46:42 +00:00
TiledGeometry tiled = TiledGeometry.slicePointsIntoTiles(extents, buffer, zoom, coords, feature.sourceId());
2021-05-16 10:42:57 +00:00
2021-05-19 10:44:28 +00:00
RenderedFeature.Group groupInfo = null;
2021-05-20 09:59:18 +00:00
if (hasLabelGrid && coords.length == 1) {
2021-05-16 10:42:57 +00:00
double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d;
2021-05-20 09:59:18 +00:00
groupInfo = labelGridTileSize < 1d / 4096d ? null : new RenderedFeature.Group(
GeoUtils.labelGridId(tilesAtZoom, labelGridTileSize, coords[0]),
feature.getLabelGridLimitAtZoom(zoom)
);
2021-05-16 10:42:57 +00:00
}
2021-06-06 12:00:04 +00:00
int emitted = 0;
2021-05-22 11:07:34 +00:00
for (var entry : tiled.getTileData()) {
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();
Geometry geom = CoordinateSequenceExtractor.reassemblePoints(result);
2021-05-20 09:59:18 +00:00
emitFeature(feature, id, attrs, tile, geom, groupInfo);
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-05-20 09:59:18 +00:00
private void emitFeature(FeatureCollector.Feature feature, long id, Map<String, Object> attrs, TileCoord tile,
Geometry geom, RenderedFeature.Group groupInfo) {
2021-05-19 10:44:28 +00:00
consumer.accept(new RenderedFeature(
tile,
new VectorTileEncoder.Feature(
feature.getLayer(),
id,
VectorTileEncoder.encodeGeometry(geom),
2021-05-31 11:16:44 +00:00
attrs,
groupInfo == null ? VectorTileEncoder.Feature.NO_GROUP : groupInfo.group()
2021-05-19 10:44:28 +00:00
),
feature.getZorder(),
Optional.ofNullable(groupInfo)
));
}
2021-05-27 09:54:45 +00:00
private void renderPoint(FeatureCollector.Feature feature, MultiPoint points) {
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-19 10:44:28 +00:00
long id = idGen.incrementAndGet();
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--) {
double scale = 1 << z;
double tolerance = feature.getPixelTolerance(z) / 256d;
double minSize = feature.getMinPixelSize(z) / 256d;
2021-05-18 11:30:46 +00:00
if (area) {
minSize *= minSize;
} else if (worldLength > 0 && worldLength * scale < minSize) {
// skip linestring, too short
continue;
}
2021-05-18 10:53:12 +00:00
2021-05-18 11:30:46 +00:00
Geometry geom = AffineTransformation.scaleInstance(scale, scale).transform(input);
2021-07-30 09:32:10 +00:00
geom = DouglasPeuckerSimplifier.simplify(geom, tolerance);
2021-05-18 11:30:46 +00:00
2021-05-20 09:59:18 +00:00
List<List<CoordinateSequence>> groups = CoordinateSequenceExtractor.extractGroups(geom, minSize);
2021-05-22 11:07:34 +00:00
double buffer = feature.getBufferPixelsAtZoom(z) / 256;
2021-05-18 11:30:46 +00:00
TileExtents.ForZoom extents = config.extents().getForZoom(z);
2021-06-23 01:46:42 +00:00
TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents, feature.sourceId());
2021-06-25 11:06:55 +00:00
Map<String, Object> attrs = feature.getAttrsAtZoom(sliced.zoomLevel());
if (numPointsAttr != null) {
attrs = new HashMap<>(attrs);
attrs.put(numPointsAttr, geom.getNumPoints());
}
writeTileFeatures(z, id, 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,
Map<String, Object> attrs) {
2021-06-06 12:00:04 +00:00
int emitted = 0;
2021-05-19 10:44:28 +00:00
for (var entry : sliced.getTileData()) {
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-05-19 10:44:28 +00:00
if (feature.area()) {
2021-05-22 11:07:34 +00:00
geom = CoordinateSequenceExtractor.reassemblePolygons(geoms);
2021-06-02 11:54:21 +00:00
geom = GeoUtils.snapAndFixPolygon(geom, TILE_PRECISON);
2021-05-23 19:06:26 +00:00
// JTS utilities "fix" the geometry to be clockwise outer/CCW inner
geom = geom.reverse();
2021-05-20 09:59:18 +00:00
} else {
2021-05-22 11:07:34 +00:00
geom = CoordinateSequenceExtractor.reassembleLineStrings(geoms);
2021-05-20 09:59:18 +00:00
}
if (!geom.isEmpty()) {
emitFeature(feature, id, attrs, tile, geom, null);
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
}
}
if (feature.area()) {
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-05-19 10:44:28 +00:00
/*
* Optimization: large input polygons that generate many filled interior tiles (ie. the ocean), the encoder avoids
2021-05-20 09:59:18 +00:00
* re-encoding if groupInfo and vector tile feature are == to previous values.
2021-05-19 10:44:28 +00:00
*/
Optional<RenderedFeature.Group> groupInfo = Optional.empty();
2021-05-20 09:59:18 +00:00
VectorTileEncoder.Feature vectorTileFeature = new VectorTileEncoder.Feature(
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-05-19 10:44:28 +00:00
feature.getZorder(),
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
}
2021-04-10 09:25:42 +00:00
}