terriajs
Version:
Geospatial data visualization platform.
239 lines (218 loc) • 8.58 kB
text/typescript
import { createTransformer } from "mobx-utils";
import defined from "terriajs-cesium/Source/Core/defined";
import isDefined from "../../../Core/isDefined";
import loadXML from "../../../Core/loadXML";
import TerriaError from "../../../Core/TerriaError";
import xml2json from "../../../ThirdParty/xml2json";
import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits";
import StratumFromTraits from "../../Definition/StratumFromTraits";
import {
CapabilitiesGeographicBoundingBox,
CapabilitiesService
} from "./WebMapServiceCapabilities";
import { isJsonString } from "../../../Core/Json";
export interface FeatureType {
readonly Name?: string;
readonly Title: string;
readonly Abstract?: string;
readonly WGS84BoundingBox?: CapabilitiesGeographicBoundingBox;
readonly Keywords?: string | string[];
readonly OutputFormats?: string[];
}
export function getRectangleFromLayer(
layer: FeatureType
): StratumFromTraits<RectangleTraits> | undefined {
var bbox = layer.WGS84BoundingBox;
if (bbox) {
return {
west: bbox.westBoundLongitude,
south: bbox.southBoundLatitude,
east: bbox.eastBoundLongitude,
north: bbox.northBoundLatitude
};
}
return undefined;
}
/**
* Get CapabilitiesService (in WMS form)
*/
function getService(json: any): CapabilitiesService {
const serviceProviderJson = json["ServiceProvider"];
const serviceIdentificationJson = json["ServiceIdentification"];
const serviceAddressJson =
serviceProviderJson?.["ServiceContact"]?.["ContactInfo"]?.["Address"];
const service: CapabilitiesService = {
Title: serviceIdentificationJson?.["Title"],
Abstract: serviceIdentificationJson?.["Abstract"],
Fees: serviceIdentificationJson?.["Fees"],
AccessConstraints: serviceIdentificationJson?.["AccessConstraints"],
KeywordList: {
Keyword: serviceIdentificationJson?.["Keywords"]?.["Keyword"]
},
ContactInformation: {
ContactPersonPrimary: {
ContactPerson:
serviceProviderJson?.["ServiceContact"]?.["IndividualName"],
ContactOrganization: serviceProviderJson?.["ProviderName"]
},
ContactPosition:
serviceProviderJson?.["ServiceContact"]?.["PositionName"],
ContactAddress: {
Address: serviceAddressJson?.["DeliveryPoint"],
City: serviceAddressJson?.["City"],
StateOrProvince: serviceAddressJson?.["AdministrativeArea"],
PostCode: serviceAddressJson?.["PostalCode"],
Country: serviceAddressJson?.["Country"]
},
ContactVoiceTelephone:
serviceProviderJson?.["ServiceContact"]?.["ContactInfo"]?.["Phone"]?.[
"Voice"
],
ContactFacsimileTelephone:
serviceProviderJson?.["ServiceContact"]?.["ContactInfo"]?.["Phone"]?.[
"Facsimile"
],
ContactElectronicMailAddress:
serviceProviderJson?.["ServiceContact"]?.["ContactInfo"]?.["Address"]?.[
"ElectronicMailAddress"
]
}
};
return service;
}
function getFeatureTypes(json: any): FeatureType[] {
let featureTypesJson = json.FeatureTypeList?.FeatureType as
| Array<any>
| string;
if (!isDefined(featureTypesJson)) {
return [];
}
if (!Array.isArray(featureTypesJson)) {
featureTypesJson = [featureTypesJson];
}
return (
featureTypesJson.map<FeatureType>((json: any) => {
const lowerCorner = json["WGS84BoundingBox"]?.["LowerCorner"].split(" ");
const upperCorner = json["WGS84BoundingBox"]?.["UpperCorner"].split(" ");
let outputFormats: string[] | undefined;
if (isDefined(json.OutputFormats)) {
outputFormats = Array.isArray(json.OutputFormats)
? json.OutputFormats.map((o: any) => o.Format)
: [json.OutputFormats.Format];
}
return {
Title: json.Title,
Name: json.Name,
Abstract: json.Abstract,
Keyword: json["Keywords"]?.["Keyword"],
WGS84BoundingBox: {
westBoundLongitude: lowerCorner && parseFloat(lowerCorner[0]),
southBoundLatitude: lowerCorner && parseFloat(lowerCorner[1]),
eastBoundLongitude: upperCorner && parseFloat(upperCorner[0]),
northBoundLatitude: upperCorner && parseFloat(upperCorner[1])
},
OutputFormats: outputFormats
};
}) || []
);
}
function getOutputTypes(json: any): string[] | undefined {
let outputTypes = json.OperationsMetadata?.Operation?.find(
(op: any) => op.name === "GetFeature"
)?.Parameter?.find((p: any) => p.name === "outputFormat")?.Value;
if (!isDefined(outputTypes)) {
return;
}
return Array.isArray(outputTypes) ? outputTypes : [outputTypes];
}
interface SrsNamesForLayer {
layerName: string;
srsArray: string[]; // First element is DefaultSRS
}
/**
* Get the coordinate systems (srsName) supported by the WFS service for each layer.
* @param json
* returns an object with an array of srsNames for each layer. The first element is the defaultSRS as specified by the WFS service.
* TODO: For catalog items that specify which layer we are interested in, why build the array describing the srsNames for all the other layers too?
*/
function getSrsNames(json: any): SrsNamesForLayer[] | undefined {
let layers = json.FeatureTypeList?.FeatureType;
let srsNamesByLayer: SrsNamesForLayer[] = [];
if (Array.isArray(layers)) {
srsNamesByLayer = layers.map(buildSrsNameObject);
} else {
srsNamesByLayer.push(buildSrsNameObject(json.FeatureTypeList?.FeatureType));
}
return srsNamesByLayer;
}
/**
* Helper function to build individual objects describing the allowable srsNames for each layer in the WFS
* @param layer
*/
function buildSrsNameObject(layer: any): SrsNamesForLayer {
let srsNames: string[] = [];
if (isJsonString(layer.DefaultSRS)) srsNames.push(layer.DefaultSRS);
if (Array.isArray(layer.OtherSRS))
layer.OtherSRS.forEach((item: string) => {
if (isJsonString(item)) srsNames.push(item);
});
else if (isJsonString(layer.OtherSRS)) srsNames.push(layer.OtherSRS);
return { layerName: layer.Name, srsArray: srsNames };
}
export default class WebFeatureServiceCapabilities {
static fromUrl: (url: string) => Promise<WebFeatureServiceCapabilities> =
createTransformer((url: string) => {
return loadXML(url).then(function (capabilitiesXml: any) {
const json = xml2json(capabilitiesXml);
if (!defined(json.ServiceIdentification)) {
throw new TerriaError({
title: "Invalid GetCapabilities",
message:
`The URL ${url} was retrieved successfully but it does not appear to be a valid Web Feature Service (WFS) GetCapabilities document.` +
`\n\nEither the catalog file has been set up incorrectly, or the server address has changed.`
});
}
return new WebFeatureServiceCapabilities(capabilitiesXml, json);
});
});
readonly service: CapabilitiesService;
readonly outputTypes: string[] | undefined;
readonly featureTypes: FeatureType[];
readonly srsNames: SrsNamesForLayer[] | undefined;
private constructor(xml: XMLDocument, json: any) {
this.service = getService(json);
this.outputTypes = getOutputTypes(json);
this.featureTypes = getFeatureTypes(json);
this.srsNames = getSrsNames(json);
}
/**
* Finds the layer in GetCapabilities corresponding to a given layer name. Names are
* resolved as foll
* * 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): FeatureType | undefined {
// Look for an exact match on the name.
let match = this.featureTypes.find((ft) => ft.Name === 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.featureTypes.find(
(ft) => ft.Name === nameWithoutNamespace
);
}
}
if (!match) {
// Try matching by title.
match = this.featureTypes.find((ft) => ft.Title === name);
}
return match;
}
}