-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Add declusteredEvent to EntityCluster #12920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add declusteredEvent to EntityCluster #12920
Conversation
Thank you for the pull request, @Oko-Tester! ✅ We can confirm we have a CLA on file for you. |
746701c
to
2d42fdc
Compare
this is ready |
Fixes CesiumGS#5760 by adding declusteredEvent that provides both clustered and declustered entities, plus new API methods for accessing clustering state. Maintains backward compatibility.
… skipping unused declustering computations
9bd82e9
to
587e235
Compare
@mzschwartz5 Is there any news regarding the approval? |
Hi @Oko-Tester, I haven't had a chance to review this yet. It might not happen until Thursday as I'm out tomorrow and Wednesday. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @Oko-Tester,
Thanks for the PR. I have a number of mostly minor comments about code style and performance, but those might be irrelevant because I'm a little unsure of the overall approach. It's a lot of extra state that we're tracking now, which means more memory consumed, more bookkeeping to sync arrays, more room for potential edge cases.
In the linked issue you said "clusterEvent does not fire when a cluster becomes empty, which makes it impossible to detect de-clustered entities." My intuition is that there's a better way to deal with this than to track more state and manually fire the decluster event accordingly. I haven't actually looked at the code closely to tell, but that's my instinct.
* Returns all entities that were processed in the most recent clustering operation. | ||
* @returns {Entity[]} Array of all processed entities | ||
*/ | ||
EntityCluster.prototype.getAllProcessedEntities = function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an opinion, but I feel like the name of this API could use some work shopping - before I even suggest an alternative, though, let me ask: does "all" entities mean all entities that are in clusters after the most recent operation, or also entities that are declustered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(And similarly rename the internal array)
* @returns {Entity[]} Array of entities that were clustered | ||
*/ | ||
EntityCluster.prototype.getLastClusteredEntities = function () { | ||
return this._lastClusteredEntities.slice(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason to return a shallow copy of the internal array (via slice()
)? Why not just make a property / standard getter for these arrays?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With zero context, I'll throw in the baseline comment: Minimize mutablilty and make ownership clear. It at least serves the purpose of avoiding the nearly-impossible-to-debug glitches that may result from a client doing this:
const entities = entityCluster.getLastClusteredEntities();
// (French accent:) A few moments later...
entities[42] = undefined;
// Even later
entityCluster.updateSomething(); // Crashes due to `undefined` in array...
It's a thin protection layer, but better than nothing. In other environments, this would/could/should return a read-only view on the internal state (Collections.unmodifiableList
et al).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I see. I would still rather we protect against undefined in updateSomething
.
In a typed language, you would just have a compile-time contract here, marking this function as returning something const
or read-only. But even there, a user can generally find a way to shoot themselves in the foot at run-time, if they really want to.
So, here too, a user could do something crazy with the array, but we can't guard against everything. (But we should still guard against the basic things)
return; | ||
} | ||
const allVisibleEntities = [ | ||
...getVisibleEntitiesFromCollection(entityCluster._labelCollection), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know there was previous discussion about the use of the spread (...
) operator on large collections. Were these instances overlooked or intentionally left as-is because we know they're not large?
]; | ||
|
||
if (allVisibleEntities.length > 0) { | ||
const uniqueEntities = Array.from(new Set(allVisibleEntities)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it possible for allVisibleEntities
to contain duplicates?
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 = []; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe this need any conditional logic. Just set
const uniqueEntities = Array.from(new Set(allVisibleEntities));
and the rest handles itself whether allVisibleEntities
is empty or not.
|
||
function getVisibleEntitiesFromCollection(collection) { | ||
if (!defined(collection)) { | ||
return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small style nit - define
const visibleEntities = [];
before this, and return it instead of a literal []
}, | ||
}, | ||
/** | ||
* Gets the event that will be raised when clustering is processed, including both clustered and declustered entities. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So does this event do everything the pre-existing clusteredEvent
did, and more? Because the comment says in includes clustered entities. So it's not just an event for declustering but also for clustering?
clustered: entityCluster._lastClusteredEntities.slice(), | ||
declustered: entityCluster._lastDeclusteredEntities.slice(), | ||
cluster: null, | ||
allProcessed: entityCluster._allProcessedEntities.slice(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to my other comment - do we want / need to be creating a shallow copy of these arrays here, via .slice()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not an expert on unit testing philosphy, so maybe this is a bad suggestion, but could we maybe have unit test(s) that check that clustering generally works for all three of: points, billboards, and labels (collections)?
I'm just a little wary given the discussion about using the item.show
property as if these classes all have a shared abstract interface (which they don't). I don't expect that to be an issue, but if someone were to ever change that property name on one of the classes, at least having a failing unit test here would prevent the issue.
Hey @mzschwartz5, As for your comment on unit tests, I am not an expert in this field either, but it seems sensible to me to use them to avoid potential future problems. |
Fixes #5760 by adding declusteredEvent that provides both clustered and declustered entities, plus new API methods for accessing clustering state. Maintains backward compatibility.
Description
This PR adds comprehensive clustering state information to EntityCluster by introducing a new
declusteredEvent
and supporting API methods. The originalclusterEvent
only provided information about entities that were successfully clustered, but developers also needed access to entities that were processed but not clustered (declustered entities).Key Changes
declusteredEvent
: Fires with complete clustering information including both clustered and declustered entitiesgetLastClusteredEntities()
- Returns currently clustered entitiesgetLastDeclusteredEntities()
- Returns currently declustered entitiesgetAllProcessedEntities()
- Returns all entities processed during clusteringclusterEvent
remains unchanged and functionalIssue number and link
Fixes #5760 - Include declustered entities in cluster callback
This addresses multiple user requests from the forum and GitHub comments spanning several years.
Testing plan
Manual Testing
declusteredEvent
fires with correctclustered
anddeclustered
arraysclusterEvent
still works as beforeAuthor checklist
CONTRIBUTORS.md
CHANGES.md
with a short summary of my change