diff options
Diffstat (limited to 'app/geometry.js')
| -rw-r--r-- | app/geometry.js | 954 |
1 files changed, 954 insertions, 0 deletions
diff --git a/app/geometry.js b/app/geometry.js new file mode 100644 index 0000000..421d90b --- /dev/null +++ b/app/geometry.js @@ -0,0 +1,954 @@ +/** + * Spatial analysis library for WGS84 coordinates using spherical Earth model + * Supports: Point, LineString, Polygon, MultiLineString + * @module Geometry + */ + +// Earth radius in meters (simplified sphere model) +const R = 6371000; +const TOLERANCE = 1e-10; + +/** + * Geographic bounds representation + * @class + */ +export class Bounds { + /** + * @param {number} minX - Minimum longitude + * @param {number} minY - Minimum latitude + * @param {number} maxX - Maximum longitude + * @param {number} maxY - Maximum latitude + */ + constructor(minX, minY, maxX, maxY) { + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + /** @returns {number} Width in degrees */ + get width() { + return this.maxX - this.minX; + } + + /** @returns {number} Height in degrees */ + get height() { + return this.maxY - this.minY; + } + + /** @returns {Coordinate} Center coordinate */ + get center() { + return [this.minX + this.width / 2, this.minY + this.height / 2]; + } + + /** + * Check if bounds contain coordinate + * @param {Coordinate} coordinate + * @returns {boolean} + */ + contains(coordinate) { + const [lng, lat] = coordinate; + return lng >= this.minX && lng <= this.maxX && lat >= this.minY && lat <= this.maxY; + } +} + +/** + * @typedef {[number, number]} Coordinate - [longitude, latitude] in decimal degrees + */ + +/** + * Base geometry class + * @abstract + */ +export class Geometry { + /** @returns {string} Geometry type */ + get type() { + return this.constructor.name; + } + + /** + * Convert to GeoJSON geometry object + * @returns {Object} GeoJSON geometry + * @abstract + */ + toGeoJSON() { + throw new Error("Not implemented"); + } + + /** + * Calculate geometry bounds + * @returns {Bounds} Geometry bounds + * @abstract + */ + bounds() { + throw new Error("Not implemented"); + } + + /** + * Calculate distance to another geometry + * @param {Geometry} other - Target geometry + * @returns {number} Distance in meters + */ + distance(other) { + return Geometry.distance(this, other); + } + + /** + * Calculate geometry length + * @returns {number} Length in meters + */ + length() { + return Geometry.length(this); + } + + /** + * Check if geometry intersects another + * @param {Geometry} other - Target geometry + * @returns {boolean} + */ + intersects(other) { + return Geometry.intersects(this, other); + } + + /** + * Check if geometry is completely within another + * @param {Geometry} other - Container geometry + * @returns {boolean} + */ + within(other) { + return Geometry.within(this, other); + } + + /** + * Deserialize geometry from GeoJSON + * @param {Object} geojson - GeoJSON geometry object + * @returns {Geometry|null} Geometry instance or null + */ + static fromGeoJSON(geojson) { + if (!geojson?.type) return null; + switch (geojson.type) { + case "Point": + return Point.fromGeoJSON(geojson); + case "LineString": + return LineString.fromGeoJSON(geojson); + case "Polygon": + return Polygon.fromGeoJSON(geojson); + case "MultiLineString": + return MultiLineString.fromGeoJSON(geojson); + default: + throw new Error(`Unsupported geometry type: ${geojson.type}`); + } + } + + /** + * Compute geometry bounds + * @param {Geometry} geometry - Input geometry + * @returns {Bounds} Geometry bounds + */ + static bounds(geometry) { + if (geometry instanceof Point) { + return new Bounds(geometry.lng, geometry.lat, geometry.lng, geometry.lat); + } + + if (geometry instanceof LineString) { + return Geometry.calculateBoundsFromCoords(geometry.coordinates); + } + + if (geometry instanceof Polygon) { + return Geometry.calculateBoundsFromCoords(geometry.rings[0]); + } + + throw new Error(`Unsupported geometry type: ${geometry.type}`); + } + + /** + * Calculate bounds from coordinate array + * @param {Coordinate[]} coords - Coordinate array + * @returns {Bounds} Calculated bounds + */ + static calculateBoundsFromCoords(coords) { + const lngs = coords.map(([lng]) => lng); + const lats = coords.map(([, lat]) => lat); + + return new Bounds(Math.min(...lngs), Math.min(...lats), Math.max(...lngs), Math.max(...lats)); + } + + /** + * Calculate distance between geometries + * @param {Geometry} a - First geometry + * @param {Geometry} b - Second geometry + * @returns {number} Minimum distance in meters + */ + static distance(a, b) { + const pointsA = Geometry.#geometryToPoints(a); + const pointsB = Geometry.#geometryToPoints(b); + + let minDistance = Infinity; + + for (const pointA of pointsA) { + for (const pointB of pointsB) { + const dist = Point.distance(pointA, pointB); + if (dist < minDistance) minDistance = dist; + } + } + + return minDistance; + } + + /** + * Convert geometry to representative points + * @param {Geometry} geometry - Input geometry + * @returns {Point[]} Array of points + */ + static #geometryToPoints(geometry) { + if (geometry instanceof Point) { + return [geometry]; + } + + if (geometry instanceof LineString) { + return geometry.coordinates.map(([lng, lat]) => new Point(lng, lat)); + } + + if (geometry instanceof Polygon) { + return geometry.rings.flatMap((ring) => ring.map(([lng, lat]) => new Point(lng, lat))); + } + + throw new Error(`Unsupported geometry type: ${geometry.type}`); + } + + /** + * Calculate geometry length + * @param {Geometry} geometry - Input geometry + * @returns {number} Length in meters + */ + static length(geometry) { + if (geometry instanceof Point) return 0; + + if (geometry instanceof LineString) { + const { coordinates } = geometry; + return coordinates.slice(1).reduce((total, coord, i) => { + const prev = coordinates[i]; + return total + Point.distance(new Point(...prev), new Point(...coord)); + }, 0); + } + + if (geometry instanceof Polygon) { + return geometry.rings.reduce( + (perimeter, ring) => perimeter + Geometry.length(new LineString(ring)), + 0, + ); + } + + throw new Error(`Unsupported geometry type: ${geometry.type}`); + } + + /** + * Find shortest connecting line between geometries + * @param {Geometry} a - First geometry + * @param {Geometry} b - Second geometry + * @returns {LineString} Shortest connecting line + */ + static shortestLine(a, b) { + const pointsA = Geometry.#geometryToPoints(a); + const pointsB = Geometry.#geometryToPoints(b); + + let minDistance = Infinity; + let closestPair = [null, null]; + + for (const pointA of pointsA) { + for (const pointB of pointsB) { + const dist = Point.distance(pointA, pointB); + if (dist < minDistance) { + minDistance = dist; + closestPair = [pointA, pointB]; + } + } + } + + const [pointA, pointB] = closestPair; + if (!pointA || !pointB) { + throw new Error("Could not find shortest line between geometries"); + } + + return new LineString([ + [pointA.lng, pointA.lat], + [pointB.lng, pointB.lat], + ]); + } + + /** + * Check if geometries intersect + * @param {Geometry} a - First geometry + * @param {Geometry} b - Second geometry + * @returns {boolean} + */ + static intersects(a, b) { + if (!Geometry.#boundsIntersect(a, b)) return false; + + if (a instanceof Point) return Geometry.#pointIntersects(a, b); + if (b instanceof Point) return Geometry.#pointIntersects(b, a); + if (a instanceof LineString && b instanceof LineString) { + return LineString.intersectsLine(a, b); + } + if (a instanceof Polygon) return Geometry.#polygonIntersects(a, b); + if (b instanceof Polygon) return Geometry.#polygonIntersects(b, a); + + return Geometry.distance(a, b) < 0.1; + } + + /** + * Check if geometry A is within geometry B + * @param {Geometry} a - Inner geometry + * @param {Geometry} b - Outer geometry + * @returns {boolean} + */ + static within(a, b) { + const bA = Geometry.bounds(a); + const bB = Geometry.bounds(b); + + // Quick bounds check + if (bA.minX < bB.minX || bA.maxX > bB.maxX || bA.minY < bB.minY || bA.maxY > bB.maxY) { + return false; + } + + if (a instanceof Point) return Geometry.#pointWithin(a, b); + if (a instanceof LineString && b instanceof Polygon) { + return Geometry.#lineWithinPolygon(a, b); + } + if (a instanceof Polygon && b instanceof Polygon) { + return Geometry.#polygonWithinPolygon(a, b); + } + + return Geometry.#geometryToPoints(a).every((point) => Geometry.#pointWithin(point, b)); + } + + /** + * Check if geometry bounds intersect + * @param {Geometry} a - First geometry + * @param {Geometry} b - Second geometry + * @returns {boolean} + */ + static #boundsIntersect(a, b) { + const ba = Geometry.bounds(a); + const bb = Geometry.bounds(b); + + return !(ba.maxX < bb.minX || ba.minX > bb.maxX || ba.maxY < bb.minY || ba.minY > bb.maxY); + } + + /** + * Check if point intersects geometry + * @param {Point} point - Test point + * @param {Geometry} geometry - Target geometry + * @returns {boolean} + */ + static #pointIntersects(point, geometry) { + if (geometry instanceof Point) return point.equals(geometry); + if (geometry instanceof LineString) return Geometry.#pointOnLine(point, geometry); + if (geometry instanceof Polygon) return Geometry.#pointInPolygon(point, geometry); + return false; + } + + /** + * Check if point is within geometry + * @param {Point} point - Test point + * @param {Geometry} geometry - Container geometry + * @returns {boolean} + */ + static #pointWithin(point, geometry) { + if (geometry instanceof Point) return point.equals(geometry); + if (geometry instanceof LineString) return Geometry.#pointOnLine(point, geometry); + if (geometry instanceof Polygon) return Geometry.#pointInPolygon(point, geometry); + return false; + } + + /** + * Check if point lies on line + * @param {Point} point - Test point + * @param {LineString} line - Target line + * @param {number} [tolerance=0.1] - Tolerance in meters + * @returns {boolean} + */ + static #pointOnLine(point, line, tolerance = 0.1) { + const points = line.getPoints(); + + // Check vertices + if (points.some((vertex) => point.equals(vertex, tolerance / 111320))) { + return true; + } + + // Check segments + for (let i = 1; i < points.length; i++) { + const p1 = points[i - 1]; + const p2 = points[i]; + const segmentLength = Point.distance(p1, p2); + if (segmentLength === 0) continue; + + const d1 = Point.distance(point, p1); + const d2 = Point.distance(point, p2); + + if (Math.abs(d1 + d2 - segmentLength) < tolerance) { + return true; + } + } + + return false; + } + + /** + * Check if point is inside polygon using ray casting + * @param {Point} point - Test point + * @param {Polygon} polygon - Target polygon + * @returns {boolean} + */ + static #pointInPolygon(point, polygon) { + const [x, y] = [point.lng, point.lat]; + const exterior = polygon.getExterior(); + + if (exterior.length < 3) return false; + + let inside = false; + const n = exterior.length; + + for (let i = 0, j = n - 1; i < n; j = i++) { + const [xi, yi] = exterior[i]; + const [xj, yj] = exterior[j]; + + const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + + if (intersect) inside = !inside; + } + + // Check holes + if (inside) { + for (const hole of polygon.getHoles()) { + if (Geometry.#pointInRing(point, hole)) { + return false; + } + } + } + + return inside; + } + + /** + * Check if point is inside ring + * @param {Point} point - Test point + * @param {Coordinate[]} ring - Coordinate ring + * @returns {boolean} + */ + static #pointInRing(point, ring) { + const [x, y] = [point.lng, point.lat]; + let inside = false; + const n = ring.length; + + for (let i = 0, j = n - 1; i < n; j = i++) { + const [xi, yi] = ring[i]; + const [xj, yj] = ring[j]; + + const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; + + if (intersect) inside = !inside; + } + + return inside; + } + + /** + * Check if polygon intersects geometry + * @param {Polygon} polygon - Test polygon + * @param {Geometry} geometry - Target geometry + * @returns {boolean} + */ + static #polygonIntersects(polygon, geometry) { + // Check if any geometry points are inside polygon + if ( + Geometry.#geometryToPoints(geometry).some((point) => Geometry.#pointInPolygon(point, polygon)) + ) { + return true; + } + + // Check intersections with exterior and holes + const boundaries = [ + new LineString(polygon.getExterior()), + ...polygon.getHoles().map((hole) => new LineString(hole)), + ]; + + return boundaries.some((boundary) => Geometry.intersects(boundary, geometry)); + } + + /** + * Check if line is within polygon + * @param {LineString} line - Test line + * @param {Polygon} polygon - Container polygon + * @returns {boolean} + */ + static #lineWithinPolygon(line, polygon) { + return ( + line.getPoints().every((point) => Geometry.#pointInPolygon(point, polygon)) && + !polygon.getHoles().some((hole) => Geometry.intersects(line, new LineString(hole))) + ); + } + + /** + * Check if polygon A is within polygon B + * @param {Polygon} polyA - Inner polygon + * @param {Polygon} polyB - Outer polygon + * @returns {boolean} + */ + static #polygonWithinPolygon(polyA, polyB) { + return ( + Geometry.#geometryToPoints(polyA).every((point) => Geometry.#pointInPolygon(point, polyB)) && + !polyB.getHoles().some((hole) => Geometry.intersects(polyA, new LineString(hole))) + ); + } +} + +/** Point geometry class */ +export class Point extends Geometry { + /** + * @param {number} lng - Longitude + * @param {number} lat - Latitude + */ + constructor(lng, lat) { + super(); + this.lng = lng; + this.lat = lat; + } + + /** @returns {Object} GeoJSON point */ + toGeoJSON() { + return { + coordinates: [this.lng, this.lat], + type: "Point", + }; + } + + /** @returns {Bounds} Point bounds */ + bounds() { + return new Bounds(this.lng, this.lat, this.lng, this.lat); + } + + /** + * Create Point from GeoJSON + * @param {Object} geojson - GeoJSON point + * @returns {Point} + */ + static fromGeoJSON(geojson) { + const [lng, lat] = geojson.coordinates; + return new Point(lng, lat); + } + + /** + * Calculate distance between points using Haversine formula + * @param {Point} a - First point + * @param {Point} b - Second point + * @returns {number} Distance in meters + */ + static distance(a, b) { + const φ1 = (a.lat * Math.PI) / 180; + const φ2 = (b.lat * Math.PI) / 180; + const Δφ = ((b.lat - a.lat) * Math.PI) / 180; + const Δλ = ((b.lng - a.lng) * Math.PI) / 180; + + const aHav = Math.sin(Δφ / 2) ** 2 + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) ** 2; + const c = 2 * Math.atan2(Math.sqrt(aHav), Math.sqrt(1 - aHav)); + + return R * c; + } + + /** + * Check if point equals another point + * @param {Point} other - Comparison point + * @param {number} [tolerance=TOLERANCE] - Tolerance in degrees + * @returns {boolean} + */ + equals(other, tolerance = TOLERANCE) { + return Math.abs(this.lng - other.lng) < tolerance && Math.abs(this.lat - other.lat) < tolerance; + } +} + +/** MultiLineString geometry class */ +export class MultiLineString extends Geometry { + /** + * @param {Coordinate[][]} coordinates - Line coordinates + */ + constructor(coordinates) { + super(); + this.coordinates = coordinates; + } + + /** + * Create from GeoJSON + * @param {Object} geojson - GeoJSON MultiLineString + * @returns {MultiLineString} + */ + static fromGeoJSON(geojson) { + return new MultiLineString(geojson.coordinates); + } + + /** @returns {Object} GeoJSON representation */ + toGeoJSON() { + return { + coordinates: this.coordinates, + type: "MultiLineString", + }; + } + + /** @returns {Bounds} Geometry bounds */ + bounds() { + const allCoords = this.coordinates.flat(); + return Geometry.calculateBoundsFromCoords(allCoords); + } +} + +/** LineString geometry class */ +export class LineString extends Geometry { + /** + * @param {Coordinate[]} coordinates - Line coordinates + */ + constructor(coordinates) { + super(); + this.coordinates = coordinates; + } + + /** + * Check if two lines intersect + * @param {LineString} line1 - First line + * @param {LineString} line2 - Second line + * @returns {boolean} + */ + static intersectsLine(line1, line2) { + const coords1 = line1.coordinates; + const coords2 = line2.coordinates; + + for (let i = 1; i < coords1.length; i++) { + const [a1x, a1y] = coords1[i - 1]; + const [a2x, a2y] = coords1[i]; + + for (let j = 1; j < coords2.length; j++) { + const [b1x, b1y] = coords2[j - 1]; + const [b2x, b2y] = coords2[j]; + + if (LineString.#segmentsIntersect(a1x, a1y, a2x, a2y, b1x, b1y, b2x, b2y)) { + return true; + } + } + } + + return false; + } + + /** + * Check if two segments intersect + */ + static #segmentsIntersect(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + const s1x = p1x - p0x; + const s1y = p1y - p0y; + const s2x = p3x - p2x; + const s2y = p3y - p2y; + + const s = (-s1y * (p0x - p2x) + s1x * (p0y - p2y)) / (-s2x * s1y + s1x * s2y); + const t = (s2x * (p0y - p2y) - s2y * (p0x - p2x)) / (-s2x * s1y + s1x * s2y); + + return s >= 0 && s <= 1 && t >= 0 && t <= 1; + } + + /** + * Interpolate point at distance along line + * @param {LineString} line - Target line + * @param {number} distance - Distance in meters + * @param {Object} [options] - Interpolation options + * @param {boolean} [options.normalized=false] - Treat distance as fraction + * @returns {Point} Interpolated point + */ + static interpolatePoint(line, distance, options = {}) { + const { normalized = false } = options; + const coords = line.coordinates; + + if (coords.length < 2) { + throw new Error("LineString must have at least 2 points"); + } + + const targetDistance = normalized ? distance * Geometry.length(line) : distance; + if (targetDistance < 0) return new Point(...coords[0]); + + let accumulatedDistance = 0; + + for (let i = 1; i < coords.length; i++) { + const [lng1, lat1] = coords[i - 1]; + const [lng2, lat2] = coords[i]; + const segmentLength = Point.distance(new Point(lng1, lat1), new Point(lng2, lat2)); + + if (accumulatedDistance + segmentLength >= targetDistance) { + const ratio = (targetDistance - accumulatedDistance) / segmentLength; + const lng = lng1 + ratio * (lng2 - lng1); + const lat = lat1 + ratio * (lat2 - lat1); + return new Point(lng, lat); + } + + accumulatedDistance += segmentLength; + } + + return new Point(...coords[coords.length - 1]); + } + + /** + * Locate point on line and return distance + * @param {LineString} line - Target line + * @param {Point} point - Test point + * @param {Object} [options] - Location options + * @param {boolean} [options.normalized=false] - Return normalized distance + * @returns {number} Distance in meters (or normalized) + */ + static locatePoint(line, point, options = {}) { + const { normalized = false } = options; + const coords = line.coordinates; + + if (coords.length < 2) { + throw new Error("LineString must have at least 2 points"); + } + + let minDistance = Infinity; + let closestDistance = 0; + let accumulatedDistance = 0; + + for (let i = 1; i < coords.length; i++) { + const [lng1, lat1] = coords[i - 1]; + const [lng2, lat2] = coords[i]; + const segmentStart = new Point(lng1, lat1); + const segmentEnd = new Point(lng2, lat2); + const segmentLength = Point.distance(segmentStart, segmentEnd); + + if (segmentLength === 0) { + accumulatedDistance += Point.distance(segmentStart, point); + continue; + } + + const u = + ((point.lng - lng1) * (lng2 - lng1) + (point.lat - lat1) * (lat2 - lat1)) / + ((lng2 - lng1) ** 2 + (lat2 - lat1) ** 2); + + const ratio = Math.max(0, Math.min(1, u)); + const projectedLng = lng1 + ratio * (lng2 - lng1); + const projectedLat = lat1 + ratio * (lat2 - lat1); + const projectedPoint = new Point(projectedLng, projectedLat); + const distToSegment = Point.distance(point, projectedPoint); + + if (distToSegment < minDistance) { + minDistance = distToSegment; + closestDistance = accumulatedDistance + ratio * segmentLength; + } + + accumulatedDistance += segmentLength; + } + + return normalized ? closestDistance / Geometry.length(line) : closestDistance; + } + + /** @returns {Object} GeoJSON representation */ + toGeoJSON() { + return { + coordinates: this.coordinates, + type: "LineString", + }; + } + + /** @returns {Bounds} Line bounds */ + bounds() { + return Geometry.bounds(this); + } + + /** + * Create from GeoJSON + * @param {Object} geojson - GeoJSON LineString + * @returns {LineString} + */ + static fromGeoJSON(geojson) { + return new LineString(geojson.coordinates); + } + + /** + * Get points as Point objects + * @returns {Point[]} + */ + getPoints() { + return this.coordinates.map(([lng, lat]) => new Point(lng, lat)); + } +} + +/** Polygon geometry class */ +export class Polygon extends Geometry { + /** + * @param {Coordinate[][]} rings - Polygon rings (first is exterior, rest are holes) + */ + constructor(rings) { + super(); + this.rings = rings; + } + + /** @returns {Object} GeoJSON representation */ + toGeoJSON() { + return { + coordinates: this.rings, + type: "Polygon", + }; + } + + /** @returns {Bounds} Polygon bounds */ + bounds() { + return Geometry.bounds(this); + } + + /** + * Calculate polygon centroid + * @returns {Point} Centroid point + */ + centroid() { + const exterior = this.getExterior(); + + let twiceArea = 0; + let centroidLng = 0; + let centroidLat = 0; + + for (let i = 0, j = exterior.length - 1; i < exterior.length; j = i++) { + const [lng1, lat1] = exterior[j]; + const [lng2, lat2] = exterior[i]; + + const cross = lng1 * lat2 - lng2 * lat1; + twiceArea += cross; + + centroidLng += (lng1 + lng2) * cross; + centroidLat += (lat1 + lat2) * cross; + } + + const area = twiceArea * 0.5; + if (Math.abs(area) < TOLERANCE) { + // Degenerate polygon, return first point + return new Point(...exterior[0]); + } + + const factor = 1 / (6 * area); + centroidLng *= factor; + centroidLat *= factor; + + return new Point(centroidLng, centroidLat); + } + + /** + * Create from GeoJSON + * @param {Object} geojson - GeoJSON Polygon + * @returns {Polygon} + */ + static fromGeoJSON(geojson) { + return new Polygon(geojson.coordinates); + } + + /** + * Get exterior ring + * @returns {Coordinate[]} + */ + getExterior() { + return this.rings[0] ?? []; + } + + /** + * Get holes + * @returns {Coordinate[][]} + */ + getHoles() { + return this.rings.slice(1); + } +} + +/** GeoJSON Feature class */ +export class Feature { + /** + * @param {Geometry} geometry - Feature geometry + * @param {Object} [properties={}] - Feature properties + * @param {string|number} [id] - Feature ID + */ + constructor(geometry, properties = {}, id = null) { + this.type = "Feature"; + this.geometry = geometry; + this.properties = properties; + this.id = id; + } + + /** + * Convert to GeoJSON object + * @returns {Object} GeoJSON feature + */ + toGeoJSON() { + return { + geometry: this.geometry?.toGeoJSON() ?? null, + properties: this.properties, + type: "Feature", + ...(this.id != null && { id: this.id }), + }; + } + + /** + * Create from GeoJSON + * @param {Object} geojson - GeoJSON feature + * @returns {Feature|null} + */ + static fromGeoJSON(geojson) { + if (!geojson?.geometry) return null; + + const geometry = Geometry.fromGeoJSON(geojson.geometry); + if (!geometry) return null; + + return new Feature(geometry, geojson.properties ?? {}, geojson.id ?? null); + } +} + +/** GeoJSON Feature Collection class */ +export class Collection { + /** + * @param {Feature[]} [features=[]] - Feature array + */ + constructor(features = []) { + this.type = "FeatureCollection"; + this.features = features; + } + + /** + * Convert to GeoJSON object + * @returns {Object} GeoJSON collection + */ + toGeoJSON() { + return { + features: this.features.map((feature) => feature.toGeoJSON()), + type: "FeatureCollection", + }; + } + + /** + * Create from GeoJSON + * @param {Object} geojson - GeoJSON collection + * @returns {Collection} + */ + static fromGeoJSON(geojson) { + const features = (geojson.features ?? []).map(Feature.fromGeoJSON).filter(Boolean); + + return new Collection(features); + } + + /** + * Get geometries by type + * @param {string} type - Geometry type + * @returns {Geometry[]} + */ + getGeometriesByType(type) { + return this.features + .filter((feature) => feature.geometry?.type === type) + .map((feature) => feature.geometry); + } +} + +// Export constants +export { R as EARTH_RADIUS, TOLERANCE }; + +// Convenience functions +export const ser = (geometry) => geometry?.toGeoJSON() ?? null; +export const de = (geojson) => Geometry.fromGeoJSON(geojson); |
