@mytec: iter10.3 start
This commit is contained in:
487
RFCP-Iteration10.3-Coverage-Boundary-Border.md
Normal file
487
RFCP-Iteration10.3-Coverage-Boundary-Border.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# RFCP - Iteration 10.3: Coverage Boundary Border
|
||||
|
||||
**Goal:** Replace misleading purple "weak signal" fill with a clean dashed border around actual coverage zone
|
||||
**Priority:** P2 (UX Polish)
|
||||
**Estimated Time:** 30-45 minutes
|
||||
**Status:** Ready for Implementation
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
After Iteration 10.2 implemented purple→orange gradient, user feedback indicated that the purple "weak signal" zone (-100 to -130 dBm) creates a false impression of coverage. This iteration implements a combined approach:
|
||||
|
||||
1. **Solid gradient only for useful signal** (-50 to -100 dBm)
|
||||
2. **Hard cutoff** at -100 dBm (nothing below rendered as fill)
|
||||
3. **Dashed border** around coverage zone edge
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Current Issue
|
||||
|
||||
**User Feedback:** "наче норм, але воно створює враження шо покриття навколо вишки по факту є, оця зона фіолетова"
|
||||
|
||||
**Problem:**
|
||||
- Purple zone (-100 to -130 dBm) looks like "there is coverage"
|
||||
- In reality, this signal is **unusable** (No Service / Very Weak)
|
||||
- Creates false expectations about actual coverage area
|
||||
|
||||
**Visual Issue:**
|
||||
```
|
||||
Current:
|
||||
┌─────────────────────────────┐
|
||||
│ ░░░░░░ Purple fill ░░░░░░░ │ ← Looks like coverage!
|
||||
│ ░░░┌───────────────┐░░░░░░ │
|
||||
│ ░░░│ Orange/Peach │░░░░░░ │
|
||||
│ ░░░│ (useful) │░░░░░░ │
|
||||
│ ░░░└───────────────┘░░░░░░ │
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Solution: Combined Approach
|
||||
|
||||
**New Visual:**
|
||||
```
|
||||
After:
|
||||
┌─ ─ ─ ─ ─ ─ ─┐
|
||||
│ │
|
||||
─ ─ ─┤ 🟠→🟡→🟣 ├─ ─ ─ ← Dashed border at -100 dBm
|
||||
│ gradient │
|
||||
│ (useful) │
|
||||
└─ ─ ─ ─ ─ ─ ─┘
|
||||
|
||||
No fill outside border!
|
||||
```
|
||||
|
||||
**Three Changes:**
|
||||
|
||||
### 1. Change Default Min Signal Threshold
|
||||
- **Old:** -120 dBm (shows purple weak zone)
|
||||
- **New:** -100 dBm (shows only useful coverage)
|
||||
|
||||
### 2. Render Dashed Border at Coverage Edge
|
||||
- Contour line at the Min Signal threshold (-100 dBm)
|
||||
- Dashed stroke style (5px dash, 3px gap)
|
||||
- Color: Dark purple (#4a148c) or dark gray (#666666)
|
||||
- Line width: 2px
|
||||
|
||||
### 3. Update Legend
|
||||
- Remove "No Service" and "Very Weak" from legend
|
||||
- Or mark them as "outside coverage zone"
|
||||
|
||||
---
|
||||
|
||||
## 📁 Implementation Plan
|
||||
|
||||
### Step 1: Change Default Min Signal
|
||||
|
||||
**File:** `src/store/settingsStore.ts` (or similar)
|
||||
|
||||
```typescript
|
||||
// Find default settings
|
||||
const defaultSettings = {
|
||||
minSignal: -100, // Changed from -120
|
||||
// ... other settings
|
||||
};
|
||||
```
|
||||
|
||||
**Or in constants file:**
|
||||
```typescript
|
||||
export const DEFAULT_MIN_SIGNAL = -100; // dBm (was -120)
|
||||
```
|
||||
|
||||
### Step 2: Create Coverage Boundary Component
|
||||
|
||||
**New File:** `src/components/map/CoverageBoundary.tsx`
|
||||
|
||||
```typescript
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useMap } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
|
||||
interface CoverageBoundaryProps {
|
||||
coveragePoints: Array<{ lat: number; lng: number; rsrp: number }>;
|
||||
threshold: number; // e.g., -100 dBm
|
||||
color?: string;
|
||||
dashArray?: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export function CoverageBoundary({
|
||||
coveragePoints,
|
||||
threshold,
|
||||
color = '#4a148c',
|
||||
dashArray = '8, 4',
|
||||
weight = 2,
|
||||
}: CoverageBoundaryProps) {
|
||||
const map = useMap();
|
||||
const layerRef = useRef<L.Layer | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!coveragePoints.length) return;
|
||||
|
||||
// Find boundary points (points near threshold)
|
||||
const boundaryPoints = findBoundaryPoints(coveragePoints, threshold);
|
||||
|
||||
// Create contour path
|
||||
const contourPath = createContourPath(boundaryPoints);
|
||||
|
||||
// Remove old layer
|
||||
if (layerRef.current) {
|
||||
map.removeLayer(layerRef.current);
|
||||
}
|
||||
|
||||
// Add new boundary layer
|
||||
if (contourPath.length > 0) {
|
||||
const polyline = L.polyline(contourPath, {
|
||||
color,
|
||||
weight,
|
||||
dashArray,
|
||||
opacity: 0.8,
|
||||
fill: false,
|
||||
});
|
||||
|
||||
polyline.addTo(map);
|
||||
layerRef.current = polyline;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (layerRef.current) {
|
||||
map.removeLayer(layerRef.current);
|
||||
}
|
||||
};
|
||||
}, [coveragePoints, threshold, color, dashArray, weight, map]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper: Find points near the threshold boundary
|
||||
function findBoundaryPoints(
|
||||
points: Array<{ lat: number; lng: number; rsrp: number }>,
|
||||
threshold: number,
|
||||
tolerance: number = 3 // dBm
|
||||
): Array<{ lat: number; lng: number }> {
|
||||
return points
|
||||
.filter(p => Math.abs(p.rsrp - threshold) <= tolerance)
|
||||
.map(p => ({ lat: p.lat, lng: p.lng }));
|
||||
}
|
||||
|
||||
// Helper: Create contour path from boundary points
|
||||
function createContourPath(
|
||||
points: Array<{ lat: number; lng: number }>
|
||||
): L.LatLngExpression[][] {
|
||||
if (points.length < 3) return [];
|
||||
|
||||
// Use convex hull or alpha shape algorithm
|
||||
// For simplicity, start with convex hull
|
||||
const hull = convexHull(points);
|
||||
|
||||
return [hull.map(p => [p.lat, p.lng] as L.LatLngExpression)];
|
||||
}
|
||||
|
||||
// Convex Hull algorithm (Graham scan)
|
||||
function convexHull(points: Array<{ lat: number; lng: number }>) {
|
||||
if (points.length < 3) return points;
|
||||
|
||||
// Sort by lat, then lng
|
||||
const sorted = [...points].sort((a, b) =>
|
||||
a.lat === b.lat ? a.lng - b.lng : a.lat - b.lat
|
||||
);
|
||||
|
||||
// Build lower hull
|
||||
const lower: typeof points = [];
|
||||
for (const p of sorted) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
||||
lower.pop();
|
||||
}
|
||||
lower.push(p);
|
||||
}
|
||||
|
||||
// Build upper hull
|
||||
const upper: typeof points = [];
|
||||
for (const p of sorted.reverse()) {
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(p);
|
||||
}
|
||||
|
||||
// Remove last point of each half (it's repeated)
|
||||
lower.pop();
|
||||
upper.pop();
|
||||
|
||||
return [...lower, ...upper];
|
||||
}
|
||||
|
||||
function cross(o: { lat: number; lng: number }, a: { lat: number; lng: number }, b: { lat: number; lng: number }) {
|
||||
return (a.lat - o.lat) * (b.lng - o.lng) - (a.lng - o.lng) * (b.lat - o.lat);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Alternative - Simpler Canvas-Based Approach
|
||||
|
||||
If the component approach is too complex, modify existing heatmap renderer:
|
||||
|
||||
**File:** `src/components/map/GeographicHeatmap.tsx` (or similar)
|
||||
|
||||
```typescript
|
||||
// After rendering heatmap tiles, add border stroke
|
||||
|
||||
function renderCoverageBorder(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
points: CoveragePoint[],
|
||||
threshold: number,
|
||||
bounds: L.LatLngBounds,
|
||||
tileSize: number
|
||||
) {
|
||||
const boundaryPoints = points.filter(p =>
|
||||
Math.abs(p.rsrp - threshold) <= 3
|
||||
);
|
||||
|
||||
if (boundaryPoints.length < 3) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#4a148c';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.setLineDash([8, 4]);
|
||||
|
||||
// Convert to pixel coordinates and draw
|
||||
const hull = convexHull(boundaryPoints);
|
||||
|
||||
hull.forEach((point, i) => {
|
||||
const pixel = latLngToPixel(point, bounds, tileSize);
|
||||
if (i === 0) {
|
||||
ctx.moveTo(pixel.x, pixel.y);
|
||||
} else {
|
||||
ctx.lineTo(pixel.x, pixel.y);
|
||||
}
|
||||
});
|
||||
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Update Legend
|
||||
|
||||
**File:** `src/constants/rsrp-thresholds.ts`
|
||||
|
||||
```typescript
|
||||
// Update legend to reflect new display
|
||||
export const RSRP_LEGEND = [
|
||||
{ min: -50, max: -70, label: 'Excellent', color: '#ffb74d' },
|
||||
{ min: -70, max: -85, label: 'Strong', color: '#ff9800' },
|
||||
{ min: -85, max: -95, label: 'Good', color: '#ff6f00' },
|
||||
{ min: -95, max: -100, label: 'Fair', color: '#ff8a65' },
|
||||
// Remove or gray out:
|
||||
// { min: -100, max: -110, label: 'Weak', color: '#ab47bc' },
|
||||
// { min: -110, max: -130, label: 'No Service', color: '#4a148c' },
|
||||
];
|
||||
|
||||
// Or add visual indicator that these are "outside coverage"
|
||||
export const RSRP_LEGEND_EXTENDED = [
|
||||
// ... useful signal levels ...
|
||||
{ min: -100, max: -130, label: 'Outside Coverage', color: 'transparent', border: '#4a148c' },
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Investigation Commands
|
||||
|
||||
```bash
|
||||
# Find settings store
|
||||
find /opt/rfcp/frontend/src -name "*store*" -o -name "*settings*" | head -10
|
||||
|
||||
# Find current min signal default
|
||||
grep -rn "minSignal\|-120\|MIN_SIGNAL" /opt/rfcp/frontend/src/
|
||||
|
||||
# Find heatmap component
|
||||
find /opt/rfcp/frontend/src -name "*eatmap*" -o -name "*overage*"
|
||||
|
||||
# Find legend component
|
||||
grep -rn "RSRP_LEGEND\|legend" /opt/rfcp/frontend/src/
|
||||
|
||||
# Check current threshold constants
|
||||
cat /opt/rfcp/frontend/src/constants/rsrp-thresholds.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Checklist
|
||||
|
||||
### Visual Testing
|
||||
|
||||
- [ ] **No purple fill:** Weak signal zone (-100 to -130) not filled
|
||||
- [ ] **Gradient visible:** Orange gradient shows useful coverage (-50 to -100)
|
||||
- [ ] **Border visible:** Dashed line around coverage edge
|
||||
- [ ] **Border follows contour:** Line traces actual coverage boundary
|
||||
- [ ] **Clean appearance:** No visual artifacts or gaps in border
|
||||
|
||||
### Functional Testing
|
||||
|
||||
- [ ] **Min Signal slider:** Still works, updates border position
|
||||
- [ ] **Zoom levels:** Border looks correct at zoom 8, 12, 16
|
||||
- [ ] **Multiple sites:** Each site has its own border
|
||||
- [ ] **Sector coverage:** Border follows sector wedge shape
|
||||
- [ ] **Grid mode:** Works correctly with new boundary
|
||||
|
||||
### Regression Testing
|
||||
|
||||
- [ ] **50m resolution:** Still works without crash
|
||||
- [ ] **Performance:** No significant slowdown
|
||||
- [ ] **Legend:** Displays correctly
|
||||
- [ ] **All other features:** Keyboard shortcuts, delete, etc.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- [ ] **Single point:** Graceful handling (no border or minimal)
|
||||
- [ ] **Overlapping sites:** Borders don't conflict
|
||||
- [ ] **Min Signal = -50:** Very small coverage, border still visible
|
||||
- [ ] **Min Signal = -130:** Large coverage (original behavior if user wants)
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Build & Deploy
|
||||
|
||||
```bash
|
||||
# Navigate to frontend
|
||||
cd /opt/rfcp/frontend
|
||||
|
||||
# Development test
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Expected: 0 TypeScript errors, 0 ESLint errors
|
||||
|
||||
# Deploy
|
||||
sudo systemctl reload caddy
|
||||
|
||||
# Test
|
||||
# Open https://rfcp.eliah.one and verify:
|
||||
# 1. No purple fill
|
||||
# 2. Dashed border around coverage
|
||||
# 3. Orange gradient inside border
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Commit Message
|
||||
|
||||
```
|
||||
feat(heatmap): add dashed coverage boundary, remove weak signal fill
|
||||
|
||||
- Changed default Min Signal from -120 to -100 dBm
|
||||
- Added dashed border around coverage zone edge
|
||||
- Removed misleading purple "weak signal" fill
|
||||
- Border uses convex hull algorithm for smooth contour
|
||||
- Color: dark purple (#4a148c), style: dashed (8px, 4px gap)
|
||||
|
||||
Visual change: coverage zone now shows only useful signal
|
||||
with clear boundary instead of gradual fade to purple.
|
||||
|
||||
Iteration: 10.3
|
||||
Fixes: User feedback about misleading coverage display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
**Must Pass:**
|
||||
1. ✅ No purple/lavender fill outside useful coverage
|
||||
2. ✅ Dashed border visible around coverage zone
|
||||
3. ✅ Orange gradient shows useful signal (-50 to -100 dBm)
|
||||
4. ✅ Border follows actual coverage shape (including sectors)
|
||||
5. ✅ TypeScript: 0 errors
|
||||
6. ✅ ESLint: 0 errors
|
||||
7. ✅ Performance: No significant slowdown
|
||||
|
||||
**User Acceptance:**
|
||||
- Олег confirms: "тепер видно реальне покриття" or similar
|
||||
- If border looks wrong → Iteration 10.3.1 to adjust algorithm
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Visual Reference
|
||||
|
||||
**Before (10.2):**
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ ▓▓▓▓▓ Purple fade ▓▓▓▓▓▓▓▓ │
|
||||
│ ▓▓┌─────────────────┐▓▓▓▓▓ │
|
||||
│ ▓▓│ Orange/Peach │▓▓▓▓▓ │
|
||||
│ ▓▓│ gradient │▓▓▓▓▓ │
|
||||
│ ▓▓└─────────────────┘▓▓▓▓▓ │
|
||||
│ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
**After (10.3):**
|
||||
```
|
||||
╭─ ─ ─ ─ ─ ─ ─ ─ ─╮
|
||||
│ │
|
||||
─ ─ ─┤ Orange/Peach ├─ ─ ─
|
||||
│ gradient │
|
||||
│ (clear map │
|
||||
│ visible here) │
|
||||
╰─ ─ ─ ─ ─ ─ ─ ─ ─╯
|
||||
|
||||
↑ Dashed border
|
||||
↑ No fill outside!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Instructions for Claude Code
|
||||
|
||||
**Context:**
|
||||
- Project: RFCP (RF Coverage Planning) - `/opt/rfcp/frontend/`
|
||||
- Framework: React 18 + TypeScript + Vite + Leaflet
|
||||
- Current state: Purple→Orange gradient working (Iteration 10.2)
|
||||
|
||||
**Implementation Priority:**
|
||||
|
||||
1. **First:** Change default Min Signal to -100 dBm
|
||||
- Find settings store or constants
|
||||
- Update default value
|
||||
- This alone will hide most purple
|
||||
|
||||
2. **Second:** Add coverage boundary border
|
||||
- Create new component or modify heatmap renderer
|
||||
- Use convex hull for boundary detection
|
||||
- Render dashed polyline
|
||||
|
||||
3. **Third:** Update legend if needed
|
||||
- Remove or gray out "Weak" and "No Service" entries
|
||||
|
||||
**Algorithm Notes:**
|
||||
- Boundary detection: Find points where RSRP ≈ threshold (±3 dBm)
|
||||
- Contour: Use convex hull for simplicity, or marching squares for precision
|
||||
- Performance: Cache boundary, recalculate only when coverage changes
|
||||
|
||||
**Success:** User sees clean coverage zone with dashed border, no misleading purple fill
|
||||
|
||||
---
|
||||
|
||||
## 📊 Complexity Assessment
|
||||
|
||||
| Component | Complexity | Time |
|
||||
|-----------|------------|------|
|
||||
| Change Min Signal default | Easy | 5 min |
|
||||
| Boundary detection algorithm | Medium | 15 min |
|
||||
| Dashed border rendering | Medium | 15 min |
|
||||
| Integration & testing | Easy | 10 min |
|
||||
| **Total** | **Medium** | **~45 min** |
|
||||
|
||||
---
|
||||
|
||||
**Document Created:** 2025-01-30
|
||||
**Author:** Claude (Opus 4.5) + Олег
|
||||
**Status:** Ready for Implementation
|
||||
**Next:** Віддати Claude Code → Test → Screenshot → Confirm
|
||||
Reference in New Issue
Block a user