diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js index f6907927..40e2d987 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js @@ -4,7 +4,7 @@ import { collapseAprsDuplicates, normalizeAprsPacket, renderAprsPacketRow -} from "./chunk-OZI7SINT.js"; +} from "./chunk-OPEIVJGD.js"; import { hostCore, hostState diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OPEIVJGD.js similarity index 84% rename from src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js rename to src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OPEIVJGD.js index 6a6a5faf..c13bbc3c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OZI7SINT.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-OPEIVJGD.js @@ -117,6 +117,9 @@ function aprsSymbolSprite(symbolTable, symbolCode) { label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}` }; } +function renderAprsSymbolSlot(packet, escapeHtml) { + return renderLocalAprsSymbol(packet, escapeHtml) || ''; +} function renderLocalAprsSymbol(packet, escapeHtml) { if (!packet.symbolTable || !packet.symbolCode) return ""; const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode); @@ -221,7 +224,7 @@ function renderAprsPacketRow(packet, options = {}) { const summary = summarizeAprsPayload(packet); const hasPosition = packet.lat != null && packet.lon != null; const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`; - row.innerHTML = `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}${escapeAprsHtml(aprsCategoryLabel(category))}${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `
>${escapeAprsHtml(packet.destCall || "--")}${escapeAprsHtml(packet.path || "no path")}${escapeAprsHtml(aprsAgeText(packet._tsMs))}CRC ${packet.crcOk ? "ok" : "failed"}` + (hasPosition ? `${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}` : "") + `
${renderAprsInfo(packet)}
` + (packet.info_bytes?.length ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `` : "") + (hasPosition ? `` : "") + `QRZ
`; + row.innerHTML = `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderAprsSymbolSlot(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}${escapeAprsHtml(aprsCategoryLabel(category))}${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `
>${escapeAprsHtml(packet.destCall || "--")}${escapeAprsHtml(packet.path || "no path")}${escapeAprsHtml(aprsAgeText(packet._tsMs))}CRC ${packet.crcOk ? "ok" : "failed"}` + (hasPosition ? `${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}` : "") + `
${renderAprsInfo(packet)}
` + (packet.info_bytes?.length ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `` : "") + (hasPosition ? `` : "") + `QRZ
`; row.querySelectorAll("[data-aprs-map]").forEach((element) => { element.addEventListener("click", (event) => { event.preventDefault(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js index 3d504ec0..d267fa66 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js @@ -4,7 +4,7 @@ import { collapseAprsDuplicates, normalizeAprsPacket, renderAprsPacketRow -} from "./chunk-OZI7SINT.js"; +} from "./chunk-OPEIVJGD.js"; import { hostCore, hostState diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js index dd456e9a..05298ff6 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js @@ -1,6 +1,6 @@ import { aprsSymbolSprite -} from "./chunk-OZI7SINT.js"; +} from "./chunk-OPEIVJGD.js"; // src/map-core.ts function mapEl(id) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 2da772a8..6642ae58 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -3153,6 +3153,8 @@ body.map-fake-fullscreen-active { 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-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-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'); } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts index 8ef8517f..8443bbcd 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts @@ -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) + || ''; +} + export function renderLocalAprsSymbol(packet: AprsPacket, escapeHtml: (value: string) => string): string { if (!packet.symbolTable || !packet.symbolCode) return ""; const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode); @@ -336,7 +343,7 @@ export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions `` + `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + - renderLocalAprsSymbol(packet, escapeAprsHtml) + + renderAprsSymbolSlot(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}` + `` + `${escapeAprsHtml(aprsCategoryLabel(category))}` + diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs index f021d128..5d70a4ca 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs @@ -174,7 +174,11 @@ const APRS_FRAMES = [ { packet_type: "position", info: "!5421.30N/01839.20E>Test beacon 73", lat: 54.35, lon: 18.65 }, ].map((frame, index) => ({ 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, })); @@ -224,6 +228,20 @@ try { assert.match(summaryOf("Telemetry"), /^#005/, `telemetry summary: ${summaryOf("Telemetry")}`); 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. await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" }) .locator(".decode-line").first().click();