export interface GeoJsonBounds { minX: number minY: number maxX: number maxY: number } function extendBounds(bounds: GeoJsonBounds | null, x: number, y: number): GeoJsonBounds { if (!bounds) { return { minX: x, minY: y, maxX: x, maxY: y } } bounds.minX = Math.min(bounds.minX, x) bounds.minY = Math.min(bounds.minY, y) bounds.maxX = Math.max(bounds.maxX, x) bounds.maxY = Math.max(bounds.maxY, y) return bounds } function scanCoordinates(coordinates: unknown, initial: GeoJsonBounds | null): GeoJsonBounds | null { let bounds = initial const stack: unknown[] = [coordinates] while (stack.length > 0) { const value = stack.pop() if (!Array.isArray(value)) { continue } if (value.length >= 2 && typeof value[0] === 'number' && typeof value[1] === 'number') { if (Number.isFinite(value[0]) && Number.isFinite(value[1])) { bounds = extendBounds(bounds, value[0], value[1]) } continue } for (const child of value) { stack.push(child) } } return bounds } export function geometryBounds(geometry: GeoJSON.Geometry | null | undefined): GeoJsonBounds | null { if (!geometry) { return null } if (geometry.type === 'GeometryCollection') { return geometry.geometries.reduce( (bounds, child) => mergeBounds(bounds, geometryBounds(child)), null, ) } return scanCoordinates(geometry.coordinates, null) } export function mergeBounds(left: GeoJsonBounds | null, right: GeoJsonBounds | null): GeoJsonBounds | null { if (!left) { return right ? { ...right } : null } if (!right) { return left } left.minX = Math.min(left.minX, right.minX) left.minY = Math.min(left.minY, right.minY) left.maxX = Math.max(left.maxX, right.maxX) left.maxY = Math.max(left.maxY, right.maxY) return left } export function featureCollectionBounds(collection: GeoJSON.FeatureCollection | null | undefined): GeoJsonBounds | null { if (!collection) { return null } return collection.features.reduce( (bounds, feature) => mergeBounds(bounds, geometryBounds(feature.geometry)), null, ) }