UNPKG

terriajs

Version:

Geospatial data visualization platform.

375 lines 18.9 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 { computed, runInAction, makeObservable } from "mobx"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import RequestErrorEvent from "terriajs-cesium/Source/Core/RequestErrorEvent"; import URI from "urijs"; import AsyncLoader from "../Core/AsyncLoader"; import isDefined from "../Core/isDefined"; import loadBlob from "../Core/loadBlob"; import loadXML from "../Core/loadXML"; import Result from "../Core/Result"; import TerriaError, { networkRequestError } from "../Core/TerriaError"; import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl"; import ResultPendingCatalogItem from "../Models/Catalog/ResultPendingCatalogItem"; import CommonStrata from "../Models/Definition/CommonStrata"; import createStratumInstance from "../Models/Definition/createStratumInstance"; import LoadableStratum from "../Models/Definition/LoadableStratum"; import StratumOrder from "../Models/Definition/StratumOrder"; import UserDrawing from "../Models/UserDrawing"; import xml2json from "../ThirdParty/xml2json"; import { InfoSectionTraits } from "../Traits/TraitsClasses/CatalogMemberTraits"; import ExportWebCoverageServiceTraits, { WebCoverageServiceParameterTraits } from "../Traits/TraitsClasses/ExportWebCoverageServiceTraits"; import { getName } from "./CatalogMemberMixin"; import ExportableMixin from "./ExportableMixin"; import filterOutUndefined from "../Core/filterOutUndefined"; /** Call WCS GetCapabilities to get list of: * - available coverages * - available CRS * - available file formats * * Note: not currently used */ class WebCoverageServiceCapabilitiesStratum extends LoadableStratum(ExportWebCoverageServiceTraits) { catalogItem; capabilities; static stratumName = "wcsCapabilitiesStratum"; static async load(catalogItem) { if (!catalogItem.linkedWcsUrl) throw "`linkedWcsUrl` is undefined"; const url = new URI(catalogItem.linkedWcsUrl) .query({ service: "WCS", request: "GetCapabilities", version: "2.0.0" }) .toString(); const capabilitiesXml = await loadXML(proxyCatalogItemUrl(catalogItem, url)); const json = xml2json(capabilitiesXml); if (!isDefined(json.ServiceMetadata)) { throw networkRequestError({ title: "Invalid GetCapabilities", message: `The URL ${url} was retrieved successfully but it does not appear to be a valid Web Coverage Service (WCS) GetCapabilities document.` + `\n\nEither the catalog file has been set up incorrectly, or the server address has changed.` }); } const coverages = json.Contents?.CoverageSummary ?? []; const formats = json.ServiceMetadata?.formatSupported ?? []; const crs = json.ServiceMetadata?.Extension?.CrsMetadata?.crsSupported ?? []; return new WebCoverageServiceCapabilitiesStratum(catalogItem, { coverages, formats, crs }); } constructor(catalogItem, capabilities) { super(); this.catalogItem = catalogItem; this.capabilities = capabilities; } duplicateLoadableStratum(model) { return new WebCoverageServiceCapabilitiesStratum(model, this.capabilities); } } /** Call WCS DescribeCoverage for a specific coverageId to get: * - Native CRS * - Native format */ class WebCoverageServiceDescribeCoverageStratum extends LoadableStratum(ExportWebCoverageServiceTraits) { catalogItem; coverage; static stratumName = "wcsDescribeCoverageStratum"; static async load(catalogItem) { if (!catalogItem.linkedWcsUrl) throw "`linkedWcsUrl` is undefined"; if (!catalogItem.linkedWcsCoverage) throw "`linkedWcsCoverage` is undefined"; const url = new URI(catalogItem.linkedWcsUrl) .query({ service: "WCS", request: "DescribeCoverage", version: "2.0.0", coverageId: catalogItem.linkedWcsCoverage }) .toString(); const capabilitiesXml = await loadXML(proxyCatalogItemUrl(catalogItem, url)); const json = xml2json(capabilitiesXml); if (typeof json.CoverageDescription?.CoverageId !== "string") { throw networkRequestError({ title: "Invalid DescribeCoverage", message: `The URL ${url} was retrieved successfully but it does not appear to be a valid Web Coverage Service (WCS) DescribeCoverage document.` + `\n\nEither the catalog file has been set up incorrectly, or the server address has changed.` }); } const nativeFormat = json.CoverageDescription?.ServiceParameters?.nativeFormat; // Try get native CRS from domainSet and then boundedBy const nativeCrs = json.CoverageDescription?.domainSet?.Grid?.srsName ?? json.CoverageDescription?.boundedBy?.EnvelopeWithTimePeriod?.srsName ?? json.CoverageDescription?.boundedBy?.Envelope?.srsName; return new WebCoverageServiceDescribeCoverageStratum(catalogItem, { nativeFormat, nativeCrs }); } constructor(catalogItem, coverage) { super(); this.catalogItem = catalogItem; this.coverage = coverage; makeObservable(this); } get linkedWcsParameters() { return createStratumInstance(WebCoverageServiceParameterTraits, { outputCrs: this.coverage.nativeCrs, outputFormat: this.coverage.nativeFormat }); } duplicateLoadableStratum(model) { return new WebCoverageServiceDescribeCoverageStratum(model, this.coverage); } } __decorate([ computed ], WebCoverageServiceDescribeCoverageStratum.prototype, "linkedWcsParameters", null); function ExportWebCoverageServiceMixin(Base) { class ExportWebCoverageServiceMixin extends ExportableMixin(Base) { _wcsCapabilitiesLoader = new AsyncLoader(this.loadWcsCapabilities.bind(this)); _wcsDescribeCoverageLoader = new AsyncLoader(this.loadWcsDescribeCoverage.bind(this)); constructor(...args) { super(...args); makeObservable(this); } get isLoadingWcsMetadata() { return (this._wcsCapabilitiesLoader.isLoading || this._wcsDescribeCoverageLoader.isLoading); } async loadWcsMetadata(force) { const results = await Promise.all([ // Disable GetCapabilities loader until we need it // this._wcsCapabilitiesLoader.load(force), this._wcsDescribeCoverageLoader.load(force) ]); return Result.combine(results, { message: `Failed to load \`${getName(this)}\` WebCoverageService metadata`, importance: -1 }); } async loadWcsCapabilities() { const capabilities = await WebCoverageServiceCapabilitiesStratum.load(this); runInAction(() => this.strata.set(WebCoverageServiceCapabilitiesStratum.stratumName, capabilities)); } async loadWcsDescribeCoverage() { const describeCoverage = await WebCoverageServiceDescribeCoverageStratum.load(this); runInAction(() => this.strata.set(WebCoverageServiceDescribeCoverageStratum.stratumName, describeCoverage)); } // ExportableMixin overrides get _canExportData() { return isDefined(this.linkedWcsCoverage) && isDefined(this.linkedWcsUrl); } _exportData() { return new Promise((resolve, reject) => { const terria = this.terria; runInAction(() => (terria.pickedFeatures = undefined)); let rectangle; const userDrawing = new UserDrawing({ terria: this.terria, messageHeader: "Click two points to draw a retangle extent.", buttonText: "Download Extent", onPointClicked: () => { if (userDrawing.pointEntities.entities.values.length >= 2) { rectangle = userDrawing?.otherEntities?.entities ?.getById("rectangle") ?.rectangle?.coordinates?.getValue(this.terria.timelineClock.currentTime); } }, onCleanUp: async () => { if (isDefined(rectangle)) { if (!this.linkedWcsUrl || !this.linkedWcsCoverage) return; return this.downloadCoverage(rectangle) .then(resolve) .catch(reject); } else { reject("Invalid drawn extent."); } }, allowPolygon: false, drawRectangle: true }); userDrawing.enterDrawMode(); }); } /** Generate WCS GetCoverage URL */ getCoverageUrl(bbox) { try { let error = undefined; if (this.linkedWcsParameters.duplicateSubsetValues && this.linkedWcsParameters.duplicateSubsetValues.length > 0) { let message = `WebCoverageService (WCS) only supports one value per dimension.\n\n `; // Add message for each duplicate subset message += this.linkedWcsParameters.duplicateSubsetValues.map((subset) => `- Multiple dimension values have been set for \`${subset.key}\`. WCS GetCoverage request will use the first value (\`${subset.key} = "${subset.value}"\`).`); error = new TerriaError({ title: "Warning: export may not reflect displayed data", message, importance: 1 }); } // Make query parameter object const query = { service: "WCS", request: "GetCoverage", version: "2.0.0", coverageId: this.linkedWcsCoverage, format: this.linkedWcsParameters.outputFormat, // Add subsets for bbox, time and dimensions subset: [ `Long(${CesiumMath.toDegrees(bbox.west)},${CesiumMath.toDegrees(bbox.east)})`, `Lat(${CesiumMath.toDegrees(bbox.south)},${CesiumMath.toDegrees(bbox.north)})`, // Turn subsets into `key=(value)` format ...filterOutUndefined((this.linkedWcsParameters.subsets ?? []).map((subset) => subset.key && subset.value ? `${subset.key}(${ // Wrap string values in double quotes typeof subset.value === "string" ? `"${subset.value}"` : subset.value})` : undefined)) ], subsettingCrs: "EPSG:4326", outputCrs: this.linkedWcsParameters.outputCrs }; // Add linkedWcsParameters.additionalParameters ontop of query object Object.assign(query, (this.linkedWcsParameters.additionalParameters ?? []).reduce((q, current) => { if (typeof current.key === "string") { q[current.key] = current.value; } return q; }, {})); return new Result(new URI(this.linkedWcsUrl).query(query).toString(), error); } catch (e) { return Result.error(e); } } /** This function downloads WCS coverage for a given bbox (in radians) * It will also create a "pendingWorkbenchItem" with loading indicator and short description. */ async downloadCoverage(bbox) { // Create pending workbench item const now = new Date(); const timestamp = `${now.getFullYear().toString().padStart(4, "0")}-${(now.getMonth() + 1) .toString() .padStart(2, "0")}-${now.getDate().toString().padStart(2, "0")}T${now .getHours() .toString() .padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now .getSeconds() .toString() .padStart(2, "0")}`; const pendingWorkbenchItem = new ResultPendingCatalogItem(`WCS: ${getName(this)} ${timestamp}`, this.terria); try { runInAction(() => { pendingWorkbenchItem.loadPromise = new Promise(() => { }); pendingWorkbenchItem.loadMetadata(); // Add WCS loading metadata message to shortReport pendingWorkbenchItem.setTrait(CommonStrata.user, "shortReport", i18next.t("models.wcs.asyncResultLoadingMetadata", { name: getName(this), timestamp: timestamp })); }); pendingWorkbenchItem.terria.workbench.add(pendingWorkbenchItem); // Load WCS metadata (DescribeCoverage request) (await this.loadWcsMetadata()).throwIfError(); // Get WCS URL // This will throw an error if URL is undefined // It will raise an error if URL is defined, but an error has occurred const urlResult = this.getCoverageUrl(bbox); const url = urlResult.throwIfUndefined({ message: "Failed to generate WCS GetCoverage request URL", importance: 2 // Higher importance than error message in `getCoverageUrl()` }); urlResult.raiseError(this.terria, `Error occurred while generating WCS GetCoverage URL`); runInAction(() => { // Add WCS "pending" message to shortReport pendingWorkbenchItem.setTrait(CommonStrata.user, "shortReport", i18next.t("models.wcs.asyncPendingDescription", { name: getName(this), timestamp: timestamp })); // Create info section from URL query parameters const info = createStratumInstance(InfoSectionTraits, { name: "Inputs", content: `<table class="cesium-infoBox-defaultTable">${Object.entries(new URI(url).query(true)).reduce((previousValue, [key, value]) => `${previousValue}<tr><td style="vertical-align: middle">${key}</td><td>${value}</td></tr>`, "")}</table>` }); pendingWorkbenchItem.setTrait(CommonStrata.user, "info", [info]); }); const blob = await loadBlob(proxyCatalogItemUrl(this, url)); runInAction(() => pendingWorkbenchItem.terria.workbench.remove(pendingWorkbenchItem)); return { name: `${getName(this)} clip.tiff`, file: blob }; } catch (error) { if (error instanceof TerriaError) { throw error; } // Attempt to get error message out of XML response if (error instanceof RequestErrorEvent && isDefined(error?.response?.type) && error.response.type?.indexOf("xml") !== -1) { try { const xml = new DOMParser().parseFromString(await error.response.text(), "text/xml"); if (xml.documentElement.localName === "ServiceExceptionReport" || xml.documentElement.localName === "ExceptionReport") { const message = xml.getElementsByTagName("ServiceException")?.[0]?.innerHTML ?? xml.getElementsByTagName("ows:ExceptionText")?.[0]?.innerHTML; if (isDefined(message)) { /* eslint-disable-next-line no-ex-assign */ error = message; } } } catch (xmlParseError) { console.log("Failed to parse WCS response"); console.log(xmlParseError); } } throw new TerriaError({ sender: this, title: i18next.t("models.wcs.exportFailedTitle"), message: i18next.t("models.wcs.exportFailedMessageII", { error }) }); } finally { runInAction(() => pendingWorkbenchItem.terria.workbench.remove(pendingWorkbenchItem)); } } dispose() { super.dispose(); this._wcsCapabilitiesLoader.dispose(); this._wcsDescribeCoverageLoader.dispose(); } } __decorate([ computed ], ExportWebCoverageServiceMixin.prototype, "isLoadingWcsMetadata", null); __decorate([ computed ], ExportWebCoverageServiceMixin.prototype, "_canExportData", null); return ExportWebCoverageServiceMixin; } (function (ExportWebCoverageServiceMixin) { function isMixedInto(model) { return (model && "loadWcsMetadata" in model && typeof model.loadWcsMetadata === "function"); } ExportWebCoverageServiceMixin.isMixedInto = isMixedInto; StratumOrder.addLoadStratum(WebCoverageServiceCapabilitiesStratum.stratumName); StratumOrder.addLoadStratum(WebCoverageServiceDescribeCoverageStratum.stratumName); })(ExportWebCoverageServiceMixin || (ExportWebCoverageServiceMixin = {})); export default ExportWebCoverageServiceMixin; //# sourceMappingURL=ExportWebCoverageServiceMixin.js.map