Class MarkerSpatialIndex
Quickly finds all markers in a geographic region without checking every marker. Grid spatial index over marker positions in normalized mercator space, so a viewport-sized query costs the cells the rect touches rather than a scan of every stored marker.
public sealed class MarkerSpatialIndex
- Inheritance
-
objectMarkerSpatialIndex
Remarks
Ground markers (altitudeMercator == 0) are bucketed by mercator
cell and only ever visited when their cell overlaps a query rect.
Raised or lowered markers (any other altitude, including NaN) go into
a separate small list and are returned from every query unconditionally,
the same exemption MarkerPicker's own bounds pre-reject already
applies: a ground-plane rect cannot safely reject a marker whose
projected screen position altitude has shifted away from the ground
point, so this index does not attempt to bucket them spatially at all
rather than invent an unverified 3D scheme.
Add/Remove/UpdatePosition are O(1) amortized: each marker's cell (or altitude-list) slot is tracked so removal is a swap with the bucket's last entry, never a scan. Query allocates nothing; results are written into a caller-owned, caller-reused list, and the cell buckets a removal empties are pooled for the next cell that needs one instead of being left for the collector.
Not thread safe. Cell size is fixed at construction: it is a world-mercator distance, independent of zoom or screen, because markers are stored across the whole view range, not just what is currently visible.
Constructors
MarkerSpatialIndex(double, int)
public MarkerSpatialIndex(double cellSize, int initialCapacity = 256)
Parameters
cellSizedoubleGrid cell width and height in normalized mercator units (the world is the unit square, see MercatorPoint). Must be positive and finite. Smaller cells narrow each query's candidate set at the cost of more buckets for a spread-out store; there is no single right answer, so this is a constructor parameter rather than a fitted constant.
initialCapacityintMarker count to preallocate the internal dictionaries for. Purely a sizing hint; the index still grows correctly from a smaller one.
Fields
DefaultCapacity
Default marker count to preallocate when constructing a new index.
public const int DefaultCapacity = 256
Field Value
Properties
AltitudeMarkerCount
Markers currently in the altitude fallback list.
public int AltitudeMarkerCount { get; }
Property Value
CellSize
The grid cell size passed to the constructor.
public double CellSize { get; }
Property Value
Count
Markers currently tracked, ground plus altitude.
public int Count { get; }
Property Value
LastQueryCellsVisited
Cells the most recent Query(in MercatorBounds, List<int>) tried, occupied or not. Exists so a caller (or a test) can verify the index is actually bounding its work to the rect rather than walking the whole grid.
public int LastQueryCellsVisited { get; }
Property Value
OccupiedCellCount
Non-empty ground-cell buckets right now. Zero once every ground marker has been removed, even if altitude markers remain.
public int OccupiedCellCount { get; }
Property Value
Methods
Add(int, MercatorPoint, double)
Starts tracking a marker.
public void Add(int markerId, MercatorPoint position, double altitudeMercator)
Parameters
markerIdintCaller-chosen identity, not reinterpreted here - the bulk-tier convention elsewhere in Core (MarkerPicker, MarkerClusterer, MarkerViewportTracker) passes the store slot index, and this type does the same without depending on MarkerStore.
positionMercatorPointGround mercator position.
altitudeMercatordoubleHeight above the ground plane in normalized mercator units, the same quantity
MarkerInstanceData.AltitudeMercatorcarries. Exactly zero means ground-plane and enters the grid; any other value, including NaN, is treated as raised and always returned by Query(in MercatorBounds, List<int>) - NaN routes here rather than to the grid because a bucket keyed on a NaN-derived cell could never be found again, and always-included is the safe direction for a query a caller trusts not to drop markers.
Exceptions
- InvalidOperationException
markerIdis already tracked. Call Remove(int) or UpdatePosition(int, MercatorPoint, double) instead.
Query(in MercatorBounds, List<int>)
Fills results (cleared first) with every marker
that overlaps bounds: ground markers whose
stored position falls inside it, plus every altitude marker
unconditionally, mirroring the ground-only pre-reject
MarkerPicker and MarkerClusterer already apply by
hand. Only cells bounds touches are visited, so
cost follows the rect's cell-span, not the marker count.
public void Query(in MercatorBounds bounds, List<int> results)
Parameters
boundsMercatorBoundsresultsList<int>
Remarks
bounds must come from the same unwrapped
mercator frame stored positions were added in - the same
constraint ContainsBounds(in MercatorBounds) documents.
A non-finite bound (NaN or infinite, which no caller should
legitimately produce) skips the ground scan entirely rather than
flooring an unspecified cast into a huge or unbounded cell range;
altitude markers are still returned in that case.
Remove(int)
Stops tracking a marker. Idempotent: removing an id that is not tracked (never added, or already removed) does nothing, the same convention Remove(int) uses.
public void Remove(int markerId)
Parameters
markerIdint
ResetForReuse(double)
Wipes every tracked marker and rebases CellSize to
cellSize, so this instance can be reused for an
unrelated span of adds/queries instead of constructing a new index
(and its backing dictionaries) each time.
public void ResetForReuse(double cellSize)
Parameters
cellSizedouble
Remarks
Destructive: every id this index was tracking stops being
findable, with no removal notification. Safe for an index a
single caller owns end to end and repopulates before the next use
(e.g. a scratch nearest-neighbour grid rebuilt once per call) -
wrong for an index something else (a MarkerLayer, say) is
still relying on to answer queries about markers it added.
UpdatePosition(int, MercatorPoint, double)
Moves a tracked marker to a new position and/or altitude, re-bucketing it if that changes which cell (or the altitude fallback) it belongs to. A stale id - not currently tracked - is ignored rather than an error, matching UpdatePosition(int, LatLon, double).
public void UpdatePosition(int markerId, MercatorPoint position, double altitudeMercator)
Parameters
markerIdintpositionMercatorPointaltitudeMercatordouble