From 65fc84ff9f9b128e7e243ca110bfa038533448f9 Mon Sep 17 00:00:00 2001 From: Adam Wisher <37659188+mrwish7@users.noreply.github.com> Date: Mon, 12 May 2025 22:58:23 +0100 Subject: [PATCH] Local database cache for TX ID Locally cache transmitter database for TX ID and visually show when multiple matches on frontend --- server/datahandler.js | 3 +- server/tx_search.js | 214 +++++++++++++++------------------------- web/css/breadcrumbs.css | 8 ++ web/index.ejs | 2 +- web/js/main.js | 1 + 5 files changed, 92 insertions(+), 136 deletions(-) diff --git a/server/datahandler.js b/server/datahandler.js index 7d54f1a..1f4af93 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -416,7 +416,8 @@ function handleData(wss, receivedData, rdsWss) { azi: currentTx.azimuth, id: currentTx.id, pi: currentTx.pi, - reg: currentTx.reg + reg: currentTx.reg, + otherMatches: currentTx.others }; } }) diff --git a/server/tx_search.js b/server/tx_search.js index 68bca46..b5556e9 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -2,7 +2,7 @@ const fetch = require('node-fetch'); const { serverConfig } = require('./server_config'); const consoleCmd = require('./console'); -let cachedData = {}; +let localDb = {}; let lastFetchTime = 0; const fetchInterval = 1000; const esSwitchCache = {"lastCheck":0, "esSwitch":false}; @@ -28,6 +28,19 @@ if (typeof algorithms[algoSetting] !== 'undefined') { weightedDist = algorithms[algoSetting][1]; } +// IIFE to build the local TX DB cache from the endpoint. +(async () => { + try { + consoleCmd.logInfo('Fetching transmitter database...'); + const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`); + if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); + localDb = await response.json(); + consoleCmd.logInfo('Transmitter database successfully loaded.'); + } catch (error) { + consoleCmd.logError("Failed to fetch transmitter database:", error); + } +})(); + // Load the US states GeoJSON data async function loadUsStatesGeoJson() { if (!usStatesGeoJson) { @@ -118,8 +131,28 @@ function validPsCompare(rdsPs, stationPs) { return false; } +function evaluateStation(station) { + let esMode = checkEs(); + let weightDistance = station.distanceKm; + if (esMode && station.distanceKm > 500) { + weightDistance = Math.abs(station.distanceKm - 1500); + } + let erp = station.erp && station.erp > 0 ? station.erp : 1; + let extraWeight = erp > 30 && station.distanceKm <= 500 ? 0.3 : 0; + let score = 0; + // If ERP is 1W, use a simpler formula to avoid zero-scoring. + if (erp === 0.001) { + score = erp / station.distanceKm; + } else { + score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight; + } + return score; +} + // Fetch data from maps async function fetchTx(freq, piCode, rdsPs) { + let match = null; + let multiMatches = []; const now = Date.now(); freq = parseFloat(freq); @@ -127,161 +160,74 @@ async function fetchTx(freq, piCode, rdsPs) { if (now - lastFetchTime < fetchInterval || serverConfig.identification.lat.length < 2 || freq < 87 + || Object.keys(localDb).length === 0 || (currentPiCode == piCode && currentRdsPs == rdsPs)) { return Promise.resolve(); } lastFetchTime = now; + if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); - if (cachedData[freq]) { - return processData(cachedData[freq], piCode, rdsPs); + const filteredLocations = Object.values(localDb.locations) + .map(locData => ({ + ...locData, + stations: locData.stations.filter(station => + station.freq === freq && + (station.pi === piCode.toUpperCase() || station.pireg === piCode.toUpperCase() ) && + validPsCompare(rdsPs, station.ps) + ) + })) + .filter(locData => locData.stations.length > 0); // Ensure locations with at least one matching station remain + + for (loc of filteredLocations) { + loc = Object.assign(loc, loc.stations[0]); + delete loc.stations; + const dist = haversine(serverConfig.identification.lat, serverConfig.identification.lon, loc.lat, loc.lon); + loc = Object.assign(loc, dist); + loc.detectedByPireg = (loc.pireg === piCode.toUpperCase()); } - - const url = "https://maps.fmdx.org/api/?freq=" + freq; - - try { - // Try POST first - const postResponse = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ freq }), // You can omit or customize this - redirect: 'manual' - }); - - if (!postResponse.ok) throw new Error(`POST failed: ${postResponse.status}`); - const data = await postResponse.json(); - cachedData[freq] = data; - if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); - return processData(data, piCode, rdsPs); - } catch (postError) { - console.warn("POST failed, trying GET:", postError); - - try { - const getResponse = await fetch(url, { redirect: 'manual' }); - if (!getResponse.ok) throw new Error(`GET failed: ${getResponse.status}`); - const data = await getResponse.json(); - cachedData[freq] = data; - if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); - return processData(data, piCode, rdsPs); - } catch (getError) { - console.error("GET also failed:", getError); - return null; + + if (filteredLocations.length > 1) { + for (loc of filteredLocations) { + loc.score = evaluateStation(loc); } + match = filteredLocations.reduce((max, obj) => obj.score > max.score ? obj : max, filteredLocations[0]); + multiMatches = filteredLocations.filter(obj => obj !== match); + } else if (filteredLocations.length === 1) { + match = filteredLocations[0]; } -} -async function processData(data, piCode, rdsPs) { - let matchingStation = null; - let matchingCity = null; - let maxScore = -Infinity; - let txAzimuth; - let maxDistance; - let esMode = checkEs(); - let detectedByPireg = false; - currentPiCode = piCode; - currentRdsPs = rdsPs; - - // First, collect all stations that match the piCode (without PS comparison). - let stationsForPi = []; - for (const cityId in data.locations) { - const city = data.locations[cityId]; - if (city.stations) { - for (const station of city.stations) { - if (station.pi === piCode.toUpperCase() && !station.extra) { - stationsForPi.push({ station, city }); - } + if (match) { + if (match.itu === 'USA') { + const state = getStateForCoordinates(match.lat, match.lon); + if (state) { + match.state = state; // Add state to matchingCity } } - } - - if (stationsForPi.length > 0) { - for (const { station, city } of stationsForPi) { - if (station.ps && validPsCompare(rdsPs, station.ps)) { - const distance = haversine(serverConfig.identification.lat, serverConfig.identification.lon, city.lat, city.lon); - evaluateStation(station, city, distance); - detectedByPireg = false; - } - } - } - - // Fallback: Check using pireg if no match was found using the piCode (with valid PS comparison) - if (!matchingStation) { - for (const cityId in data.locations) { - const city = data.locations[cityId]; - if (city.stations) { - for (const station of city.stations) { - if ( - station.pireg && - station.pireg.toUpperCase() === piCode.toUpperCase() && - !station.extra && - station.ps && - validPsCompare(rdsPs, station.ps) - ) { - const distance = haversine(serverConfig.identification.lat, serverConfig.identification.lon, city.lat, city.lon); - evaluateStation(station, city, distance); - detectedByPireg = true; - } - } - } - } - } - - // Determine the state if the city is in the USA - if (matchingStation && matchingCity.itu === 'USA') { - const state = getStateForCoordinates(matchingCity.lat, matchingCity.lon); - if (state) { - matchingCity.state = state; // Add state to matchingCity - } - } - - if (matchingStation) { return { - station: detectedByPireg - ? `${matchingStation.station.replace("R.", "Radio ")}${matchingStation.regname ? ' ' + matchingStation.regname : ''}` - : matchingStation.station.replace("R.", "Radio "), - pol: matchingStation.pol.toUpperCase(), - erp: matchingStation.erp && matchingStation.erp > 0 ? matchingStation.erp : '?', - city: matchingCity.name, - itu: matchingCity.state ? matchingCity.state + ', ' + matchingCity.itu : matchingCity.itu, - distance: maxDistance.toFixed(0), - azimuth: txAzimuth.toFixed(0), - id: matchingStation.id, - pi: matchingStation.pi, + station: match.detectedByPireg + ? `${match.station.replace("R.", "Radio ")}${match.regname ? ' ' + match.regname : ''}` + : match.station.replace("R.", "Radio "), + pol: match.pol.toUpperCase(), + erp: match.erp && match.erp > 0 ? match.erp : '?', + city: match.name, + itu: match.state ? match.state + ', ' + match.itu : match.itu, + distance: match.distanceKm.toFixed(0), + azimuth: match.azimuth.toFixed(0), + id: match.id, + pi: match.pi, foundStation: true, - reg: detectedByPireg, + reg: match.detectedByPireg, + others: multiMatches.length, }; } else { - return; - } - - function evaluateStation(station, city, distance) { - let weightDistance = distance.distanceKm; - if (esMode && distance.distanceKm > 500) { - weightDistance = Math.abs(distance.distanceKm - 1500); - } - let erp = station.erp && station.erp > 0 ? station.erp : 1; - let extraWeight = erp >= weightedErp && distance.distanceKm <= weightedDist ? 0.3 : 0; - let score = 0; - // If ERP is 1W, use a simpler formula to avoid zero-scoring. - if (erp === 0.001) { - score = erp / distance.distanceKm; - } else { - score = ((10 * Math.log10(erp * 1000)) / weightDistance) + extraWeight; - } - if (score > maxScore) { - maxScore = score; - txAzimuth = distance.azimuth; - matchingStation = station; - matchingCity = city; - maxDistance = distance.distanceKm; - } + return Promise.resolve(); } } function checkEs() { const now = Date.now(); const url = "https://fmdx.org/includes/tools/get_muf.php"; - let esSwitch = false; if (now - esSwitchCache.lastCheck < esFetchInterval) { return esSwitchCache.esSwitch; diff --git a/web/css/breadcrumbs.css b/web/css/breadcrumbs.css index d417698..79ba36d 100644 --- a/web/css/breadcrumbs.css +++ b/web/css/breadcrumbs.css @@ -119,6 +119,14 @@ label { font-size: 20px; } +#data-station-others span { + color: var(--color-main); + background: var(--color-4); + margin-left: 4px; + padding: 0 5px; + border-radius: 3px; +} + .highest-signal-container { margin-bottom: -20px !important; } diff --git a/web/index.ejs b/web/index.ejs index df37edf..6311fa5 100644 --- a/web/index.ejs +++ b/web/index.ejs @@ -287,7 +287,7 @@ [] - kW [] + kW [] diff --git a/web/js/main.js b/web/js/main.js index 5a121ac..392b215 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -976,6 +976,7 @@ const updateDataElements = throttle(function(parsedData) { updateTextIfChanged($('#data-station-itu'), parsedData.txInfo.itu); updateTextIfChanged($('#data-station-pol'), parsedData.txInfo.pol); updateHtmlIfChanged($('#data-station-azimuth'), parsedData.txInfo.azi + '°'); + updateHtmlIfChanged($('#data-station-others'), parsedData.txInfo.otherMatches > 0 ? ('+' + parsedData.txInfo.otherMatches +'') : ''); const txDistance = localStorage.getItem('imperialUnits') == "true" ? (Number(parsedData.txInfo.dist) * 0.621371192).toFixed(0) + " mi" : parsedData.txInfo.dist + " km"; updateTextIfChanged($('#data-station-distance'), txDistance); $dataStationContainer.css('display', 'block');