[fix](trx-frontend): make locator filters two-phase

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:20:38 +01:00
parent df173a3202
commit efe9dc346b
2 changed files with 110 additions and 46 deletions
@@ -3171,7 +3171,7 @@ const stationMarkers = new Map();
const locatorMarkers = new Map();
const mapMarkers = new Set();
const mapFilter = { ais: true, vdes: true, aprs: true, bookmark: true, ft8: true, wspr: true };
const mapLocatorFilter = { types: new Set(), bands: new Set() };
const mapLocatorFilter = { phase: "type", types: new Set(), bands: new Set() };
const APRS_TRACK_MAX_POINTS = 64;
const AIS_TRACK_MAX_POINTS = 64;
const aisMarkers = new Map();
@@ -3247,9 +3247,12 @@ function renderMapLocatorChipRow(container, items, selectedSet, kind) {
if (!container) return;
container.innerHTML = "";
if (!Array.isArray(items) || items.length === 0) {
container.innerHTML = `<span class="map-locator-empty">All ${kind === "type" ? "locator sources" : "bands"} visible</span>`;
container.innerHTML = `<span class="map-locator-empty">No ${kind === "type" ? "sources" : "bands"} available</span>`;
return;
}
if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
container.innerHTML = `<span class="map-locator-empty">All ${kind === "type" ? "sources" : "bands"} visible by default</span>`;
}
for (const item of items) {
const btn = document.createElement("button");
btn.type = "button";
@@ -3265,10 +3268,29 @@ function renderMapLocatorChipRow(container, items, selectedSet, kind) {
}
}
function renderMapLocatorPhaseRow(container, phase) {
if (!container) return;
container.innerHTML = "";
const phases = [
{ key: "type", label: "Source" },
{ key: "band", label: "Band" },
];
for (const item of phases) {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "map-locator-phase-btn";
if (phase === item.key) btn.classList.add("is-active");
btn.dataset.phase = item.key;
btn.textContent = item.label;
container.appendChild(btn);
}
}
function rebuildMapLocatorFilters() {
const typeEl = document.getElementById("map-locator-mode-filter");
const bandEl = document.getElementById("map-locator-band-filter");
if (!typeEl || !bandEl) return;
const phaseEl = document.getElementById("map-locator-phase");
const choiceEl = document.getElementById("map-locator-choice-filter");
const choiceLabelEl = document.getElementById("map-locator-choice-label");
if (!phaseEl || !choiceEl || !choiceLabelEl) return;
const typeMap = new Map();
const bandMap = new Map();
@@ -3285,15 +3307,23 @@ function rebuildMapLocatorFilters() {
}
const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map();
for (const [label, hz] of meta.entries()) {
const key = `${sourceType}:${label}`;
if (bandMap.has(key)) continue;
bandMap.set(key, {
key,
if (!bandMap.has(label)) {
bandMap.set(label, {
key: label,
label,
color: locatorFilterColor(sourceType),
kind: "band",
sortHz: Number.isFinite(hz) ? hz : 0,
});
continue;
}
const existing = bandMap.get(label);
if (existing && Number.isFinite(hz) && (!Number.isFinite(existing.sortHz) || hz > existing.sortHz)) {
existing.sortHz = hz;
}
if (existing && !existing.color) {
existing.color = locatorFilterColor(sourceType);
}
}
}
@@ -3310,26 +3340,29 @@ function rebuildMapLocatorFilters() {
const bandItems = Array.from(bandMap.values())
.sort((a, b) => (b.sortHz - a.sortHz) || a.label.localeCompare(b.label));
renderMapLocatorChipRow(typeEl, typeItems, mapLocatorFilter.types, "type");
renderMapLocatorChipRow(bandEl, bandItems, mapLocatorFilter.bands, "band");
renderMapLocatorPhaseRow(phaseEl, mapLocatorFilter.phase);
if (mapLocatorFilter.phase === "band") {
choiceLabelEl.textContent = "Visible Bands";
renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
} else {
choiceLabelEl.textContent = "Visible Sources";
renderMapLocatorChipRow(choiceEl, typeItems, mapLocatorFilter.types, "type");
}
}
function markerPassesLocatorFilters(marker) {
const meta = marker?._locatorFilterMeta;
if (!meta) return true;
if (mapLocatorFilter.types.size > 0 && !mapLocatorFilter.types.has(meta.sourceType)) {
if (mapLocatorFilter.phase === "band") {
if (mapLocatorFilter.bands.size === 0) return true;
if (!(meta.bands instanceof Set)) return false;
for (const label of mapLocatorFilter.bands) {
if (meta.bands.has(label)) return true;
}
return false;
}
if (mapLocatorFilter.bands.size > 0) {
const wanted = Array.from(mapLocatorFilter.bands);
const matches = wanted.some((key) => {
const sep = key.indexOf(":");
if (sep < 0) return false;
const sourceType = key.slice(0, sep);
const label = key.slice(sep + 1);
return sourceType === meta.sourceType && meta.bands instanceof Set && meta.bands.has(label);
});
if (!matches) return false;
if (mapLocatorFilter.types.size > 0 && !mapLocatorFilter.types.has(meta.sourceType)) {
return false;
}
return true;
}
@@ -3579,8 +3612,8 @@ function initAprsMap() {
const bookmarkFilter = document.getElementById("map-filter-bookmark");
const ft8Filter = document.getElementById("map-filter-ft8");
const wsprFilter = document.getElementById("map-filter-wspr");
const locatorModeFilterEl = document.getElementById("map-locator-mode-filter");
const locatorBandFilterEl = document.getElementById("map-locator-band-filter");
const locatorPhaseEl = document.getElementById("map-locator-phase");
const locatorChoiceEl = document.getElementById("map-locator-choice-filter");
if (aisFilter) {
aisFilter.addEventListener("change", () => {
mapFilter.ais = aisFilter.checked;
@@ -3623,31 +3656,30 @@ function initAprsMap() {
applyMapFilter();
});
}
if (locatorModeFilterEl) {
locatorModeFilterEl.addEventListener("click", (e) => {
const chip = e.target.closest(".map-locator-chip[data-filter-kind='type']");
if (!chip) return;
const key = String(chip.dataset.filterKey || "");
if (!key) return;
if (mapLocatorFilter.types.has(key)) {
mapLocatorFilter.types.delete(key);
} else {
mapLocatorFilter.types.add(key);
}
if (locatorPhaseEl) {
locatorPhaseEl.addEventListener("click", (e) => {
const btn = e.target.closest(".map-locator-phase-btn[data-phase]");
if (!btn) return;
const phase = String(btn.dataset.phase || "");
if (phase !== "type" && phase !== "band") return;
if (mapLocatorFilter.phase === phase) return;
mapLocatorFilter.phase = phase;
rebuildMapLocatorFilters();
applyMapFilter();
});
}
if (locatorBandFilterEl) {
locatorBandFilterEl.addEventListener("click", (e) => {
const chip = e.target.closest(".map-locator-chip[data-filter-kind='band']");
if (locatorChoiceEl) {
locatorChoiceEl.addEventListener("click", (e) => {
const chip = e.target.closest(".map-locator-chip[data-filter-kind]");
if (!chip) return;
const kind = String(chip.dataset.filterKind || "");
const key = String(chip.dataset.filterKey || "");
if (!key) return;
if (mapLocatorFilter.bands.has(key)) {
mapLocatorFilter.bands.delete(key);
const selectedSet = kind === "band" ? mapLocatorFilter.bands : mapLocatorFilter.types;
if (selectedSet.has(key)) {
selectedSet.delete(key);
} else {
mapLocatorFilter.bands.add(key);
selectedSet.add(key);
}
rebuildMapLocatorFilters();
applyMapFilter();
@@ -1585,6 +1585,38 @@ small { color: var(--text-muted); }
flex-wrap: wrap;
gap: 0.45rem;
}
.map-locator-phase-row {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.45rem;
}
.map-locator-phase-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.9rem;
padding: 0.2rem 0.8rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background: color-mix(in srgb, var(--input-bg) 88%, transparent);
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
cursor: pointer;
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease, color 120ms ease;
}
.map-locator-phase-btn:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--accent-green) 32%, var(--border-light));
}
.map-locator-phase-btn.is-active {
border-color: color-mix(in srgb, var(--accent-green) 62%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 14%, var(--input-bg));
color: var(--text-heading);
}
.map-locator-empty {
font-size: 0.75rem;
color: var(--text-muted);