diff --git a/RULES.md b/RULES.md index aa315515..2bf880b0 100644 --- a/RULES.md +++ b/RULES.md @@ -54,6 +54,7 @@ Rules are declared in the [`ValidationRules` class](https://github.com/CUTR-at-U | [E050](#E050) | `timestamp` is in the future | [E051](#E051) | GTFS-rt `stop_sequence` not found in GTFS data | [E052](#E052) | `vehicle.id` is not unique +| [E053](#E053) | `start_time` for trip has changed ### Table of Warnings @@ -717,6 +718,16 @@ From [VehiclePosition.VehicleDescriptor](https://github.com/google/transit/blob/ #### References: * [`vehicle.id`](https://github.com/google/transit/blob/master/gtfs-realtime/spec/en/reference.md#message-vehicledescriptor) + + +### E053 - `start_time` for frequency-based trip has changed + +A trip that is defined in frequencies.txt and has exact_times=0 or empty should have the same start_time in the descriptor for it's entire trip instance. A trip instance is defined as one journey of the vehicle from the starting stop_sequence for a trip_id in stop_times.txt to the ending stop_sequence for that same trip_id. + +From [TripUpdate.TripDescriptor](https://github.com/google/transit/blob/master/gtfs-realtime/spec/en/reference.md#message-tripdescriptor) for `start_time`: + +>If the trip corresponds to exact_times=0, then its start_time may be arbitrary, and is initially expected to be the first departure of the trip. Once established, the start_time of this frequency-based exact_times=0 trip should be considered immutable, even if the first departure time changes + # Warnings diff --git a/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/ValidationRules.java b/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/ValidationRules.java index 4e7b2a61..a85a5713 100644 --- a/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/ValidationRules.java +++ b/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/ValidationRules.java @@ -267,6 +267,10 @@ public class ValidationRules { public static final ValidationRule E052 = new ValidationRule("E052", "ERROR", "vehicle.id is not unique", "Each vehicle should have a unique ID", "which is used by more than one vehicle in the feed"); + + public static final ValidationRule E053 = new ValidationRule("E053", "ERROR", "start_time for frequency-based trip changed", + "An exact_times = 0 frequencies.txt trip should have the same start_time through it's trip", + "exact_times=0 must be immutable"); private static List mAllRules = new ArrayList<>(); diff --git a/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/rules/FrequencyTypeZeroValidator.java b/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/rules/FrequencyTypeZeroValidator.java index e9053388..a550b262 100644 --- a/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/rules/FrequencyTypeZeroValidator.java +++ b/gtfs-realtime-validator-lib/src/main/java/edu/usf/cutr/gtfsrtvalidator/lib/validation/rules/FrequencyTypeZeroValidator.java @@ -17,6 +17,8 @@ package edu.usf.cutr.gtfsrtvalidator.lib.validation.rules; import com.google.transit.realtime.GtfsRealtime; +import com.google.transit.realtime.GtfsRealtime.TripUpdate; + import edu.usf.cutr.gtfsrtvalidator.lib.model.MessageLogModel; import edu.usf.cutr.gtfsrtvalidator.lib.model.OccurrenceModel; import edu.usf.cutr.gtfsrtvalidator.lib.model.helper.ErrorListHelperModel; @@ -26,8 +28,9 @@ import org.onebusaway.gtfs.services.GtfsMutableDao; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; +import java.text.ParseException; +import java.util.*; +import java.text.SimpleDateFormat; import static edu.usf.cutr.gtfsrtvalidator.lib.validation.ValidationRules.*; @@ -37,6 +40,7 @@ * E006 - Missing required vehicle_position trip field for frequency-based exact_times = 0 * E013 - Frequency type 0 trip schedule_relationship should be UNSCHEDULED or empty * W005 - Missing vehicle_id in trip_update for frequency-based exact_times = 0 + * E053 - An exact_times = 0 frequencies.txt trip should have the same start_time through it's trip */ public class FrequencyTypeZeroValidator implements FeedEntityValidator { @@ -47,9 +51,22 @@ public List validate(long currentTimeMillis, GtfsMutableDa List errorListE006 = new ArrayList<>(); List errorListE013 = new ArrayList<>(); List errorListW005 = new ArrayList<>(); + List errorListE053 = new ArrayList<>(); + + HashMap> previousTripUpdates = new HashMap>(); + + if (previousFeedMessage != null) { + for (GtfsRealtime.FeedEntity entity : previousFeedMessage.getEntityList()) { + if (entity.hasTripUpdate()) { + GtfsRealtime.TripUpdate tripUpdate = entity.getTripUpdate(); + addTripUpdate(previousTripUpdates, tripUpdate); + } + } + } for (GtfsRealtime.FeedEntity entity : feedMessage.getEntityList()) { if (entity.hasTripUpdate()) { + GtfsRealtime.TripUpdate tripUpdate = entity.getTripUpdate(); if (gtfsMetadata.getExactTimesZeroTripIds().contains(tripUpdate.getTrip().getTripId())) { @@ -75,9 +92,11 @@ public List validate(long currentTimeMillis, GtfsMutableDa // W005 - Missing vehicle_id in trip_update for frequency-based exact_times = 0 RuleUtils.addOccurrence(W005, "trip_id " + tripUpdate.getTrip().getTripId(), errorListW005, _log); } + + if(previousFeedMessage!=null) + checkE053(tripUpdate, previousTripUpdates, errorListE053); } } - if (entity.hasVehicle()) { GtfsRealtime.VehiclePosition vehiclePosition = entity.getVehicle(); if (vehiclePosition.hasTrip() && @@ -109,6 +128,7 @@ public List validate(long currentTimeMillis, GtfsMutableDa } } } + List errors = new ArrayList<>(); if (!errorListE006.isEmpty()) { errors.add(new ErrorListHelperModel(new MessageLogModel(E006), errorListE006)); @@ -119,6 +139,126 @@ public List validate(long currentTimeMillis, GtfsMutableDa if (!errorListW005.isEmpty()) { errors.add(new ErrorListHelperModel(new MessageLogModel(W005), errorListW005)); } + if (!errorListE053.isEmpty()) { + errors.add(new ErrorListHelperModel(new MessageLogModel(E053), errorListE053)); + } return errors; + + } + + private void checkE053(TripUpdate currentTripUpdate, HashMap> previousTripUpdates, List errorListE053) { + + if (!inTripUpdates(currentTripUpdate, previousTripUpdates)) { + if (!isNewTrip(previousTripUpdates, currentTripUpdate)) { + String errorMessage = "vehicle_id " + currentTripUpdate.getVehicle().getId() + " startTime has changed and is now " + currentTripUpdate.getTrip().getStartTime(); + RuleUtils.addOccurrence(E053, errorMessage, errorListE053, _log); + } + } + + } + + private boolean isNewTrip(HashMap> previousTripUpdates, TripUpdate tripUpdate) { + TripUpdate latest = null; + List tripUpdates = previousTripUpdates.get(new TripUpdateKey(tripUpdate.getVehicle().getId(), tripUpdate.getTrip().getTripId())); + if (tripUpdates != null) { + for (TripUpdate previousTripUpdate : tripUpdates) { + if (latest == null) { + latest = previousTripUpdate; + } + if (new TripUpdateStartTimeComparator().compare(latest, previousTripUpdate) > 0) { + latest = previousTripUpdate; + } + } + if (new TripUpdateStartTimeComparator().compare(tripUpdate, latest) > 0) { + return true; + } else { + return false; + } + } + return true; + } + + private boolean inTripUpdates(TripUpdate tripUpdate, HashMap> previousTripUpdates ) { + + List tripUpdates = previousTripUpdates.get(new TripUpdateKey(tripUpdate.getVehicle().getId(), tripUpdate.getTrip().getTripId())); + if(tripUpdates !=null ) + { + for (TripUpdate previousTripUpdate : tripUpdates) { + if (previousTripUpdate.getTrip().getStartTime().equals(tripUpdate.getTrip().getStartTime())) { + return true; + } + } + } + return false; + } + + + private void addTripUpdate(HashMap> tripUpdatesMap, GtfsRealtime.TripUpdate tripUpdate) { + if (tripUpdate.hasVehicle() && tripUpdate.getVehicle().hasId()) { + + List tripUpdates = null; + if (!tripUpdatesMap.containsKey(new TripUpdateKey(tripUpdate.getVehicle().getId(), tripUpdate.getTrip().getTripId()))) { + tripUpdates = new ArrayList(); + } else { + tripUpdates = tripUpdatesMap.get(new TripUpdateKey(tripUpdate.getVehicle().getId(), tripUpdate.getTrip().getTripId())); + } + tripUpdates.add(tripUpdate); + + //Collections.sort(tripUpdates, new TripUpdateStartTimeComparator()); + + tripUpdatesMap.put(new TripUpdateKey(tripUpdate.getVehicle().getId(), tripUpdate.getTrip().getTripId()), tripUpdates); + } + } + private class TripUpdateKey + { + String vehicle_id; + String trip_id; + + public TripUpdateKey(String vehicle_id, String trip_id) + { + this.vehicle_id=vehicle_id; + this.trip_id=trip_id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TripUpdateKey that = (TripUpdateKey) o; + + if (vehicle_id != null ? !vehicle_id.equals(that.vehicle_id) : that.vehicle_id != null) return false; + return trip_id != null ? trip_id.equals(that.trip_id) : that.trip_id == null; + } + + @Override + public int hashCode() { + int result = vehicle_id != null ? vehicle_id.hashCode() : 0; + result = 31 * result + (trip_id != null ? trip_id.hashCode() : 0); + return result; + } + } + + private class TripUpdateStartTimeComparator implements Comparator { + SimpleDateFormat formatter = new SimpleDateFormat("hh:mm:ssa"); + + @Override + public int compare(TripUpdate t1, TripUpdate t2) { + + try { + if(t1.hasTrip()&&t1.getTrip().hasStartTime()&&t2.hasTrip()&&t2.getTrip().hasStartTime()) { + + Date t1_date = formatter.parse(t1.getTrip().getStartTime()); + + Date t2_date = formatter.parse(t2.getTrip().getStartTime()); + + return t1_date.compareTo(t2_date); + } + } catch (ParseException e) { + _log.error(e.getMessage(), e); + } + return -1; + } + } } diff --git a/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/UtilTest.java b/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/UtilTest.java index a92c1443..a9233fc4 100644 --- a/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/UtilTest.java +++ b/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/UtilTest.java @@ -844,6 +844,6 @@ public void testIsInFuture() { @Test public void testGetAllRules() { List rules = ValidationRules.getRules(); - assertEquals(61, rules.size()); + assertEquals(62, rules.size()); } } diff --git a/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/rules/FrequencyTypeZeroValidatorTest.java b/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/rules/FrequencyTypeZeroValidatorTest.java index 45236b35..c1906687 100644 --- a/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/rules/FrequencyTypeZeroValidatorTest.java +++ b/gtfs-realtime-validator-lib/src/test/java/edu/usf/cutr/gtfsrtvalidator/lib/test/rules/FrequencyTypeZeroValidatorTest.java @@ -1,6 +1,8 @@ package edu.usf.cutr.gtfsrtvalidator.lib.test.rules; import com.google.transit.realtime.GtfsRealtime; +import com.google.transit.realtime.GtfsRealtime.FeedMessage; + import edu.usf.cutr.gtfsrtvalidator.lib.model.ValidationRule; import edu.usf.cutr.gtfsrtvalidator.lib.test.FeedMessageTest; import edu.usf.cutr.gtfsrtvalidator.lib.test.util.TestUtils; @@ -262,4 +264,250 @@ public void testW005() { clearAndInitRequiredFeedFields(); } + + /** + * E053- inconsistent start time in trip descriptor for frequency-based exact_times = 0 + */ + @Test + public void testE053() { + + FrequencyTypeZeroValidator frequencyTypeZeroValidator = new FrequencyTypeZeroValidator(); + Map expected = new HashMap<>(); + FeedMessage previousMessage=null; + FeedMessage currentMessage=null; + for (int i = 0; i < 2; i++) { + GtfsRealtime.TripDescriptor.Builder tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); + + tripDescriptorBuilder.setTripId("1"); + tripDescriptorBuilder.setStartDate("4-24-2016"); + + tripDescriptorBuilder.setStartTime("08:00:00AM"); + + GtfsRealtime.VehicleDescriptor.Builder vehicleDescriptorBuilder = GtfsRealtime.VehicleDescriptor.newBuilder(); + + vehicleDescriptorBuilder.setId("1"); + + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + vehiclePositionBuilder.setTimestamp(TimestampUtils.MIN_POSIX_TIME); + vehiclePositionBuilder.setTrip(tripDescriptorBuilder.build()); + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + tripUpdateBuilder.setVehicle(vehicleDescriptorBuilder.build()); + tripUpdateBuilder.setTrip(tripDescriptorBuilder.build()); + + for (int j = 0; j < i + 1; j++) { + GtfsRealtime.TripUpdate.StopTimeUpdate.Builder stopTimeUpdateBuilder = GtfsRealtime.TripUpdate.StopTimeUpdate.newBuilder(); + + stopTimeUpdateBuilder.setStopId("" + j); + stopTimeUpdateBuilder.setStopSequence(j); + + GtfsRealtime.TripUpdate.StopTimeEvent.Builder stopTimeEventBuilder = GtfsRealtime.TripUpdate.StopTimeEvent.newBuilder(); + stopTimeUpdateBuilder.setArrival(stopTimeEventBuilder.build()); + + tripUpdateBuilder.addStopTimeUpdate(stopTimeUpdateBuilder.build()); + } + + + feedEntityBuilder.setTripUpdate(tripUpdateBuilder.build()); + + // Add vehicle_id to vehicle position - 1 warning + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + currentMessage=feedMessageBuilder.build(); + + results = frequencyTypeZeroValidator.validate(TimestampUtils.MIN_POSIX_TIME, bullRunnerGtfs, bullRunnerGtfsMetadata, currentMessage, previousMessage, null); + + previousMessage=currentMessage; + } + expected.clear(); + TestUtils.assertResults(expected, results); + previousMessage=null; + for (int i = 0; i < 2; i++) { + GtfsRealtime.TripDescriptor.Builder tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); + + tripDescriptorBuilder.setTripId("1"); + tripDescriptorBuilder.setStartDate("4-24-2016"); + if (i == 0) { + tripDescriptorBuilder.setStartTime("08:00:00AM"); + } + if (i == 1) { + tripDescriptorBuilder.setStartTime("09:00:00AM"); + } + + + GtfsRealtime.VehicleDescriptor.Builder vehicleDescriptorBuilder = GtfsRealtime.VehicleDescriptor.newBuilder(); + + vehicleDescriptorBuilder.setId("1"); + + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + vehiclePositionBuilder.setTimestamp(TimestampUtils.MIN_POSIX_TIME); + vehiclePositionBuilder.setTrip(tripDescriptorBuilder.build()); + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + tripUpdateBuilder.setVehicle(vehicleDescriptorBuilder.build()); + tripUpdateBuilder.setTrip(tripDescriptorBuilder.build()); + + for (int j = 0; j < i + 1; j++) { + GtfsRealtime.TripUpdate.StopTimeUpdate.Builder stopTimeUpdateBuilder = GtfsRealtime.TripUpdate.StopTimeUpdate.newBuilder(); + + stopTimeUpdateBuilder.setStopId("" + j); + stopTimeUpdateBuilder.setStopSequence(j); + + GtfsRealtime.TripUpdate.StopTimeEvent.Builder stopTimeEventBuilder = GtfsRealtime.TripUpdate.StopTimeEvent.newBuilder(); + stopTimeUpdateBuilder.setArrival(stopTimeEventBuilder.build()); + + tripUpdateBuilder.addStopTimeUpdate(stopTimeUpdateBuilder.build()); + } + + + feedEntityBuilder.setTripUpdate(tripUpdateBuilder.build()); + + // Add vehicle_id to vehicle position - 1 warning + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + currentMessage=feedMessageBuilder.build(); + + results = frequencyTypeZeroValidator.validate(TimestampUtils.MIN_POSIX_TIME, bullRunnerGtfs, bullRunnerGtfsMetadata, currentMessage, previousMessage, null); + + previousMessage=currentMessage; + + } + expected.clear(); + TestUtils.assertResults(expected, results); + previousMessage=null; + for (int i = 0; i < 2; i++) { + GtfsRealtime.TripDescriptor.Builder tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); + + tripDescriptorBuilder.setTripId("1"); + tripDescriptorBuilder.setStartDate("4-24-2016"); + if (i == 0) { + tripDescriptorBuilder.setStartTime("08:00:00AM"); + } + if (i == 1) { + tripDescriptorBuilder.setStartTime("07:00:00AM"); + } + + + GtfsRealtime.VehicleDescriptor.Builder vehicleDescriptorBuilder = GtfsRealtime.VehicleDescriptor.newBuilder(); + + vehicleDescriptorBuilder.setId("1"); + + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + vehiclePositionBuilder.setTimestamp(TimestampUtils.MIN_POSIX_TIME); + vehiclePositionBuilder.setTrip(tripDescriptorBuilder.build()); + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + tripUpdateBuilder.setVehicle(vehicleDescriptorBuilder.build()); + tripUpdateBuilder.setTrip(tripDescriptorBuilder.build()); + + for (int j = 0; j < i + 1; j++) { + GtfsRealtime.TripUpdate.StopTimeUpdate.Builder stopTimeUpdateBuilder = GtfsRealtime.TripUpdate.StopTimeUpdate.newBuilder(); + + stopTimeUpdateBuilder.setStopId("" + j); + stopTimeUpdateBuilder.setStopSequence(j); + + GtfsRealtime.TripUpdate.StopTimeEvent.Builder stopTimeEventBuilder = GtfsRealtime.TripUpdate.StopTimeEvent.newBuilder(); + stopTimeUpdateBuilder.setArrival(stopTimeEventBuilder.build()); + + tripUpdateBuilder.addStopTimeUpdate(stopTimeUpdateBuilder.build()); + } + + + feedEntityBuilder.setTripUpdate(tripUpdateBuilder.build()); + + // Add vehicle_id to vehicle position - 1 warning + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + currentMessage=feedMessageBuilder.build(); + + results = frequencyTypeZeroValidator.validate(TimestampUtils.MIN_POSIX_TIME, bullRunnerGtfs, bullRunnerGtfsMetadata, currentMessage, previousMessage, null); + + previousMessage=currentMessage; + + } + expected.put(ValidationRules.E053, 1); + TestUtils.assertResults(expected, results); + previousMessage=null; + for (int i = 0; i < 2; i++) { + GtfsRealtime.TripDescriptor.Builder tripDescriptorBuilder = GtfsRealtime.TripDescriptor.newBuilder(); + + tripDescriptorBuilder.setTripId(""+i); + tripDescriptorBuilder.setStartDate("4-24-2016"); + if (i == 0) { + tripDescriptorBuilder.setStartTime("08:00:00AM"); + } + if (i == 1) { + tripDescriptorBuilder.setStartTime("07:00:00AM"); + } + + + GtfsRealtime.VehicleDescriptor.Builder vehicleDescriptorBuilder = GtfsRealtime.VehicleDescriptor.newBuilder(); + + vehicleDescriptorBuilder.setId("1"); + + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + vehiclePositionBuilder.setTimestamp(TimestampUtils.MIN_POSIX_TIME); + vehiclePositionBuilder.setTrip(tripDescriptorBuilder.build()); + vehiclePositionBuilder.setVehicle(vehicleDescriptorBuilder.build()); + + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + tripUpdateBuilder.setVehicle(vehicleDescriptorBuilder.build()); + tripUpdateBuilder.setTrip(tripDescriptorBuilder.build()); + + for (int j = 0; j < i + 1; j++) { + GtfsRealtime.TripUpdate.StopTimeUpdate.Builder stopTimeUpdateBuilder = GtfsRealtime.TripUpdate.StopTimeUpdate.newBuilder(); + + stopTimeUpdateBuilder.setStopId("" + j); + stopTimeUpdateBuilder.setStopSequence(j); + + GtfsRealtime.TripUpdate.StopTimeEvent.Builder stopTimeEventBuilder = GtfsRealtime.TripUpdate.StopTimeEvent.newBuilder(); + stopTimeUpdateBuilder.setArrival(stopTimeEventBuilder.build()); + + tripUpdateBuilder.addStopTimeUpdate(stopTimeUpdateBuilder.build()); + } + + + feedEntityBuilder.setTripUpdate(tripUpdateBuilder.build()); + + // Add vehicle_id to vehicle position - 1 warning + feedEntityBuilder.setVehicle(vehiclePositionBuilder.build()); + + + feedMessageBuilder.setEntity(0, feedEntityBuilder.build()); + + currentMessage=feedMessageBuilder.build(); + + results = frequencyTypeZeroValidator.validate(TimestampUtils.MIN_POSIX_TIME, bullRunnerGtfs, bullRunnerGtfsMetadata, currentMessage, previousMessage, null); + + previousMessage=currentMessage; + + } + expected.clear(); + TestUtils.assertResults(expected, results); + clearAndInitRequiredFeedFields(); + } }