@mytec Phase 1 - Core UI & Manual Input, Phase 2 - RF Calculation Engine, Phase 3 - Heatmap Visualization

This commit is contained in:
2026-01-30 07:12:00 +02:00
parent 343c8e078d
commit 18a7d6de81
41 changed files with 6014 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
/**
* Calculate antenna pattern loss for sector antennas.
* Based on 3GPP antenna model.
*
* @param angleOffBoresight - Angle from antenna boresight (0-180 degrees)
* @param beamwidth - 3dB beamwidth in degrees
* @returns Pattern loss in dB
*/
export function calculateSectorPatternLoss(
angleOffBoresight: number,
beamwidth: number = 65
): number {
const theta3dB = beamwidth / 2;
const sideLobeLevel = 20; // dB
return Math.min(
12 * Math.pow(angleOffBoresight / theta3dB, 2),
sideLobeLevel
);
}

View File

@@ -0,0 +1,134 @@
import type { Site, CoveragePoint, CoverageResult, CoverageSettings, GridPoint } from '@/types/index.ts';
export class RFCalculator {
/**
* Calculate coverage for multiple sites using Web Workers.
*/
async calculateCoverage(
sites: Site[],
mapBounds: { north: number; south: number; east: number; west: number },
settings: CoverageSettings
): Promise<CoverageResult> {
const startTime = performance.now();
// Generate grid of points
const gridPoints = this.generateGrid(mapBounds, settings.resolution);
// Determine number of workers
const numWorkers = Math.min(4, navigator.hardwareConcurrency || 4);
const chunks = this.chunkArray(gridPoints, numWorkers);
// Serialize site data for workers
const sitesData = sites.map((s) => ({
id: s.id,
lat: s.lat,
lon: s.lon,
height: s.height,
power: s.power,
gain: s.gain,
frequency: s.frequency,
antennaType: s.antennaType,
azimuth: s.azimuth,
beamwidth: s.beamwidth,
}));
// Calculate in parallel using Web Workers
const workers: Worker[] = [];
const promises: Promise<CoveragePoint[]>[] = [];
for (let i = 0; i < chunks.length; i++) {
const worker = new Worker('/workers/rf-worker.js');
workers.push(worker);
promises.push(
this.calculateChunk(worker, sitesData, chunks[i], settings.rsrpThreshold)
);
}
const results = await Promise.all(promises);
// Cleanup workers
workers.forEach((w) => w.terminate());
// Merge results
const allPoints = results.flat();
const calculationTime = performance.now() - startTime;
return {
points: allPoints,
calculationTime,
totalPoints: allPoints.length,
settings,
};
}
private generateGrid(
bounds: { north: number; south: number; east: number; west: number },
resolution: number
): GridPoint[] {
const points: GridPoint[] = [];
// Convert resolution to degrees
const latStep = resolution / 111000; // ~111km per degree latitude
const centerLat = (bounds.north + bounds.south) / 2;
const lonStep =
resolution / (111000 * Math.cos((centerLat * Math.PI) / 180));
let lat = bounds.south;
while (lat <= bounds.north) {
let lon = bounds.west;
while (lon <= bounds.east) {
points.push({ lat, lon });
lon += lonStep;
}
lat += latStep;
}
return points;
}
private chunkArray<T>(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;
}
private calculateChunk(
worker: Worker,
sites: unknown[],
points: GridPoint[],
rsrpThreshold: number
): Promise<CoveragePoint[]> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Worker timeout'));
}, 30000);
worker.postMessage({
type: 'calculate',
sites,
points,
rsrpThreshold,
});
worker.onmessage = (e: MessageEvent) => {
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);
};
});
}
}

21
frontend/src/rf/fspl.ts Normal file
View File

@@ -0,0 +1,21 @@
/**
* Calculate Free Space Path Loss.
* Universal for all RF frequencies.
*
* FSPL(dB) = 20*log10(d_km) + 20*log10(f_MHz) + 32.45
*
* @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;
return (
20 * Math.log10(distanceKm) +
20 * Math.log10(frequencyMHz) +
32.45
);
}

47
frontend/src/rf/utils.ts Normal file
View File

@@ -0,0 +1,47 @@
/**
* Haversine distance between two lat/lon points.
* @returns Distance in kilometers
*/
export 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.
* @returns Bearing in degrees (0-360)
*/
export function calculateBearing(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const lat1Rad = (lat1 * Math.PI) / 180;
const lat2Rad = (lat2 * Math.PI) / 180;
const y = Math.sin(dLon) * Math.cos(lat2Rad);
const x =
Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLon);
const bearing = (Math.atan2(y, x) * 180) / Math.PI;
return (bearing + 360) % 360;
}