kopia lustrzana https://github.com/onthegomap/planetiler
first pass of line/are rendering
rodzic
7e06af0b1d
commit
2d4d6187bf
|
@ -1,5 +1,6 @@
|
||||||
package com.onthegomap.flatmap;
|
package com.onthegomap.flatmap;
|
||||||
|
|
||||||
|
import com.onthegomap.flatmap.collections.CacheByZoom;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -24,19 +25,19 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Feature point(String layer) {
|
public Feature point(String layer) {
|
||||||
var feature = new Feature(layer, source.isPoint() ? source.worldGeometry() : source.centroid());
|
var feature = new Feature(layer, source.isPoint() ? source.worldGeometry() : source.centroid(), false);
|
||||||
output.add(feature);
|
output.add(feature);
|
||||||
return feature;
|
return feature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Feature line(String layername) {
|
public Feature line(String layername) {
|
||||||
var feature = new Feature(layername, source.line());
|
var feature = new Feature(layername, source.line(), false);
|
||||||
output.add(feature);
|
output.add(feature);
|
||||||
return feature;
|
return feature;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Feature polygon(String layername) {
|
public Feature polygon(String layername) {
|
||||||
var feature = new Feature(layername, source.polygon());
|
var feature = new Feature(layername, source.polygon(), true);
|
||||||
output.add(feature);
|
output.add(feature);
|
||||||
return feature;
|
return feature;
|
||||||
}
|
}
|
||||||
|
@ -50,6 +51,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
|
|
||||||
public final class Feature {
|
public final class Feature {
|
||||||
|
|
||||||
|
private final boolean area;
|
||||||
private static final double DEFAULT_LABEL_GRID_SIZE = 0;
|
private static final double DEFAULT_LABEL_GRID_SIZE = 0;
|
||||||
private static final int DEFAULT_LABEL_GRID_LIMIT = 0;
|
private static final int DEFAULT_LABEL_GRID_LIMIT = 0;
|
||||||
private final String layer;
|
private final String layer;
|
||||||
|
@ -64,11 +66,14 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
private ZoomFunction<Number> minPixelSize = null;
|
private ZoomFunction<Number> minPixelSize = null;
|
||||||
private ZoomFunction<Number> labelGridPixelSize = null;
|
private ZoomFunction<Number> labelGridPixelSize = null;
|
||||||
private ZoomFunction<Number> labelGridLimit = null;
|
private ZoomFunction<Number> labelGridLimit = null;
|
||||||
|
private boolean attrsChangeByZoom = false;
|
||||||
|
private CacheByZoom<Map<String, Object>> attrCache = null;
|
||||||
|
|
||||||
private Feature(String layer, Geometry geom) {
|
private Feature(String layer, Geometry geom, boolean area) {
|
||||||
this.layer = layer;
|
this.layer = layer;
|
||||||
this.geom = geom;
|
this.geom = geom;
|
||||||
this.zOrder = 0;
|
this.zOrder = 0;
|
||||||
|
this.area = area;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getZorder() {
|
public int getZorder() {
|
||||||
|
@ -169,7 +174,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return labelGridPixelSize != null || labelGridLimit != null;
|
return labelGridPixelSize != null || labelGridLimit != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> getAttrsAtZoom(int zoom) {
|
private Map<String, Object> computeAttrsAtZoom(int zoom) {
|
||||||
Map<String, Object> result = new TreeMap<>();
|
Map<String, Object> result = new TreeMap<>();
|
||||||
for (var entry : attrs.entrySet()) {
|
for (var entry : attrs.entrySet()) {
|
||||||
Object value = entry.getValue();
|
Object value = entry.getValue();
|
||||||
|
@ -183,18 +188,34 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getAttrsAtZoom(int zoom) {
|
||||||
|
if (!attrsChangeByZoom) {
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
if (attrCache == null) {
|
||||||
|
attrCache = CacheByZoom.create(config, this::computeAttrsAtZoom);
|
||||||
|
}
|
||||||
|
return attrCache.get(zoom);
|
||||||
|
}
|
||||||
|
|
||||||
public Feature inheritFromSource(String attr) {
|
public Feature inheritFromSource(String attr) {
|
||||||
return setAttr(attr, source.getTag(attr));
|
return setAttr(attr, source.getTag(attr));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Feature setAttr(String key, Object value) {
|
public Feature setAttr(String key, Object value) {
|
||||||
|
if (value instanceof ZoomFunction) {
|
||||||
|
attrsChangeByZoom = true;
|
||||||
|
}
|
||||||
attrs.put(key, value);
|
attrs.put(key, value);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Feature setAttrWithMinzoom(String key, Object value, int minzoom) {
|
public Feature setAttrWithMinzoom(String key, Object value, int minzoom) {
|
||||||
attrs.put(key, ZoomFunction.minZoom(minzoom, value));
|
return setAttr(key, ZoomFunction.minZoom(minzoom, value));
|
||||||
return this;
|
}
|
||||||
|
|
||||||
|
public boolean area() {
|
||||||
|
return area;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.onthegomap.flatmap;
|
||||||
|
|
||||||
import com.onthegomap.flatmap.geo.GeoUtils;
|
import com.onthegomap.flatmap.geo.GeoUtils;
|
||||||
import com.onthegomap.flatmap.geo.TileCoord;
|
import com.onthegomap.flatmap.geo.TileCoord;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -10,19 +11,26 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
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.Coordinate;
|
||||||
import org.locationtech.jts.geom.CoordinateSequence;
|
import org.locationtech.jts.geom.CoordinateSequence;
|
||||||
|
import org.locationtech.jts.geom.CoordinateSequences;
|
||||||
import org.locationtech.jts.geom.CoordinateXY;
|
import org.locationtech.jts.geom.CoordinateXY;
|
||||||
import org.locationtech.jts.geom.Geometry;
|
import org.locationtech.jts.geom.Geometry;
|
||||||
import org.locationtech.jts.geom.GeometryCollection;
|
import org.locationtech.jts.geom.GeometryCollection;
|
||||||
import org.locationtech.jts.geom.LineString;
|
import org.locationtech.jts.geom.LineString;
|
||||||
|
import org.locationtech.jts.geom.LinearRing;
|
||||||
import org.locationtech.jts.geom.MultiLineString;
|
import org.locationtech.jts.geom.MultiLineString;
|
||||||
import org.locationtech.jts.geom.MultiPoint;
|
import org.locationtech.jts.geom.MultiPoint;
|
||||||
import org.locationtech.jts.geom.MultiPolygon;
|
import org.locationtech.jts.geom.MultiPolygon;
|
||||||
import org.locationtech.jts.geom.Point;
|
import org.locationtech.jts.geom.Point;
|
||||||
import org.locationtech.jts.geom.Polygon;
|
import org.locationtech.jts.geom.Polygon;
|
||||||
import org.locationtech.jts.geom.Polygonal;
|
import org.locationtech.jts.geom.Polygonal;
|
||||||
|
import org.locationtech.jts.geom.PrecisionModel;
|
||||||
|
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||||
import org.locationtech.jts.geom.util.AffineTransformation;
|
import org.locationtech.jts.geom.util.AffineTransformation;
|
||||||
|
import org.locationtech.jts.precision.GeometryPrecisionReducer;
|
||||||
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
|
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -32,6 +40,15 @@ public class FeatureRenderer {
|
||||||
private static final AtomicLong idGen = new AtomicLong(0);
|
private static final AtomicLong idGen = new AtomicLong(0);
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureRenderer.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureRenderer.class);
|
||||||
|
private static final PrecisionModel tilePrecision = new PrecisionModel(4096d / 256d);
|
||||||
|
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))));
|
||||||
private final CommonParams config;
|
private final CommonParams config;
|
||||||
private final Consumer<RenderedFeature> consumer;
|
private final Consumer<RenderedFeature> consumer;
|
||||||
|
|
||||||
|
@ -40,6 +57,22 @@ public class FeatureRenderer {
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int wrapInt(int value, int max) {
|
||||||
|
value %= max;
|
||||||
|
if (value < 0) {
|
||||||
|
value += max;
|
||||||
|
}
|
||||||
|
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) {
|
public void renderFeature(FeatureCollector.Feature feature) {
|
||||||
renderGeometry(feature.getGeometry(), feature);
|
renderGeometry(feature.getGeometry(), feature);
|
||||||
}
|
}
|
||||||
|
@ -65,23 +98,8 @@ public class FeatureRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int wrapInt(int value, int max) {
|
|
||||||
value %= max;
|
|
||||||
if (value < 0) {
|
|
||||||
value += max;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double wrapDouble(double value, double max) {
|
|
||||||
value %= max;
|
|
||||||
if (value < 0) {
|
|
||||||
value += max;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void slicePoint(Map<TileCoord, Set<Coordinate>> output, int zoom, double buffer, Coordinate coord) {
|
private void slicePoint(Map<TileCoord, Set<Coordinate>> output, int zoom, double buffer, Coordinate coord) {
|
||||||
|
// TODO put this into TiledGeometry
|
||||||
int tilesAtZoom = 1 << zoom;
|
int tilesAtZoom = 1 << zoom;
|
||||||
double worldX = coord.getX() * tilesAtZoom;
|
double worldX = coord.getX() * tilesAtZoom;
|
||||||
double worldY = coord.getY() * tilesAtZoom;
|
double worldY = coord.getY() * tilesAtZoom;
|
||||||
|
@ -108,37 +126,49 @@ public class FeatureRenderer {
|
||||||
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
|
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
|
||||||
int tilesAtZoom = 1 << zoom;
|
int tilesAtZoom = 1 << zoom;
|
||||||
for (Coordinate coord : coords) {
|
for (Coordinate coord : coords) {
|
||||||
|
// TODO TiledGeometry.sliceIntoTiles(...)
|
||||||
slicePoint(sliced, zoom, buffer, coord);
|
slicePoint(sliced, zoom, buffer, coord);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<RenderedFeature.Group> groupInfo = Optional.empty();
|
RenderedFeature.Group groupInfo = null;
|
||||||
if (feature.hasLabelGrid() && coords.length == 1) {
|
if (feature.hasLabelGrid() && coords.length == 1) {
|
||||||
double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d;
|
double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d;
|
||||||
groupInfo = labelGridTileSize >= 1d / 4096d ?
|
groupInfo = labelGridTileSize >= 1d / 4096d ? new RenderedFeature.Group(GeoUtils.longPair(
|
||||||
Optional.of(new RenderedFeature.Group(GeoUtils.longPair(
|
(int) Math.floor(wrapDouble(coords[0].getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize),
|
||||||
(int) Math.floor(wrapDouble(coords[0].getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize),
|
(int) Math.floor((coords[0].getY() * tilesAtZoom) / labelGridTileSize)
|
||||||
(int) Math.floor((coords[0].getY() * tilesAtZoom) / labelGridTileSize)
|
), feature.getLabelGridLimitAtZoom(zoom)) : null;
|
||||||
), feature.getLabelGridLimitAtZoom(zoom))) : Optional.empty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var entry : sliced.entrySet()) {
|
for (var entry : sliced.entrySet()) {
|
||||||
|
TileCoord tile = entry.getKey();
|
||||||
Set<Coordinate> value = entry.getValue();
|
Set<Coordinate> value = entry.getValue();
|
||||||
Geometry geom = value.size() == 1 ? GeoUtils.point(value.iterator().next()) : GeoUtils.multiPoint(value);
|
Geometry geom = value.size() == 1 ? GeoUtils.point(value.iterator().next()) : GeoUtils.multiPoint(value);
|
||||||
consumer.accept(new RenderedFeature(
|
// TODO stats
|
||||||
entry.getKey(),
|
// TODO writeTileFeatures
|
||||||
new VectorTileEncoder.Feature(
|
emitFeature(feature, id, attrs, groupInfo, tile, geom);
|
||||||
feature.getLayer(),
|
|
||||||
id,
|
|
||||||
VectorTileEncoder.encodeGeometry(geom),
|
|
||||||
attrs
|
|
||||||
),
|
|
||||||
feature.getZorder(),
|
|
||||||
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<String, Object> attrs,
|
||||||
|
RenderedFeature.Group groupInfo, TileCoord tile, Geometry geom) {
|
||||||
|
consumer.accept(new RenderedFeature(
|
||||||
|
tile,
|
||||||
|
new VectorTileEncoder.Feature(
|
||||||
|
feature.getLayer(),
|
||||||
|
id,
|
||||||
|
VectorTileEncoder.encodeGeometry(geom),
|
||||||
|
attrs
|
||||||
|
),
|
||||||
|
feature.getZorder(),
|
||||||
|
Optional.ofNullable(groupInfo)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
private void addPointFeature(FeatureCollector.Feature feature, MultiPoint points) {
|
private void addPointFeature(FeatureCollector.Feature feature, MultiPoint points) {
|
||||||
if (feature.hasLabelGrid()) {
|
if (feature.hasLabelGrid()) {
|
||||||
for (Coordinate coord : points.getCoordinates()) {
|
for (Coordinate coord : points.getCoordinates()) {
|
||||||
|
@ -150,6 +180,7 @@ public class FeatureRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addLinearFeature(FeatureCollector.Feature feature, Geometry input) {
|
private void addLinearFeature(FeatureCollector.Feature feature, Geometry input) {
|
||||||
|
long id = idGen.incrementAndGet();
|
||||||
// TODO move to feature?
|
// TODO move to feature?
|
||||||
double minSizeAtMaxZoom = 1d / 4096;
|
double minSizeAtMaxZoom = 1d / 4096;
|
||||||
double normalTolerance = 0.1 / 256;
|
double normalTolerance = 0.1 / 256;
|
||||||
|
@ -175,27 +206,155 @@ public class FeatureRenderer {
|
||||||
simplifier.setDistanceTolerance(tolerance);
|
simplifier.setDistanceTolerance(tolerance);
|
||||||
geom = simplifier.getResultGeometry();
|
geom = simplifier.getResultGeometry();
|
||||||
|
|
||||||
List<List<CoordinateSequence>> groups = extractGroups(geom);
|
List<List<CoordinateSequence>> groups = new ArrayList<>();
|
||||||
|
extractGroups(geom, groups, minSize);
|
||||||
double buffer = feature.getBufferPixelsAtZoom(z);
|
double buffer = feature.getBufferPixelsAtZoom(z);
|
||||||
TileExtents.ForZoom extents = config.extents().getForZoom(z);
|
TileExtents.ForZoom extents = config.extents().getForZoom(z);
|
||||||
TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
||||||
writeTileFeatures(feature, sliced);
|
writeTileFeatures(id, feature, sliced);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeTileFeatures(FeatureCollector.Feature feature, TiledGeometry sliced) {
|
private void writeTileFeatures(long id, FeatureCollector.Feature feature, TiledGeometry sliced) {
|
||||||
build polygons, enforce correctness
|
for (var entry : sliced.getTileData()) {
|
||||||
build linestrings
|
TileCoord tile = entry.getKey();
|
||||||
reduce precision
|
List<List<CoordinateSequence>> geoms = entry.getValue();
|
||||||
handle errors
|
|
||||||
fix orientation
|
Geometry geom;
|
||||||
write filled tiles
|
if (feature.area()) {
|
||||||
log stats
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!geom.isEmpty()) {
|
||||||
|
// JTS utilities "fix" the geometry to be clockwise outer/CCW inner
|
||||||
|
if (feature.area()) {
|
||||||
|
geom = geom.reverse();
|
||||||
|
}
|
||||||
|
emitFeature(feature, id, tile, geom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature.area()) {
|
||||||
|
emitFilledTiles(id, feature, sliced);
|
||||||
|
}
|
||||||
|
// TODO log stats
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<List<CoordinateSequence>> extractGroups(Geometry geom) {
|
private void emitFilledTiles(long id, FeatureCollector.Feature feature, TiledGeometry sliced) {
|
||||||
limit length
|
/*
|
||||||
limit area
|
* Optimization: large input polygons that generate many filled interior tiles (ie. the ocean), the encoder avoids
|
||||||
enforce orientation
|
* 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.
|
||||||
|
*/
|
||||||
|
Optional<RenderedFeature.Group> groupInfo = Optional.empty();
|
||||||
|
VectorTileEncoder.Feature cachedFeature = null;
|
||||||
|
int lastZoom = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
consumer.accept(new RenderedFeature(
|
||||||
|
tile,
|
||||||
|
cachedFeature,
|
||||||
|
feature.getZorder(),
|
||||||
|
groupInfo
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Geometry reassembleLineString(List<List<CoordinateSequence>> geoms) {
|
||||||
|
Geometry geom;
|
||||||
|
List<LineString> lineStrings = new ArrayList<>();
|
||||||
|
for (List<CoordinateSequence> 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<List<CoordinateSequence>> geoms) {
|
||||||
|
Geometry geom;
|
||||||
|
int numGeoms = geoms.size();
|
||||||
|
Polygon[] polygons = new Polygon[numGeoms];
|
||||||
|
for (int i = 0; i < numGeoms; i++) {
|
||||||
|
List<CoordinateSequence> 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<List<CoordinateSequence>> 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<List<CoordinateSequence>> 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<CoordinateSequence> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,10 @@ import java.util.EnumSet;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.SortedMap;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.locationtech.jts.geom.CoordinateSequence;
|
import org.locationtech.jts.geom.CoordinateSequence;
|
||||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -41,7 +44,7 @@ public class TiledGeometry {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class);
|
||||||
|
|
||||||
private final Map<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
|
private final Map<TileCoord, List<List<CoordinateSequence>>> tileContents = new HashMap<>();
|
||||||
private final Map<Column, IntRange> filledRanges = new HashMap<>();
|
private final SortedMap<Column, IntRange> filledRanges = new TreeMap<>();
|
||||||
private final TileExtents.ForZoom extents;
|
private final TileExtents.ForZoom extents;
|
||||||
private final double buffer;
|
private final double buffer;
|
||||||
private final int z;
|
private final int z;
|
||||||
|
@ -58,18 +61,19 @@ public class TiledGeometry {
|
||||||
|
|
||||||
public static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
|
public static TiledGeometry sliceIntoTiles(List<List<CoordinateSequence>> groups, double buffer, boolean area, int z,
|
||||||
TileExtents.ForZoom extents) {
|
TileExtents.ForZoom extents) {
|
||||||
|
int worldExtent = 1 << z;
|
||||||
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
|
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
|
||||||
EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
|
EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
|
||||||
if (wrapResult.contains(Direction.RIGHT)) {
|
if (wrapResult.contains(Direction.RIGHT)) {
|
||||||
result.sliceWorldCopy(groups, 1);
|
result.sliceWorldCopy(groups, -worldExtent);
|
||||||
} else if (wrapResult.contains(Direction.LEFT)) {
|
}
|
||||||
result.sliceWorldCopy(groups, -1);
|
if (wrapResult.contains(Direction.LEFT)) {
|
||||||
|
result.sliceWorldCopy(groups, worldExtent);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Iterable<TileCoord> getFilledTiles() {
|
public Iterable<TileCoord> getFilledTilesOrderedByZXY() {
|
||||||
return () -> filledRanges.entrySet().stream()
|
return () -> filledRanges.entrySet().stream()
|
||||||
.<TileCoord>mapMulti((entry, next) -> {
|
.<TileCoord>mapMulti((entry, next) -> {
|
||||||
Column column = entry.getKey();
|
Column column = entry.getKey();
|
||||||
|
@ -125,7 +129,7 @@ public class TiledGeometry {
|
||||||
for (int i = 0; i < group.size(); i++) {
|
for (int i = 0; i < group.size(); i++) {
|
||||||
CoordinateSequence segment = group.get(i);
|
CoordinateSequence segment = group.get(i);
|
||||||
boolean outer = i == 0;
|
boolean outer = i == 0;
|
||||||
IntObjectMap<List<MutableCoordinateSequence>> xSlices = sliceX(segment, outer);
|
IntObjectMap<List<MutableCoordinateSequence>> xSlices = sliceX(segment);
|
||||||
if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) {
|
if (z >= 6 && xSlices.size() >= Math.pow(2, z) - 1) {
|
||||||
LOGGER.warn("Feature crosses world at z" + z + ": " + xSlices.size());
|
LOGGER.warn("Feature crosses world at z" + z + ": " + xSlices.size());
|
||||||
}
|
}
|
||||||
|
@ -173,7 +177,7 @@ public class TiledGeometry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntObjectMap<List<MutableCoordinateSequence>> sliceX(CoordinateSequence segment, boolean outer) {
|
private IntObjectMap<List<MutableCoordinateSequence>> sliceX(CoordinateSequence segment) {
|
||||||
int maxIndex = 1 << z;
|
int maxIndex = 1 << z;
|
||||||
double k1 = -buffer;
|
double k1 = -buffer;
|
||||||
double k2 = 1 + buffer;
|
double k2 = 1 + buffer;
|
||||||
|
@ -197,7 +201,7 @@ public class TiledGeometry {
|
||||||
double bx = _bx - x;
|
double bx = _bx - x;
|
||||||
MutableCoordinateSequence slice = xSlices.get(x);
|
MutableCoordinateSequence slice = xSlices.get(x);
|
||||||
if (slice == null) {
|
if (slice == null) {
|
||||||
xSlices.put(x, slice = new MutableCoordinateSequence(outer));
|
xSlices.put(x, slice = new MutableCoordinateSequence());
|
||||||
List<MutableCoordinateSequence> newGeom = newGeoms.get(x);
|
List<MutableCoordinateSequence> newGeom = newGeoms.get(x);
|
||||||
if (newGeom == null) {
|
if (newGeom == null) {
|
||||||
newGeoms.put(x, newGeom = new ArrayList<>());
|
newGeoms.put(x, newGeom = new ArrayList<>());
|
||||||
|
@ -339,7 +343,7 @@ public class TiledGeometry {
|
||||||
tiles.add(y);
|
tiles.add(y);
|
||||||
}
|
}
|
||||||
// x is already relative to tile
|
// x is already relative to tile
|
||||||
ySlices.put(y, slice = MutableCoordinateSequence.newScalingSequence(outer, 0, y, 256));
|
ySlices.put(y, slice = MutableCoordinateSequence.newScalingSequence(0, y, 256));
|
||||||
TileCoord tileID = TileCoord.ofXYZ(x, y, z);
|
TileCoord tileID = TileCoord.ofXYZ(x, y, z);
|
||||||
List<CoordinateSequence> toAddTo = inProgressShapes.computeIfAbsent(tileID, tile -> new ArrayList<>());
|
List<CoordinateSequence> toAddTo = inProgressShapes.computeIfAbsent(tileID, tile -> new ArrayList<>());
|
||||||
|
|
||||||
|
@ -450,7 +454,12 @@ public class TiledGeometry {
|
||||||
|
|
||||||
private enum Direction {RIGHT, LEFT}
|
private enum Direction {RIGHT, LEFT}
|
||||||
|
|
||||||
private static record Column(int z, int x) {
|
private static record Column(int z, int x) implements Comparable<Column> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NotNull Column o) {
|
||||||
|
int result = Integer.compare(z, o.z);
|
||||||
|
return result == 0 ? Integer.compare(x, o.x) : result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ public class VectorTileEncoder {
|
||||||
|
|
||||||
private static Geometry decodeCommands(byte geomTypeByte, int[] commands) {
|
private static Geometry decodeCommands(byte geomTypeByte, int[] commands) {
|
||||||
VectorTile.Tile.GeomType geomType = Objects.requireNonNull(VectorTile.Tile.GeomType.forNumber(geomTypeByte));
|
VectorTile.Tile.GeomType geomType = Objects.requireNonNull(VectorTile.Tile.GeomType.forNumber(geomTypeByte));
|
||||||
GeometryFactory gf = GeoUtils.gf;
|
GeometryFactory gf = GeoUtils.JTS_FACTORY;
|
||||||
int x = 0;
|
int x = 0;
|
||||||
int y = 0;
|
int y = 0;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.onthegomap.flatmap.collections;
|
||||||
|
|
||||||
|
import com.onthegomap.flatmap.CommonParams;
|
||||||
|
import java.util.function.IntFunction;
|
||||||
|
|
||||||
|
public class CacheByZoom<T> {
|
||||||
|
|
||||||
|
private final int minzoom;
|
||||||
|
private final Object[] values;
|
||||||
|
private final IntFunction<T> supplier;
|
||||||
|
|
||||||
|
private CacheByZoom(int minzoom, int maxzoom, IntFunction<T> supplier) {
|
||||||
|
this.minzoom = minzoom;
|
||||||
|
values = new Object[maxzoom + 1 - minzoom];
|
||||||
|
this.supplier = supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> CacheByZoom<T> create(CommonParams params, IntFunction<T> supplier) {
|
||||||
|
return new CacheByZoom<>(params.minzoom(), params.maxzoom(), supplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T get(int zoom) {
|
||||||
|
@SuppressWarnings("unchecked") T[] casted = (T[]) values;
|
||||||
|
int off = zoom - minzoom;
|
||||||
|
if (values[off] != null) {
|
||||||
|
return casted[off];
|
||||||
|
}
|
||||||
|
return casted[off] = supplier.apply(zoom);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import org.msgpack.core.MessageBufferPacker;
|
import org.msgpack.core.MessageBufferPacker;
|
||||||
|
@ -93,18 +94,38 @@ public final class FeatureGroup implements Consumer<FeatureSort.Entry>, Iterable
|
||||||
}
|
}
|
||||||
|
|
||||||
public Function<RenderedFeature, FeatureSort.Entry> newRenderedFeatureEncoder() {
|
public Function<RenderedFeature, FeatureSort.Entry> newRenderedFeatureEncoder() {
|
||||||
|
/*
|
||||||
|
* Optimization: Re-use the same buffer packer to avoid allocating and resizing new byte arrays for every feature.
|
||||||
|
*/
|
||||||
var packer = MessagePack.newDefaultBufferPacker();
|
var packer = MessagePack.newDefaultBufferPacker();
|
||||||
return feature -> {
|
|
||||||
layerStats.accept(feature);
|
|
||||||
return encode(feature, packer);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private FeatureSort.Entry encode(RenderedFeature feature, MessageBufferPacker packer) {
|
/*
|
||||||
return new FeatureSort.Entry(
|
* Optimization: Avoid re-encoding values for identical fill geometries (ie. in the ocean) by memoizing based on
|
||||||
encodeSortKey(feature),
|
* the input vector tile feature. FeatureRenderer ensures that all fill vector tile features use the same instance
|
||||||
encodeValue(feature, packer)
|
* within a zoom level (and filled tiles are ordered by z, x, y).
|
||||||
);
|
*/
|
||||||
|
return new Function<>() {
|
||||||
|
private VectorTileEncoder.Feature lastFeature = null;
|
||||||
|
private byte[] lastEncodedValue = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FeatureSort.Entry apply(RenderedFeature feature) {
|
||||||
|
layerStats.accept(feature);
|
||||||
|
var group = feature.group();
|
||||||
|
var thisFeature = feature.vectorTileFeature();
|
||||||
|
byte[] encodedValue;
|
||||||
|
if (group.isEmpty()) { // don't bother memoizing if group is present
|
||||||
|
encodedValue = encodeValue(thisFeature, group, packer);
|
||||||
|
} else if (lastFeature == thisFeature) {
|
||||||
|
encodedValue = lastEncodedValue;
|
||||||
|
} else { // feature changed, memoize new value
|
||||||
|
lastFeature = thisFeature;
|
||||||
|
lastEncodedValue = encodedValue = encodeValue(feature.vectorTileFeature(), feature.group(), packer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FeatureSort.Entry(encodeSortKey(feature), encodedValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private long encodeSortKey(RenderedFeature feature) {
|
private long encodeSortKey(RenderedFeature feature) {
|
||||||
|
@ -118,16 +139,16 @@ public final class FeatureGroup implements Consumer<FeatureSort.Entry>, Iterable
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] encodeValue(RenderedFeature feature, MessageBufferPacker packer) {
|
private byte[] encodeValue(VectorTileEncoder.Feature vectorTileFeature, Optional<RenderedFeature.Group> group,
|
||||||
|
MessageBufferPacker packer) {
|
||||||
packer.clear();
|
packer.clear();
|
||||||
try {
|
try {
|
||||||
var groupInfoOption = feature.group();
|
var groupInfoOption = group;
|
||||||
if (groupInfoOption.isPresent()) {
|
if (groupInfoOption.isPresent()) {
|
||||||
var groupInfo = groupInfoOption.get();
|
var groupInfo = groupInfoOption.get();
|
||||||
packer.packLong(groupInfo.group());
|
packer.packLong(groupInfo.group());
|
||||||
packer.packInt(groupInfo.limit());
|
packer.packInt(groupInfo.limit());
|
||||||
}
|
}
|
||||||
var vectorTileFeature = feature.vectorTileFeature();
|
|
||||||
packer.packLong(vectorTileFeature.id());
|
packer.packLong(vectorTileFeature.id());
|
||||||
packer.packByte(vectorTileFeature.geometry().geomType());
|
packer.packByte(vectorTileFeature.geometry().geomType());
|
||||||
var attrs = vectorTileFeature.attrs();
|
var attrs = vectorTileFeature.attrs();
|
||||||
|
|
|
@ -9,22 +9,15 @@ import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||||
public class MutableCoordinateSequence extends PackedCoordinateSequence {
|
public class MutableCoordinateSequence extends PackedCoordinateSequence {
|
||||||
|
|
||||||
private final DoubleArrayList points = new DoubleArrayList();
|
private final DoubleArrayList points = new DoubleArrayList();
|
||||||
private final boolean inner;
|
|
||||||
|
|
||||||
public MutableCoordinateSequence(boolean outer) {
|
public MutableCoordinateSequence() {
|
||||||
super(2, 0);
|
super(2, 0);
|
||||||
this.inner = !outer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MutableCoordinateSequence newScalingSequence(boolean outer, double relX, double relY, double scale) {
|
public static MutableCoordinateSequence newScalingSequence(double relX, double relY, double scale) {
|
||||||
return new ScalingSequence(outer, scale, relX, relY);
|
return new ScalingSequence(scale, relX, relY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInnerRing() {
|
|
||||||
return inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public double getOrdinate(int index, int ordinateIndex) {
|
public double getOrdinate(int index, int ordinateIndex) {
|
||||||
return points.get((index * 2) + ordinateIndex);
|
return points.get((index * 2) + ordinateIndex);
|
||||||
|
@ -87,8 +80,7 @@ public class MutableCoordinateSequence extends PackedCoordinateSequence {
|
||||||
private final double relX;
|
private final double relX;
|
||||||
private final double relY;
|
private final double relY;
|
||||||
|
|
||||||
public ScalingSequence(boolean outer, double scale, double relX, double relY) {
|
public ScalingSequence(double scale, double relX, double relY) {
|
||||||
super(outer);
|
|
||||||
this.scale = scale;
|
this.scale = scale;
|
||||||
this.relX = relX;
|
this.relX = relX;
|
||||||
this.relY = relY;
|
this.relY = relY;
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package com.onthegomap.flatmap.geo;
|
package com.onthegomap.flatmap.geo;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import org.locationtech.jts.geom.Coordinate;
|
import org.locationtech.jts.geom.Coordinate;
|
||||||
import org.locationtech.jts.geom.CoordinateSequence;
|
import org.locationtech.jts.geom.CoordinateSequence;
|
||||||
import org.locationtech.jts.geom.CoordinateXY;
|
import org.locationtech.jts.geom.CoordinateXY;
|
||||||
import org.locationtech.jts.geom.Envelope;
|
import org.locationtech.jts.geom.Envelope;
|
||||||
import org.locationtech.jts.geom.Geometry;
|
import org.locationtech.jts.geom.Geometry;
|
||||||
import org.locationtech.jts.geom.GeometryFactory;
|
import org.locationtech.jts.geom.GeometryFactory;
|
||||||
|
import org.locationtech.jts.geom.LineString;
|
||||||
import org.locationtech.jts.geom.MultiPoint;
|
import org.locationtech.jts.geom.MultiPoint;
|
||||||
import org.locationtech.jts.geom.Point;
|
import org.locationtech.jts.geom.Point;
|
||||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||||
|
@ -15,8 +17,11 @@ import org.locationtech.jts.io.WKBReader;
|
||||||
|
|
||||||
public class GeoUtils {
|
public class GeoUtils {
|
||||||
|
|
||||||
public static final GeometryFactory gf = new GeometryFactory();
|
public static final GeometryFactory JTS_FACTORY = new GeometryFactory();
|
||||||
public static final WKBReader wkbReader = new WKBReader(gf);
|
public static final WKBReader wkbReader = new WKBReader(JTS_FACTORY);
|
||||||
|
|
||||||
|
private static final LineString[] EMPTY_LINE_STRING_ARRAY = new LineString[0];
|
||||||
|
private static final Coordinate[] EMPTY_COORD_ARRAY = new Coordinate[0];
|
||||||
|
|
||||||
private static final double WORLD_RADIUS_METERS = 6_378_137;
|
private static final double WORLD_RADIUS_METERS = 6_378_137;
|
||||||
private static final double WORLD_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS;
|
private static final double WORLD_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS;
|
||||||
|
@ -174,19 +179,23 @@ public class GeoUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Point point(double x, double y) {
|
public static Point point(double x, double y) {
|
||||||
return gf.createPoint(new CoordinateXY(x, y));
|
return JTS_FACTORY.createPoint(new CoordinateXY(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Point point(Coordinate coord) {
|
public static Point point(Coordinate coord) {
|
||||||
return gf.createPoint(coord);
|
return JTS_FACTORY.createPoint(coord);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MultiPoint multiPoint(Collection<Coordinate> coords) {
|
public static MultiPoint multiPoint(Collection<Coordinate> coords) {
|
||||||
return gf.createMultiPointFromCoords(coords.toArray(new Coordinate[0]));
|
return JTS_FACTORY.createMultiPointFromCoords(coords.toArray(EMPTY_COORD_ARRAY));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Geometry multiPoint(double... coords) {
|
public static Geometry multiPoint(double... coords) {
|
||||||
assert coords.length % 2 == 0;
|
assert coords.length % 2 == 0;
|
||||||
return gf.createMultiPoint(new PackedCoordinateSequence.Double(coords, 2, 0));
|
return JTS_FACTORY.createMultiPoint(new PackedCoordinateSequence.Double(coords, 2, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Geometry createMultiLineString(List<LineString> lineStrings) {
|
||||||
|
return JTS_FACTORY.createMultiLineString(lineStrings.toArray(EMPTY_LINE_STRING_ARRAY));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,7 @@ public abstract class Reader implements Closeable {
|
||||||
var featureCollectors = new FeatureCollector.Factory(config);
|
var featureCollectors = new FeatureCollector.Factory(config);
|
||||||
var encoder = writer.newRenderedFeatureEncoder();
|
var encoder = writer.newRenderedFeatureEncoder();
|
||||||
FeatureRenderer renderer = new FeatureRenderer(
|
FeatureRenderer renderer = new FeatureRenderer(
|
||||||
config,
|
config, encoder, next
|
||||||
rendered -> next.accept(encoder.apply(rendered))
|
|
||||||
);
|
);
|
||||||
while ((sourceFeature = prev.get()) != null) {
|
while ((sourceFeature = prev.get()) != null) {
|
||||||
featuresRead.incrementAndGet();
|
featuresRead.incrementAndGet();
|
||||||
|
|
|
@ -50,27 +50,27 @@ public class TestUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Polygon newPolygon(double... coords) {
|
public static Polygon newPolygon(double... coords) {
|
||||||
return GeoUtils.gf.createPolygon(newCoordinateList(coords).toArray(new Coordinate[0]));
|
return GeoUtils.JTS_FACTORY.createPolygon(newCoordinateList(coords).toArray(new Coordinate[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LineString newLineString(double... coords) {
|
public static LineString newLineString(double... coords) {
|
||||||
return GeoUtils.gf.createLineString(newCoordinateList(coords).toArray(new Coordinate[0]));
|
return GeoUtils.JTS_FACTORY.createLineString(newCoordinateList(coords).toArray(new Coordinate[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Point newPoint(double x, double y) {
|
public static Point newPoint(double x, double y) {
|
||||||
return GeoUtils.gf.createPoint(new CoordinateXY(x, y));
|
return GeoUtils.JTS_FACTORY.createPoint(new CoordinateXY(x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MultiPoint newMultiPoint(Point... points) {
|
public static MultiPoint newMultiPoint(Point... points) {
|
||||||
return GeoUtils.gf.createMultiPoint(points);
|
return GeoUtils.JTS_FACTORY.createMultiPoint(points);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MultiPolygon newMultiPolygon(Polygon... polys) {
|
public static MultiPolygon newMultiPolygon(Polygon... polys) {
|
||||||
return GeoUtils.gf.createMultiPolygon(polys);
|
return GeoUtils.JTS_FACTORY.createMultiPolygon(polys);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GeometryCollection newGeometryCollection(Geometry... geoms) {
|
public static GeometryCollection newGeometryCollection(Geometry... geoms) {
|
||||||
return GeoUtils.gf.createGeometryCollection(geoms);
|
return GeoUtils.JTS_FACTORY.createGeometryCollection(geoms);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Geometry round(Geometry input) {
|
public static Geometry round(Geometry input) {
|
||||||
|
@ -153,7 +153,7 @@ public class TestUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Geometry emptyGeometry() {
|
public static Geometry emptyGeometry() {
|
||||||
return GeoUtils.gf.createGeometryCollection();
|
return GeoUtils.JTS_FACTORY.createGeometryCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface GeometryComparision {
|
public interface GeometryComparision {
|
||||||
|
|
|
@ -24,7 +24,7 @@ import static com.onthegomap.flatmap.TestUtils.newMultiPoint;
|
||||||
import static com.onthegomap.flatmap.TestUtils.newMultiPolygon;
|
import static com.onthegomap.flatmap.TestUtils.newMultiPolygon;
|
||||||
import static com.onthegomap.flatmap.TestUtils.newPoint;
|
import static com.onthegomap.flatmap.TestUtils.newPoint;
|
||||||
import static com.onthegomap.flatmap.TestUtils.newPolygon;
|
import static com.onthegomap.flatmap.TestUtils.newPolygon;
|
||||||
import static com.onthegomap.flatmap.geo.GeoUtils.gf;
|
import static com.onthegomap.flatmap.geo.GeoUtils.JTS_FACTORY;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
@ -57,7 +57,7 @@ public class VectorTileEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testToGeomType() {
|
public void testToGeomType() {
|
||||||
Geometry geometry = gf.createLineString(new Coordinate[]{new CoordinateXY(1, 2), new CoordinateXY(3, 4)});
|
Geometry geometry = JTS_FACTORY.createLineString(new Coordinate[]{new CoordinateXY(1, 2), new CoordinateXY(3, 4)});
|
||||||
assertEquals((byte) VectorTile.Tile.GeomType.LINESTRING.getNumber(),
|
assertEquals((byte) VectorTile.Tile.GeomType.LINESTRING.getNumber(),
|
||||||
VectorTileEncoder.encodeGeometry(geometry).geomType());
|
VectorTileEncoder.encodeGeometry(geometry).geomType());
|
||||||
}
|
}
|
||||||
|
@ -234,12 +234,12 @@ public class VectorTileEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRoundTripPoint() {
|
public void testRoundTripPoint() {
|
||||||
testRoundTripGeometry(gf.createPoint(new CoordinateXY(1, 2)));
|
testRoundTripGeometry(JTS_FACTORY.createPoint(new CoordinateXY(1, 2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRoundTripMultipoint() {
|
public void testRoundTripMultipoint() {
|
||||||
testRoundTripGeometry(gf.createMultiPointFromCoords(new Coordinate[]{
|
testRoundTripGeometry(JTS_FACTORY.createMultiPointFromCoords(new Coordinate[]{
|
||||||
new CoordinateXY(1, 2),
|
new CoordinateXY(1, 2),
|
||||||
new CoordinateXY(3, 4)
|
new CoordinateXY(3, 4)
|
||||||
}));
|
}));
|
||||||
|
@ -247,7 +247,7 @@ public class VectorTileEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRoundTripLineString() {
|
public void testRoundTripLineString() {
|
||||||
testRoundTripGeometry(gf.createLineString(new Coordinate[]{
|
testRoundTripGeometry(JTS_FACTORY.createLineString(new Coordinate[]{
|
||||||
new CoordinateXY(1, 2),
|
new CoordinateXY(1, 2),
|
||||||
new CoordinateXY(3, 4)
|
new CoordinateXY(3, 4)
|
||||||
}));
|
}));
|
||||||
|
@ -255,8 +255,8 @@ public class VectorTileEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRoundTripPolygon() {
|
public void testRoundTripPolygon() {
|
||||||
testRoundTripGeometry(gf.createPolygon(
|
testRoundTripGeometry(JTS_FACTORY.createPolygon(
|
||||||
gf.createLinearRing(new Coordinate[]{
|
JTS_FACTORY.createLinearRing(new Coordinate[]{
|
||||||
new CoordinateXY(0, 0),
|
new CoordinateXY(0, 0),
|
||||||
new CoordinateXY(4, 0),
|
new CoordinateXY(4, 0),
|
||||||
new CoordinateXY(4, 4),
|
new CoordinateXY(4, 4),
|
||||||
|
@ -264,7 +264,7 @@ public class VectorTileEncoderTest {
|
||||||
new CoordinateXY(0, 0)
|
new CoordinateXY(0, 0)
|
||||||
}),
|
}),
|
||||||
new LinearRing[]{
|
new LinearRing[]{
|
||||||
gf.createLinearRing(new Coordinate[]{
|
JTS_FACTORY.createLinearRing(new Coordinate[]{
|
||||||
new CoordinateXY(1, 1),
|
new CoordinateXY(1, 1),
|
||||||
new CoordinateXY(1, 2),
|
new CoordinateXY(1, 2),
|
||||||
new CoordinateXY(2, 2),
|
new CoordinateXY(2, 2),
|
||||||
|
@ -277,15 +277,15 @@ public class VectorTileEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRoundTripMultiPolygon() {
|
public void testRoundTripMultiPolygon() {
|
||||||
testRoundTripGeometry(gf.createMultiPolygon(new Polygon[]{
|
testRoundTripGeometry(JTS_FACTORY.createMultiPolygon(new Polygon[]{
|
||||||
gf.createPolygon(new Coordinate[]{
|
JTS_FACTORY.createPolygon(new Coordinate[]{
|
||||||
new CoordinateXY(0, 0),
|
new CoordinateXY(0, 0),
|
||||||
new CoordinateXY(1, 0),
|
new CoordinateXY(1, 0),
|
||||||
new CoordinateXY(1, 1),
|
new CoordinateXY(1, 1),
|
||||||
new CoordinateXY(0, 1),
|
new CoordinateXY(0, 1),
|
||||||
new CoordinateXY(0, 0)
|
new CoordinateXY(0, 0)
|
||||||
}),
|
}),
|
||||||
gf.createPolygon(new Coordinate[]{
|
JTS_FACTORY.createPolygon(new Coordinate[]{
|
||||||
new CoordinateXY(3, 0),
|
new CoordinateXY(3, 0),
|
||||||
new CoordinateXY(4, 0),
|
new CoordinateXY(4, 0),
|
||||||
new CoordinateXY(4, 1),
|
new CoordinateXY(4, 1),
|
||||||
|
@ -308,7 +308,7 @@ public class VectorTileEncoderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMultipleFeaturesMultipleLayer() {
|
public void testMultipleFeaturesMultipleLayer() {
|
||||||
Point point = gf.createPoint(new CoordinateXY(0, 0));
|
Point point = JTS_FACTORY.createPoint(new CoordinateXY(0, 0));
|
||||||
Map<String, Object> attrs1 = Map.of("a", 1L, "b", 2L);
|
Map<String, Object> attrs1 = Map.of("a", 1L, "b", 2L);
|
||||||
Map<String, Object> attrs2 = Map.of("b", 3L, "c", 2L);
|
Map<String, Object> attrs2 = Map.of("b", 3L, "c", 2L);
|
||||||
byte[] encoded = new VectorTileEncoder().addLayerFeatures("layer1", List.of(
|
byte[] encoded = new VectorTileEncoder().addLayerFeatures("layer1", List.of(
|
||||||
|
@ -330,7 +330,7 @@ public class VectorTileEncoderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testRoundTripAttrs(Map<String, Object> attrs) {
|
private void testRoundTripAttrs(Map<String, Object> attrs) {
|
||||||
testRoundTrip(gf.createPoint(new CoordinateXY(0, 0)), "layer", attrs, 1);
|
testRoundTrip(JTS_FACTORY.createPoint(new CoordinateXY(0, 0)), "layer", attrs, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testRoundTripGeometry(Geometry input) {
|
private void testRoundTripGeometry(Geometry input) {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package com.onthegomap.flatmap.collections;
|
package com.onthegomap.flatmap.collections;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import com.carrotsearch.hppc.DoubleArrayList;
|
import com.carrotsearch.hppc.DoubleArrayList;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -25,28 +23,22 @@ public class MutableCoordinateSequenceTest {
|
||||||
assertEquals(DoubleArrayList.from(expected), DoubleArrayList.from(actual), "copied getX/getY");
|
assertEquals(DoubleArrayList.from(expected), DoubleArrayList.from(actual), "copied getX/getY");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testOuter() {
|
|
||||||
assertTrue(new MutableCoordinateSequence(false).isInnerRing());
|
|
||||||
assertFalse(new MutableCoordinateSequence(true).isInnerRing());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEmpty() {
|
public void testEmpty() {
|
||||||
var seq = new MutableCoordinateSequence(false);
|
var seq = new MutableCoordinateSequence();
|
||||||
assertEquals(0, seq.copy().size());
|
assertEquals(0, seq.copy().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSingle() {
|
public void testSingle() {
|
||||||
var seq = new MutableCoordinateSequence(false);
|
var seq = new MutableCoordinateSequence();
|
||||||
seq.addPoint(1, 2);
|
seq.addPoint(1, 2);
|
||||||
assertContents(seq, 1, 2);
|
assertContents(seq, 1, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTwoPoints() {
|
public void testTwoPoints() {
|
||||||
var seq = new MutableCoordinateSequence(false);
|
var seq = new MutableCoordinateSequence();
|
||||||
seq.addPoint(1, 2);
|
seq.addPoint(1, 2);
|
||||||
seq.addPoint(3, 4);
|
seq.addPoint(3, 4);
|
||||||
assertContents(seq, 1, 2, 3, 4);
|
assertContents(seq, 1, 2, 3, 4);
|
||||||
|
@ -54,7 +46,7 @@ public class MutableCoordinateSequenceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testClose() {
|
public void testClose() {
|
||||||
var seq = new MutableCoordinateSequence(false);
|
var seq = new MutableCoordinateSequence();
|
||||||
seq.addPoint(1, 2);
|
seq.addPoint(1, 2);
|
||||||
seq.addPoint(3, 4);
|
seq.addPoint(3, 4);
|
||||||
seq.addPoint(0, 1);
|
seq.addPoint(0, 1);
|
||||||
|
@ -64,7 +56,7 @@ public class MutableCoordinateSequenceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testScaling() {
|
public void testScaling() {
|
||||||
var seq = MutableCoordinateSequence.newScalingSequence(true, 1, 2, 3);
|
var seq = MutableCoordinateSequence.newScalingSequence(1, 2, 3);
|
||||||
seq.addPoint(1, 2);
|
seq.addPoint(1, 2);
|
||||||
seq.addPoint(3, 4);
|
seq.addPoint(3, 4);
|
||||||
seq.addPoint(0, 1);
|
seq.addPoint(0, 1);
|
||||||
|
|
|
@ -39,7 +39,7 @@ public class NaturalEarthReaderTest {
|
||||||
points.add(elem.latLonGeometry());
|
points.add(elem.latLonGeometry());
|
||||||
}).await();
|
}).await();
|
||||||
assertEquals(19, points.size());
|
assertEquals(19, points.size());
|
||||||
var gc = GeoUtils.gf.createGeometryCollection(points.toArray(new Geometry[0]));
|
var gc = GeoUtils.JTS_FACTORY.createGeometryCollection(points.toArray(new Geometry[0]));
|
||||||
var centroid = gc.getCentroid();
|
var centroid = gc.getCentroid();
|
||||||
assertArrayEquals(
|
assertArrayEquals(
|
||||||
new double[]{14.22422, 12.994629},
|
new double[]{14.22422, 12.994629},
|
||||||
|
|
|
@ -47,7 +47,7 @@ public class ShapefileReaderTest {
|
||||||
points.add(elem.latLonGeometry());
|
points.add(elem.latLonGeometry());
|
||||||
}).await();
|
}).await();
|
||||||
assertEquals(86, points.size());
|
assertEquals(86, points.size());
|
||||||
var gc = GeoUtils.gf.createGeometryCollection(points.toArray(new Geometry[0]));
|
var gc = GeoUtils.JTS_FACTORY.createGeometryCollection(points.toArray(new Geometry[0]));
|
||||||
var centroid = gc.getCentroid();
|
var centroid = gc.getCentroid();
|
||||||
assertEquals(-77.0297995, centroid.getX(), 5, "iter " + i);
|
assertEquals(-77.0297995, centroid.getX(), 5, "iter " + i);
|
||||||
assertEquals(38.9119684, centroid.getY(), 5, "iter " + i);
|
assertEquals(38.9119684, centroid.getY(), 5, "iter " + i);
|
||||||
|
|
Ładowanie…
Reference in New Issue