Skip to content

Commit

Permalink
optimize merger
Browse files Browse the repository at this point in the history
  • Loading branch information
msbarry committed Sep 23, 2023
1 parent 664a810 commit f5380b8
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@
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.index.strtree.STRtree;
Expand Down Expand Up @@ -94,39 +90,27 @@ public static List<VectorTile.Feature> mergeLineStrings(List<VectorTile.Feature>
public static List<VectorTile.Feature> mergeMultiPoint(List<VectorTile.Feature> features) {
return mergeGeometries(
features,
GeometryType.POINT,
Point.class,
MultiPoint.class,
GeoUtils::combinePoints
GeometryType.POINT
);
}

public static List<VectorTile.Feature> mergeMultiPolygon(List<VectorTile.Feature> features) {
return mergeGeometries(
features,
GeometryType.POLYGON,
Polygon.class,
MultiPolygon.class,
GeoUtils::combinePolygons
GeometryType.POLYGON
);
}

public static List<VectorTile.Feature> mergeMultiLineString(List<VectorTile.Feature> features) {
return mergeGeometries(
features,
GeometryType.LINE,
LineString.class,
MultiLineString.class,
GeoUtils::combineLineStrings
GeometryType.LINE
);
}

private static <S extends Geometry, M extends GeometryCollection> List<VectorTile.Feature> mergeGeometries(
private static List<VectorTile.Feature> mergeGeometries(
List<VectorTile.Feature> features,
GeometryType geometryType,
Class<S> singleClass,
Class<M> multiClass,
Function<List<S>, Geometry> combine
GeometryType geometryType
) {
List<VectorTile.Feature> result = new ArrayList<>(features.size());
var groupedByAttrs = groupByAttrs(features, result, geometryType);
Expand All @@ -135,28 +119,11 @@ private static <S extends Geometry, M extends GeometryCollection> List<VectorTil
if (groupedFeatures.size() == 1) {
result.add(feature1);
} else {
List<S> geoms = new ArrayList<>();
VectorTile.VectorGeometryMerger combined = VectorTile.newMerger(geometryType);
for (var feature : groupedFeatures) {
try {
// TODO can we avoid decoding/encoding?
var geom = feature.geometry().decode();
if (singleClass.isInstance(geom)) {
geoms.add(singleClass.cast(geom));
} else if (multiClass.isInstance(geom)) {
var mp = multiClass.cast(geom);
for (int i = 0; i < mp.getNumGeometries(); i++) {
geoms.add(singleClass.cast(mp.getGeometryN(i)));
}
} else if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Unexpected geometry type in merge({}): {}",
geometryType.name().toLowerCase(),
geom.getClass());
}
} catch (GeometryException e) {
e.log("Error merging merging into a multi" + geometryType.name().toLowerCase() + ": " + feature);
}
combined.accept(feature.geometry());
}
result.add(feature1.copyWithNewGeometry(combine.apply(geoms)));
result.add(feature1.copyWithNewGeometry(combined.finish()));
}
}
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.concurrent.NotThreadSafe;
Expand Down Expand Up @@ -206,6 +208,11 @@ private static Geometry decodeCommands(GeometryType geomType, int[] commands, in
length = commands[i++];
command = length & ((1 << 3) - 1);
length = length >> 3;
assert geomType != GeometryType.POINT || i == 1 : "Invalid multipoint, command found at index %d, expected 0"
.formatted(i);
assert geomType != GeometryType.POINT ||
(length * 2 + 1 == geometryCount) : "Invalid multipoint: int[%d] length=%d".formatted(geometryCount,
length);
}

