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.

321 lines (320 loc) 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeometryCalculator = void 0; const errors_1 = require("../utils/errors"); class GeometryCalculator { constructor() { this.features = []; this.loaded = false; } /** * Set spatial features data */ setFeatures(features) { this.features = [...features]; this.loaded = true; } /** * Calculate total area of all features in square meters */ getArea() { this.ensureLoaded(); console.log("📐 Calculating total area for all features..."); let totalArea = 0; for (const feature of this.features) { if (feature.type === "Polygon") { totalArea += this.calculatePolygonArea(feature.coordinates); } else if (feature.type === "MultiPolygon") { totalArea += this.calculateMultiPolygonArea(feature.coordinates); } } console.log(`✅ Total area calculated: ${(totalArea / 1000000).toFixed(2)} km²`); return totalArea; } /** * Calculate detailed area breakdown for each feature */ getAreaDetails() { this.ensureLoaded(); console.log("📊 Calculating detailed area breakdown..."); const featureAreas = []; let totalArea = 0; this.features.forEach((feature, index) => { if (feature.type === "Polygon") { const area = this.calculatePolygonArea(feature.coordinates); featureAreas.push({ featureIndex: index, area: area, }); totalArea += area; } else if (feature.type === "MultiPolygon") { const polygonAreas = feature.coordinates.map((polygonCoords) => this.calculatePolygonArea(polygonCoords)); const multiPolygonTotalArea = polygonAreas.reduce((sum, area) => sum + area, 0); featureAreas.push({ featureIndex: index, area: multiPolygonTotalArea, polygonAreas: polygonAreas, }); totalArea += multiPolygonTotalArea; } }); console.log(`✅ Area breakdown completed for ${featureAreas.length} features`); return { totalArea, unit: "square_meters", features: featureAreas, }; } /** * Calculate area of a single polygon (handles holes) */ calculatePolygonArea(coordinates) { if (!coordinates || coordinates.length === 0) { return 0; } // Calculate exterior ring area const exteriorArea = Math.abs(this.calculateRingAreaSpherical(coordinates[0])); // Subtract hole areas (interior rings) let holeAreas = 0; for (let i = 1; i < coordinates.length; i++) { holeAreas += Math.abs(this.calculateRingAreaSpherical(coordinates[i])); } return Math.max(0, exteriorArea - holeAreas); } /** * Calculate total area of a MultiPolygon */ calculateMultiPolygonArea(coordinates) { return coordinates.reduce((totalArea, polygonCoords) => { return totalArea + this.calculatePolygonArea(polygonCoords); }, 0); } /** * Calculate area of a linear ring using spherical geometry * Uses the spherical excess formula for accurate Earth surface area */ calculateRingAreaSpherical(ring) { if (!ring || ring.length < 4) { return 0; // Need at least 4 points (3 unique + closing point) } const EARTH_RADIUS = 6378137; // Earth's radius in meters (WGS84) // Convert coordinates to radians const coords = ring.map(([lng, lat]) => [ this.toRadians(lng), this.toRadians(lat), ]); // Calculate spherical area using spherical excess let area = 0; const n = coords.length - 1; // Exclude the closing point for (let i = 0; i < n; i++) { const j = (i + 1) % n; const [lng1, lat1] = coords[i]; const [lng2, lat2] = coords[j]; // Spherical excess contribution area += (lng2 - lng1) * (2 + Math.sin(lat1) + Math.sin(lat2)); } // Convert to square meters area = Math.abs((area * EARTH_RADIUS * EARTH_RADIUS) / 2); return area; } /** * Alternative: Calculate ring area using planar approximation (faster but less accurate) * Good for small areas where spherical distortion is minimal */ calculateRingAreaPlanar(ring) { if (!ring || ring.length < 4) { return 0; } // Shoelace formula (planar) let area = 0; for (let i = 0; i < ring.length - 1; i++) { const [x1, y1] = ring[i]; const [x2, y2] = ring[i + 1]; area += x1 * y2 - x2 * y1; } // Convert from degrees² to square meters (rough approximation) const avgLat = ring.reduce((sum, [lng, lat]) => sum + lat, 0) / ring.length; const latRadians = this.toRadians(avgLat); // Degrees to meters conversion at this latitude const metersPerDegreeLat = 111320; // Approximately constant const metersPerDegreeLng = 111320 * Math.cos(latRadians); return (Math.abs(area) / 2) * metersPerDegreeLat * metersPerDegreeLng; } /** * Get area with different units */ getAreaInUnit(unit) { const areaInSquareMeters = this.getArea(); switch (unit) { case "square_meters": return areaInSquareMeters; case "square_kilometers": return areaInSquareMeters / 1000000; case "hectares": return areaInSquareMeters / 10000; case "acres": return areaInSquareMeters / 4046.8564224; default: return areaInSquareMeters; } } /** * Get area details with custom unit */ getAreaDetailsInUnit(unit) { const details = this.getAreaDetails(); const conversionFactor = this.getConversionFactor(unit); return { totalArea: details.totalArea / conversionFactor, unit: unit, features: details.features.map((feature) => { var _a; return (Object.assign(Object.assign({}, feature), { area: feature.area / conversionFactor, polygonAreas: (_a = feature.polygonAreas) === null || _a === void 0 ? void 0 : _a.map((area) => area / conversionFactor) })); }), }; } /** * Calculate area of individual feature by index */ getFeatureArea(featureIndex) { this.ensureLoaded(); if (featureIndex < 0 || featureIndex >= this.features.length) { throw new errors_1.ValidationError(`Feature index ${featureIndex} out of range`, "INVALID_FEATURE_INDEX"); } const feature = this.features[featureIndex]; if (feature.type === "Polygon") { return this.calculatePolygonArea(feature.coordinates); } else { return this.calculateMultiPolygonArea(feature.coordinates); } } /** * Get area statistics */ getAreaStatistics() { this.ensureLoaded(); const areaDetails = this.getAreaDetails(); const areas = areaDetails.features.map((f) => f.area); if (areas.length === 0) { throw new errors_1.ValidationError("No features available for statistics", "EMPTY_DATASET"); } areas.sort((a, b) => a - b); const totalArea = areaDetails.totalArea; const averageArea = totalArea / areas.length; // Find largest and smallest features let largestIndex = 0; let smallestIndex = 0; let largestArea = areaDetails.features[0].area; let smallestArea = areaDetails.features[0].area; areaDetails.features.forEach((feature, index) => { if (feature.area > largestArea) { largestArea = feature.area; largestIndex = index; } if (feature.area < smallestArea) { smallestArea = feature.area; smallestIndex = index; } }); // Calculate statistics const median = areas.length % 2 === 0 ? (areas[areas.length / 2 - 1] + areas[areas.length / 2]) / 2 : areas[Math.floor(areas.length / 2)]; const variance = areas.reduce((sum, area) => sum + Math.pow(area - averageArea, 2), 0) / areas.length; const stdDev = Math.sqrt(variance); return { totalArea, averageArea, largestFeature: { index: largestIndex, area: largestArea }, smallestFeature: { index: smallestIndex, area: smallestArea }, areaDistribution: { min: areas[0], max: areas[areas.length - 1], median, stdDev, }, }; } // === EXISTING METHODS === getBoundingBox() { this.ensureLoaded(); if (this.features.length === 0) return null; let minLat = Infinity; let maxLat = -Infinity; let minLng = Infinity; let maxLng = -Infinity; this.features.forEach((feature) => { const bbox = feature.boundingBox; minLat = Math.min(minLat, bbox.minLat); maxLat = Math.max(maxLat, bbox.maxLat); minLng = Math.min(minLng, bbox.minLng); maxLng = Math.max(maxLng, bbox.maxLng); }); return { minLat, maxLat, minLng, maxLng }; } getMidpoint() { this.ensureLoaded(); const bbox = this.getBoundingBox(); if (!bbox) return null; return { latitude: (bbox.minLat + bbox.maxLat) / 2, longitude: (bbox.minLng + bbox.maxLng) / 2, }; } isWithinBoundingBox(lat, lng) { this.ensureLoaded(); if (!this.isValidCoordinate(lat, lng)) { throw new errors_1.ValidationError("Invalid coordinates provided"); } const bbox = this.getBoundingBox(); if (!bbox) return false; return (lat >= bbox.minLat && lat <= bbox.maxLat && lng >= bbox.minLng && lng <= bbox.maxLng); } reset() { this.features = []; this.loaded = false; } // === PRIVATE HELPER METHODS === toRadians(degrees) { return (degrees * Math.PI) / 180; } getConversionFactor(unit) { switch (unit) { case "square_meters": return 1; case "square_kilometers": return 1000000; case "hectares": return 10000; case "acres": return 4046.8564224; default: return 1; } } ensureLoaded() { if (!this.loaded) { throw new errors_1.ValidationError("No spatial data available in geometry calculator", "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.GeometryCalculator = GeometryCalculator;