import Check from "../Core/Check.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import JulianDate from "../Core/JulianDate.js"; import Request from "../Core/Request.js"; import RequestType from "../Core/RequestType.js"; /** * Provides functionality for ImageryProviders that have time dynamic imagery * * @alias TimeDynamicImagery * @constructor * * @param {object} options Object with the following properties: * @param {Clock} options.clock A Clock instance that is used when determining the value for the time dimension. Required when options.times is specified. * @param {TimeIntervalCollection} options.times TimeIntervalCollection with its data property being an object containing time dynamic dimension and their values. * @param {Function} options.requestImageFunction A function that will request imagery tiles. * @param {Function} options.reloadFunction A function that will be called when all imagery tiles need to be reloaded. */ function TimeDynamicImagery(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); //>>includeStart('debug', pragmas.debug); Check.typeOf.object("options.clock", options.clock); Check.typeOf.object("options.times", options.times); Check.typeOf.func( "options.requestImageFunction", options.requestImageFunction ); Check.typeOf.func("options.reloadFunction", options.reloadFunction); //>>includeEnd('debug'); this._tileCache = {}; this._tilesRequestedForInterval = []; const clock = (this._clock = options.clock); this._times = options.times; this._requestImageFunction = options.requestImageFunction; this._reloadFunction = options.reloadFunction; this._currentIntervalIndex = -1; clock.onTick.addEventListener(this._clockOnTick, this); this._clockOnTick(clock); } Object.defineProperties(TimeDynamicImagery.prototype, { /** * Gets or sets a clock that is used to get keep the time used for time dynamic parameters. * @memberof TimeDynamicImagery.prototype * @type {Clock} */ clock: { get: function () { return this._clock; }, set: function (value) { //>>includeStart('debug', pragmas.debug); if (!defined(value)) { throw new DeveloperError("value is required."); } //>>includeEnd('debug'); if (this._clock !== value) { this._clock = value; this._clockOnTick(value); this._reloadFunction(); } }, }, /** * Gets or sets a time interval collection. * @memberof TimeDynamicImagery.prototype * @type {TimeIntervalCollection} */ times: { get: function () { return this._times; }, set: function (value) { //>>includeStart('debug', pragmas.debug); if (!defined(value)) { throw new DeveloperError("value is required."); } //>>includeEnd('debug'); if (this._times !== value) { this._times = value; this._clockOnTick(this._clock); this._reloadFunction(); } }, }, /** * Gets the current interval. * @memberof TimeDynamicImagery.prototype * @type {TimeInterval} */ currentInterval: { get: function () { return this._times.get(this._currentIntervalIndex); }, }, }); /** * Gets the tile from the cache if its available. * * @param {number} x The tile X coordinate. * @param {number} y The tile Y coordinate. * @param {number} level The tile level. * @param {Request} [request] The request object. Intended for internal use only. * * @returns {Promise|undefined} A promise for the image that will resolve when the image is available, or * undefined if the tile is not in the cache. */ TimeDynamicImagery.prototype.getFromCache = function (x, y, level, request) { const key = getKey(x, y, level); let result; const cache = this._tileCache[this._currentIntervalIndex]; if (defined(cache) && defined(cache[key])) { const item = cache[key]; result = item.promise.catch(function (e) { // Set the correct state in case it was cancelled request.state = item.request.state; throw e; }); delete cache[key]; } return result; }; /** * Checks if the next interval is approaching and will start preload the tile if necessary. Otherwise it will * just add the tile to a list to preload when we approach the next interval. * * @param {number} x The tile X coordinate. * @param {number} y The tile Y coordinate. * @param {number} level The tile level. * @param {Request} [request] The request object. Intended for internal use only. */ TimeDynamicImagery.prototype.checkApproachingInterval = function ( x, y, level, request ) { const key = getKey(x, y, level); const tilesRequestedForInterval = this._tilesRequestedForInterval; // If we are approaching an interval, preload this tile in the next interval const approachingInterval = getApproachingInterval(this); const tile = { key: key, // Determines priority based on camera distance to the tile. // Since the imagery regardless of time will be attached to the same tile we can just steal it. priorityFunction: request.priorityFunction, }; if ( !defined(approachingInterval) || !addToCache(this, tile, approachingInterval) ) { // Add to recent request list if we aren't approaching and interval or the request was throttled tilesRequestedForInterval.push(tile); } // Don't let the tile list get out of hand if (tilesRequestedForInterval.length >= 512) { tilesRequestedForInterval.splice(0, 256); } }; TimeDynamicImagery.prototype._clockOnTick = function (clock) { const time = clock.currentTime; const times = this._times; const index = times.indexOf(time); const currentIntervalIndex = this._currentIntervalIndex; if (index !== currentIntervalIndex) { // Cancel all outstanding requests and clear out caches not from current time interval const currentCache = this._tileCache[currentIntervalIndex]; for (const t in currentCache) { if (currentCache.hasOwnProperty(t)) { currentCache[t].request.cancel(); } } delete this._tileCache[currentIntervalIndex]; this._tilesRequestedForInterval = []; this._currentIntervalIndex = index; this._reloadFunction(); return; } const approachingInterval = getApproachingInterval(this); if (defined(approachingInterval)) { // Start loading recent tiles from end of this._tilesRequestedForInterval // We keep preloading until we hit a throttling limit. const tilesRequested = this._tilesRequestedForInterval; let success = true; while (success) { if (tilesRequested.length === 0) { break; } const tile = tilesRequested.pop(); success = addToCache(this, tile, approachingInterval); if (!success) { tilesRequested.push(tile); } } } }; function getKey(x, y, level) { return `${x}-${y}-${level}`; } function getKeyElements(key) { const s = key.split("-"); if (s.length !== 3) { return undefined; } return { x: Number(s[0]), y: Number(s[1]), level: Number(s[2]), }; } function getApproachingInterval(that) { const times = that._times; if (!defined(times)) { return undefined; } const clock = that._clock; const time = clock.currentTime; const isAnimating = clock.canAnimate && clock.shouldAnimate; const multiplier = clock.multiplier; if (!isAnimating && multiplier !== 0) { return undefined; } let seconds; let index = times.indexOf(time); if (index < 0) { return undefined; } const interval = times.get(index); if (multiplier > 0) { // animating forward seconds = JulianDate.secondsDifference(interval.stop, time); ++index; } else { //backwards seconds = JulianDate.secondsDifference(interval.start, time); // Will be negative --index; } seconds /= multiplier; // Will always be positive // Less than 5 wall time seconds return index >= 0 && seconds <= 5.0 ? times.get(index) : undefined; } function addToCache(that, tile, interval) { const index = that._times.indexOf(interval.start); const tileCache = that._tileCache; let intervalTileCache = tileCache[index]; if (!defined(intervalTileCache)) { intervalTileCache = tileCache[index] = {}; } const key = tile.key; if (defined(intervalTileCache[key])) { return true; // Already in the cache } const keyElements = getKeyElements(key); const request = new Request({ throttle: false, throttleByServer: true, type: RequestType.IMAGERY, priorityFunction: tile.priorityFunction, }); const promise = that._requestImageFunction( keyElements.x, keyElements.y, keyElements.level, request, interval ); if (!defined(promise)) { return false; } intervalTileCache[key] = { promise: promise, request: request, }; return true; } export default TimeDynamicImagery;