Add zone and other info

This commit is contained in:
Jakob Kordež
2024-06-26 18:03:04 +02:00
parent bf235ed4d9
commit 62105330db
15 changed files with 31391 additions and 30925 deletions

View File

@ -13,5 +13,5 @@
"**/*.csv",
"**/*.xml"
],
"cSpell.words": ["adif", "callsign", "clublog", "dxcc", "graphviz", "ituz", "timez"]
"cSpell.words": ["adif", "callsign", "callsigns", "clublog", "dxcc", "graphviz", "ituz", "timez"]
}

View File

@ -9,10 +9,9 @@ Data about countries can be downloaded from either [Amateur Radio Country Files
### Obtaining the AD1C country database
Download any `cty.dat` file from the [Country Files](https://www.country-files.com/) website. I recommend using the [Big CTY](https://www.country-files.com/big-cty/) file.
Instead of `cty.dat` you can also use the `cty.csv` file.
Parsing can be done with the script [`cty-dat-parser.ts`](./scripts/cty-dat-parser.ts).
Instead of `cty.dat` you can also use the `cty.csv` file. Parsing can be done with the script [`cty-csv-parser.ts`](./scripts/cty-csv-parser.ts).
Parsing of `cty.dat` or `cty.csv` can be done with the script [`cty-parser.ts`](./scripts/cty-dat-parser.ts).
`Note:` The `cty.dat` does not contain the `adif` DXCC number.

View File

@ -1,83 +0,0 @@
import path from 'path';
import { fileURLToPath } from 'url';
const filePath = process.argv[2];
if (!filePath) {
console.error('Please provide a file path as the first argument');
process.exit(1);
}
const OUT_DIR = '../src/assets/';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outDir = path.resolve(__dirname, OUT_DIR);
console.log('Outputting to', outDir);
import fs from 'fs';
import { DxccEntity } from '../src/lib/models/dxcc-entity';
// Parse the cty.dat file
const file = fs.readFileSync(filePath, 'utf8');
const entities: DxccEntity[] = [];
const prefixes: Map<string, number> = new Map();
console.log('Parsing', filePath);
for (const entity of file.split(';')) {
if (!entity.trim()) continue;
const [name, cqz, ituz, cont, lat, long, timez, primaryPrefixRaw, otherPrefixesRaw] = entity
.split(':')
.map((s) => s.trim());
const hasStar = primaryPrefixRaw.startsWith('*');
const primaryPrefix = primaryPrefixRaw.replace('*', '');
const otherPrefixes = otherPrefixesRaw.split(',').map((s) => s.trim());
const entityId = entities.length + 1;
entities.push({
id: entityId,
primaryPrefix,
name,
cqz: parseInt(cqz),
ituz: parseInt(ituz),
cont,
lat: parseFloat(lat),
long: parseFloat(long),
timezone: parseFloat(timez)
});
for (const prefix of otherPrefixes) {
const find = prefixes.get(prefix);
if (find) {
// console.error('Duplicate prefix', prefix, hasStar ? 'Overwriting' : 'Skipping');
if (hasStar) prefixes.set(prefix, entityId);
} else {
prefixes.set(prefix, entityId);
}
}
}
console.log('Parsed', prefixes.size, 'prefixes');
import { buildTrie, collapseNodes, mergeNodes, minimizeIds, validateTrie } from './trie-helper';
// Build the initial trie
const root = buildTrie([...prefixes.entries()]);
// Collapse nodes that do not cause changes
collapseNodes(root);
// Merge as many nodes as possible
mergeNodes(root);
// Validate the trie
validateTrie(root, [...prefixes.entries()]);
// Minimize node IDs
minimizeIds(root);
// Output the trie
const out = root.encodeToString();
fs.writeFileSync(path.join(outDir, 'dxcc-tree.txt'), out);
// Output the entities
fs.writeFileSync(path.join(outDir, 'dxcc-entities.json'), JSON.stringify(entities, null, '\t'));

View File

@ -12,10 +12,17 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const outDir = path.resolve(__dirname, OUT_DIR);
console.log('Outputting to', outDir);
const type = filePath.split('.').pop();
if (type !== 'dat' && type !== 'csv') {
console.error('Invalid file type', type);
process.exit(1);
}
import fs from 'fs';
import { DxccEntity } from '../src/lib/models/dxcc-entity';
import { fullBuildTrie, parseCsv, parseDat } from './parser-helper';
// Parse the cty.dat file
// Parse the cty file
const file = fs.readFileSync(filePath, 'utf8');
const entities: DxccEntity[] = [];
@ -23,28 +30,25 @@ const prefixes: Map<string, number> = new Map();
console.log('Parsing', filePath);
for (const entity of file.split(';')) {
if (!entity.trim()) continue;
const [primaryPrefixRaw, name, dxcc, cont, cqz, ituz, lat, long, timez, otherPrefixesRaw] = entity
.split(',')
.map((s) => s.trim());
const parser = type === 'dat' ? parseDat : parseCsv;
for (const entity of parser(file)) {
const { primaryPrefixRaw, name, dxcc, cont, cqz, ituz, lat, long, timez, otherPrefixes } = entity;
const hasStar = primaryPrefixRaw.startsWith('*');
const primaryPrefix = primaryPrefixRaw.replace('*', '');
const otherPrefixes = otherPrefixesRaw.split(' ').map((s) => s.trim());
const entityId = entities.length + 1;
entities.push({
id: entityId,
dxcc: parseInt(dxcc),
dxcc: dxcc,
primaryPrefix,
name,
cqz: parseInt(cqz),
ituz: parseInt(ituz),
cqz: cqz,
ituz: ituz,
cont,
lat: parseFloat(lat),
long: parseFloat(long),
timezone: parseFloat(timez)
lat: lat,
long: long,
timez: timez
});
for (const prefix of otherPrefixes) {
@ -59,22 +63,22 @@ for (const entity of file.split(';')) {
}
console.log('Parsed', prefixes.size, 'prefixes');
import { buildTrie, collapseNodes, mergeNodes, minimizeIds, validateTrie } from './trie-helper';
// Check for invalid callsigns
const callsignPattern = /^([A-Z\d]+\/)?([A-Z\d]+\d+[A-Z]+)((?:\/[A-Z\d]+)*)$/i;
// Build the initial trie
const root = buildTrie([...prefixes.entries()]);
const calls: string[] = [];
for (const callRaw of prefixes.keys()) {
if (!callRaw.startsWith('=')) continue;
const [, call] = callRaw.match(/^=?((?:[A-Z\d/])+)(.*)/)!;
if (call.match(/^VER(SION|\d{8})$/)) continue;
const result = call.match(callsignPattern);
if (!result) {
calls.push(call);
}
}
console.log('Invalid callsigns:', calls.join(', '));
// Collapse nodes that do not cause changes
collapseNodes(root);
// Merge as many nodes as possible
mergeNodes(root);
// Validate the trie
validateTrie(root, [...prefixes.entries()]);
// Minimize node IDs
minimizeIds(root);
const root = fullBuildTrie([...prefixes.entries()]);
// Output the trie
const out = root.encodeToString();
@ -82,4 +86,7 @@ fs.writeFileSync(path.join(outDir, 'dxcc-tree.txt'), out);
// Output the entities
entities.sort((a, b) => a.id - b.id);
fs.writeFileSync(path.join(outDir, 'dxcc-entities.json'), JSON.stringify(entities, null, '\t'));
fs.writeFileSync(
path.join(outDir, 'dxcc-entities.json'),
JSON.stringify(entities, null, '\t') + '\n'
);

View File

@ -1,6 +1,15 @@
import { DxccOverrides } from '../src/lib/models/dxcc-overrides';
import { TrieNode } from '../src/lib/models/trie';
export function fullBuildTrie(prefixes: [string, number][]): TrieNode {
const root = buildTrie(prefixes);
collapseNodes(root);
mergeNodes(root);
minimizeIds(root);
validateTrie(root, prefixes);
return root;
}
export function buildTrie(prefixes: [string, number][]): TrieNode {
const root = new TrieNode();
for (const [callRaw, entity] of prefixes) {
@ -107,3 +116,72 @@ export function minimizeIds(root: TrieNode): void {
node.id = i++;
}
}
interface IEntity {
name: string;
dxcc?: number;
primaryPrefixRaw: string;
cont: string;
cqz: number;
ituz: number;
lat: number;
long: number;
timez: number;
otherPrefixes: string[];
}
export const parseCsv = (file: string): IEntity[] =>
file
.trim()
.split(';')
.map<IEntity | null>((entity) => {
entity = entity.trim();
if (!entity) return null;
const [primaryPrefixRaw, name, dxcc, cont, cqz, ituz, lat, long, timez, otherPrefixesRaw] =
entity.split(',').map((s) => s.trim());
const otherPrefixes = otherPrefixesRaw.split(' ').map((s) => s.trim());
return {
name,
dxcc: parseInt(dxcc),
primaryPrefixRaw,
cont,
cqz: parseInt(cqz),
ituz: parseInt(ituz),
lat: parseFloat(lat),
long: parseFloat(long),
timez: parseFloat(timez),
otherPrefixes
};
})
.filter(Boolean) as IEntity[];
export const parseDat = (file: string): IEntity[] =>
file
.trim()
.split(';')
.map<IEntity | null>((entity) => {
entity = entity.trim();
if (!entity) return null;
const [name, cqz, ituz, cont, lat, long, timez, primaryPrefixRaw, otherPrefixesRaw] = entity
.split(':')
.map((s) => s.trim());
const otherPrefixes = otherPrefixesRaw.split(',').map((s) => s.trim());
return {
name,
primaryPrefixRaw,
cont,
cqz: parseInt(cqz),
ituz: parseInt(ituz),
lat: parseFloat(lat),
long: parseFloat(long),
timez: parseFloat(timez),
otherPrefixes
};
})
.filter(Boolean) as IEntity[];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,7 @@
}}
on:input={(e) => {
const t = e.currentTarget;
if (/^[A-Z\d\/]*$/i.test(t.value)) {
if (/^[A-Z\d/]*$/i.test(t.value)) {
inputText = t.value.toUpperCase();
} else {
// Users enter the not supported characters

View File

@ -2,9 +2,9 @@ import { describe, expect, test } from 'vitest';
import { parseCallsign } from './callsign';
import { dxccEntities } from './dxcc-util';
const s5ID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5')?.id;
const svID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV')?.id;
const svaID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV/a')?.id;
const s5 = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5');
const sv = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV');
const sva = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV/a');
describe('parseCallsign', () => {
test('S52KJ', () => {
@ -14,9 +14,9 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe(null);
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(null);
expect(data?.secondarySuffixes).toEqual([]);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(s5);
});
test('s52kj', () => {
@ -26,9 +26,9 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe(null);
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(null);
expect(data?.secondarySuffixes).toEqual([]);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(s5);
});
test('S52KJ/P', () => {
@ -38,9 +38,9 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('P');
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(null);
expect(data?.secondarySuffixes).toEqual(['P']);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(s5);
});
test('SV/S52KJ', () => {
@ -50,9 +50,9 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe(null);
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(svID);
expect(data?.secondarySuffixes).toEqual([]);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(sv);
});
test('SV/S52KJ/P', () => {
@ -62,9 +62,9 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('P');
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(svID);
expect(data?.secondarySuffixes).toEqual(['P']);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(sv);
});
test('S52KJ/A', () => {
@ -74,21 +74,33 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('A');
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(null);
expect(data?.secondarySuffixes).toEqual(['A']);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(s5);
});
test('S52KJ/A/P', () => {
const data = parseCallsign('S52KJ/A/P');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffixes).toEqual(['A', 'P']);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(s5);
});
test('SV2RSG/A', () => {
const data = parseCallsign('SV2RSG/A');
expect(data).not.toBe(null);
expect(data?.secondaryPrefix).toBe(null);
expect(data?.basePrefix).toBe('SV2RSG/A');
expect(data?.baseSuffix).toBe('');
expect(data?.basePrefix).toBe('SV');
expect(data?.baseSuffix).toBe('2RSG');
expect(data?.base).toBe('SV2RSG');
expect(data?.secondarySuffix).toBe('A');
expect(data?.baseDxcc).toBe(svaID);
expect(data?.prefixDxcc).toBe(null);
expect(data?.secondarySuffixes).toEqual(['A']);
expect(data?.baseDxcc).toEqual(sv);
expect(data?.fullDxcc).toEqual(sva);
});
test('SV/S52KJ/A', () => {
@ -98,8 +110,8 @@ describe('parseCallsign', () => {
expect(data?.basePrefix).toBe('S5');
expect(data?.baseSuffix).toBe('2KJ');
expect(data?.base).toBe('S52KJ');
expect(data?.secondarySuffix).toBe('A');
expect(data?.baseDxcc).toBe(s5ID);
expect(data?.prefixDxcc).toBe(svID);
expect(data?.secondarySuffixes).toEqual(['A']);
expect(data?.baseDxcc).toEqual(s5);
expect(data?.fullDxcc).toEqual(sv);
});
});

View File

@ -1,15 +1,16 @@
import { findDxcc } from './dxcc-util';
import type { DxccEntity } from './models/dxcc-entity';
export const callsignPattern = /^([A-Z\d]+\/)?([A-Z\d]+\d+[A-Z]+)(\/[A-Z\d]+)?$/i;
export const callsignPattern = /^([A-Z\d]+\/)?([A-Z\d]+\d+[A-Z]+)((?:\/[A-Z\d]+)*)$/i;
type CallsignData = {
secondaryPrefix: string | null;
basePrefix: string | null;
baseSuffix: string | null;
base: string;
secondarySuffix: string | null;
baseDxcc: number | null;
prefixDxcc: number | null;
secondarySuffixes: string[];
baseDxcc: DxccEntity | null;
fullDxcc: DxccEntity | null;
};
export function parseCallsign(callsign: string): CallsignData | null {
@ -21,27 +22,26 @@ export function parseCallsign(callsign: string): CallsignData | null {
const secondaryPrefix = match[1]?.slice(0, -1) ?? null;
const base = match[2];
const secondarySuffix = match[3]?.slice(1) ?? null;
const secondarySuffixes = match[3]?.slice(1).split('/').filter(Boolean) ?? [];
const baseWithSuffix = base + (secondarySuffix ? '/' + secondarySuffix : '');
const baseDxcc = findDxcc(baseWithSuffix);
const prefixDxcc = secondaryPrefix ? findDxcc(callsign) : null;
const baseDxccResult = findDxcc(base);
const fullDxccResult = findDxcc(callsign);
const basePrefix = baseDxcc ? baseWithSuffix.slice(0, baseDxcc.matchLength) : null;
const baseSuffix = baseDxcc ? baseWithSuffix.slice(baseDxcc.matchLength).split('/')[0] : null;
const basePrefix = baseDxccResult ? base.slice(0, baseDxccResult.matchLength) : null;
const baseSuffix = baseDxccResult ? base.slice(baseDxccResult.matchLength).split('/')[0] : null;
return {
secondaryPrefix,
basePrefix,
baseSuffix,
base,
secondarySuffix,
baseDxcc: baseDxcc?.entityId || null,
prefixDxcc: prefixDxcc?.entityId || null
secondarySuffixes,
baseDxcc: baseDxccResult?.entity ?? null,
fullDxcc: fullDxccResult?.entity ?? null
};
}
export function getSecondarySuffixDescription(suffix: string): string | null {
export function getSecondarySuffixDescription(suffix: string): string {
switch (suffix) {
case 'P':
return 'Portable';
@ -51,7 +51,9 @@ export function getSecondarySuffixDescription(suffix: string): string | null {
return 'Aeronautical mobile';
case 'MM':
return 'Maritime mobile';
case 'QRP':
return 'Low power';
default:
return null;
return 'Alternative location';
}
}

View File

@ -8,96 +8,116 @@ describe('dxccTree', () => {
});
describe('findDxcc', () => {
const s5ID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5')?.id;
const svID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV')?.id;
const svaID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV/a')?.id;
const sv9ID = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV9')?.id;
const ituID = [...dxccEntities.values()].find((e) => e.primaryPrefix === '4U1I')?.id;
const s5 = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'S5');
const sv = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV');
const sva = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV/a');
const sv9 = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SV9');
const itu = [...dxccEntities.values()].find((e) => e.primaryPrefix === '4U1I');
const sp = [...dxccEntities.values()].find((e) => e.primaryPrefix === 'SP');
test('S52KJ', () => {
const result = findDxcc('S52KJ');
expect(result?.entityId).toBe(s5ID);
expect(result?.entity).toEqual(s5);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('s52kj', () => {
const result = findDxcc('s52kj');
expect(result?.entityId).toBe(s5ID);
expect(result?.entity).toEqual(s5);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('S52KJ/P', () => {
const result = findDxcc('S52KJ/P');
expect(result?.entityId).toBe(s5ID);
expect(result?.entity).toEqual(s5);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV2AAA', () => {
const result = findDxcc('SV2AAA');
expect(result?.entityId).toBe(svID);
expect(result?.entity).toEqual(sv);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV2RSG/A', () => {
const result = findDxcc('SV2RSG/A');
expect(result?.entityId).toBe(svaID);
expect(result?.entity).toEqual(sva);
expect(result?.matchLength).toBe(8);
expect(result?.isExact).toBe(true);
});
test('4U1ITU', () => {
const result = findDxcc('4U1ITU');
expect(result?.entityId).toBe(ituID);
expect(result?.entity).toEqual(itu);
expect(result?.matchLength).toBe(6);
expect(result?.isExact).toBe(true);
});
test('SV2AAA/AP', () => {
const result = findDxcc('SV2AAA/AP');
expect(result?.entityId).toBe(svID);
expect(result?.entity).toEqual(sv);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV2AAA/P', () => {
const result = findDxcc('SV2AAA/P');
expect(result?.entityId).toBe(svID);
expect(result?.entity).toEqual(sv);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV/S52KJ', () => {
const result = findDxcc('SV/S52KJ');
expect(result?.entityId).toBe(svID);
expect(result?.entity).toEqual(sv);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV/S52KJ/A', () => {
const result = findDxcc('SV/S52KJ/A');
expect(result?.entityId).toBe(svID);
expect(result?.entity).toEqual(sv);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV/S52KJ/P', () => {
const result = findDxcc('SV/S52KJ/P');
expect(result?.entityId).toBe(svID);
expect(result?.entity).toEqual(sv);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SV9/S52KJ/A', () => {
const result = findDxcc('SV9/S52KJ/A');
expect(result?.entityId).toBe(sv9ID);
expect(result?.entity).toEqual(sv9);
expect(result?.matchLength).toBe(3);
expect(result?.isExact).toBe(false);
});
test('S', () => {
const result = findDxcc('S');
expect(result).toBe(null);
});
test('SP', () => {
const result = findDxcc('SP');
expect(result?.entity).toEqual(sp);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('SP1', () => {
const result = findDxcc('SP1');
expect(result?.entity).toEqual(sp);
expect(result?.matchLength).toBe(2);
expect(result?.isExact).toBe(false);
});
test('empty string', () => {
const result = findDxcc('');
expect(result).toBe(null);

View File

@ -2,19 +2,55 @@ import dxccTreeFile from '../assets/dxcc-tree.txt?raw';
import dxccEntitiesFile from '../assets/dxcc-entities.json';
import { TrieNode } from './models/trie';
import type { DxccEntity } from './models/dxcc-entity';
import { DxccOverrides } from './models/dxcc-overrides';
export const dxccTree = TrieNode.decodeFromString(dxccTreeFile);
interface DxccResult {
entityId: number;
export interface DxccResult {
entity: DxccEntity;
matchLength: number;
isExact: boolean;
}
export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): DxccResult | null {
const rawResult = findRawDxcc(prefix, startingNode);
if (!rawResult) return null;
const entity = dxccEntities.get(rawResult.entityId);
if (!entity) return null;
const entityWithOverrides: DxccEntity = {
...entity,
cqz: rawResult.dxccOverrides.cqz ?? entity.cqz,
ituz: rawResult.dxccOverrides.ituz ?? entity.ituz,
cont: rawResult.dxccOverrides.cont ?? entity.cont,
lat: rawResult.dxccOverrides.lat ?? entity.lat,
long: rawResult.dxccOverrides.long ?? entity.long,
timez: rawResult.dxccOverrides.timez ?? entity.timez
};
return {
entity: entityWithOverrides,
matchLength: rawResult.matchLength,
isExact: rawResult.isExact
};
}
export interface RawDxccResult {
entityId: number;
dxccOverrides: DxccOverrides;
matchLength: number;
isExact: boolean;
}
export function findRawDxcc(
prefix: string,
startingNode: TrieNode = dxccTree
): RawDxccResult | null {
prefix = prefix.toUpperCase();
let node = startingNode;
let entityId: number | null = null;
let dxccOverrides = new DxccOverrides();
let tempPrefixLength = 0;
let matchLength = 0;
@ -31,19 +67,26 @@ export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): Dxc
entityId = node.entity;
matchLength = tempPrefixLength;
}
if (node.overrides.toString()) {
dxccOverrides = dxccOverrides.merge(node.overrides);
// TODO Debate whether to set matchLength here
matchLength = tempPrefixLength;
}
}
if (!prefix && node?.children.has('')) {
node = node.children.get('')!;
const exact = node.children.get('')!;
return {
entityId: node.entity!,
entityId: exact.entity ?? entityId!,
dxccOverrides: dxccOverrides.merge(exact.overrides),
// matchLength: exact.entity ? tempPrefixLength : matchLength,
matchLength: tempPrefixLength,
isExact: true
};
}
if (!entityId) return null;
return { entityId, matchLength, isExact: false };
return { entityId, dxccOverrides, matchLength, isExact: false };
}
export const dxccEntities: Map<number, DxccEntity> = new Map(

View File

@ -8,7 +8,7 @@ export type DxccEntity = {
cont?: string;
long?: number;
lat?: number;
timezone?: number;
timez?: number;
start?: string;
end?: string;
};

View File

@ -92,18 +92,29 @@ export class TrieNode {
currentEntity: number | null = null,
currentOverrides: DxccOverrides = new DxccOverrides()
): boolean {
if (currentEntity && currentEntity === this.entity) this.entity = null;
if (currentOverrides.cqz && currentOverrides.cqz === this.overrides.cqz)
this.overrides.cqz = undefined;
if (currentOverrides.ituz && currentOverrides.ituz === this.overrides.ituz)
this.overrides.ituz = undefined;
if (currentOverrides.cont && currentOverrides.cont === this.overrides.cont)
this.overrides.cont = undefined;
if (currentOverrides.lat && currentOverrides.lat === this.overrides.lat)
this.overrides.lat = undefined;
if (currentOverrides.long && currentOverrides.long === this.overrides.long)
this.overrides.long = undefined;
if (currentOverrides.timez && currentOverrides.timez === this.overrides.timez)
this.overrides.timez = undefined;
const newEntity = this.entity ?? currentEntity;
const newOverrides = currentOverrides.merge(this.overrides);
for (const [k, child] of this.children.entries()) {
const newEntity = this.entity ?? currentEntity;
const newOverrides = currentOverrides.merge(this.overrides);
if (child.collapseNodes(newEntity, newOverrides)) {
this.children.delete(k);
}
}
return (
this.children.size == 0 &&
(!this.entity || this.entity === currentEntity) &&
(!this.overrides || this.overrides.isSubsetOf(currentOverrides))
);
return this.children.size == 0 && !this.entity && !this.overrides.toString();
}
/**

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { getSecondarySuffixDescription, parseCallsign } from '$lib/callsign';
import { dxccEntities, findDxcc } from '$lib/dxcc-util';
import { findDxcc } from '$lib/dxcc-util';
import CallsignInput from '../components/callsign-input.svelte';
let query = new URLSearchParams($page.url.searchParams.toString());
@ -24,23 +24,25 @@
const prefixClass = 'text-blue-400';
const suffixClass = 'text-green-400';
if (rawDxcc?.matchLength === callsign.length && rawDxcc.isExact) {
return `<span class="text-amber-400">${callsign}</span>`;
}
if (!callsignData) {
const dxcc = findDxcc(callsign);
if (!dxcc) return callsign;
return `<span class="text-cyan-300">${callsign.slice(0, dxcc.matchLength)}</span>${callsign.slice(dxcc.matchLength)}`;
}
if (rawDxcc?.isExact) {
return `<span class="text-amber-400">${callsign}</span>`;
}
const { base, basePrefix, baseSuffix, secondaryPrefix, secondarySuffix } = callsignData;
const { base, basePrefix, baseSuffix, secondaryPrefix, secondarySuffixes } = callsignData;
// TODO Check if base and prefix same dxcc
return [
secondaryPrefix ? `<span class="${prefixClass}">${secondaryPrefix}/</span>` : '',
basePrefix ? `<span class="${baseClass}">${basePrefix}</span>${baseSuffix}` : base,
secondarySuffix ? `<span class="${suffixClass}">/${secondarySuffix}</span>` : ''
secondarySuffixes.length
? `<span class="${suffixClass}">/${secondarySuffixes.join('/')}</span>`
: ''
].join('');
}
</script>
@ -53,51 +55,98 @@
<CallsignInput bind:inputText={callsign} generateStyledText={styleText} />
</div>
{#if rawDxcc?.isExact}
<div class="data-box full">
<h2>Full match</h2>
<div class="font-mono text-2xl font-medium">{callsign}</div>
<div>{dxccEntities.get(rawDxcc.entityId)?.name ?? '?'}</div>
{#if rawDxcc?.matchLength === callsign.length && rawDxcc.isExact}
<div class="data-box full cols">
<div>
<div class="font-mono text-2xl font-medium">{callsign}</div>
<div>{rawDxcc.entity?.name ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>CQ Zone: {rawDxcc.entity?.cqz ?? '?'}</div>
<div>ITU Zone: {rawDxcc.entity?.ituz ?? '?'}</div>
<div>Continent: {rawDxcc.entity?.cont ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>Lat: {rawDxcc.entity?.lat ?? '?'}</div>
<div>Long: {rawDxcc.entity?.long ?? '?'}</div>
<div>TZ Offset: {rawDxcc.entity?.timez ?? '?'}</div>
</div>
</div>
{:else if callsignData}
<div class="flex flex-col gap-4 md:flex-row">
{#if callsignData.prefixDxcc}
<div class="data-box prefix">
<h2>Secondary prefix</h2>
<div class="font-mono text-2xl font-medium">{callsignData.secondaryPrefix}</div>
<div>{dxccEntities.get(callsignData.prefixDxcc)?.name ?? '?'}</div>
<div class="flex flex-col gap-4">
{#if callsignData.secondaryPrefix}
<div class="data-box prefix cols">
<div>
<div class="font-mono text-2xl font-medium">{callsignData.secondaryPrefix}</div>
<div>{callsignData.fullDxcc?.name ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>CQ Zone: {callsignData.fullDxcc?.cqz ?? '?'}</div>
<div>ITU Zone: {callsignData.fullDxcc?.ituz ?? '?'}</div>
<div>Continent: {callsignData.fullDxcc?.cont ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>Lat: {callsignData.fullDxcc?.lat ?? '?'}</div>
<div>Long: {callsignData.fullDxcc?.long ?? '?'}</div>
<div>TZ Offset: {callsignData.fullDxcc?.timez ?? '?'}</div>
</div>
</div>
{/if}
{#if callsignData.baseDxcc}
<div class="data-box base">
<h2>Prefix</h2>
<div class="font-mono text-2xl font-medium">{callsignData.basePrefix}</div>
<div>{dxccEntities.get(callsignData.baseDxcc)?.name ?? '?'}</div>
<div class="data-box base cols">
<div>
<div class="font-mono text-2xl font-medium">{callsignData.basePrefix}</div>
<div>{callsignData.baseDxcc.name ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>CQ Zone: {callsignData.baseDxcc.cqz ?? '?'}</div>
<div>ITU Zone: {callsignData.baseDxcc.ituz ?? '?'}</div>
<div>Continent: {callsignData.baseDxcc.cont ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>Lat: {callsignData.baseDxcc.lat ?? '?'}</div>
<div>Long: {callsignData.baseDxcc.long ?? '?'}</div>
<div>TZ Offset: {callsignData.baseDxcc.timez ?? '?'}</div>
</div>
</div>
{/if}
{#if callsignData.secondarySuffix}
<div class="data-box suffix">
<h2>Secondary suffix</h2>
<div class="font-mono text-2xl font-medium">{callsignData.secondarySuffix}</div>
<div>{getSecondarySuffixDescription(callsignData.secondarySuffix) ?? ''}</div>
{#if callsignData.secondarySuffixes.length > 0}
<div class="flex flex-wrap gap-4">
{#each callsignData.secondarySuffixes as suffix}
<div class="data-box suffix min-w-full flex-grow sm:min-w-[30%]">
<div class="font-mono text-2xl font-medium">{suffix}</div>
<div>{getSecondarySuffixDescription(suffix)}</div>
</div>
{/each}
</div>
{/if}
</div>
{:else if rawDxcc}
<div class="data-box base">
<h2>Prefix</h2>
<div class="font-mono text-2xl font-medium">{callsign.slice(0, rawDxcc.matchLength)}</div>
<div>{dxccEntities.get(rawDxcc.entityId)?.name ?? '?'}</div>
<div class="data-box base cols">
<div>
<div class="font-mono text-2xl font-medium">{callsign.slice(0, rawDxcc.matchLength)}</div>
<div>{rawDxcc.entity?.name ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>CQ Zone: {rawDxcc.entity?.cqz ?? '?'}</div>
<div>ITU Zone: {rawDxcc.entity?.ituz ?? '?'}</div>
<div>Continent: {rawDxcc.entity?.cont ?? '?'}</div>
</div>
<div class="my-auto text-sm">
<div>Lat: {rawDxcc.entity?.lat ?? '?'}</div>
<div>Long: {rawDxcc.entity?.long ?? '?'}</div>
<div>TZ Offset: {rawDxcc.entity?.timez ?? '?'}</div>
</div>
</div>
{/if}
</div>
<style lang="postcss">
.data-box {
@apply mx-auto w-full max-w-[50%] flex-1 rounded-xl p-4 text-center;
@apply mx-auto w-full flex-1 rounded-xl p-4 text-center;
}
.data-box > h2 {
@apply text-sm;
.data-box.cols {
@apply grid grid-cols-3;
}
.data-box.full {
@apply bg-amber-600/40;