CI / lint (pull_request) Successful in 2m16s
CI / test (pull_request) Successful in 8m12s
CI / frontend (pull_request) Successful in 3m2s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 7m28s
CI / frontend (push) Successful in 2m11s
CI / reuse (push) Successful in 3s
Resolve a table/code pair to a sprite cell in aprs-shared, and use it from both the packet lists and the map markers, which had each been printing the raw symbol character in a bordered box. A table identifier of / or \ selects the primary or alternate sheet directly. Anything else is an overlay character, which the APRS spec draws on top of the alternate symbol -- so those stack the overlay sheet over the alternate one rather than picking a sheet. Codes outside 0x21..0x7E have no cell and keep the old character box. The sheet URLs stay in the stylesheet so a min-resolution query can swap in the retina sheets; only the cell offset is computed and set inline. Map markers share the helper through the plugin chunk, so the map stays free of any remote symbol fetch. Verified in a browser against the real stylesheet and sheets: /> is a car, /_ a WX circle, /& an igate diamond, \n a red triangle, and the overlays S> and 7# carry their character on the alternate symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018huL1ELyr86yVqfAabtioA Signed-off-by: Stan Grams <sjg@haxx.space>
157 lines
5.5 KiB
JavaScript
157 lines
5.5 KiB
JavaScript
// src/plugins/aprs-shared.ts
|
|
function aprsPacketCategory(packet) {
|
|
const type = (packet.type ?? "").toLowerCase();
|
|
const info = (packet.info ?? "").toLowerCase();
|
|
if (packet.lat != null && packet.lon != null || type.includes("position")) return "position";
|
|
if (type.includes("message") || info.startsWith(":")) return "message";
|
|
if (type.includes("weather") || info.startsWith("_")) return "weather";
|
|
if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
|
|
return "other";
|
|
}
|
|
function aprsCategoryLabel(category) {
|
|
switch (category) {
|
|
case "position":
|
|
return "Position";
|
|
case "message":
|
|
return "Message";
|
|
case "weather":
|
|
return "Weather";
|
|
case "telemetry":
|
|
return "Telemetry";
|
|
default:
|
|
return "Other";
|
|
}
|
|
}
|
|
function aprsAgeText(timestampMs) {
|
|
if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now";
|
|
const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3);
|
|
if (seconds < 5) return "just now";
|
|
if (seconds < 60) return `${String(seconds)}s ago`;
|
|
const minutes = Math.round(seconds / 60);
|
|
if (minutes < 60) return `${String(minutes)}m ago`;
|
|
return `${String(Math.round(minutes / 60))}h ago`;
|
|
}
|
|
function aprsPacketSignature(packet) {
|
|
return [
|
|
packet.srcCall ?? "",
|
|
packet.destCall ?? "",
|
|
packet.path ?? "",
|
|
packet.info ?? "",
|
|
packet.type ?? "",
|
|
packet.lat?.toFixed(4) ?? "",
|
|
packet.lon?.toFixed(4) ?? ""
|
|
].join("|");
|
|
}
|
|
function collapseAprsDuplicates(packets) {
|
|
const seen = /* @__PURE__ */ new Set();
|
|
return packets.filter((packet) => {
|
|
const signature = aprsPacketSignature(packet);
|
|
if (seen.has(signature)) return false;
|
|
seen.add(signature);
|
|
return true;
|
|
});
|
|
}
|
|
function aprsHexBytes(bytes) {
|
|
if (!bytes?.length) return "--";
|
|
return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" ");
|
|
}
|
|
function renderAprsInfo(packet) {
|
|
if (packet.info_bytes?.length) {
|
|
return packet.info_bytes.map((byte) => renderAprsByte(byte)).join("");
|
|
}
|
|
return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join("");
|
|
}
|
|
function renderAprsByte(byte) {
|
|
return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `<span class="aprs-byte">0x${byte.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
|
}
|
|
function renderAprsCharacter(character) {
|
|
const code = character.charCodeAt(0);
|
|
return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `<span class="aprs-byte">0x${code.toString(16).toUpperCase().padStart(2, "0")}</span>`;
|
|
}
|
|
function escapeAprsCharacter(character) {
|
|
if (character === "<") return "<";
|
|
if (character === ">") return ">";
|
|
if (character === "&") return "&";
|
|
if (character === '"') return """;
|
|
return character;
|
|
}
|
|
var APRS_SPRITE_COLUMNS = 16;
|
|
var APRS_SPRITE_CELL_PX = 24;
|
|
var APRS_SPRITE_FIRST_CODE = 33;
|
|
var APRS_SPRITE_LAST_CODE = 126;
|
|
function aprsSpriteOffset(code) {
|
|
if (code.length !== 1) return null;
|
|
const point = code.charCodeAt(0);
|
|
if (point < APRS_SPRITE_FIRST_CODE || point > APRS_SPRITE_LAST_CODE) return null;
|
|
const index = point - APRS_SPRITE_FIRST_CODE;
|
|
const column = index % APRS_SPRITE_COLUMNS;
|
|
const row = Math.floor(index / APRS_SPRITE_COLUMNS);
|
|
return `${String(-column * APRS_SPRITE_CELL_PX)}px ${String(-row * APRS_SPRITE_CELL_PX)}px`;
|
|
}
|
|
function aprsSymbolSprite(symbolTable, symbolCode) {
|
|
if (!symbolTable || !symbolCode) return null;
|
|
const symbolOffset = aprsSpriteOffset(symbolCode);
|
|
if (!symbolOffset) return null;
|
|
if (symbolTable === "/") {
|
|
return {
|
|
className: "aprs-symbol-primary",
|
|
backgroundPosition: symbolOffset,
|
|
label: `Primary APRS symbol ${symbolTable}${symbolCode}`
|
|
};
|
|
}
|
|
if (symbolTable === "\\") {
|
|
return {
|
|
className: "aprs-symbol-alternate",
|
|
backgroundPosition: symbolOffset,
|
|
label: `Alternate APRS symbol ${symbolTable}${symbolCode}`
|
|
};
|
|
}
|
|
const overlayOffset = aprsSpriteOffset(symbolTable);
|
|
if (!overlayOffset) return null;
|
|
return {
|
|
className: "aprs-symbol-overlaid",
|
|
backgroundPosition: `${overlayOffset}, ${symbolOffset}`,
|
|
label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}`
|
|
};
|
|
}
|
|
function renderLocalAprsSymbol(packet, escapeHtml) {
|
|
if (!packet.symbolTable || !packet.symbolCode) return "";
|
|
const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
|
|
if (sprite) {
|
|
return `<span class="aprs-symbol ${sprite.className}" role="img" style="background-position:${sprite.backgroundPosition}" title="${escapeHtml(sprite.label)}" aria-label="${escapeHtml(sprite.label)}"></span>`;
|
|
}
|
|
const symbol = escapeHtml(packet.symbolCode);
|
|
const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
|
|
return `<span class="aprs-symbol aprs-symbol-local" title="${table} APRS symbol ${symbol}">${symbol}</span>`;
|
|
}
|
|
function normalizeAprsPacket(packet, receiver) {
|
|
return {
|
|
rig_id: packet.rig_id || null,
|
|
receiver,
|
|
srcCall: packet.src_call ?? "",
|
|
destCall: packet.dest_call ?? "",
|
|
path: packet.path ?? "",
|
|
info: packet.info ?? "",
|
|
info_bytes: packet.info_bytes ?? [],
|
|
type: packet.packet_type ?? "",
|
|
crcOk: packet.crc_ok ?? false,
|
|
ts_ms: packet.ts_ms ?? null,
|
|
lat: packet.lat ?? null,
|
|
lon: packet.lon ?? null,
|
|
symbolTable: packet.symbol_table ?? null,
|
|
symbolCode: packet.symbol_code ?? null
|
|
};
|
|
}
|
|
|
|
export {
|
|
aprsPacketCategory,
|
|
aprsCategoryLabel,
|
|
aprsAgeText,
|
|
collapseAprsDuplicates,
|
|
aprsHexBytes,
|
|
renderAprsInfo,
|
|
aprsSymbolSprite,
|
|
renderLocalAprsSymbol,
|
|
normalizeAprsPacket
|
|
};
|