From 8e5586d9b03d98b30bdc641d2f273d803610ea66 Mon Sep 17 00:00:00 2001 From: Trevor Gerhardt Date: Tue, 4 Mar 2025 14:55:56 +0100 Subject: [PATCH] Handle thresholds `getRegionalResults` and `getSingleCutoffGrid` Handle `threshold` taken as the query parameter and when to check for `cutoffsMinutes` vs `dualAccessibilityThresholds`. --- .../RegionalAnalysisController.java | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/conveyal/analysis/controllers/RegionalAnalysisController.java b/src/main/java/com/conveyal/analysis/controllers/RegionalAnalysisController.java index f2442dd18..6ef9a0f86 100644 --- a/src/main/java/com/conveyal/analysis/controllers/RegionalAnalysisController.java +++ b/src/main/java/com/conveyal/analysis/controllers/RegionalAnalysisController.java @@ -192,28 +192,31 @@ private record HumanKey(FileStorageKey storageKey, String humanName) { }; private HumanKey getSingleCutoffGrid ( RegionalAnalysis analysis, OpportunityDataset destinations, - int cutoffMinutes, + int threshold, int percentile, FileStorageFormat fileFormat ) throws IOException { final String regionalAnalysisId = analysis._id; final String destinationPointSetId = destinations._id; // Selecting the zeroth cutoff still makes sense for older analyses that don't allow an array of N cutoffs. - int cutoffIndex = 0; - if (analysis.cutoffsMinutes != null) { - cutoffIndex = new TIntArrayList(analysis.cutoffsMinutes).indexOf(cutoffMinutes); - checkState(cutoffIndex >= 0); + int thresholdIndex = 0; + if (analysis.request.includeTemporalDensity) { + thresholdIndex = new TIntArrayList(analysis.request.dualAccessibilityThresholds).indexOf(threshold); + checkState(thresholdIndex >= 0); + } else if (analysis.cutoffsMinutes != null) { + thresholdIndex = new TIntArrayList(analysis.cutoffsMinutes).indexOf(threshold); + checkState(thresholdIndex >= 0); } LOG.info( "Returning {} minute accessibility to pointset {} (percentile {}) for regional analysis {} in format {}.", - cutoffMinutes, destinationPointSetId, percentile, regionalAnalysisId, fileFormat + threshold, destinationPointSetId, percentile, regionalAnalysisId, fileFormat ); // Analysis grids now have the percentile and cutoff in their S3 key, because there can be many of each. // We do this even for results generated by older workers, so they will be re-extracted with the new name. // These grids are reasonably small, we may be able to just send all cutoffs to the UI instead of selecting. String singleCutoffKey = String.format( "%s_%s_P%d_C%d.%s", - regionalAnalysisId, destinationPointSetId, percentile, cutoffMinutes, + regionalAnalysisId, destinationPointSetId, percentile, threshold, fileFormat.extension.toLowerCase(Locale.ROOT) ); FileStorageKey singleCutoffFileStorageKey = new FileStorageKey(RESULTS, singleCutoffKey); @@ -247,7 +250,7 @@ private HumanKey getSingleCutoffGrid ( LOG.debug("Single-cutoff grid {} not found on S3, deriving it from {}.", singleCutoffKey, multiCutoffKey); InputStream multiCutoffInputStream = new FileInputStream(fileStorage.getFile(multiCutoffFileStorageKey)); - Grid grid = new SelectingGridReducer(cutoffIndex).compute(multiCutoffInputStream); + Grid grid = new SelectingGridReducer(thresholdIndex).compute(multiCutoffInputStream); File localFile = FileUtils.createScratchFile(fileFormat.toString()); FileOutputStream fos = new FileOutputStream(localFile); @@ -270,7 +273,7 @@ private HumanKey getSingleCutoffGrid ( String analysisHumanName = humanNameForEntity(analysis); String destinationHumanName = humanNameForEntity(destinations); String resultHumanFilename = filenameCleanString( - String.format("%s_%s_P%d_C%d", analysisHumanName, destinationHumanName, percentile, cutoffMinutes) + String.format("%s_%s_P%d_C%d", analysisHumanName, destinationHumanName, percentile, threshold) ) + "." + fileFormat.extension.toLowerCase(Locale.ROOT); // Note that the returned human filename already contains the appropriate extension. return new HumanKey(singleCutoffFileStorageKey, resultHumanFilename); @@ -405,17 +408,26 @@ private UrlWithHumanName getRegionalResults (Request req, Response res) throws I // For newer analyses that have multiple cutoffs, percentiles, or destination pointsets, these initial values // are coming from deprecated fields, are not meaningful and will be overwritten below from query parameters. int percentile = analysis.travelTimePercentile; - int cutoffMinutes = analysis.cutoffMinutes; + int threshold = analysis.cutoffMinutes; String destinationPointSetId = analysis.grid; - // Handle newer regional analyses with multiple cutoffs in an array. - // If a query parameter is supplied, range check it, otherwise use the middle value in the list. - // The cutoff variable holds the actual cutoff in minutes, not the position in the array of cutoffs. - if (analysis.cutoffsMinutes != null) { + if (analysis.request.includeTemporalDensity) { + int nThresholds = analysis.request.dualAccessibilityThresholds.length; + int[] thresholds = analysis.request.dualAccessibilityThresholds; + checkState(nThresholds > 0, "Regional analysis has no thresholds."); + threshold = getIntQueryParameter(req, "threshold", thresholds[nThresholds / 2]); + checkArgument(new TIntArrayList(thresholds).contains(threshold), + "Dual accessibility thresholds for this regional analysis must be taken from this list: (%s)", + Ints.join(", ", thresholds) + ); + } else if (analysis.cutoffsMinutes != null) { + // Handle newer regional analyses with multiple cutoffs in an array. + // If a query parameter is supplied, range check it, otherwise use the middle value in the list. + // The cutoff variable holds the actual cutoff in minutes, not the position in the array of cutoffs. int nCutoffs = analysis.cutoffsMinutes.length; checkState(nCutoffs > 0, "Regional analysis has no cutoffs."); - cutoffMinutes = getIntQueryParameter(req, "cutoff", analysis.cutoffsMinutes[nCutoffs / 2]); - checkArgument(new TIntArrayList(analysis.cutoffsMinutes).contains(cutoffMinutes), + threshold = getIntQueryParameter(req, "cutoff", analysis.cutoffsMinutes[nCutoffs / 2]); + checkArgument(new TIntArrayList(analysis.cutoffsMinutes).contains(threshold), "Travel time cutoff for this regional analysis must be taken from this list: (%s)", Ints.join(", ", analysis.cutoffsMinutes) ); @@ -452,7 +464,7 @@ private UrlWithHumanName getRegionalResults (Request req, Response res) throws I } // Significant overhead here: UI contacts backend, backend calls S3, backend responds to UI, UI contacts S3. OpportunityDataset destinations = getDestinations(destinationPointSetId, userPermissions); - HumanKey gridKey = getSingleCutoffGrid(analysis, destinations, cutoffMinutes, percentile, format); + HumanKey gridKey = getSingleCutoffGrid(analysis, destinations, threshold, percentile, format); res.type(APPLICATION_JSON.asString()); return fileStorage.getJsonUrl(gridKey.storageKey, gridKey.humanName); }