// Storage for the options that the server gives us. This will define our filters. var options = {}; // Last time we updated the spots/alerts list on display. var lastUpdateTime; // For a parameter, such as dx_continent, get the query string for the current filter options. function getQueryStringFor(parameter) { return parameter + "=" + encodeURIComponent(getSelectedFilterOptions(parameter)); } // For a parameter, such as dx_continent, get the filter options that are currently selected in the UI. function getSelectedFilterOptions(parameter) { return $(".filter-button-" + parameter).filter(function() { return this.checked; }).map(function() { return this.value; }).get().join(","); } // 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".) function allFilterOptionsSelected(parameter) { var filter = $(".filter-button-" + parameter).filter(function() { return !this.checked; }).get(); return filter.length == 0; } // Generate a filter card with multiple toggle buttons plus All/None buttons function generateMultiToggleFilterCard(displayName, filterQuery, options) { let $col = $("
") let $card = $("
"); let $card_body = $("
"); $card_body.append(`
${displayName}
`); $p = $("

"); // Create a button for each option options.forEach(o => { $p.append(` `); }); // Create All/None buttons $p.append(`  `); // Compile HTML elements to return $card_body.append($p); $card.append($card_body); $col.append($card); return $col; } // Method called when "All" or "None" is clicked function toggleFilterButtons(filterQuery, state) { $(".filter-button-" + filterQuery).each(function() { $(this).prop('checked', state); }); filtersUpdated(); } // Update the refresh timing display function updateRefreshDisplay() { if (lastUpdateTime != null) { let count = REFRESH_INTERVAL_SEC; let secSinceUpdate = moment.duration(moment().diff(lastUpdateTime)).asSeconds(); updatingString = "Updating..." if (secSinceUpdate < REFRESH_INTERVAL_SEC) { count = REFRESH_INTERVAL_SEC - secSinceUpdate; if (count <= 60) { var number = count.toFixed(0); updatingString = "Updating in " + number + " second" + (number != "1" ? "s" : "") + "."; } else { var number = Math.round(count / 60.0).toFixed(0); updatingString = "Updating in " + number + " minute" + (number != "1" ? "s" : "") + "."; } } $("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString); } } // Utility function to escape HTML characters from a string. function escapeHtml(str) { if (typeof str !== 'string') { return ''; } const escapeCharacter = (match) => { switch (match) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case '\'': return '''; case '`': return '`'; default: return match; } }; return str.replace(/[&<>"'`]/g, escapeCharacter); } // When the "use local time" field is changed, reload the table and save settings function timeZoneUpdated() { updateTable(); 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 // property. For a checkbox, that's the "checked" property. $(".storeable-checkbox").each(function() { localStorage.setItem("#" + $(this)[0].id + ":checked", JSON.stringify($(this)[0].checked)); }); $(".storeable-select").each(function() { localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value)); }); $(".storeable-text").each(function() { localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value)); }); } // Load settings from local storage and set up the filter selectors function loadSettings() { // Find all local storage entries and push their data to the corresponding UI element Object.keys(localStorage).forEach(function(key) { if (key.startsWith("#") && key.includes(":")) { // Split the key back into an element ID and a property var split = key.split(":"); $(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key))); } }); }