import type { LatLng } from './map.types'; /** * Minimal structural GeoJSON types. * * Declared here rather than pulled from `@types/geojson` because the library * ships no runtime dependencies for maps and these are the only shapes the * outline renderer reads. Anything conforming to RFC 7946 satisfies them. */ export type GeoJsonPosition = number[]; export type GeoJsonCoordinates = GeoJsonPosition | GeoJsonCoordinates[]; export interface GeoJsonGeometry { type: string; coordinates?: GeoJsonCoordinates; geometries?: GeoJsonGeometry[]; } export interface GeoJsonFeature { type: 'Feature'; geometry: GeoJsonGeometry | null; properties?: Record | null; id?: string | number; } export interface GeoJsonFeatureCollection { type: 'FeatureCollection'; features: GeoJsonFeature[]; } export type GeoJsonInput = GeoJsonFeatureCollection | GeoJsonFeature | GeoJsonGeometry; /** * A single connected run of coordinates — one polygon ring or one line. * `closed` decides whether the renderer emits a `Z` at the end of the path. */ export interface GeoJsonRing { points: LatLng[]; closed: boolean; } function isPosition(value: GeoJsonCoordinates): value is GeoJsonPosition { return Array.isArray(value) && typeof value[0] === 'number' && typeof value[1] === 'number'; } /** * GeoJSON stores coordinates as `[longitude, latitude]` — the reverse of the * `{ lat, lng }` order used everywhere else in this library, and a reliable * source of maps that render sideways. This is the only place the swap happens. */ function toLatLng(position: GeoJsonPosition): LatLng { return { lng: position[0], lat: position[1] }; } function collectRings(geometry: GeoJsonGeometry | null | undefined, out: GeoJsonRing[]): void { if (!geometry) return; if (geometry.type === 'GeometryCollection') { geometry.geometries?.forEach(child => collectRings(child, out)); return; } const coordinates = geometry.coordinates; if (!coordinates) return; const closed = geometry.type === 'Polygon' || geometry.type === 'MultiPolygon'; /* * Point, LineString, Polygon, MultiPoint, MultiLineString and MultiPolygon * differ only in how deeply the positions are nested, so walk down to the * first level that holds positions and treat each such array as one run. */ const walk = (node: GeoJsonCoordinates): void => { if (isPosition(node)) return; if (!Array.isArray(node)) return; if (node.every(isPosition)) { const points = (node as GeoJsonPosition[]).map(toLatLng); if (points.length > 1) out.push({ points, closed }); return; } node.forEach(child => walk(child as GeoJsonCoordinates)); }; walk(coordinates); } // `GeoJsonGeometry.type` is an open `string` (any RFC 7946 geometry name), so // TypeScript cannot discriminate the union on it. Explicit guards instead. function isFeatureCollection(input: GeoJsonInput): input is GeoJsonFeatureCollection { return ( (input as GeoJsonFeatureCollection).type === 'FeatureCollection' && Array.isArray((input as GeoJsonFeatureCollection).features) ); } function isFeature(input: GeoJsonInput): input is GeoJsonFeature { return (input as GeoJsonFeature).type === 'Feature'; } /** Flattens any GeoJSON input into the runs of coordinates needed to draw it. */ export function geoJsonRings(input: GeoJsonInput | null | undefined): GeoJsonRing[] { if (!input) return []; const rings: GeoJsonRing[] = []; if (isFeatureCollection(input)) { input.features.forEach(feature => collectRings(feature.geometry, rings)); } else if (isFeature(input)) { collectRings(input.geometry, rings); } else { collectRings(input, rings); } return rings; } /** Every coordinate in the input, for bounds and fitting. */ export function geoJsonPositions(input: GeoJsonInput | null | undefined): LatLng[] { return geoJsonRings(input).flatMap(ring => ring.points); }