diff --git a/core/utils.py b/core/utils.py index d4e8e30..7053338 100644 --- a/core/utils.py +++ b/core/utils.py @@ -113,6 +113,7 @@ def infer_country_from_callsign(call): # Infer a DXCC ID from a callsign def infer_dxcc_id_from_callsign(call): + get_clublog_xml_data_for_callsign("M0TRT") try: # Start with the basic country-files.com-based decoder. dxcc = CALL_INFO_BASIC.get_adif_id(call) @@ -225,7 +226,8 @@ def get_qrz_data_for_callsign(call): QRZ_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds return data except KeyError: - # QRZ had no info for the call, that's OK + # QRZ had no info for the call, that's OK. Cache a None so we don't try to look this up again + QRZ_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds return None else: return None @@ -243,7 +245,8 @@ def get_clublog_api_data_for_callsign(call): CLUBLOG_CALLSIGN_DATA_CACHE.add(call, data, expire=604800) # 1 week in seconds return data except KeyError: - # Clublog had no info for the call, that's OK + # Clublog had no info for the call, that's OK. Cache a None so we don't try to look this up again + CLUBLOG_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds return None except APIKeyMissingError: # User API key was wrong, warn @@ -260,7 +263,8 @@ def get_clublog_xml_data_for_callsign(call): data = LOOKUP_LIB_CLUBLOG_XML.lookup_callsign(callsign=call) return data except KeyError: - # Clublog had no info for the call, that's OK + # Clublog had no info for the call, that's OK. Cache a None so we don't try to look this up again + CLUBLOG_CALLSIGN_DATA_CACHE.add(call, None, expire=604800) # 1 week in seconds return None else: return None @@ -310,11 +314,21 @@ def infer_latlon_from_callsign_dxcc(call): try: data = CALL_INFO_BASIC.get_lat_long(call) if data and "latitude" in data and "longitude" in data: - return [data["latitude"], data["longitude"]] + loc = [data["latitude"], data["longitude"]] else: - return None + loc = None except KeyError: - return None + loc = None + # Couldn't get anything from basic call info database, try Clublog data + if not loc: + data = get_clublog_xml_data_for_callsign(call) + if data and "Lat" in data and "Lon" in data: + loc = [data["Lat"], data["Lon"]] + if not loc: + data = get_clublog_api_data_for_callsign(call) + if data and "Lat" in data and "Lon" in data: + loc = [data["Lat"], data["Lon"]] + return loc # Infer a grid locator from a callsign (using DXCC, probably very inaccurate) diff --git a/views/webpage_alerts.tpl b/views/webpage_alerts.tpl index 54e41e7..4f7a553 100644 --- a/views/webpage_alerts.tpl +++ b/views/webpage_alerts.tpl @@ -80,31 +80,31 @@
Table Data
- +
- +
- +
- +
- +
- +
- +
diff --git a/views/webpage_spots.tpl b/views/webpage_spots.tpl index 132b3dc..5e0bccb 100644 --- a/views/webpage_spots.tpl +++ b/views/webpage_spots.tpl @@ -92,42 +92,42 @@
-
Table Data
+
Table Columns
- +
- +
- +
- +
- +
- +
- +
- +
- +
@@ -140,7 +140,7 @@
Location
- +
diff --git a/webassets/js/alerts.js b/webassets/js/alerts.js index 256c739..58809ca 100644 --- a/webassets/js/alerts.js +++ b/webassets/js/alerts.js @@ -41,15 +41,38 @@ function updateTable() { // Use local time instead of UTC? var useLocalTime = $("#timeZone")[0].value == "local"; + // Table data toggles + var showStartTime = $("#tableShowStartTime")[0].checked; + var showEndTime = $("#tableShowEndTime")[0].checked; + var showDX = $("#tableShowDX")[0].checked; + var showFreqsModes = $("#tableShowFreqsModes")[0].checked; + var showComment = $("#tableShowComment")[0].checked; + var showSource = $("#tableShowSource")[0].checked; + var showRef = $("#tableShowRef")[0].checked; + // Populate table with headers let table = $('').append(''); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); + if (showStartTime) { + table.find('thead tr').append(``); + } + if (showEndTime) { + table.find('thead tr').append(``); + } + if (showDX) { + table.find('thead tr').append(``); + } + if (showFreqsModes) { + table.find('thead tr').append(``); + } + if (showComment) { + table.find('thead tr').append(``); + } + if (showSource) { + table.find('thead tr').append(``); + } + if (showRef) { + table.find('thead tr').append(``); + } // 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${start_time_formatted}`); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); + if (showStartTime) { + $tr.append(``); + } + if (showEndTime) { + $tr.append(``); + } + if (showDX) { + $tr.append(``); + } + if (showFreqsModes) { + $tr.append(``); + } + if (showComment) { + $tr.append(``); + } + if (showSource) { + $tr.append(``); + } + if (showRef) { + $tr.append(``); + } tbody.append($tr); // Second row for mobile view only, containing source, ref, freqs/modes & comment $tr2 = $(""); - $tr2.append(``); + $td2 = $("
${useLocalTime ? "Start (Local)" : "Start UTC"}${useLocalTime ? "End (Local)" : "End UTC"}DXFrequencies & ModesCommentSourceRef.${useLocalTime ? "Start (Local)" : "Start UTC"}${useLocalTime ? "End (Local)" : "End UTC"}DXFrequencies & ModesCommentSourceRef.${end_time_formatted}${dx_flag}${dx_calls_html}${dx_country_html}${freqsModesText}${commentText} ${sigSourceText}${sig_refs}${start_time_formatted}${end_time_formatted}${dx_flag}${dx_calls_html}${dx_country_html}${freqsModesText}${commentText} ${sigSourceText}${sig_refs}
${sig_refs} ${freqsModesText}
${commentText}
"); + if (showSource) { + $td2.append(` `); + } + if (showRef) { + $td2.append(`${sig_refs} `); + } + if (showFreqsModes) { + $td2.append(`${freqsModesText} `); + } + if (showComment) { + $td2.append(`
${commentText} `); + } + $tr2.append($td2); tbody.append($tr2); }); } diff --git a/webassets/js/common.js b/webassets/js/common.js index 513ede4..8c0312d 100644 --- a/webassets/js/common.js +++ b/webassets/js/common.js @@ -103,6 +103,97 @@ function timeZoneUpdated() { saveSettings(); } +// When one of the column toggle checkboxes are changed, reload the table and save settings +function columnsUpdated() { + updateTable(); + saveSettings(); +} + +// Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square. +// Returns null if the grid format is invalid. +function latLonForGridCentre(grid) { + let [lat, lon, latCellSize, lonCellSize] = latLonForGridSWCornerPlusSize(grid); + if (lat != null && lon != null && latCellSize != null && lonCellSize != null) { + return [lat + latCellSize / 2.0, lon + lonCellSize / 2.0]; + } else { + return null; + } +} + +// Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the +// lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and +// northeast coordinates of a grid square. +// The return type is always an array of size 4. The elements in it are null if the grid format is invalid. +function latLonForGridSWCornerPlusSize(grid) { + // Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references + grid = grid.toUpperCase(); + + // Return null if our Maidenhead string is invalid or too short + let len = grid.length; + if (len <= 0 || (len % 2) !== 0) { + return [null, null, null, null]; + } + + let lat = 0.0; // aggregated latitude + let lon = 0.0; // aggregated longitude + let latCellSize = 10; // Size in degrees latitude of the current cell. Starts at 20 and gets smaller as the calculation progresses + let lonCellSize = 20; // Size in degrees longitude of the current cell. Starts at 20 and gets smaller as the calculation progresses + let latCellNo; // grid latitude cell number this time + let lonCellNo; // grid longitude cell number this time + + // Iterate through blocks (two-character sections) + for (let block = 0; block * 2 < len; block += 1) { + if (block % 2 === 0) { + // Letters in this block + lonCellNo = grid.charCodeAt(block * 2) - 'A'.charCodeAt(0); + latCellNo = grid.charCodeAt(block * 2 + 1) - 'A'.charCodeAt(0); + // Bail if the values aren't in range. Allowed values are A-R (0-17) for the first letter block, or + // A-X (0-23) thereafter. + let maxCellNo = (block === 0) ? 17 : 23; + if (latCellNo < 0 || latCellNo > maxCellNo || lonCellNo < 0 || lonCellNo > maxCellNo) { + return [null, null, null, null]; + } + } else { + // Numbers in this block + lonCellNo = parseInt(grid.charAt(block * 2)); + latCellNo = parseInt(grid.charAt(block * 2 + 1)); + // Bail if the values aren't in range 0-9.. + if (latCellNo < 0 || latCellNo > 9 || lonCellNo < 0 || lonCellNo > 9) { + return [null, null, null, null]; + } + } + + // Aggregate the angles + lat += latCellNo * latCellSize; + lon += lonCellNo * lonCellSize; + + // Reduce the cell size for the next block, unless we are on the last cell. + if (block * 2 < len - 2) { + // Still have more work to do, so reduce the cell size + if (block % 2 === 0) { + // Just dealt with letters, next block will be numbers so cells will be 1/10 the current size + latCellSize = latCellSize / 10.0; + lonCellSize = lonCellSize / 10.0; + } else { + // Just dealt with numbers, next block will be letters so cells will be 1/24 the current size + latCellSize = latCellSize / 24.0; + lonCellSize = lonCellSize / 24.0; + } + } + } + + // Offset back to (-180, -90) where the grid starts + lon -= 180.0; + lat -= 90.0; + + // Return nulls on maths errors + if (isNaN(lat) || isNaN(lon) || isNaN(latCellSize) || isNaN(lonCellSize)) { + return [null, null, null, null]; + } + + return [lat, lon, latCellSize, lonCellSize]; +} + // Save settings to local storage function saveSettings() { // Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that diff --git a/webassets/js/spots.js b/webassets/js/spots.js index 623cf81..22e8bf5 100644 --- a/webassets/js/spots.js +++ b/webassets/js/spots.js @@ -34,16 +34,46 @@ function updateTable() { // Use local time instead of UTC? var useLocalTime = $("#timeZone")[0].value == "local"; + // Get user grid if valid, this will be null if it's not. + var userPos = latLonForGridCentre($("#userGrid").val()); + + // Table data toggles + var showTime = $("#tableShowTime")[0].checked; + var showDX = $("#tableShowDX")[0].checked; + var showFreq = $("#tableShowFreq")[0].checked; + var showMode = $("#tableShowMode")[0].checked; + var showComment = $("#tableShowComment")[0].checked; + var showBearing = $("#tableShowBearing")[0].checked && userPos != null; + var showSource = $("#tableShowSource")[0].checked; + var showRef = $("#tableShowRef")[0].checked; + var showDE = $("#tableShowDE")[0].checked; + // Populate table with headers let table = $('').append(''); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); - table.find('thead tr').append(``); + if (showTime) { + table.find('thead tr').append(``); + } + if (showDX) { + table.find('thead tr').append(``); + } + if (showFreq) { + table.find('thead tr').append(``); + } + if (showMode) { + table.find('thead tr').append(``); + } + if (showComment) { + table.find('thead tr').append(``); + } + if (showSource) { + table.find('thead tr').append(``); + } + if (showRef) { + table.find('thead tr').append(``); + } + if (showDE) { + table.find('thead tr').append(``); + } if (spots.length == 0) { table.find('tbody').append(''); @@ -142,14 +172,30 @@ function updateTable() { var bandFullName = s['band'] ? s['band'] + " band": "Unknown band"; // Populate the row - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); - $tr.append(``); + if (showTime) { + $tr.append(``); + } + if (showDX) { + $tr.append(``); + } + if (showFreq) { + $tr.append(``); + } + if (showMode) { + $tr.append(``); + } + if (showComment) { + $tr.append(``); + } + if (showSource) { + $tr.append(``); + } + if (showRef) { + $tr.append(``); + } + if (showDE) { + $tr.append(``); + } table.find('tbody').append($tr); // Second row for mobile view only, containing source, ref & comment @@ -157,7 +203,17 @@ function updateTable() { if (s["qrt"] == true) { $tr2.addClass("table-faded"); } - $tr2.append(``); + $td2 = $("
${useLocalTime ? "Local" : "UTC"}DXFrequencyModeCommentSourceRef.DE${useLocalTime ? "Local" : "UTC"}DXFrequencyModeCommentSourceRef.DE
No spots match your filters.
${time_formatted}${dx_flag}${s["dx_call"]}${freq_string}${mode_string}${commentText} ${sigSourceText}${sig_refs}${de_flag}${de_call}${time_formatted}${dx_flag}${s["dx_call"]}${freq_string}${mode_string}${commentText} ${sigSourceText}${sig_refs}${de_flag}${de_call} ${sig_refs} ${commentText}"); + if (showSource) { + $td2.append(` `); + } + if (showRef) { + $td2.append(`${sig_refs} `); + } + if (showComment) { + $td2.append(`${commentText} `); + } + $tr2.append($td2); table.find('tbody').append($tr2); }); @@ -182,9 +238,17 @@ function loadOptions() { $("#filters-container-2").append(generateMultiToggleFilterCard("Modes", "mode_type", options["mode_types"])); $("#filters-container-2").append(generateMultiToggleFilterCard("Sources", "source", options["spot_sources"])); - // Load filters from settings storage + // Load settings from settings storage now all the controls are available loadSettings(); + // Extra setting - the toggle for the "bearing" column is disabled if the user has not entered a valid grid, and + // normally this logic is handled on user input to the grid field, but we might have just loaded a value direct + // into the field, so apply the same logic here. + $("#tableShowBearing").prop('disabled', !isUserGridValid()); + if (!isUserGridValid()) { + $("#tableShowBearing").prop('checked', false); + } + // Load spots and set up the timer loadSpots(); setInterval(loadSpots, REFRESH_INTERVAL_SEC * 1000); @@ -228,12 +292,33 @@ function generateBandsMultiToggleFilterCard(band_options) { return $col; } +// Work out if the user's entered grid is a valid Maidenhead grid +function isUserGridValid() { + userGrid = $("#userGrid").val().toUpperCase(); + return latLonForGridCentre(userGrid) != null; +} + // Method called when any filter is changed to reload the spots and persist the filter settings. function filtersUpdated() { loadSpots(); saveSettings(); } +// Method called when the user's grid input is changed. +function userGridUpdated() { + var userGridValid = isUserGridValid(); + if (userGridValid) { + updateTable(); + } + // Enable/disable bearing column depending on grid validity + $("#tableShowBearing").prop('disabled', !userGridValid); + if (!userGridValid) { + $("#tableShowBearing").prop('checked', false); + } + // Save settings even if not a valid grid, this allows the user to clear their grid and have it save. + saveSettings(); +} + // React to toggling/closing panels function toggleFiltersPanel() { // If we are going to display the filters panel, hide the display panel