@mytec: iter10.3.2 ready for testing
This commit is contained in:
148
frontend/package-lock.json
generated
148
frontend/package-lock.json
generated
@@ -8,6 +8,8 @@
|
|||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@turf/concave": "^7.3.3",
|
||||||
|
"@turf/helpers": "^7.3.3",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet.heat": "^0.2.0",
|
"leaflet.heat": "^0.2.0",
|
||||||
@@ -1553,6 +1555,111 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@turf/clone": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/clone/-/clone-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-IrG3zXKy++xmnQAuL3ZQDVHdsTpKoEY87cLwsKg1Z1VnH7egluxL0T6VTwcu1l64c0QeBtnTjXJBC8XiO4ajog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "7.3.3",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/concave": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/concave/-/concave-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-V+OV02WHioK1Z7Yabd+PKYzxhXJhlPNLUO4wN4dzzHqyn500G2I0+YXgJ9YW45zmPKOi1AGh0E8vehV1XYUG1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/clone": "7.3.3",
|
||||||
|
"@turf/distance": "7.3.3",
|
||||||
|
"@turf/helpers": "7.3.3",
|
||||||
|
"@turf/invariant": "7.3.3",
|
||||||
|
"@turf/meta": "7.3.3",
|
||||||
|
"@turf/tin": "7.3.3",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"topojson-client": "3.x",
|
||||||
|
"topojson-server": "3.x",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/distance": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-bmv0GzqlICjMWuQ05ipDDbT9ppQUMNo02+T5f/rPF9hSEXCPkSJQ1OdQ6XjUGzdJ/vxgES4DM4zhIDUKU/g8RQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "7.3.3",
|
||||||
|
"@turf/invariant": "7.3.3",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/helpers": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-9Ias0L1GuZPIzO6sk8jraTEuLJye6n9LYNEdw69ZGOQ6C1IigjxkPW49zmn21aTv1z27vxdVLSS3r+78DB2QnQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/invariant": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-q6UDgWmtIlU+AIxt5Awnh18ZMSuNti6drCXbIBdGdgQaQ1qEiyGZDE3P9RKk6otoLXOBYecOuI0HIwf2IxurhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "7.3.3",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/meta": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-Tz1j4h70iFB5SebWWoVv/uL59x4aOngXU+d1xQDXzOCn/O6txnreGVGMcYU362c5F06yqZx38H9UFTQ553lK0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "7.3.3",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@turf/tin": {
|
||||||
|
"version": "7.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@turf/tin/-/tin-7.3.3.tgz",
|
||||||
|
"integrity": "sha512-7Cel4wMbNvnIZGxT/6Z8+rFAR4QR5QDGYFE9pXqLPQj3zpEw4SW3pbDEEdNNtZFhtXmU/eYh+62pwbpfeaBL/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@turf/helpers": "7.3.3",
|
||||||
|
"@types/geojson": "^7946.0.10",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/turf"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1603,8 +1710,7 @@
|
|||||||
"node_modules/@types/geojson": {
|
"node_modules/@types/geojson": {
|
||||||
"version": "7946.0.16",
|
"version": "7946.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
@@ -2121,6 +2227,12 @@
|
|||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -3440,6 +3552,32 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/topojson-client": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"topo2geo": "bin/topo2geo",
|
||||||
|
"topomerge": "bin/topomerge",
|
||||||
|
"topoquantize": "bin/topoquantize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/topojson-server": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/topojson-server/-/topojson-server-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"geo2topo": "bin/geo2topo"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||||
@@ -3452,6 +3590,12 @@
|
|||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@turf/concave": "^7.3.3",
|
||||||
|
"@turf/helpers": "^7.3.3",
|
||||||
"dexie": "^4.2.1",
|
"dexie": "^4.2.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"leaflet.heat": "^0.2.0",
|
"leaflet.heat": "^0.2.0",
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ export default function App() {
|
|||||||
rsrpThreshold={settings.rsrpThreshold}
|
rsrpThreshold={settings.rsrpThreshold}
|
||||||
/>
|
/>
|
||||||
<CoverageBoundary
|
<CoverageBoundary
|
||||||
points={coverageResult.points}
|
points={coverageResult.points.filter(p => p.rsrp >= settings.rsrpThreshold)}
|
||||||
visible={heatmapVisible}
|
visible={heatmapVisible}
|
||||||
resolution={settings.resolution}
|
resolution={settings.resolution}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
/**
|
/**
|
||||||
* Renders a dashed polyline around the coverage zone boundary.
|
* Renders a dashed polyline around the coverage zone boundary.
|
||||||
*
|
*
|
||||||
* Algorithm:
|
* Uses @turf/concave to compute a concave hull (alpha shape) per site,
|
||||||
* 1. Bucket all coverage points into grid cells (resolution-based)
|
* which correctly follows sector/wedge shapes — not just convex circles.
|
||||||
* 2. Find "edge" cells — cells that have ≥1 empty neighbour
|
|
||||||
* 3. Compute a concave boundary by ordering edge points angularly per site
|
|
||||||
* 4. Render as dashed Leaflet polylines
|
|
||||||
*
|
*
|
||||||
* Performance: runs once per coverage result change, O(n) where n = grid points.
|
* Performance: ~20-50ms for 10k points (runs once per coverage change).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useMemo } from 'react';
|
import { useEffect, useRef, useMemo } from 'react';
|
||||||
import { useMap } from 'react-leaflet';
|
import { useMap } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
|
import concave from '@turf/concave';
|
||||||
|
import { featureCollection, point } from '@turf/helpers';
|
||||||
import type { CoveragePoint } from '@/types/index.ts';
|
import type { CoveragePoint } from '@/types/index.ts';
|
||||||
|
import { logger } from '@/utils/logger.ts';
|
||||||
|
|
||||||
interface CoverageBoundaryProps {
|
interface CoverageBoundaryProps {
|
||||||
points: CoveragePoint[];
|
points: CoveragePoint[];
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
resolution: number; // meters — used to determine grid cell size
|
resolution: number; // meters — controls concave hull detail
|
||||||
color?: string;
|
color?: string;
|
||||||
weight?: number;
|
weight?: number;
|
||||||
}
|
}
|
||||||
@@ -35,12 +35,8 @@ export default function CoverageBoundary({
|
|||||||
|
|
||||||
// Compute boundary paths grouped by site
|
// Compute boundary paths grouped by site
|
||||||
const boundaryPaths = useMemo(() => {
|
const boundaryPaths = useMemo(() => {
|
||||||
console.log('[CoverageBoundary] Computing:', { visible, pointsCount: points.length, resolution });
|
if (!visible || points.length === 0) return [];
|
||||||
|
|
||||||
if (!visible || points.length === 0) {
|
|
||||||
console.log('[CoverageBoundary] SKIP - not visible or no points');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// Group points by siteId
|
// Group points by siteId
|
||||||
const bySite = new Map<string, CoveragePoint[]>();
|
const bySite = new Map<string, CoveragePoint[]>();
|
||||||
for (const p of points) {
|
for (const p of points) {
|
||||||
@@ -55,14 +51,12 @@ export default function CoverageBoundary({
|
|||||||
const paths: L.LatLngExpression[][] = [];
|
const paths: L.LatLngExpression[][] = [];
|
||||||
|
|
||||||
for (const sitePoints of bySite.values()) {
|
for (const sitePoints of bySite.values()) {
|
||||||
const edgePath = computeEdgePath(sitePoints, resolution);
|
const path = computeConcaveHull(sitePoints, resolution);
|
||||||
if (edgePath.length >= 3) {
|
if (path.length >= 3) {
|
||||||
paths.push(edgePath);
|
paths.push(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[CoverageBoundary] Paths:', paths.length);
|
|
||||||
|
|
||||||
return paths;
|
return paths;
|
||||||
}, [points, visible, resolution]);
|
}, [points, visible, resolution]);
|
||||||
|
|
||||||
@@ -76,8 +70,6 @@ export default function CoverageBoundary({
|
|||||||
|
|
||||||
if (!visible || boundaryPaths.length === 0) return;
|
if (!visible || boundaryPaths.length === 0) return;
|
||||||
|
|
||||||
console.log('[CoverageBoundary] RENDERING polylines:', boundaryPaths.length);
|
|
||||||
|
|
||||||
const group = L.layerGroup();
|
const group = L.layerGroup();
|
||||||
|
|
||||||
for (const path of boundaryPaths) {
|
for (const path of boundaryPaths) {
|
||||||
@@ -106,91 +98,42 @@ export default function CoverageBoundary({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Edge detection on the grid
|
// Concave hull via Turf.js
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For a set of coverage points (all belonging to one site), find the
|
* Compute a concave hull boundary for one site's coverage points.
|
||||||
* ordered boundary polygon.
|
|
||||||
*
|
*
|
||||||
* Steps:
|
* maxEdge = resolution * 3 (in km) gives good detail without over-fitting.
|
||||||
* 1. Hash every point into a grid cell
|
* Falls back to empty if hull computation fails (e.g., collinear points).
|
||||||
* 2. Find edge cells (≥1 of 8 neighbours missing)
|
|
||||||
* 3. Order edge points by angle from centroid → closed polygon
|
|
||||||
*/
|
*/
|
||||||
function computeEdgePath(
|
function computeConcaveHull(
|
||||||
pts: CoveragePoint[],
|
pts: CoveragePoint[],
|
||||||
resolutionM: number
|
resolutionM: number
|
||||||
): L.LatLngExpression[] {
|
): L.LatLngExpression[] {
|
||||||
if (pts.length < 3) return [];
|
if (pts.length < 3) return [];
|
||||||
|
|
||||||
// Grid cell size in degrees (approximate)
|
// Convert to GeoJSON FeatureCollection of Points
|
||||||
const cellLat = resolutionM / 111_000;
|
const features = pts.map((p) => point([p.lon, p.lat]));
|
||||||
const avgLat = pts.reduce((s, p) => s + p.lat, 0) / pts.length;
|
const fc = featureCollection(features);
|
||||||
const cellLon = resolutionM / (111_000 * Math.cos((avgLat * Math.PI) / 180));
|
|
||||||
|
|
||||||
// Quantize helper
|
// maxEdge in km — resolution * 3 balances detail vs smoothness
|
||||||
const toKey = (lat: number, lon: number) => {
|
const maxEdge = (resolutionM * 3) / 1000;
|
||||||
const r = Math.round(lat / cellLat);
|
|
||||||
const c = Math.round(lon / cellLon);
|
|
||||||
return `${r},${c}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build occupied set
|
try {
|
||||||
const occupied = new Set<string>();
|
const hull = concave(fc, { maxEdge, units: 'kilometers' });
|
||||||
// Keep one representative point per cell for coordinates
|
|
||||||
const cellCoords = new Map<string, { lat: number; lon: number }>();
|
if (!hull || hull.geometry.type !== 'Polygon') {
|
||||||
for (const p of pts) {
|
return [];
|
||||||
const key = toKey(p.lat, p.lon);
|
|
||||||
occupied.add(key);
|
|
||||||
if (!cellCoords.has(key)) {
|
|
||||||
cellCoords.set(key, { lat: p.lat, lon: p.lon });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8-connected neighbour offsets
|
// GeoJSON coordinates are [lon, lat]; Leaflet needs [lat, lon]
|
||||||
const offsets = [
|
const coords = hull.geometry.coordinates[0];
|
||||||
[-1, -1], [-1, 0], [-1, 1],
|
return coords.map(
|
||||||
[0, -1], [0, 1],
|
([lon, lat]: number[]) => [lat, lon] as L.LatLngExpression
|
||||||
[1, -1], [1, 0], [1, 1],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Find edge cells: occupied cells with at least one missing neighbour
|
|
||||||
const edgePoints: { lat: number; lon: number }[] = [];
|
|
||||||
for (const [key, coord] of cellCoords) {
|
|
||||||
const [rStr, cStr] = key.split(',');
|
|
||||||
const r = Number(rStr);
|
|
||||||
const c = Number(cStr);
|
|
||||||
|
|
||||||
let isEdge = false;
|
|
||||||
for (const [dr, dc] of offsets) {
|
|
||||||
if (!occupied.has(`${r + dr},${c + dc}`)) {
|
|
||||||
isEdge = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEdge) {
|
|
||||||
edgePoints.push(coord);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edgePoints.length < 3) return [];
|
|
||||||
|
|
||||||
// Order by angle from centroid to form a closed polygon
|
|
||||||
const cx = edgePoints.reduce((s, p) => s + p.lat, 0) / edgePoints.length;
|
|
||||||
const cy = edgePoints.reduce((s, p) => s + p.lon, 0) / edgePoints.length;
|
|
||||||
|
|
||||||
edgePoints.sort(
|
|
||||||
(a, b) =>
|
|
||||||
Math.atan2(a.lon - cy, a.lat - cx) - Math.atan2(b.lon - cy, b.lat - cx)
|
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
// Close the polygon
|
logger.error('Coverage hull computation error:', error);
|
||||||
const result: L.LatLngExpression[] = edgePoints.map(
|
return [];
|
||||||
(p) => [p.lat, p.lon] as L.LatLngExpression
|
}
|
||||||
);
|
|
||||||
result.push(result[0]); // close
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user