// IARU band plans, by region. // // The band maps used to carry one hard-coded table — Region 1's — so a US or // Australian operator saw 40 m stop at 7200 while stations sat spotted at 7250, // and the SSB wash started 25 kHz early. The region is a station-level fact the // operator states once (Settings → General); everything drawing a band edge or // a mode segment reads it from here. // // The tables are deliberately coarse: a band map's wash is CONTEXT, not a // regulatory chart. Only the segments that differ between regions in ways an // operator notices are split out; national fine print (US licence classes, // beacon slots) does not belong in a background tint. import { writeUiPref } from '@/lib/uiPref'; export type IaruRegion = 1 | 2 | 3; export const KEY_IARU_REGION = 'opslog.iaruRegion'; export function iaruRegion(): IaruRegion { try { const v = localStorage.getItem(KEY_IARU_REGION); return v === '2' ? 2 : v === '3' ? 3 : 1; } catch { return 1; } } export function setIaruRegion(r: IaruRegion): void { writeUiPref(KEY_IARU_REGION, String(r)); listeners.forEach((l) => l()); } // The band maps capture the plan inside their render, so a change must reach // them the way the distance unit reaches the grids. const listeners = new Set<() => void>(); export function subscribeIaruRegion(fn: () => void): () => void { listeners.add(fn); return () => { listeners.delete(fn); }; } export type SegMode = 'cw' | 'digi' | 'phone'; type Seg = [number, number, SegMode]; interface Plan { ranges: Record; segments: Record; } // Region 1 — Europe / Africa / Middle East / northern Asia. The baseline the // other two override. const R1: Plan = { ranges: { '160m': [1800, 2000], '80m': [3500, 3800], '60m': [5350, 5450], '40m': [7000, 7200], '30m': [10100, 10150], '20m': [14000, 14350], '17m': [18068, 18168], '15m': [21000, 21450], '12m': [24890, 24990], '10m': [28000, 29700], '6m': [50000, 50500], '4m': [70000, 70500], '2m': [144000, 146000], '70cm': [430000, 440000], }, segments: { '160m': [[1800, 1838, 'cw'], [1838, 1840, 'digi'], [1840, 2000, 'phone']], '80m': [[3500, 3580, 'cw'], [3580, 3600, 'digi'], [3600, 3800, 'phone']], '60m': [[5350, 5450, 'phone']], '40m': [[7000, 7040, 'cw'], [7040, 7100, 'digi'], [7100, 7200, 'phone']], '30m': [[10100, 10130, 'cw'], [10130, 10150, 'digi']], '20m': [[14000, 14070, 'cw'], [14070, 14100, 'digi'], [14100, 14350, 'phone']], '17m': [[18068, 18095, 'cw'], [18095, 18110, 'digi'], [18110, 18168, 'phone']], '15m': [[21000, 21070, 'cw'], [21070, 21150, 'digi'], [21150, 21450, 'phone']], '12m': [[24890, 24915, 'cw'], [24915, 24940, 'digi'], [24940, 24990, 'phone']], '10m': [[28000, 28070, 'cw'], [28070, 28300, 'digi'], [28300, 29700, 'phone']], '6m': [[50000, 50100, 'cw'], [50100, 50500, 'phone']], }, }; // Region 2 — the Americas. 80 m runs to 4000 and 40 m to 7300, with phone from // 7125 (the whole point of asking the region: a KP4 ragchew on 7250 must not // hang past the top of the map). const R2: Plan = { ranges: { ...R1.ranges, '80m': [3500, 4000], '40m': [7000, 7300], '6m': [50000, 54000], '2m': [144000, 148000], '70cm': [420000, 450000], }, segments: { ...R1.segments, '80m': [[3500, 3570, 'cw'], [3570, 3600, 'digi'], [3600, 4000, 'phone']], '40m': [[7000, 7040, 'cw'], [7040, 7125, 'digi'], [7125, 7300, 'phone']], '6m': [[50000, 50100, 'cw'], [50100, 54000, 'phone']], }, }; // Region 3 — Asia-Pacific. 80 m to 3900; 40 m as Region 1's shape. const R3: Plan = { ranges: { ...R1.ranges, '80m': [3500, 3900], '6m': [50000, 54000], '2m': [144000, 148000], '70cm': [430000, 450000], }, segments: { ...R1.segments, '80m': [[3500, 3535, 'cw'], [3535, 3600, 'digi'], [3600, 3900, 'phone']], '6m': [[50000, 50100, 'cw'], [50100, 54000, 'phone']], }, }; function plan(): Plan { const r = iaruRegion(); return r === 2 ? R2 : r === 3 ? R3 : R1; } export function bandRange(band: string): [number, number] | undefined { return plan().ranges[band]; } export function bandSegments(band: string): Seg[] { return plan().segments[band] ?? []; } // bandForMHz maps a dial frequency (MHz) to its ADIF band, or '' when it falls // outside every known allocation. // // Lives here rather than in a panel because more than one place has to answer // the same question the same way: the entry form retunes on a typed frequency, // and the QSO editor has to keep band and frequency agreeing when one of them // is corrected. Kept in step with BandFromHz on the Go side. export function bandForMHz(mhz: number): string { if (!mhz || isNaN(mhz)) return ''; const plan: [number, number, string][] = [ [1.8, 2.0, '160m'], [3.5, 4.0, '80m'], [5.06, 5.45, '60m'], [7.0, 7.3, '40m'], [10.1, 10.15, '30m'], [14.0, 14.35, '20m'], [18.068, 18.168, '17m'], [21.0, 21.45, '15m'], [24.89, 24.99, '12m'], [28.0, 29.7, '10m'], [50, 54, '6m'], [70, 71, '4m'], [144, 148, '2m'], [222, 225, '1.25m'], [420, 450, '70cm'], [902, 928, '33cm'], [1240, 1300, '23cm'], // Microwave, ADIF 3.1.7 ranges. [2300, 2450, '13cm'], [3300, 3500, '9cm'], [5650, 5925, '6cm'], [10000, 10500, '3cm'], [24000, 24250, '1.25cm'], [47000, 47200, '6mm'], [75500, 81000, '4mm'], [119980, 123000, '2.5mm'], [134000, 149000, '2mm'], [241000, 250000, '1mm'], ]; for (const [lo, hi, b] of plan) if (mhz >= lo && mhz <= hi) return b; return ''; }