feat: ADIF export field picker — choose exactly which fields to write
Global "Export to ADIF" gains a third option "Choose fields…" next to the standard-only and full-OpsLog choices, and right-clicking selected QSOs adds "Export selected — choose fields…". The picker separates official ADIF 3.1.7 fields (grouped by category) from OpsLog/non-standard tags actually present in the log (discovered via DistinctExtraKeys over extras_json), with All/None per group and reset-to-defaults; the selection is persisted per user. Backend: adif.Exporter gains a Fields allow-set that gates every promoted and extras field in writeRecord; app.go ExportADIF/ExportADIFFiltered/ ExportADIFSelected take a fields []string param, plus new ADIFExtraFields.
This commit is contained in:
+162
-131
@@ -34,6 +34,12 @@ type Exporter struct {
|
||||
// export destined for another logger; set true for a full OpsLog→OpsLog
|
||||
// round-trip that preserves everything.
|
||||
IncludeAppFields bool
|
||||
|
||||
// Fields, when non-nil, restricts the export to exactly these ADIF tags
|
||||
// (uppercase) — the "choose which fields to export" mode. nil = write every
|
||||
// field (the standard/full behaviour above). When set it overrides
|
||||
// IncludeAppFields: an APP_/vendor tag is written iff it's in the set.
|
||||
Fields map[string]bool
|
||||
}
|
||||
|
||||
// iterator streams QSOs through fn. The three concrete sources (all, filtered,
|
||||
@@ -100,7 +106,7 @@ func (e *Exporter) writeDoc(ctx context.Context, w io.Writer, iter iterator) (in
|
||||
|
||||
count := 0
|
||||
err := iter(ctx, func(q qso.QSO) error {
|
||||
writeRecord(bw, q, e.IncludeAppFields)
|
||||
writeRecord(bw, q, e.IncludeAppFields, e.Fields)
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
@@ -115,7 +121,7 @@ func SingleRecordADIF(q qso.QSO) string {
|
||||
var b strings.Builder
|
||||
bw := bufio.NewWriter(&b)
|
||||
// Uploads target other services — keep it standard (no app-specific tags).
|
||||
writeRecord(bw, q, false)
|
||||
writeRecord(bw, q, false, nil)
|
||||
bw.Flush()
|
||||
return b.String()
|
||||
}
|
||||
@@ -127,7 +133,7 @@ func SingleRecordADIF(q qso.QSO) string {
|
||||
func FullRecordADIF(q qso.QSO) string {
|
||||
var b strings.Builder
|
||||
bw := bufio.NewWriter(&b)
|
||||
writeRecord(bw, q, true)
|
||||
writeRecord(bw, q, true, nil)
|
||||
bw.Flush()
|
||||
return b.String()
|
||||
}
|
||||
@@ -155,161 +161,181 @@ func BatchRecordsADIF(records []string) string {
|
||||
// Empty fields are omitted. MODE/SUBMODE are massaged so a "promoted"
|
||||
// mode (e.g. FT4 stored without a parent) is exported as the canonical
|
||||
// pair MODE=MFSK SUBMODE=FT4 — round-trips cleanly with strict loggers.
|
||||
func writeRecord(bw *bufio.Writer, q qso.QSO, includeApp bool) {
|
||||
func writeRecord(bw *bufio.Writer, q qso.QSO, includeApp bool, allow map[string]bool) {
|
||||
// allow == nil → write every promoted field (standard/full behaviour).
|
||||
// Otherwise a promoted tag is written only when it's in the chosen set.
|
||||
// w/wi/wf wrap the raw writers with that gate so the ~150 field lines below
|
||||
// stay one-liners.
|
||||
ok := func(tag string) bool { return allow == nil || allow[tag] }
|
||||
w := func(tag, v string) {
|
||||
if ok(tag) {
|
||||
writeField(bw, tag, v)
|
||||
}
|
||||
}
|
||||
wi := func(tag string, p *int) {
|
||||
if ok(tag) {
|
||||
writeIntPtr(bw, tag, p)
|
||||
}
|
||||
}
|
||||
wf := func(tag string, p *float64, decimals int) {
|
||||
if ok(tag) {
|
||||
writeFloatPtr(bw, tag, p, decimals)
|
||||
}
|
||||
}
|
||||
// --- Core ---
|
||||
writeField(bw, "CALL", q.Callsign)
|
||||
w("CALL", q.Callsign)
|
||||
|
||||
if !q.QSODate.IsZero() {
|
||||
writeField(bw, "QSO_DATE", q.QSODate.UTC().Format("20060102"))
|
||||
writeField(bw, "TIME_ON", q.QSODate.UTC().Format("150405"))
|
||||
w("QSO_DATE", q.QSODate.UTC().Format("20060102"))
|
||||
w("TIME_ON", q.QSODate.UTC().Format("150405"))
|
||||
}
|
||||
if !q.QSODateOff.IsZero() {
|
||||
writeField(bw, "QSO_DATE_OFF", q.QSODateOff.UTC().Format("20060102"))
|
||||
writeField(bw, "TIME_OFF", q.QSODateOff.UTC().Format("150405"))
|
||||
w("QSO_DATE_OFF", q.QSODateOff.UTC().Format("20060102"))
|
||||
w("TIME_OFF", q.QSODateOff.UTC().Format("150405"))
|
||||
}
|
||||
writeField(bw, "BAND", q.Band)
|
||||
writeField(bw, "BAND_RX", q.BandRX)
|
||||
w("BAND", q.Band)
|
||||
w("BAND_RX", q.BandRX)
|
||||
|
||||
mode, submode := modeForExport(q.Mode, q.Submode)
|
||||
writeField(bw, "MODE", mode)
|
||||
writeField(bw, "SUBMODE", submode)
|
||||
w("MODE", mode)
|
||||
w("SUBMODE", submode)
|
||||
|
||||
if q.FreqHz != nil && *q.FreqHz > 0 {
|
||||
writeField(bw, "FREQ", strconv.FormatFloat(float64(*q.FreqHz)/1_000_000, 'f', 6, 64))
|
||||
w("FREQ", strconv.FormatFloat(float64(*q.FreqHz)/1_000_000, 'f', 6, 64))
|
||||
}
|
||||
if q.FreqRXHz != nil && *q.FreqRXHz > 0 {
|
||||
writeField(bw, "FREQ_RX", strconv.FormatFloat(float64(*q.FreqRXHz)/1_000_000, 'f', 6, 64))
|
||||
w("FREQ_RX", strconv.FormatFloat(float64(*q.FreqRXHz)/1_000_000, 'f', 6, 64))
|
||||
}
|
||||
|
||||
writeField(bw, "RST_SENT", q.RSTSent)
|
||||
writeField(bw, "RST_RCVD", q.RSTRcvd)
|
||||
w("RST_SENT", q.RSTSent)
|
||||
w("RST_RCVD", q.RSTRcvd)
|
||||
|
||||
// --- Contacted ---
|
||||
writeField(bw, "NAME", q.Name)
|
||||
writeField(bw, "QTH", q.QTH)
|
||||
writeField(bw, "ADDRESS", q.Address)
|
||||
writeField(bw, "EMAIL", q.Email)
|
||||
writeField(bw, "WEB", q.Web)
|
||||
writeField(bw, "GRIDSQUARE", q.Grid)
|
||||
writeField(bw, "GRIDSQUARE_EXT", q.GridExt)
|
||||
writeField(bw, "VUCC_GRIDS", q.VUCCGrids)
|
||||
writeField(bw, "COUNTRY", q.Country)
|
||||
writeField(bw, "STATE", q.State)
|
||||
writeField(bw, "CNTY", q.County)
|
||||
writeIntPtr(bw, "DXCC", q.DXCC)
|
||||
writeField(bw, "CONT", q.Continent)
|
||||
writeIntPtr(bw, "CQZ", q.CQZ)
|
||||
writeIntPtr(bw, "ITUZ", q.ITUZ)
|
||||
writeField(bw, "IOTA", q.IOTA)
|
||||
writeField(bw, "SOTA_REF", q.SOTARef)
|
||||
writeField(bw, "POTA_REF", q.POTARef)
|
||||
writeIntPtr(bw, "AGE", q.Age)
|
||||
writeFloatPtr(bw, "LAT", q.Lat, 6)
|
||||
writeFloatPtr(bw, "LON", q.Lon, 6)
|
||||
writeField(bw, "RIG", q.Rig)
|
||||
writeField(bw, "ANT", q.Ant)
|
||||
w("NAME", q.Name)
|
||||
w("QTH", q.QTH)
|
||||
w("ADDRESS", q.Address)
|
||||
w("EMAIL", q.Email)
|
||||
w("WEB", q.Web)
|
||||
w("GRIDSQUARE", q.Grid)
|
||||
w("GRIDSQUARE_EXT", q.GridExt)
|
||||
w("VUCC_GRIDS", q.VUCCGrids)
|
||||
w("COUNTRY", q.Country)
|
||||
w("STATE", q.State)
|
||||
w("CNTY", q.County)
|
||||
wi("DXCC", q.DXCC)
|
||||
w("CONT", q.Continent)
|
||||
wi("CQZ", q.CQZ)
|
||||
wi("ITUZ", q.ITUZ)
|
||||
w("IOTA", q.IOTA)
|
||||
w("SOTA_REF", q.SOTARef)
|
||||
w("POTA_REF", q.POTARef)
|
||||
wi("AGE", q.Age)
|
||||
wf("LAT", q.Lat, 6)
|
||||
wf("LON", q.Lon, 6)
|
||||
w("RIG", q.Rig)
|
||||
w("ANT", q.Ant)
|
||||
|
||||
// --- QSL / LoTW / eQSL / Clublog / HRDLog ---
|
||||
writeField(bw, "QSL_SENT", q.QSLSent)
|
||||
writeField(bw, "QSL_RCVD", q.QSLRcvd)
|
||||
writeField(bw, "QSLSDATE", q.QSLSentDate)
|
||||
writeField(bw, "QSLRDATE", q.QSLRcvdDate)
|
||||
writeField(bw, "QSL_VIA", q.QSLVia)
|
||||
writeField(bw, "QSLMSG", q.QSLMsg)
|
||||
writeField(bw, "QSLMSG_RCVD", q.QSLMsgRcvd)
|
||||
writeField(bw, "LOTW_QSL_SENT", q.LOTWSent)
|
||||
writeField(bw, "LOTW_QSL_RCVD", q.LOTWRcvd)
|
||||
writeField(bw, "LOTW_QSLSDATE", q.LOTWSentDate)
|
||||
writeField(bw, "LOTW_QSLRDATE", q.LOTWRcvdDate)
|
||||
writeField(bw, "EQSL_QSL_SENT", q.EQSLSent)
|
||||
writeField(bw, "EQSL_QSL_RCVD", q.EQSLRcvd)
|
||||
writeField(bw, "EQSL_QSLSDATE", q.EQSLSentDate)
|
||||
writeField(bw, "EQSL_QSLRDATE", q.EQSLRcvdDate)
|
||||
writeField(bw, "CLUBLOG_QSO_UPLOAD_DATE", q.ClublogUploadDate)
|
||||
writeField(bw, "CLUBLOG_QSO_UPLOAD_STATUS", q.ClublogUploadStatus)
|
||||
writeField(bw, "HRDLOG_QSO_UPLOAD_DATE", q.HRDLogUploadDate)
|
||||
writeField(bw, "HRDLOG_QSO_UPLOAD_STATUS", q.HRDLogUploadStatus)
|
||||
writeField(bw, "QRZCOM_QSO_UPLOAD_DATE", q.QRZComUploadDate)
|
||||
writeField(bw, "QRZCOM_QSO_UPLOAD_STATUS", q.QRZComUploadStatus)
|
||||
writeField(bw, "QRZCOM_QSO_DOWNLOAD_DATE", q.QRZComDownloadDate)
|
||||
writeField(bw, "QRZCOM_QSO_DOWNLOAD_STATUS", q.QRZComDownloadStatus)
|
||||
w("QSL_SENT", q.QSLSent)
|
||||
w("QSL_RCVD", q.QSLRcvd)
|
||||
w("QSLSDATE", q.QSLSentDate)
|
||||
w("QSLRDATE", q.QSLRcvdDate)
|
||||
w("QSL_VIA", q.QSLVia)
|
||||
w("QSLMSG", q.QSLMsg)
|
||||
w("QSLMSG_RCVD", q.QSLMsgRcvd)
|
||||
w("LOTW_QSL_SENT", q.LOTWSent)
|
||||
w("LOTW_QSL_RCVD", q.LOTWRcvd)
|
||||
w("LOTW_QSLSDATE", q.LOTWSentDate)
|
||||
w("LOTW_QSLRDATE", q.LOTWRcvdDate)
|
||||
w("EQSL_QSL_SENT", q.EQSLSent)
|
||||
w("EQSL_QSL_RCVD", q.EQSLRcvd)
|
||||
w("EQSL_QSLSDATE", q.EQSLSentDate)
|
||||
w("EQSL_QSLRDATE", q.EQSLRcvdDate)
|
||||
w("CLUBLOG_QSO_UPLOAD_DATE", q.ClublogUploadDate)
|
||||
w("CLUBLOG_QSO_UPLOAD_STATUS", q.ClublogUploadStatus)
|
||||
w("HRDLOG_QSO_UPLOAD_DATE", q.HRDLogUploadDate)
|
||||
w("HRDLOG_QSO_UPLOAD_STATUS", q.HRDLogUploadStatus)
|
||||
w("QRZCOM_QSO_UPLOAD_DATE", q.QRZComUploadDate)
|
||||
w("QRZCOM_QSO_UPLOAD_STATUS", q.QRZComUploadStatus)
|
||||
w("QRZCOM_QSO_DOWNLOAD_DATE", q.QRZComDownloadDate)
|
||||
w("QRZCOM_QSO_DOWNLOAD_STATUS", q.QRZComDownloadStatus)
|
||||
|
||||
// --- Contest ---
|
||||
writeField(bw, "CONTEST_ID", q.ContestID)
|
||||
writeIntPtr(bw, "SRX", q.SRX)
|
||||
writeIntPtr(bw, "STX", q.STX)
|
||||
writeField(bw, "SRX_STRING", q.SRXString)
|
||||
writeField(bw, "STX_STRING", q.STXString)
|
||||
writeField(bw, "CHECK", q.Check)
|
||||
writeField(bw, "PRECEDENCE", q.Precedence)
|
||||
writeField(bw, "ARRL_SECT", q.ARRLSect)
|
||||
w("CONTEST_ID", q.ContestID)
|
||||
wi("SRX", q.SRX)
|
||||
wi("STX", q.STX)
|
||||
w("SRX_STRING", q.SRXString)
|
||||
w("STX_STRING", q.STXString)
|
||||
w("CHECK", q.Check)
|
||||
w("PRECEDENCE", q.Precedence)
|
||||
w("ARRL_SECT", q.ARRLSect)
|
||||
|
||||
// --- Satellite / propagation ---
|
||||
writeField(bw, "PROP_MODE", q.PropMode)
|
||||
writeField(bw, "SAT_NAME", q.SatName)
|
||||
writeField(bw, "SAT_MODE", q.SatMode)
|
||||
writeFloatPtr(bw, "ANT_AZ", q.AntAz, 1)
|
||||
writeFloatPtr(bw, "ANT_EL", q.AntEl, 1)
|
||||
writeField(bw, "ANT_PATH", q.AntPath)
|
||||
w("PROP_MODE", q.PropMode)
|
||||
w("SAT_NAME", q.SatName)
|
||||
w("SAT_MODE", q.SatMode)
|
||||
wf("ANT_AZ", q.AntAz, 1)
|
||||
wf("ANT_EL", q.AntEl, 1)
|
||||
w("ANT_PATH", q.AntPath)
|
||||
|
||||
// --- My station / operator ---
|
||||
writeField(bw, "STATION_CALLSIGN", q.StationCallsign)
|
||||
writeField(bw, "OPERATOR", q.Operator)
|
||||
writeField(bw, "MY_GRIDSQUARE", q.MyGrid)
|
||||
writeField(bw, "MY_GRIDSQUARE_EXT", q.MyGridExt)
|
||||
writeField(bw, "MY_COUNTRY", q.MyCountry)
|
||||
writeField(bw, "MY_STATE", q.MyState)
|
||||
writeField(bw, "MY_CNTY", q.MyCounty)
|
||||
writeField(bw, "MY_IOTA", q.MyIOTA)
|
||||
writeField(bw, "MY_SOTA_REF", q.MySOTARef)
|
||||
writeField(bw, "MY_POTA_REF", q.MyPOTARef)
|
||||
writeIntPtr(bw, "MY_DXCC", q.MyDXCC)
|
||||
writeIntPtr(bw, "MY_CQ_ZONE", q.MyCQZone)
|
||||
writeIntPtr(bw, "MY_ITU_ZONE", q.MyITUZone)
|
||||
writeFloatPtr(bw, "MY_LAT", q.MyLat, 6)
|
||||
writeFloatPtr(bw, "MY_LON", q.MyLon, 6)
|
||||
writeField(bw, "MY_STREET", q.MyStreet)
|
||||
writeField(bw, "MY_CITY", q.MyCity)
|
||||
writeField(bw, "MY_POSTAL_CODE", q.MyPostalCode)
|
||||
writeField(bw, "MY_RIG", q.MyRig)
|
||||
writeField(bw, "MY_ANTENNA", q.MyAntenna)
|
||||
w("STATION_CALLSIGN", q.StationCallsign)
|
||||
w("OPERATOR", q.Operator)
|
||||
w("MY_GRIDSQUARE", q.MyGrid)
|
||||
w("MY_GRIDSQUARE_EXT", q.MyGridExt)
|
||||
w("MY_COUNTRY", q.MyCountry)
|
||||
w("MY_STATE", q.MyState)
|
||||
w("MY_CNTY", q.MyCounty)
|
||||
w("MY_IOTA", q.MyIOTA)
|
||||
w("MY_SOTA_REF", q.MySOTARef)
|
||||
w("MY_POTA_REF", q.MyPOTARef)
|
||||
wi("MY_DXCC", q.MyDXCC)
|
||||
wi("MY_CQ_ZONE", q.MyCQZone)
|
||||
wi("MY_ITU_ZONE", q.MyITUZone)
|
||||
wf("MY_LAT", q.MyLat, 6)
|
||||
wf("MY_LON", q.MyLon, 6)
|
||||
w("MY_STREET", q.MyStreet)
|
||||
w("MY_CITY", q.MyCity)
|
||||
w("MY_POSTAL_CODE", q.MyPostalCode)
|
||||
w("MY_RIG", q.MyRig)
|
||||
w("MY_ANTENNA", q.MyAntenna)
|
||||
|
||||
// --- Misc ---
|
||||
writeFloatPtr(bw, "TX_PWR", q.TXPower, 1)
|
||||
writeField(bw, "COMMENT", q.Comment)
|
||||
writeField(bw, "NOTES", q.Notes)
|
||||
wf("TX_PWR", q.TXPower, 1)
|
||||
w("COMMENT", q.Comment)
|
||||
w("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)
|
||||
w("SIG", q.SIG)
|
||||
w("SIG_INFO", q.SIGInfo)
|
||||
w("MY_SIG", q.MySIG)
|
||||
w("MY_SIG_INFO", q.MySIGInfo)
|
||||
w("WWFF_REF", q.WWFFRef)
|
||||
w("MY_WWFF_REF", q.MyWWFFRef)
|
||||
wf("DISTANCE", q.Distance, 1)
|
||||
wf("RX_PWR", q.RXPower, 1)
|
||||
wf("A_INDEX", q.AIndex, 0)
|
||||
wf("K_INDEX", q.KIndex, 0)
|
||||
wf("SFI", q.SFI, 0)
|
||||
w("SKCC", q.SKCC)
|
||||
w("FISTS", q.FISTS)
|
||||
w("TEN_TEN", q.TenTen)
|
||||
w("CONTACTED_OP", q.ContactedOp)
|
||||
w("EQ_CALL", q.EqCall)
|
||||
w("PFX", q.PFX)
|
||||
w("MY_NAME", q.MyName)
|
||||
w("CLASS", q.Class)
|
||||
w("DARC_DOK", q.DarcDOK)
|
||||
w("MY_DARC_DOK", q.MyDarcDOK)
|
||||
w("REGION", q.Region)
|
||||
w("SILENT_KEY", q.SilentKey)
|
||||
w("SWL", q.SWL)
|
||||
w("QSO_COMPLETE", q.QSOComplete)
|
||||
w("QSO_RANDOM", q.QSORandom)
|
||||
w("CREDIT_GRANTED", q.CreditGranted)
|
||||
w("CREDIT_SUBMITTED", q.CreditSubmitted)
|
||||
w("MY_ARRL_SECT", q.MyARRLSect)
|
||||
w("MY_VUCC_GRIDS", q.MyVUCCGrids)
|
||||
|
||||
// --- Extras (unpromoted ADIF fields preserved verbatim) ---
|
||||
// Standard mode emits ONLY valid ADIF-spec fields, so it drops APP_*
|
||||
@@ -318,7 +344,12 @@ func writeRecord(bw *bufio.Writer, q qso.QSO, includeApp bool) {
|
||||
// extra for a lossless OpsLog round-trip.
|
||||
for k, v := range q.Extras {
|
||||
tag := strings.ToUpper(k)
|
||||
if !includeApp && (strings.HasPrefix(tag, "APP_") || !IsStandardField(tag)) {
|
||||
if allow != nil {
|
||||
// Chosen-fields mode: the set alone decides (incl. APP_/vendor tags).
|
||||
if !allow[tag] {
|
||||
continue
|
||||
}
|
||||
} else if !includeApp && (strings.HasPrefix(tag, "APP_") || !IsStandardField(tag)) {
|
||||
continue
|
||||
}
|
||||
writeField(bw, tag, v)
|
||||
|
||||
Reference in New Issue
Block a user