# RFCP Iteration 10.3.2 — Fix Coverage Boundary Rendering **Date:** 2025-01-30 **Status:** Ready for Implementation **Priority:** High **Estimated Effort:** 30-45 minutes --- ## Problem Statement CoverageBoundary component renders but is nearly invisible: - Boundary polygon is only **25x25 pixels** instead of covering the entire coverage area - Edge detection algorithm returns only **~11 points** instead of hundreds - Path exists in DOM with correct styling but wrong geometry ### Debug Evidence ``` [CoverageBoundary] Computing: {visible: true, pointsCount: 9132, resolution: 200} [CoverageBoundary] Paths: 1 ``` ```javascript document.querySelectorAll('.leaflet-overlay-pane path')[1].getBoundingClientRect() // → DOMRect {width: 25, height: 25} // Should be ~500x500 or larger! // Path has only 11 vertices: // M780 422L774 422L768 416L768 402L773 397L787 397L793 403L793 415L791 419L786 422L780 422 ``` --- ## Root Cause Analysis In `/opt/rfcp/frontend/src/components/map/CoverageBoundary.tsx`, function `computeEdgePath()`: 1. **Grid cell size too coarse:** - Cell size = `resolution` (200m) converted to degrees - With 9132 points spread over coverage area, most cells have neighbors - Only ~11 cells on the very edge are detected as "boundary" 2. **Angular sorting from centroid fails for sector shapes:** - Coverage is a sector (wedge), not a circle - Sorting by angle from centroid produces zigzag paths for concave shapes 3. **Single representative point per cell:** - Algorithm picks one point per grid cell - Loses boundary detail when cells are large --- ## Solution: Use Turf.js Concave Hull Replace custom edge detection with `@turf/concave` — purpose-built for this exact use case. ### Why Turf.js? - **Concave hull** follows actual shape (sectors, irregular coverage) - **Battle-tested** library used in GIS applications - **Configurable** maxEdge parameter controls detail level - **Fast** — optimized for thousands of points --- ## Implementation Plan ### Step 1: Install Dependencies ```bash cd /opt/rfcp/frontend npm install @turf/concave @turf/helpers ``` ### Step 2: Rewrite computeEdgePath() Replace the entire `computeEdgePath` function with: ```typescript import concave from '@turf/concave'; import { featureCollection, point } from '@turf/helpers'; /** * Compute concave hull boundary for coverage points. * Uses Turf.js concave hull algorithm (alpha shape). * * @param pts - Coverage points for one site * @param resolutionM - Resolution in meters, used to set maxEdge * @returns Ordered boundary coordinates for Leaflet polyline */ function computeEdgePath( pts: CoveragePoint[], resolutionM: number ): L.LatLngExpression[] { if (pts.length < 3) return []; // Convert to GeoJSON points const features = pts.map(p => point([p.lon, p.lat])); const fc = featureCollection(features); // Compute concave hull // maxEdge in kilometers — use resolution * 3 for good detail const maxEdge = (resolutionM * 3) / 1000; try { const hull = concave(fc, { maxEdge, units: 'kilometers' }); if (!hull || hull.geometry.type !== 'Polygon') { console.warn('[CoverageBoundary] Concave hull failed, falling back to convex'); return []; } // Extract coordinates (GeoJSON is [lon, lat], Leaflet needs [lat, lon]) const coords = hull.geometry.coordinates[0]; return coords.map(([lon, lat]) => [lat, lon] as L.LatLngExpression); } catch (error) { console.error('[CoverageBoundary] Hull computation error:', error); return []; } } ``` ### Step 3: Update Imports at Top of File ```typescript import { useEffect, useRef, useMemo } from 'react'; import { useMap } from 'react-leaflet'; import L from 'leaflet'; import concave from '@turf/concave'; import { featureCollection, point } from '@turf/helpers'; import type { CoveragePoint } from '@/types/index.ts'; ``` ### Step 4: Filter Points by Threshold **Important:** Currently CoverageBoundary receives ALL points, but heatmap filters by `rsrpThreshold`. Boundary should match heatmap edge. In `App.tsx`, filter points before passing: ```tsx p.rsrp >= settings.rsrpThreshold)} visible={heatmapVisible} resolution={settings.resolution} /> ``` ### Step 5: Remove Debug Logging After confirming fix works, remove the `console.log` statements added during debugging. --- ## Files to Modify | File | Changes | |------|---------| | `package.json` | Add @turf/concave, @turf/helpers | | `src/components/map/CoverageBoundary.tsx` | Replace computeEdgePath with Turf.js implementation | | `src/App.tsx` | Filter coverage points by rsrpThreshold | --- ## Expected Results ### Before (Current Bug) - Boundary: 25x25 pixels, ~11 vertices - Not visible at normal zoom ### After (Fixed) - Boundary: Follows heatmap edge closely - Purple dashed line (#7c3aed) visible around orange gradient - Updates when Min Signal slider changes - Properly handles sector shapes (wedges, not just circles) --- ## Testing Checklist - [ ] Boundary visible around coverage area - [ ] Boundary follows actual coverage shape (sector/wedge) - [ ] Boundary updates when resolution changes - [ ] Boundary updates when Min Signal threshold changes - [ ] No console errors - [ ] Performance acceptable (< 100ms for 10k points) --- ## Rollback Plan If Turf.js causes issues, revert to previous edge detection but with smaller grid cells: ```typescript // In computeEdgePath, change: const cellLat = resolutionM / 111_000; // To: const cellLat = (resolutionM / 4) / 111_000; // 4x finer grid ``` --- ## Reference - Turf.js concave: https://turfjs.org/docs/#concave - Leaflet polyline: https://leafletjs.com/reference.html#polyline - Previous iteration: RFCP-Iteration10.3.1-Threshold-Filter-Fix.md