# 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(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