// SSE event source let evtSource; let restartSSEOnErrorTimeoutId; // Table row count, to alternate shading let rowCount = 0; // Load spots and populate the table. function loadSpots() { // If we have an ongoing SSE connection, stop it so it doesn't interfere with our reload if (evtSource != null) { evtSource.close(); } // Make the new query $.getJSON('/api/v1/spots' + buildQueryString(), function(jsonData) { // Store data spots = jsonData; // Update table updateTable(); // Start SSE connection to fetch updates in the background, if we are in "run" mode let run = $('#runButton:checked').val(); if (run) { startSSEConnection(); } }); } // Start an SSE connection (closing an existing one if it exists). This will then be used to add to the table on the // fly. function startSSEConnection() { if (evtSource != null) { evtSource.close(); } evtSource = new EventSource('/api/v1/spots/stream' + buildQueryString()); evtSource.onmessage = function(event) { // Get the new spot 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 // 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 // be too far wrong. if (spots.length > 0) { newSpot["time"] = Math.max(newSpot["time"], Math.max(...spots.map(s => s["time"]))) } // Add spot to internal data store spots.unshift(newSpot); // Work out if we need to remove an old spot if (spots.length > $("#spots-to-fetch option:selected").val()) { spots = spots.slice(0, -1); // Drop oldest spot off the end of the table. This is two rows because of the mobile view extra rows $("#table tbody tr").last().remove(); $("#table tbody tr").last().remove(); } // 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. if (spots.length == 1) { $("#table tbody tr").last().remove(); } // Add the new spot to table addSpotToTopOfTable(newSpot, true); }; evtSource.onerror = function(err) { if (evtSource != null) { evtSource.close(); } clearTimeout(restartSSEOnErrorTimeoutId) restartSSEOnErrorTimeoutId = setTimeout(startSSEConnection, 1000); }; } // Build a query string for the API, based on the filters that the user has selected. function buildQueryString() { var str = "?"; ["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => { if (!allFilterOptionsSelected(fn)) { str = str + getQueryStringFor(fn) + "&"; } }); str = str + "limit=" + $("#spots-to-fetch option:selected").val(); if ($("#search").val() != "") { str = str + "&text_includes=" + encodeURIComponent($("#search").val()); } return str; } // Update the spots table 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 showType = $("#tableShowType")[0].checked; var showRef = $("#tableShowRef")[0].checked; var showDE = $("#tableShowDE")[0].checked; var showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked; // Populate table with headers let table = $("#table"); table.find('thead tr').empty(); if (showTime) { table.find('thead tr').append(`