// GridTracker Copyright © 2023 GridTracker.org // All rights reserved. // See LICENSE for more information. var g_pota = { parks: {}, locations: {}, parksTimeout: null, callSchedule: {}, parkSchedule: {}, scheduleTimeout: null, callSpots: {}, parkSpots: {}, spotsTimeout: null, mapParks: {}, rbnReportTimes: {}, rbnFrequency: 600000 }; var g_potaSpotTemplate = { activator: "", frequency: 0, mode: "", band: "", reference: "", spotTime: 0, expire: 0, spotter: "", comments: "", source: "GT", count: 1, activatorGrid: "", spotterGrid: "" }; var g_parkTemplate = { feature: null } var g_potaUnknownPark = { name: "Unknown park (not yet spotted)", active: "0", entityId: "-1", locationDesc: "??-??", latitude: "0.0", longitude: "0.0", grid: "" }; var g_gtParkIconActive = new ol.style.Icon({ src: "./img/pota_icon_active.png", anchorYUnits: "pixels", anchorXUnits: "pixels", anchor: [10, 19] }); var g_gtParkIconInactive = new ol.style.Icon({ src: "./img/pota_icon_inactive.png", anchorYUnits: "pixels", anchorXUnits: "pixels", anchor: [10, 19] }); function initPota() { potaEnabled.checked = (g_appSettings.potaEnabled == 1); potaMenu.checked = g_appSettings.potaShowMenu; potaButton.style.display = (g_appSettings.potaEnabled == 1 && g_appSettings.potaShowMenu && g_mapSettings.offlineMode == false) ? "" : "none"; potaImg.style.filter = g_appSettings.potaMapEnabled ? "" : "grayscale(1)"; g_layerSources.pota.clear(); g_pota.mapParks = {}; if (g_appSettings.potaEnabled == 1) { getPotaParks(); } } function changePotaEnable() { g_appSettings.potaEnabled = (potaEnabled.checked == true) ? 1 : 0; potaButton.style.display = (g_appSettings.potaEnabled == 1 && g_appSettings.potaShowMenu && g_mapSettings.offlineMode == false) ? "" : "none"; if (!g_appSettings.potaEnabled) { g_layerSources.pota.clear(); } else { getPotaParks(); } saveAppSettings(); goProcessRoster(); } function changePotaMenu() { g_appSettings.potaShowMenu = potaMenu.checked; potaButton.style.display = (g_appSettings.potaEnabled == 1 && g_appSettings.potaShowMenu && g_mapSettings.offlineMode == false) ? "" : "none"; potaImg.style.filter = g_appSettings.potaMapEnabled ? "" : "grayscale(1)"; saveAppSettings(); } function togglePotaMap() { g_appSettings.potaMapEnabled = !g_appSettings.potaMapEnabled; potaImg.style.filter = g_appSettings.potaMapEnabled ? "" : "grayscale(1)"; saveAppSettings(); redrawParks(); } function redrawParks() { g_layerSources.pota.clear(); if (g_appSettings.potaEnabled == 1 && g_appSettings.potaMapEnabled) { g_pota.mapParks = {}; makeParkFeatures(); } } function makeParkFeatures() { try { for (const park in g_pota.parkSpots) { if (park in g_pota.parks) { var parkObj = Object.assign({}, g_parkTemplate); for (const call in g_pota.parkSpots[park]) { var report = g_pota.parkSpots[park][call]; if (parkObj.feature == null && validateMapBandAndMode(report.band, report.mode)) { parkObj.feature = iconFeature(ol.proj.fromLonLat([Number(g_pota.parks[park].longitude), Number(g_pota.parks[park].latitude)]), g_gtParkIconActive, 1); parkObj.feature.key = park; parkObj.feature.size = 22; g_pota.mapParks[park] = parkObj; g_layerSources.pota.addFeature(parkObj.feature); } } } } } catch (e) { console.log("exception: makeParkFeature " + park); console.log(e.message); } } function potaSpotFromDecode(callObj) { if (myDEcall != "" && myDEcall != "NOCALL") { var park = callObj.pota; if (callObj.DEcall in g_pota.callSpots && park in g_pota.parkSpots) { // update spot var newObj = spotFromCallObj(callObj, park, g_pota.parkSpots[park][callObj.DEcall].count); g_pota.parkSpots[park][callObj.DEcall] = fillObjectFromTemplate(g_pota.parkSpots[park][callObj.DEcall], newObj); // may or may not be on screen, so try if (g_appSettings.potaMapEnabled) { addParkSpotFeature(park, g_pota.parkSpots[park][callObj.DEcall]); } var hash = park + callObj.DEcall; if (!(hash in g_pota.rbnReportTimes) || Date.now() > g_pota.rbnReportTimes[hash]) { g_pota.rbnReportTimes[hash] = Date.now() + g_pota.rbnFrequency; reportPotaRBN(g_pota.parkSpots[park][callObj.DEcall]); } } else if (callObj.DEcall in g_pota.callSchedule) { // Looks like it's scheduled, so it's new g_pota.callSpots[callObj.DEcall] = park; if (!(park in g_pota.parkSpots)) { g_pota.parkSpots[park] = {}; } var newObj = spotFromCallObj(callObj, park, 0); newObj.expire = newObj.spotTime + 300000; g_pota.parkSpots[park][callObj.DEcall] = newObj; if (g_appSettings.potaMapEnabled) { addParkSpotFeature(park, g_pota.parkSpots[park][callObj.DEcall]); } var hash = park + callObj.DEcall; if (!(hash in g_pota.rbnReportTimes) || Date.now() > g_pota.rbnReportTimes[hash]) { g_pota.rbnReportTimes[hash] = Date.now() + g_pota.rbnFrequency; reportPotaRBN(g_pota.parkSpots[park][callObj.DEcall]); } } else { if (!(callObj.DEcall in g_pota.callSpots)) { console.log("No call spot: " + callObj.DEcall); } if (!(park in g_pota.parkSpots)) { console.log("No park spot: " + park); } } } } function reportPotaRBN(callSpot) { if (Date.now() < callSpot.expire) { var report = { activator: callSpot.activator, spotter: myDEcall + "-#", frequency: String(parseInt(callSpot.frequency * 1000)), reference: callSpot.reference, mode: callSpot.mode, source: "RBN", comments: callSpot.comments, activatorGrid: callSpot.activatorGrid, spotterGrid: callSpot.spotterGrid }; if (Number(report.frequency) > 0) { getPostJSONBuffer( "https://api.pota.app/spot", rbnReportResult, null, "https", 443, report, 10000, null, null ); } } } function reportPotaQSO(record) { var report = { activator: record.CALL, spotter: record.STATION_CALLSIGN, frequency: record.FREQ, reference: record.POTA, mode: record.MODE, source: "GT", comments: record.COMMENT ? record.COMMENT : "", activatorGrid: record.GRIDSQUARE ? record.GRIDSQUARE : "", spotterGrid: record.MY_GRIDSQUARE ? record.MY_GRIDSQUARE : "" }; if ("SUBMODE" in record) { report.mode = record.SUBMODE; } getPostJSONBuffer( "https://api.pota.app/spot", rbnReportResult, null, "https", 443, report, 10000, null, null ); } function rbnReportResult(buffer, flag, cookies) { // It worked! process latest spots! if (g_pota.spotsTimeout) { nodeTimers.clearTimeout(g_pota.spotsTimeout); g_pota.spotsTimeout = null; } processPotaSpots(String(buffer)); g_pota.spotsTimeout = nodeTimers.setTimeout(getPotaSpots, 300000); } function spotFromCallObj(callObj, park, inCount, rbnTime) { var callSpot = { activator: callObj.DEcall, activatorGrid: callObj.grid, spotter: myDEcall + "-#", spotterGrid: myDEGrid, frequency: Number((g_instances[callObj.instance].status.Frequency / 1000000).toFixed(3)), reference: park, mode: callObj.mode, band: callObj.band, spotTime: Date.now(), source: "GT", count: inCount + 1, comments: "GT " + callObj.RSTsent + " dB " + myDEGrid + " via " + myDEcall + "-#" }; return callSpot; } function addParkSpotFeature(park, report) { var parkObj = Object.assign({}, g_parkTemplate); if (park in g_pota.mapParks) { parkObj = g_pota.mapParks[park]; } else { g_pota.mapParks[park] = parkObj; } if (parkObj.feature == null && validateMapBandAndMode(report.band, report.mode)) { parkObj.feature = iconFeature(ol.proj.fromLonLat([Number(g_pota.parks[park].longitude), Number(g_pota.parks[park].latitude)]), g_gtParkIconActive, 1); parkObj.feature.key = park; parkObj.feature.size = 22; g_layerSources.pota.addFeature(parkObj.feature); } } function processPotaParks(buffer) { if (g_appSettings.potaEnabled == 1) { try { var data = JSON.parse(buffer); var newParks = data.parks; for (const park in newParks) { var locations = newParks[park].locationDesc.split(","); for (const i in locations) { if (locations[i] in data.locations) { locations[i] = data.locations[locations[i]]; } } newParks[park].locationDesc = locations.join(", "); } newParks["?-????"] = g_potaUnknownPark; g_pota.parks = newParks; g_pota.locations = data.locations; getPotaSchedule(); getPotaSpots(); } catch (e) { // can't write, somethings broke console.log("Failed to load parks!"); console.log(e.message); } } } function getPotaParks() { if (g_pota.parksTimeout) { nodeTimers.clearTimeout(g_pota.parksTimeout); g_pota.spotsTimeout = null; } if (g_mapSettings.offlineMode == false && g_appSettings.potaEnabled == 1) { getBuffer( "https://storage.googleapis.com/gt_app/pota.json?cb=" + Date.now(), processPotaParks, null, "https", 443 ); } g_pota.parksTimeout = nodeTimers.setTimeout(getPotaParks, 86400000) } // This is a shallow copy, don't use with objects that contain other objects or arrays function fillObjectFromTemplate(template, input) { var object = {}; for (const key in template) { if (key in input) { object[key] = input[key]; } else { // missing, use the template value object[key] = template[key]; } } return object; } function uniqueArrayFromArray(input) { return [...new Set(input)]; } function processPotaSpots(buffer) { if (g_appSettings.potaEnabled == 1) { try { var spots = JSON.parse(buffer); g_pota.callSpots = {}; g_pota.parkSpots = {}; for (const spot in spots) { if (spots[spot].reference in g_pota.parks) { var newSpot = fillObjectFromTemplate(g_potaSpotTemplate, spots[spot]); newSpot.spotTime = Date.parse(newSpot.spotTime + "Z"); newSpot.frequency = parseInt(newSpot.frequency) / 1000; newSpot.expire = newSpot.spotTime + (Number(newSpot.expire) * 1000); newSpot.band = newSpot.frequency.formatBand(); if (newSpot.spotter == newSpot.activator && newSpot.comments.match(/qrt/gi)) { // don't add the spot, they have self-QRT'ed } else if (Date.now() > newSpot.expire) { // Spot is expired! } else { g_pota.callSpots[newSpot.activator] = newSpot.reference; if (!(newSpot.reference in g_pota.parkSpots)) { g_pota.parkSpots[newSpot.reference] = {}; } g_pota.parkSpots[newSpot.reference][newSpot.activator] = newSpot; } } else { console.log("PotaSpots: unknown park id: " + spots[spot].reference); } } redrawParks(); } catch (e) { // can't write, somethings broke } } } function getPotaSpots() { if (g_pota.spotsTimeout) { nodeTimers.clearTimeout(g_pota.spotsTimeout); g_pota.spotsTimeout = null; } if (g_mapSettings.offlineMode == false && g_appSettings.potaEnabled == 1) { getBuffer( "https://api.pota.app/spot/activator", processPotaSpots, null, "https", 443 ); } g_pota.spotsTimeout = nodeTimers.setTimeout(getPotaSpots, 300000); } function processPotaSchedule(buffer) { if (g_appSettings.potaEnabled == 1) { try { var schedules = JSON.parse(buffer); g_pota.callSchedule = {}; g_pota.parkSchedule = {}; for (const i in schedules) { var newObj = {}; newObj.id = schedules[i].reference; newObj.start = Date.parse(schedules[i].startDate + "T" + schedules[i].startTime + "Z"); newObj.end = Date.parse(schedules[i].endDate + "T" + schedules[i].endTime + "Z"); newObj.frequencies = schedules[i].frequencies; newObj.comments = schedules[i].comments; if (Date.now() < newObj.end) { if (newObj.id in g_pota.parks) { (g_pota.callSchedule[schedules[i].activator] = g_pota.callSchedule[schedules[i].activator] || []).push(newObj); newObj = Object.assign({}, newObj); newObj.id = schedules[i].activator; (g_pota.parkSchedule[schedules[i].reference] = g_pota.parkSchedule[schedules[i].reference] || []).push(newObj); } else { console.log("PotaSchedule: unknown park id: " + newObj.id); } } // else it is expired and no longer relevant } // Sanity dedupe checks for (const key in g_pota.callSchedule) { g_pota.callSchedule[key] = uniqueArrayFromArray(g_pota.callSchedule[key]); } for (const key in g_pota.parkSchedule) { g_pota.parkSchedule[key] = uniqueArrayFromArray(g_pota.parkSchedule[key]); } } catch (e) { // can't write, somethings broke } } } function getPotaSchedule() { if (g_pota.scheduleTimeout) { nodeTimers.clearTimeout(g_pota.scheduleTimeout); g_pota.scheduleTimeout = null; } if (g_mapSettings.offlineMode == false && g_appSettings.potaEnabled == 1) { getBuffer( "https://api.pota.app/activation", processPotaSchedule, null, "https", 443 ); } g_pota.scheduleTimeout = nodeTimers.setTimeout(getPotaSchedule, 900000); } var g_lastPark = null; function mouseOverPark(feature) { if (g_lastPark && g_lastPark == feature) { mouseParkMove(); return; } g_lastPark = feature; createParkTipTable(feature); mouseParkMove(); myParktip.style.zIndex = 499; myParktip.style.display = "block"; } function mouseOutPark(mouseEvent) { g_lastPark = null; myParktip.style.zIndex = -1; } function mouseParkMove() { var positionInfo = myParktip.getBoundingClientRect(); var windowWidth = window.innerWidth; myParktip.style.left = getMouseX() - (positionInfo.width / 2) + "px"; if (windowWidth - getMouseX() < (positionInfo.width / 2)) { myParktip.style.left = getMouseX() - (10 + positionInfo.width) + "px"; } if (getMouseX() - (positionInfo.width / 2) < 0) { myParktip.style.left = getMouseX() + 10 + "px"; } myParktip.style.top = getMouseY() - positionInfo.height - 12 + "px"; } function createParkTipTable(toolElement) { var worker = ""; var key = toolElement.key; var now = Date.now(); worker += "
" + key + " : " + g_pota.parks[key].name + "" + " (" + g_dxccToAltName[Number(g_pota.parks[key].entityId)] + ")" + "
" + g_pota.parks[key].locationDesc + "
"; worker += ""; worker += ""; for (const i in g_pota.parkSpots[key]) { if (validateMapBandAndMode(g_pota.parkSpots[key][i].band, g_pota.parkSpots[key][i].mode)) { worker += ""; worker += ""; worker += ""; worker += ""; worker += ""; worker += ""; worker += ""; worker += ""; worker += ""; worker += ""; } } worker += "
ActivatorSpotterFreqModeCountWhenSourceComment
" + g_pota.parkSpots[key][i].activator + "" + ((g_pota.parkSpots[key][i].spotter == g_pota.parkSpots[key][i].activator) ? "Self" : g_pota.parkSpots[key][i].spotter) + "" + g_pota.parkSpots[key][i].frequency.formatMhz(3, 3) + " (" + g_pota.parkSpots[key][i].band + ")" + g_pota.parkSpots[key][i].mode + "" + g_pota.parkSpots[key][i].count + "" + parseInt((now - g_pota.parkSpots[key][i].spotTime) / 1000).toDHMS() + "" + g_pota.parkSpots[key][i].source + "" + g_pota.parkSpots[key][i].comments + "
"; /* buffer += "
Activations (scheduled)" buffer += ""; buffer += ""; for (const i in g_pota.parkSchedule[key]) { var start = g_pota.parkSchedule[key][i].start; var end = g_pota.parkSchedule[key][i].end; if (now < end) { buffer += ""; buffer += ""; buffer += ""; buffer += ""; buffer += ""; buffer += ""; buffer += ""; active++; } } */ myParktip.innerHTML = worker; return 1; }
ActivatorStartEndFrequenciesComment
" + g_pota.parkSchedule[key][i].id + "" + ((now >= start) ? "Now" : (userTimeString(start) + "
T- " + Number(start - now).msToDHMS() + "")) + "
" + (userTimeString(end) + "
T- " + Number(end - now).msToDHMS() + "") + "
" + g_pota.parkSchedule[key][i].frequencies + "" + g_pota.parkSchedule[key][i].comments.substr(0, 40) + "