You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

593 lines
18 KiB
JavaScript

import createGuid from "../Core/createGuid.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import CesiumMath from "../Core/Math.js";
import Entity from "./Entity.js";
import EntityCollection from "./EntityCollection.js";
var entityOptionsScratch = {
id: undefined,
};
var entityIdScratch = new Array(2);
function clean(entity) {
var propertyNames = entity.propertyNames;
var propertyNamesLength = propertyNames.length;
for (var i = 0; i < propertyNamesLength; i++) {
entity[propertyNames[i]] = undefined;
}
entity._name = undefined;
entity._availability = undefined;
}
function subscribeToEntity(that, eventHash, collectionId, entity) {
entityIdScratch[0] = collectionId;
entityIdScratch[1] = entity.id;
eventHash[
JSON.stringify(entityIdScratch)
] = entity.definitionChanged.addEventListener(
CompositeEntityCollection.prototype._onDefinitionChanged,
that
);
}
function unsubscribeFromEntity(that, eventHash, collectionId, entity) {
entityIdScratch[0] = collectionId;
entityIdScratch[1] = entity.id;
var id = JSON.stringify(entityIdScratch);
eventHash[id]();
eventHash[id] = undefined;
}
function recomposite(that) {
that._shouldRecomposite = true;
if (that._suspendCount !== 0) {
return;
}
var collections = that._collections;
var collectionsLength = collections.length;
var collectionsCopy = that._collectionsCopy;
var collectionsCopyLength = collectionsCopy.length;
var i;
var entity;
var entities;
var iEntities;
var collection;
var composite = that._composite;
var newEntities = new EntityCollection(that);
var eventHash = that._eventHash;
var collectionId;
for (i = 0; i < collectionsCopyLength; i++) {
collection = collectionsCopy[i];
collection.collectionChanged.removeEventListener(
CompositeEntityCollection.prototype._onCollectionChanged,
that
);
entities = collection.values;
collectionId = collection.id;
for (iEntities = entities.length - 1; iEntities > -1; iEntities--) {
entity = entities[iEntities];
unsubscribeFromEntity(that, eventHash, collectionId, entity);
}
}
for (i = collectionsLength - 1; i >= 0; i--) {
collection = collections[i];
collection.collectionChanged.addEventListener(
CompositeEntityCollection.prototype._onCollectionChanged,
that
);
//Merge all of the existing entities.
entities = collection.values;
collectionId = collection.id;
for (iEntities = entities.length - 1; iEntities > -1; iEntities--) {
entity = entities[iEntities];
subscribeToEntity(that, eventHash, collectionId, entity);
var compositeEntity = newEntities.getById(entity.id);
if (!defined(compositeEntity)) {
compositeEntity = composite.getById(entity.id);
if (!defined(compositeEntity)) {
entityOptionsScratch.id = entity.id;
compositeEntity = new Entity(entityOptionsScratch);
} else {
clean(compositeEntity);
}
newEntities.add(compositeEntity);
}
compositeEntity.merge(entity);
}
}
that._collectionsCopy = collections.slice(0);
composite.suspendEvents();
composite.removeAll();
var newEntitiesArray = newEntities.values;
for (i = 0; i < newEntitiesArray.length; i++) {
composite.add(newEntitiesArray[i]);
}
composite.resumeEvents();
}
/**
* Non-destructively composites multiple {@link EntityCollection} instances into a single collection.
* If a Entity with the same ID exists in multiple collections, it is non-destructively
* merged into a single new entity instance. If an entity has the same property in multiple
* collections, the property of the Entity in the last collection of the list it
* belongs to is used. CompositeEntityCollection can be used almost anywhere that a
* EntityCollection is used.
*
* @alias CompositeEntityCollection
* @constructor
*
* @param {EntityCollection[]} [collections] The initial list of EntityCollection instances to merge.
* @param {DataSource|CompositeEntityCollection} [owner] The data source (or composite entity collection) which created this collection.
*/
function CompositeEntityCollection(collections, owner) {
this._owner = owner;
this._composite = new EntityCollection(this);
this._suspendCount = 0;
this._collections = defined(collections) ? collections.slice() : [];
this._collectionsCopy = [];
this._id = createGuid();
this._eventHash = {};
recomposite(this);
this._shouldRecomposite = false;
}
Object.defineProperties(CompositeEntityCollection.prototype, {
/**
* Gets the event that is fired when entities are added or removed from the collection.
* The generated event is a {@link EntityCollection.collectionChangedEventCallback}.
* @memberof CompositeEntityCollection.prototype
* @readonly
* @type {Event}
*/
collectionChanged: {
get: function () {
return this._composite._collectionChanged;
},
},
/**
* Gets a globally unique identifier for this collection.
* @memberof CompositeEntityCollection.prototype
* @readonly
* @type {String}
*/
id: {
get: function () {
return this._id;
},
},
/**
* Gets the array of Entity instances in the collection.
* This array should not be modified directly.
* @memberof CompositeEntityCollection.prototype
* @readonly
* @type {Entity[]}
*/
values: {
get: function () {
return this._composite.values;
},
},
/**
* Gets the owner of this composite entity collection, ie. the data source or composite entity collection which created it.
* @memberof CompositeEntityCollection.prototype
* @readonly
* @type {DataSource|CompositeEntityCollection}
*/
owner: {
get: function () {
return this._owner;
},
},
});
/**
* Adds a collection to the composite.
*
* @param {EntityCollection} collection the collection to add.
* @param {Number} [index] the index to add the collection at. If omitted, the collection will
* added on top of all existing collections.
*
* @exception {DeveloperError} index, if supplied, must be greater than or equal to zero and less than or equal to the number of collections.
*/
CompositeEntityCollection.prototype.addCollection = function (
collection,
index
) {
var hasIndex = defined(index);
//>>includeStart('debug', pragmas.debug);
if (!defined(collection)) {
throw new DeveloperError("collection is required.");
}
if (hasIndex) {
if (index < 0) {
throw new DeveloperError("index must be greater than or equal to zero.");
} else if (index > this._collections.length) {
throw new DeveloperError(
"index must be less than or equal to the number of collections."
);
}
}
//>>includeEnd('debug');
if (!hasIndex) {
index = this._collections.length;
this._collections.push(collection);
} else {
this._collections.splice(index, 0, collection);
}
recomposite(this);
};
/**
* Removes a collection from this composite, if present.
*
* @param {EntityCollection} collection The collection to remove.
* @returns {Boolean} true if the collection was in the composite and was removed,
* false if the collection was not in the composite.
*/
CompositeEntityCollection.prototype.removeCollection = function (collection) {
var index = this._collections.indexOf(collection);
if (index !== -1) {
this._collections.splice(index, 1);
recomposite(this);
return true;
}
return false;
};
/**
* Removes all collections from this composite.
*/
CompositeEntityCollection.prototype.removeAllCollections = function () {
this._collections.length = 0;
recomposite(this);
};
/**
* Checks to see if the composite contains a given collection.
*
* @param {EntityCollection} collection the collection to check for.
* @returns {Boolean} true if the composite contains the collection, false otherwise.
*/
CompositeEntityCollection.prototype.containsCollection = function (collection) {
return this._collections.indexOf(collection) !== -1;
};
/**
* Returns true if the provided entity is in this collection, false otherwise.
*
* @param {Entity} entity The entity.
* @returns {Boolean} true if the provided entity is in this collection, false otherwise.
*/
CompositeEntityCollection.prototype.contains = function (entity) {
return this._composite.contains(entity);
};
/**
* Determines the index of a given collection in the composite.
*
* @param {EntityCollection} collection The collection to find the index of.
* @returns {Number} The index of the collection in the composite, or -1 if the collection does not exist in the composite.
*/
CompositeEntityCollection.prototype.indexOfCollection = function (collection) {
return this._collections.indexOf(collection);
};
/**
* Gets a collection by index from the composite.
*
* @param {Number} index the index to retrieve.
*/
CompositeEntityCollection.prototype.getCollection = function (index) {
//>>includeStart('debug', pragmas.debug);
if (!defined(index)) {
throw new DeveloperError("index is required.", "index");
}
//>>includeEnd('debug');
return this._collections[index];
};
/**
* Gets the number of collections in this composite.
*/
CompositeEntityCollection.prototype.getCollectionsLength = function () {
return this._collections.length;
};
function getCollectionIndex(collections, collection) {
//>>includeStart('debug', pragmas.debug);
if (!defined(collection)) {
throw new DeveloperError("collection is required.");
}
//>>includeEnd('debug');
var index = collections.indexOf(collection);
//>>includeStart('debug', pragmas.debug);
if (index === -1) {
throw new DeveloperError("collection is not in this composite.");
}
//>>includeEnd('debug');
return index;
}
function swapCollections(composite, i, j) {
var arr = composite._collections;
i = CesiumMath.clamp(i, 0, arr.length - 1);
j = CesiumMath.clamp(j, 0, arr.length - 1);
if (i === j) {
return;
}
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
recomposite(composite);
}
/**
* Raises a collection up one position in the composite.
*
* @param {EntityCollection} collection the collection to move.
*
* @exception {DeveloperError} collection is not in this composite.
*/
CompositeEntityCollection.prototype.raiseCollection = function (collection) {
var index = getCollectionIndex(this._collections, collection);
swapCollections(this, index, index + 1);
};
/**
* Lowers a collection down one position in the composite.
*
* @param {EntityCollection} collection the collection to move.
*
* @exception {DeveloperError} collection is not in this composite.
*/
CompositeEntityCollection.prototype.lowerCollection = function (collection) {
var index = getCollectionIndex(this._collections, collection);
swapCollections(this, index, index - 1);
};
/**
* Raises a collection to the top of the composite.
*
* @param {EntityCollection} collection the collection to move.
*
* @exception {DeveloperError} collection is not in this composite.
*/
CompositeEntityCollection.prototype.raiseCollectionToTop = function (
collection
) {
var index = getCollectionIndex(this._collections, collection);
if (index === this._collections.length - 1) {
return;
}
this._collections.splice(index, 1);
this._collections.push(collection);
recomposite(this);
};
/**
* Lowers a collection to the bottom of the composite.
*
* @param {EntityCollection} collection the collection to move.
*
* @exception {DeveloperError} collection is not in this composite.
*/
CompositeEntityCollection.prototype.lowerCollectionToBottom = function (
collection
) {
var index = getCollectionIndex(this._collections, collection);
if (index === 0) {
return;
}
this._collections.splice(index, 1);
this._collections.splice(0, 0, collection);
recomposite(this);
};
/**
* Prevents {@link EntityCollection#collectionChanged} events from being raised
* until a corresponding call is made to {@link EntityCollection#resumeEvents}, at which
* point a single event will be raised that covers all suspended operations.
* This allows for many items to be added and removed efficiently.
* While events are suspended, recompositing of the collections will
* also be suspended, as this can be a costly operation.
* This function can be safely called multiple times as long as there
* are corresponding calls to {@link EntityCollection#resumeEvents}.
*/
CompositeEntityCollection.prototype.suspendEvents = function () {
this._suspendCount++;
this._composite.suspendEvents();
};
/**
* Resumes raising {@link EntityCollection#collectionChanged} events immediately
* when an item is added or removed. Any modifications made while while events were suspended
* will be triggered as a single event when this function is called. This function also ensures
* the collection is recomposited if events are also resumed.
* This function is reference counted and can safely be called multiple times as long as there
* are corresponding calls to {@link EntityCollection#resumeEvents}.
*
* @exception {DeveloperError} resumeEvents can not be called before suspendEvents.
*/
CompositeEntityCollection.prototype.resumeEvents = function () {
//>>includeStart('debug', pragmas.debug);
if (this._suspendCount === 0) {
throw new DeveloperError(
"resumeEvents can not be called before suspendEvents."
);
}
//>>includeEnd('debug');
this._suspendCount--;
// recomposite before triggering events (but only if required for performance) that might depend on a composited collection
if (this._shouldRecomposite && this._suspendCount === 0) {
recomposite(this);
this._shouldRecomposite = false;
}
this._composite.resumeEvents();
};
/**
* Computes the maximum availability of the entities in the collection.
* If the collection contains a mix of infinitely available data and non-infinite data,
* It will return the interval pertaining to the non-infinite data only. If all
* data is infinite, an infinite interval will be returned.
*
* @returns {TimeInterval} The availability of entities in the collection.
*/
CompositeEntityCollection.prototype.computeAvailability = function () {
return this._composite.computeAvailability();
};
/**
* Gets an entity with the specified id.
*
* @param {String} id The id of the entity to retrieve.
* @returns {Entity|undefined} The entity with the provided id or undefined if the id did not exist in the collection.
*/
CompositeEntityCollection.prototype.getById = function (id) {
return this._composite.getById(id);
};
CompositeEntityCollection.prototype._onCollectionChanged = function (
collection,
added,
removed
) {
var collections = this._collectionsCopy;
var collectionsLength = collections.length;
var composite = this._composite;
composite.suspendEvents();
var i;
var q;
var entity;
var compositeEntity;
var removedLength = removed.length;
var eventHash = this._eventHash;
var collectionId = collection.id;
for (i = 0; i < removedLength; i++) {
var removedEntity = removed[i];
unsubscribeFromEntity(this, eventHash, collectionId, removedEntity);
var removedId = removedEntity.id;
//Check if the removed entity exists in any of the remaining collections
//If so, we clean and remerge it.
for (q = collectionsLength - 1; q >= 0; q--) {
entity = collections[q].getById(removedId);
if (defined(entity)) {
if (!defined(compositeEntity)) {
compositeEntity = composite.getById(removedId);
clean(compositeEntity);
}
compositeEntity.merge(entity);
}
}
//We never retrieved the compositeEntity, which means it no longer
//exists in any of the collections, remove it from the composite.
if (!defined(compositeEntity)) {
composite.removeById(removedId);
}
compositeEntity = undefined;
}
var addedLength = added.length;
for (i = 0; i < addedLength; i++) {
var addedEntity = added[i];
subscribeToEntity(this, eventHash, collectionId, addedEntity);
var addedId = addedEntity.id;
//We know the added entity exists in at least one collection,
//but we need to check all collections and re-merge in order
//to maintain the priority of properties.
for (q = collectionsLength - 1; q >= 0; q--) {
entity = collections[q].getById(addedId);
if (defined(entity)) {
if (!defined(compositeEntity)) {
compositeEntity = composite.getById(addedId);
if (!defined(compositeEntity)) {
entityOptionsScratch.id = addedId;
compositeEntity = new Entity(entityOptionsScratch);
composite.add(compositeEntity);
} else {
clean(compositeEntity);
}
}
compositeEntity.merge(entity);
}
}
compositeEntity = undefined;
}
composite.resumeEvents();
};
CompositeEntityCollection.prototype._onDefinitionChanged = function (
entity,
propertyName,
newValue,
oldValue
) {
var collections = this._collections;
var composite = this._composite;
var collectionsLength = collections.length;
var id = entity.id;
var compositeEntity = composite.getById(id);
var compositeProperty = compositeEntity[propertyName];
var newProperty = !defined(compositeProperty);
var firstTime = true;
for (var q = collectionsLength - 1; q >= 0; q--) {
var innerEntity = collections[q].getById(entity.id);
if (defined(innerEntity)) {
var property = innerEntity[propertyName];
if (defined(property)) {
if (firstTime) {
firstTime = false;
//We only want to clone if the property is also mergeable.
//This ensures that leaf properties are referenced and not copied,
//which is the entire point of compositing.
if (defined(property.merge) && defined(property.clone)) {
compositeProperty = property.clone(compositeProperty);
} else {
compositeProperty = property;
break;
}
}
compositeProperty.merge(property);
}
}
}
if (
newProperty &&
compositeEntity.propertyNames.indexOf(propertyName) === -1
) {
compositeEntity.addProperty(propertyName);
}
compositeEntity[propertyName] = compositeProperty;
};
export default CompositeEntityCollection;