Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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,135 @@
package org.mobilitydata.gtfsvalidator.validator;

import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.WARNING;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs;
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
import org.mobilitydata.gtfsvalidator.table.GtfsStop;
import org.mobilitydata.gtfsvalidator.table.GtfsStopSchema;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTime;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeSchema;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTimeTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsTrip;
import org.mobilitydata.gtfsvalidator.table.GtfsTripSchema;
import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer;

/**
* Validates that the trip headsign does not match the name of any intermediate stop (i.e., any stop
* that is not the last stop of the trip).
*
* <p>Generated notice: {@link TripHeadsignMatchesIntermediateStopNotice}.
*/
@GtfsValidator
public class TripHeadsignValidator extends FileValidator {
private final GtfsTripTableContainer tripTable;
private final GtfsStopTimeTableContainer stopTimeTable;
private final GtfsStopTableContainer stopTable;

@Inject
TripHeadsignValidator(
GtfsTripTableContainer tripTable,
GtfsStopTimeTableContainer stopTimeTable,
GtfsStopTableContainer stopTable) {
this.tripTable = tripTable;
this.stopTimeTable = stopTimeTable;
this.stopTable = stopTable;
}

@Override
public void validate(NoticeContainer noticeContainer) {
for (GtfsTrip trip : tripTable.getEntities()) {
if (!trip.hasTripHeadsign()) {
continue;
}
String headsign = trip.tripHeadsign();
String tripId = trip.tripId();

List<GtfsStopTime> stopTimes = stopTimeTable.byTripId(tripId);
if (stopTimes.size() < 2) {
continue; // Not enough stops to have an intermediate stop
}

// Sort by stop_sequence to find the true last stop
List<GtfsStopTime> sorted =
stopTimes.stream()
.sorted(Comparator.comparingInt(GtfsStopTime::stopSequence))
.collect(Collectors.toList());

String lastStopId = sorted.get(sorted.size() - 1).stopId();

// Check all stops except the last
for (int i = 0; i < sorted.size() - 1; i++) {
GtfsStopTime intermediateStopTime = sorted.get(i);
String stopId = intermediateStopTime.stopId();
Optional<GtfsStop> stop = stopTable.byStopId(stopId);
if (stop.isPresent()
&& stop.get().hasStopName()
&& stop.get().stopName().equalsIgnoreCase(headsign)) {
noticeContainer.addValidationNotice(
new TripHeadsignMatchesIntermediateStopNotice(
trip.csvRowNumber(),
tripId,
headsign,
stopId,
intermediateStopTime.stopSequence(),
lastStopId));
}
}
}
}

/**
* Trip headsign matches the name of an intermediate stop, not the last stop.
*
* <p>The `trip_headsign` matches the `stop_name` of a stop that is not the last stop of the trip.
* This may confuse passengers boarding after that stop, since the headsign suggests the vehicle
* is heading to a stop it has already passed.
*/
@GtfsValidationNotice(
severity = WARNING,
files = @FileRefs({GtfsTripSchema.class, GtfsStopTimeSchema.class, GtfsStopSchema.class}))
static class TripHeadsignMatchesIntermediateStopNotice extends ValidationNotice {

/** The row number of the faulty record in `trips.txt`. */
private final int csvRowNumber;

/** The id of the trip with the problematic headsign. */
private final String tripId;

/** The headsign value that matches an intermediate stop name. */
private final String tripHeadsign;

/** The id of the intermediate stop whose name matches the headsign. */
private final String stopId1;

/** The stop_sequence value of the intermediate stop that matches the headsign. */
private final int stopSequence;

/** The id of the actual last stop of the trip. */
private final String stopId2;

TripHeadsignMatchesIntermediateStopNotice(
int csvRowNumber,
String tripId,
String tripHeadsign,
String stopId1,
int stopSequence,
String stopId2) {
this.csvRowNumber = csvRowNumber;
this.tripId = tripId;
this.tripHeadsign = tripHeadsign;
this.stopId1 = stopId1;
this.stopSequence = stopSequence;
this.stopId2 = stopId2;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ public void testNoticeClassFieldNames() {
"transferCount",
"tripCsvRowNumber",
"tripFieldName",
"tripHeadsign",
"tripId",
"tripIdA",
"tripIdB",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.mobilitydata.gtfsvalidator.validator;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Test;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
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.GtfsTrip;
import org.mobilitydata.gtfsvalidator.table.GtfsTripTableContainer;
import org.mobilitydata.gtfsvalidator.validator.TripHeadsignValidator.TripHeadsignMatchesIntermediateStopNotice;

public class TripHeadsignValidatorTest {

@Test
public void headsignMatchingLastStopShouldNotGenerateNotice() {
assertThat(
generateNotices(
ImmutableList.of(createTrip(1, "r1", "s1", "t0", "Central Station")),
ImmutableList.of(
createStopTime(0, "t0", "stop_a", 1),
createStopTime(0, "t0", "stop_b", 2),
createStopTime(0, "t0", "stop_central", 3)),
ImmutableList.of(
createStop("stop_a", "Airport"),
createStop("stop_b", "City Hall"),
createStop("stop_central", "Central Station"))))
.isEmpty();
}

@Test
public void headsignMatchingIntermediateStopShouldGenerateNotice() {
assertThat(
generateNotices(
ImmutableList.of(createTrip(1, "r1", "s1", "t0", "City Hall")),
ImmutableList.of(
createStopTime(0, "t0", "stop_a", 1),
createStopTime(0, "t0", "stop_b", 2),
createStopTime(0, "t0", "stop_central", 3)),
ImmutableList.of(
createStop("stop_a", "Airport"),
createStop("stop_b", "City Hall"),
createStop("stop_central", "Central Station"))))
.containsExactly(
new TripHeadsignMatchesIntermediateStopNotice(
1, "t0", "City Hall", "stop_b", 2, "stop_central"));
}

@Test
public void tripWithNoHeadsignShouldNotGenerateNotice() {
assertThat(
generateNotices(
ImmutableList.of(createTrip(1, "r1", "s1", "t0", null)),
ImmutableList.of(
createStopTime(0, "t0", "stop_a", 1), createStopTime(0, "t0", "stop_b", 2)),
ImmutableList.of(
createStop("stop_a", "Airport"), createStop("stop_b", "City Hall"))))
.isEmpty();
}

@Test
public void tripWithSingleStopShouldNotGenerateNotice() {
assertThat(
generateNotices(
ImmutableList.of(createTrip(1, "r1", "s1", "t0", "Airport")),
ImmutableList.of(createStopTime(0, "t0", "stop_a", 1)),
ImmutableList.of(createStop("stop_a", "Airport"))))
.isEmpty();
}

@Test
public void headsignMatchingFirstStopOfMultiStopTripShouldGenerateNotice() {
assertThat(
generateNotices(
ImmutableList.of(createTrip(1, "r1", "s1", "t0", "Airport")),
ImmutableList.of(
createStopTime(0, "t0", "stop_a", 1),
createStopTime(0, "t0", "stop_b", 2),
createStopTime(0, "t0", "stop_c", 3)),
ImmutableList.of(
createStop("stop_a", "Airport"),
createStop("stop_b", "City Hall"),
createStop("stop_c", "Central Station"))))
.containsExactly(
new TripHeadsignMatchesIntermediateStopNotice(
1, "t0", "Airport", "stop_a", 1, "stop_c"));
}

private static List<ValidationNotice> generateNotices(
List<GtfsTrip> trips, List<GtfsStopTime> stopTimes, List<GtfsStop> stops) {
NoticeContainer noticeContainer = new NoticeContainer();
new TripHeadsignValidator(
GtfsTripTableContainer.forEntities(trips, noticeContainer),
GtfsStopTimeTableContainer.forEntities(stopTimes, noticeContainer),
GtfsStopTableContainer.forEntities(stops, noticeContainer))
.validate(noticeContainer);
return noticeContainer.getValidationNotices();
}

private static GtfsTrip createTrip(
int csvRowNumber, String routeId, String serviceId, String tripId, String tripHeadsign) {
return new GtfsTrip.Builder()
.setCsvRowNumber(csvRowNumber)
.setRouteId(routeId)
.setServiceId(serviceId)
.setTripId(tripId)
.setTripHeadsign(tripHeadsign)
.build();
}

private static GtfsStopTime createStopTime(
int csvRowNumber, String tripId, String stopId, int stopSequence) {
return new GtfsStopTime.Builder()
.setCsvRowNumber(csvRowNumber)
.setTripId(tripId)
.setStopId(stopId)
.setStopSequence(stopSequence)
.build();
}

private static GtfsStop createStop(String stopId, String stopName) {
return new GtfsStop.Builder().setStopId(stopId).setStopName(stopName).build();
}
}
Loading