Start adding filters #7

This commit is contained in:
Ian Renton
2025-10-03 09:58:49 +01:00
parent 0e262f68f5
commit 222e3d9c5e
7 changed files with 89 additions and 20 deletions

View File

@@ -1,4 +1,4 @@
# (S)pothole # ![Spothole](/webassets/img/logo.png)
**Work in progress.** **Work in progress.**

View File

@@ -4,9 +4,6 @@ from data.band import Band
SOFTWARE_NAME = "(S)pothole by M0TRT" SOFTWARE_NAME = "(S)pothole by M0TRT"
SOFTWARE_VERSION = "0.1" SOFTWARE_VERSION = "0.1"
# Sources
SOURCES = ["POTA", "SOTA", "WWFF", "GMA", "WWBOTA", "HEMA", "ParksNPeaks", "Cluster", "RBN", "APRS-IS"]
# Special Interest Groups # Special Interest Groups
SIGS = ["POTA", "SOTA", "WWFF", "GMA", "WWBOTA", "HEMA", "MOTA", "ARLHS", "SiOTA", "WCA"] SIGS = ["POTA", "SOTA", "WWFF", "GMA", "WWBOTA", "HEMA", "MOTA", "ARLHS", "SiOTA", "WCA"]

View File

@@ -182,10 +182,11 @@ class Spot:
# Last resort for getting a position, use the DXCC entity. # Last resort for getting a position, use the DXCC entity.
if self.dx_call and not self.latitude: if self.dx_call and not self.latitude:
latlon = infer_latlon_from_callsign_dxcc(self.dx_call) latlon = infer_latlon_from_callsign_dxcc(self.dx_call)
self.latitude = latlon[0] if latlon:
self.longitude = latlon[1] self.latitude = latlon[0]
self.grid = infer_grid_from_callsign_dxcc(self.dx_call) self.longitude = latlon[1]
self.location_source = "DXCC" self.grid = infer_grid_from_callsign_dxcc(self.dx_call)
self.location_source = "DXCC"
# Location is "good" if it is from a spot, or from QRZ if the callsign doesn't contain a slash, so the operator # Location is "good" if it is from a spot, or from QRZ if the callsign doesn't contain a slash, so the operator
# is likely at home. # is likely at home.

View File

@@ -8,7 +8,7 @@ import pytz
from bottle import run, response, template from bottle import run, response, template
from core.config import MAX_SPOT_AGE from core.config import MAX_SPOT_AGE
from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, SOURCES, CONTINENTS from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, CONTINENTS
from core.utils import serialize_everything from core.utils import serialize_everything
@@ -125,6 +125,7 @@ class WebServer:
"modes": ALL_MODES, "modes": ALL_MODES,
"mode_types": MODE_TYPES, "mode_types": MODE_TYPES,
"sigs": SIGS, "sigs": SIGS,
"sources": SOURCES, # Sources are filtered for only ones that are enabled in config, no point letting the user toggle things that aren't even available.
"sources": list(map(lambda p: p["name"], filter(lambda p: p["enabled"], self.status_data["providers"]))),
"continents": CONTINENTS, "continents": CONTINENTS,
"max_spot_age": MAX_SPOT_AGE} "max_spot_age": MAX_SPOT_AGE}

View File

@@ -7,8 +7,8 @@
</div> </div>
<div class="col-auto"> <div class="col-auto">
<p class="d-inline-flex gap-1"> <p class="d-inline-flex gap-1">
<button id="filters-button" type="button" class="btn btn-primary">Filters</button> <button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button">Filters</button>
<button id="status-button" type="button" class="btn btn-primary">Status</button> <button id="status-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button">Status</button>
</p> </p>
</div> </div>
</div> </div>
@@ -43,7 +43,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="filters-container" class="row row-cols-1 row-cols-md-2 g-4"></div> <div id="filters-container" class="row row-cols-1 row-cols-md-4 g-4"></div>
</div> </div>
</div> </div>

View File