if (length > 0) {
Expand Down Expand Up @@ -404,6 +411,14 @@ public static VectorGeometry encodeGeometry(Geometry geometry, int scale) {
return new VectorGeometry(getCommands(geometry, scale), GeometryType.typeOf(geometry), scale);
}

/**
* Returns a new {@link VectorGeometryMerger} that combines encoded geometries of the same type into a merged
* multipoint, multilinestring, or multipolygon.
*/
public static VectorGeometryMerger newMerger(GeometryType geometryType) {
return new VectorGeometryMerger(geometryType);
}

/**
* Adds features in a layer to this tile.
*
Expand Down Expand Up @@ -560,6 +575,82 @@ private enum Command {
}
}

/**
* Utility that combines encoded geometries of the same type into a merged multipoint, multilinestring, or
* multipolygon.
*/
public static class VectorGeometryMerger implements Consumer<VectorGeometry> {

private final GeometryType geometryType;
int overallX = 0;
int overallY = 0;
private final IntArrayList result = new IntArrayList();

private VectorGeometryMerger(GeometryType geometryType) {
this.geometryType = geometryType;
}

@Override
public void accept(VectorGeometry vectorGeometry) {
if (vectorGeometry.geomType != geometryType) {
throw new IllegalArgumentException(
"Cannot merge a " + vectorGeometry.geomType.name().toLowerCase(Locale.ROOT) + " geometry into a multi" +
vectorGeometry.geomType.name().toLowerCase(Locale.ROOT));
}
if (vectorGeometry.isEmpty()) {
return;
}
var commands = vectorGeometry.unscale().commands();
int x = 0;
int y = 0;

int geometryCount = commands.length;
int length = 0;
int command = 0;
int i = 0;

// For the most part combining geometries just entails concatenating the commands
// EXCEPT we need to adjust the first coordinate of each subsequent linestring to
// be an offset from the end of the previous linestring
result.ensureCapacity(result.elementsCount + commands.length);
// and multipoints will end up with only one command ("move to" with length=# points)
if (geometryType != GeometryType.POINT || result.isEmpty()) {
result.add(commands[0]);
}
result.add(zigZagEncode(zigZagDecode(commands[1]) - overallX));
result.add(zigZagEncode(zigZagDecode(commands[2]) - overallY));
if (commands.length > 3) {
result.add(commands, 3, commands.length - 3);
}

while (i < geometryCount) {
if (length <= 0) {
length = commands[i++];
command = length & ((1 << 3) - 1);
length = length >> 3;
}

if (length > 0) {
length--;
if (command != Command.CLOSE_PATH.value) {
x += zigZagDecode(commands[i++]);
y += zigZagDecode(commands[i++]);
}
}
}
overallX = x;
overallY = y;
}

public VectorGeometry finish() {
// set the correct "move to" length for multipoints based on how many points were actually added
if (geometryType == GeometryType.POINT) {
result.buffer[0] = Command.MOVE_TO.value | (((result.size() - 1) / 2) << 3);
}
return new VectorGeometry(result.toArray(), geometryType, 0);
}
}

/**
* A vector geometry encoded as a list of commands according to the
* <a href="https://github.com/mapbox/vector-tile-spec/tree/master/2.1#43-geometry-encoding">vector tile
Expand Down Expand Up @@ -759,6 +850,9 @@ public boolean isFillOrEdge(boolean allowEdges) {
return visitedEnoughSides(allowEdges, visited);
}

public boolean isEmpty() {
return commands.length == 0;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,8 @@ private static void unscale(List<VectorTile.Feature> features) {
if (feature != null) {
VectorTile.VectorGeometry geometry = feature.geometry();
if (geometry.scale() != 0) {
features.set(i, feature.copyWithNewGeometry(geometry.unscale()));
var unscaled = geometry.unscale();
features.set(i, unscaled.isEmpty() ? null : feature.copyWithNewGeometry(unscaled));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@
import com.carrotsearch.hppc.IntObjectMap;
import com.onthegomap.planetiler.collection.Hppc;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.geo.GeometryType;
import com.onthegomap.planetiler.mbtiles.Mbtiles;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
Expand Down Expand Up @@ -727,9 +728,9 @@ void mergeMultiline() throws GeometryException {

public static void main(String[] args) {
List<VectorTile.Feature> features = new ArrayList<>();
for (int i = 0; i < 1_000; i++) {
double[] points = IntStream.range(0, 100).mapToDouble(Double::valueOf).toArray();
var lineString = newLineString(points);
Random r = new Random(0);
for (int i = 0; i < 100_000; i++) {
var lineString = newPoint(r.nextDouble(256), r.nextDouble(256));
features.add(new VectorTile.Feature("layer", i, VectorTile.encodeGeometry(lineString), Map.of("a", 1)));
}
for (int j = 0; j < 10; j++) {
Expand All @@ -751,6 +752,7 @@ <S extends Geometry, M extends GeometryCollection> void testMultigeometryMerger(
var geom2 = generateGeometry.apply(2);
var geom3 = generateGeometry.apply(3);
var geom4 = generateGeometry.apply(4);
var geom5 = generateGeometry.apply(5);

assertTopologicallyEquivalentFeatures(
List.of(),
Expand All @@ -771,15 +773,17 @@ <S extends Geometry, M extends GeometryCollection> void testMultigeometryMerger(
assertTopologicallyEquivalentFeatures(
List.of(
feature(4, otherGeometry, Map.of("a", 1)),
feature(1, combineJTS.apply(List.of(geom1, geom2, geom3)), Map.of("a", 1)),
feature(3, geom4, Map.of("a", 2))
feature(1, combineJTS.apply(List.of(geom1, geom2, geom3, geom4)), Map.of("a", 1)),
feature(3, geom5, Map.of("a", 2))
),
merge.apply(
List.of(
feature(1, geom1, Map.of("a", 1)),
feature(2, combineJTS.apply(List.of(geom2, geom3)), Map.of("a", 1)),
feature(3, geom4, Map.of("a", 2)),
feature(4, otherGeometry, Map.of("a", 1))
feature(1, combineJTS.apply(List.of(geom1, geom2)), Map.of("a", 1)),
feature(2, combineJTS.apply(List.of(geom3, geom4)), Map.of("a", 1)),
feature(3, geom5, Map.of("a", 2)),
feature(4, otherGeometry, Map.of("a", 1)),
new VectorTile.Feature("layer", 5, new VectorTile.VectorGeometry(new int[0], GeometryType.typeOf(geom1), 0),
Map.of("a", 1))
)
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class PlanetilerTests {
private static final int Z13_TILES = 1 << 13;
private static final double Z13_WIDTH = 1d / Z13_TILES;
private static final int Z12_TILES = 1 << 12;
private static final double Z12_WIDTH = 1d / Z12_TILES;
private static final int Z4_TILES = 1 << 4;
private static final Polygon WORLD_POLYGON = newPolygon(
worldCoordinateList(
Expand Down Expand Up @@ -469,6 +470,33 @@ void testLineString() throws Exception {
), results.tiles);
}

@Test
void testLineStringDegenerateWhenUnscaled() throws Exception {
double x1 = 0.5 + Z12_WIDTH / 2;
double y1 = 0.5 + Z12_WIDTH / 2;
double x2 = x1 + Z12_WIDTH / 4096 / 3;
double y2 = y1 + Z12_WIDTH / 4096 / 3;
double lat1 = GeoUtils.getWorldLat(y1);
double lng1 = GeoUtils.getWorldLon(x1);
double lat2 = GeoUtils.getWorldLat(y2);
double lng2 = GeoUtils.getWorldLon(x2);

var results = runWithReaderFeatures(
Map.of("threads", "1"),
List.of(
newReaderFeature(newLineString(lng1, lat1, lng2, lat2), Map.of(
"attr", "value"
))
),
(in, features) -> features.line("layer")
.setZoomRange(12, 12)
.setMinPixelSize(0)
.setBufferPixels(4)
);

assertSubmap(Map.of(), results.tiles);
}

@Test
void testNumPointsAttr() throws Exception {
double x1 = 0.5 + Z14_WIDTH / 2;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
import static com.onthegomap.planetiler.TestUtils.*;
import static com.onthegomap.planetiler.geo.GeoUtils.JTS_FACTORY;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import com.google.common.primitives.Ints;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -517,6 +520,20 @@ Stream<DynamicTest> testScaleUnscale() throws NoninvertibleTransformationExcepti
));
}

@Test
void testUnscaleDegenerate() throws GeometryException {
var lessThanOnePx = 256d / 4096 / 4;
var encoded = VectorTile.encodeGeometry(newLineString(0, 0, lessThanOnePx, lessThanOnePx), 2);
assertEquals(6, encoded.commands().length);
var unscaled = encoded.unscale();
assertEquals(0, unscaled.commands().length);
assertFalse(encoded.isEmpty());
assertTrue(unscaled.isEmpty());
assertEquals(GeoUtils.EMPTY_GEOMETRY, unscaled.decode());
var reEncoded = VectorTile.encodeGeometry(unscaled.decode());
assertEquals(0, reEncoded.commands().length);
}

private void assertSameGeometry(Geometry expected, Geometry actual) {
if (expected.isEmpty() && actual.isEmpty()) {
// OK
Expand Down

0 comments on commit f5380b8

Please sign in to comment.