Severity: {@code SeverityLevel.ERROR}
+ */
+ public static final class TransferWithInvalidStopLocationTypeNotice extends ValidationNotice {
+ // The row number from `transfers.txt` for the faulty entry.
+ private final long csvRowNumber;
+ // The name of the stop id field (e.g. `from_stop_id`) referencing the stop.
+ private final String stopIdFieldName;
+ // The referenced stop id.
+ private final String stopId;
+ // The numeric value of the invalid location type.
+ private final int locationTypeValue;
+ // The name of the invalid location type.
+ private String locationTypeName;
+
+ public TransferWithInvalidStopLocationTypeNotice(
+ long csvRowNumber, String stopIdFieldName, String stopId, GtfsLocationType locationType) {
+ super(SeverityLevel.ERROR);
+ this.csvRowNumber = csvRowNumber;
+ this.stopIdFieldName = stopIdFieldName;
+ this.stopId = stopId;
+ this.locationTypeValue = locationType.getNumber();
+ this.locationTypeName = locationType.toString();
+ }
+ }
+}
diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidator.java
new file mode 100644
index 0000000000..a2ff52df55
--- /dev/null
+++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidator.java
@@ -0,0 +1,219 @@
+package org.mobilitydata.gtfsvalidator.validator;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.List;
+import java.util.Optional;
+import javax.inject.Inject;
+import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.notice.SeverityLevel;
+import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
+import org.mobilitydata.gtfsvalidator.table.GtfsLocationType;
+import org.mobilitydata.gtfsvalidator.table.GtfsStop;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransfer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableLoader;
+import org.mobilitydata.gtfsvalidator.table.GtfsTrip;
+import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer;
+
+/**
+ * Validates that if a transfers.txt entry references a trip, then any corresponding route reference
+ * or stop reference for the transfer are actually associated with the trip.
+ */
+@GtfsValidator
+public class TransfersTripReferenceValidator extends FileValidator {
+
+ private final GtfsTransferTableContainer transfersContainer;
+ private final GtfsTripTableContainer tripsContainer;
+ private final GtfsStopTimeTableContainer stopTimeContainer;
+ private final GtfsStopTableContainer stopsContainer;
+
+ @Inject
+ public TransfersTripReferenceValidator(
+ GtfsTransferTableContainer transfersContainer,
+ GtfsTripTableContainer tripsContainer,
+ GtfsStopTimeTableContainer stopTimeContainer,
+ GtfsStopTableContainer stopsContainer) {
+ this.transfersContainer = transfersContainer;
+ this.tripsContainer = tripsContainer;
+ this.stopTimeContainer = stopTimeContainer;
+ this.stopsContainer = stopsContainer;
+ }
+
+ @Override
+ public void validate(NoticeContainer noticeContainer) {
+ for (GtfsTransfer transfer : transfersContainer.getEntities()) {
+ validateEntity(transfer, noticeContainer);
+ }
+ }
+
+ public void validateEntity(GtfsTransfer entity, NoticeContainer noticeContainer) {
+ validateTripReferences(
+ entity,
+ GtfsTransferTableLoader.FROM_TRIP_ID_FIELD_NAME,
+ optional(entity.hasFromTripId(), entity.fromTripId()),
+ GtfsTransferTableLoader.FROM_ROUTE_ID_FIELD_NAME,
+ optional(entity.hasFromRouteId(), entity.fromRouteId()),
+ GtfsTransferTableLoader.FROM_STOP_ID_FIELD_NAME,
+ optional(entity.hasFromStopId(), entity.fromStopId()),
+ noticeContainer);
+ validateTripReferences(
+ entity,
+ GtfsTransferTableLoader.TO_TRIP_ID_FIELD_NAME,
+ optional(entity.hasToTripId(), entity.toTripId()),
+ GtfsTransferTableLoader.TO_ROUTE_ID_FIELD_NAME,
+ optional(entity.hasToRouteId(), entity.toRouteId()),
+ GtfsTransferTableLoader.TO_STOP_ID_FIELD_NAME,
+ optional(entity.hasToStopId(), entity.toStopId()),
+ noticeContainer);
+ }
+
+ void validateTripReferences(
+ GtfsTransfer entity,
+ String tripFieldName,
+ Optional Severity: {@code SeverityLevel.ERROR}
+ */
+ public static class TransferWithInvalidTripAndRouteNotice extends ValidationNotice {
+ // The row number from `transfers.txt` for the faulty entry.
+ private final long csvRowNumber;
+ // The name of the trip id field (e.g. `from_trip_id`) referencing a trip.
+ private final String tripFieldName;
+ // The referenced trip id.
+ private final String tripId;
+ // The name of the route id field (e.g. `from_route_id`) referencing the route.
+ private final String routeFieldName;
+ // The referenced route id.
+ private final String routeId;
+ // The expected route id from `trips.txt`.
+ private final String expectedRouteId;
+
+ public TransferWithInvalidTripAndRouteNotice(
+ long csvRowNumber,
+ String tripFieldName,
+ String tripId,
+ String routeFieldName,
+ String routeId,
+ String expectedRouteId) {
+ super(SeverityLevel.ERROR);
+ this.csvRowNumber = csvRowNumber;
+ this.tripFieldName = tripFieldName;
+ this.tripId = tripId;
+ this.routeFieldName = routeFieldName;
+ this.routeId = routeId;
+ this.expectedRouteId = expectedRouteId;
+ }
+ }
+
+ /**
+ * A `from_trip_id` or `to_trip_id` field from GTFS file `transfers.txt` references a stop that is
+ * not included in the referenced trip's stop-times.
+ *
+ * Severity: {@code SeverityLevel.ERROR}
+ */
+ public static class TransferWithInvalidTripAndStopNotice extends ValidationNotice {
+ // The row number from `transfers.txt` for the faulty entry.
+ private final long csvRowNumber;
+ // The name of the trip id field (e.g. `from_trip_id`) referencing a trip.
+ private final String tripFieldName;
+ // The referenced trip id.
+ private final String tripId;
+ // The name of the stop id field (e.g. `stop_route_id`) referencing the stop.
+ private final String stopFieldName;
+ // The referenced stop id.
+ private final String stopId;
+
+ public TransferWithInvalidTripAndStopNotice(
+ long csvRowNumber,
+ String tripFieldName,
+ String tripId,
+ String stopFieldName,
+ String stopId) {
+ super(SeverityLevel.ERROR);
+ this.csvRowNumber = csvRowNumber;
+ this.tripFieldName = tripFieldName;
+ this.tripId = tripId;
+ this.stopFieldName = stopFieldName;
+ this.stopId = stopId;
+ }
+ }
+}
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidatorTest.java
new file mode 100644
index 0000000000..06dccbfd8b
--- /dev/null
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersStopTypeValidatorTest.java
@@ -0,0 +1,76 @@
+package org.mobilitydata.gtfsvalidator.validator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsLocationType;
+import org.mobilitydata.gtfsvalidator.table.GtfsStop.Builder;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransfer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransferType;
+import org.mobilitydata.gtfsvalidator.validator.TransfersStopTypeValidator.TransferWithInvalidStopLocationTypeNotice;
+
+public class TransfersStopTypeValidatorTest {
+
+ private NoticeContainer noticeContainer = new NoticeContainer();
+
+ @Test
+ public void testStopToStationTransfer() {
+ // Transfers between a stop and a station are allowed.
+ GtfsStopTableContainer stops =
+ GtfsStopTableContainer.forEntities(
+ ImmutableList.of(
+ new Builder().setStopId("s0").setLocationType(GtfsLocationType.STOP).build(),
+ new Builder().setStopId("s1").setLocationType(GtfsLocationType.STATION).build()),
+ noticeContainer);
+ GtfsTransferTableContainer transfers =
+ GtfsTransferTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsTransfer.Builder()
+ .setFromStopId("s0")
+ .setToStopId("s1")
+ .setTransferType(GtfsTransferType.RECOMMENDED)
+ .build()),
+ noticeContainer);
+
+ new TransfersStopTypeValidator(transfers, stops).validate(noticeContainer);
+
+ assertThat(noticeContainer.getValidationNotices()).isEmpty();
+ }
+
+ @Test
+ public void testEntranceToGenericNodeTransfer() {
+ // Transfers between an entrance and a generic pathway node are NOT allowed.
+ GtfsStopTableContainer stops =
+ GtfsStopTableContainer.forEntities(
+ ImmutableList.of(
+ new Builder().setStopId("s0").setLocationType(GtfsLocationType.ENTRANCE).build(),
+ new Builder()
+ .setStopId("s1")
+ .setLocationType(GtfsLocationType.GENERIC_NODE)
+ .build()),
+ noticeContainer);
+ GtfsTransferTableContainer transfers =
+ GtfsTransferTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsTransfer.Builder()
+ .setCsvRowNumber(2)
+ .setFromStopId("s0")
+ .setToStopId("s1")
+ .setTransferType(GtfsTransferType.RECOMMENDED)
+ .build()),
+ noticeContainer);
+
+ new TransfersStopTypeValidator(transfers, stops).validate(noticeContainer);
+
+ assertThat(noticeContainer.getValidationNotices())
+ .containsExactly(
+ new TransferWithInvalidStopLocationTypeNotice(
+ 2, "from_stop_id", "s0", GtfsLocationType.ENTRANCE),
+ new TransferWithInvalidStopLocationTypeNotice(
+ 2, "to_stop_id", "s1", GtfsLocationType.GENERIC_NODE));
+ }
+}
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidatorTest.java
new file mode 100644
index 0000000000..f541be2e17
--- /dev/null
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TransfersTripReferenceValidatorTest.java
@@ -0,0 +1,122 @@
+package org.mobilitydata.gtfsvalidator.validator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsLocationType;
+import org.mobilitydata.gtfsvalidator.table.GtfsStop;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
+import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransfer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableContainer;
+import org.mobilitydata.gtfsvalidator.table.GtfsTransferType;
+import org.mobilitydata.gtfsvalidator.table.GtfsTrip.Builder;
+import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer;
+import org.mobilitydata.gtfsvalidator.validator.TransfersTripReferenceValidator.TransferWithInvalidTripAndRouteNotice;
+import org.mobilitydata.gtfsvalidator.validator.TransfersTripReferenceValidator.TransferWithInvalidTripAndStopNotice;
+
+public class TransfersTripReferenceValidatorTest {
+
+ private NoticeContainer noticeContainer = new NoticeContainer();
+
+ @Test
+ public void testValidTripReferences() {
+ // Trips with valid stop and route references. For the second trip, the transfer references
+ // the parent station for a stop reference.
+ GtfsTripTableContainer trips =
+ GtfsTripTableContainer.forEntities(
+ ImmutableList.of(
+ new Builder().setTripId("t0").setRouteId("r0").build(),
+ new Builder().setTripId("t1").setRouteId("r1").build()),
+ noticeContainer);
+ GtfsStopTableContainer stops =
+ GtfsStopTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsStop.Builder().setStopId("s0").build(),
+ new GtfsStop.Builder().setStopId("s1_stop").setParentStation("s1_station").build(),
+ new GtfsStop.Builder()
+ .setStopId("s1_station")
+ .setLocationType(GtfsLocationType.STATION)
+ .build()),
+ noticeContainer);
+ GtfsStopTimeTableContainer stopTimes =
+ GtfsStopTimeTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsStopTime.Builder().setTripId("t0").setStopId("s0").build(),
+ new GtfsStopTime.Builder().setTripId("t1").setStopId("s1_stop").build()),
+ noticeContainer);
+ GtfsTransferTableContainer transfers =
+ GtfsTransferTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsTransfer.Builder()
+ .setCsvRowNumber(2)
+ .setFromStopId("s0")
+ .setFromRouteId("r0")
+ .setFromTripId("t0")
+ .setToStopId("s1_station")
+ .setToRouteId("r1")
+ .setToTripId("t1")
+ .setTransferType(GtfsTransferType.IMPOSSIBLE)
+ .build()),
+ noticeContainer);
+
+ new TransfersTripReferenceValidator(transfers, trips, stopTimes, stops)
+ .validate(noticeContainer);
+
+ assertThat(noticeContainer.getValidationNotices()).isEmpty();
+ }
+
+ @Test
+ public void testInvalidTripReferences() {
+ // Trips with invalid stop and route references. For the from-trip, the route id reference is
+ // invalid. For to-trip, the stop id reference doesn't match the stop-times associated with the
+ // trip.
+ GtfsTripTableContainer trips =
+ GtfsTripTableContainer.forEntities(
+ ImmutableList.of(
+ new Builder().setTripId("t0").setRouteId("r0").build(),
+ new Builder().setTripId("t1").setRouteId("r1").build()),
+ noticeContainer);
+ GtfsStopTableContainer stops =
+ GtfsStopTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsStop.Builder().setStopId("s0").build(),
+ new GtfsStop.Builder().setStopId("s1").build(),
+ new GtfsStop.Builder().setStopId("s2").build()),
+ noticeContainer);
+ GtfsStopTimeTableContainer stopTimes =
+ GtfsStopTimeTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsStopTime.Builder().setTripId("t0").setStopId("s0").build(),
+ new GtfsStopTime.Builder().setTripId("t1").setStopId("s1").build()),
+ noticeContainer);
+ GtfsTransferTableContainer transfers =
+ GtfsTransferTableContainer.forEntities(
+ ImmutableList.of(
+ new GtfsTransfer.Builder()
+ .setCsvRowNumber(2)
+ .setFromStopId("s0")
+ // This is not the expected route id.
+ .setFromRouteId("DNE")
+ .setFromTripId("t0")
+ // This stop is not associated with the trip's stop-times.
+ .setToStopId("s2")
+ .setToRouteId("r1")
+ .setToTripId("t1")
+ .setTransferType(GtfsTransferType.IMPOSSIBLE)
+ .build()),
+ noticeContainer);
+
+ new TransfersTripReferenceValidator(transfers, trips, stopTimes, stops)
+ .validate(noticeContainer);
+
+ assertThat(noticeContainer.getValidationNotices())
+ .containsExactly(
+ new TransferWithInvalidTripAndRouteNotice(
+ 2, "from_trip_id", "t0", "from_route_id", "DNE", "r0"),
+ new TransferWithInvalidTripAndStopNotice(2, "to_trip_id", "t1", "to_stop_id", "s2"));
+ }
+}