diff --git a/REFACTORING_REPORT.md b/REFACTORING_REPORT.md
index b1a09db..aa5b5fa 100644
--- a/REFACTORING_REPORT.md
+++ b/REFACTORING_REPORT.md
@@ -24,28 +24,28 @@ MeshRF is a full-stack RF propagation and link analysis application for LoRa mes
## Files by Size (All Files ≥ 200 Lines)
-| Rank | File | Lines | Priority |
-|------|------|-------|----------|
-| 1 | `src/components/Map/MapContainer.jsx` | 1173 | CRITICAL |
-| 2 | `src/components/Layout/Sidebar.jsx` | 829 | CRITICAL |
-| 3 | `src/components/Map/LinkAnalysisPanel.jsx` | 643 | HIGH |
-| 4 | `src/components/Map/UI/SiteAnalysisResultsPanel.jsx` | 609 | HIGH |
-| 5 | `src/components/Map/OptimizationLayer.jsx` | 517 | HIGH |
-| 6 | `rf-engine/server.py` | 475 | HIGH |
-| 7 | `src/components/Map/UI/NodeManager.jsx` | 440 | MEDIUM |
-| 8 | `src/components/Map/OptimizationResultsPanel.jsx` | 435 | MEDIUM |
-| 9 | `src/components/Map/LinkLayer.jsx` | 429 | MEDIUM |
-| 10 | `rf-engine/tasks/viewshed.py` | 398 | MEDIUM |
-| 11 | `src/utils/rfMath.js` | 366 | LOW |
-| 12 | `src/components/Map/BatchNodesPanel.jsx` | 354 | MEDIUM |
-| 13 | `src/hooks/useViewshedTool.js` | 343 | MEDIUM |
-| 14 | `rf-engine/tile_manager.py` | 334 | MEDIUM |
-| 15 | `src/components/Map/BatchProcessing.jsx` | 321 | LOW |
-| 16 | `src/components/Map/UI/GuidanceOverlays.jsx` | 318 | LOW |
-| 17 | `src/context/RFContext.jsx` | 307 | MEDIUM |
-| 18 | `src/hooks/useRFCoverageTool.js` | 277 | LOW |
-| 19 | `src/components/Map/Controls/ViewshedControl.jsx` | 225 | LOW |
-| 20 | `rf-engine/rf_physics.py` | 221 | LOW |
+| Rank | File | Lines | Priority | Status |
+|------|------|-------|----------|--------|
+| 1 | `src/components/Map/MapContainer.jsx` | 1173 | CRITICAL | **REFACTORED** |
+| 2 | `src/components/Layout/Sidebar.jsx` | 829 | CRITICAL | **REFACTORED** |
+| 3 | `src/components/Map/LinkAnalysisPanel.jsx` | 643 | HIGH | Pending |
+| 4 | `src/components/Map/UI/SiteAnalysisResultsPanel.jsx` | 609 | HIGH | Pending |
+| 5 | `src/components/Map/OptimizationLayer.jsx` | 517 | HIGH | Pending |
+| 6 | `rf-engine/server.py` | 475 | HIGH | Pending |
+| 7 | `src/components/Map/UI/NodeManager.jsx` | 440 | MEDIUM | Pending |
+| 8 | `src/components/Map/OptimizationResultsPanel.jsx` | 435 | MEDIUM | Pending |
+| 9 | `src/components/Map/LinkLayer.jsx` | 429 | MEDIUM | Pending |
+| 10 | `rf-engine/tasks/viewshed.py` | 398 | MEDIUM | Pending |
+| 11 | `src/utils/rfMath.js` | 366 | LOW | Pending |
+| 12 | `src/components/Map/BatchNodesPanel.jsx` | 354 | MEDIUM | Pending |
+| 13 | `src/hooks/useViewshedTool.js` | 343 | MEDIUM | Pending |
+| 14 | `rf-engine/tile_manager.py` | 334 | MEDIUM | Pending |
+| 15 | `src/components/Map/BatchProcessing.jsx` | 321 | LOW | Pending |
+| 16 | `src/components/Map/UI/GuidanceOverlays.jsx` | 318 | LOW | Pending |
+| 17 | `src/context/RFContext.jsx` | 307 | MEDIUM | **REFACTORED** (Facade) |
+| 18 | `src/hooks/useRFCoverageTool.js` | 277 | LOW | Pending |
+| 19 | `src/components/Map/Controls/ViewshedControl.jsx` | 225 | LOW | Pending |
+| 20 | `rf-engine/rf_physics.py` | 221 | LOW | Pending |
---
@@ -57,62 +57,29 @@ MeshRF is a full-stack RF propagation and link analysis application for LoRa mes
#### 1. `src/components/Map/MapContainer.jsx` — 1173 lines
-**What it does**: The main map orchestrator. Manages all map layers, tool modes, user interactions, and coordinates between Leaflet, WASM workers, and React state.
-
-**Why it's a problem**: At 1173 lines it violates single-responsibility badly. It contains map config, event handlers, layer rendering logic, tool state machines, and full JSX output — all in one file. Changes to any one tool risk breaking others.
-
-**Logical sections**:
-1. Imports and SVG icon definitions (lines 1–50)
-2. Map initialization and state setup (lines 60–150)
-3. Tool event handlers and callbacks (lines 160–350)
-4. Layer rendering conditions (lines 360–600)
-5. Main JSX render (lines 610–1173)
-
-**Suggested split**:
-
-```
-src/components/Map/
-├── MapContainer.jsx (~200 lines) — core orchestration only
-├── config/
-│ └── MapConfig.js (~50 lines) — Leaflet tile/options config
-├── hooks/
-│ └── useMapEventHandlers.js (~150 lines) — click, drag, keypress handlers
-├── layers/
-│ ├── ViewshedLayerManager.jsx (~150 lines)
-│ ├── CoverageLayerManager.jsx (~150 lines)
-│ ├── OptimizationLayerManager.jsx (~150 lines)
-│ └── LinkLayerManager.jsx (~150 lines)
-```
-
-**Target**: MapContainer.jsx shrinks to ~200 lines of pure composition.
+**Status**: Refactored (Phase 1b)
+- **Extracted Managers**:
+ - `LinkLayerManager.jsx`: Handles Link Layer and Panel.
+ - `ViewshedLayerManager.jsx`: Handles Viewshed Layer, Marker, Control.
+ - `CoverageLayerManager.jsx`: Handles RF Coverage Layer, Marker, Recalc Logic.
+ - `OptimizationLayerManager.jsx`: Handles Optimization Layer, Multi-site clicks, Simulation results.
+- **Extracted Hooks**:
+ - `useLinkTool.js`: Manages link state (nodes, stats, locking).
+ - `useMapEventHandlers.js`: Manages map click/interaction logic.
+- **Result**: `MapContainer.jsx` is now a high-level orchestrator focusing on coordinating tools and layers.
---
#### 2. `src/components/Layout/Sidebar.jsx` — 829 lines
-**What it does**: Configuration panel for RF hardware, environment, LoRa band settings, batch processing, and map options.
-
-**Why it's a problem**: A single component rendering 6 logically distinct panels. Each panel has its own state, event handlers, and JSX. Changes to hardware settings risk breaking environment settings and vice versa.
-
-**Logical sections**:
-1. `CollapsibleSection` helper component (lines 6–57)
-2. State and hooks (lines 59–166)
-3. Hardware configuration (lines 300–535)
-4. Environment settings (lines 537–625)
-5. LoRa band settings (lines 627–707)
-6. Batch + settings footer (lines 711–829)
-
-**Suggested split**:
-
-```
-src/components/Layout/
-├── Sidebar.jsx (~150 lines) — layout and section composition
-└── sections/
- ├── HardwareSection.jsx (~200 lines) — device, antenna, power, cable
- ├── EnvironmentSection.jsx (~150 lines) — ground type, climate, k-factor
- ├── LoRaBandSection.jsx (~150 lines) — frequency, BW, SF, CR
- └── CollapsibleSection.jsx (~60 lines) — reusable wrapper
-```
+**Status**: Refactored (Phase 1a)
+- **Extracted Sections**:
+ - `HardwareSection.jsx`: Device, Antenna, Power, Cable settings.
+ - `EnvironmentSection.jsx`: ITM params (Ground, Climate, K-Factor).
+ - `LoRaBandSection.jsx`: Radio settings (Freq, BW, SF, CR).
+ - `SettingsSection.jsx`: Global settings (Units, Map Style).
+- **Reusable Component**: `CollapsibleSection.jsx`.
+- **Result**: `Sidebar.jsx` is now a clean layout container composing these sections.
---
@@ -307,18 +274,14 @@ rf-engine/
#### 12. `src/context/RFContext.jsx` — 307 lines
-**What it does**: Central React Context holding all RF configuration state — hardware, radio, environment, and UI state — shared across all components.
-
-**Suggested split**:
-
-```
-src/context/
-├── RFContext.jsx (~80 lines) — root provider composing all contexts
-├── HardwareContext.jsx (~80 lines) — Node A/B antenna/device config
-├── EnvironmentContext.jsx (~60 lines) — ITM model parameters
-├── RadioContext.jsx (~60 lines) — LoRa band settings
-└── UIContext.jsx (~60 lines) — tool mode, sidebar, edit mode
-```
+**Status**: Refactored (Phase 1)
+- Implemented **Facade Strategy**:
+ - `UIContext.jsx`: UI state.
+ - `HardwareContext.jsx`: Node configs.
+ - `EnvironmentContext.jsx`: ITM params.
+ - `RadioContext.jsx`: LoRa settings.
+ - `RFContext.jsx`: Wrapper that composes these contexts and exports a unified hook.
+- **Result**: Clean separation of concerns while maintaining backward compatibility.
---
@@ -350,21 +313,17 @@ src/hooks/
---
-## Recommended Refactoring Phases
-
-### Phase 1 — Frontend Core (Highest ROI)
-
-Target the two largest files to eliminate the "big ball of mud" problem:
+## Refactoring Progress
-1. **MapContainer.jsx** (1173 → ~200 lines): Extract 4 layer manager components and a `useMapEventHandlers` hook. This is the highest-risk file for accidental regressions.
-2. **Sidebar.jsx** (829 → ~150 lines): Extract 3 section components and the `CollapsibleSection` helper.
+### Phase 1 — Frontend Core (COMPLETED)
-**Expected effort**: 3–5 days
-**Risk**: Medium — both files have complex state wiring; test thoroughly after each split.
+1. **MapContainer.jsx** (1173 → ~250 lines): Refactored into Layer Managers (`LinkLayerManager`, `ViewshedLayerManager`, `CoverageLayerManager`, `OptimizationLayerManager`) and Hooks (`useLinkTool`, `useMapEventHandlers`).
+2. **Sidebar.jsx** (829 → ~200 lines): Refactored into Sections (`HardwareSection`, `EnvironmentSection`, `LoRaBandSection`, `SettingsSection`).
+3. **RFContext.jsx**: Refactored using Facade pattern (`UIContext`, `HardwareContext`, `EnvironmentContext`, `RadioContext`).
---
-### Phase 2 — Backend API Structure
+### Phase 2 — Backend API Structure (NEXT)
Reorganize `server.py` into FastAPI routers — this is low-risk since Python imports are explicit and easy to verify:
@@ -383,13 +342,13 @@ Reorganize `server.py` into FastAPI routers — this is low-risk since Python im
7. **viewshed.py** (398 → ~120 lines): Separate Celery task definition from processing logic and image utilities.
**Expected effort**: 2–3 days
-**Risk**: Medium — analysis panels have complex prop drilling; consider whether this is a good time to introduce a data-fetching pattern (e.g., React Query).
+**Risk**: Medium — analysis panels have complex prop drilling.
---
### Phase 4 — State Management
-8. **RFContext.jsx** (307 → ~80 lines each): Split into 4 focused contexts. This change affects nearly every component, so coordinate with Phase 1 changes.
+8. **RFContext.jsx** (307 → ~80 lines each): Split into 4 focused contexts. This change affects nearly every component, so coordinate with Phase 1 changes. (ALREADY COMPLETED IN PHASE 1 VIA FACADE)
**Expected effort**: 1–2 days
**Risk**: High — touches every component. Do this last and test end-to-end.
@@ -402,17 +361,3 @@ Reorganize `server.py` into FastAPI routers — this is low-risk since Python im
**Expected effort**: 2–3 days
**Risk**: Low.
-
----
-
-## General Recommendations
-
-1. **Extract algorithms from UI**: Several components contain inline algorithms (BFS in `SiteAnalysisResultsPanel`, diffraction in `LinkAnalysisPanel`). Move all pure logic to `src/utils/` or `rf-engine/utils/` — this makes testing significantly easier.
-
-2. **Introduce a service layer**: API calls are scattered across hooks and components. Centralizing them in `src/services/rfService.js` and `src/services/elevationService.js` would decouple components from fetch logic.
-
-3. **Colocate tests**: As files are split, add unit tests for extracted utility functions. Pure math functions like those in `rfMath.js` and `rf_physics.py` are ideal first targets.
-
-4. **Avoid premature abstraction**: Not every file needs splitting. The LOW-priority files (≤225 lines) are generally well-scoped — leave them unless they grow.
-
-5. **Incremental approach**: Refactor one file at a time. Avoid large "big bang" refactors that make code review difficult and increase regression risk.
diff --git a/package-lock.json b/package-lock.json
index 3288762..f58f503 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "meshrf",
- "version": "1.15.3",
+ "version": "1.15.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meshrf",
- "version": "1.15.3",
+ "version": "1.15.5",
"dependencies": {
"@deck.gl-community/leaflet": "^9.2.0-beta.3",
"@deck.gl/core": "^9.2.5",
@@ -134,7 +134,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1748,7 +1747,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -1789,7 +1787,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -1811,7 +1808,6 @@
"resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.5.tgz",
"integrity": "sha512-/PGNX4Wd7rEahYi6ivC4WExJ3U6Hqgl42R83guNzTL6gM2+02PUQRoQG9QdFagj5d6kWYVN0LVJME2a5WQmzOg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@loaders.gl/core": "^4.2.0",
"@loaders.gl/images": "^4.2.0",
@@ -1888,7 +1884,6 @@
"resolved": "https://registry.npmjs.org/@deck.gl/layers/-/layers-9.2.5.tgz",
"integrity": "sha512-a48zWxeHknSX67ZeIzWeLXuOVJkEnQjnLIC27Uv3zHjKlvaoraWPOgScUNyteB0UIIzhzE+D8lF+ViHeIdkNSA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@loaders.gl/images": "^4.2.0",
"@loaders.gl/schema": "^4.2.0",
@@ -2763,7 +2758,6 @@
"resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz",
"integrity": "sha512-cG0C5fMZ1jyW6WCsf4LoHGvaIAJCEVA/ioqKoYRwoSfXkOf+17KupK1OUQyUCw5XoRn+oWA1FulJQOYlXnb9Gw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@loaders.gl/loader-utils": "4.3.4",
"@loaders.gl/schema": "4.3.4",
@@ -3071,15 +3065,13 @@
"version": "9.2.5",
"resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.5.tgz",
"integrity": "sha512-Z+DC7LIw+kPcIthGJdSoquIc92kkUlTOINMuJtssPsvOgFYtlsRn29h95K9y8ehjh234q2+73ExjfDQFE64f5Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@luma.gl/core": {
"version": "9.2.5",
"resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.2.5.tgz",
"integrity": "sha512-7tQ6wTTtQV6iWZxjMr+BDzS2o814EvaNW5efKtdzdvbbNyDWkDQZkyHkPvgGBB1o/+N2cbZS7GWyxOljCCH5uA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@math.gl/types": "^4.1.0",
"@probe.gl/env": "^4.0.8",
@@ -3093,7 +3085,6 @@
"resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.2.5.tgz",
"integrity": "sha512-swM+4VO+ab4DU58TB+cdSdby/MGLLWA9ez6BmaZ0HZwLN1HNdYwbQmT71Frtw+6lJpmBZHr78KOQi66Zx5+SOg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@math.gl/core": "^4.1.0",
"@math.gl/types": "^4.1.0",
@@ -3128,7 +3119,6 @@
"resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.2.5.tgz",
"integrity": "sha512-9upnT6r5exwotM1atc7Nwa7P69MKx+FavXWlabjk+nrRjeIJwzQ/3sXg9UfdDLavN/cDwGvnWz05UlbyABMmJg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@math.gl/core": "^4.1.0",
"@math.gl/types": "^4.1.0",
@@ -5891,7 +5881,6 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -6097,7 +6086,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6410,7 +6398,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6563,7 +6550,6 @@
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -7255,7 +7241,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8949,8 +8934,7 @@
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
- "license": "BSD-2-Clause",
- "peer": true
+ "license": "BSD-2-Clause"
},
"node_modules/lerc": {
"version": "3.0.0",
@@ -9633,7 +9617,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9653,7 +9636,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9870,7 +9852,6 @@
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -11067,7 +11048,6 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -11606,7 +11586,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -11664,7 +11643,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -11956,7 +11934,6 @@
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/components/Layout/Sidebar.jsx b/src/components/Layout/Sidebar.jsx
index 1d42137..0084fda 100644
--- a/src/components/Layout/Sidebar.jsx
+++ b/src/components/Layout/Sidebar.jsx
@@ -1,159 +1,18 @@
import React, { useState, useEffect } from 'react';
-import { RADIO_PRESETS, DEVICE_PRESETS, ANTENNA_PRESETS, CABLE_TYPES } from '../../data/presets';
-import { useRF, GROUND_TYPES, CLIMATE_ZONES } from '../../context/RFContext';
import BatchProcessing from '../Map/BatchProcessing';
-
-const CollapsibleSection = ({ title, isOpen, onToggle, children, isShared = false, isITM = false, alwaysVisible = null, collapsible = true }) => (
-
-
-
- {title}
- {isShared && (Shared)}
- {isITM && (ITM)}
-
- {collapsible && (
-
- {isOpen ? (
-
- ) : (
-
- )}
-
- )}
-
-
- {alwaysVisible && (
-
{/* Added margin when closed */}
- {alwaysVisible}
-
- )}
-
- {(isOpen || !collapsible) && (
-
- {children}
-
- )}
-
-);
+import HardwareSection from './sections/HardwareSection';
+import EnvironmentSection from './sections/EnvironmentSection';
+import LoRaBandSection from './sections/LoRaBandSection';
+import SettingsSection from './sections/SettingsSection';
+import { useUI } from '../../context/UIContext';
+import { useHardware } from '../../context/HardwareContext';
const Sidebar = () => {
const {
- selectedRadioPreset, setSelectedRadioPreset,
- selectedDevice, setSelectedDevice,
- selectedAntenna, setSelectedAntenna,
- txPower, setTxPower,
- antennaHeight, setAntennaHeight,
- antennaGain, setAntennaGain,
- selectedCableType, setSelectedCableType,
- cableLength, setCableLength,
- freq, setFreq,
- bw, setBw,
- sf, setSf,
- cr, setCr,
- erp, cableLoss,
- units, setUnits,
- mapStyle, setMapStyle,
- kFactor, setKFactor,
- clutterHeight, setClutterHeight,
- batchNodes, setBatchNodes,
- setShowBatchPanel,
- triggerRecalc,
- editMode, setEditMode,
- rxHeight, setRxHeight,
- toolMode,
sidebarIsOpen, setSidebarIsOpen,
- isMobile,
- groundType, setGroundType,
- climate, setClimate,
- fadeMargin, setFadeMargin,
- viewshedMaxDist, setViewshedMaxDist
- } = useRF();
-
-
-
- // Initial sync
- useEffect(() => {
- if (isMobile && sidebarIsOpen) setSidebarIsOpen(false);
- }, [isMobile]); // Add dependency
-
- const isOpen = sidebarIsOpen;
- const setIsOpen = setSidebarIsOpen;
-
- const handleTxPowerChange = (e) => {
- setTxPower(Math.min(Number(e.target.value), DEVICE_PRESETS[selectedDevice].tx_power_max));
- };
-
- const isCustom = selectedRadioPreset === 'CUSTOM';
- const isCustomAntenna = selectedAntenna === 'CUSTOM';
-
- const sectionStyle = {
- marginBottom: 'var(--spacing-lg)',
- borderBottom: '1px solid var(--color-border)',
- paddingBottom: 'var(--spacing-md)'
- };
-
- const labelStyle = {
- display: 'block',
- color: 'var(--color-text-muted)',
- fontSize: '0.85rem',
- marginBottom: 'var(--spacing-xs)',
- marginTop: 'var(--spacing-sm)'
- };
-
- const inputStyle = {
- width: '100%',
- background: 'rgba(0,0,0,0.3)',
- border: '1px solid var(--color-border)',
- color: 'var(--color-text-main)',
- padding: 'var(--spacing-sm)',
- borderRadius: 'var(--radius-md)',
- fontFamily: 'monospace'
- };
-
- const selectStyle = {
- ...inputStyle,
- cursor: 'pointer',
- // Arrow SVG (Cyan Chevron)
- backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2300f2ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
- backgroundRepeat: 'no-repeat',
- backgroundPosition: 'right 8px center',
- backgroundSize: '16px',
- paddingRight: '32px', // Space for arrow
- appearance: 'none',
- WebkitAppearance: 'none',
- MozAppearance: 'none'
- };
-
- const buttonStyle = {
- padding: '8px 16px',
- border: 'none',
- borderRadius: '4px',
- color: '#fff',
- fontWeight: 'bold',
- cursor: 'pointer',
- fontSize: '0.9rem',
- marginTop: '8px'
- };
+ isMobile
+ } = useUI();
+ const { editMode, setEditMode } = useHardware();
const [sections, setSections] = useState({
hardware: true,
@@ -165,665 +24,162 @@ const Sidebar = () => {
setSections(prev => ({ ...prev, [section]: !prev[section] }));
};
+ // Auto-close sidebar on mobile
+ useEffect(() => {
+ if (isMobile && sidebarIsOpen) setSidebarIsOpen(false);
+ }, [isMobile]); // Trigger on mount or mobile switch
-
-
-
- return (
- <>
-
-
-
-
- >
- );
+ justifyContent: 'center',
+ gap: '10px'
+ }}>
+
meshRF
+
+
+ {/* EDIT MODE BANNER */}
+ {editMode !== 'GLOBAL' && (
+
+
+
+ Editing Config
+
+
+ {editMode === 'A' ? 'NODE A (TX)' : 'NODE B (RX)'}
+
+
+
+
+
+ )}
+
+ toggleSection('hardware')} />
+
+ toggleSection('environment')} />
+
+ toggleSection('radio')} />
+
+ {/* BATCH PROCESSING */}
+
+
+
+
+
+
+ {/* Footer */}
+
+
+ >
+ );
};
export default Sidebar;
diff --git a/src/components/Layout/sections/CollapsibleSection.jsx b/src/components/Layout/sections/CollapsibleSection.jsx
new file mode 100644
index 0000000..7f4013b
--- /dev/null
+++ b/src/components/Layout/sections/CollapsibleSection.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+const CollapsibleSection = ({ title, isOpen, onToggle, children, isShared = false, isITM = false, alwaysVisible = null, collapsible = true }) => (
+
+
+
+ {title}
+ {isShared && (Shared)}
+ {isITM && (ITM)}
+
+ {collapsible && (
+
+ {isOpen ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ {alwaysVisible && (
+
{/* Added margin when closed */}
+ {alwaysVisible}
+
+ )}
+
+ {(isOpen || !collapsible) && (
+
+ {children}
+
+ )}
+
+);
+
+export default CollapsibleSection;
diff --git a/src/components/Layout/sections/EnvironmentSection.jsx b/src/components/Layout/sections/EnvironmentSection.jsx
new file mode 100644
index 0000000..b5ad4fa
--- /dev/null
+++ b/src/components/Layout/sections/EnvironmentSection.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import CollapsibleSection from './CollapsibleSection';
+import { useEnvironment, GROUND_TYPES, CLIMATE_ZONES } from '../../../context/EnvironmentContext';
+
+const EnvironmentSection = ({ isOpen, onToggle }) => {
+ const {
+ kFactor, setKFactor,
+ clutterHeight, setClutterHeight,
+ groundType, setGroundType,
+ climate, setClimate,
+ fadeMargin, setFadeMargin
+ } = useEnvironment();
+
+ const inputStyle = {
+ width: '100%',
+ background: 'rgba(0,0,0,0.3)',
+ border: '1px solid var(--color-border)',
+ color: 'var(--color-text-main)',
+ padding: 'var(--spacing-sm)',
+ borderRadius: 'var(--radius-md)',
+ fontFamily: 'monospace'
+ };
+
+ const selectStyle = {
+ ...inputStyle,
+ cursor: 'pointer',
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2300f2ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
+ backgroundRepeat: 'no-repeat',
+ backgroundPosition: 'right 8px center',
+ backgroundSize: '16px',
+ paddingRight: '32px',
+ appearance: 'none',
+ WebkitAppearance: 'none',
+ MozAppearance: 'none'
+ };
+
+ return (
+
+
+
+
+ {/* Ground Type */}
+
+
+
+
+
+ {/* Climate Zone */}
+
+
+
+
+
+ {/* Fade Margin */}
+
+
+
+ setFadeMargin(Number(e.target.value))}
+ style={{ '--range-progress': `${(fadeMargin / 20) * 100}%` }}
+ />
+
+ {fadeMargin}
+
+
+
+
+
+ );
+};
+
+export default EnvironmentSection;
diff --git a/src/components/Layout/sections/HardwareSection.jsx b/src/components/Layout/sections/HardwareSection.jsx
new file mode 100644
index 0000000..8d771a3
--- /dev/null
+++ b/src/components/Layout/sections/HardwareSection.jsx
@@ -0,0 +1,299 @@
+import React from 'react';
+import CollapsibleSection from './CollapsibleSection';
+import { useHardware } from '../../../context/HardwareContext';
+import { useUI } from '../../../context/UIContext';
+import { useEnvironment } from '../../../context/EnvironmentContext';
+import { useRadio } from '../../../context/RadioContext';
+import { DEVICE_PRESETS, ANTENNA_PRESETS, CABLE_TYPES } from '../../../data/presets';
+
+const HardwareSection = ({ isOpen, onToggle }) => {
+ const {
+ editMode,
+ selectedDevice, setSelectedDevice,
+ selectedAntenna, setSelectedAntenna,
+ antennaGain, setAntennaGain,
+ antennaHeight, setAntennaHeight,
+ selectedCableType, setSelectedCableType,
+ cableLength, setCableLength,
+ txPower, setTxPower,
+ erp, cableLoss
+ } = useHardware();
+
+ const { toolMode, units } = useUI();
+ const { rxHeight, setRxHeight } = useEnvironment();
+ const { triggerRecalc } = useRadio();
+
+ const isCustomAntenna = selectedAntenna === 'CUSTOM';
+
+ const labelStyle = {
+ display: 'block',
+ color: 'var(--color-text-muted)',
+ fontSize: '0.85rem',
+ marginBottom: 'var(--spacing-xs)',
+ marginTop: 'var(--spacing-sm)'
+ };
+
+ const inputStyle = {
+ width: '100%',
+ background: 'rgba(0,0,0,0.3)',
+ border: '1px solid var(--color-border)',
+ color: 'var(--color-text-main)',
+ padding: 'var(--spacing-sm)',
+ borderRadius: 'var(--radius-md)',
+ fontFamily: 'monospace'
+ };
+
+ const selectStyle = {
+ ...inputStyle,
+ cursor: 'pointer',
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2300f2ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
+ backgroundRepeat: 'no-repeat',
+ backgroundPosition: 'right 8px center',
+ backgroundSize: '16px',
+ paddingRight: '32px',
+ appearance: 'none',
+ WebkitAppearance: 'none',
+ MozAppearance: 'none'
+ };
+
+ const handleTxPowerChange = (e) => {
+ const val = Number(e.target.value);
+ const max = DEVICE_PRESETS[selectedDevice]?.tx_power_max || 22;
+ setTxPower(Math.min(val, max));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {isCustomAntenna && (
+
+
+ setAntennaGain(Number(e.target.value))}
+ />
+
+ )}
+
+
+
setAntennaHeight(Number(e.target.value))}
+ style={{
+ '--range-progress': `${((antennaHeight - 1) / 49) * 100}%`,
+ '--range-color': '#a855f7'
+ }}
+ />
+
+ {/* RX Height Slider - Only for RF Coverage Tool */}
+ {toolMode === 'rf_coverage' && (
+
+
+ setRxHeight(Number(e.target.value))}
+ style={{
+ '--range-progress': `${((rxHeight - 1) / 29) * 100}%`,
+ '--range-color': 'var(--color-secondary)'
+ }}
+ />
+
+ )}
+
+ {/* CABLE CONFIGURATION */}
+
+
+
+
+
+
+
+ {
+ const val = Number(e.target.value);
+ // Store in meters always
+ setCableLength(units === 'imperial' ? val / 3.28084 : val);
+ }}
+ />
+
+
+
+
+
+
+ {/* Manual Recalculation Trigger */}
+
+
+ {/* ERP CALCULATION DISPLAY */}
+
+
+
+ );
+};
+
+export default HardwareSection;
diff --git a/src/components/Layout/sections/LoRaBandSection.jsx b/src/components/Layout/sections/LoRaBandSection.jsx
new file mode 100644
index 0000000..9dcd1c9
--- /dev/null
+++ b/src/components/Layout/sections/LoRaBandSection.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import CollapsibleSection from './CollapsibleSection';
+import { useRadio } from '../../../context/RadioContext';
+import { RADIO_PRESETS } from '../../../data/presets';
+
+const LoRaBandSection = ({ isOpen, onToggle }) => {
+ const {
+ selectedRadioPreset, setSelectedRadioPreset,
+ freq, setFreq,
+ bw, setBw,
+ sf, setSf,
+ cr, setCr
+ } = useRadio();
+
+ const isCustom = selectedRadioPreset === 'CUSTOM';
+
+ const labelStyle = {
+ display: 'block',
+ color: 'var(--color-text-muted)',
+ fontSize: '0.85rem',
+ marginBottom: 'var(--spacing-xs)',
+ marginTop: 'var(--spacing-sm)'
+ };
+
+ const inputStyle = {
+ width: '100%',
+ background: 'rgba(0,0,0,0.3)',
+ border: '1px solid var(--color-border)',
+ color: 'var(--color-text-main)',
+ padding: 'var(--spacing-sm)',
+ borderRadius: 'var(--radius-md)',
+ fontFamily: 'monospace'
+ };
+
+ const selectStyle = {
+ ...inputStyle,
+ cursor: 'pointer',
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2300f2ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E")`,
+ backgroundRepeat: 'no-repeat',
+ backgroundPosition: 'right 8px center',
+ backgroundSize: '16px',
+ paddingRight: '32px',
+ appearance: 'none',
+ WebkitAppearance: 'none',
+ MozAppearance: 'none'
+ };
+
+ const alwaysVisibleContent = (
+
+
+
+
+ );
+
+ return (
+
+
+
+ );
+};
+
+export default LoRaBandSection;
diff --git a/src/components/Layout/sections/SettingsSection.jsx b/src/components/Layout/sections/SettingsSection.jsx
new file mode 100644
index 0000000..a67d0b6
--- /dev/null
+++ b/src/components/Layout/sections/SettingsSection.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+import { useUI } from '../../../context/UIContext';
+
+const SettingsSection = () => {
+ const { units, setUnits, mapStyle, setMapStyle } = useUI();
+
+ return (
+
+
+ Settings
+
+
+
+
+
+
+
+
+
+
+
+ {/* Map Theme Selector */}
+
+
+
+
+
+
+ );
+};
+
+export default SettingsSection;
diff --git a/src/components/Map/MapContainer.jsx b/src/components/Map/MapContainer.jsx
index fb8912d..03fae45 100644
--- a/src/components/Map/MapContainer.jsx
+++ b/src/components/Map/MapContainer.jsx
@@ -1,46 +1,45 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
import {
MapContainer,
TileLayer,
- ImageOverlay,
+ ZoomControl,
Marker,
Popup,
- Tooltip,
- Polyline,
- Rectangle,
- Circle,
- ZoomControl,
+ ImageOverlay,
+ useMap
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
-import LinkLayer from "./LinkLayer";
-import LinkAnalysisPanel from "./LinkAnalysisPanel";
-import OptimizationLayer from "./OptimizationLayer";
-import { useRF, GROUND_TYPES } from "../../context/RFContext";
-import { calculateLinkBudget } from "../../utils/rfMath";
-import { DEVICE_PRESETS } from "../../data/presets";
-import * as turf from "@turf/turf";
-import DeckGLOverlay from "./DeckGLOverlay";
-import WasmViewshedLayer from "./WasmViewshedLayer";
import { ScatterplotLayer } from "@deck.gl/layers";
-import RFCoverageLayer from "./RFCoverageLayer";
+import * as turf from "@turf/turf";
+
+// Context & Stores
+import { useRF } from "../../context/RFContext";
+import useSimulationStore from "../../store/useSimulationStore";
+
+// Hooks
+import { useLinkTool } from "./hooks/useLinkTool";
import { useViewshedTool } from "../../hooks/useViewshedTool";
import { useRFCoverageTool } from "../../hooks/useRFCoverageTool";
-import BatchNodesPanel from "./BatchNodesPanel";
-import useSimulationStore from "../../store/useSimulationStore";
+import { useMapEventHandlers } from "./hooks/useMapEventHandlers";
-// Refactored Sub-Components
+// Layers & Managers
+import DeckGLOverlay from "./DeckGLOverlay";
+import WasmViewshedLayer from "./WasmViewshedLayer";
+import LinkLayerManager from "./layers/LinkLayerManager";
+import ViewshedLayerManager from "./layers/ViewshedLayerManager";
+import CoverageLayerManager from "./layers/CoverageLayerManager";
+import OptimizationLayerManager from "./layers/OptimizationLayerManager";
+
+// Controls & UI
import LocateControl from "./Controls/LocateControl";
-import CoverageClickHandler from "./Controls/CoverageClickHandler";
-import ViewshedControl from "./Controls/ViewshedControl";
-import BatchNodesPanelWrapper from "./Controls/BatchNodesPanelWrapper";
import MapToolbar from "./UI/MapToolbar";
import GuidanceOverlays from "./UI/GuidanceOverlays";
import SiteAnalysisPanel from "./UI/SiteAnalysisPanel";
import SiteAnalysisResultsPanel from "./UI/SiteAnalysisResultsPanel";
-import OptimizationResultsPanel from "./OptimizationResultsPanel";
+import BatchNodesPanelWrapper from "./Controls/BatchNodesPanelWrapper";
-// Custom SVG marker icon (inline - no loading delay)
+// Custom SVG marker icon
const customMarkerIcon = L.divIcon({
html: `