UNPKG

terriajs

Version:

Geospatial data visualization platform.

595 lines 24.8 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import i18next from "i18next"; import { action, computed, makeObservable, override, runInAction } from "mobx"; import Mustache from "mustache"; import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import TerriaError from "../../../Core/TerriaError"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import isDefined from "../../../Core/isDefined"; import loadWithXhr from "../../../Core/loadWithXhr"; import TableMixin from "../../../ModelMixins/TableMixin"; import TableAutomaticStylesStratum from "../../../Table/TableAutomaticStylesStratum"; import TableColumnType from "../../../Table/TableColumnType"; import xml2json from "../../../ThirdParty/xml2json"; import SensorObservationServiceCatalogItemTraits from "../../../Traits/TraitsClasses/SensorObservationCatalogItemTraits"; import TableChartStyleTraits, { TableChartLineStyleTraits } from "../../../Traits/TraitsClasses/Table/ChartStyleTraits"; import TablePointSizeStyleTraits from "../../../Traits/TraitsClasses/Table/PointSizeStyleTraits"; import TableStyleTraits from "../../../Traits/TraitsClasses/Table/StyleTraits"; import CommonStrata from "../../Definition/CommonStrata"; import CreateModel from "../../Definition/CreateModel"; import StratumOrder from "../../Definition/StratumOrder"; import createStratumInstance from "../../Definition/createStratumInstance"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import defaultRequestTemplate from "./SensorObservationServiceRequestTemplate.xml"; StratumOrder.addLoadStratum(TableAutomaticStylesStratum.stratumName); class SosAutomaticStylesStratum extends TableAutomaticStylesStratum { catalogItem; constructor(catalogItem) { super(catalogItem); this.catalogItem = catalogItem; makeObservable(this); } duplicateLoadableStratum(newModel) { return new SosAutomaticStylesStratum(newModel); } get activeStyle() { return this.catalogItem.procedures[0]?.identifier; } get styles() { return this.catalogItem.procedures.map((p) => { return createStratumInstance(TableStyleTraits, { id: p.identifier, title: p.title, pointSize: createStratumInstance(TablePointSizeStyleTraits, { pointSizeColumn: p.identifier }), // table style is hidden by default when the table uses only 1 color (https://github.com/TerriaJS/terriajs/blob/bbe8a11ae9bf6c0eb78c52d7b5c9b260d5ddc8cf/lib/Table/TableStyle.ts#L82) // force hidden to false so that the frequency and procedure selector will always be shown // Ideally we should rewrite frequency & procedure selector using selectable dimensions and stop using styles to display them. hidden: false }); }); } get defaultChartStyle() { const timeColumn = this.catalogItem.tableColumns.find((column) => column.type === TableColumnType.time); const valueColumn = this.catalogItem.tableColumns.find((column) => column.type === TableColumnType.scalar); if (timeColumn && valueColumn) { return createStratumInstance(TableStyleTraits, { chart: createStratumInstance(TableChartStyleTraits, { xAxisColumn: timeColumn.name, lines: [ createStratumInstance(TableChartLineStyleTraits, { yAxisColumn: valueColumn.name }) ] }) }); } } } __decorate([ override ], SosAutomaticStylesStratum.prototype, "activeStyle", null); __decorate([ override ], SosAutomaticStylesStratum.prototype, "styles", null); __decorate([ override ], SosAutomaticStylesStratum.prototype, "defaultChartStyle", null); class GetFeatureOfInterestRequest { catalogItem; requestTemplate; constructor(catalogItem, requestTemplate) { this.catalogItem = catalogItem; this.requestTemplate = requestTemplate; makeObservable(this); } get url() { return this.catalogItem.url; } get observedProperties() { return filterOutUndefined(this.catalogItem.observableProperties.map((p) => p.identifier)); } get procedures() { if (this.catalogItem.filterByProcedures) { return filterOutUndefined(this.catalogItem.procedures.map((p) => p.identifier)); } } async perform() { if (this.url === undefined) { return; } const templateContext = { action: "GetFeatureOfInterest", actionClass: "foiRetrieval", parameters: convertObjectToNameValueArray({ observedProperty: this.observedProperties, procedure: this.procedures }) }; const response = await loadSoapBody(this.catalogItem, this.url, this.requestTemplate, templateContext); return response?.GetFeatureOfInterestResponse; } } __decorate([ computed ], GetFeatureOfInterestRequest.prototype, "url", null); __decorate([ computed ], GetFeatureOfInterestRequest.prototype, "observedProperties", null); __decorate([ computed ], GetFeatureOfInterestRequest.prototype, "procedures", null); class GetObservationRequest { catalogItem; foiIdentifier; constructor(catalogItem, foiIdentifier) { this.catalogItem = catalogItem; this.foiIdentifier = foiIdentifier; makeObservable(this); } get url() { return this.catalogItem.url; } get requestTemplate() { return (this.catalogItem.requestTemplate || SensorObservationServiceCatalogItem.defaultRequestTemplate); } get parameters() { const foiIdentifier = this.catalogItem.chartFeatureOfInterestIdentifier; const observableProperty = this.catalogItem.selectedObservable; const procedure = this.catalogItem.selectedProcedure; if (foiIdentifier === undefined || procedure === undefined || observableProperty === undefined) { return; } return convertObjectToNameValueArray({ procedure: procedure.identifier, observedProperty: observableProperty.identifier, featureOfInterest: foiIdentifier }); } /** * Return the Mustache template context "temporalFilters" for this item. * If a "defaultDuration" parameter (eg. 60d or 12h) exists on either * procedure or observableProperty, restrict to that duration from item.endDate. * @param item This catalog item. * @param procedure An element from the item.procedures array. * @param observableProperty An element from the item.observableProperties array. * @return An array of {index, startDate, endDate}, or undefined. */ get temporalFilters() { const observableProperty = this.catalogItem.selectedObservable; const procedure = this.catalogItem.selectedProcedure; if (procedure === undefined || observableProperty === undefined) { return; } const defaultDuration = procedure.defaultDuration || observableProperty.defaultDuration; // If the item has no endDate, use the current datetime (to nearest second). const endDateIso8601 = this.catalogItem.endDate || JulianDate.toIso8601(JulianDate.now(), 0); if (defaultDuration) { let startDateIso8601 = addDurationToIso8601(endDateIso8601, "-" + defaultDuration); // This is just a string-based comparison, so timezones could make it up to 1 day wrong. // That much error is fine here. if (this.catalogItem.startDate && startDateIso8601 < this.catalogItem.startDate) { startDateIso8601 = this.catalogItem.startDate; } return [ { index: 1, startDate: startDateIso8601, endDate: endDateIso8601 } ]; } else { // If there is no procedure- or property-specific duration, use the item's start and end dates, if any. if (this.catalogItem.startDate) { return [ { index: 1, startDate: this.catalogItem.startDate, endDate: endDateIso8601 } ]; } } } async perform() { if (this.url === undefined || this.parameters === undefined) { return; } const templateContext = { action: "GetObservation", actionClass: "core", parameters: this.parameters, temporalFilters: this.temporalFilters }; const response = await loadSoapBody(this.catalogItem, this.url, this.requestTemplate, templateContext); return response?.GetObservationResponse; } } __decorate([ computed ], GetObservationRequest.prototype, "url", null); __decorate([ computed ], GetObservationRequest.prototype, "requestTemplate", null); __decorate([ computed ], GetObservationRequest.prototype, "parameters", null); __decorate([ computed ], GetObservationRequest.prototype, "temporalFilters", null); export default class SensorObservationServiceCatalogItem extends TableMixin(CreateModel(SensorObservationServiceCatalogItemTraits)) { static type = "sos"; static defaultRequestTemplate = defaultRequestTemplate; constructor(id, terria, sourceReference) { super(id, terria, sourceReference); makeObservable(this); this.strata.set(TableAutomaticStylesStratum.stratumName, new SosAutomaticStylesStratum(this)); } get type() { return "sos"; } async forceLoadMetadata() { } async forceLoadTableData() { if (this.showAsChart === true) { return this.loadChartData(); } else { return this.loadFeaturesData(); } } get cacheDuration() { if (isDefined(super.cacheDuration)) { return super.cacheDuration; } return "0d"; } async loadFeaturesData() { const request = new GetFeatureOfInterestRequest(this, this.requestTemplate || SensorObservationServiceCatalogItem.defaultRequestTemplate); const response = await request.perform(); if (response === undefined) { return []; } const itemName = runInAction(() => this.name || ""); if (response.featureMember === undefined) { throw new TerriaError({ sender: this, title: itemName, message: i18next.t("models.sensorObservationService.unknownFormat") }); } let featureMembers = Array.isArray(response.featureMember) ? response.featureMember : [response.featureMember]; const whiteList = runInAction(() => this.stationIdWhitelist); if (whiteList) { featureMembers = featureMembers.filter((m) => m.MonitoringPoint?.identifier && whiteList.indexOf(String(m.MonitoringPoint.identifier)) >= 0); } const blackList = runInAction(() => this.stationIdBlacklist); if (blackList) { featureMembers = featureMembers.filter((m) => m.MonitoringPoint && blackList.indexOf(String(m.MonitoringPoint.identifier)) < 0); } const identifierCols = ["identifier"]; const latCols = ["lat"]; const lonCols = ["lon"]; const nameCols = ["name"]; const idCols = ["id"]; const typeCols = ["type"]; const chartCols = ["chart"]; featureMembers.forEach((member) => { const pointShape = member.MonitoringPoint?.shape?.Point; if (!pointShape) { throw new DeveloperError("Non-point feature not shown. You may want to implement `representAsGeoJson`. " + JSON.stringify(pointShape)); } if (!member.MonitoringPoint) return; if (!pointShape.pos?.split) return; if (!member.MonitoringPoint.identifier) return; const [lat, lon] = pointShape.pos.split(" "); const identifier = member.MonitoringPoint.identifier; const name = member.MonitoringPoint.name; const id = member.MonitoringPoint["gml:id"]; const type = member.MonitoringPoint.type?.["xlink:href"]; const chart = createChartColumn(identifier, name); identifierCols.push(identifier); latCols.push(lat); lonCols.push(lon); nameCols.push(name || ""); idCols.push(id || ""); typeCols.push(type || ""); chartCols.push(chart); }); return [ identifierCols, latCols, lonCols, nameCols, idCols, typeCols, chartCols ]; } async loadChartData() { const foiIdentifier = this.chartFeatureOfInterestIdentifier; if (foiIdentifier === undefined) { return []; } const request = new GetObservationRequest(this, foiIdentifier); const response = await request.perform(); if (response === undefined) { return []; } return runInAction(() => { const procedure = this.selectedProcedure; const observableProperty = this.selectedObservable; const datesCol = ["date"]; const valuesCol = ["values"]; const observationsCol = ["observations"]; const identifiersCol = ["identifiers"]; const proceduresCol = [this.proceduresName]; const observedPropertiesCol = [this.observablePropertiesName]; const addObservationToColumns = (observation) => { let points = observation?.result?.MeasurementTimeseries?.point; if (!points) return; if (!Array.isArray(points)) points = [points]; const measurements = points.map((point) => point.MeasurementTVP); // TVP = Time value pairs, I think. const featureIdentifier = observation.featureOfInterest["xlink:href"] || ""; datesCol.push(...measurements.map((measurement) => typeof measurement.time === "object" ? "" : measurement.time)); valuesCol.push(...measurements.map((measurement) => typeof measurement.value === "object" ? "" : measurement.value)); identifiersCol.push(...measurements.map((_) => featureIdentifier)); proceduresCol.push(...measurements.map((_) => procedure.identifier || "")); observedPropertiesCol.push(...measurements.map((_) => observableProperty.identifier || "")); }; const observationData = response.observationData === undefined || Array.isArray(response.observationData) ? response.observationData : [response.observationData]; if (!observationData) { return []; } const observations = observationData.map((o) => o.OM_Observation); observations.forEach((observation) => { if (observation) { addObservationToColumns(observation); } }); runInAction(() => { // Set title for values column const valueColumn = this.addObject(CommonStrata.defaults, "columns", "values"); valueColumn?.setTrait(CommonStrata.defaults, "name", "values"); valueColumn?.setTrait(CommonStrata.defaults, "title", this.valueTitle); }); return [ datesCol, valuesCol, observationsCol, identifiersCol, proceduresCol, observedPropertiesCol ]; }); } get valueTitle() { if (this.selectedObservable === undefined || this.selectedProcedure === undefined) { return; } const units = this.selectedObservable.units || this.selectedProcedure.units; const valueTitle = this.selectedObservable.title + " " + this.selectedProcedure.title + (units !== undefined ? " (" + units + ")" : ""); return valueTitle; } get selectableDimensions() { return filterOutUndefined([ // Filter out proceduresSelector - as it duplicates TableMixin.styleDimensions ...super.selectableDimensions.filter((dim) => dim.id !== this.proceduresSelector?.id), this.proceduresSelector, this.observablesSelector ]); } get proceduresSelector() { const proceduresSelector = super.styleDimensions; if (proceduresSelector === undefined) return; const item = this; return { ...proceduresSelector, get name() { return item.proceduresName; } }; } get observablesSelector() { if (this.mapItems.length === 0) { return; } const item = this; return { get id() { return "observables"; }, get name() { return item.observablePropertiesName; }, get options() { return filterOutUndefined(item.observableProperties.map((p) => { if (p.identifier && p.title) { return { id: p.identifier, name: p.title || p.identifier }; } })); }, get selectedId() { return item.selectedObservableId; }, setDimensionValue(stratumId, observableId) { item.setTrait(stratumId, "selectedObservableId", observableId); } }; } get selectedObservableId() { return (super.selectedObservableId || this.observableProperties[0]?.identifier); } get selectedObservable() { return this.observableProperties.find((p) => p.identifier === this.selectedObservableId); } get selectedProcedure() { return this.procedures.find((p) => p.identifier === this.activeTableStyle.id); } } __decorate([ action ], SensorObservationServiceCatalogItem.prototype, "forceLoadTableData", null); __decorate([ override ], SensorObservationServiceCatalogItem.prototype, "cacheDuration", null); __decorate([ action ], SensorObservationServiceCatalogItem.prototype, "loadFeaturesData", null); __decorate([ action ], SensorObservationServiceCatalogItem.prototype, "loadChartData", null); __decorate([ computed ], SensorObservationServiceCatalogItem.prototype, "valueTitle", null); __decorate([ override ], SensorObservationServiceCatalogItem.prototype, "selectableDimensions", null); __decorate([ computed ], SensorObservationServiceCatalogItem.prototype, "proceduresSelector", null); __decorate([ computed ], SensorObservationServiceCatalogItem.prototype, "observablesSelector", null); __decorate([ override ], SensorObservationServiceCatalogItem.prototype, "selectedObservableId", null); __decorate([ computed ], SensorObservationServiceCatalogItem.prototype, "selectedObservable", null); __decorate([ computed ], SensorObservationServiceCatalogItem.prototype, "selectedProcedure", null); function createChartColumn(identifier, name) { const nameAttr = name === undefined ? "" : `name="${name}"`; // The API that provides the chart data is a SOAP API, and the download button is essentially just a link, so when you click it you get an error page. // can-download="false" will disable this broken download button. return `<sos-chart identifier="${identifier}" ${nameAttr} can-download="false"></sos-chart>`; } async function loadSoapBody(item, url, requestTemplate, templateContext) { const requestXml = Mustache.render(requestTemplate, templateContext); const responseXml = await loadWithXhr({ url: proxyCatalogItemUrl(item, url), responseType: "document", method: "POST", overrideMimeType: "text/xml", data: requestXml, headers: { "Content-Type": "application/soap+xml" } }); if (responseXml === undefined) { return; } const json = xml2json(responseXml); if (json.Exception) { let errorMessage = i18next.t("models.sensorObservationService.unknownError"); if (json.Exception.ExceptionText) { errorMessage = i18next.t("models.sensorObservationService.exceptionMessage", { exceptionText: json.Exception.ExceptionText }); } throw new TerriaError({ sender: item, title: runInAction(() => item.name || ""), message: errorMessage }); } if (json.Body === undefined) { throw new TerriaError({ sender: item, title: runInAction(() => item.name || ""), message: i18next.t("models.sensorObservationService.missingBody") }); } return json.Body; } /** * Adds a period to an iso8601-formatted date. * Periods must be (positive or negative) numbers followed by a letter: * s (seconds), h (hours), d (days), y (years). * To avoid confusion between minutes and months, do not use m. * @param dateIso8601 The date in ISO8601 format. * @param durationString The duration string, in the format described. * @return A date string in ISO8601 format. */ function addDurationToIso8601(dateIso8601, durationString) { const duration = parseFloat(durationString); if (isNaN(duration) || duration === 0) { throw new DeveloperError("Bad duration " + durationString); } const scratchJulianDate = new JulianDate(); let julianDate = JulianDate.fromIso8601(dateIso8601); const units = durationString.slice(durationString.length - 1); switch (units) { case "s": julianDate = JulianDate.addSeconds(julianDate, duration, scratchJulianDate); break; case "h": julianDate = JulianDate.addHours(julianDate, duration, scratchJulianDate); break; case "d": // Use addHours on 24 * numdays - on my casual reading of addDays, it needs an integer. julianDate = JulianDate.addHours(julianDate, duration * 24, scratchJulianDate); break; case "y": { const days = Math.round(duration * 365); julianDate = JulianDate.addDays(julianDate, days, scratchJulianDate); break; } default: throw new DeveloperError('Unknown duration type "' + durationString + '" (use s, h, d or y)'); } return JulianDate.toIso8601(julianDate); } /** * Converts parameters {x: 'y'} into an array of {name: 'x', value: 'y'} objects. * Converts {x: [1, 2, ...]} into multiple objects: * {name: 'x', value: 1}, {name: 'x', value: 2}, ... * @param parameters eg. {a: 3, b: [6, 8]} * @return eg. [{name: 'a', value: 3}, {name: 'b', value: 6}, {name: 'b', value: 8}] */ function convertObjectToNameValueArray(parameters) { return Object.keys(parameters).reduce((result, key) => { let values = parameters[key]; if (!Array.isArray(values)) { values = [values]; } if (values.length === 0) return result; return result.concat(filterOutUndefined(values.map((value) => { return value === undefined ? undefined : { name: key, value: value }; }))); }, []); } //# sourceMappingURL=SensorObservationServiceCatalogItem.js.map