up
This commit is contained in:
+38
-5
@@ -92,7 +92,7 @@ func (e *Exporter) writeDoc(ctx context.Context, w io.Writer, iter iterator) (in
|
||||
ver := strings.TrimSpace(e.AppVersion)
|
||||
now := time.Now().UTC().Format("20060102 150405")
|
||||
fmt.Fprintf(bw, "# ADIF export by %s %s — %s UTC\n", app, ver, now)
|
||||
fmt.Fprintf(bw, "<ADIF_VER:5>3.1.0 <PROGRAMID:%d>%s", len(app), app)
|
||||
fmt.Fprintf(bw, "<ADIF_VER:%d>%s <PROGRAMID:%d>%s", len(adifVersion), adifVersion, len(app), app)
|
||||
if ver != "" {
|
||||
fmt.Fprintf(bw, " <PROGRAMVERSION:%d>%s", len(ver), ver)
|
||||
}
|
||||
@@ -248,13 +248,46 @@ func writeRecord(bw *bufio.Writer, q qso.QSO, includeApp bool) {
|
||||
writeField(bw, "COMMENT", q.Comment)
|
||||
writeField(bw, "NOTES", q.Notes)
|
||||
|
||||
// --- ADIF 3.1.7 additional promoted fields ---
|
||||
writeField(bw, "SIG", q.SIG)
|
||||
writeField(bw, "SIG_INFO", q.SIGInfo)
|
||||
writeField(bw, "MY_SIG", q.MySIG)
|
||||
writeField(bw, "MY_SIG_INFO", q.MySIGInfo)
|
||||
writeField(bw, "WWFF_REF", q.WWFFRef)
|
||||
writeField(bw, "MY_WWFF_REF", q.MyWWFFRef)
|
||||
writeFloatPtr(bw, "DISTANCE", q.Distance, 1)
|
||||
writeFloatPtr(bw, "RX_PWR", q.RXPower, 1)
|
||||
writeFloatPtr(bw, "A_INDEX", q.AIndex, 0)
|
||||
writeFloatPtr(bw, "K_INDEX", q.KIndex, 0)
|
||||
writeFloatPtr(bw, "SFI", q.SFI, 0)
|
||||
writeField(bw, "SKCC", q.SKCC)
|
||||
writeField(bw, "FISTS", q.FISTS)
|
||||
writeField(bw, "TEN_TEN", q.TenTen)
|
||||
writeField(bw, "CONTACTED_OP", q.ContactedOp)
|
||||
writeField(bw, "EQ_CALL", q.EqCall)
|
||||
writeField(bw, "PFX", q.PFX)
|
||||
writeField(bw, "MY_NAME", q.MyName)
|
||||
writeField(bw, "CLASS", q.Class)
|
||||
writeField(bw, "DARC_DOK", q.DarcDOK)
|
||||
writeField(bw, "MY_DARC_DOK", q.MyDarcDOK)
|
||||
writeField(bw, "REGION", q.Region)
|
||||
writeField(bw, "SILENT_KEY", q.SilentKey)
|
||||
writeField(bw, "SWL", q.SWL)
|
||||
writeField(bw, "QSO_COMPLETE", q.QSOComplete)
|
||||
writeField(bw, "QSO_RANDOM", q.QSORandom)
|
||||
writeField(bw, "CREDIT_GRANTED", q.CreditGranted)
|
||||
writeField(bw, "CREDIT_SUBMITTED", q.CreditSubmitted)
|
||||
writeField(bw, "MY_ARRL_SECT", q.MyARRLSect)
|
||||
writeField(bw, "MY_VUCC_GRIDS", q.MyVUCCGrids)
|
||||
|
||||
// --- Extras (unpromoted ADIF fields preserved verbatim) ---
|
||||
// In standard mode we drop application-specific tags (APP_*) so the file
|
||||
// stays portable to other loggers; in full mode they're kept for a
|
||||
// lossless OpsLog round-trip.
|
||||
// Standard mode emits ONLY valid ADIF-spec fields, so it drops APP_*
|
||||
// application-specific tags AND any non-standard / vendor tag — keeping
|
||||
// the file strictly portable to other loggers. Full mode keeps every
|
||||
// extra for a lossless OpsLog round-trip.
|
||||
for k, v := range q.Extras {
|
||||
tag := strings.ToUpper(k)
|
||||
if !includeApp && strings.HasPrefix(tag, "APP_") {
|
||||
if !includeApp && (strings.HasPrefix(tag, "APP_") || !IsStandardField(tag)) {
|
||||
continue
|
||||
}
|
||||
writeField(bw, tag, v)
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package adif
|
||||
|
||||
import "strings"
|
||||
|
||||
// This file embeds the complete ADIF 3.1.7 QSO-field dictionary. It is the
|
||||
// single source of truth for:
|
||||
// - the generic "ADIF fields" editor in the UI (any field becomes editable),
|
||||
// - the "standard ADIF" export mode (only spec fields are emitted),
|
||||
// - import diagnostics (a tag absent here is non-standard / vendor-specific).
|
||||
//
|
||||
// Promoted fields (those with dedicated QSO columns) are flagged Promoted=true
|
||||
// so the UI can hide them from the generic editor — they already have proper
|
||||
// inputs in the main tabs.
|
||||
|
||||
// FieldKind is a coarse input type used by the generic editor to pick a widget.
|
||||
type FieldKind string
|
||||
|
||||
const (
|
||||
KindText FieldKind = "text" // String / IntlString / MultilineString
|
||||
KindNumber FieldKind = "number" // Number / PositiveInteger
|
||||
KindDate FieldKind = "date" // ADIF Date (YYYYMMDD)
|
||||
KindTime FieldKind = "time" // ADIF Time (HHMMSS / HHMM)
|
||||
KindBool FieldKind = "boolean" // Boolean (Y/N)
|
||||
KindEnum FieldKind = "enum" // Enumeration
|
||||
KindLoc FieldKind = "location" // Location (e.g. "N048 09.000")
|
||||
)
|
||||
|
||||
// FieldDef describes one ADIF QSO field.
|
||||
type FieldDef struct {
|
||||
Name string `json:"name"` // canonical uppercase ADIF tag
|
||||
Kind FieldKind `json:"kind"` // editor widget hint
|
||||
Category string `json:"category"` // grouping for the UI
|
||||
Promoted bool `json:"promoted"` // has a dedicated QSO column
|
||||
Deprecated bool `json:"deprecated"` // import-only per the spec
|
||||
Intl bool `json:"intl"` // *_INTL UTF-8 variant
|
||||
}
|
||||
|
||||
// adifVersion is the ADIF spec version OpsLog targets for import/export.
|
||||
const adifVersion = "3.1.7"
|
||||
|
||||
// ADIFVersion returns the ADIF spec version OpsLog conforms to.
|
||||
func ADIFVersion() string { return adifVersion }
|
||||
|
||||
// Fields is the full ADIF 3.1.7 QSO-field set. Order is alphabetical within
|
||||
// each category; categories order is roughly "most-used first" for the UI.
|
||||
var Fields = []FieldDef{
|
||||
// ── Core / contact ──────────────────────────────────────────────
|
||||
{Name: "CALL", Kind: KindText, Category: "Core", Promoted: true},
|
||||
{Name: "QSO_DATE", Kind: KindDate, Category: "Core", Promoted: true},
|
||||
{Name: "TIME_ON", Kind: KindTime, Category: "Core", Promoted: true},
|
||||
{Name: "QSO_DATE_OFF", Kind: KindDate, Category: "Core", Promoted: true},
|
||||
{Name: "TIME_OFF", Kind: KindTime, Category: "Core", Promoted: true},
|
||||
{Name: "BAND", Kind: KindEnum, Category: "Core", Promoted: true},
|
||||
{Name: "BAND_RX", Kind: KindEnum, Category: "Core", Promoted: true},
|
||||
{Name: "MODE", Kind: KindEnum, Category: "Core", Promoted: true},
|
||||
{Name: "SUBMODE", Kind: KindEnum, Category: "Core", Promoted: true},
|
||||
{Name: "FREQ", Kind: KindNumber, Category: "Core", Promoted: true},
|
||||
{Name: "FREQ_RX", Kind: KindNumber, Category: "Core", Promoted: true},
|
||||
{Name: "RST_SENT", Kind: KindText, Category: "Core", Promoted: true},
|
||||
{Name: "RST_RCVD", Kind: KindText, Category: "Core", Promoted: true},
|
||||
{Name: "QSO_COMPLETE", Kind: KindEnum, Category: "Core", Promoted: true},
|
||||
{Name: "QSO_RANDOM", Kind: KindBool, Category: "Core", Promoted: true},
|
||||
{Name: "SWL", Kind: KindBool, Category: "Core", Promoted: true},
|
||||
|
||||
// ── Contacted station ───────────────────────────────────────────
|
||||
{Name: "NAME", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "NAME_INTL", Kind: KindText, Category: "Contacted", Intl: true},
|
||||
{Name: "QTH", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "QTH_INTL", Kind: KindText, Category: "Contacted", Intl: true},
|
||||
{Name: "ADDRESS", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "ADDRESS_INTL", Kind: KindText, Category: "Contacted", Intl: true},
|
||||
{Name: "EMAIL", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "WEB", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "GRIDSQUARE", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "GRIDSQUARE_EXT", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "VUCC_GRIDS", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "COUNTRY", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "COUNTRY_INTL", Kind: KindText, Category: "Contacted", Intl: true},
|
||||
{Name: "STATE", Kind: KindEnum, Category: "Contacted", Promoted: true},
|
||||
{Name: "CNTY", Kind: KindEnum, Category: "Contacted", Promoted: true},
|
||||
{Name: "CNTY_ALT", Kind: KindEnum, Category: "Contacted"},
|
||||
{Name: "DXCC", Kind: KindEnum, Category: "Contacted", Promoted: true},
|
||||
{Name: "CONT", Kind: KindEnum, Category: "Contacted", Promoted: true},
|
||||
{Name: "CQZ", Kind: KindNumber, Category: "Contacted", Promoted: true},
|
||||
{Name: "ITUZ", Kind: KindNumber, Category: "Contacted", Promoted: true},
|
||||
{Name: "IOTA", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "IOTA_ISLAND_ID", Kind: KindText, Category: "Contacted"},
|
||||
{Name: "REGION", Kind: KindEnum, Category: "Contacted", Promoted: true},
|
||||
{Name: "PFX", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "AGE", Kind: KindNumber, Category: "Contacted", Promoted: true},
|
||||
{Name: "LAT", Kind: KindLoc, Category: "Contacted", Promoted: true},
|
||||
{Name: "LON", Kind: KindLoc, Category: "Contacted", Promoted: true},
|
||||
{Name: "ALTITUDE", Kind: KindNumber, Category: "Contacted"},
|
||||
{Name: "DISTANCE", Kind: KindNumber, Category: "Contacted", Promoted: true},
|
||||
{Name: "RIG", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "RIG_INTL", Kind: KindText, Category: "Contacted", Intl: true},
|
||||
{Name: "ANT", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "CONTACTED_OP", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "EQ_CALL", Kind: KindText, Category: "Contacted", Promoted: true},
|
||||
{Name: "GUEST_OP", Kind: KindText, Category: "Contacted", Deprecated: true},
|
||||
{Name: "OWNER_CALLSIGN", Kind: KindText, Category: "Contacted"},
|
||||
{Name: "SILENT_KEY", Kind: KindBool, Category: "Contacted", Promoted: true},
|
||||
{Name: "USACA_COUNTIES", Kind: KindText, Category: "Contacted"},
|
||||
|
||||
// ── Special activity (POTA/SOTA/WWFF/SIG) ───────────────────────
|
||||
{Name: "SIG", Kind: KindText, Category: "Activity", Promoted: true},
|
||||
{Name: "SIG_INFO", Kind: KindText, Category: "Activity", Promoted: true},
|
||||
{Name: "SIG_INTL", Kind: KindText, Category: "Activity", Intl: true},
|
||||
{Name: "SIG_INFO_INTL", Kind: KindText, Category: "Activity", Intl: true},
|
||||
{Name: "POTA_REF", Kind: KindText, Category: "Activity", Promoted: true},
|
||||
{Name: "SOTA_REF", Kind: KindText, Category: "Activity", Promoted: true},
|
||||
{Name: "WWFF_REF", Kind: KindText, Category: "Activity", Promoted: true},
|
||||
|
||||
// ── Power / propagation / space wx ──────────────────────────────
|
||||
{Name: "TX_PWR", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "RX_PWR", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "A_INDEX", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "K_INDEX", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "SFI", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "PROP_MODE", Kind: KindEnum, Category: "Propagation", Promoted: true},
|
||||
{Name: "SAT_NAME", Kind: KindText, Category: "Propagation", Promoted: true},
|
||||
{Name: "SAT_MODE", Kind: KindText, Category: "Propagation", Promoted: true},
|
||||
{Name: "ANT_AZ", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "ANT_EL", Kind: KindNumber, Category: "Propagation", Promoted: true},
|
||||
{Name: "ANT_PATH", Kind: KindEnum, Category: "Propagation", Promoted: true},
|
||||
{Name: "FORCE_INIT", Kind: KindBool, Category: "Propagation"},
|
||||
{Name: "MAX_BURSTS", Kind: KindNumber, Category: "Propagation"},
|
||||
{Name: "MS_SHOWER", Kind: KindText, Category: "Propagation"},
|
||||
{Name: "NR_BURSTS", Kind: KindNumber, Category: "Propagation"},
|
||||
{Name: "NR_PINGS", Kind: KindNumber, Category: "Propagation"},
|
||||
|
||||
// ── QSL / confirmations ─────────────────────────────────────────
|
||||
{Name: "QSL_SENT", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "QSL_RCVD", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "QSLSDATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "QSLRDATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "QSL_VIA", Kind: KindText, Category: "QSL", Promoted: true},
|
||||
{Name: "QSL_SENT_VIA", Kind: KindEnum, Category: "QSL"},
|
||||
{Name: "QSL_RCVD_VIA", Kind: KindEnum, Category: "QSL"},
|
||||
{Name: "QSLMSG", Kind: KindText, Category: "QSL", Promoted: true},
|
||||
{Name: "QSLMSG_INTL", Kind: KindText, Category: "QSL", Intl: true},
|
||||
{Name: "QSLMSG_RCVD", Kind: KindText, Category: "QSL", Promoted: true},
|
||||
{Name: "LOTW_QSL_SENT", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "LOTW_QSL_RCVD", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "LOTW_QSLSDATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "LOTW_QSLRDATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "EQSL_QSL_SENT", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "EQSL_QSL_RCVD", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "EQSL_QSLSDATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "EQSL_QSLRDATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "EQSL_AG", Kind: KindBool, Category: "QSL"},
|
||||
{Name: "DCL_QSL_SENT", Kind: KindEnum, Category: "QSL"},
|
||||
{Name: "DCL_QSL_RCVD", Kind: KindEnum, Category: "QSL"},
|
||||
{Name: "DCL_QSLSDATE", Kind: KindDate, Category: "QSL"},
|
||||
{Name: "DCL_QSLRDATE", Kind: KindDate, Category: "QSL"},
|
||||
{Name: "CLUBLOG_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "CLUBLOG_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "HRDLOG_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "HRDLOG_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "QRZCOM_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "QRZCOM_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "QRZCOM_QSO_DOWNLOAD_DATE", Kind: KindDate, Category: "QSL", Promoted: true},
|
||||
{Name: "QRZCOM_QSO_DOWNLOAD_STATUS", Kind: KindEnum, Category: "QSL", Promoted: true},
|
||||
{Name: "HAMLOGEU_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL"},
|
||||
{Name: "HAMLOGEU_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL"},
|
||||
{Name: "HAMQTH_QSO_UPLOAD_DATE", Kind: KindDate, Category: "QSL"},
|
||||
{Name: "HAMQTH_QSO_UPLOAD_STATUS", Kind: KindEnum, Category: "QSL"},
|
||||
|
||||
// ── Awards / credits ────────────────────────────────────────────
|
||||
{Name: "CREDIT_SUBMITTED", Kind: KindText, Category: "Awards", Promoted: true},
|
||||
{Name: "CREDIT_GRANTED", Kind: KindText, Category: "Awards", Promoted: true},
|
||||
{Name: "AWARD_SUBMITTED", Kind: KindText, Category: "Awards"},
|
||||
{Name: "AWARD_GRANTED", Kind: KindText, Category: "Awards"},
|
||||
|
||||
// ── Contest ─────────────────────────────────────────────────────
|
||||
{Name: "CONTEST_ID", Kind: KindText, Category: "Contest", Promoted: true},
|
||||
{Name: "SRX", Kind: KindNumber, Category: "Contest", Promoted: true},
|
||||
{Name: "STX", Kind: KindNumber, Category: "Contest", Promoted: true},
|
||||
{Name: "SRX_STRING", Kind: KindText, Category: "Contest", Promoted: true},
|
||||
{Name: "STX_STRING", Kind: KindText, Category: "Contest", Promoted: true},
|
||||
{Name: "CHECK", Kind: KindText, Category: "Contest", Promoted: true},
|
||||
{Name: "CLASS", Kind: KindText, Category: "Contest", Promoted: true},
|
||||
{Name: "PRECEDENCE", Kind: KindText, Category: "Contest", Promoted: true},
|
||||
{Name: "ARRL_SECT", Kind: KindEnum, Category: "Contest", Promoted: true},
|
||||
|
||||
// ── Club memberships ────────────────────────────────────────────
|
||||
{Name: "SKCC", Kind: KindText, Category: "Clubs", Promoted: true},
|
||||
{Name: "FISTS", Kind: KindNumber, Category: "Clubs", Promoted: true},
|
||||
{Name: "FISTS_CC", Kind: KindNumber, Category: "Clubs"},
|
||||
{Name: "TEN_TEN", Kind: KindNumber, Category: "Clubs", Promoted: true},
|
||||
{Name: "UKSMG", Kind: KindNumber, Category: "Clubs"},
|
||||
{Name: "DARC_DOK", Kind: KindText, Category: "Clubs", Promoted: true},
|
||||
|
||||
// ── Morse key (3.1.5+) ──────────────────────────────────────────
|
||||
{Name: "MORSE_KEY_TYPE", Kind: KindEnum, Category: "Morse key"},
|
||||
{Name: "MORSE_KEY_INFO", Kind: KindText, Category: "Morse key"},
|
||||
|
||||
// ── Misc / crypto ───────────────────────────────────────────────
|
||||
{Name: "COMMENT", Kind: KindText, Category: "Misc", Promoted: true},
|
||||
{Name: "COMMENT_INTL", Kind: KindText, Category: "Misc", Intl: true},
|
||||
{Name: "NOTES", Kind: KindText, Category: "Misc", Promoted: true},
|
||||
{Name: "NOTES_INTL", Kind: KindText, Category: "Misc", Intl: true},
|
||||
{Name: "PUBLIC_KEY", Kind: KindText, Category: "Misc"},
|
||||
{Name: "VE_PROV", Kind: KindText, Category: "Misc", Deprecated: true},
|
||||
|
||||
// ── My station / operator ───────────────────────────────────────
|
||||
{Name: "STATION_CALLSIGN", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "OPERATOR", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_NAME", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_NAME_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_GRIDSQUARE", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_GRIDSQUARE_EXT", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_VUCC_GRIDS", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_COUNTRY", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_COUNTRY_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_STATE", Kind: KindEnum, Category: "My station", Promoted: true},
|
||||
{Name: "MY_CNTY", Kind: KindEnum, Category: "My station", Promoted: true},
|
||||
{Name: "MY_CNTY_ALT", Kind: KindEnum, Category: "My station"},
|
||||
{Name: "MY_DXCC", Kind: KindEnum, Category: "My station", Promoted: true},
|
||||
{Name: "MY_CQ_ZONE", Kind: KindNumber, Category: "My station", Promoted: true},
|
||||
{Name: "MY_ITU_ZONE", Kind: KindNumber, Category: "My station", Promoted: true},
|
||||
{Name: "MY_IOTA", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_IOTA_ISLAND_ID", Kind: KindText, Category: "My station"},
|
||||
{Name: "MY_SOTA_REF", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_POTA_REF", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_WWFF_REF", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_SIG", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_SIG_INFO", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_SIG_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_SIG_INFO_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_LAT", Kind: KindLoc, Category: "My station", Promoted: true},
|
||||
{Name: "MY_LON", Kind: KindLoc, Category: "My station", Promoted: true},
|
||||
{Name: "MY_ALTITUDE", Kind: KindNumber, Category: "My station"},
|
||||
{Name: "MY_STREET", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_STREET_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_CITY", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_CITY_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_POSTAL_CODE", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_POSTAL_CODE_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_RIG", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_RIG_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_ANTENNA", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_ANTENNA_INTL", Kind: KindText, Category: "My station", Intl: true},
|
||||
{Name: "MY_ARRL_SECT", Kind: KindEnum, Category: "My station", Promoted: true},
|
||||
{Name: "MY_USACA_COUNTIES", Kind: KindText, Category: "My station"},
|
||||
{Name: "MY_DARC_DOK", Kind: KindText, Category: "My station", Promoted: true},
|
||||
{Name: "MY_FISTS", Kind: KindNumber, Category: "My station"},
|
||||
{Name: "MY_MORSE_KEY_TYPE", Kind: KindEnum, Category: "My station"},
|
||||
{Name: "MY_MORSE_KEY_INFO", Kind: KindText, Category: "My station"},
|
||||
}
|
||||
|
||||
// fieldIndex maps an uppercase ADIF tag to its definition for O(1) lookup.
|
||||
var fieldIndex = func() map[string]FieldDef {
|
||||
m := make(map[string]FieldDef, len(Fields))
|
||||
for _, f := range Fields {
|
||||
m[f.Name] = f
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// IsStandardField reports whether tag (any case) is a defined ADIF 3.1.7
|
||||
// field. APP_* and USERDEF tags are non-standard and return false.
|
||||
func IsStandardField(tag string) bool {
|
||||
_, ok := fieldIndex[strings.ToUpper(strings.TrimSpace(tag))]
|
||||
return ok
|
||||
}
|
||||
|
||||
// LookupField returns the definition for a tag (any case), ok=false if unknown.
|
||||
func LookupField(tag string) (FieldDef, bool) {
|
||||
f, ok := fieldIndex[strings.ToUpper(strings.TrimSpace(tag))]
|
||||
return f, ok
|
||||
}
|
||||
@@ -284,6 +284,12 @@ var adifPromoted = stringSet(
|
||||
"my_street", "my_city", "my_postal_code", "my_rig", "my_antenna",
|
||||
// Misc
|
||||
"tx_pwr", "comment", "notes",
|
||||
// ADIF 3.1.7 additional promoted fields
|
||||
"sig", "sig_info", "my_sig", "my_sig_info", "wwff_ref", "my_wwff_ref",
|
||||
"distance", "rx_pwr", "a_index", "k_index", "sfi",
|
||||
"skcc", "fists", "ten_ten", "contacted_op", "eq_call", "pfx", "my_name", "class",
|
||||
"darc_dok", "my_darc_dok", "region", "silent_key", "swl", "qso_complete", "qso_random",
|
||||
"credit_granted", "credit_submitted", "my_arrl_sect", "my_vucc_grids",
|
||||
)
|
||||
|
||||
func stringSet(items ...string) map[string]struct{} {
|
||||
@@ -482,6 +488,48 @@ func recordToQSO(rec Record) (qso.QSO, bool) {
|
||||
q.Comment = rec["comment"]
|
||||
q.Notes = rec["notes"]
|
||||
|
||||
// ADIF 3.1.7 additional promoted fields
|
||||
q.SIG = rec["sig"]
|
||||
q.SIGInfo = rec["sig_info"]
|
||||
q.MySIG = rec["my_sig"]
|
||||
q.MySIGInfo = rec["my_sig_info"]
|
||||
q.WWFFRef = strings.ToUpper(rec["wwff_ref"])
|
||||
q.MyWWFFRef = strings.ToUpper(rec["my_wwff_ref"])
|
||||
if v, ok := parseFloat(rec["distance"]); ok {
|
||||
q.Distance = &v
|
||||
}
|
||||
if v, ok := parseFloat(rec["rx_pwr"]); ok {
|
||||
q.RXPower = &v
|
||||
}
|
||||
if v, ok := parseFloat(rec["a_index"]); ok {
|
||||
q.AIndex = &v
|
||||
}
|
||||
if v, ok := parseFloat(rec["k_index"]); ok {
|
||||
q.KIndex = &v
|
||||
}
|
||||
if v, ok := parseFloat(rec["sfi"]); ok {
|
||||
q.SFI = &v
|
||||
}
|
||||
q.SKCC = rec["skcc"]
|
||||
q.FISTS = rec["fists"]
|
||||
q.TenTen = rec["ten_ten"]
|
||||
q.ContactedOp = strings.ToUpper(rec["contacted_op"])
|
||||
q.EqCall = strings.ToUpper(rec["eq_call"])
|
||||
q.PFX = strings.ToUpper(rec["pfx"])
|
||||
q.MyName = rec["my_name"]
|
||||
q.Class = rec["class"]
|
||||
q.DarcDOK = rec["darc_dok"]
|
||||
q.MyDarcDOK = rec["my_darc_dok"]
|
||||
q.Region = rec["region"]
|
||||
q.SilentKey = strings.ToUpper(rec["silent_key"])
|
||||
q.SWL = strings.ToUpper(rec["swl"])
|
||||
q.QSOComplete = rec["qso_complete"]
|
||||
q.QSORandom = strings.ToUpper(rec["qso_random"])
|
||||
q.CreditGranted = rec["credit_granted"]
|
||||
q.CreditSubmitted = rec["credit_submitted"]
|
||||
q.MyARRLSect = strings.ToUpper(rec["my_arrl_sect"])
|
||||
q.MyVUCCGrids = strings.ToUpper(rec["my_vucc_grids"])
|
||||
|
||||
// Everything else lands in extras (uppercased ADIF names).
|
||||
var extras map[string]string
|
||||
for k, v := range rec {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package adif
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hamlog/internal/qso"
|
||||
)
|
||||
|
||||
// TestPromotedFieldsRoundTrip writes a QSO carrying the ADIF 3.1.7 promoted
|
||||
// fields, parses it back, and checks they survive — guarding the export
|
||||
// writeRecord ↔ import recordToQSO field-name mapping against typos.
|
||||
func TestPromotedFieldsRoundTrip(t *testing.T) {
|
||||
dist := 1234.5
|
||||
rxp := 5.0
|
||||
a := 12.0
|
||||
in := qso.QSO{
|
||||
Callsign: "EA8ABC", Band: "20m", Mode: "SSB",
|
||||
QSODate: time.Date(2026, 6, 6, 12, 0, 0, 0, time.UTC),
|
||||
SIG: "POTA", SIGInfo: "US-0001", MySIG: "WWFF", MySIGInfo: "ONFF-0001",
|
||||
WWFFRef: "ONFF-0001", MyWWFFRef: "F-FFF-0001",
|
||||
Distance: &dist, RXPower: &rxp, AIndex: &a,
|
||||
SKCC: "12345S", FISTS: "999", TenTen: "55555",
|
||||
ContactedOp: "EA8XYZ", EqCall: "EA8OLD", PFX: "EA8", MyName: "Greg",
|
||||
Class: "1A", DarcDOK: "A01", MyDarcDOK: "B02", Region: "IV",
|
||||
SilentKey: "N", SWL: "N", QSOComplete: "Y", QSORandom: "Y",
|
||||
CreditGranted: "DXCC", CreditSubmitted: "WAS",
|
||||
MyARRLSect: "EMA", MyVUCCGrids: "FN20,FN21",
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
bw := bufio.NewWriter(&buf)
|
||||
bw.WriteString("<EOH>\n")
|
||||
writeRecord(bw, in, true)
|
||||
bw.Flush()
|
||||
|
||||
var rec Record
|
||||
if err := Parse(strings.NewReader(buf.String()), func(r Record) error { rec = r; return nil }); err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
out, ok := recordToQSO(rec)
|
||||
if !ok {
|
||||
t.Fatal("recordToQSO returned !ok")
|
||||
}
|
||||
|
||||
checks := map[string]struct{ got, want string }{
|
||||
"SIG": {out.SIG, in.SIG},
|
||||
"SIG_INFO": {out.SIGInfo, in.SIGInfo},
|
||||
"MY_SIG": {out.MySIG, in.MySIG},
|
||||
"MY_SIG_INFO": {out.MySIGInfo, in.MySIGInfo},
|
||||
"WWFF_REF": {out.WWFFRef, in.WWFFRef},
|
||||
"MY_WWFF_REF": {out.MyWWFFRef, in.MyWWFFRef},
|
||||
"SKCC": {out.SKCC, in.SKCC},
|
||||
"FISTS": {out.FISTS, in.FISTS},
|
||||
"TEN_TEN": {out.TenTen, in.TenTen},
|
||||
"CONTACTED_OP": {out.ContactedOp, in.ContactedOp},
|
||||
"EQ_CALL": {out.EqCall, in.EqCall},
|
||||
"PFX": {out.PFX, in.PFX},
|
||||
"MY_NAME": {out.MyName, in.MyName},
|
||||
"CLASS": {out.Class, in.Class},
|
||||
"DARC_DOK": {out.DarcDOK, in.DarcDOK},
|
||||
"MY_DARC_DOK": {out.MyDarcDOK, in.MyDarcDOK},
|
||||
"REGION": {out.Region, in.Region},
|
||||
"SILENT_KEY": {out.SilentKey, in.SilentKey},
|
||||
"SWL": {out.SWL, in.SWL},
|
||||
"QSO_COMPLETE": {out.QSOComplete, in.QSOComplete},
|
||||
"QSO_RANDOM": {out.QSORandom, in.QSORandom},
|
||||
"CREDIT_GRANTED": {out.CreditGranted, in.CreditGranted},
|
||||
"CREDIT_SUBMITTED": {out.CreditSubmitted, in.CreditSubmitted},
|
||||
"MY_ARRL_SECT": {out.MyARRLSect, in.MyARRLSect},
|
||||
"MY_VUCC_GRIDS": {out.MyVUCCGrids, in.MyVUCCGrids},
|
||||
}
|
||||
for tag, c := range checks {
|
||||
if c.got != c.want {
|
||||
t.Errorf("%s round-trip = %q, want %q", tag, c.got, c.want)
|
||||
}
|
||||
}
|
||||
if out.Distance == nil || *out.Distance != dist {
|
||||
t.Errorf("DISTANCE round-trip = %v, want %v", out.Distance, dist)
|
||||
}
|
||||
if out.RXPower == nil || *out.RXPower != rxp {
|
||||
t.Errorf("RX_PWR round-trip = %v, want %v", out.RXPower, rxp)
|
||||
}
|
||||
if out.AIndex == nil || *out.AIndex != a {
|
||||
t.Errorf("A_INDEX round-trip = %v, want %v", out.AIndex, a)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStandardExportDropsNonStandard verifies that standard mode strips
|
||||
// vendor/APP tags while full mode keeps them.
|
||||
func TestStandardExportDropsNonStandard(t *testing.T) {
|
||||
q := qso.QSO{
|
||||
Callsign: "F4BPO", Band: "20m", Mode: "CW",
|
||||
Extras: map[string]string{
|
||||
"APP_LOG4OM_FOO": "x",
|
||||
"DARC_DOK": "A01", // standard → kept in both
|
||||
"MY_VENDOR_TAG": "y", // non-standard → dropped in standard mode
|
||||
},
|
||||
}
|
||||
|
||||
standard := renderRecord(q, false)
|
||||
if strings.Contains(standard, "APP_LOG4OM_FOO") || strings.Contains(standard, "MY_VENDOR_TAG") {
|
||||
t.Errorf("standard export should drop non-standard tags:\n%s", standard)
|
||||
}
|
||||
|
||||
full := renderRecord(q, true)
|
||||
if !strings.Contains(full, "APP_LOG4OM_FOO") || !strings.Contains(full, "MY_VENDOR_TAG") {
|
||||
t.Errorf("full export should keep all extras:\n%s", full)
|
||||
}
|
||||
}
|
||||
|
||||
func renderRecord(q qso.QSO, includeApp bool) string {
|
||||
var buf bytes.Buffer
|
||||
bw := bufio.NewWriter(&buf)
|
||||
writeRecord(bw, q, includeApp)
|
||||
bw.Flush()
|
||||
return buf.String()
|
||||
}
|
||||
@@ -210,8 +210,13 @@ type refList struct {
|
||||
byCode map[string]RefMeta // uppercased code → metadata
|
||||
codes []string // codes in input order (for stable unworked listing)
|
||||
withPattern []string // codes whose reference declares a regex (usually none)
|
||||
names []nameCode // (uppercased name → code) for MatchBy="description"
|
||||
}
|
||||
|
||||
// nameCode pairs a reference's uppercased description with its code, for
|
||||
// description-based matching (e.g. WAJA finding a prefecture NAME in the QTH).
|
||||
type nameCode struct{ name, code string }
|
||||
|
||||
// RefMeta is one reference's metadata for the engine: enough to enforce a
|
||||
// predefined list, per-reference DXCC scoping, a per-reference pattern, and to
|
||||
// label results.
|
||||
@@ -243,6 +248,9 @@ func NewRefList(metas []RefMeta) refList {
|
||||
m.Code = code
|
||||
if _, dup := rl.byCode[code]; !dup {
|
||||
rl.codes = append(rl.codes, code)
|
||||
if nm := strings.ToUpper(strings.TrimSpace(m.Name)); nm != "" {
|
||||
rl.names = append(rl.names, nameCode{name: nm, code: code})
|
||||
}
|
||||
}
|
||||
rl.byCode[code] = m
|
||||
}
|
||||
@@ -376,6 +384,15 @@ func Compute(defs []Def, qsos []qso.QSO, refMetas map[string][]RefMeta, nameOf N
|
||||
for _, b := range sortedBands(bandWorked) {
|
||||
r.Bands = append(r.Bands, BandCount{Band: b, Worked: bandWorked[b], Confirmed: bandConfirmed[b]})
|
||||
}
|
||||
// Never return nil slices: they marshal to JSON null, and the UI calls
|
||||
// .filter/.length on them (an award with nothing worked yet — e.g. a
|
||||
// freshly-created WWFF/WAJA — would otherwise white-screen the panel).
|
||||
if r.Refs == nil {
|
||||
r.Refs = []Ref{}
|
||||
}
|
||||
if r.Bands == nil {
|
||||
r.Bands = []BandCount{}
|
||||
}
|
||||
out[i] = r
|
||||
}
|
||||
return out
|
||||
@@ -428,12 +445,27 @@ func candidates(d *Def, re *regexp.Regexp, q *qso.QSO, rl refList, hasList bool)
|
||||
return nil
|
||||
}
|
||||
predefined := hasList && !d.Dynamic
|
||||
byDesc := predefined && strings.EqualFold(strings.TrimSpace(d.MatchBy), "description")
|
||||
|
||||
var found []string
|
||||
switch {
|
||||
case re != nil:
|
||||
// Award-level regex: capture group 1 (or whole match) for each hit.
|
||||
found = regexTokens(re, raw)
|
||||
case byDesc:
|
||||
// Match references by their DESCRIPTION/name appearing in the field
|
||||
// (e.g. WAJA finds the prefecture name inside the QTH). ExactMatch means
|
||||
// the field equals the name; otherwise the name is a substring of it.
|
||||
up := strings.ToUpper(raw)
|
||||
for _, nc := range rl.names {
|
||||
if d.ExactMatch {
|
||||
if up == nc.name {
|
||||
found = append(found, nc.code)
|
||||
}
|
||||
} else if strings.Contains(up, nc.name) {
|
||||
found = append(found, nc.code)
|
||||
}
|
||||
}
|
||||
case predefined && !d.ExactMatch:
|
||||
// "Search reference inside the field": look up each token of the field in
|
||||
// the list — O(tokens), not O(all references) — plus test the few
|
||||
|
||||
@@ -96,6 +96,38 @@ func TestComputeMultiRef(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// WAJA-style award: MatchBy="description", non-exact, scanning the QTH for a
|
||||
// reference's NAME (the prefecture). Also guards against the nil-slice crash:
|
||||
// an award with nothing worked must return empty (non-nil) Refs/Bands.
|
||||
func TestComputeMatchByDescription(t *testing.T) {
|
||||
def := Def{Code: "WAJA", Type: TypeQSOFields, Field: "qth", MatchBy: "description",
|
||||
DXCCFilter: []int{339}, Confirm: []string{"lotw", "qsl"}, Valid: true}
|
||||
qsos := []qso.QSO{
|
||||
{Callsign: "JA1ABC", Band: "20m", DXCC: ip(339), QTH: "Tokyo city", LOTWRcvd: "Y"},
|
||||
{Callsign: "JA3DEF", Band: "40m", DXCC: ip(339), QTH: "Osaka"},
|
||||
{Callsign: "JA9XYZ", Band: "20m", DXCC: ip(339), QTH: "nowhere special"}, // no prefecture name
|
||||
}
|
||||
refMetas := map[string][]RefMeta{"WAJA": {
|
||||
{Code: "100", Name: "Tokyo", Valid: true},
|
||||
{Code: "270", Name: "Osaka", Valid: true},
|
||||
{Code: "010", Name: "Hokkaido", Valid: true},
|
||||
}}
|
||||
r := Compute([]Def{def}, qsos, refMetas, nil)[0]
|
||||
if r.Worked != 2 { // Tokyo + Osaka found by name inside QTH
|
||||
t.Errorf("WAJA worked = %d, want 2 (%v)", r.Worked, refCodes(r))
|
||||
}
|
||||
if r.Total != 3 { // predefined denominator = list size
|
||||
t.Errorf("WAJA total = %d, want 3", r.Total)
|
||||
}
|
||||
|
||||
// Nil-slice guard: an award with zero worked refs must still return
|
||||
// non-nil (empty) Refs/Bands so the JSON isn't null (UI white-screen).
|
||||
empty := Compute([]Def{{Code: "WWFF", Type: TypeReference, Field: "wwff", Dynamic: true, Valid: true}}, nil, nil, nil)[0]
|
||||
if empty.Refs == nil || empty.Bands == nil {
|
||||
t.Errorf("empty award must have non-nil Refs/Bands, got Refs=%v Bands=%v", empty.Refs, empty.Bands)
|
||||
}
|
||||
}
|
||||
|
||||
func refCodes(r Result) []string {
|
||||
out := make([]string, 0, len(r.Refs))
|
||||
for _, rf := range r.Refs {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
-- Promote ~30 more ADIF 3.1.7 fields to dedicated columns so they are
|
||||
-- editable, queryable and exported as proper tags (rather than living only
|
||||
-- in extras_json). The long tail of rarely-used fields still rides in
|
||||
-- extras_json and is reachable via the generic "ADIF fields" editor.
|
||||
-- SQLite ADD COLUMN is metadata-only — fast even on large logbooks.
|
||||
|
||||
-- --- Special-activity group (POTA/SOTA/WWFF/SIG) ---
|
||||
ALTER TABLE qso ADD COLUMN sig TEXT; -- e.g. "POTA", "WWFF"
|
||||
ALTER TABLE qso ADD COLUMN sig_info TEXT; -- the reference for SIG
|
||||
ALTER TABLE qso ADD COLUMN my_sig TEXT;
|
||||
ALTER TABLE qso ADD COLUMN my_sig_info TEXT;
|
||||
ALTER TABLE qso ADD COLUMN wwff_ref TEXT; -- contacted WWFF reference
|
||||
ALTER TABLE qso ADD COLUMN my_wwff_ref TEXT; -- my WWFF activation
|
||||
|
||||
-- --- Distance / power / space weather ---
|
||||
ALTER TABLE qso ADD COLUMN distance REAL; -- km
|
||||
ALTER TABLE qso ADD COLUMN rx_pwr REAL; -- contacted station power (W)
|
||||
ALTER TABLE qso ADD COLUMN a_index REAL;
|
||||
ALTER TABLE qso ADD COLUMN k_index REAL;
|
||||
ALTER TABLE qso ADD COLUMN sfi REAL; -- solar flux index
|
||||
|
||||
-- --- Club memberships ---
|
||||
ALTER TABLE qso ADD COLUMN skcc TEXT; -- can carry suffix letters
|
||||
ALTER TABLE qso ADD COLUMN fists TEXT;
|
||||
ALTER TABLE qso ADD COLUMN ten_ten TEXT;
|
||||
|
||||
-- --- Contacted / station identity ---
|
||||
ALTER TABLE qso ADD COLUMN contacted_op TEXT; -- the actual operator worked
|
||||
ALTER TABLE qso ADD COLUMN eq_call TEXT; -- former / alternate callsign
|
||||
ALTER TABLE qso ADD COLUMN pfx TEXT; -- WPX prefix
|
||||
ALTER TABLE qso ADD COLUMN my_name TEXT;
|
||||
ALTER TABLE qso ADD COLUMN class TEXT; -- Field Day class
|
||||
|
||||
-- --- German DOK / region ---
|
||||
ALTER TABLE qso ADD COLUMN darc_dok TEXT;
|
||||
ALTER TABLE qso ADD COLUMN my_darc_dok TEXT;
|
||||
ALTER TABLE qso ADD COLUMN region TEXT;
|
||||
|
||||
-- --- Flags ---
|
||||
ALTER TABLE qso ADD COLUMN silent_key TEXT; -- Y/N
|
||||
ALTER TABLE qso ADD COLUMN swl TEXT; -- Y/N (SWL report)
|
||||
ALTER TABLE qso ADD COLUMN qso_complete TEXT; -- Y/N/NIL/?
|
||||
ALTER TABLE qso ADD COLUMN qso_random TEXT; -- Y/N
|
||||
|
||||
-- --- Award credits ---
|
||||
ALTER TABLE qso ADD COLUMN credit_granted TEXT;
|
||||
ALTER TABLE qso ADD COLUMN credit_submitted TEXT;
|
||||
|
||||
-- --- My station extras ---
|
||||
ALTER TABLE qso ADD COLUMN my_arrl_sect TEXT;
|
||||
ALTER TABLE qso ADD COLUMN my_vucc_grids TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_qso_sig ON qso(sig);
|
||||
CREATE INDEX IF NOT EXISTS idx_qso_wwff_ref ON qso(wwff_ref);
|
||||
CREATE INDEX IF NOT EXISTS idx_qso_skcc ON qso(skcc);
|
||||
+110
-4
@@ -156,8 +156,42 @@ type QSO struct {
|
||||
Comment string `json:"comment,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
|
||||
// --- ADIF 3.1.7 additional promoted fields ---
|
||||
// Kept in one block so columnList / args() / scanQSO stay trivially in
|
||||
// sync (they are appended at the end, before extras_json).
|
||||
SIG string `json:"sig,omitempty"`
|
||||
SIGInfo string `json:"sig_info,omitempty"`
|
||||
MySIG string `json:"my_sig,omitempty"`
|
||||
MySIGInfo string `json:"my_sig_info,omitempty"`
|
||||
WWFFRef string `json:"wwff_ref,omitempty"`
|
||||
MyWWFFRef string `json:"my_wwff_ref,omitempty"`
|
||||
Distance *float64 `json:"distance,omitempty"`
|
||||
RXPower *float64 `json:"rx_pwr,omitempty"`
|
||||
AIndex *float64 `json:"a_index,omitempty"`
|
||||
KIndex *float64 `json:"k_index,omitempty"`
|
||||
SFI *float64 `json:"sfi,omitempty"`
|
||||
SKCC string `json:"skcc,omitempty"`
|
||||
FISTS string `json:"fists,omitempty"`
|
||||
TenTen string `json:"ten_ten,omitempty"`
|
||||
ContactedOp string `json:"contacted_op,omitempty"`
|
||||
EqCall string `json:"eq_call,omitempty"`
|
||||
PFX string `json:"pfx,omitempty"`
|
||||
MyName string `json:"my_name,omitempty"`
|
||||
Class string `json:"class,omitempty"`
|
||||
DarcDOK string `json:"darc_dok,omitempty"`
|
||||
MyDarcDOK string `json:"my_darc_dok,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
SilentKey string `json:"silent_key,omitempty"`
|
||||
SWL string `json:"swl,omitempty"`
|
||||
QSOComplete string `json:"qso_complete,omitempty"`
|
||||
QSORandom string `json:"qso_random,omitempty"`
|
||||
CreditGranted string `json:"credit_granted,omitempty"`
|
||||
CreditSubmitted string `json:"credit_submitted,omitempty"`
|
||||
MyARRLSect string `json:"my_arrl_sect,omitempty"`
|
||||
MyVUCCGrids string `json:"my_vucc_grids,omitempty"`
|
||||
|
||||
// Extras holds ADIF fields not promoted to columns. Keys are uppercase
|
||||
// ADIF field names (e.g. "DARC_DOK"); values are the raw string content.
|
||||
// ADIF field names (e.g. "MS_SHOWER"); values are the raw string content.
|
||||
Extras map[string]string `json:"extras,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -205,7 +239,13 @@ const columnList = `callsign, qso_date, qso_date_off, band, band_rx, mode, submo
|
||||
station_callsign, operator, my_grid, my_gridsquare_ext, my_country, my_state, my_cnty, my_iota,
|
||||
my_sota_ref, my_pota_ref, my_dxcc, my_cq_zone, my_itu_zone, my_lat, my_lon,
|
||||
my_street, my_city, my_postal_code, my_rig, my_antenna,
|
||||
tx_pwr, comment, notes, extras_json`
|
||||
tx_pwr, comment, notes,
|
||||
sig, sig_info, my_sig, my_sig_info, wwff_ref, my_wwff_ref,
|
||||
distance, rx_pwr, a_index, k_index, sfi,
|
||||
skcc, fists, ten_ten, contacted_op, eq_call, pfx, my_name, class,
|
||||
darc_dok, my_darc_dok, region, silent_key, swl, qso_complete, qso_random,
|
||||
credit_granted, credit_submitted, my_arrl_sect, my_vucc_grids,
|
||||
extras_json`
|
||||
|
||||
const selectCols = `id, ` + columnList + `, created_at, updated_at`
|
||||
|
||||
@@ -258,7 +298,13 @@ func (q *QSO) args() []any {
|
||||
q.StationCallsign, q.Operator, q.MyGrid, q.MyGridExt, q.MyCountry, q.MyState, q.MyCounty, q.MyIOTA,
|
||||
q.MySOTARef, q.MyPOTARef, q.MyDXCC, q.MyCQZone, q.MyITUZone, q.MyLat, q.MyLon,
|
||||
q.MyStreet, q.MyCity, q.MyPostalCode, q.MyRig, q.MyAntenna,
|
||||
q.TXPower, q.Comment, q.Notes, extras,
|
||||
q.TXPower, q.Comment, q.Notes,
|
||||
q.SIG, q.SIGInfo, q.MySIG, q.MySIGInfo, q.WWFFRef, q.MyWWFFRef,
|
||||
q.Distance, q.RXPower, q.AIndex, q.KIndex, q.SFI,
|
||||
q.SKCC, q.FISTS, q.TenTen, q.ContactedOp, q.EqCall, q.PFX, q.MyName, q.Class,
|
||||
q.DarcDOK, q.MyDarcDOK, q.Region, q.SilentKey, q.SWL, q.QSOComplete, q.QSORandom,
|
||||
q.CreditGranted, q.CreditSubmitted, q.MyARRLSect, q.MyVUCCGrids,
|
||||
extras,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1472,6 +1518,15 @@ func scanQSO(s scanner) (QSO, error) {
|
||||
myRig, myAntenna sql.NullString
|
||||
txp sql.NullFloat64
|
||||
comment, notes sql.NullString
|
||||
sig, sigInfo, mySig, mySigInfo sql.NullString
|
||||
wwffRef, myWWFFRef sql.NullString
|
||||
distance, rxPwr, aIndex, kIndex, sfi sql.NullFloat64
|
||||
skcc, fists, tenTen sql.NullString
|
||||
contactedOp, eqCall, pfx, myName sql.NullString
|
||||
class, darcDOK, myDarcDOK, region sql.NullString
|
||||
silentKey, swl, qsoComplete, qsoRandom sql.NullString
|
||||
creditGranted, creditSubmitted sql.NullString
|
||||
myARRLSect, myVUCCGrids sql.NullString
|
||||
extrasJSON sql.NullString
|
||||
createdStr, updatedStr string
|
||||
)
|
||||
@@ -1494,7 +1549,13 @@ func scanQSO(s scanner) (QSO, error) {
|
||||
&stCall, &op, &myGrid, &myGridExt, &myCountry, &myState, &myCnty, &myIOTA,
|
||||
&mySOTA, &myPOTA, &myDXCC, &myCQZ, &myITUZ, &myLat, &myLon,
|
||||
&myStreet, &myCity, &myPostal, &myRig, &myAntenna,
|
||||
&txp, &comment, ¬es, &extrasJSON, &createdStr, &updatedStr,
|
||||
&txp, &comment, ¬es,
|
||||
&sig, &sigInfo, &mySig, &mySigInfo, &wwffRef, &myWWFFRef,
|
||||
&distance, &rxPwr, &aIndex, &kIndex, &sfi,
|
||||
&skcc, &fists, &tenTen, &contactedOp, &eqCall, &pfx, &myName, &class,
|
||||
&darcDOK, &myDarcDOK, ®ion, &silentKey, &swl, &qsoComplete, &qsoRandom,
|
||||
&creditGranted, &creditSubmitted, &myARRLSect, &myVUCCGrids,
|
||||
&extrasJSON, &createdStr, &updatedStr,
|
||||
); err != nil {
|
||||
return QSO{}, fmt.Errorf("scan qso: %w", err)
|
||||
}
|
||||
@@ -1645,6 +1706,51 @@ func scanQSO(s scanner) (QSO, error) {
|
||||
}
|
||||
q.Comment = comment.String
|
||||
q.Notes = notes.String
|
||||
q.SIG = sig.String
|
||||
q.SIGInfo = sigInfo.String
|
||||
q.MySIG = mySig.String
|
||||
q.MySIGInfo = mySigInfo.String
|
||||
q.WWFFRef = wwffRef.String
|
||||
q.MyWWFFRef = myWWFFRef.String
|
||||
if distance.Valid {
|
||||
v := distance.Float64
|
||||
q.Distance = &v
|
||||
}
|
||||
if rxPwr.Valid {
|
||||
v := rxPwr.Float64
|
||||
q.RXPower = &v
|
||||
}
|
||||
if aIndex.Valid {
|
||||
v := aIndex.Float64
|
||||
q.AIndex = &v
|
||||
}
|
||||
if kIndex.Valid {
|
||||
v := kIndex.Float64
|
||||
q.KIndex = &v
|
||||
}
|
||||
if sfi.Valid {
|
||||
v := sfi.Float64
|
||||
q.SFI = &v
|
||||
}
|
||||
q.SKCC = skcc.String
|
||||
q.FISTS = fists.String
|
||||
q.TenTen = tenTen.String
|
||||
q.ContactedOp = contactedOp.String
|
||||
q.EqCall = eqCall.String
|
||||
q.PFX = pfx.String
|
||||
q.MyName = myName.String
|
||||
q.Class = class.String
|
||||
q.DarcDOK = darcDOK.String
|
||||
q.MyDarcDOK = myDarcDOK.String
|
||||
q.Region = region.String
|
||||
q.SilentKey = silentKey.String
|
||||
q.SWL = swl.String
|
||||
q.QSOComplete = qsoComplete.String
|
||||
q.QSORandom = qsoRandom.String
|
||||
q.CreditGranted = creditGranted.String
|
||||
q.CreditSubmitted = creditSubmitted.String
|
||||
q.MyARRLSect = myARRLSect.String
|
||||
q.MyVUCCGrids = myVUCCGrids.String
|
||||
q.Extras = decodeExtras(extrasJSON.String)
|
||||
return q, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user