Fix some IDE warnings

This commit is contained in:
Ian Renton
2026-06-19 19:31:56 +01:00
parent 725eb619b4
commit 05ac652cee
22 changed files with 310 additions and 356 deletions

View File

@@ -1,6 +1,45 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="Annotator" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="BadExpressionStatementJS" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="CssOverwrittenProperties" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="CssUnresolvedCustomProperty" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="CssUnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="GrazieStyle" enabled="false" level="STYLE_SUGGESTION" enabled_by_default="false" />
<inspection_tool class="HtmlUnknownAttribute" enabled="false" level="WARNING" enabled_by_default="false">
<option name="myValues">
<value>
<list size="0" />
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="HtmlUnknownTag" enabled="false" level="WARNING" enabled_by_default="false">
<option name="myValues">
<value>
<list size="6">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="HttpUrlsUsage" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSDeprecatedSymbols" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSIgnoredPromiseFromCall" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSJQueryEfficiency" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnnecessarySemicolon" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnresolvedReference" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="OutdatedRequirementInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />
<option name="processLiterals" value="true" /> <option name="processLiterals" value="true" />

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<file url="file://$PROJECT_DIR$" libraries="{redoc.standalone}" />
</component>
</project>

2
.idea/spothole.iml generated
View File

@@ -3,8 +3,10 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/webassets/vendor" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at ~/code/spothole/.venv" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.13 virtualenv at ~/code/spothole/.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="redoc.standalone" level="application" />
</component> </component>
</module> </module>

View File

@@ -69,7 +69,7 @@
</div> </div>
<script src="/js/add-spot.js?v=1781809662"></script> <script src="/js/add-spot.js?v=1781893916"></script>
<script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-add-spot").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -70,7 +70,7 @@
</div> </div>
<script src="/js/alerts.js?v=1781809662"></script> <script src="/js/alerts.js?v=1781893916"></script>
<script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-alerts").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -76,8 +76,8 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/spotsbandsandmap.js?v=1781809662"></script> <script src="/js/spotsbandsandmap.js?v=1781893916"></script>
<script src="/js/bands.js?v=1781809662"></script> <script src="/js/bands.js?v=1781893916"></script>
<script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-bands").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/css/style.css?v=1781809662" type="text/css"> <link rel="stylesheet" href="/css/style.css?v=1781893916" type="text/css">
<link href="/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -10,10 +10,10 @@
<script src="/vendor/js/bootstrap-5.3.8.bundle.min.js"></script> <script src="/vendor/js/bootstrap-5.3.8.bundle.min.js"></script>
<script src="/vendor/js/tinycolor2-1.6.0.min.js"></script> <script src="/vendor/js/tinycolor2-1.6.0.min.js"></script>
<script src="/js/utils.js?v=1781809662"></script> <script src="/js/utils.js?v=1781893916"></script>
<script src="/js/ui-ham.js?v=1781809662"></script> <script src="/js/ui-ham.js?v=1781893916"></script>
<script src="/js/geo.js?v=1781809662"></script> <script src="/js/geo.js?v=1781893916"></script>
<script src="/js/common.js?v=1781809662"></script> <script src="/js/common.js?v=1781893916"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">

View File

@@ -284,7 +284,7 @@
</div> </div>
<script src="/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/js/conditions.js?v=1781809662"></script> <script src="/js/conditions.js?v=1781893916"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>

View File

@@ -94,8 +94,8 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/spotsbandsandmap.js?v=1781809662"></script> <script src="/js/spotsbandsandmap.js?v=1781893916"></script>
<script src="/js/map.js?v=1781809662"></script> <script src="/js/map.js?v=1781893916"></script>
<script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-map").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -104,8 +104,8 @@
<script> <script>
let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %}; let spotProvidersEnabledByDefault = {% raw json_encode(web_ui_options["spot-providers-enabled-by-default"]) %};
</script> </script>
<script src="/js/spotsbandsandmap.js?v=1781809662"></script> <script src="/js/spotsbandsandmap.js?v=1781893916"></script>
<script src="/js/spots.js?v=1781809662"></script> <script src="/js/spots.js?v=1781893916"></script>
<script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script> <script>$(document).ready(function() { $("#nav-link-spots").addClass("active"); }); <!-- highlight active page in nav --></script>
{% end %} {% end %}

View File

