feat: added Worked All Italian Provinces award and FFMA

This commit is contained in:
2026-07-14 00:03:10 +02:00
parent 08f4b61523
commit 7f95a71426
6 changed files with 1046 additions and 37 deletions
+16 -9
View File
@@ -1180,22 +1180,29 @@ export default function App() {
// Effective antenna heading(s): the rotor azimuth, transformed by the
// Ultrabeam pattern when one is active — reversed (180°) points opposite,
// bidirectional radiates both ways, normal is the heading itself.
// Headings are rounded to whole degrees: a rotor reports a jittering float, and
// a fraction of a degree changes nothing on a compass or a beam lobe — but it
// does invalidate every memo downstream and force the map to rebuild its whole
// overlay on each reading.
const rotorAz = useMemo<number | null>(() => (
rotatorHeading.enabled && rotatorHeading.ok
? Math.round((((rotatorHeading.azimuth % 360) + 360) % 360)) % 360
: null
), [rotatorHeading.enabled, rotatorHeading.ok, rotatorHeading.azimuth]);
const beamHeadings = useMemo<number[]>(() => {
if (!(rotatorHeading.enabled && rotatorHeading.ok)) return [];
const base = ((rotatorHeading.azimuth % 360) + 360) % 360;
if (rotorAz == null) return [];
if (ubStatus.enabled && ubStatus.connected) {
if (ubStatus.direction === 1) return [(base + 180) % 360];
if (ubStatus.direction === 2) return [base, (base + 180) % 360];
if (ubStatus.direction === 1) return [(rotorAz + 180) % 360];
if (ubStatus.direction === 2) return [rotorAz, (rotorAz + 180) % 360];
}
return [base];
}, [rotatorHeading.enabled, rotatorHeading.ok, rotatorHeading.azimuth, ubStatus.enabled, ubStatus.connected, ubStatus.direction]);
return [rotorAz];
}, [rotorAz, ubStatus.enabled, ubStatus.connected, ubStatus.direction]);
// Mechanical boom (rotor) heading + Ultrabeam pattern — so the compass/map can
// show where the antenna physically points (boom) vs where it radiates when
// the Ultrabeam is reversed/bidirectional.
const boomHeading = useMemo<number | null>(() => (
rotatorHeading.enabled && rotatorHeading.ok ? ((rotatorHeading.azimuth % 360) + 360) % 360 : null
), [rotatorHeading.enabled, rotatorHeading.ok, rotatorHeading.azimuth]);
const boomHeading = rotorAz;
const ubPattern = useMemo<'normal' | 'reverse' | 'bi' | null>(() => {
if (!(ubStatus.enabled && ubStatus.connected)) return null;
return ubStatus.direction === 1 ? 'reverse' : ubStatus.direction === 2 ? 'bi' : 'normal';
+3 -2
View File
@@ -742,10 +742,11 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
<button className="text-muted-foreground hover:text-destructive" onClick={() => delRef(sel.code)}><Trash2 className="size-4" /></button>
</div>
<Field2 label={t('awed.description')}><Input className="h-8" value={sel.name ?? ''} onChange={(e) => patchSel({ name: e.target.value })} /></Field2>
<div className="grid grid-cols-2 gap-3">
{/* One per row: side by side, each half spends 120px of its width on
the label column and the input is left too narrow to read a group
name ("Basilicata" → "Basili"). */}
<Field2 label={t('awed.group')}><Input className="h-8" value={sel.group ?? ''} onChange={(e) => patchSel({ group: e.target.value })} /></Field2>
<Field2 label={t('awed.subgroup')}><Input className="h-8" value={sel.subgrp ?? ''} onChange={(e) => patchSel({ subgrp: e.target.value })} /></Field2>
</div>
<Field2 label="DXCC"><Input type="number" className="h-8 w-32 font-mono" value={sel.dxcc || ''} onChange={(e) => patchSel({ dxcc: parseInt(e.target.value, 10) || 0 })} /></Field2>
<Field2 label={t('awed.patternRegex')}><Input className="h-8 font-mono text-xs" value={sel.pattern ?? ''} onChange={(e) => patchSel({ pattern: e.target.value })} placeholder={t('awed.perRefRegex')} /></Field2>
<div className="grid grid-cols-3 gap-3">
+39 -13
View File
@@ -64,6 +64,24 @@ function loadBasemap(): BasemapKey {
return v === 'voyager' || v === 'street' || v === 'satellite' ? v : 'light';
}
// addBasemap (re)installs the imagery layer and, for satellite, its transparent
// place-name overlay. updateWhenIdle/keepBuffer keep the number of live tiles
// down: satellite loads TWO tile layers, so its tile count — and the composited
// layers WebView2 has to hold — is double every other basemap's.
function addBasemap(
m: L.Map,
key: BasemapKey,
base: React.MutableRefObject<L.TileLayer | null>,
labels: React.MutableRefObject<L.TileLayer | null>,
) {
if (base.current) { m.removeLayer(base.current); base.current = null; }
if (labels.current) { m.removeLayer(labels.current); labels.current = null; }
const bm = BASEMAPS[key];
const opts: L.TileLayerOptions = { maxZoom: 19, updateWhenIdle: true, updateWhenZooming: false, keepBuffer: 1 };
base.current = L.tileLayer(bm.url, { ...opts, attribution: bm.attr, subdomains: bm.subdomains ?? 'abc' }).addTo(m);
if (bm.labelsUrl) labels.current = L.tileLayer(bm.labelsUrl, opts).addTo(m);
}
function dot(color: string): L.DivIcon {
return L.divIcon({
className: '',
@@ -103,11 +121,14 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
// One-time map creation.
useEffect(() => {
if (worldRef.current && !worldMap.current) {
const m = L.map(worldRef.current, { zoomControl: true, attributionControl: true, worldCopyJump: true })
// preferCanvas: the beam lobe is a dense FAN of translucent radials — up to
// ~120 thick strokes with a bidirectional Ultrabeam. As SVG that is ~120
// composited paths re-rasterised on every pan, zoom and redraw, which is
// enough to blow WebView2's raster budget and leave the window painting in
// patches. On canvas it is a single layer.
const m = L.map(worldRef.current, { zoomControl: true, attributionControl: true, worldCopyJump: true, preferCanvas: true })
.setView([20, 0], 1);
const bm = BASEMAPS[basemap];
baseLayer.current = L.tileLayer(bm.url, { attribution: bm.attr, subdomains: bm.subdomains ?? 'abc', maxZoom: 19 }).addTo(m);
if (bm.labelsUrl) labelsLayer.current = L.tileLayer(bm.labelsUrl, { maxZoom: 19 }).addTo(m);
addBasemap(m, basemap, baseLayer, labelsLayer);
worldOverlay.current = L.layerGroup().addTo(m);
worldMap.current = m;
const sv = loadMapView();
@@ -115,7 +136,12 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
m.on('moveend', () => { if (!autoZoomRef.current) saveMapView(m); });
}
const t = window.setTimeout(() => { worldMap.current?.invalidateSize(); }, 80);
return () => window.clearTimeout(t);
// Resizing the pane is the ONLY thing that needs invalidateSize. Calling it on
// every overlay redraw (as before) forced a full repaint of the map each time
// the rotor moved a degree.
const ro = new ResizeObserver(() => worldMap.current?.invalidateSize());
if (worldRef.current) ro.observe(worldRef.current);
return () => { window.clearTimeout(t); ro.disconnect(); };
}, []);
// Swap the basemap (and its optional place-name overlay) when the operator
@@ -123,12 +149,7 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
// overlayPane, always above any tile layer, so nothing to re-stack there.
useEffect(() => {
const m = worldMap.current;
if (!m) return;
if (baseLayer.current) { m.removeLayer(baseLayer.current); baseLayer.current = null; }
if (labelsLayer.current) { m.removeLayer(labelsLayer.current); labelsLayer.current = null; }
const bm = BASEMAPS[basemap];
baseLayer.current = L.tileLayer(bm.url, { attribution: bm.attr, subdomains: bm.subdomains ?? 'abc', maxZoom: 19 }).addTo(m);
if (bm.labelsUrl) labelsLayer.current = L.tileLayer(bm.labelsUrl, { maxZoom: 19 }).addTo(m);
if (m) addBasemap(m, basemap, baseLayer, labelsLayer);
}, [basemap]);
// Redraw overlays whenever the operator/DX grids (or beam) change.
@@ -224,9 +245,14 @@ export function WorldMap({ fromGrid, toGrid, fromLabel, toLabel, beamAzimuths, b
wm.setView([from.lat, from.lon], 3);
}
}
setTimeout(() => { wm.invalidateSize(); }, 0);
// No invalidateSize() here — a ResizeObserver handles the only case that needs
// it. Forcing a full map repaint on every redraw is what made a moving rotor
// thrash the compositor.
// Headings are rounded in the deps: a rotor reports a jittering float, and a
// tenth of a degree is invisible on a 5500 km lobe but rebuilds every polyline.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fromGrid, toGrid, fromLabel, toLabel, (beamAzimuths ?? []).map((a) => Math.round(a)).join(','), beamWidth, boomAzimuth, autoZoom, zoomSignal]);
}, [fromGrid, toGrid, fromLabel, toLabel, (beamAzimuths ?? []).map((a) => Math.round(a)).join(','), beamWidth,
boomAzimuth == null ? null : Math.round(boomAzimuth), autoZoom, zoomSignal]);
const path = pathBetween(fromGrid, toGrid);
+28 -12
View File
@@ -693,7 +693,15 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool)
// only while nothing has matched yet, and stop at the first that yields a
// reference (short-circuit). So a province already found by NAME isn't also
// re-derived, possibly differently, from a later city-regex rule.
found := searchOne(d.Field, d.MatchBy, re, d.ExactMatch, d.LeadingStr, d.TrailingStr, "", q, rl, predefined)
//
// The short-circuit tests what a rule REALLY yielded — the references that
// survive the predefined list — not its raw candidates. A rule can always
// produce a raw candidate and still find nothing: "the whole ADDRESS field is
// the code" hands back "SERIATE (BG) 24068 ITALY", which is not a province.
// Testing the raw candidate would call that a hit, skip every fallback, and
// only then drop it as unlisted — leaving the QSO unmatched even though the
// next rule ("find the code inside the QTH") would have found BG.
found := keepRefs(predefined, rl, searchOne(d.Field, d.MatchBy, re, d.ExactMatch, d.LeadingStr, d.TrailingStr, "", q, rl, predefined))
for i := 0; len(found) == 0 && i < len(d.OrRules); i++ {
r := &d.OrRules[i]
var rre *regexp.Regexp
@@ -704,7 +712,7 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool)
}
rre = c
}
found = searchOne(r.Field, r.MatchBy, rre, r.ExactMatch, r.LeadingStr, r.TrailingStr, r.Prefix, q, rl, predefined)
found = keepRefs(predefined, rl, searchOne(r.Field, r.MatchBy, rre, r.ExactMatch, r.LeadingStr, r.TrailingStr, r.Prefix, q, rl, predefined))
}
// Merge operator-assigned references (manual override, ManualRefsKey). Lets
@@ -714,18 +722,26 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool)
// hand. Applied HERE (not just in MatchQSO) so Compute — which powers the
// awards panel and the per-QSO refs editor — honours overrides too. For a
// predefined award the ref is still validated against the list below.
for _, c := range manualRefs(q, d.Code) {
found = append(found, normalizeRef(c))
}
if !predefined {
found = append(found, keepRefs(predefined, rl, manualRefs(q, d.Code))...)
return dedupe(found)
}
// Enforce the predefined list: keep only listed, valid references. The
// award-level DXCCFilter already scopes which QSOs are considered (see
// inScope), so we do NOT additionally require the QSO's entity to match the
// reference's own DXCC — that wrongly excluded e.g. WAS Alaska (state AK is
// DXCC entity 6, not 291). Per-reference DXCC stays metadata for the picker.
// keepRefs reduces a rule's raw candidates to the references that actually count.
// For a predefined award that means the codes on the list, enabled. The
// award-level DXCCFilter already scopes which QSOs are considered (see inScope),
// so we do NOT additionally require the QSO's entity to match the reference's own
// DXCC — that wrongly excluded e.g. WAS Alaska (state AK is DXCC entity 6, not
// 291). Per-reference DXCC stays metadata for the picker.
func keepRefs(predefined bool, rl refList, found []string) []string {
if !predefined {
out := make([]string, 0, len(found))
for _, c := range found {
if c = normalizeRef(c); c != "" {
out = append(out, c)
}
}
return dedupe(out)
}
var out []string
seen := map[string]struct{}{}
for _, c := range found {
+30
View File
@@ -354,6 +354,36 @@ func TestCatalogFFMA(t *testing.T) {
}
}
// WAIP: the primary rule takes the WHOLE address as the province code (exact
// match), which can never be one — but it does produce a raw candidate. The OR
// fallback (find the code inside the QTH, "SERIATE (BG)") is the rule that works,
// and it must still run: a rule that yields nothing VALID is not a hit.
func TestComputeOrFallbackAfterUnlistedPrimary(t *testing.T) {
def := Def{
Code: "WAIP", Name: "Worked All Italian Provinces", Valid: true,
Type: TypeReference, Field: "address", MatchBy: "code", ExactMatch: true,
OrRules: []OrRule{{Field: "qth", MatchBy: "code"}}, // not exact → search inside the field
Confirm: []string{"lotw", "qsl"},
}
metas := []RefMeta{
{Code: "BG", Name: "Bergamo", Valid: true},
{Code: "MI", Name: "Milano", Valid: true},
}
qsos := []qso.QSO{
{Callsign: "I2IFT", Band: "30m", QTH: "SERIATE (BG)", Address: "Seriate (Bg) 24068 Italy", LOTWRcvd: "Y"},
}
r := Compute([]Def{def}, qsos, map[string][]RefMeta{"WAIP": metas}, nil)[0]
if r.Worked != 1 {
t.Fatalf("WAIP worked = %d, want 1 (BG, from the QTH fallback)", r.Worked)
}
for _, rf := range r.Refs {
if rf.Ref == "BG" && rf.Worked {
return
}
}
t.Errorf("BG not worked; refs = %v", refCodes(r))
}
func refCodes(r Result) []string {
out := make([]string, 0, len(r.Refs))
for _, rf := range r.Refs {
+929
View File
@@ -0,0 +1,929 @@
{
"version": 1,
"exported_at": "2026-07-13T21:57:23Z",
"awards": [
{
"def": {
"code": "WAIP",
"name": "ARI Worked All Italian Provinces",
"valid": true,
"url": "https://ari.it/en/english-area/awards/1734-waip-worked-all-italian-provinces.html",
"ref_url": "https://ari.it/en/english-area/awards/1734-waip-worked-all-italian-provinces.html",
"type": "REFERENCE",
"field": "qth",
"match_by": "code",
"exact_match": true,
"pattern": "",
"or_rules": [
{
"field": "qth",
"match_by": "description"
},
{
"field": "address",
"match_by": "code"
},
{
"field": "address",
"match_by": "description"
}
],
"dxcc_filter": [
248
],
"confirm": [
"lotw",
"qsl"
],
"validate": [
"lotw",
"qsl"
],
"total": 0,
"builtin": false
},
"references": [
{
"code": "AG",
"name": "Agrigento",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "AL",
"name": "Alessandria",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "AN",
"name": "Ancona",
"dxcc": 248,
"group": "Marche",
"subgrp": "",
"valid": true
},
{
"code": "AO",
"name": "Aosta",
"dxcc": 248,
"group": "Val d'Aosta",
"subgrp": "",
"valid": true
},
{
"code": "AP",
"name": "Ascoli Piceno",
"dxcc": 248,
"group": "Marche",
"subgrp": "",
"valid": true
},
{
"code": "AQ",
"name": "L'Aquila",
"dxcc": 248,
"group": "Abruzzo",
"subgrp": "",
"valid": true
},
{
"code": "AR",
"name": "Arezzo",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "AT",
"name": "Asti",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "AV",
"name": "Avellino",
"dxcc": 248,
"group": "Campania",
"subgrp": "",
"valid": true
},
{
"code": "BA",
"name": "Bari",
"dxcc": 248,
"group": "Puglia",
"subgrp": "",
"valid": true
},
{
"code": "BG",
"name": "Bergamo",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "BI",
"name": "Biella",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "BL",
"name": "Belluno",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "BN",
"name": "Benevento",
"dxcc": 248,
"group": "Campania",
"subgrp": "",
"valid": true
},
{
"code": "BO",
"name": "Bologna",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "BR",
"name": "Brindisi",
"dxcc": 248,
"group": "Puglia",
"subgrp": "",
"valid": true
},
{
"code": "BS",
"name": "Brescia",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "BT",
"name": "Barletta-Andria-Trani",
"dxcc": 248,
"group": "Puglia",
"subgrp": "",
"valid": true
},
{
"code": "BZ",
"name": "Bolzano/Bozen",
"dxcc": 248,
"group": "Trentino-AltoAdige/Südtirol",
"subgrp": "",
"valid": true
},
{
"code": "CA",
"name": "Cagliari",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "CB",
"name": "Campobasso",
"dxcc": 248,
"group": "Molise",
"subgrp": "",
"valid": true
},
{
"code": "CE",
"name": "Caserta",
"dxcc": 248,
"group": "Campania",
"subgrp": "",
"valid": true
},
{
"code": "CH",
"name": "Chieti",
"dxcc": 248,
"group": "Abruzzo",
"subgrp": "",
"valid": true
},
{
"code": "CI",
"name": "Carbonia-Iglesias",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "CL",
"name": "Caltanissetta",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "CN",
"name": "Cuneo",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "CO",
"name": "Como",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "CR",
"name": "Cremona",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "CS",
"name": "Cosenza",
"dxcc": 248,
"group": "Calabria",
"subgrp": "",
"valid": true
},
{
"code": "CT",
"name": "Catania",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "CZ",
"name": "Catanzaro",
"dxcc": 248,
"group": "Calabria",
"subgrp": "",
"valid": true
},
{
"code": "EN",
"name": "Enna",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "FC",
"name": "Forlì-Cesena",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "FE",
"name": "Ferrara",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "FG",
"name": "Foggia",
"dxcc": 248,
"group": "Puglia",
"subgrp": "",
"valid": true
},
{
"code": "FI",
"name": "Firenze",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "FM",
"name": "Fermo",
"dxcc": 248,
"group": "Marche",
"subgrp": "",
"valid": true
},
{
"code": "FR",
"name": "Frosinone",
"dxcc": 248,
"group": "Lazio",
"subgrp": "",
"valid": true
},
{
"code": "GE",
"name": "Genova",
"dxcc": 248,
"group": "Liguria",
"subgrp": "",
"valid": true
},
{
"code": "GO",
"name": "Gorizia",
"dxcc": 248,
"group": "Friuli-Venezia Giulia",
"subgrp": "",
"valid": true
},
{
"code": "GR",
"name": "Grosseto",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "IM",
"name": "Imperia",
"dxcc": 248,
"group": "Liguria",
"subgrp": "",
"valid": true
},
{
"code": "IS",
"name": "Isernia",
"dxcc": 248,
"group": "Molise",
"subgrp": "",
"valid": true
},
{
"code": "KR",
"name": "Crotone",
"dxcc": 248,
"group": "Calabria",
"subgrp": "",
"valid": true
},
{
"code": "LC",
"name": "Lecco",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "LE",
"name": "Lecce",
"dxcc": 248,
"group": "Puglia",
"subgrp": "",
"valid": true
},
{
"code": "LI",
"name": "Livorno",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "LO",
"name": "Lodi",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "LT",
"name": "Latina",
"dxcc": 248,
"group": "Lazio",
"subgrp": "",
"valid": true
},
{
"code": "LU",
"name": "Lucca",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "MB",
"name": "Monza e Brianza",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "MC",
"name": "Macerata",
"dxcc": 248,
"group": "Marche",
"subgrp": "",
"valid": true
},
{
"code": "ME",
"name": "Messina",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "MI",
"name": "Milano",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "MN",
"name": "Mantova",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "MO",
"name": "Modena",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "MS",
"name": "Massa-Carrara",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "MT",
"name": "Matera",
"dxcc": 248,
"group": "Basilicata",
"subgrp": "",
"valid": true
},
{
"code": "NA",
"name": "Napoli",
"dxcc": 248,
"group": "Campania",
"subgrp": "",
"valid": true
},
{
"code": "NO",
"name": "Novara",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "NU",
"name": "Nuoro",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "OG",
"name": "Ogliastra",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "OR",
"name": "Oristano",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "OT",
"name": "Olbia-Tempio",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "PA",
"name": "Palermo",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "PC",
"name": "Piacenza",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "PD",
"name": "Padova",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "PE",
"name": "Pescara",
"dxcc": 248,
"group": "Abruzzo",
"subgrp": "",
"valid": true
},
{
"code": "PG",
"name": "Perugia",
"dxcc": 248,
"group": "Umbria",
"subgrp": "",
"valid": true
},
{
"code": "PI",
"name": "Pisa",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "PN",
"name": "Pordenone",
"dxcc": 248,
"group": "Friuli-Venezia Giulia",
"subgrp": "",
"valid": true
},
{
"code": "PO",
"name": "Prato",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "PR",
"name": "Parma",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "PT",
"name": "Pistoia",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "PU",
"name": "Pesaro-Urbino",
"dxcc": 248,
"group": "Marche",
"subgrp": "",
"valid": true
},
{
"code": "PV",
"name": "Pavia",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "PZ",
"name": "Potenza",
"dxcc": 248,
"group": "Basilicata",
"subgrp": "",
"valid": true
},
{
"code": "RA",
"name": "Ravenna",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "RC",
"name": "Reggio Calabria",
"dxcc": 248,
"group": "Calabria",
"subgrp": "",
"valid": true
},
{
"code": "RE",
"name": "Reggio Emilia",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "RG",
"name": "Ragusa",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "RI",
"name": "Rieti",
"dxcc": 248,
"group": "Lazio",
"subgrp": "",
"valid": true
},
{
"code": "RM",
"name": "Roma",
"dxcc": 248,
"group": "Lazio",
"subgrp": "",
"valid": true
},
{
"code": "RN",
"name": "Rimini",
"dxcc": 248,
"group": "Emilia-Romagna",
"subgrp": "",
"valid": true
},
{
"code": "RO",
"name": "Rovigo",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "SA",
"name": "Salerno",
"dxcc": 248,
"group": "Campania",
"subgrp": "",
"valid": true
},
{
"code": "SI",
"name": "Siena",
"dxcc": 248,
"group": "Toscana",
"subgrp": "",
"valid": true
},
{
"code": "SO",
"name": "Sondrio",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "SP",
"name": "La Spezia",
"dxcc": 248,
"group": "Liguria",
"subgrp": "",
"valid": true
},
{
"code": "SR",
"name": "Siracusa",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "SS",
"name": "Sassari",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "SV",
"name": "Savona",
"dxcc": 248,
"group": "Liguria",
"subgrp": "",
"valid": true
},
{
"code": "TA",
"name": "Taranto",
"dxcc": 248,
"group": "Puglia",
"subgrp": "",
"valid": true
},
{
"code": "TE",
"name": "Teramo",
"dxcc": 248,
"group": "Abruzzo",
"subgrp": "",
"valid": true
},
{
"code": "TN",
"name": "Trento",
"dxcc": 248,
"group": "Trentino-AltoAdige/Südtirol",
"subgrp": "",
"valid": true
},
{
"code": "TO",
"name": "Torino",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "TP",
"name": "Trapani",
"dxcc": 248,
"group": "Sicilia",
"subgrp": "",
"valid": true
},
{
"code": "TR",
"name": "Terni",
"dxcc": 248,
"group": "Umbria",
"subgrp": "",
"valid": true
},
{
"code": "TS",
"name": "Trieste",
"dxcc": 248,
"group": "Friuli-Venezia Giulia",
"subgrp": "",
"valid": true
},
{
"code": "TV",
"name": "Treviso",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "UD",
"name": "Udine",
"dxcc": 248,
"group": "Friuli-Venezia Giulia",
"subgrp": "",
"valid": true
},
{
"code": "VA",
"name": "Varese",
"dxcc": 248,
"group": "Lombardia",
"subgrp": "",
"valid": true
},
{
"code": "VB",
"name": "Verbano-Cusio-Ossola",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "VC",
"name": "Vercelli",
"dxcc": 248,
"group": "Piemonte",
"subgrp": "",
"valid": true
},
{
"code": "VE",
"name": "Venezia",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "VI",
"name": "Vicenza",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "VR",
"name": "Verona",
"dxcc": 248,
"group": "Veneto",
"subgrp": "",
"valid": true
},
{
"code": "VS",
"name": "Medio Campidano",
"dxcc": 248,
"group": "Sardegna",
"subgrp": "",
"valid": true
},
{
"code": "VT",
"name": "Viterbo",
"dxcc": 248,
"group": "Lazio",
"subgrp": "",
"valid": true
},
{
"code": "VV",
"name": "Vibo Valentia",
"dxcc": 248,
"group": "Calabria",
"subgrp": "",
"valid": true
}
]
}
]
}