/*global define*/
define([
'../Core/BoundingRectangle',
'../Core/Cartesian2',
'../Core/Cartesian3',
'../Core/Color',
'../Core/defaultValue',
'../Core/defined',
'../Core/defineProperties',
'../Core/destroyObject',
'../Core/EllipsoidalOccluder',
'../Core/Event',
'../Core/Matrix4',
'../Scene/Billboard',
'../Scene/BillboardCollection',
'../Scene/HeightReference',
'../Scene/HorizontalOrigin',
'../Scene/Label',
'../Scene/LabelCollection',
'../Scene/LabelStyle',
'../Scene/PointPrimitive',
'../Scene/PointPrimitiveCollection',
'../Scene/SceneTransforms',
'../Scene/VerticalOrigin',
'../ThirdParty/kdbush',
'./Entity',
'./Property'
], function(
BoundingRectangle,
Cartesian2,
Cartesian3,
Color,
defaultValue,
defined,
defineProperties,
destroyObject,
EllipsoidalOccluder,
Event,
Matrix4,
Billboard,
BillboardCollection,
HeightReference,
HorizontalOrigin,
Label,
LabelCollection,
LabelStyle,
PointPrimitive,
PointPrimitiveCollection,
SceneTransforms,
VerticalOrigin,
kdbush,
Entity,
Property) {
'use strict';
/**
* Defines how screen space objects (billboards, points, labels) are clustered.
*
* @param {Object} [options] An object with the following properties:
* @param {Boolean} [options.enabled=false] Whether or not to enable clustering.
* @param {Number} [options.pixelRange=80] The pixel range to extend the screen space bounding box.
* @param {Number} [options.minimumClusterSize=2] The minimum number of screen space objects that can be clustered.
* @param {Boolean} [options.clusterBillboards=true] Whether or not to cluster the billboards of an entity.
* @param {Boolean} [options.clusterLabels=true] Whether or not to cluster the labels of an entity.
* @param {Boolean} [options.clusterPoints=true] Whether or not to cluster the points of an entity.
*
* @alias EntityCluster
* @constructor
*
* @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Clustering.html|Cesium Sandcastle Clustering Demo}
*/
function EntityCluster(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
this._enabled = defaultValue(options.enabled, false);
this._pixelRange = defaultValue(options.pixelRange, 80);
this._minimumClusterSize = defaultValue(options.minimumClusterSize, 2);
this._clusterBillboards = defaultValue(options.clusterBillboards, true);
this._clusterLabels = defaultValue(options.clusterLabels, true);
this._clusterPoints = defaultValue(options.clusterPoints, true);
this._labelCollection = undefined;
this._billboardCollection = undefined;
this._pointCollection = undefined;
this._clusterBillboardCollection = undefined;
this._clusterLabelCollection = undefined;
this._clusterPointCollection = undefined;
this._collectionIndicesByEntity = {};
this._unusedLabelIndices = [];
this._unusedBillboardIndices = [];
this._unusedPointIndices = [];
this._previousClusters = [];
this._previousHeight = undefined;
this._enabledDirty = false;
this._clusterDirty = false;
this._cluster = undefined;
this._removeEventListener = undefined;
this._clusterEvent = new Event();
}
function getX(point) {
return point.coord.x;
}
function getY(point) {
return point.coord.y;
}
function expandBoundingBox(bbox, pixelRange) {
bbox.x -= pixelRange;
bbox.y -= pixelRange;
bbox.width += pixelRange * 2.0;
bbox.height += pixelRange * 2.0;
}
var labelBoundingBoxScratch = new BoundingRectangle();
function getBoundingBox(item, coord, pixelRange, entityCluster, result) {
if (defined(item._labelCollection) && entityCluster._clusterLabels) {
result = Label.getScreenSpaceBoundingBox(item, coord, result);
} else if (defined(item._billboardCollection) && entityCluster._clusterBillboards) {
result = Billboard.getScreenSpaceBoundingBox(item, coord, result);
} else if (defined(item._pointPrimitiveCollection) && entityCluster._clusterPoints) {
result = PointPrimitive.getScreenSpaceBoundingBox(item, coord, result);
}
expandBoundingBox(result, pixelRange);
if (entityCluster._clusterLabels && !defined(item._labelCollection) && defined(item.id) && hasLabelIndex(entityCluster, item.id) && defined(item.id._label)) {
var labelIndex = entityCluster._collectionIndicesByEntity[item.id];
var label = entityCluster._labelCollection.get(labelIndex);
var labelBBox = Label.getScreenSpaceBoundingBox(label, coord, labelBoundingBoxScratch);
expandBoundingBox(labelBBox, pixelRange);
result = BoundingRectangle.union(result, labelBBox, result);
}
return result;
}
function addNonClusteredItem(item, entityCluster) {
item.clusterShow = true;
if (!defined(item._labelCollection) && defined(item.id) && hasLabelIndex(entityCluster, item.id) && defined(item.id._label)) {
var labelIndex = entityCluster._collectionIndicesByEntity[item.id];
var label = entityCluster._labelCollection.get(labelIndex);
label.clusterShow = true;
}
}
function addCluster(position, numPoints, ids, entityCluster) {
var cluster = {
billboard : entityCluster._clusterBillboardCollection.add(),
label : entityCluster._clusterLabelCollection.add(),
point : entityCluster._clusterPointCollection.add()
};
cluster.billboard.show = false;
cluster.point.show = false;
cluster.label.show = true;
cluster.label.text = numPoints.toLocaleString();
cluster.billboard.position = cluster.label.position = cluster.point.position = position;
entityCluster._clusterEvent.raiseEvent(ids, cluster);
}
function hasLabelIndex(entityCluster, entityId) {
return defined(entityCluster) && defined(entityCluster._collectionIndicesByEntity[entityId]) && defined(entityCluster._collectionIndicesByEntity[entityId].labelIndex);
}
function getScreenSpacePositions(collection, points, scene, occluder, entityCluster) {
if (!defined(collection)) {
return;
}
var length = collection.length;
for (var i = 0; i < length; ++i) {
var item = collection.get(i);
item.clusterShow = false;
if (!item.show || !occluder.isPointVisible(item.position)) {
continue;
}
var canClusterLabels = entityCluster._clusterLabels && defined(item._labelCollection);
var canClusterBillboards = entityCluster._clusterBillboards && defined(item.id._billboard);
var canClusterPoints = entityCluster._clusterPoints && defined(item.id._point);
if (canClusterLabels && (canClusterPoints || canClusterBillboards)) {
continue;
}
var coord = item.computeScreenSpacePosition(scene);
if (!defined(coord)) {
continue;
}
points.push({
index : i,
collection : collection,
clustered : false,
coord : coord
});
}
}
var pointBoundinRectangleScratch = new BoundingRectangle();
var totalBoundingRectangleScratch = new BoundingRectangle();
var neighborBoundingRectangleScratch = new BoundingRectangle();
function createDeclutterCallback(entityCluster) {
return function(amount) {
if ((defined(amount) && amount < 0.05) || !entityCluster.enabled) {
return;
}
var scene = entityCluster._scene;
var labelCollection = entityCluster._labelCollection;
var billboardCollection = entityCluster._billboardCollection;
var pointCollection = entityCluster._pointCollection;
if ((!defined(labelCollection) && !defined(billboardCollection) && !defined(pointCollection)) ||
(!entityCluster._clusterBillboards && !entityCluster._clusterLabels && !entityCluster._clusterPoints)) {
return;
}
var clusteredLabelCollection = entityCluster._clusterLabelCollection;
var clusteredBillboardCollection = entityCluster._clusterBillboardCollection;
var clusteredPointCollection = entityCluster._clusterPointCollection;
if (defined(clusteredLabelCollection)) {
clusteredLabelCollection.removeAll();
} else {
clusteredLabelCollection = entityCluster._clusterLabelCollection = new LabelCollection({
scene : scene
});
}
if (defined(clusteredBillboardCollection)) {
clusteredBillboardCollection.removeAll();
} else {
clusteredBillboardCollection = entityCluster._clusterBillboardCollection = new BillboardCollection({
scene : scene
});
}
if (defined(clusteredPointCollection)) {
clusteredPointCollection.removeAll();
} else {
clusteredPointCollection = entityCluster._clusterPointCollection = new PointPrimitiveCollection();
}
var pixelRange = entityCluster._pixelRange;
var minimumClusterSize = entityCluster._minimumClusterSize;
var clusters = entityCluster._previousClusters;
var newClusters = [];
var previousHeight = entityCluster._previousHeight;
var currentHeight = scene.camera.positionCartographic.height;
var ellipsoid = scene.mapProjection.ellipsoid;
var cameraPosition = scene.camera.positionWC;
var occluder = new EllipsoidalOccluder(ellipsoid, cameraPosition);
var points = [];
if (entityCluster._clusterLabels) {
getScreenSpacePositions(labelCollection, points, scene, occluder, entityCluster);
}
if (entityCluster._clusterBillboards) {
getScreenSpacePositions(billboardCollection, points, scene, occluder, entityCluster);
}
if (entityCluster._clusterPoints) {
getScreenSpacePositions(pointCollection, points, scene, occluder, entityCluster);
}
var i;
var j;
var length;
var bbox;
var neighbors;
var neighborLength;
var neighborIndex;
var neighborPoint;
var ids;
var numPoints;
var collection;
var collectionIndex;
var index = kdbush(points, getX, getY, 64, Int32Array);
if (currentHeight < previousHeight) {
length = clusters.length;
for (i = 0; i < length; ++i) {
var cluster = clusters[i];
if (!occluder.isPointVisible(cluster.position)) {
continue;
}
var coord = Billboard._computeScreenSpacePosition(Matrix4.IDENTITY, cluster.position, Cartesian3.ZERO, Cartesian2.ZERO, scene);
if (!defined(coord)) {
continue;
}
var factor = 1.0 - currentHeight / previousHeight;
var width = cluster.width = cluster.width * factor;
var height = cluster.height = cluster.height * factor;
width = Math.max(width, cluster.minimumWidth);
height = Math.max(height, cluster.minimumHeight);
var minX = coord.x - width * 0.5;
var minY = coord.y - height * 0.5;
var maxX = coord.x + width;
var maxY = coord.y + height;
neighbors = index.range(minX, minY, maxX, maxY);
neighborLength = neighbors.length;
numPoints = 0;
ids = [];
for (j = 0; j < neighborLength; ++j) {
neighborIndex = neighbors[j];
neighborPoint = points[neighborIndex];
if (!neighborPoint.clustered) {
++numPoints;
collection = neighborPoint.collection;
collectionIndex = neighborPoint.index;
ids.push(collection.get(collectionIndex).id);
}
}
if (numPoints >= minimumClusterSize) {
addCluster(cluster.position, numPoints, ids, entityCluster);
newClusters.push(cluster);
for (j = 0; j < neighborLength; ++j) {
points[neighbors[j]].clustered = true;
}
}
}
}
length = points.length;
for (i = 0; i < length; ++i) {
var point = points[i];
if (point.clustered) {
continue;
}
point.clustered = true;
collection = point.collection;
collectionIndex = point.index;
var item = collection.get(collectionIndex);
bbox = getBoundingBox(item, point.coord, pixelRange, entityCluster, pointBoundinRectangleScratch);
var totalBBox = BoundingRectangle.clone(bbox, totalBoundingRectangleScratch);
neighbors = index.range(bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height);
neighborLength = neighbors.length;
var clusterPosition = Cartesian3.clone(item.position);
numPoints = 1;
ids = [item.id];
for (j = 0; j < neighborLength; ++j) {
neighborIndex = neighbors[j];
neighborPoint = points[neighborIndex];
if (!neighborPoint.clustered) {
var neighborItem = neighborPoint.collection.get(neighborPoint.index);
var neighborBBox = getBoundingBox(neighborItem, neighborPoint.coord, pixelRange, entityCluster, neighborBoundingRectangleScratch);
Cartesian3.add(neighborItem.position, clusterPosition, clusterPosition);
BoundingRectangle.union(totalBBox, neighborBBox, totalBBox);
++numPoints;
ids.push(neighborItem.id);
}
}
if (numPoints >= minimumClusterSize) {
var position = Cartesian3.multiplyByScalar(clusterPosition, 1.0 / numPoints, clusterPosition);
addCluster(position, numPoints, ids, entityCluster);
newClusters.push({
position : position,
width : totalBBox.width,
height : totalBBox.height,
minimumWidth : bbox.width,
minimumHeight : bbox.height
});
for (j = 0; j < neighborLength; ++j) {
points[neighbors[j]].clustered = true;
}
} else {
addNonClusteredItem(item, entityCluster);
}
}
if (clusteredLabelCollection.length === 0) {
clusteredLabelCollection.destroy();
entityCluster._clusterLabelCollection = undefined;
}
if (clusteredBillboardCollection.length === 0) {
clusteredBillboardCollection.destroy();
entityCluster._clusterBillboardCollection = undefined;
}
if (clusteredPointCollection.length === 0) {
clusteredPointCollection.destroy();
entityCluster._clusterPointCollection = undefined;
}
entityCluster._previousClusters = newClusters;
entityCluster._previousHeight = currentHeight;
};
}
EntityCluster.prototype._initialize = function(scene) {
this._scene = scene;
var cluster = createDeclutterCallback(this);
this._cluster = cluster;
this._removeEventListener = scene.camera.changed.addEventListener(cluster);
};
defineProperties(EntityCluster.prototype, {
/**
* Gets or sets whether clustering is enabled.
* @memberof EntityCluster.prototype
* @type {Boolean}
*/
enabled : {
get : function() {
return this._enabled;
},
set : function(value) {
this._enabledDirty = value !== this._enabled;
this._enabled = value;
}
},
/**
* Gets or sets the pixel range to extend the screen space bounding box.
* @memberof EntityCluster.prototype
* @type {Number}
*/
pixelRange : {
get : function() {
return this._pixelRange;
},
set : function(value) {
this._clusterDirty = this._clusterDirty || value !== this._pixelRange;
this._pixelRange = value;
}
},
/**
* Gets or sets the minimum number of screen space objects that can be clustered.
* @memberof EntityCluster.prototype
* @type {Number}
*/
minimumClusterSize : {
get : function() {
return this._minimumClusterSize;
},
set : function(value) {
this._clusterDirty = this._clusterDirty || value !== this._minimumClusterSize;
this._minimumClusterSize = value;
}
},
/**
* Gets the event that will be raised when a new cluster will be displayed. The signature of the event listener is {@link EntityCluster~newClusterCallback}.
* @memberof EntityCluster.prototype
* @type {Event}
*/
clusterEvent : {
get : function() {
return this._clusterEvent;
}
},
/**
* Gets or sets whether clustering billboard entities is enabled.
* @memberof EntityCluster.prototype
* @type {Boolean}
*/
clusterBillboards : {
get : function() {
return this._clusterBillboards;
},
set : function(value) {
this._clusterDirty = this._clusterDirty || value !== this._clusterBillboards;
this._clusterBillboards = value;
}
},
/**
* Gets or sets whether clustering labels entities is enabled.
* @memberof EntityCluster.prototype
* @type {Boolean}
*/
clusterLabels : {
get : function() {
return this._clusterLabels;
},
set : function(value) {
this._clusterDirty = this._clusterDirty || value !== this._clusterLabels;
this._clusterLabels = value;
}
},
/**
* Gets or sets whether clustering point entities is enabled.
* @memberof EntityCluster.prototype
* @type {Boolean}
*/
clusterPoints : {
get : function() {
return this._clusterPoints;
},
set : function(value) {
this._clusterDirty = this._clusterDirty || value !== this._clusterPoints;
this._clusterPoints = value;
}
}
});
function createGetEntity(collectionProperty, CollectionConstructor, unusedIndicesProperty, entityIndexProperty) {
return function(entity) {
var collection = this[collectionProperty];
if (!defined(this._collectionIndicesByEntity)) {
this._collectionIndicesByEntity = {};
}
var entityIndices = this._collectionIndicesByEntity[entity.id];
if (!defined(entityIndices)) {
entityIndices = this._collectionIndicesByEntity[entity.id] = {
billboardIndex: undefined,
labelIndex: undefined,
pointIndex: undefined
};
}
if (defined(collection) && defined(entityIndices[entityIndexProperty])) {
return collection.get(entityIndices[entityIndexProperty]);
}
if (!defined(collection)) {
collection = this[collectionProperty] = new CollectionConstructor({
scene : this._scene
});
}
var index;
var entityItem;
var unusedIndices = this[unusedIndicesProperty];
if (unusedIndices.length > 0) {
index = unusedIndices.pop();
entityItem = collection.get(index);
} else {
entityItem = collection.add();
index = collection.length - 1;
}
entityIndices[entityIndexProperty] = index;
this._clusterDirty = true;
return entityItem;
};
}
function removeEntityIndicesIfUnused(entityCluster, entityId) {
var indices = entityCluster._collectionIndicesByEntity[entityId];
if (!defined(indices.billboardIndex) && !defined(indices.labelIndex) && !defined(indices.pointIndex)) {
delete entityCluster._collectionIndicesByEntity[entityId];
}
}
/**
* Returns a new {@link Label}.
* @param {Entity} entity The entity that will use the returned {@link Label} for visualization.
* @returns {Label} The label that will be used to visualize an entity.
*
* @private
*/
EntityCluster.prototype.getLabel = createGetEntity('_labelCollection', LabelCollection, '_unusedLabelIndices', 'labelIndex');
/**
* Removes the {@link Label} associated with an entity so it can be reused by another entity.
* @param {Entity} entity The entity that will uses the returned {@link Label} for visualization.
*
* @private
*/
EntityCluster.prototype.removeLabel = function(entity) {
var entityIndices = this._collectionIndicesByEntity && this._collectionIndicesByEntity[entity.id];
if (!defined(this._labelCollection) || !defined(entityIndices) || !defined(entityIndices.labelIndex)) {
return;
}
var index = entityIndices.labelIndex;
entityIndices.labelIndex = undefined;
removeEntityIndicesIfUnused(this, entity.id);
var label = this._labelCollection.get(index);
label.show = false;
label.text = '';
label.id = undefined;
this._unusedLabelIndices.push(index);
this._clusterDirty = true;
};
/**
* Returns a new {@link Billboard}.
* @param {Entity} entity The entity that will use the returned {@link Billboard} for visualization.
* @returns {Billboard} The label that will be used to visualize an entity.
*
* @private
*/
EntityCluster.prototype.getBillboard = createGetEntity('_billboardCollection', BillboardCollection, '_unusedBillboardIndices', 'billboardIndex');
/**
* Removes the {@link Billboard} associated with an entity so it can be reused by another entity.
* @param {Entity} entity The entity that will uses the returned {@link Billboard} for visualization.
*
* @private
*/
EntityCluster.prototype.removeBillboard = function(entity) {
var entityIndices = this._collectionIndicesByEntity && this._collectionIndicesByEntity[entity.id];
if (!defined(this._billboardCollection) || !defined(entityIndices) || !defined(entityIndices.billboardIndex)) {
return;
}
var index = entityIndices.billboardIndex;
entityIndices.billboardIndex = undefined;
removeEntityIndicesIfUnused(this, entity.id);
var billboard = this._billboardCollection.get(index);
billboard.id = undefined;
billboard.show = false;
billboard.image = undefined;
this._unusedBillboardIndices.push(index);
this._clusterDirty = true;
};
/**
* Returns a new {@link Point}.
* @param {Entity} entity The entity that will use the returned {@link Point} for visualization.
* @returns {Point} The label that will be used to visualize an entity.
*
* @private
*/
EntityCluster.prototype.getPoint = createGetEntity('_pointCollection', PointPrimitiveCollection, '_unusedPointIndices', 'pointIndex');
/**
* Removes the {@link Point} associated with an entity so it can be reused by another entity.
* @param {Entity} entity The entity that will uses the returned {@link Point} for visualization.
*
* @private
*/
EntityCluster.prototype.removePoint = function(entity) {
var entityIndices = this._collectionIndicesByEntity && this._collectionIndicesByEntity[entity.id];
if (!defined(this._pointCollection) || !defined(entityIndices) || !defined(entityIndices.pointIndex)) {
return;
}
var index = entityIndices.pointIndex;
entityIndices.pointIndex = undefined;
removeEntityIndicesIfUnused(this, entity.id);
var point = this._pointCollection.get(index);
point.show = false;
point.id = undefined;
this._unusedPointIndices.push(index);
this._clusterDirty = true;
};
function disableCollectionClustering(collection) {
if (!defined(collection)) {
return;
}
var length = collection.length;
for (var i = 0; i < length; ++i) {
collection.get(i).clusterShow = true;
}
}
function updateEnable(entityCluster) {
if (entityCluster.enabled) {
return;
}
if (defined(entityCluster._clusterLabelCollection)) {
entityCluster._clusterLabelCollection.destroy();
}
if (defined(entityCluster._clusterBillboardCollection)) {
entityCluster._clusterBillboardCollection.destroy();
}
if (defined(entityCluster._clusterPointCollection)) {
entityCluster._clusterPointCollection.destroy();
}
entityCluster._clusterLabelCollection = undefined;
entityCluster._clusterBillboardCollection = undefined;
entityCluster._clusterPointCollection = undefined;
disableCollectionClustering(entityCluster._labelCollection);
disableCollectionClustering(entityCluster._billboardCollection);
disableCollectionClustering(entityCluster._pointCollection);
}
/**
* Gets the draw commands for the clustered billboards/points/labels if enabled, otherwise,
* queues the draw commands for billboards/points/labels created for entities.
* @private
*/
EntityCluster.prototype.update = function(frameState) {
// If clustering is enabled before the label collection is updated,
// the glyphs haven't been created so the screen space bounding boxes
// are incorrect.
if (defined(this._labelCollection) && this._labelCollection.length > 0 && this._labelCollection.get(0)._glyphs.length === 0) {
var commandList = frameState.commandList;
frameState.commandList = [];
this._labelCollection.update(frameState);
frameState.commandList = commandList;
}
if (this._enabledDirty) {
this._enabledDirty = false;
updateEnable(this);
this._clusterDirty = true;
}
if (this._clusterDirty) {
this._clusterDirty = false;
this._cluster();
}
if (defined(this._clusterLabelCollection)) {
this._clusterLabelCollection.update(frameState);
}
if (defined(this._clusterBillboardCollection)) {
this._clusterBillboardCollection.update(frameState);
}
if (defined(this._clusterPointCollection)) {
this._clusterPointCollection.update(frameState);
}
if (defined(this._labelCollection)) {
this._labelCollection.update(frameState);
}
if (defined(this._billboardCollection)) {
this._billboardCollection.update(frameState);
}
if (defined(this._pointCollection)) {
this._pointCollection.update(frameState);
}
};
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* <p>
* Unlike other objects that use WebGL resources, this object can be reused. For example, if a data source is removed
* from a data source collection and added to another.
* </p>
*
* @returns {undefined}
*/
EntityCluster.prototype.destroy = function() {
this._labelCollection = this._labelCollection && this._labelCollection.destroy();
this._billboardCollection = this._billboardCollection && this._billboardCollection.destroy();
this._pointCollection = this._pointCollection && this._pointCollection.destroy();
this._clusterLabelCollection = this._clusterLabelCollection && this._clusterLabelCollection.destroy();
this._clusterBillboardCollection = this._clusterBillboardCollection && this._clusterBillboardCollection.destroy();
this._clusterPointCollection = this._clusterPointCollection && this._clusterPointCollection.destroy();
if (defined(this._removeEventListener)) {
this._removeEventListener();
this._removeEventListener = undefined;
}
this._labelCollection = undefined;
this._billboardCollection = undefined;
this._pointCollection = undefined;
this._clusterBillboardCollection = undefined;
this._clusterLabelCollection = undefined;
this._clusterPointCollection = undefined;
this._collectionIndicesByEntity = undefined;
this._unusedLabelIndices = [];
this._unusedBillboardIndices = [];
this._unusedPointIndices = [];
this._previousClusters = [];
this._previousHeight = undefined;
this._enabledDirty = false;
this._pixelRangeDirty = false;
this._minimumClusterSizeDirty = false;
return undefined;
};
/**
* A event listener function used to style clusters.
* @callback EntityCluster~newClusterCallback
*
* @param {Entity[]} clusteredEntities An array of the entities contained in the cluster.
* @param {Object} cluster An object containing billboard, label, and point properties. The values are the same as
* billboard, label and point entities, but must be the values of the ConstantProperty.
*
* @example
* // The default cluster values.
* dataSource.clustering.clusterEvent.addEventListener(function(entities, cluster) {
* cluster.label.show = true;
* cluster.label.text = entities.length.toLocaleString();
* });
*/
return EntityCluster;
});