# 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! πŸš€