From 201aeeabd6d26ed5af8d2d901ebfeae54750532b Mon Sep 17 00:00:00 2001 From: mytec Date: Fri, 30 Jan 2026 11:45:27 +0200 Subject: [PATCH] @mytec: Iteration 4: Critical Fixes start --- RFCP-Iteration4-Critical-Fixes.md | 419 ++++++++++++++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 RFCP-Iteration4-Critical-Fixes.md diff --git a/RFCP-Iteration4-Critical-Fixes.md b/RFCP-Iteration4-Critical-Fixes.md new file mode 100644 index 0000000..2be2ef2 --- /dev/null +++ b/RFCP-Iteration4-Critical-Fixes.md @@ -0,0 +1,419 @@ +# 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!