# RFCP - Iteration 4: Critical Fixes ## Issues Found in Production After Iteration 3 deployment, three critical issues identified: 1. ❌ **Antenna directivity not working** - Coverage is omni even for sector antennas 2. ❌ **Heatmap gradient broken** - Everything shows as solid red/orange 3. ❌ **Terrain overlay invisible** - OpenTopoMap layer not visible enough --- ## CRITICAL FIX 1: Antenna Directivity **Problem:** Coverage calculation ignores antenna azimuth and beamwidth. A 75° sector antenna shows full 360° coverage. **Root Cause:** The coverage calculation worker doesn't filter points based on antenna direction. ### Implementation **File:** `frontend/src/workers/coverage.worker.ts` Add directivity filter in the point loop: ```typescript // In calculateCoverage function, after distance calculation: for (let latIdx = 0; latIdx < latPoints; latIdx++) { for (let lonIdx = 0; lonIdx < lonPoints; lonIdx++) { const lat = minLat + latIdx * latStep; const lon = minLon + lonIdx * lonStep; const distance = calculateDistance(site.lat, site.lon, lat, lon); if (distance > radius) continue; // NEW: Check antenna directivity if (site.antennaType === 'sector') { const bearing = calculateBearing(site.lat, site.lon, lat, lon); const azimuth = site.azimuth || 0; const beamwidth = 75; // degrees (could be configurable) // Calculate angular difference let angleDiff = Math.abs(bearing - azimuth); if (angleDiff > 180) angleDiff = 360 - angleDiff; // Skip points outside beamwidth if (angleDiff > beamwidth / 2) continue; } // Calculate RSRP (existing code) const fspl = calculateFSPL(distance, site.frequency); const rsrp = site.power - fspl; // ... rest of code } } // Add helper function: function calculateBearing(lat1: number, lon1: number, lat2: number, lon2: number): number { const φ1 = lat1 * Math.PI / 180; const φ2 = lat2 * Math.PI / 180; const Δλ = (lon2 - lon1) * Math.PI / 180; const y = Math.sin(Δλ) * Math.cos(φ2); const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); let θ = Math.atan2(y, x); θ = θ * 180 / Math.PI; // radians to degrees return (θ + 360) % 360; // normalize to 0-360 } ``` ### Visual Indicator **File:** `frontend/src/components/map/SiteMarkers.tsx` Add sector wedge visualization: ```typescript import { Polygon } from 'react-leaflet'; // In SiteMarker component, for sector antennas: {site.antennaType === 'sector' && ( )} // Helper function: function generateSectorWedge(site: Site): [number, number][] { const points: [number, number][] = [[site.lat, site.lon]]; const radius = 0.5; // km on map (visual only, not coverage radius) const beamwidth = 75; const azimuth = site.azimuth || 0; const startAngle = azimuth - beamwidth / 2; const endAngle = azimuth + beamwidth / 2; // Generate arc points for (let angle = startAngle; angle <= endAngle; angle += 5) { const rad = angle * Math.PI / 180; const latOffset = (radius / 111) * Math.cos(rad); // 1° lat ≈ 111km const lonOffset = (radius / (111 * Math.cos(site.lat * Math.PI / 180))) * Math.sin(rad); points.push([site.lat + latOffset, site.lon + lonOffset]); } points.push([site.lat, site.lon]); // close the wedge return points; } ``` --- ## CRITICAL FIX 2: Heatmap Gradient (Retry) **Problem:** Heatmap shows solid red/orange at all zoom levels despite Iteration 3 fix attempt. **Root Cause:** The fix wasn't applied correctly, or RSRP values are outside expected range. ### Debug First Check actual RSRP values being generated: ```typescript // In Heatmap.tsx, add console.log: console.log('Heatmap Debug:', { pointCount: points.length, rsrpSample: points.slice(0, 5).map(p => p.rsrp), rsrpMin: Math.min(...points.map(p => p.rsrp)), rsrpMax: Math.max(...points.map(p => p.rsrp)), mapZoom: mapZoom, maxIntensity: maxIntensity }); ``` ### Apply Fix (Corrected) **File:** `frontend/src/components/map/Heatmap.tsx` ```typescript export function Heatmap({ points, visible, opacity = 0.7 }: HeatmapProps) { const map = useMap(); const [mapZoom, setMapZoom] = useState(map.getZoom()); useEffect(() => { const handleZoomEnd = () => setMapZoom(map.getZoom()); map.on('zoomend', handleZoomEnd); return () => { map.off('zoomend', handleZoomEnd); }; }, [map]); if (!visible || points.length === 0) return null; // CRITICAL: Normalize RSRP correctly const normalizeRSRP = (rsrp: number): number => { const minRSRP = -120; // Very weak signal const maxRSRP = -70; // Excellent signal (CHANGED from -60) const normalized = (rsrp - minRSRP) / (maxRSRP - minRSRP); return Math.max(0, Math.min(1, normalized)); }; // Dynamic heatmap parameters const radius = Math.max(8, Math.min(40, 50 - mapZoom * 2.5)); const blur = Math.max(6, Math.min(20, 30 - mapZoom * 1.5)); // CRITICAL: Dynamic max intensity to prevent saturation const maxIntensity = Math.max(0.3, Math.min(1.0, 1.2 - mapZoom * 0.05)); // Convert to heatmap format const heatmapPoints = points.map(p => [ p.lat, p.lon, normalizeRSRP(p.rsrp) ] as [number, number, number]); return (
p[1]} latitudeExtractor={(p) => p[0]} intensityExtractor={(p) => p[2]} gradient={{ 0.0: '#0d47a1', // Dark Blue (very weak) 0.2: '#00bcd4', // Cyan (weak) 0.4: '#4caf50', // Green (fair) 0.6: '#ffeb3b', // Yellow (good) 0.8: '#ff9800', // Orange (strong) 1.0: '#f44336', // Red (excellent) }} radius={radius} blur={blur} max={maxIntensity} // DYNAMIC! minOpacity={0.3} />
); } ``` ### If Still Red After Fix The issue might be that ALL points have very strong RSRP (> -70 dBm). Solution: **Option A: Adjust normalization range** ```typescript const minRSRP = -140; // Extend weak end const maxRSRP = -50; // Extend strong end ``` **Option B: Add RSRP clipping in worker** ```typescript // In worker, clip RSRP to realistic range const rsrp = Math.max(-140, Math.min(-50, site.power - fspl)); ``` --- ## CRITICAL FIX 3: Terrain Overlay Visibility **Problem:** OpenTopoMap terrain overlay is barely visible or completely invisible. **Root Causes:** 1. Opacity too low 2. Wrong layer order (under heatmap) 3. Tile URL might be wrong ### Fix Implementation **File:** `frontend/src/components/map/Map.tsx` ```typescript import { TileLayer, LayersControl } from 'react-leaflet'; // In Map component: {/* Base map layer */} {/* Terrain overlay - ABOVE base map, BELOW heatmap */} {showTerrain && ( )} {/* Heatmap on top */} {/* Site markers on very top */} ``` ### Alternative: Hillshade Layer If OpenTopoMap is too subtle, try dedicated hillshade: ```typescript {showTerrain && ( )} ``` ### UI Improvements Add terrain opacity slider: **File:** `frontend/src/components/panels/CoverageSettings.tsx` ```typescript updateSettings({ terrainOpacity: value })} suffix="" help="Adjust terrain layer visibility" disabled={!showTerrain} /> ``` --- ## TESTING CHECKLIST ### Antenna Directivity Test: - [ ] Create sector antenna with azimuth 0° (north) - [ ] Coverage should show wedge facing north only - [ ] Create sector with azimuth 180° (south) - [ ] Coverage wedge should face south - [ ] Omni antenna should still show 360° coverage - [ ] Sector wedge visualization should match coverage area ### Heatmap Gradient Test: - [ ] Zoom level 6-8: Smooth blue→red gradient - [ ] Zoom level 10-12: Colors distinct, no solid blob - [ ] Zoom level 14-16: Full gradient visible, especially blue/cyan at edges - [ ] Console log shows RSRP values in -120 to -70 range - [ ] maxIntensity value changes with zoom (check console) ### Terrain Overlay Test: - [ ] Click Topo button: Contour lines visible - [ ] Terrain overlay shows hills/valleys clearly - [ ] Opacity slider adjusts terrain visibility - [ ] Terrain doesn't obscure heatmap or markers - [ ] Works in both light and dark theme --- ## BUILD & DEPLOY After changes: ```bash cd /opt/rfcp/frontend # Build npm run build # Deploy sudo systemctl reload caddy # Test from VPS curl http://localhost:8888/health curl https://rfcp.eliah.one/ | head -20 # Test from Windows # https://rfcp.eliah.one ``` --- ## COMMIT MESSAGE ``` fix(coverage): implement antenna directivity and sector patterns - Calculate bearing for each coverage point - Filter points outside sector beamwidth (75°) - Add sector wedge visualization on map - Omni antennas maintain 360° coverage fix(heatmap): resolve solid red gradient issue (retry) - Corrected RSRP normalization range (-120 to -70 dBm) - Applied dynamic max intensity based on zoom - Added debug logging for RSRP values - Full blue→red gradient now visible at all zoom levels fix(terrain): improve terrain overlay visibility - Increased opacity from 0.3 to 0.5 - Correct layer ordering (base → terrain → heatmap) - Added terrain opacity slider - Alternative hillshade layer option ``` --- ## Expected Results **After Fix:** 1. **Directivity:** - Sector antenna (75°) shows wedge-shaped coverage - Coverage concentrated in azimuth direction - Visual sector indicator on map 2. **Heatmap:** - Blue at coverage edge (-120 dBm, weak) - Cyan/green in medium range (-100 to -90 dBm) - Yellow/orange closer to site (-85 to -75 dBm) - Red only very close to site (-70 dBm, excellent) 3. **Terrain:** - Contour lines clearly visible - Hills and valleys distinguishable - Doesn't obscure coverage data - Opacity adjustable --- ## Phase 4 Preview (Next) Once these fixes work, Phase 4 will add: - **Terrain Loss Calculation** - Height + elevation profile affects RSRP - **Backend Terrain API** - `/api/terrain/elevation?lat={}&lon={}` - **Line-of-Sight Analysis** - Check if path obstructed by terrain - **3D Terrain Visualization** - Optional 3D view with coverage overlay 🚀 Good luck!