@mytec Phase 1 - Core UI & Manual Input, Phase 2 - RF Calculation Engine, Phase 3 - Heatmap Visualization
This commit is contained in:
20
frontend/src/rf/antenna-pattern.ts
Normal file
20
frontend/src/rf/antenna-pattern.ts
Normal 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
|
||||
);
|
||||
}
|
||||
134
frontend/src/rf/calculator.ts
Normal file
134
frontend/src/rf/calculator.ts
Normal 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
21
frontend/src/rf/fspl.ts
Normal 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
47
frontend/src/rf/utils.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user