implement line merge

pull/1/head
Mike Barry 2021-06-01 06:29:55 -04:00
rodzic 0dacc1d33b
commit 3ad8bb9f56
6 zmienionych plików z 484 dodań i 13 usunięć

Wyświetl plik

@ -0,0 +1,136 @@
package com.onthegomap.flatmap;
import com.onthegomap.flatmap.collections.MutableCoordinateSequence;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.operation.linemerge.LineMerger;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FeatureMerge {
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureMerge.class);
public static List<VectorTileEncoder.Feature> mergeLineStrings(List<VectorTileEncoder.Feature> items,
double minLength, double tolerance, double clip) throws GeometryException {
return mergeLineStrings(items, attrs -> minLength, tolerance, clip);
}
public static List<VectorTileEncoder.Feature> mergeLineStrings(List<VectorTileEncoder.Feature> features,
Function<Map<String, Object>, Double> lengthLimitCalculator, double tolerance, double clip)
throws GeometryException {
List<VectorTileEncoder.Feature> result = new ArrayList<>(features.size());
LinkedHashMap<Map<String, Object>, List<VectorTileEncoder.Feature>> groupedByAttrs = new LinkedHashMap<>();
for (VectorTileEncoder.Feature feature : features) {
if (feature.geometry().geomType() != GeometryType.LINE) {
// just ignore and pass through non-linestring features
result.add(feature);
} else {
groupedByAttrs
.computeIfAbsent(feature.attrs(), k -> new ArrayList<>())
.add(feature);
}
}
for (var entry : groupedByAttrs.entrySet()) {
List<VectorTileEncoder.Feature> groupedFeatures = entry.getValue();
VectorTileEncoder.Feature feature1 = groupedFeatures.get(0);
double lengthLimit = lengthLimitCalculator.apply(feature1.attrs());
// as a shortcut, can skip line merging only if there is:
// - only 1 element in the group
// - it doesn't need to be clipped
// - it can't possibly be filtered out for being too short
if (groupedFeatures.size() == 1 && clip == 0d && lengthLimit == 0) {
result.add(feature1);
} else {
LineMerger merger = new LineMerger();
for (VectorTileEncoder.Feature feature : groupedFeatures) {
merger.add(feature.geometry().decode());
}
List<LineString> outputSegments = new ArrayList<>();
for (Object merged : merger.getMergedLineStrings()) {
if (merged instanceof LineString line && line.getLength() >= lengthLimit) {
// re-simplify since some endpoints of merged segments may be unnecessary
if (line.getNumPoints() > 2) {
DouglasPeuckerSimplifier simplifier = new DouglasPeuckerSimplifier(line);
simplifier.setDistanceTolerance(tolerance);
simplifier.setEnsureValid(false);
Geometry simplified = simplifier.getResultGeometry();
if (simplified instanceof LineString simpleLineString) {
line = simpleLineString;
} else {
LOGGER.warn("line string merge simplify emitted " + simplified.getGeometryType());
}
}
if (clip > 0) {
removeDetailOutsideTile(line, clip, outputSegments);
} else {
outputSegments.add(line);
}
}
}
if (outputSegments.size() == 0) {
// no segments to output - skip this feature
} else {
Geometry newGeometry =
outputSegments.size() == 1 ?
outputSegments.get(0) :
GeoUtils.createMultiLineString(outputSegments);
result.add(feature1.copyWithNewGeometry(newGeometry));
}
}
}
return result;
}
private static void removeDetailOutsideTile(LineString input, double buffer, List<LineString> output) {
MutableCoordinateSequence current = new MutableCoordinateSequence();
CoordinateSequence seq = input.getCoordinateSequence();
boolean wasIn = false;
double min = -buffer, max = 256 + buffer;
double x = seq.getX(0), y = seq.getY(0);
Envelope env = new Envelope();
Envelope outer = new Envelope(min, max, min, max);
for (int i = 0; i < seq.size() - 1; i++) {
double nextX = seq.getX(i + 1), nextY = seq.getY(i + 1);
env.init(x, nextX, y, nextY);
boolean nowIn = env.intersects(outer);
if (nowIn || wasIn) {
current.addPoint(x, y);
} else { // out
// wait to flush until 2 consecutive outs
if (!current.isEmpty()) {
output.add(GeoUtils.JTS_FACTORY.createLineString(current));
current = new MutableCoordinateSequence();
}
}
wasIn = nowIn;
x = nextX;
y = nextY;
}
double lastX = seq.getX(seq.size() - 1), lastY = seq.getY(seq.size() - 1);
env.init(x, lastX, y, lastY);
if (env.intersects(outer) || wasIn) {
current.addPoint(lastX, lastY);
}
if (!current.isEmpty()) {
output.add(GeoUtils.JTS_FACTORY.createLineString(current));
}
}
public static List<VectorTileEncoder.Feature> mergePolygons(List<VectorTileEncoder.Feature> items, double minSize,
double minDist, double buffer) {
return items;
}
}