@@ -56,3 +56,7 @@ div.appearing-panel {
div.status-card { div.status-card {
max-width: 18rem; max-width: 18rem;
} }
p.filter-card-text {
line-height: 2.5em !important;
}

View File

@@ -58,6 +58,9 @@ function updateTable() {
// Format the mode // Format the mode
mode_string = s["mode"]; mode_string = s["mode"];
if (s["mode"] == null) {
mode_string = "???"
}
if (s["mode_source"] == "BANDPLAN") { if (s["mode_source"] == "BANDPLAN") {
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>"
} }
@@ -151,12 +154,19 @@ function loadOptions() {
options = jsonData; options = jsonData;
// Separately store colour lookups for bands // Separately store colour lookups for bands
options["bands"].forEach(m => { options["bands"].forEach(b => {
band_colors[m["name"]] = m["color"] 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").text(JSON.stringify(options)); $("#filters-container").append(generateFilterCard("DX Continent", "dx_continent", options["continents"]));
$("#filters-container").append(generateFilterCard("DE Continent", "de_continent", options["continents"]));
$("#filters-container").append(generateFilterCard("Modes", "mode_type", options["mode_types"]));
$("#filters-container").append(generateFilterCard("Sources", "source", options["sources"]));
$("#filters-container").append(generateBandsFilterCard("Bands", "band", options["bands"]));
// Load spots and set up the timer // Load spots and set up the timer
loadSpots(); loadSpots();
@@ -164,6 +174,48 @@ function loadOptions() {
}); });
} }
// Dynamically add CSS code for the band toggle buttons to be in the appropriate colour
function addBandToggleButtonCSS(band_options) {
var $style = $('<style>');
band_options.forEach(o => {
$style.append(`#filter-button-label-band-${o['name']} { border-color: ${o['color']}; color: var(--bs-primary);}`);
$style.append(`.btn-check:checked + #filter-button-label-band-${o['name']} { background-color: ${o['color']}; color: ${o['contrast_color']};}`);
});
$('html > head').append($style);
}
// Generate filter card
function generateFilterCard(displayName, filterQuery, options) {
let $col = $("<div class='col-3'>")
let $card = $("<div class='card status-card'>");
let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>${displayName}</h5>`);
$p = $("<p class='card-text filter-card-text'>");
options.forEach(o => {
$p.append(`<input type="checkbox" class="btn-check filter-button-${filterQuery}" name="options" id="filter-button-${filterQuery}-${o}" value="${filterQuery}:${o}" autocomplete="off" checked><label class="btn btn-outline-primary" for="filter-button-${filterQuery}-${o}">${o}</label> `);
});
$card_body.append($p);
$card.append($card_body);
$col.append($card);
return $col;
}
// Generate bands filter card. This one is a special case.
function generateBandsFilterCard(displayName, filterQuery, band_options) {
let $col = $("<div class='col-12'>")
let $card = $("<div class='card status-card'>");
let $card_body = $("<div class='card-body'>");
$card_body.append(`<h5 class='card-title'>${displayName}</h5>`);
$p = $("<p class='card-text filter-card-text'>");
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> `);
});
$card_body.append($p);
$card.append($card_body);
$col.append($card);
return $col;
}
// Update the refresh timing display // Update the refresh timing display
function updateRefreshDisplay() { function updateRefreshDisplay() {
if (lastUpdateTime != null) { if (lastUpdateTime != null) {
@@ -208,13 +260,27 @@ $(document).ready(function() {
// Event listeners // Event listeners
$("#status-button").click(function() { $("#status-button").click(function() {
// If we are going to display status, load the data // If we are going to display status, load the data for the status panel
if (!$("#status-area").is(":visible")) { if (!$("#status-area").is(":visible")) {
loadStatus(); loadStatus();
} }
$("#status-area").toggle(); $("#status-area").toggle();
}); });
$("#close-status-button").click(function() { $("#status-area").hide(); }); $("#close-status-button").click(function() {
$("#filters-button").click(function() { $("#filters-area").toggle(); }); $("#status-button").button("toggle");
$("#close-filters-button").click(function() { $("#filters-area").hide(); }); $("#status-area").hide();
});
$("#filters-button").click(function() {
$("#filters-area").toggle();
// If we have just dismissed the filters panel, reload spots
if (!$("#filters-area").is(":visible")) {
loadSpots()();
}
});
$("#close-filters-button").click(function() {
$("#filters-button").button("toggle");
$("#filters-area").hide();
// We have just dismissed the filters panel, reload spots
loadSpots();
});
}); });