mirror of
https://github.com/jakobkordez/call-tester.git
synced 2025-07-03 12:37:42 +00:00
Add zone and other info
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -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"]
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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'));
|
@ -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'
|
||||
);
|
@ -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
61038
src/assets/dxcc-tree.txt
61038
src/assets/dxcc-tree.txt
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -8,7 +8,7 @@ export type DxccEntity = {
|
||||
cont?: string;
|
||||
long?: number;
|
||||
lat?: number;
|
||||
timezone?: number;
|
||||
timez?: number;
|
||||
start?: string;
|
||||
end?: string;
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user