terriajs
Version:
Geospatial data visualization platform.
883 lines (784 loc) • 28.7 kB
text/typescript
// Problems in current architecture:
// 1. After loading, can't tell what user actually set versus what came from e.g. GetCapabilities.
// Solution: layering
// 2. CkanCatalogItem producing a WebMapServiceCatalogItem on load
// 3. Observable spaghetti
// Solution: think in terms of pipelines with computed observables, document patterns.
// 4. All code for all catalog item types needs to be loaded before we can do anything.
import { FeatureCollection } from "geojson";
import i18next from "i18next";
import { computed, makeObservable, override, runInAction } from "mobx";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
import GeographicProjection from "terriajs-cesium/Source/Core/GeographicProjection";
import GeographicTilingScheme from "terriajs-cesium/Source/Core/GeographicTilingScheme";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
import MapProjection from "terriajs-cesium/Source/Core/MapProjection";
import WebMercatorTilingScheme from "terriajs-cesium/Source/Core/WebMercatorTilingScheme";
import combine from "terriajs-cesium/Source/Core/combine";
import GetFeatureInfoFormat from "terriajs-cesium/Source/Scene/GetFeatureInfoFormat";
import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo";
import WebMapServiceImageryProvider from "terriajs-cesium/Source/Scene/WebMapServiceImageryProvider";
import URI from "urijs";
import { JsonObject } from "../../../Core/Json";
import TerriaError from "../../../Core/TerriaError";
import createTransformerAllowUndefined from "../../../Core/createTransformerAllowUndefined";
import filterOutUndefined from "../../../Core/filterOutUndefined";
import isDefined from "../../../Core/isDefined";
import CatalogMemberMixin, {
getName
} from "../../../ModelMixins/CatalogMemberMixin";
import DiffableMixin, { DiffStratum } from "../../../ModelMixins/DiffableMixin";
import ExportWebCoverageServiceMixin from "../../../ModelMixins/ExportWebCoverageServiceMixin";
import GetCapabilitiesMixin from "../../../ModelMixins/GetCapabilitiesMixin";
import MappableMixin, {
ImageryParts
} from "../../../ModelMixins/MappableMixin";
import MinMaxLevelMixin from "../../../ModelMixins/MinMaxLevelMixin";
import TileErrorHandlerMixin from "../../../ModelMixins/TileErrorHandlerMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import {
TimeSeriesFeatureInfoContext,
csvFeatureInfoContext
} from "../../../Table/tableFeatureInfoContext";
import WebMapServiceCatalogItemTraits, {
SUPPORTED_CRS_3857,
SUPPORTED_CRS_4326
} from "../../../Traits/TraitsClasses/WebMapServiceCatalogItemTraits";
import CommonStrata from "../../Definition/CommonStrata";
import CreateModel from "../../Definition/CreateModel";
import LoadableStratum from "../../Definition/LoadableStratum";
import { BaseModel } from "../../Definition/Model";
import StratumOrder from "../../Definition/StratumOrder";
import TerriaFeature from "../../Feature/Feature";
import FeatureInfoContext from "../../Feature/FeatureInfoContext";
import SelectableDimensions, {
SelectableDimensionEnum
} from "../../SelectableDimensions/SelectableDimensions";
import Terria from "../../Terria";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import WebMapServiceCapabilities from "./WebMapServiceCapabilities";
import WebMapServiceCapabilitiesStratum from "./WebMapServiceCapabilitiesStratum";
import WebMapServiceCatalogGroup from "./WebMapServiceCatalogGroup";
// Remove problematic query parameters from URLs (GetCapabilities, GetMap, ...) - these are handled separately
const QUERY_PARAMETERS_TO_REMOVE = [
"request",
"service",
"x",
"y",
"width",
"height",
"bbox",
"layers",
"styles",
"version",
"format",
"srs",
"crs"
];
/** This LoadableStratum is responsible for setting WMS version based on CatalogItem.url */
export class WebMapServiceUrlStratum extends LoadableStratum(
WebMapServiceCatalogItemTraits
) {
static stratumName = "wms-url-stratum";
constructor(readonly catalogItem: WebMapServiceCatalogItem) {
super();
makeObservable(this);
}
duplicateLoadableStratum(model: BaseModel): this {
return new WebMapServiceUrlStratum(
model as WebMapServiceCatalogItem
) as this;
}
get useWmsVersion130() {
if (
this.catalogItem.url?.toLowerCase().includes("version=1.1.0") ||
this.catalogItem.url?.toLowerCase().includes("version=1.1.1")
) {
return false;
}
}
}
// Order is important so that the traits are overridden correctly
StratumOrder.addLoadStratum(WebMapServiceUrlStratum.stratumName);
StratumOrder.addLoadStratum(DiffStratum.stratumName);
class WebMapServiceCatalogItem
extends TileErrorHandlerMixin(
ExportWebCoverageServiceMixin(
DiffableMixin(
MinMaxLevelMixin(
GetCapabilitiesMixin(
UrlMixin(
MappableMixin(
CatalogMemberMixin(CreateModel(WebMapServiceCatalogItemTraits))
)
)
)
)
)
)
)
implements SelectableDimensions, FeatureInfoContext
{
/**
* 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 WMS..." stock abstract.
*/
static abstractsToIgnore = ["A compliant implementation of WMS"];
// hide elements in the info section which might show information about the datasource
_sourceInfoItemNames = [
i18next.t("models.webMapServiceCatalogItem.getCapabilitiesUrl")
];
_webMapServiceCatalogGroup: undefined | WebMapServiceCatalogGroup = undefined;
/** Default WMS parameters for version=1.3.0 */
static defaultParameters130 = {
transparent: true,
format: "image/png",
exceptions: "XML",
styles: "",
version: "1.3.0"
};
static defaultGetFeatureParameters130 = {
exceptions: "XML",
version: "1.3.0"
};
/** Default WMS parameters for version=1.1.1 */
static defaultParameters111 = {
transparent: true,
format: "image/png",
exceptions: "application/vnd.ogc.se_xml",
styles: "",
tiled: true,
version: "1.1.1"
};
static defaultGetFeatureParameters111 = {
exceptions: "application/vnd.ogc.se_xml",
version: "1.1.1"
};
static readonly type = "wms";
constructor(
id: string | undefined,
terria: Terria,
sourceReference?: BaseModel | undefined
) {
super(id, terria, sourceReference);
makeObservable(this);
this.strata.set(
WebMapServiceUrlStratum.stratumName,
new WebMapServiceUrlStratum(this)
);
}
get type() {
return WebMapServiceCatalogItem.type;
}
get shortReport(): string | undefined {
if (
this.tilingScheme instanceof GeographicTilingScheme &&
this.terria.currentViewer.type === "Leaflet"
) {
return i18next.t("map.cesium.notWebMercatorTilingScheme", this);
}
return super.shortReport;
}
get colorScaleRange(): string | undefined {
if (this.supportsColorScaleRange) {
return `${this.colorScaleMinimum},${this.colorScaleMaximum}`;
}
return undefined;
}
async createGetCapabilitiesStratumFromParent(
capabilities: WebMapServiceCapabilities
) {
const stratum = await WebMapServiceCapabilitiesStratum.load(
this,
capabilities
);
runInAction(() => {
this.strata.set(GetCapabilitiesMixin.getCapabilitiesStratumName, stratum);
});
}
protected async forceLoadMapItems(): Promise<void> {
if (this.invalidLayers.length > 0)
throw new TerriaError({
sender: this,
title: i18next.t("models.webMapServiceCatalogItem.noLayerFoundTitle"),
message: i18next.t(
"models.webMapServiceCatalogItem.noLayerFoundMessage",
{ name: getName(this), layers: this.invalidLayers.join(", ") }
)
});
}
protected async forceLoadMetadata(): Promise<void> {
if (
this.strata.get(GetCapabilitiesMixin.getCapabilitiesStratumName) !==
undefined
)
return;
const stratum = await WebMapServiceCapabilitiesStratum.load(this);
runInAction(() => {
this.strata.set(GetCapabilitiesMixin.getCapabilitiesStratumName, stratum);
});
}
get cacheDuration(): string {
if (isDefined(super.cacheDuration)) {
return super.cacheDuration;
}
return "0d";
}
get layersArray(): ReadonlyArray<string> {
if (Array.isArray(this.layers)) {
return this.layers;
} else if (this.layers) {
return this.layers.split(",");
} else {
return [];
}
}
/** LAYERS which are valid (i.e. exist in GetCapabilities).
* These can be fetched from the server (eg GetMap request)
*/
get validLayers() {
const gcStratum: WebMapServiceCapabilitiesStratum | undefined =
this.strata.get(
GetCapabilitiesMixin.getCapabilitiesStratumName
) as WebMapServiceCapabilitiesStratum;
if (gcStratum)
return this.layersArray
.map((layer) => gcStratum.capabilities.findLayer(layer)?.Name)
.filter(isDefined);
return [];
}
/** LAYERS which are **INVALID** - they do **not** exist in GetCapabilities
* These layers can **not** be fetched the server (eg GetMap request)
*/
get invalidLayers() {
const gcStratum: WebMapServiceCapabilitiesStratum | undefined =
this.strata.get(
GetCapabilitiesMixin.getCapabilitiesStratumName
) as WebMapServiceCapabilitiesStratum;
if (gcStratum)
return this.layersArray.filter(
(layer) => !isDefined(gcStratum.capabilities.findLayer(layer)?.Name)
);
return [];
}
get stylesArray(): ReadonlyArray<string> {
return this.styles?.split(",") ?? [];
}
get discreteTimes() {
const getCapabilitiesStratum: WebMapServiceCapabilitiesStratum | undefined =
this.strata.get(
GetCapabilitiesMixin.getCapabilitiesStratumName
) as WebMapServiceCapabilitiesStratum;
return getCapabilitiesStratum?.discreteTimes;
}
protected get defaultGetCapabilitiesUrl(): string | undefined {
if (this.uri) {
const baseUrl = QUERY_PARAMETERS_TO_REMOVE.reduce(
(url, parameter) =>
url
.removeQuery(parameter)
.removeQuery(parameter.toUpperCase())
.removeQuery(parameter.toLowerCase()),
this.uri.clone()
);
return baseUrl
.setSearch({
service: "WMS",
version: this.useWmsVersion130 ? "1.3.0" : "1.1.1",
request: "GetCapabilities"
})
.toString();
} else {
return undefined;
}
}
get canDiffImages(): boolean {
const hasValidDiffStyles = this.availableDiffStyles.some((diffStyle) =>
this.styleSelectableDimensions?.[0]?.options?.find(
(style) => style.id === diffStyle
)
);
return hasValidDiffStyles === true;
}
showDiffImage(
firstDate: JulianDate,
secondDate: JulianDate,
diffStyleId: string
) {
if (this.canDiffImages === false) {
return;
}
// A helper to get the diff tag given a date string
const firstDateStr = this.getTagForTime(firstDate);
const secondDateStr = this.getTagForTime(secondDate);
this.setTrait(CommonStrata.user, "firstDiffDate", firstDateStr);
this.setTrait(CommonStrata.user, "secondDiffDate", secondDateStr);
this.setTrait(CommonStrata.user, "diffStyleId", diffStyleId);
this.setTrait(CommonStrata.user, "isShowingDiff", true);
}
clearDiffImage() {
this.setTrait(CommonStrata.user, "firstDiffDate", undefined);
this.setTrait(CommonStrata.user, "secondDiffDate", undefined);
this.setTrait(CommonStrata.user, "diffStyleId", undefined);
this.setTrait(CommonStrata.user, "isShowingDiff", false);
}
getLegendBaseUrl(): string {
// Remove problematic query parameters from URL
const baseUrl = QUERY_PARAMETERS_TO_REMOVE.reduce(
(url, parameter) =>
url
.removeQuery(parameter)
.removeQuery(parameter.toUpperCase())
.removeQuery(parameter.toLowerCase()),
new URI(this.url)
);
return baseUrl.toString();
}
getLegendUrlForStyle(
styleId: string,
firstDate?: JulianDate,
secondDate?: JulianDate
) {
const firstTag = firstDate && this.getTagForTime(firstDate);
const secondTag = secondDate && this.getTagForTime(secondDate);
const time = filterOutUndefined([firstTag, secondTag]).join(",");
const layerName = this.availableStyles.find((style) =>
style.styles.some((s) => s.name === styleId)
)?.layerName;
const uri = URI(
`${this.url}?service=WMS&version=1.1.0&request=GetLegendGraphic&format=image/png&transparent=True`
)
.addQuery("layer", encodeURIComponent(layerName || ""))
.addQuery("styles", encodeURIComponent(styleId));
if (time) {
uri.addQuery("time", time);
}
return uri.toString();
}
get mapItems() {
// Don't return anything if there are invalid layers
// See forceLoadMapItems for error message
if (this.invalidLayers.length > 0) return [];
if (this.isShowingDiff === true) {
return this._diffImageryParts ? [this._diffImageryParts] : [];
}
const result = [];
const current = this._currentImageryParts;
if (current) {
result.push(current);
}
const next = this._nextImageryParts;
if (next) {
result.push(next);
}
return result;
}
get tilingScheme() {
if (this.crs) {
if (SUPPORTED_CRS_3857.includes(this.crs))
return new WebMercatorTilingScheme();
if (SUPPORTED_CRS_4326.includes(this.crs))
return new GeographicTilingScheme();
}
return new WebMercatorTilingScheme();
}
private get _currentImageryParts(): ImageryParts | undefined {
const imageryProvider = this._createImageryProvider(
this.currentDiscreteTimeTag
);
if (imageryProvider === undefined) {
return undefined;
}
// Reset feature picking for the current imagery layer.
// We disable feature picking for the next imagery layer.
imageryProvider.enablePickFeatures = this.allowFeaturePicking;
return {
imageryProvider,
alpha: this.opacity,
show: this.show,
clippingRectangle: this.clipToRectangle ? this.cesiumRectangle : undefined
};
}
private get _nextImageryParts(): ImageryParts | undefined {
if (
this.terria.timelineStack.contains(this) &&
!this.isPaused &&
this.nextDiscreteTimeTag
) {
const imageryProvider = this._createImageryProvider(
this.nextDiscreteTimeTag
);
if (imageryProvider === undefined) {
return undefined;
}
// Disable feature picking for the next imagery layer.
imageryProvider.enablePickFeatures = false;
return {
imageryProvider,
alpha: 0.0,
show: true,
clippingRectangle: this.clipToRectangle
? this.cesiumRectangle
: undefined
};
} else {
return undefined;
}
}
private get _diffImageryParts(): ImageryParts | undefined {
const diffStyleId = this.diffStyleId;
if (
this.firstDiffDate === undefined ||
this.secondDiffDate === undefined ||
diffStyleId === undefined
) {
return;
}
const time = `${this.firstDiffDate},${this.secondDiffDate}`;
const imageryProvider = this._createImageryProvider(time);
if (imageryProvider) {
return {
imageryProvider,
alpha: this.opacity,
show: this.show,
clippingRectangle: this.clipToRectangle
? this.cesiumRectangle
: undefined
};
}
return undefined;
}
get diffModeParameters(): JsonObject {
return this.isShowingDiff ? { styles: this.diffStyleId } : {};
}
get diffModeGetFeatureInfoParameters(): JsonObject {
return this.isShowingDiff ? { styles: this.diffStyleId } : {};
}
getTagForTime(date: JulianDate): string | undefined {
const index = this.getDiscreteTimeIndex(date);
return index !== undefined
? this.discreteTimesAsSortedJulianDates?.[index].tag
: undefined;
}
private _createImageryProvider = createTransformerAllowUndefined(
(time: string | undefined): WebMapServiceImageryProvider | undefined => {
// Don't show anything on the map until GetCapabilities finishes loading.
if (this.isLoadingMetadata) {
return undefined;
}
if (this.url === undefined) {
return undefined;
}
console.log(`Creating new ImageryProvider for time ${time}`);
// Set dimensionParameters
const dimensionParameters = formatDimensionsForOws(this.dimensions);
if (time !== undefined) {
dimensionParameters.time = time;
}
// Construct parameters objects
// We use slightly different parameters for GetMap and GetFeatureInfo requests
const parameters: { [key: string]: any } = {
...(this.useWmsVersion130
? WebMapServiceCatalogItem.defaultParameters130
: WebMapServiceCatalogItem.defaultParameters111),
...this.parameters,
...dimensionParameters
};
const getFeatureInfoParameters: { [key: string]: any } = {
...(this.useWmsVersion130
? WebMapServiceCatalogItem.defaultGetFeatureParameters130
: WebMapServiceCatalogItem.defaultGetFeatureParameters111),
feature_count:
1 +
(this.maximumShownFeatureInfos ??
this.terria.configParameters.defaultMaximumShownFeatureInfos),
...this.parameters,
// Note order is important here, as getFeatureInfoParameters may override `time` dimension value
...dimensionParameters,
...this.getFeatureInfoParameters
};
if (this.supportsColorScaleRange) {
parameters.COLORSCALERANGE = this.colorScaleRange;
}
// Styles parameter is mandatory (for GetMap and GetFeatureInfo requests), but can be empty string to use default style
parameters.styles = this.styles ?? "";
getFeatureInfoParameters.styles = this.styles ?? "";
Object.assign(parameters, this.diffModeParameters);
Object.assign(
getFeatureInfoParameters,
this.diffModeGetFeatureInfoParameters
);
// Remove problematic query parameters from URL - these are handled by the parameters objects
const baseUrl = QUERY_PARAMETERS_TO_REMOVE.reduce(
(url, parameter) =>
url
.removeQuery(parameter)
.removeQuery(parameter.toUpperCase())
.removeQuery(parameter.toLowerCase()),
new URI(this.url)
);
// Set CRS for WMS 1.3.0
// Set SRS for WMS 1.1.1
const crs = this.useWmsVersion130 ? this.crs : undefined;
const srs = this.useWmsVersion130 ? undefined : this.crs;
const imageryOptions: WebMapServiceImageryProvider.ConstructorOptions = {
url: proxyCatalogItemUrl(this, baseUrl.toString()),
layers: this.validLayers.length > 0 ? this.validLayers.join(",") : "",
parameters,
crs,
srs,
getFeatureInfoParameters,
getFeatureInfoUrl: this.getFeatureInfoUrl,
tileWidth: this.tileWidth,
tileHeight: this.tileHeight,
tilingScheme: this.tilingScheme,
maximumLevel: this.getMaximumLevel(true) ?? this.maximumLevel,
minimumLevel: this.minimumLevel,
credit: this.attribution
// Note: we set enablePickFeatures in _currentImageryParts and _nextImageryParts
};
imageryOptions.getFeatureInfoFormats = this.getFeatureInfoFormats;
if (
imageryOptions.maximumLevel !== undefined &&
this.hideLayerAfterMinScaleDenominator
) {
// Make Cesium request one extra level so we can tell the user what's happening and return a blank image.
++imageryOptions.maximumLevel;
}
const imageryProvider = new WebMapServiceImageryProvider(imageryOptions);
return this.updateRequestImage(imageryProvider);
}
);
get styleSelectableDimensions(): SelectableDimensionEnum[] {
return this.availableStyles.map((layer, layerIndex) => {
let name = "Styles";
// If multiple layers -> prepend layer name to name
if (this.availableStyles.length > 1) {
// Attempt to get layer title from GetCapabilitiesStratum
const layerTitle =
layer.layerName &&
(
this.strata.get(
GetCapabilitiesMixin.getCapabilitiesStratumName
) as WebMapServiceCapabilitiesStratum
).capabilitiesLayers.get(layer.layerName)?.Title;
name = `${
layerTitle || layer.layerName || `Layer ${layerIndex + 1}`
} styles`;
}
const options = filterOutUndefined(
layer.styles.map(function (s) {
if (isDefined(s.name)) {
return {
name: s.title || s.name || "",
id: s.name as string
};
}
})
);
// Try to set selectedId to value stored in `styles` trait for this `layerIndex`
// The `styles` parameter is CSV, a style for each layer
const selectedId = this.styles?.split(",")?.[layerIndex];
return {
name,
id: `${this.uniqueId}-${layer.layerName}-styles`,
options,
selectedId,
setDimensionValue: (
stratumId: string,
newStyle: string | undefined
) => {
if (!newStyle) return;
runInAction(() => {
const styles = this.styleSelectableDimensions.map(
(style) => style.selectedId || ""
);
styles[layerIndex] = newStyle;
this.setTrait(stratumId, "styles", styles.join(","));
});
},
// There is no way of finding out default style if no style has been selected :(
// To use the default style, we just send empty "styles" to WMS server
// But if the server doesn't support GetLegendGraphic, then we can't request the default legend
// Therefore - we only add the "Default style" / undefined option if supportsGetLegendGraphic is true
allowUndefined: this.supportsGetLegendGraphic && options.length > 1,
undefinedLabel: i18next.t(
"models.webMapServiceCatalogItem.defaultStyleLabel"
),
disable: this.isShowingDiff
};
});
}
get wmsDimensionSelectableDimensions(): SelectableDimensionEnum[] {
const dimensions: SelectableDimensionEnum[] = [];
// For each layer -> For each dimension
this.availableDimensions.forEach((layer) => {
layer.dimensions.forEach((dim) => {
// Only add dimensions if hasn't already been added (multiple layers may have the same dimension)
if (
!isDefined(dim.name) ||
dim.values.length < 2 ||
dimensions.findIndex((findDim) => findDim.name === dim.name) !== -1
) {
return;
}
dimensions.push({
name: dim.name,
id: `${this.uniqueId}-${dim.name}`,
options: dim.values.map((value) => {
let name = value;
// Add units and unitSybol if defined
if (typeof dim.units === "string" && dim.units !== "") {
if (typeof dim.unitSymbol === "string" && dim.unitSymbol !== "") {
name = `${value} (${dim.units} ${dim.unitSymbol})`;
} else {
name = `${value} (${dim.units})`;
}
}
return {
name,
id: value
};
}),
// Set selectedId to value stored in `dimensions` trait, the default value, or the first available value
selectedId:
this.dimensions?.[dim.name]?.toString() ||
dim.default ||
dim.values[0],
setDimensionValue: (
stratumId: string,
newDimension: string | undefined
) => {
let newDimensions: any = {};
newDimensions[dim.name!] = newDimension;
if (isDefined(this.dimensions)) {
newDimensions = combine(newDimensions, this.dimensions);
}
runInAction(() => {
this.setTrait(stratumId, "dimensions", newDimensions);
});
}
});
});
});
return dimensions;
}
get selectableDimensions() {
if (this.disableDimensionSelectors) {
return super.selectableDimensions;
}
return filterOutUndefined([
...super.selectableDimensions,
...this.wmsDimensionSelectableDimensions,
...this.styleSelectableDimensions
]);
}
/** If GetFeatureInfo/GetTimeseries request is returning CSV, we need to parse it into TimeSeriesFeatureInfoContext.
*/
get featureInfoContext(): (
feature: TerriaFeature
) => TimeSeriesFeatureInfoContext {
if (this.getFeatureInfoFormat.format !== "text/csv") return () => ({});
return csvFeatureInfoContext(this);
}
get getFeatureInfoFormats() {
const customFormat = this.getFeatureInfoFormat;
if (customFormat) {
if (customFormat.type === "json") {
return [
new GetFeatureInfoFormat("json", "application/json", (json) =>
geoJsonToFeatureInfoWithProject(json, this.tilingScheme.projection)
)
];
} else {
return [
new GetFeatureInfoFormat(customFormat.type, customFormat.format)
];
}
}
return [
new GetFeatureInfoFormat("json", "application/json", (json) =>
geoJsonToFeatureInfoWithProject(json, this.tilingScheme.projection)
),
new GetFeatureInfoFormat("xml", "text/xml"),
new GetFeatureInfoFormat("csv", "text/csv")
];
}
}
/**
* Add `_dim` prefix to dimensions for OWS (WMS, WCS...) excluding time, styles and elevation
*/
export function formatDimensionsForOws(
dimensions: { [key: string]: string } | undefined
) {
if (!isDefined(dimensions)) {
return {};
}
return Object.entries(dimensions).reduce<{ [key: string]: string }>(
(formattedDimensions, [key, value]) =>
// elevation is specified as simply "elevation", styles is specified as "styles"
// Other (custom) dimensions are prefixed with 'dim_'.
// See WMS 1.3.0 spec section C.3.2 and C.3.3.
{
formattedDimensions[
["time", "styles", "elevation"].includes(key?.toLowerCase())
? key
: `dim_${key}`
] = value;
return formattedDimensions;
},
{}
);
}
// Take the GetFeatureInfo response and reproject the feature coordinates to geographic if necessary, so the position is correct when displayed on the map.
// Cesium [GetFeatureInfoFormat](https://github.com/CesiumGS/cesium/blob/5754031f65646bee5f9d0e9a56dec7d3677a8b08/packages/engine/Source/Scene/GetFeatureInfoFormat.js#L74) assumes picked features is returned in geographic projection, so we need to convert them when tile scheme is not geographic.
function geoJsonToFeatureInfoWithProject(
json: FeatureCollection,
projection: MapProjection
) {
const result = [];
const features = json.features;
for (let i = 0; i < features.length; ++i) {
const feature = features[i];
const featureInfo = new ImageryLayerFeatureInfo();
featureInfo.data = feature;
featureInfo.properties = feature.properties;
featureInfo.configureNameFromProperties(feature.properties);
featureInfo.configureDescriptionFromProperties(feature.properties);
// If this is a point feature, use the coordinates of the point.
if (!!feature.geometry && feature.geometry.type === "Point") {
const x = feature.geometry.coordinates[0];
const y = feature.geometry.coordinates[1];
if (!projection || projection instanceof GeographicProjection) {
featureInfo.position = Cartographic.fromDegrees(x, y);
} else {
const positionInMeters = new Cartesian3(x, y, 0);
const cartographic = projection.unproject(positionInMeters);
featureInfo.position = cartographic;
}
}
result.push(featureInfo);
}
return result;
}
export default WebMapServiceCatalogItem;