Compare commits
2
Commits
2c1df75d19
...
2c56d82a81
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c56d82a81 | ||
|
|
0fc2115973 |
@@ -5601,6 +5601,39 @@ var _activeTab = "main";
|
|||||||
function tabFromPath2(pathname = window.location.pathname) {
|
function tabFromPath2(pathname = window.location.pathname) {
|
||||||
return tabFromPath(pathname);
|
return tabFromPath(pathname);
|
||||||
}
|
}
|
||||||
|
var pendingMapTarget = null;
|
||||||
|
function applyMapTarget(target) {
|
||||||
|
const map = window.trx.modules.map;
|
||||||
|
if (!map) return false;
|
||||||
|
if (target.kind === "position") {
|
||||||
|
map.focusMapPosition?.(target.lat, target.lon);
|
||||||
|
} else {
|
||||||
|
map.focusMapLocator?.(target.grid, target.preferredType);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function requestMapTarget(target) {
|
||||||
|
pendingMapTarget = target;
|
||||||
|
navigateToTab("map");
|
||||||
|
if (window.trx.modules.map) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
drainPendingMapTarget();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function drainPendingMapTarget() {
|
||||||
|
const target = pendingMapTarget;
|
||||||
|
if (!target) return;
|
||||||
|
if (applyMapTarget(target)) pendingMapTarget = null;
|
||||||
|
}
|
||||||
|
window.navigateToAprsMap = (lat, lon) => {
|
||||||
|
if (!isFiniteNumber(lat) || !isFiniteNumber(lon)) return;
|
||||||
|
requestMapTarget({ kind: "position", lat, lon });
|
||||||
|
};
|
||||||
|
window.navigateToMapLocator = (grid, preferredType = null) => {
|
||||||
|
if (!grid) return;
|
||||||
|
requestMapTarget({ kind: "locator", grid, preferredType });
|
||||||
|
};
|
||||||
var _mapInitTimer = null;
|
var _mapInitTimer = null;
|
||||||
function _initMapWhenReady() {
|
function _initMapWhenReady() {
|
||||||
const loadingEl2 = document.getElementById("map-loading");
|
const loadingEl2 = document.getElementById("map-loading");
|
||||||
@@ -5617,6 +5650,7 @@ function _initMapWhenReady() {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
map.sizeAprsMapToViewport();
|
map.sizeAprsMapToViewport();
|
||||||
map.aprsMap?.invalidateSize();
|
map.aprsMap?.invalidateSize();
|
||||||
|
drainPendingMapTarget();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -6324,15 +6358,15 @@ function renderSdrSquelch() {
|
|||||||
}
|
}
|
||||||
if (sdrSquelchDbEl) sdrSquelchDbEl.disabled = !sdrSquelchSupported;
|
if (sdrSquelchDbEl) sdrSquelchDbEl.disabled = !sdrSquelchSupported;
|
||||||
if (sdrSquelchToggleBtn) {
|
if (sdrSquelchToggleBtn) {
|
||||||
sdrSquelchToggleBtn.textContent = sdrSquelchEnabled ? "On" : "Off";
|
|
||||||
sdrSquelchToggleBtn.setAttribute("aria-pressed", String(sdrSquelchEnabled));
|
sdrSquelchToggleBtn.setAttribute("aria-pressed", String(sdrSquelchEnabled));
|
||||||
|
sdrSquelchToggleBtn.title = sdrSquelchEnabled ? "Turn the squelch off" : "Turn the squelch on";
|
||||||
}
|
}
|
||||||
const state = !sdrSquelchEnabled ? "off" : sdrSquelchIsPassing() ? "open" : "closed";
|
const state = !sdrSquelchEnabled ? "off" : sdrSquelchIsPassing() ? "open" : "closed";
|
||||||
if (sdrSquelchStateEl) {
|
if (sdrSquelchStateEl) {
|
||||||
sdrSquelchStateEl.dataset.state = state;
|
sdrSquelchStateEl.dataset.state = state;
|
||||||
sdrSquelchStateEl.setAttribute(
|
sdrSquelchStateEl.parentElement?.setAttribute(
|
||||||
"aria-label",
|
"aria-label",
|
||||||
state === "off" ? "Squelch off" : state === "open" ? "Squelch open" : "Squelch closed"
|
state === "off" ? "Squelch off" : state === "open" ? "Squelch on, open" : "Squelch on, closed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (squelchLabelEl) squelchLabelEl.textContent = String(sdrSquelchThresholdDb);
|
if (squelchLabelEl) squelchLabelEl.textContent = String(sdrSquelchThresholdDb);
|
||||||
|
|||||||
@@ -1669,16 +1669,7 @@ var mapWindow = window;
|
|||||||
popupAnchor: [0, -12]
|
popupAnchor: [0, -12]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
mapWindow.navigateToAprsMap = function(lat, lon) {
|
function focusMapPosition(lat, lon) {
|
||||||
T._activeTab = "map";
|
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => {
|
|
||||||
t.classList.remove("active");
|
|
||||||
});
|
|
||||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
|
||||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
|
||||||
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
|
||||||
const mapPanel = mapEl("tab-map");
|
|
||||||
if (mapPanel) mapPanel.style.display = "";
|
|
||||||
initAprsMap();
|
initAprsMap();
|
||||||
sizeAprsMapToViewport();
|
sizeAprsMapToViewport();
|
||||||
if (aprsMap) {
|
if (aprsMap) {
|
||||||
@@ -1690,19 +1681,10 @@ var mapWindow = window;
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
mapWindow.navigateToMapLocator = function(grid, preferredType = null) {
|
function focusMapLocator(grid, preferredType = null) {
|
||||||
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
||||||
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
||||||
T._activeTab = "map";
|
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => {
|
|
||||||
t.classList.remove("active");
|
|
||||||
});
|
|
||||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
|
||||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
|
||||||
document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
|
|
||||||
const mapPanel = mapEl("tab-map");
|
|
||||||
if (mapPanel) mapPanel.style.display = "";
|
|
||||||
initAprsMap();
|
initAprsMap();
|
||||||
sizeAprsMapToViewport();
|
sizeAprsMapToViewport();
|
||||||
if (!aprsMap) return false;
|
if (!aprsMap) return false;
|
||||||
@@ -1747,7 +1729,7 @@ var mapWindow = window;
|
|||||||
requestAnimationFrame(focusMarker);
|
requestAnimationFrame(focusMarker);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
};
|
}
|
||||||
function buildReceiverPopupHtml(rigIds) {
|
function buildReceiverPopupHtml(rigIds) {
|
||||||
const call = T.serverCallsign || T.ownerCallsign || "Receiver";
|
const call = T.serverCallsign || T.ownerCallsign || "Receiver";
|
||||||
let meta = "";
|
let meta = "";
|
||||||
@@ -2332,7 +2314,7 @@ var mapWindow = window;
|
|||||||
selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey ?? null;
|
selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey ?? null;
|
||||||
syncDecodeContactPathVisibility();
|
syncDecodeContactPathVisibility();
|
||||||
if (selectedMapQsoKey && entry.sourceGrid) {
|
if (selectedMapQsoKey && entry.sourceGrid) {
|
||||||
mapWindow.navigateToMapLocator?.(entry.sourceGrid, entry.sourceType);
|
focusMapLocator(entry.sourceGrid, entry.sourceType);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const head = document.createElement("div");
|
const head = document.createElement("div");
|
||||||
@@ -2438,7 +2420,7 @@ var mapWindow = window;
|
|||||||
card.className = "map-qso-card";
|
card.className = "map-qso-card";
|
||||||
if (entry.grid) {
|
if (entry.grid) {
|
||||||
card.addEventListener("click", () => {
|
card.addEventListener("click", () => {
|
||||||
mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
|
focusMapLocator(entry.grid ?? "", entry.sourceType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const head = document.createElement("div");
|
const head = document.createElement("div");
|
||||||
@@ -2544,7 +2526,7 @@ var mapWindow = window;
|
|||||||
card.className = "map-qso-card";
|
card.className = "map-qso-card";
|
||||||
if (entry.grid) {
|
if (entry.grid) {
|
||||||
card.addEventListener("click", () => {
|
card.addEventListener("click", () => {
|
||||||
mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
|
focusMapLocator(entry.grid ?? "", entry.sourceType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const head = document.createElement("div");
|
const head = document.createElement("div");
|
||||||
@@ -3049,6 +3031,8 @@ var mapWindow = window;
|
|||||||
}
|
}
|
||||||
modules.map = {
|
modules.map = {
|
||||||
initAprsMap,
|
initAprsMap,
|
||||||
|
focusMapPosition,
|
||||||
|
focusMapLocator,
|
||||||
sizeAprsMapToViewport,
|
sizeAprsMapToViewport,
|
||||||
syncAprsReceiverMarker,
|
syncAprsReceiverMarker,
|
||||||
updateMapRigFilter,
|
updateMapRigFilter,
|
||||||
|
|||||||
@@ -416,11 +416,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
|
<label class="vol-label">RX<input type="range" id="rx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="rx-vol-pct">80%</small></label>
|
||||||
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
|
<label class="vol-label">TX<input type="range" id="tx-vol" min="0" max="100" value="80" class="vol-slider" /><small class="vol-pct" id="tx-vol-pct">80%</small></label>
|
||||||
<span class="sql-control" id="sdr-squelch-wrap" style="display:none;">
|
<span class="sql-control" id="sdr-squelch-wrap" style="display:none;">
|
||||||
<span class="sql-title">SQL</span>
|
<button id="sdr-squelch-toggle" type="button" class="sql-toggle" aria-pressed="false" title="Turn the squelch on or off"><span class="sql-state" id="sdr-squelch-state" data-state="off" aria-hidden="true"></span>SQL</button>
|
||||||
<span class="sql-state" id="sdr-squelch-state" data-state="off" role="img" aria-label="Squelch state"></span>
|
|
||||||
<label class="sql-db"><input type="number" id="sdr-squelch-db" min="-120" max="-30" step="1" value="-95" inputmode="numeric" aria-label="Squelch threshold in dB" /><span class="sql-db-unit">dB</span></label>
|
<label class="sql-db"><input type="number" id="sdr-squelch-db" min="-120" max="-30" step="1" value="-95" inputmode="numeric" aria-label="Squelch threshold in dB" /><span class="sql-db-unit">dB</span></label>
|
||||||
<button id="sdr-squelch-auto" type="button" class="sql-auto-btn" title="Set the threshold just above the noise floor">Auto</button>
|
<button id="sdr-squelch-auto" type="button" class="sql-auto-btn" title="Set the threshold just above the noise floor">Auto</button>
|
||||||
<button id="sdr-squelch-toggle" type="button" class="sql-auto-btn" aria-pressed="false" title="Enable or disable the squelch">Off</button>
|
|
||||||
</span>
|
</span>
|
||||||
<div id="audio-level">
|
<div id="audio-level">
|
||||||
<div id="audio-level-fill"></div>
|
<div id="audio-level-fill"></div>
|
||||||
|
|||||||
@@ -1856,9 +1856,32 @@ small { color: var(--text-muted); }
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.sql-title {
|
/* The name is the switch: one target instead of a label and a button that
|
||||||
|
said the same thing twice, with the dot carrying the state. */
|
||||||
|
.sql-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.sql-toggle:hover {
|
||||||
|
border-color: color-mix(in srgb, var(--border-light) 60%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.sql-toggle[aria-pressed="true"] {
|
||||||
|
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
|
||||||
|
background: color-mix(in srgb, var(--accent-green) 10%, transparent);
|
||||||
|
color: var(--accent-text);
|
||||||
}
|
}
|
||||||
.sql-state {
|
.sql-state {
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ interface TrxModules {
|
|||||||
map?: {
|
map?: {
|
||||||
aprsMap?: { invalidateSize(): void } | null;
|
aprsMap?: { invalidateSize(): void } | null;
|
||||||
initAprsMap(): void;
|
initAprsMap(): void;
|
||||||
|
focusMapPosition?(lat: number, lon: number): void;
|
||||||
|
focusMapLocator?(grid: string, preferredType?: string | null): boolean;
|
||||||
sizeAprsMapToViewport(): void;
|
sizeAprsMapToViewport(): void;
|
||||||
pruneMapHistory(): void;
|
pruneMapHistory(): void;
|
||||||
updateMapBaseLayerForTheme(theme: string): void;
|
updateMapBaseLayerForTheme(theme: string): void;
|
||||||
@@ -359,6 +361,10 @@ declare global {
|
|||||||
trx: { state: TrxState; core: Readonly<Record<string, unknown>>; modules: TrxModules };
|
trx: { state: TrxState; core: Readonly<Record<string, unknown>>; modules: TrxModules };
|
||||||
trxUi: TrxUi;
|
trxUi: TrxUi;
|
||||||
trxPluginRuntime: TrxPluginRuntime;
|
trxPluginRuntime: TrxPluginRuntime;
|
||||||
|
// Owned here, not by the lazy map module: decode rows link to the map long
|
||||||
|
// before it has been loaded.
|
||||||
|
navigateToAprsMap(lat: number, lon: number): void;
|
||||||
|
navigateToMapLocator(grid: string, preferredType?: string | null): void;
|
||||||
lastSpectrumData: SpectrumFrame | null;
|
lastSpectrumData: SpectrumFrame | null;
|
||||||
lastFreqHz: number | null;
|
lastFreqHz: number | null;
|
||||||
currentBandwidthHz: number;
|
currentBandwidthHz: number;
|
||||||
@@ -4611,6 +4617,54 @@ function tabFromPath(pathname = window.location.pathname) {
|
|||||||
return tabFromPathname(pathname);
|
return tabFromPathname(pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map links fire from decode rows — an AIS position, an APRS frame, an FT8
|
||||||
|
// grid — that exist long before the Map tab has ever been opened, and the map
|
||||||
|
// module is lazy. It used to install these two globals itself, so until
|
||||||
|
// something had opened the tab the AIS links threw ("not a function") and the
|
||||||
|
// APRS ones silently did nothing. The app owns them instead: it is the only
|
||||||
|
// place that can materialise the panel, load the module and update history,
|
||||||
|
// and the target waits here until the module is up.
|
||||||
|
type PendingMapTarget =
|
||||||
|
| { kind: "position"; lat: number; lon: number }
|
||||||
|
| { kind: "locator"; grid: string; preferredType: string | null };
|
||||||
|
let pendingMapTarget: PendingMapTarget | null = null;
|
||||||
|
|
||||||
|
function applyMapTarget(target: PendingMapTarget): boolean {
|
||||||
|
const map = window.trx.modules.map;
|
||||||
|
if (!map) return false;
|
||||||
|
if (target.kind === "position") {
|
||||||
|
map.focusMapPosition?.(target.lat, target.lon);
|
||||||
|
} else {
|
||||||
|
map.focusMapLocator?.(target.grid, target.preferredType);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestMapTarget(target: PendingMapTarget) {
|
||||||
|
pendingMapTarget = target;
|
||||||
|
navigateToTab("map");
|
||||||
|
// Already loaded: the tab switch has shown the panel, so focus can happen
|
||||||
|
// now. Otherwise _initMapWhenReady drains it once the module arrives.
|
||||||
|
if (window.trx.modules.map) {
|
||||||
|
requestAnimationFrame(() => { drainPendingMapTarget(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drainPendingMapTarget() {
|
||||||
|
const target = pendingMapTarget;
|
||||||
|
if (!target) return;
|
||||||
|
if (applyMapTarget(target)) pendingMapTarget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.navigateToAprsMap = (lat: number, lon: number) => {
|
||||||
|
if (!isFiniteNumber(lat) || !isFiniteNumber(lon)) return;
|
||||||
|
requestMapTarget({ kind: "position", lat, lon });
|
||||||
|
};
|
||||||
|
window.navigateToMapLocator = (grid: string, preferredType: string | null = null) => {
|
||||||
|
if (!grid) return;
|
||||||
|
requestMapTarget({ kind: "locator", grid, preferredType });
|
||||||
|
};
|
||||||
|
|
||||||
// Initialise the Leaflet map, waiting for both Leaflet (L) and map-core.js
|
// Initialise the Leaflet map, waiting for both Leaflet (L) and map-core.js
|
||||||
// (window.trx.modules.map) if they haven't loaded yet.
|
// (window.trx.modules.map) if they haven't loaded yet.
|
||||||
let _mapInitTimer: ReturnType<typeof setInterval> | null = null;
|
let _mapInitTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
@@ -4630,6 +4684,7 @@ function _initMapWhenReady() {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
map.sizeAprsMapToViewport();
|
map.sizeAprsMapToViewport();
|
||||||
map.aprsMap?.invalidateSize();
|
map.aprsMap?.invalidateSize();
|
||||||
|
drainPendingMapTarget();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -5288,15 +5343,15 @@ function renderSdrSquelch() {
|
|||||||
}
|
}
|
||||||
if (sdrSquelchDbEl) sdrSquelchDbEl.disabled = !sdrSquelchSupported;
|
if (sdrSquelchDbEl) sdrSquelchDbEl.disabled = !sdrSquelchSupported;
|
||||||
if (sdrSquelchToggleBtn) {
|
if (sdrSquelchToggleBtn) {
|
||||||
sdrSquelchToggleBtn.textContent = sdrSquelchEnabled ? "On" : "Off";
|
|
||||||
sdrSquelchToggleBtn.setAttribute("aria-pressed", String(sdrSquelchEnabled));
|
sdrSquelchToggleBtn.setAttribute("aria-pressed", String(sdrSquelchEnabled));
|
||||||
|
sdrSquelchToggleBtn.title = sdrSquelchEnabled ? "Turn the squelch off" : "Turn the squelch on";
|
||||||
}
|
}
|
||||||
const state = !sdrSquelchEnabled ? "off" : (sdrSquelchIsPassing() ? "open" : "closed");
|
const state = !sdrSquelchEnabled ? "off" : (sdrSquelchIsPassing() ? "open" : "closed");
|
||||||
if (sdrSquelchStateEl) {
|
if (sdrSquelchStateEl) {
|
||||||
sdrSquelchStateEl.dataset.state = state;
|
sdrSquelchStateEl.dataset.state = state;
|
||||||
sdrSquelchStateEl.setAttribute(
|
sdrSquelchStateEl.parentElement?.setAttribute(
|
||||||
"aria-label",
|
"aria-label",
|
||||||
state === "off" ? "Squelch off" : (state === "open" ? "Squelch open" : "Squelch closed"),
|
state === "off" ? "Squelch off" : (state === "open" ? "Squelch on, open" : "Squelch on, closed"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (squelchLabelEl) squelchLabelEl.textContent = String(sdrSquelchThresholdDb);
|
if (squelchLabelEl) squelchLabelEl.textContent = String(sdrSquelchThresholdDb);
|
||||||
|
|||||||
@@ -2016,15 +2016,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mapWindow.navigateToAprsMap = function(lat, lon) {
|
function focusMapPosition(lat: number, lon: number) {
|
||||||
// Activate the map tab
|
|
||||||
T._activeTab = "map";
|
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => { t.classList.remove("active"); });
|
|
||||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
|
||||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
|
||||||
document.querySelectorAll<HTMLElement>(".tab-panel").forEach((p) => (p.style.display = "none"));
|
|
||||||
const mapPanel = mapEl("tab-map");
|
|
||||||
if (mapPanel) mapPanel.style.display = "";
|
|
||||||
initAprsMap();
|
initAprsMap();
|
||||||
sizeAprsMapToViewport();
|
sizeAprsMapToViewport();
|
||||||
if (aprsMap) {
|
if (aprsMap) {
|
||||||
@@ -2036,20 +2028,12 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
mapWindow.navigateToMapLocator = function(grid, preferredType = null) {
|
function focusMapLocator(grid: string, preferredType: string | null = null) {
|
||||||
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
const normalizedGrid = String(grid || "").trim().toUpperCase();
|
||||||
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
|
||||||
|
|
||||||
T._activeTab = "map";
|
|
||||||
document.querySelectorAll(".tab-bar .tab").forEach((t) => { t.classList.remove("active"); });
|
|
||||||
const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
|
|
||||||
if (mapTabBtn) mapTabBtn.classList.add("active");
|
|
||||||
document.querySelectorAll<HTMLElement>(".tab-panel").forEach((p) => (p.style.display = "none"));
|
|
||||||
const mapPanel = mapEl("tab-map");
|
|
||||||
if (mapPanel) mapPanel.style.display = "";
|
|
||||||
|
|
||||||
initAprsMap();
|
initAprsMap();
|
||||||
sizeAprsMapToViewport();
|
sizeAprsMapToViewport();
|
||||||
if (!aprsMap) return false;
|
if (!aprsMap) return false;
|
||||||
@@ -2099,7 +2083,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
requestAnimationFrame(focusMarker);
|
requestAnimationFrame(focusMarker);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2810,7 +2794,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey ?? null;
|
selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey ?? null;
|
||||||
syncDecodeContactPathVisibility();
|
syncDecodeContactPathVisibility();
|
||||||
if (selectedMapQsoKey && entry.sourceGrid) {
|
if (selectedMapQsoKey && entry.sourceGrid) {
|
||||||
mapWindow.navigateToMapLocator?.(entry.sourceGrid, entry.sourceType);
|
focusMapLocator(entry.sourceGrid, entry.sourceType);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2937,7 +2921,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
card.className = "map-qso-card";
|
card.className = "map-qso-card";
|
||||||
if (entry.grid) {
|
if (entry.grid) {
|
||||||
card.addEventListener("click", () => {
|
card.addEventListener("click", () => {
|
||||||
mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
|
focusMapLocator(entry.grid ?? "", entry.sourceType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3063,7 +3047,7 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
card.className = "map-qso-card";
|
card.className = "map-qso-card";
|
||||||
if (entry.grid) {
|
if (entry.grid) {
|
||||||
card.addEventListener("click", () => {
|
card.addEventListener("click", () => {
|
||||||
mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
|
focusMapLocator(entry.grid ?? "", entry.sourceType);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3627,6 +3611,8 @@ const mapWindow = window as unknown as MapWindow;
|
|||||||
// Register module API for core to call
|
// Register module API for core to call
|
||||||
modules.map = {
|
modules.map = {
|
||||||
initAprsMap,
|
initAprsMap,
|
||||||
|
focusMapPosition,
|
||||||
|
focusMapLocator,
|
||||||
sizeAprsMapToViewport,
|
sizeAprsMapToViewport,
|
||||||
syncAprsReceiverMarker,
|
syncAprsReceiverMarker,
|
||||||
updateMapRigFilter,
|
updateMapRigFilter,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { chromium } from "playwright-core";
|
|||||||
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
||||||
|
|
||||||
// page.evaluate callbacks run in the browser, not in this Node process.
|
// page.evaluate callbacks run in the browser, not in this Node process.
|
||||||
/* global document, getComputedStyle */
|
/* global document, getComputedStyle, window, location */
|
||||||
|
|
||||||
const fixture = await startWebFixture();
|
const fixture = await startWebFixture();
|
||||||
const { selectedRigs } = fixture;
|
const { selectedRigs } = fixture;
|
||||||
@@ -23,6 +23,33 @@ try {
|
|||||||
await page.locator("summary", { hasText: "Audio controls" }).click();
|
await page.locator("summary", { hasText: "Audio controls" }).click();
|
||||||
assert.equal(await page.locator("#rx-audio-btn").count(), 1);
|
assert.equal(await page.locator("#rx-audio-btn").count(), 1);
|
||||||
|
|
||||||
|
// Map links from decode rows, before anything has opened the Map tab. The
|
||||||
|
// lazy map module used to install these globals itself, so an AIS pin threw
|
||||||
|
// "not a function" and an APRS link silently did nothing until the tab had
|
||||||
|
// been visited once.
|
||||||
|
const mapLinkReady = await page.evaluate(() => ({
|
||||||
|
position: typeof window.navigateToAprsMap,
|
||||||
|
locator: typeof window.navigateToMapLocator,
|
||||||
|
mapModuleLoaded: !!window.trx.modules.map,
|
||||||
|
}));
|
||||||
|
assert.equal(mapLinkReady.position, "function", "navigateToAprsMap is missing before the map loads");
|
||||||
|
assert.equal(mapLinkReady.locator, "function", "navigateToMapLocator is missing before the map loads");
|
||||||
|
assert.equal(mapLinkReady.mapModuleLoaded, false, "the map module was already loaded, so this proves nothing");
|
||||||
|
|
||||||
|
await page.evaluate(() => { window.navigateToAprsMap(52.2, 21.0); });
|
||||||
|
await page.locator("#aprs-map .leaflet-pane").first().waitFor({ state: "attached" });
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const followed = await page.evaluate(() => ({
|
||||||
|
path: location.pathname,
|
||||||
|
active: [...document.querySelectorAll(".tab-bar .tab.active")].map((tab) => tab.dataset.tab || tab.id),
|
||||||
|
mapHeight: Math.round(document.getElementById("aprs-map").getBoundingClientRect().height),
|
||||||
|
}));
|
||||||
|
assert.equal(followed.path, "/map", `the map link left the page on ${followed.path}`);
|
||||||
|
assert.ok(followed.active.includes("map"), `the strip marks ${JSON.stringify(followed.active)}`);
|
||||||
|
assert.ok(followed.mapHeight > 100, `the map came up ${followed.mapHeight}px tall`);
|
||||||
|
await page.locator('.tab[data-tab="main"]').click();
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
|
||||||
// Section order in the tray. "Advanced radio controls" is built at runtime,
|
// Section order in the tray. "Advanced radio controls" is built at runtime,
|
||||||
// so it lands wherever ui-core puts it rather than where the markup says —
|
// so it lands wherever ui-core puts it rather than where the markup says —
|
||||||
// appending, as it once did, always left it last.
|
// appending, as it once did, always left it last.
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ try {
|
|||||||
shown: getComputedStyle(line).display !== "none",
|
shown: getComputedStyle(line).display !== "none",
|
||||||
db: Number(document.getElementById("sdr-squelch-db").value),
|
db: Number(document.getElementById("sdr-squelch-db").value),
|
||||||
label: Number(document.getElementById("spectrum-squelch-label").textContent),
|
label: Number(document.getElementById("spectrum-squelch-label").textContent),
|
||||||
toggle: document.getElementById("sdr-squelch-toggle").textContent,
|
toggle: document.getElementById("sdr-squelch-toggle").getAttribute("aria-pressed"),
|
||||||
top: Math.round(line.getBoundingClientRect().top),
|
top: Math.round(line.getBoundingClientRect().top),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
assert.equal(squelchOn.shown, true, "the threshold line did not appear with the squelch on");
|
assert.equal(squelchOn.shown, true, "the threshold line did not appear with the squelch on");
|
||||||
assert.equal(squelchOn.toggle, "On", "the toggle did not follow the squelch state");
|
assert.equal(squelchOn.toggle, "true", "the SQL switch did not follow the squelch state");
|
||||||
assert.equal(squelchOn.label, squelchOn.db, "the line and the readout disagree on the threshold");
|
assert.equal(squelchOn.label, squelchOn.db, "the line and the readout disagree on the threshold");
|
||||||
|
|
||||||
// Dragging the line down lowers the threshold and tells the server.
|
// Dragging the line down lowers the threshold and tells the server.
|
||||||
|
|||||||
Reference in New Issue
Block a user