@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