Skip to content
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

- Adds Google2DImageryProvider to load imagery from [Google Maps](https://developers.google.com/maps/documentation/tile/2d-tiles-overview) [#12913](https://github.com/CesiumGS/cesium/pull/12913)
- Adds an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566)
- Adds new declusteredEvent: Fires with complete clustering information including both clustered and declustered entities [#5760](https://github.com/CesiumGS/cesium/issues/5760)

## 1.133.1 - 2025-09-08

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,3 +432,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
- [Pamela Augustine](https://github.com/pamelaAugustine)
- [宋时旺](https://github.com/BlockCnFuture)
- [Marco Zhan](https://github.com/marcoYxz)
- [Alexander Remer](https://github.com/Oko-Tester)
161 changes: 159 additions & 2 deletions packages/engine/Source/DataSources/EntityCluster.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ function EntityCluster(options) {

this._clusterEvent = new Event();

this._declusteredEvent = new Event();
this._allProcessedEntities = [];
this._lastClusteredEntities = [];
this._lastDeclusteredEntities = [];

/**
* Determines if entities in this collection will be shown.
*
Expand Down Expand Up @@ -127,6 +132,10 @@ function getBoundingBox(item, coord, pixelRange, entityCluster, result) {
function addNonClusteredItem(item, entityCluster) {
item.clusterShow = true;

if (defined(item.id)) {
entityCluster._lastDeclusteredEntities.push(item.id);
}

if (
!defined(item._labelCollection) &&
defined(item.id) &&
Expand Down Expand Up @@ -157,7 +166,16 @@ function addCluster(position, numPoints, ids, entityCluster) {
cluster.point.position =
position;

entityCluster._lastClusteredEntities =
entityCluster._lastClusteredEntities.concat(ids);

entityCluster._clusterEvent.raiseEvent(ids, cluster);

entityCluster._declusteredEvent.raiseEvent({
clustered: ids,
declustered: entityCluster._lastDeclusteredEntities.slice(),
cluster: cluster,
});
}

function hasLabelIndex(entityCluster, entityId) {
Expand Down Expand Up @@ -207,6 +225,10 @@ function getScreenSpacePositions(
continue;
}

if (defined(item.id)) {
entityCluster._allProcessedEntities.push(item.id);
}

points.push({
index: i,
collection: collection,
Expand All @@ -216,7 +238,7 @@ function getScreenSpacePositions(
}
}

const pointBoundinRectangleScratch = new BoundingRectangle();
const pointBoundingRectangleScratch = new BoundingRectangle();
const totalBoundingRectangleScratch = new BoundingRectangle();
const neighborBoundingRectangleScratch = new BoundingRectangle();

Expand All @@ -226,6 +248,10 @@ function createDeclutterCallback(entityCluster) {
return;
}

entityCluster._allProcessedEntities = [];
entityCluster._lastClusteredEntities = [];
entityCluster._lastDeclusteredEntities = [];

const scene = entityCluster._scene;

const labelCollection = entityCluster._labelCollection;
Expand All @@ -240,6 +266,11 @@ function createDeclutterCallback(entityCluster) {
!entityCluster._clusterLabels &&
!entityCluster._clusterPoints)
) {
entityCluster._declusteredEvent.raiseEvent({
clustered: [],
declustered: [],
cluster: null,
});
return;
}

Expand Down Expand Up @@ -414,7 +445,7 @@ function createDeclutterCallback(entityCluster) {
point.coord,
pixelRange,
entityCluster,
pointBoundinRectangleScratch,
pointBoundingRectangleScratch,
);
const totalBBox = BoundingRectangle.clone(
bbox,
Expand Down Expand Up @@ -485,6 +516,18 @@ function createDeclutterCallback(entityCluster) {
}
}

if (
entityCluster._lastClusteredEntities.length > 0 ||
entityCluster._lastDeclusteredEntities.length > 0
) {
entityCluster._declusteredEvent.raiseEvent({
clustered: entityCluster._lastClusteredEntities.slice(),
declustered: entityCluster._lastDeclusteredEntities.slice(),
cluster: null,
allProcessed: entityCluster._allProcessedEntities.slice(),
});
}

if (clusteredLabelCollection.length === 0) {
clusteredLabelCollection.destroy();
entityCluster._clusterLabelCollection = undefined;
Expand Down Expand Up @@ -567,6 +610,16 @@ Object.defineProperties(EntityCluster.prototype, {
return this._clusterEvent;
},
},
/**
* Gets the event that will be raised when clustering is processed, including both clustered and declustered entities.
* @memberof EntityCluster.prototype
* @type {Event}
*/
declusteredEvent: {
get: function () {
return this._declusteredEvent;
},
},
/**
* Gets or sets whether clustering billboard entities is enabled.
* @memberof EntityCluster.prototype
Expand Down Expand Up @@ -847,6 +900,58 @@ function disableCollectionClustering(collection) {
}
}

function getVisibleEntitiesFromCollection(collection) {
if (!defined(collection)) {
return [];
}

const visibleEntities = [];
for (let i = 0; i < collection.length; i++) {
const item = collection.get(i);
if (defined(item.id) && item.show) {
visibleEntities.push(item.id);
}
}
return visibleEntities;
}

function handleDeclusteredEvent(entityCluster) {
if (entityCluster._declusteredEvent.numberOfListeners === 0) {
return;
}
const allVisibleEntities = [
...getVisibleEntitiesFromCollection(entityCluster._labelCollection),
...getVisibleEntitiesFromCollection(entityCluster._billboardCollection),
...getVisibleEntitiesFromCollection(entityCluster._pointCollection),
];

if (allVisibleEntities.length > 0) {
const uniqueEntities = Array.from(new Set(allVisibleEntities));

entityCluster._declusteredEvent.raiseEvent({
clustered: [],
declustered: uniqueEntities,
cluster: null,
allProcessed: uniqueEntities,
});

entityCluster._lastClusteredEntities = [];
entityCluster._lastDeclusteredEntities = uniqueEntities.slice();
entityCluster._allProcessedEntities = uniqueEntities.slice();
} else {
entityCluster._declusteredEvent.raiseEvent({
clustered: [],
declustered: [],
cluster: null,
allProcessed: [],
});

entityCluster._lastClusteredEntities = [];
entityCluster._lastDeclusteredEntities = [];
entityCluster._allProcessedEntities = [];
}
}

function updateEnable(entityCluster) {
if (entityCluster.enabled) {
return;
Expand All @@ -869,6 +974,8 @@ function updateEnable(entityCluster) {
disableCollectionClustering(entityCluster._labelCollection);
disableCollectionClustering(entityCluster._billboardCollection);
disableCollectionClustering(entityCluster._pointCollection);

handleDeclusteredEvent(entityCluster);
}

/**
Expand Down Expand Up @@ -998,9 +1105,37 @@ EntityCluster.prototype.destroy = function () {
this._pixelRangeDirty = false;
this._minimumClusterSizeDirty = false;

this._allProcessedEntities = [];
this._lastClusteredEntities = [];
this._lastDeclusteredEntities = [];

return undefined;
};

/**
* Returns the last set of clustered entities from the most recent clustering operation.
* @returns {Entity[]} Array of entities that were clustered
*/
EntityCluster.prototype.getLastClusteredEntities = function () {
return this._lastClusteredEntities.slice();
};

/**
* Returns the last set of declustered entities from the most recent clustering operation.
* @returns {Entity[]} Array of entities that were not clustered
*/
EntityCluster.prototype.getLastDeclusteredEntities = function () {
return this._lastDeclusteredEntities.slice();
};

/**
* Returns all entities that were processed in the most recent clustering operation.
* @returns {Entity[]} Array of all processed entities
*/
EntityCluster.prototype.getAllProcessedEntities = function () {
return this._allProcessedEntities.slice();
};

/**
* A event listener function used to style clusters.
* @callback EntityCluster.newClusterCallback
Expand All @@ -1019,4 +1154,26 @@ EntityCluster.prototype.destroy = function () {
* cluster.label.text = entities.length.toLocaleString();
* });
*/

/**
* A event listener function for enhanced clustering information.
* @callback EntityCluster.declusteredCallback
*
* @param {object} clusteringData An object containing clustering information.
* @param {Entity[]} clusteringData.clustered An array of entities that were clustered.
* @param {Entity[]} clusteringData.declustered An array of entities that were not clustered.
* @param {object|null} clusteringData.cluster The cluster object (if this event is for a specific cluster) or null for summary events.
* @param {Entity[]} [clusteringData.allProcessed] An array of all entities processed (only in summary events).
*
* @example
* // Using the enhanced declusteredEvent to access both clustered and declustered entities
* dataSource.clustering.declusteredEvent.addEventListener(function(data) {
* console.log('Clustered entities:', data.clustered.length);
* console.log('Declustered entities:', data.declustered.length);
* if (data.allProcessed) {
* console.log('Total processed entities:', data.allProcessed.length);
* }
* });
*/

export default EntityCluster;
Loading