Skip to content

Commit e3b4353

Browse files
committed
record transfers from gtfs transfers.txt
they are not yet used in routing deprecate point-to-point routing uses of transfer finder for clarity
1 parent 245a110 commit e3b4353

File tree

10 files changed

+247
-42
lines changed

10 files changed

+247
-42
lines changed

src/main/java/com/conveyal/r5/analyst/cluster/TransportNetworkConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,13 @@ public class TransportNetworkConfig {
8787
* Should this be configurable or interact with the transfer entries?
8888
*
8989
*/
90-
public GtfsTransferConfig gtfsTransfers;
90+
public TransferConfig transfers;
9191

92-
enum GtfsTransferConfig {
92+
public enum TransferConfig {
9393
OSM_ONLY, // Find all transfers by searching through the OSM street network
9494
GTFS_ONLY, // Load transfers only from transfers.txt, do not use the street network
9595
PER_STOP_PAIR, // Find transfers via streets for stop pairs not connected by GTFS transfers
9696
PER_STOP, // Find transfers via streets only from and to stops not referenced by GTFS transfers
97-
PER_FEED // GTFS_ONLY for feeds with transfers.txt, OSM_ONLY for those without
97+
PER_FEED // TODO: PER_FEED options, allowing different behavior for intra- and inter-feed transfers
9898
}
9999
}

src/main/java/com/conveyal/r5/kryo/KryoNetworkSerializer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,15 @@ public abstract class KryoNetworkSerializer {
4646
* We considered using an ISO date string as the version but that could get confusing when seen in filenames.
4747
*
4848
* History of Network Version (NV) changes (in production releases):
49+
* nv4 since 2025-11: network config and transfer config retained in TransportNetwork, new GTFS transfer handling
4950
* nv3 since v7.0: switched to Kryo 5 serialization, WebMercatorGridPointSet now contains nested WebMercatorExtents
5051
* nv2 since 2022-04-05
5152
* nv1 since 2021-04-30: stopped rebuilding networks for every new r5 version, manually setting this version string
5253
*
5354
* When prototyping new features, use a unique identifier such as the branch or a commit ID, not sequential nvX ones.
5455
* This avoids conflicts when multiple changes are combined in a single production release, or some are abandoned.
5556
*/
56-
public static final String NETWORK_FORMAT_VERSION = "nv3";
57+
public static final String NETWORK_FORMAT_VERSION = "nv4-testing"; // TODO change to nv4 for release
5758

5859
public static final byte[] HEADER = "R5NETWORK".getBytes();
5960

src/main/java/com/conveyal/r5/point_to_point/PointToPointRouterServer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
* It can build point to point TransportNetwork and start a server with API for point to point searches
6363
*
6464
*/
65+
@Deprecated
6566
public class PointToPointRouterServer {
6667
private static final Logger LOG = LoggerFactory.getLogger(PointToPointRouterServer.class);
6768

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package com.conveyal.r5.transit;
2+
3+
import com.conveyal.gtfs.GTFSFeed;
4+
import com.conveyal.gtfs.model.Transfer;
5+
import gnu.trove.list.TIntList;
6+
import gnu.trove.list.array.TIntArrayList;
7+
import gnu.trove.map.TIntObjectMap;
8+
import gnu.trove.map.TObjectIntMap;
9+
import gnu.trove.map.hash.TIntObjectHashMap;
10+
import gnu.trove.map.hash.TObjectIntHashMap;
11+
12+
import java.util.BitSet;
13+
import java.util.HashSet;
14+
import java.util.Set;
15+
16+
import static com.conveyal.r5.analyst.cluster.TransportNetworkConfig.TransferConfig;
17+
import static com.conveyal.r5.analyst.cluster.TransportNetworkConfig.TransferConfig.*;
18+
import static com.google.common.base.Strings.isNullOrEmpty;
19+
20+
/**
21+
* Transfers between transit stops can come from two sources: transfers.txt in GTFS inputs and
22+
* routing through the OSM-derived street network. This class handles loading from transfers.txt and
23+
* retains enough information so the subsequent street routing approach can adopt the behavior
24+
* specified with TransportNetworkConfig.TransferConfig. The way in which GTFS transfers take
25+
* priority over transfers found through the street network can be configured.
26+
* <p>
27+
* There can be multiple GTFS feeds and they are loaded in a streaming fashion, with only one open
28+
* and consuming memory at a time. Therefore we lose a lot of context by the time the street routing
29+
* happens.
30+
* <p>
31+
* Use a single instance across all GTFS feeds. Call the load method once on each feed in turn. This
32+
* accumulates information from all feeds that will later be important for finding transfers through
33+
* the OSM street network.
34+
* <p>
35+
* The GTFS transfers we load are specified in terms of minimum time, while the street transfers are
36+
* stored as distances and resolved to time during searches based on the specified walk speed. On
37+
* the downside this requires two separate data structures, but on the upside it simplifies loading
38+
* because GTFS must be loaded one feed at a time, while on-street transfers must be found later
39+
* after all GTFS feeds are loaded and all stops linked (because street transfers are expected to
40+
* connect stops from different feeds).
41+
* <p>
42+
* TODO Further increase transfer time accuracy using GTFS pathways.txt
43+
*/
44+
public class GtfsTransferLoader {
45+
46+
final TransitLayer transit;
47+
final TransferConfig transferConfig;
48+
49+
int nMissingStop = 0;
50+
int nUnsupportedSpecificity = 0;
51+
int nUnsupportedTransferType = 0;
52+
TObjectIntMap<String> otherErrors = new TObjectIntHashMap<>();
53+
54+
public record StopPair(int fromStop, int toStop) { }
55+
56+
final BitSet stopsWithTransfers = new BitSet();
57+
final Set<StopPair> stopPairsWithTransfers = new HashSet<>();
58+
// Transfer keys are from-stop indexes, values are packed lists of (to-stop, distance) pairs.
59+
final TIntObjectMap<TIntList> transfers = new TIntObjectHashMap<>();
60+
61+
public GtfsTransferLoader (TransitLayer transit, TransferConfig transferConfig) {
62+
this.transit = transit;
63+
this.transferConfig = transferConfig;
64+
}
65+
66+
public void loadTransfersTxt (GTFSFeed feed) {
67+
if (transferConfig == OSM_ONLY) return;
68+
if (feed.transfers == null || feed.transfers.isEmpty()) return;
69+
// The keys of GtfsFeed.transfers are just arbitrary unique numbers (the input line numbers).
70+
for (Transfer transfer : feed.transfers.values()) {
71+
if (shouldSkipTransfer(transfer)) continue;
72+
int from = transit.indexForStopId.get(transfer.from_stop_id);
73+
int to = transit.indexForStopId.get(transfer.to_stop_id);
74+
if (untrue(from < 0 || to < 0, "Transfer references stop that was not loaded.")) continue;
75+
if (untrue(transfer.min_transfer_time < 0, "Negative transfer times not allowed.")) continue;
76+
if (untrue(transfer.min_transfer_time > 3600, "Transfer time suspiciously high.")) continue;
77+
TIntList packedTransfers = transfers.get(from);
78+
if (packedTransfers == null) {
79+
packedTransfers = new TIntArrayList();
80+
transfers.put(from, packedTransfers);
81+
}
82+
packedTransfers.add(to);
83+
packedTransfers.add(transfer.min_transfer_time);
84+
stopsWithTransfers.set(from);
85+
stopsWithTransfers.set(to);
86+
// Conditional to avoid excessive object instance bloat when unused.
87+
if (transferConfig == PER_STOP_PAIR) {
88+
stopPairsWithTransfers.add(new StopPair(from, to));
89+
}
90+
}
91+
if (transferConfig == PER_FEED) {
92+
throw new UnsupportedOperationException();
93+
// Prevent later on-street transfer calculation for every stop in this feed.
94+
// However, this behavior probably needs to be different for inter- and intra-feed transfers.
95+
// And it may need to be manually set independently for each individual feed.
96+
// for (String stopId : feed.stops.keySet()) {
97+
// int stopIndex = transit.indexForStopId.get(stopId);
98+
// if (stopIndex > 0) stopsWithTransfers.set(stopIndex);
99+
// }
100+
}
101+
}
102+
103+
// TODO encapsulate and reuse elsewhere.
104+
private boolean untrue (boolean condition, String errorMessage) {
105+
if (condition) otherErrors.adjustOrPutValue(errorMessage, 1, 1);
106+
return !condition;
107+
}
108+
109+
/**
110+
* Validate one transfer and decide whether it should be processed by this class, maintaining
111+
* some counts and flags for debugging and status reporting.
112+
*/
113+
private boolean shouldSkipTransfer (Transfer transfer) {
114+
boolean skip = false;
115+
if (!(isNullOrEmpty(transfer.from_route_id) && isNullOrEmpty(transfer.from_trip_id) &&
116+
isNullOrEmpty(transfer.to_route_id) && isNullOrEmpty(transfer.to_trip_id))) {
117+
nUnsupportedSpecificity += 1;
118+
skip = true;
119+
}
120+
if (isNullOrEmpty(transfer.from_stop_id) || isNullOrEmpty(transfer.to_stop_id)) {
121+
nMissingStop += 1;
122+
skip = true;
123+
}
124+
if (transfer.transfer_type < 2) {
125+
nUnsupportedTransferType += 1;
126+
skip = true;
127+
}
128+
if (transfer.transfer_type > 3) {
129+
// In-seat transfer information may be "supported", but not consumed by this class.
130+
skip = true;
131+
}
132+
return skip;
133+
}
134+
135+
/**
136+
* This is an optimization to avoid a slow street search when every transfer it yields will be ignored (because
137+
* every stop pair involving this source stop is slated to be skipped).
138+
* @return whether the osm street transfer generation should skip producing any transfers from the given stop.
139+
*/
140+
public boolean shouldSkipFromStop (int fromStopIndex) {
141+
if (transferConfig == PER_STOP) {
142+
return stopsWithTransfers.get(fromStopIndex);
143+
}
144+
return false;
145+
}
146+
147+
/**
148+
* A GTFS transfer between a specific pair of stops or involving particular stops may take priority over any
149+
* transfer found by routing through the OSM street network.
150+
* @return whether osm street transfer generation should skip making transfers between the given pair of stops.
151+
*/
152+
public boolean shouldSkipStopPair (int fromStopIndex, int toStopIndex) {
153+
if (transferConfig == PER_STOP_PAIR) {
154+
return stopPairsWithTransfers.contains(new StopPair(fromStopIndex, toStopIndex));
155+
} else if (transferConfig == PER_STOP) {
156+
return stopsWithTransfers.get(fromStopIndex) || stopsWithTransfers.get(toStopIndex);
157+
}
158+
return false;
159+
}
160+
161+
}

src/main/java/com/conveyal/r5/transit/TransferFinder.java

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,37 @@
1616
import java.util.stream.Collectors;
1717
import java.util.stream.IntStream;
1818

19+
import static com.conveyal.r5.analyst.cluster.TransportNetworkConfig.*;
1920
import static com.conveyal.r5.streets.StreetRouter.State.RoutingVariable;
2021
import static com.conveyal.r5.transit.TransitLayer.PARKRIDE_DISTANCE_LIMIT_METERS;
2122
import static com.conveyal.r5.transit.TransitLayer.TRANSFER_DISTANCE_LIMIT_METERS;
2223

2324
/**
2425
* Pre-compute walking transfers between transit stops via the street network, up to a given distance limit.
2526
* TODO optimization: combine TransferFinder with stop-to-vertex distance table builder.
27+
* TODO rename to OsmStreetTransferFinder by contrast with GtfsTransferLoader
2628
*/
2729
public class TransferFinder {
2830

2931
private static final Logger LOG = LoggerFactory.getLogger(TransferFinder.class);
3032

3133
// Optimization: use the same empty list for all stops with no transfers
3234
private static final TIntArrayList EMPTY_INT_LIST = new TIntArrayList();
33-
3435
// Optimization: use the same empty list for all stops with no transfers
3536
private static final TIntObjectMap<StreetRouter.State> EMPTY_STATE_MAP = new TIntObjectHashMap<>();
36-
3737
TransitLayer transitLayer;
38-
3938
StreetLayer streetLayer;
39+
GtfsTransferLoader gtfsTransferLoader;
4040

4141
/**
4242
* Eventually this should choose whether to search via the street network or straight line distance based on the
4343
* presence of OSM street data (whether the street layer is null). However the street layer will always be present,
4444
* at least to contain transit stop vertices, so the choice cannot be made based only on the absence of a streetLayer.
4545
*/
46-
public TransferFinder(TransportNetwork network) {
46+
public TransferFinder(TransportNetwork network, GtfsTransferLoader gtfsTransferLoader) {
4747
this.transitLayer = network.transitLayer;
4848
this.streetLayer = network.streetLayer;
49+
this.gtfsTransferLoader = gtfsTransferLoader;
4950
}
5051

5152
public void findParkRideTransfer() {
@@ -100,22 +101,34 @@ public void findParkRideTransfer() {
100101
* However, existing transfer lists will be extended if new stops are reachable from existing stops.
101102
*/
102103
public void findTransfers () {
104+
if (gtfsTransferLoader.transferConfig == TransferConfig.GTFS_ONLY) {
105+
LOG.info("Not finding transfers through street network due to GTFS_ONLY in TransportNetworkConfig.");
106+
return;
107+
}
103108
// Look at the existing list of transfers (if any) and do enough iterations to make that transfer list as long
104109
// as the list of stops.
105110
int firstStopToProcess = transitLayer.transfersForStop.size();
106111
int nStopsTotal = transitLayer.getStopCount();
107-
int nStopsToProcess = nStopsTotal - firstStopToProcess;
108-
LOG.info("Finding transfers through the street network from {} stops...", nStopsToProcess);
112+
int nStopsToProcess = nStopsTotal - firstStopToProcess;
113+
LOG.info("Finding transfers through the street network from {} new transit stops...", nStopsToProcess);
109114
LambdaCounter stopCounter = new LambdaCounter(LOG, nStopsToProcess, 10_000,
110-
"Found transfers from {} of {} transit stops.");
115+
"Processed OSM street transfers from {} of {} new transit stops.");
111116
LambdaCounter unconnectedCounter = new LambdaCounter(LOG, nStopsToProcess, 1_000,
112-
"{} of {} transit stops are unlinked.");
117+
"{} of {} new transit stops are not linked to the street network.");
118+
LambdaCounter skippedFromStopCounter = new LambdaCounter(LOG, nStopsToProcess, 1_000,
119+
"Deferred to GTFS transfers for {} of {} transfer source stops.");
120+
LambdaCounter skippedPairCounter = new LambdaCounter(LOG, 1_000,
121+
"Deferred to GTFS for {} stop pairs.");
113122

114123
// Create transfers for all new stops, appending them to the list of transfers for any existing stops.
115124
// This handles both newly built networks and the case where a scenario adds stops to an existing network.
116125
transitLayer.transfersForStop.addAll(
117126
IntStream.range(firstStopToProcess, nStopsTotal).parallel().mapToObj(sourceStopIndex -> {
118127
stopCounter.increment();
128+
if (gtfsTransferLoader.shouldSkipFromStop(sourceStopIndex)) {
129+
skippedFromStopCounter.increment();
130+
return EMPTY_INT_LIST;
131+
}
119132
// From each stop, run a street search looking for other transit stops.
120133
int originStreetVertex = transitLayer.streetVertexForStop.get(sourceStopIndex);
121134
if (originStreetVertex == -1) {
@@ -131,29 +144,38 @@ public void findTransfers () {
131144
streetRouter.quantityToMinimize = RoutingVariable.DISTANCE_MILLIMETERS;
132145

133146
streetRouter.route();
147+
// A map from stop indexes to values of the routing objective variable (DISTANCE_MILLIMETERS).
134148
TIntIntMap distancesToReachedStops = streetRouter.getReachedStops();
135149
// Same-stop "transfers" are handled in the router and do not need to be materialized in our list of
136150
// transfer distances. It's actually important to remove the source stop to handle certain cases with
137151
// loop routes (see CTA Brown Line to Purple Line example in discussion on #763).
138152
distancesToReachedStops.remove(sourceStopIndex);
153+
// TODO consider how this interferes with deferring to GTFS-specified transfers on stop pairs
139154
retainClosestStopsOnPatterns(distancesToReachedStops);
140155
// At this point we have the distances to all stops that are the closest one on some pattern.
141156
// Make transfers to them, packed as pairs of (target stop index, distance).
142157
TIntList packedTransfers = new TIntArrayList();
143158
distancesToReachedStops.forEachEntry((targetStopIndex, distance) -> {
144-
packedTransfers.add(targetStopIndex);
145-
packedTransfers.add(distance);
159+
if (gtfsTransferLoader.shouldSkipStopPair(sourceStopIndex, targetStopIndex)) {
160+
skippedPairCounter.increment();
161+
} else {
162+
packedTransfers.add(targetStopIndex);
163+
packedTransfers.add(distance);
164+
}
146165
return true;
147166
});
148167
// Record this list of transfers as leading out of the stop with index sourceStopIndex.
149-
// Deduplicate empty lists.
150168
if (packedTransfers.size() > 0) {
151169
return packedTransfers;
152170
} else {
153171
return EMPTY_INT_LIST;
154172
}
155173
}).collect(Collectors.toList()));
156-
LOG.info("Done finding transfers. {} stops were not linked to the street network.", unconnectedCounter.getCount());
174+
175+
stopCounter.done();
176+
unconnectedCounter.logIfNonZero();
177+
skippedFromStopCounter.logIfNonZero();
178+
skippedPairCounter.logIfNonZero();
157179

158180
// If we are applying a scenario (extending the transfers list rather than starting from scratch), for
159181
// all transfers out of a scenario stop into a base network stop we must also create the reverse transfer.

src/main/java/com/conveyal/r5/transit/TransitLayer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public class TransitLayer implements Serializable, Cloneable {
112112
// Inverse map of streetVertexForStop, and reconstructed from that list.
113113
public transient TIntIntMap stopForStreetVertex;
114114

115-
// For each stop, a packed list of transfers to other stops
115+
// For each stop, a packed list of transfers to other stops in the form (stopIndex, distance, stopIndex, distance...)
116116
// FIXME we may currently be storing weight or time to reach other stop, which we did to avoid floating point division. Instead, store distances in millimeters, and divide by speed in mm/sec.
117117
public List<TIntList> transfersForStop = new ArrayList<>();
118118

0 commit comments

Comments
 (0)