EntityCollection.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import AssociativeArray from "../Core/AssociativeArray.js";
  2. import createGuid from "../Core/createGuid.js";
  3. import defined from "../Core/defined.js";
  4. import DeveloperError from "../Core/DeveloperError.js";
  5. import Event from "../Core/Event.js";
  6. import Iso8601 from "../Core/Iso8601.js";
  7. import JulianDate from "../Core/JulianDate.js";
  8. import RuntimeError from "../Core/RuntimeError.js";
  9. import TimeInterval from "../Core/TimeInterval.js";
  10. import Entity from "./Entity.js";
  11. const entityOptionsScratch = {
  12. id: undefined,
  13. };
  14. function fireChangedEvent(collection) {
  15. if (collection._firing) {
  16. collection._refire = true;
  17. return;
  18. }
  19. if (collection._suspendCount === 0) {
  20. const added = collection._addedEntities;
  21. const removed = collection._removedEntities;
  22. const changed = collection._changedEntities;
  23. if (changed.length !== 0 || added.length !== 0 || removed.length !== 0) {
  24. collection._firing = true;
  25. do {
  26. collection._refire = false;
  27. const addedArray = added.values.slice(0);
  28. const removedArray = removed.values.slice(0);
  29. const changedArray = changed.values.slice(0);
  30. added.removeAll();
  31. removed.removeAll();
  32. changed.removeAll();
  33. collection._collectionChanged.raiseEvent(
  34. collection,
  35. addedArray,
  36. removedArray,
  37. changedArray
  38. );
  39. } while (collection._refire);
  40. collection._firing = false;
  41. }
  42. }
  43. }
  44. /**
  45. * An observable collection of {@link Entity} instances where each entity has a unique id.
  46. * @alias EntityCollection
  47. * @constructor
  48. *
  49. * @param {DataSource|CompositeEntityCollection} [owner] The data source (or composite entity collection) which created this collection.
  50. */
  51. function EntityCollection(owner) {
  52. this._owner = owner;
  53. this._entities = new AssociativeArray();
  54. this._addedEntities = new AssociativeArray();
  55. this._removedEntities = new AssociativeArray();
  56. this._changedEntities = new AssociativeArray();
  57. this._suspendCount = 0;
  58. this._collectionChanged = new Event();
  59. this._id = createGuid();
  60. this._show = true;
  61. this._firing = false;
  62. this._refire = false;
  63. }
  64. /**
  65. * Prevents {@link EntityCollection#collectionChanged} events from being raised
  66. * until a corresponding call is made to {@link EntityCollection#resumeEvents}, at which
  67. * point a single event will be raised that covers all suspended operations.
  68. * This allows for many items to be added and removed efficiently.
  69. * This function can be safely called multiple times as long as there
  70. * are corresponding calls to {@link EntityCollection#resumeEvents}.
  71. */
  72. EntityCollection.prototype.suspendEvents = function () {
  73. this._suspendCount++;
  74. };
  75. /**
  76. * Resumes raising {@link EntityCollection#collectionChanged} events immediately
  77. * when an item is added or removed. Any modifications made while while events were suspended
  78. * will be triggered as a single event when this function is called.
  79. * This function is reference counted and can safely be called multiple times as long as there
  80. * are corresponding calls to {@link EntityCollection#resumeEvents}.
  81. *
  82. * @exception {DeveloperError} resumeEvents can not be called before suspendEvents.
  83. */
  84. EntityCollection.prototype.resumeEvents = function () {
  85. //>>includeStart('debug', pragmas.debug);
  86. if (this._suspendCount === 0) {
  87. throw new DeveloperError(
  88. "resumeEvents can not be called before suspendEvents."
  89. );
  90. }
  91. //>>includeEnd('debug');
  92. this._suspendCount--;
  93. fireChangedEvent(this);
  94. };
  95. /**
  96. * The signature of the event generated by {@link EntityCollection#collectionChanged}.
  97. * @callback EntityCollection.CollectionChangedEventCallback
  98. *
  99. * @param {EntityCollection} collection The collection that triggered the event.
  100. * @param {Entity[]} added The array of {@link Entity} instances that have been added to the collection.
  101. * @param {Entity[]} removed The array of {@link Entity} instances that have been removed from the collection.
  102. * @param {Entity[]} changed The array of {@link Entity} instances that have been modified.
  103. */
  104. Object.defineProperties(EntityCollection.prototype, {
  105. /**
  106. * Gets the event that is fired when entities are added or removed from the collection.
  107. * The generated event is a {@link EntityCollection.CollectionChangedEventCallback}.
  108. * @memberof EntityCollection.prototype
  109. * @readonly
  110. * @type {Event<EntityCollection.CollectionChangedEventCallback>}
  111. */
  112. collectionChanged: {
  113. get: function () {
  114. return this._collectionChanged;
  115. },
  116. },
  117. /**
  118. * Gets a globally unique identifier for this collection.
  119. * @memberof EntityCollection.prototype
  120. * @readonly
  121. * @type {String}
  122. */
  123. id: {
  124. get: function () {
  125. return this._id;
  126. },
  127. },
  128. /**
  129. * Gets the array of Entity instances in the collection.
  130. * This array should not be modified directly.
  131. * @memberof EntityCollection.prototype
  132. * @readonly
  133. * @type {Entity[]}
  134. */
  135. values: {
  136. get: function () {
  137. return this._entities.values;
  138. },
  139. },
  140. /**
  141. * Gets whether or not this entity collection should be
  142. * displayed. When true, each entity is only displayed if
  143. * its own show property is also true.
  144. * @memberof EntityCollection.prototype
  145. * @type {Boolean}
  146. */
  147. show: {
  148. get: function () {
  149. return this._show;
  150. },
  151. set: function (value) {
  152. //>>includeStart('debug', pragmas.debug);
  153. if (!defined(value)) {
  154. throw new DeveloperError("value is required.");
  155. }
  156. //>>includeEnd('debug');
  157. if (value === this._show) {
  158. return;
  159. }
  160. //Since entity.isShowing includes the EntityCollection.show state
  161. //in its calculation, we need to loop over the entities array
  162. //twice, once to get the old showing value and a second time
  163. //to raise the changed event.
  164. this.suspendEvents();
  165. let i;
  166. const oldShows = [];
  167. const entities = this._entities.values;
  168. const entitiesLength = entities.length;
  169. for (i = 0; i < entitiesLength; i++) {
  170. oldShows.push(entities[i].isShowing);
  171. }
  172. this._show = value;
  173. for (i = 0; i < entitiesLength; i++) {
  174. const oldShow = oldShows[i];
  175. const entity = entities[i];
  176. if (oldShow !== entity.isShowing) {
  177. entity.definitionChanged.raiseEvent(
  178. entity,
  179. "isShowing",
  180. entity.isShowing,
  181. oldShow
  182. );
  183. }
  184. }
  185. this.resumeEvents();
  186. },
  187. },
  188. /**
  189. * Gets the owner of this entity collection, ie. the data source or composite entity collection which created it.
  190. * @memberof EntityCollection.prototype
  191. * @readonly
  192. * @type {DataSource|CompositeEntityCollection}
  193. */
  194. owner: {
  195. get: function () {
  196. return this._owner;
  197. },
  198. },
  199. });
  200. /**
  201. * Computes the maximum availability of the entities in the collection.
  202. * If the collection contains a mix of infinitely available data and non-infinite data,
  203. * it will return the interval pertaining to the non-infinite data only. If all
  204. * data is infinite, an infinite interval will be returned.
  205. *
  206. * @returns {TimeInterval} The availability of entities in the collection.
  207. */
  208. EntityCollection.prototype.computeAvailability = function () {
  209. let startTime = Iso8601.MAXIMUM_VALUE;
  210. let stopTime = Iso8601.MINIMUM_VALUE;
  211. const entities = this._entities.values;
  212. for (let i = 0, len = entities.length; i < len; i++) {
  213. const entity = entities[i];
  214. const availability = entity.availability;
  215. if (defined(availability)) {
  216. const start = availability.start;
  217. const stop = availability.stop;
  218. if (
  219. JulianDate.lessThan(start, startTime) &&
  220. !start.equals(Iso8601.MINIMUM_VALUE)
  221. ) {
  222. startTime = start;
  223. }
  224. if (
  225. JulianDate.greaterThan(stop, stopTime) &&
  226. !stop.equals(Iso8601.MAXIMUM_VALUE)
  227. ) {
  228. stopTime = stop;
  229. }
  230. }
  231. }
  232. if (Iso8601.MAXIMUM_VALUE.equals(startTime)) {
  233. startTime = Iso8601.MINIMUM_VALUE;
  234. }
  235. if (Iso8601.MINIMUM_VALUE.equals(stopTime)) {
  236. stopTime = Iso8601.MAXIMUM_VALUE;
  237. }
  238. return new TimeInterval({
  239. start: startTime,
  240. stop: stopTime,
  241. });
  242. };
  243. /**
  244. * Add an entity to the collection.
  245. *
  246. * @param {Entity | Entity.ConstructorOptions} entity The entity to be added.
  247. * @returns {Entity} The entity that was added.
  248. * @exception {DeveloperError} An entity with <entity.id> already exists in this collection.
  249. */
  250. EntityCollection.prototype.add = function (entity) {
  251. //>>includeStart('debug', pragmas.debug);
  252. if (!defined(entity)) {
  253. throw new DeveloperError("entity is required.");
  254. }
  255. //>>includeEnd('debug');
  256. if (!(entity instanceof Entity)) {
  257. entity = new Entity(entity);
  258. }
  259. const id = entity.id;
  260. const entities = this._entities;
  261. if (entities.contains(id)) {
  262. throw new RuntimeError(
  263. `An entity with id ${id} already exists in this collection.`
  264. );
  265. }
  266. entity.entityCollection = this;
  267. entities.set(id, entity);
  268. if (!this._removedEntities.remove(id)) {
  269. this._addedEntities.set(id, entity);
  270. }
  271. entity.definitionChanged.addEventListener(
  272. EntityCollection.prototype._onEntityDefinitionChanged,
  273. this
  274. );
  275. fireChangedEvent(this);
  276. return entity;
  277. };
  278. /**
  279. * Removes an entity from the collection.
  280. *
  281. * @param {Entity} entity The entity to be removed.
  282. * @returns {Boolean} true if the item was removed, false if it did not exist in the collection.
  283. */
  284. EntityCollection.prototype.remove = function (entity) {
  285. if (!defined(entity)) {
  286. return false;
  287. }
  288. return this.removeById(entity.id);
  289. };
  290. /**
  291. * Returns true if the provided entity is in this collection, false otherwise.
  292. *
  293. * @param {Entity} entity The entity.
  294. * @returns {Boolean} true if the provided entity is in this collection, false otherwise.
  295. */
  296. EntityCollection.prototype.contains = function (entity) {
  297. //>>includeStart('debug', pragmas.debug);
  298. if (!defined(entity)) {
  299. throw new DeveloperError("entity is required");
  300. }
  301. //>>includeEnd('debug');
  302. return this._entities.get(entity.id) === entity;
  303. };
  304. /**
  305. * Removes an entity with the provided id from the collection.
  306. *
  307. * @param {String} id The id of the entity to remove.
  308. * @returns {Boolean} true if the item was removed, false if no item with the provided id existed in the collection.
  309. */
  310. EntityCollection.prototype.removeById = function (id) {
  311. if (!defined(id)) {
  312. return false;
  313. }
  314. const entities = this._entities;
  315. const entity = entities.get(id);
  316. if (!this._entities.remove(id)) {
  317. return false;
  318. }
  319. if (!this._addedEntities.remove(id)) {
  320. this._removedEntities.set(id, entity);
  321. this._changedEntities.remove(id);
  322. }
  323. this._entities.remove(id);
  324. entity.definitionChanged.removeEventListener(
  325. EntityCollection.prototype._onEntityDefinitionChanged,
  326. this
  327. );
  328. fireChangedEvent(this);
  329. return true;
  330. };
  331. /**
  332. * Removes all Entities from the collection.
  333. */
  334. EntityCollection.prototype.removeAll = function () {
  335. //The event should only contain items added before events were suspended
  336. //and the contents of the collection.
  337. const entities = this._entities;
  338. const entitiesLength = entities.length;
  339. const array = entities.values;
  340. const addedEntities = this._addedEntities;
  341. const removed = this._removedEntities;
  342. for (let i = 0; i < entitiesLength; i++) {
  343. const existingItem = array[i];
  344. const existingItemId = existingItem.id;
  345. const addedItem = addedEntities.get(existingItemId);
  346. if (!defined(addedItem)) {
  347. existingItem.definitionChanged.removeEventListener(
  348. EntityCollection.prototype._onEntityDefinitionChanged,
  349. this
  350. );
  351. removed.set(existingItemId, existingItem);
  352. }
  353. }
  354. entities.removeAll();
  355. addedEntities.removeAll();
  356. this._changedEntities.removeAll();
  357. fireChangedEvent(this);
  358. };
  359. /**
  360. * Gets an entity with the specified id.
  361. *
  362. * @param {String} id The id of the entity to retrieve.
  363. * @returns {Entity|undefined} The entity with the provided id or undefined if the id did not exist in the collection.
  364. */
  365. EntityCollection.prototype.getById = function (id) {
  366. //>>includeStart('debug', pragmas.debug);
  367. if (!defined(id)) {
  368. throw new DeveloperError("id is required.");
  369. }
  370. //>>includeEnd('debug');
  371. return this._entities.get(id);
  372. };
  373. /**
  374. * Gets an entity with the specified id or creates it and adds it to the collection if it does not exist.
  375. *
  376. * @param {String} id The id of the entity to retrieve or create.
  377. * @returns {Entity} The new or existing object.
  378. */
  379. EntityCollection.prototype.getOrCreateEntity = function (id) {
  380. //>>includeStart('debug', pragmas.debug);
  381. if (!defined(id)) {
  382. throw new DeveloperError("id is required.");
  383. }
  384. //>>includeEnd('debug');
  385. let entity = this._entities.get(id);
  386. if (!defined(entity)) {
  387. entityOptionsScratch.id = id;
  388. entity = new Entity(entityOptionsScratch);
  389. this.add(entity);
  390. }
  391. return entity;
  392. };
  393. EntityCollection.prototype._onEntityDefinitionChanged = function (entity) {
  394. const id = entity.id;
  395. if (!this._addedEntities.contains(id)) {
  396. this._changedEntities.set(id, entity);
  397. }
  398. fireChangedEvent(this);
  399. };
  400. export default EntityCollection;