This document defines the configuration shape for geometry placement (Option B) and the validation rules the layer should enforce.
The goal is to keep defaults sensible, ensure errors are actionable, and make it clear when view-dependent recomputation will occur.
type PlacementStrategy =
| 'point' // Pass-through points
| 'centroid' // One anchor per feature (polygon/line centroid)
| 'polylabel' // Better polygon label placement (optional dependency; falls back to centroid)
| 'line-sample' // Sample points along line/multiline
| 'grid-geo' // Sample polygon interior at geodesic spacing (meters; respects holes)
| 'grid-screen'; // Sample polygon interior at screen-grid centers (pixels; view-dependent)
type PlacementPartition = 'union' | 'per-part'; // Multipolygons
type Spacing =
| { meters: number }
| { pixels: number };
interface PlacementConfig {
strategy: PlacementStrategy;
spacing?: Spacing;
partition?: PlacementPartition;
maxPerFeature?: number;
minArea?: number; // m²; polygons smaller than this may fallback
minLength?: number; // meters; lines shorter than this may fallback
jitterPixels?: number;
zoomAdaptive?: boolean; // Convert pixels<->meters using current zoom/lat
}
type RenderMode = 'screen-grid' | 'feature-anchors';
interface LayerOptionsExtension {
// Mutually exclusive with existing `data` + `getPosition`
source?: GeoJSON.FeatureCollection | GeoJSON.Feature[];
placement?: PlacementConfig;
renderMode?: RenderMode; // default: 'screen-grid'
anchorSizePixels?: number; // only in 'feature-anchors' mode
}
1) Mutual exclusivity
data + getPosition (current behavior) OR source + placement. If both are set, throw an error:
data/getPosition or source/placement, not both.”2) Required fields for geometry input
source is provided:
placement.strategy is required.renderMode defaults to 'screen-grid' if omitted.3) Strategy-specific requirements
'centroid':
spacing required.partition for multipolygons. Default 'union'.'polylabel':
spacing required.partition. Default 'union'.'centroid' unless strict mode is added later.'line-sample':
spacing is required (either { meters } or { pixels }).minLength, maxPerFeature, zoomAdaptive.'grid-geo':
spacing is required with { meters }.minArea, maxPerFeature, zoomAdaptive (ignored; spacing already meters-based).'grid-screen':
spacing is required with { pixels }.maxPerFeature, jitterPixels.4) Spacing constraints
spacing:
> 0 && isFinite(value).meters and pixels are present at once.5) Numeric bounds
maxPerFeature: integer >= 1. If set to a very high number, log a performance warning.minArea: >= 0. Units m².minLength: >= 0. Units meters.jitterPixels: >= 0.anchorSizePixels: > 0 (only used in ‘feature-anchors’).6) Partition
partition must be 'union' | 'per-part'. Default 'union'.7) Double aggregation safeguard
placement.strategy === 'grid-screen' and renderMode === 'screen-grid':
renderMode to 'feature-anchors' to avoid double aggregation and logs a single, deduplicated console warning to inform users of the switch.8) View-dependent recomputation
spacing uses { pixels } or zoomAdaptive: true, mark placement as view-dependent.move, zoom, or resize, recompute anchors only when a threshold is exceeded:
9) Fallbacks
minArea) and short lines (below minLength) should fallback to a single 'centroid' anchor unless maxPerFeature is forcing at least one sample.10) Source validation
source must be FeatureCollection or array of Features.geometry.type:
data/getPosition or source/placement, not both.”placement.strategy is required when source is provided.”spacing is required for strategy ‘{strategy}’.”{ meters } or { pixels }.”{key} must be a finite positive number.”source is present:
placement.strategy has no default (must be explicit).placement.partition defaults to 'union'.renderMode defaults to 'screen-grid' (for grid-screen, the system auto-switches to 'feature-anchors' to avoid double aggregation and logs a warning).anchorSizePixels defaults to Math.round(cellSizePixels * glyphSize * 0.9) for visual parity.// 1) Admin boundaries, one glyph per polygon (centroid)
{
source: adminGeoJSON,
placement: { strategy: 'centroid', partition: 'union' },
renderMode: 'feature-anchors',
anchorSizePixels: 18,
glyph: 'circle',
enableGlyphs: true
}
// 2) Roads sampled every 200 meters, still aggregated into screen grid
{
source: roadsGeoJSON,
placement: { strategy: 'line-sample', spacing: { meters: 200 }, zoomAdaptive: true },
renderMode: 'screen-grid',
cellSizePixels: 60
}
// 3) Admin areas filled with anchors from screen grid centers; drawn per anchor
{
source: adminGeoJSON,
placement: { strategy: 'grid-screen', spacing: { pixels: 60 } },
renderMode: 'feature-anchors',
anchorSizePixels: 14,
glyph: 'heatmap',
enableGlyphs: true
}
setConfig updates.project/unproject to maintain consistency between placement and rendering coordinates.polylabel, treat the dependency as optional; if not present or if computation times out, fallback to centroid and warn.docs/GEOMETRY_INPUT_AND_PLACEMENT.md