UNPKG

terriajs

Version:

Geospatial data visualization platform.

1,138 lines (983 loc) 71.5 kB
'use strict'; /*global require*/ var URI = require('urijs'); var moment = require('moment'); var clone = require('terriajs-cesium/Source/Core/clone'); var combine = require('terriajs-cesium/Source/Core/combine'); var defaultValue = require('terriajs-cesium/Source/Core/defaultValue'); var defined = require('terriajs-cesium/Source/Core/defined'); var defineProperties = require('terriajs-cesium/Source/Core/defineProperties'); var Ellipsoid = require('terriajs-cesium/Source/Core/Ellipsoid'); var freezeObject = require('terriajs-cesium/Source/Core/freezeObject'); var GeographicTilingScheme = require('terriajs-cesium/Source/Core/GeographicTilingScheme'); var GetFeatureInfoFormat = require('terriajs-cesium/Source/Scene/GetFeatureInfoFormat'); var getToken = require('./getToken'); var JulianDate = require('terriajs-cesium/Source/Core/JulianDate'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var loadXML = require('../Core/loadXML'); var Rectangle = require('terriajs-cesium/Source/Core/Rectangle'); var TimeInterval = require('terriajs-cesium/Source/Core/TimeInterval'); var TimeIntervalCollection = require('terriajs-cesium/Source/Core/TimeIntervalCollection'); var UrlTemplateImageryProvider = require('terriajs-cesium/Source/Scene/UrlTemplateImageryProvider'); var WebMapServiceImageryProvider = require('terriajs-cesium/Source/Scene/WebMapServiceImageryProvider'); var WebMercatorTilingScheme = require('terriajs-cesium/Source/Core/WebMercatorTilingScheme'); var when = require('terriajs-cesium/Source/ThirdParty/when'); var containsAny = require('../Core/containsAny'); var Metadata = require('./Metadata'); var MetadataItem = require('./MetadataItem'); var TerriaError = require('../Core/TerriaError'); var ImageryLayerCatalogItem = require('./ImageryLayerCatalogItem'); var inherit = require('../Core/inherit'); var overrideProperty = require('../Core/overrideProperty'); var proxyCatalogItemUrl = require('./proxyCatalogItemUrl'); var unionRectangleArray = require('../Map/unionRectangleArray'); var xml2json = require('../ThirdParty/xml2json'); var LegendUrl = require('../Map/LegendUrl'); /** * A {@link ImageryLayerCatalogItem} representing a layer from a Web Map Service (WMS) server. * * @alias WebMapServiceCatalogItem * @constructor * @extends ImageryLayerCatalogItem * * @param {Terria} terria The Terria instance. */ var WebMapServiceCatalogItem = function(terria) { ImageryLayerCatalogItem.call(this, terria); this._rawMetadata = undefined; this._thisLayerInRawMetadata = undefined; this._allLayersInRawMetadata = undefined; this._metadata = undefined; this._getCapabilitiesUrl = undefined; this._rectangle = undefined; this._rectangleFromMetadata = undefined; this._intervalsFromMetadata = undefined; this._lastToken = undefined; this._newTokenRequestInFlight = undefined; /** * Gets or sets the WMS layers to include. To specify multiple layers, separate them * with a commas. This property is observable. * @type {String} */ this.layers = ''; /** * Gets or sets the comma-separated list of styles to request, one per layer list in {@link WebMapServiceCatalogItem#layers}. * This property is observable. * @type {String} */ this.styles = ''; /** * Gets or sets the additional parameters to pass to the WMS server when requesting images. * All parameter names must be entered in lowercase in order to be consistent with references in TerrisJS code. * If this property is undefined, {@link WebMapServiceCatalogItem.defaultParameters} is used. * @type {Object} */ this.parameters = {}; /** * Gets or sets the tiling scheme to pass to the WMS server when requesting images. * If this property is undefiend, the default tiling scheme of the provider is used. * @type {Object} */ this.tilingScheme = undefined; /** * Gets or sets the formats in which to try WMS GetFeatureInfo requests. If this property is undefined, the `WebMapServiceImageryProvider` defaults * are used. This property is observable. * @type {GetFeatureInfoFormat[]} */ this.getFeatureInfoFormats = undefined; /** * Gets or sets a value indicating whether a time dimension, if it exists in GetCapabilities, should be used to populate * the {@link ImageryLayerCatalogItem#intervals}. If the {@link ImageryLayerCatalogItem#intervals} property is set explicitly * on this catalog item, the value of this property is ignored. * @type {Boolean} * @default true */ this.populateIntervalsFromTimeDimension = true; /** * Gets or sets the denominator of the largest scale (smallest denominator) for which tiles should be requested. For example, if this value is 1000, then tiles representing * a scale larger than 1:1000 (i.e. numerically smaller denominator, when zooming in closer) will not be requested. Instead, tiles of the largest-available scale, as specified by this property, * will be used and will simply get blurier as the user zooms in closer. * @type {Number} */ this.minScaleDenominator = undefined; /** * Gets or sets the maximum number of intervals that can be created by a single * date range, when specified in the form time/time/periodicity. * eg. 2015-04-27T16:15:00/2015-04-27T18:45:00/PT15M has 11 intervals * @type {Number} */ this.maxRefreshIntervals = 1000; /** * Gets or sets whether this WMS has been identified as being provided by a GeoServer. * @type {Boolean} */ this.isGeoServer = undefined; /** * Gets or sets whether this WMS has been identified as being provided by an Esri ArcGIS MapServer. No assumption is made about where an ArcGIS MapServer endpoint also exists. * @type {Boolean} */ this.isEsri = undefined; /** * Gets or sets whether this WMS has been identified as being provided by ncWMS. * @type {Boolean} */ this.isNcWMS = undefined; /** * Gets or sets whether this WMS server has been identified as supporting the COLORSCALERANGE parameter. * @type {Boolean} */ this.supportsColorScaleRange = undefined; /** * Gets or sets how many seconds time-series data with a start date but no end date should last, in seconds. * @type {Number} */ this.displayDuration = undefined; /** * Gets or sets a value indicating whether the user's ability to change the display properties of this * catalog item is disabled. For example, if true, {@link WebMapServiceCatalogItem#styles} should not be * changeable through the user interface. * This property is observable. * @type {Boolean} * @default false */ this.disableUserChanges = false; /** * Gets or sets the available styles for each selected layer in {@link WebMapServiceCatalogItem#layers}. If undefined, * this property is automatically populated from the WMS GetCapabilities on load. This property is an object that has a * property named for each layer. The value of the property is an array where each element in the array is a style supported * by the layer. The style has `name`, `title`, `abstract`, and `legendUrl` properties. * This property is observable. * @type {Object} * @example * wmsItem.availableStyles = { * 'FVCOM-NECOFS-GOM3/x': [ * { * name: 'default-scalar/default', * title: 'default-scalar/default', * abstract: 'default-scalar style, using the default palette.', * legendUrl: new LegendUrl('http://www.smast.umassd.edu:8080/ncWMS2/wms?REQUEST=GetLegendGraphic&PALETTE=default&COLORBARONLY=true&WIDTH=110&HEIGHT=264', 'image/png') * } * ] * }; */ this.availableStyles = undefined; /** * Gets or sets the minumum of the color scale range. Because COLORSCALERANGE is a non-standard * property supported by ncWMS servers, this property is ignored unless {@link WebMapServiceCatalogItem#supportsColorScaleRange} * is true. {@link WebMapServiceCatalogItem#colorScaleMaximum} must be set as well. * @type {Number} */ this.colorScaleMinimum = undefined; /** * Gets or sets the maximum of the color scale range. Because COLORSCALERANGE is a non-standard * property supported by ncWMS servers, this property is ignored unless {@link WebMapServiceCatalogItem#supportsColorScaleRange} * is true. {@link WebMapServiceCatalogItem#colorScaleMinimum} must be set as well. * @type {Number} */ this.colorScaleMaximum = undefined; /** * Gets or sets the list of additional dimensions (e.g. elevation) and their possible values available from the * WMS server. If undefined, this property is automatically populated from the WMS GetCapabilities on load. * This property is an object that has a property named for each layer. The value of the property is an array * of dimensions available for this layer. A dimension has the fields shown in the example below. See the * WMS 1.3.0 specification, section C.2, for a description of the fields. All fields are optional except * `name` and `options`. This property is observable. * @type {Object} * @example * wmsItem.availableDimensions = { * mylayer: [ * { * name: 'elevation', * units: 'CRS:88', * unitSymbol: 'm', * default: -0.03125, * multipleValues: false, * nearestValue: false, * options: [ * -0.96875, * -0.90625, * -0.84375, * -0.78125, * -0.71875, * -0.65625, * -0.59375, * -0.53125, * -0.46875, * -0.40625, * -0.34375, * -0.28125, * -0.21875, * -0.15625, * -0.09375, * -0.03125 * ] * } * ] * }; */ this.availableDimensions = undefined; /** * Gets or sets the selected values for dimensions available for this WMS layer. The value of this property is * an object where each key is the name of a dimension and each value is the value to use for that dimension. * Note that WMS does not allow dimensions to be explicitly specified per layer. So the selected dimension values are * applied to all layers with a corresponding dimension. * This property is observable. * @type {Object} * @example * wmsItem.dimensions = { * elevation: -0.65625 * }; */ this.dimensions = undefined; /** * Gets or sets the URL to use for requesting tokens. Typically, this is set to `/esri-token-auth` to use * the ArcGIS token mechanism built into terriajs-server. * @type {String} */ this.tokenUrl = undefined; /** * Gets or sets the name of the URL query parameter used to provide the token * to the server. This property is ignored if {@link WebMapServiceCatalogItem#tokenUrl} is undefined. * @type {String} * @default 'token' */ this.tokenParameterName = 'token'; /** * Gets or sets the set of HTTP status codes that indicate that a token is invalid. * This property is ignored if {@link WebMapServiceCatalogItem#tokenUrl} is undefined. * @type {Number[]} * @default [401, 498, 499] */ this.tokenInvalidHttpCodes = [401, 498, 499]; this._sourceInfoItemNames = ['GetCapabilities URL']; knockout.track(this, [ '_getCapabilitiesUrl', '_rectangle', '_rectangleFromMetadata', '_intervalsFromMetadata', 'layers', 'styles', 'parameters', 'getFeatureInfoFormats', 'tilingScheme', 'populateIntervalsFromTimeDimension', 'minScaleDenominator', 'disableUserChanges', 'availableStyles', 'colorScaleMinimum', 'colorScaleMaximum', 'availableDimensions', 'dimensions', 'tokenUrl', 'tokenParameterName', 'tokenInvalidHttpCodes', '_lastToken', '_thisLayerInRawMetadata', '_allLayersInRawMetadata']); // getCapabilitiesUrl and legendUrl are derived from url if not explicitly specified. overrideProperty(this, 'getCapabilitiesUrl', { get: function() { if (defined(this._getCapabilitiesUrl)) { return this._getCapabilitiesUrl; } if (defined(this.metadataUrl)) { return this.metadataUrl; } if (!defined(this.url)) { return undefined; } return cleanUrl(this.url) + '?service=WMS&version=1.3.0&request=GetCapabilities'; }, set: function(value) { this._getCapabilitiesUrl = value; } }); var legendUrlsBase = Object.getOwnPropertyDescriptor(this, 'legendUrls'); overrideProperty(this, 'legendUrls', { get : function() { if (defined(this._legendUrls)) { return this._legendUrls; } else if (defined(this._legendUrl)) { return [this._legendUrl]; } else { return computeLegendUrls(this); } }, set : function(value) { legendUrlsBase.set.call(this, value); } }); // The dataUrl must be explicitly specified. Don't try to use `url` as the the dataUrl, because it won't work for a WMS URL. overrideProperty(this, 'dataUrl', { get : function() { return this._dataUrl; }, set : function(value) { this._dataUrl = value; } }); overrideProperty(this, 'dataUrlType', { get : function() { return this._dataUrlType; }, set : function(value) { this._dataUrlType = value; } }); }; inherit(ImageryLayerCatalogItem, WebMapServiceCatalogItem); defineProperties(WebMapServiceCatalogItem.prototype, { /** * Gets the type of data item represented by this instance. * @memberOf WebMapServiceCatalogItem.prototype * @type {String} */ type : { get : function() { return 'wms'; } }, /** * Gets a human-readable name for this type of data source, 'Web Map Service (WMS)'. * @memberOf WebMapServiceCatalogItem.prototype * @type {String} */ typeName : { get : function() { return 'Web Map Service (WMS)'; } }, /** * Gets a value indicating whether this {@link ImageryLayerCatalogItem} supports the {@link ImageryLayerCatalogItem#intervals} * property for configuring time-dynamic imagery. * @type {Boolean} */ supportsIntervals : { get : function() { return true; } }, /** * Gets the metadata associated with this data source and the server that provided it, if applicable. * @memberOf WebMapServiceCatalogItem.prototype * @type {Metadata} */ metadata : { get : function() { if (!defined(this._metadata)) { this._metadata = requestMetadata(this); } return this._metadata; } }, /** * Gets the set of functions used to update individual properties in {@link CatalogMember#updateFromJson}. * When a property name in the returned object literal matches the name of a property on this instance, the value * will be called as a function and passed a reference to this instance, a reference to the source JSON object * literal, and the name of the property. * @memberOf WebMapServiceCatalogItem.prototype * @type {Object} */ updaters : { get : function() { return WebMapServiceCatalogItem.defaultUpdaters; } }, /** * Gets the set of functions used to serialize individual properties in {@link CatalogMember#serializeToJson}. * When a property name on the model matches the name of a property in the serializers object literal, * the value will be called as a function and passed a reference to the model, a reference to the destination * JSON object literal, and the name of the property. * @memberOf WebMapServiceCatalogItem.prototype * @type {Object} */ serializers : { get : function() { return WebMapServiceCatalogItem.defaultSerializers; } }, /** * Gets the set of names of the properties to be serialized for this object when {@link CatalogMember#serializeToJson} is called * for a share link. * @memberOf WebMapServiceCatalogItem.prototype * @type {String[]} */ propertiesForSharing : { get : function() { return WebMapServiceCatalogItem.defaultPropertiesForSharing; } }, /** * Gets the title of each of the layers in {@link WebMapServiceCatalogItem#layers}. If the layer * titles are not yet known (because GetCapabilities has not been loaded yet, for example), this * property will return undefined. * @memberOf ImageryLayerCatalogItem.prototype * @type {String[]} */ layerTitles : { get : function() { if (!defined(this._allLayersInRawMetadata)) { return undefined; } return this._allLayersInRawMetadata.map(function(layer) { return layer.Title || layer.Name; }); } } }); WebMapServiceCatalogItem.defaultUpdaters = clone(ImageryLayerCatalogItem.defaultUpdaters); WebMapServiceCatalogItem.defaultUpdaters.tilingScheme = function(wmsItem, json, propertyName, options) { if (json.tilingScheme === 'geographic') { wmsItem.tilingScheme = new GeographicTilingScheme(); } else if (json.tilingScheme === 'web-mercator') { wmsItem.tilingScheme = new WebMercatorTilingScheme(); } else { wmsItem.tilingScheme = json.tilingScheme; } }; WebMapServiceCatalogItem.defaultUpdaters.getFeatureInfoFormats = function(wmsItem, json, propertyName, options) { var formats = []; for (var i = 0; i < json.getFeatureInfoFormats.length; ++i) { var format = json.getFeatureInfoFormats[i]; formats.push(new GetFeatureInfoFormat(format.type, format.format)); } wmsItem.getFeatureInfoFormats = formats; }; freezeObject(WebMapServiceCatalogItem.defaultUpdaters); WebMapServiceCatalogItem.defaultSerializers = clone(ImageryLayerCatalogItem.defaultSerializers); // Serialize the underlying properties instead of the public views of them. WebMapServiceCatalogItem.defaultSerializers.getCapabilitiesUrl = function(wmsItem, json, propertyName) { json.getCapabilitiesUrl = wmsItem._getCapabilitiesUrl; }; WebMapServiceCatalogItem.defaultSerializers.tilingScheme = function(wmsItem, json, propertyName) { if (wmsItem.tilingScheme instanceof GeographicTilingScheme) { json.tilingScheme = 'geographic'; } else if (wmsItem.tilingScheme instanceof WebMercatorTilingScheme) { json.tilingScheme = 'web-mercator'; } else { json.tilingScheme = wmsItem.tilingScheme; } }; // Do not serialize availableDimensions, availableStyles, intervals, description, info - these can be huge and can be recovered from the server. // Normally when you share a WMS item, it is inside a WMS group, and when CatalogGroups are shared, they share their contents applying the // CatalogMember.propertyFilters.sharedOnly filter, which only shares the "propertiesForSharing". // However, if you create a straight WMS item outside a group (eg. by duplicating it), then share it, it will serialize everything it can. WebMapServiceCatalogItem.defaultSerializers.availableDimensions = function() { }; WebMapServiceCatalogItem.defaultSerializers.availableStyles = function() { }; WebMapServiceCatalogItem.defaultSerializers.intervals = function() { }; WebMapServiceCatalogItem.defaultSerializers.description = function() { }; WebMapServiceCatalogItem.defaultSerializers.info = function() { }; freezeObject(WebMapServiceCatalogItem.defaultSerializers); /** * Gets or sets the default set of properties that are serialized when serializing a {@link CatalogItem}-derived object * for a share link. * @type {String[]} */ WebMapServiceCatalogItem.defaultPropertiesForSharing = clone(ImageryLayerCatalogItem.defaultPropertiesForSharing); WebMapServiceCatalogItem.defaultPropertiesForSharing.push('styles'); WebMapServiceCatalogItem.defaultPropertiesForSharing.push('colorScaleMinimum'); WebMapServiceCatalogItem.defaultPropertiesForSharing.push('colorScaleMaximum'); WebMapServiceCatalogItem.defaultPropertiesForSharing.push('dimensions'); freezeObject(WebMapServiceCatalogItem.defaultPropertiesForSharing); /** * 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. * @type {Array} */ WebMapServiceCatalogItem.abstractsToIgnore = [ 'A compliant implementation of WMS' ]; WebMapServiceCatalogItem.getAllAvailableStylesFromCapabilities = function(capabilities, layers, result, inheritedStyles) { if (!defined(result)) { result = {}; layers = capabilities && capabilities.Capability ? capabilities.Capability.Layer : []; } if (!defined(layers)) { return result; } layers = Array.isArray(layers) ? layers : [layers]; for (var i = 0; i < layers.length; ++i) { var layer = layers[i]; var styles = WebMapServiceCatalogItem.getSingleLayerStylesFromCapabilities(layer, inheritedStyles); if (defined(layer.Name) && layer.Name.length > 0) { result[layer.Name] = styles; } WebMapServiceCatalogItem.getAllAvailableStylesFromCapabilities(capabilities, layer.Layer, result, styles); } return result; }; WebMapServiceCatalogItem.getSingleLayerStylesFromCapabilities = function(layerInCapabilities, inheritedStyles) { inheritedStyles = inheritedStyles || []; if (!defined(layerInCapabilities) || !defined(layerInCapabilities.Style)) { return inheritedStyles; } var styles = Array.isArray(layerInCapabilities.Style) ? layerInCapabilities.Style : [layerInCapabilities.Style]; return inheritedStyles.concat(styles.map(function(style) { var legendUrl = Array.isArray(style.LegendURL) ? style.LegendURL[0] : style.LegendURL; var legendUri, legendMimeType; if (legendUrl && legendUrl.OnlineResource && legendUrl.OnlineResource['xlink:href']) { legendUri = new URI(decodeURIComponent(legendUrl.OnlineResource['xlink:href'])); legendMimeType = legendUrl.Format; } return { name: style.Name, title: style.Title, abstract: style.Abstract, legendUri: legendUri ? new LegendUrl(legendUri.toString(), legendMimeType) : undefined }; })); }; WebMapServiceCatalogItem.getAllAvailableDimensionsFromCapabilities = function(capabilities, layers, result, inheritedDimensions) { if (!defined(result)) { result = {}; layers = capabilities && capabilities.Capability ? capabilities.Capability.Layer : []; } if (!defined(layers)) { return result; } layers = Array.isArray(layers) ? layers : [layers]; for (var i = 0; i < layers.length; ++i) { var layer = layers[i]; var dimensions = WebMapServiceCatalogItem.getSingleLayerDimensionsFromCapabilities(layer, inheritedDimensions); if (defined(layer.Name) && layer.Name.length > 0) { result[layer.Name] = dimensions; } WebMapServiceCatalogItem.getAllAvailableDimensionsFromCapabilities(capabilities, layer.Layer, result, dimensions); } return result; }; WebMapServiceCatalogItem.getSingleLayerDimensionsFromCapabilities = function(layerInCapabilities, inheritedDimensions) { inheritedDimensions = inheritedDimensions || []; if (!defined(layerInCapabilities) || !defined(layerInCapabilities.Dimension)) { return inheritedDimensions; } var dimensions = Array.isArray(layerInCapabilities.Dimension) ? layerInCapabilities.Dimension : [layerInCapabilities.Dimension]; // WMS 1.1.1 puts dimension values in an Extent element instead of directly in the Dimension element. var extents = layerInCapabilities.Extent ? (Array.isArray(layerInCapabilities.Extent) ? layerInCapabilities.Extent : [layerInCapabilities.Extent]) : []; // Filter out inherited dimensions that are duplicated here. Child layer dimensions override parent layer dimensions. inheritedDimensions = inheritedDimensions.filter(inheritedDimension => dimensions.filter(dimension => dimension.name === inheritedDimension.name).length === 0); return inheritedDimensions.concat(dimensions.map(dimension => { var correspondingExtent = extents.filter(extent => extent.name === dimension.name)[0]; var options; if (correspondingExtent && correspondingExtent.split) { options = correspondingExtent.split(','); } else if (dimension.split) { options = dimension.split(','); } else { options = []; } return { name: dimension.name, units: dimension.units, unitSymbol: dimension.unitSymbol, default: dimension.default, multipleValues: dimension.multipleValues, nearestValue: dimension.nearestValue, options: options }; })); }; /** * Updates this catalog item from a WMS GetCapabilities document. * @param {Object|XMLDocument} capabilities The capabilities document. This may be a JSON object or an XML document. If it * is a JSON object, each layer is expected to have a `_parent` property with a reference to its * parent layer. * @param {Boolean} [overwrite=false] True to overwrite existing property values with data from the capabilities; false to * preserve any existing values. * @param {Object} [thisLayer] A reference to this layer within the JSON capabilities object. If this parameter is not * specified or if `capabilities` is an XML document, the layer is found automatically based on this * catalog item's `layers` property. * @param {Object} [infoDerivedFromCapabilities] Additional information already derived from the GetCapabilities document, including: * @param {Object} [infoDerivedFromCapabilities.availableStyles] The available styles from this WMS server, structured as in the * {@link WebMapServiceCatalogItem#availableStyles} property. * @param {Object} [infoDerivedFromCapabilities.availableDimensions] The available dimensions from this WMS server, structured as in * the {@link WebMapServiceCatalogItem#availableDimensions} property. */ WebMapServiceCatalogItem.prototype.updateFromCapabilities = function(capabilities, overwrite, thisLayer, infoDerivedFromCapabilities) { if (defined(capabilities.documentElement)) { capabilities = capabilitiesXmlToJson(this, capabilities); thisLayer = undefined; } if (!defined(this.availableStyles)) { if (defined(infoDerivedFromCapabilities) && defined(infoDerivedFromCapabilities.availableStyles)) { this.availableStyles = infoDerivedFromCapabilities.availableStyles; } else { this.availableStyles = WebMapServiceCatalogItem.getAllAvailableStylesFromCapabilities(capabilities); } } if (!defined(this.availableDimensions)) { if (defined(infoDerivedFromCapabilities) && defined(infoDerivedFromCapabilities.availableDimensions)) { this.availableDimensions = infoDerivedFromCapabilities.availableDimensions; } else { this.availableDimensions = WebMapServiceCatalogItem.getAllAvailableDimensionsFromCapabilities(capabilities); } } if (!defined(this.isGeoServer) && capabilities && capabilities.Service && capabilities.Service.KeywordList && capabilities.Service.KeywordList.Keyword && capabilities.Service.KeywordList.Keyword.indexOf('GEOSERVER') >= 0) { this.isGeoServer = true; } if (!defined(this.isEsri) && defined(capabilities["xmlns:esri_wms"]) || this.url.match(/\/MapServer\//)) { this.isEsri = true; } if (!defined(this.isNcWMS) && capabilities && capabilities.Capability && capabilities.Capability.Layer) { var myLayer = findLayers(capabilities.Capability.Layer, this.layers); if (defined(myLayer) && myLayer.length > 0) { myLayer = myLayer[0]; if (myLayer && myLayer.Style && myLayer.Style.length > 0) { for (var j = 0; j < myLayer.Style.length; ++j) { if (!defined(this.isNcWMS) && myLayer.Style[j].Name && (myLayer.Style[j].Name.match(/boxfill\/rainbow/i) || myLayer.Style[j].Name.match(/default-scalar\/default/i) || myLayer.Style[j].Name.match(/default-vector\/default/i))) { this.isNcWMS = true; } } } } } if (!defined(this.supportsColorScaleRange)) { this.supportsColorScaleRange = this.isNcWMS; if (!this.supportsColorScaleRange) { var hasExtendedRequests = capabilities.Capability && capabilities.Capability.ExtendedCapabilities && capabilities.Capability.ExtendedCapabilities.ExtendedRequest; if (hasExtendedRequests) { var extendedRequests = capabilities.Capability.ExtendedCapabilities.ExtendedRequest; extendedRequests = Array.isArray(extendedRequests) ? extendedRequests : [extendedRequests]; var extendedGetMap = extendedRequests.filter(request => request.Request === 'GetMap')[0]; if (extendedGetMap) { var urlParameters = Array.isArray(extendedGetMap.UrlParameter) ? extendedGetMap.UrlParameter : [extendedGetMap.UrlParameter]; var colorScaleRangeParameter = urlParameters.filter(parameter => parameter.ParameterName === 'COLORSCALERANGE')[0]; this.supportsColorScaleRange = defined(colorScaleRangeParameter); } } } } if (!defined(thisLayer)) { thisLayer = findLayers(capabilities.Capability.Layer, this.layers); if (defined(this.layers)) { var layers = this.layers.split(','); var styles = (this.styles || this.parameters.styles || '').split(','); for (var i = 0; i < thisLayer.length; ++i) { if (!defined(thisLayer[i])) { if (thisLayer.length > 1) { console.log('A layer with the name or ID \"' + layers[i] + '\" does not exist on the WMS Server - ignoring it.'); thisLayer.splice(i, 1); layers.splice(i, 1); styles.splice(i, 1); --i; } else { var suggested = capabilities && capabilities.Capability && capabilities.Capability.Layer && capabilities.Capability.Layer.Layer && capabilities.Capability.Layer.Layer.Name; suggested = suggested ? ' (Perhaps it should be "' + suggested + '").' : ''; throw new TerriaError({ title: 'No layer found', message: 'The WMS dataset "' + this.name + '" has no layers matching "' + this.layers + '".' + suggested + '\n\nEither the catalog file has been set up incorrectly, or the WMS server has changed.' + '\n\nPlease report this error by sending an email to <a href="mailto:' + this.terria.supportEmail + '">' + this.terria.supportEmail + '</a>.' }); } } else { layers[i] = thisLayer[i].Name; } } this.layers = layers.join(','); this.styles = styles.join(','); } if (thisLayer.length === 0) { return; } } this._rawMetadata = capabilities; if (Array.isArray(thisLayer)) { this._thisLayerInRawMetadata = thisLayer[0]; this._allLayersInRawMetadata = thisLayer; thisLayer = this._thisLayerInRawMetadata; } else { this._thisLayerInRawMetadata = thisLayer; this._allLayersInRawMetadata = [thisLayer]; } this._overwriteFromGetCapabilities = overwrite; }; function loadFromCapabilities(wmsItem) { var thisLayer = wmsItem._thisLayerInRawMetadata; if (!defined(thisLayer)) { return; } var overwrite = wmsItem._overwriteFromGetCapabilities; var capabilities = wmsItem._rawMetadata; if (!containsAny(thisLayer.Abstract, WebMapServiceCatalogItem.abstractsToIgnore)) { updateInfoSection(wmsItem, overwrite, 'Data Description', thisLayer.Abstract); } var service = defined(capabilities.Service) ? capabilities.Service : {}; // Show the service abstract if there is one, and if it isn't the Geoserver default "A compliant implementation..." if (!containsAny(service.Abstract, WebMapServiceCatalogItem.abstractsToIgnore) && service.Abstract !== thisLayer.Abstract) { updateInfoSection(wmsItem, overwrite, 'Service Description', service.Abstract); } // If style is defined in parameters, use that, but only if a style with that name can be found. // Otherwise use first style in list. var style = Array.isArray(thisLayer.Style) ? thisLayer.Style[0] : thisLayer.Style; if (defined(wmsItem.parameters.styles)) { var styleName = wmsItem.parameters.styles; if (Array.isArray(thisLayer.Style)) { for (var ind = 0; ind < thisLayer.Style.length; ind++) { if (thisLayer.Style[ind].Name === styleName) { style = thisLayer.Style[ind]; } } } else { if (defined(thisLayer.style) && thisLayer.style.styleName === styleName) { style = thisLayer.style; } } } if (defined(style) && defined(style.MetadataURL)) { var metadataUrls = (Array.isArray(style.MetadataURL) ? style.MetadataURL : [style.MetadataURL]) .map(function(metadataUrl) { return metadataUrl && metadataUrl.OnlineResource ? metadataUrl.OnlineResource['xlink:href'] : undefined; }) .filter(url => defined(url)) .join('<br>'); updateInfoSection(wmsItem, overwrite, 'Metadata Links', metadataUrls); } // Show the Access Constraints if it isn't "none" (because that's the default, and usually a lie). if (defined(service.AccessConstraints) && !/^none$/i.test(service.AccessConstraints)) { updateInfoSection(wmsItem, overwrite, 'Access Constraints', service.AccessConstraints); } updateInfoSection(wmsItem, overwrite, 'Service Contact', getServiceContactInformation(capabilities)); updateInfoSection(wmsItem, overwrite, 'GetCapabilities URL', wmsItem.getCapabilitiesUrl); updateValue(wmsItem, overwrite, 'minScaleDenominator', thisLayer.MinScaleDenominator); updateValue(wmsItem, overwrite, 'getFeatureInfoFormats', getFeatureInfoFormats(capabilities)); updateValue(wmsItem, overwrite, 'rectangle', getRectangleFromLayers(wmsItem._allLayersInRawMetadata)); updateValue(wmsItem, overwrite, 'intervals', getIntervalsFromLayer(wmsItem, thisLayer)); var crs = defaultValue(getInheritableProperty(thisLayer, 'CRS', true), getInheritableProperty(thisLayer, 'SRS', true)); var tilingScheme; var srs; if (defined(crs)) { if (crsIsMatch(crs, 'EPSG:3857')) { // Standard Web Mercator tilingScheme = new WebMercatorTilingScheme(); srs = 'EPSG:3857'; } else if (crsIsMatch(crs, 'EPSG:900913')) { // Older code for Web Mercator tilingScheme = new WebMercatorTilingScheme(); srs = 'EPSG:900913'; } else if (crsIsMatch(crs, 'EPSG:4326')) { // Standard Geographic tilingScheme = new GeographicTilingScheme(); srs = 'EPSG:4326'; } else if (crsIsMatch(crs, 'CRS:84')) { // Another name for EPSG:4326 tilingScheme = new GeographicTilingScheme(); srs = 'CRS:84'; } else if (crsIsMatch(crs, 'EPSG:4283')) { // Australian system that is equivalent to EPSG:4326. tilingScheme = new GeographicTilingScheme(); srs = 'EPSG:4283'; } else { // No known supported CRS listed. Try the default, EPSG:3857, and hope for the best. tilingScheme = new WebMercatorTilingScheme(); srs = 'EPSG:3857'; } } updateValue(wmsItem, overwrite, 'tilingScheme', tilingScheme); if (!defined(wmsItem.parameters)) { wmsItem.parameters = {}; } updateValue(wmsItem.parameters, overwrite, 'srs', srs); if (wmsItem.supportsColorScaleRange) { updateValue(wmsItem, overwrite, 'colorScaleMinimum', -50); updateValue(wmsItem, overwrite, 'colorScaleMaximum', 50); } } function addToken(url, tokenParameterName, token) { if (!defined(token)) { return url; } else { return new URI(url).setQuery(tokenParameterName, token).toString(); } } WebMapServiceCatalogItem.prototype._load = function() { var that = this; var promise = when(); if (this.tokenUrl) { promise = getToken(this.terria, this.tokenUrl, this.url); } return promise.then(function(token) { that._lastToken = token; var promises = []; if (!defined(that._rawMetadata) && defined(that.getCapabilitiesUrl)) { promises.push(loadXML(proxyCatalogItemUrl(that, addToken(that.getCapabilitiesUrl, that.tokenParameterName, that._lastToken), '1d')).then(function(xml) { var metadata = capabilitiesXmlToJson(that, xml); that.updateFromCapabilities(metadata, false); loadFromCapabilities(that); })); } else { loadFromCapabilities(that); } // Query WMS for wfs or wcs URL if no dataUrl is present if (!defined(that.dataUrl) && defined(that.url)) { var describeLayersURL = cleanUrl(that.url) + '?service=WMS&version=1.1.1&sld_version=1.1.0&request=DescribeLayer&layers=' + encodeURIComponent(that.layers); promises.push(loadXML(proxyCatalogItemUrl(that, addToken(describeLayersURL, that.tokenParameterName, that._lastToken), '1d')).then(function(xml) { var json = xml2json(xml); // LayerDescription could be an array. If so, only use the first element var LayerDescription = (json.LayerDescription instanceof Array) ? json.LayerDescription[0] : json.LayerDescription; if (defined(LayerDescription) && defined(LayerDescription.owsURL) && defined(LayerDescription.owsType)) { switch (LayerDescription.owsType.toLowerCase()) { case 'wfs': if (defined(LayerDescription.Query) && defined(LayerDescription.Query.typeName)) { that.dataUrl = addToken(cleanUrl(LayerDescription.owsURL) + '?service=WFS&version=1.1.0&request=GetFeature&typeName=' + LayerDescription.Query.typeName + '&srsName=EPSG%3A4326&maxFeatures=1000', that.tokenParameterName, that._lastToken); that.dataUrlType = 'wfs-complete'; } else { that.dataUrl = addToken(cleanUrl(LayerDescription.owsURL), that.tokenParameterName, that._lastToken); that.dataUrlType = 'wfs'; } break; case 'wcs': if (defined(LayerDescription.Query) && defined(LayerDescription.Query.typeName)) { that.dataUrl = addToken(cleanUrl(LayerDescription.owsURL) + '?service=WCS&version=1.1.1&request=DescribeCoverage&identifiers=' + LayerDescription.Query.typeName, that.tokenParameterName, that._lastToken); that.dataUrlType = 'wcs-complete'; } else { that.dataUrl = addToken(cleanUrl(LayerDescription.owsURL), that.tokenParameterName, that._lastToken); that.dataUrlType = 'wcs'; } break; } } }).otherwise(function(err) { })); // Catch potential XML error - doesn't matter if URL can't be retrieved } return when.all(promises); }); }; function fixPlaceholders(urlString) { return urlString.replace(/%7B/g, '{').replace(/%7D/g, '}'); } WebMapServiceCatalogItem.prototype.handleTileError = function(detailsRequestPromise, imageryProvider, x, y, level) { if (!defined(this.tokenUrl)) { return detailsRequestPromise; } const that = this; return detailsRequestPromise.otherwise(function(e) { if (e && (e.statusCode === 498 || e.statusCode === 499)) { // This looks like an invalid token error, so try requesting a new one. if (!defined(that._newTokenRequestInFlight)) { that._newTokenRequestInFlight = getToken(that.terria, that.tokenUrl, that.url).then(function(token) { that._lastToken = token; // Turns out setting a parameter after the WMS provider is created is not a thing we can do elegantly. // So here we do it super hackily. const oldTemplateProvider = imageryProvider._tileProvider; const newTemplateProvider = new UrlTemplateImageryProvider({ url: fixPlaceholders(addToken(oldTemplateProvider.url, that.tokenParameterName, that._lastToken)), pickFeaturesUrl: fixPlaceholders(addToken(oldTemplateProvider.pickFeaturesUrl, that.tokenParameterName, that._lastToken)), tilingScheme: oldTemplateProvider.tilingScheme, rectangle: oldTemplateProvider.rectangle, tileWidth: oldTemplateProvider.tileWidth, tileHeight: oldTemplateProvider.tileHeight, minimumLevel: oldTemplateProvider.minimumLevel, maximumLevel: oldTemplateProvider.maximumLevel, proxy: oldTemplateProvider.proxy, subdomains: oldTemplateProvider.subdomains, tileDiscardPolicy: oldTemplateProvider.tileDiscardPolicy, credit: oldTemplateProvider.credit, getFeatureInfoFormats: oldTemplateProvider.getFeatureInfoFormats, enablePickFeatures: oldTemplateProvider.enablePickFeatures, hasAlphaChannel: oldTemplateProvider.hasAlphaChannel, urlSchemeZeroPadding: oldTemplateProvider.urlSchemeZeroPadding }); newTemplateProvider._errorEvent = oldTemplateProvider._errorEvent; imageryProvider._tileProvider = newTemplateProvider; that._newTokenRequestInFlight = undefined; }); } return that._newTokenRequestInFlight; } else { return when.reject(e); } }); }; WebMapServiceCatalogItem.prototype._createImageryProvider = function(time) { var parameters = objectToLowercase(this.parameters); if (defined(time)) { parameters = combine({ time: time }, parameters); } if (defined(this._lastToken)) { parameters = combine({ [this.tokenParameterName]: this._lastToken }); } parameters = combine(parameters, WebMapServiceCatalogItem.defaultParameters); // request one more feature than we will show, so that we can tell the user if there are more not shown if (defined(parameters.feature_count)) { console.log(this.name + ': using parameters.feature_count (' + parameters.feature_count + ') to override maximumShownFeatureInfos (' + this.maximumShownFeatureInfos + ').'); if (parameters.feature_count === 1) { this.maximumShownFeatureInfos = 1; } else { this.maximumShownFeatureInfos = parameters.feature_count - 1; } } else { parameters.feature_count = this.maximumShownFeatureInfos + 1; } if (defined(this.styles) && (!defined(parameters.styles) || parameters.styles.length === 0)) { parameters.styles = this.styles; } if (defined(this.colorScaleMinimum) && defined(this.colorScaleMaximum) && !defined(parameters.colorscalerange)) { parameters.colorscalerange = [this.colorScaleMinimum, this.colorScaleMaximum].join(','); } var maximumLevel; if (defined(this.minScaleDenominator)) { var metersPerPixel = 0.00028; // from WMS 1.3.0 spec section 7.2.4.6.9 var tileWidth = 256; var circumferenceAtEquator = 2 * Math.PI * Ellipsoid.WGS84.maximumRadius; var distancePerPixelAtLevel0 = circumferenceAtEquator / tileWidth; var level0ScaleDenominator = distancePerPixelAtLevel0 / metersPerPixel; // 1e-6 epsilon from WMS 1.3.0 spec, section 7.2.4.6.9. var ratio = level0ScaleDenominator / (this.minScaleDenominator - 1e-6); var levelAtMinScaleDenominator = Math.log(ratio) / Math.log(2); maximumLevel = levelAtMinScaleDenominator | 0; } if (defined(this.dimensions) && (!defined(parameters.dimensions) || parameters.dimensions.length === 0)) { for (var dimensionName in this.dimensions) { if (this.dimensions.hasOwnProperty(dimensionName)) { // elevation is specified as simply elevation. // Other (custom) dimensions are prefixed with 'dim_'. // See WMS 1.3.0 spec section C.3.2 and C.3.3. if (dimensionName.toLowerCase() === 'elevation') { parameters.elevation = this.dimensions[dimensionName]; } else { parameters['dim_' + dimensionName] = this.dimensions[dimensionName]; } } } } return new WebMapServiceImageryProvider({ url : cleanAndProxyUrl(this, this.url), layers : this.layers, getFeatureInfoFormats : this.getFeatureInfoFormats, parameters : parameters, getFeatureInfoParameters : parameters, tilingScheme : defined(this.tilingScheme) ? this.tilingScheme : new WebMercatorTilingScheme(), maximumLevel: maximumLevel }); }; WebMapServiceCatalogItem.defaultParameters = { transparent: true, format: 'image/png', exceptions: 'application/vnd.ogc.se_xml', styles: '', tiled: true }; function cleanAndProxyUrl(catalogItem, url) { return proxyCatalogItemUrl(catalogItem, cleanUrl(url)); } function cleanUrl(url) { // Strip off the search portion of the URL var uri = new URI(url); uri.search(''); return uri.toString(); } function getRectangleFromLayer(layer) { var egbb = layer.EX_GeographicBoundingBox; // required in WMS 1.3.0 if (defined(egbb)) { return Rectangle.fromDegrees(egbb.westBoundLongitude, egbb.southBoundLatitude, egbb.eastBoundLongitude, egbb.northBoundLatitude); } else { var llbb = layer.LatLonBoundingBox; // required in WMS 1.0.0 through 1.1.1 if (defined(llbb)) { return Rectangle.fromDegrees(llbb.minx, llbb.miny, llbb.maxx, llbb.maxy); } } return undefined; } function getRectangleFromLayers(layers) { if (!Array.isArray(layers)) { return getRectangleFromLayer(layers); } return unionRectangleArray(layers.map(function(item) { return getRectangleFromLayer(item); }));