Wyświetl plik

@ -1,6 +1,7 @@
package com.onthegomap.flatmap;
import com.graphhopper.reader.ReaderRelation;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.read.OpenStreetMapReader;
import java.util.List;
@ -13,7 +14,7 @@ public interface Profile {
void release();
List<VectorTileEncoder.Feature> postProcessLayerFeatures(String layer, int zoom,
List<VectorTileEncoder.Feature> items);
List<VectorTileEncoder.Feature> items) throws GeometryException;
String name();

Wyświetl plik

@ -6,6 +6,7 @@ import com.onthegomap.flatmap.GeometryType;
import com.onthegomap.flatmap.LayerStats;
import com.onthegomap.flatmap.Profile;
import com.onthegomap.flatmap.VectorTileEncoder;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.geo.TileCoord;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.render.RenderedFeature;
@ -371,23 +372,26 @@ public final class FeatureGroup implements Consumer<FeatureSort.Entry>, Iterable
if (currentLayer == null) {
currentLayer = layer;
} else if (!currentLayer.equals(layer)) {
encoder.addLayerFeatures(
currentLayer,
profile.postProcessLayerFeatures(currentLayer, tile.z(), items)
);
emitLayer(encoder, items, currentLayer);
currentLayer = layer;
items.clear();
}
items.add(feature);
}
encoder.addLayerFeatures(
currentLayer,
profile.postProcessLayerFeatures(currentLayer, tile.z(), items)
);
emitLayer(encoder, items, currentLayer);
return encoder;
}
private void emitLayer(VectorTileEncoder encoder, List<VectorTileEncoder.Feature> items, String currentLayer) {
try {
items = profile.postProcessLayerFeatures(currentLayer, tile.z(), items);
} catch (GeometryException e) {
LOGGER.warn("error postprocessing features for " + currentLayer + " layer on " + tile + ": " + e.getMessage());
}
encoder.addLayerFeatures(currentLayer, items);
}
@Override
public void accept(FeatureSort.Entry entry) {
long sortKey = entry.sortKey();

Wyświetl plik

@ -77,6 +77,10 @@ public class MutableCoordinateSequence extends PackedCoordinateSequence {
}
}
public boolean isEmpty() {
return points.isEmpty();
}
private static class ScalingSequence extends MutableCoordinateSequence {
private final double scale;

Wyświetl plik

@ -0,0 +1,225 @@
package com.onthegomap.flatmap;
import static com.onthegomap.flatmap.TestUtils.newLineString;
import static com.onthegomap.flatmap.TestUtils.newMultiLineString;
import static com.onthegomap.flatmap.TestUtils.newPoint;
import static com.onthegomap.flatmap.TestUtils.rectangle;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.onthegomap.flatmap.geo.GeometryException;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Geometry;
public class FeatureMergeTest {
private VectorTileEncoder.Feature feature(long id, Geometry geom, Map<String, Object> attrs) {
return new VectorTileEncoder.Feature(
"layer",
id,
VectorTileEncoder.encodeGeometry(geom),
attrs
);
}
@Test
public void mergeMergeZeroLineStrings() throws GeometryException {
assertEquals(
List.of(),
FeatureMerge.mergeLineStrings(
List.of(),
0,
0,
0
)
);
}
@Test
public void mergeMergeOneLineStrings() throws GeometryException {
assertEquals(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of())
),
FeatureMerge.mergeLineStrings(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of())
),
0,
0,
0
)
);
}
@Test
public void dontMergeDisconnectedLineStrings() throws GeometryException {
assertEquals(
List.of(
feature(1, newMultiLineString(
newLineString(10, 10, 20, 20),
newLineString(30, 30, 40, 40)
), Map.of())
),
FeatureMerge.mergeLineStrings(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of()),
feature(2, newLineString(30, 30, 40, 40), Map.of())
),
0,
0,
0
)
);
}
@Test
public void dontMergeConnectedLineStringsDifferentAttr() throws GeometryException {
assertEquals(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of("a", 1)),
feature(2, newLineString(20, 20, 30, 30), Map.of("b", 2))
),
FeatureMerge.mergeLineStrings(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of("a", 1)),
feature(2, newLineString(20, 20, 30, 30), Map.of("b", 2))
),
0,
0,
0
)
);
}
@Test
public void mergeConnectedLineStringsSameAttrs() throws GeometryException {
assertEquals(
List.of(
feature(1, newLineString(10, 10, 30, 30), Map.of("a", 1))
),
FeatureMerge.mergeLineStrings(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of("a", 1)),
feature(2, newLineString(20, 20, 30, 30), Map.of("a", 1))
),
0,
0,
0
)
);
}
@Test
public void mergeMultiLineString() throws GeometryException {
assertEquals(
List.of(
feature(1, newLineString(10, 10, 40, 40), Map.of("a", 1))
),
FeatureMerge.mergeLineStrings(
List.of(
feature(1, newMultiLineString(
newLineString(10, 10, 20, 20),
newLineString(30, 30, 40, 40)
), Map.of("a", 1)),
feature(2, newLineString(20, 20, 30, 30), Map.of("a", 1))
),
0,
0,
0
)
);
}
@Test
public void mergeLineStringIgnoreNonLineString() throws GeometryException {
assertEquals(
List.of(
feature(3, newPoint(5, 5), Map.of("a", 1)),
feature(4, rectangle(50, 60), Map.of("a", 1)),
feature(1, newLineString(10, 10, 30, 30), Map.of("a", 1))
),
FeatureMerge.mergeLineStrings(
List.of(
feature(1, newLineString(10, 10, 20, 20), Map.of("a", 1)),
feature(2, newLineString(20, 20, 30, 30), Map.of("a", 1)),
feature(3, newPoint(5, 5), Map.of("a", 1)),
feature(4, rectangle(50, 60), Map.of("a", 1))
),
0,
0,
0
)
);
}
@Test
public void mergeLineStringRemoveDetailOutsideTile() throws GeometryException {
assertEquals(
List.of(
feature(1, newMultiLineString(
newLineString(
10, 10,
-10, 20,
10, 30,
-10, 40,
-10, 50,
10, 60,
-10, 70
),
newLineString(
-10, 100,
10, 100
)
), Map.of("a", 1))
),
FeatureMerge.mergeLineStrings(
List.of(
// one point goes out - dont clip
feature(1, newLineString(10, 10, -10, 20), Map.of("a", 1)),
feature(2, newLineString(-10, 20, 10, 30), Map.of("a", 1)),
feature(3, newLineString(10, 30, -10, 40), Map.of("a", 1)),
// two points goes out - dont clip
feature(4, newLineString(-10, 40, -10, 50), Map.of("a", 1)),
feature(5, newLineString(-10, 50, 10, 60), Map.of("a", 1)),
feature(5, newLineString(10, 60, -10, 70), Map.of("a", 1)),
// three points out - do clip
feature(6, newLineString(-10, 70, -10, 80), Map.of("a", 1)),
feature(7, newLineString(-10, 80, -11, 90), Map.of("a", 1)),
feature(8, newLineString(-10, 90, -10, 100), Map.of("a", 1)),
feature(9, newLineString(-10, 100, 10, 100), Map.of("a", 1))
),
0,
0,
1
)
);
}
@Test
public void mergeLineStringMinLength() throws GeometryException {
assertEquals(
List.of(
feature(2, newLineString(20, 20, 20, 25), Map.of("a", 1))
),
FeatureMerge.mergeLineStrings(
List.of(
// too short - omit entire feature
feature(1, newLineString(10, 10, 10, 14), Map.of("b", 1)),
// too short - omit from combined group
feature(2, newLineString(20, 10, 20, 12), Map.of("a", 1)),
feature(3, newLineString(20, 12, 20, 14), Map.of("a", 1)),
// just long enough
feature(4, newLineString(20, 20, 20, 24), Map.of("a", 1)),
feature(5, newLineString(20, 24, 20, 25), Map.of("a", 1))
),
5,
0,
0
)
);
}
}

