Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HARMONY-1889: Add support for requesting time or area based averaging #627

Merged
merged 9 commits into from
Sep 26, 2024
3 changes: 3 additions & 0 deletions docs/guides/adapting-new-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ The structure of an entry in the [services.yml](../../config/services.yml) file
temporal: true # Can subset by a time range
variable: true # Can subset by UMM-Var variable
multiple_variable: true # Can subset multiple variables at once
averaging:
time: true # Can perform averaging over time
area: true # Can perform averaging over area
output_formats: # A list of output mime types the service can produce
- image/tiff
- image/png
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextFunction, Response } from 'express';
import DataOperation from '../../models/data-operation';
import HarmonyRequest from '../../models/harmony-request';
import wrap from '../../util/array';
import { handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleHeight, handleScaleExtent, handleScaleSize, handleWidth } from '../../util/parameter-parsers';
import { handleAveragingType, handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleHeight, handleScaleExtent, handleScaleSize, handleWidth } from '../../util/parameter-parsers';
import { createDecrypter, createEncrypter } from '../../util/crypto';
import env from '../../util/env';
import { RequestValidationError } from '../../util/errors';
Expand Down Expand Up @@ -41,6 +41,7 @@ export default function getCoverageRangeset(
handleScaleSize(operation, query);
handleHeight(operation, query);
handleWidth(operation, query);
handleAveragingType(operation, query);

operation.interpolationMethod = query.interpolation;
if (query.forceasync) {
Expand Down
3 changes: 2 additions & 1 deletion services/harmony/app/frontends/ogc-edr/get-data-common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DataOperation from '../../models/data-operation';
import HarmonyRequest from '../../models/harmony-request';
import wrap from '../../util/array';
import { handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleScaleExtent, handleScaleSize } from '../../util/parameter-parsers';
import { handleAveragingType, handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleScaleExtent, handleScaleSize } from '../../util/parameter-parsers';
import { createDecrypter, createEncrypter } from '../../util/crypto';
import env from '../../util/env';
import { RequestValidationError } from '../../util/errors';
Expand Down Expand Up @@ -35,6 +35,7 @@ export function getDataCommon(
handleCrs(operation, query.crs);
handleScaleExtent(operation, query);
handleScaleSize(operation, query);
handleAveragingType(operation, query);

operation.interpolationMethod = query.interpolation;
operation.outputWidth = query.width;
Expand Down
2 changes: 2 additions & 0 deletions services/harmony/app/markdown/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ As such it accepts parameters in the URL path as well as query parameters.
| ignoreErrors | if "true", continue processing a request to completion even if some items fail. If "false" immediately fail the request. Defaults to true |
| destinationUrl | destination url specified by the client; currently only s3 link urls are supported (e.g. s3://my-bucket-name/mypath) and will result in the job being run asynchronously |
| variable | the variable(s) to be used for variable subsetting. Multiple variables can be specified as a comma-separated list. This parameter is only used if the url `variable` path element is "parameter_vars" |
| averagingType | requests the data to be averaged over either time or area |
---
**Table {{tableCounter}}** - Harmony OGC Coverages API query parameters

Expand Down Expand Up @@ -131,6 +132,7 @@ Currently only the `/position`, `/cube`, `/trajectory` and `/area` routes are su
| subset | get a subset of the coverage by slicing or trimming along one axis. Harmony supports arbitrary dimension names for subsetting on numeric ranges for that dimension. |
| height | number of rows to return in the output coverage |
| width | number of columns to return in the output coverage |
| averagingType | requests the data to be averaged over either time or area |
---
**Table {{tableCounter}}** - Harmony extended parameters for all OGC EDR API routes

Expand Down
2 changes: 2 additions & 0 deletions services/harmony/app/models/data-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export default class DataOperation {

ignoreErrors?: boolean;

averagingType: string;

destinationUrl: string;

ummCollections: CmrUmmCollection[];
Expand Down
4 changes: 4 additions & 0 deletions services/harmony/app/models/services/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface ServiceCapabilities {
variable?: boolean;
multiple_variable?: true;
};
averaging?: {
time?: boolean;
area?: boolean;
};
output_formats?: string[];
reprojection?: boolean;
extend?: boolean;
Expand Down
100 changes: 99 additions & 1 deletion services/harmony/app/models/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,42 @@ function supportsDimensionSubsetting(configs: ServiceConfig<unknown>[]): Service
return configs.filter((config) => getIn(config, 'capabilities.subsetting.dimension', false));
}

/**
* Returns true if the operation requires time averaging
* @param operation - The operation to perform.
* @returns true if the provided operation requires time averaging and false otherwise
*/
function requiresTimeAveraging(operation: DataOperation): boolean {
return operation.averagingType === 'time';
}

/**
* Returns any services that support time averaging from the list of configs
* @param configs - The potential matching service configurations
* @returns Any configurations that support time averaging
*/
function supportsTimeAveraging(configs: ServiceConfig<unknown>[]): ServiceConfig<unknown>[] {
return configs.filter((config) => getIn(config, 'capabilities.averaging.time', false));
}

/**
* Returns true if the operation requires area averaging
* @param operation - The operation to perform.
* @returns true if the provided operation requires area averaging and false otherwise
*/
function requiresAreaAveraging(operation: DataOperation): boolean {
return operation.averagingType === 'area';
}

/**
* Returns any services that support area averaging from the list of configs
* @param configs - The potential matching service configurations
* @returns Any configurations that support area averaging
*/
function supportsAreaAveraging(configs: ServiceConfig<unknown>[]): ServiceConfig<unknown>[] {
return configs.filter((config) => getIn(config, 'capabilities.averaging.area', false));
}

export class UnsupportedOperation extends HttpError {
operation: DataOperation;

Expand Down Expand Up @@ -755,6 +791,64 @@ function filterDimensionSubsettingMatches(
return services;
}

/**
* Returns any services that support time averaging from the list of configs
* if the operation requires time averaging.
* @param operation - The operation to perform.
* @param context - Additional context that's not part of the operation, but influences the
* choice regarding the service to use
* @param configs - All service configurations that have matched up to this call
* @param requestedOperations - Operations that have been considered in filtering out services up to
* this call
* @returns Any service configurations that could still support the request
*/
function filterTimeAveragingMatches(
operation: DataOperation,
context: RequestContext,
configs: ServiceConfig<unknown>[],
requestedOperations: string[],
): ServiceConfig<unknown>[] {
let services = configs;
if (requiresTimeAveraging(operation)) {
requestedOperations.push('time averaging');
services = supportsTimeAveraging(configs);
}

if (services.length === 0) {
throw new UnsupportedOperation(operation, requestedOperations);
}
return services;
}

/**
* Returns any services that support area averaging from the list of configs
* if the operation requires area averaging.
* @param operation - The operation to perform.
* @param context - Additional context that's not part of the operation, but influences the
* choice regarding the service to use
* @param configs - All service configurations that have matched up to this call
* @param requestedOperations - Operations that have been considered in filtering out services up to
* this call
* @returns Any service configurations that could still support the request
*/
function filterAreaAveragingMatches(
operation: DataOperation,
context: RequestContext,
configs: ServiceConfig<unknown>[],
requestedOperations: string[],
): ServiceConfig<unknown>[] {
let services = configs;
if (requiresAreaAveraging(operation)) {
requestedOperations.push('area averaging');
services = supportsAreaAveraging(configs);
}

if (services.length === 0) {
throw new UnsupportedOperation(operation, requestedOperations);
}
return services;
}

type FilterFunction = (
// The operation to perform
operation: DataOperation,
Expand All @@ -776,11 +870,13 @@ const allFilterFns = [
filterConcatenationMatches,
filterVariableSubsettingMatches,
filterSpatialSubsettingMatches,
filterShapefileSubsettingMatches,
filterTemporalSubsettingMatches,
filterDimensionSubsettingMatches,
filterReprojectionMatches,
filterExtendMatches,
filterAreaAveragingMatches,
filterTimeAveragingMatches,
filterShapefileSubsettingMatches,
// This filter must be last because it chooses a format based on the accepted MimeTypes and
// the remaining services that could support the operation. If it ran earlier we could
// potentially eliminate services that a different accepted MimeType would have allowed. We
Expand All @@ -798,6 +894,8 @@ const requiredFilterFns = [
filterDimensionSubsettingMatches,
filterReprojectionMatches,
filterExtendMatches,
filterAreaAveragingMatches,
filterTimeAveragingMatches,
// See caveat above in allFilterFns about why this filter must be applied last
filterOutputFormatMatches,
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ paths:
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/variable"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A coverage's range set.
Expand Down Expand Up @@ -384,6 +385,7 @@ paths:
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/variable"
- $ref: "#/components/parameters/averagingType"
requestBody:
content:
multipart/form-data:
Expand Down Expand Up @@ -984,3 +986,12 @@ components:
items:
type: string
minLength: 1
averagingType:
name: averagingType
in: query
description: |
requests the data to be averaged over time or area
required: false
schema:
type: string

Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ paths:
- $ref: "#/components/parameters/granuleName"
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -258,6 +259,8 @@ paths:
type: string
extend:
type: string
averagingType:
type: string
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -304,6 +307,7 @@ paths:
- $ref: "#/components/parameters/granuleName"
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -396,6 +400,8 @@ paths:
type: string
extend:
type: string
averagingType:
type: string
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -442,6 +448,7 @@ paths:
- $ref: "#/components/parameters/granuleName"
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -532,6 +539,8 @@ paths:
type: string
extend:
type: string
averagingType:
type: string
responses:
"200":
description: A edr description.
Expand Down Expand Up @@ -576,6 +585,7 @@ paths:
- $ref: "#/components/parameters/granuleName"
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -661,6 +671,8 @@ paths:
type: string
extend:
type: string
averagingType:
type: string
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -705,6 +717,7 @@ paths:
- $ref: "#/components/parameters/granuleName"
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -791,6 +804,8 @@ paths:
type: string
extend:
type: string
averagingType:
type: string
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -841,6 +856,7 @@ paths:
- $ref: "#/components/parameters/granuleName"
- $ref: "#/components/parameters/grid"
- $ref: "#/components/parameters/extend"
- $ref: "#/components/parameters/averagingType"
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -943,6 +959,8 @@ paths:
type: string
extend:
type: string
averagingType:
type: string
responses:
"200":
description: A collection's EDR.
Expand Down Expand Up @@ -1644,3 +1662,11 @@ components:
required: false
schema:
type: string
averagingType:
name: averagingType
in: query
description: |
requests the data to be averaged over time or area
required: false
schema:
type: string
19 changes: 19 additions & 0 deletions services/harmony/app/util/parameter-parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,22 @@ export function handleWidth(
}
}
}

/**
* Handle the averaging parameter in a Harmony query, adding it to the DataOperation
* if necessary.
*
* @param operation - the DataOperation for the request
* @param query - the query for the request
*/
export function handleAveragingType(
operation: DataOperation,
query: Record<string, string>): void {
if (query.averagingtype) {
const value = query.averagingtype.toLowerCase();
if (value !== 'time' && value !== 'area') {
throw new RequestValidationError('query parameter "averagingType" must be either "time" or "area"');
}
operation.averagingType = value;
}
}
13 changes: 13 additions & 0 deletions services/harmony/test/helpers/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Partially applies a function by pre-filling some arguments.
*
* @param fn - The original function to partially apply.
* @param presetArgs - Arguments to pre-fill when calling the function.
*
* @returns A new function that takes the remaining arguments and calls the original function
* with both the preset and remaining arguments.
*
*/
export function partialApply(fn: (...args: unknown[]) => void, ...presetArgs: unknown[]) {
return (...laterArgs: unknown[]): void => fn(...presetArgs, ...laterArgs);
}
Loading
Loading