More bands, filters layout, screenshots #7

This commit is contained in:
Ian Renton
2025-10-03 11:56:04 +01:00
parent 222e3d9c5e
commit f725e0e57b
7 changed files with 56 additions and 39 deletions

View File

@@ -4,6 +4,8 @@
(S)pothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data. (S)pothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.
![Screenshot](/images/screenshot.png)
While there are several other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, (S)pothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it. While there are several other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, (S)pothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it.
The API is deliberately well-defined with an OpenAPI specification and auto-generated API documentation. The API delivers spots in a consistent format regardless of the data source, freeing developers from needing to know how each individual data source presents its data. The API is deliberately well-defined with an OpenAPI specification and auto-generated API documentation. The API delivers spots in a consistent format regardless of the data source, freeing developers from needing to know how each individual data source presents its data.
@@ -22,7 +24,11 @@ TODO
The software can take a few seconds to start up, mostly because it is downloading an updated file to match callsigns to countries. This is normal, don't panic! The software can take a few seconds to start up, mostly because it is downloading an updated file to match callsigns to countries. This is normal, don't panic!
### Writing your own Providers ### Writing your own client
TODO
### Extending the server
(S)pothole is designed to be easily extensible. If you want to write your own provider, simply add a module to the `providers` package containing your class. (Currently, in order to be loaded correctly, the module (file) name should be the same as the class name, but lower case.) (S)pothole is designed to be easily extensible. If you want to write your own provider, simply add a module to the `providers` package containing your class. (Currently, in order to be loaded correctly, the module (file) name should be the same as the class name, but lower case.)

View File

