321 lines
8.8 KiB
Markdown
321 lines
8.8 KiB
Markdown
# 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:**
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
{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
|
|
|
|
```bash
|
|
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:**
|
|
1. ✅ No purple/lavender fill when Min Signal = -100 dBm
|
|
2. ✅ Heatmap updates when Min Signal slider moves
|
|
3. ✅ Only orange gradient visible (excellent → fair signal)
|
|
4. ✅ Dashed boundary aligns with heatmap edge
|
|
5. ✅ TypeScript: 0 errors
|
|
6. ✅ 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.ts` doesn't filter by threshold
|
|
|
|
**Implementation Order:**
|
|
1. `HeatmapTileRenderer.ts` — add threshold property + setter + filter
|
|
2. `GeographicHeatmap.tsx` — add prop, pass to renderer, update hash
|
|
3. `App.tsx` — pass `settings.rsrpThreshold` to 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)
|