Skip to content

Commit

Permalink
Merge pull request #7 from ibi-group/feature/DT-423-rename-stop-areas…
Browse files Browse the repository at this point in the history
…-location-groups

Flex spec v2
  • Loading branch information
br648 authored Oct 22, 2024
2 parents 2bd924c + c990844 commit a0b5eef
Show file tree
Hide file tree
Showing 190 changed files with 4,874 additions and 14,644 deletions.
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

0 comments on commit a0b5eef

Please sign in to comment.