[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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user