From 56b138f11a65e657e8ea39c10eb6c3334fe663d1 Mon Sep 17 00:00:00 2001 From: Vincent Paturet Date: Mon, 16 Dec 2024 15:18:26 +0100 Subject: [PATCH] Add NonIncreasingTimeValidator and MismatchedTransportModeValidator --- pom.xml | 16 +- .../validator/jaxb/JAXBValidationContext.java | 2 +- .../jaxb/SiteFrameStopPlaceRepository.java | 2 +- .../validator/jaxb/StopPlaceRepository.java | 2 +- .../jaxb/model/stoptime/AbstractStopTime.java | 83 + .../jaxb/model/stoptime/FlexibleStopTime.java | 118 ++ .../jaxb/model/stoptime/RegularStopTime.java | 131 ++ .../model/stoptime/SortStopTimesUtil.java | 121 ++ .../jaxb/model/stoptime/StopTime.java | 88 + .../NonIncreasingPassingTimeValidator.java | 128 ++ ...smatchedTransportModeSubModeValidator.java | 221 +++ .../support/DatedServiceJourneyUtils.java | 2 +- .../validator/jaxb/support/NetexUtils.java | 94 + .../model/TransportModeAndSubMode.java | 2 +- .../test/jaxb/support/JAXBUtils.java | 3 +- .../support/NetexEntitiesTestFactory.java | 1687 +++++++++++++++++ .../support/TestCommonDataRepository.java | 65 + .../jaxb/support/TestStopPlaceRepository.java | 177 ++ ...NonIncreasingPassingTimeValidatorTest.java | 390 ++++ ...chedTransportModeSubModeValidatorTest.java | 485 +++++ 20 files changed, 3809 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java create mode 100644 src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java create mode 100644 src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java create mode 100644 src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java create mode 100644 src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java create mode 100644 src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java create mode 100644 src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java diff --git a/pom.xml b/pom.xml index 5d5575f6..aafea238 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.entur netex-validator-java - 9.0.2-SNAPSHOT + 10.0.0-SNAPSHOT netex-validator-java Library for validating NeTEx datasets against the Nordic NeTEx Profile. @@ -64,6 +64,7 @@ 1.17 5.11.3 5.14.2 + 2.2 1.5.12 1.6.0 @@ -102,6 +103,7 @@ gpg ${env.GPG_KEY_NAME} + @@ -154,6 +156,12 @@ ${slf4j.version} + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + + org.junit.jupiter @@ -179,6 +187,12 @@ ${mockito-core.version} test + + org.hamcrest + hamcrest + ${hamcrest.version} + test + org.zeroturnaround zt-zip diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java b/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java index e1fb7fb4..fe6c22d7 100644 --- a/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/JAXBValidationContext.java @@ -1,8 +1,8 @@ package org.entur.netex.validation.validator.jaxb; +import jakarta.annotation.Nullable; import java.util.*; import java.util.function.Function; -import javax.annotation.Nullable; import org.entur.netex.index.api.NetexEntitiesIndex; import org.entur.netex.validation.validator.DataLocation; import org.entur.netex.validation.validator.ValidationContext; diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java b/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java index 7e1f7c52..5b1862d6 100644 --- a/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/SiteFrameStopPlaceRepository.java @@ -1,6 +1,6 @@ package org.entur.netex.validation.validator.jaxb; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import org.entur.netex.index.api.NetexEntitiesIndex; import org.entur.netex.validation.validator.model.QuayCoordinates; import org.entur.netex.validation.validator.model.QuayId; diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java b/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java index 15921381..8664559f 100644 --- a/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/StopPlaceRepository.java @@ -15,7 +15,7 @@ package org.entur.netex.validation.validator.jaxb; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import org.entur.netex.validation.validator.model.QuayCoordinates; import org.entur.netex.validation.validator.model.QuayId; import org.entur.netex.validation.validator.model.StopPlaceId; diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java new file mode 100644 index 00000000..f6a532ef --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/AbstractStopTime.java @@ -0,0 +1,83 @@ +package org.entur.netex.validation.validator.jaxb.model.stoptime; + +import java.math.BigInteger; +import java.time.LocalTime; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.rutebanken.netex.model.TimetabledPassingTime; + +/** + * Wrapper around {@link TimetabledPassingTime} that provides a simpler interface for passing times + * comparison. Passing times are exposed as seconds since midnight, taking into account the day + * offset. + *

+ * This class does not take Daylight Saving Time transitions into account, this is an error and + * should be fixed. See https://github.com/opentripplanner/OpenTripPlanner/issues/5109 + */ +abstract sealed class AbstractStopTime + implements StopTime + permits FlexibleStopTime, RegularStopTime { + + private final ScheduledStopPointId scheduledStopPointId; + private final TimetabledPassingTime timetabledPassingTime; + + protected AbstractStopTime( + ScheduledStopPointId scheduledStopPointId, + TimetabledPassingTime timetabledPassingTime + ) { + this.scheduledStopPointId = scheduledStopPointId; + this.timetabledPassingTime = timetabledPassingTime; + } + + @Override + public ScheduledStopPointId scheduledStopPointId() { + return scheduledStopPointId; + } + + protected LocalTime arrivalTime() { + return timetabledPassingTime.getArrivalTime(); + } + + protected BigInteger arrivalDayOffset() { + return timetabledPassingTime.getArrivalDayOffset(); + } + + protected LocalTime latestArrivalTime() { + return timetabledPassingTime.getLatestArrivalTime(); + } + + protected BigInteger latestArrivalDayOffset() { + return timetabledPassingTime.getLatestArrivalDayOffset(); + } + + protected LocalTime departureTime() { + return timetabledPassingTime.getDepartureTime(); + } + + protected BigInteger departureDayOffset() { + return timetabledPassingTime.getDepartureDayOffset(); + } + + protected LocalTime earliestDepartureTime() { + return timetabledPassingTime.getEarliestDepartureTime(); + } + + protected BigInteger earliestDepartureDayOffset() { + return timetabledPassingTime.getEarliestDepartureDayOffset(); + } + + protected boolean isRegularStopFollowedByAreaStopValid( + FlexibleStopTime next + ) { + return ( + normalizedDepartureTimeOrElseArrivalTime() <= + next.normalizedEarliestDepartureTime() + ); + } + + protected boolean isAreaStopFollowedByRegularStopValid(RegularStopTime next) { + return ( + normalizedLatestArrivalTime() <= + next.normalizedArrivalTimeOrElseDepartureTime() + ); + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java new file mode 100644 index 00000000..16599655 --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/FlexibleStopTime.java @@ -0,0 +1,118 @@ +package org.entur.netex.validation.validator.jaxb.model.stoptime; + +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.rutebanken.netex.model.TimetabledPassingTime; + +/** + * Wrapper around {@link TimetabledPassingTime} that provides a simpler interface + * for passing times comparison. + * Passing times are exposed as seconds since midnight, taking into account the day offset. + */ +final class FlexibleStopTime extends AbstractStopTime { + + FlexibleStopTime( + ScheduledStopPointId scheduledStopPointId, + TimetabledPassingTime timetabledPassingTime + ) { + super(scheduledStopPointId, timetabledPassingTime); + } + + @Override + public boolean isComplete() { + return hasLatestArrivalTime() && hasEarliestDepartureTime(); + } + + @Override + public boolean isConsistent() { + return ( + !isComplete() || + normalizedLatestArrivalTime() >= normalizedEarliestDepartureTime() + ); + } + + @Override + public boolean isStopTimesIncreasing(StopTime next) { + if (next instanceof RegularStopTime regularStopTime) { + return isAreaStopFollowedByRegularStopValid(regularStopTime); + } + return isAreaStopFollowedByAreaStopValid((FlexibleStopTime) next); + } + + @Override + public int getStopTimeDiff(StopTime given) { + // TODO: This should be fixed. We need to take into account the type of given. + // Is it the same type as this, or not. See how we have done in + // isRegularStopFollowedByRegularStopValid, isAreaStopFollowedByAreaStopValid, + // isRegularStopFollowedByAreaStopValid, isAreaStopFollowedByRegularStopValid + + if (given instanceof FlexibleStopTime) { + return isComplete() + ? normalizedEarliestDepartureTime() - normalizedLatestArrivalTime() + : 0; + } + return ( + given.normalizedEarliestDepartureTime() - + normalizedArrivalTimeOrElseDepartureTime() + ); + } + + @Override + public int normalizedEarliestDepartureTime() { + return elapsedTimeSinceMidnight( + earliestDepartureTime(), + earliestDepartureDayOffset() + ); + } + + @Override + public int normalizedLatestArrivalTime() { + return elapsedTimeSinceMidnight( + latestArrivalTime(), + latestArrivalDayOffset() + ); + } + + @Override + public int normalizedDepartureTimeOrElseArrivalTime() { + throw new UnsupportedOperationException(); + } + + @Override + public int normalizedArrivalTimeOrElseDepartureTime() { + throw new UnsupportedOperationException(); + } + + private boolean hasLatestArrivalTime() { + return latestArrivalTime() != null; + } + + private boolean hasEarliestDepartureTime() { + return earliestDepartureTime() != null; + } + + private boolean isAreaStopFollowedByAreaStopValid(FlexibleStopTime next) { + int earliestDepartureTime = normalizedEarliestDepartureTime(); + int nextEarliestDepartureTime = next.normalizedEarliestDepartureTime(); + int latestArrivalTime = normalizedLatestArrivalTime(); + int nextLatestArrivalTime = next.normalizedLatestArrivalTime(); + + return ( + earliestDepartureTime <= nextEarliestDepartureTime && + latestArrivalTime <= nextLatestArrivalTime + ); + } + + @Override + public boolean isArrivalInMinutesResolution() { + return hasLatestArrivalTime() + ? latestArrivalTime().getSecond() == 0 + : earliestDepartureTime().getSecond() == 0; + } + + @Override + public boolean isDepartureInMinutesResolution() { + return hasEarliestDepartureTime() + ? earliestDepartureTime().getSecond() == 0 + : latestArrivalTime().getSecond() == 0; + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java new file mode 100644 index 00000000..b634a94d --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/RegularStopTime.java @@ -0,0 +1,131 @@ +package org.entur.netex.validation.validator.jaxb.model.stoptime; + +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.rutebanken.netex.model.TimetabledPassingTime; + +/** + * Wrapper around {@link TimetabledPassingTime} that provides a simpler interface + * for passing times comparison. + * Passing times are exposed as seconds since midnight, taking into account the day offset. + */ +final class RegularStopTime extends AbstractStopTime { + + RegularStopTime( + ScheduledStopPointId scheduledStopPointId, + TimetabledPassingTime timetabledPassingTime + ) { + super(scheduledStopPointId, timetabledPassingTime); + } + + @Override + public boolean isComplete() { + return hasArrivalTime() || hasDepartureTime(); + } + + @Override + public boolean isConsistent() { + return ( + arrivalTime() == null || + departureTime() == null || + normalizedDepartureTime() >= normalizedArrivalTime() + ); + } + + @Override + public boolean isStopTimesIncreasing(StopTime next) { + if (next instanceof RegularStopTime regularStopTime) { + return isRegularStopFollowedByRegularStopValid(regularStopTime); + } + return isRegularStopFollowedByAreaStopValid((FlexibleStopTime) next); + } + + @Override + public int getStopTimeDiff(StopTime given) { + // TODO: This should be fixed. We need to take into account the type of given. + // Is it the same type as this, or not. See how we have done in + // isRegularStopFollowedByRegularStopValid, isAreaStopFollowedByAreaStopValid, + // isRegularStopFollowedByAreaStopValid, isAreaStopFollowedByRegularStopValid + + if (given instanceof RegularStopTime) { + return ( + given.normalizedArrivalTimeOrElseDepartureTime() - + normalizedDepartureTimeOrElseArrivalTime() + ); + } + return ( + given.normalizedLatestArrivalTime() - + normalizedDepartureTimeOrElseArrivalTime() + ); + } + + @Override + public int normalizedEarliestDepartureTime() { + throw new UnsupportedOperationException(); + } + + @Override + public int normalizedLatestArrivalTime() { + throw new UnsupportedOperationException(); + } + + @Override + public int normalizedDepartureTimeOrElseArrivalTime() { + return hasDepartureTime() + ? normalizedDepartureTime() + : normalizedArrivalTime(); + } + + @Override + public int normalizedArrivalTimeOrElseDepartureTime() { + return hasArrivalTime() + ? normalizedArrivalTime() + : normalizedDepartureTime(); + } + + /** + * Return the elapsed time in second between midnight and the departure time, taking into account + * the day offset. + */ + private int normalizedDepartureTime() { + return elapsedTimeSinceMidnight(departureTime(), departureDayOffset()); + } + + /** + * Return the elapsed time in second between midnight and the arrival time, taking into account + * the day offset. + */ + private int normalizedArrivalTime() { + return elapsedTimeSinceMidnight(arrivalTime(), arrivalDayOffset()); + } + + private boolean hasArrivalTime() { + return arrivalTime() != null; + } + + private boolean hasDepartureTime() { + return departureTime() != null; + } + + private boolean isRegularStopFollowedByRegularStopValid( + RegularStopTime next + ) { + return ( + normalizedDepartureTimeOrElseArrivalTime() <= + next.normalizedArrivalTimeOrElseDepartureTime() + ); + } + + @Override + public boolean isArrivalInMinutesResolution() { + return hasArrivalTime() + ? arrivalTime().getSecond() == 0 + : departureTime().getSecond() == 0; + } + + @Override + public boolean isDepartureInMinutesResolution() { + return hasDepartureTime() + ? departureTime().getSecond() == 0 + : arrivalTime().getSecond() == 0; + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java new file mode 100644 index 00000000..2b6f84bf --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/SortStopTimesUtil.java @@ -0,0 +1,121 @@ +package org.entur.netex.validation.validator.jaxb.model.stoptime; + +import static java.util.Comparator.comparing; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.entur.netex.index.api.NetexEntitiesIndex; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.support.NetexUtils; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.rutebanken.netex.model.EntityStructure; +import org.rutebanken.netex.model.JourneyPattern; +import org.rutebanken.netex.model.ServiceJourney; +import org.rutebanken.netex.model.StopPointInJourneyPatternRefStructure; +import org.rutebanken.netex.model.TimetabledPassingTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This utility class is used to sort the timetabled passing times of a service journey according to + * their order in the journey pattern. + * The order of the passing times is determined by the order of the stop points in the journey pattern. + * The passing times are sorted by their order in the journey pattern, and warped in a StopTime object. + */ +public final class SortStopTimesUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger( + SortStopTimesUtil.class + ); + + /** + * Prevent instantiation of this utility class. + */ + private SortStopTimesUtil() {} + + /** + * Sort the timetabled passing times according to their order in the journey pattern. + */ + public static List getSortedStopTimes( + ServiceJourney serviceJourney, + JAXBValidationContext validationContext + ) { + JourneyPattern journeyPattern = validationContext.journeyPattern( + serviceJourney + ); + + if (journeyPattern == null) { + LOGGER.debug( + "No journey pattern ref found on service journey {}", + serviceJourney.getId() + ); + return List.of(); + } + + Map stopPointIdToOrder = getStopPointIdsOrder( + journeyPattern + ); + + Map scheduledStopPointIdByStopPointId = + NetexUtils.scheduledStopPointIdByStopPointId(journeyPattern); + + return validationContext + .timetabledPassingTimes(serviceJourney) + .stream() + .filter(SortStopTimesUtil::hasStopPointInJourneyPatternRef) + .sorted( + comparing(timetabledPassingTime -> + stopPointIdToOrder.get(NetexUtils.stopPointRef(timetabledPassingTime)) + ) + ) + .map(timetabledPassingTime -> + StopTime.of( + scheduledStopPointIdByStopPointId.get( + NetexUtils.stopPointRef(timetabledPassingTime) + ), + timetabledPassingTime, + hasFlexibleStopPoint( + validationContext.getNetexEntitiesIndex(), + scheduledStopPointIdByStopPointId.get( + NetexUtils.stopPointRef(timetabledPassingTime) + ) + ) + ) + ) + .toList(); + } + + private static boolean hasStopPointInJourneyPatternRef( + TimetabledPassingTime timetabledPassingTime + ) { + return ( + timetabledPassingTime + .getPointInJourneyPatternRef() + .getValue() instanceof StopPointInJourneyPatternRefStructure + ); + } + + private static Map getStopPointIdsOrder( + JourneyPattern journeyPattern + ) { + return NetexUtils + .stopPointsInJourneyPattern(journeyPattern) + .stream() + .collect( + Collectors.toMap( + EntityStructure::getId, + point -> point.getOrder().intValueExact() + ) + ); + } + + private static boolean hasFlexibleStopPoint( + NetexEntitiesIndex netexEntitiesIndex, + ScheduledStopPointId scheduledStopPointId + ) { + return netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .containsKey(scheduledStopPointId.id()); + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java new file mode 100644 index 00000000..1eb7cc96 --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/model/stoptime/StopTime.java @@ -0,0 +1,88 @@ +package org.entur.netex.validation.validator.jaxb.model.stoptime; + +import java.math.BigInteger; +import java.time.Duration; +import java.time.LocalTime; +import java.util.Objects; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.rutebanken.netex.model.TimetabledPassingTime; + +public sealed interface StopTime permits AbstractStopTime { + static StopTime of( + ScheduledStopPointId scheduledStopPointId, + TimetabledPassingTime timetabledPassingTime, + boolean stopIsFlexibleArea + ) { + return stopIsFlexibleArea + ? new FlexibleStopTime(scheduledStopPointId, timetabledPassingTime) + : new RegularStopTime(scheduledStopPointId, timetabledPassingTime); + } + + /** + * A passing time on a regular stop is complete if either arrival or departure time is present. A + * passing time on an area stop is complete if both earliest departure time and latest arrival + * time are present. + */ + boolean isComplete(); + + /** + * A passing time on a regular stop is consistent if departure time is after arrival time. A + * passing time on an area stop is consistent if latest arrival time is after earliest departure + * time. + */ + boolean isConsistent(); + + /** + * Return the elapsed time in second between midnight and the earliest departure time, taking into + * account the day offset. Only valid for area-stops, throw an exception if not. + */ + int normalizedEarliestDepartureTime(); + + /** + * Return the elapsed time in second between midnight and the latest arrival time, taking into + * account the day offset. Only valid for area-stops, throw an exception if not. + */ + int normalizedLatestArrivalTime(); + + /** + * Return the elapsed time in second between midnight and the departure time, taking into account + * the day offset. Fallback to arrival time if departure time is missing. + */ + int normalizedDepartureTimeOrElseArrivalTime(); + + /** + * Return the elapsed time in second between midnight and the arrival time, taking into account + * the day offset. Fallback to departure time if arrival time is missing. + */ + int normalizedArrivalTimeOrElseDepartureTime(); + + ScheduledStopPointId scheduledStopPointId(); + + /** + * Return {@code true} if this stop-time is before or equal to the given {@code next} stop time. + */ + boolean isStopTimesIncreasing(StopTime next); + + /** + * Return time between this and given time values with offset handling. + */ + int getStopTimeDiff(StopTime given); + + boolean isDepartureInMinutesResolution(); + + boolean isArrivalInMinutesResolution(); + + /** + * Return the elapsed time in second since midnight for a given local time, taking into account + * the day offset. + */ + default int elapsedTimeSinceMidnight(LocalTime time, BigInteger dayOffset) { + Objects.requireNonNull(time); + + int intOffsetValue = dayOffset != null ? dayOffset.intValueExact() : 0; + return (int) Duration + .between(LocalTime.MIDNIGHT, time) + .plus(Duration.ofDays(intOffsetValue)) + .toSeconds(); + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java new file mode 100644 index 00000000..7c73e5f2 --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidator.java @@ -0,0 +1,128 @@ +package org.entur.netex.validation.validator.jaxb.rules.servicejourney.passingtime; + +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import org.entur.netex.validation.validator.Severity; +import org.entur.netex.validation.validator.ValidationIssue; +import org.entur.netex.validation.validator.ValidationRule; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.JAXBValidator; +import org.entur.netex.validation.validator.jaxb.model.stoptime.SortStopTimesUtil; +import org.entur.netex.validation.validator.jaxb.model.stoptime.StopTime; +import org.rutebanken.netex.model.ServiceJourney; + +/** + * Validates that the passing times of a service journey are non-decreasing. + * This means that the time between each stop must be greater than or equal to zero. + */ +public class NonIncreasingPassingTimeValidator implements JAXBValidator { + + static final ValidationRule RULE_NON_INCREASING_TIME = new ValidationRule( + "TIMETABLED_PASSING_TIME_NON_INCREASING_TIME", + "ServiceJourney has non-increasing TimetabledPassingTime", + "ServiceJourney has non-increasing TimetabledPassingTime at: %s", + Severity.ERROR + ); + + static final ValidationRule RULE_INCOMPLETE_TIME = new ValidationRule( + "TIMETABLED_PASSING_TIME_INCOMPLETE_TIME", + "ServiceJourney has incomplete TimetabledPassingTime", + "ServiceJourney has incomplete TimetabledPassingTime at: %s", + Severity.ERROR + ); + + static final ValidationRule RULE_INCONSISTENT_TIME = new ValidationRule( + "TIMETABLED_PASSING_TIME_INCONSISTENT_TIME", + "ServiceJourney has inconsistent TimetabledPassingTime", + "ServiceJourney has inconsistent TimetabledPassingTime at: %s", + Severity.ERROR + ); + + @Override + public List validate( + JAXBValidationContext validationContext + ) { + return validationContext + .serviceJourneys() + .stream() + .map(serviceJourney -> + validateServiceJourney(serviceJourney, validationContext) + ) + .filter(Objects::nonNull) + .toList(); + } + + @Override + public Set getRules() { + return Set.of(RULE_NON_INCREASING_TIME); + } + + @Nullable + public ValidationIssue validateServiceJourney( + ServiceJourney serviceJourney, + JAXBValidationContext validationContext + ) { + List sortedTimetabledPassingTime = + SortStopTimesUtil.getSortedStopTimes(serviceJourney, validationContext); + var previousPassingTime = sortedTimetabledPassingTime.get(0); + ValidationIssue issueOnFirstStop = validateStopTime( + serviceJourney, + validationContext, + previousPassingTime + ); + if (issueOnFirstStop != null) { + return issueOnFirstStop; + } + + for (int i = 1; i < sortedTimetabledPassingTime.size(); i++) { + var currentPassingTime = sortedTimetabledPassingTime.get(i); + + ValidationIssue issue = validateStopTime( + serviceJourney, + validationContext, + currentPassingTime + ); + if (issue != null) { + return issue; + } + + if (!previousPassingTime.isStopTimesIncreasing(currentPassingTime)) { + return new ValidationIssue( + RULE_NON_INCREASING_TIME, + validationContext.dataLocation(serviceJourney.getId()), + validationContext.stopPointName( + previousPassingTime.scheduledStopPointId() + ) + ); + } + + previousPassingTime = currentPassingTime; + } + return null; + } + + @Nullable + private static ValidationIssue validateStopTime( + ServiceJourney serviceJourney, + JAXBValidationContext validationContext, + StopTime stopTime + ) { + if (!stopTime.isComplete()) { + return new ValidationIssue( + RULE_INCOMPLETE_TIME, + validationContext.dataLocation(serviceJourney.getId()), + validationContext.stopPointName(stopTime.scheduledStopPointId()) + ); + } + if (!stopTime.isConsistent()) { + return new ValidationIssue( + RULE_INCONSISTENT_TIME, + validationContext.dataLocation(serviceJourney.getId()), + validationContext.stopPointName(stopTime.scheduledStopPointId()) + ); + } + return null; + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java new file mode 100644 index 00000000..40571f0a --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidator.java @@ -0,0 +1,221 @@ +package org.entur.netex.validation.validator.jaxb.rules.servicejourney.transportmode; + +import static org.entur.netex.validation.validator.jaxb.support.NetexUtils.stopPointsInJourneyPattern; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.entur.netex.validation.validator.Severity; +import org.entur.netex.validation.validator.ValidationIssue; +import org.entur.netex.validation.validator.ValidationRule; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.JAXBValidator; +import org.entur.netex.validation.validator.model.QuayId; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.entur.netex.validation.validator.model.TransportModeAndSubMode; +import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration; +import org.rutebanken.netex.model.BusSubmodeEnumeration; +import org.rutebanken.netex.model.JourneyPattern; +import org.rutebanken.netex.model.ServiceJourney; +import org.rutebanken.netex.model.StopPointInJourneyPattern; + +/** + * Validates that the transport mode and sub-mode of a service journey matches the quays it visits. + */ +public class MismatchedTransportModeSubModeValidator implements JAXBValidator { + + static final ValidationRule RULE_INVALID_TRANSPORT_MODE = new ValidationRule( + "INVALID_TRANSPORT_MODE", + "Invalid transport mode", + "Invalid transport mode: The quay %s accepts %s, but the ServiceJourney %s is defined as %s", + Severity.ERROR + ); + + static final ValidationRule RULE_INVALID_TRANSPORT_SUB_MODE = + new ValidationRule( + "INVALID_TRANSPORT_SUB_MODE", + "Invalid transport sub-mode", + "Invalid transport sub-mode: The quay %s accepts %s, but the ServiceJourney %s is defined as %s", + Severity.ERROR + ); + + /** + * Iterate through all stop points of all service journeys and compare the transport mode of the associated quay with + * the transport mode of the service journey. + */ + @Override + public List validate( + JAXBValidationContext validationContext + ) { + List issues = new ArrayList<>(); + + for (ServiceJourney serviceJourney : validationContext.serviceJourneys()) { + TransportModeAndSubMode serviceJourneyTransportMode = + validationContext.transportModeAndSubMode(serviceJourney); + + // skip if neither the service journey nor the line have a transport mode. + // this should be validated separately. + if (serviceJourneyTransportMode == null) { + continue; + } + + JourneyPattern journeyPattern = validationContext.journeyPattern( + serviceJourney + ); + List stopPointInJourneyPatterns = + stopPointsInJourneyPattern(journeyPattern); + + for (StopPointInJourneyPattern stopPointInJourneyPattern : stopPointInJourneyPatterns) { + QuayId quayId = validationContext.quayIdForScheduledStopPoint( + ScheduledStopPointId.of(stopPointInJourneyPattern) + ); + + // skip if the scheduled stop point is not mapped to a quay. + // this should be validated separately. + if (quayId == null) { + continue; + } + TransportModeAndSubMode quayTransportModeAndSubMode = + validationContext.transportModeAndSubModeForQuayId(quayId); + + // skip if the quay does not have a transport mode. + // this can be caused by a data issue in the external stop register. + if (quayTransportModeAndSubMode == null) { + continue; + } + + validationIssue( + quayId.id(), + serviceJourney.getId(), + quayTransportModeAndSubMode, + serviceJourneyTransportMode, + validationContext + ) + .ifPresent(issues::add); + } + } + return issues; + } + + /** + * Return a validation issue if the service journey transport mode/submode does not match + * the quay transport mode/submode. + */ + private Optional validationIssue( + String quayId, + String serviceJourneyId, + TransportModeAndSubMode quayTransportModeAndSubMode, + TransportModeAndSubMode serviceJourneyTransportModeAndSubMode, + JAXBValidationContext validationContext + ) { + if ( + quayTransportModeAndSubMode.mode() != + serviceJourneyTransportModeAndSubMode.mode() + ) { + // Coach and bus are interchangeable. + if ( + busServingCoachStop( + quayTransportModeAndSubMode, + serviceJourneyTransportModeAndSubMode + ) || + coachServingBusStop( + quayTransportModeAndSubMode, + serviceJourneyTransportModeAndSubMode + ) + ) { + return Optional.empty(); + } + + // Taxi can stop on bus and coach stops. + if ( + taxiServingBusOrCoachStop( + quayTransportModeAndSubMode, + serviceJourneyTransportModeAndSubMode + ) + ) { + return Optional.empty(); + } + + return Optional.of( + new ValidationIssue( + RULE_INVALID_TRANSPORT_MODE, + validationContext.dataLocation(serviceJourneyId), + quayId, + quayTransportModeAndSubMode.mode(), + serviceJourneyId, + serviceJourneyTransportModeAndSubMode.mode() + ) + ); + } + + // Only rail replacement bus service can visit rail replacement bus stops. + if ( + quayTransportModeAndSubMode.subMode() != null && + BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS + .value() + .equals(quayTransportModeAndSubMode.subMode().name()) && + !BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS + .value() + .equals(serviceJourneyTransportModeAndSubMode.subMode().name()) + ) { + return Optional.of( + new ValidationIssue( + RULE_INVALID_TRANSPORT_SUB_MODE, + validationContext.dataLocation(serviceJourneyId), + quayId, + quayTransportModeAndSubMode.subMode(), + serviceJourneyId, + serviceJourneyTransportModeAndSubMode.subMode() + ) + ); + } + + return Optional.empty(); + } + + private static boolean taxiServingBusOrCoachStop( + TransportModeAndSubMode quayTransportModeAndSubMode, + TransportModeAndSubMode serviceJourneyTransportModeAndSubMode + ) { + return ( + serviceJourneyTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.TAXI && + ( + quayTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.BUS || + quayTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.COACH + ) + ); + } + + private static boolean coachServingBusStop( + TransportModeAndSubMode quayTransportModeAndSubMode, + TransportModeAndSubMode serviceJourneyTransportModeAndSubMode + ) { + return ( + quayTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.BUS && + serviceJourneyTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.COACH + ); + } + + private static boolean busServingCoachStop( + TransportModeAndSubMode quayTransportModeAndSubMode, + TransportModeAndSubMode serviceJourneyTransportModeAndSubMode + ) { + return ( + quayTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.COACH && + serviceJourneyTransportModeAndSubMode.mode() == + AllVehicleModesOfTransportEnumeration.BUS + ); + } + + @Override + public Set getRules() { + return Set.of(RULE_INVALID_TRANSPORT_MODE, RULE_INVALID_TRANSPORT_SUB_MODE); + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java b/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java index c6b35122..3e786e5c 100644 --- a/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/support/DatedServiceJourneyUtils.java @@ -1,7 +1,7 @@ package org.entur.netex.validation.validator.jaxb.support; +import jakarta.annotation.Nullable; import jakarta.xml.bind.JAXBElement; -import javax.annotation.Nullable; import org.rutebanken.netex.model.DatedServiceJourney; import org.rutebanken.netex.model.DatedServiceJourneyRefStructure; import org.rutebanken.netex.model.VersionOfObjectRefStructure; diff --git a/src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java b/src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java new file mode 100644 index 00000000..c7acd32f --- /dev/null +++ b/src/main/java/org/entur/netex/validation/validator/jaxb/support/NetexUtils.java @@ -0,0 +1,94 @@ +package org.entur.netex.validation.validator.jaxb.support; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.rutebanken.netex.model.JourneyPattern; +import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure; +import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure; +import org.rutebanken.netex.model.StopPointInJourneyPattern; +import org.rutebanken.netex.model.TimetabledPassingTime; + +/** + * Utility methods for JAXB NeTEx entities. + */ +public class NetexUtils { + + private NetexUtils() {} + + /** + * Return the StopPointInJourneyPattern ID of a given TimeTabledPassingTime. + */ + public static String stopPointRef( + TimetabledPassingTime timetabledPassingTime + ) { + return timetabledPassingTime + .getPointInJourneyPatternRef() + .getValue() + .getRef(); + } + + /** + * Return the mapping between stop point id and scheduled stop point id for the journey + * pattern. + */ + public static Map scheduledStopPointIdByStopPointId( + JourneyPattern journeyPattern + ) { + return stopPointsInJourneyPattern(journeyPattern) + .stream() + .collect( + Collectors.toMap( + StopPointInJourneyPattern::getId, + ScheduledStopPointId::of + ) + ); + } + + /** + * Find the stop points in journey pattern for the given journey pattern, sorted by order. + */ + public static List stopPointsInJourneyPattern( + JourneyPattern journeyPattern + ) { + return Optional + .ofNullable(journeyPattern.getPointsInSequence()) + .map( + PointsInJourneyPattern_RelStructure::getPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern + ) + .map(stopPointsInJourneyPattern -> + stopPointsInJourneyPattern + .stream() + .filter(StopPointInJourneyPattern.class::isInstance) + .map(StopPointInJourneyPattern.class::cast) + .sorted( + Comparator.comparing( + PointInLinkSequence_VersionedChildStructure::getOrder + ) + ) + ) + .orElse(Stream.empty()) + .toList(); + } + + /** + * Find the stop point in journey pattern for the + * given stop point in journey pattern reference. + */ + public static StopPointInJourneyPattern stopPointInJourneyPattern( + String stopPointInJourneyPatternRef, + JourneyPattern journeyPattern + ) { + return stopPointsInJourneyPattern(journeyPattern) + .stream() + .filter(stopPointInJourneyPattern -> + stopPointInJourneyPattern.getId().equals(stopPointInJourneyPatternRef) + ) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java b/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java index efc99e5c..68353ebf 100644 --- a/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java +++ b/src/main/java/org/entur/netex/validation/validator/model/TransportModeAndSubMode.java @@ -1,7 +1,7 @@ package org.entur.netex.validation.validator.model; +import jakarta.annotation.Nullable; import java.util.Objects; -import javax.annotation.Nullable; import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration; import org.rutebanken.netex.model.StopPlace; import org.rutebanken.netex.model.TransportSubmodeStructure; diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java index aeb0e014..359ee4fd 100644 --- a/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java +++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/JAXBUtils.java @@ -1,7 +1,6 @@ package org.entur.netex.validation.test.jaxb.support; import jakarta.xml.bind.JAXBElement; -import javax.annotation.Nonnull; import javax.xml.namespace.QName; import org.rutebanken.netex.model.VersionOfObjectRefStructure; @@ -53,7 +52,7 @@ public static T createRef( * @return the value wrapped in a JAXBElement */ @SuppressWarnings("unchecked") - public static JAXBElement createJaxbElement(@Nonnull T value) { + public static JAXBElement createJaxbElement(T value) { return new JAXBElement<>( new QName("x"), (Class) value.getClass(), diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java new file mode 100644 index 00000000..114162e3 --- /dev/null +++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/NetexEntitiesTestFactory.java @@ -0,0 +1,1687 @@ +package org.entur.netex.validation.test.jaxb.support; + +import static org.entur.netex.validation.test.jaxb.support.JAXBUtils.createJaxbElement; + +import jakarta.xml.bind.JAXBElement; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.math.BigInteger; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +import net.opengis.gml._3.AbstractRingPropertyType; +import net.opengis.gml._3.DirectPositionListType; +import net.opengis.gml._3.DirectPositionType; +import net.opengis.gml._3.LineStringType; +import net.opengis.gml._3.LinearRingType; +import net.opengis.gml._3.ObjectFactory; +import net.opengis.gml._3.PolygonType; +import org.entur.netex.index.api.NetexEntitiesIndex; +import org.entur.netex.index.impl.NetexEntitiesIndexImpl; +import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration; +import org.rutebanken.netex.model.DatedServiceJourney; +import org.rutebanken.netex.model.DatedServiceJourneyRefStructure; +import org.rutebanken.netex.model.DayType; +import org.rutebanken.netex.model.DayTypeAssignment; +import org.rutebanken.netex.model.DayTypeRefStructure; +import org.rutebanken.netex.model.DayTypeRefs_RelStructure; +import org.rutebanken.netex.model.DeadRun; +import org.rutebanken.netex.model.DestinationDisplayRefStructure; +import org.rutebanken.netex.model.EntityStructure; +import org.rutebanken.netex.model.FlexibleArea; +import org.rutebanken.netex.model.FlexibleLine; +import org.rutebanken.netex.model.FlexibleLineTypeEnumeration; +import org.rutebanken.netex.model.FlexibleStopPlace; +import org.rutebanken.netex.model.FlexibleStopPlace_VersionStructure; +import org.rutebanken.netex.model.JourneyPattern; +import org.rutebanken.netex.model.JourneyPatternRefStructure; +import org.rutebanken.netex.model.JourneyRefStructure; +import org.rutebanken.netex.model.Line; +import org.rutebanken.netex.model.LineRefStructure; +import org.rutebanken.netex.model.Line_VersionStructure; +import org.rutebanken.netex.model.LinkInJourneyPattern; +import org.rutebanken.netex.model.LinkInLinkSequence_VersionedChildStructure; +import org.rutebanken.netex.model.LinkSequenceProjection_VersionStructure; +import org.rutebanken.netex.model.LinksInJourneyPattern_RelStructure; +import org.rutebanken.netex.model.MultilingualString; +import org.rutebanken.netex.model.OperatingDay; +import org.rutebanken.netex.model.OperatingDayRefStructure; +import org.rutebanken.netex.model.OperatingPeriodRefStructure; +import org.rutebanken.netex.model.PassengerStopAssignment; +import org.rutebanken.netex.model.PointInLinkSequence_VersionedChildStructure; +import org.rutebanken.netex.model.PointsInJourneyPattern_RelStructure; +import org.rutebanken.netex.model.Projections_RelStructure; +import org.rutebanken.netex.model.QuayRefStructure; +import org.rutebanken.netex.model.Route; +import org.rutebanken.netex.model.RouteRefStructure; +import org.rutebanken.netex.model.ScheduledStopPointRefStructure; +import org.rutebanken.netex.model.ServiceAlterationEnumeration; +import org.rutebanken.netex.model.ServiceJourney; +import org.rutebanken.netex.model.ServiceJourneyInterchange; +import org.rutebanken.netex.model.ServiceJourneyRefStructure; +import org.rutebanken.netex.model.ServiceLink; +import org.rutebanken.netex.model.ServiceLinkRefStructure; +import org.rutebanken.netex.model.StopPlaceRefStructure; +import org.rutebanken.netex.model.StopPointInJourneyPattern; +import org.rutebanken.netex.model.StopPointInJourneyPatternRefStructure; +import org.rutebanken.netex.model.TimetabledPassingTime; +import org.rutebanken.netex.model.TimetabledPassingTimes_RelStructure; +import org.rutebanken.netex.model.TransportSubmodeStructure; +import org.rutebanken.netex.model.VehicleJourneyRefStructure; +import org.rutebanken.netex.model.VersionOfObjectRefStructure; + +/** + * Create JAXB NeTEx test data. + */ +public class NetexEntitiesTestFactory { + + private static final DayType EVERYDAY = new DayType() + .withId("EVERYDAY") + .withName(new MultilingualString().withValue("everyday")); + + private CreateGenericLine line; + + private CreateRoute route; + + private final List journeyPatterns = new ArrayList<>(); + private final List serviceJourneys = new ArrayList<>(); + private final List datedServiceJourneys = + new ArrayList<>(); + + private final List interchanges = + new ArrayList<>(); + private final List serviceLinks = new ArrayList<>(); + private final List flexibleStopPlaces = + new ArrayList<>(); + private final List deadRuns = new ArrayList<>(); + private final List passengerStopAssignments = + new ArrayList<>(); + + public NetexEntitiesIndex create() { + NetexEntitiesIndex netexEntitiesIndex = new NetexEntitiesIndexImpl(); + + if (line != null && line instanceof CreateLine createLine) { + netexEntitiesIndex.getLineIndex().put(line.ref(), createLine.create()); + } + + if (line != null && line instanceof CreateFlexibleLine createFlexibleLine) { + netexEntitiesIndex + .getFlexibleLineIndex() + .put(line.ref(), createFlexibleLine.create()); + } + + if (route != null) { + netexEntitiesIndex.getRouteIndex().put(route.ref(), route.create()); + } + + fillIndexes(netexEntitiesIndex); + return netexEntitiesIndex; + } + + private void fillIndexes(NetexEntitiesIndex netexEntitiesIndex) { + passengerStopAssignments + .stream() + .map(CreatePassengerStopAssignment::create) + .forEach(passengerStopAssignment -> { + // PassengerStopAssignmentsByStopPointRefIndex + netexEntitiesIndex + .getPassengerStopAssignmentsByStopPointRefIndex() + .put( + passengerStopAssignment + .getScheduledStopPointRef() + .getValue() + .getRef(), + passengerStopAssignment + ); + + // QuayIdByStopPointRefIndex + netexEntitiesIndex + .getQuayIdByStopPointRefIndex() + .put( + passengerStopAssignment + .getScheduledStopPointRef() + .getValue() + .getRef(), + passengerStopAssignment.getQuayRef().getValue().getRef() + ); + }); + + journeyPatterns + .stream() + .map(CreateJourneyPattern::create) + .forEach(journeyPattern -> + netexEntitiesIndex + .getJourneyPatternIndex() + .put(journeyPattern.getId(), journeyPattern) + ); + + interchanges + .stream() + .map(CreateServiceJourneyInterchange::create) + .forEach(interchange -> + netexEntitiesIndex + .getServiceJourneyInterchangeIndex() + .put(interchange.getId(), interchange) + ); + + serviceJourneys + .stream() + .map(CreateServiceJourney::create) + .forEach(journey -> + netexEntitiesIndex + .getServiceJourneyIndex() + .put(journey.getId(), journey) + ); + + datedServiceJourneys + .stream() + .map(CreateDatedServiceJourney::create) + .forEach(journey -> + netexEntitiesIndex + .getDatedServiceJourneyIndex() + .put(journey.getId(), journey) + ); + + deadRuns + .stream() + .map(CreateDeadRun::create) + .forEach(deadRun -> + netexEntitiesIndex.getDeadRunIndex().put(deadRun.getId(), deadRun) + ); + + serviceLinks + .stream() + .map(CreateServiceLink::create) + .forEach(serviceLink -> + netexEntitiesIndex + .getServiceLinkIndex() + .put(serviceLink.getId(), serviceLink) + ); + flexibleStopPlaces + .stream() + .map(CreateFlexibleStopPlace::create) + .forEach(flexibleStopPlace -> + netexEntitiesIndex + .getFlexibleStopPlaceIndex() + .put(flexibleStopPlace.getId(), flexibleStopPlace) + ); + } + + /** + * Create a line with the given id + * The existing line will be overwritten. + * + * @param id the id of the line + * @return CreateLine + */ + public CreateLine createLine(int id) { + line = new CreateLine(id); + return (CreateLine) line; + } + + /** + * Create a line with id 1. + * The existing line will be overwritten. + * + * @return CreateLine + */ + public CreateLine createLine() { + line = new CreateLine(1); + return (CreateLine) line; + } + + /** + * Create a flexible line with the given id + * The existing line will be overwritten. + * + * @param id the id of the line + * @return CreateFlexibleLine + */ + public CreateFlexibleLine createFlexibleLine(int id) { + line = new CreateFlexibleLine(id); + return (CreateFlexibleLine) line; + } + + /** + * Create a flexible line with id 1. + * The existing line will be overwritten. + * + * @return CreateFlexibleLine + */ + public CreateFlexibleLine createFlexibleLine() { + line = new CreateFlexibleLine(1); + return (CreateFlexibleLine) line; + } + + /** + * Create a route with the given id + * The existing route will be overwritten. + * + * @param id the id of the route + * @return CreateRoute + */ + public CreateRoute createRoute(int id) { + if (line == null) { + line = new CreateLine(1); + } + + route = new CreateRoute(id, line); + return route; + } + + /** + * Create a route with id 1. + * The existing route will be overwritten. + * + * @return CreateRoute + */ + public CreateRoute createRoute() { + if (line == null) { + line = new CreateLine(1); + } + + route = new CreateRoute(1, line); + return route; + } + + /** + * Adds a new journey pattern with the given id + * + * @param id the id of the journey pattern + * @return CreateJourneyPattern + */ + public CreateJourneyPattern createJourneyPattern(int id) { + CreateJourneyPattern createJourneyPattern = new CreateJourneyPattern(id); + journeyPatterns.add(createJourneyPattern); + return createJourneyPattern; + } + + /** + * Adds a new journey pattern with id 1 + * + * @return CreateJourneyPattern + */ + public CreateJourneyPattern createJourneyPattern() { + CreateJourneyPattern createJourneyPattern = new CreateJourneyPattern(1); + journeyPatterns.add(createJourneyPattern); + return createJourneyPattern; + } + + /** + * Adds a new service journey with the given id and the given journey pattern + * The line will be created if it does not exist, with id 1 + * + * @param id the id of the journey pattern + * @param journeyPattern the journey pattern ref for the service journey + * @return CreateServiceJourney + */ + public CreateServiceJourney createServiceJourney( + int id, + CreateJourneyPattern journeyPattern + ) { + if (line == null) { + line = new CreateLine(1); + } + + CreateServiceJourney createServiceJourney = new CreateServiceJourney( + id, + line, + journeyPattern + ); + serviceJourneys.add(createServiceJourney); + return createServiceJourney; + } + + /** + * Adds a new service journey with id 1 and the given journey pattern + * The line will be created if it does not exist, with id 1 + * + * @param journeyPattern the journey pattern ref for the service journey + * @return CreateServiceJourney + */ + public CreateServiceJourney createServiceJourney( + CreateJourneyPattern journeyPattern + ) { + if (line == null) { + line = new CreateLine(1); + } + + CreateServiceJourney createServiceJourney = new CreateServiceJourney( + 1, + line, + journeyPattern + ); + serviceJourneys.add(createServiceJourney); + return createServiceJourney; + } + + /** + * Adds a new dead run with the given id and the given journey pattern + * The line will be created if it does not exist, with id 1 + * + * @param id the id of the journey pattern + * @param journeyPattern the journey pattern ref for the dead run + * @return CreateDeadRun + */ + public CreateDeadRun createDeadRun( + int id, + CreateJourneyPattern journeyPattern + ) { + if (line == null) { + line = new CreateLine(1); + } + CreateDeadRun deadRun = new CreateDeadRun(id, line, journeyPattern); + deadRuns.add(deadRun); + return deadRun; + } + + /** + * Adds a new dead run with id 1 and the given journey pattern + * The line will be created if it does not exist, with id 1 + * + * @param journeyPattern the journey pattern ref for the dead run + * @return CreateDeadRun + */ + public CreateDeadRun createDeadRun(CreateJourneyPattern journeyPattern) { + if (line == null) { + line = new CreateLine(1); + } + CreateDeadRun deadRun = new CreateDeadRun(1, line, journeyPattern); + deadRuns.add(deadRun); + return deadRun; + } + + /** + * Adds a new dated service journey with the given id, service journey and operating day + * + * @param id the id of the dated service journey + * @param serviceJourneyRef the service journey ref for the dated service journey + * @param operatingDayRef the operating day ref for the dated service journey + * @return CreateDatedServiceJourney + */ + public CreateDatedServiceJourney createDatedServiceJourney( + int id, + CreateServiceJourney serviceJourneyRef, + CreateOperatingDay operatingDayRef + ) { + CreateDatedServiceJourney createDatedServiceJourney = + new CreateDatedServiceJourney(id, serviceJourneyRef, operatingDayRef); + datedServiceJourneys.add(createDatedServiceJourney); + return createDatedServiceJourney; + } + + /** + * Adds a new dated service journey with id 1, service journey and operating day + * + * @param serviceJourneyRef the service journey ref for the dated service journey + * @param operatingDayRef the operating day ref for the dated service journey + * @return CreateDatedServiceJourney + */ + public CreateDatedServiceJourney createDatedServiceJourney( + CreateServiceJourney serviceJourneyRef, + CreateOperatingDay operatingDayRef + ) { + CreateDatedServiceJourney createDatedServiceJourney = + new CreateDatedServiceJourney(1, serviceJourneyRef, operatingDayRef); + datedServiceJourneys.add(createDatedServiceJourney); + return createDatedServiceJourney; + } + + /** + * Adds numberOfServiceJourneys new service journeys with the given journey pattern. + * The line will be created if it does not exist, with id 1 + * The service journeys will have ids from 1 to numberOfServiceJourneys + * + * @param createJourneyPattern the journey pattern ref for the service journeys + * @param numberOfServiceJourneys the number of service journeys to create + * @return List of CreateServiceJourney created + */ + public List createServiceJourneys( + CreateJourneyPattern createJourneyPattern, + int numberOfServiceJourneys + ) { + if (line == null) { + line = new CreateLine(1); + } + List createServiceJourneys = IntStream + .rangeClosed(1, numberOfServiceJourneys) + .mapToObj(index -> + new CreateServiceJourney(index, line, createJourneyPattern) + ) + .toList(); + serviceJourneys.addAll(createServiceJourneys); + return createServiceJourneys; + } + + /** + * Adds a new service journey interchange with the given id + * + * @param id the id of the service journey interchange + * @return CreateServiceJourneyInterchange + */ + public CreateServiceJourneyInterchange createServiceJourneyInterchange( + int id + ) { + CreateServiceJourneyInterchange createServiceJourneyInterchange = + new CreateServiceJourneyInterchange(id); + interchanges.add(createServiceJourneyInterchange); + return createServiceJourneyInterchange; + } + + /** + * Adds a new service journey interchange with id 1 + * + * @return CreateServiceJourneyInterchange + */ + public CreateServiceJourneyInterchange createServiceJourneyInterchange() { + CreateServiceJourneyInterchange createServiceJourneyInterchange = + new CreateServiceJourneyInterchange(1); + interchanges.add(createServiceJourneyInterchange); + return createServiceJourneyInterchange; + } + + /** + * Adds a new service link with the given id + * + * @param id the id of the service link + * @return CreateServiceLink + */ + public CreateServiceLink createServiceLink( + int id, + ScheduledStopPointRefStructure fromScheduledStopPointRef, + ScheduledStopPointRefStructure toScheduledStopPointRef + ) { + CreateServiceLink createServiceLink = new CreateServiceLink(id) + .withFromScheduledStopPointRef(fromScheduledStopPointRef) + .withToScheduledStopPointRef(toScheduledStopPointRef); + serviceLinks.add(createServiceLink); + return createServiceLink; + } + + /** + * Adds a new service link with id 1 + * + * @return CreateServiceLink + */ + public CreateServiceLink createServiceLink( + ScheduledStopPointRefStructure fromScheduledStopPointRef, + ScheduledStopPointRefStructure toScheduledStopPointRef + ) { + CreateServiceLink createServiceLink = new CreateServiceLink(1) + .withFromScheduledStopPointRef(fromScheduledStopPointRef) + .withToScheduledStopPointRef(toScheduledStopPointRef); + serviceLinks.add(createServiceLink); + return createServiceLink; + } + + /** + * Adds a new flexible stop place with the given id + * + * @param id the id of the flexible stop place + * @return CreateFlexibleStopPlace + */ + public CreateFlexibleStopPlace createFlexibleStopPlace(int id) { + CreateFlexibleStopPlace createFlexibleStopPlace = + new CreateFlexibleStopPlace(id); + flexibleStopPlaces.add(createFlexibleStopPlace); + return createFlexibleStopPlace; + } + + /** + * Adds a new flexible stop place with id 1 + * + * @return CreateFlexibleStopPlace + */ + public CreateFlexibleStopPlace createFlexibleStopPlace() { + CreateFlexibleStopPlace createFlexibleStopPlace = + new CreateFlexibleStopPlace(1); + flexibleStopPlaces.add(createFlexibleStopPlace); + return createFlexibleStopPlace; + } + + /** + * Adds a new passenger stop assignment with the given id + * + * @param id the id of the passenger stop assignment + * @return CreatePassengerStopAssignment + */ + public CreatePassengerStopAssignment createPassengerStopAssignment(int id) { + CreatePassengerStopAssignment createPassengerStopAssignment = + new CreatePassengerStopAssignment(id); + passengerStopAssignments.add(createPassengerStopAssignment); + return createPassengerStopAssignment; + } + + /** + * Adds a new passenger stop assignment with id 1 + * + * @return CreatePassengerStopAssignment + */ + public CreatePassengerStopAssignment createPassengerStopAssignment() { + CreatePassengerStopAssignment createPassengerStopAssignment = + new CreatePassengerStopAssignment(1); + passengerStopAssignments.add(createPassengerStopAssignment); + return createPassengerStopAssignment; + } + + /** + * Adds a new day type with the given id + * + * @param id the id of the day type + * @return CreateDayType + */ + public CreateOperatingDay createOperatingDay(int id, LocalDate date) { + return new CreateOperatingDay(id, date); + } + + /** + * Adds a new day type with id 1 + * + * @return CreateDayType + */ + public CreateOperatingDay createOperatingDay(LocalDate date) { + return new CreateOperatingDay(1, date); + } + + /** + * Creates the new ScheduledStopPointRefStructure with the given id + * + * @param id the id of the ScheduledStopPoint + * @return ScheduledStopPointRefStructure + */ + public static ScheduledStopPointRefStructure createScheduledStopPointRef( + int id + ) { + return new ScheduledStopPointRefStructure() + .withRef("TST:ScheduledStopPoint:" + id); + } + + /** + * Creates the new QuayRefStructure with the given id + * + * @param id the id of the Quay + * @return QuayRefStructure + */ + public static QuayRefStructure createQuayRef(int id) { + return new QuayRefStructure().withRef("TST:Quay:" + id); + } + + /** + * Creates the new StopPlaceRefStructure with the given id + * + * @param id the id of the StopPlace + * @return StopPlaceRefStructure + */ + public static StopPlaceRefStructure createStopPointRef(int id) { + return new StopPlaceRefStructure().withRef("TST:StopPoint:" + id); + } + + /** + * Creates the new StopPlaceRefStructure with the given id + * + * @param id the id of the StopPlace + * @return StopPlaceRefStructure + */ + public static ServiceLinkRefStructure createServiceLinkRef(int id) { + return new ServiceLinkRefStructure().withRef("TST:ServiceLink:" + id); + } + + /** + * Creates the new VehicleJourneyRefStructure with the given id + * + * @param id the id of the VehicleJourney + * @return VehicleJourneyRefStructure + */ + public static VehicleJourneyRefStructure createServiceJourneyRef(int id) { + return new VehicleJourneyRefStructure().withRef("TST:ServiceJourney:" + id); + } + + /** + * Creates the new DatedServiceJourneyRefStructure with the given id + * + * @param id the id of the DatedServiceJourney + * @return DatedServiceJourneyRefStructure + */ + public static DestinationDisplayRefStructure createDestinationDisplayRef( + int id + ) { + return new DestinationDisplayRefStructure() + .withRef("TST:DestinationDisplay:" + id); + } + + /** + * This interface enables the CreateEntity classes to the reference object of their ids. + * + * @param + */ + public interface CreateRef { + R refObject(); + } + + /** + * Abstract class for automatic handling of entity reference. + * It creates the reference using reflection, based on the integer id provided + * in the constructor + */ + public abstract static class CreateEntity { + + protected final int id; + + public CreateEntity(int id) { + this.id = id; + } + + public final String ref() { + Type type = + ( + (ParameterizedType) getClass().getGenericSuperclass() + ).getActualTypeArguments()[0]; + return "TST:" + ((Class) type).getSimpleName() + ":" + id; + } + + public abstract T create(); + } + + public static class CreateFlexibleArea extends CreateEntity { + + private List coordinates; + private boolean withNullPolygon = false; + + public CreateFlexibleArea(int id) { + super(id); + } + + public CreateFlexibleArea withCoordinates(List coordinates) { + this.coordinates = coordinates; + return this; + } + + public CreateFlexibleArea withNullPolygon(boolean withNullPolygon) { + this.withNullPolygon = withNullPolygon; + return this; + } + + public FlexibleArea create() { + LinearRingType linearRing = new LinearRingType(); + DirectPositionListType positionList = new DirectPositionListType() + .withValue(coordinates); + linearRing.withPosList(positionList); + + FlexibleArea flexibleArea = new FlexibleArea() + .withId(ref()) + .withName(new MultilingualString().withValue("FlexibleArea " + id)); + + if (withNullPolygon) { + return flexibleArea.withPolygon(null); + } + + return flexibleArea.withPolygon( + new PolygonType() + .withExterior( + new AbstractRingPropertyType() + .withAbstractRing( + new ObjectFactory().createLinearRing(linearRing) + ) + ) + ); + } + } + + public static class CreateFlexibleStopPlace + extends CreateEntity { + + private CreateFlexibleArea flexibleArea; + + public CreateFlexibleStopPlace(int id) { + super(id); + } + + /** + * Creates a new flexible area with the given id, + * if it does not already exist. + * + * @param id the id of the flexible area + * @return CreateFlexibleArea + */ + public CreateFlexibleArea flexibleArea(int id) { + if (flexibleArea == null) { + flexibleArea = new CreateFlexibleArea(id); + } + return flexibleArea; + } + + public FlexibleStopPlace create() { + return new FlexibleStopPlace() + .withId(ref()) + .withName(new MultilingualString().withValue("FlexibleStopPlace " + id)) + .withAreas( + new FlexibleStopPlace_VersionStructure.Areas() + .withFlexibleAreaOrFlexibleAreaRefOrHailAndRideArea( + Optional + .ofNullable(flexibleArea) + .map(CreateFlexibleArea::create) + .orElse(null) + ) + ); + } + } + + public static class CreatePassengerStopAssignment + extends CreateEntity { + + private ScheduledStopPointRefStructure scheduleStopPointRef; + + private StopPlaceRefStructure stopPlaceRef; + + private QuayRefStructure quayRef; + + public CreatePassengerStopAssignment(int id) { + super(id); + } + + public CreatePassengerStopAssignment withScheduledStopPointRef( + ScheduledStopPointRefStructure scheduledStopPointRef + ) { + this.scheduleStopPointRef = scheduledStopPointRef; + return this; + } + + public CreatePassengerStopAssignment withStopPlaceRef( + StopPlaceRefStructure stopPlaceRef + ) { + this.stopPlaceRef = stopPlaceRef; + return this; + } + + public CreatePassengerStopAssignment withQuayRef(QuayRefStructure QuayRef) { + this.quayRef = QuayRef; + return this; + } + + public PassengerStopAssignment create() { + return new PassengerStopAssignment() + .withId(ref()) + .withScheduledStopPointRef(createJaxbElement(scheduleStopPointRef)) + .withQuayRef(createJaxbElement(quayRef)) + .withStopPlaceRef(createJaxbElement(stopPlaceRef)); + } + } + + public static class CreateDatedServiceJourney + extends CreateEntity { + + private final CreateOperatingDay operatingDayRef; + private final CreateServiceJourney serviceJourneyRef; + private CreateDatedServiceJourney datedServiceJourneyRef; + private ServiceAlterationEnumeration serviceAlteration; + + public CreateDatedServiceJourney( + int id, + CreateServiceJourney serviceJourneyRef, + CreateOperatingDay operatingDayRef + ) { + super(id); + this.serviceJourneyRef = serviceJourneyRef; + this.operatingDayRef = operatingDayRef; + } + + public CreateDatedServiceJourney withServiceAlteration( + ServiceAlterationEnumeration serviceAlteration + ) { + this.serviceAlteration = serviceAlteration; + return this; + } + + public CreateDatedServiceJourney withDatedServiceJourneyRef( + CreateDatedServiceJourney datedServiceJourneyRef + ) { + this.datedServiceJourneyRef = datedServiceJourneyRef; + return this; + } + + public DatedServiceJourney create() { + DatedServiceJourney datedServiceJourney = new DatedServiceJourney() + .withId(ref()); + + Collection> journeyRefs = + new ArrayList<>(); + journeyRefs.add( + createJaxbElement( + new ServiceJourneyRefStructure().withRef(serviceJourneyRef.ref()) + ) + ); + if (datedServiceJourneyRef != null) { + journeyRefs.add( + createJaxbElement( + new DatedServiceJourneyRefStructure() + .withRef(datedServiceJourneyRef.ref()) + ) + ); + } + + return datedServiceJourney + .withJourneyRef(journeyRefs) + .withOperatingDayRef( + new OperatingDayRefStructure().withRef(operatingDayRef.ref()) + ) + .withServiceAlteration(serviceAlteration); + } + } + + public static class CreateOperatingDay extends CreateEntity { + + private final LocalDate calendarDate; + + public CreateOperatingDay(int id, LocalDate calendarDate) { + super(id); + this.calendarDate = calendarDate; + } + + public OperatingDay create() { + return new OperatingDay() + .withId(ref()) + .withCalendarDate(calendarDate.atStartOfDay()); + } + } + + public static class CreateDayType extends CreateEntity { + + public CreateDayType(int id) { + super(id); + } + + public DayType create() { + return new DayType().withId(ref()); + } + } + + public static class CreateDayTypeAssignment + extends CreateEntity { + + private CreateDayType DayTypeRef; + private LocalDate date; + private CreateOperatingDay operatingDayRef; + // TODO: create CreateOperatingPeriod + private String operatingPeriodRef; + + public CreateDayTypeAssignment(int id) { + super(id); + } + + public CreateDayTypeAssignment withDate(LocalDate date) { + this.date = date; + this.operatingDayRef = null; + this.operatingPeriodRef = null; + return this; + } + + public CreateDayTypeAssignment withOperatingDayRef( + CreateOperatingDay operatingDayRef + ) { + this.operatingDayRef = operatingDayRef; + this.date = null; + this.operatingPeriodRef = null; + return this; + } + + public CreateDayTypeAssignment withOperatingPeriodRef( + String operatingPeriodRef + ) { + this.operatingPeriodRef = operatingPeriodRef; + this.date = null; + this.operatingDayRef = null; + return this; + } + + public DayTypeAssignment create() { + DayTypeAssignment dayTypeAssignment = new DayTypeAssignment() + .withId(ref()); + + Optional + .ofNullable(date) + .ifPresent(d -> dayTypeAssignment.withDate(d.atStartOfDay())); + + Optional + .ofNullable(operatingDayRef) + .ifPresent(ref -> + dayTypeAssignment.withOperatingDayRef( + new OperatingDayRefStructure().withRef(ref.ref()) + ) + ); + + Optional + .ofNullable(operatingPeriodRef) + .ifPresent(ref -> + dayTypeAssignment.withOperatingPeriodRef( + createJaxbElement(new OperatingPeriodRefStructure().withRef(ref)) + ) + ); + + return dayTypeAssignment; + } + } + + public abstract static class CreateGenericLine< + T extends Line_VersionStructure + > + extends CreateEntity { + + protected AllVehicleModesOfTransportEnumeration transportMode; + protected TransportSubmodeStructure transportSubmode; + + public CreateGenericLine(int id) { + super(id); + } + + public CreateGenericLine withTransportMode( + AllVehicleModesOfTransportEnumeration transportMode + ) { + this.transportMode = transportMode; + return this; + } + + public CreateGenericLine withTransportSubmode( + TransportSubmodeStructure transportSubmode + ) { + this.transportSubmode = transportSubmode; + return this; + } + } + + public static class CreateLine extends CreateGenericLine { + + public CreateLine(int id) { + super(id); + } + + public Line create() { + return new Line() + .withId(ref()) + .withName(new MultilingualString().withValue("Line " + id)) + .withTransportMode(transportMode) + .withTransportSubmode(transportSubmode); + } + } + + public static class CreateFlexibleLine + extends CreateGenericLine { + + private FlexibleLineTypeEnumeration flexibleLineType; + + public CreateFlexibleLine(int id) { + super(id); + } + + public CreateFlexibleLine withFlexibleLineType( + FlexibleLineTypeEnumeration flexibleLineType + ) { + this.flexibleLineType = flexibleLineType; + return this; + } + + public FlexibleLine create() { + return new FlexibleLine() + .withId(ref()) + .withFlexibleLineType(flexibleLineType) + .withName(new MultilingualString().withValue("FlexibleLine " + id)) + .withTransportMode(transportMode) + .withTransportSubmode(transportSubmode); + } + } + + public static class CreateRoute extends CreateEntity { + + private final CreateGenericLine lineRef; + + public CreateRoute( + int id, + CreateGenericLine lineRef + ) { + super(id); + this.lineRef = lineRef; + } + + public Route create() { + return new Route() + .withId(ref()) + .withLineRef( + createJaxbElement(new LineRefStructure().withRef(lineRef.ref())) + ); + } + } + + public static class CreateJourneyPattern + extends CreateEntity { + + private CreateRoute routeRef; + + private final List stopPointsInJourneyPatterns = + new ArrayList<>(); + + private final List serviceLinksInJourneyPatterns = + new ArrayList<>(); + + private boolean noServiceLinksInJourneyPattern = false; + + public CreateJourneyPattern(int id) { + super(id); + } + + public CreateJourneyPattern withRoute(CreateRoute routeRef) { + this.routeRef = routeRef; + return this; + } + + public CreateJourneyPattern withNoServiceLinksInJourneyPattern() { + this.noServiceLinksInJourneyPattern = true; + return this; + } + + /** + * Adds a new stop point in the journey pattern with the given id + * + * @param id the id of the stop point in the journey pattern + * @return CreateStopPointInJourneyPattern + */ + public CreateStopPointInJourneyPattern createStopPointInJourneyPattern( + int id + ) { + CreateStopPointInJourneyPattern createStopPointInJourneyPattern = + new CreateStopPointInJourneyPattern(id) + .withOrder(id) + .withScheduledStopPointRef(createScheduledStopPointRef(id)); + stopPointsInJourneyPatterns.add(createStopPointInJourneyPattern); + return createStopPointInJourneyPattern; + } + + /** + * Adds a new service link in the journey pattern with the given id + * + * @param id the id of the service link in the journey pattern + * @return CreateLinkInJourneyPattern + */ + public CreateLinkInJourneyPattern createServiceLinkInJourneyPattern( + int id + ) { + CreateLinkInJourneyPattern createLinkInJourneyPattern = + new CreateLinkInJourneyPattern(id); + serviceLinksInJourneyPatterns.add(createLinkInJourneyPattern); + return createLinkInJourneyPattern; + } + + /** + * Adds numberOfStopPointInJourneyPattern new stop points in the journey pattern + * The stop points will have ids from 1 to numberOfStopPointInJourneyPattern + * + * @param numberOfStopPointInJourneyPattern the number of stop points to create + * @return List of CreateStopPointInJourneyPattern created + */ + public List createStopPointsInJourneyPattern( + int numberOfStopPointInJourneyPattern + ) { + List stopPointsInJourneyPatterns = + IntStream + .rangeClosed(1, numberOfStopPointInJourneyPattern) + .mapToObj(index -> { + CreateStopPointInJourneyPattern createStopPointInJourneyPattern = + new CreateStopPointInJourneyPattern(index) + .withOrder(index) + .withScheduledStopPointRef(createScheduledStopPointRef(index)) + .withForBoarding(index == 1) // first stop point + .withForAlighting(index == numberOfStopPointInJourneyPattern); // last stop point + + // Setting destination display id for first and last stop point + if (index == 1 || index == numberOfStopPointInJourneyPattern) { + createStopPointInJourneyPattern.withDestinationDisplayId( + createDestinationDisplayRef(index) + ); + } + + return createStopPointInJourneyPattern; + }) + .toList(); + + this.stopPointsInJourneyPatterns.addAll(stopPointsInJourneyPatterns); + return stopPointsInJourneyPatterns; + } + + /** + * Adds numberOfServiceLinksInJourneyPattern new service links in the journey pattern + * The service links will have ids from 1 to numberOfServiceLinksInJourneyPattern + * + * @param numberOfServiceLinksInJourneyPattern the number of service links to create + * @return List of CreateLinkInJourneyPattern created + */ + public List createServiceLinksInJourneyPattern( + int numberOfServiceLinksInJourneyPattern + ) { + List linksInJourneyPatterns = IntStream + .range(0, numberOfServiceLinksInJourneyPattern) + .mapToObj(index -> + new CreateLinkInJourneyPattern(index + 1) + .withOrder(index + 1) + .withServiceLinkRef(createServiceLinkRef(index + 1)) + ) + .toList(); + + serviceLinksInJourneyPatterns.addAll(linksInJourneyPatterns); + return linksInJourneyPatterns; + } + + public JourneyPattern create() { + JourneyPattern journeyPattern = new JourneyPattern().withId(ref()); + + if (routeRef != null) { + journeyPattern.withRouteRef( + new RouteRefStructure().withRef(routeRef.ref()) + ); + } + + journeyPattern.withPointsInSequence( + new PointsInJourneyPattern_RelStructure() + .withPointInJourneyPatternOrStopPointInJourneyPatternOrTimingPointInJourneyPattern( + this.stopPointsInJourneyPatterns.isEmpty() + ? List.of() + : this.stopPointsInJourneyPatterns.stream() + .map(CreateStopPointInJourneyPattern::create) + .map(PointInLinkSequence_VersionedChildStructure.class::cast) + .toList() + ) + ); + + if (!noServiceLinksInJourneyPattern) { + journeyPattern.withLinksInSequence( + new LinksInJourneyPattern_RelStructure() + .withServiceLinkInJourneyPatternOrTimingLinkInJourneyPattern( + this.serviceLinksInJourneyPatterns.isEmpty() + ? List.of() + : this.serviceLinksInJourneyPatterns.stream() + .map(CreateLinkInJourneyPattern::create) + .map(LinkInLinkSequence_VersionedChildStructure.class::cast) + .toList() + ) + ); + } + + return journeyPattern; + } + } + + public static class CreateStopPointInJourneyPattern + extends CreateEntity { + + private int order = 1; + private ScheduledStopPointRefStructure scheduledStopPointRef; + private DestinationDisplayRefStructure destinationDisplayRef; + private boolean forAlighting = false; + private boolean forBoarding = false; + + public CreateStopPointInJourneyPattern(int id) { + super(id); + } + + public CreateStopPointInJourneyPattern withOrder(int order) { + this.order = order; + return this; + } + + public CreateStopPointInJourneyPattern withScheduledStopPointRef( + ScheduledStopPointRefStructure scheduledStopPointRef + ) { + this.scheduledStopPointRef = scheduledStopPointRef; + return this; + } + + public CreateStopPointInJourneyPattern withDestinationDisplayId( + DestinationDisplayRefStructure destinationDisplayRef + ) { + this.destinationDisplayRef = destinationDisplayRef; + return this; + } + + public CreateStopPointInJourneyPattern withForAlighting( + boolean forAlighting + ) { + this.forAlighting = forAlighting; + return this; + } + + public CreateStopPointInJourneyPattern withForBoarding( + boolean forBoarding + ) { + this.forBoarding = forBoarding; + return this; + } + + public StopPointInJourneyPattern create() { + StopPointInJourneyPattern stopPointInJourneyPattern = + new StopPointInJourneyPattern() + .withId(ref()) + .withOrder(BigInteger.valueOf(order)); + + if (scheduledStopPointRef != null) { + stopPointInJourneyPattern.withScheduledStopPointRef( + createJaxbElement(scheduledStopPointRef) + ); + } + + if (destinationDisplayRef != null) { + stopPointInJourneyPattern.setDestinationDisplayRef( + createJaxbElement(destinationDisplayRef).getValue() + ); + } + + stopPointInJourneyPattern.withForAlighting(forAlighting); + stopPointInJourneyPattern.withForBoarding(forBoarding); + + return stopPointInJourneyPattern; + } + } + + public static class CreateLinkInJourneyPattern + extends CreateEntity { + + private int order = 1; + private ServiceLinkRefStructure serviceLinkRef; + + public CreateLinkInJourneyPattern(int id) { + super(id); + } + + public CreateLinkInJourneyPattern withOrder(int order) { + this.order = order; + return this; + } + + public CreateLinkInJourneyPattern withServiceLinkRef( + ServiceLinkRefStructure serviceLinkRef + ) { + this.serviceLinkRef = serviceLinkRef; + return this; + } + + public LinkInJourneyPattern create() { + return new LinkInJourneyPattern() + .withId(ref()) + .withOrder(BigInteger.valueOf(order)) + .withServiceLinkRef(serviceLinkRef); + } + } + + public static class CreateDeadRun extends CreateEntity { + + private final CreateGenericLine lineRef; + private final CreateJourneyPattern journeyPattern; + private final List timetabledPassingTimes = + new ArrayList<>(); + + public CreateDeadRun( + int id, + CreateGenericLine lineRef, + CreateJourneyPattern journeyPattern + ) { + super(id); + this.lineRef = lineRef; + this.journeyPattern = journeyPattern; + } + + /** + * Adds a new timetabled passing time with the given id + * + * @param id the id of the timetabled passing time + * @param createStopPointInJourneyPattern the stop point in the journey pattern ref for the timetabled passing time + * @return CreateTimetabledPassingTime + */ + public CreateTimetabledPassingTime createTimetabledPassingTime( + int id, + CreateStopPointInJourneyPattern createStopPointInJourneyPattern + ) { + CreateTimetabledPassingTime createTimetabledPassingTime = + new CreateTimetabledPassingTime(id, createStopPointInJourneyPattern); + timetabledPassingTimes.add(createTimetabledPassingTime); + return createTimetabledPassingTime; + } + + public DeadRun create() { + DeadRun deadRun = new DeadRun() + .withId(ref()) + .withLineRef( + createJaxbElement(new LineRefStructure().withRef(lineRef.ref())) + ) + .withDayTypes(createEveryDayRefs()) + .withJourneyPatternRef( + createJaxbElement( + new JourneyPatternRefStructure().withRef(journeyPattern.ref()) + ) + ); + + deadRun.withPassingTimes( + new TimetabledPassingTimes_RelStructure() + .withTimetabledPassingTime( + timetabledPassingTimes + .stream() + .map(CreateTimetabledPassingTime::create) + .toList() + ) + ); + + return deadRun; + } + } + + public static class CreateServiceJourney + extends CreateEntity + implements CreateRef { + + private final CreateGenericLine line; + private final CreateJourneyPattern journeyPattern; + private final List timetabledPassingTimes = + new ArrayList<>(); + private AllVehicleModesOfTransportEnumeration transportMode; + private TransportSubmodeStructure transportSubmode; + + public CreateServiceJourney( + int id, + CreateGenericLine line, + CreateJourneyPattern journeyPattern + ) { + super(id); + this.line = line; + this.journeyPattern = journeyPattern; + } + + public VehicleJourneyRefStructure refObject() { + return NetexEntitiesTestFactory.createServiceJourneyRef(id); + } + + /** + * Adds a new timetabled passing time with the given id + * + * @param id the id of the timetabled passing time + * @param createStopPointInJourneyPattern the stop point in the journey pattern ref for the timetabled passing time + * @return CreateTimetabledPassingTime + */ + public CreateTimetabledPassingTime createTimetabledPassingTime( + int id, + CreateStopPointInJourneyPattern createStopPointInJourneyPattern + ) { + CreateTimetabledPassingTime createTimetabledPassingTime = + new CreateTimetabledPassingTime(id, createStopPointInJourneyPattern); + timetabledPassingTimes.add(createTimetabledPassingTime); + return createTimetabledPassingTime; + } + + public CreateServiceJourney withTransportMode( + AllVehicleModesOfTransportEnumeration transportMode + ) { + this.transportMode = transportMode; + return this; + } + + public CreateServiceJourney withTransportSubmode( + TransportSubmodeStructure transportSubmode + ) { + this.transportSubmode = transportSubmode; + return this; + } + + public ServiceJourney create() { + ServiceJourney serviceJourney = new ServiceJourney() + .withId(ref()) + .withLineRef( + createJaxbElement(new LineRefStructure().withRef(line.ref())) + ) + .withDayTypes(createEveryDayRefs()) + .withJourneyPatternRef( + createJaxbElement( + new JourneyPatternRefStructure().withRef(journeyPattern.ref()) + ) + ); + + serviceJourney.withPassingTimes( + new TimetabledPassingTimes_RelStructure() + .withTimetabledPassingTime( + timetabledPassingTimes + .stream() + .map(CreateTimetabledPassingTime::create) + .toList() + ) + ); + + if (transportMode != null) { + serviceJourney.withTransportMode(transportMode); + } + + if (transportSubmode != null) { + serviceJourney.withTransportSubmode(transportSubmode); + } + + return serviceJourney; + } + } + + public static class CreateServiceJourneyInterchange + extends CreateEntity { + + private boolean guaranteed = true; + private Duration maximumWaitTime; + private ScheduledStopPointRefStructure fromPointRef; + private ScheduledStopPointRefStructure toPointRef; + private VehicleJourneyRefStructure fromJourneyRef; + private VehicleJourneyRefStructure toJourneyRef; + + public CreateServiceJourneyInterchange(int id) { + super(id); + } + + public CreateServiceJourneyInterchange withGuaranteed(boolean guaranteed) { + this.guaranteed = guaranteed; + return this; + } + + public CreateServiceJourneyInterchange withMaximumWaitTime( + Duration maximumWaitTime + ) { + this.maximumWaitTime = maximumWaitTime; + return this; + } + + public CreateServiceJourneyInterchange withFromPointRef( + ScheduledStopPointRefStructure fromPointRef + ) { + this.fromPointRef = fromPointRef; + return this; + } + + public CreateServiceJourneyInterchange withToPointRef( + ScheduledStopPointRefStructure toPointRef + ) { + this.toPointRef = toPointRef; + return this; + } + + public CreateServiceJourneyInterchange withFromJourneyRef( + VehicleJourneyRefStructure fromJourneyRef + ) { + this.fromJourneyRef = fromJourneyRef; + return this; + } + + public CreateServiceJourneyInterchange withToJourneyRef( + VehicleJourneyRefStructure toJourneyRef + ) { + this.toJourneyRef = toJourneyRef; + return this; + } + + public ServiceJourneyInterchange create() { + ServiceJourneyInterchange serviceJourneyInterchange = + new ServiceJourneyInterchange() + .withId(ref()) + .withGuaranteed(guaranteed) + .withMaximumWaitTime(maximumWaitTime); + + if (fromPointRef != null) { + serviceJourneyInterchange.withFromPointRef(fromPointRef); + } + + if (toPointRef != null) { + serviceJourneyInterchange.withToPointRef(toPointRef); + } + + if (fromJourneyRef != null) { + serviceJourneyInterchange.withFromJourneyRef(fromJourneyRef); + } + + if (toJourneyRef != null) { + serviceJourneyInterchange.withToJourneyRef(toJourneyRef); + } + + return serviceJourneyInterchange; + } + } + + public static class CreateTimetabledPassingTime + extends CreateEntity { + + private final CreateStopPointInJourneyPattern pointInJourneyPattern; + private LocalTime departureTime; + private LocalTime arrivalTime; + private LocalTime earliestDepartureTime; + private LocalTime latestArrivalTime; + + public CreateTimetabledPassingTime( + int id, + CreateStopPointInJourneyPattern pointInJourneyPattern + ) { + super(id); + this.pointInJourneyPattern = pointInJourneyPattern; + } + + public CreateTimetabledPassingTime withDepartureTime( + LocalTime departureTime + ) { + this.departureTime = departureTime; + return this; + } + + public CreateTimetabledPassingTime withArrivalTime(LocalTime arrivalTime) { + this.arrivalTime = arrivalTime; + return this; + } + + public CreateTimetabledPassingTime withEarliestDepartureTime( + LocalTime earliestDepartureTime + ) { + this.earliestDepartureTime = earliestDepartureTime; + return this; + } + + public CreateTimetabledPassingTime withLatestArrivalTime( + LocalTime latestArrivalTime + ) { + this.latestArrivalTime = latestArrivalTime; + return this; + } + + public TimetabledPassingTime create() { + return new TimetabledPassingTime() + .withId(ref()) + .withDepartureTime(departureTime) + .withArrivalTime(arrivalTime) + .withEarliestDepartureTime(earliestDepartureTime) + .withLatestArrivalTime(latestArrivalTime) + .withPointInJourneyPatternRef( + createJaxbElement( + new StopPointInJourneyPatternRefStructure() + .withRef(pointInJourneyPattern.ref()) + ) + ); + } + } + + public static class CreateServiceLink extends CreateEntity { + + private ScheduledStopPointRefStructure fromScheduledStopPointRef; + private ScheduledStopPointRefStructure toScheduledStopPointRef; + private LinkSequenceProjection_VersionStructure linkSequenceProjection_VersionStructure; + + public CreateServiceLink(int id) { + super(id); + } + + public CreateServiceLink withFromScheduledStopPointRef( + ScheduledStopPointRefStructure fromScheduledStopPointRef + ) { + this.fromScheduledStopPointRef = fromScheduledStopPointRef; + return this; + } + + public CreateServiceLink withToScheduledStopPointRef( + ScheduledStopPointRefStructure toScheduledStopPointRef + ) { + this.toScheduledStopPointRef = toScheduledStopPointRef; + return this; + } + + public CreateServiceLink withLineStringList( + List lineStringPositions + ) { + this.linkSequenceProjection_VersionStructure = + new LinkSequenceProjection_VersionStructure() + .withId("TST:ServiceLinkProjection:" + id) + .withLineString( + new LineStringType() + .withPosList( + new DirectPositionListType().withValue(lineStringPositions) + ) + ); + return this; + } + + public CreateServiceLink withLineStringPositions( + List lineStringPositions + ) { + this.linkSequenceProjection_VersionStructure = + new LinkSequenceProjection_VersionStructure() + .withId("TST:ServiceLinkProjection:" + id) + .withLineString( + new LineStringType() + .withPosOrPointProperty( + lineStringPositions.toArray(Object[]::new) + ) + ); + return this; + } + + public ServiceLink create() { + return new ServiceLink() + .withId(ref()) + .withFromPointRef(fromScheduledStopPointRef) + .withToPointRef(toScheduledStopPointRef) + .withProjections( + new Projections_RelStructure() + .withProjectionRefOrProjection( + createJaxbElement(linkSequenceProjection_VersionStructure) + ) + ); + } + } + + private static DayTypeRefs_RelStructure createEveryDayRefs() { + return new DayTypeRefs_RelStructure() + .withDayTypeRef(Collections.singleton(createEveryDayRef())); + } + + private static JAXBElement createEveryDayRef() { + return createJaxbElement( + new DayTypeRefStructure().withRef(EVERYDAY.getId()) + ); + } +} diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java new file mode 100644 index 00000000..153de38f --- /dev/null +++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestCommonDataRepository.java @@ -0,0 +1,65 @@ +package org.entur.netex.validation.test.jaxb.support; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.entur.netex.validation.validator.jaxb.CommonDataRepository; +import org.entur.netex.validation.validator.model.FromToScheduledStopPointId; +import org.entur.netex.validation.validator.model.QuayId; +import org.entur.netex.validation.validator.model.ScheduledStopPointId; +import org.entur.netex.validation.validator.model.ServiceLinkId; + +/** + * CommonRepository implementation for tests. + */ +public class TestCommonDataRepository implements CommonDataRepository { + + private final Map quayForScheduledStopPoint; + + TestCommonDataRepository( + Map quayForScheduledStopPoint + ) { + this.quayForScheduledStopPoint = quayForScheduledStopPoint; + } + + /** + * Return a common data repository that maps ScheduledStopPoint #i to Quay #i + */ + public static CommonDataRepository of(int numScheduledStopPoints) { + Map stopPointIdQuayIdMap = IntStream + .rangeClosed(1, numScheduledStopPoints) + .boxed() + .collect( + Collectors.toUnmodifiableMap( + index -> new ScheduledStopPointId("TST:ScheduledStopPoint:" + index), + index -> new QuayId("TST:Quay:" + index) + ) + ); + + return new TestCommonDataRepository(stopPointIdQuayIdMap); + } + + @Override + public boolean hasSharedScheduledStopPoints(String validationReportId) { + return !quayForScheduledStopPoint.isEmpty(); + } + + @Override + public QuayId quayIdForScheduledStopPoint( + ScheduledStopPointId scheduledStopPointId, + String validationReportId + ) { + if (scheduledStopPointId == null) { + return null; + } + return quayForScheduledStopPoint.get(scheduledStopPointId); + } + + @Override + public FromToScheduledStopPointId fromToScheduledStopPointIdForServiceLink( + ServiceLinkId serviceLinkId, + String validationReportId + ) { + return null; + } +} diff --git a/src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java new file mode 100644 index 00000000..246913b2 --- /dev/null +++ b/src/test/java/org/entur/netex/validation/test/jaxb/support/TestStopPlaceRepository.java @@ -0,0 +1,177 @@ +package org.entur.netex.validation.test.jaxb.support; + +import jakarta.annotation.Nullable; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.entur.netex.validation.validator.jaxb.StopPlaceRepository; +import org.entur.netex.validation.validator.model.QuayCoordinates; +import org.entur.netex.validation.validator.model.QuayId; +import org.entur.netex.validation.validator.model.StopPlaceId; +import org.entur.netex.validation.validator.model.TransportModeAndSubMode; +import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration; +import org.rutebanken.netex.model.BusSubmodeEnumeration; +import org.rutebanken.netex.model.CoachSubmodeEnumeration; +import org.rutebanken.netex.model.MultilingualString; +import org.rutebanken.netex.model.Quay; +import org.rutebanken.netex.model.RailSubmodeEnumeration; +import org.rutebanken.netex.model.StopPlace; + +/** + * StopPlaceRepository implementation for tests. + */ +public class TestStopPlaceRepository implements StopPlaceRepository { + + private final Map stopPlaces; + private final Map quays; + private final Map stopPlaceForQuay; + + TestStopPlaceRepository(Map quayforStopPlace) { + this.quays = + quayforStopPlace + .values() + .stream() + .collect( + Collectors.toUnmodifiableMap(QuayId::ofValidId, Function.identity()) + ); + this.stopPlaces = + quayforStopPlace + .keySet() + .stream() + .collect( + Collectors.toUnmodifiableMap( + stopPlace -> new StopPlaceId(stopPlace.getId()), + Function.identity() + ) + ); + this.stopPlaceForQuay = + quayforStopPlace + .entrySet() + .stream() + .collect( + Collectors.toUnmodifiableMap( + entry -> QuayId.ofValidId(entry.getValue()), + Map.Entry::getKey + ) + ); + } + + /** + * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode + * bus/local bus + */ + public static StopPlaceRepository ofLocalBusStops(int numStops) { + return ofTransportMode( + numStops, + stopPlace -> + stopPlace + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + } + + /** + * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode + * bus/rail replacement bus + */ + public static StopPlaceRepository ofRailReplacementBusStops(int numStops) { + return ofTransportMode( + numStops, + stopPlace -> + stopPlace + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withBusSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS) + ); + } + + /** + * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode + * coach/national coach + */ + + public static StopPlaceRepository ofNationalCoachStops(int numStops) { + return ofTransportMode( + numStops, + stopPlace -> + stopPlace + .withTransportMode(AllVehicleModesOfTransportEnumeration.COACH) + .withCoachSubmode(CoachSubmodeEnumeration.NATIONAL_COACH) + ); + } + + /** + * Return a stop place repository containing numStops stop places and numStops quays with transport mode/submode + * rail/local + */ + public static StopPlaceRepository ofLocalTrainStops(int numStops) { + return ofTransportMode( + numStops, + stopPlace -> + stopPlace + .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL) + .withRailSubmode(RailSubmodeEnumeration.LOCAL) + ); + } + + /** + * Return a stop place repository containing numStops stop places and numStops quays where the transport modes and + * submodes are missing + */ + public static StopPlaceRepository ofMissingTransportModeAndSubMode( + int numStops + ) { + return ofTransportMode(numStops, Function.identity()); + } + + private static StopPlaceRepository ofTransportMode( + int numStops, + Function setTransportMode + ) { + Map stopPlaceQuayMap = IntStream + .rangeClosed(1, numStops) + .boxed() + .collect( + Collectors.toUnmodifiableMap( + stopIndex -> + setTransportMode.apply( + new StopPlace() + .withId("TST:StopPlace:" + stopIndex) + .withName( + new MultilingualString().withValue("StopPlace " + stopIndex) + ) + ), + quayIndex -> new Quay().withId("TST:Quay:" + quayIndex) + ) + ); + return new TestStopPlaceRepository(stopPlaceQuayMap); + } + + @Override + public boolean hasStopPlaceId(StopPlaceId stopPlaceId) { + return stopPlaces.containsKey(stopPlaceId); + } + + @Override + public boolean hasQuayId(QuayId quayId) { + return quays.containsKey(quayId); + } + + @Nullable + @Override + public TransportModeAndSubMode getTransportModesForQuayId(QuayId quayId) { + return TransportModeAndSubMode.of(stopPlaceForQuay.get(quayId)); + } + + @Nullable + @Override + public QuayCoordinates getCoordinatesForQuayId(QuayId quayId) { + return QuayCoordinates.of(quays.get(quayId)); + } + + @Nullable + @Override + public String getStopPlaceNameForQuayId(QuayId quayId) { + return stopPlaceForQuay.get(quayId).getName().getValue(); + } +} diff --git a/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java new file mode 100644 index 00000000..d6493519 --- /dev/null +++ b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/passingtime/NonIncreasingPassingTimeValidatorTest.java @@ -0,0 +1,390 @@ +package org.entur.netex.validation.validator.jaxb.rules.servicejourney.passingtime; + +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +import org.entur.netex.index.api.NetexEntitiesIndex; +import org.entur.netex.validation.test.jaxb.support.NetexEntitiesTestFactory; +import org.entur.netex.validation.test.jaxb.support.TestCommonDataRepository; +import org.entur.netex.validation.test.jaxb.support.TestStopPlaceRepository; +import org.entur.netex.validation.validator.ValidationIssue; +import org.entur.netex.validation.validator.ValidationRule; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rutebanken.netex.model.ScheduledStopPointRefStructure; + +class NonIncreasingPassingTimeValidatorTest { + + private static final String TEST_CODESPACE = "ENT"; + private static final String TEST_LINE_XML_FILE = "line.xml"; + private static final String VALIDATION_REPORT_ID = "Test1122"; + + private static final int NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN = 4; + + private NonIncreasingPassingTimeValidator validator; + private NetexEntitiesTestFactory netexEntitiesTestFactory; + private List timetabledPassingTimes; + private List departureTimes; + private List scheduledStopPointRefs; + + @BeforeEach + void setup() { + validator = new NonIncreasingPassingTimeValidator(); + + netexEntitiesTestFactory = new NetexEntitiesTestFactory(); + + NetexEntitiesTestFactory.CreateJourneyPattern createJourneyPattern = + netexEntitiesTestFactory.createJourneyPattern(); + + NetexEntitiesTestFactory.CreateServiceJourney createServiceJourney = + netexEntitiesTestFactory.createServiceJourney(createJourneyPattern); + + scheduledStopPointRefs = + IntStream + .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN) + .mapToObj(NetexEntitiesTestFactory::createScheduledStopPointRef) + .toList(); + + List stopPointInJourneyPatterns = + IntStream + .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN) + .mapToObj(index -> + createJourneyPattern + .createStopPointInJourneyPattern(index) + .withScheduledStopPointRef(scheduledStopPointRefs.get(index - 1)) + ) + .toList(); + + departureTimes = + IntStream + .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN) + .mapToObj(index -> LocalTime.of(5, index * 5)) + .toList(); + + timetabledPassingTimes = + IntStream + .rangeClosed(1, NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN) + .mapToObj(index -> + createServiceJourney + .createTimetabledPassingTime( + index, + stopPointInJourneyPatterns.get(index - 1) + ) + .withDepartureTime(departureTimes.get(index - 1)) + ) + .toList(); + } + + @Test + void testValidateServiceJourneyWithRegularStop() { + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + assertNoIssue(netexEntitiesIndex); + } + + @Test + void testValidateServiceJourneyWithRegularStopMissingTime() { + // remove arrival time and departure time for the first passing time + timetabledPassingTimes.get(0).withDepartureTime(null).withArrivalTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME + ); + } + + @Test + void testValidateServiceJourneyWithRegularStopInconsistentTime() { + // set arrival time after departure time for the first passing time + timetabledPassingTimes + .get(0) + .withArrivalTime(departureTimes.get(0).plusMinutes(1)); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCONSISTENT_TIME + ); + } + + @Test + void testValidateServiceJourneyWithAreaStop() { + // remove arrival time and departure time and add flex window + timetabledPassingTimes + .get(0) + .withDepartureTime(null) + .withArrivalTime(null) + .withEarliestDepartureTime(LocalTime.MIDNIGHT) + .withLatestArrivalTime(LocalTime.MIDNIGHT.plusMinutes(1)); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertNoIssue(netexEntitiesIndex); + } + + @Test + void testValidateServiceJourneyWithAreaStopMissingTimeWindow() { + // remove arrival time and departure time and add flex window + timetabledPassingTimes.get(0).withDepartureTime(null).withArrivalTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME + ); + } + + @Test + void testValidateServiceJourneyWithAreaStopInconsistentTimeWindow() { + // remove arrival time and departure time and add flex window + timetabledPassingTimes + .get(0) + .withDepartureTime(null) + .withArrivalTime(null) + .withEarliestDepartureTime(LocalTime.MIDNIGHT.plusMinutes(1)) + .withLatestArrivalTime(LocalTime.MIDNIGHT); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCONSISTENT_TIME + ); + } + + @Test + void testValidateServiceJourneyWithRegularStopFollowedByRegularStopNonIncreasingTime() { + // remove arrival time and departure time and add flex window on second stop + timetabledPassingTimes + .get(1) + .withArrivalTime(departureTimes.get(0).minusMinutes(1)); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_NON_INCREASING_TIME + ); + } + + /** + * This test makes sure all passing times are complete and consistent, before it checks for + * increasing times. + */ + @Test + void testValidateWithRegularStopFollowedByRegularStopWithMissingTime() { + // Set arrivalTime AFTER departure time (not valid) + timetabledPassingTimes.get(1).withArrivalTime(null).withDepartureTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME + ); + } + + @Test + void testValidateServiceJourneyWithRegularStopFollowedByStopArea() { + // remove arrival time and departure time and add flex window on second stop + timetabledPassingTimes + .get(1) + .withDepartureTime(null) + .withArrivalTime(null) + .withEarliestDepartureTime(departureTimes.get(1)) + .withLatestArrivalTime(departureTimes.get(1).plusMinutes(1)); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(1).getRef(), ""); + + assertNoIssue(netexEntitiesIndex); + } + + @Test + void testValidateServiceJourneyWithRegularStopFollowedByStopAreaNonIncreasingTime() { + // remove arrival time and departure time and add flex window with decreasing time on second stop + timetabledPassingTimes + .get(1) + .withEarliestDepartureTime(departureTimes.get(0).minusMinutes(1)) + .withArrivalTime(null) + .withDepartureTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME + ); + } + + @Test + void testValidateServiceJourneyWithStopAreaFollowedByRegularStop() { + // remove arrival time and departure time and add flex window on first stop + timetabledPassingTimes + .get(0) + .withEarliestDepartureTime(departureTimes.get(0)) + .withLatestArrivalTime(departureTimes.get(0)) + .withArrivalTime(null) + .withDepartureTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertNoIssue(netexEntitiesIndex); + } + + @Test + void testValidateServiceJourneyWithStopAreaFollowedByStopArea() { + timetabledPassingTimes + .get(0) + .withEarliestDepartureTime(departureTimes.get(0)) + .withLatestArrivalTime(departureTimes.get(0)) + .withArrivalTime(null) + .withDepartureTime(null); + + timetabledPassingTimes + .get(1) + .withEarliestDepartureTime(departureTimes.get(1)) + .withLatestArrivalTime(departureTimes.get(1).plusMinutes(1)) + .withArrivalTime(null) + .withDepartureTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(1).getRef(), ""); + + assertNoIssue(netexEntitiesIndex); + } + + @Test + void testValidateServiceJourneyWithStopAreaFollowedByStopAreaNonIncreasingTime() { + // remove arrival time and departure time and add flex window on first stop and second stop + // and add decreasing time on second stop + timetabledPassingTimes + .get(0) + .withEarliestDepartureTime(departureTimes.get(0)) + .withLatestArrivalTime(departureTimes.get(0)) + .withArrivalTime(null) + .withDepartureTime(null); + + timetabledPassingTimes + .get(1) + .withEarliestDepartureTime(departureTimes.get(1).minusMinutes(1)) + .withLatestArrivalTime(departureTimes.get(1).plusMinutes(1)) + .withArrivalTime(null) + .withDepartureTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_INCOMPLETE_TIME + ); + } + + @Test + void testValidateServiceJourneyWithStopAreaFollowedByRegularStopNonIncreasingTime() { + // remove arrival time and departure time and add flex window on first stop + // and add decreasing time on second stop + timetabledPassingTimes + .get(0) + .withEarliestDepartureTime(departureTimes.get(0)) + .withLatestArrivalTime(departureTimes.get(0)) + .withArrivalTime(null) + .withDepartureTime(null); + + timetabledPassingTimes + .get(1) + .withArrivalTime(departureTimes.get(0).minusMinutes(1)) + .withDepartureTime(null); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getFlexibleStopPlaceIdByStopPointRefIndex() + .put(scheduledStopPointRefs.get(0).getRef(), ""); + + assertIssue( + netexEntitiesIndex, + NonIncreasingPassingTimeValidator.RULE_NON_INCREASING_TIME + ); + } + + private void assertIssue( + NetexEntitiesIndex netexEntitiesIndex, + ValidationRule rule + ) { + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesIndex + ); + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertEquals(1, validationIssues.size()); + Assertions.assertEquals(rule, validationIssues.get(0).rule()); + } + + private void assertNoIssue(NetexEntitiesIndex netexEntitiesIndex) { + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesIndex + ); + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + private static JAXBValidationContext createValidationContext( + NetexEntitiesIndex netexEntitiesIndex + ) { + return new JAXBValidationContext( + VALIDATION_REPORT_ID, + netexEntitiesIndex, + TestCommonDataRepository.of(NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN), + v -> + TestStopPlaceRepository.ofLocalBusStops( + NUMBER_OF_STOP_POINTS_IN_JOURNEY_PATTERN + ), + TEST_CODESPACE, + TEST_LINE_XML_FILE, + Map.of() + ); + } +} diff --git a/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java new file mode 100644 index 00000000..52ed7dce --- /dev/null +++ b/src/test/java/org/entur/netex/validation/validator/jaxb/rules/servicejourney/transportmode/MismatchedTransportModeSubModeValidatorTest.java @@ -0,0 +1,485 @@ +package org.entur.netex.validation.validator.jaxb.rules.servicejourney.transportmode; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import org.entur.netex.index.api.NetexEntitiesIndex; +import org.entur.netex.validation.test.jaxb.support.NetexEntitiesTestFactory; +import org.entur.netex.validation.test.jaxb.support.TestCommonDataRepository; +import org.entur.netex.validation.test.jaxb.support.TestStopPlaceRepository; +import org.entur.netex.validation.validator.ValidationIssue; +import org.entur.netex.validation.validator.jaxb.CommonDataRepository; +import org.entur.netex.validation.validator.jaxb.JAXBValidationContext; +import org.entur.netex.validation.validator.jaxb.StopPlaceRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.rutebanken.netex.model.AllVehicleModesOfTransportEnumeration; +import org.rutebanken.netex.model.BusSubmodeEnumeration; +import org.rutebanken.netex.model.CoachSubmodeEnumeration; +import org.rutebanken.netex.model.FlexibleLineTypeEnumeration; +import org.rutebanken.netex.model.Line_VersionStructure; +import org.rutebanken.netex.model.RailSubmodeEnumeration; +import org.rutebanken.netex.model.TaxiSubmodeEnumeration; +import org.rutebanken.netex.model.TransportSubmodeStructure; + +class MismatchedTransportModeSubModeValidatorTest { + + private static final String TEST_REPORT_ID = "report id"; + private static final String TEST_CODESPACE = "ENT"; + private static final String TEST_FILENAME = "netex.xml"; + private MismatchedTransportModeSubModeValidator validator; + private NetexEntitiesTestFactory netexEntitiesTestFactory; + private NetexEntitiesTestFactory.CreateGenericLine line; + private NetexEntitiesTestFactory.CreateServiceJourney serviceJourney; + + @BeforeEach + void setUp() { + validator = new MismatchedTransportModeSubModeValidator(); + netexEntitiesTestFactory = new NetexEntitiesTestFactory(); + line = netexEntitiesTestFactory.createLine(1); + NetexEntitiesTestFactory.CreateRoute route = + netexEntitiesTestFactory.createRoute(1); + NetexEntitiesTestFactory.CreateJourneyPattern journeyPattern = + netexEntitiesTestFactory.createJourneyPattern(1).withRoute(route); + journeyPattern.createStopPointsInJourneyPattern(4); + serviceJourney = + netexEntitiesTestFactory.createServiceJourney(1, journeyPattern); + } + + @Test + void transportModeOnLineMatchesWithStopPlace() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void stopAssignmentsWithValidModeDefinedInLineFileShouldBeConsidered() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getQuayIdByStopPointRefIndex() + .put("TST:ScheduledStopPoint:1", "TST:Quay:1"); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesIndex, + TestCommonDataRepository.of(0), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void stopAssignmentsWithInvalidModeDefinedInLineFileShouldBeConsidered() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL) + .withTransportSubmode( + new TransportSubmodeStructure() + .withRailSubmode(RailSubmodeEnumeration.LOCAL) + ); + + NetexEntitiesIndex netexEntitiesIndex = netexEntitiesTestFactory.create(); + + netexEntitiesIndex + .getQuayIdByStopPointRefIndex() + .put("TST:ScheduledStopPoint:1", "TST:Quay:1"); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesIndex, + TestCommonDataRepository.of(0), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + Assertions.assertFalse(validationIssues.isEmpty()); + } + + @Test + void transportModeOverriddenOnServiceJourneyMatchesWithStopPlace() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL) + .withTransportSubmode( + new TransportSubmodeStructure() + .withRailSubmode(RailSubmodeEnumeration.LOCAL) + ); + serviceJourney + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void railReplacementBusStopsCanBeVisitedByRailReplacementBusService() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.RAIL_REPLACEMENT_BUS) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofRailReplacementBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void railReplacementBusStopsCanOnlyBeVisitedByRailReplacementBusService() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofRailReplacementBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertFalse(validationIssues.isEmpty()); + } + + @Test + void transportModeBusOnServiceJourneyShouldMatchWithTransportModeCoachOnStopPlace() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofNationalCoachStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void transportModeCoachOnServiceJourneyShouldMatchWithTransportModeBusOnStopPlace() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.COACH) + .withTransportSubmode( + new TransportSubmodeStructure() + .withCoachSubmode(CoachSubmodeEnumeration.NATIONAL_COACH) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void taxiCanStopOnBusStops() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI) + .withTransportSubmode( + new TransportSubmodeStructure() + .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void taxiCanStopOnCoachStops() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI) + .withTransportSubmode( + new TransportSubmodeStructure() + .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofNationalCoachStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void taxiCannotStopOnStopOtherThanBusOrCoach() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI) + .withTransportSubmode( + new TransportSubmodeStructure() + .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalTrainStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertFalse(validationIssues.isEmpty()); + } + + @Test + void validateOkWhenTransportModeNotFoundOnServiceJourneyNorLine() { + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void validateOkWhenTransportSubModeNotFoundOnServiceJourneyNorLine() { + line.withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI); + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void validateOkWhenTransportModeAndSubModeNotFoundOnQuay() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.TAXI) + .withTransportSubmode( + new TransportSubmodeStructure() + .withTaxiSubmode(TaxiSubmodeEnumeration.CHARTER_TAXI) + ); + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofMissingTransportModeAndSubMode(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void transportModeMissMatchShouldGenerateValidationIssue() { + line + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ); + + JAXBValidationContext validationContext = createValidationContext( + netexEntitiesTestFactory.create(), + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalTrainStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertFalse(validationIssues.isEmpty()); + } + + @Test + void correctTransportModeOnFlexibleLineShouldBeValidated() { + NetexEntitiesIndex flexNetexEntitiesIndex = + createFlexNetexEntitiesIndex(createFlexibleLine -> + createFlexibleLine + .withFlexibleLineType(FlexibleLineTypeEnumeration.FIXED) + .withTransportMode(AllVehicleModesOfTransportEnumeration.BUS) + .withTransportSubmode( + new TransportSubmodeStructure() + .withBusSubmode(BusSubmodeEnumeration.LOCAL_BUS) + ) + ); + + JAXBValidationContext validationContext = createValidationContext( + flexNetexEntitiesIndex, + TestCommonDataRepository.of(4), + TestStopPlaceRepository.ofLocalBusStops(4) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertTrue(validationIssues.isEmpty()); + } + + @Test + void incorrectTransportModeOnFlexibleLineShouldBeReported() { + NetexEntitiesIndex flexNetexEntitiesIndex = + createFlexNetexEntitiesIndex(createFlexibleLine -> + createFlexibleLine + .withTransportMode(AllVehicleModesOfTransportEnumeration.RAIL) + .withTransportSubmode( + new TransportSubmodeStructure() + .withRailSubmode(RailSubmodeEnumeration.LOCAL) + ) + ); + + // create common data and stop place repositories where only the first two stops are mapped to fixed quays + // (the two following quays can be mapped to flexible areas) + JAXBValidationContext validationContext = createValidationContext( + flexNetexEntitiesIndex, + TestCommonDataRepository.of(2), + TestStopPlaceRepository.ofLocalBusStops(2) + ); + + List validationIssues = validator.validate( + validationContext + ); + + Assertions.assertEquals(2, validationIssues.size()); + Assertions.assertTrue( + validationIssues + .stream() + .allMatch(validationIssue -> + validationIssue + .rule() + .equals( + MismatchedTransportModeSubModeValidator.RULE_INVALID_TRANSPORT_MODE + ) + ) + ); + } + + /** + * Create a NetexEntitiesIndex containing a flexible line. + */ + private NetexEntitiesIndex createFlexNetexEntitiesIndex( + Consumer configureFlexibleLine + ) { + NetexEntitiesTestFactory netexEntitiesTestFactory = + new NetexEntitiesTestFactory(); + + NetexEntitiesTestFactory.CreateFlexibleLine createFlexibleLine = + netexEntitiesTestFactory + .createFlexibleLine() + .withFlexibleLineType(FlexibleLineTypeEnumeration.MIXED_FLEXIBLE); + + configureFlexibleLine.accept(createFlexibleLine); + + NetexEntitiesTestFactory.CreateRoute route = + netexEntitiesTestFactory.createRoute(); + + NetexEntitiesTestFactory.CreateJourneyPattern journeyPattern = + netexEntitiesTestFactory.createJourneyPattern().withRoute(route); + journeyPattern.createStopPointsInJourneyPattern(4); + + netexEntitiesTestFactory.createServiceJourney(journeyPattern); + + return netexEntitiesTestFactory.create(); + } + + private static JAXBValidationContext createValidationContext( + NetexEntitiesIndex netexEntitiesIndex, + CommonDataRepository commonDataRepository, + StopPlaceRepository stopPlaceRepository + ) { + return new JAXBValidationContext( + TEST_REPORT_ID, + netexEntitiesIndex, + commonDataRepository, + n -> stopPlaceRepository, + TEST_CODESPACE, + TEST_FILENAME, + Map.of() + ); + } +}