diff --git a/src/main/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatch.java b/src/main/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatch.java index 10c990a31d0..28fc710a6ae 100644 --- a/src/main/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatch.java +++ b/src/main/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatch.java @@ -1,9 +1,19 @@ package org.opentripplanner.netex.validation; +import java.util.function.Predicate; import org.opentripplanner.graph_builder.issue.api.DataImportIssue; import org.rutebanken.netex.model.JourneyPattern_VersionStructure; +import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure; import org.rutebanken.netex.model.ServiceJourney; +import org.rutebanken.netex.model.StopPointInJourneyPattern; +import org.rutebanken.netex.model.StopUseEnumeration; +/** + * Validates that the number of passing times in the journey and the number of stop points in the + * pattern are equal. + * It also takes into account that some points in the pattern can be set to stopUse=passthrough + * which means that those must not be referenced in the journey. + */ class JourneyPatternSJMismatch extends AbstractHMapValidationRule { @Override @@ -12,16 +22,29 @@ public Status validate(ServiceJourney sj) { .getJourneyPatternsById() .lookup(getPatternId(sj)); - int nStopPointsInJourneyPattern = journeyPattern + int nStopPointsInJourneyPattern = (int) journeyPattern .getPointsInSequence() .getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern() - .size(); + .stream() + .filter(Predicate.not(JourneyPatternSJMismatch::isPassThrough)) + .count(); int nTimetablePassingTimes = sj.getPassingTimes().getTimetabledPassingTime().size(); return nStopPointsInJourneyPattern != nTimetablePassingTimes ? Status.DISCARD : Status.OK; } + /** + * Does the stop point in the sequence represent a stop where the vehicle passes through without + * stopping? + */ + private static boolean isPassThrough(PointInLinkSequence_VersionedChildStructure point) { + return ( + point instanceof StopPointInJourneyPattern spijp && + spijp.getStopUse() == StopUseEnumeration.PASSTHROUGH + ); + } + @Override public DataImportIssue logMessage(String key, ServiceJourney sj) { return new StopPointsMismatch(sj.getId(), getPatternId(sj)); diff --git a/src/test/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatchTest.java b/src/test/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatchTest.java new file mode 100644 index 00000000000..f1c87342800 --- /dev/null +++ b/src/test/java/org/opentripplanner/netex/validation/JourneyPatternSJMismatchTest.java @@ -0,0 +1,182 @@ +package org.opentripplanner.netex.validation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.netex.index.api.HMapValidationRule.Status.DISCARD; +import static org.opentripplanner.netex.index.api.HMapValidationRule.Status.OK; +import static org.rutebanken.netex.model.StopUseEnumeration.ACCESS; +import static org.rutebanken.netex.model.StopUseEnumeration.PASSTHROUGH; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opentripplanner.netex.index.NetexEntityIndex; +import org.opentripplanner.netex.mapping.MappingSupport; +import org.rutebanken.netex.model.EntityStructure; +import org.rutebanken.netex.model.JourneyPatternRefStructure; +import org.rutebanken.netex.model.PointInJourneyPatternRefStructure; +import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure; +import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure; +import org.rutebanken.netex.model.ServiceJourney; +import org.rutebanken.netex.model.ServiceJourneyPattern; +import org.rutebanken.netex.model.StopPointInJourneyPattern; +import org.rutebanken.netex.model.StopUseEnumeration; +import org.rutebanken.netex.model.TimetabledPassingTime; +import org.rutebanken.netex.model.TimetabledPassingTimes_RelStructure; + +class JourneyPatternSJMismatchTest { + + private static final String PATTERN_ID = "pattern"; + private static final String JOURNEY_ID = "journey"; + + @Test + void patternAndJourneyMatch() { + var pattern = new ServiceJourneyPatternBuilder(PATTERN_ID) + .withPointsInSequence(1, 2, 3) + .build(); + + var index = new NetexEntityIndex(); + index.journeyPatternsById.add(pattern); + + var journey = new ServiceJourneyBuilder(JOURNEY_ID) + .withPatternId(PATTERN_ID) + .withPassingTimes(pattern.getPointsInSequence()) + .build(); + + var rule = new JourneyPatternSJMismatch(); + rule.setup(index.readOnlyView()); + + assertEquals(OK, rule.validate(journey)); + } + + @Test + void differentNumberOfPassingTimes() { + var pattern = new ServiceJourneyPatternBuilder(PATTERN_ID) + .withPointsInSequence(1, 2, 3) + .build(); + + var index = new NetexEntityIndex(); + index.journeyPatternsById.add(pattern); + + var journey = new ServiceJourneyBuilder(JOURNEY_ID) + .withPatternId(PATTERN_ID) + .withPassingTimes(List.of("P-1", "P-2")) + .build(); + + var rule = new JourneyPatternSJMismatch(); + rule.setup(index.readOnlyView()); + + assertEquals(DISCARD, rule.validate(journey)); + } + + @Test + void passThrough() { + var pattern = new ServiceJourneyPatternBuilder(PATTERN_ID) + .addStopPointInSequence(1, ACCESS) + .addStopPointInSequence(2, PASSTHROUGH) + .addStopPointInSequence(3, ACCESS) + .build(); + + var index = new NetexEntityIndex(); + index.journeyPatternsById.add(pattern); + + var journey = new ServiceJourneyBuilder(JOURNEY_ID) + .withPatternId(PATTERN_ID) + .withPassingTimes(List.of("P-1", "P-3")) + .build(); + + var rule = new JourneyPatternSJMismatch(); + rule.setup(index.readOnlyView()); + + assertEquals(OK, rule.validate(journey)); + } + + static class ServiceJourneyPatternBuilder { + + private final ServiceJourneyPattern pattern = new ServiceJourneyPattern(); + private final PointsInJourneyPattern_RelStructure points = new PointsInJourneyPattern_RelStructure(); + + ServiceJourneyPatternBuilder(String id) { + pattern.setId(id); + pattern.setPointsInSequence(points); + } + + ServiceJourneyPatternBuilder withPointsInSequence(int... orders) { + var items = Arrays.stream(orders).mapToObj(order -> pointInPattern(order, ACCESS)).toList(); + points.withPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern( + items + ); + return this; + } + + ServiceJourneyPatternBuilder addStopPointInSequence(int order, StopUseEnumeration stopUse) { + var point = pointInPattern(order, stopUse); + points + .getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern() + .add(point); + return this; + } + + ServiceJourneyPattern build() { + return pattern; + } + + private static PointInLinkSequence_VersionedChildStructure pointInPattern( + int order, + StopUseEnumeration stopUse + ) { + var p = new StopPointInJourneyPattern(); + p.setId("P-%s".formatted(order)); + p.setOrder(BigInteger.valueOf(order)); + p.setStopUse(stopUse); + return p; + } + } + + static class ServiceJourneyBuilder { + + private final ServiceJourney journey = new ServiceJourney(); + + ServiceJourneyBuilder(String id) { + journey.setId(id); + } + + ServiceJourneyBuilder withPatternId(String id) { + var ref = MappingSupport.createWrappedRef(id, JourneyPatternRefStructure.class); + journey.withJourneyPatternRef(ref); + return this; + } + + ServiceJourneyBuilder withPassingTimes(PointsInJourneyPattern_RelStructure pointsInSequence) { + return withPassingTimes( + pointsInSequence + .getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern() + .stream() + .map(EntityStructure::getId) + .toList() + ); + } + + ServiceJourneyBuilder withPassingTimes(Collection ids) { + var passingTimes = new TimetabledPassingTimes_RelStructure(); + passingTimes.withTimetabledPassingTime( + ids.stream().map(ServiceJourneyBuilder::timetabledPassingTime).toList() + ); + journey.withPassingTimes(passingTimes); + return this; + } + + ServiceJourney build() { + return journey; + } + + private static TimetabledPassingTime timetabledPassingTime(String pointInPatternRef) { + var passingTime = new TimetabledPassingTime(); + passingTime.withPointInJourneyPatternRef( + MappingSupport.createWrappedRef(pointInPatternRef, PointInJourneyPatternRefStructure.class) + ); + return passingTime; + } + } +}