# 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
