Files
rfcp/frontend/public/workers/rf-worker.js
2026-01-30 12:12:13 +02:00

166 lines
4.6 KiB
JavaScript

// RF Coverage Calculation Web Worker
// Runs in a separate thread for parallel processing
self.onmessage = function (e) {
const { type, sites, points, rsrpThreshold } = e.data;
if (type === 'calculate') {
try {
const results = [];
for (let i = 0; i < points.length; i++) {
const point = points[i];
let bestRSRP = -Infinity;
let bestSiteId = '';
for (let j = 0; j < sites.length; j++) {
const site = sites[j];
const rsrp = calculatePointRSRP(site, point);
if (rsrp > bestRSRP) {
bestRSRP = rsrp;
bestSiteId = site.id;
}
}
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)
*
* RSRP = P_tx + G_tx - FSPL - PatternLoss
*
* Includes:
* - Antenna gain (site.gain)
* - 3GPP sector pattern with back lobe (no hard cutoff)
* - Radio horizon limit based on antenna height
*/
function calculatePointRSRP(site, point) {
var distance = haversineDistance(site.lat, site.lon, point.lat, point.lon);
// Minimum distance to prevent -Infinity
if (distance < 0.01) distance = 0.01;
// Radio horizon check: d_horizon = 3.57 * sqrt(h_meters)
// Uses 4/3 Earth radius for standard atmospheric refraction
var siteHeight = site.height || 10; // default 10m if missing
var horizon = calculateRadioHorizon(siteHeight);
if (distance > horizon) {
return -Infinity;
}
// Free space path loss (universal)
var fspl =
20 * Math.log10(distance) + 20 * Math.log10(site.frequency) + 32.45;
// Link budget: RSRP = P_tx + G_tx - FSPL
var rsrp = site.power + site.gain - fspl;
// Apply 3GPP sector antenna pattern (main lobe + side lobes + back lobe)
// No hard cutoff — back lobe is attenuated by front-to-back ratio (~25 dB)
if (site.antennaType === 'sector' && site.azimuth !== undefined) {
var bearing = calculateBearing(site.lat, site.lon, point.lat, point.lon);
var beamwidth = site.beamwidth || 65;
var frontBackRatio = 25; // dB, typical for sector panel antennas
var patternLoss = calculate3GPPPattern(
site.azimuth,
bearing,
beamwidth,
frontBackRatio
);
rsrp -= patternLoss; // patternLoss is positive dB
}
return rsrp;
}
/**
* Radio horizon distance in km.
* d = 3.57 * sqrt(h) where h is antenna height in meters.
* Accounts for 4/3 Earth radius (standard atmosphere refraction).
*/
function calculateRadioHorizon(heightMeters) {
return 3.57 * Math.sqrt(heightMeters);
}
/**
* 3GPP TR 36.814 Horizontal Antenna Pattern.
*
* A(θ) = min[ 12 * (θ / θ_3dB)², A_m ]
*
* Returns POSITIVE dB loss value (to be subtracted from RSRP).
*
* - θ = angular offset from boresight (0-180°)
* - θ_3dB = half-power beamwidth / 2
* - A_m = maximum attenuation = front-to-back ratio
*
* At 0° → 0 dB loss (boresight)
* At ±θ_3dB → 3 dB loss (half-power points)
* At ±90° → capped at A_m (~25 dB)
* At 180° → capped at A_m (~25 dB, back lobe)
*/
function calculate3GPPPattern(azimuth, bearing, beamwidth, frontBackRatio) {
// Normalize angle difference to -180…+180
var angleDiff = bearing - azimuth;
while (angleDiff > 180) angleDiff -= 360;
while (angleDiff < -180) angleDiff += 360;
var theta = Math.abs(angleDiff);
var theta3dB = beamwidth / 2;
var Am = frontBackRatio;
return Math.min(12 * Math.pow(theta / theta3dB, 2), Am);
}
/**
* Haversine distance in km
*/
function haversineDistance(lat1, lon1, lat2, lon2) {
var R = 6371;
var dLat = ((lat2 - lat1) * Math.PI) / 180;
var dLon = ((lon2 - lon1) * Math.PI) / 180;
var 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);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Calculate bearing from A to B in degrees (0-360)
*/
function calculateBearing(lat1, lon1, lat2, lon2) {
var dLon = ((lon2 - lon1) * Math.PI) / 180;
var lat1Rad = (lat1 * Math.PI) / 180;
var lat2Rad = (lat2 * Math.PI) / 180;
var y = Math.sin(dLon) * Math.cos(lat2Rad);
var x =
Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLon);
var bearing = (Math.atan2(y, x) * 180) / Math.PI;
return (bearing + 360) % 360;
}