diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/ContinuousPickupDropOffValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/ContinuousPickupDropOffValidator.java new file mode 100644 index 0000000000..6ba24582f3 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/ContinuousPickupDropOffValidator.java @@ -0,0 +1,112 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR; + +import javax.inject.Inject; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.*; +import org.mobilitydata.gtfsvalidator.type.GtfsTime; + +/** + * Validates that if `routes.continuous_pickup` or `routes.continuous_drop_off` are included, then + * `stop_times.start_pickup_drop_off_window` or `stop_times.end_pickup_drop_off_window` are not + * defined for any trip of this route. + * + *

Generated notice: {@link ForbiddenContinuousPickupDropOffNotice}. + */ +@GtfsValidator +public class ContinuousPickupDropOffValidator extends FileValidator { + private final GtfsRouteTableContainer routeTable; + private final GtfsTripTableContainer tripTable; + private final GtfsStopTimeTableContainer stopTimeTable; + + @Inject + public ContinuousPickupDropOffValidator( + GtfsRouteTableContainer routeTable, + GtfsTripTableContainer tripTable, + GtfsStopTimeTableContainer stopTimeTable) { + this.routeTable = routeTable; + this.tripTable = tripTable; + this.stopTimeTable = stopTimeTable; + } + + @Override + public void validate(NoticeContainer noticeContainer) { + for (GtfsRoute route : routeTable.getEntities()) { + boolean continuous = + (route.continuousPickup() == GtfsContinuousPickupDropOff.ALLOWED + || route.continuousPickup() == GtfsContinuousPickupDropOff.MUST_PHONE + || route.continuousPickup() == GtfsContinuousPickupDropOff.ON_REQUEST_TO_DRIVER) + || (route.continuousDropOff() == GtfsContinuousPickupDropOff.ALLOWED + || route.continuousDropOff() == GtfsContinuousPickupDropOff.MUST_PHONE + || route.continuousDropOff() == GtfsContinuousPickupDropOff.ON_REQUEST_TO_DRIVER); + if (!continuous) { + continue; + } + for (GtfsTrip trip : tripTable.byRouteId(route.routeId())) { + for (GtfsStopTime stopTime : stopTimeTable.byTripId(trip.tripId())) { + if (stopTime.hasStartPickupDropOffWindow() || stopTime.hasEndPickupDropOffWindow()) { + noticeContainer.addValidationNotice( + new ForbiddenContinuousPickupDropOffNotice( + route.csvRowNumber(), + trip.tripId(), + stopTime.csvRowNumber(), + stopTime.startPickupDropOffWindow(), + stopTime.endPickupDropOffWindow())); + } + } + } + } + } + + @Override + public boolean shouldCallValidate() { + if (routeTable != null && stopTimeTable != null) { + return routeTable.hasColumn(GtfsRoute.CONTINUOUS_PICKUP_FIELD_NAME) + || routeTable.hasColumn(GtfsRoute.CONTINUOUS_DROP_OFF_FIELD_NAME) + && (stopTimeTable.hasColumn(GtfsStopTime.START_PICKUP_DROP_OFF_WINDOW_FIELD_NAME) + || stopTimeTable.hasColumn(GtfsStopTime.END_PICKUP_DROP_OFF_WINDOW_FIELD_NAME)); + } else { + return false; + } + } + + /** + * Continuous pickup or drop-off are forbidden when routes.continuous_pickup or + * routes.continuous_drop_off are 0, 2 or 3 and stop_times.start_pickup_drop_off_window or + * stop_times.end_pickup_drop_off_window are defined for any trip of this route. + */ + @GtfsValidationNotice(severity = ERROR) + public static class ForbiddenContinuousPickupDropOffNotice extends ValidationNotice { + /** The row number of the route in the `routes.txt` file. */ + private final int routeCsvRowNumber; + + /** The ID of the trip. */ + private final String tripId; + + /** The row number of the stop time in the `stop_times.txt` file. */ + private final int stopTimeCsvRowNumber; + + /** The start time of the pickup/drop-off window. */ + private final GtfsTime startPickupDropOffWindow; + + /** The end time of the pickup/drop-off window. */ + private final GtfsTime endPickupDropOffWindow; + + public ForbiddenContinuousPickupDropOffNotice( + int routeCsvRowNumber, + String tripId, + int stopTimesCsvRowNumber, + GtfsTime startPickupDropOffWindow, + GtfsTime endPickupDropOffWindow) { + this.routeCsvRowNumber = routeCsvRowNumber; + this.tripId = tripId; + this.stopTimeCsvRowNumber = stopTimesCsvRowNumber; + this.startPickupDropOffWindow = startPickupDropOffWindow; + this.endPickupDropOffWindow = endPickupDropOffWindow; + } + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/ContinuousPickupDropOffValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/ContinuousPickupDropOffValidatorTest.java new file mode 100644 index 0000000000..1034455647 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/ContinuousPickupDropOffValidatorTest.java @@ -0,0 +1,99 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.*; +import org.mobilitydata.gtfsvalidator.type.GtfsTime; + +@RunWith(JUnit4.class) +public class ContinuousPickupDropOffValidatorTest { + + private static List generateNotices( + List routes, List trips, List stopTimes) { + NoticeContainer noticeContainer = new NoticeContainer(); + GtfsRouteTableContainer routeTable = + GtfsRouteTableContainer.forEntities(routes, noticeContainer); + GtfsTripTableContainer tripTable = GtfsTripTableContainer.forEntities(trips, noticeContainer); + GtfsStopTimeTableContainer stopTimeTable = + GtfsStopTimeTableContainer.forEntities(stopTimes, noticeContainer); + new ContinuousPickupDropOffValidator(routeTable, tripTable, stopTimeTable) + .validate(noticeContainer); + return noticeContainer.getValidationNotices(); + } + + @Test + public void continuousPickupWithPickupDropOffWindowShouldGenerateNotice() { + List notices = + generateNotices( + List.of( + new GtfsRoute.Builder() + .setCsvRowNumber(1) + .setRouteId("route1") + .setContinuousPickup(2) + .build()), + List.of( + new GtfsTrip.Builder() + .setCsvRowNumber(2) + .setTripId("trip1") + .setRouteId("route1") + .build()), + List.of( + new GtfsStopTime.Builder() + .setCsvRowNumber(3) + .setTripId("trip1") + .setStartPickupDropOffWindow(GtfsTime.fromString("08:00:00")) + .setEndPickupDropOffWindow(GtfsTime.fromString("09:00:00")) + .build())); + assertThat(notices) + .containsExactly( + new ContinuousPickupDropOffValidator.ForbiddenContinuousPickupDropOffNotice( + 1, "trip1", 3, GtfsTime.fromString("08:00:00"), GtfsTime.fromString("09:00:00"))); + } + + @Test + public void noContinuousPickupOrDropOffShouldNotGenerateNotice() { + List notices = + generateNotices( + List.of(new GtfsRoute.Builder().setCsvRowNumber(1).setRouteId("route1").build()), + List.of( + new GtfsTrip.Builder() + .setCsvRowNumber(2) + .setTripId("trip1") + .setRouteId("route1") + .build()), + List.of( + new GtfsStopTime.Builder() + .setCsvRowNumber(3) + .setTripId("trip1") + .setStartPickupDropOffWindow(GtfsTime.fromString("08:00:00")) + .build())); + assertThat(notices).isEmpty(); + } + + @Test + public void continuousPickupAndDropOffWithoutPickupDropOffWindowShouldNotGenerateNotice() { + List notices = + generateNotices( + List.of( + new GtfsRoute.Builder() + .setCsvRowNumber(1) + .setRouteId("route1") + .setContinuousPickup(1) + .setContinuousDropOff(1) + .build()), + List.of( + new GtfsTrip.Builder() + .setCsvRowNumber(2) + .setTripId("trip1") + .setRouteId("route1") + .build()), + List.of(new GtfsStopTime.Builder().setCsvRowNumber(3).setTripId("trip1").build())); + assertThat(notices).isEmpty(); + } +}