TimeDynamicImagery.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import Check from "../Core/Check.js";
  2. import defaultValue from "../Core/defaultValue.js";
  3. import defined from "../Core/defined.js";
  4. import DeveloperError from "../Core/DeveloperError.js";
  5. import JulianDate from "../Core/JulianDate.js";
  6. import Request from "../Core/Request.js";
  7. import RequestType from "../Core/RequestType.js";
  8. /**
  9. * Provides functionality for ImageryProviders that have time dynamic imagery
  10. *
  11. * @alias TimeDynamicImagery
  12. * @constructor
  13. *
  14. * @param {Object} options Object with the following properties:
  15. * @param {Clock} options.clock A Clock instance that is used when determining the value for the time dimension. Required when <code>options.times</code> is specified.
  16. * @param {TimeIntervalCollection} options.times TimeIntervalCollection with its <code>data</code> property being an object containing time dynamic dimension and their values.
  17. * @param {Function} options.requestImageFunction A function that will request imagery tiles.
  18. * @param {Function} options.reloadFunction A function that will be called when all imagery tiles need to be reloaded.
  19. */
  20. function TimeDynamicImagery(options) {
  21. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  22. //>>includeStart('debug', pragmas.debug);
  23. Check.typeOf.object("options.clock", options.clock);
  24. Check.typeOf.object("options.times", options.times);
  25. Check.typeOf.func(
  26. "options.requestImageFunction",
  27. options.requestImageFunction
  28. );
  29. Check.typeOf.func("options.reloadFunction", options.reloadFunction);
  30. //>>includeEnd('debug');
  31. this._tileCache = {};
  32. this._tilesRequestedForInterval = [];
  33. const clock = (this._clock = options.clock);
  34. this._times = options.times;
  35. this._requestImageFunction = options.requestImageFunction;
  36. this._reloadFunction = options.reloadFunction;
  37. this._currentIntervalIndex = -1;
  38. clock.onTick.addEventListener(this._clockOnTick, this);
  39. this._clockOnTick(clock);
  40. }
  41. Object.defineProperties(TimeDynamicImagery.prototype, {
  42. /**
  43. * Gets or sets a clock that is used to get keep the time used for time dynamic parameters.
  44. * @memberof TimeDynamicImagery.prototype
  45. * @type {Clock}
  46. */
  47. clock: {
  48. get: function () {
  49. return this._clock;
  50. },
  51. set: function (value) {
  52. //>>includeStart('debug', pragmas.debug);
  53. if (!defined(value)) {
  54. throw new DeveloperError("value is required.");
  55. }
  56. //>>includeEnd('debug');
  57. if (this._clock !== value) {
  58. this._clock = value;
  59. this._clockOnTick(value);
  60. this._reloadFunction();
  61. }
  62. },
  63. },
  64. /**
  65. * Gets or sets a time interval collection.
  66. * @memberof TimeDynamicImagery.prototype
  67. * @type {TimeIntervalCollection}
  68. */
  69. times: {
  70. get: function () {
  71. return this._times;
  72. },
  73. set: function (value) {
  74. //>>includeStart('debug', pragmas.debug);
  75. if (!defined(value)) {
  76. throw new DeveloperError("value is required.");
  77. }
  78. //>>includeEnd('debug');
  79. if (this._times !== value) {
  80. this._times = value;
  81. this._clockOnTick(this._clock);
  82. this._reloadFunction();
  83. }
  84. },
  85. },
  86. /**
  87. * Gets the current interval.
  88. * @memberof TimeDynamicImagery.prototype
  89. * @type {TimeInterval}
  90. */
  91. currentInterval: {
  92. get: function () {
  93. return this._times.get(this._currentIntervalIndex);
  94. },
  95. },
  96. });
  97. /**
  98. * Gets the tile from the cache if its available.
  99. *
  100. * @param {Number} x The tile X coordinate.
  101. * @param {Number} y The tile Y coordinate.
  102. * @param {Number} level The tile level.
  103. * @param {Request} [request] The request object. Intended for internal use only.
  104. *
  105. * @returns {Promise.<HTMLImageElement>|undefined} A promise for the image that will resolve when the image is available, or
  106. * undefined if the tile is not in the cache.
  107. */
  108. TimeDynamicImagery.prototype.getFromCache = function (x, y, level, request) {
  109. const key = getKey(x, y, level);
  110. let result;
  111. const cache = this._tileCache[this._currentIntervalIndex];
  112. if (defined(cache) && defined(cache[key])) {
  113. const item = cache[key];
  114. result = item.promise.catch(function (e) {
  115. // Set the correct state in case it was cancelled
  116. request.state = item.request.state;
  117. throw e;
  118. });
  119. delete cache[key];
  120. }
  121. return result;
  122. };
  123. /**
  124. * Checks if the next interval is approaching and will start preload the tile if necessary. Otherwise it will
  125. * just add the tile to a list to preload when we approach the next interval.
  126. *
  127. * @param {Number} x The tile X coordinate.
  128. * @param {Number} y The tile Y coordinate.
  129. * @param {Number} level The tile level.
  130. * @param {Request} [request] The request object. Intended for internal use only.
  131. */
  132. TimeDynamicImagery.prototype.checkApproachingInterval = function (
  133. x,
  134. y,
  135. level,
  136. request
  137. ) {
  138. const key = getKey(x, y, level);
  139. const tilesRequestedForInterval = this._tilesRequestedForInterval;
  140. // If we are approaching an interval, preload this tile in the next interval
  141. const approachingInterval = getApproachingInterval(this);
  142. const tile = {
  143. key: key,
  144. // Determines priority based on camera distance to the tile.
  145. // Since the imagery regardless of time will be attached to the same tile we can just steal it.
  146. priorityFunction: request.priorityFunction,
  147. };
  148. if (
  149. !defined(approachingInterval) ||
  150. !addToCache(this, tile, approachingInterval)
  151. ) {
  152. // Add to recent request list if we aren't approaching and interval or the request was throttled
  153. tilesRequestedForInterval.push(tile);
  154. }
  155. // Don't let the tile list get out of hand
  156. if (tilesRequestedForInterval.length >= 512) {
  157. tilesRequestedForInterval.splice(0, 256);
  158. }
  159. };
  160. TimeDynamicImagery.prototype._clockOnTick = function (clock) {
  161. const time = clock.currentTime;
  162. const times = this._times;
  163. const index = times.indexOf(time);
  164. const currentIntervalIndex = this._currentIntervalIndex;
  165. if (index !== currentIntervalIndex) {
  166. // Cancel all outstanding requests and clear out caches not from current time interval
  167. const currentCache = this._tileCache[currentIntervalIndex];
  168. for (const t in currentCache) {
  169. if (currentCache.hasOwnProperty(t)) {
  170. currentCache[t].request.cancel();
  171. }
  172. }
  173. delete this._tileCache[currentIntervalIndex];
  174. this._tilesRequestedForInterval = [];
  175. this._currentIntervalIndex = index;
  176. this._reloadFunction();
  177. return;
  178. }
  179. const approachingInterval = getApproachingInterval(this);
  180. if (defined(approachingInterval)) {
  181. // Start loading recent tiles from end of this._tilesRequestedForInterval
  182. // We keep preloading until we hit a throttling limit.
  183. const tilesRequested = this._tilesRequestedForInterval;
  184. let success = true;
  185. while (success) {
  186. if (tilesRequested.length === 0) {
  187. break;
  188. }
  189. const tile = tilesRequested.pop();
  190. success = addToCache(this, tile, approachingInterval);
  191. if (!success) {
  192. tilesRequested.push(tile);
  193. }
  194. }
  195. }
  196. };
  197. function getKey(x, y, level) {
  198. return `${x}-${y}-${level}`;
  199. }
  200. function getKeyElements(key) {
  201. const s = key.split("-");
  202. if (s.length !== 3) {
  203. return undefined;
  204. }
  205. return {
  206. x: Number(s[0]),
  207. y: Number(s[1]),
  208. level: Number(s[2]),
  209. };
  210. }
  211. function getApproachingInterval(that) {
  212. const times = that._times;
  213. if (!defined(times)) {
  214. return undefined;
  215. }
  216. const clock = that._clock;
  217. const time = clock.currentTime;
  218. const isAnimating = clock.canAnimate && clock.shouldAnimate;
  219. const multiplier = clock.multiplier;
  220. if (!isAnimating && multiplier !== 0) {
  221. return undefined;
  222. }
  223. let seconds;
  224. let index = times.indexOf(time);
  225. if (index < 0) {
  226. return undefined;
  227. }
  228. const interval = times.get(index);
  229. if (multiplier > 0) {
  230. // animating forward
  231. seconds = JulianDate.secondsDifference(interval.stop, time);
  232. ++index;
  233. } else {
  234. //backwards
  235. seconds = JulianDate.secondsDifference(interval.start, time); // Will be negative
  236. --index;
  237. }
  238. seconds /= multiplier; // Will always be positive
  239. // Less than 5 wall time seconds
  240. return index >= 0 && seconds <= 5.0 ? times.get(index) : undefined;
  241. }
  242. function addToCache(that, tile, interval) {
  243. const index = that._times.indexOf(interval.start);
  244. const tileCache = that._tileCache;
  245. let intervalTileCache = tileCache[index];
  246. if (!defined(intervalTileCache)) {
  247. intervalTileCache = tileCache[index] = {};
  248. }
  249. const key = tile.key;
  250. if (defined(intervalTileCache[key])) {
  251. return true; // Already in the cache
  252. }
  253. const keyElements = getKeyElements(key);
  254. const request = new Request({
  255. throttle: false,
  256. throttleByServer: true,
  257. type: RequestType.IMAGERY,
  258. priorityFunction: tile.priorityFunction,
  259. });
  260. const promise = that._requestImageFunction(
  261. keyElements.x,
  262. keyElements.y,
  263. keyElements.level,
  264. request,
  265. interval
  266. );
  267. if (!defined(promise)) {
  268. return false;
  269. }
  270. intervalTileCache[key] = {
  271. promise: promise,
  272. request: request,
  273. };
  274. return true;
  275. }
  276. export default TimeDynamicImagery;