UNPKG

terriajs

Version:

Geospatial data visualization platform.

1,018 lines (891 loc) 33.8 kB
import i18next from "i18next"; import { computed, makeObservable } from "mobx"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import URI from "urijs"; import { JsonObject, isJsonArray, isJsonString } from "../../../Core/Json"; import TerriaError from "../../../Core/TerriaError"; import containsAny from "../../../Core/containsAny"; import createDiscreteTimesFromIsoSegments from "../../../Core/createDiscreteTimes"; import filterOutUndefined from "../../../Core/filterOutUndefined"; import isDefined from "../../../Core/isDefined"; import isReadOnlyArray from "../../../Core/isReadOnlyArray"; import { terriaTheme } from "../../../ReactViews/StandardUserInterface/StandardTheme"; import { InfoSectionTraits, MetadataUrlTraits } from "../../../Traits/TraitsClasses/CatalogMemberTraits"; import { KeyValueTraits, WebCoverageServiceParameterTraits } from "../../../Traits/TraitsClasses/ExportWebCoverageServiceTraits"; import { FeatureInfoTemplateTraits } from "../../../Traits/TraitsClasses/FeatureInfoTraits"; import LegendTraits from "../../../Traits/TraitsClasses/LegendTraits"; import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits"; import WebMapServiceCatalogItemTraits, { GetFeatureInfoFormat, SUPPORTED_CRS_3857, SUPPORTED_CRS_4326, WebMapServiceAvailableLayerDimensionsTraits, WebMapServiceAvailableLayerStylesTraits, WebMapServiceAvailableStyleTraits } from "../../../Traits/TraitsClasses/WebMapServiceCatalogItemTraits"; import LoadableStratum from "../../Definition/LoadableStratum"; import Model, { BaseModel } from "../../Definition/Model"; import StratumFromTraits from "../../Definition/StratumFromTraits"; import createStratumInstance from "../../Definition/createStratumInstance"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import { CapabilitiesStyle } from "./OwsInterfaces"; import WebMapServiceCapabilities, { CapabilitiesContactInformation, CapabilitiesDimension, CapabilitiesLayer, MetadataURL, getRectangleFromLayer } from "./WebMapServiceCapabilities"; import WebMapServiceCatalogItem from "./WebMapServiceCatalogItem"; import dateFormat from "dateformat"; /** Transforms WMS GetCapabilities XML into WebMapServiceCatalogItemTraits */ export default class WebMapServiceCapabilitiesStratum extends LoadableStratum( WebMapServiceCatalogItemTraits ) { static async load( catalogItem: WebMapServiceCatalogItem, capabilities?: WebMapServiceCapabilities ): Promise<WebMapServiceCapabilitiesStratum> { if (!isDefined(catalogItem.getCapabilitiesUrl)) { throw new TerriaError({ title: i18next.t("models.webMapServiceCatalogItem.missingUrlTitle"), message: i18next.t("models.webMapServiceCatalogItem.missingUrlMessage") }); } if (!isDefined(capabilities)) capabilities = await WebMapServiceCapabilities.fromUrl( proxyCatalogItemUrl( catalogItem, catalogItem.getCapabilitiesUrl, catalogItem.getCapabilitiesCacheDuration ) ); return new WebMapServiceCapabilitiesStratum(catalogItem, capabilities); } constructor( readonly catalogItem: WebMapServiceCatalogItem, readonly capabilities: WebMapServiceCapabilities ) { super(); makeObservable(this); } duplicateLoadableStratum(model: BaseModel): this { return new WebMapServiceCapabilitiesStratum( model as WebMapServiceCatalogItem, this.capabilities ) as this; } @computed get metadataUrls() { const metadataUrls: MetadataURL[] = []; Array.from(this.capabilitiesLayers.values()).forEach((layer) => { if (!layer?.MetadataURL) return; if (Array.isArray(layer?.MetadataURL)) { // eslint-disable-next-line no-unsafe-optional-chaining metadataUrls.push(...layer?.MetadataURL); } else { metadataUrls.push(layer?.MetadataURL as MetadataURL); } }); return metadataUrls .filter((m) => m.OnlineResource?.["xlink:href"]) .map((m) => createStratumInstance(MetadataUrlTraits, { url: m.OnlineResource!["xlink:href"] }) ); } @computed get layers(): string | undefined { let layers: string | undefined; if (this.catalogItem.uri !== undefined) { // Try to extract a layer from the URL const query: any = this.catalogItem.uri.query(true) ?? {}; layers = query.layers ?? query.LAYERS; } if (layers === undefined) { // Use all the top-level, named layers layers = filterOutUndefined( this.capabilities.topLevelNamedLayers.map((layer) => layer.Name) ).join(","); } return layers; } @computed get tileWidth() { const queryParams: any = this.catalogItem.uri?.query(true) ?? {}; if (isDefined(queryParams.width ?? queryParams.WIDTH)) { return parseInt(queryParams.width ?? queryParams.WIDTH, 10); } } @computed get tileHeight() { const queryParams: any = this.catalogItem.uri?.query(true) ?? {}; if (isDefined(queryParams.height ?? queryParams.HEIGHT)) { return parseInt(queryParams.height ?? queryParams.HEIGHT, 10); } } /** * **How we determine WMS legends (in order)** 1. Defined manually in catalog JSON 2. If `style` is undefined, and server doesn't support `GetLegendGraphic`, we must select first style as default - as there is no way to know what the default style is, and to request a legend for it 3. If `style` is is set and it has a `legendUrl` -> use it! 4. If server supports `GetLegendGraphic`, we can request a legend (with or without `style` parameter) */ @computed get legends(): StratumFromTraits<LegendTraits>[] | undefined { const availableStyles = this.catalogItem.availableStyles || []; const layers = this.catalogItem.layersArray; const styles = this.catalogItem.stylesArray; const result: StratumFromTraits<LegendTraits>[] = []; for (let i = 0; i < layers.length; ++i) { const layer = layers[i]; const style = i < styles.length ? styles[i] : undefined; let legendUri: URI | undefined; let legendUrlMimeType: string | undefined; let legendScaling: number | undefined; const layerAvailableStyles = availableStyles.find( (candidate) => candidate.layerName === layer )?.styles; let layerStyle: Model<WebMapServiceAvailableStyleTraits> | undefined; if (isDefined(style)) { // Attempt to find layer style based on AvailableStyleTraits layerStyle = layerAvailableStyles?.find( (candidate) => candidate.name === style ); } // If no style is selected and this WMS doesn't support GetLegendGraphics - we must use the first style if none is explicitly specified. // (If WMS supports GetLegendGraphics we can use it and omit style parameter to get the "default" style's legend) if (!isDefined(layerStyle) && !this.catalogItem.supportsGetLegendGraphic) layerStyle = layerAvailableStyles?.[0]; // If legend found - proxy URL and set mimetype if (layerStyle?.legend?.url) { legendUri = URI( proxyCatalogItemUrl(this.catalogItem, layerStyle.legend.url) ); legendUrlMimeType = layerStyle.legend.urlMimeType; } // If no legends found and WMS supports GetLegendGraphics - make one up! if ( !isDefined(legendUri) && isDefined(this.catalogItem.url) && this.catalogItem.supportsGetLegendGraphic ) { legendUri = URI( proxyCatalogItemUrl( this.catalogItem, this.catalogItem.getLegendBaseUrl() ) ); legendUri .setQuery("service", "WMS") .setQuery( "version", this.catalogItem.useWmsVersion130 ? "1.3.0" : "1.1.1" ) .setQuery("request", "GetLegendGraphic") .setQuery("format", "image/png") .setQuery("sld_version", "1.1.0") .setQuery("layer", layer); // From OGC — about style property for GetLegendGraphic request: // If not present, the default style is selected. The style may be any valid style available for a layer, including non-SLD internally-defined styles. if (style) { legendUri.setQuery("style", style); } legendUrlMimeType = "image/png"; } if (isDefined(legendUri)) { // Add geoserver related LEGEND_OPTIONS to match terria styling (if supported) if ( this.catalogItem.isGeoServer && legendUri.hasQuery("request", "GetLegendGraphic") ) { let legendOptions = "fontName:Courier;fontStyle:bold;fontSize:12;forceLabels:on;fontAntiAliasing:true;labelMargin:5"; // Geoserver fontColor must be a hex value - use `textLight` theme colour let fontColor = terriaTheme.textLight.split("#")?.[1]; if (isDefined(fontColor)) { // If fontColor is a 3-character hex -> turn into 6 if (fontColor.length === 3) { fontColor = `${fontColor[0]}${fontColor[0]}${fontColor[1]}${fontColor[1]}${fontColor[2]}${fontColor[2]}`; } legendOptions += `;fontColor:0x${fontColor}`; } legendOptions += ";dpi:182"; // enable if we can scale the image back down by 50%. legendScaling = 0.5; legendUri.setQuery("LEGEND_OPTIONS", legendOptions); legendUri.setQuery("transparent", "true"); } // Add colour scale range params if supported if ( this.catalogItem.supportsColorScaleRange && this.catalogItem.colorScaleRange ) { legendUri.setQuery( "colorscalerange", this.catalogItem.colorScaleRange ); } result.push( createStratumInstance(LegendTraits, { url: legendUri.toString(), urlMimeType: legendUrlMimeType, imageScaling: legendScaling }) ); } } return result; } @computed get capabilitiesLayers(): ReadonlyMap<string, CapabilitiesLayer | undefined> { const lookup: (name: string) => [string, CapabilitiesLayer | undefined] = ( name ) => [name, this.capabilities && this.capabilities.findLayer(name)]; return new Map(this.catalogItem.layersArray.map(lookup)); } @computed get availableCrs() { // Get set of supported CRS from layer hierarchy const layerCrs = new Set<string>(); this.capabilitiesLayers.forEach((layer) => { if (layer) { const srs = this.capabilities.getInheritedValues(layer, "SRS"); const crs = this.capabilities.getInheritedValues(layer, "CRS"); [ ...(Array.isArray(srs) ? srs : [srs]), ...(Array.isArray(crs) ? crs : [crs]) ].forEach((c) => layerCrs.add(c)); } }); return Array.from(layerCrs); } @computed get crs() { // Note order is important here, the first one found will be used const supportedCrs = [...SUPPORTED_CRS_3857, ...SUPPORTED_CRS_4326]; // First check to see if URL has CRS or SRS const queryParams: any = this.catalogItem.uri?.query(true) ?? {}; const urlCrs = queryParams.crs ?? queryParams.CRS ?? queryParams.srs ?? queryParams.SRS; if (urlCrs && supportedCrs.includes(urlCrs)) return urlCrs; // If nothing is supported, ask for EPSG:3857, and hope for the best. return ( supportedCrs.find((crs) => this.availableCrs.includes(crs)) ?? "EPSG:3857" ); } @computed get availableDimensions(): StratumFromTraits<WebMapServiceAvailableLayerDimensionsTraits>[] { const result: StratumFromTraits<WebMapServiceAvailableLayerDimensionsTraits>[] = []; if (!this.capabilities) { return result; } const capabilitiesLayers = this.capabilitiesLayers; for (const layerTuple of capabilitiesLayers) { const layerName = layerTuple[0]; const layer = layerTuple[1]; const dimensions: ReadonlyArray<CapabilitiesDimension> = layer ? this.capabilities.getInheritedValues(layer, "Dimension") : []; result.push({ layerName: layerName, dimensions: dimensions .filter((dim) => dim.name !== "time") .map((dim) => { return { name: dim.name, units: dim.units, unitSymbol: dim.unitSymbol, default: dim.default, multipleValues: dim.multipleValues, current: dim.current, nearestValue: dim.nearestValue, values: dim.text?.split(",") }; }) }); } return result; } @computed get availableStyles(): StratumFromTraits<WebMapServiceAvailableLayerStylesTraits>[] { const result: StratumFromTraits<WebMapServiceAvailableLayerStylesTraits>[] = []; if (!this.capabilities) { return result; } const capabilitiesLayers = this.capabilitiesLayers; for (const layerTuple of capabilitiesLayers) { const layerName = layerTuple[0]; const layer = layerTuple[1]; const styles: ReadonlyArray<CapabilitiesStyle> = layer ? this.capabilities.getInheritedValues(layer, "Style") : []; result.push({ layerName: layerName, styles: styles.map((style) => { const wmsLegendUrl = isReadOnlyArray(style.LegendURL) ? style.LegendURL[0] : style.LegendURL; let legendUri, legendMimeType; if ( wmsLegendUrl && wmsLegendUrl.OnlineResource && wmsLegendUrl.OnlineResource["xlink:href"] ) { legendUri = new URI( decodeURIComponent(wmsLegendUrl.OnlineResource["xlink:href"]) ); legendMimeType = wmsLegendUrl.Format; } const legend = !legendUri ? undefined : createStratumInstance(LegendTraits, { url: legendUri.toString(), urlMimeType: legendMimeType, title: (capabilitiesLayers.size > 1 && layer?.Title) || undefined // Add layer Title as legend title if showing multiple layers }); return { name: style.Name, title: style.Title, abstract: style.Abstract, legend: legend }; }) }); } return result; } /** There is no way of finding out default style if no style has been selected :( * If !supportsGetLegendGraphic - we have to just use the first available style (for each layer) * This is because, to request a "default" legend we need GetLegendGraphics **/ @computed get styles() { if (this.catalogItem.uri !== undefined) { // Try to extract a styles from the URL const query: any = this.catalogItem.uri.query(true) ?? {}; if (isDefined(query.styles ?? query.STYLES)) return query.styles ?? query.STYLES; } if (!this.catalogItem.supportsGetLegendGraphic) { return this.catalogItem.availableStyles .map((layer) => { if (layer.layerName && layer.styles.length > 0) { return layer.styles[0].name ?? ""; } return ""; }) .join(","); } } @computed get info(): StratumFromTraits<InfoSectionTraits>[] { const result: StratumFromTraits<InfoSectionTraits>[] = []; let firstDataDescription: string | undefined; result.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.webMapServiceCatalogItem.serviceDescription"), contentAsObject: this.capabilities.Service as JsonObject, // Hide big ugly table by default show: false }) ); const onlyHasSingleLayer = this.catalogItem.layersArray.length === 1; if (onlyHasSingleLayer) { // Clone the capabilitiesLayer as we'll modify it in a second const out = Object.assign( {}, this.capabilitiesLayers.get(this.catalogItem.layersArray[0]) ) as any; if (out !== undefined) { // The Dimension object is really weird and has a bunch of stray text in there if ("Dimension" in out) { const goodDimension: any = {}; Object.keys(out.Dimension).forEach((k: any) => { if (isNaN(k)) { goodDimension[k] = out.Dimension[k]; } }); out.Dimension = goodDimension; } // remove a circular reference to the parent delete out._parent; try { result.push( createStratumInstance(InfoSectionTraits, { name: i18next.t( "models.webMapServiceCatalogItem.dataDescription" ), contentAsObject: out as JsonObject, // Hide big ugly table by default show: false }) ); } catch (e) { console.log( `FAILED to create InfoSection with WMS layer Capabilities` ); console.log(e); } } } for (const layer of this.capabilitiesLayers.values()) { if ( !layer || !layer.Abstract || containsAny(layer.Abstract, WebMapServiceCatalogItem.abstractsToIgnore) ) { continue; } const suffix = this.capabilitiesLayers.size === 1 ? "" : ` - ${layer.Title}`; const name = `Web Map Service Layer Description${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.ContactInformation !== undefined) { result.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.webMapServiceCatalogItem.serviceContact"), content: getServiceContactInformation(service.ContactInformation) }) ); } result.push( createStratumInstance(InfoSectionTraits, { name: i18next.t("models.webMapServiceCatalogItem.getCapabilitiesUrl"), content: this.catalogItem.getCapabilitiesUrl }) ); if ( service && service.Abstract && !containsAny( service.Abstract, WebMapServiceCatalogItem.abstractsToIgnore ) && service.Abstract !== firstDataDescription ) { result.push( createStratumInstance(InfoSectionTraits, { name: i18next.t( "models.webMapServiceCatalogItem.serviceDescription" ), 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.webMapServiceCatalogItem.accessConstraints" ), content: service.AccessConstraints }) ); } } return result; } @computed get infoSectionOrder(): string[] { let layerDescriptions = [`Web Map Service Layer Description`]; // If more than one layer, push layer description titles for each applicable layer if (this.capabilitiesLayers.size > 1) { layerDescriptions = []; this.capabilitiesLayers.forEach((layer) => { if ( layer && layer.Abstract && !containsAny( layer.Abstract, WebMapServiceCatalogItem.abstractsToIgnore ) ) { layerDescriptions.push( `Web Map Service Layer Description - ${layer.Title}` ); } }); } return [ i18next.t("preview.disclaimer"), i18next.t("description.name"), ...layerDescriptions, i18next.t("preview.datasetDescription"), i18next.t("preview.serviceDescription"), i18next.t("models.webMapServiceCatalogItem.serviceDescription"), i18next.t("preview.resourceDescription"), i18next.t("preview.licence"), i18next.t("preview.accessConstraints"), i18next.t("models.webMapServiceCatalogItem.accessConstraints"), i18next.t("preview.author"), i18next.t("preview.contact"), i18next.t("models.webMapServiceCatalogItem.serviceContact"), i18next.t("preview.created"), i18next.t("preview.modified"), i18next.t("preview.updateFrequency"), i18next.t("models.webMapServiceCatalogItem.getCapabilitiesUrl") ]; } @computed get shortReport() { const catalogItem = this.catalogItem; if (catalogItem.isShowingDiff) { const format = "yyyy/mm/dd"; const d1 = dateFormat(catalogItem.firstDiffDate, format); const d2 = dateFormat(catalogItem.secondDiffDate, format); return `Showing difference image computed for ${catalogItem.diffStyleId} style on dates ${d1} and ${d2}`; } } @computed get rectangle(): StratumFromTraits<RectangleTraits> | undefined { const layers: CapabilitiesLayer[] = [...this.capabilitiesLayers.values()] .filter((layer) => layer !== undefined) .map((l) => l!); // Get union of bounding rectangles for all layers const allLayersRectangle = layers.reduce<Rectangle | undefined>( (unionRectangle, layer) => { // Convert to cesium Rectangle (so we can use Rectangle.union) const latLonRect = getRectangleFromLayer(layer); if ( !isDefined(latLonRect?.west) || !isDefined(latLonRect?.south) || !isDefined(latLonRect?.east) || !isDefined(latLonRect?.north) ) return; const cesiumRectangle = Rectangle.fromDegrees( latLonRect?.west, latLonRect?.south, latLonRect?.east, latLonRect?.north ); if (!unionRectangle) { return cesiumRectangle; } return Rectangle.union(unionRectangle, cesiumRectangle); }, undefined ); if ( allLayersRectangle && isDefined(allLayersRectangle.west) && isDefined(allLayersRectangle.south) && isDefined(allLayersRectangle.east) && isDefined(allLayersRectangle.north) ) { return { west: CesiumMath.toDegrees(allLayersRectangle.west), south: CesiumMath.toDegrees(allLayersRectangle.south), east: CesiumMath.toDegrees(allLayersRectangle.east), north: CesiumMath.toDegrees(allLayersRectangle.north) }; } } @computed get isGeoServer(): boolean | undefined { const keyword = this.capabilities?.Service?.KeywordList?.Keyword; return ( (isReadOnlyArray(keyword) && keyword.indexOf("GEOSERVER") >= 0) || keyword === "GEOSERVER" || this.catalogItem.url?.toLowerCase().includes("geoserver") ); } // TODO - There is possibly a better way to do this @computed get isThredds(): boolean { if ( this.catalogItem.url && (this.catalogItem.url.indexOf("thredds") > -1 || this.catalogItem.url.indexOf("tds") > -1) ) { return true; } return false; } // TODO - Geoserver also support NCWMS via a plugin, just need to work out how to detect that @computed get isNcWMS(): boolean { if (this.catalogItem.isThredds) return true; return false; } @computed get isEsri(): boolean { if (this.catalogItem.url !== undefined) return ( this.catalogItem.url.toLowerCase().indexOf("mapserver/wmsserver") > -1 ); return false; } @computed get supportsGetLegendGraphic(): boolean { const capabilities = this.capabilities?.json?.Capability; return ( isDefined(this.capabilities?.json?.["xmlns:sld"]) || isDefined(capabilities?.Request?.GetLegendGraphic) || (Array.isArray(capabilities?.ExtendedCapabilities?.ExtendedRequest) && capabilities.ExtendedCapabilities.ExtendedRequest.find( (r: JsonObject) => r?.Request === "GetLegendGraphic" )) || (this.catalogItem.isGeoServer ?? false) || (this.catalogItem.isNcWMS ?? false) ); } @computed get supportsGetTimeseries() { // Don't use GetTimeseries if there is only one timeslice if ((this.catalogItem.discreteTimes?.length ?? 0) <= 1) return false; const capabilities = this.capabilities?.json?.Capability; return !!( isDefined(capabilities?.Request?.GetTimeseries) || (Array.isArray(capabilities?.ExtendedCapabilities?.ExtendedRequest) && capabilities.ExtendedCapabilities.ExtendedRequest.find( (r: JsonObject) => r?.Request === "GetTimeseries" )) ); } @computed get supportsColorScaleRange(): boolean { return this.catalogItem.isNcWMS; } @computed get discreteTimes(): { time: string; tag: string | undefined }[] | undefined { const result = []; for (const layer of this.capabilitiesLayers.values()) { if (!layer) { continue; } const dimensions = this.capabilities.getInheritedValues( layer, "Dimension" ); const timeDimension = dimensions.find( (dimension) => dimension.name.toLowerCase() === "time" ); if (!timeDimension) { continue; } let extent: string = timeDimension; // WMS 1.1.1 puts dimension values in an Extent element instead of directly in the Dimension element. const extentElements = this.capabilities.getInheritedValues( layer, "Extent" ); const extentElement = extentElements.find( (extent) => extent.name.toLowerCase() === "time" ); if (extentElement) { extent = extentElement; } if (!extent || !extent.split) { continue; } const values = extent.split(","); for (let i = 0; i < values.length; ++i) { const value = values[i]; const isoSegments = value.split("/"); if (isoSegments.length === 1) { result.push({ time: values[i], tag: undefined }); } else { createDiscreteTimesFromIsoSegments( result, isoSegments[0], isoSegments[1], isoSegments[2], this.catalogItem.maxRefreshIntervals ); } } } return result; } @computed get initialTimeSource() { return "now"; } @computed get currentTime() { // Get default times for all layers const defaultTimes = filterOutUndefined( Array.from(this.capabilitiesLayers).map(([_layerName, layer]) => { if (!layer) return; const dimensions = this.capabilities.getInheritedValues( layer, "Dimension" ); const timeDimension = dimensions.find( (dimension) => dimension.name.toLowerCase() === "time" ); return timeDimension?.default; }) ); // From WMS 1.3.0 spec: // For the TIME parameter, the special keyword “current” may be used if the <Dimension name="time"> service metadata element includes a nonzero value for the “current” attribute, as described in C.2. // The expression “TIME=current” means “send the most current data available”. // Here we return undefined, because WebMapServiceCapabilitiesStratum.initialTimeSource is set to "now" if (defaultTimes[0] === "current") { return undefined; } // Return first default time return defaultTimes[0]; } /** Prioritize format of GetFeatureInfo: * - JSON/GeoJSON * - If ESRI, then we prioritise XML next * - HTML * - GML * - XML * - Plain text * * If no matching format can be found in GetCapabilities, then Cesium will use defaults (see `WebMapServiceImageryProvider.DefaultGetFeatureInfoFormats`) * * If supportsGetTimeseries, use CSV */ @computed get getFeatureInfoFormat(): | StratumFromTraits<GetFeatureInfoFormat> | undefined { const formats: string | string[] | undefined = this.capabilities.json?.Capability?.Request?.GetFeatureInfo?.Format; const formatsArray = isJsonArray(formats) ? formats : isJsonString(formats) ? [formats] : []; if (this.catalogItem.supportsGetTimeseries) { return { format: "text/csv", type: "text" }; } if (formatsArray.includes("application/json")) return { format: "application/json", type: "json" }; if (formatsArray.includes("application/geo+json")) return { format: "application/geo+json", type: "json" }; if (formatsArray.includes("application/vnd.geo+json")) return { format: "application/vnd.geo+json", type: "json" }; // Special case for Esri WMS, use XML before HTML/GML // as HTML includes <table> with rowbg that is hard to read if (this.isEsri && formatsArray.includes("text/xml")) { return { format: "text/xml", type: "xml" }; } if (formatsArray.includes("text/html")) return { format: "text/html", type: "html" }; if (formatsArray.includes("application/vnd.ogc.gml")) return { format: "application/vnd.ogc.gml", type: "xml" }; // For non-Esri services, we use XML after HTML/GML if (formatsArray.includes("text/xml")) { return { format: "text/xml", type: "xml" }; } if (formatsArray.includes("text/plain")) return { format: "text/plain", type: "text" }; } /** If supportsGetTimeseries, override the "request" parameter in GetFeatureInfo to be "GetTimeseries". * We also set time to empty, so we get values for all times (as opposed to just the current time) */ @computed get getFeatureInfoParameters() { if (this.catalogItem.supportsGetTimeseries) { return { request: "GetTimeseries", time: "" }; } return undefined; } /** If getFeatureInfoFormat is text/csv, set featureInfoTemplate to show chart. */ @computed get featureInfoTemplate() { if (this.catalogItem.getFeatureInfoFormat.format === "text/csv") return createStratumInstance(FeatureInfoTemplateTraits, { template: `{{terria.timeSeries.chart}}`, showFeatureInfoDownloadWithTemplate: true }); } @computed get linkedWcsParameters() { // Get outputCrs // Note: this will be overridden by `WebCoverageServiceDescribeCoverageStratum` if a better outputCrs is found let outputCrs = this.availableCrs[0]; // Unless it is Web Mercator of course - that would be stupid // If this is the case - use 4326 outputCrs = outputCrs && SUPPORTED_CRS_3857.includes(outputCrs) ? "EPSG:4326" : outputCrs; // Get WCS subsets from time and WMS dimensions // These are used to "subset" WCS coverage (dataset) // This is used to flag subsets (dimensions) which have multiple values // Each element in this array represents the **actual** value used for a subset which has multiple values const duplicateSubsetValues: StratumFromTraits<KeyValueTraits>[] = []; // Get dimensionSubsets const dimensionSubsets: { key: string; value: string }[] = []; if (this.catalogItem.dimensions) { Object.entries(this.catalogItem.dimensions).forEach(([key, values]) => { if (isDefined(values)) { // If we have multiple values for a particular dimension, they will be comma separated // WCS only supports a single value per dimension - so we take the first value const valuesArray = values.split(","); const value = valuesArray[0]; if (valuesArray.length > 1) { duplicateSubsetValues.push( createStratumInstance(KeyValueTraits, { key, value }) ); } // Wrap string values in double quotes dimensionSubsets.push({ key, value }); } }); } const subsets = filterOutUndefined([ // Add time dimension this.catalogItem.currentDiscreteTimeTag ? { key: "time", value: this.catalogItem.currentDiscreteTimeTag } : undefined, // Add other dimensions ...dimensionSubsets ]).map((subset) => createStratumInstance(KeyValueTraits, subset)); return createStratumInstance(WebCoverageServiceParameterTraits, { outputCrs, subsets, duplicateSubsetValues, // Add styles parameter for OpenDataCube WCS additionalParameters: [{ key: "styles", value: this.catalogItem.styles }] }); } } function getServiceContactInformation( contactInfo: CapabilitiesContactInformation ) { const primary = contactInfo.ContactPersonPrimary; let text = ""; if (isDefined(primary)) { if ( isDefined(primary.ContactOrganization) && primary.ContactOrganization.length > 0 && // Geoserver default primary.ContactOrganization !== "The Ancient Geographers" ) { text += primary.ContactOrganization + "<br/>"; } } if ( isDefined(contactInfo.ContactElectronicMailAddress) && contactInfo.ContactElectronicMailAddress.length > 0 && // Geoserver default contactInfo.ContactElectronicMailAddress !== "claudius.ptolomaeus@gmail.com" ) { text += `[${contactInfo.ContactElectronicMailAddress}](mailto:${contactInfo.ContactElectronicMailAddress})`; } return text; }