fix: normalization of city name address
This commit is contained in:
@@ -788,10 +788,30 @@ func (a *App) applyStationDefaults(q *qso.QSO) {
|
|||||||
v := *p.TxPower
|
v := *p.TxPower
|
||||||
q.TXPower = &v
|
q.TXPower = &v
|
||||||
}
|
}
|
||||||
// Resolve my zones / lat / lon via cty.dat using the profile's
|
// Profile-stored MY_* DXCC metadata wins (the user can override the
|
||||||
// callsign. The profile only stores the human-friendly fields
|
// auto-filled values in Station Information).
|
||||||
// (callsign, grid, country name); cty.dat fills the structured
|
if q.MyDXCC == nil && p.MyDXCC != nil {
|
||||||
// DXCC metadata that the ADIF spec wants for every QSO.
|
v := *p.MyDXCC
|
||||||
|
q.MyDXCC = &v
|
||||||
|
}
|
||||||
|
if q.MyCQZone == nil && p.MyCQZone != nil {
|
||||||
|
v := *p.MyCQZone
|
||||||
|
q.MyCQZone = &v
|
||||||
|
}
|
||||||
|
if q.MyITUZone == nil && p.MyITUZone != nil {
|
||||||
|
v := *p.MyITUZone
|
||||||
|
q.MyITUZone = &v
|
||||||
|
}
|
||||||
|
if q.MyLat == nil && p.MyLat != nil {
|
||||||
|
v := *p.MyLat
|
||||||
|
q.MyLat = &v
|
||||||
|
}
|
||||||
|
if q.MyLon == nil && p.MyLon != nil {
|
||||||
|
v := *p.MyLon
|
||||||
|
q.MyLon = &v
|
||||||
|
}
|
||||||
|
// Resolve any still-missing my zones / lat / lon via cty.dat using the
|
||||||
|
// profile's callsign — the fallback when the profile didn't store them.
|
||||||
if a.dxcc != nil && p.Callsign != "" {
|
if a.dxcc != nil && p.Callsign != "" {
|
||||||
if m, ok := a.dxcc.Lookup(p.Callsign); ok && m.Entity != nil {
|
if m, ok := a.dxcc.Lookup(p.Callsign); ok && m.Entity != nil {
|
||||||
if q.MyCQZone == nil && m.CQZone != 0 {
|
if q.MyCQZone == nil && m.CQZone != 0 {
|
||||||
|
|||||||
@@ -1288,14 +1288,14 @@ export default function App() {
|
|||||||
<div className="flex flex-col w-24">
|
<div className="flex flex-col w-24">
|
||||||
<Label className="mb-1 h-3.5 flex items-center gap-1">Band <LockBtn k="band" title="band" /></Label>
|
<Label className="mb-1 h-3.5 flex items-center gap-1">Band <LockBtn k="band" title="band" /></Label>
|
||||||
<Select value={band} onValueChange={onBandUserChange}>
|
<Select value={band} onValueChange={onBandUserChange}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger tabIndex={-1}><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>{bands.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
|
<SelectContent>{bands.map((b) => <SelectItem key={b} value={b}>{b}</SelectItem>)}</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-28">
|
<div className="flex flex-col w-28">
|
||||||
<Label className="mb-1 h-3.5 flex items-center gap-1">Mode <LockBtn k="mode" title="mode" /></Label>
|
<Label className="mb-1 h-3.5 flex items-center gap-1">Mode <LockBtn k="mode" title="mode" /></Label>
|
||||||
<Select value={mode} onValueChange={onModeUserChange}>
|
<Select value={mode} onValueChange={onModeUserChange}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger tabIndex={-1}><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>{modes.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
<SelectContent>{modes.map((m) => <SelectItem key={m} value={m}>{m}</SelectItem>)}</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1304,6 +1304,7 @@ export default function App() {
|
|||||||
{catState.split ? 'TX Freq (MHz)' : 'Freq (MHz)'} <LockBtn k="freq" title="frequency" />
|
{catState.split ? 'TX Freq (MHz)' : 'Freq (MHz)'} <LockBtn k="freq" title="frequency" />
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
tabIndex={-1}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
value={freqFocused ? freqMhz : (freqMhz ? fmtFreqDots(freqMhz) : '')}
|
value={freqFocused ? freqMhz : (freqMhz ? fmtFreqDots(freqMhz) : '')}
|
||||||
placeholder="14.250"
|
placeholder="14.250"
|
||||||
@@ -1316,6 +1317,7 @@ export default function App() {
|
|||||||
<div className="flex flex-col w-28">
|
<div className="flex flex-col w-28">
|
||||||
<Label className="mb-1 h-3.5 text-rose-600">RX Freq (MHz)</Label>
|
<Label className="mb-1 h-3.5 text-rose-600">RX Freq (MHz)</Label>
|
||||||
<Input
|
<Input
|
||||||
|
tabIndex={-1}
|
||||||
value={freqFocused ? rxFreqMhz : (rxFreqMhz ? fmtFreqDots(rxFreqMhz) : '')}
|
value={freqFocused ? rxFreqMhz : (rxFreqMhz ? fmtFreqDots(rxFreqMhz) : '')}
|
||||||
placeholder="14.255"
|
placeholder="14.255"
|
||||||
onFocus={() => setFreqFocused(true)}
|
onFocus={() => setFreqFocused(true)}
|
||||||
|
|||||||
@@ -125,41 +125,6 @@ interface Props {
|
|||||||
// the callsign + grid in the Station Information form. Debounces the
|
// the callsign + grid in the Station Information form. Debounces the
|
||||||
// backend resolver so we don't fire on every keystroke; refreshes when
|
// backend resolver so we don't fire on every keystroke; refreshes when
|
||||||
// inputs change. Empty card when no callsign yet.
|
// inputs change. Empty card when no callsign yet.
|
||||||
function StationInfoComputedBadge({ callsign, grid }: { callsign: string; grid: string }) {
|
|
||||||
const [info, setInfo] = useState<{
|
|
||||||
country: string; dxcc: number; cqz: number; ituz: number; lat: number; lon: number;
|
|
||||||
} | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const c = callsign.trim();
|
|
||||||
if (!c) { setInfo(null); return; }
|
|
||||||
const t = window.setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
const i = await ComputeStationInfo(c, grid.trim());
|
|
||||||
setInfo(i as any);
|
|
||||||
} catch { setInfo(null); }
|
|
||||||
}, 200);
|
|
||||||
return () => window.clearTimeout(t);
|
|
||||||
}, [callsign, grid]);
|
|
||||||
if (!info || (!info.country && !info.cqz && !info.ituz)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="rounded-md border border-primary/30 bg-primary/5 p-2.5">
|
|
||||||
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">
|
|
||||||
Auto-filled on each QSO (MY_*)
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[11px] font-mono">
|
|
||||||
{info.country && <span><span className="text-muted-foreground">Country:</span> <strong>{info.country}</strong></span>}
|
|
||||||
{info.dxcc > 0 && <span><span className="text-muted-foreground">DXCC#:</span> <strong>{info.dxcc}</strong></span>}
|
|
||||||
{info.cqz > 0 && <span><span className="text-muted-foreground">CQ:</span> <strong>{info.cqz}</strong></span>}
|
|
||||||
{info.ituz > 0 && <span><span className="text-muted-foreground">ITU:</span> <strong>{info.ituz}</strong></span>}
|
|
||||||
{info.lat !== 0 && <span><span className="text-muted-foreground">Lat:</span> <strong>{info.lat.toFixed(4)}</strong></span>}
|
|
||||||
{info.lon !== 0 && <span><span className="text-muted-foreground">Lon:</span> <strong>{info.lon.toFixed(4)}</strong></span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ====== Tree definition ======
|
/* ====== Tree definition ======
|
||||||
Section IDs are stable strings — adding new ones means adding a panel below.
|
Section IDs are stable strings — adding new ones means adding a panel below.
|
||||||
`disabled: true` greys them out and shows the "coming soon" placeholder. */
|
`disabled: true` greys them out and shows the "coming soon" placeholder. */
|
||||||
@@ -499,6 +464,36 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Auto-fill the active profile's MY_* DXCC metadata from the station
|
||||||
|
// callsign (country, DXCC#, CQ/ITU zones) and the grid (lat/lon). These
|
||||||
|
// are derived values, so they always recompute when the callsign or grid
|
||||||
|
// changes — the user can still edit a field, it just re-populates when the
|
||||||
|
// source changes. Debounced so we don't hammer cty.dat while typing.
|
||||||
|
useEffect(() => {
|
||||||
|
const call = (activeProfile?.callsign ?? '').trim();
|
||||||
|
if (!call) return;
|
||||||
|
const grid = (activeProfile?.my_grid ?? '').trim();
|
||||||
|
const t = window.setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const i: any = await ComputeStationInfo(call, grid);
|
||||||
|
setActiveProfile((p) => {
|
||||||
|
if (!p) return p;
|
||||||
|
const patch: any = {};
|
||||||
|
if (i.country) patch.my_country = i.country;
|
||||||
|
if (i.dxcc) patch.my_dxcc = i.dxcc;
|
||||||
|
if (i.cqz) patch.my_cqz = i.cqz;
|
||||||
|
if (i.ituz) patch.my_ituz = i.ituz;
|
||||||
|
if (i.lat) patch.my_lat = i.lat;
|
||||||
|
if (i.lon) patch.my_lon = i.lon;
|
||||||
|
// Only re-render when a value actually changed (prevents loops).
|
||||||
|
const changed = Object.keys(patch).some((k) => (p as any)[k] !== patch[k]);
|
||||||
|
return changed ? { ...p, ...patch } : p;
|
||||||
|
});
|
||||||
|
} catch { /* offline / unknown prefix — leave fields as-is */ }
|
||||||
|
}, 250);
|
||||||
|
return () => window.clearTimeout(t);
|
||||||
|
}, [activeProfile?.callsign, activeProfile?.my_grid]);
|
||||||
|
|
||||||
// ── Band selection helpers (dual-list shuttle) ──────────────────────────
|
// ── Band selection helpers (dual-list shuttle) ──────────────────────────
|
||||||
function addBand(tag: string) {
|
function addBand(tag: string) {
|
||||||
const b = tag.trim().toLowerCase();
|
const b = tag.trim().toLowerCase();
|
||||||
@@ -681,7 +676,9 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
<Input className="font-mono uppercase max-w-xs" value={p.owner_callsign ?? ''} onChange={(e) => updateActive({ owner_callsign: e.target.value })} placeholder="(leave blank if same as station)" />
|
<Input className="font-mono uppercase max-w-xs" value={p.owner_callsign ?? ''} onChange={(e) => updateActive({ owner_callsign: e.target.value })} placeholder="(leave blank if same as station)" />
|
||||||
<div className="text-[10px] text-muted-foreground">Legal station owner — only differs at club stations or remote setups (ADIF STATION_OWNER).</div>
|
<div className="text-[10px] text-muted-foreground">Legal station owner — only differs at club stations or remote setups (ADIF STATION_OWNER).</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2"><StationInfoComputedBadge callsign={p.callsign ?? ''} grid={p.my_grid ?? ''} /></div>
|
<div className="col-span-2 text-[10px] text-muted-foreground uppercase tracking-wider mt-1">
|
||||||
|
Auto-filled from the callsign — editable (stamped as MY_* on each QSO)
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>My grid</Label>
|
<Label>My grid</Label>
|
||||||
<Input className="font-mono uppercase" value={p.my_grid ?? ''} onChange={(e) => updateActive({ my_grid: e.target.value })} placeholder="JN18BU" />
|
<Input className="font-mono uppercase" value={p.my_grid ?? ''} onChange={(e) => updateActive({ my_grid: e.target.value })} placeholder="JN18BU" />
|
||||||
@@ -690,6 +687,33 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
|||||||
<Label>My country</Label>
|
<Label>My country</Label>
|
||||||
<Input value={p.my_country ?? ''} onChange={(e) => updateActive({ my_country: e.target.value })} placeholder="France" />
|
<Input value={p.my_country ?? ''} onChange={(e) => updateActive({ my_country: e.target.value })} placeholder="France" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 col-span-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>DXCC #</Label>
|
||||||
|
<Input type="number" className="font-mono" value={(p as any).my_dxcc ?? ''}
|
||||||
|
onChange={(e) => updateActive({ my_dxcc: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>CQ zone</Label>
|
||||||
|
<Input type="number" className="font-mono" value={(p as any).my_cqz ?? ''}
|
||||||
|
onChange={(e) => updateActive({ my_cqz: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>ITU zone</Label>
|
||||||
|
<Input type="number" className="font-mono" value={(p as any).my_ituz ?? ''}
|
||||||
|
onChange={(e) => updateActive({ my_ituz: e.target.value === '' ? undefined : (parseInt(e.target.value, 10) || undefined) } as any)} placeholder="—" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Latitude</Label>
|
||||||
|
<Input type="number" step="0.0001" className="font-mono" value={(p as any).my_lat ?? ''}
|
||||||
|
onChange={(e) => updateActive({ my_lat: e.target.value === '' ? undefined : (parseFloat(e.target.value) || undefined) } as any)} placeholder="—" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Longitude</Label>
|
||||||
|
<Input type="number" step="0.0001" className="font-mono" value={(p as any).my_lon ?? ''}
|
||||||
|
onChange={(e) => updateActive({ my_lon: e.target.value === '' ? undefined : (parseFloat(e.target.value) || undefined) } as any)} placeholder="—" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>State / pref</Label>
|
<Label>State / pref</Label>
|
||||||
<Input value={p.my_state ?? ''} onChange={(e) => updateActive({ my_state: e.target.value })} />
|
<Input value={p.my_state ?? ''} onChange={(e) => updateActive({ my_state: e.target.value })} />
|
||||||
|
|||||||
@@ -741,6 +741,11 @@ export namespace profile {
|
|||||||
my_pota_ref: string;
|
my_pota_ref: string;
|
||||||
my_rig: string;
|
my_rig: string;
|
||||||
my_antenna: string;
|
my_antenna: string;
|
||||||
|
my_dxcc?: number;
|
||||||
|
my_cqz?: number;
|
||||||
|
my_ituz?: number;
|
||||||
|
my_lat?: number;
|
||||||
|
my_lon?: number;
|
||||||
tx_pwr?: number;
|
tx_pwr?: number;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
sort_order: number;
|
sort_order: number;
|
||||||
@@ -771,6 +776,11 @@ export namespace profile {
|
|||||||
this.my_pota_ref = source["my_pota_ref"];
|
this.my_pota_ref = source["my_pota_ref"];
|
||||||
this.my_rig = source["my_rig"];
|
this.my_rig = source["my_rig"];
|
||||||
this.my_antenna = source["my_antenna"];
|
this.my_antenna = source["my_antenna"];
|
||||||
|
this.my_dxcc = source["my_dxcc"];
|
||||||
|
this.my_cqz = source["my_cqz"];
|
||||||
|
this.my_ituz = source["my_ituz"];
|
||||||
|
this.my_lat = source["my_lat"];
|
||||||
|
this.my_lon = source["my_lon"];
|
||||||
this.tx_pwr = source["tx_pwr"];
|
this.tx_pwr = source["tx_pwr"];
|
||||||
this.is_active = source["is_active"];
|
this.is_active = source["is_active"];
|
||||||
this.sort_order = source["sort_order"];
|
this.sort_order = source["sort_order"];
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Per-profile MY_* DXCC metadata. These were previously derived from the
|
||||||
|
-- station callsign via cty.dat on every QSO; storing them on the profile
|
||||||
|
-- lets the user see and override them in Station Information (auto-filled
|
||||||
|
-- from the callsign, but editable for club/special situations).
|
||||||
|
ALTER TABLE station_profiles ADD COLUMN my_dxcc INTEGER;
|
||||||
|
ALTER TABLE station_profiles ADD COLUMN my_cqz INTEGER;
|
||||||
|
ALTER TABLE station_profiles ADD COLUMN my_ituz INTEGER;
|
||||||
|
ALTER TABLE station_profiles ADD COLUMN my_lat REAL;
|
||||||
|
ALTER TABLE station_profiles ADD COLUMN my_lon REAL;
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNotFound is returned by providers when a callsign is unknown.
|
// ErrNotFound is returned by providers when a callsign is unknown.
|
||||||
@@ -100,6 +101,7 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
|
|||||||
// corrected entity mapping (e.g. Sicily → Italy) heal stale cached
|
// corrected entity mapping (e.g. Sicily → Italy) heal stale cached
|
||||||
// rows without waiting for the TTL to expire.
|
// rows without waiting for the TTL to expire.
|
||||||
fillFromDXCC(&r, dxcc)
|
fillFromDXCC(&r, dxcc)
|
||||||
|
normalizeNames(&r)
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +113,7 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
|
|||||||
r.Source = p.Name()
|
r.Source = p.Name()
|
||||||
r.FetchedAt = time.Now().UTC()
|
r.FetchedAt = time.Now().UTC()
|
||||||
fillFromDXCC(&r, dxcc)
|
fillFromDXCC(&r, dxcc)
|
||||||
|
normalizeNames(&r)
|
||||||
_ = m.cache.Put(ctx, r)
|
_ = m.cache.Put(ctx, r)
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
@@ -145,6 +148,43 @@ func (m *Manager) Lookup(ctx context.Context, callsign string) (Result, error) {
|
|||||||
return Result{}, lastErr
|
return Result{}, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeNames title-cases the human-readable text fields so a QRZ/HamQTH
|
||||||
|
// reply in ALL CAPS ("NOEL CHENAVARD", "VETRAZ-MONTHOUX") is stored and shown
|
||||||
|
// consistently ("Noel Chenavard", "Vetraz-Monthoux"). State/zones/grid are
|
||||||
|
// left untouched (codes like CT must stay as-is).
|
||||||
|
func normalizeNames(r *Result) {
|
||||||
|
r.Name = titleCase(r.Name)
|
||||||
|
r.QTH = titleCase(r.QTH)
|
||||||
|
r.Address = titleCase(r.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// titleCase lowercases the whole string then capitalises the first letter of
|
||||||
|
// each word. Word boundaries are any non-alphanumeric rune (space, hyphen,
|
||||||
|
// apostrophe, slash…), so "vetraz-monthoux" → "Vetraz-Monthoux" and
|
||||||
|
// "o'brien" → "O'Brien". Digits never get a leading capital ("74140" stays).
|
||||||
|
func titleCase(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
runes := []rune(strings.ToLower(s))
|
||||||
|
atWordStart := true
|
||||||
|
for i, r := range runes {
|
||||||
|
switch {
|
||||||
|
case unicode.IsLetter(r):
|
||||||
|
if atWordStart {
|
||||||
|
runes[i] = unicode.ToUpper(r)
|
||||||
|
}
|
||||||
|
atWordStart = false
|
||||||
|
case unicode.IsDigit(r):
|
||||||
|
atWordStart = false
|
||||||
|
default:
|
||||||
|
atWordStart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
|
||||||
// fillFromDXCC fills (or overrides) country/continent/zones/lat/lon from
|
// fillFromDXCC fills (or overrides) country/continent/zones/lat/lon from
|
||||||
// the cty.dat resolver. cty.dat is the authoritative source for DXCC
|
// the cty.dat resolver. cty.dat is the authoritative source for DXCC
|
||||||
// mapping, so Country/Continent/CQZ/ITUZ are ALWAYS overridden when it
|
// mapping, so Country/Continent/CQZ/ITUZ are ALWAYS overridden when it
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package lookup
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTitleCase(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"NOEL CHENAVARD": "Noel Chenavard",
|
||||||
|
"VETRAZ-MONTHOUX": "Vetraz-Monthoux",
|
||||||
|
"o'brien": "O'Brien",
|
||||||
|
"866 ROUTE DES VOIRONS": "866 Route Des Voirons",
|
||||||
|
"PARIS": "Paris",
|
||||||
|
"": "",
|
||||||
|
" saint-étienne ": "Saint-Étienne",
|
||||||
|
"JOHN": "John",
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
if got := titleCase(in); got != want {
|
||||||
|
t.Errorf("titleCase(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,11 @@ type Profile struct {
|
|||||||
MyPOTARef string `json:"my_pota_ref"`
|
MyPOTARef string `json:"my_pota_ref"`
|
||||||
MyRig string `json:"my_rig"`
|
MyRig string `json:"my_rig"`
|
||||||
MyAntenna string `json:"my_antenna"`
|
MyAntenna string `json:"my_antenna"`
|
||||||
|
MyDXCC *int `json:"my_dxcc,omitempty"`
|
||||||
|
MyCQZone *int `json:"my_cqz,omitempty"`
|
||||||
|
MyITUZone *int `json:"my_ituz,omitempty"`
|
||||||
|
MyLat *float64 `json:"my_lat,omitempty"`
|
||||||
|
MyLon *float64 `json:"my_lon,omitempty"`
|
||||||
TxPower *float64 `json:"tx_pwr,omitempty"`
|
TxPower *float64 `json:"tx_pwr,omitempty"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
@@ -48,7 +53,8 @@ func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }
|
|||||||
|
|
||||||
const selectCols = `id, name, callsign, operator, owner_callsign, my_grid, my_country, my_state, my_cnty,
|
const selectCols = `id, name, callsign, operator, owner_callsign, my_grid, my_country, my_state, my_cnty,
|
||||||
my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref,
|
my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref,
|
||||||
my_rig, my_antenna, tx_pwr, is_active, sort_order, created_at, updated_at`
|
my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr,
|
||||||
|
is_active, sort_order, created_at, updated_at`
|
||||||
|
|
||||||
// List returns every profile, active first then by sort_order/id.
|
// List returns every profile, active first then by sort_order/id.
|
||||||
func (r *Repo) List(ctx context.Context) ([]Profile, error) {
|
func (r *Repo) List(ctx context.Context) ([]Profile, error) {
|
||||||
@@ -96,11 +102,14 @@ func (r *Repo) Save(ctx context.Context, p *Profile) error {
|
|||||||
INSERT INTO station_profiles
|
INSERT INTO station_profiles
|
||||||
(name, callsign, operator, owner_callsign, my_grid, my_country, my_state, my_cnty,
|
(name, callsign, operator, owner_callsign, my_grid, my_country, my_state, my_cnty,
|
||||||
my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref,
|
my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref,
|
||||||
my_rig, my_antenna, tx_pwr, is_active, sort_order, created_at, updated_at)
|
my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr,
|
||||||
VALUES(?,?,?,?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?,?,?)`,
|
is_active, sort_order, created_at, updated_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?,?,?,?, ?,?,?,?)`,
|
||||||
p.Name, p.Callsign, p.Operator, p.OwnerCallsign, p.MyGrid, p.MyCountry, p.MyState, p.MyCounty,
|
p.Name, p.Callsign, p.Operator, p.OwnerCallsign, p.MyGrid, p.MyCountry, p.MyState, p.MyCounty,
|
||||||
p.MyStreet, p.MyCity, p.MyPostalCode, p.MySOTARef, p.MyPOTARef,
|
p.MyStreet, p.MyCity, p.MyPostalCode, p.MySOTARef, p.MyPOTARef,
|
||||||
p.MyRig, p.MyAntenna, nullableFloat(p.TxPower), boolInt(p.IsActive), p.SortOrder, now, now)
|
p.MyRig, p.MyAntenna, nullableInt(p.MyDXCC), nullableInt(p.MyCQZone), nullableInt(p.MyITUZone),
|
||||||
|
nullableFloat(p.MyLat), nullableFloat(p.MyLon), nullableFloat(p.TxPower),
|
||||||
|
boolInt(p.IsActive), p.SortOrder, now, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -112,12 +121,15 @@ func (r *Repo) Save(ctx context.Context, p *Profile) error {
|
|||||||
UPDATE station_profiles SET
|
UPDATE station_profiles SET
|
||||||
name = ?, callsign = ?, operator = ?, owner_callsign = ?, my_grid = ?, my_country = ?,
|
name = ?, callsign = ?, operator = ?, owner_callsign = ?, my_grid = ?, my_country = ?,
|
||||||
my_state = ?, my_cnty = ?, my_street = ?, my_city = ?, my_postal_code = ?,
|
my_state = ?, my_cnty = ?, my_street = ?, my_city = ?, my_postal_code = ?,
|
||||||
my_sota_ref = ?, my_pota_ref = ?, my_rig = ?, my_antenna = ?, tx_pwr = ?,
|
my_sota_ref = ?, my_pota_ref = ?, my_rig = ?, my_antenna = ?,
|
||||||
|
my_dxcc = ?, my_cqz = ?, my_ituz = ?, my_lat = ?, my_lon = ?, tx_pwr = ?,
|
||||||
sort_order = ?, updated_at = ?
|
sort_order = ?, updated_at = ?
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
p.Name, p.Callsign, p.Operator, p.OwnerCallsign, p.MyGrid, p.MyCountry,
|
p.Name, p.Callsign, p.Operator, p.OwnerCallsign, p.MyGrid, p.MyCountry,
|
||||||
p.MyState, p.MyCounty, p.MyStreet, p.MyCity, p.MyPostalCode,
|
p.MyState, p.MyCounty, p.MyStreet, p.MyCity, p.MyPostalCode,
|
||||||
p.MySOTARef, p.MyPOTARef, p.MyRig, p.MyAntenna, nullableFloat(p.TxPower),
|
p.MySOTARef, p.MyPOTARef, p.MyRig, p.MyAntenna,
|
||||||
|
nullableInt(p.MyDXCC), nullableInt(p.MyCQZone), nullableInt(p.MyITUZone),
|
||||||
|
nullableFloat(p.MyLat), nullableFloat(p.MyLon), nullableFloat(p.TxPower),
|
||||||
p.SortOrder, now, p.ID)
|
p.SortOrder, now, p.ID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -210,13 +222,15 @@ func scan(row scannable) (Profile, error) {
|
|||||||
callsign, operator, ownerCall, myGrid, myCountry, myState, myCnty,
|
callsign, operator, ownerCall, myGrid, myCountry, myState, myCnty,
|
||||||
myStreet, myCity, myPostal, mySOTA, myPOTA,
|
myStreet, myCity, myPostal, mySOTA, myPOTA,
|
||||||
myRig, myAntenna sql.NullString
|
myRig, myAntenna sql.NullString
|
||||||
txPwr sql.NullFloat64
|
myDXCC, myCQZ, myITUZ sql.NullInt64
|
||||||
|
myLat, myLon, txPwr sql.NullFloat64
|
||||||
isActive, sortOrder int
|
isActive, sortOrder int
|
||||||
createdAt, updatedAt string
|
createdAt, updatedAt string
|
||||||
)
|
)
|
||||||
err := row.Scan(&p.ID, &p.Name, &callsign, &operator, &ownerCall, &myGrid, &myCountry, &myState, &myCnty,
|
err := row.Scan(&p.ID, &p.Name, &callsign, &operator, &ownerCall, &myGrid, &myCountry, &myState, &myCnty,
|
||||||
&myStreet, &myCity, &myPostal, &mySOTA, &myPOTA,
|
&myStreet, &myCity, &myPostal, &mySOTA, &myPOTA,
|
||||||
&myRig, &myAntenna, &txPwr, &isActive, &sortOrder, &createdAt, &updatedAt)
|
&myRig, &myAntenna, &myDXCC, &myCQZ, &myITUZ, &myLat, &myLon, &txPwr,
|
||||||
|
&isActive, &sortOrder, &createdAt, &updatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
@@ -234,6 +248,26 @@ func scan(row scannable) (Profile, error) {
|
|||||||
p.MyPOTARef = myPOTA.String
|
p.MyPOTARef = myPOTA.String
|
||||||
p.MyRig = myRig.String
|
p.MyRig = myRig.String
|
||||||
p.MyAntenna = myAntenna.String
|
p.MyAntenna = myAntenna.String
|
||||||
|
if myDXCC.Valid {
|
||||||
|
v := int(myDXCC.Int64)
|
||||||
|
p.MyDXCC = &v
|
||||||
|
}
|
||||||
|
if myCQZ.Valid {
|
||||||
|
v := int(myCQZ.Int64)
|
||||||
|
p.MyCQZone = &v
|
||||||
|
}
|
||||||
|
if myITUZ.Valid {
|
||||||
|
v := int(myITUZ.Int64)
|
||||||
|
p.MyITUZone = &v
|
||||||
|
}
|
||||||
|
if myLat.Valid {
|
||||||
|
v := myLat.Float64
|
||||||
|
p.MyLat = &v
|
||||||
|
}
|
||||||
|
if myLon.Valid {
|
||||||
|
v := myLon.Float64
|
||||||
|
p.MyLon = &v
|
||||||
|
}
|
||||||
if txPwr.Valid {
|
if txPwr.Valid {
|
||||||
v := txPwr.Float64
|
v := txPwr.Float64
|
||||||
p.TxPower = &v
|
p.TxPower = &v
|
||||||
@@ -251,6 +285,12 @@ func nullableFloat(p *float64) any {
|
|||||||
}
|
}
|
||||||
return *p
|
return *p
|
||||||
}
|
}
|
||||||
|
func nullableInt(p *int) any {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *p
|
||||||
|
}
|
||||||
func boolInt(b bool) int {
|
func boolInt(b bool) int {
|
||||||
if b {
|
if b {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user