diff --git a/RFCP-Iteration2-Task.md b/RFCP-Iteration2-Task.md new file mode 100644 index 0000000..b5d8330 --- /dev/null +++ b/RFCP-Iteration2-Task.md @@ -0,0 +1,625 @@ +# RFCP - Iteration 2: Terrain Overlay + Heatmap Fixes + Batch Operations + +## Context +Iteration 1 completed successfully (dark theme, new colors, radius 100km, shortcuts). +Now addressing: heatmap pixelation at close zoom, terrain elevation overlay, batch height operations. + +--- + +## CRITICAL FIXES + +### 1. Dynamic Heatmap Radius Based on Zoom Level +**Problem:** At close zoom (12-15), heatmap becomes blocky/pixelated with square artifacts +**Cause:** Fixed radius (25px) doesn't scale with zoom + +**File:** `frontend/src/components/map/Heatmap.tsx` + +**Solution:** +```typescript +import { useEffect, useState } from 'react'; +import { useMap } from 'react-leaflet'; + +export function Heatmap({ points }: HeatmapProps) { + const map = useMap(); + const [mapZoom, setMapZoom] = useState(map.getZoom()); + + // Track zoom changes + useEffect(() => { + const handleZoomEnd = () => { + setMapZoom(map.getZoom()); + }; + + map.on('zoomend', handleZoomEnd); + return () => { + map.off('zoomend', handleZoomEnd); + }; + }, [map]); + + // Calculate adaptive radius and blur based on zoom + // Lower zoom (zoomed out) = larger radius + // Higher zoom (zoomed in) = smaller radius + const getHeatmapParams = (zoom: number) => { + // Zoom 6 (country view): radius=40, blur=20 + // Zoom 10 (regional): radius=28, blur=14 + // Zoom 14 (city): radius=16, blur=10 + // Zoom 18 (street): radius=8, blur=6 + + const radius = Math.max(8, Math.min(40, 50 - zoom * 2.5)); + const blur = Math.max(6, Math.min(20, 30 - zoom * 1.5)); + + return { radius, blur }; + }; + + const { radius, blur } = getHeatmapParams(mapZoom); + + return ( + p[1]} + latitudeExtractor={(p: any) => p[0]} + intensityExtractor={(p: any) => p[2]} + gradient={{ + 0.0: '#0d47a1', + 0.2: '#00bcd4', + 0.4: '#4caf50', + 0.6: '#ffeb3b', + 0.8: '#ff9800', + 1.0: '#f44336', + }} + radius={radius} // ← Dynamic + blur={blur} // ← Dynamic + max={1.0} + minOpacity={0.3} + /> + ); +} +``` + +**Test:** Zoom in close to a site, heatmap should remain smooth, not blocky + +--- + +### 2. Terrain Elevation Overlay πŸ”οΈ +**Feature:** Toggle terrain/topography layer to see elevation while planning + +**Files:** +- `frontend/src/components/map/Map.tsx` - add layer +- `frontend/src/store/settings.ts` - persist toggle state + +**Implementation:** + +**A) Add to settings store:** + +```typescript +// src/store/settings.ts +interface SettingsState { + theme: 'light' | 'dark' | 'system'; + showTerrain: boolean; // ← New + setTheme: (theme: 'light' | 'dark' | 'system') => void; + setShowTerrain: (show: boolean) => void; // ← New +} + +export const useSettingsStore = create()( + persist( + (set) => ({ + theme: 'system', + showTerrain: false, // ← Default off + setTheme: (theme) => { + set({ theme }); + applyTheme(theme); + }, + setShowTerrain: (show) => set({ showTerrain: show }), + }), + { + name: 'rfcp-settings', + } + ) +); +``` + +**B) Add terrain layer to Map:** + +```typescript +// src/components/map/Map.tsx +import { useSettingsStore } from '@/store/settings'; +import { TileLayer } from 'react-leaflet'; + +function Map() { + const showTerrain = useSettingsStore((s) => s.showTerrain); + + return ( + + {/* Base OSM layer */} + + + {/* Terrain overlay (when enabled) */} + {showTerrain && ( + + )} + + {/* Rest of map content */} + + ); +} +``` + +**C) Add toggle button:** + +```typescript +// In Map controls section: +
+ {/* Existing buttons (Fit, Reset) */} + + {/* Terrain toggle */} + +
+``` + +**Alternative terrain sources:** +```typescript +// Option 1: OpenTopoMap (best for Europe, shows contour lines) +url="https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png" + +// Option 2: USGS Topo (USA focused, detailed) +url="https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}" + +// Option 3: Thunderforest Landscape (requires API key but beautiful) +url="https://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png?apikey=YOUR_KEY" + +// Option 4: Stamen Terrain (classic look) +url="https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg" +``` + +**Recommendation:** Start with OpenTopoMap (no API key needed, good for Ukraine) + +--- + +### 3. Batch Operations for Site Height πŸ“Š +**Feature:** Select multiple sites and adjust height together + +**Files:** +- `frontend/src/components/panels/SiteList.tsx` - selection UI +- `frontend/src/components/panels/BatchEdit.tsx` - new component +- `frontend/src/store/sites.ts` - batch update methods + +**Implementation:** + +**A) Update sites store with batch operations:** + +```typescript +// src/store/sites.ts +interface SitesState { + sites: Site[]; + selectedSite: Site | null; + selectedSiteIds: string[]; // ← New for batch selection + placementMode: boolean; + + // Existing methods... + + // New batch methods: + toggleSiteSelection: (siteId: string) => void; + selectAllSites: () => void; + clearSelection: () => void; + batchUpdateHeight: (adjustment: number) => void; + batchSetHeight: (height: number) => void; +} + +export const useSitesStore = create((set, get) => ({ + sites: [], + selectedSite: null, + selectedSiteIds: [], + placementMode: false, + + // ... existing methods ... + + toggleSiteSelection: (siteId) => { + set((state) => { + const isSelected = state.selectedSiteIds.includes(siteId); + return { + selectedSiteIds: isSelected + ? state.selectedSiteIds.filter(id => id !== siteId) + : [...state.selectedSiteIds, siteId] + }; + }); + }, + + selectAllSites: () => { + set((state) => ({ + selectedSiteIds: state.sites.map(s => s.id) + })); + }, + + clearSelection: () => { + set({ selectedSiteIds: [] }); + }, + + batchUpdateHeight: (adjustment) => { + set((state) => { + const selectedIds = new Set(state.selectedSiteIds); + return { + sites: state.sites.map(site => + selectedIds.has(site.id) + ? { ...site, height: Math.max(1, Math.min(100, site.height + adjustment)) } + : site + ) + }; + }); + }, + + batchSetHeight: (height) => { + set((state) => { + const selectedIds = new Set(state.selectedSiteIds); + return { + sites: state.sites.map(site => + selectedIds.has(site.id) + ? { ...site, height: Math.max(1, Math.min(100, height)) } + : site + ) + }; + }); + }, +})); +``` + +**B) Create BatchEdit component:** + +```typescript +// src/components/panels/BatchEdit.tsx +import { useState } from 'react'; +import { useSitesStore } from '@/store/sites'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { toast } from '@/components/ui/Toast'; + +export function BatchEdit() { + const { + selectedSiteIds, + batchUpdateHeight, + batchSetHeight, + clearSelection + } = useSitesStore(); + + const [customHeight, setCustomHeight] = useState(''); + + if (selectedSiteIds.length === 0) return null; + + const handleAdjustHeight = (delta: number) => { + batchUpdateHeight(delta); + toast.success(`Adjusted ${selectedSiteIds.length} site(s) by ${delta > 0 ? '+' : ''}${delta}m`); + }; + + const handleSetHeight = () => { + const height = parseInt(customHeight); + if (isNaN(height) || height < 1 || height > 100) { + toast.error('Height must be between 1-100m'); + return; + } + batchSetHeight(height); + toast.success(`Set ${selectedSiteIds.length} site(s) to ${height}m`); + setCustomHeight(''); + }; + + return ( +
+
+