Wyświetl plik

@ -13,6 +13,7 @@ import com.onthegomap.flatmap.collections.FeatureGroup;
import com.onthegomap.flatmap.collections.FeatureSort;
import com.onthegomap.flatmap.collections.LongLongMap;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import com.onthegomap.flatmap.geo.TileCoord;
import com.onthegomap.flatmap.monitoring.Stats;
import com.onthegomap.flatmap.read.OpenStreetMapReader;
@ -34,6 +35,7 @@ import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@ -171,7 +173,7 @@ public class FlatMapTest {
return run(
args,
(featureGroup, profile, config) -> processOsmFeatures(featureGroup, profile, config, features),
new TestProfile(profileFunction, preprocessOsmRelation, (a, b, c) -> null)
new TestProfile(profileFunction, preprocessOsmRelation, (a, b, c) -> c)
);
}
@ -933,6 +935,104 @@ public class FlatMapTest {
), results.tiles);
}
@Test
public void testMergeLineStrings() throws Exception {
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);
double lng4 = GeoUtils.getWorldLon(x1 + Z14_WIDTH * 30d / 256);
var results = runWithReaderFeatures(
Map.of("threads", "1"),
List.of(
// merge at z13 (same "group"):
new ReaderFeature(newLineString(
lng1, lat,
lng2, lat
), Map.of("group", "1", "other", "1")),
new ReaderFeature(newLineString(
lng2, lat,
lng3, lat
), Map.of("group", "1", "other", "2")),
// don't merge at z13:
new ReaderFeature(newLineString(
lng3, lat,
lng4, lat
), Map.of("group", "2", "other", "3"))
),
(in, features) -> {
features.line("layer")
.setZoomRange(13, 14)
.setAttrWithMinzoom("z14attr", in.getTag("other"), 14)
.inheritFromSource("group");
},
(layer, zoom, items) -> FeatureMerge.mergeLineStrings(items, 0, 0, 0)
);
assertSubmap(sortListValues(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
feature(newLineString(64, 128, 74, 128), Map.of("group", "1", "z14attr", "1")),
feature(newLineString(74, 128, 84, 128), Map.of("group", "1", "z14attr", "2")),
feature(newLineString(84, 128, 94, 128), Map.of("group", "2", "z14attr", "3"))
),
TileCoord.ofXYZ(Z13_TILES / 2, Z13_TILES / 2, 13), List.of(
// merge 32->37 and 37->42 since they have same attrs
feature(newLineString(32, 64, 42, 64), Map.of("group", "1")),
feature(newLineString(42, 64, 47, 64), Map.of("group", "2"))
)
)), sortListValues(results.tiles));
}
@Test
@Disabled
public void testMergePolygons() throws Exception {
var results = runWithReaderFeatures(
Map.of("threads", "1"),
List.of(
// merge at z13 (same "group"):
new ReaderFeature(newLineString(z14CoordinateList(
10, 10,
20, 10,
20, 20,
10, 20,
10, 10
)), Map.of("group", "1")),
new ReaderFeature(newLineString(
20.5, 10,
30, 10,
30, 20,
20.5, 20,
20.5, 10
), Map.of("group", "1")),
// don't merge at z13:
new ReaderFeature(newLineString(
10, 20.5,
20, 20.5,
20, 30,
10, 30,
10, 20.5
), Map.of("group", "2"))
),
(in, features) -> {
features.line("layer")
.setZoomRange(14, 14)
.inheritFromSource("group");
},
(layer, zoom, items) -> FeatureMerge.mergePolygons(items, 0, 1, 1)
);
assertSubmap(sortListValues(Map.of(
TileCoord.ofXYZ(Z14_TILES / 2, Z14_TILES / 2, 14), List.of(
feature(rectangle(10, 10, 30, 20), Map.of("group", "1")),
feature(rectangle(10, 20.5, 20, 30), Map.of("group", "2"))
)
)), sortListValues(results.tiles));
}
private <K extends Comparable<? super K>, V extends List<?>> Map<K, ?> sortListValues(Map<K, V> input) {
Map<K, List<?>> result = new TreeMap<>();
for (var entry : input.entrySet()) {
@ -954,7 +1054,8 @@ public class FlatMapTest {
private interface LayerPostprocessFunction {
List<VectorTileEncoder.Feature> process(String layer, int zoom, List<VectorTileEncoder.Feature> items);
List<VectorTileEncoder.Feature> process(String layer, int zoom, List<VectorTileEncoder.Feature> items)
throws GeometryException;
}
private static record FlatMapResults(
@ -982,7 +1083,7 @@ public class FlatMapTest {
}
static TestProfile processSourceFeatures(BiConsumer<SourceFeature, FeatureCollector> processFeature) {
return new TestProfile(processFeature, (a) -> null, (a, b, c) -> null);
return new TestProfile(processFeature, (a) -> null, (a, b, c) -> c);
}
@Override
@ -1002,7 +1103,7 @@ public class FlatMapTest {
@Override
public List<VectorTileEncoder.Feature> postProcessLayerFeatures(String layer, int zoom,
List<VectorTileEncoder.Feature> items) {
List<VectorTileEncoder.Feature> items) throws GeometryException {
return postprocessLayerFeatures.process(layer, zoom, items);
}
}