// How often to query the server?
const REFRESH_INTERVAL_SEC = 60 * 10;
// Last time the alerts list was updated on display.
let lastUpdateTime;
// Storage for the alert data that the server gives us.
let alerts = [];
// Load alerts and populate the table. No need for QRZ/HamQTH credential headers as these won't add any useful data
// to alerts
function loadAlerts() {
$.ajax({
url: '/api/v3/alerts' + buildQueryString(), dataType: 'json', success: function (jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
// Store data
alerts = jsonData;
// Update table
updateTable();
}
});
}
// Build a query string for the API, based on the filters that the user has selected.
function buildQueryString() {
let str = "?";
["dx_continent", "source", "activity"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
});
str = str + "limit=" + $("#alerts_to_fetch option:selected").val();
const maxDur = $("#max-duration option:selected").val();
if (maxDur !== "9999999999") {
str = str + "&max_duration=" + maxDur;
}
if ($("#dxpeditions_skip_max_duration_check")[0].checked) {
str = str + "&dxpeditions_skip_max_duration_check=true";
}
if ($("#contests_skip_max_duration_check")[0].checked) {
str = str + "&contests_skip_max_duration_check=true";
}
return str;
}
// Update the alerts table
function updateTable() {
// Use local time instead of UTC?
const useLocalTime = $("#timeZone")[0].value === "local";
// Table data toggles
const showStartTime = $("#tableShowStartTime")[0].checked;
const showEndTime = $("#tableShowEndTime")[0].checked;
const showDX = $("#tableShowDX")[0].checked;
const showFreqsModes = $("#tableShowFreqsModes")[0].checked;
const showComment = $("#tableShowComment")[0].checked;
const showActivity = $("#tableShowActivity")[0].checked;
const showRef = $("#tableShowRef")[0].checked;
// Populate table with headers
let table = $("#table");
table.find('thead tr').empty();
if (showStartTime) {
table.find('thead tr').append(`
${useLocalTime ? "Start (Local)" : "Start UTC"} | `);
}
if (showEndTime) {
table.find('thead tr').append(`${useLocalTime ? "End (Local)" : "End UTC"} | `);
}
if (showDX) {
table.find('thead tr').append(`DX | `);
}
if (showFreqsModes) {
table.find('thead tr').append(`Frequencies & Modes | `);
}
if (showComment) {
table.find('thead tr').append(`Comment | `);
}
if (showActivity) {
table.find('thead tr').append(`Activity | `);
}
if (showRef) {
table.find('thead tr').append(`Reference | `);
}
table.find('tbody').empty();
// Split alerts into three types, each of which will get its own table header: On now, next 24h, and later. "On now"
// is considered to be events with an end_time where start (a["end_time"] != null && a["end_time"] !== 0 && moment.unix(a["end_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore())
|| ((a["end_time"] == null || a["end_time"] === 0) && moment.unix(a["start_time"]).utc().add(1, 'hours').isSameOrAfter() && moment.unix(a["start_time"]).utc().isBefore()));
const next24h = alerts.filter(a => moment.unix(a["start_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().subtract(24, 'hours').isBefore());
const later = alerts.filter(a => moment.unix(a["start_time"]).utc().subtract(24, 'hours').isSameOrAfter());
if (onNow.length > 0) {
table.find('tbody').append('| On Now |
');
addAlertRowsToTable(table.find('tbody'), onNow);
}
if (next24h.length > 0) {
table.find('tbody').append('| Starting within 24 hours |
');
addAlertRowsToTable(table.find('tbody'), next24h);
}
if (later.length > 0) {
table.find('tbody').append('| Starting later |
');
addAlertRowsToTable(table.find('tbody'), later);
}
if (onNow.length === 0 && next24h.length === 0 && later.length === 0) {
table.find('tbody').append('| No alerts match your filters. |
');
}
}
// Add a row to tbody for each alert in the provided list
function addAlertRowsToTable(tbody, alerts) {
let count = 0;
alerts.forEach(a => {
// Create row
let $tr = $('');
// Calculate some values
let wantThis = false;
if (a["dx_calls"] != null) {
for (i in a["dx_calls"]) {
if (callWanted(a["dx_calls"][i])) {
wantThis = true;
break;
}
}
}
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of
// extra faff to deal with, like the mobile view having extra rows, and the On Now / Next 24h / Later banners
// which cause the table-striped colouring to go awry. If the call is wanted, suppress the striping so the
// highlight colour looks right.
if (count % 2 === 1 && !wantThis) {
$tr.addClass("table-active");
}
// If this is a wanted call, highlight it
if (wantThis) {
$tr.addClass("row-wanted");
}
// Use local time instead of UTC?
const useLocalTime = $("#timeZone")[0].value === "local";
// Table data toggles
const showStartTime = $("#tableShowStartTime")[0].checked;
const showEndTime = $("#tableShowEndTime")[0].checked;
const showDX = $("#tableShowDX")[0].checked;
const showFreqsModes = $("#tableShowFreqsModes")[0].checked;
const showComment = $("#tableShowComment")[0].checked;
const showActivity = $("#tableShowActivity")[0].checked;
const showRef = $("#tableShowRef")[0].checked;
// Get times for the alert, and convert to local time if necessary.
const start_time_utc = moment.unix(a["start_time"]).utc();
const start_time_local = start_time_utc.clone().local();
const start_time = useLocalTime ? start_time_local : start_time_utc;
const end_time_utc = moment.unix(a["end_time"]).utc();
const end_time_local = end_time_utc.clone().local();
const end_time = useLocalTime ? end_time_local : end_time_utc;
// Format the times for display. Start time is displayed as e.g. 7 Oct 12:34 unless the time is in a
// different year to the current year, in which case the year is inserted between month and hour.
// If the time is set to local not UTC, and the date in local time is "today", we display that instead.
// End time is displayed the same as above, except if the end date is the same as the start date, in which case
// just e.g. 23:45 is used.
// Overriding all of that, if the start time is 00:00 and the end time is 23:59 when considered in UTC, the
// hours and minutes are stripped out from the display, as we assume the server is just giving us full days.
// Finally, if there is no end date set, "---" is displayed.
const whole_days = start_time_utc.format("HH:mm") === "00:00" &&
(end_time_utc === 0 || end_time_utc.format("HH:mm") === "23:59");
const hours_minutes_format = whole_days ? "" : " HH:mm";
let start_time_formatted = start_time.format("D MMM" + hours_minutes_format);
if (start_time.format("YYYY") !== moment().format("YYYY")) {
start_time_formatted = start_time.format("D MMM YYYY" + hours_minutes_format);
} else if (useLocalTime && start_time.format("D MMM YYYY") === moment().format("D MMM YYYY")) {
start_time_formatted = start_time.format("[Today]" + hours_minutes_format);
}
let end_time_formatted = "---";
if (end_time_utc > 0 && end_time != null) {
end_time_formatted = whole_days ? start_time_formatted : end_time.format("HH:mm");
if (end_time.format("D MMM") !== start_time.format("D MMM")) {
if (end_time.format("YYYY") !== moment().format("YYYY")) {
end_time_formatted = end_time.format("D MMM YYYY" + hours_minutes_format);
} else if (useLocalTime && end_time.format("D MMM YYYY") === moment().format("D MMM YYYY")) {
end_time_formatted = end_time.format("[Today]" + hours_minutes_format);
} else {
end_time_formatted = end_time.format("D MMM" + hours_minutes_format);
}
}
}
// Format dx country
let dx_country = a["dx_country"];
if (dx_country == null) {
dx_country = "Unknown or not a country"
}
// Format DX flag
let dx_flag = "";
if (a["dx_dxcc_id"] && a["dx_dxcc_id"] != null && a["dx_dxcc_id"] !== 0) {
dx_flag = `
`;
}
// Format dx calls
let dx_calls_html = "";
if (a["dx_calls"] != null) {
dx_calls_html = a["dx_calls"].map(call => `${call}`).join(", ");
}
if (dx_calls_html === "" && a["activities"] != null && a["activities"].includes("Contest")) {
// Contest = true and no DX callsigns, so display "Contest"
dx_calls_html = "Contest"
}
// Format DXpedition country
let dx_country_html = "";
if (a["activities"] != null && a["activities"].includes("DXpedition") && a["dx_country"] != null && a["dx_country"] !== "") {
dx_country_html = `
${a["dx_country"]}`;
}
// Format freqs & modes
let freqsModesText = "";
if (a["freqs_modes"] != null) {
freqsModesText = escapeHtml(a["freqs_modes"]);
}
// Format comment
let commentText = "";
if (a["comment"] != null) {
commentText = escapeHtml(a["comment"]);
}
// Format extra text, like URL and attribution
let subComment = a["url"] != null || a["source"] === "NG3K" || a["source"] === "WA7BNM Contest Calendar";
if (subComment) {
let subCommentText = ""
if (a["url"] != null) {
subCommentText += `More info`;
}
if ((a["source"] === "NG3K" || a["source"] === "WA7BNM Contest Calendar")) {
if (subCommentText !== "") {
subCommentText += " | ";
}
subCommentText += `From ${a["source"]}`;
}
commentText += `${subCommentText}
`;
}
// Activity or fallback to "General DX"
let activityText = "General DX";
if (a["activities"] != null && a["activities"].length > 0) {
activityText = a["activities"].join(", ");
}
// Format activity refs
let activityRefs = "";
if (a["activity_refs"] != null) {
const items = [];
for (let i = 0; i < a["activity_refs"].length; i++) {
if (a["activity_refs"][i]["url"] != null) {
items[i] = `${escapeHtml(a["activity_refs"][i]["id"])}`
} else {
items[i] = `${escapeHtml(a["activity_refs"][i]["id"])}`
}
// If this is a satellite alert the ref will just be the satellite, but DX grid is also important, so
// show that if we can.
if (a["activity_refs"][i]["activity"] === "Satellite" && a["dx_grid"] != null) {
items[i] += " from " + a["dx_grid"];
}
}
activityRefs = items.join(", ");
}
// Populate the row
if (showStartTime) {
$tr.append(`${start_time_formatted} | `);
}
if (showEndTime) {
$tr.append(`${end_time_formatted} | `);
}
if (showDX) {
$tr.append(`${dx_flag}${dx_calls_html}${dx_country_html} | `);
}
if (showFreqsModes) {
$tr.append(`${freqsModesText} | `);
}
if (showComment) {
$tr.append(`${commentText} | `);
}
if (showActivity) {
$tr.append(` ${activityText} | `);
}
if (showRef) {
$tr.append(`${activityRefs} | `);
}
tbody.append($tr);
// Second row for mobile view only, containing source, ref, freqs/modes & comment
const $tr2 = $("
");
if (count % 2 === 1 && !wantThis) {
$tr2.addClass("table-active");
}
if (wantThis) {
$tr2.addClass("row-wanted");
}
const $td2 = $("");
if (showActivity) {
$td2.append(` `);
}
if (showRef) {
$td2.append(`${activityRefs} `);
}
if (showFreqsModes) {
$td2.append(`${freqsModesText} `);
}
if (showComment) {
$td2.append(` ${commentText} `);
}
$tr2.append($td2);
tbody.append($tr2);
count++;
});
}
// Method called when any filter is changed to reload the alerts and persist the filter settings.
function filtersUpdated() {
loadAlerts();
saveSettings();
}
// Startup
$(document).ready(function () {
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
// after some time has passed with it in the background.
addEventListener("visibilitychange", () => {
if (!document.hidden) {
loadAlerts();
}
}); |