planetiler/planetiler-core/src/test/java/com/onthegomap/planetiler/reader/osm/OsmReaderTest.java

662 wiersze
22 KiB
Java

package com.onthegomap.planetiler.reader.osm;
import static com.onthegomap.planetiler.TestUtils.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.onthegomap.planetiler.Profile;
import com.onthegomap.planetiler.TestUtils;
import com.onthegomap.planetiler.collection.LongLongMap;
import com.onthegomap.planetiler.geo.GeoUtils;
import com.onthegomap.planetiler.geo.GeometryException;
import com.onthegomap.planetiler.reader.SourceFeature;
import com.onthegomap.planetiler.stats.Stats;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
public class OsmReaderTest {
public final OsmBlockSource osmSource = next -> {
};
private final Stats stats = Stats.inMemory();
private final Profile profile = new Profile.NullProfile();
private final LongLongMap nodeMap = LongLongMap.newInMemorySortedTable();
@Test
public void testPoint() throws GeometryException {
OsmReader reader = newOsmReader();
var node = new OsmElement.Node(1, 0, 0);
node.setTag("key", "value");
reader.processPass1Block(List.of(node));
SourceFeature feature = reader.processNodePass2(node);
assertTrue(feature.isPoint());
assertFalse(feature.canBePolygon());
assertFalse(feature.canBeLine());
assertSameNormalizedFeature(
newPoint(0.5, 0.5),
feature.worldGeometry(),
feature.centroid(),
feature.pointOnSurface(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertEquals(0, feature.area());
assertEquals(0, feature.length());
assertThrows(GeometryException.class, feature::line);
assertThrows(GeometryException.class, feature::polygon);
assertEquals(Map.of("key", "value"), feature.tags());
}
@Test
public void testLine() throws GeometryException {
OsmReader reader = newOsmReader();
var nodeCache = reader.newNodeLocationProvider();
var node1 = new OsmElement.Node(1, 0, 0);
var node2 = node(2, 0.75, 0.75);
var way = new OsmElement.Way(3);
way.nodes().add(node1.id(), node2.id());
way.setTag("key", "value");
reader.processPass1Block(List.of(node1, node2, way));
SourceFeature feature = reader.processWayPass2(way, nodeCache);
assertTrue(feature.canBeLine());
assertFalse(feature.isPoint());
assertFalse(feature.canBePolygon());
assertSameNormalizedFeature(
newLineString(
0.5, 0.5,
0.75, 0.75
),
feature.worldGeometry(),
feature.line(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertThrows(GeometryException.class, feature::polygon);
assertEquals(
newPoint(0.625, 0.625),
feature.centroid()
);
assertPointOnSurface(feature);
assertEquals(0, feature.area());
assertEquals(Math.sqrt(2 * 0.25 * 0.25), feature.length(), 1e-5);
assertEquals(Map.of("key", "value"), feature.tags());
}
@Test
public void testPolygonAreaNotSpecified() throws GeometryException {
OsmReader reader = newOsmReader();
var nodeCache = reader.newNodeLocationProvider();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
var node3 = node(3, 0.75, 0.75);
var node4 = node(4, 0.75, 0.5);
var way = new OsmElement.Way(3);
way.nodes().add(1, 2, 3, 4, 1);
way.setTag("key", "value");
reader.processPass1Block(List.of(node1, node2, node3, node4, way));
SourceFeature feature = reader.processWayPass2(way, nodeCache);
assertTrue(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
assertSameNormalizedFeature(
rectangle(0.5, 0.75),
feature.worldGeometry(),
feature.polygon(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertSameNormalizedFeature(
rectangle(0.5, 0.75).getExteriorRing(),
feature.line()
);
assertEquals(
newPoint(0.625, 0.625),
feature.centroid()
);
assertPointOnSurface(feature);
assertEquals(0.25 * 0.25, feature.area());
assertEquals(1, feature.length());
}
@Test
public void testPolygonAreaYes() throws GeometryException {
OsmReader reader = newOsmReader();
var nodeCache = reader.newNodeLocationProvider();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
var node3 = node(3, 0.75, 0.75);
var node4 = node(4, 0.75, 0.5);
var way = new OsmElement.Way(3);
way.nodes().add(1, 2, 3, 4, 1);
way.setTag("area", "yes");
reader.processPass1Block(List.of(node1, node2, node3, node4, way));
SourceFeature feature = reader.processWayPass2(way, nodeCache);
assertFalse(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
assertSameNormalizedFeature(
rectangle(0.5, 0.75),
feature.worldGeometry(),
feature.polygon(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertThrows(GeometryException.class, feature::line);
assertEquals(
newPoint(0.625, 0.625),
feature.centroid()
);
assertPointOnSurface(feature);
assertEquals(0.25 * 0.25, feature.area());
assertEquals(1, feature.length());
}
@Test
public void testPolygonAreaNo() throws GeometryException {
OsmReader reader = newOsmReader();
var nodeCache = reader.newNodeLocationProvider();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
var node3 = node(3, 0.75, 0.75);
var node4 = node(4, 0.75, 0.5);
var way = new OsmElement.Way(5);
way.nodes().add(1, 2, 3, 4, 1);
way.setTag("area", "no");
reader.processPass1Block(List.of(node1, node2, node3, node4, way));
SourceFeature feature = reader.processWayPass2(way, nodeCache);
assertTrue(feature.canBeLine());
assertFalse(feature.isPoint());
assertFalse(feature.canBePolygon());
assertSameNormalizedFeature(
rectangle(0.5, 0.75).getExteriorRing(),
feature.worldGeometry(),
feature.line(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertThrows(GeometryException.class, feature::polygon);
assertEquals(
newPoint(0.625, 0.625),
feature.centroid()
);
assertPointOnSurface(feature);
assertEquals(0, feature.area());
assertEquals(1, feature.length());
}
@Test
public void testLineWithTooFewPoints() throws GeometryException {
OsmReader reader = newOsmReader();
var node1 = node(1, 0.5, 0.5);
var way = new OsmElement.Way(3);
way.nodes().add(1);
reader.processPass1Block(List.of(node1, way));
SourceFeature feature = reader.processWayPass2(way, reader.newNodeLocationProvider());
assertFalse(feature.canBeLine());
assertFalse(feature.isPoint());
assertFalse(feature.canBePolygon());
assertThrows(GeometryException.class, feature::worldGeometry);
assertThrows(GeometryException.class, feature::latLonGeometry);
assertThrows(GeometryException.class, feature::line);
assertThrows(GeometryException.class, feature::centroid);
assertThrows(GeometryException.class, feature::pointOnSurface);
assertEquals(0, feature.area());
assertEquals(0, feature.length());
}
@Test
public void testPolygonWithTooFewPoints() throws GeometryException {
OsmReader reader = newOsmReader();
var node1 = node(1, 0.5, 0.5);
var node2 = node(2, 0.5, 0.75);
var way = new OsmElement.Way(3);
way.nodes().add(1, 2, 1);
reader.processPass1Block(List.of(node1, node2, way));
SourceFeature feature = reader.processWayPass2(way, reader.newNodeLocationProvider());
assertTrue(feature.canBeLine());
assertFalse(feature.isPoint());
assertFalse(feature.canBePolygon());
assertSameNormalizedFeature(
newLineString(0.5, 0.5, 0.5, 0.75, 0.5, 0.5),
feature.worldGeometry(),
feature.line(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertSameNormalizedFeature(
newPoint(0.5, 0.625),
feature.centroid()
);
assertPointOnSurface(feature);
assertEquals(0, feature.area());
assertEquals(0.5, feature.length());
}
private static void assertPointOnSurface(SourceFeature feature) throws GeometryException {
TestUtils.assertPointOnSurface(feature.worldGeometry(), feature.pointOnSurface());
}
@Test
public void testInvalidPolygon() throws GeometryException {
OsmReader reader = newOsmReader();
reader.processPass1Block(List.of(
node(1, 0.5, 0.5),
node(2, 0.75, 0.5),
node(3, 0.5, 0.75),
node(4, 0.75, 0.75)
));
var way = new OsmElement.Way(6);
way.setTag("area", "yes");
way.nodes().add(1, 2, 3, 4, 1);
reader.processPass1Block(List.of(way));
SourceFeature feature = reader.processWayPass2(way, reader.newNodeLocationProvider());
assertFalse(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
assertSameNormalizedFeature(
newPolygon(
0.5, 0.5,
0.75, 0.5,
0.5, 0.75,
0.75, 0.75,
0.5, 0.5
),
feature.worldGeometry(),
GeoUtils.latLonToWorldCoords(feature.latLonGeometry())
);
assertThrows(GeometryException.class, feature::line);
assertSameNormalizedFeature(
newPoint(0.625, 0.625),
feature.centroid()
);
assertPointOnSurface(feature);
assertEquals(0, feature.area());
assertEquals(1.207, feature.length(), 1e-2);
}
private OsmElement.Node node(long id, double x, double y) {
return new OsmElement.Node(id, GeoUtils.getWorldLat(y), GeoUtils.getWorldLon(x));
}
@Test
public void testLineReferencingNonexistentNode() {
OsmReader reader = newOsmReader();
var way = new OsmElement.Way(321);
way.nodes().add(123, 2222, 333, 444, 123);
reader.processPass1Block(List.of(way));
SourceFeature feature = reader.processWayPass2(way, reader.newNodeLocationProvider());
assertTrue(feature.canBeLine());
assertFalse(feature.isPoint());
assertTrue(feature.canBePolygon());
GeometryException exception = assertThrows(GeometryException.class, feature::line);
assertTrue(exception.getMessage().contains("321") && exception.getMessage().contains("123"),
"Exception message did not contain way and missing node ID: " + exception.getMessage()
);
assertThrows(GeometryException.class, feature::worldGeometry);
assertThrows(GeometryException.class, feature::centroid);
assertThrows(GeometryException.class, feature::polygon);
assertThrows(GeometryException.class, feature::pointOnSurface);
assertThrows(GeometryException.class, feature::area);
assertThrows(GeometryException.class, feature::length);
}
private final Function<OsmElement, Stream<OsmElement.Node>> nodes =
elem -> elem instanceof OsmElement.Node node ? Stream.of(node) : Stream.empty();
private final Function<OsmElement, Stream<OsmElement.Way>> ways =
elem -> elem instanceof OsmElement.Way way ? Stream.of(way) : Stream.empty();
@ParameterizedTest
@ValueSource(strings = {"multipolygon", "boundary", "land_area"})
public void testMultiPolygon(String relationType) throws GeometryException {
OsmReader reader = newOsmReader();
var outerway = new OsmElement.Way(9);
outerway.nodes().add(1, 2, 3, 4, 1);
var innerway = new OsmElement.Way(10);
innerway.nodes().add(5, 6, 7, 8, 5);
var relation = new OsmElement.Relation(11);
relation.setTag("type", relationType);
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, outerway.id(), "outer"));
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, innerway.id(), "inner"));
List<OsmElement> 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
);
reader.processPass1Block(elements);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeLocationProvider();
elements.stream().flatMap(ways).forEach(way -> reader.processWayPass2(way, nodeCache));
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
public void testMultipolygonInfersCorrectParent() throws GeometryException {
OsmReader reader = newOsmReader();
var outerway = new OsmElement.Way(13);
outerway.nodes().add(1, 2, 3, 4, 1);
var innerway = new OsmElement.Way(14);
innerway.nodes().add(5, 6, 7, 8, 5);
var innerinnerway = new OsmElement.Way(15);
innerinnerway.nodes().add(9, 10, 11, 12, 9);
var relation = new OsmElement.Relation(16);
relation.setTag("type", "multipolygon");
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, outerway.id(), "outer"));
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, innerway.id(), "inner"));
// nested hole marked as inner, but should actually be outer
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, innerinnerway.id(), "inner"));
List<OsmElement> 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
);
reader.processPass1Block(elements);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeLocationProvider();
elements.stream().flatMap(ways).forEach(way -> reader.processWayPass2(way, nodeCache));
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
public void testInvalidMultipolygon() throws GeometryException {
OsmReader reader = newOsmReader();
var outerway = new OsmElement.Way(13);
outerway.nodes().add(1, 2, 3, 4, 1);
var innerway = new OsmElement.Way(14);
innerway.nodes().add(5, 6, 7, 8, 5);
var innerinnerway = new OsmElement.Way(15);
innerinnerway.nodes().add(9, 10, 11, 12, 9);
var relation = new OsmElement.Relation(16);
relation.setTag("type", "multipolygon");
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, outerway.id(), "outer"));
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, innerway.id(), "inner"));
// nested hole marked as inner, but should actually be outer
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, innerinnerway.id(), "inner"));
List<OsmElement> 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
);
reader.processPass1Block(elements);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeLocationProvider();
elements.stream().flatMap(ways).forEach(way -> reader.processWayPass2(way, nodeCache));
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() {
OsmReader reader = newOsmReader();
var outerway = new OsmElement.Way(5);
outerway.nodes().add(1, 2, 3, 4, 1);
var relation = new OsmElement.Relation(6);
relation.setTag("type", "multipolygon");
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, outerway.id(), "outer"));
List<OsmElement> 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
);
reader.processPass1Block(elements);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeLocationProvider();
elements.stream().flatMap(ways).forEach(way -> reader.processWayPass2(way, nodeCache));
var feature = reader.processRelationPass2(relation, nodeCache);
assertThrows(GeometryException.class, feature::worldGeometry);
assertThrows(GeometryException.class, feature::polygon);
assertThrows(GeometryException.class, feature::validatedPolygon);
}
@Test
public void testMultiPolygonRefersToNonexistentWay() {
OsmReader reader = newOsmReader();
var relation = new OsmElement.Relation(6);
relation.setTag("type", "multipolygon");
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, 5, "outer"));
List<OsmElement> 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
);
reader.processPass1Block(elements);
elements.stream().flatMap(nodes).forEach(reader::processNodePass2);
var nodeCache = reader.newNodeLocationProvider();
elements.stream().flatMap(ways).forEach(way -> reader.processWayPass2(way, nodeCache));
var feature = reader.processRelationPass2(relation, nodeCache);
assertThrows(GeometryException.class, feature::worldGeometry);
assertThrows(GeometryException.class, feature::polygon);
assertThrows(GeometryException.class, feature::validatedPolygon);
}
@Test
public void testWayInRelation() {
record OtherRelInfo(long id) implements OsmRelationInfo {}
record TestRelInfo(long id, String name) implements OsmRelationInfo {}
OsmReader reader = new OsmReader("osm", () -> osmSource, nodeMap, new Profile.NullProfile() {
@Override
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
return List.of(new TestRelInfo(1, "name"));
}
}, stats);
var nodeCache = reader.newNodeLocationProvider();
var node1 = new OsmElement.Node(1, 0, 0);
var node2 = node(2, 0.75, 0.75);
var way = new OsmElement.Way(3);
way.nodes().add(node1.id(), node2.id());
way.setTag("key", "value");
var relation = new OsmElement.Relation(4);
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.WAY, 3, "rolename"));
reader.processPass1Block(List.of(node1, node2, way, relation));
SourceFeature feature = reader.processWayPass2(way, nodeCache);
assertEquals(List.of(), feature.relationInfo(OtherRelInfo.class));
assertEquals(List.of(new OsmReader.RelationMember<>("rolename", new TestRelInfo(1, "name"))),
feature.relationInfo(TestRelInfo.class));
}
@Test
public void testNodeOrWayRelationInRelationDoesntTriggerWay() {
record TestRelInfo(long id, String name) implements OsmRelationInfo {}
OsmReader reader = new OsmReader("osm", () -> osmSource, nodeMap, new Profile.NullProfile() {
@Override
public List<OsmRelationInfo> preprocessOsmRelation(OsmElement.Relation relation) {
return List.of(new TestRelInfo(1, "name"));
}
}, stats);
var nodeCache = reader.newNodeLocationProvider();
var node1 = new OsmElement.Node(1, 0, 0);
var node2 = node(2, 0.75, 0.75);
var way = new OsmElement.Way(3);
way.nodes().add(node1.id(), node2.id());
way.setTag("key", "value");
var relation = new OsmElement.Relation(4);
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.RELATION, 3, "rolename"));
relation.members().add(new OsmElement.Relation.Member(OsmElement.Type.NODE, 3, "rolename"));
reader.processPass1Block(List.of(node1, node2, way, relation));
SourceFeature feature = reader.processWayPass2(way, nodeCache);
assertEquals(List.of(), feature.relationInfo(TestRelInfo.class));
}
private OsmReader newOsmReader() {
return new OsmReader("osm", () -> osmSource, nodeMap, profile, stats);
}
}