Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package org.mobilitydata.gtfsvalidator.validator;

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.table.GtfsContinuousPickupDropOff;
import org.mobilitydata.gtfsvalidator.table.GtfsRoute;
import org.mobilitydata.gtfsvalidator.table.GtfsRouteTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsTrip;
import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer;

/**
* Validates that {@code trips.shape_id} is defined when the trip has continuous pickup or drop-off
* behavior.
*
* <p>The spec makes {@code trips.shape_id} conditionally required: it is required if the trip has a
* continuous pickup or drop-off behavior defined either in {@code routes.txt} or in {@code
* stop_times.txt}, and optional otherwise.
*
* <p>A value of {@code 1}, like an empty value, means no continuous stopping behavior, so only
* {@code 0}, {@code 2} and {@code 3} make {@code shape_id} required. Values in {@code
* stop_times.txt} override those in {@code routes.txt}.
*
* <p>Generated notice: {@link MissingRequiredFieldNotice}.
*/
@GtfsValidator
public class TripShapeIdConditionalValidator extends FileValidator {
private final GtfsRouteTableContainer routeTable;
private final GtfsTripTableContainer tripTable;
private final GtfsStopTimeTableContainer stopTimeTable;

@Inject
TripShapeIdConditionalValidator(
GtfsRouteTableContainer routeTable,
GtfsTripTableContainer tripTable,
GtfsStopTimeTableContainer stopTimeTable) {
this.routeTable = routeTable;
this.tripTable = tripTable;
this.stopTimeTable = stopTimeTable;
}

@Override
public void validate(NoticeContainer noticeContainer) {
for (GtfsTrip trip : tripTable.getEntities()) {
if (trip.hasShapeId()) {
continue;
}
if (hasContinuousBehavior(trip)) {
noticeContainer.addValidationNotice(
new MissingRequiredFieldNotice(
GtfsTrip.FILENAME, trip.csvRowNumber(), GtfsTrip.SHAPE_ID_FIELD_NAME));
}
}
}

/**
* Returns true if any stop time of this trip has continuous pickup or drop-off behavior, falling
* back to the values on the route where the stop time does not set them.
*/
private boolean hasContinuousBehavior(GtfsTrip trip) {
Optional<GtfsRoute> route = routeTable.byRouteId(trip.routeId());
GtfsContinuousPickupDropOff routePickup =
route.map(GtfsRoute::continuousPickup).orElse(GtfsContinuousPickupDropOff.NOT_AVAILABLE);
GtfsContinuousPickupDropOff routeDropOff =
route.map(GtfsRoute::continuousDropOff).orElse(GtfsContinuousPickupDropOff.NOT_AVAILABLE);

List<GtfsStopTime> stopTimes = stopTimeTable.byTripId(trip.tripId());
if (stopTimes.isEmpty()) {
return isContinuous(routePickup) || isContinuous(routeDropOff);
}
for (GtfsStopTime stopTime : stopTimes) {
GtfsContinuousPickupDropOff pickup =
stopTime.hasContinuousPickup() ? stopTime.continuousPickup() : routePickup;
GtfsContinuousPickupDropOff dropOff =
stopTime.hasContinuousDropOff() ? stopTime.continuousDropOff() : routeDropOff;
if (isContinuous(pickup) || isContinuous(dropOff)) {
return true;
}
}
return false;
}

private static boolean isContinuous(GtfsContinuousPickupDropOff value) {
return value == GtfsContinuousPickupDropOff.ALLOWED
|| value == GtfsContinuousPickupDropOff.MUST_PHONE
|| value == GtfsContinuousPickupDropOff.ON_REQUEST_TO_DRIVER;
}

@Override
public boolean shouldCallValidate() {
if (tripTable == null) {
return false;
}
boolean routeDefinesContinuous =
routeTable != null
&& (routeTable.hasColumn(GtfsRoute.CONTINUOUS_PICKUP_FIELD_NAME)
|| routeTable.hasColumn(GtfsRoute.CONTINUOUS_DROP_OFF_FIELD_NAME));
boolean stopTimeDefinesContinuous =
stopTimeTable != null
&& (stopTimeTable.hasColumn(GtfsStopTime.CONTINUOUS_PICKUP_FIELD_NAME)
|| stopTimeTable.hasColumn(GtfsStopTime.CONTINUOUS_DROP_OFF_FIELD_NAME));
return routeDefinesContinuous || stopTimeDefinesContinuous;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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.MissingRequiredFieldNotice;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
import org.mobilitydata.gtfsvalidator.table.GtfsRoute;
import org.mobilitydata.gtfsvalidator.table.GtfsRouteTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsTrip;
import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer;

@RunWith(JUnit4.class)
public class TripShapeIdConditionalValidatorTest {

private static List<ValidationNotice> generateNotices(
List<GtfsRoute> routes, List<GtfsTrip> trips, List<GtfsStopTime> stopTimes) {
NoticeContainer noticeContainer = new NoticeContainer();
new TripShapeIdConditionalValidator(
GtfsRouteTableContainer.forEntities(routes, noticeContainer),
GtfsTripTableContainer.forEntities(trips, noticeContainer),
GtfsStopTimeTableContainer.forEntities(stopTimes, noticeContainer))
.validate(noticeContainer);
return noticeContainer.getValidationNotices();
}

private static GtfsRoute.Builder route() {
return new GtfsRoute.Builder().setCsvRowNumber(2).setRouteId("route1");
}

private static GtfsTrip.Builder trip() {
return new GtfsTrip.Builder().setCsvRowNumber(3).setTripId("trip1").setRouteId("route1");
}

private static GtfsStopTime.Builder stopTime(int csvRowNumber) {
return new GtfsStopTime.Builder()
.setCsvRowNumber(csvRowNumber)
.setTripId("trip1")
.setStopSequence(csvRowNumber);
}

private static MissingRequiredFieldNotice expectedNotice() {
return new MissingRequiredFieldNotice("trips.txt", 3, "shape_id");
}

@Test
public void continuousPickupOnRouteWithoutShapeIdShouldGenerateNotice() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().setContinuousPickup(0).build()),
List.of(trip().build()),
List.of(stopTime(4).build()));
assertThat(notices).containsExactly(expectedNotice());
}

