[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 DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, wspr: true };
|
||||||
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||||
const mapLocatorFilter = { phase: "type", bands: new Set() };
|
const mapLocatorFilter = { phase: "type", bands: new Set() };
|
||||||
|
let mapSearchFilter = "";
|
||||||
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();
|
||||||
@@ -3757,6 +3758,82 @@ function markerPassesLocatorFilters(marker) {
|
|||||||
return true;
|
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() {
|
function syncAprsReceiverMarker() {
|
||||||
if (!aprsMap) return;
|
if (!aprsMap) return;
|
||||||
const hasLocation = serverLat != null && serverLon != null;
|
const hasLocation = serverLat != null && serverLon != null;
|
||||||
@@ -4044,6 +4121,7 @@ function initAprsMap() {
|
|||||||
|
|
||||||
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");
|
||||||
|
const mapSearchEl = document.getElementById("map-search-filter");
|
||||||
const fullscreenBtn = document.getElementById("map-fullscreen-btn");
|
const fullscreenBtn = document.getElementById("map-fullscreen-btn");
|
||||||
if (locatorPhaseEl) {
|
if (locatorPhaseEl) {
|
||||||
locatorPhaseEl.addEventListener("click", (e) => {
|
locatorPhaseEl.addEventListener("click", (e) => {
|
||||||
@@ -4084,6 +4162,13 @@ function initAprsMap() {
|
|||||||
applyMapFilter();
|
applyMapFilter();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (mapSearchEl) {
|
||||||
|
mapSearchEl.value = mapSearchFilter;
|
||||||
|
mapSearchEl.addEventListener("input", () => {
|
||||||
|
mapSearchFilter = String(mapSearchEl.value || "").trim();
|
||||||
|
applyMapFilter();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (fullscreenBtn) {
|
if (fullscreenBtn) {
|
||||||
fullscreenBtn.addEventListener("click", () => {
|
fullscreenBtn.addEventListener("click", () => {
|
||||||
toggleMapFullscreen();
|
toggleMapFullscreen();
|
||||||
@@ -4646,7 +4731,7 @@ function applyMapFilter() {
|
|||||||
if (!aprsMap) return;
|
if (!aprsMap) return;
|
||||||
mapMarkers.forEach((marker) => {
|
mapMarkers.forEach((marker) => {
|
||||||
const type = marker.__trxType;
|
const type = marker.__trxType;
|
||||||
const visible = markerPassesLocatorFilters(marker) && (
|
const visible = markerPassesSearchFilter(marker) && markerPassesLocatorFilters(marker) && (
|
||||||
(type === "bookmark" && mapFilter.bookmark) ||
|
(type === "bookmark" && mapFilter.bookmark) ||
|
||||||
(type === "ais" && mapFilter.ais) ||
|
(type === "ais" && mapFilter.ais) ||
|
||||||
(type === "vdes" && mapFilter.vdes) ||
|
(type === "vdes" && mapFilter.vdes) ||
|
||||||
@@ -4842,6 +4927,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
|||||||
const key = `${markerType}:${grid}`;
|
const key = `${markerType}:${grid}`;
|
||||||
const existing = locatorMarkers.get(key);
|
const existing = locatorMarkers.get(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
existing.grid = grid;
|
||||||
if (stationId) existing.stations.add(stationId);
|
if (stationId) existing.stations.add(stationId);
|
||||||
if (!(existing.stationDetails instanceof Map)) existing.stationDetails = new Map();
|
if (!(existing.stationDetails instanceof Map)) existing.stationDetails = new Map();
|
||||||
existing.stationDetails.set(detailKey, { ...detailEntry });
|
existing.stationDetails.set(detailKey, { ...detailEntry });
|
||||||
@@ -4875,7 +4961,7 @@ window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null,
|
|||||||
marker.__trxType = markerType;
|
marker.__trxType = markerType;
|
||||||
sendLocatorOverlayToBack(marker);
|
sendLocatorOverlayToBack(marker);
|
||||||
assignLocatorMarkerMeta(marker, markerType, bandMeta);
|
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);
|
mapMarkers.add(marker);
|
||||||
}
|
}
|
||||||
rebuildMapLocatorFilters();
|
rebuildMapLocatorFilters();
|
||||||
|
|||||||
@@ -578,6 +578,10 @@
|
|||||||
<span class="map-locator-filter-label" id="map-locator-choice-label">Show</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 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>
|
||||||
<div id="map-stage">
|
<div id="map-stage">
|
||||||
<button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button>
|
<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-size: 0.77rem;
|
||||||
font-weight: 600;
|
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-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; align-items: baseline; margin-bottom: 1rem; }
|
||||||
.rds-field { display: contents; }
|
.rds-field { display: contents; }
|
||||||
|
|||||||
Reference in New Issue
Block a user