@mytec: Iteration 4: Critical Fixes start
This commit is contained in:
419
RFCP-Iteration4-Critical-Fixes.md
Normal file
419
RFCP-Iteration4-Critical-Fixes.md
Normal file
@@ -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' && (
|
||||
<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='© OpenStreetMap'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
|
||||
{/* Terrain overlay - ABOVE base map, BELOW heatmap */}
|
||||
{showTerrain && (
|
||||
<TileLayer
|
||||
attribution='© 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!
|
||||
Reference in New Issue
Block a user