Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flex spec v2 #7

Merged
merged 28 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
61752ec
refactor(Initial work to change from stop areas to location groups):
br648 May 9, 2024
e623aef
refactor(Various changes to get GraphQL unit tests to pass):
br648 May 10, 2024
57506da
refactor(Many changes to accom flex v2):
br648 Jun 24, 2024
458639c
refactor(Update to consider all halts and not just stops as part of p…
br648 Jul 3, 2024
0dd268b
refactor(Added FLEX_OPTIONAL to allow for missing fields when updatin…
br648 Jul 8, 2024
20346cd
refactor(Update to handle insert/update of flex and non-flex pattern …
br648 Jul 9, 2024
62099f9
refactor(Updated flex validation to include checks on routes):
br648 Jul 10, 2024
7667a27
refactor(Addition unit test regarding flex and proprietary files and …
br648 Jul 11, 2024
3b25aa1
refactor(Updates and house keeping following review):
br648 Jul 11, 2024
238c919
Update src/main/java/com/conveyal/gtfs/PatternFinder.java
br648 Aug 20, 2024
e03527d
Update src/main/java/com/conveyal/gtfs/error/NewGTFSErrorType.java
br648 Aug 20, 2024
df99d50
Update src/main/java/com/conveyal/gtfs/loader/JdbcTableWriter.java
br648 Aug 20, 2024
37b613b
Update src/main/java/com/conveyal/gtfs/validator/FlexValidator.java
br648 Aug 20, 2024
074afd1
Update src/main/java/com/conveyal/gtfs/loader/JdbcTableWriter.java
br648 Aug 20, 2024
a45ff11
Update src/main/java/com/conveyal/gtfs/validator/FlexValidator.java
br648 Aug 20, 2024
69d8140
Update src/main/java/com/conveyal/gtfs/validator/ReferencesTripValida…
br648 Aug 20, 2024
cf15ca1
refactor(Addressed PR feedback part 1):
br648 Aug 22, 2024
c1b51eb
refactor(Various updates to address PR feedback):
br648 Aug 30, 2024
0f8adce
refactor(PR feedback): Addressed typos and removed commented code.
br648 Sep 4, 2024
16fecc1
refactor(Added another pattern reconciliation test):
br648 Sep 9, 2024
abd7b9d
refactor(JDBCTableWriterTest.java): Added required pattern id to patt…
br648 Sep 9, 2024
7c9a09c
refactor(Moved pattern reconcile to always be triggered after child t…
br648 Sep 17, 2024
26824f5
refactor(JdbcTableWriter.java): Refactor to update pattern frequencie…
br648 Sep 18, 2024
4458ca1
refactor(Changed the graphql files to have a graphql extension):
br648 Oct 11, 2024
2ce3840
refactor(Addressed PR feedback):
br648 Oct 16, 2024
863ec45
refactor(LocationGroupStop.java): Replaced <p> tags with <pre> for be…
br648 Oct 16, 2024
43bd72f
refactor(Addressed PR feedback):
br648 Oct 21, 2024
c990844
refactor(GeoJsonUtil.java): Reinstated methods required by MergeFeedJ…
br648 Oct 22, 2024
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
26 changes: 13 additions & 13 deletions src/main/java/com/conveyal/gtfs/GTFSFeed.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ public class GTFSFeed implements Cloneable, Closeable {
// This is how you do a multimap in mapdb: https://github.com/jankotek/MapDB/blob/release-1.0/src/test/java/examples/MultiMap.java
public final NavigableSet<Tuple2<String, Frequency>> frequencies;
public final Map<String, Route> routes;
public final Map<String, StopArea> stopAreas;
public final Map<String, Area> areas;
public final Map<String, LocationGroupStop> locationGroupStops;
public final Map<String, LocationGroup> locationGroup;
public final Map<String, Stop> stops;
public final Map<String, Transfer> transfers;
public final BTreeMap<String, Trip> trips;
Expand Down Expand Up @@ -179,15 +179,15 @@ else if (feedId == null || feedId.isEmpty()) {

// Flex tables. These must be loaded before stop times. If any of these tables contain data it is assumed that
// we are working with a flex feed.
new Area.Loader(this).loadTable(zip);
new BookingRule.Loader(this).loadTable(zip);
new LocationGroup.Loader(this).loadTable(zip);
new LocationGroupStop.Loader(this).loadTable(zip);
new Location.Loader(this).loadTable(zip);
new LocationShape.Loader(this).loadTable(zip);
new Pattern.Loader(this).loadTable(zip);
new Route.Loader(this).loadTable(zip);
new ShapePoint.Loader(this).loadTable(zip);
new Stop.Loader(this).loadTable(zip);
new StopArea.Loader(this).loadTable(zip);
new Transfer.Loader(this).loadTable(zip);
new Trip.Loader(this).loadTable(zip);
new Frequency.Loader(this).loadTable(zip);
Expand Down Expand Up @@ -221,7 +221,6 @@ public void toFile (String file) {
// don't write empty feed_info.txt
if (!this.feedInfo.isEmpty()) new FeedInfo.Writer(this).writeTable(zip);

new Area.Writer(this).writeTable(zip);
new Agency.Writer(this).writeTable(zip);
new Calendar.Writer(this).writeTable(zip);
new CalendarDate.Writer(this).writeTable(zip);
Expand All @@ -237,9 +236,10 @@ public void toFile (String file) {
new Pattern.Writer(this).writeTable(zip);

if (!this.bookingRules.isEmpty()) new BookingRule.Writer(this).writeTable(zip);
if (!this.stopAreas.isEmpty()) {
// export stop areas
JdbcGtfsExporter.writeStopAreasToFile(zip, new ArrayList<>(stopAreas.values()));
if (!this.locationGroup.isEmpty()) new LocationGroup.Writer(this).writeTable(zip);
if (!this.locationGroupStops.isEmpty()) {
// Export location group stops.
JdbcGtfsExporter.writeLocationGroupStopsToFile(zip, new ArrayList<>(locationGroupStops.values()));
}
if (!this.locations.isEmpty()) {
// export locations
Expand Down Expand Up @@ -680,10 +680,10 @@ private GTFSFeed (DB db) {
calendars = db.getTreeMap("calendars");

// Flex tables.
areas = db.getTreeMap("areas");
locationGroup = db.getTreeMap("location_groups");
bookingRules = db.getTreeMap("booking_rules");
locations = db.getTreeMap("locations");
stopAreas = db.getTreeMap("stop_areas");
locationGroupStops = db.getTreeMap("location_group_stops");
locationShapes = db.getTreeMap("location_shapes");

feedId = db.getAtomicString("feed_id").get();
Expand All @@ -701,14 +701,14 @@ private GTFSFeed (DB db) {
}

/**
* If booking rules, stop areas or location shapes have been created and contain data, the assumption is that
* this is a GTFS Flex feed. These tables must be loaded before this can be referenced. At the moment
* If booking rules, location group stops or location shapes have been created and contain data, the assumption is
* that this is a GTFS Flex feed. These tables must be loaded before this can be referenced. At the moment
* {@link StopTime} references this and is loaded after the check is made on these tables.
*/
public boolean isGTFSFlexFeed() {
return
!bookingRules.isEmpty() ||
!stopAreas.isEmpty() ||
!locationGroupStops.isEmpty() ||
!locationShapes.isEmpty();
}
}
237 changes: 52 additions & 185 deletions src/main/java/com/conveyal/gtfs/PatternBuilder.java

Large diffs are not rendered by default.

118 changes: 57 additions & 61 deletions src/main/java/com/conveyal/gtfs/PatternFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import com.conveyal.gtfs.error.NewGTFSError;
import com.conveyal.gtfs.error.NewGTFSErrorType;
import com.conveyal.gtfs.error.SQLErrorStorage;
import com.conveyal.gtfs.model.Area;
import com.conveyal.gtfs.model.LocationGroup;
import com.conveyal.gtfs.model.Location;
import com.conveyal.gtfs.model.Pattern;
import com.conveyal.gtfs.model.Stop;
import com.conveyal.gtfs.model.StopArea;
import com.conveyal.gtfs.model.LocationGroupStop;
import com.conveyal.gtfs.model.StopTime;
import com.conveyal.gtfs.model.Trip;
import com.google.common.collect.HashMultimap;
Expand Down Expand Up @@ -72,8 +72,8 @@ public void processTrip(Trip trip, Iterable<StopTime> orderedStopTimes) {
public Map<TripPatternKey, Pattern> createPatternObjects(
Map<String, Stop> stopById,
Map<String, Location> locationById,
Map<String, StopArea> stopAreaById,
Map<String, Area> areaById,
Map<String, LocationGroupStop> locationGroupStopById,
Map<String, LocationGroup> locationGroupById,
List<Pattern> patternsFromFeed,
SQLErrorStorage errorStorage
) {
Expand All @@ -87,7 +87,7 @@ public Map<TripPatternKey, Pattern> createPatternObjects(
// TODO assign patterns sequential small integer IDs (may include route)
for (TripPatternKey key : tripsForPattern.keySet()) {
Collection<Trip> trips = tripsForPattern.get(key);
Pattern pattern = new Pattern(key.stops, trips, null);
Pattern pattern = new Pattern(key.orderedHalts, trips, null);
if (usePatternsFromFeed) {
pattern.pattern_id = patternsFromFeed.get(patternsFromFeedIndex).pattern_id;
pattern.name = patternsFromFeed.get(patternsFromFeedIndex).name;
Expand All @@ -112,22 +112,18 @@ public Map<TripPatternKey, Pattern> createPatternObjects(
}
if (!usePatternsFromFeed) {
// Name patterns before storing in SQL database if they have not already been provided with a feed.
renamePatterns(patterns.values(), stopById, locationById, stopAreaById, areaById);
renamePatterns(patterns.values(), stopById, locationById, locationGroupStopById, locationGroupById);
}
LOG.info("Total patterns: {}", tripsForPattern.keySet().size());
return patterns;
}

/**
* Destructively rename the supplied collection of patterns. This process requires access to all stops, locations
* and stop areas in the feed. Some validators already cache a map of all the stops. There's probably a
* cleaner way to do this.
*
* If there is a difference in the number of patterns provided by a feed and the number of patterns generated here,
* the patterns provided by the feed are rejected.
*/
public boolean canUsePatternsFromFeed(List<Pattern> patternsFromFeed) {
boolean usePatternsFromFeed = patternsFromFeed.size() == tripsForPattern.keySet().size();
boolean usePatternsFromFeed = patternsFromFeed != null && patternsFromFeed.size() == tripsForPattern.keySet().size();
LOG.info("Using patterns from feed: {}", usePatternsFromFeed);
return usePatternsFromFeed;
}
Expand All @@ -141,15 +137,15 @@ public static void renamePatterns(
Collection<Pattern> patterns,
Map<String, Stop> stopById,
Map<String, Location> locationById,
Map<String, StopArea> stopAreaById,
Map<String, Area> areaById
Map<String, LocationGroupStop> locationGroupStopById,
Map<String, LocationGroup> locationGroupById
) {
LOG.info("Generating unique names for patterns");

Map<String, PatternNamingInfo> namingInfoForRoute = new HashMap<>();

for (Pattern pattern : patterns) {
if (pattern.associatedTrips.isEmpty() || pattern.orderedStops.isEmpty()) continue;
if (pattern.associatedTrips.isEmpty() || pattern.orderedHalts.isEmpty()) continue;

// Each pattern within a route has a unique name (within that route, not across the entire feed)

Expand All @@ -163,15 +159,15 @@ public static void renamePatterns(
// Stop names, unlike IDs, are not guaranteed to be unique.
// Therefore we must track used names carefully to avoid duplicates.

String fromName = getTerminusName(pattern, stopById, locationById, stopAreaById, areaById, true);
String toName = getTerminusName(pattern, stopById, locationById, stopAreaById, areaById, false);
String fromName = getTerminusName(pattern, stopById, locationById, locationGroupStopById, locationGroupById, true);
String toName = getTerminusName(pattern, stopById, locationById, locationGroupStopById, locationGroupById, false);

namingInfo.fromStops.put(fromName, pattern);
namingInfo.toStops.put(toName, pattern);

for (String stopId : pattern.orderedStops) {
for (String stopId : pattern.orderedHalts) {
Stop stop = stopById.get(stopId);
// If the stop doesn't exist, it's probably a location or stop area and can be ignored for renaming.
// If the stop doesn't exist, it's probably a location or location group stop and can be ignored for renaming.
if (stop == null || fromName.equals(stop.stop_name) || toName.equals(stop.stop_name)) continue;
namingInfo.vias.put(stop.stop_name, pattern);
}
Expand All @@ -182,8 +178,8 @@ public static void renamePatterns(
for (PatternNamingInfo info : namingInfoForRoute.values()) {
for (Pattern pattern : info.patternsOnRoute) {
pattern.name = null; // clear this now so we don't get confused later on
String fromName = getTerminusName(pattern, stopById, locationById, stopAreaById, areaById, true);
String toName = getTerminusName(pattern, stopById, locationById, stopAreaById, areaById, false);
String fromName = getTerminusName(pattern, stopById, locationById, locationGroupStopById, locationGroupById, true);
String toName = getTerminusName(pattern, stopById, locationById, locationGroupStopById, locationGroupById, false);

// check if combination from, to is unique
Set<Pattern> intersection = new HashSet<>(info.fromStops.get(fromName));
Expand All @@ -195,28 +191,27 @@ public static void renamePatterns(
}

// check for unique via stop
pattern.orderedStops.stream().map(
uniqueEntityId -> getStopType(uniqueEntityId, stopById, locationById, stopAreaById)
).forEach(entity -> {
Set<Pattern> viaIntersection = new HashSet<>(intersection);
String stopName = getStopName(entity, areaById);
viaIntersection.retainAll(info.vias.get(stopName));

if (viaIntersection.size() == 1) {
pattern.name = String.format(Locale.US, "from %s to %s via %s", fromName, toName, stopName);
}
});
pattern.orderedHalts.stream()
.map(haltId -> getStopType(haltId, stopById, locationById, locationGroupStopById))
.forEach(entity -> {
Set<Pattern> viaIntersection = new HashSet<>(intersection);
String stopName = getStopName(entity, locationGroupById);
viaIntersection.retainAll(info.vias.get(stopName));
if (viaIntersection.size() == 1) {
pattern.name = String.format(Locale.US, "from %s to %s via %s", fromName, toName, stopName);
}
});

if (pattern.name == null) {
// no unique via, one pattern is subset of other.
if (intersection.size() == 2) {
Iterator<Pattern> it = intersection.iterator();
Pattern p0 = it.next();
Pattern p1 = it.next();
if (p0.orderedStops.size() > p1.orderedStops.size()) {
if (p0.orderedHalts.size() > p1.orderedHalts.size()) {
p1.name = String.format(Locale.US, "from %s to %s express", fromName, toName);
p0.name = String.format(Locale.US, "from %s to %s local", fromName, toName);
} else if (p1.orderedStops.size() > p0.orderedStops.size()){
} else if (p1.orderedHalts.size() > p0.orderedHalts.size()){
p0.name = String.format(Locale.US, "from %s to %s express", fromName, toName);
p1.name = String.format(Locale.US, "from %s to %s local", fromName, toName);
}
Expand All @@ -232,76 +227,77 @@ public static void renamePatterns(
// attach a stop and trip count to each
for (Pattern pattern : info.patternsOnRoute) {
pattern.name = String.format(Locale.US, "%s stops %s (%s trips)",
pattern.orderedStops.size(), pattern.name, pattern.associatedTrips.size());
pattern.orderedHalts.size(), pattern.name, pattern.associatedTrips.size());
}
}
}

/**
* Using the 'unique stop id' return the object it actually relates to. Under flex, a stop id can either be a stop,
* location or stop area, this method decides which.
* Using the ordered stop or location id, return the object it actually relates to. Under flex, a stop can either be a
* stop, location or location group stop, this method decides which.
*/
private static Object getStopType(
String uniqueEntityId,
String orderedHaltId,
Map<String, Stop> stopById,
Map<String, Location> locationById,
Map<String, StopArea> stopAreaById
Map<String, LocationGroupStop> locationGroupStopById
) {
if (stopById.get(uniqueEntityId) != null) {
return stopById.get(uniqueEntityId);
} else if (locationById.get(uniqueEntityId) != null) {
return locationById.get(uniqueEntityId);
} else if (stopAreaById.get(uniqueEntityId) != null) {
return stopAreaById.get(uniqueEntityId);
Object stop = stopById.get(orderedHaltId);
Object location = locationById.get(orderedHaltId);
Object locationGroupStop = locationGroupStopById.get(orderedHaltId);
if (stop != null) {
return stop;
} else if (location != null) {
return location;
} else {
return null;
return locationGroupStop;
}
}

/**
* Extract the 'stop name' from either a stop, location or area (via stop area) depending on the entity type.
* Extract the 'stop name' from either a stop, location or location group stop depending on the entity type.
*/
private static String getStopName(Object entity, Map<String, Area> areaById) {
private static String getStopName(Object entity, Map<String, LocationGroup> locationGroupById) {
if (entity != null) {
if (entity instanceof Stop) {
return ((Stop) entity).stop_name;
} else if (entity instanceof Location) {
return ((Location) entity).stop_name;
} else if (entity instanceof StopArea) {
StopArea stopArea = (StopArea) entity;
Area area = areaById.get(stopArea.area_id);
if (area != null) {
return area.area_name;
} else if (entity instanceof LocationGroupStop) {
LocationGroupStop locationGroupStop = (LocationGroupStop) entity;
LocationGroup locationGroup = locationGroupById.get(locationGroupStop.location_group_id);
if (locationGroup != null) {
return locationGroup.location_group_name;
}
}
}
return "stopNameUnknown";
}

/**
* Return either the 'from' or 'to' terminus name. Check the stops followed by locations and then areas (via stop
* areas). If a match is found return the name (or id if this is no available). If there are no matches return the
* Return either the 'from' or 'to' terminus name. Check the stops followed by locations and then location group
* stops. If a match is found return the name (or id if this is no available). If there are no matches return the
* default value.
*/
private static String getTerminusName(
Pattern pattern,
Map<String, Stop> stopById,
Map<String, Location> locationById,
Map<String, StopArea> stopAreaById,
Map<String, Area> areaById,
Map<String, LocationGroupStop> locationGroupStopById,
Map<String, LocationGroup> locationGroupById,
boolean isFrom
) {
int id = isFrom ? 0 : pattern.orderedStops.size() - 1;
String haltId = pattern.orderedStops.get(id);
int id = isFrom ? 0 : pattern.orderedHalts.size() - 1;
String haltId = pattern.orderedHalts.get(id);
if (stopById.containsKey(haltId)) {
Stop stop = stopById.get(haltId);
return stop.stop_name != null ? stop.stop_name : stop.stop_id;
} else if (locationById.containsKey(haltId)) {
Location location = locationById.get(haltId);
return location.stop_name != null ? location.stop_name : location.location_id;
} else if (stopAreaById.containsKey(haltId)) {
Area area = areaById.get(haltId);
return area.area_name != null ? area.area_name : area.area_id;
} else if (locationGroupStopById.containsKey(haltId)) {
LocationGroup locationGroup = locationGroupById.get(haltId);
return locationGroup.location_group_name != null ? locationGroup.location_group_name : locationGroup.location_group_id;
}
return isFrom ? "fromTerminusNameUnknown" : "toTerminusNameUnknown";
}
Expand Down
Loading
Loading