@mytec: iter10.3.1 start
This commit is contained in:
320
RFCP-Iteration10.3.1-Threshold-Filter-Fix.md
Normal file
320
RFCP-Iteration10.3.1-Threshold-Filter-Fix.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user