Skip to content

Commit 81a25c8

Browse files
committed
Merge remote-tracking branch 'origin/dev'
2 parents 5962e18 + c138c91 commit 81a25c8

36 files changed

+696
-230
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020-2023 Conveyal
3+
Copyright (c) 2020-2025 Conveyal LLC
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

build.gradle

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ plugins {
33
id 'application'
44
id 'maven-publish'
55
id 'com.palantir.git-version' version '2.0.0'
6-
id 'com.github.johnrengelman.shadow' version '8.1.1'
6+
id 'com.gradleup.shadow' version '9.0.1'
77
}
88

99
group = 'com.conveyal'
@@ -26,7 +26,7 @@ jar {
2626
manifest {
2727
attributes 'Automatic-Module-Name': 'com.conveyal.r5',
2828
'Main-Class': 'com.conveyal.analysis.BackendMain',
29-
'Build-Jdk-Spec': targetCompatibility.getMajorVersion(),
29+
'Build-Jdk-Spec': java.toolchain.languageVersion,
3030
'Implementation-Title': 'Conveyal Analysis Backend',
3131
'Implementation-Vendor': 'Conveyal LLC',
3232
'Implementation-Version': project.version
@@ -270,5 +270,9 @@ dependencies {
270270
// Although rarely used it should be low-impact: it is a test-only dependency with no transitive dependenices.
271271
testImplementation('org.jfree:jfreechart:1.5.1')
272272

273+
// Load the test launcher, no longer automatic in Gradle 9:
274+
// https://github.com/gradle/gradle/issues/34512
275+
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
276+
273277
}
274278

src/main/java/com/conveyal/analysis/SelectingGridReducer.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.conveyal.r5.analyst.Grid;
44
import com.google.common.io.LittleEndianDataInputStream;
55

6+
import java.io.BufferedInputStream;
67
import java.io.IOException;
78
import java.io.InputStream;
89
import java.util.zip.GZIPInputStream;
@@ -33,8 +34,10 @@ public SelectingGridReducer(int index) {
3334
this.index = index;
3435
}
3536

37+
private static final int BUFSIZE = 32 * 1024;
38+
3639
public Grid compute (InputStream rawInput) throws IOException {
37-
LittleEndianDataInputStream input = new LittleEndianDataInputStream(new GZIPInputStream(rawInput));
40+
LittleEndianDataInputStream input = new LittleEndianDataInputStream(new BufferedInputStream(new GZIPInputStream(rawInput, BUFSIZE), BUFSIZE));
3841

3942
char[] header = new char[8];
4043
for (int i = 0; i < 8; i++) {

src/main/java/com/conveyal/analysis/controllers/RegionalAnalysisController.java

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060
import static com.conveyal.analysis.util.JsonUtil.toJson;
6161
import static com.conveyal.file.FileCategory.BUNDLES;
6262
import static com.conveyal.file.FileCategory.RESULTS;
63+
import static com.conveyal.file.FileStorageFormat.GEOTIFF;
64+
import static com.conveyal.file.FileStorageFormat.GRID;
65+
import static com.conveyal.file.FileStorageFormat.PNG;
6366
import static com.conveyal.file.UrlWithHumanName.filenameCleanString;
6467
import static com.conveyal.r5.transit.TransportNetworkCache.getScenarioFilename;
6568
import static com.google.common.base.Preconditions.checkArgument;
@@ -161,10 +164,10 @@ private RegionalAnalysis deleteRegionalAnalysis (Request req, Response res) {
161164
return analysis;
162165
}
163166

164-
private int getIntQueryParameter (Request req, String parameterName, int defaultValue) {
167+
private int getIntQueryParameter (Request req, String parameterName) {
165168
String paramValue = req.queryParams(parameterName);
166169
if (paramValue == null) {
167-
return defaultValue;
170+
throw new IllegalArgumentException("Must provide query parameter " + parameterName);
168171
}
169172
try {
170173
return Integer.parseInt(paramValue);
@@ -294,21 +297,21 @@ private Object getAllRegionalResults (Request req, Response res) throws IOExcept
294297
}
295298
// File did not exist. Create it in the background and ask caller to request it later.
296299
filesBeingPrepared.add(zippedResultsKey.path);
297-
Task task = Task.create("Zip all geotiffs for regional analysis " + analysis.name)
300+
Task task = Task.create("Preparing regional results archive (hit download again when complete)")
298301
.forUser(userPermissions)
299302
.withAction(progressListener -> {
300303
int nSteps = analysis.destinationPointSetIds.length * analysis.cutoffsMinutes.length *
301304
analysis.travelTimePercentiles.length * 2 + 1;
302305
progressListener.beginTask("Creating and archiving geotiffs...", nSteps);
303306
// Iterate over all dest, cutoff, percentile combinations and generate one geotiff for each combination.
304307
List<HumanKey> humanKeys = new ArrayList<>();
308+
GridResultType gridResultType = determineGridResultType(analysis);
305309
for (String destinationPointSetId : analysis.destinationPointSetIds) {
306310
OpportunityDataset destinations = getDestinations(destinationPointSetId, userPermissions);
307-
// TODO handle dual access
308-
for (int cutoffMinutes : analysis.cutoffsMinutes) {
311+
for (int threshold : getValidThresholds(analysis)) {
309312
for (int percentile : analysis.travelTimePercentiles) {
310313
HumanKey gridKey = getSingleCutoffGrid(
311-
analysis, destinations, cutoffMinutes, percentile, GridResultType.ACCESS, FileStorageFormat.GEOTIFF
314+
analysis, destinations, threshold, percentile, gridResultType, GEOTIFF
312315
);
313316
humanKeys.add(gridKey);
314317
progressListener.increment();
@@ -384,59 +387,17 @@ private UrlWithHumanName getRegionalResults (Request req, Response res) throws I
384387
// expected to have no gridded results and cleanly return a 404?
385388
final String regionalAnalysisId = req.params("_id");
386389
FileStorageFormat format = FileStorageFormat.valueOf(req.params("format").toUpperCase());
387-
if (!FileStorageFormat.GRID.equals(format) && !FileStorageFormat.PNG.equals(format) && !FileStorageFormat.GEOTIFF.equals(format)) {
388-
throw AnalysisServerException.badRequest("Format \"" + format + "\" is invalid. Request format must be \"grid\", \"png\", or \"geotiff\".");
390+
if (!List.of(GRID, PNG, GEOTIFF).contains(format)) {
391+
throw AnalysisServerException.badRequest("Parameter 'format' must be one of [grid, png, geotiff].");
389392
}
390393
final UserPermissions userPermissions = UserPermissions.from(req);
391394
RegionalAnalysis analysis = getAnalysis(regionalAnalysisId, userPermissions);
392-
393-
// TODO handle a regional analysis that includes both regular accessibility and dual access results.
394-
GridResultType gridResultType = analysis.request.includeTemporalDensity ? GridResultType.DUAL_ACCESS : GridResultType.ACCESS;
395-
396-
// If a query parameter is supplied, range check it, otherwise use the middle value in the list.
397-
int threshold;
398-
if (gridResultType.equals(GridResultType.DUAL_ACCESS)) {
399-
int nThresholds = analysis.request.dualAccessThresholds.length;
400-
int[] thresholds = analysis.request.dualAccessThresholds;
401-
checkState(nThresholds > 0, "Regional analysis has no dual access thresholds.");
402-
threshold = getIntQueryParameter(req, "threshold", thresholds[nThresholds / 2]);
403-
checkArgument(new TIntArrayList(thresholds).contains(threshold),
404-
"Dual access thresholds for this regional analysis must be taken from this list: (%s)",
405-
Ints.join(", ", thresholds)
406-
);
407-
} else {
408-
// Handle newer regional analyses with multiple cutoffs in an array.
409-
// The cutoff variable holds the actual cutoff in minutes, not the position in the array of cutoffs.
410-
checkState(analysis.cutoffsMinutes != null, "Regional analysis has no cutoffs.");
411-
int nCutoffs = analysis.cutoffsMinutes.length;
412-
checkState(nCutoffs > 0, "Regional analysis has no cutoffs.");
413-
threshold = getIntQueryParameter(req, "threshold", analysis.cutoffsMinutes[nCutoffs / 2]);
414-
checkArgument(new TIntArrayList(analysis.cutoffsMinutes).contains(threshold),
415-
"Travel time cutoff for this regional analysis must be taken from this list: (%s)",
416-
Ints.join(", ", analysis.cutoffsMinutes)
417-
);
418-
}
419-
420-
// If a query parameter is supplied, range check it, otherwise use the middle value in the list.
421-
// The percentile variable holds the actual percentile (25, 50, 95) not the position in the array.
422-
int nPercentiles = analysis.travelTimePercentiles.length;
423-
checkState(nPercentiles > 0, "Regional analysis has no percentiles.");
424-
int percentile = getIntQueryParameter(req, "percentile", analysis.travelTimePercentiles[nPercentiles / 2]);
425-
checkArgument(new TIntArrayList(analysis.travelTimePercentiles).contains(percentile),
426-
"Percentile for this regional analysis must be taken from this list: (%s)",
427-
Ints.join(", ", analysis.travelTimePercentiles));
428-
429-
// Handle regional analyses with multiple destination pointsets per analysis.
430-
int nGrids = analysis.destinationPointSetIds.length;
431-
checkState(nGrids > 0, "Regional analysis has no grids.");
432-
String destinationPointSetId = req.queryParams("destinationPointSetId");
433-
if (destinationPointSetId == null) {
434-
destinationPointSetId = analysis.destinationPointSetIds[0];
435-
}
436-
checkArgument(Arrays.asList(analysis.destinationPointSetIds).contains(destinationPointSetId),
437-
"Destination gridId must be one of: %s",
438-
String.join(",", analysis.destinationPointSetIds));
439-
395+
GridResultType gridResultType = determineGridResultType(analysis);
396+
// The threshold parameter holds the value in minutes, not the position in the array of thresholds.
397+
int threshold = getAndValidateIntParameter(req, "threshold", getValidThresholds(analysis));
398+
int percentile = getAndValidateIntParameter(req, "percentile", analysis.travelTimePercentiles);
399+
String destinationPointSetId = getAndValidateStringParameter(
400+
req, "destinationPointSetId", analysis.destinationPointSetIds);
440401
// We started implementing the ability to retrieve and display partially completed analyses.
441402
// We eventually decided these should not be available here at the same endpoint as complete, immutable results.
442403
if (broker.findJob(regionalAnalysisId) != null) {
@@ -449,6 +410,41 @@ private UrlWithHumanName getRegionalResults (Request req, Response res) throws I
449410
return fileStorage.getJsonUrl(gridKey.storageKey, gridKey.humanName);
450411
}
451412

413+
private int[] getValidThresholds (RegionalAnalysis analysis) {
414+
return switch (determineGridResultType(analysis)) {
415+
case ACCESS -> analysis.cutoffsMinutes;
416+
case DUAL_ACCESS -> analysis.request.dualAccessThresholds;
417+
};
418+
}
419+
420+
// This assumes each set of regional analysis results has only primal or dual access, not both.
421+
// TODO handle regional analyses that include both regular accessibility and dual access results.
422+
private GridResultType determineGridResultType (RegionalAnalysis analysis) {
423+
return analysis.request.includeTemporalDensity ? GridResultType.DUAL_ACCESS : GridResultType.ACCESS;
424+
}
425+
426+
/// Get the value for a given query parameter name, check that it's non-null and can be parsed
427+
/// as an integer, and check that the value is present in an array of valid values.
428+
private int getAndValidateIntParameter (Request req, String parameterName, int[] allowedValues) {
429+
int value = getIntQueryParameter(req, parameterName);
430+
checkState(allowedValues != null && allowedValues.length > 0, "Lacking values for " + parameterName);
431+
checkArgument(Ints.contains(allowedValues, value), "Parameter '%s' must be one of: %s",
432+
parameterName, Arrays.toString(allowedValues));
433+
return value;
434+
}
435+
436+
/// Should behave identically to getAndValidateIntParameter, but for Strings.
437+
private String getAndValidateStringParameter (Request req, String parameterName, String[] allowedValues) {
438+
checkState(allowedValues != null && allowedValues.length > 0, "Lacking values for " + parameterName);
439+
String value = req.queryParams(parameterName);
440+
if (value == null || value.isEmpty()) {
441+
throw new IllegalArgumentException("Must provide query parameter " + parameterName);
442+
}
443+
checkArgument(List.of(allowedValues).contains(value), "Parameter '%s' must be one of: %s",
444+
parameterName, Arrays.toString(allowedValues));
445+
return value;
446+
}
447+
452448
private Object getCsvResults (Request req, Response res) {
453449
final String regionalAnalysisId = req.params("_id");
454450
final CsvResultType resultType = CsvResultType.valueOf(req.params("resultType").toUpperCase());

src/main/java/com/conveyal/gtfs/model/Transfer.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public void loadOneRow() throws IOException {
3434
tr.sourceFileLine = row;
3535
tr.from_stop_id = getStringField("from_stop_id", true);
3636
tr.to_stop_id = getStringField("to_stop_id", true);
37-
tr.transfer_type = getIntField("transfer_type", true, 0, 3);
37+
tr.transfer_type = getIntField("transfer_type", true, 0, 5);
3838
tr.min_transfer_time = getIntField("min_transfer_time", false, 0, Integer.MAX_VALUE);
3939
tr.from_route_id = getStringField("from_route_id", false);
4040
tr.to_route_id = getStringField("to_route_id", false);
@@ -43,10 +43,12 @@ public void loadOneRow() throws IOException {
4343

4444
getRefField("from_stop_id", true, feed.stops);
4545
getRefField("to_stop_id", true, feed.stops);
46-
getRefField("from_route_id", false, feed.routes);
47-
getRefField("to_route_id", false, feed.routes);
48-
getRefField("from_trip_id", false, feed.trips);
49-
getRefField("to_trip_id", false, feed.trips);
46+
// We do not validate referential integrity of these fields because they are not
47+
// consumed by R5, and some prominent feeds contain thousands of such errors.
48+
// getRefField("from_route_id", false, feed.routes);
49+
// getRefField("to_route_id", false, feed.routes);
50+
// getRefField("from_trip_id", false, feed.trips);
51+
// getRefField("to_trip_id", false, feed.trips);
5052

5153
// row number used as an arbitrary unique string to give MapDB a key.
5254
feed.transfers.put(Long.toString(row), tr);

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@
2525
@JsonInclude(JsonInclude.Include.NON_NULL)
2626
public class TransportNetworkConfig {
2727

28-
/** ID of the OSM file, for use with OSMCache */
28+
/** ID of the OSM file. Used as a key for fetching from OSMCache. */
2929
public String osmId;
3030

31-
/** IDs of the GTFS files, for use with GTFSCache */
31+
/** IDs of the GTFS files. Used as keys for fetching from GTFSCache. */
3232
public List<String> gtfsIds;
3333

34-
/** The fare calculator for analysis, if any. TODO this is not yet wired up to TransportNetwork.setFareCalculator. */
34+
/**
35+
* The fare calculator for analysis, if any.
36+
* TODO this is not yet wired up to TransportNetwork.setFareCalculator.
37+
*/
3538
public InRoutingFareCalculator analysisFareCalculator;
3639

3740
/** A list of _R5_ modifications to apply during network build. May be null. */
@@ -57,4 +60,57 @@ public class TransportNetworkConfig {
5760
*/
5861
public String traversalPermissionLabeler;
5962

63+
/**
64+
* Whether to save detailed trip shapes from GTFS (e.g., for Conveyal Taui sites or the Network Viewer).
65+
* If false, straight line segments between stops will be used in visualizations.
66+
*/
67+
public boolean saveShapes;
68+
69+
/**
70+
* How to handle stop-to-stop transfers in GTFS transfers.txt, and how to combine them with OSM-derived transfers.
71+
* OSM_ONLY is the default, but STOP_TO_PATTERN should generally provide better results where GTFS data is good.
72+
* In some cases path results may be strange or incorrect, but the quality of travel times should be no worse than
73+
* the default OSM_ONLY option. For example: a large station in which subway platforms are separated by long walk
74+
* times specified in GTFS but no times specified for transfers between those platforms and nearby bus stops.
75+
*
76+
* Currently we only handle stop-to-stop transfers in GTFS transfers.txt. Other more specific transfers types like
77+
* route-to-route and trip-to-trip are not compatible with our current routing approach, so will be ignored with
78+
* a warning. We also only import transfer type 2 ("minimum amount of time between arrival and departure to ensure
79+
* a connection"). For any option except OSM_ONLY, these GTFS transfers override and replace any OSM street distance
80+
* calculations. Although the GTFS spec text says "minimum amount of time", implying that other information like OSM
81+
* routing could make times longer, we believe the proper interpretation is that transfers.txt provides a typical
82+
* safe amount of time needed to walk between the two stops, which would only be made worse by comparing with OSM.
83+
*
84+
* Additional considerations:
85+
* Strangely, BOARD_SLACK_SECONDS appears to only be used in classes for displaying paths, not for routing.
86+
* We currently apply a hard lower limit of 60 seconds between alighting and boarding.
87+
* Should this be configurable or interact with the transfer entries?
88+
*/
89+
public TransferConfig transfers;
90+
91+
public enum TransferConfig {
92+
/// Find transfers only by searching through the OSM street network, ignore GTFS transfers
93+
OSM_ONLY,
94+
/// Load transfers only from GTFS transfers.txt, do not use the OSM street network
95+
GTFS_ONLY,
96+
/// Use OSM where GTFS does not provide a transfer from a given stop to a given trip pattern
97+
STOP_TO_PATTERN,
98+
/// Find transfers via streets for any pair of stops not connected by a GTFS transfer
99+
STOP_PAIR,
100+
}
101+
102+
/**
103+
* Steepest allowable slope for traversal. If a way has an "incline" tag (e.g., from OSW or GATIS rather than
104+
* a typical OSM source) with an absolute value that exceeds this limit, custom TraversalPermissionLabelers can
105+
* remove permissions. Currently implemented only for pedestrians.
106+
*/
107+
public Double maxIncline;
108+
109+
/**
110+
* Whether to exclude pedestrian traversal of ways with highway=stairs tags and nodes with kerb=raised tags. This
111+
* option should generally be used with detailed sidewalk networks and a TraversalPermissionLabeler that forces
112+
* use of sidewalks (i.e., disallows walking on roadways).
113+
*/
114+
public boolean stepFree;
115+
60116
}

0 commit comments

Comments
 (0)