@mytec: iter7.3 start
This commit is contained in:
292
RFCP-Iteration7.3-Geographic-Scale.md
Normal file
292
RFCP-Iteration7.3-Geographic-Scale.md
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
# RFCP - Iteration 7.3: Geographic Scale Compensation
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
|
||||||
|
**Previous fix failed because:**
|
||||||
|
- `radius` is in **pixels**
|
||||||
|
- But coverage is in **kilometers**
|
||||||
|
- At zoom 8: 1km = ~100px
|
||||||
|
- At zoom 14: 1km = ~6400px
|
||||||
|
- Same `radius=30px` covers VERY different geographic areas!
|
||||||
|
|
||||||
|
## The REAL Solution
|
||||||
|
|
||||||
|
We need to keep **geographic radius constant**, not pixel radius!
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/map/Heatmap.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useMap } from 'react-leaflet';
|
||||||
|
import { HeatmapLayerFactory } from '@vgrid/react-leaflet-heatmap-layer';
|
||||||
|
|
||||||
|
const HeatmapLayer = HeatmapLayerFactory<[number, number, number]>();
|
||||||
|
|
||||||
|
interface HeatmapProps {
|
||||||
|
points: Array<{ lat: number; lon: number; rsrp: number; siteId: string }>;
|
||||||
|
visible: boolean;
|
||||||
|
opacity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// RSRP normalization
|
||||||
|
const normalizeRSRP = (rsrp: number): number => {
|
||||||
|
const minRSRP = -130;
|
||||||
|
const maxRSRP = -50;
|
||||||
|
const normalized = (rsrp - minRSRP) / (maxRSRP - minRSRP);
|
||||||
|
return Math.max(0, Math.min(1, normalized));
|
||||||
|
};
|
||||||
|
|
||||||
|
// CRITICAL FIX: Calculate pixels per kilometer at current zoom
|
||||||
|
// Leaflet formula: pixelsPerKm = 2^zoom * 256 / (40075 * cos(lat))
|
||||||
|
// At equator (simplified): pixelsPerKm ≈ 2^zoom * 6.4
|
||||||
|
const pixelsPerKm = Math.pow(2, mapZoom) * 6.4;
|
||||||
|
|
||||||
|
// Target: 300m geographic radius (coverage point spacing)
|
||||||
|
const targetRadiusKm = 0.3; // 300 meters
|
||||||
|
const radiusPixels = targetRadiusKm * pixelsPerKm;
|
||||||
|
|
||||||
|
// Clamp for visual quality
|
||||||
|
const radius = Math.max(8, Math.min(60, radiusPixels));
|
||||||
|
|
||||||
|
// Blur proportional to radius
|
||||||
|
const blur = radius * 0.6; // 60% of radius
|
||||||
|
|
||||||
|
// FIXED maxIntensity (no compensation needed now!)
|
||||||
|
const maxIntensity = 0.75;
|
||||||
|
|
||||||
|
const heatmapPoints = points.map(p => [
|
||||||
|
p.lat,
|
||||||
|
p.lon,
|
||||||
|
normalizeRSRP(p.rsrp)
|
||||||
|
] as [number, number, number]);
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
if (import.meta.env.DEV && points.length > 0) {
|
||||||
|
const rsrpValues = points.map(p => p.rsrp);
|
||||||
|
console.log('🔍 Heatmap Geographic:', {
|
||||||
|
zoom: mapZoom,
|
||||||
|
pixelsPerKm: pixelsPerKm.toFixed(1),
|
||||||
|
targetRadiusKm,
|
||||||
|
radiusPixels: radiusPixels.toFixed(1),
|
||||||
|
radiusClamped: radius.toFixed(1),
|
||||||
|
blur: blur.toFixed(1),
|
||||||
|
maxIntensity,
|
||||||
|
points: points.length,
|
||||||
|
rsrpRange: `${Math.min(...rsrpValues).toFixed(1)} to ${Math.max(...rsrpValues).toFixed(1)} dBm`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ opacity }}>
|
||||||
|
<HeatmapLayer
|
||||||
|
points={heatmapPoints}
|
||||||
|
longitudeExtractor={(p) => p[1]}
|
||||||
|
latitudeExtractor={(p) => p[0]}
|
||||||
|
intensityExtractor={(p) => p[2]}
|
||||||
|
gradient={{
|
||||||
|
0.0: '#1a237e',
|
||||||
|
0.15: '#0d47a1',
|
||||||
|
0.25: '#2196f3',
|
||||||
|
0.35: '#00bcd4',
|
||||||
|
0.45: '#00897b',
|
||||||
|
0.55: '#4caf50',
|
||||||
|
0.65: '#8bc34a',
|
||||||
|
0.75: '#ffeb3b',
|
||||||
|
0.85: '#ff9800',
|
||||||
|
1.0: '#f44336',
|
||||||
|
}}
|
||||||
|
radius={radius}
|
||||||
|
blur={blur}
|
||||||
|
max={maxIntensity} // CONSTANT! No more compensation!
|
||||||
|
minOpacity={0.3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Works
|
||||||
|
|
||||||
|
**Geographic consistency:**
|
||||||
|
- Same 300m radius covers same geographic area at ALL zooms
|
||||||
|
- Pixel radius auto-adjusts: zoom 8 = 20px, zoom 14 = 1920px
|
||||||
|
- Colors stay consistent because geographic coverage is consistent!
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
Zoom 8: pixelsPerKm = 1638 → radius = 0.3 * 1638 = 491px (clamped to 60)
|
||||||
|
Zoom 10: pixelsPerKm = 6553 → radius = 0.3 * 6553 = 1966px (clamped to 60)
|
||||||
|
Zoom 14: pixelsPerKm = 104857 → radius = 0.3 * 104857 = 31457px (clamped to 60)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wait, problem!** At high zoom, radius gets clamped to 60px which is TOO SMALL!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Better Approach: Remove Clamp
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Don't clamp! Let radius grow with zoom
|
||||||
|
const radius = targetRadiusKm * pixelsPerKm;
|
||||||
|
const blur = radius * 0.6;
|
||||||
|
|
||||||
|
// But limit max for performance
|
||||||
|
const radius = Math.min(radiusPixels, 100); // Max 100px
|
||||||
|
const blur = Math.min(radius * 0.6, 60);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Even Better: Adaptive Target Radius
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Smaller geographic radius at high zoom (more detail)
|
||||||
|
// Larger geographic radius at low zoom (smooth coverage)
|
||||||
|
const targetRadiusKm = mapZoom < 10 ? 0.5 : 0.3; // km
|
||||||
|
const radiusPixels = targetRadiusKm * pixelsPerKm;
|
||||||
|
const radius = Math.max(15, Math.min(50, radiusPixels));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternative: Fix Resolution Instead
|
||||||
|
|
||||||
|
**Problem:** 200m resolution creates visible grid at high zoom.
|
||||||
|
|
||||||
|
**Solution:** Adaptive resolution based on zoom.
|
||||||
|
|
||||||
|
**File:** `frontend/src/store/coverage.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const calculateCoverage = async () => {
|
||||||
|
// Adaptive resolution: finer at close zoom
|
||||||
|
const adaptiveResolution = mapZoom >= 12
|
||||||
|
? 100 // 100m for close zoom
|
||||||
|
: 200; // 200m for far zoom
|
||||||
|
|
||||||
|
await coverageWorker.calculateCoverage({
|
||||||
|
sites,
|
||||||
|
radius,
|
||||||
|
resolution: adaptiveResolution,
|
||||||
|
rsrpThreshold
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ultimate Solution: Multi-Resolution
|
||||||
|
|
||||||
|
Calculate different resolutions for different zoom levels:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Calculate 3 datasets
|
||||||
|
const coarse = calculate(resolution: 500); // Zoom 6-9
|
||||||
|
const medium = calculate(resolution: 200); // Zoom 10-13
|
||||||
|
const fine = calculate(resolution: 100); // Zoom 14+
|
||||||
|
|
||||||
|
// Show appropriate one based on zoom
|
||||||
|
const pointsToShow = mapZoom < 10 ? coarse
|
||||||
|
: mapZoom < 14 ? medium
|
||||||
|
: fine;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Fix: Just Increase Radius at High Zoom
|
||||||
|
|
||||||
|
**File:** `frontend/src/components/map/Heatmap.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simple progressive formula
|
||||||
|
const radius = mapZoom < 10
|
||||||
|
? Math.max(20, Math.min(45, 55 - mapZoom * 2.5)) // Small zoom: old formula
|
||||||
|
: Math.max(30, Math.min(80, 10 + mapZoom * 3)); // Large zoom: grow radius
|
||||||
|
|
||||||
|
const blur = radius * 0.6;
|
||||||
|
const maxIntensity = 0.75; // Keep constant
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## My Recommendation
|
||||||
|
|
||||||
|
**Try Option 1 first (geographic scale)** with adjusted clamps:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const pixelsPerKm = Math.pow(2, mapZoom) * 6.4;
|
||||||
|
const targetRadiusKm = 0.4; // 400m
|
||||||
|
const radiusPixels = targetRadiusKm * pixelsPerKm;
|
||||||
|
|
||||||
|
// Progressive clamps
|
||||||
|
const minRadius = mapZoom < 10 ? 15 : 30;
|
||||||
|
const maxRadius = mapZoom < 10 ? 45 : 80;
|
||||||
|
const radius = Math.max(minRadius, Math.min(maxRadius, radiusPixels));
|
||||||
|
|
||||||
|
const blur = radius * 0.6;
|
||||||
|
const maxIntensity = 0.75;
|
||||||
|
```
|
||||||
|
|
||||||
|
**If that fails, try Option 2 (adaptive resolution).**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
1. **Coverage extent:**
|
||||||
|
- [ ] Zoom 8: Coverage reaches expected radius (e.g., 10km)
|
||||||
|
- [ ] Zoom 12: Coverage still reaches same distance
|
||||||
|
- [ ] No "shrinking" at any zoom
|
||||||
|
|
||||||
|
2. **Grid visibility:**
|
||||||
|
- [ ] Zoom 16+: No visible dots/grid pattern
|
||||||
|
- [ ] Smooth gradient even at close zoom
|
||||||
|
|
||||||
|
3. **Color consistency:**
|
||||||
|
- [ ] Pick point at 5km from site
|
||||||
|
- [ ] Check color at zoom 8, 12, 16
|
||||||
|
- [ ] Should be within 1-2 gradient stops
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build & Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/rfcp/frontend
|
||||||
|
npm run build
|
||||||
|
sudo systemctl reload caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Message
|
||||||
|
|
||||||
|
```
|
||||||
|
fix(heatmap): geographic scale-aware radius calculation
|
||||||
|
|
||||||
|
- Calculate pixels per km based on zoom level
|
||||||
|
- Keep geographic radius constant (400m)
|
||||||
|
- Adjust pixel radius to maintain geographic coverage
|
||||||
|
- Progressive clamps for visual quality at all zooms
|
||||||
|
- Fixed maxIntensity at 0.75 (no compensation needed)
|
||||||
|
|
||||||
|
Coverage extent now consistent across zoom levels.
|
||||||
|
No visible grid pattern at high zoom.
|
||||||
|
Colors remain stable due to consistent geographic scale.
|
||||||
|
```
|
||||||
|
|
||||||
|
🚀 Ready for Iteration 7.3!
|
||||||
Reference in New Issue
Block a user