kopia lustrzana https://gitlab.com/gridtracker.org/gridtracker
649 wiersze
17 KiB
JavaScript
649 wiersze
17 KiB
JavaScript
// GridTracker Copyright © 2023 GridTracker.org
|
|
// All rights reserved.
|
|
// See LICENSE for more information.
|
|
|
|
GT.pota = {
|
|
parks: {},
|
|
locations: {},
|
|
parksTimeout: null,
|
|
callSchedule: {},
|
|
parkSchedule: {},
|
|
scheduleTimeout: null,
|
|
callSpots: {},
|
|
parkSpots: {},
|
|
spotsTimeout: null,
|
|
mapParks: {},
|
|
rbnReportTimes: {},
|
|
rbnFrequency: 600000
|
|
};
|
|
|
|
GT.potaSpotTemplate = {
|
|
activator: "",
|
|
frequency: 0,
|
|
mode: "",
|
|
band: "",
|
|
reference: "",
|
|
spotTime: 0,
|
|
expire: 0,
|
|
spotter: "",
|
|
comments: "",
|
|
source: "GT",
|
|
count: 1,
|
|
activatorGrid: "",
|
|
spotterGrid: ""
|
|
};
|
|
|
|
GT.parkTemplate = {
|
|
feature: null
|
|
}
|
|
|
|
GT.potaUnknownPark = {
|
|
name: "Unknown park (not yet spotted)",
|
|
active: "0",
|
|
entityId: "-1",
|
|
locationDesc: "??-??",
|
|
latitude: "0.0",
|
|
longitude: "0.0",
|
|
grid: ""
|
|
};
|
|
|
|
GT.gtParkIconActive = new ol.style.Icon({
|
|
src: "./img/pota_icon_active.png",
|
|
anchorYUnits: "pixels",
|
|
anchorXUnits: "pixels",
|
|
anchor: [10, 19]
|
|
});
|
|
|
|
GT.gtParkIconInactive = new ol.style.Icon({
|
|
src: "./img/pota_icon_inactive.png",
|
|
anchorYUnits: "pixels",
|
|
anchorXUnits: "pixels",
|
|
anchor: [10, 19]
|
|
});
|
|
|
|
function initPota()
|
|
{
|
|
potaEnabled.checked = (GT.appSettings.potaEnabled == 1);
|
|
potaMenu.checked = GT.appSettings.potaShowMenu;
|
|
potaButton.style.display = (GT.appSettings.potaEnabled == 1 && GT.appSettings.potaShowMenu && GT.mapSettings.offlineMode == false) ? "" : "none";
|
|
potaImg.style.filter = GT.appSettings.potaMapEnabled ? "" : "grayscale(1)";
|
|
|
|
GT.layerSources.pota.clear();
|
|
GT.pota.mapParks = {};
|
|
|
|
if (GT.appSettings.potaEnabled == 1)
|
|
{
|
|
getPotaParks();
|
|
}
|
|
}
|
|
|
|
function changePotaEnable()
|
|
{
|
|
GT.appSettings.potaEnabled = (potaEnabled.checked == true) ? 1 : 0;
|
|
potaButton.style.display = (GT.appSettings.potaEnabled == 1 && GT.appSettings.potaShowMenu && GT.mapSettings.offlineMode == false) ? "" : "none";
|
|
if (!GT.appSettings.potaEnabled)
|
|
{
|
|
GT.layerSources.pota.clear();
|
|
}
|
|
else
|
|
{
|
|
getPotaParks();
|
|
}
|
|
|
|
saveAppSettings();
|
|
goProcessRoster();
|
|
}
|
|
|
|
function changePotaMenu()
|
|
{
|
|
GT.appSettings.potaShowMenu = potaMenu.checked;
|
|
|
|
potaButton.style.display = (GT.appSettings.potaEnabled == 1 && GT.appSettings.potaShowMenu && GT.mapSettings.offlineMode == false) ? "" : "none";
|
|
potaImg.style.filter = GT.appSettings.potaMapEnabled ? "" : "grayscale(1)";
|
|
|
|
saveAppSettings();
|
|
}
|
|
|
|
function togglePotaMap()
|
|
{
|
|
GT.appSettings.potaMapEnabled = !GT.appSettings.potaMapEnabled;
|
|
potaImg.style.filter = GT.appSettings.potaMapEnabled ? "" : "grayscale(1)";
|
|
|
|
saveAppSettings();
|
|
|
|
redrawParks();
|
|
}
|
|
|
|
function redrawParks()
|
|
{
|
|
GT.layerSources.pota.clear();
|
|
|
|
if (GT.appSettings.potaEnabled == 1 && GT.appSettings.potaMapEnabled)
|
|
{
|
|
GT.pota.mapParks = {};
|
|
makeParkFeatures();
|
|
}
|
|
}
|
|
|
|
function makeParkFeatures()
|
|
{
|
|
try
|
|
{
|
|
for (const park in GT.pota.parkSpots)
|
|
{
|
|
if (park in GT.pota.parks)
|
|
{
|
|
var parkObj = Object.assign({}, GT.parkTemplate);
|
|
for (const call in GT.pota.parkSpots[park])
|
|
{
|
|
var report = GT.pota.parkSpots[park][call];
|
|
if (parkObj.feature == null && validateMapBandAndMode(report.band, report.mode) && Date.now() < report.expire)
|
|
{
|
|
parkObj.feature = iconFeature(ol.proj.fromLonLat([Number(GT.pota.parks[park].longitude), Number(GT.pota.parks[park].latitude)]), GT.gtParkIconActive, 1);
|
|
parkObj.feature.key = park;
|
|
parkObj.feature.size = 22;
|
|
|
|
GT.pota.mapParks[park] = parkObj;
|
|
GT.layerSources.pota.addFeature(parkObj.feature);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (e)
|
|
{
|
|
console.log("exception: makeParkFeature " + park);
|
|
console.log(e.message);
|
|
}
|
|
}
|
|
|
|
function potaSpotFromDecode(callObj)
|
|
{
|
|
if (GT.appSettings.myCall != "" && GT.appSettings.myCall != "NOCALL")
|
|
{
|
|
var park = callObj.pota;
|
|
|
|
if (callObj.DEcall in GT.pota.callSpots && park in GT.pota.parkSpots)
|
|
{
|
|
// update spot
|
|
var newObj = spotFromCallObj(callObj, park, GT.pota.parkSpots[park][callObj.DEcall].count);
|
|
GT.pota.parkSpots[park][callObj.DEcall] = fillObjectFromTemplate(GT.pota.parkSpots[park][callObj.DEcall], newObj);
|
|
|
|
// may or may not be on screen, so try
|
|
if (GT.appSettings.potaMapEnabled)
|
|
{
|
|
addParkSpotFeature(park, GT.pota.parkSpots[park][callObj.DEcall]);
|
|
}
|
|
|
|
var hash = park + callObj.DEcall;
|
|
if (!(hash in GT.pota.rbnReportTimes) || Date.now() > GT.pota.rbnReportTimes[hash])
|
|
{
|
|
GT.pota.rbnReportTimes[hash] = Date.now() + GT.pota.rbnFrequency;
|
|
// reportPotaRBN(GT.pota.parkSpots[park][callObj.DEcall]);
|
|
}
|
|
}
|
|
else if (callObj.DEcall in GT.pota.callSchedule)
|
|
{
|
|
// Looks like it's scheduled, so it's new
|
|
GT.pota.callSpots[callObj.DEcall] = park;
|
|
|
|
if (!(park in GT.pota.parkSpots))
|
|
{
|
|
GT.pota.parkSpots[park] = {};
|
|
}
|
|
|
|
var newObj = spotFromCallObj(callObj, park, 0);
|
|
newObj.expire = newObj.spotTime + 300000;
|
|
GT.pota.parkSpots[park][callObj.DEcall] = newObj;
|
|
|
|
if (GT.appSettings.potaMapEnabled)
|
|
{
|
|
addParkSpotFeature(park, GT.pota.parkSpots[park][callObj.DEcall]);
|
|
}
|
|
|
|
var hash = park + callObj.DEcall;
|
|
if (!(hash in GT.pota.rbnReportTimes) || Date.now() > GT.pota.rbnReportTimes[hash])
|
|
{
|
|
GT.pota.rbnReportTimes[hash] = Date.now() + GT.pota.rbnFrequency;
|
|
// reportPotaRBN(GT.pota.parkSpots[park][callObj.DEcall]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!(callObj.DEcall in GT.pota.callSpots))
|
|
{
|
|
console.log("No call spot: " + callObj.DEcall);
|
|
}
|
|
if (!(park in GT.pota.parkSpots))
|
|
{
|
|
console.log("No park spot: " + park);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* function reportPotaRBN(callSpot)
|
|
{
|
|
if (Date.now() < callSpot.expire)
|
|
{
|
|
var report = {
|
|
activator: callSpot.activator,
|
|
spotter: GT.appSettings.myCall + "-#",
|
|
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 (GT.pota.spotsTimeout)
|
|
{
|
|
nodeTimers.clearTimeout(GT.pota.spotsTimeout);
|
|
GT.pota.spotsTimeout = null;
|
|
}
|
|
|
|
processPotaSpots(String(buffer));
|
|
|
|
GT.pota.spotsTimeout = nodeTimers.setTimeout(getPotaSpots, 300000);
|
|
}
|
|
|
|
function spotFromCallObj(callObj, park, inCount, rbnTime)
|
|
{
|
|
var callSpot = {
|
|
activator: callObj.DEcall,
|
|
activatorGrid: callObj.grid,
|
|
spotter: GT.appSettings.myCall + "-#",
|
|
spotterGrid: GT.appSettings.myGrid,
|
|
frequency: Number((GT.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 " + GT.appSettings.myGrid + " via " + GT.appSettings.myCall + "-#"
|
|
};
|
|
return callSpot;
|
|
}
|
|
|
|
function addParkSpotFeature(park, report)
|
|
{
|
|
var parkObj = Object.assign({}, GT.parkTemplate);
|
|
if (park in GT.pota.mapParks)
|
|
{
|
|
parkObj = GT.pota.mapParks[park];
|
|
}
|
|
else
|
|
{
|
|
GT.pota.mapParks[park] = parkObj;
|
|
}
|
|
|
|
if (parkObj.feature == null && validateMapBandAndMode(report.band, report.mode))
|
|
{
|
|
parkObj.feature = iconFeature(ol.proj.fromLonLat([Number(GT.pota.parks[park].longitude), Number(GT.pota.parks[park].latitude)]), GT.gtParkIconActive, 1);
|
|
parkObj.feature.key = park;
|
|
parkObj.feature.size = 22;
|
|
GT.layerSources.pota.addFeature(parkObj.feature);
|
|
}
|
|
}
|
|
|
|
function processPotaParks(buffer)
|
|
{
|
|
if (GT.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["?-????"] = GT.potaUnknownPark;
|
|
|
|
GT.pota.parks = newParks;
|
|
GT.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 (GT.pota.parksTimeout)
|
|
{
|
|
nodeTimers.clearTimeout(GT.pota.parksTimeout);
|
|
GT.pota.spotsTimeout = null;
|
|
}
|
|
|
|
if (GT.mapSettings.offlineMode == false && GT.appSettings.potaEnabled == 1)
|
|
{
|
|
getBuffer(
|
|
"https://storage.googleapis.com/gt_app/pota.json?cb=" + Date.now(),
|
|
processPotaParks,
|
|
null,
|
|
"https",
|
|
443
|
|
);
|
|
}
|
|
|
|
GT.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 (GT.appSettings.potaEnabled == 1)
|
|
{
|
|
try
|
|
{
|
|
var spots = JSON.parse(buffer);
|
|
GT.pota.callSpots = {};
|
|
GT.pota.parkSpots = {};
|
|
for (const spot in spots)
|
|
{
|
|
if (spots[spot].reference in GT.pota.parks)
|
|
{
|
|
var newSpot = fillObjectFromTemplate(GT.potaSpotTemplate, spots[spot]);
|
|
newSpot.spotTime = Date.parse(newSpot.spotTime + "Z");
|
|
newSpot.frequency = parseInt(newSpot.frequency) / 1000;
|
|
|
|
newSpot.expire = Date.now() + (Number(newSpot.expire) * 1000);
|
|
newSpot.band = formatBand(newSpot.frequency);
|
|
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
|
|
{
|
|
GT.pota.callSpots[newSpot.activator] = newSpot.reference;
|
|
|
|
if (!(newSpot.reference in GT.pota.parkSpots))
|
|
{
|
|
GT.pota.parkSpots[newSpot.reference] = {};
|
|
}
|
|
|
|
GT.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 (GT.pota.spotsTimeout)
|
|
{
|
|
nodeTimers.clearTimeout(GT.pota.spotsTimeout);
|
|
GT.pota.spotsTimeout = null;
|
|
}
|
|
|
|
if (GT.mapSettings.offlineMode == false && GT.appSettings.potaEnabled == 1)
|
|
{
|
|
getBuffer(
|
|
"https://api.pota.app/spot/activator",
|
|
processPotaSpots,
|
|
null,
|
|
"https",
|
|
443
|
|
);
|
|
}
|
|
|
|
GT.pota.spotsTimeout = nodeTimers.setTimeout(getPotaSpots, 300000);
|
|
}
|
|
|
|
function processPotaSchedule(buffer)
|
|
{
|
|
if (GT.appSettings.potaEnabled == 1)
|
|
{
|
|
try
|
|
{
|
|
var schedules = JSON.parse(buffer);
|
|
GT.pota.callSchedule = {};
|
|
GT.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 GT.pota.parks)
|
|
{
|
|
(GT.pota.callSchedule[schedules[i].activator] = GT.pota.callSchedule[schedules[i].activator] || []).push(newObj);
|
|
|
|
newObj = Object.assign({}, newObj);
|
|
newObj.id = schedules[i].activator;
|
|
(GT.pota.parkSchedule[schedules[i].reference] = GT.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 GT.pota.callSchedule)
|
|
{
|
|
GT.pota.callSchedule[key] = uniqueArrayFromArray(GT.pota.callSchedule[key]);
|
|
}
|
|
for (const key in GT.pota.parkSchedule)
|
|
{
|
|
GT.pota.parkSchedule[key] = uniqueArrayFromArray(GT.pota.parkSchedule[key]);
|
|
}
|
|
}
|
|
catch (e)
|
|
{
|
|
// can't write, somethings broke
|
|
}
|
|
}
|
|
}
|
|
|
|
function getPotaSchedule()
|
|
{
|
|
if (GT.pota.scheduleTimeout)
|
|
{
|
|
nodeTimers.clearTimeout(GT.pota.scheduleTimeout);
|
|
GT.pota.scheduleTimeout = null;
|
|
}
|
|
|
|
if (GT.mapSettings.offlineMode == false && GT.appSettings.potaEnabled == 1)
|
|
{
|
|
getBuffer(
|
|
"https://api.pota.app/activation",
|
|
processPotaSchedule,
|
|
null,
|
|
"https",
|
|
443
|
|
);
|
|
}
|
|
GT.pota.scheduleTimeout = nodeTimers.setTimeout(getPotaSchedule, 900000);
|
|
}
|
|
|
|
GT.lastPark = null;
|
|
function mouseOverPark(feature)
|
|
{
|
|
if (GT.lastPark && GT.lastPark == feature)
|
|
{
|
|
mouseParkMove();
|
|
return;
|
|
}
|
|
GT.lastPark = feature;
|
|
|
|
createParkTipTable(feature);
|
|
|
|
mouseParkMove();
|
|
|
|
myParktip.style.zIndex = 499;
|
|
myParktip.style.display = "block";
|
|
}
|
|
|
|
function mouseOutPark(mouseEvent)
|
|
{
|
|
GT.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 += "<div style='background-color:#000;color:lightgreen;font-weight:bold;font-size:12px;border:1px solid gray;margin:0px' class='roundBorder'>" +
|
|
key +
|
|
" : <font color='cyan'>" + GT.pota.parks[key].name + "" +
|
|
" (<font color='yellow'>" + GT.dxccToAltName[Number(GT.pota.parks[key].entityId)] + "</font>)" +
|
|
"</font></br><font color='lightblue'>" + GT.pota.parks[key].locationDesc + "</font></div>";
|
|
|
|
worker += "<table id='potaSpotsTable' class='darkTable' style='margin: 0 auto;'>";
|
|
worker += "<tr><th>Activator</th><th>Spotter</th><th>Freq</th><th>Mode</th><th>Count</th><th>When</th><th>Source</th><th>Comment</th></tr>";
|
|
for (const i in GT.pota.parkSpots[key])
|
|
{
|
|
if (validateMapBandAndMode(GT.pota.parkSpots[key][i].band, GT.pota.parkSpots[key][i].mode))
|
|
{
|
|
worker += "<tr>";
|
|
worker += "<td style='color:yellow'>" + GT.pota.parkSpots[key][i].activator + "</td>";
|
|
worker += "<td style='color:cyan'>" + ((GT.pota.parkSpots[key][i].spotter == GT.pota.parkSpots[key][i].activator) ? "Self" : GT.pota.parkSpots[key][i].spotter) + "</td>";
|
|
worker += "<td style='color:lightgreen' >" + formatMhz(GT.pota.parkSpots[key][i].frequency, 3, 3) + " <font color='yellow'>(" + GT.pota.parkSpots[key][i].band + ")</font></td>";
|
|
worker += "<td style='color:orange'>" + GT.pota.parkSpots[key][i].mode + "</td>";
|
|
worker += "<td>" + GT.pota.parkSpots[key][i].count + "</td>";
|
|
worker += "<td style='color:lightblue' >" + toDHMS(parseInt((now - GT.pota.parkSpots[key][i].spotTime) / 1000)) + "</td>";
|
|
worker += "<td>" + GT.pota.parkSpots[key][i].source + "</td>";
|
|
worker += "<td>" + GT.pota.parkSpots[key][i].comments + "</td>";
|
|
worker += "</tr>";
|
|
}
|
|
}
|
|
worker += "</table>";
|
|
myParktip.innerHTML = worker;
|
|
}
|