[feat](trx-frontend-http): refine map and scheduler controls
Add separate map path toggles, move scheduler handoff into the channels row, and show a live countdown to the next scheduler cycle. Co-authored-by: OpenAI Codex <codex@openai.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -3746,6 +3746,7 @@ let aprsRadioPath = null;
|
|||||||
let selectedLocatorMarker = null;
|
let selectedLocatorMarker = null;
|
||||||
let selectedLocatorPulseRaf = null;
|
let selectedLocatorPulseRaf = null;
|
||||||
let mapFullscreenListenerBound = false;
|
let mapFullscreenListenerBound = false;
|
||||||
|
let mapP2pRadioPathsEnabled = loadSetting("mapP2pRadioPathsEnabled", true) !== false;
|
||||||
let mapDecodeContactPathsEnabled = loadSetting("mapDecodeContactPathsEnabled", true) !== false;
|
let mapDecodeContactPathsEnabled = loadSetting("mapDecodeContactPathsEnabled", true) !== false;
|
||||||
const stationMarkers = new Map();
|
const stationMarkers = new Map();
|
||||||
const locatorMarkers = new Map();
|
const locatorMarkers = new Map();
|
||||||
@@ -4169,7 +4170,7 @@ function syncDecodeContactPathVisibility() {
|
|||||||
|
|
||||||
function setMapRadioPathTo(lat, lon, className = "aprs-radio-path") {
|
function setMapRadioPathTo(lat, lon, className = "aprs-radio-path") {
|
||||||
clearMapRadioPath();
|
clearMapRadioPath();
|
||||||
if (serverLat == null || serverLon == null || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
|
if (!mapP2pRadioPathsEnabled || serverLat == null || serverLon == null || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
aprsRadioPath = L.polyline(
|
aprsRadioPath = L.polyline(
|
||||||
@@ -4800,6 +4801,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 mapSearchEl = document.getElementById("map-search-filter");
|
||||||
|
const mapP2pPathsToggleEl = document.getElementById("map-p2p-paths-toggle");
|
||||||
const mapContactPathsToggleEl = document.getElementById("map-contact-paths-toggle");
|
const mapContactPathsToggleEl = document.getElementById("map-contact-paths-toggle");
|
||||||
const fullscreenBtn = document.getElementById("map-fullscreen-btn");
|
const fullscreenBtn = document.getElementById("map-fullscreen-btn");
|
||||||
if (locatorPhaseEl) {
|
if (locatorPhaseEl) {
|
||||||
@@ -4855,6 +4857,15 @@ function initAprsMap() {
|
|||||||
applyMapFilter();
|
applyMapFilter();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (mapP2pPathsToggleEl) {
|
||||||
|
updateMapP2pPathsToggle();
|
||||||
|
mapP2pPathsToggleEl.addEventListener("click", () => {
|
||||||
|
mapP2pRadioPathsEnabled = !mapP2pRadioPathsEnabled;
|
||||||
|
saveSetting("mapP2pRadioPathsEnabled", mapP2pRadioPathsEnabled);
|
||||||
|
updateMapP2pPathsToggle();
|
||||||
|
if (!mapP2pRadioPathsEnabled) clearMapRadioPath();
|
||||||
|
});
|
||||||
|
}
|
||||||
if (mapContactPathsToggleEl) {
|
if (mapContactPathsToggleEl) {
|
||||||
updateMapContactPathsToggle();
|
updateMapContactPathsToggle();
|
||||||
mapContactPathsToggleEl.addEventListener("click", () => {
|
mapContactPathsToggleEl.addEventListener("click", () => {
|
||||||
@@ -5547,6 +5558,13 @@ function updateMapContactPathsToggle() {
|
|||||||
btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
|
btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMapP2pPathsToggle() {
|
||||||
|
const btn = document.getElementById("map-p2p-paths-toggle");
|
||||||
|
if (!btn) return;
|
||||||
|
btn.textContent = mapP2pRadioPathsEnabled ? "TRX Paths On" : "TRX Paths Off";
|
||||||
|
btn.classList.toggle("is-active", mapP2pRadioPathsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
function escapeMapHtml(input) {
|
function escapeMapHtml(input) {
|
||||||
return String(input)
|
return String(input)
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
|
|||||||
@@ -259,18 +259,18 @@
|
|||||||
<div class="label"><span>VFO</span></div>
|
<div class="label"><span>VFO</span></div>
|
||||||
<div class="vfo-picker" id="vfo-picker"></div>
|
<div class="vfo-picker" id="vfo-picker"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="full-row label-below-row" id="scheduler-control-row">
|
<div class="full-row label-below-row" id="vchan-row">
|
||||||
<div class="label"><span>Scheduler</span></div>
|
<div class="label"><span>Channels / Scheduler</span></div>
|
||||||
|
<div class="channel-scheduler-controls">
|
||||||
|
<div class="vchan-picker" id="vchan-picker"></div>
|
||||||
<div class="scheduler-control-row">
|
<div class="scheduler-control-row">
|
||||||
<div class="scheduler-release-wrap">
|
<div class="scheduler-release-wrap">
|
||||||
<button id="scheduler-release-btn" type="button">Release to Scheduler</button>
|
<button id="scheduler-release-btn" type="button">Release to Scheduler</button>
|
||||||
<div id="scheduler-release-status" class="scheduler-release-status">Scheduler is controlling the rig.</div>
|
<div id="scheduler-release-status" class="scheduler-release-status">Scheduler is controlling the rig.</div>
|
||||||
|
<div id="scheduler-cycle-status" class="scheduler-cycle-status">Next scheduler cycle in 30s.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="full-row label-below-row" id="vchan-row" style="display:none;">
|
|
||||||
<div class="label"><span>Channels</span></div>
|
|
||||||
<div class="vchan-picker" id="vchan-picker"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="full-row label-below-row">
|
<div class="full-row label-below-row">
|
||||||
<div class="label"><span>Signal</span></div>
|
<div class="label"><span>Signal</span></div>
|
||||||
@@ -687,8 +687,9 @@
|
|||||||
<div class="map-locator-filter-group">
|
<div class="map-locator-filter-group">
|
||||||
<span class="map-locator-filter-label">Paths</span>
|
<span class="map-locator-filter-label">Paths</span>
|
||||||
<div class="map-locator-phase-row">
|
<div class="map-locator-phase-row">
|
||||||
|
<button type="button" id="map-p2p-paths-toggle" class="map-locator-phase-btn">TRX Paths On</button>
|
||||||
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn">Contact Paths On</button>
|
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn">Contact Paths On</button>
|
||||||
<span class="map-locator-empty">Directed FT8 contacts with both locators known</span>
|
<span class="map-locator-empty">TRX paths on popup, directed contact paths when both locators are known</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ let vchanChannels = [];
|
|||||||
let vchanActiveId = null;
|
let vchanActiveId = null;
|
||||||
let schedulerReleaseState = null;
|
let schedulerReleaseState = null;
|
||||||
let schedulerReleasePollTimer = null;
|
let schedulerReleasePollTimer = null;
|
||||||
|
let schedulerCycleCountdownTimer = null;
|
||||||
|
|
||||||
|
function schedulerNextCycleSeconds() {
|
||||||
|
const periodMs = 30000;
|
||||||
|
const remMs = periodMs - (Date.now() % periodMs);
|
||||||
|
const secs = Math.ceil(remMs / 1000);
|
||||||
|
return Math.max(1, Math.min(30, secs));
|
||||||
|
}
|
||||||
|
|
||||||
|
function schedulerCycleSummaryText() {
|
||||||
|
return `Next scheduler cycle in ${schedulerNextCycleSeconds()}s.`;
|
||||||
|
}
|
||||||
|
|
||||||
function vchanFmtFreq(hz) {
|
function vchanFmtFreq(hz) {
|
||||||
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
if (!Number.isFinite(hz) || hz <= 0) return "--";
|
||||||
@@ -47,12 +59,14 @@ function schedulerReleaseSummaryText(state) {
|
|||||||
function vchanRenderSchedulerRelease() {
|
function vchanRenderSchedulerRelease() {
|
||||||
const btn = document.getElementById("scheduler-release-btn");
|
const btn = document.getElementById("scheduler-release-btn");
|
||||||
const status = document.getElementById("scheduler-release-status");
|
const status = document.getElementById("scheduler-release-status");
|
||||||
|
const cycleStatus = document.getElementById("scheduler-cycle-status");
|
||||||
if (!btn || !status) return;
|
if (!btn || !status) return;
|
||||||
const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
|
const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
|
||||||
btn.disabled = !vchanSessionId || currentReleased;
|
btn.disabled = !vchanSessionId || currentReleased;
|
||||||
btn.classList.toggle("active", !currentReleased);
|
btn.classList.toggle("active", !currentReleased);
|
||||||
btn.textContent = "Release to Scheduler";
|
btn.textContent = "Release to Scheduler";
|
||||||
status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
|
status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
|
||||||
|
if (cycleStatus) cycleStatus.textContent = schedulerCycleSummaryText();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function vchanPollSchedulerRelease() {
|
async function vchanPollSchedulerRelease() {
|
||||||
@@ -76,6 +90,10 @@ function vchanStartSchedulerReleasePolling() {
|
|||||||
clearInterval(schedulerReleasePollTimer);
|
clearInterval(schedulerReleasePollTimer);
|
||||||
}
|
}
|
||||||
schedulerReleasePollTimer = setInterval(vchanPollSchedulerRelease, 10000);
|
schedulerReleasePollTimer = setInterval(vchanPollSchedulerRelease, 10000);
|
||||||
|
if (schedulerCycleCountdownTimer) {
|
||||||
|
clearInterval(schedulerCycleCountdownTimer);
|
||||||
|
}
|
||||||
|
schedulerCycleCountdownTimer = setInterval(vchanRenderSchedulerRelease, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function vchanToggleSchedulerRelease() {
|
async function vchanToggleSchedulerRelease() {
|
||||||
@@ -288,9 +306,9 @@ function vchanReconnectAudio() {
|
|||||||
// Called by app.js from applyCapabilities().
|
// Called by app.js from applyCapabilities().
|
||||||
// Shows the channel picker only for SDR rigs.
|
// Shows the channel picker only for SDR rigs.
|
||||||
function vchanApplyCapabilities(caps) {
|
function vchanApplyCapabilities(caps) {
|
||||||
const row = document.getElementById("vchan-row");
|
const picker = document.getElementById("vchan-picker");
|
||||||
if (!row) return;
|
if (!picker) return;
|
||||||
row.style.display = (caps && caps.filter_controls) ? "" : "none";
|
picker.style.display = (caps && caps.filter_controls) ? "" : "none";
|
||||||
vchanRenderSchedulerRelease();
|
vchanRenderSchedulerRelease();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,15 +366,24 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
background: var(--btn-bg);
|
background: var(--btn-bg);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.channel-scheduler-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.6rem 0.8rem;
|
||||||
|
}
|
||||||
.vchan-picker {
|
.vchan-picker {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
flex: 1 1 18rem;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.scheduler-control-row {
|
.scheduler-control-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
flex: 0 1 22rem;
|
||||||
}
|
}
|
||||||
.scheduler-release-wrap {
|
.scheduler-release-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -392,6 +401,12 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
|
|||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
.scheduler-cycle-status {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
text-align: left;
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
.vchan-picker button {
|
.vchan-picker button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3642,8 +3657,16 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
|||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
}
|
}
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
.channel-scheduler-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
.vchan-picker {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
.scheduler-control-row {
|
.scheduler-control-row {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
flex-basis: auto;
|
||||||
}
|
}
|
||||||
.scheduler-release-wrap {
|
.scheduler-release-wrap {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
Reference in New Issue
Block a user