@@ -59,7 +59,7 @@
</div> </div>
</div> </div>
<script src="/js/status.js?v=1781809662"></script> <script src="/js/status.js?v=1781893916"></script>
<script> <script>
$(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav --> $(document).ready(function() { $("#nav-link-status").addClass("active"); }); <!-- highlight active page in nav -->
</script> </script>

View File

@@ -90,7 +90,7 @@ input#search {
i#searchicon { i#searchicon {
position: absolute; position: absolute;
left: 0rem; left: 0;
top: 2px; top: 2px;
padding: 10px; padding: 10px;
pointer-events: none; pointer-events: none;
@@ -217,7 +217,7 @@ div#map {
} }
.leaflet-container { .leaflet-container {
font-family: var(--bs-body-font-family) !important; font-family: var(--bs-body-font-family) sans-serif !important;
} }
.leaflet-control-attribution { .leaflet-control-attribution {
background: none; background: none;

View File

@@ -33,44 +33,44 @@ function addSpot() {
saveSettings(); saveSettings();
// Unpack the user's entered values // Unpack the user's entered values
var dx = $("#dx-call").val().toUpperCase(); const dx = $("#dx-call").val().toUpperCase();
var freqStr = $("#freq").val(); const freqStr = $("#freq").val();
var mode = $("#mode")[0].value; const mode = $("#mode")[0].value;
var sig = $("#sig")[0].value; const sig = $("#sig")[0].value;
var sigRef = $("#sig-ref").val(); const sigRef = $("#sig-ref").val();
var dxGrid = $("#dx-grid").val(); const dxGrid = $("#dx-grid").val();
var comment = $("#comment").val(); const comment = $("#comment").val();
var de = $("#de-call").val().toUpperCase(); const de = $("#de-call").val().toUpperCase();
var spot = {} const spot = {};
if (dx != "") { if (dx !== "") {
spot["dx_call"] = dx; spot["dx_call"] = dx;
} else { } else {
showAddSpotError("A DX callsign is required in order to spot."); showAddSpotError("A DX callsign is required in order to spot.");
return; return;
} }
if (freqStr != "") { if (freqStr !== "") {
spot["freq"] = parseFloat(freqStr) * 1000; spot["freq"] = parseFloat(freqStr) * 1000;
} else { } else {
showAddSpotError("A frequency is required in order to spot."); showAddSpotError("A frequency is required in order to spot.");
return; return;
} }
if (mode != "") { if (mode !== "") {
spot["mode"] = mode; spot["mode"] = mode;
} }
if (sig != "") { if (sig !== "") {
spot["sig"] = sig; spot["sig"] = sig;
} }
if (sigRef != "") { if (sigRef !== "") {
spot["sig_refs"] = [{id: sigRef}]; spot["sig_refs"] = [{id: sigRef}];
} }
if (dxGrid != "") { if (dxGrid !== "") {
spot["dx_grid"] = dxGrid; spot["dx_grid"] = dxGrid;
} }
if (comment != "") { if (comment !== "") {
spot["comment"] = comment; spot["comment"] = comment;
} }
if (de != "") { if (de !== "") {
spot["de_call"] = de; spot["de_call"] = de;
} else { } else {
showAddSpotError("A spotter callsign is required in order to spot."); showAddSpotError("A spotter callsign is required in order to spot.");
@@ -83,7 +83,7 @@ function addSpot() {
contentType : 'application/json', contentType : 'application/json',
type : 'POST', type : 'POST',
timeout: 10000, timeout: 10000,
success: async function (result) { success: async function() {
$("#result-good").html("<div class='alert alert-success fade show mb-0 mt-4' role='alert'><i class='fa-solid fa-check'></i> Spot submitted. Returning you to the spots list...</div>"); $("#result-good").html("<div class='alert alert-success fade show mb-0 mt-4' role='alert'><i class='fa-solid fa-check'></i> Spot submitted. Returning you to the spots list...</div>");
$("#result-bad").html(""); $("#result-bad").html("");
setTimeout(() => { setTimeout(() => {
@@ -103,7 +103,7 @@ function addSpot() {
// Show an "add spot" error. // Show an "add spot" error.
function showAddSpotError(text) { function showAddSpotError(text) {
var div = $("<div class='alert alert-danger alert-dismissible fade show mb-0 mt-4' role='alert'></div>"); const div = $("<div class='alert alert-danger alert-dismissible fade show mb-0 mt-4' role='alert'></div>");
div.append("<i class='fa-solid fa-triangle-exclamation'></i> "); div.append("<i class='fa-solid fa-triangle-exclamation'></i> ");
div.append(document.createTextNode(text)); div.append(document.createTextNode(text));
div.append("<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>"); div.append("<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button>");

View File

@@ -2,7 +2,7 @@
const REFRESH_INTERVAL_SEC = 60 * 10; const REFRESH_INTERVAL_SEC = 60 * 10;
// Storage for the alert data that the server gives us. // Storage for the alert data that the server gives us.
var alerts = [] let alerts = [];
// Load alerts and populate the table. // Load alerts and populate the table.
function loadAlerts() { function loadAlerts() {
@@ -19,15 +19,15 @@ function loadAlerts() {
// Build a query string for the API, based on the filters that the user has selected. // Build a query string for the API, based on the filters that the user has selected.
function buildQueryString(includeCredentials) { function buildQueryString(includeCredentials) {
var str = "?"; let str = "?";
["dx_continent", "source"].forEach(fn => { ["dx_continent", "source"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) { if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&"; str = str + getQueryStringFor(fn) + "&";
} }
}); });
str = str + "limit=" + $("#alerts-to-fetch option:selected").val(); str = str + "limit=" + $("#alerts-to-fetch option:selected").val();
var maxDur = $("#max-duration option:selected").val(); const maxDur = $("#max-duration option:selected").val();
if (maxDur != "9999999999") { if (maxDur !== "9999999999") {
str = str + "&max_duration=" + maxDur; str = str + "&max_duration=" + maxDur;
} }
if ($("#dxpeditions_skip_max_duration_check")[0].checked) { if ($("#dxpeditions_skip_max_duration_check")[0].checked) {
@@ -42,16 +42,16 @@ function buildQueryString(includeCredentials) {
// Update the alerts table // Update the alerts table
function updateTable() { function updateTable() {
// Use local time instead of UTC? // Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local"; const useLocalTime = $("#timeZone")[0].value === "local";
// Table data toggles // Table data toggles
var showStartTime = $("#tableShowStartTime")[0].checked; const showStartTime = $("#tableShowStartTime")[0].checked;
var showEndTime = $("#tableShowEndTime")[0].checked; const showEndTime = $("#tableShowEndTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked; const showDX = $("#tableShowDX")[0].checked;
var showFreqsModes = $("#tableShowFreqsModes")[0].checked; const showFreqsModes = $("#tableShowFreqsModes")[0].checked;
var showComment = $("#tableShowComment")[0].checked; const showComment = $("#tableShowComment")[0].checked;
var showSource = $("#tableShowSource")[0].checked; const showSource = $("#tableShowSource")[0].checked;
var showRef = $("#tableShowRef")[0].checked; const showRef = $("#tableShowRef")[0].checked;
// Populate table with headers // Populate table with headers
let table = $("#table"); let table = $("#table");
@@ -83,10 +83,10 @@ function updateTable() {
// Split alerts into three types, each of which will get its own table header: On now, next 24h, and later. "On now" // 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 // 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. // 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()) const 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())); || ((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()); const 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()); const later = alerts.filter(a => moment.unix(a["start_time"]).utc().subtract(24, 'hours').isSameOrAfter());
if (onNow.length > 0) { if (onNow.length > 0) {
table.find('tbody').append('<tr><td colspan="100" class="bg-primary-subtle" style="text-align:center;">On Now</td></tr>'); table.find('tbody').append('<tr><td colspan="100" class="bg-primary-subtle" style="text-align:center;">On Now</td></tr>');
@@ -103,14 +103,14 @@ function updateTable() {
addAlertRowsToTable(table.find('tbody'), later); addAlertRowsToTable(table.find('tbody'), later);
} }
if (onNow.length == 0 && next24h.length == 0 && later.length == 0) { if (onNow.length === 0 && next24h.length === 0 && later.length === 0) {
table.find('tbody').append('<tr class="bg-danger-subtle"><td colspan="100" style="text-align:center;">No alerts match your filters.</td></tr>'); table.find('tbody').append('<tr class="bg-danger-subtle"><td colspan="100" style="text-align:center;">No alerts match your filters.</td></tr>');
} }
} }
// Add a row to tbody for each alert in the provided list // Add a row to tbody for each alert in the provided list
function addAlertRowsToTable(tbody, alerts) { function addAlertRowsToTable(tbody, alerts) {
var count = 0; let count = 0;
alerts.forEach(a => { alerts.forEach(a => {
// Create row // Create row
let $tr = $('<tr>'); let $tr = $('<tr>');
@@ -118,29 +118,29 @@ function addAlertRowsToTable(tbody, alerts) {
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of // 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 // 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. // which cause the table-striped colouring to go awry.
if (count % 2 == 1) { if (count % 2 === 1) {
$tr.addClass("table-active"); $tr.addClass("table-active");
} }
// Use local time instead of UTC? // Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local"; const useLocalTime = $("#timeZone")[0].value === "local";
// Table data toggles // Table data toggles
var showStartTime = $("#tableShowStartTime")[0].checked; const showStartTime = $("#tableShowStartTime")[0].checked;
var showEndTime = $("#tableShowEndTime")[0].checked; const showEndTime = $("#tableShowEndTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked; const showDX = $("#tableShowDX")[0].checked;
var showFreqsModes = $("#tableShowFreqsModes")[0].checked; const showFreqsModes = $("#tableShowFreqsModes")[0].checked;
var showComment = $("#tableShowComment")[0].checked; const showComment = $("#tableShowComment")[0].checked;
var showSource = $("#tableShowSource")[0].checked; const showSource = $("#tableShowSource")[0].checked;
var showRef = $("#tableShowRef")[0].checked; const showRef = $("#tableShowRef")[0].checked;
// Get times for the alert, and convert to local time if necessary. // Get times for the alert, and convert to local time if necessary.
var start_time_utc = moment.unix(a["start_time"]).utc(); const start_time_utc = moment.unix(a["start_time"]).utc();
var start_time_local = start_time_utc.clone().local(); const start_time_local = start_time_utc.clone().local();
start_time = useLocalTime ? start_time_local : start_time_utc; const start_time = useLocalTime ? start_time_local : start_time_utc;
var end_time_utc = moment.unix(a["end_time"]).utc(); const end_time_utc = moment.unix(a["end_time"]).utc();
var end_time_local = end_time_utc.clone().local(); const end_time_local = end_time_utc.clone().local();
end_time = useLocalTime ? end_time_local : end_time_utc; 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 // 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. // different year to the current year, in which case the year is inserted between month and hour.
@@ -150,22 +150,22 @@ function addAlertRowsToTable(tbody, alerts) {
// Overriding all of that, if the start time is 00:00 and the end time is 23:59 when considered in UTC, the // 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. // 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. // Finally, if there is no end date set, "---" is displayed.
var whole_days = start_time_utc.format("HH:mm") == "00:00" && const whole_days = start_time_utc.format("HH:mm") === "00:00" &&
(end_time_utc != null || end_time_utc > 0 || end_time_utc.format("HH:mm") == "23:59"); (end_time_utc === 0 || end_time_utc.format("HH:mm") === "23:59");
var hours_minutes_format = whole_days ? "" : " HH:mm"; const hours_minutes_format = whole_days ? "" : " HH:mm";
var start_time_formatted = start_time.format("D MMM" + hours_minutes_format); let start_time_formatted = start_time.format("D MMM" + hours_minutes_format);
if (start_time.format("YYYY") != moment().format("YYYY")) { if (start_time.format("YYYY") !== moment().format("YYYY")) {
start_time_formatted = start_time.format("D MMM YYYY" + hours_minutes_format); 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")) { } else if (useLocalTime && start_time.format("D MMM YYYY") === moment().format("D MMM YYYY")) {
start_time_formatted = start_time.format("[Today]" + hours_minutes_format); start_time_formatted = start_time.format("[Today]" + hours_minutes_format);
} }
var end_time_formatted = "---"; let end_time_formatted = "---";
if (end_time_utc != null && end_time_utc > 0 && end_time != null) { if (end_time_utc > 0 && end_time != null) {
var end_time_formatted = whole_days ? start_time_formatted : end_time.format("HH:mm"); 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("D MMM") !== start_time.format("D MMM")) {
if (end_time.format("YYYY") != moment().format("YYYY")) { if (end_time.format("YYYY") !== moment().format("YYYY")) {
end_time_formatted = end_time.format("D MMM YYYY" + hours_minutes_format); 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")) { } else if (useLocalTime && end_time.format("D MMM YYYY") === moment().format("D MMM YYYY")) {
end_time_formatted = end_time.format("[Today]" + hours_minutes_format); end_time_formatted = end_time.format("[Today]" + hours_minutes_format);
} else { } else {
end_time_formatted = end_time.format("D MMM" + hours_minutes_format); end_time_formatted = end_time.format("D MMM" + hours_minutes_format);
@@ -174,52 +174,52 @@ function addAlertRowsToTable(tbody, alerts) {
} }
// Format dx country // Format dx country
var dx_country = a["dx_country"] let dx_country = a["dx_country"];
if (dx_country == null) { if (dx_country == null) {
dx_country = "Unknown or not a country" dx_country = "Unknown or not a country"
} }
// Format DX flag // Format DX flag
var dx_flag = "<i class='fa-solid fa-globe-africa'></i>"; let dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (a["dx_dxcc_id"] && a["dx_dxcc_id"] != null && a["dx_dxcc_id"] != 0) { if (a["dx_dxcc_id"] && a["dx_dxcc_id"] != null && a["dx_dxcc_id"] !== 0) {
dx_flag = `<img src="img/flags/${a['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`; dx_flag = `<img src="img/flags/${a['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
} }
// Format dx calls // Format dx calls
var dx_calls_html = ""; let dx_calls_html = "";
if (a["dx_calls"] != null) { 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(", "); 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 // Format DXpedition country
var dx_country_html = ""; let dx_country_html = "";
if (a["is_dxpedition"] == true && a["dx_country"] != null && a["dx_country"] != "") { if (a["is_dxpedition"] === true && a["dx_country"] != null && a["dx_country"] !== "") {
dx_country_html = `<br/>${a["dx_country"]}`; dx_country_html = `<br/>${a["dx_country"]}`;
} }
// Format freqs & modes // Format freqs & modes
var freqsModesText = ""; let freqsModesText = "";
if (a["freqs_modes"] != null) { if (a["freqs_modes"] != null) {
freqsModesText = escapeHtml(a["freqs_modes"]); freqsModesText = escapeHtml(a["freqs_modes"]);
} }
// Format comment // Format comment
var commentText = ""; let commentText = "";
if (a["comment"] != null) { if (a["comment"] != null) {
commentText = escapeHtml(a["comment"]); commentText = escapeHtml(a["comment"]);
} }
// Sig or fallback to source // Sig or fallback to source
var sigSourceText = a["source"]; let sigSourceText = a["source"];
if (a["sig"]) { if (a["sig"]) {
sigSourceText = a["sig"]; sigSourceText = a["sig"];
} }
// Format sig_refs // Format sig_refs
var sig_refs = ""; let sig_refs = "";
if (a["sig_refs"] != null) { if (a["sig_refs"] != null) {
var items = [] const items = [];
for (var i = 0; i < a["sig_refs"].length; i++) { for (let i = 0; i < a["sig_refs"].length; i++) {
if (a["sig_refs"][i]["url"] != null) { if (a["sig_refs"][i]["url"] != null) {
items[i] = `<a href='${encodeURI(a["sig_refs"][i]["url"])}' title='${escapeHtml(a["sig_refs"][i]["name"])}' target='_new' class='sig-ref-link'>${escapeHtml(a["sig_refs"][i]["id"])}</a>` items[i] = `<a href='${encodeURI(a["sig_refs"][i]["url"])}' title='${escapeHtml(a["sig_refs"][i]["name"])}' target='_new' class='sig-ref-link'>${escapeHtml(a["sig_refs"][i]["id"])}</a>`
} else { } else {
@@ -254,11 +254,11 @@ function addAlertRowsToTable(tbody, alerts) {
tbody.append($tr); tbody.append($tr);
// Second row for mobile view only, containing source, ref, freqs/modes & comment // Second row for mobile view only, containing source, ref, freqs/modes & comment
$tr2 = $("<tr class='hidenotonmobile'>"); const $tr2 = $("<tr class='hidenotonmobile'>");
if (count % 2 == 1) { if (count % 2 === 1) {
$tr2.addClass("table-active"); $tr2.addClass("table-active");
} }
$td2 = $("<td colspan='100'>"); const $td2 = $("<td colspan='100'>");
if (showSource) { if (showSource) {
$td2.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(a["sig"], "fa-globe-africa")}'></i></span> `); $td2.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(a["sig"], "fa-globe-africa")}'></i></span> `);
} }
@@ -319,7 +319,7 @@ $(document).ready(function() {
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA // 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. // after some time has passed with it in the background.
addEventListener("visibilitychange", (event) => { addEventListener("visibilitychange", () => {
if (!document.hidden) { if (!document.hidden) {
loadAlerts(); loadAlerts();
} }

View File

@@ -25,7 +25,7 @@ function loadSpots() {
// Build a query string for the API, based on the filters that the user has selected. // Build a query string for the API, based on the filters that the user has selected.
function buildQueryString(includeCredentials) { function buildQueryString(includeCredentials) {
var str = "?"; let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => { ["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) { if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&"; str = str + getQueryStringFor(fn) + "&";
@@ -43,7 +43,7 @@ function buildQueryString(includeCredentials) {
// Update the bands display // Update the bands display
function updateBands() { function updateBands() {
// Stop here if nothing to display // Stop here if nothing to display
var bandsContainer = $("#bands-container"); const bandsContainer = $("#bands-container");
if (spots.length === 0) { if (spots.length === 0) {
bandsContainer.html("<div class='alert alert-danger' role='alert'>No spots match your filters.</div>"); bandsContainer.html("<div class='alert alert-danger' role='alert'>No spots match your filters.</div>");
return; return;
@@ -52,7 +52,7 @@ function updateBands() {
// Do some harsher de-duping. Because we only display callsign, frequency and mode here, the previous // Do some harsher de-duping. Because we only display callsign, frequency and mode here, the previous
// de-duplication could have let some through that don't look like dupes on the map, but would do here. // de-duplication could have let some through that don't look like dupes on the map, but would do here.
// Typically that's a person activating two programs at the same time, e.g. POTA & WWFF. // Typically that's a person activating two programs at the same time, e.g. POTA & WWFF.
spotList = removeDuplicatesForBandPanel(spots); const spotList = removeDuplicatesForBandPanel(spots);
// Convert to a map of band names to the spots on that band. Bands with no // Convert to a map of band names to the spots on that band. Bands with no
// spots in view will not be present. // spots in view will not be present.
@@ -67,10 +67,10 @@ function updateBands() {
}); });
// Track if any columns end up taller than expected, so we can resize the container and avoid vertical scroll. // Track if any columns end up taller than expected, so we can resize the container and avoid vertical scroll.
var maxHeightBand = 0; let maxHeightBand = 0;
// Build up table content for each band // Build up table content for each band
var table = $('<table id="bands-table">').append('<thead><tr></tr></thead><tbody><tr></tr></tbody>'); const table = $('<table id="bands-table">').append('<thead><tr></tr></thead><tbody><tr></tr></tbody>');
bandToSpots.forEach(function (spotList, bandName) { bandToSpots.forEach(function (spotList, bandName) {
// Get the colours for the band from the first spot, and prepare the header // Get the colours for the band from the first spot, and prepare the header
table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`); table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`);
@@ -82,7 +82,7 @@ function updateBands() {
// Print the frequency band markers. This is 41 steps to divide the band evenly into 40 markers. One in every // Print the frequency band markers. This is 41 steps to divide the band evenly into 40 markers. One in every
// four will show the actual frequency, the others will just be dashes. // four will show the actual frequency, the others will just be dashes.
bandMarkersDiv = $('<div class="band-markers">'); const bandMarkersDiv = $('<div class="band-markers">');
const freqStep = (band.end_freq - band.start_freq) / 40.0; const freqStep = (band.end_freq - band.start_freq) / 40.0;
for (let i = 0; i <= 40; i++) { for (let i = 0; i <= 40; i++) {
if (i % 4 === 0) { if (i % 4 === 0) {
@@ -95,8 +95,8 @@ function updateBands() {
} }
// Prepare the spots list // Prepare the spots list
var bandSpotsDiv = $("<div class='band-spots'>"); const bandSpotsDiv = $("<div class='band-spots'>");
var lastSpotPxDownBand = -999; let lastSpotPxDownBand = -999;
// Sort by frequency so have a consistent order in which to plan where they will appear on the band div. // Sort by frequency so have a consistent order in which to plan where they will appear on the band div.
spotList.sort(function(a, b) { return a.freq - b.freq; }); spotList.sort(function(a, b) { return a.freq - b.freq; });
// First calculate how we should be displaying the spots. There are three "modes" to try to place them in a // First calculate how we should be displaying the spots. There are three "modes" to try to place them in a
@@ -118,8 +118,8 @@ function updateBands() {
// Mode 1 or 2. Run through adding things to the list forwards as a test. // Mode 1 or 2. Run through adding things to the list forwards as a test.
spotList.forEach(s => { spotList.forEach(s => {
// Work out how far down the div to draw it // Work out how far down the div to draw it
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text const percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
var pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX; let pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX;
if (pxDownBand < lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX) { if (pxDownBand < lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
pxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap pxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap
} }
@@ -135,8 +135,8 @@ function updateBands() {
lastSpotPxDownBand = 999999; lastSpotPxDownBand = 999999;
spotList.reverse().forEach(s => { spotList.reverse().forEach(s => {
// Work out how far down the div to draw it // Work out how far down the div to draw it
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text const percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
var pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX; let pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX;
if (pxDownBand > lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX) { if (pxDownBand > lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX) {
pxDownBand = lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap pxDownBand = lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap
} }
@@ -155,19 +155,19 @@ function updateBands() {
// Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some // Work out how tall the canvas should be. Normally this is matching the normal band column height, but if some
// spots have gone off the end of the band markers and stretched their div, we need to resize the canvas to // spots have gone off the end of the band markers and stretched their div, we need to resize the canvas to
// match, otherwise we have nowhere to draw their connecting lines. // match, otherwise we have nowhere to draw their connecting lines.
var canvasHeight = Math.max(BAND_COLUMN_HEIGHT_PX, lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX); const canvasHeight = Math.max(BAND_COLUMN_HEIGHT_PX, lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX);
maxHeightBand = Math.max(maxHeightBand, canvasHeight); maxHeightBand = Math.max(maxHeightBand, canvasHeight);
// Draw horizontal or diagonal lines to join up the "real" frequency with where the spot div ended up // Draw horizontal or diagonal lines to join up the "real" frequency with where the spot div ended up
var bandLinesCanvas = $(`<canvas class='band-lines-canvas' width='${BAND_COLUMN_CANVAS_WIDTH_PX}px' height='${canvasHeight}px' style='height:${canvasHeight}px !important;'>`); const bandLinesCanvas = $(`<canvas class='band-lines-canvas' width='${BAND_COLUMN_CANVAS_WIDTH_PX}px' height='${canvasHeight}px' style='height:${canvasHeight}px !important;'>`);
spotList.forEach(s => { spotList.forEach(s => {
// Work out how far down the div to draw it // Work out how far down the div to draw it
var percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text const percentDownBand = (s.freq - band.start_freq) / (band.end_freq - band.start_freq) * 0.97; // not 100% due to fudge, the first and last dashes are not exactly at the top and bottom of the div as some space is needed for text
var pxDownBandFreq = (percentDownBand + 0.015) * BAND_COLUMN_HEIGHT_PX; // same fudge but add half to put the left end of the line in the right place const pxDownBandFreq = (percentDownBand + 0.015) * BAND_COLUMN_HEIGHT_PX; // same fudge but add half to put the left end of the line in the right place
var pxDownBandLabel = s["pxDownBandLabel"] + (BAND_COLUMN_SPOT_DIV_HEIGHT_PX / 1.75); // line should be to the vertical text-centre spot, not to the top corner const pxDownBandLabel = s["pxDownBandLabel"] + (BAND_COLUMN_SPOT_DIV_HEIGHT_PX / 1.75); // line should be to the vertical text-centre spot, not to the top corner
// Draw the line on the canvas // Draw the line on the canvas
var ctx = bandLinesCanvas[0].getContext('2d'); const ctx = bandLinesCanvas[0].getContext('2d');
ctx.beginPath(); ctx.beginPath();
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.lineCap = "round"; ctx.lineCap = "round";
@@ -178,8 +178,8 @@ function updateBands() {
}); });
// Assemble the table cell // Assemble the table cell
td = $("<td>"); const td = $("<td>");
container = $("<div class='band-container'>"); const container = $("<div class='band-container'>");
container.append(bandLinesCanvas); container.append(bandLinesCanvas);
container.append(bandMarkersDiv); container.append(bandMarkersDiv);
container.append(bandSpotsDiv); container.append(bandSpotsDiv);
@@ -213,7 +213,6 @@ function removeDuplicatesForBandPanel(spotList) {
if (s.dx_call === check.dx_call && s.freq === check.freq && s.mode === check.mode) { if (s.dx_call === check.dx_call && s.freq === check.freq && s.mode === check.mode) {
// Find which one to keep and which to delete // Find which one to keep and which to delete
const checkSpotNewer = check.time > s.time; const checkSpotNewer = check.time > s.time;
const keepSpot = checkSpotNewer ? check : s;
const deleteSpot = checkSpotNewer ? s : check; const deleteSpot = checkSpotNewer ? s : check;
// Aggregate list of spots to remove // Aggregate list of spots to remove
spotsToRemove.push(deleteSpot.uid); spotsToRemove.push(deleteSpot.uid);

View File

@@ -1,7 +1,7 @@
// Storage for the options that the server gives us. This will define our filters. // Storage for the options that the server gives us. This will define our filters.
var options = {}; let options = {};
// Last time we updated the spots/alerts list on display. // Last time we updated the spots/alerts list on display.
var lastUpdateTime; let lastUpdateTime;
// Normally load user settings from local storage, unless embedded mode is in use // Normally load user settings from local storage, unless embedded mode is in use
let useLocalStorage = true; let useLocalStorage = true;
@@ -21,8 +21,8 @@ function saveSettings() {
}); });
// Password fields are only saved if the corresponding "remember password" checkbox is ticked. // Password fields are only saved if the corresponding "remember password" checkbox is ticked.
$(".password-field").each(function () { $(".password-field").each(function () {
var pwKey = "#" + $(this)[0].id + ":value"; const pwKey = "#" + $(this)[0].id + ":value";
var rememberCheckboxId = $(this).data("remember-checkbox"); const rememberCheckboxId = $(this).data("remember-checkbox");
if (rememberCheckboxId && $("#" + rememberCheckboxId)[0] && $("#" + rememberCheckboxId)[0].checked) { if (rememberCheckboxId && $("#" + rememberCheckboxId)[0] && $("#" + rememberCheckboxId)[0].checked) {
localStorage.setItem(pwKey, JSON.stringify($(this)[0].value)); localStorage.setItem(pwKey, JSON.stringify($(this)[0].value));
} else { } else {
@@ -39,7 +39,7 @@ function loadSettings() {
Object.keys(localStorage).forEach(function (key) { Object.keys(localStorage).forEach(function (key) {
if (key.startsWith("#") && key.includes(":")) { if (key.startsWith("#") && key.includes(":")) {
// Split the key back into an element ID and a property // Split the key back into an element ID and a property
var split = key.split(":"); const split = key.split(":");
$(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key))); $(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key)));
} }
}); });
@@ -76,21 +76,13 @@ function loadURLParams() {
updateFilterFromParam(params, "de_continent", "de_continent"); updateFilterFromParam(params, "de_continent", "de_continent");
} }
// Update an HTML checkbox element so that its selected matches the given parameter (which must have a true or false value)
function updateCheckboxFromParam(params, paramName, checkboxID) {
let v = params.get(paramName);
if (v != null) {
$("#" + checkboxID).prop("checked", (v === "true") ? true : false);
}
}
// Update an HTML select element so that its value matches the given parameter // Update an HTML select element so that its value matches the given parameter
function updateSelectFromParam(params, paramName, selectID) { function updateSelectFromParam(params, paramName, selectID) {
let v = params.get(paramName); let v = params.get(paramName);
if (v != null) { if (v != null) {
$("#" + selectID).prop("value", v); $("#" + selectID).prop("value", v);
// Extra check if this is the "color scheme" select // Extra check if this is the "color scheme" select
if (selectID == "color-scheme") { if (selectID === "color-scheme") {
setColorScheme(v); setColorScheme(v);
} }
} }
@@ -128,16 +120,16 @@ function getSelectedFilterOptions(parameter) {
// For a parameter, such as dx_continent, return true if all possible options are enabled. (In this case, we don't need // For a parameter, such as dx_continent, return true if all possible options are enabled. (In this case, we don't need
// to bother sending this as one of the query parameters to the API; no parameter provided implies "send everything".) // to bother sending this as one of the query parameters to the API; no parameter provided implies "send everything".)
function allFilterOptionsSelected(parameter) { function allFilterOptionsSelected(parameter) {
var filter = $(".filter-button-" + parameter).filter(function () { const filter = $(".filter-button-" + parameter).filter(function () {
return !this.checked; return !this.checked;
}).get(); }).get();
return filter.length == 0; return filter.length === 0;
} }
// Generate a filter card with inline checkboxes plus All/None links. // Generate a filter card with inline checkboxes plus All/None links.
function generateMultiToggleFilterCard(elementID, filterQuery, options) { function generateMultiToggleFilterCard(elementID, filterQuery, options) {
var $row = $('<div>'); const $row = $('<div>');
options.forEach(o => { options.forEach(o => {
$row.append(`<div class="form-check form-check-inline"><input type="checkbox" class="form-check-input filter-button-${filterQuery} storeable-checkbox" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" for="filter-button-${filterQuery}-${o}">${o}</label></div>`); $row.append(`<div class="form-check form-check-inline"><input type="checkbox" class="form-check-input filter-button-${filterQuery} storeable-checkbox" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" for="filter-button-${filterQuery}-${o}">${o}</label></div>`);
}); });
@@ -161,12 +153,13 @@ function updateRefreshDisplay() {
let updatingString = "Updating..." let updatingString = "Updating..."
if (secSinceUpdate < REFRESH_INTERVAL_SEC) { if (secSinceUpdate < REFRESH_INTERVAL_SEC) {
count = REFRESH_INTERVAL_SEC - secSinceUpdate; count = REFRESH_INTERVAL_SEC - secSinceUpdate;
let number;
if (count <= 60) { if (count <= 60) {
var number = count.toFixed(0); number = count.toFixed(0);
updatingString = "<span class='nowrap'>Updating in " + number + " second" + (number != "1" ? "s" : "") + ".</span>"; updatingString = "<span class='nowrap'>Updating in " + number + " second" + (number !== "1" ? "s" : "") + ".</span>";
} else { } else {
var number = Math.round(count / 60.0).toFixed(0); number = Math.round(count / 60.0).toFixed(0);
updatingString = "<span class='nowrap'>Updating in " + number + " minute" + (number != "1" ? "s" : "") + ".</span>"; updatingString = "<span class='nowrap'>Updating in " + number + " minute" + (number !== "1" ? "s" : "") + ".</span>";
} }
} }
$("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString); $("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString);
@@ -188,7 +181,7 @@ function columnsUpdated() {
// Function to set the colour scheme based on the state of the UI select box // Function to set the colour scheme based on the state of the UI select box
function setColorSchemeFromUI() { function setColorSchemeFromUI() {
let theme = $("#color-scheme option:selected").val(); let theme = $("#color-scheme option:selected").val();
if (theme != "") { if (theme !== "") {
setColorScheme(theme); setColorScheme(theme);
saveSettings(); saveSettings();
} }
@@ -196,8 +189,8 @@ function setColorSchemeFromUI() {
// Function to set the color scheme. Supported values: "dark", "light", "auto" // Function to set the color scheme. Supported values: "dark", "light", "auto"
function setColorScheme(mode) { function setColorScheme(mode) {
let effectiveModeDark = mode == "dark"; let effectiveModeDark = mode === "dark";
if (mode == "auto") { if (mode === "auto") {
effectiveModeDark = window.matchMedia('(prefers-color-scheme: dark)').matches effectiveModeDark = window.matchMedia('(prefers-color-scheme: dark)').matches
} }
$("html").attr("data-bs-theme", effectiveModeDark ? "dark" : "light"); $("html").attr("data-bs-theme", effectiveModeDark ? "dark" : "light");
@@ -283,16 +276,16 @@ function closeDataPanel() {
// Build a query string fragment containing any QRZ.com / HamQTH credentials the user has supplied, // Build a query string fragment containing any QRZ.com / HamQTH credentials the user has supplied,
// provided the corresponding "enabled" checkbox is ticked. // provided the corresponding "enabled" checkbox is ticked.
function getCredentialQueryString() { function getCredentialQueryString() {
var str = ""; let str = "";
if ($("#qrz-enabled")[0] && $("#qrz-enabled")[0].checked) { if ($("#qrz-enabled")[0] && $("#qrz-enabled")[0].checked) {
var qrzUsername = $("#qrz-username").val(); const qrzUsername = $("#qrz-username").val();
var qrzPassword = $("#qrz-password").val(); const qrzPassword = $("#qrz-password").val();
if (qrzUsername) str += "&qrz_username=" + encodeURIComponent(qrzUsername); if (qrzUsername) str += "&qrz_username=" + encodeURIComponent(qrzUsername);
if (qrzPassword) str += "&qrz_password=" + encodeURIComponent(qrzPassword); if (qrzPassword) str += "&qrz_password=" + encodeURIComponent(qrzPassword);
} }
if ($("#hamqth-enabled")[0] && $("#hamqth-enabled")[0].checked) { if ($("#hamqth-enabled")[0] && $("#hamqth-enabled")[0].checked) {
var hamqthUsername = $("#hamqth-username").val(); const hamqthUsername = $("#hamqth-username").val();
var hamqthPassword = $("#hamqth-password").val(); const hamqthPassword = $("#hamqth-password").val();
if (hamqthUsername) str += "&hamqth_username=" + encodeURIComponent(hamqthUsername); if (hamqthUsername) str += "&hamqth_username=" + encodeURIComponent(hamqthUsername);
if (hamqthPassword) str += "&hamqth_password=" + encodeURIComponent(hamqthPassword); if (hamqthPassword) str += "&hamqth_password=" + encodeURIComponent(hamqthPassword);
} }

View File

@@ -9,10 +9,10 @@ function calcBearing(lat1, lon1, lat2, lon2) {
lon1 *= Math.PI / 180; lon1 *= Math.PI / 180;
lat2 *= Math.PI / 180; lat2 *= Math.PI / 180;
lon2 *= Math.PI / 180; lon2 *= Math.PI / 180;
var lonDelta = lon2 - lon1; const lonDelta = lon2 - lon1;
var y = Math.sin(lonDelta) * Math.cos(lat2); const y = Math.sin(lonDelta) * Math.cos(lat2);
var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta); const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta);
var bearing = Math.atan2(y, x); let bearing = Math.atan2(y, x);
bearing = bearing * (180 / Math.PI); bearing = bearing * (180 / Math.PI);
if ( bearing < 0 ) { bearing += 360; } if ( bearing < 0 ) { bearing += 360; }
return bearing; return bearing;

View File

@@ -12,19 +12,19 @@ const ITU_ZONES_COLOR_DARK = 'rgba(120, 120, 60, 1.0)';
const WAB_WAI_GRID_COLOR_DARK = 'rgba(60, 60, 120, 1.0)'; const WAB_WAI_GRID_COLOR_DARK = 'rgba(60, 60, 120, 1.0)';
// Map layers // Map layers
var backgroundTileLayer; let backgroundTileLayer;
var markersLayer; let markersLayer;
var geodesicsLayer; let geodesicsLayer;
var oms; let oms;
var terminator; let terminator;
var maidenheadGrid; let maidenheadGrid;
var cqZones; let cqZones;
var ituZones; let ituZones;
var wabwaiGrid; let wabwaiGrid;
// Tracks the currently-loaded basemap provider string to avoid unnecessary tile reloads // Tracks the currently-loaded basemap provider string to avoid unnecessary tile reloads
var loadedBasemap; let loadedBasemap;
// Tracks whether this is the first display of markers after page load // Tracks whether this is the first display of markers after page load
var firstLoad = true; let firstLoad = true;
// Load spots and populate the map. // Load spots and populate the map.
function loadSpots() { function loadSpots() {
@@ -41,7 +41,7 @@ function loadSpots() {
// Build a query string for the API, based on the filters that the user has selected. // Build a query string for the API, based on the filters that the user has selected.
function buildQueryString(includeCredentials) { function buildQueryString(includeCredentials) {
var str = "?"; let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => { ["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) { if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&"; str = str + getQueryStringFor(fn) + "&";
@@ -65,7 +65,7 @@ function updateMap() {
// Make new markers for all spots that match the filter // Make new markers for all spots that match the filter
spots.forEach(function (s) { spots.forEach(function (s) {
var m = L.marker([s["dx_latitude"], s["dx_longitude"]], {icon: getIcon(s)}); const m = L.marker([s["dx_latitude"], s["dx_longitude"]], {icon: getIcon(s)});
m.bindPopup(getTooltipText(s)); m.bindPopup(getTooltipText(s));
markersLayer.addLayer(m); markersLayer.addLayer(m);
oms.addMarker(m); oms.addMarker(m);
@@ -73,7 +73,7 @@ function updateMap() {
// Create geodesics if required // Create geodesics if required
if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) { if ($("#mapShowGeodesics")[0].checked && s["de_latitude"] != null && s["de_longitude"] != null) {
try { try {
var geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], { const geodesic = L.geodesic([[s["de_latitude"], s["de_longitude"]], m.getLatLng()], {
color: bandToColor(s['band']), color: bandToColor(s['band']),
wrap: false, wrap: false,
steps: 5 steps: 5
@@ -88,7 +88,7 @@ function updateMap() {
// On first load, zoom to the extent of the markers // On first load, zoom to the extent of the markers
if (firstLoad) { if (firstLoad) {
if (markersLayer.getLayers().length >= 2) { if (markersLayer.getLayers().length >= 2) {
var group = new L.featureGroup(markersLayer.getLayers()); const group = new L.featureGroup(markersLayer.getLayers());
map.fitBounds(group.getBounds().pad(0.1)); map.fitBounds(group.getBounds().pad(0.1));
} }
firstLoad = false; firstLoad = false;
@@ -110,48 +110,50 @@ function getIcon(s) {
// Tooltip text for the markers // Tooltip text for the markers
function getTooltipText(s) { function getTooltipText(s) {
// Format DX call // Format DX call
var dx_call = s["dx_call"]; let dx_call = s["dx_call"];
if (dx_call == null) { if (dx_call == null) {
dx_call = ""; dx_call = "";
dx_flag = "";
} }
if (s["dx_ssid"] != null) { if (s["dx_ssid"] != null) {
dx_call = dx_call + "-" + s["dx_ssid"]; dx_call = dx_call + "-" + s["dx_ssid"];
} }
// Format DX flag // Format DX flag
var dx_flag = "<i class='fa-solid fa-globe-africa'></i>"; let dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (s["dx_flag"] && s["dx_flag"] != null && s["dx_flag"] != "") { if (dx_call == null) {
dx_flag = "";
}
if (s["dx_flag"] && s["dx_flag"] != null && s["dx_flag"] !== "") {
dx_flag = s["dx_flag"]; dx_flag = s["dx_flag"];
} }
// Format the frequency // Format the frequency
var freq_string = "Unknown" let freq_string = "Unknown";
if (s["freq"] != null) { if (s["freq"] != null) {
var mhz = Math.floor(s["freq"] / 1000000.0); const mhz = Math.floor(s["freq"] / 1000000.0);
var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0); const khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0);
var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0)); const hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0));
var hz_string = (hz > 0) ? hz.toFixed(0)[0] : ""; const hz_string = (hz > 0) ? hz.toFixed(0)[0] : "";
freq_string = `<span class='freq-mhz freq-mhz-pad'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz hideonmobile'>${hz_string}</span>` freq_string = `<span class='freq-mhz freq-mhz-pad'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz hideonmobile'>${hz_string}</span>`
} }
// Format comment // Format comment
var commentText = ""; let commentText = "";
if (s["comment"] != null) { if (s["comment"] != null) {
commentText = escapeHtml(s["comment"]); commentText = escapeHtml(s["comment"]);
} }
// Sig or fallback to source // Sig or fallback to source
var sigSourceText = s["source"]; let sigSourceText = s["source"];
if (s["sig"]) { if (s["sig"]) {
sigSourceText = s["sig"]; sigSourceText = s["sig"];
} }
// Format sig_refs // Format sig_refs
var sig_refs = ""; let sig_refs = "";
if (s["sig_refs"] != null) { if (s["sig_refs"] != null) {
var items = [] const items = [];
for (var i = 0; i < s["sig_refs"].length; i++) { for (let i = 0; i < s["sig_refs"].length; i++) {
if (s["sig_refs"][i]["url"] != null) { if (s["sig_refs"][i]["url"] != null) {
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>` items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='sig-ref-link'>${s["sig_refs"][i]["id"]}</a>`
} else { } else {
@@ -162,7 +164,7 @@ function getTooltipText(s) {
} }
// DX // DX
ttt = `<span class='nowrap'><span class='icon-wrapper'>${dx_flag}</span> <a href='https://www.qrz.com/db/${dx_call}' target='_blank' class="dx-link">${dx_call}</a></span><br/>`; let ttt = `<span class='nowrap'><span class='icon-wrapper'>${dx_flag}</span> <a href='https://www.qrz.com/db/${dx_call}' target='_blank' class="dx-link">${dx_call}</a></span><br/>`;
// Frequency & band // Frequency & band
ttt += `<span class='icon-wrapper'><i class='fa-solid fa-radio markerPopupIcon'></i></span>&nbsp;${freq_string}`; ttt += `<span class='icon-wrapper'><i class='fa-solid fa-radio markerPopupIcon'></i></span>&nbsp;${freq_string}`;
@@ -273,7 +275,7 @@ function setBasemap(basemapname) {
backgroundTileLayer.addTo(map); backgroundTileLayer.addTo(map);
backgroundTileLayer.bringToBack(); backgroundTileLayer.bringToBack();
if (basemapname === "OpenStreetMap.Mapnik.Dark") { if (basemapname === "OpenStreetMap.Mapnik.Dark") {
var container = backgroundTileLayer.getContainer(); const container = backgroundTileLayer.getContainer();
if (container) { if (container) {
container.style.filter = 'invert(100%) hue-rotate(180deg) brightness(80%)'; container.style.filter = 'invert(100%) hue-rotate(180deg) brightness(80%)';
} }
@@ -409,7 +411,7 @@ function setUpMap() {
backgroundTileLayer.addTo(map); backgroundTileLayer.addTo(map);
backgroundTileLayer.bringToBack(); backgroundTileLayer.bringToBack();
if (loadedBasemap === "OpenStreetMap.Mapnik.Dark") { if (loadedBasemap === "OpenStreetMap.Mapnik.Dark") {
var container = backgroundTileLayer.getContainer(); const container = backgroundTileLayer.getContainer();
if (container) { if (container) {
container.style.filter = 'invert(100%) hue-rotate(180deg) brightness(80%)'; container.style.filter = 'invert(100%) hue-rotate(180deg) brightness(80%)';
} }

View File

@@ -43,7 +43,7 @@ function startSSEConnection() {
evtSource.onmessage = function(event) { evtSource.onmessage = function(event) {
// Get the new spot // Get the new spot
newSpot = JSON.parse(event.data); const newSpot = JSON.parse(event.data);
// Awful fudge to ensure new incoming spots at the top of the list don't have timestamps that make them look // Awful fudge to ensure new incoming spots at the top of the list don't have timestamps that make them look
// like they belong further down the list. If the spot is older than the latest one we already have, bump its // like they belong further down the list. If the spot is older than the latest one we already have, bump its
// time up to match it. This isn't great but since we poll spot providers every 2 minutes anyway, it shouldn't // time up to match it. This isn't great but since we poll spot providers every 2 minutes anyway, it shouldn't
@@ -63,7 +63,7 @@ function startSSEConnection() {
} }
// If we had zero spots before (i.e. one now), the table will have a "No spots" row that we need to remove now // If we had zero spots before (i.e. one now), the table will have a "No spots" row that we need to remove now
// that we have one. // that we have one.
if (spots.length == 1) { if (spots.length === 1) {
$("#table tbody tr").last().remove(); $("#table tbody tr").last().remove();
} }
@@ -76,7 +76,7 @@ function startSSEConnection() {
} }
}; };
evtSource.onerror = function(err) { evtSource.onerror = function() {
if (evtSource != null) { if (evtSource != null) {
evtSource.close(); evtSource.close();
} }
@@ -87,14 +87,14 @@ function startSSEConnection() {
// Build a query string for the API, based on the filters that the user has selected. // Build a query string for the API, based on the filters that the user has selected.
function buildQueryString(includeCredentials) { function buildQueryString(includeCredentials) {
var str = "?"; let str = "?";
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => { ["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
if (!allFilterOptionsSelected(fn)) { if (!allFilterOptionsSelected(fn)) {
str = str + getQueryStringFor(fn) + "&"; str = str + getQueryStringFor(fn) + "&";
} }
}); });
str = str + "limit=" + $("#spots-to-fetch option:selected").val(); str = str + "limit=" + $("#spots-to-fetch option:selected").val();
if ($("#search").val() != "") { if ($("#search").val() !== "") {
str = str + "&text_includes=" + encodeURIComponent($("#search").val()); str = str + "&text_includes=" + encodeURIComponent($("#search").val());
} }
if (includeCredentials) { if (includeCredentials) {
@@ -106,22 +106,22 @@ function buildQueryString(includeCredentials) {
// Update the spots table // Update the spots table
function updateTable() { function updateTable() {
// Use local time instead of UTC? // Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local"; const useLocalTime = $("#timeZone")[0].value === "local";
// Get user grid if valid, this will be null if it's not. // Get user grid if valid, this will be null if it's not.
var userPos = latLonForGridCentre($("#userGrid").val()); const userPos = latLonForGridCentre($("#userGrid").val());
// Table data toggles // Table data toggles
var showTime = $("#tableShowTime")[0].checked; const showTime = $("#tableShowTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked; const showDX = $("#tableShowDX")[0].checked;
var showFreq = $("#tableShowFreq")[0].checked; const showFreq = $("#tableShowFreq")[0].checked;
var showMode = $("#tableShowMode")[0].checked; const showMode = $("#tableShowMode")[0].checked;
var showComment = $("#tableShowComment")[0].checked; const showComment = $("#tableShowComment")[0].checked;
var showBearing = $("#tableShowBearing")[0].checked && userPos != null; const showBearing = $("#tableShowBearing")[0].checked && userPos != null;
var showType = $("#tableShowType")[0].checked; const showType = $("#tableShowType")[0].checked;
var showRef = $("#tableShowRef")[0].checked; const showRef = $("#tableShowRef")[0].checked;
var showDE = $("#tableShowDE")[0].checked; const showDE = $("#tableShowDE")[0].checked;
var showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked; const showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked;
// Populate table with headers // Populate table with headers
let table = $("#table"); let table = $("#table");
@@ -158,7 +158,7 @@ function updateTable() {
} }
table.find('tbody').empty(); table.find('tbody').empty();
if (spots.length == 0) { if (spots.length === 0) {
table.find('tbody').append('<tr class="bg-danger-subtle"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>'); table.find('tbody').append('<tr class="bg-danger-subtle"><td colspan="100" style="text-align:center;">No spots match your filters.</td></tr>');
} }
@@ -182,22 +182,22 @@ function addSpotToTopOfTable(s, highlightNew) {
// highlightNew = false for an initial load, true for new SSE-loaded spots // highlightNew = false for an initial load, true for new SSE-loaded spots
function createNewTableRowsForSpot(s, highlightNew) { function createNewTableRowsForSpot(s, highlightNew) {
// Use local time instead of UTC? // Use local time instead of UTC?
var useLocalTime = $("#timeZone")[0].value == "local"; const useLocalTime = $("#timeZone")[0].value === "local";
// Get user grid if valid, this will be null if it's not. // Get user grid if valid, this will be null if it's not.
var userPos = latLonForGridCentre($("#userGrid").val()); const userPos = latLonForGridCentre($("#userGrid").val());
// Table data toggles // Table data toggles
var showTime = $("#tableShowTime")[0].checked; const showTime = $("#tableShowTime")[0].checked;
var showDX = $("#tableShowDX")[0].checked; const showDX = $("#tableShowDX")[0].checked;
var showFreq = $("#tableShowFreq")[0].checked; const showFreq = $("#tableShowFreq")[0].checked;
var showMode = $("#tableShowMode")[0].checked; const showMode = $("#tableShowMode")[0].checked;
var showComment = $("#tableShowComment")[0].checked; const showComment = $("#tableShowComment")[0].checked;
var showBearing = $("#tableShowBearing")[0].checked && userPos != null; const showBearing = $("#tableShowBearing")[0].checked && userPos != null;
var showType = $("#tableShowType")[0].checked; const showType = $("#tableShowType")[0].checked;
var showRef = $("#tableShowRef")[0].checked; const showRef = $("#tableShowRef")[0].checked;
var showDE = $("#tableShowDE")[0].checked; const showDE = $("#tableShowDE")[0].checked;
var showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked; const showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked;
// Create row // Create row
let $tr = $('<tr>'); let $tr = $('<tr>');
@@ -205,13 +205,13 @@ function createNewTableRowsForSpot(s, highlightNew) {
// Apply striping to the table. We can't just use Bootstrap's table-striped class because we have all sorts of // 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 // 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. // which cause the table-striped colouring to go awry.
if (rowCount % 2 == 1) { if (rowCount % 2 === 1) {
$tr.addClass("table-active"); $tr.addClass("table-active");
} }
// Show faded out if QRT or already worked // Show faded out if QRT or already worked
let alreadyWorkedThis = alreadyWorked(s["dx_call"], s["band"], s["mode"]); let alreadyWorkedThis = alreadyWorked(s["dx_call"], s["band"], s["mode"]);
if (s["qrt"] == true || alreadyWorkedThis) { if (s["qrt"] === true || alreadyWorkedThis) {
$tr.addClass("table-faded"); $tr.addClass("table-faded");
} }
@@ -222,65 +222,67 @@ function createNewTableRowsForSpot(s, highlightNew) {
} }
// Format a UTC or local time for display // Format a UTC or local time for display
var time = moment.unix(s["time"]).utc(); const time = moment.unix(s["time"]).utc();
if (useLocalTime) { if (useLocalTime) {
time.local(); time.local();
} }
var time_formatted = time.format("HH:mm"); const time_formatted = time.format("HH:mm");
// Format DX call // Format DX call
var dx_call = s["dx_call"]; let dx_call = s["dx_call"];
if (dx_call == null) { if (dx_call == null) {
dx_call = ""; dx_call = "";
dx_flag = "";
} }
if (s["dx_ssid"] != null) { if (s["dx_ssid"] != null) {
dx_call = dx_call + "-" + s["dx_ssid"]; dx_call = dx_call + "-" + s["dx_ssid"];
} }
// Format dx country // Format dx country
var dx_country = s["dx_country"]; let dx_country = s["dx_country"];
if (dx_country == null) { if (dx_country == null) {
dx_country = "Unknown or not a country"; dx_country = "Unknown or not a country";
} }
// Format DX flag // Format DX flag
var dx_flag = "<i class='fa-solid fa-globe-africa'></i>"; let dx_flag = "<i class='fa-solid fa-globe-africa'></i>";
if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] != 0) { if (dx_call == null) {
dx_flag = "";
}
if (s["dx_dxcc_id"] && s["dx_dxcc_id"] != null && s["dx_dxcc_id"] !== 0) {
dx_flag = `<img src="img/flags/${s['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`; dx_flag = `<img src="img/flags/${s['dx_dxcc_id']}.png" class="flag" width="24" alt="${dx_country}" title="${dx_country}"/>`;
} }
// Format the frequency // Format the frequency
var freq_string = "Unknown" let freq_string = "Unknown";
if (s["freq"] != null) { if (s["freq"] != null) {
var mhz = Math.floor(s["freq"] / 1000000.0); const mhz = Math.floor(s["freq"] / 1000000.0);
var khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0); const khz = Math.floor((s["freq"] - (mhz * 1000000.0)) / 1000.0);
var hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0)); const hz = Math.floor(s["freq"] - (mhz * 1000000.0) - (khz * 1000.0));
var hz_string = (hz > 0) ? hz.toFixed(0)[0] : ""; const hz_string = (hz > 0) ? hz.toFixed(0)[0] : "";
freq_string = `<span class='freq-mhz freq-mhz-pad'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz hideonmobile'>${hz_string}</span>` freq_string = `<span class='freq-mhz freq-mhz-pad'>${mhz.toFixed(0)}</span><span class='freq-khz'>${khz.toFixed(0).padStart(3, '0')}</span><span class='freq-hz hideonmobile'>${hz_string}</span>`
} }
// Format the mode // Format the mode
mode_string = s["mode"]; let mode_string = s["mode"];
if (s["mode"] == null) { if (s["mode"] == null) {
mode_string = ""; mode_string = "";
} else if (s["mode_source"] == "BANDPLAN") { } else if (s["mode_source"] === "BANDPLAN") {
mode_string = mode_string + "<span class='mode-q hideonmobile'><i class='fa-solid fa-circle-question' title='The mode was not reported via the spotting service. This is a guess based on the frequency.'></i></span>"; mode_string = mode_string + "<span class='mode-q hideonmobile'><i class='fa-solid fa-circle-question' title='The mode was not reported via the spotting service. This is a guess based on the frequency.'></i></span>";
} }
// Format comment // Format comment
var commentText = ""; let commentText = "";
if (s["comment"] != null) { if (s["comment"] != null) {
commentText = escapeHtml(s["comment"]); commentText = escapeHtml(s["comment"]);
} }
// Format bearing text // Format bearing text
var bearingText = "---<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service, and we could not determine one. A bearing to this DX is not available.'></i></span>"; let bearingText = "---<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service, and we could not determine one. A bearing to this DX is not available.'></i></span>";
if (userPos != null && s["dx_latitude"] != null && s["dx_longitude"] != null) { if (userPos != null && s["dx_latitude"] != null && s["dx_longitude"] != null) {
var bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]); const bearing = calcBearing(userPos[0], userPos[1], s["dx_latitude"], s["dx_longitude"]);
bearingText = bearing.toFixed(0).padStart(3, '0') + "°"; bearingText = bearing.toFixed(0).padStart(3, '0') + "°";
if (s["dx_location_good"] == null || s["dx_location_good"] == false) { if (s["dx_location_good"] == null || s["dx_location_good"] === false) {
if (s["dx_location_source"] == "HOME QTH") { if (s["dx_location_source"] === "HOME QTH") {
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to a QRZ \"home\" location for a portable/mobile/alternative spot, so this bearing may not be accurate if the DX is close to you..'></i></span>"; bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to a QRZ \"home\" location for a portable/mobile/alternative spot, so this bearing may not be accurate if the DX is close to you..'></i></span>";
} else { } else {
bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to just using the centre of a DXCC entity, so this bearing may not be accurate if the DX is close to you.'></i></span>"; bearingText = bearingText + "<span class='bearing-q hideonmobile'><i class='fa-solid fa-circle-question' title='The position was not reported via the spotting service. We had to fall back to just using the centre of a DXCC entity, so this bearing may not be accurate if the DX is close to you.'></i></span>";
@@ -289,16 +291,16 @@ function createNewTableRowsForSpot(s, highlightNew) {
} }
// Format "type" (Sig or fallback to source) // Format "type" (Sig or fallback to source)
var typeText = s["source"]; let typeText = s["source"];
if (s["sig"]) { if (s["sig"]) {
typeText = s["sig"]; typeText = s["sig"];
} }
// Format sig_refs // Format sig_refs
var sig_refs = ""; let sig_refs = "";
if (s["sig_refs"] != null) { if (s["sig_refs"] != null) {
var items = [] const items = [];
for (var i = 0; i < s["sig_refs"].length; i++) { for (let i = 0; i < s["sig_refs"].length; i++) {
if (s["sig_refs"][i]["url"] != null) { if (s["sig_refs"][i]["url"] != null) {
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["sig_refs"][i]["url"])}' title='${escapeHtml(s["sig_refs"][i]["name"])}' target='_new' class='sig-ref-link'>${escapeHtml(s["sig_refs"][i]["id"])}</a></span>` items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["sig_refs"][i]["url"])}' title='${escapeHtml(s["sig_refs"][i]["name"])}' target='_new' class='sig-ref-link'>${escapeHtml(s["sig_refs"][i]["id"])}</a></span>`
} else { } else {
@@ -309,19 +311,19 @@ function createNewTableRowsForSpot(s, highlightNew) {
} }
// Format de country // Format de country
var de_country = s["de_country"]; let de_country = s["de_country"];
if (de_country == null) { if (de_country == null) {
de_country = "Unknown or not a country"; de_country = "Unknown or not a country";
} }
// Format DE flag // Format DE flag
var de_flag = "<i class='fa-solid fa-circle-question'></i>"; let de_flag = "<i class='fa-solid fa-circle-question'></i>";
if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] != 0) { if (s["de_dxcc_id"] && s["de_dxcc_id"] != null && s["de_dxcc_id"] !== 0) {
de_flag = `<img src="img/flags/${s['de_dxcc_id']}.png" class="flag" width="24" alt="${de_country}" title="${de_country}"/>`; de_flag = `<img src="img/flags/${s['de_dxcc_id']}.png" class="flag" width="24" alt="${de_country}" title="${de_country}"/>`;
} }
// Format de call // Format de call
var de_call = s["de_call"]; let de_call = s["de_call"];
if (de_call == null) { if (de_call == null) {
de_call = ""; de_call = "";
de_flag = ""; de_flag = "";
@@ -331,10 +333,10 @@ function createNewTableRowsForSpot(s, highlightNew) {
} }
// Format band name // Format band name
var bandFullName = s['band'] ? s['band'] + " band": "Unknown band"; const bandFullName = s['band'] ? s['band'] + " band" : "Unknown band";
// Format "worked" checkbox // Format "worked" checkbox
var workedCheckbox = `<input type="checkbox" ${alreadyWorkedThis ? "checked" : ""} onClick="setWorkedState('${s['dx_call']}', '${s['band']}', '${s['mode']}', ${alreadyWorkedThis ? "false" : "true"});" title="Check this box to record that you have worked this callsign on their current band and mode.">`; const workedCheckbox = `<input type="checkbox" ${alreadyWorkedThis ? "checked" : ""} onClick="setWorkedState('${s['dx_call']}', '${s['band']}', '${s['mode']}', ${alreadyWorkedThis ? "false" : "true"});" title="Check this box to record that you have worked this callsign on their current band and mode.">`;
// Populate the row // Populate the row
if (showTime) { if (showTime) {
@@ -369,21 +371,21 @@ function createNewTableRowsForSpot(s, highlightNew) {
} }
// Second row for mobile view only, containing type, ref & comment // Second row for mobile view only, containing type, ref & comment
$tr2 = $("<tr class='hidenotonmobile'>"); const $tr2 = $("<tr class='hidenotonmobile'>");
// Apply styles as per the first row // Apply styles as per the first row
if (rowCount % 2 == 1) { if (rowCount % 2 === 1) {
$tr2.addClass("table-active"); $tr2.addClass("table-active");
} }
if (s["qrt"] == true || alreadyWorkedThis) { if (s["qrt"] === true || alreadyWorkedThis) {
$tr2.addClass("table-faded"); $tr2.addClass("table-faded");
} }
if (highlightNew) { if (highlightNew) {
$tr2.addClass("new"); $tr2.addClass("new");
} }
$td2 = $("<td colspan='100'>"); const $td2 = $("<td colspan='100'>");
$td2floatleft = $(`<div style="float: left;">`); const $td2floatleft = $(`<div style="float: left;">`);
if (showType) { if (showType) {
$td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${typeText} `); $td2floatleft.append(`<span class='icon-wrapper'><i class='fa-solid ${sigToIcon(s["sig"], "fa-tower-cell")}'></i></span> ${typeText} `);
} }
@@ -391,7 +393,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
$td2floatleft.append(`${sig_refs} `); $td2floatleft.append(`${sig_refs} `);
} }
$td2.append($td2floatleft); $td2.append($td2floatleft);
$td2floatright = $(`<div style="float: right;">`); const $td2floatright = $(`<div style="float: right;">`);
if (showBearing) { if (showBearing) {
$td2floatright.append(`${bearingText} &nbsp;`); $td2floatright.append(`${bearingText} &nbsp;`);
} }
@@ -459,13 +461,13 @@ function loadOptions() {
// Work out if the user's entered grid is a valid Maidenhead grid // Work out if the user's entered grid is a valid Maidenhead grid
function isUserGridValid() { function isUserGridValid() {
userGrid = $("#userGrid").val().toUpperCase(); const userGrid = $("#userGrid").val().toUpperCase();
return latLonForGridCentre(userGrid) != null; return latLonForGridCentre(userGrid) != null;
} }
// Method called when the user's grid input is changed. // Method called when the user's grid input is changed.
function userGridUpdated() { function userGridUpdated() {
var userGridValid = isUserGridValid(); const userGridValid = isUserGridValid();
if (userGridValid) { if (userGridValid) {
updateTable(); updateTable();
} }

View File

@@ -1,5 +1,5 @@
// Storage for the spot data that the server gives us. // Storage for the spot data that the server gives us.
var spots = [] let spots = [];
// List of people the user has worked. Each entry has the format callsign-band-mode. These can be added to the list by // List of people the user has worked. Each entry has the format callsign-band-mode. These can be added to the list by
// ticking the checkbox on a row of the table, and cleared from the Display menu. Where a row would be added to the // ticking the checkbox on a row of the table, and cleared from the Display menu. Where a row would be added to the
// table and the callsign-band-mode is in this list, it is shown struck through as already worked. This is persisted // table and the callsign-band-mode is in this list, it is shown struck through as already worked. This is persisted
@@ -9,9 +9,9 @@ let worked = []
// Dynamically add CSS code for the band checkboxes to show in the appropriate colour. // Dynamically add CSS code for the band checkboxes to show in the appropriate colour.
// Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p". // Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p".
function addBandToggleColourCSS(band_options) { function addBandToggleColourCSS(band_options) {
var $style = $('<style>'); const $style = $('<style>');
band_options.forEach(o => { band_options.forEach(o => {
var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, ""); const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$style.append(`#filter-button-label-band-${domSafeName} { padding-left: 0.3em; border-left: 5px solid ${bandToColor(o['name'])};}`); $style.append(`#filter-button-label-band-${domSafeName} { padding-left: 0.3em; border-left: 5px solid ${bandToColor(o['name'])};}`);
}); });
$('html > head').append($style); $('html > head').append($style);
@@ -19,9 +19,9 @@ function addBandToggleColourCSS(band_options) {
// Generate bands filter card. This one is a special case. // Generate bands filter card. This one is a special case.
function generateBandsMultiToggleFilterCard(band_options) { function generateBandsMultiToggleFilterCard(band_options) {
var $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">'); const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">');
band_options.forEach(o => { band_options.forEach(o => {
var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, ""); const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-band storeable-checkbox" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked> <label class="form-check-label" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label></div></div>`); $grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-band storeable-checkbox" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked> <label class="form-check-label" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label></div></div>`);
}); });
$("#band-options").append($grid); $("#band-options").append($grid);
@@ -40,9 +40,9 @@ function setHamHFBandToggles() {
// Generate SIGs filter card. This one is also a special case. // Generate SIGs filter card. This one is also a special case.
function generateSIGsMultiToggleFilterCard(sig_options) { function generateSIGsMultiToggleFilterCard(sig_options) {
var $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">'); const $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
sig_options.forEach(o => { sig_options.forEach(o => {
var domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, ""); const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label></div></div>`); $grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${sigToIcon(o['name'], 'fa-tower-cell')}"></i> ${o['name']}</label></div></div>`);
}); });
// Bonus "NO_SIG" / "General DX" option // Bonus "NO_SIG" / "General DX" option
@@ -53,9 +53,9 @@ function generateSIGsMultiToggleFilterCard(sig_options) {
// Generate modes filter card. This one is also a special case. // Generate modes filter card. This one is also a special case.
function generateModesMultiToggleFilterCard(mode_options) { function generateModesMultiToggleFilterCard(mode_options) {
var $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">'); const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">');
mode_options.forEach(o => { mode_options.forEach(o => {
var domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, ""); const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label></div></div>`); $grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label></div></div>`);
}); });
$("#mode-options").append($grid); $("#mode-options").append($grid);
@@ -84,10 +84,10 @@ function setDigiModeToggles() {
// set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise // set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise
// names here for HTML elements. // names here for HTML elements.
function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) { function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) {
var $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">'); const $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
source_options.forEach(o => { source_options.forEach(o => {
var enable = sources_enabled_by_default.includes(o); const enable = sources_enabled_by_default.includes(o);
var domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, ""); const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-source storeable-checkbox" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="form-check-label" for="filter-button-source-${domSafeName}">${o}</label></div></div>`); $grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-source storeable-checkbox" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="form-check-label" for="filter-button-source-${domSafeName}">${o}</label></div></div>`);
}); });
$("#source-options").append($grid); $("#source-options").append($grid);
@@ -115,7 +115,7 @@ function alreadyWorked(callsign, band, mode) {
// Reload spots on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA // Reload spots 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. // after some time has passed with it in the background.
addEventListener("visibilitychange", (event) => { addEventListener("visibilitychange", () => {
if (!document.hidden) { if (!document.hidden) {
loadSpots(); loadSpots();
} }
@@ -124,7 +124,7 @@ addEventListener("visibilitychange", (event) => {
// Startup // Startup
$(document).ready(function() { $(document).ready(function() {
// Load worked list // Load worked list
var tmpWorked = JSON.parse(localStorage.getItem("worked")); const tmpWorked = JSON.parse(localStorage.getItem("worked"));
if (tmpWorked) { if (tmpWorked) {
worked = tmpWorked; worked = tmpWorked;
} }

View File

@@ -296,16 +296,6 @@ function setBandColorScheme(scheme) {
return ret; return ret;
} }
// Get the list of known bands
function getKnownBands() {
return Array.from(Object.keys(BAND_COLOR_SCHEMES[bandColorScheme]));
}
// Get the list of available band colour schemes
function getAvailableBandColorSchemes() {
return Array.from(Object.keys(BAND_COLOR_SCHEMES));
}
// Band name to colour (in the current colour scheme). If the band is unknown, black will be returned. // Band name to colour (in the current colour scheme). If the band is unknown, black will be returned.
function bandToColor(band) { function bandToColor(band) {
let col = (band != null) ? BAND_COLOR_SCHEMES[bandColorScheme][band] : null; let col = (band != null) ? BAND_COLOR_SCHEMES[bandColorScheme][band] : null;
@@ -324,22 +314,6 @@ function bandToContrastColor(band) {
return (lum > 128) ? "#000000" : "#ffffff"; return (lum > 128) ? "#000000" : "#ffffff";
} }
const MODE_TYPE_COLOR_SCHEMES = {
"CW": "red",
"PHONE": "green",
"DATA": "blue"
}
// Mode type (CW, PHONE, DATA) to colour. If the mode type is unknown, black will be returned.
function modeTypeToColor(modeType) {
let col = (modeType != null) ? MODE_TYPE_COLOR_SCHEMES[modeType.toUpperCase()] : null;
if (col) {
return col;
} else {
return "#000000";
}
}
const SIG_ICONS = { const SIG_ICONS = {
"POTA": "fa-tree", "POTA": "fa-tree",
"SOTA": "fa-mountain-sun", "SOTA": "fa-mountain-sun",
@@ -365,31 +339,6 @@ const SIG_ICONS = {
"TOTA": "fa-toilet" "TOTA": "fa-toilet"
} }
const SIG_NAMES = {
"POTA": "Parks on the Air",
"SOTA": "Summits on the Air",
"WWFF": "Worldwide Flora & Fauna",
"GMA": "Global Mountain Activity",
"WWBOTA": "Bunkers on the Air",
"HEMA": "Humps Excluding Marilyns Award",
"IOTA": "Islands on the Air",
"MOTA": "Mills on the Air",
"ARLHS": "Amateur Radio Lighthouse Society",
"ILLW": "International Lighthouse Lightship Weekend",
"SIOTA": "Silos on the Air",
"WCA": "World Castles Award",
"ZLOTA": "New Zealand on the Air",
"WOTA": "Wainwrights on the Air",
"BOTA": "Beaches on the Air",
"KRMNPA": "Keith Roget Memorial National Parks Award",
"LLOTA": "Lagos y Lagunas on the Air",
"WWTOTA": "Towers on the Air",
"WAB": "Worked All Britain",
"WAI": "Worked All Ireland",
"Tiles": "Tiles on the Air",
"TOTA": "Toilets on the Air"
}
// Get the Font Awesome icon for a given SIG. If the SIG is unknown, the provided default symbol will be returned // Get the Font Awesome icon for a given SIG. If the SIG is unknown, the provided default symbol will be returned
function sigToIcon(sig, defaultIcon) { function sigToIcon(sig, defaultIcon) {
let col = (sig != null) ? SIG_ICONS[sig] : null; let col = (sig != null) ? SIG_ICONS[sig] : null;
@@ -404,35 +353,3 @@ function sigToIcon(sig, defaultIcon) {
} }
} }
} }
// Get the full name for a given SIG abbreviation. If the SIG is unknown, an empty string will be returned.
function sigToName(sig) {
let col = (sig != null) ? SIG_NAMES[sig] : null;
if (col) {
return col;
} else {
let col = (sig != null) ? SIG_NAMES[sig.toUpperCase()] : null;
if (col) {
return col;
} else {
return "";
}
}
}
// Get the list of known SIGs
function getKnownSIGs() {
return Array.from(Object.keys(SIG_ICONS));
}
// Format a Maidenhead grid with alternating alphabetic blocks in lower case
function formatGrid(grid) {
grid = grid.toUpperCase();
if (grid.length >= 6) {
grid = grid.substring(0, 4) + grid.substring(4, 6).toLowerCase() + grid.substring(6);
}
if (grid.length >= 12) {
grid = grid.substring(0, 10) + grid.substring(10, 12).toLowerCase() + grid.substring(14);
}
return grid;
}