UNPKG

terriajs

Version:

Geospatial data visualization platform.

1,244 lines (1,095 loc) 59 kB
'use strict'; /*global require*/ var CatalogMember = require('./CatalogMember'); var CesiumMath = require('terriajs-cesium/Source/Core/Math'); var clone = require('terriajs-cesium/Source/Core/clone'); var createCatalogMemberFromType = require('./createCatalogMemberFromType'); var Credit = require('terriajs-cesium/Source/Core/Credit'); var defined = require('terriajs-cesium/Source/Core/defined'); var defineProperties = require('terriajs-cesium/Source/Core/defineProperties'); var DeveloperError = require('terriajs-cesium/Source/Core/DeveloperError'); var freezeObject = require('terriajs-cesium/Source/Core/freezeObject'); var inherit = require('../Core/inherit'); var JulianDate = require('terriajs-cesium/Source/Core/JulianDate'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var LegendUrl = require('../Map/LegendUrl'); var Metadata = require('./Metadata'); var raiseErrorOnRejectedPromise = require('./raiseErrorOnRejectedPromise'); var Rectangle = require('terriajs-cesium/Source/Core/Rectangle'); var when = require('terriajs-cesium/Source/ThirdParty/when'); /** * A data item in a {@link CatalogGroup}. * * @alias CatalogItem * @constructor * @extends CatalogMember * @abstract * * @param {Terria} terria The Terria instance. */ var CatalogItem = function(terria) { CatalogMember.call(this, terria); this._enabledDate = undefined; this._shownDate = undefined; this._loadForEnablePromise = undefined; this._lastLoadInfluencingValues = undefined; // The catalog item to show in the Now Viewing when this item is enabled, instead of this item. // If undefined, this item itself is shown. this.nowViewingCatalogItem = undefined; // The catalog item that created this one. Usually this is undefined, but may be defined if // the {@see CatalogItem} in the catalog acts like a factory to produce a different catalog item for the // {@see NowViewing}, rather than being added to the {@see NowViewing} itself. In that scenario, this // property on the item in the now viewing would be a reference to the item in the catalog. // @type {CatalogItem} this.creatorCatalogItem = undefined; /** * The index of the item in the Now Viewing list. Setting this property does not automatically change the order. * This property is used intenally to save/restore the Now Viewing order and is not intended for general use. * @private * @type {Number} */ this.nowViewingIndex = undefined; /** * Gets or sets the geographic rectangle (extent or bounding box) containing this data item. This property is observable. * @type {Rectangle} */ this.rectangle = undefined; /** * Gets or sets the URL of this data. This property is observable. * @type {String} */ this.url = undefined; /** * Gets or sets a description of the custodian of this data item. * This property is an HTML string that must be sanitized before display to the user. * This property is observable. * @type {String} */ this.dataCustodian = undefined; /** * Gets or sets an attribution displayed on the map when this catalog item is enabled. * This property is observable. * @type {Credit} */ this.attribution = undefined; /** * Gets or sets the URL from which this data item's metadata description can be retrieved, or undefined if * metadata is not available for this data item. The format of the metadata depends on the type of data item. * For example, Web Map Service (WMS) data items provide their metadata via their GetCapabilities document. * This property is observable. * @type {String} */ this.metadataUrl = undefined; /** * Gets or sets a value indicating whether this data item is enabled. An enabled data item appears in the * "Now Viewing" pane, but is not necessarily shown on the map. This property is observable. * @type {Boolean} */ this.isEnabled = false; /** * Gets or sets a value indicating whether this data item is currently shown on the map. In order to be shown, * the item must also be enabled. This property is observable. * @type {Boolean} */ this.isShown = false; /** * Gets or sets a value indicating whether the legend for this data item is currently visible. * This property is observable. * @type {Boolean} */ this.isLegendVisible = true; /** * Gets or sets a flag which determines whether the legend comes before (false) or after (true) the display variable choice. * Default false. * @type {Boolean} */ this.displayChoicesBeforeLegend = false; /** * Gets or sets the clock parameters for this data item. If this property is undefined, this data item * does not have any time-varying data. This property is observable. * @type {DataSourceClock} */ this.clock = undefined; /** * Gets or sets a value indicating whether this data source is currently loading. This property is observable. * @type {Boolean} */ this.isLoading = false; /* * Gets or sets a value indicating whether this data source can be enabled via a checkbox in the Data Catalog Tab. * This property is observable. * @type {Boolean} */ this.isEnableable = true; /** * Gets or sets a value indicating whether this data source is mappable. * This property is observable. * @type {Boolean} */ this.isMappable = true; /** * Gets or sets a value indicating whether this data source should show an info icon. This property is observable. * @type {Boolean} */ this.showsInfo = true; /** * Gets or sets a message to show when this item is enabled for the first time in order to call attention to the Now Viewing panel. * @type {String} */ this.nowViewingMessage = undefined; /** * Gets or sets a template to display message in a info box. * May be a string or an object with template, name and/or partials properties. * @type {String|Object} */ this.featureInfoTemplate = undefined; /** * The maximum number of features whose information can be shown at one time in the Feature Info Panel, from this item. * Defaults to terria.configParameters.defaultMaximumShownFeatureInfos * @type {Number} */ this.maximumShownFeatureInfos = terria.configParameters.defaultMaximumShownFeatureInfos; /** * Gets or sets a value indicating whether the map will automatically zoom to this catalog item when it is enabled. * * Note that within a single init source: * * * Catalog items with both `isEnabled` and `zoomOnEnable` set to true will override the top-level `initialCamera` property. * * If multiple catalog items have both `isEnabled` and `zoomOnEnable` set to true, it is undefined which one will affect the camera. * * In the case of multiple init sources, however, the camera will reflect whatever happens in the _last_ init source, whether * it is a result of a `zoomOnEnable` or an `initialCamera`, * @type {Boolean} * @default false */ this.zoomOnEnable = false; /** * Options for formatting current time and timeline tic labels. Options are: * currentTime // Current time in time slider will be shown in this format. For example "mmmm yyyy" for Jan 2016. * timelineTic // Timeline tics will have this label. For example "yyyy" will cause each tic to be labelled with the year. * @type {Object} */ this.dateFormat = {}; /** * Gets or sets a flag indicating whether imagery should be displayed using this item's own clock (currentTime, multiplier), * or, if false, the terria clock (whose current time is shown in the timeline UI). Default false. * This property is observable. * @type {Boolean} */ this.useOwnClock = false; // _currentTime is effectively a view of clock.currentTime. It is defined as a separate state (mirror state) so that // it can be an independent knockout observable with all of the machinary that implies for free, without tracking a sub // property of a cesium primitive (namely clock.currentTime) which seems kind of nasty. this._currentTime = undefined; this._legendUrl = undefined; this._legendUrls = undefined; this._dataUrl = undefined; this._dataUrlType = undefined; knockout.track(this, ['rectangle', 'dataCustodian', 'attribution', 'metadataUrl', 'isEnabled', 'isShown', 'isLegendVisible', 'clock', '_currentTime', 'isLoading', 'isMappable', 'nowViewingMessage', 'zoomOnEnable', 'isEnableable', 'showsInfo', 'nowViewingMessage', 'url', '_legendUrl', '_legendUrls', '_dataUrl', '_dataUrlType', 'nowViewingCatalogItem', 'useOwnClock']); var evaluatingLegendUrl = false; /** * Gets or sets the URLs of the legends to show when this catalog item is enabled. * @member {LegendUrl} legendUrls * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'legendUrls', { get: function() { if (!defined(this._legendUrls) || this._legendUrls.length === 0) { var legendUrl = evaluatingLegendUrl ? undefined : this.legendUrl; if (defined(legendUrl) && defined(legendUrl.url) && legendUrl.url.length > 0) { return [legendUrl]; } } return this._legendUrls; }, set: function(value) { this._legendUrls = value; this._legendUrl = undefined; } }); /** * Gets or sets the URL of the legend to show when this catalog item is enabled. If there is more than one * legend URL, this property returns the first one. * @member {LegendUrl} legendUrl * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'legendUrl', { get: function() { evaluatingLegendUrl = true; try { if (defined(this._legendUrl)) { return this._legendUrl; } else { var legendUrls = this.legendUrls; if (defined(legendUrls)) { return this.legendUrls[0]; } return undefined; } } finally { evaluatingLegendUrl = false; } }, set: function(value) { this._legendUrl = value; this._legendUrls = undefined; } }); /** * Gets or sets the URL from which this data item's raw data can be retrieved, or undefined if raw data for * this data item is not available. This property is observable. * @member {String} dataUrl * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'dataUrl', { get : function() { // dataUrl is derived from url if not explicitly specified. if (defined(this._dataUrl)) { return this._dataUrl; } return this.url; }, set : function(value) { this._dataUrl = value; } }); /** * Gets or sets the type of the {@link CatalogItem#dataUrl}, or undefined if raw data for this data * source is not available. This property is observable. * Valid values are: * * `direct` - A direct link to the data. * * `wfs` - A Web Feature Service (WFS) base URL. If {@link CatalogItem#dataUrl} is not * specified, the base URL will be this data item's URL. * * `wfs-complete` - A complete, ready-to-use link to download features from a WFS server. * * `none` - There is no data link. * @member {String} dataUrlType * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'dataUrlType', { get : function() { if (defined(this._dataUrlType)) { return this._dataUrlType; } else { return 'direct'; } }, set : function(value) { this._dataUrlType = value; } }); /** * Gets / sets the CatalogItems current time. * * This property is an observable version of clock.currentTime, they will always have the same value. * * When setting the currentTime through this property the correct clock (terria.clock or this.clock) is updated * depending on whether .useOwnClock is true or false so that the catalog items state will reflect the new time * correctly. * * The get component of this property is effectively an interface adapter for clock.definitionChanged which changes * the structure from an Event when the current time changes to a knockout property which can be observed. * * @member {JulianDate} currentTime * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'currentTime', { get : function() { return this._currentTime; }, set : function(value) { // Note: We don't explicitly need to set this._currentTime since our other machinery regarding updating // this._currentTime should take care of this. if (this.useOwnClock) { updateCurrentTime(this, value); } else { this.terria.clock.currentTime = JulianDate.clone(value); this.terria.clock.tick(); } }, }); /** * Gets the CatalogItems current time as the discrete time that the CatalogItem has information for. * Returns undefined if the clock is beyond the range of the intervals specified by the layer. * Returns undefined if it is not possible to query the time (i.e. the item doesn't have a clock, availableDates or * intervals). * * See also clampedDiscreteTime if you want the discrete time that is clamped to the first / last value if the current * time is beyond the range of the intervals specified by the item. * * @member {Date} discreteTime * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'discreteTime', { get : function() { return timeAtIndex(this, getCurrentIndex(this)); }, }); /** * Gets the CatalogItems current time as the discrete time that the CatalogItem has information for. * Returns the nearest time in-range if the clock is beyond the range of the intervals specified by the layer. * Returns undefined if it is not possible to query the time (i.e. the item doesn't have a clock, availableDates or * intervals). * * See also discreteTime if you want the discrete time that is undefined if the current time is beyond the range of * the intervals specified by the item. * * @member {Date} clampedDiscreteTime * @memberOf CatalogItem.prototype */ knockout.defineProperty(this, 'clampedDiscreteTime', { get : function() { if (defined(this.discreteTime)) { return this.discreteTime; } if (!hasValidCurrentTimeAndIntervals(this)) { return undefined; } if (timeIsBeforeStart(this, this.currentTime)) { return timeAtIndex(this, 0); } if (timeIsAfterStop(this, this.currentTime)) { return timeAtIndex(this, this.intervals.length - 1); } }, }); // A property which defines when we are using the terria clock. // We define this as a knockout property so that we can watch for changes to the propery triggered by observables // that it depends on and update the subscription (watcher) on the terria.clock when the state changes. knockout.defineProperty(this, '_mirroringTerriaClock', { get: function() { return this.isEnabled && !this.useOwnClock && defined(this.clock); } }); // A property which defines when the clock is defined. // We define this as a knockout property so that we can watch for all changes to the propery including when // .clock is overriden from derrived classes (and not just base CatalogItem.clock). knockout.defineProperty(this, '_clockDefined', { get: function() { return defined(this.clock); } }); knockout.getObservable(this, '_clockDefined').subscribe(function(newValue) { clockChanged(this); }, this); knockout.getObservable(this, 'isEnabled').subscribe(function(newValue) { isEnabledChanged(this); }, this); knockout.getObservable(this, 'isShown').subscribe(function(newValue) { isShownChanged(this); }, this); knockout.getObservable(this, '_mirroringTerriaClock').subscribe(function(newValue) { updateTerriaClockWatcher(this); }, this); knockout.getObservable(this, 'useOwnClock').subscribe(function(newValue) { useOwnClockChanged(this); }, this); }; inherit(CatalogMember, CatalogItem); defineProperties(CatalogItem.prototype, { /** * Gets a value indicating whether this data item, when enabled, can be reordered with respect to other data items. * Data items that cannot be reordered are typically displayed above reorderable data items. * @memberOf CatalogItem.prototype * @type {Boolean} */ supportsReordering : { get : function() { return false; } }, /** * Gets a value indicating whether the visibility of this data item can be toggled. * @memberOf CatalogItem.prototype * @type {Boolean} */ supportsToggleShown : { get : function() { return true; } }, /** * Gets a value indicating whether the opacity of this data item can be changed. * @memberOf CatalogItem.prototype * @type {Boolean} */ supportsOpacity : { get : function() { return false; } }, /** * Gets a value indicating whether this layer can be split so that it is * only shown on the left or right side of the screen. * @memberOf CatalogItem.prototype */ supportsSplitting : { get : function() { return false; } }, /** * Gets a value indicating whether this data item has a legend. * @memberOf CatalogItem.prototype * @type {Boolean} */ hasLegend: { get: function() { return defined(this.legendUrl); } }, /** * Returns true if this item currently has a rectangle to zoom to. Depends on observable properties, and so updates once loaded. * @memberOf CatalogItem.prototype * @type {Boolean} */ canZoomTo: { get: function() { if (defined(this.nowViewingCatalogItem)) { return this.nowViewingCatalogItem.canZoomTo; } return defined(this.rectangle); } }, /** * Gets the metadata associated with this data item and the server that provided it, if applicable. * @memberOf CatalogItem.prototype * @type {Metadata} */ metadata : { get : function() { return CatalogItem.defaultMetadata; } }, /** * 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 CatalogItem.prototype * @type {Object} */ updaters : { get : function() { return CatalogItem.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 CatalogItem.prototype * @type {Object} */ serializers : { get : function() { return CatalogItem.defaultSerializers; } }, /** * Gets the set of names of the properties to be serialized for this object for a share link. * @memberOf CatalogItem.prototype * @type {String[]} */ propertiesForSharing : { get : function() { return CatalogItem.defaultPropertiesForSharing; } } }); /** * Gets or sets the default metadata to use for data items that don't provide anything better from their * {@link CatalogItem#metadata} property. The default simply indicates that no metadata is available. * @type {Metadata} */ CatalogItem.defaultMetadata = new Metadata(); CatalogItem.defaultMetadata.isLoading = false; CatalogItem.defaultMetadata.dataSourceErrorMessage = 'This data item does not have any details available.'; CatalogItem.defaultMetadata.serviceErrorMessage = 'This service does not have any details available.'; freezeObject(CatalogItem.defaultMetadata); /** * Gets or sets the set of default updater functions to use in {@link CatalogMember#updateFromJson}. Types derived from this type * should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#updaters} property. * @type {Object} */ CatalogItem.defaultUpdaters = clone(CatalogMember.defaultUpdaters); CatalogItem.defaultUpdaters.rectangle = function(catalogItem, json, propertyName) { if (defined(json.rectangle)) { catalogItem.rectangle = Rectangle.fromDegrees(json.rectangle[0], json.rectangle[1], json.rectangle[2], json.rectangle[3]); } else { catalogItem.rectangle = Rectangle.MAX_VALUE; } }; CatalogItem.defaultUpdaters.attribution = function(catalogItem, json, prototypeName) { if (defined(json.attribution)) { if (typeof json.attribution === 'object' && json.attribution.text && json.attribution.link) { const a = document.createElement('a'); a.href = json.attribution.link; a.target = '_blank'; a.innerText = json.attribution.text; catalogItem.attribution = new Credit(a.outerHTML); } else if (typeof json.attribution === 'object' && json.attribution.text) { catalogItem.attribution = new Credit(json.attribution.text); } else if (typeof json.attribution === 'string') { catalogItem.attribution = new Credit(json.attribution); } } }; CatalogItem.defaultUpdaters.legendUrl = function(catalogItem, json, prototypeName) { if (defined(json.legendUrl)) { var url, mimeType; if (typeof json.legendUrl === 'string') { url = json.legendUrl; } else { url = json.legendUrl.url; mimeType = json.legendUrl.mimeType; } catalogItem.legendUrl = new LegendUrl(url, mimeType); } }; CatalogItem.defaultUpdaters.legendUrls = function(catalogItem, json, prototypeName) { if (defined(json.legendUrls)) { catalogItem.legendUrls = json.legendUrls.map(function(legendUrl) { var url, mimeType; if (typeof legendUrl === 'string') { url = legendUrl; } else { url = legendUrl.url; mimeType = legendUrl.mimeType; } return new LegendUrl(url, mimeType); }); } }; CatalogItem.defaultUpdaters.nowViewingCatalogItem = function(catalogItem, json, prototypeName, options) { if (defined(json.nowViewingCatalogItem)) { return when(catalogItem.load()).then(function() { if (!defined(catalogItem.nowViewingCatalogItem)) { catalogItem.nowViewingCatalogItem = createCatalogMemberFromType(json.nowViewingCatalogItem.type, catalogItem.terria); } return catalogItem.nowViewingCatalogItem.updateFromJson(json.nowViewingCatalogItem, options); }); } }; CatalogItem.defaultUpdaters.currentTime = function(catalogItem, json, propertyName) { // Do not update .currentTime as it is a view of .clock.currentTime. }; CatalogItem.defaultUpdaters.discreteTime = function(catalogItem, json, propertyName) { // Do not update .currentTime as it is a view of .clock.currentTime. }; freezeObject(CatalogItem.defaultUpdaters); /** * Gets or sets the set of default serializer functions to use in {@link CatalogMember#serializeToJson}. Types derived from this type * should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#serializers} property. * @type {Object} */ CatalogItem.defaultSerializers = clone(CatalogMember.defaultSerializers); CatalogItem.defaultSerializers.rectangle = function(catalogItem, json, propertyName) { if (defined(catalogItem.rectangle)) { json.rectangle = [ CesiumMath.toDegrees(catalogItem.rectangle.west), CesiumMath.toDegrees(catalogItem.rectangle.south), CesiumMath.toDegrees(catalogItem.rectangle.east), CesiumMath.toDegrees(catalogItem.rectangle.north) ]; } }; // Serialize the underlying properties instead of the public views of them. CatalogItem.defaultSerializers.legendUrl = function(catalogItem, json, propertyName) { if (defined(catalogItem._legendUrl)) { json.legendUrl = catalogItem._legendUrl; } }; CatalogItem.defaultSerializers.legendUrls = function(catalogItem, json, propertyName) { if (defined(catalogItem._legendUrls) && catalogItem._legendUrls.length > 0) { json.legendUrls = catalogItem._legendUrls; } }; CatalogItem.defaultSerializers.attribution = function(catalogItem, json, propertyName) { if (defined(catalogItem.attribution)) { if (defined(catalogItem.attribution.link)) { json.attribution = { text: catalogItem.attribution.text, link: catalogItem.attribution.link }; } else { json.attribution = catalogItem.attribution.text; } } }; CatalogItem.defaultSerializers.dataUrl = function(catalogItem, json, prototypeName) { if(defined(catalogItem._dataUrl)){ json.dataUrl = catalogItem._dataUrl; } }; CatalogItem.defaultSerializers.dataUrlType = function(catalogItem, json, prototypeName) { if(defined(catalogItem._dataUrlType)){ json.dataUrlType = catalogItem._dataUrlType; } }; CatalogItem.defaultSerializers.nowViewingCatalogItem = function(catalogItem, json, prototypeName, options) { if (catalogItem.isEnabled && defined(catalogItem.nowViewingCatalogItem)) { json.nowViewingCatalogItem = catalogItem.nowViewingCatalogItem.serializeToJson(options); } }; CatalogItem.defaultSerializers.currentTime = function(catalogItem, json, propertyName) { // Do not serialise .currentTime as it is a view of .clock.currentTime. }; CatalogItem.defaultSerializers.discreteTime = function(catalogItem, json, propertyName) { // Do not serialise .discreteTime as it is a view of .clock.currentTime. }; CatalogItem.defaultSerializers.clampedDiscreteTime = function(catalogItem, json, propertyName) { // Do not serialise .clampedDiscreteTime as it is a view of .clock.currentTime. }; freezeObject(CatalogItem.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[]} */ CatalogItem.defaultPropertiesForSharing = clone(CatalogMember.defaultPropertiesForSharing); CatalogItem.defaultPropertiesForSharing.push('isEnabled'); CatalogItem.defaultPropertiesForSharing.push('isShown'); CatalogItem.defaultPropertiesForSharing.push('isLegendVisible'); CatalogItem.defaultPropertiesForSharing.push('nowViewingIndex'); CatalogItem.defaultPropertiesForSharing.push('nowViewingCatalogItem'); CatalogItem.defaultPropertiesForSharing.push('useOwnClock'); freezeObject(CatalogItem.defaultPropertiesForSharing); /** * Loads this catalog item, if it's not already loaded. It is safe to * call this method multiple times. The {@link CatalogItem#isLoading} flag will be set while the load is in progress. * Derived classes should implement {@link CatalogItem#_load} to perform the actual loading for the item. * Derived classes may optionally implement {@link CatalogItem#_getValuesThatInfluenceLoad} to provide an array containing * the current value of all properties that influence this item's load process. Each time that {@link CatalogItem#load} * is invoked, these values are checked against the list of values returned last time, and {@link CatalogItem#_load} is * invoked again if they are different. If {@link CatalogItem#_getValuesThatInfluenceLoad} is undefined or returns an * empty array, {@link CatalogItem#_load} will only be invoked once, no matter how many times * {@link CatalogItem#load} is invoked. * * @returns {Promise} A promise that resolves when the load is complete, or undefined if the item is already loaded. * */ CatalogItem.prototype.load = function() { var parentPromise = CatalogMember.prototype.load.call(this); if (parentPromise) { return parentPromise.then(function(loadResult) { if (loadResult instanceof CatalogItem) { this.nowViewingCatalogItem = loadResult; loadResult.creatorCatalogItem = this; } this.terria.currentViewer.notifyRepaintRequired(); }.bind(this)).otherwise(function(e) { this.isEnabled = false; throw e; // keep throwing this so we can chain more otherwises. }.bind(this)); } }; /** * Enables this catalog item, and returns a promise that resolves when the load process, if any, completes. * @return {Promise} The promise. */ CatalogItem.prototype.loadAndEnable = function() { this.isEnabled = true; return this._loadingPromise; }; /** * When implemented in a derived class, this method loads the item. The base class implementation does nothing. * This method should not be called directly; call {@link CatalogItem#load} instead. * @return {Promise} A promise that resolves when the load is complete. * @protected */ CatalogItem.prototype._load = function() { return when(); }; var emptyArray = freezeObject([]); /** * When implemented in a derived class, gets an array containing the current value of all properties that * influence this item's load process. See {@link CatalogItem#load} for more information on when and * how this is used. The base class implementation returns an empty array. * @return {Array} The array of values that influence the load process. * @protected */ CatalogItem.prototype._getValuesThatInfluenceLoad = function() { // In the future, we can implement auto-reloading when any of these properties change. Just create a knockout // computed property that calls this method and subscribe to change notifications on that computed property. // (Will need to use the rateLimit extender, presumably). return emptyArray; }; /** * Toggles the {@link CatalogItem#isEnabled} property of this item. If it is enabled, calling this method * will disable it. If it is disabled, calling this method will enable it. * * @returns {Boolean} true if the item is now enabled, false if it is now disabled. */ CatalogItem.prototype.toggleEnabled = function() { this.isEnabled = !this.isEnabled; return this.isEnabled; }; /** * Toggles the {@link CatalogItem#isShown} property of this item. If it is shown, calling this method * will hide it. If it is hidden, calling this method will show it. * * @returns {Boolean} true if the item is now shown, false if it is now hidden. */ CatalogItem.prototype.toggleShown = function() { this.isShown = !this.isShown; return this.isShown; }; /** * Toggles the {@link CatalogItem#isLegendVisible} property of this item. If it is visible, calling this * method will hide it. If it is hidden, calling this method will make it visible. * @return {Boolean} true if the legend is now visible, false if it is now hidden. */ CatalogItem.prototype.toggleLegendVisible = function() { this.isLegendVisible = !this.isLegendVisible; return this.isLegendVisible; }; var scratchRectangle = new Rectangle(); /** * Moves the camera so that the item's bounding rectangle is visible. If {@link CatalogItem#rectangle} is * undefined or covers more than about half the world in the longitude direction, or if the data item is not enabled * or not shown, this method does nothing. Because the zoom may happen asynchronously (for example, if the item's * rectangle is not yet known), this method returns a Promise that resolves when the zoom animation starts. * @returns {Promise} A promise that resolves when the zoom animation starts. */ CatalogItem.prototype.zoomTo = function() { var that = this; return when(this.load(), function() { if (defined(that.nowViewingCatalogItem)) { return that.nowViewingCatalogItem.zoomTo(); } if (!defined(that.rectangle)) { return; } var rect = Rectangle.clone(that.rectangle, scratchRectangle); if (rect.east - rect.west > 3.14) { rect = Rectangle.clone( that.terria.homeView.rectangle, scratchRectangle); console.log('Extent is wider than world so using homeView.'); } var terria = that.terria; terria.analytics.logEvent('dataSource', 'zoomTo', that.name); var epsilon = CesiumMath.EPSILON3; if (rect.east === rect.west) { rect.east += epsilon; rect.west -= epsilon; } if (rect.north === rect.south) { rect.north += epsilon; rect.south -= epsilon; } return terria.currentViewer.zoomTo(rect); }); }; /** * Uses the {@link CatalogItem#clock} settings from this data item. If this data item * has no clock settings, or has the useOwnClock property true, this method does nothing. * Because the clock update may happen asynchronously (for example, if the item's clock parameters are not yet known), * this method returns a Promise that resolves when the clock has been updated. * @returns {Promise} A promise that resolves when the clock has been updated. */ CatalogItem.prototype.useClock = function() { var that = this; return when(this.load(), function() { if (defined(that.nowViewingCatalogItem)) { return that.nowViewingCatalogItem.useClock(); } if (defined(that.clock)) { if (that.isEnabled && that.isShown && !that.useOwnClock) { that.terria.timeSeriesStack.addLayerToTop(that); } else { that.terria.timeSeriesStack.removeLayer(that); } } }); }; /** * Move the current time to the next time interval. */ CatalogItem.prototype.moveToNextTime = function() { updateIndex(this, nextIndex(this)); }; /** * Move the current time to the previous time interval. */ CatalogItem.prototype.moveToPreviousTime = function() { updateIndex(this, previousIndex(this)); }; /** * Whether it is possible to move to a time interval after the current time. * @returns {Boolean} True if it is possible. */ CatalogItem.prototype.isNextTimeAvaliable = function() { return defined(nextIndex(this)); }; /** * Whether it is possible to move to a time interval before the current time. * @returns {Boolean} True if it is possible. */ CatalogItem.prototype.isPreviousTimeAvaliable = function() { return defined(previousIndex(this)); }; /** * Moves the camera so that the data item's bounding rectangle is visible, and updates the TerriaJS clock according to this * data item's clock settings. This method simply calls {@link CatalogItem#zoomTo} and * {@link CatalogItem#useClock}. Because the zoom and clock update may happen asynchronously (for example, if the item's * rectangle is not yet known), this method returns a Promise that resolves when the zoom animation starts and the clock * has been updated. * @returns {Promise} A promise that resolves when the clock has been updated and the zoom animation has started. */ CatalogItem.prototype.zoomToAndUseClock = function() { return when.all([this.zoomTo(), this.useClock()]); }; /** * Enables this data item on the globe or map. This method: * * Should not be called directly. Instead, set the {@link CatalogItem#isEnabled} property to true. * * Will not necessarily be called immediately when {@link CatalogItem#isEnabled} is set to true; it will be deferred until * {@link CatalogItem#isLoading} is false. * * Should NOT also show the data item on the globe/map (see {@link CatalogItem#_show}), so in some cases it may not do * anything at all. * * Calls {@link CatalogItem#_enableInCesium} or {@link CatalogItem#_enableInLeaflet} in the base-class implementation, * depending on which viewer is active. Derived classes that have identical enable logic for both viewers may override * this method instead of the viewer-specific ones. * @protected */ CatalogItem.prototype._enable = function() { if (defined(this.nowViewingCatalogItem)) { this.nowViewingCatalogItem.isEnabled = true; return; } var terria = this.terria; if (defined(terria.cesium)) { terria.cesium.stoppedRendering = true; this._enableInCesium(); } if (defined(terria.leaflet)) { this._enableInLeaflet(); } }; /** * Disables this data item on the globe or map. This method: * * Should not be called directly. Instead, set the {@link CatalogItem#isEnabled} property to false. * * Will not be called if {@link CatalogItem#_enable} was not called (for example, because the previous call was deferred * while the data item loaded, and the user disabled the data item before the load completed). * * Will only be called after {@link CatalogItem#_hide} when a shown data item is disabled. * * Calls {@link CatalogItem#_disableInCesium} or {@link CatalogItem#_disableInLeaflet} in the base-class implementation, * depending on which viewer is active. Derived classes that have identical disable logic for both viewers may override * this method instead of the viewer-specific ones. * @protected */ CatalogItem.prototype._disable = function() { if (defined(this.nowViewingCatalogItem)) { this.nowViewingCatalogItem.isEnabled = false; return; } var terria = this.terria; if (defined(terria.cesium)) { this._disableInCesium(); } if (defined(terria.leaflet)) { this._disableInLeaflet(); } }; /** * Shows this data item on the globe or map. This method: * * Should not be called directly. Instead, set the {@link CatalogItem#isShown} property to true. * * Will only be called after {@link CatalogItem#_enable}; you can count on that method having been called first. * * Will not necessarily be called immediately when {@link CatalogItem#isShown} is set to true; it will be deferred until * {@link CatalogItem#isLoading} is false. * * Calls {@link CatalogItem#_showInCesium} or {@link CatalogItem#_showInLeaflet} in the base-class implementation, * depending on which viewer is active. Derived classes that have identical show logic for both viewers * may override this method instead of the viewer-specific ones. * @protected */ CatalogItem.prototype._show = function() { if (defined(this.nowViewingCatalogItem)) { this.nowViewingCatalogItem.isShown = true; return; } var terria = this.terria; if (defined(terria.cesium)) { this._showInCesium(); } if (defined(terria.leaflet)) { this._showInLeaflet(); } }; /** * Hides this data item on the globe or map. This method: * * Should not be called directly. Instead, set the {@link CatalogItem#isShown} property to false. * * Will not be called if {@link CatalogItem#_show} was not called (for example, because the previous call was deferred * while the data item loaded, and the user hid the data item before the load completed). * * Calls {@link CatalogItem#_hideInCesium} or {@link CatalogItem#_hideInLeaflet} in the base-class implementation, * depending on which viewer is active. Derived classes that have identical hide logic for both viewers may override * this method instead of the viewer-specific ones. * @protected */ CatalogItem.prototype._hide = function() { if (defined(this.nowViewingCatalogItem)) { this.nowViewingCatalogItem.isShown = false; return; } var terria = this.terria; if (defined(terria.cesium)) { this._hideInCesium(); } if (defined(terria.leaflet)) { this._hideInLeaflet(); } }; /** * When implemented in a derived class, enables this data item on the Cesium globe. You should not call this * directly, but instead set the {@link CatalogItem#isEnabled} property to true. See * {@link CatalogItem#_enable} for more information. * @abstract * @protected */ CatalogItem.prototype._enableInCesium = function() { throw new DeveloperError('_enableInCesium must be implemented in the derived class.'); }; /** * When implemented in a derived class, disables this data item on the Cesium globe. You should not call this * directly, but instead set the {@link CatalogItem#isEnabled} property to false. See * {@link CatalogItem#_disable} for more information. * @abstract * @protected */ CatalogItem.prototype._disableInCesium = function() { throw new DeveloperError('_disableInCesium must be implemented in the derived class.'); }; /** * When implemented in a derived class, shows this data item on the Cesium globe. You should not call this * directly, but instead set the {@link CatalogItem#isShown} property to true. See * {@link CatalogItem#_show} for more information. * @abstract * @protected */ CatalogItem.prototype._showInCesium = function() { throw new DeveloperError('_showInCesium must be implemented in the derived class.'); }; /** * When implemented in a derived class, hides this data item on the Cesium globe. You should not call this * directly, but instead set the {@link CatalogItem#isShown} property to false. See * {@link CatalogItem#_hide} for more information. * @abstract * @protected */ CatalogItem.prototype._hideInCesium = function() { throw new DeveloperError('_hideInCesium must be implemented in the derived class.'); }; /** * When implemented in a derived class, enables this data item on the Leaflet map. You should not call this * directly, but instead set the {@link CatalogItem#isEnabled} property to true. See * {@link CatalogItem#_enable} for more information. * @abstract * @protected */ CatalogItem.prototype._enableInLeaflet = function() { throw new DeveloperError('enableInLeaflet must be implemented in the derived class.'); }; /** * When implemented in a derived class, disables this data item on the Leaflet map. You should not call this * directly, but instead set the {@link CatalogItem#isEnabled} property to false. See * {@link CatalogItem#_disable} for more information. * @abstract * @protected */ CatalogItem.prototype._disableInLeaflet = function() { throw new DeveloperError('disableInLeaflet must be implemented in the derived class.'); }; /** * When implemented in a derived class, shows this data item on the Leaflet map. You should not call this * directly, but instead set the {@link CatalogItem#isShown} property to true. See * {@link CatalogItem#_show} for more information. * @abstract * @protected */ CatalogItem.prototype._showInLeaflet = function() { throw new DeveloperError('_showInLeaflet must be implemented in the derived class.'); }; /** * When implemented in a derived class, hides this data item on the Leaflet map. You should not call this * directly, but instead set the {@link CatalogItem#isShown} property to false. See * {@link CatalogItem#_hide} for more information. * @abstract * @protected */ CatalogItem.prototype._hideInLeaflet = function() { throw new DeveloperError('_hideInLeaflet must be implemented in the derived class.'); }; CatalogItem.prototype.enableWithParents = function() { this.isEnabled = true; if (this.parent) { this.parent.enableWithParents(); } }; /** * Handles an error in loading a tile. If this function returns a promise that resolves successfully, * the tile request will be retried. If the returned promise rejects, it must reject with an instance * of `RequestErrorEvent` with the details of the failure, and the default handling of tile * failures will be used. The default handling takes into account the `treat404AsError`, `treat403AsError`, * and `ignoreUnknownTileErrors` properties. The default implementation simply returns `detailsRequestPromise`. * * @param {Promise} detailsRequestPromise A promise which is the result of a simple call to `loadWithXhr` for the URL * that failed. If it resolves, it will resolve to the successfully-download content of the tile URL, * as text. If it rejects, it will reject with a `RequestErrorEvent`. * @param {ImageryProvider} imageryProvider The imagery provider that generated the failed request. * @param {Number} x The x coordinate of the failed tile. * @param {Number} y The y coordinate of the failed tile. * @param {Number} level The level of the failed tile. * @returns {Promise} A promise, as described above. */ CatalogItem.prototype.handleTileError = function(detailsRequestPromise, imageryProvider, x, y, level) { return detailsRequestPromise; }; function clockChanged(catalogItem) { removeCurrentTimeSubscription(catalogItem); if (defined(catalogItem.clock)) { catalogItem._removeCurrentTimeChange = catalogItem.clock.definitionChanged.addEventListener(function () { catalogItem._currentTime = JulianDate.clone(catalogItem.clock.currentTime); }); catalogItem._currentTime = JulianDate.clone(catalogItem.clock.currentTime); } } // Removes catalogItem.clock.definitionChanged subscriptions. function removeCurrentTimeSubscription(catalogItem) { if (defined(catalogItem._removeCurrentTimeChange)) { catalogItem._removeCurrentTimeChange(); catalogItem._removeCurrentTimeChange = undefined; } } function isEnabledChanged(catalogItem) { var terria = catalogItem.terria; if (defined(catalogItem.creatorCatalogItem)) { catalogItem.creatorCatalogItem.isEnabled = catalogItem.isEnabled; } if (catalogItem.isEnabled) { terria.nowViewing.add(catalogItem); // Load this catalog item's data (if we haven't already) when it is enabled. // Don't actually enable until the load finishes. // Be careful not to call _enable multiple times or to call _enable // after the item has already been disabled. if (!defined(catalogItem._loadForEnablePromise)) { var resolvedOrRejected = false; var loadPromise = when.all([catalogItem.load(), catalogItem.waitForDisclaimerIfNeeded()]).then(function() { if (catalogItem.isEnabled) { // If there's a separate now viewing item, remove this catalog item from the // now viewing list, if it exists. if (defined(catalogItem.nowViewingCatalogItem)) { catalogItem.terria.nowViewing.items.remove(catalogItem); } catalogItem._enable(); catalogItem.terria.currentViewer.notifyRepaintRequired(); catalogItem.terria.currentViewer.addAttribution(catalogItem.attribution); if(defined(catalogItem.imageryLayer)){ catalogItem.imageryLayer.featureInfoTemplate = catalogItem.featureInfoTemplate; } // Zoom to this catalog item if requested. if (catalogItem.zoomOnEnable) { return catalogItem.zoomTo(); } } }); rai