UNPKG

terriajs

Version:

Geospatial data visualization platform.

318 lines (284 loc) 10.2 kB
import { createTransformer } from "mobx-utils"; import defined from "terriajs-cesium/Source/Core/defined"; import isReadOnlyArray from "../../../Core/isReadOnlyArray"; import loadXML from "../../../Core/loadXML"; import TerriaError, { networkRequestError } from "../../../Core/TerriaError"; import xml2json from "../../../ThirdParty/xml2json"; import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits"; import { CapabilitiesStyle, OnlineResource, OwsKeywordList } from "./OwsInterfaces"; import StratumFromTraits from "../../Definition/StratumFromTraits"; export interface CapabilitiesGeographicBoundingBox { readonly westBoundLongitude: number; readonly southBoundLatitude: number; readonly eastBoundLongitude: number; readonly northBoundLatitude: number; } export interface CapabilitiesLatLonBoundingBox { readonly minx: number; readonly miny: number; readonly maxx: number; readonly maxy: number; } export type CapabilitiesDimension = string & { readonly name: string; readonly units: string; readonly unitSymbol?: string; readonly default?: string; readonly text?: string; readonly multipleValues?: boolean; readonly nearestValue?: boolean; readonly current?: boolean; }; export type CapabilitiesExtent = string & { readonly name: string; readonly default?: string; readonly multipleValues?: boolean; readonly nearestValues?: boolean; readonly current?: boolean; }; export interface MetadataURL { readonly OnlineResource?: OnlineResource; readonly type?: string; } export interface CapabilitiesLayer { readonly _parent?: CapabilitiesLayer; /** ### Adapted from WMS 1.3.0 spec: * * #### 7.2.4.2 * * A number of elements have both a `<Name>` and a `<Title>`. * The `Name` is a text string used for machine-to-machine communication while the `Title` is for the benefit of humans. * * #### 7.2.4.6.3 * * If, and only if, a layer has a `<Name>`, then it is a map layer that can be requested by using that `Name` in the `LAYERS` parameter of a GetMap request. * If the layer has a `Title` but no `Name`, then that layer is only a category title for all the layers nested within. * A client shall not attempt to request a layer that has a `Title` but no `Name`. */ readonly Name?: string; readonly Title: string; readonly Abstract?: string; readonly MetadataURL?: MetadataURL | ReadonlyArray<MetadataURL>; readonly EX_GeographicBoundingBox?: CapabilitiesGeographicBoundingBox; // WMS 1.3.0 readonly LatLonBoundingBox?: CapabilitiesLatLonBoundingBox; // WMS 1.0.0-1.1.1 readonly Style?: CapabilitiesStyle | ReadonlyArray<CapabilitiesStyle>; readonly Layer?: CapabilitiesLayer | ReadonlyArray<CapabilitiesLayer>; readonly Dimension?: | CapabilitiesDimension | ReadonlyArray<CapabilitiesDimension>; // WMS 1.1.1 puts dimension values in an Extent element instead of directly in the Dimension element. readonly Extent?: CapabilitiesExtent | ReadonlyArray<CapabilitiesExtent>; readonly CRS?: string | string[]; // WMS 1.3.0 readonly SRS?: string | string[]; // WMS 1.1.1 } export interface CapabilitiesService { /** Title of the service. */ readonly Title?: string; /** Longer narative description of the service. */ readonly Abstract?: string; /** Information about a contact person for the service. */ readonly ContactInformation?: CapabilitiesContactInformation; /** Fees for this service */ readonly Fees?: string; /** Access contraints for this service. */ readonly AccessConstraints?: string; /** List of keywords or keyword phrases to help catalog searching. */ readonly KeywordList?: OwsKeywordList; } /** * Information about a contact person for the service. */ export interface CapabilitiesContactInformation { ContactPersonPrimary?: ContactInformationContactPersonPrimary; ContactPosition?: string; ContactAddress?: ContactInformationContactAddress; ContactVoiceTelephone?: string; ContactFacsimileTelephone?: string; ContactElectronicMailAddress?: string; } export interface ContactInformationContactPersonPrimary { ContactPerson?: string; ContactOrganization?: string; } export interface ContactInformationContactAddress { AddressType?: string; Address?: string; City?: string; StateOrProvince?: string; PostCode?: string; Country?: string; } type ElementTypeIfArray<T> = T extends ReadonlyArray<infer U> ? U : T; type Mutable<T> = { -readonly [P in keyof T]: T[P] }; export function getRectangleFromLayer( layer: CapabilitiesLayer ): StratumFromTraits<RectangleTraits> | undefined { var egbb = layer.EX_GeographicBoundingBox; // required in WMS 1.3.0 if (egbb) { return { west: egbb.westBoundLongitude, south: egbb.southBoundLatitude, east: egbb.eastBoundLongitude, north: egbb.northBoundLatitude }; } else { var llbb = layer.LatLonBoundingBox; // required in WMS 1.0.0 through 1.1.1 if (llbb) { return { west: llbb.minx, south: llbb.miny, east: llbb.maxx, north: llbb.maxy }; } } return undefined; } export default class WebMapServiceCapabilities { static fromUrl: (url: string) => Promise<WebMapServiceCapabilities> = createTransformer((url: string) => { return Promise.resolve(loadXML(url)).then(function (capabilitiesXml) { const json = xml2json(capabilitiesXml); if (!defined(json.Capability)) { throw networkRequestError({ title: "Invalid GetCapabilities", message: `The URL ${url} was retrieved successfully but it does not appear to be a valid Web Map Service (WMS) GetCapabilities document.` + `\n\nEither the catalog file has been set up incorrectly, or the server address has changed.` }); } return new WebMapServiceCapabilities(capabilitiesXml, json); }); }); get Service(): CapabilitiesService { return this.json.Service; } readonly rootLayers: CapabilitiesLayer[]; readonly allLayers: CapabilitiesLayer[]; readonly topLevelNamedLayers: CapabilitiesLayer[]; readonly layersByName: { readonly [name: string]: CapabilitiesLayer; }; readonly layersByTitle: { readonly [name: string]: CapabilitiesLayer; }; private constructor(readonly xml: XMLDocument, readonly json: any) { this.allLayers = []; this.rootLayers = []; this.topLevelNamedLayers = []; this.layersByName = {}; this.layersByTitle = {}; const allLayers = this.allLayers; const rootLayers = this.rootLayers; const topLevelNamedLayers = this.topLevelNamedLayers; const layersByName: { [name: string]: CapabilitiesLayer } = this.layersByName; const layersByTitle: { [name: string]: CapabilitiesLayer } = this.layersByTitle; function traverseLayer( layer: Mutable<CapabilitiesLayer>, isTopLevel: boolean = false, parent?: CapabilitiesLayer | undefined ) { allLayers.push(layer); if (layer.Name) { layersByName[layer.Name] = layer; if (isTopLevel) { topLevelNamedLayers.push(layer); isTopLevel = false; } } if (layer.Title) { layersByTitle[layer.Title] = layer; } layer._parent = parent; const layers = layer.Layer; if (isReadOnlyArray(layers)) { for (let i = 0; i < layers.length; ++i) { traverseLayer(layers[i], isTopLevel, layer); } } else if (layers !== undefined) { traverseLayer(layers, isTopLevel, layer); } } if (json.Capability && json.Capability.Layer) { const layerElements = json.Capability.Layer; if (Array.isArray(layerElements)) { rootLayers.push(...layerElements); } else { rootLayers.push(layerElements); } rootLayers.forEach((layer) => traverseLayer(layer, true)); } } /** * Finds the layer in GetCapabilities corresponding to a given layer name. Names are * resolved as follows: * * The layer has the exact name specified. * * The layer name matches the name in the spec if the namespace portion is removed. * * The name in the spec matches the title of the layer. * * @param {String} name The layer name to resolve. * @returns {CapabilitiesLayer} The resolved layer, or `undefined` if the layer name could not be resolved. */ findLayer(name: string): CapabilitiesLayer { // Look for an exact match on the name. let match = this.layersByName[name]; if (!match) { const colonIndex = name.indexOf(":"); if (colonIndex >= 0) { // This looks like a namespaced name. Such names will (usually?) show up in GetCapabilities // as just their name without the namespace qualifier. const nameWithoutNamespace = name.substring(colonIndex + 1); match = this.layersByName[nameWithoutNamespace]; } } if (!match) { // Try matching by title. match = this.layersByTitle[name]; } return match; } /** * Gets the ancestry of a layer. The returned array has the layer itself at position 0, its parent * layer at position 1, and so on until the root of the layer hierarchy. * * @param layer The layer for which to obtain ancestry. * @returns The ancestry of the layer. */ getLayerAncestry( layer: CapabilitiesLayer | undefined ): ReadonlyArray<CapabilitiesLayer> { const result = []; while (layer) { result.push(layer); layer = layer._parent; } return result; } getInheritedValues<K extends keyof CapabilitiesLayer>( layer: CapabilitiesLayer, property: K ): ReadonlyArray< ElementTypeIfArray<Exclude<CapabilitiesLayer[K], undefined>> > { type TResultElement = ElementTypeIfArray< Exclude<CapabilitiesLayer[K], undefined> >; type TResultArray = TResultElement[]; const values = this.getLayerAncestry(layer).reduce((p: TResultArray, c) => { const value = c[property]; if (Array.isArray(value)) { p.push(...value); } else if (value !== undefined) { p.push(<TResultElement>value); } return p; }, []); return values; } }