@@ -16,6 +16,8 @@ MODE_TYPES = ["CW", "PHONE", "DATA"]
# Band definitions # Band definitions
BANDS = [ BANDS = [
Band(name="2200m", start_freq=135700, end_freq=137800, color="#ff4500", contrast_color="white"),
Band(name="600m", start_freq=472000, end_freq=479000, color="#1e90ff", contrast_color="white"),
Band(name="160m", start_freq=1800000, end_freq=2000000, color="#7cfc00", contrast_color="black"), Band(name="160m", start_freq=1800000, end_freq=2000000, color="#7cfc00", contrast_color="black"),
Band(name="80m", start_freq=3500000, end_freq=4000000, color="#e550e5", contrast_color="black"), Band(name="80m", start_freq=3500000, end_freq=4000000, color="#e550e5", contrast_color="black"),
Band(name="60m", start_freq=5250000, end_freq=5410000, color="#00008b", contrast_color="white"), Band(name="60m", start_freq=5250000, end_freq=5410000, color="#00008b", contrast_color="white"),
@@ -25,13 +27,21 @@ BANDS = [
Band(name="17m", start_freq=18068000, end_freq=18168000, color="#f2f261", contrast_color="black"), Band(name="17m", start_freq=18068000, end_freq=18168000, color="#f2f261", contrast_color="black"),
Band(name="15m", start_freq=21000000, end_freq=21450000, color="#cca166", contrast_color="black"), Band(name="15m", start_freq=21000000, end_freq=21450000, color="#cca166", contrast_color="black"),
Band(name="12m", start_freq=24890000, end_freq=24990000, color="#b22222", contrast_color="white"), Band(name="12m", start_freq=24890000, end_freq=24990000, color="#b22222", contrast_color="white"),
Band(name="11m", start_freq=26965000, end_freq=27405000, color="#00ff00", contrast_color="black"),
Band(name="10m", start_freq=28000000, end_freq=29700000, color="#ff69b4", contrast_color="black"), Band(name="10m", start_freq=28000000, end_freq=29700000, color="#ff69b4", contrast_color="black"),
Band(name="6m", start_freq=50000000, end_freq=54000000, color="#FF0000", contrast_color="white"), Band(name="6m", start_freq=50000000, end_freq=54000000, color="#FF0000", contrast_color="white"),
Band(name="5m", start_freq=56000000, end_freq=60500000, color="#e0e0e0", contrast_color="black"),
Band(name="4m", start_freq=70000000, end_freq=70500000, color="#cc0044", contrast_color="white"), Band(name="4m", start_freq=70000000, end_freq=70500000, color="#cc0044", contrast_color="white"),
Band(name="2m", start_freq=144000000, end_freq=148000000, color="#FF1493", contrast_color="black"), Band(name="2m", start_freq=144000000, end_freq=148000000, color="#FF1493", contrast_color="black"),
Band(name="1.25m", start_freq=219000000, end_freq=225000000, color="#CCFF00", contrast_color="black"),
Band(name="70cm", start_freq=420000000, end_freq=450000000, color="#999900", contrast_color="white"), Band(name="70cm", start_freq=420000000, end_freq=450000000, color="#999900", contrast_color="white"),
Band(name="23cm", start_freq=1240000000, end_freq=1325000000, color="#5AB8C7", contrast_color="black"), Band(name="23cm", start_freq=1240000000, end_freq=1325000000, color="#5AB8C7", contrast_color="black"),
Band(name="13cm", start_freq=2300000000, end_freq=2450000000, color="#FF7F50", contrast_color="black")] Band(name="2.4GHz", start_freq=2300000000, end_freq=2450000000, color="#FF7F50", contrast_color="black"),
Band(name="5.8GHz", start_freq=5725000000, end_freq=5850000000, color="#cc0099", contrast_color="white"),
Band(name="10GHz", start_freq=10000000000, end_freq=10500000000, color="#696969", contrast_color="white"),
Band(name="24GHz", start_freq=24000000000, end_freq=24050000000, color="#f3edc6", contrast_color="black"),
Band(name="47GHz", start_freq=47000000000, end_freq=47200000000, color="#ffe786", contrast_color="black"),
Band(name="76GHz", start_freq=75500000000, end_freq=81500000000, color="#baf9d8", contrast_color="black")]
UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white") UNKNOWN_BAND = Band(name="Unknown", start_freq=0, end_freq=0, color="black", contrast_color="white")
# Continents # Continents

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -43,7 +43,8 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="filters-container" class="row row-cols-1 row-cols-md-4 g-4"></div> <div id="filters-container-1" class="row row-cols-1 g-4 mb-4"></div>
<div id="filters-container-2" class="row row-cols-1 row-cols-md-4 g-4"></div>
</div> </div>
</div> </div>

View File

@@ -16,6 +16,11 @@ span.flag-wrapper {
cursor: default; cursor: default;
} }
span.band-bullet {
display: inline-block;
cursor: default;
}
span.icon-wrapper { span.icon-wrapper {
display: inline-block; display: inline-block;
width: 1.5em; width: 1.5em;
@@ -25,7 +30,7 @@ span.icon-wrapper {
span.freq-mhz { span.freq-mhz {
display: inline-block; display: inline-block;
min-width: 1.5em; min-width: 1.7em;
text-align: right; text-align: right;
font-weight: bold; font-weight: bold;
} }
@@ -53,10 +58,6 @@ div.appearing-panel {
display: none; display: none;
} }
div.status-card {
max-width: 18rem;
}
p.filter-card-text { p.filter-card-text {
line-height: 2.5em !important; line-height: 2.5em !important;
} }

View File

@@ -7,8 +7,6 @@ var spots = []
var options = {}; var options = {};
// Last time we updated the spots list on display. // Last time we updated the spots list on display.
var lastUpdateTime; var lastUpdateTime;
// Options-based lookups for band colours
band_colors = {}
// Load spots and populate the table. // Load spots and populate the table.
function loadSpots() { function loadSpots() {
@@ -65,12 +63,6 @@ function updateTable() {
mode_string = mode_string + "<span class='mode-q'><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'><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>"
} }
// Band-based colour
var band_dot_style = ""
if (band_colors[s["band"]]) {
band_dot_style = `color: ${band_colors[s["band"]]}; `
}
// Format sig_refs // Format sig_refs
var sig_refs = "" var sig_refs = ""
if (s["sig_refs"]) { if (s["sig_refs"]) {
@@ -89,10 +81,15 @@ function updateTable() {
de_country = "Unknown or not a country" de_country = "Unknown or not a country"
} }
// CSS doesn't like classes with decimal points in, so we need to replace that in the same way as when we originally
// queried the options endpoint and set our CSS.
var cssFormattedBandName = s['band'] ? s['band'].replace('.', 'p') : "unknown";
var bandFullName = s['band'] ? s['band'] + " band": "Unknown band";
// Populate the row // Populate the row
$tr.append(`<td>${time_formatted}</td>`); $tr.append(`<td>${time_formatted}</td>`);
$tr.append(`<td><span class='flag-wrapper' title='${dx_country}'>${s["dx_flag"]}</span>${s["dx_call"]}</td>`); $tr.append(`<td><span class='flag-wrapper' title='${dx_country}'>${s["dx_flag"]}</span>${s["dx_call"]}</td>`);
$tr.append(`<td><span class='band-dot' style='${band_dot_style}'>&#9632;</span>${freq_string}</td>`); $tr.append(`<td><span class='band-bullet band-bullet-${cssFormattedBandName}' title='${bandFullName}'>&#9632;</span>${freq_string}</td>`);
$tr.append(`<td>${mode_string}</td>`); $tr.append(`<td>${mode_string}</td>`);
$tr.append('<td>' + escapeHtml(`${s["comment"]}`) + '</td>'); $tr.append('<td>' + escapeHtml(`${s["comment"]}`) + '</td>');
$tr.append(`<td><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${s["source"]}</td>`); $tr.append(`<td><span class='icon-wrapper'><i class='fa-solid fa-${s["icon"]}'></i></span> ${s["source"]}</td>`);
@@ -137,7 +134,7 @@ function loadStatus() {
// Generate a status card // Generate a status card
function generateStatusCard(title, textLines) { function generateStatusCard(title, textLines) {
let $col = $("<div class='col'>") let $col = $("<div class='col'>")
let $card = $("<div class='card status-card'>"); let $card = $("<div class='card'>");
let $card_body = $("<div class='card-body'>"); let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>${title}</h5>`); $card_body.append(`<h5 class='card-title'>${title}</h5>`);
$card_body.append(`<p class='card-text'>${textLines.join("<br/>")}</p>`); $card_body.append(`<p class='card-text'>${textLines.join("<br/>")}</p>`);
@@ -153,20 +150,15 @@ function loadOptions() {
// Store options // Store options
options = jsonData; options = jsonData;
// Separately store colour lookups for bands // Add CSS for band bullets and band toggle buttons
options["bands"].forEach(b => { addBandColourCSS(options["bands"]);
band_colors[b["name"]] = b["color"];
});
// Add CSS for band toggle buttons
addBandToggleButtonCSS(options["bands"]);
// Populate the filters panel // Populate the filters panel
$("#filters-container").append(generateFilterCard("DX Continent", "dx_continent", options["continents"])); $("#filters-container-1").append(generateBandsFilterCard("Bands", "band", options["bands"]));
$("#filters-container").append(generateFilterCard("DE Continent", "de_continent", options["continents"])); $("#filters-container-2").append(generateFilterCard("DX Continent", "dx_continent", options["continents"]));
$("#filters-container").append(generateFilterCard("Modes", "mode_type", options["mode_types"])); $("#filters-container-2").append(generateFilterCard("DE Continent", "de_continent", options["continents"]));
$("#filters-container").append(generateFilterCard("Sources", "source", options["sources"])); $("#filters-container-2").append(generateFilterCard("Modes", "mode_type", options["mode_types"]));
$("#filters-container").append(generateBandsFilterCard("Bands", "band", options["bands"])); $("#filters-container-2").append(generateFilterCard("Sources", "source", options["sources"]));
// Load spots and set up the timer // Load spots and set up the timer
loadSpots(); loadSpots();
@@ -174,20 +166,24 @@ function loadOptions() {
}); });
} }
// Dynamically add CSS code for the band toggle buttons to be in the appropriate colour // Dynamically add CSS code for the band bullets and band toggle buttons to be in the appropriate colour.
function addBandToggleButtonCSS(band_options) { // Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p".
function addBandColourCSS(band_options) {
var $style = $('<style>'); var $style = $('<style>');
band_options.forEach(o => { band_options.forEach(o => {
$style.append(`#filter-button-label-band-${o['name']} { border-color: ${o['color']}; color: var(--bs-primary);}`); // CSS doesn't like IDs with decimal points in, so we need to replace that
$style.append(`.btn-check:checked + #filter-button-label-band-${o['name']} { background-color: ${o['color']}; color: ${o['contrast_color']};}`); var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
$style.append(`.band-bullet-${cssFormattedBandName} { color: ${o['color']}; }`);
$style.append(`#filter-button-label-band-${cssFormattedBandName} { border-color: ${o['color']}; color: var(--bs-primary);}`);
$style.append(`.btn-check:checked + #filter-button-label-band-${cssFormattedBandName} { background-color: ${o['color']}; color: ${o['contrast_color']};}`);
}); });
$('html > head').append($style); $('html > head').append($style);
} }
// Generate filter card // Generate filter card
function generateFilterCard(displayName, filterQuery, options) { function generateFilterCard(displayName, filterQuery, options) {
let $col = $("<div class='col-3'>") let $col = $("<div class='col'>")
let $card = $("<div class='card status-card'>"); let $card = $("<div class='card'>");
let $card_body = $("<div class='card-body'>"); let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>${displayName}</h5>`); $card_body.append(`<h5 class='card-title'>${displayName}</h5>`);
$p = $("<p class='card-text filter-card-text'>"); $p = $("<p class='card-text filter-card-text'>");
@@ -202,13 +198,16 @@ function generateFilterCard(displayName, filterQuery, options) {
// Generate bands filter card. This one is a special case. // Generate bands filter card. This one is a special case.
function generateBandsFilterCard(displayName, filterQuery, band_options) { function generateBandsFilterCard(displayName, filterQuery, band_options) {
let $col = $("<div class='col-12'>") let $col = $("<div class='col'>")
let $card = $("<div class='card status-card'>"); let $card = $("<div class='card'>");
let $card_body = $("<div class='card-body'>"); let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>${displayName}</h5>`); $card_body.append(`<h5 class='card-title'>${displayName}</h5>`);
$p = $("<p class='card-text filter-card-text'>"); $p = $("<p class='card-text filter-card-text'>");
band_options.forEach(o => { band_options.forEach(o => {
$p.append(`<input type="checkbox" class="btn-check filter-button-${filterQuery}" name="options" id="filter-button-${filterQuery}-${o['name']}" value="${filterQuery}:${o['name']}" autocomplete="off" checked><label class="btn btn-outline" id="filter-button-label-${filterQuery}-${o['name']}" for="filter-button-${filterQuery}-${o['name']}">${o['name']}</label> `); // CSS doesn't like IDs with decimal points in, so we need to replace that in the same way as when we originally
// queried the options endpoint and set our CSS.
var cssFormattedBandName = o['name'] ? o['name'].replace('.', 'p') : "unknown";
$p.append(`<input type="checkbox" class="btn-check filter-button-${filterQuery}" name="options" id="filter-button-${filterQuery}-${o['name']}" value="${filterQuery}:${o['name']}" autocomplete="off" checked><label class="btn btn-outline" id="filter-button-label-${filterQuery}-${cssFormattedBandName}" for="filter-button-${filterQuery}-${o['name']}">${o['name']}</label> `);
}); });
$card_body.append($p); $card_body.append($p);
$card.append($card_body); $card.append($card_body);