[feat](trx-frontend): add map search filter
Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3227,6 +3227,7 @@ const mapMarkers = new Set();
|
||||
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, wspr: true };
|
||||
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||
const mapLocatorFilter = { phase: "type", bands: new Set() };
|
||||
let mapSearchFilter = "";
|
||||
const APRS_TRACK_MAX_POINTS = 64;
|
||||
const AIS_TRACK_MAX_POINTS = 64;
|
||||
const aisMarkers = new Map();
|
||||
@@ -3757,6 +3758,82 @@ function markerPassesLocatorFilters(marker) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function markerSearchText(marker) {
|
||||
const type = marker?.__trxType;
|
||||
if (type === "bookmark" || type === "ft8" || type === "wspr") {
|
||||
const entry = locatorEntryForMarker(marker);
|
||||
const parts = [];
|
||||
if (entry?.grid) parts.push(entry.grid);
|
||||
if (entry?.sourceType) parts.push(locatorSourceLabel(entry.sourceType));
|
||||
if (entry?.bandMeta instanceof Map) parts.push(...Array.from(entry.bandMeta.keys()));
|
||||
if (Array.isArray(entry?.bookmarks)) {
|
||||
for (const bm of entry.bookmarks) {
|
||||
if (bm?.name) parts.push(String(bm.name));
|
||||
if (bm?.locator) parts.push(String(bm.locator));
|
||||
if (bm?.mode) parts.push(String(bm.mode));
|
||||
if (bm?.category) parts.push(String(bm.category));
|
||||
if (bm?.comment) parts.push(String(bm.comment));
|
||||
if (Number.isFinite(bm?.freq_hz)) parts.push(String(Math.round(Number(bm.freq_hz))));
|
||||
}
|
||||
}
|
||||
if (entry?.stations instanceof Set) {
|
||||
parts.push(...Array.from(entry.stations.values()).map((v) => String(v)));
|
||||
}
|
||||
if (entry?.stationDetails instanceof Map) {
|
||||
for (const detail of entry.stationDetails.values()) {
|
||||
if (detail?.station) parts.push(String(detail.station));
|
||||
if (detail?.message) parts.push(String(detail.message));
|
||||
if (Number.isFinite(detail?.freq_hz)) parts.push(String(Math.round(Number(detail.freq_hz))));
|
||||
}
|
||||
}
|
||||
return parts.join(" ").toLowerCase();
|
||||
}
|
||||
if (type === "aprs") {
|
||||
const call = marker?._aprsCall ? String(marker._aprsCall) : "";
|
||||
const entry = stationMarkers.get(call);
|
||||
const info = entry?.info ? String(entry.info) : "";
|
||||
const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
|
||||
return `${call} ${info} ${pktRaw}`.toLowerCase();
|
||||
}
|
||||
if (type === "ais") {
|
||||
const key = marker?._aisMmsi ? String(marker._aisMmsi) : "";
|
||||
const msg = aisMarkers.get(key)?.msg;
|
||||
return [
|
||||
key,
|
||||
msg?.name,
|
||||
msg?.callsign,
|
||||
msg?.destination,
|
||||
Number.isFinite(msg?.mmsi) ? String(msg.mmsi) : "",
|
||||
Number.isFinite(msg?.lat) ? String(msg.lat) : "",
|
||||
Number.isFinite(msg?.lon) ? String(msg.lon) : "",
|
||||
].join(" ").toLowerCase();
|
||||
}
|
||||
if (type === "vdes") {
|
||||
const key = marker?._vdesKey ? String(marker._vdesKey) : "";
|
||||
const msg = vdesMarkers.get(key)?.msg;
|
||||
return [
|
||||
key,
|
||||
msg?.name,
|
||||
msg?.mmsi,
|
||||
msg?.message,
|
||||
msg?.raw,
|
||||
Number.isFinite(msg?.lat) ? String(msg.lat) : "",
|
||||
Number.isFinite(msg?.lon) ? String(msg.lon) : "",
|
||||
].join(" ").toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function markerPassesSearchFilter(marker) {
|
||||
const query = String(mapSearchFilter || "").trim().toLowerCase();
|
||||
if (!query) return true;
|
||||
const terms = query.split(/\s+/).filter(Boolean);
|
||||
if (terms.length === 0) return true;
|
||||
const haystack = markerSearchText(marker);
|
||||
if (!haystack) return false;
|
||||
return terms.every((term) => haystack.includes(term));
|
||||
}
|
||||
|
||||
function syncAprsReceiverMarker() {
|
||||
if (!aprsMap) return;
|
||||
const hasLocation = serverLat != null && serverLon != null;
|
||||
@@ -4044,6 +4121,7 @@ function initAprsMap() {
|
||||
|
||||
const locatorPhaseEl = document.getElementById("map-locator-phase");
|
||||
const locatorChoiceEl = document.getElementById("map-locator-choice-filter");
|
||||
const mapSearchEl = document.getElementById("map-search-filter");
|
||||
const fullscreenBtn = document.getElementById("map-fullscreen-btn");
|
||||
if (locatorPhaseEl) {
|
||||
locatorPhaseEl.addEventListener("click", (e) => {
|
||||
@@ -4084,6 +4162,13 @@ function initAprsMap() {
|
||||
applyMapFilter();
|
||||
});
|
||||
}
|
||||
if (mapSearchEl) {
|
||||
mapSearchEl.value = mapSearchFilter;
|
||||
mapSearchEl.addEventListener("input", () => {
|
||||
mapSearchFilter = String(mapSearchEl.value || "").trim();
|
||||
applyMapFilter();
|
||||
});
|
||||
}
|
||||
if (fullscreenBtn) {
|
||||
fullscreenBtn.addEventListener("click", () => {
|
||||
toggleMapFullscreen();
|
||||
@@ -4646,7 +4731,7 @@ function applyMapFilter() {
|
||||
if (!aprsMap) return;
|
||||
mapMarkers.forEach((marker) => {
|
||||
const type = marker.__trxType;
|
||||
const visible = markerPassesLocatorFilters(marker) && (
|
||||
const visible = markerPassesSearchFilter(marker) && markerPassesLocatorFilters(marker) && (
|
||||
(type === "bookmark" && mapFilter.bookmark) ||
|
||||
(type === "ais" && mapFilter.ais) ||
|
||||
(type === "vdes" && mapFilter.vdes) ||
|
||||
@@ -4842,6 +4927,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
||||
const key = `${markerType}:${grid}`;
|
||||
const existing = locatorMarkers.get(key);
|
||||
if (existing) {
|
||||
existing.grid = grid;
|
||||
if (stationId) existing.stations.add(stationId);
|
||||
if (!(existing.stationDetails instanceof Map)) existing.stationDetails = new Map();
|
||||
existing.stationDetails.set(detailKey, { ...detailEntry });
|
||||
@@ -4875,7 +4961,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
||||
marker.__trxType = markerType;
|
||||
sendLocatorOverlayToBack(marker);
|
||||
assignLocatorMarkerMeta(marker, markerType, bandMeta);
|
||||
locatorMarkers.set(key, { marker, stations, stationDetails, sourceType: markerType, bandMeta });
|
||||
locatorMarkers.set(key, { marker, grid, stations, stationDetails, sourceType: markerType, bandMeta });
|
||||
mapMarkers.add(marker);
|
||||
}
|
||||
rebuildMapLocatorFilters();
|
||||
|
||||
@@ -578,6 +578,10 @@
|
||||
<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>
|
||||
<div class="map-locator-filter-group">
|
||||
<span class="map-locator-filter-label">Search</span>
|
||||
<input type="text" id="map-search-filter" class="map-search-input" placeholder="Callsign, MMSI, locator, message..." />
|
||||
</div>
|
||||
</div>
|
||||
<div id="map-stage">
|
||||
<button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button>
|
||||
|
||||
@@ -1690,6 +1690,20 @@ small { color: var(--text-muted); }
|
||||
font-size: 0.77rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.map-search-input {
|
||||
width: 100%;
|
||||
max-width: 22rem;
|
||||
min-height: 1.95rem;
|
||||
padding: 0.3rem 0.55rem;
|
||||
border-radius: 7px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
|
||||
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
|
||||
color: var(--text);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.map-search-input::placeholder {
|
||||
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
|
||||
}
|
||||
|
||||
.rds-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; align-items: baseline; margin-bottom: 1rem; }
|
||||
.rds-field { display: contents; }
|
||||
|
||||
Reference in New Issue
Block a user