terriajs
Version:
Geospatial data visualization platform.
851 lines (730 loc) • 36.7 kB
JavaScript
'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;