UNPKG

terriajs

Version:

Geospatial data visualization platform.

359 lines 17 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, makeObservable, override, runInAction } from "mobx"; import combine from "terriajs-cesium/Source/Core/combine"; import { toFeatureCollection } from "../../../Core/GeoJson"; import TerriaError from "../../../Core/TerriaError"; import containsAny from "../../../Core/containsAny"; import isDefined from "../../../Core/isDefined"; import isReadOnlyArray from "../../../Core/isReadOnlyArray"; import loadText from "../../../Core/loadText"; import gmlToGeoJson from "../../../Map/Vector/gmlToGeoJson"; import { getName } from "../../../ModelMixins/CatalogMemberMixin"; import GeoJsonMixin from "../../../ModelMixins/GeojsonMixin"; import GetCapabilitiesMixin from "../../../ModelMixins/GetCapabilitiesMixin"; import xml2json from "../../../ThirdParty/xml2json"; import { InfoSectionTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits"; import WebFeatureServiceCatalogItemTraits, { SUPPORTED_CRS_3857, SUPPORTED_CRS_4326 } from "../../../Traits/TraitsClasses/WebFeatureServiceCatalogItemTraits"; import CreateModel from "../../Definition/CreateModel"; import LoadableStratum from "../../Definition/LoadableStratum"; import createStratumInstance from "../../Definition/createStratumInstance"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import WebFeatureServiceCapabilities, { getRectangleFromLayer } from "./WebFeatureServiceCapabilities"; export class GetCapabilitiesStratum extends LoadableStratum(WebFeatureServiceCatalogItemTraits) { catalogItem; capabilities; static async load(catalogItem, capabilities) { if (!isDefined(catalogItem.getCapabilitiesUrl)) { throw new TerriaError({ title: i18next.t("models.webFeatureServiceCatalogItem.missingUrlTitle"), message: i18next.t("models.webFeatureServiceCatalogItem.missingUrlMessage") }); } if (!isDefined(capabilities)) capabilities = await WebFeatureServiceCapabilities.fromUrl(proxyCatalogItemUrl(catalogItem, catalogItem.getCapabilitiesUrl, catalogItem.getCapabilitiesCacheDuration)); return new GetCapabilitiesStratum(catalogItem, capabilities); } constructor(catalogItem, capabilities) { super(); this.catalogItem = catalogItem; this.capabilities = capabilities; makeObservable(this); } duplicateLoadableStratum(model) { return new GetCapabilitiesStratum(model, this.capabilities); } get capabilitiesFeatureTypes() { const lookup = (name) => [name, this.capabilities && this.capabilities.findLayer(name)]; return new Map(this.catalogItem.typeNamesArray.map(lookup)); } get info() { const result = [ createStratumInstance(InfoSectionTraits, { name: i18next.t("models.webFeatureServiceCatalogItem.getCapabilitiesUrl"), content: this.catalogItem.getCapabilitiesUrl }) ]; let firstDataDescription; for (const layer of this.capabilitiesFeatureTypes.values()) { if (!layer || !layer.Abstract || containsAny(layer.Abstract, WebFeatureServiceCatalogItem.abstractsToIgnore)) { continue; } const suffix = this.capabilitiesFeatureTypes.size === 1 ? "" : ` - ${layer.Title}`; const name = `${i18next.t("models.webFeatureServiceCatalogItem.abstract")}${suffix}`; result.push(createStratumInstance(InfoSectionTraits, { name, content: layer.Abstract })); firstDataDescription = firstDataDescription || layer.Abstract; } // Show the service abstract if there is one and if it isn't the Geoserver default "A compliant implementation..." const service = this.capabilities && this.capabilities.service; if (service) { if (service && service.Abstract && !containsAny(service.Abstract, WebFeatureServiceCatalogItem.abstractsToIgnore) && service.Abstract !== firstDataDescription) { result.push(createStratumInstance(InfoSectionTraits, { name: i18next.t("models.webFeatureServiceCatalogItem.abstract"), content: service.Abstract })); } // Show the Access Constraints if it isn't "none" (because that's the default, and usually a lie). if (service.AccessConstraints && !/^none$/i.test(service.AccessConstraints)) { result.push(createStratumInstance(InfoSectionTraits, { name: i18next.t("models.webFeatureServiceCatalogItem.accessConstraints"), content: service.AccessConstraints })); } } return result; } get infoSectionOrder() { let layerDescriptions = [ i18next.t("models.webFeatureServiceCatalogItem.abstract") ]; // If more than one layer, push layer description titles for each applicable layer if (this.capabilitiesFeatureTypes.size > 1) { layerDescriptions = []; this.capabilitiesFeatureTypes.forEach((layer) => { if (layer && layer.Abstract && !containsAny(layer.Abstract, WebFeatureServiceCatalogItem.abstractsToIgnore)) { layerDescriptions.push(`${i18next.t("models.webFeatureServiceCatalogItem.abstract")} - ${layer.Title}`); } }); } return [ i18next.t("preview.disclaimer"), i18next.t("description.name"), ...layerDescriptions, i18next.t("preview.datasetDescription"), i18next.t("preview.serviceDescription"), i18next.t("models.webFeatureServiceCatalogItem.serviceDescription"), i18next.t("preview.resourceDescription"), i18next.t("preview.licence"), i18next.t("preview.accessConstraints"), i18next.t("models.webFeatureServiceCatalogItem.accessConstraints"), i18next.t("preview.author"), i18next.t("preview.contact"), i18next.t("models.webFeatureServiceCatalogItem.serviceContact"), i18next.t("preview.created"), i18next.t("preview.modified"), i18next.t("preview.updateFrequency"), i18next.t("models.webFeatureServiceCatalogItem.getCapabilitiesUrl") ]; } get rectangle() { const layers = [ ...this.capabilitiesFeatureTypes.values() ].filter(isDefined); // Only return first layer's rectangle - as we don't support multiple WFS layers return layers.length > 0 ? getRectangleFromLayer(layers[0]) : undefined; } get isGeoServer() { if (!this.capabilities) { return undefined; } if (!this.capabilities.service || !this.capabilities.service.KeywordList || !this.capabilities.service.KeywordList.Keyword) { return false; } const keyword = this.capabilities.service.KeywordList.Keyword; if (isReadOnlyArray(keyword)) { return keyword.indexOf("GEOSERVER") >= 0; } else { return keyword === "GEOSERVER"; } } // Helper function to check if geojson output is supported (by checking GetCapabilities OutputTypes OR FeatureType OutputTypes) hasJsonOutputFormat = (outputFormats) => { return isDefined(outputFormats?.find((format) => ["json", "JSON", "application/json"].includes(format))); }; // Find which GML formats are supported, choose the one most suited to Terria. If not available, default to "gml3" get outputFormat() { const supportsGeojson = this.hasJsonOutputFormat(this.capabilities.outputTypes) || [...this.capabilitiesFeatureTypes.values()].reduce((hasGeojson, current) => hasGeojson && this.hasJsonOutputFormat(current?.OutputFormats), true); const searchValue = new RegExp(".*gml/3.1.1.*|.*gml3.1.1.*"); return supportsGeojson ? "JSON" : (this.capabilities.outputTypes?.find((outputFormat) => searchValue.test(outputFormat)) ?? "gml3"); } /** Finds the best srsName to use. * First checks if one provided in url. If one is provided in url, and this is supported by Terria, will use this. * Note that an error will be thrown if user supplied srsName is not supported by the user supplied WFS service. * If no srsName provided, or the provided one is not supported by Terria, * then checks getCapabilities response and returns the first listed srs that is included in our list of supported srs. * This enables us to use a urn identifier if supported, or a normal EPSG code if not. * e.g. "urn:ogc:def:crs:EPSG::4326" or "EPSG:4326" **/ get srsName() { // First check to see if URL has CRS or SRS const supportedCrs = [...SUPPORTED_CRS_3857, ...SUPPORTED_CRS_4326]; const queryParams = this.catalogItem.uri?.query(true) ?? {}; const urlCrs = queryParams.srsName ?? queryParams.crs ?? queryParams.CRS ?? queryParams.srs ?? queryParams.SRS; if (urlCrs && supportedCrs.includes(urlCrs)) return urlCrs; // If no srsName provided, then find what the server supports and use the best one for Terria const layerSrsArray = this.capabilities.srsNames?.find((layer) => layer.layerName === this.catalogItem.typeNamesArray[0] //If multiple layers in this WFS request, only use the first layer to find best srsName ); return (layerSrsArray?.srsArray.find((srsName) => SUPPORTED_CRS_4326.includes(srsName)) ?? "urn:ogc:def:crs:EPSG::4326" // Default to urn identifier for WGS84 if we cant find something better. Sometimes WFS service will support this even if not specified in GetCapabilities response. ); } } __decorate([ computed ], GetCapabilitiesStratum.prototype, "capabilitiesFeatureTypes", null); __decorate([ computed ], GetCapabilitiesStratum.prototype, "info", null); __decorate([ computed ], GetCapabilitiesStratum.prototype, "infoSectionOrder", null); __decorate([ computed ], GetCapabilitiesStratum.prototype, "rectangle", null); __decorate([ computed ], GetCapabilitiesStratum.prototype, "isGeoServer", null); __decorate([ computed ], GetCapabilitiesStratum.prototype, "outputFormat", null); __decorate([ computed ], GetCapabilitiesStratum.prototype, "srsName", null); class WebFeatureServiceCatalogItem extends GetCapabilitiesMixin(GeoJsonMixin(CreateModel(WebFeatureServiceCatalogItemTraits))) { /** * The collection of strings that indicate an Abstract property should be ignored. If these strings occur anywhere * in the Abstract, the Abstract will not be used. This makes it easy to filter out placeholder data like * Geoserver's "A compliant implementation of WFS..." stock abstract. */ static abstractsToIgnore = [ "A compliant implementation of WFS", "This is the reference implementation of WFS 1.0.0 and WFS 1.1.0, supports all WFS operations including Transaction." ]; // hide elements in the info section which might show information about the datasource _sourceInfoItemNames = [ i18next.t("models.webFeatureServiceCatalogItem.getCapabilitiesUrl") ]; static type = "wfs"; constructor(...args) { super(...args); makeObservable(this); } get type() { return WebFeatureServiceCatalogItem.type; } get defaultGetCapabilitiesUrl() { if (this.uri) { return this.uri .clone() .setSearch({ service: "WFS", version: "1.1.0", request: "GetCapabilities" }) .toString(); } else { return undefined; } } async createGetCapabilitiesStratumFromParent(capabilities) { const stratum = await GetCapabilitiesStratum.load(this, capabilities); runInAction(() => { this.strata.set(GetCapabilitiesMixin.getCapabilitiesStratumName, stratum); }); } async forceLoadMetadata() { if (this.strata.get(GetCapabilitiesMixin.getCapabilitiesStratumName) !== undefined) return; const stratum = await GetCapabilitiesStratum.load(this); runInAction(() => { this.strata.set(GetCapabilitiesMixin.getCapabilitiesStratumName, stratum); }); } async forceLoadGeojsonData() { const getCapabilitiesStratum = this.strata.get(GetCapabilitiesMixin.getCapabilitiesStratumName); if (!this.uri) { throw new TerriaError({ sender: this, title: i18next.t("models.webFeatureServiceCatalogItem.missingUrlTitle"), message: i18next.t("models.webFeatureServiceCatalogItem.missingUrlMessage", this) }); } // Check if layers exist const missingLayers = this.typeNamesArray.filter((layer) => !isDefined(getCapabilitiesStratum.capabilitiesFeatureTypes.get(layer))); if (missingLayers.length > 0) { throw new TerriaError({ sender: this, title: i18next.t("models.webFeatureServiceCatalogItem.noLayerFoundTitle"), message: i18next.t("models.webFeatureServiceCatalogItem.noLayerFoundMessage", { name: getName(this), typeNames: missingLayers.join(", ") }) }); } const url = this.uri .clone() .setSearch(combine({ service: "WFS", request: "GetFeature", typeName: this.typeNames, version: "1.1.0", outputFormat: this.outputFormat, srsName: this.srsName, maxFeatures: this.maxFeatures }, this.parameters)) .toString(); const getFeatureResponse = await loadText(proxyCatalogItemUrl(this, url)); // Check for errors (if supportsGeojson and the request returns XML, OR the response includes ExceptionReport) if ((this.outputFormat === "JSON" && getFeatureResponse.startsWith("<")) || getFeatureResponse.includes("ExceptionReport")) { let errorMessage; try { errorMessage = xml2json(getFeatureResponse).Exception?.ExceptionText; } catch { } const originalError = isDefined(errorMessage) ? new TerriaError({ sender: this, title: "Exception from service", message: errorMessage }) : undefined; throw new TerriaError({ sender: this, title: i18next.t("models.webFeatureServiceCatalogItem.missingDataTitle"), message: `${i18next.t("models.webFeatureServiceCatalogItem.missingDataMessage", { name: getName(this) })}`, originalError }); } const geojsonData = this.outputFormat === "JSON" ? JSON.parse(getFeatureResponse) : gmlToGeoJson(getFeatureResponse); const fc = toFeatureCollection(geojsonData); if (fc) return fc; throw TerriaError.from("Invalid geojson data - only FeatureCollection and Feature are supported"); } get typeNamesArray() { if (Array.isArray(this.typeNames)) { return this.typeNames; } else if (this.typeNames) { return this.typeNames.split(","); } else { return []; } } get shortReport() { // Show notice if reached if (this.readyData?.features !== undefined && this.readyData.features.length >= this.maxFeatures) { return i18next.t("models.webFeatureServiceCatalogItem.reachedMaxFeatureLimit", this); } return undefined; } } __decorate([ computed ], WebFeatureServiceCatalogItem.prototype, "typeNamesArray", null); __decorate([ override ], WebFeatureServiceCatalogItem.prototype, "shortReport", null); export default WebFeatureServiceCatalogItem; //# sourceMappingURL=WebFeatureServiceCatalogItem.js.map