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;
|
||||
|
||||
import com.onthegomap.flatmap.collections.CacheByZoom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -24,19 +25,19 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
}
|
||||
|
||||
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);
|
||||
return feature;
|
||||
}
|
||||
|
||||
public Feature line(String layername) {
|
||||
var feature = new Feature(layername, source.line());
|
||||
var feature = new Feature(layername, source.line(), false);
|
||||
output.add(feature);
|
||||
return feature;
|
||||
}
|
||||
|
||||
public Feature polygon(String layername) {
|
||||
var feature = new Feature(layername, source.polygon());
|
||||
var feature = new Feature(layername, source.polygon(), true);
|
||||
output.add(feature);
|
||||
return feature;
|
||||
}
|
||||
|
@ -50,6 +51,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
|
||||
public final class Feature {
|
||||
|
||||
private final boolean area;
|
||||
private static final double DEFAULT_LABEL_GRID_SIZE = 0;
|
||||
private static final int DEFAULT_LABEL_GRID_LIMIT = 0;
|
||||
private final String layer;
|
||||
|
@ -64,11 +66,14 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
private ZoomFunction<Number> minPixelSize = null;
|
||||
private ZoomFunction<Number> labelGridPixelSize = 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.geom = geom;
|
||||
this.zOrder = 0;
|
||||
this.area = area;
|
||||
}
|
||||
|
||||
public int getZorder() {
|
||||
|
@ -169,7 +174,7 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
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<>();
|
||||
for (var entry : attrs.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
|
@ -183,18 +188,34 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
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) {
|
||||
return setAttr(attr, source.getTag(attr));
|
||||
}
|
||||
|
||||
public Feature setAttr(String key, Object value) {
|
||||
if (value instanceof ZoomFunction) {
|
||||
attrsChangeByZoom = true;
|
||||
}
|
||||
attrs.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Feature setAttrWithMinzoom(String key, Object value, int minzoom) {
|
||||
attrs.put(key, ZoomFunction.minZoom(minzoom, value));
|
||||
return this;
|
||||
return setAttr(key, ZoomFunction.minZoom(minzoom, value));
|
||||
}
|
||||
|
||||
public boolean area() {
|
||||
return area;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.onthegomap.flatmap;
|
|||
|
||||
import com.onthegomap.flatmap.geo.GeoUtils;
|
||||
import com.onthegomap.flatmap.geo.TileCoord;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
@ -10,19 +11,26 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.locationtech.jts.algorithm.Area;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
import org.locationtech.jts.geom.CoordinateSequences;
|
||||
import org.locationtech.jts.geom.CoordinateXY;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.GeometryCollection;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.LinearRing;
|
||||
import org.locationtech.jts.geom.MultiLineString;
|
||||
import org.locationtech.jts.geom.MultiPoint;
|
||||
import org.locationtech.jts.geom.MultiPolygon;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
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.precision.GeometryPrecisionReducer;
|
||||
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -32,6 +40,15 @@ public class FeatureRenderer {
|
|||
private static final AtomicLong idGen = new AtomicLong(0);
|
||||
|
||||
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 Consumer<RenderedFeature> consumer;
|
||||
|
||||
|
@ -40,6 +57,22 @@ public class FeatureRenderer {
|
|||
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) {
|
||||
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) {
|
||||
// TODO put this into TiledGeometry
|
||||
int tilesAtZoom = 1 << zoom;
|
||||
double worldX = coord.getX() * tilesAtZoom;
|
||||
double worldY = coord.getY() * tilesAtZoom;
|
||||
|
@ -108,37 +126,49 @@ public class FeatureRenderer {
|
|||
double buffer = feature.getBufferPixelsAtZoom(zoom) / 256;
|
||||
int tilesAtZoom = 1 << zoom;
|
||||
for (Coordinate coord : coords) {
|
||||
// TODO TiledGeometry.sliceIntoTiles(...)
|
||||
slicePoint(sliced, zoom, buffer, coord);
|
||||
}
|
||||
|
||||
Optional<RenderedFeature.Group> groupInfo = Optional.empty();
|
||||
RenderedFeature.Group groupInfo = null;
|
||||
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();
|
||||
groupInfo = labelGridTileSize >= 1d / 4096d ? new RenderedFeature.Group(GeoUtils.longPair(
|
||||
(int) Math.floor(wrapDouble(coords[0].getX() * tilesAtZoom, tilesAtZoom) / labelGridTileSize),
|
||||
(int) Math.floor((coords[0].getY() * tilesAtZoom) / labelGridTileSize)
|
||||
), feature.getLabelGridLimitAtZoom(zoom)) : null;
|
||||
}
|
||||
|
||||
for (var entry : sliced.entrySet()) {
|
||||
TileCoord tile = entry.getKey();
|
||||
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
|
||||
));
|
||||
// TODO stats
|
||||
// TODO writeTileFeatures
|
||||
emitFeature(feature, id, attrs, groupInfo, tile, geom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (feature.hasLabelGrid()) {
|
||||
for (Coordinate coord : points.getCoordinates()) {
|
||||
|
@ -150,6 +180,7 @@ public class FeatureRenderer {
|
|||
}
|
||||
|
||||
private void addLinearFeature(FeatureCollector.Feature feature, Geometry input) {
|
||||
long id = idGen.incrementAndGet();
|
||||
// TODO move to feature?
|
||||
double minSizeAtMaxZoom = 1d / 4096;
|
||||
double normalTolerance = 0.1 / 256;
|
||||
|
@ -175,27 +206,155 @@ public class FeatureRenderer {
|
|||
simplifier.setDistanceTolerance(tolerance);
|
||||
geom = simplifier.getResultGeometry();
|
||||
|
||||
List<List<CoordinateSequence>> groups = extractGroups(geom);
|
||||
List<List<CoordinateSequence>> groups = new ArrayList<>();
|
||||
extractGroups(geom, groups, minSize);
|
||||
double buffer = feature.getBufferPixelsAtZoom(z);
|
||||
TileExtents.ForZoom extents = config.extents().getForZoom(z);
|
||||
TiledGeometry sliced = TiledGeometry.sliceIntoTiles(groups, buffer, area, z, extents);
|
||||
writeTileFeatures(feature, sliced);
|
||||
writeTileFeatures(id, feature, sliced);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTileFeatures(FeatureCollector.Feature feature, TiledGeometry sliced) {
|
||||
build polygons, enforce correctness
|
||||
build linestrings
|
||||
reduce precision
|
||||
handle errors
|
||||
fix orientation
|
||||
write filled tiles
|
||||
log stats
|
||||
private void writeTileFeatures(long id, FeatureCollector.Feature feature, TiledGeometry sliced) {
|
||||
for (var entry : sliced.getTileData()) {
|
||||
TileCoord tile = entry.getKey();
|
||||
List<List<CoordinateSequence>> geoms = entry.getValue();
|
||||
|
||||
Geometry geom;
|
||||
if (feature.area()) {
|
||||
geom = reassemblePolygon(feature, tile, geoms);
|
||||
} else {
|
||||
geom = reassembleLineString(geoms);
|
||||
}
|
||||
|
||||
try {
|
||||
geom = GeometryPrecisionReducer.reduce(geom, tilePrecision);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.warn("Error reducing precision of " + feature + " on " + tile + ": " + e);
|
||||
}
|
||||
|
||||
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) {
|
||||
limit length
|
||||
limit area
|
||||
enforce orientation
|
||||
private void emitFilledTiles(long id, FeatureCollector.Feature feature, TiledGeometry sliced) {
|
||||
/*
|
||||
* Optimization: large input polygons that generate many filled interior tiles (ie. the ocean), the encoder avoids
|
||||
* re-encoding if groupInfo and vector tile feature are == to previous values. The feature can have different
|
||||
* attributes at different zoom levels though, so need to cache each vector tile feature instance by zoom level.
|
||||
*/
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -41,7 +44,7 @@ public class TiledGeometry {
|
|||
private static final Logger LOGGER = LoggerFactory.getLogger(TiledGeometry.class);
|
||||
|
||||
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 double buffer;
|
||||
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,
|
||||
TileExtents.ForZoom extents) {
|
||||
|
||||
int worldExtent = 1 << z;
|
||||
TiledGeometry result = new TiledGeometry(extents, buffer, z, area);
|
||||
EnumSet<Direction> wrapResult = result.sliceWorldCopy(groups, 0);
|
||||
if (wrapResult.contains(Direction.RIGHT)) {
|
||||
result.sliceWorldCopy(groups, 1);
|
||||
} else if (wrapResult.contains(Direction.LEFT)) {
|
||||
result.sliceWorldCopy(groups, -1);
|
||||
result.sliceWorldCopy(groups, -worldExtent);
|
||||
}
|
||||
if (wrapResult.contains(Direction.LEFT)) {
|
||||
result.sliceWorldCopy(groups, worldExtent);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Iterable<TileCoord> getFilledTiles() {
|
||||
public Iterable<TileCoord> getFilledTilesOrderedByZXY() {
|
||||
return () -> filledRanges.entrySet().stream()
|
||||
.<TileCoord>mapMulti((entry, next) -> {
|
||||
Column column = entry.getKey();
|
||||
|
@ -125,7 +129,7 @@ public class TiledGeometry {
|
|||
for (int i = 0; i < group.size(); i++) {
|
||||
CoordinateSequence segment = group.get(i);
|
||||
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) {
|
||||
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;
|
||||
double k1 = -buffer;
|
||||
double k2 = 1 + buffer;
|
||||
|
@ -197,7 +201,7 @@ public class TiledGeometry {
|
|||
double bx = _bx - x;
|
||||
MutableCoordinateSequence slice = xSlices.get(x);
|
||||
if (slice == null) {
|
||||
xSlices.put(x, slice = new MutableCoordinateSequence(outer));
|
||||
xSlices.put(x, slice = new MutableCoordinateSequence());
|
||||
List<MutableCoordinateSequence> newGeom = newGeoms.get(x);
|
||||
if (newGeom == null) {
|
||||
newGeoms.put(x, newGeom = new ArrayList<>());
|
||||
|
@ -339,7 +343,7 @@ public class TiledGeometry {
|
|||
tiles.add(y);
|
||||
}
|
||||
// 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);
|
||||
List<CoordinateSequence> toAddTo = inProgressShapes.computeIfAbsent(tileID, tile -> new ArrayList<>());
|
||||
|
||||
|
@ -450,7 +454,12 @@ public class TiledGeometry {
|
|||
|
||||
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) {
|
||||
VectorTile.Tile.GeomType geomType = Objects.requireNonNull(VectorTile.Tile.GeomType.forNumber(geomTypeByte));
|
||||
GeometryFactory gf = GeoUtils.gf;
|
||||
GeometryFactory gf = GeoUtils.JTS_FACTORY;
|
||||
int x = 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.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import org.msgpack.core.MessageBufferPacker;
|
||||
|
@ -93,18 +94,38 @@ public final class FeatureGroup implements Consumer<FeatureSort.Entry>, Iterable
|
|||
}
|
||||
|
||||
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();
|
||||
return feature -> {
|
||||
layerStats.accept(feature);
|
||||
return encode(feature, packer);
|
||||
};
|
||||
}
|
||||
|
||||
private FeatureSort.Entry encode(RenderedFeature feature, MessageBufferPacker packer) {
|
||||
return new FeatureSort.Entry(
|
||||
encodeSortKey(feature),
|
||||
encodeValue(feature, packer)
|
||||
);
|
||||
/*
|
||||
* Optimization: Avoid re-encoding values for identical fill geometries (ie. in the ocean) by memoizing based on
|
||||
* the input vector tile feature. FeatureRenderer ensures that all fill vector tile features use the same instance
|
||||
* 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) {
|
||||
|
@ -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();
|
||||
try {
|
||||
var groupInfoOption = feature.group();
|
||||
var groupInfoOption = group;
|
||||
if (groupInfoOption.isPresent()) {
|
||||
var groupInfo = groupInfoOption.get();
|
||||
packer.packLong(groupInfo.group());
|
||||
packer.packInt(groupInfo.limit());
|
||||
}
|
||||
var vectorTileFeature = feature.vectorTileFeature();
|
||||
packer.packLong(vectorTileFeature.id());
|
||||
packer.packByte(vectorTileFeature.geometry().geomType());
|
||||
var attrs = vectorTileFeature.attrs();
|
||||
|
|
|
@ -9,22 +9,15 @@ import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
|||
public class MutableCoordinateSequence extends PackedCoordinateSequence {
|
||||
|
||||
private final DoubleArrayList points = new DoubleArrayList();
|
||||
private final boolean inner;
|
||||
|
||||
public MutableCoordinateSequence(boolean outer) {
|
||||
public MutableCoordinateSequence() {
|
||||
super(2, 0);
|
||||
this.inner = !outer;
|
||||
}
|
||||
|
||||
public static MutableCoordinateSequence newScalingSequence(boolean outer, double relX, double relY, double scale) {
|
||||
return new ScalingSequence(outer, scale, relX, relY);
|
||||
public static MutableCoordinateSequence newScalingSequence(double relX, double relY, double scale) {
|
||||
return new ScalingSequence(scale, relX, relY);
|
||||
}
|
||||
|
||||
public boolean isInnerRing() {
|
||||
return inner;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public double getOrdinate(int index, int ordinateIndex) {
|
||||
return points.get((index * 2) + ordinateIndex);
|
||||
|
@ -87,8 +80,7 @@ public class MutableCoordinateSequence extends PackedCoordinateSequence {
|
|||
private final double relX;
|
||||
private final double relY;
|
||||
|
||||
public ScalingSequence(boolean outer, double scale, double relX, double relY) {
|
||||
super(outer);
|
||||
public ScalingSequence(double scale, double relX, double relY) {
|
||||
this.scale = scale;
|
||||
this.relX = relX;
|
||||
this.relY = relY;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.onthegomap.flatmap.geo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
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.LineString;
|
||||
import org.locationtech.jts.geom.MultiPoint;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
|
||||
|
@ -15,8 +17,11 @@ import org.locationtech.jts.io.WKBReader;
|
|||
|
||||
public class GeoUtils {
|
||||
|
||||
public static final GeometryFactory gf = new GeometryFactory();
|
||||
public static final WKBReader wkbReader = new WKBReader(gf);
|
||||
public static final GeometryFactory JTS_FACTORY = new GeometryFactory();
|
||||
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_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS;
|
||||
|
@ -174,19 +179,23 @@ public class GeoUtils {
|
|||
}
|
||||
|
||||
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) {
|
||||
return gf.createPoint(coord);
|
||||
return JTS_FACTORY.createPoint(coord);
|
||||
}
|
||||
|
||||
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) {
|
||||
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 encoder = writer.newRenderedFeatureEncoder();
|
||||
FeatureRenderer renderer = new FeatureRenderer(
|
||||
config,
|
||||
rendered -> next.accept(encoder.apply(rendered))
|
||||
config, encoder, next
|
||||
);
|
||||
while ((sourceFeature = prev.get()) != null) {
|
||||
featuresRead.incrementAndGet();
|
||||
|
|
|
@ -50,27 +50,27 @@ public class TestUtils {
|
|||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
return GeoUtils.gf.createPoint(new CoordinateXY(x, y));
|
||||
return GeoUtils.JTS_FACTORY.createPoint(new CoordinateXY(x, y));
|
||||
}
|
||||
|
||||
public static MultiPoint newMultiPoint(Point... points) {
|
||||
return GeoUtils.gf.createMultiPoint(points);
|
||||
return GeoUtils.JTS_FACTORY.createMultiPoint(points);
|
||||
}
|
||||
|
||||
public static MultiPolygon newMultiPolygon(Polygon... polys) {
|
||||
return GeoUtils.gf.createMultiPolygon(polys);
|
||||
return GeoUtils.JTS_FACTORY.createMultiPolygon(polys);
|
||||
}
|
||||
|
||||
public static GeometryCollection newGeometryCollection(Geometry... geoms) {
|
||||
return GeoUtils.gf.createGeometryCollection(geoms);
|
||||
return GeoUtils.JTS_FACTORY.createGeometryCollection(geoms);
|
||||
}
|
||||
|
||||
public static Geometry round(Geometry input) {
|
||||
|
@ -153,7 +153,7 @@ public class TestUtils {
|
|||
}
|
||||
|
||||
public static Geometry emptyGeometry() {
|
||||
return GeoUtils.gf.createGeometryCollection();
|
||||
return GeoUtils.JTS_FACTORY.createGeometryCollection();
|
||||
}
|
||||
|
||||
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.newPoint;
|
||||
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.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -57,7 +57,7 @@ public class VectorTileEncoderTest {
|
|||
|
||||
@Test
|
||||
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(),
|
||||
VectorTileEncoder.encodeGeometry(geometry).geomType());
|
||||
}
|
||||
|
@ -234,12 +234,12 @@ public class VectorTileEncoderTest {
|
|||
|
||||
@Test
|
||||
public void testRoundTripPoint() {
|
||||
testRoundTripGeometry(gf.createPoint(new CoordinateXY(1, 2)));
|
||||
testRoundTripGeometry(JTS_FACTORY.createPoint(new CoordinateXY(1, 2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRoundTripMultipoint() {
|
||||
testRoundTripGeometry(gf.createMultiPointFromCoords(new Coordinate[]{
|
||||
testRoundTripGeometry(JTS_FACTORY.createMultiPointFromCoords(new Coordinate[]{
|
||||
new CoordinateXY(1, 2),
|
||||
new CoordinateXY(3, 4)
|
||||
}));
|
||||
|
@ -247,7 +247,7 @@ public class VectorTileEncoderTest {
|
|||
|
||||
@Test
|
||||
public void testRoundTripLineString() {
|
||||
testRoundTripGeometry(gf.createLineString(new Coordinate[]{
|
||||
testRoundTripGeometry(JTS_FACTORY.createLineString(new Coordinate[]{
|
||||
new CoordinateXY(1, 2),
|
||||
new CoordinateXY(3, 4)
|
||||
}));
|
||||
|
@ -255,8 +255,8 @@ public class VectorTileEncoderTest {
|
|||
|
||||
@Test
|
||||
public void testRoundTripPolygon() {
|
||||
testRoundTripGeometry(gf.createPolygon(
|
||||
gf.createLinearRing(new Coordinate[]{
|
||||
testRoundTripGeometry(JTS_FACTORY.createPolygon(
|
||||
JTS_FACTORY.createLinearRing(new Coordinate[]{
|
||||
new CoordinateXY(0, 0),
|
||||
new CoordinateXY(4, 0),
|
||||
new CoordinateXY(4, 4),
|
||||
|
@ -264,7 +264,7 @@ public class VectorTileEncoderTest {
|
|||
new CoordinateXY(0, 0)
|
||||
}),
|
||||
new LinearRing[]{
|
||||
gf.createLinearRing(new Coordinate[]{
|
||||
JTS_FACTORY.createLinearRing(new Coordinate[]{
|
||||
new CoordinateXY(1, 1),
|
||||
new CoordinateXY(1, 2),
|
||||
new CoordinateXY(2, 2),
|
||||
|
@ -277,15 +277,15 @@ public class VectorTileEncoderTest {
|
|||
|
||||
@Test
|
||||
public void testRoundTripMultiPolygon() {
|
||||
testRoundTripGeometry(gf.createMultiPolygon(new Polygon[]{
|
||||
gf.createPolygon(new Coordinate[]{
|
||||
testRoundTripGeometry(JTS_FACTORY.createMultiPolygon(new Polygon[]{
|
||||
JTS_FACTORY.createPolygon(new Coordinate[]{
|
||||
new CoordinateXY(0, 0),
|
||||
new CoordinateXY(1, 0),
|
||||
new CoordinateXY(1, 1),
|
||||
new CoordinateXY(0, 1),
|
||||
new CoordinateXY(0, 0)
|
||||
}),
|
||||
gf.createPolygon(new Coordinate[]{
|
||||
JTS_FACTORY.createPolygon(new Coordinate[]{
|
||||
new CoordinateXY(3, 0),
|
||||
new CoordinateXY(4, 0),
|
||||
new CoordinateXY(4, 1),
|
||||
|
@ -308,7 +308,7 @@ public class VectorTileEncoderTest {
|
|||
|
||||
@Test
|
||||
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> attrs2 = Map.of("b", 3L, "c", 2L);
|
||||
byte[] encoded = new VectorTileEncoder().addLayerFeatures("layer1", List.of(
|
||||
|
@ -330,7 +330,7 @@ public class VectorTileEncoderTest {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package com.onthegomap.flatmap.collections;
|
||||
|
||||
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 org.junit.jupiter.api.Test;
|
||||
|
@ -25,28 +23,22 @@ public class MutableCoordinateSequenceTest {
|
|||
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
|
||||
public void testEmpty() {
|
||||
var seq = new MutableCoordinateSequence(false);
|
||||
var seq = new MutableCoordinateSequence();
|
||||
assertEquals(0, seq.copy().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingle() {
|
||||
var seq = new MutableCoordinateSequence(false);
|
||||
var seq = new MutableCoordinateSequence();
|
||||
seq.addPoint(1, 2);
|
||||
assertContents(seq, 1, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoPoints() {
|
||||
var seq = new MutableCoordinateSequence(false);
|
||||
var seq = new MutableCoordinateSequence();
|
||||
seq.addPoint(1, 2);
|
||||
seq.addPoint(3, 4);
|
||||
assertContents(seq, 1, 2, 3, 4);
|
||||
|
@ -54,7 +46,7 @@ public class MutableCoordinateSequenceTest {
|
|||
|
||||
@Test
|
||||
public void testClose() {
|
||||
var seq = new MutableCoordinateSequence(false);
|
||||
var seq = new MutableCoordinateSequence();
|
||||
seq.addPoint(1, 2);
|
||||
seq.addPoint(3, 4);
|
||||
seq.addPoint(0, 1);
|
||||
|
@ -64,7 +56,7 @@ public class MutableCoordinateSequenceTest {
|
|||
|
||||
@Test
|
||||
public void testScaling() {
|
||||
var seq = MutableCoordinateSequence.newScalingSequence(true, 1, 2, 3);
|
||||
var seq = MutableCoordinateSequence.newScalingSequence(1, 2, 3);
|
||||
seq.addPoint(1, 2);
|
||||
seq.addPoint(3, 4);
|
||||
seq.addPoint(0, 1);
|
||||
|
|
|
@ -39,7 +39,7 @@ public class NaturalEarthReaderTest {
|
|||
points.add(elem.latLonGeometry());
|
||||
}).await();
|
||||
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();
|
||||
assertArrayEquals(
|
||||
new double[]{14.22422, 12.994629},
|
||||
|
|
|
@ -47,7 +47,7 @@ public class ShapefileReaderTest {
|
|||
points.add(elem.latLonGeometry());
|
||||
}).await();
|
||||
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();
|
||||
assertEquals(-77.0297995, centroid.getX(), 5, "iter " + i);
|
||||
assertEquals(38.9119684, centroid.getY(), 5, "iter " + i);
|
||||
|
|
Ładowanie…
Reference in New Issue