terriajs
Version:
Geospatial data visualization platform.
770 lines (694 loc) • 22.9 kB
text/typescript
import i18next from "i18next";
import flatten from "lodash-es/flatten";
import {
action,
computed,
isObservableArray,
runInAction,
makeObservable,
override
} from "mobx";
import URI from "urijs";
import filterOutUndefined from "../../../Core/filterOutUndefined";
import isDefined from "../../../Core/isDefined";
import TerriaError, { networkRequestError } from "../../../Core/TerriaError";
import Reproject from "../../../Map/Vector/Reproject";
import CatalogFunctionMixin from "../../../ModelMixins/CatalogFunctionMixin";
import XmlRequestMixin from "../../../ModelMixins/XmlRequestMixin";
import xml2json from "../../../ThirdParty/xml2json";
import WebProcessingServiceCatalogFunctionTraits from "../../../Traits/TraitsClasses/WebProcessingServiceCatalogFunctionTraits";
import CommonStrata from "../../Definition/CommonStrata";
import CreateModel from "../../Definition/CreateModel";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel } from "../../Definition/Model";
import StratumOrder from "../../Definition/StratumOrder";
import updateModelFromJson from "../../Definition/updateModelFromJson";
import BooleanParameter from "../../FunctionParameters/BooleanParameter";
import DateParameter from "../../FunctionParameters/DateParameter";
import DateTimeParameter from "../../FunctionParameters/DateTimeParameter";
import EnumerationParameter from "../../FunctionParameters/EnumerationParameter";
import FunctionParameter, {
Options as FunctionParameterOptions
} from "../../FunctionParameters/FunctionParameter";
import GeoJsonParameter, {
isGeoJsonFunctionParameter
} from "../../FunctionParameters/GeoJsonParameter";
import LineParameter from "../../FunctionParameters/LineParameter";
import PointParameter from "../../FunctionParameters/PointParameter";
import PolygonParameter from "../../FunctionParameters/PolygonParameter";
import RectangleParameter from "../../FunctionParameters/RectangleParameter";
import RegionParameter from "../../FunctionParameters/RegionParameter";
import RegionTypeParameter from "../../FunctionParameters/RegionTypeParameter";
import StringParameter from "../../FunctionParameters/StringParameter";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import { ModelConstructorParameters } from "../../Definition/Model";
import WebProcessingServiceCatalogFunctionJob from "./WebProcessingServiceCatalogFunctionJob";
import NumberParameter from "../../FunctionParameters/NumberParameter";
type AllowedValues = {
Value?: string | string[];
Range?: Range;
};
type Range = {
MaximumValue?: number;
MinimumValue?: number;
};
type LiteralDataType = {
"ows:reference"?: string;
text?: string;
};
type LiteralData = {
AllowedValues?: AllowedValues;
AllowedValue?: AllowedValues;
AnyValue?: unknown;
DefaultValue?: unknown;
DataType?: LiteralDataType | string;
dataType?: string;
};
type ComplexData = {
Default?: { Format?: { Schema?: string } };
};
type BoundingBoxData = {
Default?: { CRS?: string };
Supported?: { CRS?: string[] };
};
type Input = {
Identifier?: string;
Title?: string;
Abstract?: string;
LiteralData?: LiteralData;
ComplexData?: ComplexData;
BoundingBoxData?: BoundingBoxData;
minOccurs?: number;
};
type ProcessDescription = {
DataInputs?: { Input: Input[] | Input };
storeSupported?: string;
statusSupported?: string;
};
export type WpsInputData = {
inputValue: Promise<string | undefined> | string | undefined;
inputType: string;
};
type ParameterConverter = {
inputToParameter: (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) => FunctionParameter | undefined;
parameterToInput: (parameter: FunctionParameter) => WpsInputData | undefined;
};
class WpsLoadableStratum extends LoadableStratum(
WebProcessingServiceCatalogFunctionTraits
) {
static stratumName = "wpsLoadable";
constructor(
readonly item: WebProcessingServiceCatalogFunction,
readonly processDescription: ProcessDescription
) {
super();
makeObservable(this);
}
duplicateLoadableStratum(newModel: BaseModel): this {
return new WpsLoadableStratum(
newModel as WebProcessingServiceCatalogFunction,
this.processDescription
) as this;
}
static async load(item: WebProcessingServiceCatalogFunction) {
if (!isDefined(item.describeProcessUrl)) {
return;
}
const xml = await item.getXml(item.describeProcessUrl);
if (
!isDefined(xml) ||
!isDefined(xml.documentElement) ||
xml.documentElement.localName !== "ProcessDescriptions"
) {
throwInvalidWpsServerError(item, "DescribeProcess");
}
const json = xml2json(xml);
if (!isDefined(json.ProcessDescription)) {
throw networkRequestError({
sender: this,
title: i18next.t(
"models.webProcessingService.processDescriptionErrorTitle"
),
message: i18next.t(
"models.webProcessingService.processDescriptionErrorMessage"
)
});
}
return new WpsLoadableStratum(item, json.ProcessDescription);
}
/**
* Return the inputs in the processDescription
*/
get inputs(): Input[] {
if (!isDefined(this.processDescription)) {
return [];
}
const dataInputs = this.processDescription.DataInputs;
if (!isDefined(dataInputs) || !isDefined(dataInputs.Input)) {
throw networkRequestError({
sender: this,
title: i18next.t("models.webProcessingService.processInputErrorTitle"),
message: i18next.t(
"models.webProcessingService.processInputErrorMessage"
)
});
}
const inputs =
Array.isArray(dataInputs.Input) || isObservableArray(dataInputs.Input)
? dataInputs.Input
: [dataInputs.Input];
return inputs;
}
get storeSupported() {
const value = this.processDescription.storeSupported?.toLowerCase();
return value === "true" ? true : value === "false" ? false : undefined;
}
get statusSupported() {
const value = this.processDescription.statusSupported?.toLowerCase();
return value === "true" ? true : value === "false" ? false : undefined;
}
}
StratumOrder.addLoadStratum(WpsLoadableStratum.stratumName);
export default class WebProcessingServiceCatalogFunction extends XmlRequestMixin(
CatalogFunctionMixin(CreateModel(WebProcessingServiceCatalogFunctionTraits))
) {
static readonly type = "wps";
constructor(...args: ModelConstructorParameters) {
super(...args);
makeObservable(this);
}
get type() {
return WebProcessingServiceCatalogFunction.type;
}
get typeName() {
return "Web Processing Service (WPS)";
}
get cacheDuration(): string {
if (isDefined(super.cacheDuration)) {
return super.cacheDuration;
}
return "0d";
}
/**
* Returns the proxied URL for the DescribeProcess endpoint.
*/
get describeProcessUrl() {
if (!isDefined(this.url) || !isDefined(this.identifier)) {
return;
}
const uri = new URI(this.url).query({
service: "WPS",
request: "DescribeProcess",
version: "1.0.0",
Identifier: this.identifier
});
return proxyCatalogItemUrl(this, uri.toString());
}
async forceLoadMetadata() {
if (!this.strata.has(WpsLoadableStratum.stratumName)) {
const stratum = await WpsLoadableStratum.load(this);
if (isDefined(stratum)) {
runInAction(() => {
this.strata.set(WpsLoadableStratum.stratumName, stratum);
});
}
}
}
/**
* Must be kept alive due to `subtype` observable property of GeoJsonFunctionParameter
*/
get functionParameters() {
const stratum = this.strata.get(
WpsLoadableStratum.stratumName
) as WpsLoadableStratum;
if (!isDefined(stratum)) return [];
return stratum.inputs.map((input) => {
const parameter = this.convertInputToParameter(this, input);
if (isDefined(parameter)) {
return parameter;
}
throw new TerriaError({
sender: this,
title: "Unsupported parameter type",
message: `The parameter '${input.Identifier}' is not a supported type of parameter.`
});
});
}
protected async createJob(id: string) {
const job = new WebProcessingServiceCatalogFunctionJob(id, this.terria);
const dataInputs = filterOutUndefined(
await Promise.all(
this.functionParameters
.filter((p) => isDefined(p.value) && p.value !== null)
.map((p) => this.convertParameterToInput(p))
)
);
runInAction(() =>
updateModelFromJson(job, CommonStrata.user, {
name: `WPS: ${
this.name || this.identifier || this.uniqueId
} result ${new Date().toISOString()}`,
geojsonFeatures: flatten(
this.functionParameters
.map((param) =>
isGeoJsonFunctionParameter(param)
? param.geoJsonFeature
: undefined
)
.filter(isDefined)
) as any,
url: this.url,
identifier: this.identifier,
executeWithHttpGet: this.executeWithHttpGet,
statusSupported: this.statusSupported,
storeSupported: this.storeSupported,
wpsParameters: dataInputs,
forceConvertResultsToV8: this.forceConvertResultsToV8
})
).raiseError(this.terria, "Error ocurred while updating job model JSON");
return job;
}
convertInputToParameter(
catalogFunction: CatalogFunctionMixin.Instance,
input: Input
) {
if (!isDefined(input.Identifier)) {
return;
}
const isRequired = isDefined(input.minOccurs) && input.minOccurs > 0;
for (let i = 0; i < parameterConverters.length; i++) {
const converter = parameterConverters[i];
const parameter = converter.inputToParameter(catalogFunction, input, {
id: input.Identifier,
name: input.Title,
description: input.Abstract,
isRequired
});
if (isDefined(parameter)) {
return parameter;
}
}
}
async convertParameterToInput(parameter: FunctionParameter) {
const converter = parameterTypeToConverter(parameter);
const result = converter?.parameterToInput(parameter);
if (!isDefined(result)) {
return;
}
const inputValue = await Promise.resolve(result.inputValue);
if (!isDefined(inputValue)) {
return;
}
return {
inputIdentifier: parameter.id,
inputValue: inputValue,
inputType: result.inputType
};
}
}
const LiteralDataConverter = {
inputToParameter: function (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) {
if (!isDefined(input.LiteralData)) {
return;
}
const allowedValues =
input.LiteralData.AllowedValues || input.LiteralData.AllowedValue;
if (isDefined(allowedValues) && isDefined(allowedValues.Value)) {
return new EnumerationParameter(catalogFunction, {
...options,
options: (Array.isArray(allowedValues.Value) ||
isObservableArray(allowedValues.Value)
? (allowedValues.Value as string[])
: [allowedValues.Value]
).map((id) => {
return { id };
})
});
} else if (isDefined(allowedValues) && isDefined(allowedValues.Range)) {
const np = new NumberParameter(catalogFunction, {
...options
});
np.minimum = isDefined(allowedValues.Range.MinimumValue)
? allowedValues.Range.MinimumValue
: np.minimum;
np.maximum = allowedValues.Range.MaximumValue;
np.defaultValue = isDefined(input.LiteralData.DefaultValue)
? (input.LiteralData.DefaultValue as number)
: np.minimum;
return np;
} else if (isDefined(input.LiteralData.AnyValue)) {
let dtype: string | null = null;
if (isDefined(input.LiteralData["dataType"])) {
dtype = input.LiteralData["dataType"];
} else if (isDefined(input.LiteralData.DataType)) {
if (typeof input.LiteralData.DataType === "string") {
dtype = input.LiteralData.DataType;
} else if (isDefined(input.LiteralData.DataType["ows:reference"])) {
dtype = input.LiteralData.DataType["ows:reference"];
} else if (isDefined(input.LiteralData.DataType.text)) {
dtype = input.LiteralData.DataType.text;
}
}
if (dtype !== null) {
if (dtype.indexOf("http://www.w3.org/TR/xmlschema-2/#") === 0) {
dtype = dtype.substring(dtype.lastIndexOf("#") + 1).toLowerCase();
}
}
if (dtype === "string") {
return new StringParameter(catalogFunction, {
...options
});
} else if (dtype === "boolean") {
return new BooleanParameter(catalogFunction, {
...options
});
} else if (dtype === "date") {
const dt = new DateParameter(catalogFunction, {
...options,
clock: catalogFunction.terria.timelineClock
});
dt.variant = "literal";
return dt;
} else if (dtype?.toLowerCase() === "datetime") {
const dt = new DateTimeParameter(catalogFunction, {
...options,
clock: catalogFunction.terria.timelineClock
});
dt.variant = "literal";
return dt;
}
// Assume its a string, if no literal datatype given
return new StringParameter(catalogFunction, {
...options
});
}
},
parameterToInput: function (parameter: FunctionParameter) {
return {
inputValue: parameter.value as string | undefined,
inputType: "LiteralData"
};
}
};
const ComplexDateConverter = {
inputToParameter: function (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) {
if (
!isDefined(input.ComplexData) ||
!isDefined(input.ComplexData.Default) ||
!isDefined(input.ComplexData.Default.Format) ||
!isDefined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
const schema = input.ComplexData.Default.Format.Schema;
if (schema !== "http://www.w3.org/TR/xmlschema-2/#date") {
return undefined;
}
const dparam = new DateParameter(catalogFunction, {
...options,
clock: catalogFunction.terria.timelineClock
});
dparam.variant = "complex";
return dparam;
},
parameterToInput: function (parameter: FunctionParameter) {
return {
inputType: "ComplexData",
inputValue: DateParameter.formatValueForUrl(
parameter?.value?.toString() || ""
)
};
}
};
const ComplexDateTimeConverter = {
inputToParameter: function (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) {
if (
!isDefined(input.ComplexData) ||
!isDefined(input.ComplexData.Default) ||
!isDefined(input.ComplexData.Default.Format) ||
!isDefined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
const schema = input.ComplexData.Default.Format.Schema;
if (schema !== "http://www.w3.org/TR/xmlschema-2/#dateTime") {
return undefined;
}
const dt = new DateTimeParameter(catalogFunction, {
...options,
clock: catalogFunction.terria.timelineClock
});
dt.variant = "complex";
return dt;
},
parameterToInput: function (parameter: FunctionParameter) {
return {
inputType: "ComplexData",
inputValue: DateTimeParameter.formatValueForUrl(
parameter?.value?.toString() || ""
)
};
}
};
export const PointConverter = simpleGeoJsonDataConverter(
"point",
PointParameter
);
const LineConverter = simpleGeoJsonDataConverter("linestring", LineParameter);
const PolygonConverter = simpleGeoJsonDataConverter(
"polygon",
PolygonParameter
);
const RectangleConverter = {
inputToParameter: function (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) {
if (
!isDefined(input.BoundingBoxData) ||
!isDefined(input.BoundingBoxData.Default) ||
!isDefined(input.BoundingBoxData.Default.CRS)
) {
return undefined;
}
let code = Reproject.crsStringToCode(input.BoundingBoxData.Default.CRS);
let usedCrs = input.BoundingBoxData.Default.CRS;
// Find out if Terria's CRS is supported.
if (
code !== Reproject.TERRIA_CRS &&
isDefined(input.BoundingBoxData.Supported) &&
isDefined(input.BoundingBoxData.Supported.CRS)
) {
for (let i = 0; i < input.BoundingBoxData.Supported.CRS.length; i++) {
if (
Reproject.crsStringToCode(input.BoundingBoxData.Supported.CRS[i]) ===
Reproject.TERRIA_CRS
) {
code = Reproject.TERRIA_CRS;
usedCrs = input.BoundingBoxData.Supported.CRS[i];
break;
}
}
}
// We are currently only supporting Terria's CRS, because if we reproject we don't know the URI or whether
// the bounding box order is lat-long or long-lat.
if (!isDefined(code)) {
return undefined;
}
return new RectangleParameter(catalogFunction, {
...options,
crs: usedCrs
});
},
parameterToInput: function (functionParameter: FunctionParameter) {
const parameter = functionParameter as RectangleParameter;
const value = parameter.value;
if (!isDefined(value)) {
return;
}
let bboxMinCoord1, bboxMinCoord2, bboxMaxCoord1, bboxMaxCoord2, urn;
// We only support CRS84 and EPSG:4326
if (parameter.crs.indexOf("crs84") !== -1) {
// CRS84 uses long, lat rather that lat, long order.
bboxMinCoord1 = value.west;
bboxMinCoord2 = value.south;
bboxMaxCoord1 = value.east;
bboxMaxCoord2 = value.north;
// Comfortingly known as WGS 84 longitude-latitude according to Table 3 in OGC 07-092r1.
urn = "urn:ogc:def:crs:OGC:1.3:CRS84";
} else {
// The URN value urn:ogc:def:crs:EPSG:6.6:4326 shall mean the Coordinate Reference System (CRS) with code
// 4326 specified in version 6.6 of the EPSG database available at http://www.epsg.org/. That CRS specifies
// the axis order as Latitude followed by Longitude.
// We don't know about other URN versions, so are going to return 6.6 regardless of what was requested.
bboxMinCoord1 = value.south;
bboxMinCoord2 = value.west;
bboxMaxCoord1 = value.north;
bboxMaxCoord2 = value.east;
urn = "urn:ogc:def:crs:EPSG:6.6:4326";
}
return {
inputType: "BoundingBoxData",
inputValue:
bboxMinCoord1 +
"," +
bboxMinCoord2 +
"," +
bboxMaxCoord1 +
"," +
bboxMaxCoord2 +
"," +
urn
};
}
};
const GeoJsonGeometryConverter = {
inputToParameter: function (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) {
if (
!isDefined(input.ComplexData) ||
!isDefined(input.ComplexData.Default) ||
!isDefined(input.ComplexData.Default.Format) ||
!isDefined(input.ComplexData.Default.Format.Schema)
) {
return;
}
const schema = input.ComplexData.Default.Format.Schema;
if (schema.indexOf("http://geojson.org/geojson-spec.html#") !== 0) {
return undefined;
}
const regionTypeParameter = new RegionTypeParameter(catalogFunction, {
id: "regionType",
name: "Region Type",
description: "The type of region to analyze."
});
const regionParameter = new RegionParameter(catalogFunction, {
id: "regionParameter",
name: "Region Parameter",
regionProvider: regionTypeParameter
});
return new GeoJsonParameter(catalogFunction, {
...options,
regionParameter
});
},
parameterToInput: function (
parameter: FunctionParameter
): WpsInputData | undefined {
if (!isDefined(parameter.value) || parameter.value === null) {
return;
}
return (parameter as GeoJsonParameter).getProcessedValue(
(parameter as GeoJsonParameter).value!
);
}
};
function simpleGeoJsonDataConverter(schemaType: string, klass: any) {
return {
inputToParameter: function (
catalogFunction: CatalogFunctionMixin.Instance,
input: Input,
options: FunctionParameterOptions
) {
if (
!isDefined(input.ComplexData) ||
!isDefined(input.ComplexData.Default) ||
!isDefined(input.ComplexData.Default.Format) ||
!isDefined(input.ComplexData.Default.Format.Schema)
) {
return undefined;
}
const schema = input.ComplexData.Default.Format.Schema;
if (schema.indexOf("http://geojson.org/geojson-spec.html#") !== 0) {
return undefined;
}
if (schema.substring(schema.lastIndexOf("#") + 1) !== schemaType) {
return undefined;
}
return new klass(catalogFunction, options);
},
parameterToInput: function (parameter: FunctionParameter) {
return {
inputType: "ComplexData",
inputValue: klass.formatValueForUrl(parameter.value)
};
}
};
}
function parameterTypeToConverter(
parameter: FunctionParameter
): ParameterConverter | undefined {
switch (parameter.type) {
case BooleanParameter.type:
case StringParameter.type:
case EnumerationParameter.type:
return LiteralDataConverter;
case DateParameter.type:
return (parameter as DateParameter).variant === "literal"
? LiteralDataConverter
: ComplexDateConverter;
case DateTimeParameter.type:
return (parameter as DateTimeParameter).variant === "literal"
? LiteralDataConverter
: ComplexDateTimeConverter;
case PointParameter.type:
return PointConverter;
case LineParameter.type:
return LineConverter;
case PolygonParameter.type:
return PolygonConverter;
case RectangleParameter.type:
return RectangleConverter;
case GeoJsonParameter.type:
return GeoJsonGeometryConverter;
default:
break;
}
}
const parameterConverters: ParameterConverter[] = [
LiteralDataConverter,
ComplexDateConverter,
ComplexDateTimeConverter,
PointConverter,
LineConverter,
PolygonConverter,
RectangleConverter,
GeoJsonGeometryConverter
];
function throwInvalidWpsServerError(
wps: WebProcessingServiceCatalogFunction,
endpoint: string
) {
throw networkRequestError({
title: i18next.t("models.webProcessingService.invalidWPSServerTitle"),
message: i18next.t("models.webProcessingService.invalidWPSServerMessage", {
name: wps.name,
endpoint
})
});
}