Files
spothole/webassets/js/alerts.js

274 lines
13 KiB
JavaScript

// How often to query the server?
const REFRESH_INTERVAL_SEC = 60 * 30;
// Storage for the alert data that the server gives us.
var alerts = []
// Load alerts and populate the table.
function loadAlerts() {
$.getJSON('/api/v1/alerts' + buildQueryString(), function(jsonData) {
// Store last updated time
lastUpdateTime = moment.utc();
updateRefreshDisplay();
// 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() {
var str = "?";
["dx_continent", "source"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&";
}
});
str = str + "limit=" + $("#alerts-to-fetch option:selected").val();
var 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";
}
return str;
}
// Update the alerts table
function updateTable() {
// Use local time instead of UTC?
var useLocalTime = $("#useLocalTime")[0].checked;
// Populate table with headers
let table = $('<table class="table table-striped-custom table-hover">').append('<thead><tr class="table-primary"></tr></thead><tbody></tbody>');
table.find('thead tr').append(`<th>${useLocalTime ? "Start&nbsp;(Local)" : "Start&nbsp;UTC"}</th>`);
table.find('thead tr').append(`<th>${useLocalTime ? "End&nbsp;(Local)" : "End&nbsp;UTC"}</th>`);
table.find('thead tr').append(`<th>DX</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Freq<span class='hideonmobile'>uencie</span>s & Modes</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Comment</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Source</th>`);
table.find('thead tr').append(`<th class='hideonmobile'>Ref.</th>`);
// 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<now<end, or events with no end time that started in the
// last hour.
onNow = alerts.filter(a => (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()));
next24h = alerts.filter(a => moment.unix(a["start_time"]).utc().isSameOrAfter() && moment.unix(a["start_time"]).utc().subtract(24, 'hours').isBefore());
later = alerts.filter(a => moment.unix(a["start_time"]).utc().subtract(24, 'hours').isSameOrAfter());
if (onNow.length > 0) {
table.find('tbody').append('<tr class="table-primary"><td colspan="100" style="text-align:center;">On Now</td></tr>');
addAlertRowsToTable(table.find('tbody'), onNow);
}
if (next24h.length > 0) {
table.find('tbody').append('<tr class="table-primary"><td colspan="100" style="text-align:center;">Starting within 24 hours</td></tr>');
addAlertRowsToTable(table.find('tbody'), next24h);
}
if (later.length > 0) {
table.find('tbody').append('<tr class="table-primary"><td colspan="100" style="text-align:center;">Starting later </td></tr>');
addAlertRowsToTable(table.find('tbody'), later);
}
if (onNow.length == 0 && next24h.length == 0 && later.length == 0) {
table.find('tbody').append('<tr class="table-danger"><td colspan="100" style="text-align:center;">No alerts match your filters.</td></tr>');
}
// Update DOM
$('#table-container').html(table);
}
// Add a row to tbody for each alert in the provided list
function addAlertRowsToTable(tbody, alerts) {
alerts.forEach(a => {
// Create row
let $tr = $('<tr>');
// Use local time instead of UTC?
var useLocalTime = $("#useLocalTime")[0].checked;
// Get times for the alert, and convert to local time if necessary.
var start_time_unix = moment.unix(a["start_time"]);
var start_time = start_time_unix.utc();
if (useLocalTime) {
start_time = start_time.local();
}
var end_time_unix = moment.unix(a["end_time"]);
var end_time = end_time_unix.utc();
if (useLocalTime) {
end_time = end_time.local();
}
// 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.
var whole_days = start_time_unix.utc().format("HH:mm") == "00:00" &&
(end_time_unix != null || end_time_unix > 0 || end_time_unix.utc().format("HH:mm") == "23:59");
var hours_minutes_format = whole_days ? "" : " HH:mm";
var 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);
}
var end_time_formatted = "---";
if (end_time_unix != null && end_time_unix > 0 && end_time != null) {
var 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 {
end_time_formatted = end_time.format("D MMM" + hours_minutes_format);
}
}
}
// Format DX flag
var dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (a["dx_flag"] && a["dx_flag"] != null && a["dx_flag"] != "") {
dx_flag = a["dx_flag"];
}
// Format dx country
var dx_country = a["dx_country"]
if (dx_country == null) {
dx_country = "Unknown or not a country"
}
// Format dx calls
var dx_calls_html = "";
if (a["dx_calls"] != null) {
dx_calls_html = a["dx_calls"].map(call => `<a class='dx-link' href='https://qrz.com/db/${call}' target='_new'>${call}</a>`).join(", ");
}
// Format DXpedition country
var dx_country_html = "";
if (a["is_dxpedition"] == true && a["dx_country"] != null && a["dx_country"] != "") {
dx_country_html = `<br/>${a["dx_country"]}`;
}
// Format freqs & modes
var freqsModesText = "";
if (a["freqs_modes"] != null) {
freqsModesText = escapeHtml(a["freqs_modes"]);
}
// Format comment
var commentText = "";
if (a["comment"] != null) {
commentText = escapeHtml(a["comment"]);
}
// Sig or fallback to source
var sigSourceText = a["source"];
if (a["sig"]) {
sigSourceText = a["sig"];
}
// Format sig_refs
var sig_refs = ""
if (a["sig_refs"]) {
sig_refs = a["sig_refs"].map(a => `<span class='nowrap'>${a}</span>`).join(", ");
}
// Populate the row
$tr.append(`<td class='nowrap'>${start_time_formatted}</td>`);
$tr.append(`<td class='nowrap'>${end_time_formatted}</td>`);
$tr.append(`<td class='nowrap'><span class='flag-wrapper hideonmobile' title='${dx_country}'>${dx_flag}</span>${dx_calls_html}${dx_country_html}</td>`);
$tr.append(`<td class='hideonmobile'>${freqsModesText}</td>`);
$tr.append(`<td class='hideonmobile'>${commentText}</td>`);
$tr.append(`<td class='nowrap hideonmobile'><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sigSourceText}</td>`);
$tr.append(`<td class='hideonmobile'>${sig_refs}</td>`);
tbody.append($tr);
// Second row for mobile view only, containing source, ref, freqs/modes & comment
$tr2 = $("<tr class='hidenotonmobile'>");
$tr2.append(`<td colspan="100"><span class='icon-wrapper'><i class='fa-solid fa-${a["icon"]}'></i></span> ${sig_refs} ${freqsModesText}<br/>${commentText}</td>`);
tbody.append($tr2);
});
}
// Load server options. Once a successful callback is made from this, we then query alerts.
function loadOptions() {
$.getJSON('/api/v1/options', function(jsonData) {
// Store options
options = jsonData;
// Populate the filters panel
$("#filters-container").append(generateMultiToggleFilterCard("DX Continent", "dx_continent", options["continents"]));
$("#filters-container").append(generateMultiToggleFilterCard("Sources", "source", options["alert_sources"]));
// Options doesn't give us anything for Max Duration as it's a free numeric input, but we generate our own
// filter card for this.
$("#filters-container").append(generateMaxDurationDropdownFilterCard(options["alert_sources"]));
// Load filters from settings storage
loadSettings();
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
}
// Generate maximum duration drop-down filter card. This one is a special case.
function generateMaxDurationDropdownFilterCard(band_options) {
let $col = $("<div class='col'>")
let $card = $("<div class='card'>");
let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>Duration Limit <i class='fa-solid fa-circle-question' title='Some users create long-duration alerts for the period they will be generally in and around xOTA references, when they are not indending to be on the air most of the time. Use this control to restrict the maximum duration of spots that the software will display, and exclude any with a long duration, to avoid these filling up the list. By default, we allow DXpeditions to be displayed even if they are longer than this limit, because on a DXpedition the operators typically ARE on the air most of the time.'></i></h5>`);
$p = $("<p class='card-text filter-card-text'>");
$p.append("Hide any alerts lasting more than:<br/>");
$p.append(`<select id="max-duration" class="storeable-select form-select" onclick="filtersUpdated();" style="width: 8em; display: inline-block;">
<option value="10800">3 hours</option>
<option value="43200">12 hours</option>
<option value="86400" selected>24 hours</option>
<option value="604800">1 week</option>
<option value="2419200">4 weeks</option>
<option value="9999999999">No limit</option>
</select>`);
$p2 = $("<p class='card-text filter-card-text' style='line-height: 1.5em !important;'>");
$p2.append(`<input class="form-check-input storeable-checkbox" type="checkbox" value="" onclick="filtersUpdated();" id="dxpeditions_skip_max_duration_check" checked><label class="form-check-label ms-2" for="dxpeditions_skip_max_duration_check">Allow DXpeditions that are longer</label>`);
// Compile HTML elements to return
$card_body.append($p);
$card_body.append($p2);
$card.append($card_body);
$col.append($card);
return $col;
}
// Method called when any filter is changed to reload the alerts and persist the filter settings.
function filtersUpdated() {
loadAlerts();
saveSettings();
}
// Set up UI element event listeners, after the document is ready
function setUpEventListeners() {
$("#filters-button").click(function() {
$("#filters-area").toggle();
});
$("#close-filters-button").click(function() {
$("#filters-button").button("toggle");
$("#filters-area").hide();
});
}
// Startup
$(document).ready(function() {
// Call loadOptions(), this will then trigger loading alerts and setting up timers.
loadOptions();
// Update the refresh timing display every second
setInterval(updateRefreshDisplay, 1000);
// Set up event listeners
setUpEventListeners();
});