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
JavaScript
"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;