@Test
public void continuousDropOffOnRouteWithoutShapeIdShouldGenerateNotice() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().setContinuousDropOff(2).build()),
List.of(trip().build()),
List.of(stopTime(4).build()));
assertThat(notices).containsExactly(expectedNotice());
}

@Test
public void continuousPickupOnStopTimeWithoutShapeIdShouldGenerateNotice() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().build()),
List.of(trip().build()),
List.of(stopTime(4).build(), stopTime(5).setContinuousPickup(3).build()));
assertThat(notices).containsExactly(expectedNotice());
}

@Test
public void continuousBehaviorWithShapeIdShouldNotGenerateNotice() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().setContinuousPickup(0).build()),
List.of(trip().setShapeId("shape1").build()),
List.of(stopTime(4).build()));
assertThat(notices).isEmpty();
}

@Test
public void notAvailableContinuousBehaviorShouldNotGenerateNotice() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().setContinuousPickup(1).setContinuousDropOff(1).build()),
List.of(trip().build()),
List.of(stopTime(4).setContinuousPickup(1).setContinuousDropOff(1).build()));
assertThat(notices).isEmpty();
}

@Test
public void stopTimeShouldOverrideContinuousBehaviorOnRoute() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().setContinuousPickup(0).build()),
List.of(trip().build()),
List.of(stopTime(4).setContinuousPickup(1).build()));
assertThat(notices).isEmpty();
}

@Test
public void noContinuousBehaviorShouldNotGenerateNotice() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().build()), List.of(trip().build()), List.of(stopTime(4).build()));
assertThat(notices).isEmpty();
}

@Test
public void tripWithoutStopTimesShouldStillUseRouteContinuousBehavior() {
List<ValidationNotice> notices =
generateNotices(
List.of(route().setContinuousPickup(2).build()), List.of(trip().build()), List.of());
assertThat(notices).containsExactly(expectedNotice());
}
}
Loading