[fix](trx-frontend): unify map source filters

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-03 22:31:44 +01:00
parent 52bf573e4e
commit a194043caf
3 changed files with 86 additions and 138 deletions
@@ -3171,7 +3171,7 @@ const stationMarkers = new Map();
const locatorMarkers = new Map(); const locatorMarkers = new Map();
const mapMarkers = new Set(); const mapMarkers = new Set();
const mapFilter = { ais: true, vdes: true, aprs: true, bookmark: true, ft8: true, wspr: true }; const mapFilter = { ais: true, vdes: true, aprs: true, bookmark: true, ft8: true, wspr: true };
const mapLocatorFilter = { phase: "type", types: new Set(), bands: new Set() }; const mapLocatorFilter = { phase: "type", bands: new Set() };
const APRS_TRACK_MAX_POINTS = 64; const APRS_TRACK_MAX_POINTS = 64;
const AIS_TRACK_MAX_POINTS = 64; const AIS_TRACK_MAX_POINTS = 64;
const aisMarkers = new Map(); const aisMarkers = new Map();
@@ -3222,12 +3222,24 @@ function locatorSourceLabel(type) {
return "FT8"; return "FT8";
} }
function mapSourceLabel(type) {
if (type === "bookmark") return "Bookmarks";
return String(type || "").toUpperCase();
}
function locatorFilterColor(type) { function locatorFilterColor(type) {
if (type === "bookmark") return "#22c55e"; if (type === "bookmark") return "#22c55e";
if (type === "wspr") return "#ff6a3d"; if (type === "wspr") return "#ff6a3d";
return "#ff9b1a"; return "#ff9b1a";
} }
function mapSourceColor(type) {
if (type === "ais") return "#38bdf8";
if (type === "vdes") return "#a78bfa";
if (type === "aprs") return "#00d17f";
return locatorFilterColor(type);
}
function bandForHz(hz) { function bandForHz(hz) {
const rfHz = normalizeLocatorFreqHz(hz); const rfHz = normalizeLocatorFreqHz(hz);
if (!Number.isFinite(rfHz) || rfHz <= 0) return null; if (!Number.isFinite(rfHz) || rfHz <= 0) return null;
@@ -3267,17 +3279,23 @@ function renderMapLocatorChipRow(container, items, selectedSet, kind) {
if (!container) return; if (!container) return;
container.innerHTML = ""; container.innerHTML = "";
if (!Array.isArray(items) || items.length === 0) { if (!Array.isArray(items) || items.length === 0) {
container.innerHTML = `<span class="map-locator-empty">No ${kind === "type" ? "sources" : "bands"} available</span>`; container.innerHTML = `<span class="map-locator-empty">No ${kind === "band" ? "bands" : "sources"} available</span>`;
return; return;
} }
if (!(selectedSet instanceof Set) || selectedSet.size === 0) { if (kind === "source") {
container.innerHTML = `<span class="map-locator-empty">All ${kind === "type" ? "sources" : "bands"} visible by default</span>`; const allVisible = items.every((item) => mapFilter[item.key]);
if (allVisible) {
container.innerHTML = '<span class="map-locator-empty">All sources visible by default</span>';
}
} else if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
container.innerHTML = `<span class="map-locator-empty">All ${kind === "band" ? "bands" : "sources"} visible by default</span>`;
} }
for (const item of items) { for (const item of items) {
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.type = "button"; btn.type = "button";
btn.className = "map-locator-chip"; btn.className = "map-locator-chip";
if (!selectedSet.has(item.key)) btn.classList.add("is-inactive"); const isActive = kind === "source" ? !!mapFilter[item.key] : selectedSet.has(item.key);
if (!isActive) btn.classList.add("is-inactive");
btn.dataset.filterKind = kind; btn.dataset.filterKind = kind;
btn.dataset.filterKey = item.key; btn.dataset.filterKey = item.key;
btn.style.setProperty("--chip-color", item.color); btn.style.setProperty("--chip-color", item.color);
@@ -3312,19 +3330,10 @@ function rebuildMapLocatorFilters() {
const choiceLabelEl = document.getElementById("map-locator-choice-label"); const choiceLabelEl = document.getElementById("map-locator-choice-label");
if (!phaseEl || !choiceEl || !choiceLabelEl) return; if (!phaseEl || !choiceEl || !choiceLabelEl) return;
const typeMap = new Map();
const bandMap = new Map(); const bandMap = new Map();
for (const entry of locatorMarkers.values()) { for (const entry of locatorMarkers.values()) {
const sourceType = entry?.sourceType; const sourceType = entry?.sourceType;
if (!sourceType) continue; if (!sourceType) continue;
if (!typeMap.has(sourceType)) {
typeMap.set(sourceType, {
key: sourceType,
label: locatorSourceLabel(sourceType),
color: locatorFilterColor(sourceType),
kind: "type",
});
}
const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map(); const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map();
for (const [label, hz] of meta.entries()) { for (const [label, hz] of meta.entries()) {
if (!bandMap.has(label)) { if (!bandMap.has(label)) {
@@ -3347,16 +3356,16 @@ function rebuildMapLocatorFilters() {
} }
} }
for (const key of Array.from(mapLocatorFilter.types)) {
if (!typeMap.has(key)) mapLocatorFilter.types.delete(key);
}
for (const key of Array.from(mapLocatorFilter.bands)) { for (const key of Array.from(mapLocatorFilter.bands)) {
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key); if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
} }
const typeItems = ["bookmark", "ft8", "wspr"] const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "wspr"].map((key) => ({
.filter((key) => typeMap.has(key)) key,
.map((key) => typeMap.get(key)); label: mapSourceLabel(key),
color: mapSourceColor(key),
kind: "source",
}));
const bandItems = Array.from(bandMap.values()) const bandItems = Array.from(bandMap.values())
.sort((a, b) => (b.sortHz - a.sortHz) || a.label.localeCompare(b.label)); .sort((a, b) => (b.sortHz - a.sortHz) || a.label.localeCompare(b.label));
@@ -3366,7 +3375,7 @@ function rebuildMapLocatorFilters() {
renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band"); renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
} else { } else {
choiceLabelEl.textContent = "Visible Sources"; choiceLabelEl.textContent = "Visible Sources";
renderMapLocatorChipRow(choiceEl, typeItems, mapLocatorFilter.types, "type"); renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
} }
} }
@@ -3381,9 +3390,6 @@ function markerPassesLocatorFilters(marker) {
} }
return false; return false;
} }
if (mapLocatorFilter.types.size > 0 && !mapLocatorFilter.types.has(meta.sourceType)) {
return false;
}
return true; return true;
} }
@@ -3626,56 +3632,8 @@ function initAprsMap() {
rebuildMapLocatorFilters(); rebuildMapLocatorFilters();
applyMapFilter(); applyMapFilter();
const aisFilter = document.getElementById("map-filter-ais");
const vdesFilter = document.getElementById("map-filter-vdes");
const aprsFilter = document.getElementById("map-filter-aprs");
const bookmarkFilter = document.getElementById("map-filter-bookmark");
const ft8Filter = document.getElementById("map-filter-ft8");
const wsprFilter = document.getElementById("map-filter-wspr");
const locatorPhaseEl = document.getElementById("map-locator-phase"); const locatorPhaseEl = document.getElementById("map-locator-phase");
const locatorChoiceEl = document.getElementById("map-locator-choice-filter"); const locatorChoiceEl = document.getElementById("map-locator-choice-filter");
if (aisFilter) {
aisFilter.addEventListener("change", () => {
mapFilter.ais = aisFilter.checked;
applyMapFilter();
if (!mapFilter.ais && selectedAisTrackMmsi) {
const entry = aisMarkers.get(String(selectedAisTrackMmsi));
if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
entry.track.removeFrom(aprsMap);
}
}
});
}
if (vdesFilter) {
vdesFilter.addEventListener("change", () => {
mapFilter.vdes = vdesFilter.checked;
applyMapFilter();
});
}
if (aprsFilter) {
aprsFilter.addEventListener("change", () => {
mapFilter.aprs = aprsFilter.checked;
applyMapFilter();
});
}
if (bookmarkFilter) {
bookmarkFilter.addEventListener("change", () => {
mapFilter.bookmark = bookmarkFilter.checked;
applyMapFilter();
});
}
if (ft8Filter) {
ft8Filter.addEventListener("change", () => {
mapFilter.ft8 = ft8Filter.checked;
applyMapFilter();
});
}
if (wsprFilter) {
wsprFilter.addEventListener("change", () => {
mapFilter.wspr = wsprFilter.checked;
applyMapFilter();
});
}
if (locatorPhaseEl) { if (locatorPhaseEl) {
locatorPhaseEl.addEventListener("click", (e) => { locatorPhaseEl.addEventListener("click", (e) => {
const btn = e.target.closest(".map-locator-phase-btn[data-phase]"); const btn = e.target.closest(".map-locator-phase-btn[data-phase]");
@@ -3695,11 +3653,21 @@ function initAprsMap() {
const kind = String(chip.dataset.filterKind || ""); const kind = String(chip.dataset.filterKind || "");
const key = String(chip.dataset.filterKey || ""); const key = String(chip.dataset.filterKey || "");
if (!key) return; if (!key) return;
const selectedSet = kind === "band" ? mapLocatorFilter.bands : mapLocatorFilter.types; if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
if (selectedSet.has(key)) { mapFilter[key] = !mapFilter[key];
selectedSet.delete(key); if (!mapFilter.ais && selectedAisTrackMmsi) {
} else { const entry = aisMarkers.get(String(selectedAisTrackMmsi));
selectedSet.add(key); if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
entry.track.removeFrom(aprsMap);
}
selectedAisTrackMmsi = null;
}
} else if (kind === "band") {
if (mapLocatorFilter.bands.has(key)) {
mapLocatorFilter.bands.delete(key);
} else {
mapLocatorFilter.bands.add(key);
}
} }
rebuildMapLocatorFilters(); rebuildMapLocatorFilters();
applyMapFilter(); applyMapFilter();
@@ -569,21 +569,13 @@
</div> </div>
</div> </div>
<div id="tab-map" class="tab-panel" style="display:none;"> <div id="tab-map" class="tab-panel" style="display:none;">
<div class="map-controls">
<label><input type="checkbox" id="map-filter-ais" checked /> AIS</label>
<label><input type="checkbox" id="map-filter-vdes" checked /> VDES</label>
<label><input type="checkbox" id="map-filter-aprs" checked /> APRS</label>
<label><input type="checkbox" id="map-filter-bookmark" checked /> Bookmark Locators</label>
<label><input type="checkbox" id="map-filter-ft8" checked /> FT8</label>
<label><input type="checkbox" id="map-filter-wspr" checked /> WSPR</label>
</div>
<div class="map-locator-filters"> <div class="map-locator-filters">
<div class="map-locator-filter-group"> <div class="map-locator-filter-group">
<span class="map-locator-filter-label">Phase 1</span> <span class="map-locator-filter-label">Filter by</span>
<div id="map-locator-phase" class="map-locator-phase-row"></div> <div id="map-locator-phase" class="map-locator-phase-row"></div>
</div> </div>
<div class="map-locator-filter-group"> <div class="map-locator-filter-group">
<span class="map-locator-filter-label" id="map-locator-choice-label">Phase 2</span> <span class="map-locator-filter-label" id="map-locator-choice-label">Show</span>
<div id="map-locator-choice-filter" class="map-locator-chip-row"></div> <div id="map-locator-choice-filter" class="map-locator-chip-row"></div>
</div> </div>
</div> </div>
@@ -1547,35 +1547,26 @@ small { color: var(--text-muted); }
.ft8-freq { color: var(--accent-green); min-width: 4.6rem; text-align: right; } .ft8-freq { color: var(--accent-green); min-width: 4.6rem; text-align: right; }
.ft8-msg { flex: 1; } .ft8-msg { flex: 1; }
.ft8-locator { color: var(--accent-green); background: rgba(0, 209, 127, 0.12); border: 1px solid rgba(0, 209, 127, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-weight: 600; } .ft8-locator { color: var(--accent-green); background: rgba(0, 209, 127, 0.12); border: 1px solid rgba(0, 209, 127, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-weight: 600; }
.map-controls { display: flex; gap: 1rem; align-items: center; margin-bottom: 0.6rem; color: var(--text-muted); font-size: 0.82rem; }
.map-controls label {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.45rem;
border-radius: 999px;
border: 1px solid var(--filter-border);
background: var(--filter-bg);
color: var(--filter-fg);
}
.map-controls input[type="checkbox"] { margin-right: 0.3rem; }
.map-locator-filters { .map-locator-filters {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.45rem; gap: 0.3rem;
margin-bottom: 0.7rem; margin-bottom: 0.55rem;
padding-top: 0.15rem;
border-top: 1px solid color-mix(in srgb, var(--border) 86%, transparent);
} }
.map-locator-filter-group { .map-locator-filter-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.55rem; gap: 0.5rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.map-locator-filter-label { .map-locator-filter-label {
flex: 0 0 auto; flex: 0 0 auto;
font-size: 0.75rem; min-width: 4.8rem;
font-size: 0.72rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.04em; letter-spacing: 0.03em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted); color: var(--text-muted);
} }
@@ -1595,66 +1586,68 @@ small { color: var(--text-muted); }
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 1.9rem; min-height: 1.8rem;
padding: 0.2rem 0.8rem; padding: 0.1rem 0.65rem;
border-radius: 999px; border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent); border: 1px solid color-mix(in srgb, var(--border-light) 82%, transparent);
background: color-mix(in srgb, var(--input-bg) 88%, transparent); background: color-mix(in srgb, var(--input-bg) 92%, transparent);
color: var(--text-muted); color: var(--text-muted);
font-size: 0.78rem; font-size: 0.76rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.03em; letter-spacing: 0.02em;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, color 120ms ease; transition: border-color 120ms ease, background 120ms ease, color 120ms ease;
} }
.map-locator-phase-btn:hover { .map-locator-phase-btn:hover {
transform: translateY(-1px); border-color: color-mix(in srgb, var(--accent-green) 24%, var(--border-light));
border-color: color-mix(in srgb, var(--accent-green) 32%, var(--border-light)); color: var(--text);
} }
.map-locator-phase-btn.is-active { .map-locator-phase-btn.is-active {
border-color: color-mix(in srgb, var(--accent-green) 62%, var(--border-light)); border-color: var(--accent-green);
background: color-mix(in srgb, var(--accent-green) 14%, var(--input-bg)); background: color-mix(in srgb, var(--accent-green) 10%, var(--input-bg));
color: var(--text-heading); color: var(--accent-green);
} }
.map-locator-empty { .map-locator-empty {
font-size: 0.75rem; font-size: 0.74rem;
color: var(--text-muted); color: var(--text-muted);
opacity: 0.8; opacity: 0.8;
align-self: center;
} }
.map-locator-chip { .map-locator-chip {
--chip-color: var(--filter-border); --chip-color: var(--filter-border);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.35rem; gap: 0.35rem;
min-height: 1.9rem; min-height: 1.8rem;
padding: 0.2rem 0.7rem; padding: 0.1rem 0.55rem;
border-radius: 999px; border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--chip-color) 72%, var(--border-light)); border: 1px solid color-mix(in srgb, var(--chip-color) 52%, var(--border-light));
background: color-mix(in srgb, var(--chip-color) 12%, var(--input-bg)); background: color-mix(in srgb, var(--chip-color) 8%, var(--input-bg));
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, opacity 120ms ease; transition: border-color 120ms ease, background 120ms ease, opacity 120ms ease, color 120ms ease;
} }
.map-locator-chip:hover { .map-locator-chip:hover {
transform: translateY(-1px); color: var(--text-heading);
} }
.map-locator-chip.is-inactive { .map-locator-chip.is-inactive {
opacity: 0.55; opacity: 0.62;
background: color-mix(in srgb, var(--input-bg) 92%, transparent); border-color: color-mix(in srgb, var(--border-light) 68%, transparent);
background: color-mix(in srgb, var(--input-bg) 96%, transparent);
} }
.map-locator-chip-text { .map-locator-chip-text {
font-size: 0.8rem; font-size: 0.77rem;
font-weight: 600; font-weight: 600;
} }
.map-locator-chip-band { .map-locator-chip-band {
font-size: 0.78rem; font-size: 0.75rem;
font-weight: 700; font-weight: 700;
color: var(--chip-color); color: var(--chip-color);
padding: 0.08rem 0.4rem; padding: 0.05rem 0.34rem;
border-radius: 999px; border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--chip-color) 38%, transparent); border: 1px solid color-mix(in srgb, var(--chip-color) 28%, transparent);
background: color-mix(in srgb, var(--chip-color) 12%, transparent); background: color-mix(in srgb, var(--chip-color) 10%, transparent);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
@@ -1950,8 +1943,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
} }
.aprs-controls, .aprs-controls,
.ft8-controls, .ft8-controls,
.cw-controls, .cw-controls {
.map-controls {
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
} }
@@ -1993,10 +1985,6 @@ button:focus-visible, input:focus-visible, select:focus-visible {
width: 100%; width: 100%;
flex-basis: 100%; flex-basis: 100%;
} }
.map-controls label {
flex: 1 1 calc(50% - 0.5rem);
justify-content: center;
}
.map-locator-filter-group { .map-locator-filter-group {
align-items: stretch; align-items: stretch;
} }