UNPKG

geo-toolkits

Version:

A comprehensive TypeScript library for geospatial operations including geofencing, polygon containment, area calculations, and coordinate processing. Supports GeoJSON, KML, and KMZ file formats with both local file and remote URL loading capabilities.

297 lines (296 loc) 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeofencingAnalyzer = void 0; const errors_1 = require("../utils/errors"); class GeofencingAnalyzer { constructor() { this.features = []; this.loaded = false; } /** * Set spatial features data */ setFeatures(features) { this.features = [...features]; this.loaded = true; } /** * Check if coordinate is contained within any polygon (geofencing) */ contains(lat, lng) { this.ensureLoaded(); if (!this.isValidCoordinate(lat, lng)) { throw new errors_1.ValidationError("Invalid coordinates provided"); } // Check each feature for containment for (let i = 0; i < this.features.length; i++) { const feature = this.features[i]; // Quick bounding box check first (performance optimization) if (!this.isWithinBoundingBox(lat, lng, feature.boundingBox)) { continue; } if (feature.type === "Polygon") { if (this.isPointInPolygon(lat, lng, feature.coordinates)) { return { isInside: true, geometryType: "Polygon", featureIndex: i, properties: feature.properties, }; } } else if (feature.type === "MultiPolygon") { const polygonIndex = this.findContainingPolygonInMultiPolygon(lat, lng, feature.coordinates); if (polygonIndex !== -1) { return { isInside: true, geometryType: "MultiPolygon", featureIndex: i, polygonIndex, properties: feature.properties, }; } } } return { isInside: false }; } /** * Find closest feature to given coordinate */ findClosestFeature(lat, lng) { this.ensureLoaded(); if (!this.isValidCoordinate(lat, lng)) { throw new errors_1.ValidationError("Invalid coordinates provided"); } if (this.features.length === 0) { return null; } let closestIndex = 0; let minDistance = Infinity; let closestFeature; for (let i = 0; i < this.features.length; i++) { const feature = this.features[i]; let distance; if (feature.type === "Polygon") { distance = this.distanceToPolygon(lat, lng, feature.coordinates); } else { // MultiPolygon - find distance to closest polygon distance = Math.min(...feature.coordinates.map((polygonCoords) => this.distanceToPolygon(lat, lng, polygonCoords))); } if (distance < minDistance) { minDistance = distance; closestIndex = i; closestFeature = this.features[i]; } } return { featureIndex: closestIndex, distance: minDistance, feature: closestFeature, }; } /** * Get all features that contain the coordinate */ getAllContainingFeatures(lat, lng) { this.ensureLoaded(); if (!this.isValidCoordinate(lat, lng)) { throw new errors_1.ValidationError("Invalid coordinates provided"); } const containingFeatures = []; for (let i = 0; i < this.features.length; i++) { const feature = this.features[i]; // Quick bounding box check if (!this.isWithinBoundingBox(lat, lng, feature.boundingBox)) { continue; } if (feature.type === "Polygon") { if (this.isPointInPolygon(lat, lng, feature.coordinates)) { containingFeatures.push({ isInside: true, geometryType: "Polygon", featureIndex: i, properties: feature.properties, }); } } else if (feature.type === "MultiPolygon") { const polygonIndex = this.findContainingPolygonInMultiPolygon(lat, lng, feature.coordinates); if (polygonIndex !== -1) { containingFeatures.push({ isInside: true, geometryType: "MultiPolygon", featureIndex: i, polygonIndex, properties: feature.properties, }); } } } return containingFeatures; } /** * Check if point is within any feature's bounding box (quick filter) */ isWithinAnyBoundingBox(lat, lng) { this.ensureLoaded(); if (!this.isValidCoordinate(lat, lng)) { throw new errors_1.ValidationError("Invalid coordinates provided"); } return this.features.some((feature) => this.isWithinBoundingBox(lat, lng, feature.boundingBox)); } /** * Reset analyzer data */ reset() { this.features = []; this.loaded = false; } // ===== PRIVATE GEOMETRIC ALGORITHMS ===== /** * Point-in-Polygon using Ray Casting Algorithm * Works with holes (interior rings) */ isPointInPolygon(lat, lng, coordinates) { // coordinates[0] = exterior ring // coordinates[1...n] = interior rings (holes) const exteriorRing = coordinates[0]; const interiorRings = coordinates.slice(1); // Check if point is inside exterior ring if (!this.isPointInRing(lat, lng, exteriorRing)) { return false; // Outside the main polygon } // Check if point is inside any holes (interior rings) for (const hole of interiorRings) { if (this.isPointInRing(lat, lng, hole)) { return false; // Inside a hole, so not in polygon } } return true; // Inside exterior ring and not in any holes } /** * Ray casting algorithm for a single ring */ isPointInRing(lat, lng, ring) { 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]; // Ray casting: check if ray from point crosses edge if (yi > lat !== yj > lat && lng < ((xj - xi) * (lat - yi)) / (yj - yi) + xi) { inside = !inside; } } return inside; } /** * Find which polygon in MultiPolygon contains the point */ findContainingPolygonInMultiPolygon(lat, lng, coordinates) { for (let i = 0; i < coordinates.length; i++) { if (this.isPointInPolygon(lat, lng, coordinates[i])) { return i; } } return -1; // Not contained in any polygon } /** * Calculate distance from point to polygon boundary */ distanceToPolygon(lat, lng, coordinates) { // If point is inside polygon, distance is 0 if (this.isPointInPolygon(lat, lng, coordinates)) { return 0; } // Find minimum distance to any ring boundary let minDistance = Infinity; for (const ring of coordinates) { const distanceToRing = this.distanceToRing(lat, lng, ring); minDistance = Math.min(minDistance, distanceToRing); } return minDistance; } /** * Calculate minimum distance from point to ring boundary */ distanceToRing(lat, lng, ring) { let minDistance = Infinity; for (let i = 0; i < ring.length - 1; i++) { const [x1, y1] = ring[i]; const [x2, y2] = ring[i + 1]; const distance = this.distanceToLineSegment(lat, lng, y1, x1, y2, x2); minDistance = Math.min(minDistance, distance); } return minDistance; } /** * Calculate distance from point to line segment using Haversine for accuracy */ distanceToLineSegment(pointLat, pointLng, lat1, lng1, lat2, lng2) { // Convert to radians const toRad = (deg) => (deg * Math.PI) / 180; const φ1 = toRad(lat1); const φ2 = toRad(lat2); const φp = toRad(pointLat); const Δλ1 = toRad(lng1 - pointLng); const Δλ2 = toRad(lng2 - pointLng); // Project point onto line segment const a = Math.sin(φ1) * Math.sin(φp) + Math.cos(φ1) * Math.cos(φp) * Math.cos(Δλ1); const b = Math.sin(φ2) * Math.sin(φp) + Math.cos(φ2) * Math.cos(φp) * Math.cos(Δλ2); const c = Math.sin(φ1) * Math.sin(φ2) + Math.cos(φ1) * Math.cos(φ2) * Math.cos(lng2 - lng1); // If point projects beyond segment, use endpoint distances if (a < c || b < c) { const dist1 = this.haversineDistance(pointLat, pointLng, lat1, lng1); const dist2 = this.haversineDistance(pointLat, pointLng, lat2, lng2); return Math.min(dist1, dist2); } // Calculate perpendicular distance to line return this.haversineDistance(pointLat, pointLng, lat1, lng1); } /** * Haversine formula for great circle distance between two points * Returns distance in meters */ haversineDistance(lat1, lng1, lat2, lng2) { const R = 6371000; // Earth's radius in meters const toRad = (deg) => (deg * Math.PI) / 180; const φ1 = toRad(lat1); const φ2 = toRad(lat2); const Δφ = toRad(lat2 - lat1); const Δλ = toRad(lng2 - lng1); const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in meters } /** * Quick bounding box check for performance optimization */ isWithinBoundingBox(lat, lng, bbox) { return (lat >= bbox.minLat && lat <= bbox.maxLat && lng >= bbox.minLng && lng <= bbox.maxLng); } // ===== VALIDATION HELPERS ===== ensureLoaded() { if (!this.loaded) { throw new errors_1.ValidationError("No spatial data available in geofencing analyzer", "NOT_LOADED"); } } isValidCoordinate(lat, lng) { return (typeof lat === "number" && typeof lng === "number" && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180 && !isNaN(lat) && !isNaN(lng)); } } exports.GeofencingAnalyzer = GeofencingAnalyzer;