Files
rfcp/RFCP-Iteration10.3.2-Fix-Boundary-Rendering.md
2026-01-30 18:53:19 +02:00

5.7 KiB

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
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

cd /opt/rfcp/frontend
npm install @turf/concave @turf/helpers

Step 2: Rewrite computeEdgePath()

Replace the entire computeEdgePath function with:

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

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:

<CoverageBoundary
  points={coverageResult.points.filter(p => 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:

// In computeEdgePath, change:
const cellLat = resolutionM / 111_000;
// To:
const cellLat = (resolutionM / 4) / 111_000;  // 4x finer grid

Reference