6060import static com .conveyal .analysis .util .JsonUtil .toJson ;
6161import static com .conveyal .file .FileCategory .BUNDLES ;
6262import 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 ;
6366import static com .conveyal .file .UrlWithHumanName .filenameCleanString ;
6467import static com .conveyal .r5 .transit .TransportNetworkCache .getScenarioFilename ;
6568import 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 ());
0 commit comments