OpenTripPlanner/application/src/test/java/org/opentripplanner/graph_builder/module/OsmBoardingLocationsModuleTest.java
2026-02-18 07:55:23 +01:00

412 lines
15 KiB
Java

package org.opentripplanner.graph_builder.module;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.opentripplanner.routing.linking.TransitStopVertexBuilderFactory.ofStop;
import java.io.File;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.opentripplanner.core.model.i18n.I18NString;
import org.opentripplanner.core.model.i18n.NonLocalizedString;
import org.opentripplanner.graph_builder.module.osm.OsmModuleTestFactory;
import org.opentripplanner.osm.DefaultOsmProvider;
import org.opentripplanner.routing.linking.VertexLinkerTestFactory;
import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildRepository;
import org.opentripplanner.service.osminfo.internal.DefaultOsmInfoGraphBuildService;
import org.opentripplanner.street.geometry.SphericalDistanceLibrary;
import org.opentripplanner.street.graph.Graph;
import org.opentripplanner.street.model.edge.AreaEdge;
import org.opentripplanner.street.model.edge.BoardingLocationToStopLink;
import org.opentripplanner.street.model.edge.Edge;
import org.opentripplanner.street.model.edge.StreetEdge;
import org.opentripplanner.street.model.vertex.OsmBoardingLocationVertex;
import org.opentripplanner.street.model.vertex.TransitStopVertex;
import org.opentripplanner.street.model.vertex.Vertex;
import org.opentripplanner.street.model.vertex.VertexLabel;
import org.opentripplanner.streetadapter.VertexFactory;
import org.opentripplanner.test.support.ResourceLoader;
import org.opentripplanner.transit.model._data.TimetableRepositoryForTest;
import org.opentripplanner.transit.model.site.RegularStop;
import org.opentripplanner.transit.service.TimetableRepository;
class OsmBoardingLocationsModuleTest {
private final TimetableRepositoryForTest testModel = TimetableRepositoryForTest.of();
static Stream<Arguments> herrenbergTestCases() {
return Stream.of(
Arguments.of(
false,
Stream.of(
302563833L,
3223067049L,
302563836L,
3223067680L,
302563834L,
768590748L,
302563839L
)
.map(VertexLabel::osm)
.collect(Collectors.toSet())
),
Arguments.of(true, Set.of(VertexLabel.osm(768590748)))
);
}
/**
* We test that the platform area at Herrenberg station (https://www.openstreetmap.org/way/27558650)
* is correctly linked to the stop even though it is not the closest edge to the stop.
*/
@ParameterizedTest(
name = "add boarding locations and link them to platform edges when skipVisibility={0}"
)
@MethodSource("herrenbergTestCases")
void addAndLinkBoardingLocations(boolean areaVisibility, Set<String> linkedVertices) {
File file = ResourceLoader.of(OsmBoardingLocationsModuleTest.class).file(
"herrenberg-minimal.osm.pbf"
);
RegularStop platform = testModel
.stop("de:08115:4512:4:101")
.withCoordinate(48.59328, 8.86128)
.build();
RegularStop busStop = testModel.stop("de:08115:4512:5:C", 48.59434, 8.86452).build();
RegularStop floatingBusStop = testModel.stop("floating-bus-stop", 48.59417, 8.86464).build();
var siteRepo = testModel
.siteRepositoryBuilder()
.withRegularStops(List.of(platform, busStop, floatingBusStop))
.build();
var graph = new Graph();
var timetableRepository = new TimetableRepository(siteRepo);
var factory = new VertexFactory(graph);
var provider = new DefaultOsmProvider(file, false);
var floatingBusVertex = factory.transitStop(ofStop(floatingBusStop));
var floatingBoardingLocation = factory.osmBoardingLocation(
floatingBusVertex.getCoordinate(),
"floating-bus-stop",
Set.of(floatingBusVertex.getId().getId()),
new NonLocalizedString("bus stop not connected to street network")
);
var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository();
var osmModule = OsmModuleTestFactory.of(provider)
.withGraph(graph)
.withOsmInfoGraphBuildRepository(osmInfoRepository)
.builder()
.withBoardingAreaRefTags(Set.of("ref", "ref:IFOPT"))
.withAreaVisibility(areaVisibility)
.build();
osmModule.buildGraph();
var platformVertex = factory.transitStop(ofStop(platform));
var busVertex = factory.transitStop(ofStop(busStop));
timetableRepository.index();
graph.index();
assertEquals(0, busVertex.getIncoming().size());
assertEquals(0, busVertex.getOutgoing().size());
assertEquals(0, platformVertex.getIncoming().size());
assertEquals(0, platformVertex.getOutgoing().size());
var osmService = new DefaultOsmInfoGraphBuildService(osmInfoRepository);
new OsmBoardingLocationsModule(
graph,
timetableRepository,
VertexLinkerTestFactory.of(graph),
osmService
).buildGraph();
var boardingLocations = graph.getVerticesOfType(OsmBoardingLocationVertex.class);
// 3 nodes connected to the street network, plus one "floating" and one area centroid created by
// the module
assertEquals(5, boardingLocations.size());
assertEquals(1, platformVertex.getIncoming().size());
assertEquals(1, platformVertex.getOutgoing().size());
assertEquals(1, busVertex.getIncoming().size());
assertEquals(1, busVertex.getOutgoing().size());
var platformCentroids = boardingLocations
.stream()
.filter(l -> l.references.contains(platform.getId().getId()))
.toList();
var busBoardingLocation = boardingLocations
.stream()
.filter(b -> b.references.contains(busStop.getId().getId()))
.findFirst()
.orElseThrow();
assertConnections(
busBoardingLocation,
Set.of(BoardingLocationToStopLink.class, StreetEdge.class)
);
assertConnections(
floatingBoardingLocation,
Set.of(BoardingLocationToStopLink.class, StreetEdge.class)
);
assertEquals(1, platformCentroids.size());
var platformCentroid = platformCentroids.get(0);
assertConnections(platformCentroid, Set.of(BoardingLocationToStopLink.class, AreaEdge.class));
assertEquals(
linkedVertices,
platformCentroid
.getOutgoingStreetEdges()
.stream()
.map(Edge::getToVertex)
.map(Vertex::getLabel)
.collect(Collectors.toSet())
);
assertEquals(
linkedVertices,
platformCentroid
.getIncomingStreetEdges()
.stream()
.map(Edge::getFromVertex)
.map(Vertex::getLabel)
.collect(Collectors.toSet())
);
platformCentroids
.stream()
.flatMap(c -> Stream.concat(c.getIncoming().stream(), c.getOutgoing().stream()))
.forEach(e -> assertNotNull(e.getName(), "Edge " + e + " returns null for getName()"));
platformCentroids
.stream()
.flatMap(c -> Stream.concat(c.getIncoming().stream(), c.getOutgoing().stream()))
.filter(StreetEdge.class::isInstance)
.forEach(e -> assertEquals("Platform 101;102", e.getName().toString()));
}
/**
* We test that the underground platforms at Moorgate station (https://www.openstreetmap.org/way/1328222021)
* is correctly linked to the stop even though it is not the closest edge to the stop.
*/
@Test
void testLinearPlatforms() {
var graph = new Graph();
var osmInfoRepository = new DefaultOsmInfoGraphBuildRepository();
var osmModule = OsmModuleTestFactory.of(
new DefaultOsmProvider(
ResourceLoader.of(OsmBoardingLocationsModuleTest.class).file("moorgate.osm.pbf"),
false
)
)
.withGraph(graph)
.withOsmInfoGraphBuildRepository(osmInfoRepository)
.builder()
.withBoardingAreaRefTags(Set.of("naptan:AtcoCode"))
.build();
osmModule.buildGraph();
graph.index();
var factory = new VertexFactory(graph);
class TestCase {
/**
* The linear platform to be tested
*/
public final RegularStop platform;
/**
* The label of a vertex where the centroid should be connected to
*/
public final VertexLabel beginLabel;
/**
* The label of the other vertex where the centroid should be connected to
*/
public final VertexLabel endLabel;
private TransitStopVertex platformVertex = null;
public TestCase(RegularStop platform, VertexLabel beginLabel, VertexLabel endLabel) {
this.platform = platform;
this.beginLabel = beginLabel;
this.endLabel = endLabel;
}
/**
* Get a TransitStopVertex for the platform in the graph. It is made and added to the graph
* on the first call.
*/
TransitStopVertex getPlatformVertex() {
if (platformVertex == null) {
platformVertex = factory.transitStop(ofStop(platform));
}
return platformVertex;
}
}
var platform9 = testModel
.stop("9100MRGT9")
.withName(I18NString.of("Moorgate (Platform 9)"))
.withCoordinate(51.51922107872304, -0.08767468698832413)
.withPlatformCode("9")
.build();
var platform7 = testModel
.stop("9400ZZLUMGT3")
.withName(I18NString.of("Moorgate (Platform 7)"))
.withCoordinate(51.51919235051611, -0.08769925990953176)
.withPlatformCode("7")
.build();
var testCases = List.of(
new TestCase(platform9, VertexLabel.osm(12288669589L), VertexLabel.osm(12288675219L)),
new TestCase(platform7, VertexLabel.osm(12288669575L), VertexLabel.osm(12288675230L))
);
for (var testCase : testCases) {
// test that the platforms are not connected
var platformVertex = testCase.getPlatformVertex();
assertEquals(0, platformVertex.getIncoming().size());
assertEquals(0, platformVertex.getOutgoing().size());
// test that the vertices to be connected by the centroid are currently connected
var fromVertex = Objects.requireNonNull(graph.getVertex(testCase.beginLabel));
var toVertex = Objects.requireNonNull(graph.getVertex(testCase.endLabel));
assertTrue(
getEdge(fromVertex, toVertex).isPresent(),
"malformed test: the vertices where the centroid is supposed to be located between aren't connected"
);
assertTrue(
getEdge(toVertex, fromVertex).isPresent(),
"malformed test: the vertices where the centroid is supposed to be located between aren't connected"
);
}
var siteRepo = testModel
.siteRepositoryBuilder()
.withRegularStops(List.of(platform9, platform7))
.build();
new OsmBoardingLocationsModule(
graph,
new TimetableRepository(siteRepo),
VertexLinkerTestFactory.of(graph),
new DefaultOsmInfoGraphBuildService(osmInfoRepository)
).buildGraph();
var boardingLocations = graph.getVerticesOfType(OsmBoardingLocationVertex.class);
for (var testCase : testCases) {
var platformVertex = testCase.getPlatformVertex();
var fromVertex = Objects.requireNonNull(graph.getVertex(testCase.beginLabel));
var toVertex = Objects.requireNonNull(graph.getVertex(testCase.endLabel));
var centroid = boardingLocations
.stream()
.filter(b -> b.references.contains(testCase.platform.getId().getId()))
.findFirst()
.orElseThrow();
// TODO: we should ideally place the centroid vertex directly on the platform by splitting
// the platform edge, but it is too difficult to touch the splitter code to use a given
// centroid vertex instead of a generated split vertex, so what we actually do is to directly
// connect the platform vertex to the split vertex
// the actual centroid isn't used
assertEquals(0, centroid.getDegreeIn());
assertEquals(0, centroid.getDegreeOut());
for (var vertex : platformVertex.getIncoming()) {
assertSplitVertex(vertex.getFromVertex(), centroid, fromVertex, toVertex);
}
for (var vertex : platformVertex.getOutgoing()) {
assertSplitVertex(vertex.getToVertex(), centroid, fromVertex, toVertex);
}
}
}
/**
* Assert that a split vertex is near to the given centroid, and it is possible to travel between
* the original vertices through the split vertex in a straight line
*/
private static void assertSplitVertex(
Vertex splitVertex,
OsmBoardingLocationVertex centroid,
Vertex begin,
Vertex end
) {
var distance = SphericalDistanceLibrary.distance(
splitVertex.getCoordinate(),
centroid.getCoordinate()
);
// FIXME: I am not sure why the calculated centroid from the original OSM geometry is about 2 m
// from the platform
assertTrue(distance < 4, "The split vertex is more than 4 m apart from the centroid");
assertConnections(splitVertex, begin, end);
if (splitVertex != begin && splitVertex != end) {
var forwardEdges = getEdge(begin, splitVertex).flatMap(first ->
getEdge(splitVertex, end).map(second -> List.of(first, second))
);
var backwardEdges = getEdge(end, splitVertex).flatMap(first ->
getEdge(splitVertex, begin).map(second -> List.of(first, second))
);
for (var edgeList : List.of(forwardEdges, backwardEdges)) {
edgeList.ifPresent(edges ->
assertEquals(
edges.getFirst().getOutAngle(),
edges.getLast().getInAngle(),
"The split vertex is not on a straight line between the connected vertices"
)
);
}
}
}
/**
* Assert that there is a one-way path from the beginning through the given vertex to the end
* or vice versa.
*/
private static void assertConnections(Vertex vertex, Vertex beginning, Vertex end) {
if (vertex == beginning || vertex == end) {
assertTrue(beginning.isConnected(end));
}
assertTrue(
(getEdge(beginning, vertex).isPresent() && getEdge(vertex, end).isPresent()) ||
(getEdge(end, vertex).isPresent() && getEdge(vertex, beginning).isPresent())
);
}
private void assertConnections(
OsmBoardingLocationVertex busBoardingLocation,
Set<Class<? extends Edge>> expected
) {
Stream.of(busBoardingLocation.getIncoming(), busBoardingLocation.getOutgoing()).forEach(edges ->
assertEquals(expected, edges.stream().map(Edge::getClass).collect(Collectors.toSet()))
);
}
private static Optional<StreetEdge> getEdge(Vertex from, Vertex to) {
return from
.getOutgoingStreetEdges()
.stream()
.filter(edge -> edge.getToVertex() == to)
.findFirst();
}
}