Skip to content

Commit 76d89e9

Browse files
committed
fix(validation): handle zero hops separately if all travel times rounded
refs #110
1 parent f98abe5 commit 76d89e9

File tree

5 files changed

+61
-10
lines changed

5 files changed

+61
-10
lines changed

src/main/java/com/conveyal/gtfs/error/NewGTFSErrorType.java

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public enum NewGTFSErrorType {
3636
TABLE_TOO_LONG(Priority.MEDIUM, "Table is too long to record line numbers with a 32-bit integer, overflow will occur."),
3737
TIME_ZONE_FORMAT(Priority.MEDIUM, "Time zone format should be X."),
3838
REQUIRED_TABLE_EMPTY(Priority.MEDIUM, "This table is required by the GTFS specification but is empty."),
39+
FEED_TRAVEL_TIMES_ROUNDED(Priority.LOW, "All travel times in the feed are rounded to the minute, which may cause unexpected results in routing applications where travel times are zero."),
3940
ROUTE_DESCRIPTION_SAME_AS_NAME(Priority.LOW, "The description of a route is identical to its name, so does not add any information."),
4041
ROUTE_LONG_NAME_CONTAINS_SHORT_NAME(Priority.LOW, "The long name of a route should complement the short name, not include it."),
4142
ROUTE_SHORT_AND_LONG_NAME_MISSING(Priority.MEDIUM, "A route has neither a long nor a short name."),

src/main/java/com/conveyal/gtfs/error/SQLErrorStorage.java

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.sql.SQLException;
1212
import java.sql.Statement;
1313
import java.util.Map;
14+
import java.util.Set;
1415

1516
import static com.conveyal.gtfs.util.Util.ensureValidNamespace;
1617

@@ -86,6 +87,12 @@ public void storeError (NewGTFSError error) {
8687
}
8788
}
8889

90+
public void storeErrors (Set<NewGTFSError> errors) {
91+
for (NewGTFSError error : errors) {
92+
storeError(error);
93+
}
94+
}
95+
8996
/**
9097
* Commits any outstanding error inserts and returns the error count via a SQL query.
9198
*/

src/main/java/com/conveyal/gtfs/validator/SpeedTripValidator.java

+36-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package com.conveyal.gtfs.validator;
22

3+
import com.conveyal.gtfs.error.NewGTFSError;
34
import com.conveyal.gtfs.error.SQLErrorStorage;
45
import com.conveyal.gtfs.loader.Feed;
56
import com.conveyal.gtfs.model.Route;
6-
import com.conveyal.gtfs.model.ShapePoint;
77
import com.conveyal.gtfs.model.Stop;
88
import com.conveyal.gtfs.model.StopTime;
99
import com.conveyal.gtfs.model.Trip;
1010

11+
import java.util.HashSet;
1112
import java.util.List;
13+
import java.util.Set;
1214

