Class MarkerClusterer
public sealed class MarkerClusterer
- Inheritance
-
objectMarkerClusterer
Properties
LastBuildMembers
The slot ids of every candidate this instance's most recent Build
call considered, grouped contiguously by cluster: a given
cluster's members are LastBuildMembers[c.MemberStart ..
c.MemberStart + c.MemberCount). Valid until the next Build
call on this same instance.
public IReadOnlyList<int> LastBuildMembers { get; }
Property Value
Methods
Build(MarkerStore, double, int, double, List<MarkerCluster>)
public void Build(MarkerStore store, double zoom, int tileSizePx, double clusterRadiusPx, List<MarkerCluster> results)
Parameters
storeMarkerStorezoomdoubletileSizePxintclusterRadiusPxdoubleresultsList<MarkerCluster>
Build(MarkerStore, double, int, double, List<MarkerCluster>, in MercatorBounds)
Viewport-bounded variant: only markers whose padded position can
overlap bounds enter the candidate pool.
public void Build(MarkerStore store, double zoom, int tileSizePx, double clusterRadiusPx, List<MarkerCluster> results, in MercatorBounds bounds)
Parameters
storeMarkerStorezoomdoubletileSizePxintclusterRadiusPxdoubleresultsList<MarkerCluster>boundsMercatorBounds
Remarks
Unlike the fixed-grid algorithm this replaced, agreement with the
unbounded overload is no longer exact for every candidate inside
bounds. The old grid decided a marker's
cluster purely from that marker's own coordinates, so one radius
of padding was provably enough: a cell touching the bounds always
kept every one of its members. Greedy distance clustering
decides a candidate's cluster from the current mean of whichever
cluster is nearest when that candidate is visited, and a
cluster's mean can have drifted through a chain of earlier
merges - in principle arbitrarily far from any single member,
including ones outside the padded region this overload never
sees. In practice, real marker distributions do not chain that
far relative to one radius of padding, and
MarkerClusterController.RebuildClusters pads by a further 3x3
viewports on top of this overload's own one-radius pad before
ever calling it, so the gap has not been observed to reach a
screen in practice - but it is not proven closed the way the old
grid's was, and a test asserting byte-identical clusters at the
padding boundary is asserting a property this algorithm does not
have. [tried, not proven: MarkerClustererSpatialIndexTests
exercises this overload against the index-backed one under the
same bounds and stays exact, because both sides see an identical
candidate set - the gap above is specifically between this
overload and the UNBOUNDED one, which sees strictly more.]
Build(MarkerStore, double, int, double, List<MarkerCluster>, in MercatorBounds, MarkerSpatialIndex, List<int>)
Same contract as the bounds-only overload, but candidate markers
come from index instead of a linear store scan
- only cells the padded bounds touch are visited.
indexmust already hold every live marker instore; this method only reads it (see MarkerPicker's matching overload for the same caller-keeps-it-synced contract).candidateScratchis caller-owned and reused (Query clears it), so this overload allocates nothing.indexnull falls back to the bounds-only overload above. Collects the identical candidate set the bounds-only overload would for the same bounds, so - unlike the bounds-only-vs-unbounded gap documented there - this overload agrees with it exactly.
public void Build(MarkerStore store, double zoom, int tileSizePx, double clusterRadiusPx, List<MarkerCluster> results, in MercatorBounds bounds, MarkerSpatialIndex index, List<int> candidateScratch)
Parameters
storeMarkerStorezoomdoubletileSizePxintclusterRadiusPxdoubleresultsList<MarkerCluster>boundsMercatorBoundsindexMarkerSpatialIndexcandidateScratchList<int>
Remarks
Unlike TryPick(MarkerStore, MapViewTransform, ScreenPoint, double, MarkerSpatialIndex, List<int>, out int, out double),
this method's brute-force bounds test applies to EVERY marker,
altitude included - there is no ground-plane exemption here, since
a cluster's screen position already comes from its stored ground
mean, not a projection. Query(in MercatorBounds, List<int>)
returns altitude markers unconditionally regardless of bounds, so
the altitude candidates it hands back still need the same
ContainsPoint filter the ground candidates get for free
from the query rect itself, or this overload would keep
out-of-view raised markers the brute-force path would have
dropped.
BuildFromSeeds(List<ClusterSeed>, double, int, double, List<MarkerCluster>)
Clusters an arbitrary list of seeds, letting a caller cluster something other than a plain marker store - most usefully, one clustering level's own output, so a whole zoom hierarchy can be folded level by level instead of re-reading every marker at each level.
public void BuildFromSeeds(List<ClusterSeed> seeds, double zoom, int tileSizePx, double clusterRadiusPx, List<MarkerCluster> results)
Parameters
seedsList<ClusterSeed>Inputs to cluster, visited in the order given. Caller-owned and only read. The order decides the result, so a caller that needs two builds of the same data to agree must supply the same order both times (see the remarks).
zoomdoubleMap zoom; quantized with QuantizeZoom(double).
tileSizePxintTile edge length in screen px.
clusterRadiusPxdoubleMerge radius in screen px at the quantized zoom.
resultsList<MarkerCluster>Caller-owned output list; cleared first.
Remarks
A seed whose Count is not at least 1 stands for no markers and is skipped: it joins no cluster, founds none, and contributes no membership entry. Skipping rather than admitting it keeps every cluster's marker total exact and its centroid finite, which admitting a zero count would not - a cluster whose seeds summed to zero markers has no mean position to report.
Each cluster's Count is the total
markers its seeds stand for, so counts are conserved through any
depth of folding, while its
MemberStart /
MemberCount range slices
LastBuildMembers by seed, holding one
SourceId per seed that joined. Those
two numbers are equal only when every seed stands for one marker,
which is what the Build overloads feed in.
Clustering is sequential: a seed joins the nearest cluster already placed within the merge radius, or founds a new one, so which cluster a seed lands in depends on what exists when it is visited. This method does not sort - it deliberately preserves the caller's order, since a folding caller's natural order (the previous level's cluster order) is already deterministic and re-sorting it would discard that. A caller building its own seed list from unordered data owes it a stable order, and must keep that order in an ordered list rather than recovering it from a hash container's iteration order.
QuantizeZoom(double)
public static double QuantizeZoom(double zoom)
Parameters
zoomdouble