126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
"use strict";
|
|
/**
|
|
* Spatial utility functions for geographic calculations.
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.isPointInPolygon = isPointInPolygon;
|
|
exports.parseGeoJsonPolygon = parseGeoJsonPolygon;
|
|
exports.calculateBounds = calculateBounds;
|
|
exports.haversineDistance = haversineDistance;
|
|
exports.boundsFromCenterZoom = boundsFromCenterZoom;
|
|
exports.calculateCentroid = calculateCentroid;
|
|
/**
|
|
* Ray-casting algorithm to determine if a point is inside a polygon.
|
|
* @param lat Point latitude
|
|
* @param lng Point longitude
|
|
* @param polygonCoords Array of [lng, lat] coordinate pairs (GeoJSON order)
|
|
*/
|
|
function isPointInPolygon(lat, lng, polygonCoords) {
|
|
let inside = false;
|
|
for (let i = 0, j = polygonCoords.length - 1; i < polygonCoords.length; j = i++) {
|
|
const xi = polygonCoords[i][1]; // lat
|
|
const yi = polygonCoords[i][0]; // lng
|
|
const xj = polygonCoords[j][1];
|
|
const yj = polygonCoords[j][0];
|
|
const intersect = ((yi > lng) !== (yj > lng)) &&
|
|
(lat < (xj - xi) * (lng - yi) / (yj - yi) + xi);
|
|
if (intersect)
|
|
inside = !inside;
|
|
}
|
|
return inside;
|
|
}
|
|
/**
|
|
* Parse GeoJSON string and extract coordinate arrays for all polygons.
|
|
* Supports Polygon and MultiPolygon types.
|
|
* Returns array of coordinate rings (outer rings only).
|
|
*/
|
|
function parseGeoJsonPolygon(geojsonString) {
|
|
const geojson = JSON.parse(geojsonString);
|
|
if (geojson.type === 'Polygon') {
|
|
// Polygon coordinates: [ring][point][lng,lat]
|
|
return [geojson.coordinates[0]];
|
|
}
|
|
if (geojson.type === 'MultiPolygon') {
|
|
// MultiPolygon coordinates: [polygon][ring][point][lng,lat]
|
|
return geojson.coordinates.map((poly) => poly[0]);
|
|
}
|
|
throw new Error(`Unsupported GeoJSON type: ${geojson.type}`);
|
|
}
|
|
/**
|
|
* Calculate bounding box from an array of [lng, lat] coordinate pairs.
|
|
* Returns { minLat, maxLat, minLng, maxLng }.
|
|
*/
|
|
function calculateBounds(coordinates) {
|
|
let minLat = Infinity;
|
|
let maxLat = -Infinity;
|
|
let minLng = Infinity;
|
|
let maxLng = -Infinity;
|
|
for (const coord of coordinates) {
|
|
const lng = coord[0];
|
|
const lat = coord[1];
|
|
if (lat < minLat)
|
|
minLat = lat;
|
|
if (lat > maxLat)
|
|
maxLat = lat;
|
|
if (lng < minLng)
|
|
minLng = lng;
|
|
if (lng > maxLng)
|
|
maxLng = lng;
|
|
}
|
|
return { minLat, maxLat, minLng, maxLng };
|
|
}
|
|
/**
|
|
* Haversine distance between two lat/lng points in meters.
|
|
*/
|
|
function haversineDistance(lat1, lng1, lat2, lng2) {
|
|
const R = 6371000; // Earth radius in meters
|
|
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
const dLat = toRad(lat2 - lat1);
|
|
const dLng = toRad(lng2 - lng1);
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
|
Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return R * c;
|
|
}
|
|
/**
|
|
* Calculate the centroid of an array of [lng, lat] coordinate pairs.
|
|
* Returns { lat, lng }.
|
|
*/
|
|
/**
|
|
* Calculate a bounding box from a map center point and zoom level.
|
|
* Uses Web Mercator tile math to derive the geographic extent visible
|
|
* on a map of the given pixel dimensions at the given zoom.
|
|
* Default viewport: 1024x768 pixels.
|
|
*/
|
|
function boundsFromCenterZoom(lat, lng, zoom, widthPx = 1024, heightPx = 768) {
|
|
// Meters per pixel at a given zoom level at the equator
|
|
const metersPerPixelAtEquator = 156543.03392;
|
|
const metersPerPixel = metersPerPixelAtEquator * Math.cos((lat * Math.PI) / 180) / Math.pow(2, zoom);
|
|
const halfWidthMeters = (widthPx / 2) * metersPerPixel;
|
|
const halfHeightMeters = (heightPx / 2) * metersPerPixel;
|
|
// Convert meters offset to degrees
|
|
const latDegPerMeter = 1 / 111320;
|
|
const lngDegPerMeter = 1 / (111320 * Math.cos((lat * Math.PI) / 180));
|
|
const latOffset = halfHeightMeters * latDegPerMeter;
|
|
const lngOffset = halfWidthMeters * lngDegPerMeter;
|
|
return {
|
|
minLat: lat - latOffset,
|
|
maxLat: lat + latOffset,
|
|
minLng: lng - lngOffset,
|
|
maxLng: lng + lngOffset,
|
|
};
|
|
}
|
|
function calculateCentroid(coordinates) {
|
|
let sumLat = 0;
|
|
let sumLng = 0;
|
|
for (const coord of coordinates) {
|
|
sumLng += coord[0];
|
|
sumLat += coord[1];
|
|
}
|
|
return {
|
|
lat: sumLat / coordinates.length,
|
|
lng: sumLng / coordinates.length,
|
|
};
|
|
}
|
|
//# sourceMappingURL=spatial.js.map
|