-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add validation support for transfers.txt transfer_type 4 and 5. (…
…#1266) * Add validation support for transfers.txt transfer_type 4 and 5. * Update validation of in-seat transfers to match online discussion. * Add notice documentation. Co-authored-by: isabelle-dr <[email protected]>
- Loading branch information
1 parent
ba7a83e
commit 8416316
Showing
12 changed files
with
623 additions
and
124 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
core/src/main/java/org/mobilitydata/gtfsvalidator/validator/ValidatorReference.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package org.mobilitydata.gtfsvalidator.validator; | ||
|
||
/** | ||
* This is a no-op class that allows for static references to other validators as a form of | ||
* dependency documentation. | ||
*/ | ||
public final class ValidatorReference { | ||
private ValidatorReference() {} | ||
|
||
/** | ||
* A no-op method that allows one to statically document that a particular validation condition is | ||
* handled by another validator. | ||
*/ | ||
public static final void validatedElsewhereBy(Class<?>... validator) {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TransferDirection.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package org.mobilitydata.gtfsvalidator.validator; | ||
|
||
import org.mobilitydata.gtfsvalidator.table.GtfsTransfer; | ||
import org.mobilitydata.gtfsvalidator.table.GtfsTransferTableLoader; | ||
|
||
/** | ||
* An enum type, along with various convenience methods, for identifying the direction of transfer | ||
* in a `transfers.txt` entry and accessing associated fields. | ||
*/ | ||
public enum TransferDirection { | ||
/** | ||
* The source of the transfer, including fields `from_stop_id`, `from_route_id`, and | ||
* `from_trip_id`. | ||
*/ | ||
TRANSFER_FROM, | ||
/** | ||
* The destination of the transfer, including fields `to_stop_id`, `to_route_id`, and | ||
* `to_trip_id`. | ||
*/ | ||
TRANSFER_TO; | ||
|
||
public String stopIdFieldName() { | ||
return isFrom() | ||
? GtfsTransferTableLoader.FROM_STOP_ID_FIELD_NAME | ||
: GtfsTransferTableLoader.TO_STOP_ID_FIELD_NAME; | ||
} | ||
|
||
public String stopId(GtfsTransfer transfer) { | ||
return isFrom() ? transfer.fromStopId() : transfer.toStopId(); | ||
} | ||
|
||
public boolean hasStopId(GtfsTransfer transfer) { | ||
return isFrom() ? transfer.hasFromStopId() : transfer.hasToStopId(); | ||
} | ||
|
||
public String routeIdFieldName() { | ||
return isFrom() | ||
? GtfsTransferTableLoader.FROM_ROUTE_ID_FIELD_NAME | ||
: GtfsTransferTableLoader.TO_ROUTE_ID_FIELD_NAME; | ||
} | ||
|
||
public boolean hasRouteId(GtfsTransfer transfer) { | ||
return isFrom() ? transfer.hasFromRouteId() : transfer.hasToRouteId(); | ||
} | ||
|
||
public String routeId(GtfsTransfer transfer) { | ||
return isFrom() ? transfer.fromRouteId() : transfer.toRouteId(); | ||
} | ||
|
||
public String tripIdFieldName() { | ||
return isFrom() | ||
? GtfsTransferTableLoader.FROM_TRIP_ID_FIELD_NAME | ||
: GtfsTransferTableLoader.TO_TRIP_ID_FIELD_NAME; | ||
} | ||
|
||
public boolean hasTripId(GtfsTransfer transfer) { | ||
return isFrom() ? transfer.hasFromTripId() : transfer.hasToTripId(); | ||
} | ||
|
||
public String tripId(GtfsTransfer transfer) { | ||
return isFrom() ? transfer.fromTripId() : transfer.toTripId(); | ||
} | ||
|
||
private boolean isFrom() { | ||
return this == TRANSFER_FROM; | ||
} | ||
} |
167 changes: 167 additions & 0 deletions
167
...n/java/org/mobilitydata/gtfsvalidator/validator/TransfersInSeatTransferTypeValidator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package org.mobilitydata.gtfsvalidator.validator; | ||
|
||
import static org.mobilitydata.gtfsvalidator.validator.ValidatorReference.validatedElsewhereBy; | ||
|
||
import java.util.List; | ||
import java.util.Optional; | ||
import javax.inject.Inject; | ||
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; | ||
import org.mobilitydata.gtfsvalidator.notice.MissingRequiredFieldNotice; | ||
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.GtfsTransferType; | ||
import org.mobilitydata.gtfsvalidator.validator.TransfersStopTypeValidator.TransferWithInvalidStopLocationTypeNotice; | ||
|
||
/** | ||
* Validates that entries in `transfers.txt` with an in-seat transfer type are properly specified. | ||
* | ||
* @see TransfersStopTypeValidator | ||
* @see TransfersTripReferenceValidator | ||
*/ | ||
@GtfsValidator | ||
public class TransfersInSeatTransferTypeValidator extends FileValidator { | ||
|
||
private final GtfsTransferTableContainer transfers; | ||
private final GtfsStopTableContainer stops; | ||
private final GtfsStopTimeTableContainer stopTimes; | ||
|
||
@Inject | ||
public TransfersInSeatTransferTypeValidator( | ||
GtfsTransferTableContainer transfers, | ||
GtfsStopTableContainer stops, | ||
GtfsStopTimeTableContainer stopTimes) { | ||
this.transfers = transfers; | ||
this.stops = stops; | ||
this.stopTimes = stopTimes; | ||
} | ||
|
||
@Override | ||
public void validate(NoticeContainer noticeContainer) { | ||
for (GtfsTransfer transfer : transfers.getEntities()) { | ||
validateEntity(transfer, noticeContainer); | ||
} | ||
} | ||
|
||
public void validateEntity(GtfsTransfer transfer, NoticeContainer noticeContainer) { | ||
if (!transfer.hasTransferType()) { | ||
return; | ||
} | ||
if (!isInSeatTransferType(transfer.transferType())) { | ||
return; | ||
} | ||
|
||
for (TransferDirection transferDirection : TransferDirection.values()) { | ||
|
||
// Trip IDs are required for in-seat transfer types. | ||
if (!transferDirection.hasTripId(transfer)) { | ||
noticeContainer.addValidationNotice( | ||
new MissingRequiredFieldNotice( | ||
GtfsTransferTableLoader.FILENAME, | ||
transfer.csvRowNumber(), | ||
transferDirection.tripIdFieldName())); | ||
} | ||
|
||
validateStop(transfer, transferDirection, noticeContainer); | ||
} | ||
} | ||
|
||
private boolean isInSeatTransferType(GtfsTransferType transferType) { | ||
switch (transferType) { | ||
case IN_SEAT_TRANSFER_ALLOWED: | ||
case IN_SEAT_TRANSFER_NOT_ALLOWED: | ||
return true; | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
private void validateStop( | ||
GtfsTransfer transfer, TransferDirection transferDirection, NoticeContainer noticeContainer) { | ||
String stopId = transferDirection.stopId(transfer); | ||
Optional<GtfsStop> optStop = stops.byStopId(stopId); | ||
if (optStop.isEmpty()) { | ||
// This foreign key reference is | ||
validatedElsewhereBy( | ||
GtfsTransferFromStopIdForeignKeyValidator.class, | ||
GtfsTransferToStopIdForeignKeyValidator.class); | ||
return; | ||
} | ||
// Per the spec, normally a stop or station location type is required for a transfer entry. | ||
// However, for in-seat transfers, stations are specifically forbidden. | ||
GtfsLocationType locationType = optStop.get().locationType(); | ||
if (locationType == GtfsLocationType.STATION) { | ||
noticeContainer.addValidationNotice( | ||
new TransferWithInvalidStopLocationTypeNotice(transfer, transferDirection, locationType)); | ||
} | ||
|
||
List<GtfsStopTime> stopTimesForTrip = stopTimes.byTripId(transferDirection.tripId(transfer)); | ||
if (stopTimesForTrip.isEmpty() | ||
|| !stopTimesForTrip.stream().anyMatch((st) -> st.stopId().equals(stopId))) { | ||
// Requiring that a transfer trip's stop-times reference the transfer stop is | ||
validatedElsewhereBy(TransfersTripReferenceValidator.class); | ||
return; | ||
} | ||
|
||
GtfsStopTime transferStop = getInSeatTransferStopTime(stopTimesForTrip, transferDirection); | ||
if (!transferStop.stopId().equals(stopId)) { | ||
noticeContainer.addValidationNotice( | ||
new TransferWithSuspiciousMidTripInSeatNotice(transfer, transferDirection)); | ||
} | ||
} | ||
|
||
private GtfsStopTime getInSeatTransferStopTime( | ||
List<GtfsStopTime> stopTimesForTrip, TransferDirection transferDirection) { | ||
switch (transferDirection) { | ||
case TRANSFER_FROM: | ||
return stopTimesForTrip.get(stopTimesForTrip.size() - 1); | ||
case TRANSFER_TO: | ||
return stopTimesForTrip.get(0); | ||
default: | ||
throw new UnsupportedOperationException("Unhandled TransferDirection=" + transferDirection); | ||
} | ||
} | ||
|
||
/** | ||
* A `from_trip_id` or `to_trip_id` field from GTFS file `transfers.txt` with an in-seat transfer | ||
* type references a stop that is not in the expected position in the trip's stop-times. | ||
* | ||
* <p>For in-seat transfers, we expect the stop to be the last stop-time in the trip sequence for | ||
* `from_stop_id` and the first stop-time for `to_stop_id`. If you are intentionally using this | ||
* feature to model mid-trip transfers, you can ignore this warning, but be aware that this | ||
* functionality is still considered to be partially experimental in some interpretations of the | ||
* spec. | ||
* | ||
* <p>Severity: {@code SeverityLevel.WARNING} | ||
*/ | ||
public static class TransferWithSuspiciousMidTripInSeatNotice 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 tripIdFieldName; | ||
// The referenced trip id. | ||
private final String tripId; | ||
// 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; | ||
|
||
public TransferWithSuspiciousMidTripInSeatNotice( | ||
GtfsTransfer transfer, TransferDirection transferDirection) { | ||
super(SeverityLevel.WARNING); | ||
this.csvRowNumber = transfer.csvRowNumber(); | ||
this.tripIdFieldName = transferDirection.tripIdFieldName(); | ||
this.tripId = transferDirection.tripId(transfer); | ||
this.stopIdFieldName = transferDirection.stopIdFieldName(); | ||
this.stopId = transferDirection.stopId(transfer); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.