kopia lustrzana https://github.com/onthegomap/planetiler
merge polygons
rodzic
3ad8bb9f56
commit
103b5c43cf
|
@ -1,17 +1,27 @@
|
|||
package com.onthegomap.flatmap;
|
||||
|
||||
import com.carrotsearch.hppc.IntArrayList;
|
||||
import com.carrotsearch.hppc.IntObjectMap;
|
||||
import com.graphhopper.coll.GHIntObjectHashMap;
|
||||
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.BitSet;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import org.locationtech.jts.algorithm.Area;
|
||||
import org.locationtech.jts.geom.CoordinateSequence;
|
||||
import org.locationtech.jts.geom.Envelope;
|
||||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.GeometryCollection;
|
||||
import org.locationtech.jts.geom.LineString;
|
||||
import org.locationtech.jts.geom.Polygon;
|
||||
import org.locationtech.jts.operation.buffer.BufferOp;
|
||||
import org.locationtech.jts.operation.buffer.BufferParameters;
|
||||
import org.locationtech.jts.operation.linemerge.LineMerger;
|
||||
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -30,19 +40,8 @@ public class FeatureMerge {
|
|||
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();
|
||||
var groupedByAttrs = groupByAttrs(features, result, GeometryType.LINE);
|
||||
for (List<VectorTileEncoder.Feature> groupedFeatures : groupedByAttrs) {
|
||||
VectorTileEncoder.Feature feature1 = groupedFeatures.get(0);
|
||||
double lengthLimit = lengthLimitCalculator.apply(feature1.attrs());
|
||||
|
||||
|
@ -82,10 +81,7 @@ public class FeatureMerge {
|
|||
if (outputSegments.size() == 0) {
|
||||
// no segments to output - skip this feature
|
||||
} else {
|
||||
Geometry newGeometry =
|
||||
outputSegments.size() == 1 ?
|
||||
outputSegments.get(0) :
|
||||
GeoUtils.createMultiLineString(outputSegments);
|
||||
Geometry newGeometry = GeoUtils.combineLineStrings(outputSegments);
|
||||
result.add(feature1.copyWithNewGeometry(newGeometry));
|
||||
}
|
||||
}
|
||||
|
@ -129,8 +125,195 @@ public class FeatureMerge {
|
|||
}
|
||||
}
|
||||
|
||||
public static List<VectorTileEncoder.Feature> mergePolygons(List<VectorTileEncoder.Feature> items, double minSize,
|
||||
double minDist, double buffer) {
|
||||
return items;
|
||||
private static final BufferParameters bufferOps = new BufferParameters();
|
||||
|
||||
static {
|
||||
bufferOps.setJoinStyle(BufferParameters.JOIN_MITRE);
|
||||
}
|
||||
|
||||
public static List<VectorTileEncoder.Feature> mergePolygons(List<VectorTileEncoder.Feature> features, double minArea,
|
||||
double minDist, double buffer) throws GeometryException {
|
||||
List<VectorTileEncoder.Feature> result = new ArrayList<>(features.size());
|
||||
Collection<List<VectorTileEncoder.Feature>> groupedByAttrs = groupByAttrs(features, result, GeometryType.POLYGON);
|
||||
for (List<VectorTileEncoder.Feature> groupedFeatures : groupedByAttrs) {
|
||||
List<Polygon> outPolygons = new ArrayList<>();
|
||||
VectorTileEncoder.Feature feature1 = groupedFeatures.get(0);
|
||||
List<Geometry> geometries = new ArrayList<>(groupedFeatures.size());
|
||||
for (var feature : groupedFeatures) {
|
||||
geometries.add(feature.geometry().decode());
|
||||
}
|
||||
Collection<List<Geometry>> groupedByProximity = groupPolygonsByProximity(geometries, minDist);
|
||||
for (List<Geometry> polygonGroup : groupedByProximity) {
|
||||
Geometry merged;
|
||||
if (polygonGroup.size() > 1) {
|
||||
merged = GeoUtils.createGeometryCollection(polygonGroup);
|
||||
merged = new BufferOp(merged, bufferOps).getResultGeometry(buffer);
|
||||
if (buffer > 0) {
|
||||
merged = new BufferOp(merged, bufferOps).getResultGeometry(-buffer);
|
||||
}
|
||||
if (!(merged instanceof Polygon poly)
|
||||
|| Area.ofRing(poly.getExteriorRing().getCoordinateSequence()) < minArea) {
|
||||
continue;
|
||||
}
|
||||
merged = GeoUtils.snapAndFixPolygon(merged).reverse();
|
||||
} else {
|
||||
merged = polygonGroup.get(0);
|
||||
if (!(merged instanceof Polygon poly)
|
||||
|| Area.ofRing(poly.getExteriorRing().getCoordinateSequence()) < minArea) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// TODO VW simplify?
|
||||
// TODO limit inner ring area
|
||||
extractPolygons(merged, outPolygons);
|
||||
}
|
||||
if (!outPolygons.isEmpty()) {
|
||||
Geometry combined = GeoUtils.combinePolygons(outPolygons);
|
||||
result.add(feature1.copyWithNewGeometry(combined));
|
||||
}
|
||||
// TODO:
|
||||
// - for each geom, find all other geoms within minDist distance
|
||||
// - grow the groups
|
||||
// - for each group, buffer/unbuffer
|
||||
|
||||
// STRtree
|
||||
// for (int i = 0; i < groupedFeatures.size(); i++) {
|
||||
// Geometry poly = allGeoms[i] = groupedFeatures.get(i).geometry().decode();
|
||||
// Envelope env = poly.getEnvelopeInternal();
|
||||
// env.expandBy(buffer);
|
||||
// index.add(envs[i] = env);
|
||||
// }
|
||||
// index.finish();
|
||||
// IntSet visited = new IntHashSet();
|
||||
// IntArrayDeque queue = new IntArrayDeque();
|
||||
// for (int i = 0; i < groupedFeatures.size(); i++) {
|
||||
// if (!visited.contains(i)) {
|
||||
// List<Geometry> thisGroup = new ArrayList<>();
|
||||
// queue.addFirst(i);
|
||||
// visited.add(i);
|
||||
// while (!queue.isEmpty()) {
|
||||
// int toVisit = queue.removeLast();
|
||||
// thisGroup.add(allGeoms[toVisit]);
|
||||
// IntArrayList matches = index.search(envs[toVisit]);
|
||||
// for (int k = 0; k < matches.size(); k++) {
|
||||
// int match = matches.get(k);
|
||||
// // TODO test if distance < epsilon
|
||||
// if (!visited.contains(match)) {
|
||||
// visited.add(match);
|
||||
// queue.addFirst(match);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Geometry geom;
|
||||
// if (thisGroup.size() > 1) {
|
||||
// geom = OSMToVectorTiles.gf.createGeometryCollection(GeometryFactory.toGeometryArray(thisGroup));
|
||||
// geom = new BufferOp(geom, bufferOps).getResultGeometry(buffer).union();
|
||||
// if (buffer > 0) {
|
||||
// geom = new BufferOp(geom, bufferOps).getResultGeometry(-buffer);
|
||||
// }
|
||||
// } else {
|
||||
// geom = thisGroup.get(0);
|
||||
// }
|
||||
// // TODO VW simplify
|
||||
// // TODO limit inner ring area
|
||||
// extractPolygons(feature1, result, geom, minSize);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void extractPolygons(Geometry geom, List<Polygon> result) {
|
||||
if (geom instanceof Polygon poly) {
|
||||
result.add(poly);
|
||||
} else if (geom instanceof GeometryCollection) {
|
||||
for (int i = 0; i < geom.getNumGeometries(); i++) {
|
||||
extractPolygons(geom.getGeometryN(i), result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IntObjectMap<IntArrayList> extractAdjacencyList(List<Geometry> geometries, double minDist) {
|
||||
// TODO use spatial index
|
||||
IntObjectMap<IntArrayList> result = new GHIntObjectHashMap<>();
|
||||
for (int i = 0; i < geometries.size(); i++) {
|
||||
Geometry a = geometries.get(i);
|
||||
for (int j = 0; j < i; j++) {
|
||||
Geometry b = geometries.get(j);
|
||||
if (a.isWithinDistance(b, minDist)) {
|
||||
put(result, i, j);
|
||||
put(result, j, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void put(IntObjectMap<IntArrayList> result, int from, int to) {
|
||||
IntArrayList ilist = result.get(from);
|
||||
if (ilist == null) {
|
||||
result.put(from, ilist = new IntArrayList());
|
||||
}
|
||||
ilist.add(to);
|
||||
}
|
||||
|
||||
static List<IntArrayList> extractConnectedComponents(IntObjectMap<IntArrayList> adjacencyList, int numItems) {
|
||||
List<IntArrayList> result = new ArrayList<>();
|
||||
BitSet visited = new BitSet(numItems);
|
||||
|
||||
for (int i = 0; i < numItems; i++) {
|
||||
if (!visited.get(i)) {
|
||||
visited.set(i, true);
|
||||
IntArrayList group = new IntArrayList();
|
||||
group.add(i);
|
||||
result.add(group);
|
||||
dfs(i, group, adjacencyList, visited);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void dfs(int start, IntArrayList group, IntObjectMap<IntArrayList> adjacencyList, BitSet visited) {
|
||||
IntArrayList adjacent = adjacencyList.get(start);
|
||||
if (adjacent != null) {
|
||||
for (var cursor : adjacent) {
|
||||
int index = cursor.value;
|
||||
if (!visited.get(index)) {
|
||||
visited.set(index, true);
|
||||
group.add(index);
|
||||
dfs(index, group, adjacencyList, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Collection<List<Geometry>> groupPolygonsByProximity(List<Geometry> geometries, double minDist) {
|
||||
IntObjectMap<IntArrayList> adjacencyList = extractAdjacencyList(geometries, minDist);
|
||||
List<IntArrayList> groups = extractConnectedComponents(adjacencyList, geometries.size());
|
||||
return groups.stream().map(ids -> {
|
||||
List<Geometry> geomsInGroup = new ArrayList<>(ids.size());
|
||||
for (var cursor : ids) {
|
||||
geomsInGroup.add(geometries.get(cursor.value));
|
||||
}
|
||||
return geomsInGroup;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
private static Collection<List<VectorTileEncoder.Feature>> groupByAttrs(
|
||||
List<VectorTileEncoder.Feature> features, List<VectorTileEncoder.Feature> result, GeometryType geometryType) {
|
||||
LinkedHashMap<Map<String, Object>, List<VectorTileEncoder.Feature>> groupedByAttrs = new LinkedHashMap<>();
|
||||
for (VectorTileEncoder.Feature feature : features) {
|
||||
if (feature.geometry().geomType() != geometryType) {
|
||||
// just ignore and pass through non-polygon features
|
||||
result.add(feature);
|
||||
} else {
|
||||
groupedByAttrs
|
||||
.computeIfAbsent(feature.attrs(), k -> new ArrayList<>())
|
||||
.add(feature);
|
||||
}
|
||||
}
|
||||
return groupedByAttrs.values();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ public class VectorTileEncoder {
|
|||
if (first) {
|
||||
first = false;
|
||||
outerCCW = ccw;
|
||||
assert outerCCW;
|
||||
assert outerCCW : "outer ring is not counter-clockwise";
|
||||
}
|
||||
if (ccw == outerCCW) {
|
||||
ringsForCurrentPolygon = new ArrayList<>();
|
||||
|
|
|
@ -32,6 +32,7 @@ public class GeoUtils {
|
|||
public static final WKBReader wkbReader = new WKBReader(JTS_FACTORY);
|
||||
|
||||
private static final LineString[] EMPTY_LINE_STRING_ARRAY = new LineString[0];
|
||||
private static final Polygon[] EMPTY_POLYGON_ARRAY = new Polygon[0];
|
||||
private static final Coordinate[] EMPTY_COORD_ARRAY = new Coordinate[0];
|
||||
private static final Point[] EMPTY_POINT_ARRAY = new Point[0];
|
||||
|
||||
|
@ -168,6 +169,10 @@ public class GeoUtils {
|
|||
return JTS_FACTORY.createMultiLineString(lineStrings.toArray(EMPTY_LINE_STRING_ARRAY));
|
||||
}
|
||||
|
||||
public static Geometry createMultiPolygon(List<Polygon> polygon) {
|
||||
return JTS_FACTORY.createMultiPolygon(polygon.toArray(EMPTY_POLYGON_ARRAY));
|
||||
}
|
||||
|
||||
public static Geometry fixPolygon(Geometry geom) throws GeometryException {
|
||||
try {
|
||||
return geom.buffer(0);
|
||||
|
@ -176,6 +181,25 @@ public class GeoUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static Geometry combineLineStrings(List<LineString> lineStrings) {
|
||||
return lineStrings.size() == 1 ? lineStrings.get(0) : createMultiLineString(lineStrings);
|
||||
}
|
||||
|
||||
public static Geometry combinePolygons(List<Polygon> polys) {
|
||||
return polys.size() == 1 ? polys.get(0) : createMultiPolygon(polys);
|
||||
}
|
||||
|
||||
public static Geometry combinePoints(List<Point> points) {
|
||||
return points.size() == 1 ? points.get(0) : createMultiPoint(points);
|
||||
}
|
||||
|
||||
|
||||
public static final PrecisionModel TILE_PRECISON = new PrecisionModel(4096d / 256d);
|
||||
|
||||
public static Geometry snapAndFixPolygon(Geometry geom) throws GeometryException {
|
||||
return snapAndFixPolygon(geom, TILE_PRECISON);
|
||||
}
|
||||
|
||||
public static Geometry snapAndFixPolygon(Geometry geom, PrecisionModel tilePrecision) throws GeometryException {
|
||||
try {
|
||||
return GeometryPrecisionReducer.reduce(geom, tilePrecision);
|
||||
|
@ -251,6 +275,10 @@ public class GeoUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static Geometry createGeometryCollection(List<Geometry> polygonGroup) {
|
||||
return JTS_FACTORY.createGeometryCollection(polygonGroup.toArray(Geometry[]::new));
|
||||
}
|
||||
|
||||
private static record PolyAndArea(Polygon poly, double area) implements Comparable<PolyAndArea> {
|
||||
|
||||
PolyAndArea(Polygon poly) {
|
||||
|
|
|
@ -79,7 +79,7 @@ class CoordinateSequenceExtractor {
|
|||
}
|
||||
}
|
||||
}
|
||||
return lineStrings.size() == 1 ? lineStrings.get(0) : GeoUtils.createMultiLineString(lineStrings);
|
||||
return GeoUtils.combineLineStrings(lineStrings);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
@ -120,6 +120,6 @@ class CoordinateSequenceExtractor {
|
|||
}
|
||||
}
|
||||
}
|
||||
return points.size() == 1 ? points.get(0) : GeoUtils.createMultiPoint(points);
|
||||
return GeoUtils.combinePoints(points);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.onthegomap.flatmap.render;
|
||||
|
||||
import static com.onthegomap.flatmap.geo.GeoUtils.TILE_PRECISON;
|
||||
|
||||
import com.onthegomap.flatmap.CommonParams;
|
||||
import com.onthegomap.flatmap.FeatureCollector;
|
||||
import com.onthegomap.flatmap.TileExtents;
|
||||
|
@ -23,7 +25,6 @@ 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.simplify.DouglasPeuckerSimplifier;
|
||||
|
@ -35,7 +36,6 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature> {
|
|||
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,
|
||||
|
@ -172,7 +172,7 @@ public class FeatureRenderer implements Consumer<FeatureCollector.Feature> {
|
|||
Geometry geom;
|
||||
if (feature.area()) {
|
||||
geom = CoordinateSequenceExtractor.reassemblePolygons(geoms);
|
||||
geom = GeoUtils.snapAndFixPolygon(geom, tilePrecision);
|
||||
geom = GeoUtils.snapAndFixPolygon(geom, TILE_PRECISON);
|
||||
// JTS utilities "fix" the geometry to be clockwise outer/CCW inner
|
||||
geom = geom.reverse();
|
||||
} else {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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 com.onthegomap.flatmap.TestUtils.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.carrotsearch.hppc.IntArrayList;
|
||||
import com.carrotsearch.hppc.IntObjectMap;
|
||||
import com.graphhopper.coll.GHIntObjectHashMap;
|
||||
import com.onthegomap.flatmap.geo.GeometryException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -222,4 +222,301 @@ public class FeatureMergeTest {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* POLYGON MERGE TESTS
|
||||
*/
|
||||
|
||||
@Test
|
||||
public void mergePolygonEmptyList() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(),
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dontMergeDisconnectedPolygons() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, newMultiPolygon(
|
||||
rectangle(10, 20),
|
||||
rectangle(22, 10, 30, 20)
|
||||
), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(22, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dontMergeConnectedPolygonsWithDifferentAttrs() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(20, 10, 30, 20), Map.of("b", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(20, 10, 30, 20), Map.of("b", 1))
|
||||
),
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeConnectedPolygonsWithSameAttrs() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(20, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
0,
|
||||
0,
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergeMultiPolygons() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 10, 40, 20), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, newMultiPolygon(
|
||||
rectangle(10, 20),
|
||||
rectangle(30, 10, 40, 20)
|
||||
), Map.of("a", 1)),
|
||||
feature(2, rectangle(15, 10, 35, 20), Map.of("a", 1))
|
||||
),
|
||||
0,
|
||||
0,
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergePolygonsIgnoreNonPolygons() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(2, newLineString(20, 10, 30, 20), Map.of("a", 1)),
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, newLineString(20, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mergePolygonsWithinMinDist() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(20.9, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
0,
|
||||
1,
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dontMergePolygonsAboveMinDist() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, newMultiPolygon(
|
||||
rectangle(10, 20),
|
||||
rectangle(21.1, 10, 30, 20)
|
||||
), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(21.1, 10, 30, 20), Map.of("a", 1))
|
||||
),
|
||||
0,
|
||||
1,
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removePolygonsBelowMinSize() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(30, 10, 36, 20), Map.of("a", 1)),
|
||||
feature(3, rectangle(35, 10, 40, 20), Map.of("a", 1))
|
||||
),
|
||||
101,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allowPolygonsAboveMinSize() throws GeometryException {
|
||||
assertEquivalentFeatures(
|
||||
List.of(
|
||||
feature(1, newMultiPolygon(
|
||||
rectangle(10, 20),
|
||||
rectangle(30, 10, 40, 20)
|
||||
), Map.of("a", 1))
|
||||
),
|
||||
FeatureMerge.mergePolygons(
|
||||
List.of(
|
||||
feature(1, rectangle(10, 20), Map.of("a", 1)),
|
||||
feature(2, rectangle(30, 10, 36, 20), Map.of("a", 1)),
|
||||
feature(3, rectangle(35, 10, 40, 20), Map.of("a", 1))
|
||||
),
|
||||
99,
|
||||
0,
|
||||
1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertEquivalentFeatures(List<VectorTileEncoder.Feature> expected,
|
||||
List<VectorTileEncoder.Feature> actual) throws GeometryException {
|
||||
for (var feature : actual) {
|
||||
Geometry geom = feature.geometry().decode();
|
||||
TestUtils.validateGeometry(geom);
|
||||
}
|
||||
assertEquals(
|
||||
expected.stream().map(f -> f.copyWithNewGeometry(newPoint(0, 0))).toList(),
|
||||
actual.stream().map(f -> f.copyWithNewGeometry(newPoint(0, 0))).toList(),
|
||||
"comparison without geometries"
|
||||
);
|
||||
assertEquals(
|
||||
expected.stream().map(f -> new NormGeometry(silence(() -> f.geometry().decode()))).toList(),
|
||||
actual.stream().map(f -> new NormGeometry(silence(() -> f.geometry().decode()))).toList(),
|
||||
"geometry comparison"
|
||||
);
|
||||
}
|
||||
|
||||
private interface SupplierThatThrows<T> {
|
||||
|
||||
T get() throws Exception;
|
||||
}
|
||||
|
||||
private static <T> T silence(SupplierThatThrows<T> fn) {
|
||||
try {
|
||||
return fn.get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
private static IntObjectMap<IntArrayList> adjacencyListFromGroups(List<Integer>... groups) {
|
||||
IntObjectMap<IntArrayList> result = new GHIntObjectHashMap<>();
|
||||
for (List<Integer> group : groups) {
|
||||
for (int i = 0; i < group.size(); i++) {
|
||||
Integer a = group.get(i);
|
||||
for (int j = 0; j < i; j++) {
|
||||
Integer b = group.get(j);
|
||||
var aval = result.getOrDefault(a, new IntArrayList());
|
||||
aval.add(b);
|
||||
result.put(a, aval);
|
||||
|
||||
var bval = result.getOrDefault(b, new IntArrayList());
|
||||
bval.add(a);
|
||||
result.put(b, bval);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractConnectedComponentsEmpty() {
|
||||
assertEquals(
|
||||
List.of(), FeatureMerge.extractConnectedComponents(new GHIntObjectHashMap<>(), 0)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractConnectedComponentsOne() {
|
||||
assertEquals(
|
||||
List.of(
|
||||
IntArrayList.from(0)
|
||||
), FeatureMerge.extractConnectedComponents(new GHIntObjectHashMap<>(), 1)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractConnectedComponentsTwoDisconnected() {
|
||||
assertEquals(
|
||||
List.of(
|
||||
IntArrayList.from(0),
|
||||
IntArrayList.from(1)
|
||||
), FeatureMerge.extractConnectedComponents(new GHIntObjectHashMap<>(), 2)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractConnectedComponentsTwoConnected() {
|
||||
assertEquals(
|
||||
List.of(
|
||||
IntArrayList.from(0, 1)
|
||||
), FeatureMerge.extractConnectedComponents(adjacencyListFromGroups(
|
||||
List.of(0, 1)
|
||||
), 2)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractConnectedComponents() {
|
||||
assertEquals(
|
||||
List.of(
|
||||
IntArrayList.from(0, 1, 2, 3),
|
||||
IntArrayList.from(4),
|
||||
IntArrayList.from(5, 6)
|
||||
), FeatureMerge.extractConnectedComponents(adjacencyListFromGroups(
|
||||
List.of(0, 1, 2, 3),
|
||||
List.of(4),
|
||||
List.of(5, 6)
|
||||
), 7)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +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 java.util.stream.DoubleStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
@ -43,7 +43,6 @@ import org.locationtech.jts.geom.Coordinate;
|
|||
import org.locationtech.jts.geom.Geometry;
|
||||
import org.locationtech.jts.geom.MultiPolygon;
|
||||
import org.locationtech.jts.io.InputStreamInStream;
|
||||
import org.locationtech.jts.io.ParseException;
|
||||
import org.locationtech.jts.io.WKBReader;
|
||||
|
||||
/**
|
||||
|
@ -445,6 +444,10 @@ public class FlatMapTest {
|
|||
return points;
|
||||
}
|
||||
|
||||
public List<Coordinate> z14CoordinatePixelList(double... coords) {
|
||||
return z14CoordinateList(DoubleStream.of(coords).map(c -> c / 256d).toArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPolygonWithHoleSpanningMultipleTiles() throws Exception {
|
||||
List<Coordinate> outerPoints = z14CoordinateList(
|
||||
|
@ -595,7 +598,7 @@ public class FlatMapTest {
|
|||
"njshore.wkb, 10571"
|
||||
})
|
||||
public void testComplexShorelinePolygons__TAKES_A_MINUTE_OR_TWO(String fileName, int expected)
|
||||
throws Exception, ParseException {
|
||||
throws Exception {
|
||||
MultiPolygon geometry = (MultiPolygon) new WKBReader()
|
||||
.read(new InputStreamInStream(Files.newInputStream(Path.of("src", "test", "resources", fileName))));
|
||||
assertNotNull(geometry);
|
||||
|
@ -988,37 +991,36 @@ public class FlatMapTest {
|
|||
}
|
||||
|
||||
@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(
|
||||
// merge same group:
|
||||
new ReaderFeature(newPolygon(z14CoordinatePixelList(
|
||||
10, 10,
|
||||
20, 10,
|
||||
20, 20,
|
||||
10, 20,
|
||||
10, 10
|
||||
)), Map.of("group", "1")),
|
||||
new ReaderFeature(newLineString(
|
||||
new ReaderFeature(newPolygon(z14CoordinatePixelList(
|
||||
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(
|
||||
)), Map.of("group", "1")),
|
||||
// don't merge - different group:
|
||||
new ReaderFeature(newPolygon(z14CoordinatePixelList(
|
||||
10, 20.5,
|
||||
20, 20.5,
|
||||
20, 30,
|
||||
10, 30,
|
||||
10, 20.5
|
||||
), Map.of("group", "2"))
|
||||
)), Map.of("group", "2"))
|
||||
),
|
||||
(in, features) -> {
|
||||
features.line("layer")
|
||||
features.polygon("layer")
|
||||
.setZoomRange(14, 14)
|
||||
.inheritFromSource("group");
|
||||
},
|
||||
|
|
|
@ -121,6 +121,10 @@ public class TestUtils {
|
|||
return rectangle(min, min, max, max);
|
||||
}
|
||||
|
||||
public static Polygon newPolygon(List<Coordinate> outer) {
|
||||
return newPolygon(outer, List.of());
|
||||
}
|
||||
|
||||
public static Polygon newPolygon(List<Coordinate> outer, List<List<Coordinate>> inner) {
|
||||
return GeoUtils.JTS_FACTORY.createPolygon(
|
||||
GeoUtils.JTS_FACTORY.createLinearRing(outer.toArray(new Coordinate[0])),
|
||||
|
@ -281,7 +285,7 @@ public class TestUtils {
|
|||
|
||||
public static void validateGeometry(Geometry g) {
|
||||
if (g instanceof Polygonal) {
|
||||
assertTrue(g.isSimple(), "JTS isValid()");
|
||||
assertTrue(g.isSimple(), "JTS isSimple()");
|
||||
}
|
||||
validateGeometryRecursive(g);
|
||||
}
|
||||
|
@ -331,7 +335,7 @@ public class TestUtils {
|
|||
}
|
||||
}
|
||||
|
||||
private static record TopoGeometry(Geometry geom) implements GeometryComparision {
|
||||
public static record TopoGeometry(Geometry geom) implements GeometryComparision {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
|
Ładowanie…
Reference in New Issue