kopia lustrzana https://github.com/onthegomap/planetiler
render points
rodzic
57dba3bb14
commit
1e36e9e940
|
@ -5,14 +5,12 @@ import java.util.Iterator;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.Lineal;
|
||||
import org.locationtech.jts.geom.Puntal;
|
||||
|
||||
public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
||||
|
||||
private static final AtomicLong idGen = new AtomicLong(0);
|
||||
private final SourceFeature source;
|
||||
private final List<Feature<?>> output = new ArrayList<>();
|
||||
private final CommonParams config;
|
||||
|
@ -36,7 +34,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
|||
}
|
||||
|
||||
public PointFeature point(String layer) {
|
||||
var feature = new PointFeature(layer, source.centroid());
|
||||
var feature = new PointFeature(layer, source.isPoint() ? source.worldGeometry() : source.centroid());
|
||||
output.add(feature);
|
||||
return feature;
|
||||
}
|
||||
|
@ -97,6 +95,10 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
|||
protected PointFeature self() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasLabelGrid() {
|
||||
return labelGridPixelSize != null || labelGridLimit != null;
|
||||
}
|
||||
}
|
||||
|
||||
public class LineFeature extends Feature<LineFeature> {
|
||||
|
@ -162,18 +164,16 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
|||
|
||||
private final String layer;
|
||||
private final Geometry geom;
|
||||
private final long id;
|
||||
private int zOrder;
|
||||
private int minzoom = config.minzoom();
|
||||
private int maxzoom = config.maxzoom();
|
||||
private double defaultBuffer;
|
||||
private ZoomFunction<Number> bufferOverrides;
|
||||
private double defaultBufferPixels = 4;
|
||||
private ZoomFunction<Number> bufferPixelOverrides;
|
||||
private final Map<String, Object> attrs = new TreeMap<>();
|
||||
|
||||
private Feature(String layer, Geometry geom) {
|
||||
this.layer = layer;
|
||||
this.geom = geom;
|
||||
this.id = idGen.incrementAndGet();
|
||||
this.zOrder = 0;
|
||||
}
|
||||
|
||||
|
@ -210,10 +210,6 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
|||
return maxzoom;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getLayer() {
|
||||
return layer;
|
||||
}
|
||||
|
@ -222,17 +218,17 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
|||
return geom;
|
||||
}
|
||||
|
||||
public double getBufferAtZoom(int zoom) {
|
||||
return ZoomFunction.applyAsDoubleOrElse(bufferOverrides, zoom, defaultBuffer);
|
||||
public double getBufferPixelsAtZoom(int zoom) {
|
||||
return ZoomFunction.applyAsDoubleOrElse(bufferPixelOverrides, zoom, defaultBufferPixels);
|
||||
}
|
||||
|
||||
public T setBuffer(double buffer) {
|
||||
defaultBuffer = buffer;
|
||||
public T setBufferPixels(double buffer) {
|
||||
defaultBufferPixels = buffer;
|
||||
return self();
|
||||
}
|
||||
|
||||
public T setBufferOverrides(ZoomFunction<Number> buffer) {
|
||||
bufferOverrides = buffer;
|
||||
public T setBufferPixelOverrides(ZoomFunction<Number> buffer) {
|
||||
bufferPixelOverrides = buffer;
|
||||
return self();
|
||||
}
|
||||
|
||||
|
@ -264,5 +260,13 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature<?>> {
|
|||
return self();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Feature{" +
|
||||
"layer='" + layer + '\'' +
|
||||
", geom=" + geom.getGeometryType() +
|
||||
", attrs=" + attrs +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,15 @@ package com.onthegomap.flatmap;
|
|||
import com.onthegomap.flatmap.geo.GeoUtils;
|
||||
import com.onthegomap.flatmap.geo.TileCoord;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.CoordinateXY;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.GeometryCollection;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
|
@ -20,6 +25,8 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
public class FeatureRenderer {
|
||||
|
||||
private static final AtomicLong idGen = new AtomicLong(0);
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureRenderer.class);
|
||||
private final CommonParams config;
|
||||
private final Consumer<RenderedFeature> consumer;
|
||||
|
@ -33,12 +40,14 @@ public class FeatureRenderer {
|
|||
renderGeometry(feature.getGeometry(), feature);
|
||||
}
|
||||
|
||||
public void renderGeometry(Geometry geom, FeatureCollector.Feature<?> feature) {
|
||||
private void renderGeometry(Geometry geom, FeatureCollector.Feature<?> feature) {
|
||||
// TODO what about converting between area and line?
|
||||
// TODO generate ID in here?
|
||||
if (feature instanceof FeatureCollector.PointFeature pointFeature) {
|
||||
if (geom.isEmpty()) {
|
||||
LOGGER.warn("Empty geometry " + feature);
|
||||
} else if (feature instanceof FeatureCollector.PointFeature pointFeature) {
|
||||
if (geom instanceof Point point) {
|
||||
addPointFeature(pointFeature, point);
|
||||
addPointFeature(pointFeature, point.getCoordinates());
|
||||
} else if (geom instanceof MultiPoint points) {
|
||||
addPointFeature(pointFeature, points);
|
||||
} else {
|
||||
|
@ -82,7 +91,7 @@ public class FeatureRenderer {
|
|||
// }
|
||||
}
|
||||
|
||||
private static double clip(double value, double max) {
|
||||
private static int wrapInt(int value, int max) {
|
||||
value %= max;
|
||||
if (value < 0) {
|
||||
value += max;
|
||||
|
@ -90,55 +99,80 @@ public class FeatureRenderer {
|
|||
return value;
|
||||
}
|
||||
|
||||
private void addPointFeature(FeatureCollector.PointFeature feature, Point point) {
|
||||
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) {
|
||||
int tilesAtZoom = 1 << zoom;
|
||||
double worldX = coord.getX() * tilesAtZoom;
|
||||
double worldY = coord.getY() * tilesAtZoom;
|
||||
int minX = (int) Math.floor(worldX - buffer);
|
||||
int maxX = (int) Math.floor(worldX + buffer);
|
||||
int minY = Math.max(0, (int) Math.floor(worldY - buffer));
|
||||
int maxY = Math.min(tilesAtZoom - 1, (int) Math.floor(worldY + buffer));
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
double tileX = worldX - x;
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
TileCoord tile = TileCoord.ofXYZ(wrapInt(x, tilesAtZoom), y, zoom);
|
||||
double tileY = worldY - y;
|
||||
Coordinate outCoordinate = new CoordinateXY(tileX * 256, tileY * 256);
|
||||
output.computeIfAbsent(tile, t -> new HashSet<>()).add(outCoordinate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addPointFeature(FeatureCollector.PointFeature feature, Coordinate... coords) {
|
||||
long id = idGen.incrementAndGet();
|
||||
for (int zoom = feature.getMaxZoom(); zoom >= feature.getMinZoom(); zoom--) {
|
||||
Map<TileCoord, Set<Coordinate>> sliced = new HashMap<>();
|
||||
Map<String, Object> attrs = feature.getAttrsAtZoom(zoom);
|
||||
double buffer = feature.getBufferAtZoom(zoom);
|
||||
double tilesAtZoom = 1 << zoom;
|
||||
double worldX = point.getX() * tilesAtZoom;
|
||||
double worldY = point.getY() * tilesAtZoom;
|
||||
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
|
||||
int tilesAtZoom = 1 << zoom;
|
||||
for (Coordinate coord : coords) {
|
||||
slicePoint(sliced, zoom, buffer, coord);
|
||||
}
|
||||
|
||||
double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d;
|
||||
Optional<RenderedFeature.Group> groupInfo = labelGridTileSize >= 1d / 4096d ?
|
||||
Optional.of(new RenderedFeature.Group(GeoUtils.longPair(
|
||||
(int) Math.floor(clip(worldX, tilesAtZoom) / labelGridTileSize),
|
||||
(int) Math.floor(worldY / labelGridTileSize)
|
||||
), feature.getLabelGridLimitAtZoom(zoom))) : Optional.empty();
|
||||
Optional<RenderedFeature.Group> groupInfo = Optional.empty();
|
||||
if (feature.hasLabelGrid() && coords.length == 1) {
|
||||
double labelGridTileSize = feature.getLabelGridPixelSizeAtZoom(zoom) / 256d;
|
||||
groupInfo = labelGridTileSize >= 1d / 4096d ?
|
||||
Optional.of(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))) : Optional.empty();
|
||||
}
|
||||
|
||||
int minX = (int) (worldX - buffer);
|
||||
int maxX = (int) (worldX + buffer);
|
||||
int minY = (int) (worldY - buffer);
|
||||
int maxY = (int) (worldY + buffer);
|
||||
|
||||
// TODO test (real, feature, unit)
|
||||
// TODO wrap x at z0
|
||||
// TODO multipoints?
|
||||
// TODO factor-out rendered feature creation
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
TileCoord tile = TileCoord.ofXYZ(x, y, zoom);
|
||||
double tileX = worldX - x;
|
||||
double tileY = worldY - y;
|
||||
consumer.accept(new RenderedFeature(
|
||||
tile,
|
||||
new VectorTileEncoder.Feature(
|
||||
feature.getLayer(),
|
||||
feature.getId(),
|
||||
VectorTileEncoder.encodeGeometry(GeoUtils.point(tileX * 256, tileY * 256)),
|
||||
attrs
|
||||
),
|
||||
feature.getZorder(),
|
||||
groupInfo
|
||||
));
|
||||
}
|
||||
for (var entry : sliced.entrySet()) {
|
||||
Set<Coordinate> value = entry.getValue();
|
||||
Geometry geom = value.size() == 1 ? GeoUtils.point(value.iterator().next()) : GeoUtils.multiPoint(value);
|
||||
consumer.accept(new RenderedFeature(
|
||||
entry.getKey(),
|
||||
new VectorTileEncoder.Feature(
|
||||
feature.getLayer(),
|
||||
id,
|
||||
VectorTileEncoder.encodeGeometry(geom),
|
||||
attrs
|
||||
),
|
||||
feature.getZorder(),
|
||||
groupInfo
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addPointFeature(FeatureCollector.PointFeature feature, MultiPoint points) {
|
||||
Map<TileCoord, List<Point>> tilePoints = new HashMap<>();
|
||||
|
||||
// TODO render features into tile
|
||||
if (feature.hasLabelGrid()) {
|
||||
for (Coordinate coord : points.getCoordinates()) {
|
||||
addPointFeature(feature, coord);
|
||||
}
|
||||
} else {
|
||||
addPointFeature(feature, points.getCoordinates());
|
||||
}
|
||||
}
|
||||
|
||||
private void addLinearFeature(FeatureCollector.Feature<?> feature, Geometry geom) {
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package com.onthegomap.flatmap.geo;
|
||||
|
||||
import java.util.Collection;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
import org.locationtech.jts.geom.CoordinateXY;
|
||||
import org.locationtech.jts.geom.Envelope;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.MultiPoint;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||
import org.locationtech.jts.geom.util.GeometryTransformer;
|
||||
import org.locationtech.jts.io.WKBReader;
|
||||
|
@ -169,7 +173,20 @@ public class GeoUtils {
|
|||
return (pair << 16) >> 16;
|
||||
}
|
||||
|
||||
public static Geometry point(double x, double y) {
|
||||
public static Point point(double x, double y) {
|
||||
return gf.createPoint(new CoordinateXY(x, y));
|
||||
}
|
||||
|
||||
public static Point point(Coordinate coord) {
|
||||
return gf.createPoint(coord);
|
||||
}
|
||||
|
||||
public static MultiPoint multiPoint(Collection<Coordinate> coords) {
|
||||
return gf.createMultiPointFromCoords(coords.toArray(new Coordinate[0]));
|
||||
}
|
||||
|
||||
public static Geometry multiPoint(double... coords) {
|
||||
assert coords.length % 2 == 0;
|
||||
return gf.createMultiPoint(new PackedCoordinateSequence.Double(coords, 2, 0));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,10 +67,7 @@ public record TileCoord(int encoded, int x, int y, int z) implements Comparable<
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" +
|
||||
z + "/" + x + "/" + y +
|
||||
" (" + encoded +
|
||||
")]";
|
||||
return "{x=" + x + " y=" + y + " z=" + z + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -30,7 +30,6 @@ import java.util.List;
|
|||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
import org.locationtech.jts.geom.CoordinateXY;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||
|
||||
|
@ -245,10 +244,10 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
|
|||
|
||||
@Override
|
||||
protected Geometry computeWorldGeometry() {
|
||||
return GeoUtils.gf.createPoint(new CoordinateXY(
|
||||
return GeoUtils.point(
|
||||
GeoUtils.getWorldX(lon),
|
||||
GeoUtils.getWorldY(lat)
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -42,8 +42,8 @@ public class FeatureCollectorTest {
|
|||
.setZoomRange(12, 14)
|
||||
.setZorder(3)
|
||||
.setAttr("attr1", 2)
|
||||
.setBuffer(10d)
|
||||
.setBufferOverrides(ZoomFunction.maxZoom(12, 11d))
|
||||
.setBufferPixels(10d)
|
||||
.setBufferPixelOverrides(ZoomFunction.maxZoom(12, 11d))
|
||||
.setLabelGridSizeAndLimit(12, 100, 10);
|
||||
assertFeatures(14, List.of(
|
||||
Map.of(
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
package com.onthegomap.flatmap;
|
||||
|
||||
import static com.onthegomap.flatmap.TestUtils.assertSameNormalizedFeatures;
|
||||
import static com.onthegomap.flatmap.TestUtils.emptyGeometry;
|
||||
import static com.onthegomap.flatmap.TestUtils.newMultiPoint;
|
||||
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.geo.GeoUtils;
|
||||
import com.onthegomap.flatmap.geo.TileCoord;
|
||||
import com.onthegomap.flatmap.read.ReaderFeature;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeMap;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
|
||||
public class FeatureRendererTest {
|
||||
|
||||
private final CommonParams config = CommonParams.defaults();
|
||||
|
||||
private FeatureCollector collector(Geometry worldGeom) {
|
||||
var latLonGeom = GeoUtils.worldToLatLonCoords(worldGeom);
|
||||
return new FeatureCollector.Factory(config).get(new ReaderFeature(latLonGeom, 0));
|
||||
}
|
||||
|
||||
private Map<TileCoord, Collection<Geometry>> renderGeometry(FeatureCollector.Feature<?> feature) {
|
||||
Map<TileCoord, Collection<Geometry>> result = new TreeMap<>();
|
||||
new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>())
|
||||
.add(rendered.vectorTileFeature().geometry().decode())).renderFeature(feature);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<TileCoord, Collection<RenderedFeature>> renderFeatures(FeatureCollector.Feature<?> feature) {
|
||||
Map<TileCoord, Collection<RenderedFeature>> result = new TreeMap<>();
|
||||
new FeatureRenderer(config, rendered -> result.computeIfAbsent(rendered.tile(), tile -> new HashSet<>())
|
||||
.add(rendered)).renderFeature(feature);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final int Z14_TILES = 1 << 14;
|
||||
private static final double Z14_WIDTH = 1d / Z14_TILES;
|
||||
|
||||
@Test
|
||||
public void testEmptyGeometry() {
|
||||
var feature = collector(emptyGeometry()).point("layer");
|
||||
assertSameNormalizedFeatures(Map.of(), renderGeometry(feature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSinglePoint() {
|
||||
var feature = pointFeature(newPoint(0.5 + Z14_WIDTH / 2, 0.5 + Z14_WIDTH / 2))
|
||||
.setZoomRange(14, 14);
|
||||
assertSameNormalizedFeatures(Map.of(
|
||||
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
|
||||
newPoint(128, 128)
|
||||
)
|
||||
), renderGeometry(feature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRepeatSinglePointNeighboringTiles() {
|
||||
var feature = pointFeature(newPoint(0.5 + 1d / 512, 0.5 + 1d / 512))
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(2);
|
||||
assertSameNormalizedFeatures(Map.of(
|
||||
TileCoord.ofXYZ(0, 0, 0), List.of(newPoint(128.5, 128.5)),
|
||||
TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(257, 257)),
|
||||
TileCoord.ofXYZ(1, 0, 1), List.of(newPoint(1, 257)),
|
||||
TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(257, 1)),
|
||||
TileCoord.ofXYZ(1, 1, 1), List.of(newPoint(1, 1))
|
||||
), renderGeometry(feature));
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
public List<DynamicTest> testProcessPointsNearInternationalDateLineAndPoles() {
|
||||
double d = 1d / 512;
|
||||
record X(double x, double wrapped, double z1x0, double z1x1) {
|
||||
|
||||
}
|
||||
record Y(double y, int z1ty, double tyoff) {
|
||||
|
||||
}
|
||||
var xs = List.of(
|
||||
new X(-d, 1 - d, -1, 255),
|
||||
new X(d, 1 + d, 1, 257),
|
||||
new X(1 - d, -d, -1, 255),
|
||||
new X(1 + d, d, 1, 257)
|
||||
);
|
||||
var ys = List.of(
|
||||
new Y(0.25, 0, 128),
|
||||
new Y(-d, 0, -1),
|
||||
new Y(d, 0, 1),
|
||||
new Y(1 - d, 1, 255),
|
||||
new Y(1 + d, 1, 257)
|
||||
);
|
||||
List<DynamicTest> tests = new ArrayList<>();
|
||||
for (X x : xs) {
|
||||
for (Y y : ys) {
|
||||
tests.add(dynamicTest((x.x * 256) + ", " + (y.y * 256), () -> {
|
||||
var feature = pointFeature(newPoint(x.x, y.y))
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(2);
|
||||
assertSameNormalizedFeatures(Map.of(
|
||||
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint(
|
||||
newPoint(x.x * 256, y.y * 256),
|
||||
newPoint(x.wrapped * 256, y.y * 256)
|
||||
)),
|
||||
TileCoord.ofXYZ(0, y.z1ty, 1), List.of(newPoint(x.z1x0, y.tyoff)),
|
||||
TileCoord.ofXYZ(1, y.z1ty, 1), List.of(newPoint(x.z1x1, y.tyoff))
|
||||
), renderGeometry(feature));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return tests;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testZ0FullTileBuffer() {
|
||||
var feature = pointFeature(newPoint(0.25, 0.25))
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(256);
|
||||
assertSameNormalizedFeatures(Map.of(
|
||||
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint(
|
||||
newPoint(-192, 64),
|
||||
newPoint(64, 64),
|
||||
newPoint(320, 64)
|
||||
)),
|
||||
TileCoord.ofXYZ(0, 0, 1), List.of(newPoint(128, 128)),
|
||||
TileCoord.ofXYZ(1, 0, 1), List.of(newMultiPoint(
|
||||
newPoint(-128, 128),
|
||||
newPoint(256 + 128, 128)
|
||||
)),
|
||||
TileCoord.ofXYZ(0, 1, 1), List.of(newPoint(128, -128)),
|
||||
TileCoord.ofXYZ(1, 1, 1), List.of(newMultiPoint(
|
||||
newPoint(-128, -128),
|
||||
newPoint(256 + 128, -128)
|
||||
))
|
||||
), renderGeometry(feature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipointNoLabelGrid() {
|
||||
var feature = pointFeature(newMultiPoint(
|
||||
newPoint(0.25, 0.25),
|
||||
newPoint(0.25 + 1d / 256, 0.25 + 1d / 256)
|
||||
))
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(4);
|
||||
assertSameNormalizedFeatures(Map.of(
|
||||
TileCoord.ofXYZ(0, 0, 0), List.of(newMultiPoint(
|
||||
newPoint(64, 64),
|
||||
newPoint(65, 65)
|
||||
)),
|
||||
TileCoord.ofXYZ(0, 0, 1), List.of(newMultiPoint(
|
||||
newPoint(128, 128),
|
||||
newPoint(130, 130)
|
||||
))
|
||||
), renderGeometry(feature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipointWithLabelGridSplits() {
|
||||
var feature = pointFeature(newMultiPoint(
|
||||
newPoint(0.25, 0.25),
|
||||
newPoint(0.25 + 1d / 256, 0.25 + 1d / 256)
|
||||
))
|
||||
.setLabelGridPixelSize(10, 256)
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(4);
|
||||
assertSameNormalizedFeatures(Map.of(
|
||||
TileCoord.ofXYZ(0, 0, 0), List.of(
|
||||
newPoint(64, 64),
|
||||
newPoint(65, 65)
|
||||
),
|
||||
TileCoord.ofXYZ(0, 0, 1), List.of(
|
||||
newPoint(128, 128),
|
||||
newPoint(130, 130)
|
||||
)
|
||||
), renderGeometry(feature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLabelGrid() {
|
||||
var feature = pointFeature(newPoint(0.75, 0.75))
|
||||
.setLabelGridSizeAndLimit(10, 256, 2)
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(4);
|
||||
var rendered = renderFeatures(feature);
|
||||
var z0Feature = rendered.get(TileCoord.ofXYZ(0, 0, 0)).iterator().next();
|
||||
var z1Feature = rendered.get(TileCoord.ofXYZ(1, 1, 1)).iterator().next();
|
||||
assertEquals(Optional.of(new RenderedFeature.Group(0, 2)), z0Feature.group());
|
||||
assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) + 1, 2)), z1Feature.group());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrapLabelGrid() {
|
||||
var feature = pointFeature(newPoint(1.1, -0.1))
|
||||
.setLabelGridSizeAndLimit(10, 256, 2)
|
||||
.setZoomRange(0, 1)
|
||||
.setBufferPixels(64);
|
||||
var rendered = renderFeatures(feature);
|
||||
System.err.println(rendered);
|
||||
var z0Feature = rendered.get(TileCoord.ofXYZ(0, 0, 0)).iterator().next();
|
||||
var z1Feature = rendered.get(TileCoord.ofXYZ(0, 0, 1)).iterator().next();
|
||||
assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) - 1, 2)), z0Feature.group());
|
||||
assertEquals(Optional.of(new RenderedFeature.Group((1L << 32) - 1, 2)), z1Feature.group());
|
||||
}
|
||||
|
||||
private FeatureCollector.PointFeature pointFeature(Geometry geom) {
|
||||
return collector(geom).point("layer");
|
||||
}
|
||||
|
||||
// happy tests:
|
||||
// TODO: multipoint together?
|
||||
// TODO: multipoint separate (if has group)? separate IDs?
|
||||
// TODO: world wrap
|
||||
|
||||
// TODO: line
|
||||
// TODO: multilinestring
|
||||
// TODO: poly
|
||||
// TODO: multipolygon
|
||||
// TODO: geometry collection
|
||||
|
||||
// sad tests:
|
||||
// TODO: invalid line
|
||||
// TODO: invalid poly
|
||||
// TODO: coerce poly -> line
|
||||
// TODO: coerce line -> poly
|
||||
// TODO: wrong types: point/line/poly -> point/line/poly
|
||||
}
|
|
@ -28,7 +28,10 @@ import java.util.function.BiConsumer;
|
|||
import java.util.function.Function;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class FeatureTest {
|
||||
/**
|
||||
* In-memory tests with fake data and profiles to ensure all features work end-to-end.
|
||||
*/
|
||||
public class FlatMapTest {
|
||||
|
||||
private static final String TEST_PROFILE_NAME = "test name";
|
||||
private static final String TEST_PROFILE_DESCRIPTION = "test description";
|
||||
|
@ -162,6 +165,46 @@ public class FeatureTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLabelGridLimit() throws IOException, SQLException {
|
||||
double y = 0.5 + Z14_WIDTH / 2;
|
||||
double lat = GeoUtils.getWorldLat(y);
|
||||
|
||||
double x1 = 0.5 + Z14_WIDTH / 4;
|
||||
double lng1 = GeoUtils.getWorldLon(x1);
|
||||
double lng2 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 10d / 256);
|
||||
double lng3 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 20d / 256);
|
||||
|
||||
var results = runWithReaderFeatures(
|
||||
Map.of("threads", "1"),
|
||||
List.of(
|
||||
new ReaderFeature(newPoint(lng1, lat), Map.of("rank", "1")),
|
||||
new ReaderFeature(newPoint(lng2, lat), Map.of("rank", "2")),
|
||||
new ReaderFeature(newPoint(lng3, lat), Map.of("rank", "3"))
|
||||
),
|
||||
(in, features) -> {
|
||||
features.point("layer")
|
||||
.setZoomRange(13, 14)
|
||||
.inheritFromSource("rank")
|
||||
.setZorder(Integer.parseInt(in.getTag("rank").toString()))
|
||||
.setLabelGridSizeAndLimit(13, 128, 2);
|
||||
}
|
||||
);
|
||||
|
||||
assertSubmap(Map.of(
|
||||
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
|
||||
feature(newPoint(64, 128), Map.of("rank", "1")),
|
||||
feature(newPoint(74, 128), Map.of("rank", "2")),
|
||||
feature(newPoint(84, 128), Map.of("rank", "3"))
|
||||
),
|
||||
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
|
||||
// omit rank=1 due to label grid size
|
||||
feature(newPoint(37, 64), Map.of("rank", "2")),
|
||||
feature(newPoint(42, 64), Map.of("rank", "3"))
|
||||
)
|
||||
), results.tiles);
|
||||
}
|
||||
|
||||
private interface LayerPostprocessFunction {
|
||||
|
||||
List<VectorTileEncoder.Feature> process(String layer, int zoom, List<VectorTileEncoder.Feature> items);
|
||||
|
@ -218,6 +261,4 @@ public class FeatureTest {
|
|||
return postprocessLayerFeatures.process(layer, zoom, items);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactor into parameterized test?
|
||||
}
|
|
@ -86,7 +86,6 @@ public class LayerStatsTest {
|
|||
), layerStats.getTileStats());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testMergeFromMultipleThreads() throws InterruptedException {
|
||||
Thread t1 = new Thread(() -> {
|
||||
|
@ -103,7 +102,6 @@ public class LayerStatsTest {
|
|||
));
|
||||
});
|
||||
t1.start();
|
||||
t1.join();
|
||||
Thread t2 = new Thread(() -> {
|
||||
layerStats.accept(new RenderedFeature(
|
||||
TileCoord.ofXYZ(1, 2, 4),
|
||||
|
@ -118,6 +116,7 @@ public class LayerStatsTest {
|
|||
));
|
||||
});
|
||||
t2.start();
|
||||
t1.join();
|
||||
t2.join();
|
||||
assertEquals(new Mbtiles.MetadataJson(
|
||||
new Mbtiles.MetadataJson.VectorLayer("layer1", Map.of(
|
||||
|
|
|
@ -13,11 +13,15 @@ import java.sql.ResultSet;
|
|||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
|
@ -148,25 +152,67 @@ public class TestUtils {
|
|||
assertEquals(new TreeMap<>(expectedSubmap), actualFiltered, message + " others: " + others);
|
||||
}
|
||||
|
||||
public static Geometry emptyGeometry() {
|
||||
return GeoUtils.gf.createGeometryCollection();
|
||||
}
|
||||
|
||||
public interface GeometryComparision {
|
||||
|
||||
Geometry geom();
|
||||
}
|
||||
|
||||
public static record ExactGeometry(Geometry geom) implements GeometryComparision {
|
||||
private static record NormGeometry(Geometry geom) implements GeometryComparision {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof GeometryComparision that && geom.equalsNorm(that.geom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Norm{" + geom + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static record ExactGeometry(Geometry geom) implements GeometryComparision {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof GeometryComparision that && geom.equalsExact(that.geom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Exact{" + geom + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static record TopoGeometry(Geometry geom) implements GeometryComparision {
|
||||
private static record TopoGeometry(Geometry geom) implements GeometryComparision {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof GeometryComparision that && geom.equalsTopo(that.geom());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Topo{" + geom + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static record ComparableFeature(
|
||||
|
@ -184,8 +230,7 @@ public class TestUtils {
|
|||
TreeMap<String, Object> result = new TreeMap<>(feature.getAttrsAtZoom(zoom));
|
||||
result.put("_minzoom", feature.getMinZoom());
|
||||
result.put("_maxzoom", feature.getMaxZoom());
|
||||
result.put("_buffer", feature.getBufferAtZoom(zoom));
|
||||
result.put("_id", feature.getId());
|
||||
result.put("_buffer", feature.getBufferPixelsAtZoom(zoom));
|
||||
result.put("_layer", feature.getLayer());
|
||||
result.put("_zorder", feature.getZorder());
|
||||
result.put("_geom", new TopoGeometry(feature.getGeometry()));
|
||||
|
@ -211,4 +256,45 @@ public class TestUtils {
|
|||
objectMapper.readTree(actual)
|
||||
);
|
||||
}
|
||||
|
||||
public static <T> Map<TileCoord, Collection<T>> mapTileFeatures(Map<TileCoord, Collection<Geometry>> in,
|
||||
Function<Geometry, T> fn) {
|
||||
TreeMap<TileCoord, Collection<T>> out = new TreeMap<>();
|
||||
for (var entry : in.entrySet()) {
|
||||
out.put(entry.getKey(), entry.getValue().stream().map(fn)
|
||||
.sorted(Comparator.comparing(Object::toString))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static void assertExactSameFeatures(
|
||||
Map<TileCoord, Collection<Geometry>> expected,
|
||||
Map<TileCoord, Collection<Geometry>> actual
|
||||
) {
|
||||
assertEquals(
|
||||
mapTileFeatures(expected, ExactGeometry::new),
|
||||
mapTileFeatures(actual, ExactGeometry::new)
|
||||
);
|
||||
}
|
||||
|
||||
public static void assertTopologicallyEquivalentFeatures(
|
||||
Map<TileCoord, Collection<Geometry>> expected,
|
||||
Map<TileCoord, Collection<Geometry>> actual
|
||||
) {
|
||||
assertEquals(
|
||||
mapTileFeatures(expected, TopoGeometry::new),
|
||||
mapTileFeatures(actual, TopoGeometry::new)
|
||||
);
|
||||
}
|
||||
|
||||
public static void assertSameNormalizedFeatures(
|
||||
Map<TileCoord, Collection<Geometry>> expected,
|
||||
Map<TileCoord, Collection<Geometry>> actual
|
||||
) {
|
||||
assertEquals(
|
||||
mapTileFeatures(expected, NormGeometry::new),
|
||||
mapTileFeatures(actual, NormGeometry::new)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue