Skip to content

Commit

Permalink
Merge pull request #208 from conveyal/overlapping-trips
Browse files Browse the repository at this point in the history
Fix overlapping trips in block check
  • Loading branch information
Landon Reed authored Mar 29, 2019
2 parents 98a2ea8 + 7f41695 commit 2cd68f0
Show file tree
Hide file tree
Showing 16 changed files with 154 additions and 227 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public enum NewGTFSErrorType {
TRAVEL_TIME_ZERO(Priority.HIGH, "The vehicle arrives at this stop at the same time it departs from the previous stop."),
MISSING_ARRIVAL_OR_DEPARTURE(Priority.MEDIUM, "First and last stop times are required to have both an arrival and departure time."),
TRIP_TOO_FEW_STOP_TIMES(Priority.MEDIUM, "A trip must have at least two stop times to represent travel."),
TRIP_OVERLAP_IN_BLOCK(Priority.MEDIUM, "Blocks"),
TRIP_OVERLAP_IN_BLOCK(Priority.MEDIUM, "A trip overlaps another trip and shares the same block_id."),
TRAVEL_TOO_SLOW(Priority.MEDIUM, "The vehicle is traveling very slowly to reach this stop from the previous one."),
TRAVEL_TOO_FAST(Priority.MEDIUM, "The vehicle travels extremely fast to reach this stop from the previous one."),
VALIDATOR_FAILED(Priority.HIGH, "The specified validation stage failed due to an error encountered during loading. This is likely due to an error encountered during loading (e.g., a date or number field is formatted incorrectly.)."),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public NewTripTimesValidator(Feed feed, SQLErrorStorage errorStorage) {
tripValidators = new TripValidator[] {
new SpeedTripValidator(feed, errorStorage),
new ReferencesTripValidator(feed, errorStorage),
//new OverlappingTripValidator(feed, errorStorage),
new ReversedTripValidator(feed, errorStorage),
new ServiceValidator(feed, errorStorage),
new PatternFinderValidator(feed, errorStorage)
Expand Down Expand Up @@ -164,11 +163,10 @@ private void processTrip (List<StopTime> stopTimes) {
// Repair the case where an arrival or departure time is provided, but not both.
for (StopTime stopTime : stopTimes) fixMissingTimes(stopTime);
// TODO check characteristics of timepoints
// All bad references should have been recorded at import, we can just ignore nulls.
Route route = null;
if (trip != null) route = routeById.get(trip.route_id);
// All bad references should have been recorded at import and null trip check is handled above, we can just
// ignore nulls.
Route route = routeById.get(trip.route_id);
// Pass these same cleaned lists of stop_times and stops into each trip validator in turn.

for (TripValidator tripValidator : tripValidators) tripValidator.validateTrip(trip, route, stopTimes, stops);
}

Expand All @@ -177,7 +175,9 @@ private void processTrip (List<StopTime> stopTimes) {
*/
public void complete (ValidationResult validationResult) {
for (TripValidator tripValidator : tripValidators) {
LOG.info("Running complete stage for {}", tripValidator.getClass().getSimpleName());
tripValidator.complete(validationResult);
LOG.info("{} finished", tripValidator.getClass().getSimpleName());
}
}

Expand Down

This file was deleted.

100 changes: 88 additions & 12 deletions src/main/java/com/conveyal/gtfs/validator/ServiceValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.conveyal.gtfs.error.NewGTFSErrorType.TRIP_OVERLAP_IN_BLOCK;

/**
* This will validate that service date information is coherent, and attempt to deduce or validate the range of dates
* covered by a GTFS feed.
Expand All @@ -52,7 +56,7 @@
public class ServiceValidator extends TripValidator {

private static final Logger LOG = LoggerFactory.getLogger(ServiceValidator.class);

private HashMap<String, List<BlockInterval>> blockIntervals = new HashMap<>();
private Map<String, ServiceInfo> serviceInfoForServiceId = new HashMap<>();

private Map<LocalDate, DateInfo> dateInfoForDate = new HashMap<>();
Expand All @@ -63,6 +67,19 @@ public ServiceValidator(Feed feed, SQLErrorStorage errorStorage) {

@Override
public void validateTrip(Trip trip, Route route, List<StopTime> stopTimes, List<Stop> stops) {
if (trip.block_id != null) {
// If the trip has a block_id, add a new block interval to the map.
BlockInterval blockInterval = new BlockInterval();
blockInterval.trip = trip;
StopTime firstStopTime = stopTimes.get(0);
blockInterval.startTime = firstStopTime.departure_time;
blockInterval.firstStop = firstStopTime;
blockInterval.lastStop = stopTimes.get(stopTimes.size() - 1);
// Construct new list of intervals if none exists for encountered block_id.
blockIntervals
.computeIfAbsent(trip.block_id, k -> new ArrayList<>())
.add(blockInterval);
}
int firstStopDeparture = stopTimes.get(0).departure_time;
int lastStopArrival = stopTimes.get(stopTimes.size() - 1).arrival_time;
if (firstStopDeparture == Entity.INT_MISSING || lastStopArrival == Entity.INT_MISSING) {
Expand Down Expand Up @@ -95,7 +112,11 @@ public void validateTrip(Trip trip, Route route, List<StopTime> stopTimes, List<
*/
@Override
public void complete(ValidationResult validationResult) {
validateServiceInfo(validationResult);
validateBlocks();
}

private void validateServiceInfo(ValidationResult validationResult) {
LOG.info("Merging calendars and calendar_dates...");

// First handle the calendar entries, which define repeating weekly schedules.
Expand All @@ -106,12 +127,12 @@ public void complete(ValidationResult validationResult) {
for (LocalDate date = calendar.start_date; date.isBefore(endDate) || date.isEqual(endDate); date = date.plusDays(1)) {
DayOfWeek dayOfWeek = date.getDayOfWeek();
if ( (dayOfWeek == DayOfWeek.MONDAY && calendar.monday > 0) ||
(dayOfWeek == DayOfWeek.TUESDAY && calendar.tuesday > 0) ||
(dayOfWeek == DayOfWeek.WEDNESDAY && calendar.wednesday > 0) ||
(dayOfWeek == DayOfWeek.THURSDAY && calendar.thursday > 0) ||
(dayOfWeek == DayOfWeek.FRIDAY && calendar.friday > 0) ||
(dayOfWeek == DayOfWeek.SATURDAY && calendar.saturday > 0) ||
(dayOfWeek == DayOfWeek.SUNDAY && calendar.sunday > 0)) {
(dayOfWeek == DayOfWeek.TUESDAY && calendar.tuesday > 0) ||
(dayOfWeek == DayOfWeek.WEDNESDAY && calendar.wednesday > 0) ||
(dayOfWeek == DayOfWeek.THURSDAY && calendar.thursday > 0) ||
(dayOfWeek == DayOfWeek.FRIDAY && calendar.friday > 0) ||
(dayOfWeek == DayOfWeek.SATURDAY && calendar.saturday > 0) ||
(dayOfWeek == DayOfWeek.SUNDAY && calendar.sunday > 0)) {
// Service is active on this date.
serviceInfoForServiceId.computeIfAbsent(calendar.service_id, ServiceInfo::new).datesActive.add(date);
}
Expand Down Expand Up @@ -155,7 +176,7 @@ select durations.service_id, duration_seconds, days_active from (
registerError(NewGTFSError.forFeed(NewGTFSErrorType.SERVICE_NEVER_ACTIVE, serviceInfo.serviceId));
for (String tripId : serviceInfo.tripIds) {
registerError(
NewGTFSError.forTable(Table.TRIPS, NewGTFSErrorType.TRIP_NEVER_ACTIVE)
NewGTFSError.forTable(Table.TRIPS, NewGTFSErrorType.TRIP_NEVER_ACTIVE)
.setEntityId(tripId)
.setBadValue(tripId));
}
Expand Down Expand Up @@ -220,7 +241,7 @@ select durations.service_id, duration_seconds, days_active from (
// Check for low or zero service, which seems to happen even when services are defined.
// This will also catch cases where dateInfo was null and the new instance contains no service.
registerError(NewGTFSError.forFeed(NewGTFSErrorType.DATE_NO_SERVICE,
DateField.GTFS_DATE_FORMATTER.format(date)));
DateField.GTFS_DATE_FORMATTER.format(date)));
}
}
}
Expand Down Expand Up @@ -293,7 +314,7 @@ select durations.service_id, duration_seconds, days_active from (

String serviceDurationsTableName = feed.tablePrefix + "service_durations";
sql = String.format("create table %s (service_id varchar, route_type integer, " +
"duration_seconds integer, primary key (service_id, route_type))", serviceDurationsTableName);
"duration_seconds integer, primary key (service_id, route_type))", serviceDurationsTableName);
LOG.info(sql);
statement.execute(sql);
sql = String.format("insert into %s values (?, ?, ?)", serviceDurationsTableName);
Expand Down Expand Up @@ -328,7 +349,7 @@ select durations.service_id, duration_seconds, days_active from (
LOG.info("Done.");
}

private static class ServiceInfo {
static class ServiceInfo {

final String serviceId;
TIntIntHashMap durationByRouteType = new TIntIntHashMap();
Expand All @@ -345,7 +366,7 @@ public int getTotalServiceDurationSeconds() {

}

private static class DateInfo {
static class DateInfo {

final LocalDate date;
TIntIntHashMap durationByRouteType = new TIntIntHashMap();
Expand All @@ -369,4 +390,59 @@ public void add (ServiceInfo serviceInfo) {
tripCount += serviceInfo.tripIds.size();
}
}

/**
* Checks that trips which run on the same block (i.e., share a block_id) do not overlap. The block_id
* represents a vehicle in service, so there must not be any trips on the same block interval that start while another
* block trip is running.
*
* NOTE: This validation check happens in the {@link ServiceValidator} because it depends on information derived
* about which service calendars operate on which feed dates ({@link #serviceInfoForServiceId}).
*/
private void validateBlocks () {
// Iterate over each block and determine if there are any trips that overlap one another.
for (String blockId : blockIntervals.keySet()) {
List<BlockInterval> intervals = blockIntervals.get(blockId);
intervals.sort(Comparator.comparingInt(i -> i.startTime));
// Iterate over each interval (except for the last) comparing it to every other interval (so the last interval
// is handled through the course of iteration).
// FIXME this has complexity of n^2, there has to be a better way.
for (int n = 0; n < intervals.size() - 1; n++) {
BlockInterval interval1 = intervals.get(n);
// Compare the interval at position N with all other intervals at position N+1 to the end of the list.
for (BlockInterval interval2 : intervals.subList(n + 1, intervals.size())) {
if (interval1.lastStop.departure_time <= interval2.firstStop.arrival_time || interval2.lastStop.departure_time <= interval1.firstStop.arrival_time) {
continue;
}
// If either trip's last departure occurs after the other's first arrival, they overlap. We still
// need to determine if they operate on the same day though.
if (interval1.trip.service_id.equals(interval2.trip.service_id)) {
// If the overlapping trips share a service_id, record an error.
registerError(interval1.trip, TRIP_OVERLAP_IN_BLOCK, interval2.trip.trip_id);
} else {
// Trips overlap but don't have the same service_id.
// Check to see if service days fall on the same days of the week.
ServiceValidator.ServiceInfo info1 = serviceInfoForServiceId.get(interval1.trip.service_id);
ServiceValidator.ServiceInfo info2 = serviceInfoForServiceId.get(interval2.trip.service_id);
Set<LocalDate> overlappingDates = new HashSet<>(info1.datesActive); // use the copy constructor
overlappingDates.retainAll(info2.datesActive);
if (overlappingDates.size() > 0) {
registerError(interval1.trip, TRIP_OVERLAP_IN_BLOCK, interval2.trip.trip_id);
}
}
}
}
}
}


/**
* A simple class used during validation to store details the run interval for a block trip.
*/
private class BlockInterval {
Trip trip;
Integer startTime;
StopTime firstStop;
StopTime lastStop;
}
}
Loading

0 comments on commit 2cd68f0

Please sign in to comment.