terriajs
Version:
Geospatial data visualization platform.
1,018 lines (891 loc) • 33.8 kB
text/typescript
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;
}
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"]
})
);
}
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;
}
get tileWidth() {
const queryParams: any = this.catalogItem.uri?.query(true) ?? {};
if (isDefined(queryParams.width ?? queryParams.WIDTH)) {
return parseInt(queryParams.width ?? queryParams.WIDTH, 10);
}
}
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)
*/
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;
}
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));
}
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);
}
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"
);
}
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;
}
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
**/
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(",");
}
}
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;
}
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")
];
}
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}`;
}
}
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)
};
}
}
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
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
get isNcWMS(): boolean {
if (this.catalogItem.isThredds) return true;
return false;
}
get isEsri(): boolean {
if (this.catalogItem.url !== undefined)
return (
this.catalogItem.url.toLowerCase().indexOf("mapserver/wmsserver") > -1
);
return false;
}
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)
);
}
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"
))
);
}
get supportsColorScaleRange(): boolean {
return this.catalogItem.isNcWMS;
}
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;
}
get initialTimeSource() {
return "now";
}
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
*/
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)
*/
get getFeatureInfoParameters() {
if (this.catalogItem.supportsGetTimeseries) {
return { request: "GetTimeseries", time: "" };
}
return undefined;
}
/** If getFeatureInfoFormat is text/csv, set featureInfoTemplate to show chart. */
get featureInfoTemplate() {
if (this.catalogItem.getFeatureInfoFormat.format === "text/csv")
return createStratumInstance(FeatureInfoTemplateTraits, {
template: `{{terria.timeSeries.chart}}`,
showFeatureInfoDownloadWithTemplate: true
});
}
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;
}