1315
import static com.conveyal.gtfs.error.NewGTFSErrorType.*;
1416
import static com.conveyal.gtfs.util.Util.fastDistance;
@@ -20,6 +22,8 @@
2022
public class SpeedTripValidator extends TripValidator {
2123

2224
public static final double MIN_SPEED_KPH = 0.5;
25+
private boolean allTravelTimesAreRounded = true;
26+
private Set<NewGTFSError> travelTimeZeroErrors = new HashSet<>();
2327

2428
public SpeedTripValidator(Feed feed, SQLErrorStorage errorStorage) {
2529
super(feed, errorStorage);
@@ -53,9 +57,13 @@ public void validateTrip(Trip trip, Route route, List<StopTime> stopTimes, List<
5357
if (currStopTime.departure_time < currStopTime.arrival_time) {
5458
registerError(currStopTime, DEPARTURE_BEFORE_ARRIVAL);
5559
}
56-
// TODO detect the case where all travel times are rounded off to minutes (i.e. tt % 60 == 0)
57-
// In that case determine the maximum and minimum possible speed by adding/removing one minute of slack.
60+
// Detect if travel times are rounded off to minutes.
61+
boolean bothTravelTimesRounded = areTravelTimesRounded(prevStopTime);
5862
double travelTimeSeconds = currStopTime.arrival_time - prevStopTime.departure_time;
63+
// If travel times are rounded and travel time is zero, determine the maximum and minimum possible speed
64+
// by adding/removing one minute of slack.
65+
if (bothTravelTimesRounded && travelTimeSeconds == 0)
66+
travelTimeSeconds += 60;
5967
if (checkDistanceAndTime(distanceMeters, travelTimeSeconds, currStopTime)) {
6068
// If distance and time are OK, we've got valid numbers to calculate a travel speed.
6169
double kph = (distanceMeters / 1000D) / (travelTimeSeconds / 60D / 60D);
@@ -73,6 +81,26 @@ public void validateTrip(Trip trip, Route route, List<StopTime> stopTimes, List<
7381
}
7482
}
7583

84+
/**
85+
* Completing this feed validator means checking if there were any unrounded travel times in the feed and (if so)
86+
* registering any zero travel time errors that were passed over before the first unrounded travel time was
87+
* encountered. If in fact all travel times are rounded to the minute, store a special feed-wide error in this case.
88+
*/
89+
public void complete (ValidationResult validationResult) {
90+
if (!allTravelTimesAreRounded) storeErrors(travelTimeZeroErrors);
91+
else registerError(NewGTFSError.forFeed(FEED_TRAVEL_TIMES_ROUNDED, null));
92+
}
93+
94+
/**
95+
* Check that arrival and departure time for a stop time are rounded to the minute and update
96+
* {@link #allTravelTimesAreRounded} accordingly.
97+
*/
98+
private boolean areTravelTimesRounded(StopTime stopTime) {
99+
boolean bothTravelTimesAreRounded = stopTime.departure_time % 60 == 0 && stopTime.arrival_time % 60 == 0;
100+
if (!bothTravelTimesAreRounded) this.allTravelTimesAreRounded = false;
101+
return bothTravelTimesAreRounded;
102+
}
103+
76104
/**
77105
* This just pulls some of the range checking logic out of the main trip checking loop so it's more readable.
78106
* @return true if all values are OK
@@ -88,7 +116,10 @@ private boolean checkDistanceAndTime (double distanceMeters, double travelTimeSe
88116
registerError(stopTime, TRAVEL_TIME_NEGATIVE, travelTimeSeconds);
89117
good = false;
90118
} else if (travelTimeSeconds == 0) {
91-
registerError(stopTime, TRAVEL_TIME_ZERO);
119+
// Only register the travel time zero error if not all travel times are rounded. Otherwise, hold onto the
120+
// error in the travelTimeZeroErrors collection until the completion of this validator.
121+
if (!allTravelTimesAreRounded) registerError(stopTime, TRAVEL_TIME_ZERO);
122+
else travelTimeZeroErrors.add(createUnregisteredError(stopTime, TRAVEL_TIME_ZERO));
92123
good = false;
93124
}
94125
return good;
@@ -97,7 +128,7 @@ private boolean checkDistanceAndTime (double distanceMeters, double travelTimeSe
97128
/**
98129
* @return max speed in km/hour.
99130
*/
100-
private double getMaxSpeedKph (Route route) {
131+
private static double getMaxSpeedKph (Route route) {
101132
int type = -1;
102133
if (route != null) type = route.route_type;
103134
switch (type) {

src/main/java/com/conveyal/gtfs/validator/TripValidator.java

-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package com.conveyal.gtfs.validator;
22

3-
import com.conveyal.gtfs.error.NewGTFSError;
43
import com.conveyal.gtfs.error.SQLErrorStorage;
54
import com.conveyal.gtfs.loader.Feed;
65
import com.conveyal.gtfs.model.Route;
7-
import com.conveyal.gtfs.model.ShapePoint;
86
import com.conveyal.gtfs.model.Stop;
97
import com.conveyal.gtfs.model.StopTime;
108
import com.conveyal.gtfs.model.Trip;

src/main/java/com/conveyal/gtfs/validator/Validator.java

+17-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
import com.conveyal.gtfs.loader.Feed;
77
import com.conveyal.gtfs.model.Entity;
88

9-
import java.sql.SQLException;
10-
import java.util.ArrayList;
11-
import java.util.List;
9+
import java.util.Set;
1210

1311
/**
1412
* A Validator examines a whole GTFS feed or a single trip within a GTFS feed. It accumulates error messages for
@@ -39,6 +37,22 @@ public void registerError(Entity entity, NewGTFSErrorType errorType) {
3937
errorStorage.storeError(NewGTFSError.forEntity(entity, errorType));
4038
}
4139

40+
/**
41+
* Stores a set of errors.
42+
*/
43+
public void storeErrors(Set<NewGTFSError> errors) {
44+
errorStorage.storeErrors(errors);
45+
}
46+
47+
/**
48+
* WARNING: this method creates but DOES NOT STORE a new GTFS error. It should only be used in cases where a
49+
* collection of errors need to be temporarily held before storing in batch (e.g., waiting to store travel time zero
50+
* errors before it is determined that the entire feed uses travel times rounded to the minute).
51+
*/
52+
NewGTFSError createUnregisteredError (Entity entity, NewGTFSErrorType errorType) {
53+
return NewGTFSError.forEntity(entity, errorType);
54+
}
55+
4256
// /**
4357
// * Store an error that affects a single line of a single table. Add a single key-value pair to it. Wraps the
4458
// * underlying error constructor.

0 commit comments

Comments
 (0)