UNPKG

terriajs

Version:

Geospatial data visualization platform.

851 lines (730 loc) 36.7 kB
'use strict'; /*global require*/ var ClockRange =require('terriajs-cesium/Source/Core/ClockRange'); var clone = require('terriajs-cesium/Source/Core/clone'); var DataSourceClock = require('terriajs-cesium/Source/DataSources/DataSourceClock'); 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 DeveloperError = require('terriajs-cesium/Source/Core/DeveloperError'); var formatError = require('terriajs-cesium/Source/Core/formatError'); var freezeObject = require('terriajs-cesium/Source/Core/freezeObject'); var ImagerySplitDirection = require('terriajs-cesium/Source/Scene/ImagerySplitDirection'); var JulianDate = require('terriajs-cesium/Source/Core/JulianDate'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var loadWithXhr = require('../Core/loadWithXhr'); 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 when = require('terriajs-cesium/Source/ThirdParty/when'); var CatalogItem = require('./CatalogItem'); var CompositeCatalogItem = require('./CompositeCatalogItem'); var getUrlForImageryTile = require('../Map/getUrlForImageryTile'); var inherit = require('../Core/inherit'); var TerriaError = require('../Core/TerriaError'); var overrideProperty = require('../Core/overrideProperty'); var { setOpacity, fixNextLayerOrder } = require('./ImageryLayerPreloadHelpers'); var setClockCurrentTime = require('./setClockCurrentTime'); /** * A {@link CatalogItem} that is added to the map as a rasterized imagery layer. * * @alias ImageryLayerCatalogItem * @constructor * @extends CatalogItem * @abstract * * @param {Terria} terria The Terria instance. */ var ImageryLayerCatalogItem = function(terria) { CatalogItem.call(this, terria); this._imageryLayer = undefined; this._clock = undefined; this._currentIntervalIndex = -1; this._nextIntervalIndex = undefined; this._nextLayer = undefined; /** * Gets or sets the opacity (alpha) of the data item, where 0.0 is fully transparent and 1.0 is * fully opaque. This property is observable. * @type {Number} * @default 0.6 */ this.opacity = 0.6; /** * Gets or sets a value indicating whether a 404 response code when requesting a tile should be * treated as an error. If false, 404s are assumed to just be missing tiles and need not be * reported to the user. * @type {Boolean} * @default false */ this.treat404AsError = false; /** * Gets or sets a value indicating whether a 403 response code when requesting a tile should be * treated as an error. If false, 403s are assumed to just be missing tiles and need not be * reported to the user. * @type {Boolean} * @default false */ this.treat403AsError = true; /** * Gets or sets a value indicating whether non-specific (no HTTP status code) tile errors should be ignored. This is a * last resort, for dealing with odd cases such as data sources that return non-images (eg XML) with a 200 status code. * No error messages will be shown to the user. * @type {Boolean} * @default false */ this.ignoreUnknownTileErrors = false; /** * Gets or sets the {@link TimeIntervalCollection} defining the intervals of distinct imagery. If this catalog item * is not time-dynamic, property is undefined. This property is observable. * @type {ImageryLayerInterval[]} * @default undefined */ this.intervals = undefined; /** * Keeps the layer on top of all other imagery layers. This property is observable. * @type {Boolean} * @default false */ this.keepOnTop = false; /** * Gets or sets a value indicating whether this dataset should be clipped to the {@link CatalogItem#rectangle}. * If true, no part of the dataset will be displayed outside the rectangle. This property is true by default, * leading to better performance and avoiding tile request errors that might occur when requesting tiles outside the * server-specified rectangle. However, it may also cause features to be cut off in some cases, such as if a server * reports an extent that does not take into account that the representation of features sometimes require a larger * spatial extent than the features themselves. For example, if a point feature on the edge of the extent is drawn * as a circle with a radius of 5 pixels, half of that circle will be cut off. * @type {Boolean} * @default false */ this.clipToRectangle = true; /** * Gets or sets a value indicating whether tiles of this catalog item are required to be loaded before terrain * tiles to which they're attached can be rendered. This should usually be set to true for base layers and * false for all others. * @type {Boolean} * @default false */ this.isRequiredForRendering = false; /** * Options for the value of the animation timeline at start. Valid options in config file are: * initialTimeSource: "present" // closest to today's date * initialTimeSource: "start" // start of time range of animation * initialTimeSource: "end" // end of time range of animation * initialTimeSource: An ISO8601 date e.g. "2015-08-08" // specified date or nearest if date is outside range * @type {String} */ this.initialTimeSource = undefined; /** * Gets or sets which side of the splitter (if present) to display this imagery layer on. Defaults to both sides. * This property is observable. * @type {ImagerySplitDirection} */ this.splitDirection = ImagerySplitDirection.NONE; // NONE means show on both sides of the splitter, if there is one. // Need to track initialTimeSource so we can set it in the specs after setting intervals, and then have the current time update (via the clock property). knockout.track(this, ['_clock', 'opacity', 'treat404AsError', 'ignoreUnknownTileErrors', 'intervals', 'clipToRectangle', 'splitDirection', 'initialTimeSource']); overrideProperty(this, 'clock', { get : function() { var clock = this._clock; if (!clock && this.intervals && this.intervals.length > 0) { var startTime = this.intervals.start; var stopTime = this.intervals.stop; // Average about 5 seconds per interval. var totalDuration = JulianDate.secondsDifference(stopTime, startTime); var numIntervals = this.intervals.length; var averageDuration = totalDuration / numIntervals; var timePerSecond = averageDuration / 5; clock = new DataSourceClock(); clock.startTime = startTime; clock.stopTime = stopTime; clock.multiplier = timePerSecond; setClockCurrentTime(clock, this.initialTimeSource); } return clock; }, set : function(value) { this._clock = value; } }); /** * Gets javascript dates describing the discrete datetimes (or intervals) available for this item. * By declaring this as a knockout defined property, it is cached. * @member {Date[]} An array of discrete dates or intervals available for this item, or [] if none. * @memberOf ImageryLayerCatalogItem.prototype */ knockout.defineProperty(this, 'availableDates', { get: function() { if (defined(this.intervals)) { const datetimes = []; // Only show the start of each interval. If only time instants were given, this is the instant. for (let i = 0; i < this.intervals.length; i++) { datetimes.push(JulianDate.toDate(this.intervals.get(i).start, 3)); } return datetimes; } return []; } }, this); knockout.getObservable(this, 'opacity').subscribe(function() { updateOpacity(this); }, this); knockout.getObservable(this, 'splitDirection').subscribe(function() { updateSplitDirection(this); }, this); }; inherit(CatalogItem, ImageryLayerCatalogItem); defineProperties(ImageryLayerCatalogItem.prototype, { /** * 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 false; } }, /** * Gets the Cesium or Leaflet imagery layer object associated with this data source. * This property is undefined if the data source is not enabled. * @memberOf ImageryLayerCatalogItem.prototype * @type {Object} */ imageryLayer : { get : function() { return this._imageryLayer; } }, /** * Gets a value indicating whether this data source, when enabled, can be reordered with respect to other data sources. * Data sources that cannot be reordered are typically displayed above reorderable data sources. * @memberOf ImageryLayerCatalogItem.prototype * @type {Boolean} */ supportsReordering : { get : function() { return !this.keepOnTop; } }, /** * Gets a value indicating whether the opacity of this data source can be changed. * @memberOf ImageryLayerCatalogItem.prototype * @type {Boolean} */ supportsOpacity : { get : function() { return true; } }, /** * 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 ImageryLayerCatalogItem.prototype */ supportsSplitting : { get : function() { return true; } }, /** * 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 ImageryLayerCatalogItem.prototype * @type {Object} */ updaters : { get : function() { return ImageryLayerCatalogItem.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 ImageryLayerCatalogItem.prototype * @type {Object} */ serializers : { get : function() { return ImageryLayerCatalogItem.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 ImageryLayerCatalogItem.prototype * @type {String[]} */ propertiesForSharing : { get : function() { return ImageryLayerCatalogItem.defaultPropertiesForSharing; } } }); ImageryLayerCatalogItem.defaultUpdaters = clone(CatalogItem.defaultUpdaters); ImageryLayerCatalogItem.defaultUpdaters.intervals = function(catalogItem, json, propertyName) { if (!defined(json.intervals)) { return; } if (!catalogItem.supportsIntervals) { throw new TerriaError({ sender: catalogItem, title: 'Intervals not supported', message: 'Sorry, ' + catalogItem.typeName + ' (' + catalogItem.type + ') catalog items cannot currently be made time-varying by specifying the "intervals" property.' }); } var result = new TimeIntervalCollection(); for (var i = 0; i < json.intervals.length; ++i) { var interval = json.intervals[i]; result.addInterval(TimeInterval.fromIso8601({ iso8601: interval.interval, data: interval.data })); } catalogItem.intervals = result; }; ImageryLayerCatalogItem.defaultUpdaters.availableDates = function() { // Do not update/serialize availableDates. }; freezeObject(ImageryLayerCatalogItem.defaultUpdaters); ImageryLayerCatalogItem.defaultSerializers = clone(CatalogItem.defaultSerializers); ImageryLayerCatalogItem.defaultSerializers.intervals = function(catalogItem, json, propertyName) { if (defined(catalogItem.intervals)) { var result = []; for (var i = 0; i < catalogItem.intervals.length; ++i) { var interval = catalogItem.intervals.get(i); result.push({ interval: TimeInterval.toIso8601(interval), data: interval.data }); } json.intervals = result; } }; // Do not serialize the original intialTimeSource - serialize the current time. // That way if the item is shared, the desired time is used. ImageryLayerCatalogItem.defaultSerializers.initialTimeSource = function(catalogItem, json, propertyName) { if (defined(catalogItem.clock)) { json.initialTimeSource = JulianDate.toIso8601(catalogItem.clock.currentTime); } else { json.initialTimeSource = catalogItem.initialTimeSource; } }; ImageryLayerCatalogItem.defaultSerializers.clock = function() { // Do not serialize the clock when duplicating the item. // Since this is not shared, it is not serialized for sharing anyway. }; ImageryLayerCatalogItem.defaultSerializers.availableDates = function() { // Do not update/serialize availableDates. }; freezeObject(ImageryLayerCatalogItem.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[]} */ ImageryLayerCatalogItem.defaultPropertiesForSharing = clone(CatalogItem.defaultPropertiesForSharing); ImageryLayerCatalogItem.defaultPropertiesForSharing.push('opacity'); ImageryLayerCatalogItem.defaultPropertiesForSharing.push('keepOnTop'); ImageryLayerCatalogItem.defaultPropertiesForSharing.push('initialTimeSource'); ImageryLayerCatalogItem.defaultPropertiesForSharing.push('splitDirection'); freezeObject(ImageryLayerCatalogItem.defaultPropertiesForSharing); /** * Creates the {@link ImageryProvider} for this catalog item. * @param {ImageryLayerTime} [time] The time for which to create an imagery provider. In layers that are not time-dynamic, * this parameter is ignored. * @return {ImageryProvider} The created imagery provider. */ ImageryLayerCatalogItem.prototype.createImageryProvider = function(time) { var result = this._createImageryProvider(time); return result; }; /** * When implemented in a derived class, creates the {@link ImageryProvider} for this catalog item. * @abstract * @protected * @param {ImageryLayerTime} [time] The time for which to create an imagery provider. In layers that are not time-dynamic, * this parameter is ignored. * @return {ImageryProvider} The created imagery provider. */ ImageryLayerCatalogItem.prototype._createImageryProvider = function(time) { throw new DeveloperError('_createImageryProvider must be implemented in the derived class.'); }; ImageryLayerCatalogItem.prototype._enable = function(layerIndex) { if (defined(this._imageryLayer)) { return; } var isTimeDynamic = false; var currentTimeIdentifier; var nextTimeIdentifier; if (defined(this.intervals) && (this.intervals.length > 0) && defined(this.clock)) { isTimeDynamic = true; var index = this.intervals.indexOf(this.clock.currentTime); // Here we use the terria clock because we want to optomise for the case where the item is playing on the // timeline (which is linked to the terria clock) and preload the layer at the next time that the timeslider // will move to. const multiplier = this.terria.clock.multiplier; var nextIndex; if (index < 0) { this._currentIntervalIndex = -1; currentTimeIdentifier = undefined; nextIndex = ~index; if (multiplier < 0.0) { --nextIndex; } } else { this._currentIntervalIndex = index; currentTimeIdentifier = this.intervals.get(index).data; if (multiplier < 0.0) { nextIndex = index - 1; } else { nextIndex = index + 1; } } // Ideally we should also check (this.terria.clock.clockRange === ClockRange.LOOP_STOP) here (to save preloading // where it won't be used), but due to initaliseation order this.terria.clock.clockRange is not necessarily in // the state that it will be when nextIndex is needed. So we make the assumption that this is the most likely // case and optomise for this (since for the other cases UNBOUNDED / CLAMPED there is nothing to effectively preload). if (nextIndex === this.intervals.length) { nextIndex = 0; } if (nextIndex >= 0 && nextIndex < this.intervals.length) { this._nextIntervalIndex = nextIndex; nextTimeIdentifier = this.intervals.get(nextIndex).data; } else { this._nextIntervalIndex = -1; } } if (!isTimeDynamic || defined(currentTimeIdentifier)) { var currentImageryProvider = this.createImageryProvider(currentTimeIdentifier); this._imageryLayer = ImageryLayerCatalogItem.enableLayer(this, currentImageryProvider, this.opacity, layerIndex); } if (isTimeDynamic) { this._currentTimeSubscription = knockout.getObservable(this, 'currentTime').subscribe(function() { onClockTick(this); }, this); } if (defined(nextTimeIdentifier)) { var nextImageryProvider = this.createImageryProvider(nextTimeIdentifier); // Do not allow picking from the preloading layer. nextImageryProvider.enablePickFeatures = false; this._nextLayer = ImageryLayerCatalogItem.enableLayer(this, nextImageryProvider, 0.0, layerIndex + 1); } updateSplitDirection(this); }; ImageryLayerCatalogItem.prototype._disable = function() { if (defined(this._currentTimeSubscription)) { this._currentTimeSubscription.dispose(); this._currentTimeSubscription = undefined; } ImageryLayerCatalogItem.disableLayer(this, this._imageryLayer); this._imageryLayer = undefined; ImageryLayerCatalogItem.disableLayer(this, this._nextLayer); this._nextLayer = undefined; }; ImageryLayerCatalogItem.prototype._show = function() { // When the layer is not shown .showDataForTime() has no effect so if someone has updated the currentTime while the // item was not shown update the layer now. showDataForTime(this, this.currentTime); ImageryLayerCatalogItem.showLayer(this, this._imageryLayer); ImageryLayerCatalogItem.showLayer(this, this._nextLayer); }; ImageryLayerCatalogItem.prototype._hide = function() { ImageryLayerCatalogItem.hideLayer(this, this._imageryLayer); ImageryLayerCatalogItem.hideLayer(this, this._nextLayer); }; ImageryLayerCatalogItem.prototype.showOnSeparateMap = function(globeOrMap) { var imageryProvider = this._createImageryProvider(); var layer = ImageryLayerCatalogItem.enableLayer(this, imageryProvider, this.opacity, undefined, globeOrMap); globeOrMap.updateItemForSplitter(this); // equivalent to updateSplitDirection(catalogItem), but for any viewer (globeOrMap). ImageryLayerCatalogItem.showLayer(this, layer, globeOrMap); var that = this; return function() { ImageryLayerCatalogItem.hideLayer(that, layer, globeOrMap); ImageryLayerCatalogItem.disableLayer(that, layer, globeOrMap); }; }; /** * Refreshes this layer on the map. This is useful when, for example, parameters that went into * {@link ImageryLayerCatalogItem#_createImageryProvider} change. */ ImageryLayerCatalogItem.prototype.refresh = function() { if (!defined(this._imageryLayer)) { return; } var currentIndex; if (defined(this.terria.cesium)) { var imageryLayers = this.terria.cesium.scene.imageryLayers; currentIndex = imageryLayers.indexOf(this._imageryLayer); } this._hide(); this._disable(); if (this.isEnabled) { this._enable(currentIndex); if (this.isShown) { this._show(); } } this.terria.currentViewer.notifyRepaintRequired(); }; function updateOpacity(item) { if (defined(item._imageryLayer) && item.isEnabled && item.isShown) { if (defined(item._imageryLayer.alpha)) { item._imageryLayer.alpha = item.opacity; } if (defined(item._imageryLayer.setOpacity)) { item._imageryLayer.setOpacity(item.opacity); } item.terria.currentViewer.notifyRepaintRequired(); } } function updateSplitDirection(item) { item.terria.currentViewer.updateItemForSplitter(item); } ImageryLayerCatalogItem.enableLayer = function(catalogItem, imageryProvider, opacity, layerIndex, globeOrMap) { globeOrMap = defaultValue(globeOrMap, catalogItem.terria.currentViewer); let tileFailures = 0; const layer = globeOrMap.addImageryProvider({ imageryProvider: imageryProvider, rectangle: catalogItem.rectangle, clipToRectangle: catalogItem.clipToRectangle, opacity: opacity, layerIndex: layerIndex, treat403AsError: catalogItem.treat403AsError, treat404AsError: catalogItem.treat404AsError, ignoreUnknownTileErrors: catalogItem.ignoreUnknownTileErrors, isRequiredForRendering: catalogItem.isRequiredForRendering, onLoadError: function(tileProviderError) { if (!defined(layer) || !globeOrMap.isImageryLayerShown({layer})) { // If the layer is no longer shown, ignore errors and don't retry. return undefined; } if (tileProviderError.timesRetried === 0) { tileFailures = 0; } tileProviderError.retry = undefined; if (defined(tileProviderError.error) && defined(tileProviderError.error.statusCode)) { tileProviderError.retry = when.reject(tileProviderError.error); } else if (defined(tileProviderError.x) && defined(tileProviderError.y) && defined(tileProviderError.level)) { // Something went wrong, but we don't really know what, probably because this is a failed image load. // Browsers tell us almost nothing on a failed image load. // So re-do the request with XMLHttpRequest so we can get more details of the failure. We need to // hackily get the URL out in order to do this. const tileUrl = getUrlForImageryTile(imageryProvider, tileProviderError.x, tileProviderError.y, tileProviderError.level); if (tileUrl) { tileProviderError.retry = loadWithXhr({ url: tileUrl }); } } if (!tileProviderError.retry) { // We couldn't get any details. Oh well, carry on. tileProviderError.retry = when.reject(tileProviderError.error); } if (catalogItem.handleTileError) { tileProviderError.retry = catalogItem.handleTileError(tileProviderError.retry, imageryProvider, tileProviderError.x, tileProviderError.y, tileProviderError.level); } tileProviderError.retry = tileProviderError.retry.then(function() { // Hey wait, the request succeeded this time! Let's just try the image again, then. // Just letting the retry promise resolve will trigger a retry of the image load. }); tileProviderError.retry = tileProviderError.retry.otherwise(function(e) { e = e || {}; // The details request failed, as expected, and now we should know why. We can trigger retry // by resolving or give up by rejecting, depending on the detailed nature of the failure. // We're only concerned about failures for tiles that actually overlap this item's extent. if (defined(catalogItem.rectangle)) { var tilingScheme = imageryProvider.tilingScheme; var tileExtent = tilingScheme.tileXYToRectangle(tileProviderError.x, tileProviderError.y, tileProviderError.level); var intersection = Rectangle.intersection(tileExtent, catalogItem.rectangle); if (!defined(intersection)) { return when.reject(e); // tile failed and that's ok } } // Note that ignoreUnknownTileErrors is only for genuinely unknown (no status code) issues. if (e.statusCode === 404) { if(!catalogItem.treat404AsError) { return when.reject(e); // tile failed and that's ok } } else if (e.statusCode === 403) { if(!catalogItem.treat403AsError) { return when.reject(e); // tile failed and that's ok } } else if (catalogItem.ignoreUnknownTileErrors && !defined(e.statusCode)) { return when.reject(e); // tile failed and that's ok } // This failure is not ok. We allow a few "not ok" failures and then turn off the layer and // display a message to the user. // Retry 5 times. We can't count on the TileProviderError's accounting of the times retried because it will // quickly spin up without taking into account our logic above that allows some types of failures. ++tileFailures; if (tileFailures <= 5) { // Let this tile fail silently, even though it probably indicates something wrong with the server. return when.reject(e); } // Too many failures! if (defined(layer) && globeOrMap.isImageryLayerShown({layer})) { if (catalogItem === catalogItem.terria.baseMap || catalogItem.terria.baseMap instanceof CompositeCatalogItem && catalogItem.terria.baseMap.items.indexOf(catalogItem) >= 0) { globeOrMap.terria.error.raiseEvent(new TerriaError({ sender: catalogItem, title: 'Error accessing base map', message: 'An error occurred while attempting to download tiles for base map ' + catalogItem.terria.baseMap.name + '. This may indicate that there is a ' + 'problem with your internet connection, that the base map is temporarily unavailable, or that the base map ' + 'is invalid. Please select a different base map using the Map button in the top-right corner. Further technical details may be found below.<br/><br/>' + '<pre>' + formatError(e) + '</pre>' })); globeOrMap.hideImageryLayer({ layer: layer }); catalogItem.terria.baseMap = undefined; // Don't use this base map again on startup. catalogItem.terria.setLocalProperty('basemap', undefined); } else { globeOrMap.terria.error.raiseEvent(new TerriaError({ sender: catalogItem, title: 'Error accessing catalogue item', message: 'An error occurred while attempting to download tiles for catalogue item ' + catalogItem.name + '. This may indicate that there is a ' + 'problem with your internet connection, that the catalogue item is temporarily unavailable, or that the catalogue item ' + 'is invalid. The catalogue item has been hidden from the map. You may re-show it in the Now Viewing panel to try again. Further technical details may be found below.<br/><br/>' + '<pre>' + formatError(e) + '</pre>' })); if (globeOrMap === catalogItem.terria.currentViewer) { catalogItem.isShown = false; } else { globeOrMap.hideImageryLayer({ layer: layer }); } } } // Don't retry again. return when.reject(e); }); }, onProjectionError: function() { // If the TileLayer experiences an error, hide the catalog item and inform the user. globeOrMap.terria.error.raiseEvent({ sender: catalogItem, title: 'Unable to display dataset', message: catalogItem.name + '" cannot be shown in 2D because it does not support the standard Web Mercator (EPSG:3857) projection. ' + 'Please switch to 3D if it is supported on your system, update the dataset to support the projection, or use a different dataset.' }); if (globeOrMap === catalogItem.terria.currentViewer) { catalogItem.isShown = false; } else { globeOrMap.hideImageryLayer({ layer: layer }); } } }); return layer; }; ImageryLayerCatalogItem.disableLayer = function(catalogItem, layer, globeOrMap) { if (!defined(layer)) { return; } globeOrMap = defaultValue(globeOrMap, catalogItem.terria.currentViewer); globeOrMap.removeImageryLayer({ layer: layer }); }; function showDataForTime(catalogItem, currentTime, preloadNext) { if (!defined(currentTime)) { return; } var intervals = catalogItem.intervals; if (!defined(intervals) || !catalogItem.isEnabled || !catalogItem.isShown) { return; } const preload = defaultValue(preloadNext, true); var index = catalogItem._currentIntervalIndex; if (index < 0 || index >= intervals.length || !TimeInterval.contains(intervals.get(index), currentTime)) { // Find the interval containing the current time. index = intervals.indexOf(currentTime); if (index < 0) { // No interval contains this time, so do not show imagery at this time. ImageryLayerCatalogItem.disableLayer(catalogItem, catalogItem._imageryLayer); catalogItem._imageryLayer = undefined; catalogItem._currentIntervalIndex = -1; return; } // If the "next" layer is not applicable to this time, throw it away and create a new one. if (index !== catalogItem._nextIntervalIndex) { // Throw away the "next" layer, since it's not applicable. ImageryLayerCatalogItem.disableLayer(catalogItem, catalogItem._nextLayer); catalogItem._nextLayer = undefined; catalogItem._nextIntervalIndex = -1; // Create the new "next" layer var imageryProvider = catalogItem.createImageryProvider(catalogItem.intervals.get(index).data); imageryProvider.enablePickFeatures = false; catalogItem._nextLayer = ImageryLayerCatalogItem.enableLayer(catalogItem, imageryProvider, 0.0); updateSplitDirection(catalogItem); ImageryLayerCatalogItem.showLayer(catalogItem, catalogItem._nextLayer); } // At this point we can assume that _nextLayer is applicable to this time. // Make it visible setOpacity(catalogItem, catalogItem._nextLayer, catalogItem.opacity); fixNextLayerOrder(catalogItem, catalogItem._imageryLayer, catalogItem._nextLayer); ImageryLayerCatalogItem.disableLayer(catalogItem, catalogItem._imageryLayer); catalogItem._imageryLayer = catalogItem._nextLayer; if (defined(catalogItem._nextLayer)) { catalogItem._imageryLayer.imageryProvider.enablePickFeatures = true; } catalogItem._nextLayer = undefined; catalogItem._nextIntervalIndex = -1; catalogItem._currentIntervalIndex = index; if (preload) { // Prefetch the (predicted) next layer. // Here we use the terria clock because we want to optomise for the case where the item is playing on the // timeline (which is linked to the terria clock) and preload the layer at the next time that the timeslider // will move to. var nextIndex = catalogItem.terria.clock.multiplier >= 0.0 ? index + 1 : index - 1; if ((nextIndex === intervals.length) && (catalogItem.terria.clock.clockRange === ClockRange.LOOP_STOP)) { nextIndex = 0; } if (nextIndex >= 0 && nextIndex < intervals.length && nextIndex !== catalogItem._nextIntervalIndex) { var nextImageryProvider = catalogItem.createImageryProvider(catalogItem.intervals.get(nextIndex).data); nextImageryProvider.enablePickFeatures = false; catalogItem._nextLayer = ImageryLayerCatalogItem.enableLayer(catalogItem, nextImageryProvider, 0.0); updateSplitDirection(catalogItem); ImageryLayerCatalogItem.showLayer(catalogItem, catalogItem._nextLayer); catalogItem._nextIntervalIndex = nextIndex; } } } } function onClockTick(catalogItem) { var intervals = catalogItem.intervals; if (!defined(intervals) || !catalogItem.isEnabled || !catalogItem.isShown || !defined(catalogItem.clock)) { return; } showDataForTime(catalogItem, catalogItem.clock.currentTime); } ImageryLayerCatalogItem.showLayer = function(catalogItem, layer, globeOrMap) { if (!defined(layer)) { return; } globeOrMap = defaultValue(globeOrMap, catalogItem.terria.currentViewer); globeOrMap.showImageryLayer({ layer: layer }); }; ImageryLayerCatalogItem.hideLayer = function(catalogItem, layer, globeOrMap) { if (!defined(layer)) { return; } globeOrMap = defaultValue(globeOrMap, catalogItem.terria.currentViewer); globeOrMap.hideImageryLayer({ layer: layer }); }; module.exports = ImageryLayerCatalogItem;