diff --git a/README.md b/README.md index b29e8f1..769dc63 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), th ![Screenshot](/images/screenshot2.png) +![Screenshot](/images/screenshot3.png) + ### Accessing the public version You can access the public version's web interface at [https://spothole.app](https://spothole.app), and see [https://spothole.app/apidocs](https://spothole.app/apidocs) for the API details. diff --git a/core/constants.py b/core/constants.py index 426e924..96c0c27 100644 --- a/core/constants.py +++ b/core/constants.py @@ -32,7 +32,7 @@ SIGS = [ # Modes. Note "DIGI" and "DIGITAL" are also supported but are normalised into "DATA". CW_MODES = ["CW"] PHONE_MODES = ["PHONE", "SSB", "USB", "LSB", "AM", "FM", "DV", "DMR", "DSTAR", "C4FM", "M17"] -DATA_MODES = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "BPSK", "PSK", "PSK31", "BPSK31", "OLIVIA"] +DATA_MODES = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "BPSK", "PSK", "PSK31", "BPSK31", "OLIVIA", "MFSK", "MFSK32"] ALL_MODES = CW_MODES + PHONE_MODES + DATA_MODES MODE_TYPES = ["CW", "PHONE", "DATA"] diff --git a/images/screenshot3.png b/images/screenshot3.png new file mode 100644 index 0000000..22c759b Binary files /dev/null and b/images/screenshot3.png differ diff --git a/views/webpage_about.tpl b/views/webpage_about.tpl index 49ab890..8770431 100644 --- a/views/webpage_about.tpl +++ b/views/webpage_about.tpl @@ -24,6 +24,8 @@

Why does this website ask me if I want to install it?

Spothole is a Progressive Web App, which means you can install it on an Android or iOS device by opening the site in Chrome or Safari respectively, and clicking "Install" on the pop-up panel. It'll only prompt you once, so if you dismiss the prompt and change your mind, you'll find an Install / Add to Home Screen option on your browser's menu.

Installing Spothole on your phone is completely optional, the website works exactly the same way as the "app" does.

+

Why hasn't my spot/alert shown up yet?

+

To avoid putting too much load on the various servers that Spothole connects to, the Spothole server only polls them once every two minutes for spots, and once every hour for alerts. (Some sources, such as DX clusters, RBN, APRS-IS and WWBOTA use a non-polling mechanism, and their updates will therefore arrive more quickly.) Then if you are using the web interface, that has its own rate at which it reloads the data from Spothole, which is once a minute for spots or 30 minutes for alerts. So you could be waiting around three minutes to see a newly added spot, or 90 minutes to see a newly added alert.

What licence does Spothole use?

Spothole's source code is licenced under the Public Domain. You can write a Spothole client, run your own server, modify it however you like, you can claim you wrote it and charge people £1000 for a copy, I don't really mind. (Please don't do the last one. But if you're using my code for something cool, it would be nice to hear from you!)

Privacy

