feat: upload to external services clublog qrz

This commit is contained in:
2026-05-28 22:52:50 +02:00
parent e82e30dd02
commit 5c004f5e2f
26 changed files with 1710 additions and 31 deletions
+40 -6
View File
@@ -160,13 +160,47 @@ export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, o
filtered.push(s);
}
filtered.sort((a, b) => b.freq_khz - a.freq_khz);
// Desired pill-CENTRE Y for each spot = its true frequency's Y.
const desired = filtered.map(
(s) => TOP_PAD + (1 - (s.freq_khz - lo) / span) * innerH,
);
// Non-overlapping label placement via isotonic regression (pool-
// adjacent-violators). We want centres c_0 ≤ c_1 ≤ … with
// c_{i+1} c_i ≥ PILL_H, minimising the squared displacement from each
// label's desired centre. Substituting q_i = c_i i·PILL_H turns the
// gap constraint into "q non-decreasing", which PAVA solves exactly in
// one pass. The win over the old greedy push-down: a tight cluster is
// centred on its mean, so its labels fan out symmetrically ABOVE and
// below the frequency (Log4OM style) instead of all spilling downward.
const n = filtered.length;
type Block = { sum: number; count: number; start: number };
const blocks: Block[] = [];
for (let i = 0; i < n; i++) {
// e_i = desired_i i·PILL_H is the target for the substituted q.
let cur: Block = { sum: desired[i] - i * PILL_H, count: 1, start: i };
while (blocks.length > 0) {
const prev = blocks[blocks.length - 1];
if (prev.sum / prev.count <= cur.sum / cur.count) break;
blocks.pop();
cur = { sum: prev.sum + cur.sum, count: prev.count + cur.count, start: prev.start };
}
blocks.push(cur);
}
const centers = new Array<number>(n);
for (const b of blocks) {
const mean = b.sum / b.count; // optimal q for the whole block
for (let i = b.start; i < b.start + b.count; i++) centers[i] = mean + i * PILL_H;
}
// Centres are non-decreasing, so centers[0] is the topmost. Shift the
// whole set down by any overflow above the band edge so the first label
// isn't clipped (preserves the ≥ PILL_H spacing).
const shift = n > 0 ? Math.max(0, TOP_PAD - (centers[0] - PILL_H / 2)) : 0;
const out: Placed[] = [];
let prevY = -Infinity;
for (const s of filtered) {
const fy = TOP_PAD + (1 - (s.freq_khz - lo) / span) * innerH;
const ly = Math.max(fy, prevY + PILL_H);
out.push({ spot: s, freqY: fy, labelY: ly });
prevY = ly;
for (let i = 0; i < n; i++) {
out.push({ spot: filtered[i], freqY: desired[i], labelY: centers[i] + shift - PILL_H / 2 });
}
const lastLabelBottom = out.length ? out[out.length - 1].labelY + PILL_H : 0;
return {