diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
index 54dd065..1c51090 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
@@ -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 = `All ${kind === "type" ? "locator sources" : "bands"} visible`;
+ container.innerHTML = `No ${kind === "type" ? "sources" : "bands"} available`;
return;
}
+ if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
+ container.innerHTML = `All ${kind === "type" ? "sources" : "bands"} visible by default`;
+ }
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,
- label,
- color: locatorFilterColor(sourceType),
- kind: "band",
- sortHz: Number.isFinite(hz) ? hz : 0,
- });
+ 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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index 39ec3b7..d627241 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -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);