Table of Contents

Class MarkerSpatialIndex

Namespace
LansMap.Core.Markers

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
object
MarkerSpatialIndex

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

cellSize double

Grid 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.

initialCapacity int

Marker 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

int

Properties

AltitudeMarkerCount

Markers currently in the altitude fallback list.

public int AltitudeMarkerCount { get; }

Property Value

int

CellSize

The grid cell size passed to the constructor.

public double CellSize { get; }

Property Value

double

Count

Markers currently tracked, ground plus altitude.

public int Count { get; }

Property Value

int

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

int

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

int

Methods

Add(int, MercatorPoint, double)

Starts tracking a marker.

public void Add(int markerId, MercatorPoint position, double altitudeMercator)

Parameters

markerId int

Caller-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.

position MercatorPoint

Ground mercator position.

altitudeMercator double

Height above the ground plane in normalized mercator units, the same quantity MarkerInstanceData.AltitudeMercator carries. 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

markerId is 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

bounds MercatorBounds
results List<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

markerId int

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

cellSize double

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

markerId int
position MercatorPoint
altitudeMercator double