8.8 KiB
RFCP - Iteration 10.3.1: Heatmap Threshold Filter Fix
Goal: Fix purple "weak signal" still rendering by adding RSRP threshold filtering to heatmap renderer
Priority: P1 (Bug Fix)
Estimated Time: 15-20 minutes
Status: Ready for Implementation
📋 Overview
Iteration 10.3 added coverage boundary and changed default Min Signal to -100 dBm, but the heatmap tile renderer still draws all points including those below threshold. This creates the misleading purple fill that should have been removed.
Root Cause: HeatmapTileRenderer.ts filters points only by geographic bounds, not by RSRP threshold.
🐛 Current Bug
Symptom: Purple/lavender fill visible outside the "useful" coverage zone despite Min Signal = -100 dBm
Location: src/components/map/HeatmapTileRenderer.ts lines 100-107
Current Code:
// Filter relevant points
const relevant = points.filter(
(p) =>
p.lat >= latMin - bufferDeg &&
p.lat <= latMax + bufferDeg &&
p.lon >= lonMin - bufferDeg &&
p.lon <= lonMax + bufferDeg
);
Problem: No RSRP threshold check — all points render regardless of signal strength.
✅ Solution
Add rsrpThreshold parameter to the rendering pipeline and filter out weak signals.
Step 1: Update HeatmapTileRenderer.ts
File: src/components/map/HeatmapTileRenderer.ts
1.1 Add threshold to class state
// Around line 35, add new property:
export class HeatmapTileRenderer {
private tileSize = 256;
private radiusMeters: number;
private rsrpThreshold: number; // NEW
// LRU cache...
constructor(radiusMeters = 400, maxCacheSize = 150, rsrpThreshold = -100) {
this.radiusMeters = radiusMeters;
this.maxCacheSize = maxCacheSize;
this.rsrpThreshold = rsrpThreshold; // NEW
}
1.2 Add setter for threshold
// After setRadiusMeters(), add:
/** Update the RSRP threshold - points below this are not rendered. */
setRsrpThreshold(threshold: number): void {
if (threshold !== this.rsrpThreshold) {
this.rsrpThreshold = threshold;
this.clearCache(); // Important: invalidate cache when threshold changes
}
}
1.3 Update renderTile() filter
// Around line 100, update the filter:
// Filter relevant points - geographic bounds AND RSRP threshold
const relevant = points.filter(
(p) =>
p.rsrp >= this.rsrpThreshold && // 🔥 NEW: skip weak signals
p.lat >= latMin - bufferDeg &&
p.lat <= latMax + bufferDeg &&
p.lon >= lonMin - bufferDeg &&
p.lon <= lonMax + bufferDeg
);
Step 2: Update GeographicHeatmap.tsx
File: src/components/map/GeographicHeatmap.tsx
2.1 Add threshold prop
interface GeographicHeatmapProps {
points: HeatmapPoint[];
visible: boolean;
opacity?: number;
radiusMeters?: number;
rsrpThreshold?: number; // NEW
}
export default function GeographicHeatmap({
points,
visible,
opacity = 0.7,
radiusMeters = 400,
rsrpThreshold = -100, // NEW
}: GeographicHeatmapProps) {
2.2 Update renderer when threshold changes
// After the useEffect for radiusMeters, add:
// Update renderer threshold when prop changes
useEffect(() => {
rendererRef.current.setRsrpThreshold(rsrpThreshold);
}, [rsrpThreshold]);
2.3 Include threshold in cache invalidation
// Update the pointsHash useEffect to include threshold:
useEffect(() => {
if (points.length === 0) {
rendererRef.current.setPointsHash('empty');
return;
}
const first = points[0];
const last = points[points.length - 1];
// Include threshold in hash so cache invalidates when it changes
const hash = `${points.length}:${first.lat.toFixed(4)},${first.lon.toFixed(4)}:${last.rsrp}:${rsrpThreshold}`;
rendererRef.current.setPointsHash(hash);
}, [points, rsrpThreshold]);
Step 3: Update App.tsx
File: src/App.tsx
Pass threshold from settings to GeographicHeatmap:
{coverageResult && (
<>
<GeographicHeatmap
points={coverageResult.points}
visible={heatmapVisible}
opacity={settings.heatmapOpacity}
radiusMeters={settings.heatmapRadius}
rsrpThreshold={settings.rsrpThreshold} // NEW
/>
<CoverageBoundary
points={coverageResult.points}
visible={heatmapVisible}
resolution={settings.resolution}
/>
</>
)}
📁 Files to Modify
| File | Changes |
|---|---|
src/components/map/HeatmapTileRenderer.ts |
Add rsrpThreshold property, setter, filter in renderTile() |
src/components/map/GeographicHeatmap.tsx |
Add rsrpThreshold prop, pass to renderer, update cache hash |
src/App.tsx |
Pass settings.rsrpThreshold to GeographicHeatmap |
Total: 3 files, ~20 lines changed
🧪 Testing Checklist
Visual Testing
- No purple fill: Only orange gradient visible (no purple/lavender outside)
- Threshold respected: Changing Min Signal slider updates heatmap immediately
- Boundary matches: Dashed border aligns with heatmap edge
- Different thresholds: Test -90, -100, -110 — heatmap adjusts accordingly
Functional Testing
- Slider reactive: Moving Min Signal slider updates heatmap without "Calculate Coverage"
- Cache invalidation: Threshold change clears old tiles, renders new ones
- Zoom levels: Works at zoom 8, 12, 16
- Multiple sites: Each site respects threshold
Regression Testing
- 50m resolution: Still works without crash
- Performance: No significant slowdown
- Boundary component: Still renders correctly
- Legend: Still shows correct colors and threshold indicator
🏗️ Build & Deploy
cd /opt/rfcp/frontend
# Build
npm run build
# Expected: 0 TypeScript errors, 0 ESLint errors
# Deploy
sudo systemctl reload caddy
# Test at https://rfcp.eliah.one
# 1. Set Min Signal to -100 dBm
# 2. Verify NO purple visible outside orange zone
# 3. Move slider to -90 — coverage zone shrinks
# 4. Move slider to -110 — coverage zone expands (some purple appears)
📝 Commit Message
fix(heatmap): filter points by RSRP threshold to remove purple fill
- Added rsrpThreshold property to HeatmapTileRenderer
- Filter points below threshold before rendering tiles
- Cache invalidates when threshold changes
- Passed threshold from settings through GeographicHeatmap
Points below Min Signal threshold are now completely hidden,
not just dimmed. This removes the misleading purple "weak signal"
fill that made it look like coverage existed where it doesn't.
Iteration: 10.3.1
Fixes: Purple fill still visible after 10.3
✅ Success Criteria
Must Pass:
- ✅ No purple/lavender fill when Min Signal = -100 dBm
- ✅ Heatmap updates when Min Signal slider moves
- ✅ Only orange gradient visible (excellent → fair signal)
- ✅ Dashed boundary aligns with heatmap edge
- ✅ TypeScript: 0 errors
- ✅ ESLint: 0 errors
User Acceptance:
- Олег confirms: "фіолету нема, тільки оранжевий градієнт"
🎨 Expected Visual Result
Before (10.3):
┌─────────────────────────────┐
│ ▓▓▓ Purple fill ▓▓▓▓▓▓▓▓▓▓ │ ← Still visible!
│ ▓▓┌─ ─ ─ ─ ─ ─ ─ ─ ─┐▓▓▓▓ │
│ ▓▓│ Orange gradient │▓▓▓▓ │
│ ▓▓└─ ─ ─ ─ ─ ─ ─ ─ ─┘▓▓▓▓ │
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
└─────────────────────────────┘
After (10.3.1):
┌─ ─ ─ ─ ─ ─ ─┐
│ │
─ ─ ─┤ 🟠 Orange ├─ ─ ─ ← Dashed border
│ gradient │
│ (no purple │
│ outside!) │
└─ ─ ─ ─ ─ ─ ─┘
🤖 Instructions for Claude Code
Context:
- Project: RFCP -
/opt/rfcp/frontend/ - Bug: Purple fill still renders despite Min Signal = -100 dBm
- Root cause:
HeatmapTileRenderer.tsdoesn't filter by threshold
Implementation Order:
HeatmapTileRenderer.ts— add threshold property + setter + filterGeographicHeatmap.tsx— add prop, pass to renderer, update hashApp.tsx— passsettings.rsrpThresholdto component
Key Points:
- Threshold setter MUST call
clearCache()to invalidate tiles - Include threshold in pointsHash for proper cache invalidation
- Default threshold = -100 dBm (matches store default)
Success: User sees only orange gradient, no purple fill outside coverage zone
Document Created: 2025-01-30
Author: Claude (Opus 4.5) + Олег
Status: Ready for Implementation
Depends On: Iteration 10.3 (boundary component)