kopia lustrzana https://github.com/onthegomap/planetiler
multipolygons
rodzic
09baa06e09
commit
318a314199
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Ładowanie…
Reference in New Issue