pull/1/head
Mike Barry 2021-05-30 07:42:06 -04:00
rodzic 09baa06e09
commit 318a314199
8 zmienionych plików z 1132 dodań i 128 usunięć

Wyświetl plik

@ -76,6 +76,15 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
}
}
public Feature validatedPolygon(String layer) {
try {
return geometry(layer, source.validatedPolygon());
} catch (GeometryException e) {
LOGGER.warn("Error constructing validated polygon for " + source + ": " + e);
return new Feature(layer, EMPTY_GEOM);
}
}
public Feature pointOnSurface(String layer) {
try {
return geometry(layer, source.pointOnSurface());

Wyświetl plik

@ -79,6 +79,23 @@ public abstract class SourceFeature {
return polygonGeometry != null ? polygonGeometry : (polygonGeometry = computePolygon());
}
private Geometry validPolygon = null;
private Geometry computeValidPolygon() throws GeometryException {
Geometry polygon = polygon();
if (!polygon.isValid()) {
polygon = GeoUtils.fixPolygon(polygon);
}
return polygon;
}
public final Geometry validatedPolygon() throws GeometryException {
if (!canBePolygon()) {
throw new GeometryException("cannot be polygon");
}
return validPolygon != null ? validPolygon : (validPolygon = computeValidPolygon());
}
private double area = Double.NaN;
public double area() throws GeometryException {

Wyświetl plik

@ -20,6 +20,7 @@ import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.PrecisionModel;
import org.locationtech.jts.geom.TopologyException;
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
import org.locationtech.jts.geom.util.GeometryTransformer;
import org.locationtech.jts.io.WKBReader;
@ -35,7 +36,7 @@ public class GeoUtils {
private static final Point[] EMPTY_POINT_ARRAY = new Point[0];
private static final double WORLD_RADIUS_METERS = 6_378_137;
private static final double WORLD_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS;
public static final double WORLD_CIRCUMFERENCE_METERS = Math.PI * 2 * WORLD_RADIUS_METERS;
private static final double DEGREES_TO_RADIANS = Math.PI / 180;
private static final double RADIANS_TO_DEGREES = 180 / Math.PI;
private static final double MAX_LAT = getWorldLat(-0.1);
@ -167,18 +168,26 @@ public class GeoUtils {
return JTS_FACTORY.createMultiLineString(lineStrings.toArray(EMPTY_LINE_STRING_ARRAY));
}
public static Geometry fixPolygon(Geometry geom) throws GeometryException {
try {
return geom.buffer(0);
} catch (TopologyException e) {
throw new GeometryException("robustness error fixing polygon: " + e);
}
}
public static Geometry snapAndFixPolygon(Geometry geom, PrecisionModel tilePrecision) throws GeometryException {
try {
return GeometryPrecisionReducer.reduce(geom, tilePrecision);
} catch (IllegalArgumentException e) {
// precision reduction fails if geometry is invalid, so attempt
// to fix it then try again
geom = geom.buffer(0);
geom = fixPolygon(geom);
try {
return GeometryPrecisionReducer.reduce(geom, tilePrecision);
} catch (IllegalArgumentException e2) {
// give it one last try, just in case
geom = geom.buffer(0);
geom = fixPolygon(geom);
try {
return GeometryPrecisionReducer.reduce(geom, tilePrecision);
} catch (IllegalArgumentException e3) {

Wyświetl plik

@ -27,15 +27,24 @@ import com.onthegomap.flatmap.render.FeatureRenderer;
import com.onthegomap.flatmap.worker.Topology;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateList;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.impl.CoordinateArraySequence;
import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstimate {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenStreetMapReader.class);
private final OsmSource osmInputFile;
private final Stats stats;
private final LongLongMap nodeDb;
@ -134,7 +143,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
.<FeatureSort.Entry>addWorker("process", processThreads, (prev, next) -> {
ReaderElement readerElement;
var featureCollectors = new FeatureCollector.Factory(config);
NodeGeometryCache nodeCache = newNodeGeometryCache();
NodeLocationProvider nodeCache = newNodeGeometryCache();
var encoder = writer.newRenderedFeatureEncoder();
FeatureRenderer renderer = new FeatureRenderer(
config,
@ -155,7 +164,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
waysDone.await();
}
relsProcessed.incrementAndGet();
feature = processRelationPass2(rel);
feature = processRelationPass2(rel, nodeCache);
}
if (feature != null) {
FeatureCollector features = featureCollectors.get(feature);
@ -187,12 +196,17 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
topology.awaitAndLog(logger, config.logInterval());
}
SourceFeature processRelationPass2(ReaderRelation rel) {
return rel.hasTag("type", "multipolygon") ? new MultipolygonSourceFeature(rel) : null;
SourceFeature processRelationPass2(ReaderRelation rel, NodeLocationProvider nodeCache) {
return rel.hasTag("type", "multipolygon") ? new MultipolygonSourceFeature(rel, nodeCache) : null;
}
SourceFeature processWayPass2(NodeGeometryCache nodeCache, ReaderWay way) {
SourceFeature processWayPass2(NodeLocationProvider nodeCache, ReaderWay way) {
LongArrayList nodes = way.getNodes();
if (waysInMultipolygon.contains(way.getId())) {
synchronized (multipolygonWayGeometries) {
multipolygonWayGeometries.putAll(way.getId(), nodes);
}
}
boolean closed = nodes.size() > 1 && nodes.get(0) == nodes.get(nodes.size() - 1);
String area = way.getTag("area");
return new WaySourceFeature(way, closed, area, nodeCache);
@ -305,10 +319,10 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
private class WaySourceFeature extends ProxyFeature {
private final NodeGeometryCache nodeCache;
private final NodeLocationProvider nodeCache;
private final LongArrayList nodeIds;
public WaySourceFeature(ReaderWay way, boolean closed, String area, NodeGeometryCache nodeCache) {
public WaySourceFeature(ReaderWay way, boolean closed, String area, NodeLocationProvider nodeCache) {
super(way, false,
(!closed || !"yes".equals(area)) && way.getNodes().size() >= 2,
(closed && !"no".equals(area)) && way.getNodes().size() >= 4
@ -341,39 +355,79 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
protected Geometry computeWorldGeometry() throws GeometryException {
return canBePolygon() ? polygon() : line();
}
@Override
public boolean isPoint() {
return false;
}
}
private class MultipolygonSourceFeature extends ProxyFeature {
public MultipolygonSourceFeature(ReaderRelation relation) {
private final ReaderRelation relation;
private final NodeLocationProvider nodeCache;
public MultipolygonSourceFeature(ReaderRelation relation, NodeLocationProvider nodeCache) {
super(relation, false, false, true);
this.relation = relation;
this.nodeCache = nodeCache;
}
@Override
protected Geometry computeWorldGeometry() {
return null;
}
@Override
public boolean isPoint() {
return false;
protected Geometry computeWorldGeometry() throws GeometryException {
List<LongArrayList> rings = new ArrayList<>(relation.getMembers().size());
for (ReaderRelation.Member member : relation.getMembers()) {
LongArrayList poly = multipolygonWayGeometries.get(member.getRef());
if (poly != null && !poly.isEmpty()) {
rings.add(poly);
} else {
LOGGER
.warn("Missing " + member.getRole() + " way " + member.getRef() + " for multipolygon relation " + osmId);
}
}
return OsmMultipolygon.build(rings, nodeCache, osmId);
}
}
NodeGeometryCache newNodeGeometryCache() {
NodeLocationProvider newNodeGeometryCache() {
return new NodeGeometryCache();
}
class NodeGeometryCache {
public interface NodeLocationProvider {
default CoordinateSequence getWayGeometry(LongArrayList nodeIds) {
CoordinateList coordList = new CoordinateList();
for (var cursor : nodeIds) {
coordList.add(getCoordinate(cursor.value));
}
return new CoordinateArraySequence(coordList.toCoordinateArray());
}
Coordinate getCoordinate(long id);
default void reset() {
}
}
private class NodeGeometryCache implements NodeLocationProvider {
private final LongDoubleHashMap xs = new LongDoubleHashMap();
private final LongDoubleHashMap ys = new LongDoubleHashMap();
@Override
public Coordinate getCoordinate(long id) {
double worldX, worldY;
worldX = xs.getOrDefault(id, Double.NaN);
if (Double.isNaN(worldX)) {
long encoded = nodeDb.get(id);
if (encoded == LongLongMap.MISSING_VALUE) {
throw new IllegalArgumentException("Missing location for node: " + id);
}
xs.put(id, worldX = GeoUtils.decodeWorldX(encoded));
ys.put(id, worldY = GeoUtils.decodeWorldY(encoded));
} else {
worldY = ys.get(id);
}
return new CoordinateXY(worldX, worldY);
}
@Override
public CoordinateSequence getWayGeometry(LongArrayList nodeIds) {
int num = nodeIds.size();
CoordinateSequence seq = new PackedCoordinateSequence.Double(nodeIds.size(), 2, 0);
@ -399,6 +453,7 @@ public class OpenStreetMapReader implements Closeable, MemoryEstimator.HasEstima
return seq;
}
@Override
public void reset() {
xs.clear();
ys.clear();

Wyświetl plik

@ -0,0 +1,252 @@
/*****************************************************************
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
****************************************************************/
package com.onthegomap.flatmap.read;
import com.carrotsearch.hppc.LongArrayList;
import com.carrotsearch.hppc.LongObjectMap;
import com.carrotsearch.hppc.cursors.LongObjectCursor;
import com.graphhopper.coll.GHLongObjectHashMap;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.prep.PreparedPolygon;
/**
* This class is ported to Java from https://github.com/omniscale/imposm3/blob/master/geom/multipolygon.go and
* https://github.com/omniscale/imposm3/blob/master/geom/ring.go
*/
class OsmMultipolygon {
private static final double MIN_CLOSE_RING_GAP = 0.1 / GeoUtils.WORLD_CIRCUMFERENCE_METERS;
private static final Comparator<Ring> BY_AREA_DESCENDING = Comparator.comparingDouble(ring -> -ring.area);
private static class Ring {
private final Polygon geom;
private final double area;
private Ring containedBy = null;
private final Set<Ring> holes = new HashSet<>();
private Ring(Polygon geom) {
this.geom = geom;
this.area = geom.getArea();
}
public Polygon toPolygon() {
return GeoUtils.JTS_FACTORY.createPolygon(
geom.getExteriorRing(),
holes.stream().map(ring -> ring.geom.getExteriorRing()).toArray(LinearRing[]::new)
);
}
public boolean isHole() {
int containedCounter = 0;
for (Ring ring = this; ring != null; ring = ring.containedBy) {
containedCounter++;
}
return containedCounter % 2 == 0;
}
}
public static Geometry build(
List<LongArrayList> rings,
OpenStreetMapReader.NodeLocationProvider nodeCache,
long osmId
) throws GeometryException {
try {
if (rings.size() == 0) {
throw new IllegalArgumentException("no rings to process");
}
List<LongArrayList> idSegments = connectPolygonSegments(rings);
List<Ring> polygons = new ArrayList<>(idSegments.size());
for (LongArrayList segment : idSegments) {
int size = segment.size();
long firstId = segment.get(0), lastId = segment.get(size - 1);
if (firstId == lastId || tryClose(segment, nodeCache)) {
CoordinateSequence coordinates = nodeCache.getWayGeometry(segment);
Polygon poly = GeoUtils.JTS_FACTORY.createPolygon(coordinates);
polygons.add(new Ring(poly));
}
}
polygons.sort(BY_AREA_DESCENDING);
Set<Ring> shells = groupParentChildShells(polygons);
if (shells.size() == 0) {
throw new IllegalArgumentException("multipolygon not closed");
} else if (shells.size() == 1) {
return shells.iterator().next().toPolygon();
} else {
Polygon[] finished = shells.stream().map(Ring::toPolygon).toArray(Polygon[]::new);
return GeoUtils.JTS_FACTORY.createMultiPolygon(finished);
}
} catch (IllegalArgumentException e) {
throw new GeometryException("error building multipolygon " + osmId + ": " + e);
}
}
private static Set<Ring> groupParentChildShells(List<Ring> polygons) {
Set<Ring> shells = new HashSet<>();
int numPolygons = polygons.size();
if (numPolygons == 0) {
return shells;
}
shells.add(polygons.get(0));
if (numPolygons == 1) {
return shells;
}
for (int i = 0; i < numPolygons; i++) {
Ring outer = polygons.get(i);
if (i < numPolygons - 1) {
PreparedPolygon prepared = new PreparedPolygon(outer.geom);
for (int j = i + 1; j < numPolygons; j++) {
Ring inner = polygons.get(j);
if (prepared.contains(inner.geom)) {
if (inner.containedBy != null) {
inner.containedBy.holes.remove(inner);
shells.remove(inner);
}
inner.containedBy = outer;
if (inner.isHole()) {
outer.holes.add(inner);
} else {
shells.add(inner);
}
}
}
}
if (outer.containedBy == null) {
shells.add(outer);
}
}
return shells;
}
private static boolean tryClose(LongArrayList segment, OpenStreetMapReader.NodeLocationProvider nodeCache) {
int size = segment.size();
long firstId = segment.get(0);
Coordinate firstCoord = nodeCache.getCoordinate(firstId);
Coordinate lastCoord = nodeCache.getCoordinate(segment.get(size - 1));
if (firstCoord.distance(lastCoord) <= MIN_CLOSE_RING_GAP) {
segment.set(size - 1, firstId);
return true;
}
return false;
}
private static void reverseInPlace(LongArrayList orig) {
for (int i = 0, j = orig.size() - 1; i < j; i++, j--) {
long temp = orig.get(i);
orig.set(i, orig.get(j));
orig.set(j, temp);
}
}
private static LongArrayList reversedCopy(LongArrayList orig) {
LongArrayList result = new LongArrayList(orig.size());
for (int i = orig.size() - 1; i >= 0; i--) {
result.add(orig.get(i));
}
return result;
}
static LongArrayList appendToSkipFirst(LongArrayList orig, LongArrayList toAppend) {
int size = orig.size() + toAppend.size() - 1;
orig.ensureCapacity(size);
System.arraycopy(toAppend.buffer, 1, orig.buffer, orig.size(), toAppend.size() - 1);
orig.elementsCount = size;
return orig;
}
static LongArrayList prependToSkipLast(LongArrayList orig, LongArrayList toPrepend) {
int size = orig.size() + toPrepend.size() - 1;
orig.ensureCapacity(size);
System.arraycopy(orig.buffer, 0, orig.buffer, toPrepend.size() - 1, orig.size());
System.arraycopy(toPrepend.buffer, 0, orig.buffer, 0, toPrepend.size() - 1);
orig.elementsCount = size;
return orig;
}
static List<LongArrayList> connectPolygonSegments(List<LongArrayList> outer) {
LongObjectMap<LongArrayList> endpointIndex = new GHLongObjectHashMap<>(outer.size() * 2);
List<LongArrayList> completeRings = new ArrayList<>(outer.size());
for (LongArrayList ids : outer) {
if (ids.size() <= 1) {
continue;
}
long first = ids.get(0);
long last = ids.get(ids.size() - 1);
// optimization - skip rings that are already closed (as long as they have enough points)
if (first == last) {
if (ids.size() >= 4) {
completeRings.add(ids);
}
continue;
}
LongArrayList match, nextMatch;
if ((match = endpointIndex.get(first)) != null) {
endpointIndex.remove(first);
if (first == match.get(0)) {
reverseInPlace(match);
}
appendToSkipFirst(match, ids);
if ((nextMatch = endpointIndex.get(last)) != null && nextMatch != match) {
endpointIndex.remove(last);
if (last == nextMatch.get(0)) {
appendToSkipFirst(match, nextMatch);
} else {
appendToSkipFirst(match, reversedCopy(nextMatch));
}
endpointIndex.put(match.get(match.size() - 1), match);
} else {
endpointIndex.put(last, match);
}
} else if ((match = endpointIndex.get(last)) != null) {
endpointIndex.remove(last);
if (last == match.get(0)) {
prependToSkipLast(match, ids);
} else {
appendToSkipFirst(match, reversedCopy(ids));
}
endpointIndex.put(first, match);
} else {
LongArrayList copy = new LongArrayList(ids);
endpointIndex.put(first, copy);
endpointIndex.put(last, copy);
}
}
for (LongObjectCursor<LongArrayList> cursor : endpointIndex) {
LongArrayList value = cursor.value;
if (value.size() >= 4) {
if (value.get(0) == value.get(value.size() - 1) || cursor.key == value.get(0)) {
completeRings.add(value);
}
}
}
return completeRings;
}
}

Wyświetl plik

@ -26,8 +26,10 @@ import com.onthegomap.flatmap.write.MbtilesWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
@ -698,10 +700,9 @@ public class FlatMapTest {
new ReaderNode(2, GeoUtils.getWorldLat(0.25), GeoUtils.getWorldLon(0.75)),
new ReaderNode(3, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.75)),
new ReaderNode(4, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.25)),
new ReaderNode(5, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.25)),
with(new ReaderWay(6), way -> {
way.setTag("attr", "value");
way.getNodes().add(1, 2, 3, 4, 5);
way.getNodes().add(1, 2, 3, 4, 1);
})
),
(in, features) -> {
@ -720,24 +721,33 @@ public class FlatMapTest {
}
);
assertSubmap(Map.of(
assertSubmap(sortListValues(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(
feature(newLineString(
128, 128,
192, 128,
64, 64,
192, 64,
192, 192,
128, 192,
128, 128
64, 192,
64, 64
), Map.of(
"attr", "value",
"name", "name value1"
)),
feature(rectangle(128, 192), Map.of(
feature(rectangle(64, 192), Map.of(
"attr", "value",
"name", "name value2"
))
)
), results.tiles);
)), 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()) {
List<?> sorted = entry.getValue().stream().sorted(Comparator.comparing(Object::toString)).toList();
result.put(entry.getKey(), sorted);
}
return result;
}
@Test
@ -745,38 +755,37 @@ public class FlatMapTest {
var results = runWithOsmElements(
Map.of("threads", "1"),
List.of(
new ReaderNode(1, GeoUtils.getWorldLat(0.25), GeoUtils.getWorldLon(0.25)),
new ReaderNode(2, GeoUtils.getWorldLat(0.25), GeoUtils.getWorldLon(0.75)),
new ReaderNode(3, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.75)),
new ReaderNode(4, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.25)),
new ReaderNode(5, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.25)),
new ReaderNode(1, GeoUtils.getWorldLat(0.125), GeoUtils.getWorldLon(0.125)),
new ReaderNode(2, GeoUtils.getWorldLat(0.125), GeoUtils.getWorldLon(0.875)),
new ReaderNode(3, GeoUtils.getWorldLat(0.875), GeoUtils.getWorldLon(0.875)),
new ReaderNode(4, GeoUtils.getWorldLat(0.875), GeoUtils.getWorldLon(0.125)),
new ReaderNode(6, GeoUtils.getWorldLat(0.3), GeoUtils.getWorldLon(0.3)),
new ReaderNode(7, GeoUtils.getWorldLat(0.3), GeoUtils.getWorldLon(0.7)),
new ReaderNode(8, GeoUtils.getWorldLat(0.7), GeoUtils.getWorldLon(0.7)),
new ReaderNode(9, GeoUtils.getWorldLat(0.7), GeoUtils.getWorldLon(0.3)),
new ReaderNode(10, GeoUtils.getWorldLat(0.7), GeoUtils.getWorldLon(0.3)),
new ReaderNode(5, GeoUtils.getWorldLat(0.25), GeoUtils.getWorldLon(0.25)),
new ReaderNode(6, GeoUtils.getWorldLat(0.25), GeoUtils.getWorldLon(0.75)),
new ReaderNode(7, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.75)),
new ReaderNode(8, GeoUtils.getWorldLat(0.75), GeoUtils.getWorldLon(0.25)),
new ReaderNode(11, GeoUtils.getWorldLat(0.4), GeoUtils.getWorldLon(0.4)),
new ReaderNode(12, GeoUtils.getWorldLat(0.4), GeoUtils.getWorldLon(0.6)),
new ReaderNode(13, GeoUtils.getWorldLat(0.6), GeoUtils.getWorldLon(0.6)),
new ReaderNode(14, GeoUtils.getWorldLat(0.6), GeoUtils.getWorldLon(0.4)),
new ReaderNode(15, GeoUtils.getWorldLat(0.6), GeoUtils.getWorldLon(0.4)),
new ReaderNode(9, GeoUtils.getWorldLat(0.375), GeoUtils.getWorldLon(0.375)),
new ReaderNode(10, GeoUtils.getWorldLat(0.375), GeoUtils.getWorldLon(0.625)),
new ReaderNode(11, GeoUtils.getWorldLat(0.625), GeoUtils.getWorldLon(0.625)),
new ReaderNode(12, GeoUtils.getWorldLat(0.625), GeoUtils.getWorldLon(0.375)),
new ReaderNode(13, GeoUtils.getWorldLat(0.375 + 1e-12), GeoUtils.getWorldLon(0.375)),
with(new ReaderWay(16), way -> way.getNodes().add(1, 2, 3, 4, 5)),
with(new ReaderWay(17), way -> way.getNodes().add(6, 7, 8, 9, 10)),
with(new ReaderWay(18), way -> way.getNodes().add(11, 12, 13, 14, 15)),
with(new ReaderWay(14), way -> way.getNodes().add(1, 2, 3, 4, 1)),
with(new ReaderWay(15), way -> way.getNodes().add(5, 6, 7, 8, 5)),
with(new ReaderWay(16), way -> way.getNodes().add(9, 10, 11, 12, 13)),
with(new ReaderRelation(19), rel -> {
with(new ReaderRelation(17), rel -> {
rel.setTag("type", "multipolygon");
rel.setTag("attr", "value");
rel.add(new ReaderRelation.Member(ReaderRelation.Member.WAY, 16, "outer"));
rel.add(new ReaderRelation.Member(ReaderRelation.Member.WAY, 17, "inner"));
rel.add(new ReaderRelation.Member(ReaderRelation.Member.WAY, 18, "outer"));
rel.setTag("should_emit", "yes");
rel.add(new ReaderRelation.Member(ReaderRelation.Member.WAY, 14, "outer"));
rel.add(new ReaderRelation.Member(ReaderRelation.Member.WAY, 15, "inner"));
rel.add(new ReaderRelation.Member(ReaderRelation.Member.WAY, 16, "inner")); // incorrect
})
),
(in, features) -> {
if (in.canBePolygon()) {
if (in.hasTag("should_emit")) {
features.polygon("layer")
.setZoomRange(0, 0)
.setAttr("name", "name value")
@ -788,9 +797,13 @@ public class FlatMapTest {
assertSubmap(Map.of(
TileCoord.ofXYZ(0, 0, 0), List.of(
feature(newMultiPolygon(
rectangle(0.25 * 256, 0.75 * 256),
rectangle(0.3 * 256, 0.7 * 256),
rectangle(0.4 * 256, 0.6 * 256)
newPolygon(
rectangleCoordList(0.125 * 256, 0.875 * 256),
List.of(
rectangleCoordList(0.25 * 256, 0.75 * 256)
)
),
rectangle(0.375 * 256, 0.625 * 256)
), Map.of(
"attr", "value",
"name", "name value"

Wyświetl plik

@ -6,6 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.graphhopper.reader.ReaderElement;
import com.graphhopper.reader.ReaderNode;
import com.graphhopper.reader.ReaderRelation;
import com.graphhopper.reader.ReaderWay;
@ -19,8 +20,7 @@ import com.onthegomap.flatmap.monitoring.Stats;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Disabled;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
public class OpenStreetMapReaderTest {
@ -42,12 +42,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testPoint() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var node = new ReaderNode(1, 0, 0);
node.setTag("key", "value");
reader.processPass1(node);
@ -71,12 +66,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testLine() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var nodeCache = reader.newNodeGeometryCache();
var node1 = new ReaderNode(1, 0, 0);
var node2 = node(2, 0.75, 0.75);
@ -116,12 +106,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testPolygonAreaNotSpecified() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var nodeCache = reader.newNodeGeometryCache();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
@ -164,12 +149,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testPolygonAreaYes() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var nodeCache = reader.newNodeGeometryCache();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
@ -209,12 +189,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testPolygonAreaNo() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var nodeCache = reader.newNodeGeometryCache();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
@ -254,12 +229,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testLineWithTooFewPoints() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var node1 = node(1, 0.5, 0.5);
var way = new ReaderWay(3);
way.getNodes().add(1);
@ -284,12 +254,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testPolygonWithTooFewPoints() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
var way = new ReaderWay(3);
@ -326,12 +291,7 @@ public class OpenStreetMapReaderTest {
@Test
public void testInvalidPolygon() throws GeometryException {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
reader.processPass1(node(1, 0.5, 0.5));
reader.processPass1(node(2, 0.75, 0.5));
@ -369,20 +329,13 @@ public class OpenStreetMapReaderTest {
assertEquals(1.207, feature.length(), 1e-2);
}
@NotNull
private ReaderNode node(long id, double x, double y) {
return new ReaderNode(id, GeoUtils.getWorldLat(y), GeoUtils.getWorldLon(x));
}
@Test
@Disabled
public void testLineReferencingNonexistentNode() {
OpenStreetMapReader reader = new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
OpenStreetMapReader reader = newOsmReader();
var way = new ReaderWay(321);
way.getNodes().add(123, 2222, 333, 444, 123);
reader.processPass1(way);
@ -404,27 +357,300 @@ public class OpenStreetMapReaderTest {
assertThrows(GeometryException.class, feature::length);
}
private final Function<ReaderElement, Stream<ReaderNode>> nodes = elem ->
elem instanceof ReaderNode node ? Stream.of(node) : Stream.empty();
private final Function<ReaderElement, Stream<ReaderWay>> ways = elem ->
elem instanceof ReaderWay way ? Stream.of(way) : Stream.empty();
private final Function<ReaderElement, Stream<ReaderRelation>> rels = elem ->
elem instanceof ReaderRelation rel ? Stream.of(rel) : Stream.empty();
@Test
@Disabled
public void testMultiPolygon() {
public void testMultiPolygon() throws GeometryException {
OpenStreetMapReader reader = newOsmReader();
var outerway = new ReaderWay(9);
outerway.getNodes().add(1, 2, 3, 4, 1);
var innerway = new ReaderWay(10);
innerway.getNodes().add(5, 6, 7, 8, 5);
var relation = new ReaderRelation(11);
relation.setTag("type", "multipolygon");
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, outerway.getId(), "outer"));
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, innerway.getId(), "inner"));
List<ReaderElement> elements = List.of(
node(1, 0.1, 0.1),
node(2, 0.9, 0.1),
node(3, 0.9, 0.9),
node(4, 0.1, 0.9),
node(5, 0.2, 0.2),
node(6, 0.8, 0.2),
node(7, 0.8, 0.8),
node(8, 0.2, 0.8),
outerway,
innerway,
relation
);
elements.forEach(reader::processPass1);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeGeometryCache();
elements.stream().flatMap(ways).forEach(way -> {
reader.processWayPass2(nodeCache, way);
nodeCache.reset();
});
var feature = reader.processRelationPass2(relation, nodeCache);
assertFalse(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
assertSameNormalizedFeature(
newPolygon(
rectangleCoordList(0.1, 0.9),
List.of(rectangleCoordList(0.2, 0.8))
),
round(feature.worldGeometry()),
round(feature.polygon()),
round(feature.validatedPolygon()),
round(GeoUtils.latLonToWorldCoords(feature.latLonGeometry()))
);
assertThrows(GeometryException.class, feature::line);
assertSameNormalizedFeature(
newPoint(0.5, 0.5),
round(feature.centroid())
);
assertPointOnSurface(feature);
assertEquals(0.28, feature.area(), 1e-5);
assertEquals(5.6, feature.length(), 1e-2);
}
@Test
@Disabled
public void testMultiPolygonInfersCorrectParents() {
public void testMultipolygonInfersCorrectParent() throws GeometryException {
OpenStreetMapReader reader = newOsmReader();
var outerway = new ReaderWay(13);
outerway.getNodes().add(1, 2, 3, 4, 1);
var innerway = new ReaderWay(14);
innerway.getNodes().add(5, 6, 7, 8, 5);
var innerinnerway = new ReaderWay(15);
innerinnerway.getNodes().add(9, 10, 11, 12, 9);
var relation = new ReaderRelation(16);
relation.setTag("type", "multipolygon");
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, outerway.getId(), "outer"));
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, innerway.getId(), "inner"));
// nested hole marked as inner, but should actually be outer
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, innerinnerway.getId(), "inner"));
List<ReaderElement> elements = List.of(
node(1, 0.1, 0.1),
node(2, 0.9, 0.1),
node(3, 0.9, 0.9),
node(4, 0.1, 0.9),
node(5, 0.2, 0.2),
node(6, 0.8, 0.2),
node(7, 0.8, 0.8),
node(8, 0.2, 0.8),
node(9, 0.3, 0.3),
node(10, 0.7, 0.3),
node(11, 0.7, 0.7),
node(12, 0.3, 0.7),
outerway,
innerway,
innerinnerway,
relation
);
elements.forEach(reader::processPass1);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeGeometryCache();
elements.stream().flatMap(ways).forEach(way -> {
reader.processWayPass2(nodeCache, way);
nodeCache.reset();
});
var feature = reader.processRelationPass2(relation, nodeCache);
assertFalse(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
assertSameNormalizedFeature(
newMultiPolygon(
newPolygon(
rectangleCoordList(0.1, 0.9),
List.of(rectangleCoordList(0.2, 0.8))
),
rectangle(0.3, 0.7)
),
round(feature.worldGeometry()),
round(feature.polygon()),
round(feature.validatedPolygon()),
round(GeoUtils.latLonToWorldCoords(feature.latLonGeometry()))
);
}
@Test
@Disabled
public void testInvalidMultiPolygon() {
public void testInvalidMultipolygon() throws GeometryException {
OpenStreetMapReader reader = newOsmReader();
var outerway = new ReaderWay(13);
outerway.getNodes().add(1, 2, 3, 4, 1);
var innerway = new ReaderWay(14);
innerway.getNodes().add(5, 6, 7, 8, 5);
var innerinnerway = new ReaderWay(15);
innerinnerway.getNodes().add(9, 10, 11, 12, 9);
var relation = new ReaderRelation(16);
relation.setTag("type", "multipolygon");
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, outerway.getId(), "outer"));
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, innerway.getId(), "inner"));
// nested hole marked as inner, but should actually be outer
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, innerinnerway.getId(), "inner"));
List<ReaderElement> elements = List.of(
node(1, 0.1, 0.1),
node(2, 0.9, 0.1),
node(3, 0.9, 0.9),
node(4, 0.1, 0.9),
node(5, 0.2, 0.3),
node(6, 0.8, 0.3),
node(7, 0.8, 0.8),
node(8, 0.2, 0.8),
node(9, 0.2, 0.2),
node(10, 0.8, 0.2),
node(11, 0.8, 0.7),
node(12, 0.2, 0.7),
outerway,
innerway,
innerinnerway,
relation
);
elements.forEach(reader::processPass1);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeGeometryCache();
elements.stream().flatMap(ways).forEach(way -> {
reader.processWayPass2(nodeCache, way);
nodeCache.reset();
});
var feature = reader.processRelationPass2(relation, nodeCache);
assertFalse(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
assertTopologicallyEquivalentFeature(
newPolygon(
rectangleCoordList(0.1, 0.9),
List.of(rectangleCoordList(0.2, 0.8))
),
round(feature.validatedPolygon())
);
assertSameNormalizedFeature(
newPolygon(
rectangleCoordList(0.1, 0.9),
List.of(
rectangleCoordList(0.2, 0.3, 0.8, 0.8),
rectangleCoordList(0.2, 0.2, 0.8, 0.7)
)
),
round(feature.polygon())
);
}
@Test
public void testMultiPolygonRefersToNonexistentNode() {
OpenStreetMapReader reader = newOsmReader();
var outerway = new ReaderWay(5);
outerway.getNodes().add(1, 2, 3, 4, 1);
var relation = new ReaderRelation(6);
relation.setTag("type", "multipolygon");
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, outerway.getId(), "outer"));
List<ReaderElement> elements = List.of(
node(1, 0.1, 0.1),
// node(2, 0.9, 0.1), MISSING!
node(3, 0.9, 0.9),
node(4, 0.1, 0.9),
outerway,
relation
);
elements.forEach(reader::processPass1);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeGeometryCache();
elements.stream().flatMap(ways).forEach(way -> {
reader.processWayPass2(nodeCache, way);
nodeCache.reset();
});
var feature = reader.processRelationPass2(relation, nodeCache);
assertThrows(GeometryException.class, feature::worldGeometry);
assertThrows(GeometryException.class, feature::polygon);
assertThrows(GeometryException.class, feature::validatedPolygon);
}
@Test
@Disabled
public void testMultiPolygonRefersToNonexistentWay() {
OpenStreetMapReader reader = newOsmReader();
var relation = new ReaderRelation(6);
relation.setTag("type", "multipolygon");
relation.add(new ReaderRelation.Member(ReaderRelation.WAY, 5, "outer"));
List<ReaderElement> elements = List.of(
node(1, 0.1, 0.1),
node(2, 0.9, 0.1),
node(3, 0.9, 0.9),
node(4, 0.1, 0.9),
// outerway, // missing!
relation
);
elements.forEach(reader::processPass1);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeGeometryCache();
elements.stream().flatMap(ways).forEach(way -> {
reader.processWayPass2(nodeCache, way);
nodeCache.reset();
});
var feature = reader.processRelationPass2(relation, nodeCache);
assertThrows(GeometryException.class, feature::worldGeometry);
assertThrows(GeometryException.class, feature::polygon);
assertThrows(GeometryException.class, feature::validatedPolygon);
}
// TODO what about:
// - relation info / storage size
// - multilevel multipolygon relationship containers
private OpenStreetMapReader newOsmReader() {
return new OpenStreetMapReader(
osmSource,
longLongMap,
profile,
stats
);
}
// TODO: relation info / storage size
}

Wyświetl plik

@ -0,0 +1,423 @@
/*****************************************************************
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
****************************************************************/
package com.onthegomap.flatmap.read;
import static com.onthegomap.flatmap.TestUtils.*;
import static com.onthegomap.flatmap.read.OsmMultipolygon.appendToSkipFirst;
import static com.onthegomap.flatmap.read.OsmMultipolygon.connectPolygonSegments;
import static com.onthegomap.flatmap.read.OsmMultipolygon.prependToSkipLast;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.carrotsearch.hppc.LongArrayList;
import com.onthegomap.flatmap.geo.GeoUtils;
import com.onthegomap.flatmap.geo.GeometryException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Geometry;
/**
* This class is ported to Java from https://github.com/omniscale/imposm3/blob/master/geom/multipolygon_test.go
*/
public class OsmMultipolygonTest {
private static LongArrayList longs(long... input) {
return LongArrayList.from(input);
}
private DynamicTest testConnect(List<LongArrayList> input, List<LongArrayList> expectedOutput) {
return DynamicTest.dynamicTest("connectPolygonSegments(" + input + ")", () -> {
var expected = expectedOutput.stream().sorted(Comparator.comparing(Object::toString)).toList();
var actual = connectPolygonSegments(input);
actual.sort(Comparator.comparing(Object::toString));
assertEquals(expected, actual);
});
}
@Test
public void testConnectUtils() {
assertEquals(longs(1, 2, 3, 4), appendToSkipFirst(longs(1, 2), longs(2, 3, 4)));
assertEquals(longs(1, 2, 3, 4), prependToSkipLast(longs(3, 4), longs(1, 2, 3)));
}
@TestFactory
public List<DynamicNode> testConnectPolygonSegments() {
return List.of(
testConnect(List.of(), List.of()),
testConnect(List.of(longs(1)), List.of()),
testConnect(List.of(longs(1, 2)), List.of()),
testConnect(List.of(longs(1, 2, 1)), List.of()),
testConnect(List.of(longs(1, 2, 3, 1)), List.of(longs(1, 2, 3, 1))),
testConnect(List.of(
longs(1, 2),
longs(3, 2),
longs(3, 1)
), List.of(longs(1, 2, 3, 1))),
testConnect(List.of(
longs(10, 11, 10),
longs(1, 2),
longs(3, 2),
longs(3, 1),
longs(4, 5),
longs(5, 6),
longs(6, 4)
), List.of(
longs(4, 5, 6, 4),
longs(1, 2, 3, 1)
)),
testConnect(List.of(
longs(1L, 2L),
longs(3L, 4L),
longs(4L, 5L),
longs(5L, 6L),
longs(6L, 7L),
longs(3L, 2L),
longs(7L, 1L)
), List.of(
longs(1, 2, 3, 4, 5, 6, 7, 1)
))
);
}
private long id = 1;
private record Node(long id, double x, double y) {}
private Node node(double x, double y) {
return new Node(id++, x, y);
}
private void testBuildMultipolygon(List<List<Node>> ways, Geometry expected) throws GeometryException {
Map<Long, Coordinate> coords = new HashMap<>();
List<LongArrayList> rings = new ArrayList<>();
for (List<Node> way : ways) {
LongArrayList ring = new LongArrayList();
rings.add(ring);
for (Node node : way) {
ring.add(node.id);
coords.put(node.id, new CoordinateXY(node.x, node.y));
}
}
OpenStreetMapReader.NodeLocationProvider nodeLocs = coords::get;
Geometry actual = OsmMultipolygon.build(rings, nodeLocs, 0);
assertSameNormalizedFeature(expected, actual);
}
@Test
public void testConnectSimplePolygon() throws GeometryException {
var node1 = node(0.5, 0.5);
var node2 = node(0.75, 0.5);
var node3 = node(0.75, 0.75);
testBuildMultipolygon(
List.of(
List.of(node1, node2),
List.of(node2, node3),
List.of(node1, node3)
),
newPolygon(
0.5, 0.5,
0.75, 0.5,
0.75, 0.75,
0.5, 0.5
)
);
}
@Test
public void testConnectAlmostClosed() throws GeometryException {
var node1 = node(0.5, 0.5);
var node1a = node(0.5 + 1e-10, 0.5);
var node2 = node(0.75, 0.5);
var node3 = node(0.75, 0.75);
testBuildMultipolygon(
List.of(
List.of(node1, node2),
List.of(node2, node3),
List.of(node1a, node3)
),
newPolygon(
0.5, 0.5,
0.75, 0.5,
0.75, 0.75,
0.5, 0.5
)
);
}
@Test
public void testThrowWhenNoClosed() {
var node1 = node(0.5, 0.5);
var node1a = node(0.5 + 1e-1, 0.5);
var node2 = node(0.75, 0.5);
var node3 = node(0.75, 0.75);
assertThrows(GeometryException.class, () -> testBuildMultipolygon(
List.of(
List.of(node1, node2),
List.of(node2, node3),
List.of(node1a, node3)
),
GeoUtils.JTS_FACTORY.createGeometryCollection()
));
}
@Test
public void testIgnoreSingleNotClosed() throws GeometryException {
testBuildMultipolygon(
List.of(
rectangleNodes(0, 10),
List.of(
node(20, 20),
node(30, 20),
node(30, 30),
node(20, 30)
// not closed
)
),
rectangle(0, 10)
);
}
// tests from https://github.com/omniscale/imposm3/blob/master/geom/multipolygon_test.go below
@Test
public void testSimplePolygonWithHole() throws GeometryException {
testBuildMultipolygon(
List.of(
rectangleNodes(0, 10),
rectangleNodes(2, 8)
),
newPolygon(
rectangleCoordList(0, 10),
List.of(rectangleCoordList(2, 8))
)
);
}
@Test
public void testSimplePolygonWithMultipleHoles() throws GeometryException {
testBuildMultipolygon(
List.of(
rectangleNodes(0, 10),
rectangleNodes(1, 2),
rectangleNodes(3, 4)
),
newPolygon(
rectangleCoordList(0, 10),
List.of(rectangleCoordList(1, 2), rectangleCoordList(3, 4))
)
);
}
public List<Node> rectangleNodes(double xMin, double yMin, double xMax, double yMax) {
var startEnd = node(xMin, yMin);
return List.of(startEnd, node(xMax, yMin), node(xMax, yMax), node(xMin, yMax), startEnd);
}
public List<Node> rectangleNodes(double min, double max) {
return rectangleNodes(min, min, max, max);
}
@Test
public void testMultiPolygonWithNestedHoles() throws GeometryException {
testBuildMultipolygon(
List.of(
rectangleNodes(0, 10),
rectangleNodes(1, 9),
rectangleNodes(2, 8),
rectangleNodes(3, 7),
rectangleNodes(4, 6)
),
newMultiPolygon(
newPolygon(
rectangleCoordList(0, 10),
List.of(
rectangleCoordList(1, 9)
)
),
newPolygon(
rectangleCoordList(2, 8),
List.of(
rectangleCoordList(3, 7)
)
),
rectangle(4, 6)
)
);
}
@Test
public void testTouchingPolygonsWithHole() throws GeometryException {
testBuildMultipolygon(
List.of(
rectangleNodes(0, 10),
rectangleNodes(10, 0, 30, 10),
rectangleNodes(2, 8)
),
newMultiPolygon(
newPolygon(
rectangleCoordList(0, 10),
List.of(
rectangleCoordList(2, 8)
)
),
rectangle(10, 0, 30, 10)
)
);
}
@Test
public void testBrokenPolygonSelfIntersect1() throws GeometryException {
Node startEnd = node(0, 0);
testBuildMultipolygon(
List.of(
List.of(
startEnd,
node(0, 10),
node(10, 10),
node(10, 0),
node(20, 0),
node(20, 10),
node(30, 10),
node(30, 0),
startEnd
),
rectangleNodes(2, 8)
),
newPolygon(
newCoordinateList(
0, 0,
0, 10,
10, 10,
10, 0,
20, 0,
20, 10,
30, 10,
30, 0,
0, 0
),
List.of(
rectangleCoordList(2, 8)
)
)
);
}
@Test
public void testBrokenPolygonSelfIntersect2() throws GeometryException {
Node startEnd = node(10, 0);
testBuildMultipolygon(
List.of(
List.of(
startEnd,
node(10, 0),
node(0, 0),
node(0, 10),
node(10, 10),
node(10, 0),
node(20, 0),
node(20, 10),
node(30, 10),
node(30, 0),
startEnd
),
rectangleNodes(2, 8)
),
newPolygon(
newCoordinateList(
10, 0,
10, 0,
0, 0,
0, 10,
10, 10,
10, 0,
20, 0,
20, 10,
30, 10,
30, 0,
10, 0
),
List.of(
rectangleCoordList(2, 8)
)
)
);
}
@Test
public void testBrokenPolygonSelfIntersectTriangleSmallOverlap() throws GeometryException {
Node startEnd = node(0, 0);
testBuildMultipolygon(
List.of(
List.of(
startEnd,
node(0, 100),
node(100, 50 - 0.00001),
node(100, 50 + 0.00001),
startEnd
),
rectangleNodes(10, 45, 20, 55)
),
newPolygon(
newCoordinateList(
0, 0,
0, 100,
100, 50 - 0.00001,
100, 50 + 0.00001,
0, 0
),
List.of(
rectangleCoordList(10, 45, 20, 55)
)
)
);
}
@Test
public void testBrokenPolygonSelfIntersectTriangleLargeOverlap() throws GeometryException {
Node startEnd = node(0, 0);
testBuildMultipolygon(
List.of(
List.of(
startEnd,
node(0, 100),
node(100, 50 - 1),
node(100, 50 + 1),
startEnd
),
rectangleNodes(10, 45, 20, 55)
),
newPolygon(
newCoordinateList(
0, 0,
0, 100,
100, 50 - 1,
100, 50 + 1,
0, 0
),
List.of(
rectangleCoordList(10, 45, 20, 55)
)
)
);
}
}