[fix](trx-frontend-http): hold the APRS symbol column open for frames without one
renderLocalAprsSymbol() returns nothing when a packet carries no symbol table or code, so those rows lost the icon's 24px slot and every column after it -- callsign, type badge, summary -- slid left against the rows around them. Frames that do carry a symbol then read as indented. Render an empty slot of the same size instead, so a list mixing position reports with messages and telemetry still lines up. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
|||||||
collapseAprsDuplicates,
|
collapseAprsDuplicates,
|
||||||
normalizeAprsPacket,
|
normalizeAprsPacket,
|
||||||
renderAprsPacketRow
|
renderAprsPacketRow
|
||||||
} from "./chunk-OZI7SINT.js";
|
} from "./chunk-OPEIVJGD.js";
|
||||||
import {
|
import {
|
||||||
hostCore,
|
hostCore,
|
||||||
hostState
|
hostState
|
||||||
|
|||||||
+4
-1
@@ -117,6 +117,9 @@ function aprsSymbolSprite(symbolTable, symbolCode) {
|
|||||||
label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}`
|
label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function renderAprsSymbolSlot(packet, escapeHtml) {
|
||||||
|
return renderLocalAprsSymbol(packet, escapeHtml) || '<span class="aprs-symbol aprs-symbol-empty" aria-hidden="true"></span>';
|
||||||
|
}
|
||||||
function renderLocalAprsSymbol(packet, escapeHtml) {
|
function renderLocalAprsSymbol(packet, escapeHtml) {
|
||||||
if (!packet.symbolTable || !packet.symbolCode) return "";
|
if (!packet.symbolTable || !packet.symbolCode) return "";
|
||||||
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
|
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
|
||||||
@@ -221,7 +224,7 @@ function renderAprsPacketRow(packet, options = {}) {
|
|||||||
const summary = summarizeAprsPayload(packet);
|
const summary = summarizeAprsPayload(packet);
|
||||||
const hasPosition = packet.lat != null && packet.lon != null;
|
const hasPosition = packet.lat != null && packet.lon != null;
|
||||||
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`;
|
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`;
|
||||||
row.innerHTML = `<summary class="decode-line"><span class="aprs-time">${escapeAprsHtml(time)}</span>` + (options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span><span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">${escapeAprsHtml(aprsCategoryLabel(category))}</span><span class="decode-line-summary">${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}</span>` + (packet.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC</span>') + (options.distance ? `<span class="decode-line-distance">${escapeAprsHtml(options.distance)}</span>` : "") + `</summary><div class="decode-expanded"><div class="decode-expanded-meta"><span>>${escapeAprsHtml(packet.destCall || "--")}</span><span>${escapeAprsHtml(packet.path || "no path")}</span><span>${escapeAprsHtml(aprsAgeText(packet._tsMs))}</span><span>CRC ${packet.crcOk ? "ok" : "failed"}</span>` + (hasPosition ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${packet.lat},${packet.lon}">${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}</a>` : "") + `</div><div class="decode-expanded-raw">${renderAprsInfo(packet)}</div>` + (packet.info_bytes?.length ? `<div class="decode-expanded-bytes">${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}</div>` : "") + `<div class="aprs-row-actions">` + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${packet.lat},${packet.lon}">Map</button>` : "") + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${packet.lat},${packet.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div></div>`;
|
row.innerHTML = `<summary class="decode-line"><span class="aprs-time">${escapeAprsHtml(time)}</span>` + (options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") + renderAprsSymbolSlot(packet, escapeAprsHtml) + `<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span><span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">${escapeAprsHtml(aprsCategoryLabel(category))}</span><span class="decode-line-summary">${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}</span>` + (packet.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC</span>') + (options.distance ? `<span class="decode-line-distance">${escapeAprsHtml(options.distance)}</span>` : "") + `</summary><div class="decode-expanded"><div class="decode-expanded-meta"><span>>${escapeAprsHtml(packet.destCall || "--")}</span><span>${escapeAprsHtml(packet.path || "no path")}</span><span>${escapeAprsHtml(aprsAgeText(packet._tsMs))}</span><span>CRC ${packet.crcOk ? "ok" : "failed"}</span>` + (hasPosition ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${packet.lat},${packet.lon}">${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}</a>` : "") + `</div><div class="decode-expanded-raw">${renderAprsInfo(packet)}</div>` + (packet.info_bytes?.length ? `<div class="decode-expanded-bytes">${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}</div>` : "") + `<div class="aprs-row-actions">` + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${packet.lat},${packet.lon}">Map</button>` : "") + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${packet.lat},${packet.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div></div>`;
|
||||||
row.querySelectorAll("[data-aprs-map]").forEach((element) => {
|
row.querySelectorAll("[data-aprs-map]").forEach((element) => {
|
||||||
element.addEventListener("click", (event) => {
|
element.addEventListener("click", (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
collapseAprsDuplicates,
|
collapseAprsDuplicates,
|
||||||
normalizeAprsPacket,
|
normalizeAprsPacket,
|
||||||
renderAprsPacketRow
|
renderAprsPacketRow
|
||||||
} from "./chunk-OZI7SINT.js";
|
} from "./chunk-OPEIVJGD.js";
|
||||||
import {
|
import {
|
||||||
hostCore,
|
hostCore,
|
||||||
hostState
|
hostState
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
aprsSymbolSprite
|
aprsSymbolSprite
|
||||||
} from "./chunk-OZI7SINT.js";
|
} from "./chunk-OPEIVJGD.js";
|
||||||
|
|
||||||
// src/map-core.ts
|
// src/map-core.ts
|
||||||
function mapEl(id) {
|
function mapEl(id) {
|
||||||
|
|||||||
@@ -3153,6 +3153,8 @@ body.map-fake-fullscreen-active {
|
|||||||
displays can swap in the @2x variants. */
|
displays can swap in the @2x variants. */
|
||||||
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-repeat: no-repeat; background-size: 384px 144px; vertical-align: middle; margin-right: 0.3rem; }
|
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-repeat: no-repeat; background-size: 384px 144px; vertical-align: middle; margin-right: 0.3rem; }
|
||||||
.aprs-symbol-marker { margin-right: 0; }
|
.aprs-symbol-marker { margin-right: 0; }
|
||||||
|
/* Holds the column open for a frame that carries no symbol. */
|
||||||
|
.aprs-symbol-empty { background-image: none; }
|
||||||
.aprs-symbol-primary { background-image: url('/vendor/aprs-symbols-24-0.png'); }
|
.aprs-symbol-primary { background-image: url('/vendor/aprs-symbols-24-0.png'); }
|
||||||
.aprs-symbol-alternate { background-image: url('/vendor/aprs-symbols-24-1.png'); }
|
.aprs-symbol-alternate { background-image: url('/vendor/aprs-symbols-24-1.png'); }
|
||||||
.aprs-symbol-overlaid { background-image: url('/vendor/aprs-symbols-24-2.png'), url('/vendor/aprs-symbols-24-1.png'); }
|
.aprs-symbol-overlaid { background-image: url('/vendor/aprs-symbols-24-2.png'), url('/vendor/aprs-symbols-24-1.png'); }
|
||||||
|
|||||||
@@ -182,6 +182,13 @@ export function aprsSymbolSprite(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** An empty slot of the symbol's size, so a frame without one still lines up
|
||||||
|
* with the frames around it in the list. */
|
||||||
|
export function renderAprsSymbolSlot(packet: AprsPacket, escapeHtml: (value: string) => string): string {
|
||||||
|
return renderLocalAprsSymbol(packet, escapeHtml)
|
||||||
|
|| '<span class="aprs-symbol aprs-symbol-empty" aria-hidden="true"></span>';
|
||||||
|
}
|
||||||
|
|
||||||
export function renderLocalAprsSymbol(packet: AprsPacket, escapeHtml: (value: string) => string): string {
|
export function renderLocalAprsSymbol(packet: AprsPacket, escapeHtml: (value: string) => string): string {
|
||||||
if (!packet.symbolTable || !packet.symbolCode) return "";
|
if (!packet.symbolTable || !packet.symbolCode) return "";
|
||||||
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
|
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
|
||||||
@@ -336,7 +343,7 @@ export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions
|
|||||||
`<summary class="decode-line">` +
|
`<summary class="decode-line">` +
|
||||||
`<span class="aprs-time">${escapeAprsHtml(time)}</span>` +
|
`<span class="aprs-time">${escapeAprsHtml(time)}</span>` +
|
||||||
(options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") +
|
(options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") +
|
||||||
renderLocalAprsSymbol(packet, escapeAprsHtml) +
|
renderAprsSymbolSlot(packet, escapeAprsHtml) +
|
||||||
`<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span>` +
|
`<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span>` +
|
||||||
`<span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">` +
|
`<span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">` +
|
||||||
`${escapeAprsHtml(aprsCategoryLabel(category))}</span>` +
|
`${escapeAprsHtml(aprsCategoryLabel(category))}</span>` +
|
||||||
|
|||||||
@@ -174,7 +174,11 @@ const APRS_FRAMES = [
|
|||||||
{ packet_type: "position", info: "!5421.30N/01839.20E>Test beacon 73", lat: 54.35, lon: 18.65 },
|
{ packet_type: "position", info: "!5421.30N/01839.20E>Test beacon 73", lat: 54.35, lon: 18.65 },
|
||||||
].map((frame, index) => ({
|
].map((frame, index) => ({
|
||||||
src_call: `SP2SJG-${index}`, dest_call: "APRS", path: "WIDE1-1", crc_ok: true,
|
src_call: `SP2SJG-${index}`, dest_call: "APRS", path: "WIDE1-1", crc_ok: true,
|
||||||
symbol_table: "/", symbol_code: ">", rig_id: "rig-a", ts_ms: Date.now() - index * 1000,
|
// Only position reports carry a symbol here, which is the case the columns
|
||||||
|
// have to survive: a frame without one used to close the gap and shift
|
||||||
|
// everything after it left.
|
||||||
|
...(frame.packet_type === "position" ? { symbol_table: "/", symbol_code: ">" } : {}),
|
||||||
|
rig_id: "rig-a", ts_ms: Date.now() - index * 1000,
|
||||||
...frame,
|
...frame,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -224,6 +228,20 @@ try {
|
|||||||
assert.match(summaryOf("Telemetry"), /^#005/, `telemetry summary: ${summaryOf("Telemetry")}`);
|
assert.match(summaryOf("Telemetry"), /^#005/, `telemetry summary: ${summaryOf("Telemetry")}`);
|
||||||
assert.match(summaryOf("Position"), /54\.3500, 18\.6500/, `position summary: ${summaryOf("Position")}`);
|
assert.match(summaryOf("Position"), /54\.3500, 18\.6500/, `position summary: ${summaryOf("Position")}`);
|
||||||
|
|
||||||
|
// Frames with and without a symbol line up: the slot is held open either way.
|
||||||
|
const columns = await aprs.page.evaluate(() =>
|
||||||
|
[...document.querySelectorAll("#aprs-packets .aprs-packet")].map((row) => ({
|
||||||
|
call: Math.round(row.querySelector(".aprs-call").getBoundingClientRect().x),
|
||||||
|
summary: Math.round(row.querySelector(".decode-line-summary").getBoundingClientRect().x),
|
||||||
|
symbol: !!row.querySelector(".aprs-symbol:not(.aprs-symbol-empty)"),
|
||||||
|
})));
|
||||||
|
assert.ok(columns.some((column) => column.symbol) && columns.some((column) => !column.symbol),
|
||||||
|
"the sample has to mix frames with and without a symbol to test this");
|
||||||
|
assert.equal(new Set(columns.map((column) => column.call)).size, 1,
|
||||||
|
`callsigns start at ${JSON.stringify(columns.map((column) => column.call))}`);
|
||||||
|
assert.equal(new Set(columns.map((column) => column.summary)).size, 1,
|
||||||
|
`summaries start at ${JSON.stringify(columns.map((column) => column.summary))}`);
|
||||||
|
|
||||||
// The frame as it arrived is still there, one click away.
|
// The frame as it arrived is still there, one click away.
|
||||||
await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" })
|
await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" })
|
||||||
.locator(".decode-line").first().click();
|
.locator(".decode-line").first().click();
|
||||||
|
|||||||
Reference in New Issue
Block a user