first pass of line/are rendering

pull/1/head
Mike Barry 2021-05-19 06:44:28 -04:00
rodzic 7e06af0b1d
commit 2d4d6187bf
14 zmienionych plików z 366 dodań i 134 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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);
}
}
}
}
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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;

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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);

Wyświetl plik

@ -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},

Wyświetl plik

@ -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);