# RFCP - RF Coverage Planning Tool ## Technical Specification v3.0 (Final) **Project:** RF Coverage Planner **Version:** 3.0 (PWA/Offline-First/Manual Input) **Target URL:** https://rfcp.eliah.one **Repository:** https://git.eliah.one/mytec/rfcp.git **Created:** 2025-01-30 **Author:** Claude + ะžะปะตะณ (mytec) **Status:** ๐Ÿš€ Ready for Implementation --- ## Executive Summary RFCP - Progressive Web Application ะดะปั ะฟะปะฐะฝัƒะฒะฐะฝะฝั ั‚ะฐ ะฒั–ะทัƒะฐะปั–ะทะฐั†ั–ั— ะฟะพะบั€ะธั‚ั‚ั wireless ะผะตั€ะตะถ (LTE, UHF/VHF) ะท ะฟะพะฒะฝะพัŽ ะฟั–ะดั‚ั€ะธะผะบะพัŽ offline ั€ะตะถะธะผัƒ. ะ†ะฝัั‚ั€ัƒะผะตะฝั‚ ะฝะฐะดะฐั” ะฒั–ะนััŒะบะพะฒะธะผ ะทะฒ'ัะทะบั–ะฒั†ัะผ ะณะฝัƒั‡ะบะธะน, ั‚ะพั‡ะฝะธะน ะทะฐัั–ะฑ ะฟะปะฐะฝัƒะฒะฐะฝะฝั ั€ะพะทะณะพั€ั‚ะฐะฝะฝั ั€ะฐะดั–ะพัะธัั‚ะตะผ ะฝะฐ ะผั–ัั†ะตะฒะพัั‚ั– ะท ัƒั€ะฐั…ัƒะฒะฐะฝะฝัะผ ั€ะตะปัŒั”ั„ัƒ. ### Core Philosophy **"Flexibility over Presets"** - ะšะพั€ะธัั‚ัƒะฒะฐั‡ ะฒะฒะพะดะธั‚ัŒ ะฟะฐั€ะฐะผะตั‚ั€ะธ ะฒั€ัƒั‡ะฝัƒ, ั€ะพะทัƒะผั–ั” ั‰ะพ ั€ะพะฑะธั‚ัŒ, ะตะบัะฟะตั€ะธะผะตะฝั‚ัƒั”. ะั–ัะบะธั… ะถะพั€ัั‚ะบะธั… ะพะฑะผะตะถะตะฝัŒ ะฝะฐ ะพะฑะปะฐะดะฝะฐะฝะฝั. ### Key Features - ๐Ÿ“ด **100% Offline Capable** - ะฟั€ะฐั†ัŽั” ะฑะตะท ั–ะฝั‚ะตั€ะฝะตั‚ัƒ ะฟั–ัะปั ะฟะตั€ัˆะพะณะพ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั - ๐ŸŽ›๏ธ **Manual Input First** - ะฟะพะฒะฝะฐ ะณะฝัƒั‡ะบั–ัั‚ัŒ ะฟะฐั€ะฐะผะตั‚ั€ั–ะฒ, ะฑะตะท ะฟั€ะธะฒ'ัะทะบะธ ะดะพ ะบะพะฝะบั€ะตั‚ะฝะพะณะพ ะพะฑะปะฐะดะฝะฐะฝะฝั - ๐Ÿ“ฑ **Mobile-First** - ะพะฟั‚ะธะผั–ะทะพะฒะฐะฝะพ ะดะปั ะฟะปะฐะฝัˆะตั‚ั–ะฒ ั‚ะฐ ั‚ะตะปะตั„ะพะฝั–ะฒ - ๐Ÿ—บ๏ธ **High-Res Terrain** - 30m SRTM elevation data ะดะปั ั‚ะพั‡ะฝะธั… ั€ะพะทั€ะฐั…ัƒะฝะบั–ะฒ - ๐Ÿ” **Optional Auth** - MAS OAuth ะดะปั ัะธะฝั…ั€ะพะฝั–ะทะฐั†ั–ั— ะผั–ะถ ะฟั€ะธัั‚ั€ะพัะผะธ - ๐Ÿ“ฆ **Smart Download** - terrain on-demand ะดะปั 2-3 ะพะฑะปะฐัั‚ะตะน (~800MB-1.2GB) - โšก **Fast** - ะฒัั– RF ั€ะพะทั€ะฐั…ัƒะฝะบะธ ะฒ ะฑั€ะฐัƒะทะตั€ั– (Web Workers) - ๐ŸŒ **Universal** - LTE (MVP) โ†’ UHF/VHF (v1.1+) ### Target Users - ะ’ั–ะนััŒะบะพะฒั– ะทะฒ'ัะทะบั–ะฒั†ั– (ะฟะปะฐะฝัƒะฒะฐะฝะฝั tactical LTE/radio networks) - ะขะตะปะตะบะพะผ ั–ะฝะถะตะฝะตั€ะธ (proof-of-concept deployments) - UMTC infrastructure ะฟะปะฐะฝัƒะฒะฐะปัŒะฝะธะบะธ - Amateur radio operators (ะผะฐะนะฑัƒั‚ะฝั”) --- ## Architecture Overview ### High-Level Architecture ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Browser / Mobile Device โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ RFCP PWA Application โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ UI Layer โ”‚ โ”‚ Service Worker โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (React) โ”‚โ—„โ”€โ”€โ”€โ”€โ–บโ”‚ (Offline Cache) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - Manual โ”‚ โ”‚ - Map tiles โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Input UI โ”‚ โ”‚ - App assets โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - Dynamic โ”‚ โ”‚ - Terrain data โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Bands โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ RF Calculation Engine โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (TypeScript + Web Workers) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - Universal FSPL (any frequency) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - Terrain loss (30m SRTM) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - Sector antenna patterns โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - Multi-site aggregation โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Offline Storage โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - IndexedDB (Terrain + Configs) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ - LocalStorage (Settings) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ (Optional Network Connection) โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Backend API (FastAPI) - Optional โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ MAS OAuth โ”‚ โ”‚ Terrain Data CDN โ”‚ โ”‚ โ”‚ โ”‚ Integration โ”‚ โ”‚ (30m SRTM files) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Config Sync (for multi-device users) โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` --- ## Technology Stack ### Frontend (PWA Core) ```yaml Core Framework: - React: 18.x (with TypeScript) - TypeScript: 5.x - Vite: 5.x (build tool) - Tailwind CSS: 3.x Mapping Libraries: - Leaflet: 1.9.x (map rendering) - React-Leaflet: 4.x - Leaflet.heat: 0.2.x (heatmap visualization) Offline Capabilities: - Workbox: 7.x (Service Worker management) - Dexie.js: 4.x (IndexedDB wrapper) - localforage: 1.x (fallback storage) State Management: - Zustand: 4.x (lightweight state) - React Query: 5.x (server state, optional) RF Calculations: - Pure TypeScript (no dependencies) - Web Workers for parallel processing - Custom SRTM parser (30m resolution) UI Components: - Headless UI (accessible components) - Heroicons (icons) - React Hook Form (forms) - React Range (sliders) ``` ### Backend (Optional/Supplementary) ```yaml API Framework: - FastAPI: 0.110.x - Python: 3.11+ - Uvicorn: ASGI server Authentication: - matrix-auth-server OAuth client - JWT token validation Data Storage: - SQLite (config backups) - File system (30m SRTM tiles) CDN/Static Files: - Caddy reverse proxy - Pre-processed SRTM tiles ``` ### Infrastructure ```yaml Hosting: - VPS-A (Germany) or dedicated container - Caddy 2.x (reverse proxy + TLS) - Docker + Docker Compose Domain: - rfcp.eliah.one Monitoring: - Uptime Kuma integration ``` --- ## Data Models ### TypeScript Types (Frontend) ```typescript // types/site.ts export interface Site { // Identity id: string; // UUID name: string; // User-defined name // Location lat: number; // Latitude lon: number; // Longitude height: number; // meters above ground (1-100) // RF Parameters (all manually editable) power: number; // dBm (10-50) gain: number; // dBi (0-25) frequency: number; // MHz (400-6000, user can enter any value) // Antenna Configuration antennaType: 'omni' | 'sector'; azimuth?: number; // degrees (0-360), for sector only beamwidth?: number; // degrees (30-120), for sector only // UI/Display color: string; // hex color for map marker visible: boolean; // show/hide on map // User Metadata (optional, free-text) notes?: string; // e.g., "ZTE B8200 + Huawei APX4518R0" equipment?: string; // free-form equipment description // Timestamps createdAt: Date; updatedAt: Date; } // types/coverage.ts export interface CoveragePoint { lat: number; lon: number; rsrp: number; // dBm (calculated signal strength) siteId: string; // which site provides this coverage } export interface CoverageResult { points: CoveragePoint[]; calculationTime: number; // milliseconds totalPoints: number; bounds: LatLngBounds; settings: CoverageSettings; } export interface CoverageSettings { radius: number; // km (calculation radius) resolution: number; // meters (grid resolution) rsrpThreshold: number; // dBm (minimum signal to display) } // types/terrain.ts export interface TerrainTile { region: string; // e.g., "ukraine-east" bounds: { north: number; south: number; east: number; west: number; }; resolution: 30; // arc-seconds (30m SRTM) data: Int16Array; // elevation data width: number; // tile width in pixels height: number; // tile height in pixels downloadedAt: number; // timestamp sizeBytes: number; // compressed size } // types/frequency.ts export interface FrequencyBand { value: number; // MHz name: string; // e.g., "Band 3" range: string; // e.g., "1710-1880 MHz" type: 'LTE' | 'UHF' | 'VHF' | '5G' | 'Custom'; characteristics: { range: 'short' | 'medium' | 'long'; penetration: 'poor' | 'fair' | 'good' | 'excellent'; typical: string; // use case description }; } // types/project.ts export interface ProjectConfig { id: string; name: string; description?: string; sites: Site[]; mapCenter: [number, number]; mapZoom: number; coverageSettings: CoverageSettings; createdAt: Date; updatedAt: Date; syncedAt?: Date; // for cloud sync } // types/settings.ts export interface AppSettings { // UI theme: 'light' | 'dark' | 'auto'; language: 'uk' | 'en'; // Units units: 'metric'; // only metric for now // Defaults defaultFrequency: number; // MHz defaultPower: number; // dBm defaultHeight: number; // meters defaultGain: number; // dBi // Features autoDownloadTerrain: boolean; enableCloudSync: boolean; // Map mapTileServer: string; defaultMapZoom: number; } ``` ### IndexedDB Schema (Dexie) ```typescript // db/schema.ts import Dexie, { Table } from 'dexie'; export interface DBTerrainTile { region: string; // primary key bounds: { north: number; south: number; east: number; west: number; }; resolution: 30; data: ArrayBuffer; // compressed elevation data width: number; height: number; downloadedAt: number; sizeBytes: number; } export interface DBProjectConfig { id: string; // primary key (UUID) name: string; data: string; // JSON serialized ProjectConfig createdAt: number; updatedAt: number; syncedAt?: number; } export class RFCPDatabase extends Dexie { terrain!: Table; projects!: Table; constructor() { super('rfcp-db'); this.version(1).stores({ terrain: 'region, downloadedAt', projects: 'id, updatedAt, syncedAt' }); } } export const db = new RFCPDatabase(); ``` --- ## Frequency Bands & Configuration ### Common Frequency Bands (Reference Data) ```typescript // constants/frequencies.ts export const COMMON_FREQUENCIES: FrequencyBand[] = [ // LTE Bands { value: 800, name: 'Band 20', range: '791-862 MHz', type: 'LTE', characteristics: { range: 'long', penetration: 'excellent', typical: 'Rural coverage, deep building penetration' } }, { value: 1800, name: 'Band 3', range: '1710-1880 MHz', type: 'LTE', characteristics: { range: 'medium', penetration: 'good', typical: 'Urban/suburban, most common in Ukraine' } }, { value: 1900, name: 'Band 2', range: '1850-1990 MHz', type: 'LTE', characteristics: { range: 'medium', penetration: 'good', typical: 'North America, some military equipment' } }, { value: 2600, name: 'Band 7', range: '2500-2690 MHz', type: 'LTE', characteristics: { range: 'short', penetration: 'fair', typical: 'High capacity urban, shorter range' } }, // Future: UHF/VHF (v1.1) { value: 150, name: 'VHF High', range: '136-174 MHz', type: 'VHF', characteristics: { range: 'long', penetration: 'excellent', typical: 'Tactical radio, emergency services' } }, { value: 450, name: 'UHF', range: '400-470 MHz', type: 'UHF', characteristics: { range: 'medium', penetration: 'good', typical: 'Military tactical radio, PMR446' } }, // Future: 5G NR (v1.2) { value: 3500, name: 'Band 42/43 (n78)', range: '3400-3800 MHz', type: '5G', characteristics: { range: 'short', penetration: 'poor', typical: '5G NR, high bandwidth' } } ]; export function getFrequencyInfo(frequency: number): FrequencyBand | null { // Find closest match return COMMON_FREQUENCIES.find(band => Math.abs(band.value - frequency) < 50 ) || null; } export function getWavelength(frequencyMHz: number): string { const wavelengthMeters = 300 / frequencyMHz; if (wavelengthMeters >= 1) { return `${wavelengthMeters.toFixed(2)} m`; } else { return `${(wavelengthMeters * 100).toFixed(1)} cm`; } } ``` --- ## RF Propagation Models ### Free Space Path Loss (FSPL) - Universal ```typescript /** * Calculate Free Space Path Loss * Works with ANY frequency (LTE, UHF, VHF, 5G) * * @param distanceKm - Distance in kilometers * @param frequencyMHz - Frequency in megahertz * @returns Path loss in dB */ export function calculateFSPL(distanceKm: number, frequencyMHz: number): number { if (distanceKm <= 0) return 0; // FSPL(dB) = 20ร—logโ‚โ‚€(d) + 20ร—logโ‚โ‚€(f) + 32.45 // This formula is universal for all RF frequencies return 20 * Math.log10(distanceKm) + 20 * Math.log10(frequencyMHz) + 32.45; } /** * Calculate RSRP at receiver location * Universal calculator - works with any user-provided parameters * * @param site - Transmitter site configuration (user inputs) * @param rxLat - Receiver latitude * @param rxLon - Receiver longitude * @param terrainLoss - Additional terrain loss in dB * @returns RSRP in dBm */ export function calculateRSRP( site: Site, rxLat: number, rxLon: number, terrainLoss: number = 0 ): number { const distanceKm = haversineDistance( site.lat, site.lon, rxLat, rxLon ); // Free space path loss (universal formula) const fspl = calculateFSPL(distanceKm, site.frequency); // Link budget calculation // RSRP = P_tx + G_tx - FSPL - TerrainLoss - SectorLoss let rsrp = site.power + site.gain - fspl - terrainLoss; // Apply antenna pattern loss (for sector antennas) if (site.antennaType === 'sector' && site.azimuth !== undefined) { const bearing = calculateBearing(site.lat, site.lon, rxLat, rxLon); const relativeAngle = Math.abs(bearing - site.azimuth); const normalizedAngle = relativeAngle > 180 ? 360 - relativeAngle : relativeAngle; const patternLoss = calculateSectorPatternLoss( normalizedAngle, site.beamwidth || 65 ); rsrp -= patternLoss; } return rsrp; } ``` ### Terrain Loss Model (Enhanced for 30m SRTM) ```typescript /** * Calculate additional path loss due to terrain * Uses 30m resolution SRTM data for accurate results * Implements simplified knife-edge diffraction */ export function calculateTerrainLoss( txLat: number, txLon: number, txHeight: number, rxLat: number, rxLon: number, rxHeight: number = 1.5, terrain: TerrainManager ): number { // Get elevations from 30m SRTM data const txElevation = terrain.getElevation(txLat, txLon); const rxElevation = terrain.getElevation(rxLat, rxLon); const distance = haversineDistance(txLat, txLon, rxLat, rxLon); // Effective antenna heights above sea level const h1 = txHeight + txElevation; const h2 = rxHeight + rxElevation; // Sample terrain profile (5 points between TX and RX) const profilePoints = 5; let maxObstruction = 0; for (let i = 1; i < profilePoints; i++) { const ratio = i / profilePoints; const midLat = txLat + (rxLat - txLat) * ratio; const midLon = txLon + (rxLon - txLon) * ratio; const midElevation = terrain.getElevation(midLat, midLon); // Line-of-sight height at this point const losHeight = h1 + (h2 - h1) * ratio; // Obstruction height const obstruction = midElevation - losHeight; if (obstruction > maxObstruction) { maxObstruction = obstruction; } } if (maxObstruction > 0) { // Knife-edge diffraction loss (simplified) // More accurate with 30m resolution const v = maxObstruction * Math.sqrt(2 * distance / 0.3); // wavelength ~0.3m for 1GHz const diffraction = 6.9 + 20 * Math.log10(Math.sqrt((v - 0.1) * (v - 0.1) + 1) + v - 0.1); return Math.min(diffraction, 40); // Cap at 40 dB } return 0; // Clear line-of-sight } ``` ### Sector Antenna Pattern ```typescript /** * Calculate antenna pattern loss for sector antennas * Based on 3GPP antenna models * * @param angleOffBoresight - Angle from antenna boresight in degrees (0-180) * @param beamwidth - 3dB beamwidth in degrees * @returns Pattern loss in dB */ export function calculateSectorPatternLoss( angleOffBoresight: number, beamwidth: number = 65 ): number { // 3GPP antenna pattern model const theta3dB = beamwidth / 2; const sideLobeLevel = 20; // dB // Main lobe if (angleOffBoresight <= theta3dB) { return 12 * Math.pow(angleOffBoresight / theta3dB, 2); } // Side/back lobes return Math.min( 12 * Math.pow(angleOffBoresight / theta3dB, 2), sideLobeLevel ); } ``` ### Signal Quality Thresholds ```typescript // constants/rsrp-thresholds.ts export const RSRP_THRESHOLDS = { EXCELLENT: -70, // > -70 dBm (very strong) GOOD: -85, // -70 to -85 dBm (strong) FAIR: -100, // -85 to -100 dBm (acceptable) POOR: -110, // -100 to -110 dBm (marginal) WEAK: -120, // -110 to -120 dBm (very weak) NO_SERVICE: -120 // < -120 dBm (no service) } as const; export function getSignalQuality(rsrp: number): string { if (rsrp >= RSRP_THRESHOLDS.EXCELLENT) return 'excellent'; if (rsrp >= RSRP_THRESHOLDS.GOOD) return 'good'; if (rsrp >= RSRP_THRESHOLDS.FAIR) return 'fair'; if (rsrp >= RSRP_THRESHOLDS.POOR) return 'poor'; if (rsrp >= RSRP_THRESHOLDS.WEAK) return 'weak'; return 'no-service'; } export const SIGNAL_COLORS = { excellent: '#22c55e', // green-600 good: '#84cc16', // lime-500 fair: '#eab308', // yellow-500 poor: '#f97316', // orange-500 weak: '#ef4444', // red-500 } as const; export function getRSRPColor(rsrp: number): string { return SIGNAL_COLORS[getSignalQuality(rsrp)]; } ``` --- ## User Interface Design ### Main Application Layout ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ RFCP - RF Coverage Planner ๐ŸŒ UA ๐Ÿ‘ค ะžะปะตะณ โš™๏ธ โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ—บ๏ธ Interactive Map โ”‚ โ”‚ โ”‚ โ”‚ (Leaflet + OSM) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ Click to place base station โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ [Heatmap overlay when calculated] โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ Sites (3) [+ Add]โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ”ด Station-1 1800 MHz 43 dBm [Edit] [ร—] โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ”ต Station-2 2600 MHz 30 dBm [Edit] [ร—] โ”‚ โ”‚ โ”‚ โ”‚ ๐ŸŸข Station-3 800 MHz 46 dBm [Edit] [ร—] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ [๐Ÿงฎ ะ ะพะทั€ะฐั…ัƒะฒะฐั‚ะธ ะฟะพะบั€ะธั‚ั‚ั] [๐Ÿ“ฅ ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ terrain] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ### Site Configuration Panel (Manual Input) ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ ๐Ÿ“ก ะšะพะฝั„ั–ะณัƒั€ะฐั†ั–ั ะฑะฐะทะพะฒะพั— ัั‚ะฐะฝั†ั–ั— โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ”‚ ๐Ÿ“ ะะฐะทะฒะฐ ัั‚ะฐะฝั†ั–ั— โ”‚ โ”‚ [Station-1 ] โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ ะšะพะพั€ะดะธะฝะฐั‚ะธ โ”‚ โ”‚ Lat: [48.4647] Lon: [35.0462] [๐Ÿ“ ะ—ะผั–ะฝะธั‚ะธ] โ”‚ โ”‚ โ”‚ โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ะ ะฐะดั–ะพะฟะฐั€ะฐะผะตั‚ั€ะธ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ โ”‚ โ”‚ โ”‚ โšก ะŸะพั‚ัƒะถะฝั–ัั‚ัŒ ะฟะตั€ะตะดะฐะฒะฐั‡ะฐ (dBm) โ”‚ โ”‚ [โ•โ•โ•โ•โ•โ•โ•โ•โ—โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•] 43 โ”‚ โ”‚ 10 50 โ”‚ โ”‚ ๐Ÿ’ก ะขะธะฟะพะฒั–: LimeSDR 20, BBU 43, RRU 46 โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ถ ะšะพะตั„ั–ั†ั–ั”ะฝั‚ ะฟั–ะดัะธะปะตะฝะฝั ะฐะฝั‚ะตะฝะธ (dBi) โ”‚ โ”‚ [โ•โ•โ•โ•โ•โ•โ—โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•] 8 โ”‚ โ”‚ 0 25 โ”‚ โ”‚ ๐Ÿ’ก Omni 2-8, Sector 15-18, Parabolic 20-25 โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ป ะ ะพะฑะพั‡ะฐ ั‡ะฐัั‚ะพั‚ะฐ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ [800] [1800] [1900] [2600] ะฐะฑะพ: โ”‚ โ”‚ โ”‚ โ”‚ [1800] MHz โ”‚ โ”‚ โ”‚ โ”‚ โ„น๏ธ Band 3 (1710-1880 MHz) โ”‚ โ”‚ โ”‚ โ”‚ ฮป = 16.7 cm โ€ข Medium range โ€ข Good penetr. โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ะคั–ะทะธั‡ะฝั– ะฟะฐั€ะฐะผะตั‚ั€ะธ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ ะ’ะธัะพั‚ะฐ ะฟั–ะดะฒั–ััƒ ะฐะฝั‚ะตะฝะธ (ะผะตั‚ั€ั–ะฒ) โ”‚ โ”‚ [โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ—โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•] 30 โ”‚ โ”‚ 1 100 โ”‚ โ”‚ ๐Ÿ’ก ะ’ะธัะพั‚ะฐ ะฒั–ะด ะทะตะผะปั– ะดะพ ั†ะตะฝั‚ั€ัƒ ะฐะฝั‚ะตะฝะธ โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ก ะขะธะฟ ะฐะฝั‚ะตะฝะธ โ”‚ โ”‚ โ—‰ ะ’ัะตะฝะฐะฟั€ะฐะฒะปะตะฝะฐ (Omni) โ”‚ โ”‚ โ—‹ ะกะตะบั‚ะพั€ะฝะฐ (Sector) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€ [Sector parameters (collapsed)] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ ๐Ÿ“ ะะทะธะผัƒั‚ (ะณั€ะฐะดัƒัะธ) [0] โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“Š ะจะธั€ะธะฝะฐ ะฟั€ะพะผะตะฝั (ะณั€ะฐะดัƒัะธ) [65] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ะŸั€ะธะผั–ั‚ะบะธ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ“ ะžะฑะปะฐะดะฝะฐะฝะฝั / ะŸั€ะธะผั–ั‚ะบะธ (ะฝะตะพะฑะพะฒ'ัะทะบะพะฒะพ) โ”‚ โ”‚ [ZTE B8200 BBU + custom omni antenna ] โ”‚ โ”‚ [Test deployment, field validation needed ] โ”‚ โ”‚ โ”‚ โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ โ”‚ โ”‚ โ”‚ ๐Ÿ’พ Quick Templates (optional) โ”‚ โ”‚ [๐Ÿš€ LimeSDR] [๐Ÿ“ก Low BBU] [๐Ÿ“ก High BBU] โ”‚ โ”‚ โ”‚ โ”‚ โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” โ”‚ โ”‚ โ”‚ โ”‚ [๐Ÿ’พ ะ—ะฑะตั€ะตะณั‚ะธ ะทะผั–ะฝะธ] [๐Ÿ—‘๏ธ ะ’ะธะดะฐะปะธั‚ะธ ัั‚ะฐะฝั†ั–ัŽ] โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` **Key UI Principles:** - โœ… **Sliders with values** - ะฒะธะทัƒะฐะปัŒะฝะธะน feedback + ั‚ะพั‡ะฝะธะน ะบะพะฝั‚ั€ะพะปัŒ - โœ… **Helpful hints** - ั‚ะธะฟะพะฒั– ะทะฝะฐั‡ะตะฝะฝั ะฟั–ะด ะฟะพะปัะผะธ - โœ… **Contextual info** - band info, wavelength automatically shown - โœ… **Progressive disclosure** - sector params hidden if omni selected - โœ… **Optional templates** - ัˆะฒะธะดะบะธะน ัั‚ะฐั€ั‚, ะฐะปะต ะฝะต ะพะฑะพะฒ'ัะทะบะพะฒะพ - โœ… **Free-text notes** - ะบะพั€ะธัั‚ัƒะฒะฐั‡ ะพะฟะธััƒั” ัะฒั–ะน setup --- ## Terrain Data Management (30m SRTM) ### Ukraine Terrain Regions (Updated for 30m) ```typescript // terrain/regions.ts export const UKRAINE_REGIONS = { 'ukraine-west': { name: 'ะ—ะฐั…ั–ะดะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', nameEn: 'Western Ukraine', bounds: { north: 52.5, south: 47.9, west: 22.1, east: 27.0 }, resolution: 30, // meters (1 arc-second) size: '~400MB', // compressed includes: ['ะ›ัŒะฒั–ะฒ', 'ะ†ะฒะฐะฝะพ-ะคั€ะฐะฝะบั–ะฒััŒะบ', 'ะงะตั€ะฝั–ะฒั†ั–', 'ะขะตั€ะฝะพะฟั–ะปัŒ', 'ะ›ัƒั†ัŒะบ'] }, 'ukraine-center': { name: 'ะฆะตะฝั‚ั€ะฐะปัŒะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', nameEn: 'Central Ukraine', bounds: { north: 52.5, south: 48.0, west: 27.0, east: 32.0 }, resolution: 30, size: '~400MB', includes: ['ะšะธั—ะฒ', 'ะ’ั–ะฝะฝะธั†ั', 'ะ–ะธั‚ะพะผะธั€', 'ะงะตั€ะบะฐัะธ', 'ะŸะพะปั‚ะฐะฒะฐ'] }, 'ukraine-east': { name: 'ะกั…ั–ะดะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', nameEn: 'Eastern Ukraine', bounds: { north: 50.5, south: 47.0, west: 32.0, east: 40.3 }, resolution: 30, size: '~400MB', includes: ['ะฅะฐั€ะบั–ะฒ', 'ะ”ะพะฝะตั†ัŒะบ', 'ะ›ัƒะณะฐะฝััŒะบ', 'ะ”ะฝั–ะฟั€ะพ', 'ะšั€ะธะฒะธะน ะ ั–ะณ'] }, 'ukraine-south': { name: 'ะŸั–ะฒะดะตะฝะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', nameEn: 'Southern Ukraine', bounds: { north: 48.0, south: 44.4, west: 27.0, east: 38.0 }, resolution: 30, size: '~400MB', includes: ['ะžะดะตัะฐ', 'ะœะธะบะพะปะฐั—ะฒ', 'ะฅะตั€ัะพะฝ', 'ะ—ะฐะฟะพั€ั–ะถะถั', 'ะœะตะปั–ั‚ะพะฟะพะปัŒ'] }, 'ukraine-north': { name: 'ะŸั–ะฒะฝั–ั‡ะฝะฐ ะฃะบั€ะฐั—ะฝะฐ', nameEn: 'Northern Ukraine', bounds: { north: 52.4, south: 50.4, west: 27.0, east: 35.0 }, resolution: 30, size: '~300MB', includes: ['ะงะตั€ะฝั–ะณั–ะฒ', 'ะกัƒะผะธ', 'ะ ั–ะฒะฝะต', 'ะ–ะผะตั€ะธะฝะบะฐ'] } } as const; export type UkraineRegion = keyof typeof UKRAINE_REGIONS; /** * Determine which region contains the given coordinates */ export function latLonToRegion(lat: number, lon: number): UkraineRegion | null { for (const [region, data] of Object.entries(UKRAINE_REGIONS)) { const { north, south, west, east } = data.bounds; if (lat >= south && lat <= north && lon >= west && lon <= east) { return region as UkraineRegion; } } return null; } /** * Get total size for typical 2-3 region deployment */ export function getTypicalDeploymentSize(): string { // Most users will download 2-3 regions // ukraine-east (400MB) + ukraine-south (400MB) + ukraine-center (400MB) return '~800MB - 1.2GB for 2-3 regions'; } ``` ### Enhanced Terrain Manager (30m resolution) ```typescript // terrain/manager.ts export class TerrainManager { private cache = new Map(); private downloading = new Map>(); constructor(private db: RFCPDatabase) {} /** * Get elevation at specific coordinates * Uses 30m SRTM data with bilinear interpolation * Auto-downloads terrain if needed and online */ async getElevation(lat: number, lon: number): Promise { const region = latLonToRegion(lat, lon); if (!region) { console.warn('Coordinates outside Ukraine bounds'); return 0; } // Check memory cache if (this.cache.has(region)) { return this.interpolateElevation(this.cache.get(region)!, lat, lon); } // Check IndexedDB cache const cached = await this.db.terrain.get(region); if (cached) { const tile = this.deserializeTile(cached); this.cache.set(region, tile); return this.interpolateElevation(tile, lat, lon); } // Download if online if (navigator.onLine) { console.log(`Auto-downloading terrain for ${region}`); await this.downloadRegion(region); return this.getElevation(lat, lon); // Recursive call after download } console.warn(`Terrain data for ${region} not available offline`); return 0; } /** * Download 30m SRTM terrain data for region * ~400MB per region */ async downloadRegion(region: UkraineRegion): Promise { // Prevent duplicate downloads if (this.downloading.has(region)) { await this.downloading.get(region); return; } const promise = this._downloadRegion(region); this.downloading.set(region, promise); try { await promise; } finally { this.downloading.delete(region); } } private async _downloadRegion(region: UkraineRegion): Promise { const regionData = UKRAINE_REGIONS[region]; console.log(`Downloading 30m terrain: ${regionData.name} (~${regionData.size})`); const startTime = Date.now(); const response = await fetch(`/api/terrain/${region}`); if (!response.ok) { throw new Error(`Failed to download terrain: ${response.statusText}`); } const buffer = await response.arrayBuffer(); const tile = this.parseSRTM30m(buffer, region); const downloadTime = Date.now() - startTime; console.log( `Terrain downloaded: ${regionData.name} ` + `(${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB in ${(downloadTime / 1000).toFixed(1)}s)` ); // Save to IndexedDB await this.db.terrain.put({ region, bounds: regionData.bounds, resolution: 30, data: buffer, width: tile.width, height: tile.height, downloadedAt: Date.now(), sizeBytes: buffer.byteLength }); // Cache in memory this.cache.set(region, tile); return tile; } /** * Parse 30m SRTM binary data (1 arc-second) */ private parseSRTM30m(buffer: ArrayBuffer, region: UkraineRegion): TerrainTile { const bounds = UKRAINE_REGIONS[region].bounds; const resolution = 1; // 1 arc-second (~30m) // Calculate tile dimensions // 1 degree = 3600 arc-seconds // With 1 arc-second resolution, we get 3600 pixels per degree const width = Math.ceil((bounds.east - bounds.west) * 3600 / resolution); const height = Math.ceil((bounds.north - bounds.south) * 3600 / resolution); // Parse as 16-bit signed integers (big-endian) // SRTM format: 2 bytes per sample const data = new Int16Array(buffer); // Handle byte order (SRTM is big-endian) if (!this.isBigEndian()) { for (let i = 0; i < data.length; i++) { data[i] = ((data[i] & 0xFF) << 8) | ((data[i] >> 8) & 0xFF); } } return { region, bounds, resolution: 30, // meters data, width, height, downloadedAt: Date.now(), sizeBytes: buffer.byteLength }; } /** * Bilinear interpolation for smooth elevation lookup * Essential for 30m data to get accurate intermediate values */ private interpolateElevation( tile: TerrainTile, lat: number, lon: number ): number { const { bounds, width, height, data } = tile; // Convert lat/lon to pixel coordinates const x = ((lon - bounds.west) / (bounds.east - bounds.west)) * (width - 1); const y = ((bounds.north - lat) / (bounds.north - bounds.south)) * (height - 1); // Get surrounding pixel indices const x0 = Math.floor(x); const x1 = Math.min(x0 + 1, width - 1); const y0 = Math.floor(y); const y1 = Math.min(y0 + 1, height - 1); // Interpolation weights const dx = x - x0; const dy = y - y0; // Get elevation at 4 corners const e00 = data[y0 * width + x0]; const e10 = data[y0 * width + x1]; const e01 = data[y1 * width + x0]; const e11 = data[y1 * width + x1]; // Handle void data (SRTM uses -32768 for no data) if (e00 === -32768 || e10 === -32768 || e01 === -32768 || e11 === -32768) { return 0; // Default to sea level for void areas } // Bilinear interpolation const e0 = e00 * (1 - dx) + e10 * dx; const e1 = e01 * (1 - dx) + e11 * dx; return e0 * (1 - dy) + e1 * dy; } /** * Get terrain profile between two points * Useful for line-of-sight analysis */ async getTerrainProfile( lat1: number, lon1: number, lat2: number, lon2: number, numSamples: number = 50 ): Promise { const profile: number[] = []; for (let i = 0; i <= numSamples; i++) { const ratio = i / numSamples; const lat = lat1 + (lat2 - lat1) * ratio; const lon = lon1 + (lon2 - lon1) * ratio; const elevation = await this.getElevation(lat, lon); profile.push(elevation); } return profile; } /** * Get download status for all regions */ async getDownloadStatus(): Promise> { const status: any = {}; for (const region of Object.keys(UKRAINE_REGIONS) as UkraineRegion[]) { const cached = await this.db.terrain.get(region); status[region] = !!cached; } return status; } /** * Get total storage used by terrain data */ async getStorageUsed(): Promise { const tiles = await this.db.terrain.toArray(); return tiles.reduce((sum, tile) => sum + tile.sizeBytes, 0); } /** * Clear terrain cache (free up space) */ async clearCache(region?: UkraineRegion): Promise { if (region) { await this.db.terrain.delete(region); this.cache.delete(region); } else { await this.db.terrain.clear(); this.cache.clear(); } } private isBigEndian(): boolean { const buffer = new ArrayBuffer(2); new DataView(buffer).setInt16(0, 256, true); return new Int16Array(buffer)[0] !== 256; } } ``` --- ## RF Calculation Engine (Universal) ### Core Calculator Class ```typescript // rf/calculator.ts export class RFCalculator { private terrain: TerrainManager; constructor(terrain: TerrainManager) { this.terrain = terrain; } /** * Calculate coverage for multiple sites * Works with any user-provided parameters (LTE, UHF, VHF, etc.) * Uses Web Workers for parallel processing */ async calculateCoverage( sites: Site[], bounds: LatLngBounds, settings: CoverageSettings ): Promise { const startTime = performance.now(); // Generate grid of points to calculate const gridPoints = this.generateGrid(bounds, settings.resolution); console.log( `Calculating coverage for ${sites.length} sites, ` + `${gridPoints.length} points (${settings.resolution}m resolution)` ); // Split work across multiple workers (4 parallel threads) const numWorkers = Math.min(4, navigator.hardwareConcurrency || 4); const workers = this.createWorkers(numWorkers); const chunks = this.chunkArray(gridPoints, numWorkers); // Calculate coverage in parallel const results = await Promise.all( chunks.map((chunk, i) => this.calculateChunk( workers[i], sites, chunk, settings.rsrpThreshold ) ) ); // Merge results from all workers const allPoints = results.flat(); // Cleanup workers workers.forEach(w => w.terminate()); const calculationTime = performance.now() - startTime; console.log( `Coverage calculated: ${allPoints.length} points in ` + `${(calculationTime / 1000).toFixed(2)}s` ); return { points: allPoints, calculationTime, totalPoints: allPoints.length, bounds, settings }; } /** * Generate grid of lat/lon points */ private generateGrid( bounds: LatLngBounds, resolution: number // meters ): GridPoint[] { const points: GridPoint[] = []; // Convert resolution to degrees (approximate) const latStep = resolution / 111000; // ~111km per degree latitude const center = bounds.getCenter(); const lonStep = resolution / (111000 * Math.cos(center.lat * Math.PI / 180)); let lat = bounds.getSouth(); while (lat <= bounds.getNorth()) { let lon = bounds.getWest(); while (lon <= bounds.getEast()) { points.push({ lat, lon }); lon += lonStep; } lat += latStep; } return points; } /** * Create Web Workers for parallel calculation */ private createWorkers(count: number): Worker[] { const workers: Worker[] = []; for (let i = 0; i < count; i++) { workers.push(new Worker('/workers/rf-worker.js')); } return workers; } /** * Split array into chunks for parallel processing */ private chunkArray(array: T[], numChunks: number): T[][] { const chunks: T[][] = []; const chunkSize = Math.ceil(array.length / numChunks); for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } /** * Calculate coverage for a chunk of points (worker task) */ private async calculateChunk( worker: Worker, sites: Site[], points: GridPoint[], rsrpThreshold: number ): Promise { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Worker timeout')); }, 30000); // 30 second timeout worker.postMessage({ type: 'calculate', sites, points, rsrpThreshold }); worker.onmessage = (e) => { clearTimeout(timeout); if (e.data.type === 'complete') { resolve(e.data.results); } else if (e.data.type === 'error') { reject(new Error(e.data.message)); } }; worker.onerror = (err) => { clearTimeout(timeout); reject(err); }; }); } } ``` ### Web Worker Implementation ```typescript // workers/rf-worker.ts // This runs in a separate thread for parallel processing self.onmessage = async (e) => { const { type, sites, points, rsrpThreshold } = e.data; if (type === 'calculate') { try { const results: CoveragePoint[] = []; // Process each point in this chunk for (const point of points) { let bestRSRP = -Infinity; let bestSiteId = ''; // Find best serving site for this point for (const site of sites) { const rsrp = calculatePointRSRP(site, point); if (rsrp > bestRSRP) { bestRSRP = rsrp; bestSiteId = site.id; } } // Only include points above threshold if (bestRSRP >= rsrpThreshold) { results.push({ lat: point.lat, lon: point.lon, rsrp: bestRSRP, siteId: bestSiteId }); } } self.postMessage({ type: 'complete', results }); } catch (error) { self.postMessage({ type: 'error', message: error.message }); } } }; /** * Calculate RSRP at a specific point * Universal formula - works with any frequency */ function calculatePointRSRP(site: any, point: any): number { const distance = haversineDistance( site.lat, site.lon, point.lat, point.lon ); // Free space path loss (universal) const fspl = 20 * Math.log10(distance) + 20 * Math.log10(site.frequency) + 32.45; // Link budget let rsrp = site.power + site.gain - fspl; // TODO: Add terrain loss (requires terrain data in worker) // For MVP, terrain loss will be added in Phase 4 // Apply sector antenna pattern if (site.antennaType === 'sector' && site.azimuth !== undefined) { const bearing = calculateBearing( site.lat, site.lon, point.lat, point.lon ); const relativeAngle = Math.abs(bearing - site.azimuth); const normalizedAngle = relativeAngle > 180 ? 360 - relativeAngle : relativeAngle; const patternLoss = calculateSectorPatternLoss( normalizedAngle, site.beamwidth || 65 ); rsrp -= patternLoss; } return rsrp; } /** * Haversine distance formula */ function haversineDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371; // Earth radius in km const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } /** * Calculate bearing from point A to point B */ function calculateBearing( lat1: number, lon1: number, lat2: number, lon2: number ): number { const dLon = (lon2 - lon1) * Math.PI / 180; const y = Math.sin(dLon) * Math.cos(lat2 * Math.PI / 180); const x = Math.cos(lat1 * Math.PI / 180) * Math.sin(lat2 * Math.PI / 180) - Math.sin(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.cos(dLon); const bearing = Math.atan2(y, x) * 180 / Math.PI; return (bearing + 360) % 360; } /** * Sector antenna pattern loss */ function calculateSectorPatternLoss( angleOffBoresight: number, beamwidth: number ): number { const theta3dB = beamwidth / 2; const sideLobeLevel = 20; if (angleOffBoresight <= theta3dB) { return 12 * Math.pow(angleOffBoresight / theta3dB, 2); } return Math.min( 12 * Math.pow(angleOffBoresight / theta3dB, 2), sideLobeLevel ); } ``` --- ## Development Roadmap ### Phase 1: Core UI & Manual Input (Days 1-2) **Goal:** Working PWA with map and flexible input UI **Tasks:** - [ ] Initialize React + TypeScript + Vite project - [ ] Configure Tailwind CSS - [ ] Setup Workbox for Service Worker - [ ] Create PWA manifest.json - [ ] Implement IndexedDB schema (Dexie) - [ ] Setup Leaflet map component - [ ] Add OpenStreetMap tile layer - [ ] Implement click-to-place site marker - [ ] Create manual input form with: - [ ] Power slider (10-50 dBm) - [ ] Gain slider (0-25 dBi) - [ ] Dynamic frequency input (quick buttons + custom) - [ ] Height slider (1-100m) - [ ] Antenna type selector (omni/sector) - [ ] Sector parameters (azimuth, beamwidth) - [ ] Free-text notes field - [ ] Implement frequency band info display - [ ] Add wavelength calculator - [ ] Create optional quick templates - [ ] Setup Zustand state management - [ ] Add site list panel (view/edit/delete) **Deliverables:** - โœ… Working PWA that can be installed - โœ… Map with OSM tiles (cached offline) - โœ… Can place multiple site markers - โœ… Fully flexible manual input form - โœ… Contextual frequency information **Testing:** ```bash npm run dev # Open http://localhost:5173 # Click on map โ†’ marker appears # Configure site parameters manually # Add multiple sites # Verify all inputs work ``` --- ### Phase 2: RF Calculation Engine (Days 3-4) **Goal:** Working RF calculations in browser (universal) **Tasks:** - [ ] Implement universal FSPL calculator - [ ] Create haversine distance function - [ ] Implement bearing calculator - [ ] Create grid generator - [ ] Implement sector antenna pattern calculator - [ ] Create Web Worker template - [ ] Implement RFCalculator class - [ ] Add multi-site coverage aggregation - [ ] Create signal quality color mapping - [ ] Add calculation progress indicator - [ ] Unit tests for RF functions - [ ] Performance optimization (target < 3s for 3 sites) **Deliverables:** - โœ… Can calculate coverage for single site (any frequency) - โœ… Can calculate composite coverage (multiple sites) - โœ… Calculations run in Web Workers (non-blocking UI) - โœ… Results ready for visualization - โœ… Works with user's custom parameters **Testing:** ```typescript // Test universal FSPL const fspl1800 = calculateFSPL(1.0, 1800); const fspl150 = calculateFSPL(1.0, 150); // VHF console.assert(fspl150 < fspl1800, 'Lower freq = less path loss'); // Test coverage calculation const calculator = new RFCalculator(terrain); const result = await calculator.calculateCoverage( [customSite], // User's parameters bounds, { radius: 5, resolution: 100, rsrpThreshold: -120 } ); console.log(`Calculated ${result.points.length} points`); ``` --- ### Phase 3: Heatmap Visualization (Days 5-6) **Goal:** Beautiful coverage heatmap on map **Tasks:** - [ ] Integrate Leaflet.heat plugin - [ ] Convert coverage points to heatmap format - [ ] Implement color gradient mapping (RSRP โ†’ color) - [ ] Add heatmap layer to map - [ ] Create legend component with RSRP ranges - [ ] Add toggle heatmap visibility - [ ] Implement zoom-dependent detail levels - [ ] Add loading spinner during calculation - [ ] Optimize rendering for 5000+ points - [ ] Add "Download as PNG" feature **Deliverables:** - โœ… Colorful heatmap overlay on map - โœ… Legend showing RSRP ranges and colors - โœ… Smooth performance with large datasets - โœ… Toggle to show/hide heatmap - โœ… Visual feedback during calculation **Testing:** ```typescript // Visual test // 1. Place site with power=43 dBm, freq=1800 MHz // 2. Calculate coverage // 3. Should see green circle near site (~2km radius) // 4. Yellow/orange further away (~3-5km) // 5. Red at edges (~5-7km) // 6. Toggle heatmap on/off smoothly // 7. Performance: rendering < 2s for 3 sites ``` --- ### Phase 4: Terrain Integration (Days 7-9) **Goal:** Real 30m terrain data in calculations **Tasks:** - [ ] Implement enhanced TerrainManager class - [ ] Create 30m SRTM parser (1 arc-second) - [ ] Add byte-order handling (big-endian) - [ ] Implement bilinear interpolation - [ ] Add region detection (lat/lon โ†’ region) - [ ] Implement terrain loss calculation - [ ] Add terrain profile generation - [ ] Create terrain download UI - [ ] Implement auto-download on map pan - [ ] Add download progress indicator (with size display) - [ ] Store terrain in IndexedDB - [ ] Handle offline terrain access - [ ] Add storage management UI - [ ] Test with real Ukraine terrain **Deliverables:** - โœ… Can download 30m terrain regions (~400MB each) - โœ… Terrain data persists offline - โœ… Elevation lookup works with bilinear interpolation - โœ… Coverage calculations consider terrain (more accurate) - โœ… Storage management (view usage, clear cache) **Testing:** ```typescript // Download terrain const terrain = new TerrainManager(db); await terrain.downloadRegion('ukraine-east'); // ~400MB, 30-60s // Check elevation accuracy const dnipro = await terrain.getElevation(48.4647, 35.0462); console.log(`Dnipro elevation: ${dnipro}m`); // Should be ~50-70m // Check storage const used = await terrain.getStorageUsed(); console.log(`Storage used: ${(used / 1024 / 1024).toFixed(0)} MB`); // Test offline // Go offline // Pan to Dnipro // Should still get elevation data from IndexedDB ``` --- ### Phase 5: Multi-Site & UX Polish (Days 10-11) **Goal:** Production-ready MVP **Tasks:** - [ ] Support multiple sites on map (tested with 5+ sites) - [ ] Add site list panel with thumbnails - [ ] Implement site color coding - [ ] Add calculation settings panel (radius, resolution) - [ ] Implement site drag-to-reposition - [ ] Add keyboard shortcuts (Delete, Esc, etc.) - [ ] Create user guide overlay (first-time experience) - [ ] Mobile responsive design - [ ] Touch gestures for mobile (pinch-zoom, pan) - [ ] Add error handling & toast notifications - [ ] Implement undo/redo for site changes - [ ] Add project save/load functionality - [ ] Export coverage as PNG/GeoJSON - [ ] Performance optimization - [ ] Internationalization (Ukrainian/English) **Deliverables:** - โœ… Can manage 5+ sites easily - โœ… Intuitive UI, works on mobile/tablet - โœ… Polished user experience - โœ… Helpful onboarding - โœ… Bilingual support --- ### Phase 6: Backend & Auth (Days 12-13) **Goal:** Optional cloud sync **Tasks:** - [ ] Create FastAPI backend project - [ ] Implement terrain CDN endpoints - [ ] Setup 30m SRTM data preprocessing - [ ] Add MAS OAuth integration - [ ] Create config sync API - [ ] Implement JWT auth middleware - [ ] Setup SQLite for config storage - [ ] Create docker-compose.yml - [ ] Configure Caddy reverse proxy - [ ] Add background sync in Service Worker - [ ] Test multi-device sync **Deliverables:** - โœ… Backend API running - โœ… Can login with MAS - โœ… Configs sync across devices - โœ… Terrain downloads work via CDN --- ### Phase 7: Deployment (Days 14-15) **Goal:** Live at rfcp.eliah.one **Tasks:** - [ ] Setup domain DNS (rfcp.eliah.one) - [ ] Deploy backend to VPS-A - [ ] Deploy frontend with Caddy - [ ] Configure SSL/TLS - [ ] Upload 30m SRTM terrain data - [ ] Test from external network - [ ] Test on mobile devices (iOS/Android) - [ ] Create offline installer ZIP - [ ] Write user documentation - [ ] Setup monitoring (Uptime Kuma) - [ ] Create backup procedures - [ ] Performance testing - [ ] Security audit **Deliverables:** - โœ… Live at https://rfcp.eliah.one - โœ… Works on desktop/mobile/tablet - โœ… Offline installer available (~200MB base + terrain) - โœ… Documentation complete (Ukrainian/English) - โœ… Monitoring active --- ## Post-MVP Roadmap ### v1.1 - UHF/VHF Support (Week 4-5) ๐ŸŽฏ **Goal:** Add tactical radio frequency planning **New Features:** - [ ] VHF frequencies (30-300 MHz) - [ ] VHF Low (30-50 MHz) - long range - [ ] VHF High (136-174 MHz) - tactical radio - [ ] FM broadcast (88-108 MHz) - [ ] UHF frequencies (300-3000 MHz) - [ ] UHF (400-470 MHz) - military tactical - [ ] PMR446 (446 MHz) - personal radio - [ ] Amateur radio bands - [ ] Enhanced propagation models: - [ ] Longley-Rice (VHF/UHF specific) - [ ] ITU-R P.1546 (broadcasting) - [ ] Frequency-specific characteristics: - [ ] Ground wave propagation (VHF low) - [ ] Ionospheric reflection (HF) - [ ] Tropospheric scatter - [ ] New UI elements: - [ ] "Radio Type" selector (LTE/VHF/UHF/Amateur) - [ ] Band-specific presets - [ ] Propagation mode selector **Technical Implementation:** ```typescript // Extended frequency support export const RADIO_TYPES = { 'lte': { name: 'LTE (4G)', freqRange: [700, 2700], propagationModel: 'FSPL + terrain' }, 'vhf': { name: 'VHF Radio', freqRange: [30, 300], propagationModel: 'Longley-Rice' }, 'uhf': { name: 'UHF Radio', freqRange: [300, 3000], propagationModel: 'Longley-Rice' }, 'amateur': { name: 'Amateur Radio', freqRange: [1.8, 30000], // HF to SHF propagationModel: 'Mixed' } }; ``` **Use Cases:** - โœ… Planning tactical radio networks (VHF 150 MHz) - โœ… UHF repeater placement (450 MHz) - โœ… Amateur radio link planning - โœ… Emergency services coordination --- ### v1.2 - Advanced Features (Week 6-7) **Features:** - [ ] Okumura-Hata propagation model (urban areas) - [ ] COST 231 extension (dense urban) - [ ] 3D sector antenna visualization - [ ] Interference calculation between sites - [ ] Best server boundary display - [ ] Handover zone visualization - [ ] Link budget calculator - [ ] Export to KML/KMZ (Google Earth) - [ ] PDF report generation - [ ] CSV import/export - [ ] Real RF measurements overlay --- ### v2.0 - Advanced Platform (Month 3+) **Features:** - [ ] 3D terrain visualization (Three.js) - [ ] Drive test integration - [ ] Network optimization algorithms - [ ] Machine learning coverage prediction - [ ] Multi-technology support (5G NR) - [ ] Collaborative planning (WebRTC) - [ ] Cost estimation - [ ] Weather effects modeling - [ ] Real-time spectrum monitoring --- ## File Structure ``` rfcp/ โ”œโ”€โ”€ .github/ โ”‚ โ””โ”€โ”€ workflows/ โ”‚ โ””โ”€โ”€ deploy.yml โ”‚ โ”œโ”€โ”€ frontend/ โ”‚ โ”œโ”€โ”€ public/ โ”‚ โ”‚ โ”œโ”€โ”€ icons/ # PWA icons โ”‚ โ”‚ โ”œโ”€โ”€ workers/ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ rf-worker.js # RF calculation worker โ”‚ โ”‚ โ”œโ”€โ”€ manifest.json # PWA manifest โ”‚ โ”‚ โ”œโ”€โ”€ offline.html # Offline fallback โ”‚ โ”‚ โ””โ”€โ”€ robots.txt โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ”œโ”€โ”€ components/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ map/ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Map.tsx # Main map component โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SiteMarker.tsx # Site marker โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Heatmap.tsx # Heatmap layer โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Legend.tsx # Signal legend โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ panels/ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SiteForm.tsx # Manual input form โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SiteList.tsx # Sites list โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Settings.tsx # Settings panel โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TerrainPanel.tsx # Terrain management โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ FrequencySelector.tsx # Dynamic freq input โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ui/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Button.tsx โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Input.tsx โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Slider.tsx โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ Select.tsx โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ Toast.tsx โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ rf/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ calculator.ts # Main RF calculator โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ fspl.ts # Universal FSPL โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain-loss.ts # Terrain calculations โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ antenna-pattern.ts # Sector patterns โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ utils.ts # Haversine, bearing, etc. โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ manager.ts # TerrainManager (30m) โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ regions.ts # Ukraine regions โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ parser.ts # SRTM parser โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ interpolation.ts # Bilinear interpolation โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ store/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sites.ts # Sites state (Zustand) โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ settings.ts # Settings state โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ coverage.ts # Coverage state โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.ts # Auth state โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ db/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ schema.ts # IndexedDB schema โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ migrations.ts # DB migrations โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ api/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ client.ts # API client โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.ts # Terrain endpoints โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sync.ts # Sync endpoints โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.ts # Auth endpoints โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ types/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ site.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ coverage.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ frequency.ts โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ constants/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ frequencies.ts # Common bands โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ rsrp-thresholds.ts โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ colors.ts โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ utils/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ geo.ts # Geographic utilities โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ colors.ts # Color utilities โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ format.ts # Formatters โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ validation.ts # Input validation โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hooks/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ useRFCalculator.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ useTerrain.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ useOffline.ts โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ useFrequencyInfo.ts โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ i18n/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ uk.json # Ukrainian โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ en.json # English โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ App.tsx โ”‚ โ”‚ โ”œโ”€โ”€ main.tsx โ”‚ โ”‚ โ”œโ”€โ”€ service-worker.ts # Service Worker โ”‚ โ”‚ โ””โ”€โ”€ vite-env.d.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ tests/ โ”‚ โ”‚ โ”œโ”€โ”€ unit/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ fspl.test.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ haversine.test.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.test.ts โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ antenna-pattern.test.ts โ”‚ โ”‚ โ””โ”€โ”€ integration/ โ”‚ โ”‚ โ”œโ”€โ”€ calculator.test.ts โ”‚ โ”‚ โ””โ”€โ”€ terrain-manager.test.ts โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ .gitignore โ”‚ โ”œโ”€โ”€ package.json โ”‚ โ”œโ”€โ”€ tsconfig.json โ”‚ โ”œโ”€โ”€ vite.config.ts โ”‚ โ”œโ”€โ”€ tailwind.config.js โ”‚ โ””โ”€โ”€ README.md โ”‚ โ”œโ”€โ”€ backend/ โ”‚ โ”œโ”€โ”€ app/ โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”‚ โ”œโ”€โ”€ main.py # FastAPI app โ”‚ โ”‚ โ”œโ”€โ”€ config.py # Configuration โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ api/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.py # Terrain CDN endpoints โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sync.py # Sync endpoints โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.py # MAS OAuth โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ services/ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ terrain.py # Terrain service โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ sync.py # Sync service โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ auth.py # Auth service โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ models/ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ config.py # Pydantic models โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ db/ โ”‚ โ”‚ โ””โ”€โ”€ database.py # SQLite setup โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ data/ โ”‚ โ”‚ โ””โ”€โ”€ terrain/ # 30m SRTM files โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-west.hgt โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-center.hgt โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-east.hgt โ”‚ โ”‚ โ”œโ”€โ”€ ukraine-south.hgt โ”‚ โ”‚ โ””โ”€โ”€ ukraine-north.hgt โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ scripts/ โ”‚ โ”‚ โ”œโ”€โ”€ download-srtm.py # Download SRTM data โ”‚ โ”‚ โ””โ”€โ”€ preprocess-terrain.py # Preprocess for web โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ tests/ โ”‚ โ”‚ โ”œโ”€โ”€ test_terrain.py โ”‚ โ”‚ โ””โ”€โ”€ test_sync.py โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ .gitignore โ”‚ โ”œโ”€โ”€ requirements.txt โ”‚ โ”œโ”€โ”€ Dockerfile โ”‚ โ””โ”€โ”€ README.md โ”‚ โ”œโ”€โ”€ docs/ โ”‚ โ”œโ”€โ”€ README.md # Project overview โ”‚ โ”œโ”€โ”€ USAGE-uk.md # User guide (Ukrainian) โ”‚ โ”œโ”€โ”€ USAGE-en.md # User guide (English) โ”‚ โ”œโ”€โ”€ API.md # API documentation โ”‚ โ””โ”€โ”€ DEPLOYMENT.md # Deployment guide โ”‚ โ”œโ”€โ”€ docker-compose.yml โ”œโ”€โ”€ Caddyfile โ”œโ”€โ”€ .gitignore โ”œโ”€โ”€ LICENSE โ””โ”€โ”€ RFCP-TechSpec-v3.0.md # This file ``` --- ## Performance Targets ### Load Time - Initial page load (online): < 3 seconds - Initial page load (offline, cached): < 1 second - Service Worker registration: < 500ms - PWA installation prompt: < 2 seconds after load ### Calculation Performance - Single site, 5km radius, 100m resolution: < 1 second - Three sites, 5km radius, 100m resolution: < 2.5 seconds - Five sites, 10km radius, 100m resolution: < 8 seconds - With 30m terrain data: +20% calculation time (acceptable) ### Rendering Performance - Heatmap render (1000 points): < 500ms - Heatmap render (5000 points): < 1.5 seconds - Heatmap render (10000 points): < 3 seconds - Map pan/zoom: 60 FPS (smooth) ### Memory Usage - Base app: < 100MB - With 3 sites calculated: < 200MB - With 30m terrain loaded (1 region): < 600MB - With 30m terrain loaded (3 regions): < 1.5GB ### Storage - Base app cache: ~15MB - 30m terrain per region: ~400MB - User projects: < 1MB each - Total max storage: ~1.5GB (for 3 regions) ### Network - Terrain download speed: depends on connection (30-60s for 400MB on 4G) - API response time: < 500ms - OAuth flow: < 2 seconds --- ## Security & Privacy ### PWA Security - Service Worker scope limited to origin - Content Security Policy (CSP) headers - No eval() or inline scripts - HTTPS required for production - Subresource Integrity (SRI) for CDN assets ### Data Privacy - All calculations client-side (no data sent to server) - Terrain data cached locally - Optional cloud sync requires authentication - User projects encrypted in IndexedDB (future) - No analytics or tracking ### Authentication - OAuth 2.0 with MAS - JWT tokens with short expiry (1 hour) - Refresh token rotation - No password storage - Logout clears all tokens ### Network Security - TLS 1.3 required - HSTS enabled - Certificate pinning (future) - Rate limiting on API (100 req/hour) - CORS properly configured --- ## Testing Strategy ### Unit Tests ```typescript // tests/unit/fspl.test.ts import { describe, it, expect } from 'vitest'; import { calculateFSPL } from '@/rf/fspl'; describe('Universal FSPL Calculator', () => { it('calculates correct path loss at 1km, 1800MHz', () => { const fspl = calculateFSPL(1.0, 1800); expect(fspl).toBeCloseTo(92.5, 1); }); it('calculates correct path loss for VHF (150 MHz)', () => { const fspl = calculateFSPL(1.0, 150); expect(fspl).toBeCloseTo(71.0, 1); }); it('lower frequency = less path loss', () => { const fspl150 = calculateFSPL(5.0, 150); const fspl1800 = calculateFSPL(5.0, 1800); expect(fspl150).toBeLessThan(fspl1800); }); it('handles zero distance', () => { const fspl = calculateFSPL(0, 1800); expect(fspl).toBe(0); }); }); ``` ### Integration Tests ```typescript // tests/integration/calculator.test.ts import { describe, it, expect, beforeAll } from 'vitest'; import { RFCalculator } from '@/rf/calculator'; import { TerrainManager } from '@/terrain/manager'; describe('RF Calculator Integration', () => { let calculator: RFCalculator; beforeAll(() => { const terrain = new TerrainManager(db); calculator = new RFCalculator(terrain); }); it('calculates coverage for single LTE site', async () => { const lteSite: Site = { id: '1', name: 'Test LTE', lat: 48.0, lon: 35.0, height: 30, power: 43, gain: 8, frequency: 1800, antennaType: 'omni', // ... other fields }; const result = await calculator.calculateCoverage( [lteSite], testBounds, { radius: 5, resolution: 100, rsrpThreshold: -120 } ); expect(result.points.length).toBeGreaterThan(0); expect(result.calculationTime).toBeLessThan(2000); }); it('calculates coverage for custom frequency', async () => { const customSite: Site = { // ... LTE site params frequency: 450, // UHF }; const result = await calculator.calculateCoverage([customSite], bounds, settings); expect(result.points.length).toBeGreaterThan(0); }); }); ``` ### E2E Tests ```typescript // tests/e2e/coverage-flow.spec.ts import { test, expect } from '@playwright/test'; test('complete coverage calculation flow with manual input', async ({ page }) => { await page.goto('http://localhost:5173'); await page.waitForSelector('.leaflet-container'); // Click on map to place site await page.click('.leaflet-container', { position: { x: 400, y: 300 } }); // Fill in manual parameters await page.fill('input[name="name"]', 'Test Station'); await page.getByLabel('ะŸะพั‚ัƒะถะฝั–ัั‚ัŒ ะฟะตั€ะตะดะฐะฒะฐั‡ะฐ').fill('43'); await page.getByLabel('ะšะพะตั„ั–ั†ั–ั”ะฝั‚ ะฐะฝั‚ะตะฝะธ').fill('8'); // Select frequency await page.click('button:has-text("1800")'); await page.getByLabel('ะ’ะธัะพั‚ะฐ ะฐะฝั‚ะตะฝะธ').fill('30'); // Save site await page.click('button:has-text("ะ—ะฑะตั€ะตะณั‚ะธ")'); // Calculate coverage await page.click('button:has-text("ะ ะพะทั€ะฐั…ัƒะฒะฐั‚ะธ ะฟะพะบั€ะธั‚ั‚ั")'); // Wait for heatmap await page.waitForSelector('.leaflet-heatmap-layer', { timeout: 10000 }); // Verify heatmap is visible const heatmap = await page.locator('.leaflet-heatmap-layer'); await expect(heatmap).toBeVisible(); }); ``` --- ## Deployment ### Prerequisites ```bash # VPS requirements - Ubuntu 22.04 LTS - 4GB RAM (for terrain processing) - 30GB disk space (20GB for terrain data) - Docker + Docker Compose installed - Domain configured (rfcp.eliah.one) ``` ### Deployment Steps ```bash # 1. Clone repository git clone https://git.eliah.one/mytec/rfcp.git cd rfcp # 2. Configure environment cp .env.example .env nano .env # 3. Download and preprocess 30m SRTM terrain data cd backend python scripts/download-srtm.py # Downloads raw SRTM python scripts/preprocess-terrain.py # Converts to optimized format cd .. # 4. Build containers docker-compose build # 5. Start services docker-compose up -d # 6. Check status docker-compose ps curl https://rfcp.eliah.one/health curl https://rfcp.eliah.one/api/terrain/ukraine-east # Should return 400MB file ``` ### Docker Compose ```yaml version: '3.8' services: frontend: build: context: ./frontend dockerfile: Dockerfile environment: - NODE_ENV=production - VITE_API_URL=https://rfcp.eliah.one/api restart: unless-stopped backend: build: context: ./backend dockerfile: Dockerfile environment: - ENVIRONMENT=production - MAS_URL=https://mas.umtc.dev - DATABASE_URL=sqlite:///data/rfcp.db volumes: - ./backend/data:/app/data - terrain_data:/app/data/terrain restart: unless-stopped caddy: image: caddy:2-alpine ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data - caddy_config:/config restart: unless-stopped volumes: caddy_data: caddy_config: terrain_data: ``` ### Caddyfile ``` rfcp.eliah.one { # Frontend reverse_proxy frontend:5173 # Backend API reverse_proxy /api/* backend:8000 # Security headers header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.openstreetmap.org; worker-src 'self' blob:; connect-src 'self' https://rfcp.eliah.one" } # Cache static assets @static { path *.js *.css *.woff2 *.png *.jpg *.svg } header @static Cache-Control "public, max-age=31536000" # Service Worker - no cache @sw { path /service-worker.js } header @sw Cache-Control "no-cache" # Terrain data - long cache @terrain { path /api/terrain/* } header @terrain Cache-Control "public, max-age=7776000" # 90 days } ``` --- ## Success Metrics ### MVP Success Criteria โœ… **Technical:** - PWA installable on mobile/desktop/tablet - Works offline after initial load - Coverage calculation < 3 seconds (3 sites, 5km, 100m) - Heatmap renders smoothly (< 2s for 5000 points) - Supports 5+ sites simultaneously - 30m terrain integration working - Manual input fully functional โœ… **User Experience:** - Intuitive UI (no tutorial needed for basic tasks) - Works on 4G tablet - Terrain data downloads automatically - Can save and load projects - Bilingual support (UK/EN) - Helpful contextual info (band info, wavelength) โœ… **Deployment:** - Live at https://rfcp.eliah.one - 99% uptime - Accessible from military networks - Offline installer available (~200MB + terrain) ### KPIs - **Performance:** 95% of calculations complete in < 3s - **Adoption:** 20+ active users in first month - **Reliability:** < 1 critical bug per week post-launch - **Offline Usage:** 60%+ of sessions offline - **Terrain Downloads:** Average 2-3 regions per user --- ## Open Questions & Next Steps ### Questions Answered โœ… 1. โœ… **Terrain detail:** 30m SRTM (1 arc-second) 2. โœ… **Frequency bands:** Band 3, 7, 20, 2 + dynamic input (any 400-6000 MHz) 3. โœ… **Input method:** Manual input > equipment presets 4. โœ… **Equipment parameters:** Not critical for MVP, add later 5. โœ… **RF measurements:** v1.1 feature (CSV import) 6. โœ… **Languages:** Ukrainian + English only ### Next Actions 1. **Initialize Project** โœ… - [x] Git repo created - [x] TechSpec finalized - [ ] Start Phase 1 implementation 2. **Phase 1 Start** (Tomorrow) - [ ] Setup React + TypeScript + Vite - [ ] Configure Tailwind - [ ] Create manual input UI - [ ] Integrate Leaflet map 3. **Ongoing** - [ ] Weekly progress reviews - [ ] Adjust roadmap based on feedback - [ ] Test on real devices (tablets) --- ## Contact & Support **Project Lead:** ะžะปะตะณ (mytec) **Repository:** https://git.eliah.one/mytec/rfcp **Documentation:** https://git.eliah.one/mytec/rfcp/wiki **Issues:** https://git.eliah.one/mytec/rfcp/issues **Deployment:** https://rfcp.eliah.one --- **Document Version:** 3.0 (Final) **Last Updated:** 2025-01-30 **Status:** ๐Ÿš€ Ready for Development **Next Action:** Phase 1 Implementation - Manual Input UI **Changes from v2.0:** - โœ… 30m SRTM resolution (vs 90m) - โœ… Manual input UI (vs equipment presets) - โœ… Dynamic frequency input (vs fixed dropdown) - โœ… Removed equipment database - โœ… Added UHF/VHF roadmap (v1.1) - โœ… Enhanced terrain manager for 30m data - โœ… Updated UI mockups for manual input - โœ… Clarified typical deployment (2-3 regions)