Files
rfcp/docs/devlog/front/RFCP-Iteration4-Critical-Fixes.md
2026-01-30 20:39:13 +02:00

420 lines
11 KiB
Markdown

# 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' && (
<Polygon
positions={generateSectorWedge(site)}
pathOptions={{
color: '#ffffff',
weight: 2,
opacity: 0.5,
fillOpacity: 0.1,
dashArray: '5, 5',
}}
/>
)}
// 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 (
<div style={{ opacity }}>
<HeatmapLayer
points={heatmapPoints}
longitudeExtractor={(p) => 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}
/>
</div>
);
}
```
### 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:
<MapContainer {...props}>
{/* Base map layer */}
<TileLayer
attribution='&copy; OpenStreetMap'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* Terrain overlay - ABOVE base map, BELOW heatmap */}
{showTerrain && (
<TileLayer
attribution='&copy; OpenTopoMap'
url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png"
opacity={0.5} // Increased from 0.3 or whatever was there
zIndex={100} // Ensure it's above base map
/>
)}
{/* Heatmap on top */}
<Heatmap
points={coveragePoints}
visible={showHeatmap}
opacity={heatmapOpacity}
/>
{/* Site markers on very top */}
<SiteMarkers sites={sites} />
</MapContainer>
```
### Alternative: Hillshade Layer
If OpenTopoMap is too subtle, try dedicated hillshade:
```typescript
{showTerrain && (
<TileLayer
url="https://tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png"
opacity={0.6}
zIndex={100}
/>
)}
```
### UI Improvements
Add terrain opacity slider:
**File:** `frontend/src/components/panels/CoverageSettings.tsx`
```typescript
<Slider
label="Terrain Opacity"
min={0.1}
max={1.0}
step={0.1}
value={terrainOpacity}
onChange={(value) => 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!