+ πŸ“Š Batch Edit ({selectedSiteIds.length} selected) +

+ +
+ + {/* Quick adjustments */} +
+ +
+ + + + +
+
+ + {/* Set exact height */} +
+ +
+ setCustomHeight(e.target.value)} + placeholder="meters" + className="flex-1" + /> + +
+
+
+ ); +} +``` + +**C) Update SiteList with checkboxes:** + +```typescript +// src/components/panels/SiteList.tsx +import { useSitesStore } from '@/store/sites'; +import { BatchEdit } from './BatchEdit'; + +export function SiteList() { + const { + sites, + selectedSiteIds, + toggleSiteSelection, + selectAllSites, + clearSelection + } = useSitesStore(); + + const allSelected = sites.length > 0 && selectedSiteIds.length === sites.length; + const someSelected = selectedSiteIds.length > 0 && !allSelected; + + return ( +
+ {/* Header with select all */} +
+

Sites ({sites.length})

+
+ + +
+
+ + {/* Batch edit panel (shown when items selected) */} + + + {/* Sites list */} +
+ {sites.map((site) => { + const isSelected = selectedSiteIds.includes(site.id); + + return ( +
+
+ {/* Checkbox */} + toggleSiteSelection(site.id)} + className="mt-1 w-4 h-4 rounded border-gray-300" + /> + + {/* Color indicator */} +
+ + {/* Site info */} +
+
+ {site.name} +
+
+ πŸ“» {site.frequency} MHz β€’ + ⚑ {site.power} dBm β€’ + πŸ“ {site.height}m β€’ + πŸ“‘ {site.antennaType === 'omni' ? 'Omni' : `Sector ${site.azimuth}Β°`} +
+
+ + {/* Actions */} +
+ + +
+
+
+ ); + })} +
+
+ ); +} +``` + +--- + +## ADDITIONAL IMPROVEMENTS + +### 4. Show Current Elevation on Map Hover (Future) +**Feature:** When hovering mouse, show elevation at cursor position + +*Note: Requires terrain data loaded (Phase 4). Mark as TODO for now.* + +```typescript +// TODO Phase 4: Add cursor elevation display +// When terrain manager is available: +const [cursorElevation, setCursorElevation] = useState(null); + +useEffect(() => { + const handleMouseMove = async (e: L.LeafletMouseEvent) => { + const { lat, lng } = e.latlng; + const elevation = await terrainManager.getElevation(lat, lng); + setCursorElevation(elevation); + }; + + map.on('mousemove', handleMouseMove); + return () => map.off('mousemove', handleMouseMove); +}, [map]); + +// Display in corner: +{cursorElevation !== null && ( +
+ πŸ”οΈ Elevation: {cursorElevation}m +
+)} +``` + +--- + +### 5. Persist Batch Selection Across Panel Close +**Enhancement:** Remember which sites were selected even if user closes/reopens panel + +Already handled by zustand store persistence! + +--- + +## TESTING CHECKLIST + +### Heatmap Zoom Test: +- [ ] Zoom out (level 6-8): heatmap should be smooth, large radius +- [ ] Zoom in (level 12-14): heatmap should remain smooth, smaller radius +- [ ] No blocky/square artifacts at any zoom level + +### Terrain Overlay Test: +- [ ] Toggle terrain on β†’ contour lines visible +- [ ] Toggle terrain off β†’ back to normal OSM +- [ ] Works in both light and dark theme +- [ ] Terrain opacity allows base map to show through +- [ ] Toggle state persists after page refresh + +### Batch Operations Test: +- [ ] Select individual sites with checkboxes +- [ ] "Select All" selects all sites +- [ ] BatchEdit panel appears when sites selected +- [ ] +10m button increases height of selected sites +- [ ] -10m button decreases height (min 1m) +- [ ] Custom height input sets exact height (1-100m validation) +- [ ] Toast notifications show number of sites affected +- [ ] Clear selection removes batch panel +- [ ] Height changes reflected in site list immediately +- [ ] Can still edit individual sites while others selected + +--- + +## FILES TO CREATE/MODIFY + +### New Files: +- `frontend/src/components/panels/BatchEdit.tsx` - batch operations UI + +### Modified Files: +- `frontend/src/components/map/Heatmap.tsx` - dynamic radius/blur +- `frontend/src/components/map/Map.tsx` - terrain overlay toggle +- `frontend/src/components/panels/SiteList.tsx` - checkboxes + batch UI +- `frontend/src/store/settings.ts` - add showTerrain +- `frontend/src/store/sites.ts` - batch operations methods + +--- + +## IMPLEMENTATION ORDER + +1. **Heatmap zoom fix** (5 min) - quick visual improvement +2. **Terrain overlay** (10 min) - new feature, easy to add +3. **Batch operations** (20 min) - more complex, needs store + UI + +**Total time:** ~35 minutes + +--- + +## SUCCESS CRITERIA + +βœ… Heatmap stays smooth at all zoom levels +βœ… Terrain overlay toggle works and persists +βœ… Can select multiple sites and batch-adjust height +βœ… BatchEdit panel is intuitive and responsive +βœ… All operations work in dark mode +βœ… No TypeScript errors +βœ… Toast feedback for all batch operations + +--- + +## NOTES + +**About antenna height in calculations:** +Currently height is stored but not used in FSPL calculations (correct behavior). +Height will matter in Phase 4 when terrain loss is added: +- `terrainLoss = f(txHeight, rxHeight, elevation profile)` +- Higher antenna = better line-of-sight = less terrain loss + +For now, height is cosmetic but will be critical in Phase 4. + +--- + +**About terrain overlay vs terrain data:** +- **Terrain overlay** (this iteration): Visual layer showing topography +- **Terrain data** (Phase 4): 30m SRTM elevation data for calculations + +They're different! Overlay is for user visualization, data is for RF calculations. + +Good luck! πŸš€