[feat](trx-frontend-http): add FT4 as map source

- Add ft4 to DEFAULT_MAP_SOURCE_FILTER (visible by default)
- Add ft4 hue to locatorThemeHues (peakHue + 30° offset from FT8)
- Propagate ft4 through locatorSourceLabel, locatorFilterColor,
  locatorHueForEntry, isLocatorOverlay, applyMapFilter,
  ensureDecodeLocatorMarker, pruneLocatorEntry, rebuildDecodeContactPaths,
  clearMapMarkersByType, markerSearchText, navigateToMapLocator, and
  the source items chip row
- ft8MapAddLocator now maps type="ft4" to markerType="ft4" so FT4
  locators get their own distinct marker colour and filter chip
- ft4.js: pass "ft4" (not "ft8") to ft8MapAddLocator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-14 18:57:58 +01:00
parent 780e68089f
commit 9b8ea37a6e
2 changed files with 21 additions and 17 deletions
@@ -3986,7 +3986,7 @@ const locatorMarkers = new Map();
const decodeContactPaths = new Map();
let selectedMapQsoKey = null;
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, ft4: true, wspr: true };
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
const mapLocatorFilter = { phase: "band", bands: new Set() };
let mapSearchFilter = "";
@@ -4163,7 +4163,7 @@ function ensureVdesMarker(key, entry) {
}
function ensureDecodeLocatorMarker(entry) {
if (!aprsMap || !entry || entry.marker || !entry.grid || (entry.sourceType !== "ft8" && entry.sourceType !== "wspr")) return;
if (!aprsMap || !entry || entry.marker || !entry.grid || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "wspr")) return;
const bounds = maidenheadToBounds(entry.grid);
if (!bounds) return;
const count = Math.max(entry.stationDetails?.size || 0, entry.stations?.size || 0, 1);
@@ -4245,7 +4245,7 @@ function pruneAisEntry(key, entry, cutoffMs) {
function pruneLocatorEntry(key, entry, cutoffMs) {
const canRenderMap = !!aprsMap && !decodeHistoryReplayActive;
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "wspr")) return true;
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "wspr")) return true;
if (!(entry.allStationDetails instanceof Map)) {
entry.allStationDetails = entry.stationDetails instanceof Map
? new Map(entry.stationDetails)
@@ -4330,6 +4330,7 @@ function pruneMapHistory() {
function locatorSourceLabel(type) {
if (type === "bookmark") return "Bookmarks";
if (type === "wspr") return "WSPR";
if (type === "ft4") return "FT4";
return "FT8";
}
@@ -4345,7 +4346,7 @@ function locatorFilterColor(type) {
const light = lightTheme ? 42 : 56;
const hue = type === "bookmark"
? hues.bookmark
: (type === "wspr" ? hues.wspr : hues.ft8);
: (type === "wspr" ? hues.wspr : (type === "ft4" ? hues.ft4 : hues.ft8));
return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
}
@@ -4470,6 +4471,7 @@ function locatorThemeHues() {
return {
bookmark: wrapHue(baseHue),
ft8: wrapHue(peakHue),
ft4: wrapHue(peakHue + 30),
wspr: wrapHue((waveHue + baseHue) / 2),
bandBase: wrapHue((baseHue * 0.65) + (peakHue * 0.35)),
};
@@ -4519,6 +4521,7 @@ function locatorHueForEntry(entry) {
}
if (entry?.sourceType === "bookmark") return hues.bookmark;
if (entry?.sourceType === "wspr") return hues.wspr;
if (entry?.sourceType === "ft4") return hues.ft4;
return hues.ft8;
}
@@ -4785,7 +4788,7 @@ function setSelectedLocatorMarker(marker) {
function isLocatorOverlay(marker) {
const type = marker?.__trxType;
return type === "bookmark" || type === "ft8" || type === "wspr";
return type === "bookmark" || type === "ft8" || type === "ft4" || type === "wspr";
}
function sendLocatorOverlayToBack(marker) {
@@ -4916,7 +4919,7 @@ function rebuildMapLocatorFilters() {
for (const entry of locatorMarkers.values()) {
const sourceType = entry?.sourceType;
if (!sourceType) continue;
if ((sourceType === "ft8" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue;
if ((sourceType === "ft8" || sourceType === "ft4" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue;
availableSources.add(sourceType);
const meta = entry?.bandMeta instanceof Map ? entry.bandMeta : new Map();
for (const [label, hz] of meta.entries()) {
@@ -4944,7 +4947,7 @@ function rebuildMapLocatorFilters() {
if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
}
const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "wspr"]
const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "wspr"]
.filter((key) => availableSources.has(key))
.map((key) => ({
key,
@@ -4986,7 +4989,7 @@ function markerPassesLocatorFilters(marker) {
function markerSearchText(marker) {
const type = marker?.__trxType;
if (type === "bookmark" || type === "ft8" || type === "wspr") {
if (type === "bookmark" || type === "ft8" || type === "ft4" || type === "wspr") {
const entry = locatorEntryForMarker(marker);
const parts = [];
if (entry?.grid) parts.push(entry.grid);
@@ -5132,7 +5135,7 @@ window.clearMapMarkersByType = function(type) {
return;
}
if (type === "ft8" || type === "wspr") {
if (type === "ft8" || type === "ft4" || type === "wspr") {
const prefix = `${type}:`;
for (const [key, entry] of locatorMarkers.entries()) {
if (!key.startsWith(prefix)) continue;
@@ -5379,7 +5382,7 @@ function initAprsMap() {
return;
}
if (marker.__trxType === "bookmark" || marker.__trxType === "ft8" || marker.__trxType === "wspr") {
if (marker.__trxType === "bookmark" || marker.__trxType === "ft8" || marker.__trxType === "ft4" || marker.__trxType === "wspr") {
const center = locatorMarkerCenter(marker);
if (center) {
setSelectedLocatorMarker(marker);
@@ -5618,10 +5621,10 @@ window.navigateToMapLocator = function(grid, preferredType = null) {
sizeAprsMapToViewport();
if (!aprsMap) return false;
const pref = preferredType === "wspr" ? "wspr" : (preferredType === "ft8" ? "ft8" : null);
const pref = preferredType === "wspr" ? "wspr" : (preferredType === "ft4" ? "ft4" : (preferredType === "ft8" ? "ft8" : null));
const keys = pref
? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`]
: [`ft8:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`]
: [`ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
let entry = null;
for (const key of keys) {
entry = locatorMarkers.get(key);
@@ -6160,6 +6163,7 @@ function applyMapFilter() {
(type === "vdes" && mapFilter.vdes) ||
(type === "aprs" && mapFilter.aprs) ||
(type === "ft8" && mapFilter.ft8) ||
(type === "ft4" && mapFilter.ft4) ||
(type === "wspr" && mapFilter.wspr)
);
const onMap = aprsMap.hasLayer(marker);
@@ -6271,7 +6275,7 @@ function rebuildDecodeContactPaths() {
const stationLocators = new Map();
const directedMessages = [];
for (const entry of locatorMarkers.values()) {
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "wspr")) continue;
if (!entry || (entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "wspr")) continue;
const grid = String(entry.grid || "").trim().toUpperCase();
if (!grid || !(entry.stationDetails instanceof Map)) continue;
for (const detail of entry.stationDetails.values()) {
@@ -6520,7 +6524,7 @@ window.syncBookmarkMapLocators = function(bookmarks) {
window.ft8MapAddLocator = function(message, grids, type = "ft8", station = null, details = null) {
if (!Array.isArray(grids) || grids.length === 0) return;
const markerType = type === "wspr" ? "wspr" : "ft8";
const markerType = type === "wspr" ? "wspr" : (type === "ft4" ? "ft4" : "ft8");
const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))];
const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : "";
const locatorDetails = new Map();
@@ -132,7 +132,7 @@ window.onServerFt4Batch = function(messages) {
for (const msg of messages) {
const next = normalizeServerFt4Message(msg);
if (next.grids.length > 0 && window.ft8MapAddLocator) {
window.ft8MapAddLocator(next.raw, next.grids, "ft8", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
window.ft8MapAddLocator(next.raw, next.grids, "ft4", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
}
next.history._tsMs = Number.isFinite(next.history?.ts_ms) ? Number(next.history.ts_ms) : Date.now();
normalized.push(next.history);
@@ -183,7 +183,7 @@ window.onServerFt4 = function(msg) {
if (ft4Status) ft4Status.textContent = ft4Paused ? "Paused" : "Receiving";
const next = normalizeServerFt4Message(msg);
if (next.grids.length > 0 && window.ft8MapAddLocator) {
window.ft8MapAddLocator(next.raw, next.grids, "ft8", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
window.ft8MapAddLocator(next.raw, next.grids, "ft4", next.station, { ...msg, freq_hz: next.rfHz, locator_details: next.locatorDetails });
}
addFt4Message(next.history);
};