diff --git a/views/webpage_bands.tpl b/views/webpage_bands.tpl index 6352139..d1a229e 100644 --- a/views/webpage_bands.tpl +++ b/views/webpage_bands.tpl @@ -1,11 +1,5 @@ % rebase('webpage_base.tpl') -
- -
-
diff --git a/webassets/css/style.css b/webassets/css/style.css index 33e2a41..887733f 100644 --- a/webassets/css/style.css +++ b/webassets/css/style.css @@ -153,104 +153,95 @@ div#map { /* BANDS PANEL */ div#bands-container { + min-height: 64em; margin: 0; padding: 0; overflow-x: auto; - overflow-y: auto; white-space: nowrap; - display: flex; overscroll-behavior-x: none; } -/* Bands panel inner layout */ -div.bandCol { - height: 100%; - min-width: 8em; - display: flex; - flex-flow: column; - overflow-y: clip; +#bands-table { + min-width: 100%; } -div.bandColHeader { - flex: 0 1 auto; -} - -div.bandColMiddle { - flex: 1 1 auto; -} - -div.bandColMiddle ul { - display: table; - table-layout: fixed; - width: 100%; - min-height: 100%; - margin: 0; - padding: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -div.bandColMiddle ul li { - display: table-row; - line-height: 0.5em; -} - -/*noinspection CssUnusedSymbol*/ -div.bandColMiddle ul li.withSpots { - line-height: 1em; -} - -div.bandColMiddle ul li span { - display: table-cell; - vertical-align: middle; -} - -div.bandColMiddle ul { - display: table; - table-layout: fixed; - width: 100%; - height: 100%; - margin: 0; - padding: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; - border-left: 2px dotted; -} - -div.bandColHeader { +#bands-table th { + width: 20%; + max-height: 40px; + min-width: 12em; + padding: 0.5em; text-align: center; font-weight: bold; - padding: 0.5em; } -div.bandColMiddle { - margin-left: 3px; - border-left: 2px dotted var(--text); +#bands-table td { + width: 20%; + min-width: 12em; + height: 62em; } -div.bandColSpot { - display: block; +div.band-container { + height: 62em; + width: 20%; + min-width: 12em; + position: relative; +} + +div.band-markers { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 13; + border-left: 2px dotted black; +} + +div.band-spots { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 15; +} + +canvas.band-lines-canvas { + width: 5em; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 11; +} + +div.band-spot { + position: absolute; + left: 5em; + padding: 0 0.25em; + background-color: white; border-radius: 3px; - padding: 3px; - background: lightyellow; - margin-right: 2em; + cursor: default; } -span.bandColSpot { - vertical-align: bottom; - display: inline !important; +div.band-spot:hover { + z-index: 999; } -/* Don't wrap frequencies */ -span.bandColSpotFreq { - white-space: nowrap; - display: inline !important; +div.band-spot span.band-spot-call { + display: inline; } -span.bandColSpotMode { - padding-left: 0.5em; - font-size: 0.8em; - line-height: 0.4em; +div.band-spot:hover span.band-spot-call { + display: none; +} + +div.band-spot span.band-spot-info { + display: none; +} + +div.band-spot:hover span.band-spot-info { + display: inline; } diff --git a/webassets/js/bands.js b/webassets/js/bands.js index ffcda9a..ecbde3a 100644 --- a/webassets/js/bands.js +++ b/webassets/js/bands.js @@ -1,3 +1,12 @@ +// A couple of constants that must match what's in CSS. We need to know them before the content actually renders, so we +// can't just ask the elements themselves for their dimensions. +BAND_COLUMN_HEIGHT_EM = 62; +BAND_COLUMN_CANVAS_WIDTH_EM = 4; +BAND_COLUMN_FONT_SIZE = 16; +BAND_COLUMN_HEIGHT_PX = BAND_COLUMN_HEIGHT_EM * BAND_COLUMN_FONT_SIZE; +BAND_COLUMN_CANVAS_WIDTH_PX = BAND_COLUMN_CANVAS_WIDTH_EM * BAND_COLUMN_FONT_SIZE; +BAND_COLUMN_SPOT_DIV_HEIGHT_PX = BAND_COLUMN_FONT_SIZE * 1.6; + // Load spots and populate the bands display. function loadSpots() { $.getJSON('/api/v1/spots' + buildQueryString(), function(jsonData) { @@ -28,9 +37,9 @@ function buildQueryString() { // Update the bands display function updateBands() { // Stop here if nothing to display - var bandsPanel = $("#bands-container"); + var bandsContainer = $("#bands-container"); if (spots.length === 0) { - bandsPanel.html(""); + bandsContainer.html(""); return; } @@ -51,73 +60,137 @@ function updateBands() { } }); - // Build up HTML content for each band - let html = ""; - const columnWidthPercent = Math.max(30, 100 / bandToSpots.size); - let columnIndex = 0; + // Track if any columns end up taller than expected, so we can resize the container and avoid vertical scroll. + var maxHeightBand = 0; + + // Build up table content for each band + var table = $('').append(''); bandToSpots.forEach(function (spotList, bandName) { // Get the colours for the band from the first spot, and prepare the header - html += "
"; - html += "
" + spotList[0].band + "
"; - html += "
"; + table.find('thead tr').append(`
`); // Get the band data to fetch start and end frequencies let band = options["bands"].filter(function (b) { return b.name === bandName; })[0]; - // Start printing the band + + // 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. + bandMarkersDiv = $('
'); const freqStep = (band.end_freq - band.start_freq) / 40.0; - html += "
    "; - html += "
  • -
  • "; - - // Do 40 steps down the band for (let i = 0; i <= 40; i++) { - - // Work out if there are any spots in this step - const freqStepStart = band.start_freq + i * freqStep; - const freqStepEnd = freqStepStart + freqStep; - const spotsInStep = spotList.filter(function (s) { - // Normally we do >= start and < end, but in the special case where this is the last step and there is a spot - // right at the end of the band, we include this too - return s.freq >= freqStepStart && (s.freq < freqStepEnd || (s.freq === freqStepEnd && freqStepEnd === band.end_freq)); - }); - - if (spotsInStep.length > 0) { - // If this step has spots in it, print them - html += "
  • "; - spotsInStep.sort((a, b) => (a.freq > b.freq) ? 1 : ((b.freq > a.freq) ? -1 : 0)); - spotsInStep.forEach(function (s) { - html += "
    " + s.dx_call + "
    " + (s.freq/1000000) + ""; - if (s.mode != null && s.mode.length > 0 && s.mode !== "Unknown") { - html += "" + s.mode + ""; - } - html += "
    "; - }); - html += "
  • "; - + if (i % 4 === 0) { + bandMarkersDiv.append("—" + ((band.start_freq + i * freqStep)/1000000).toFixed(3) + "
    "); + } else if (i % 4 === 2) { + bandMarkersDiv.append("–
    "); } else { - // Step had no spots in it, so just print a marker. This is a frequency on multiples of 4, or a dash otherwise. - if (i % 4 === 0) { - html += "
  • —" + ((band.start_freq + i * freqStep)/1000000).toFixed(3) + "
  • "; - } else if (i % 4 === 2) { - html += "
  • "; - } else { - html += "
  • -
  • "; - } + bandMarkersDiv.append("-
    "); } } - html += "
  • -
  • "; - html += "
"; - html += "
"; - columnIndex++; + // Prepare the spots list + var bandSpotsDiv = $("
"); + var lastSpotPxDownBand = -999; + // 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; }); + // First calculate how we should be displaying the spots. There are three "modes" to try to place them in a + // visually appealing way: + // 1) Spaced normally, not going over the end of the band, so we populate them forwards. + // 2) Would go over the end, but the spots don't fill the band, so we populate them backwards. + // 3) Spots totally fill the band (or more), so we space them evenly starting at the top. + // In each case, we don't add anything to the DOM yet, we just calculate "pxDownBandLabel" (how far the *top* of + // the label is from the top of the div) and add that as a property to the spot for later use. + if (spotList.length >= BAND_COLUMN_HEIGHT_PX / BAND_COLUMN_SPOT_DIV_HEIGHT_PX) { + // Mode 3. + // Just lay out all spots simply, starting at 0px offset and working down with each one touching. + lastSpotPxDownBand = 0 - BAND_COLUMN_SPOT_DIV_HEIGHT_PX; + spotList.forEach(s => { + lastSpotPxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX; + s["pxDownBandLabel"] = lastSpotPxDownBand; + }); + } else { + // Mode 1 or 2. Run through adding things to the list forwards as a test. + spotList.forEach(s => { + // 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 + var pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX; + if (pxDownBand < lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX) { + pxDownBand = lastSpotPxDownBand + BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap + } + s["pxDownBandLabel"] = pxDownBand; + lastSpotPxDownBand = pxDownBand; + }); + + // Work out if we overflowed the end. + if (lastSpotPxDownBand <= BAND_COLUMN_HEIGHT_PX) { + // Mode 1. Current positions are fine and there's nothing to do. + } else { + // Mode 2. Repeat the process but backwards, starting at the end and working upwards. + lastSpotPxDownBand = 999999; + spotList.reverse().forEach(s => { + // 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 + var pxDownBand = percentDownBand * BAND_COLUMN_HEIGHT_PX; + if (pxDownBand > lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX) { + pxDownBand = lastSpotPxDownBand - BAND_COLUMN_SPOT_DIV_HEIGHT_PX; // Prevent overlap + } + s["pxDownBandLabel"] = pxDownBand; + lastSpotPxDownBand = pxDownBand; + }); + } + } + + // Now each spot is tagged with how far down the div it should go, add them to the DOM. + spotList.forEach(s => { + bandSpotsDiv.append(`
${s.dx_call}${s.dx_call} ${(s.freq/1000000).toFixed(3)} ${s.mode}
`); + }); + + // 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 + // 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); + maxHeightBand = Math.max(maxHeightBand, canvasHeight); + + // Draw horizontal or diagonal lines to join up the "real" frequency with where the spot div ended up + var bandLinesCanvas = $(``); + spotList.forEach(s => { + // 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 + 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 + 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 + + // Draw the line on the canvas + var ctx = bandLinesCanvas[0].getContext('2d'); + ctx.beginPath(); + ctx.lineWidth = 2; + ctx.lineCap = "round"; + ctx.strokeStyle = s.band_color; + ctx.moveTo(0, pxDownBandFreq); + ctx.lineTo(BAND_COLUMN_CANVAS_WIDTH_PX, pxDownBandLabel); + ctx.stroke(); + }); + + // Assemble the table cell + td = $("
${spotList[0].band}"); + container = $("
"); + container.append(bandLinesCanvas); + container.append(bandMarkersDiv); + container.append(bandSpotsDiv); + td.append(container); + table.find('tbody tr').append(td); }); + // Update the DOM with the band HTML - bandsPanel.html(html); + bandsContainer.html(table); + + // Increase the height of the bands container so we don't have any vertical scroll bars except the browser ones + bandsContainer.css("min-height", `${maxHeightBand + 42}px`); // Desktop mouse wheel to scroll bands horizontally if used on the headers - // noinspection JSDeprecatedSymbols - $(".bandColHeader").on("wheel", () => bandsPanel.scrollLeft(bandsPanel.scrollLeft() + event.deltaY / 10.0)); + table.find('thead tr').on("wheel", () => { + bandsContainer.scrollLeft(bandsContainer.scrollLeft() + event.deltaY / 10.0); + return false; + }); } // Iterate through a temporary list of spots, merging duplicates in a way suitable for the band panel. If two or more