// 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; }