refactor: add typed frontend bootstrap
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -3041,9 +3041,13 @@
|
||||
}
|
||||
function autoInitIfVisible() {
|
||||
const panel = mapEl("tab-map");
|
||||
if (panel && panel.style.display !== "none" && typeof _initMapWhenReady === "function") {
|
||||
_initMapWhenReady();
|
||||
if (panel.style.display === "none") return;
|
||||
if (typeof L === "undefined") {
|
||||
window.setTimeout(autoInitIfVisible, 50);
|
||||
return;
|
||||
}
|
||||
initAprsMap();
|
||||
requestAnimationFrame(() => requestAnimationFrame(sizeAprsMapToViewport));
|
||||
}
|
||||
modules.map = {
|
||||
initAprsMap,
|
||||
|
||||
@@ -16,7 +16,6 @@ await rm(outputDir, { recursive: true, force: true });
|
||||
await build({
|
||||
entryPoints: {
|
||||
"api-client": path.join(sourceDir, "api", "client.ts"),
|
||||
app: path.join(sourceDir, "app.js"),
|
||||
"ui-core": path.join(sourceDir, "ui-core.ts"),
|
||||
"plugin-loader": path.join(sourceDir, "plugin-loader.ts"),
|
||||
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
|
||||
@@ -32,6 +31,19 @@ await build({
|
||||
logLevel: "info",
|
||||
});
|
||||
|
||||
await build({
|
||||
entryPoints: { app: path.join(sourceDir, "bootstrap.ts") },
|
||||
outdir: outputDir,
|
||||
bundle: true,
|
||||
format: "iife",
|
||||
platform: "browser",
|
||||
target: "es2022",
|
||||
sourcemap: false,
|
||||
legalComments: "inline",
|
||||
charset: "utf8",
|
||||
logLevel: "info",
|
||||
});
|
||||
|
||||
await build({
|
||||
entryPoints: {
|
||||
ft2: path.join(sourceDir, "plugins", "ft2.ts"),
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import {
|
||||
formatDistanceKm,
|
||||
formatTimeAgo,
|
||||
haversineKm,
|
||||
latLonToMaidenhead,
|
||||
locatorToLatLon,
|
||||
} from "./core/geo.js";
|
||||
|
||||
// --- Decoder registry (fetched from /decoders on load) ---
|
||||
/** @type {Array<{id:string,label:string,activation:string,active_modes:string[],background_decode:boolean,bookmark_selectable:boolean}>} */
|
||||
let decoderRegistry = [];
|
||||
@@ -4913,41 +4921,6 @@ window.addEventListener("resize", resizeHeaderSignalCanvas);
|
||||
// Core communicates with the map module via window.trx.modules.map.* namespace.
|
||||
|
||||
// ── Geo utilities (shared with map-core.js via window.trx) ─────────────────
|
||||
function haversineKm(lat1, lon1, lat2, lon2) {
|
||||
const R = 6371;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
function locatorToLatLon(locator) {
|
||||
const raw = String(locator || "").trim().toUpperCase();
|
||||
if (!/^[A-R]{2}\d{2}([A-X]{2})?$/.test(raw)) return null;
|
||||
let lon = -180;
|
||||
let lat = -90;
|
||||
lon += (raw.charCodeAt(0) - 65) * 20;
|
||||
lat += (raw.charCodeAt(1) - 65) * 10;
|
||||
lon += Number(raw.slice(2, 3)) * 2;
|
||||
lat += Number(raw.slice(3, 4));
|
||||
if (raw.length >= 6) {
|
||||
lon += (raw.charCodeAt(4) - 65) * (5 / 60);
|
||||
lat += (raw.charCodeAt(5) - 65) * (2.5 / 60);
|
||||
lon += 2.5 / 60;
|
||||
lat += 1.25 / 60;
|
||||
} else {
|
||||
lon += 1;
|
||||
lat += 0.5;
|
||||
}
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
function formatDistanceKm(distKm) {
|
||||
if (!Number.isFinite(distKm)) return null;
|
||||
return distKm < 1 ? `${Math.round(distKm * 1000)} m` : `${distKm.toFixed(1)} km`;
|
||||
}
|
||||
|
||||
function bookmarkDistanceText(bm) {
|
||||
if (!bm || serverLat == null || serverLon == null) return null;
|
||||
const latLon = locatorToLatLon(bm.locator);
|
||||
@@ -4989,33 +4962,6 @@ function nearestBookmarkForHz(hz, widthPx, range) {
|
||||
return best;
|
||||
}
|
||||
|
||||
function formatTimeAgo(tsMs) {
|
||||
if (!tsMs) return null;
|
||||
const secs = Math.round((Date.now() - tsMs) / 1000);
|
||||
if (secs < 60) return `${secs}s ago`;
|
||||
const mins = Math.round(secs / 60);
|
||||
if (mins < 60) return `${mins} min ago`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
const remMins = mins % 60;
|
||||
return remMins > 0 ? `${hrs}h ${remMins}min ago` : `${hrs}h ago`;
|
||||
}
|
||||
|
||||
|
||||
function latLonToMaidenhead(lat, lon) {
|
||||
const adjustedLon = lon + 180;
|
||||
const adjustedLat = lat + 90;
|
||||
const A = "A".charCodeAt(0);
|
||||
const a = "a".charCodeAt(0);
|
||||
const field1 = String.fromCharCode(A + Math.floor(adjustedLon / 20));
|
||||
const field2 = String.fromCharCode(A + Math.floor(adjustedLat / 10));
|
||||
const square1 = Math.floor((adjustedLon % 20) / 2);
|
||||
const square2 = Math.floor(adjustedLat % 10);
|
||||
const sub1 = String.fromCharCode(a + Math.floor((adjustedLon % 2) * 12));
|
||||
const sub2 = String.fromCharCode(a + Math.floor((adjustedLat % 1) * 24));
|
||||
return `${field1}${field2}${square1}${square2}${sub1}${sub2}`;
|
||||
}
|
||||
|
||||
|
||||
// --- Sub-tab navigation ---
|
||||
function _wireSubTabBar(bar) {
|
||||
if (bar._subtabWired) return;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import "./app.js";
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
export interface LatLon {
|
||||
readonly lat: number;
|
||||
readonly lon: number;
|
||||
}
|
||||
|
||||
export function haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const radiusKm = 6371;
|
||||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||
const a = Math.sin(dLat / 2) ** 2
|
||||
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
|
||||
return radiusKm * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
export function locatorToLatLon(locator: unknown): LatLon | null {
|
||||
const raw = typeof locator === "string" ? locator.trim().toUpperCase() : "";
|
||||
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(raw)) return null;
|
||||
let lon = -180 + (raw.charCodeAt(0) - 65) * 20 + Number(raw.slice(2, 3)) * 2;
|
||||
let lat = -90 + (raw.charCodeAt(1) - 65) * 10 + Number(raw.slice(3, 4));
|
||||
if (raw.length >= 6) {
|
||||
lon += (raw.charCodeAt(4) - 65) * (5 / 60) + 2.5 / 60;
|
||||
lat += (raw.charCodeAt(5) - 65) * (2.5 / 60) + 1.25 / 60;
|
||||
} else {
|
||||
lon += 1;
|
||||
lat += 0.5;
|
||||
}
|
||||
return { lat, lon };
|
||||
}
|
||||
|
||||
export function formatDistanceKm(distanceKm: number): string | null {
|
||||
if (!Number.isFinite(distanceKm)) return null;
|
||||
return distanceKm < 1 ? `${Math.round(distanceKm * 1000)} m` : `${distanceKm.toFixed(1)} km`;
|
||||
}
|
||||
|
||||
export function formatTimeAgo(timestampMs: number): string | null {
|
||||
if (!timestampMs) return null;
|
||||
const seconds = Math.round((Date.now() - timestampMs) / 1000);
|
||||
if (seconds < 60) return `${seconds}s ago`;
|
||||
const minutes = Math.round(seconds / 60);
|
||||
if (minutes < 60) return `${minutes} min ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}min ago` : `${hours}h ago`;
|
||||
}
|
||||
|
||||
export function latLonToMaidenhead(lat: number, lon: number): string {
|
||||
const adjustedLon = lon + 180;
|
||||
const adjustedLat = lat + 90;
|
||||
const upperA = "A".charCodeAt(0);
|
||||
const lowerA = "a".charCodeAt(0);
|
||||
const field1 = String.fromCharCode(upperA + Math.floor(adjustedLon / 20));
|
||||
const field2 = String.fromCharCode(upperA + Math.floor(adjustedLat / 10));
|
||||
const square1 = Math.floor((adjustedLon % 20) / 2);
|
||||
const square2 = Math.floor(adjustedLat % 10);
|
||||
const sub1 = String.fromCharCode(lowerA + Math.floor((adjustedLon % 2) * 12));
|
||||
const sub2 = String.fromCharCode(lowerA + Math.floor((adjustedLat % 1) * 24));
|
||||
return `${field1}${field2}${square1}${square2}${sub1}${sub2}`;
|
||||
}
|
||||
@@ -3610,12 +3610,15 @@ const mapWindow = window as unknown as MapWindow;
|
||||
|
||||
|
||||
// Auto-init map if the map tab is already visible (e.g. direct /map navigation).
|
||||
// Delegates to _initMapWhenReady() in app.js which handles the Leaflet load race.
|
||||
function autoInitIfVisible() {
|
||||
const panel = mapEl("tab-map");
|
||||
if (panel && panel.style.display !== "none" && typeof _initMapWhenReady === "function") {
|
||||
_initMapWhenReady();
|
||||
if (panel.style.display === "none") return;
|
||||
if (typeof L === "undefined") {
|
||||
window.setTimeout(autoInitIfVisible, 50);
|
||||
return;
|
||||
}
|
||||
initAprsMap();
|
||||
requestAnimationFrame(() => requestAnimationFrame(sizeAprsMapToViewport));
|
||||
}
|
||||
|
||||
// Register module API for core to call
|
||||
|
||||
Reference in New Issue
Block a user