js8call/JS8_Logbook/adif.cpp
Chris-AC9KH 356bdc0f50 Code cleanup - move loose source and header files in source tree to group folders
Rename all header files with .h extension
Fix function declaration in HamlibTransceiver.h that overrode member function but was not marked override
Remove unused variable in Modulator.cpp
Remove unused files from source tree
Remove duplicate LazyFillComboBox files
Reformat codebase to LLVM C++ standard
2026-01-02 16:46:38 -06:00

404 lines
11 KiB
C++

#include "adif.h"
#include <QFile>
#include <QTextStream>
#include <QDateTime>
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(adif_js8)
const QStringList ADIF_FIELDS = {
// ADIF 3.1.0 - pulled from http://www.adif.org/310/adx310.xsd on 2019-06-04
"APP",
"ADDRESS",
"ADDRESS_INTL",
"AGE",
"A_INDEX",
"ANT_AZ",
"ANT_EL",
"ANT_PATH",
"ARRL_SECT",
"AWARD_SUBMITTED",
"AWARD_GRANTED",
"BAND",
"BAND_RX",
"CALL",
"CHECK",
"CLASS",
"CLUBLOG_QSO_UPLOAD_DATE",
"CLUBLOG_QSO_UPLOAD_STATUS",
"CNTY",
"COMMENT",
"COMMENT_INTL",
"CONT",
"CONTACTED_OP",
"CONTEST_ID",
"COUNTRY",
"COUNTRY_INTL",
"CQZ",
"CREDIT_SUBMITTED",
"CREDIT_GRANTED",
"DARC_DOK",
"DISTANCE",
"DXCC",
"EMAIL",
"EQ_CALL",
"EQSL_QSLRDATE",
"EQSL_QSLSDATE",
"EQSL_QSL_RCVD",
"EQSL_QSL_SENT",
"FISTS",
"FISTS_CC",
"FORCE_INIT",
"FREQ",
"FREQ_RX",
"GRIDSQUARE",
"GUEST_OP",
"HRDLOG_QSO_UPLOAD_DATE",
"HRDLOG_QSO_UPLOAD_STATUS",
"IOTA",
"IOTA_ISLAND_ID",
"ITUZ",
"K_INDEX",
"LAT",
"LON",
"LOTW_QSLRDATE",
"LOTW_QSLSDATE",
"LOTW_QSL_RCVD",
"LOTW_QSL_SENT",
"MAX_BURSTS",
"MODE",
"MS_SHOWER",
"MY_ANTENNA",
"MY_ANTENNA_INTL",
"MY_CITY",
"MY_CITY_INTL",
"MY_CNTY",
"MY_COUNTRY",
"MY_COUNTRY_INTL",
"MY_CQ_ZONE",
"MY_DXCC",
"MY_FISTS",
"MY_GRIDSQUARE",
"MY_IOTA",
"MY_IOTA_ISLAND_ID",
"MY_ITU_ZONE",
"MY_LAT",
"MY_LON",
"MY_NAME",
"MY_NAME_INTL",
"MY_POSTAL_CODE",
"MY_POSTAL_CODE_INTL",
"MY_RIG",
"MY_RIG_INTL",
"MY_SIG",
"MY_SIG_INTL",
"MY_SIG_INFO",
"MY_SIG_INFO_INTL",
"MY_SOTA_REF",
"MY_STATE",
"MY_STREET",
"MY_STREET_INTL",
"MY_USACA_COUNTIES",
"MY_VUCC_GRIDS",
"NAME",
"NAME_INTL",
"NOTES",
"NOTES_INTL",
"NR_BURSTS",
"NR_PINGS",
"OPERATOR",
"OWNER_CALLSIGN",
"PFX",
"PRECEDENCE",
"PROP_MODE",
"PUBLIC_KEY",
"QRZCOM_QSO_UPLOAD_DATE",
"QRZCOM_QSO_UPLOAD_STATUS",
"QSLMSG",
"QSLMSG_INTL",
"QSLRDATE",
"QSLSDATE",
"QSL_RCVD",
"QSL_RCVD_VIA",
"QSL_SENT",
"QSL_SENT_VIA",
"QSL_VIA",
"QSO_COMPLETE",
"QSO_DATE",
"QSO_DATE_OFF",
"QSO_RANDOM",
"QTH",
"QTH_INTL",
"REGION",
"RIG",
"RIG_INTL",
"RST_RCVD",
"RST_SENT",
"RX_PWR",
"SAT_MODE",
"SAT_NAME",
"SFI",
"SIG",
"SIG_INTL",
"SIG_INFO",
"SIG_INFO_INTL",
"SILENT_KEY",
"SKCC",
"SOTA_REF",
"SRX",
"SRX_STRING",
"STATE",
"STATION_CALLSIGN",
"STX",
"STX_STRING",
"SUBMODE",
"SWL",
"TEN_TEN",
"TIME_OFF",
"TIME_ON",
"TX_PWR",
"UKSMG",
"USACA_COUNTIES",
"VUCC_GRIDS",
"WEB",
};
/*
<CALL:4>W1XT<BAND:3>20m<FREQ:6>14.076<GRIDSQUARE:4>DM33<MODE:4>JT65<RST_RCVD:3>-21<RST_SENT:3>-14<QSO_DATE:8>20110422<TIME_ON:6>041712<TIME_OFF:6>042435<TX_PWR:1>4<COMMENT:34>1st JT65A QSO. Him: mag loop 20W<STATION_CALLSIGN:6>VK3ACF<MY_GRIDSQUARE:6>qf22lb<eor>
<CALL:6>IK1SOW<BAND:3>20m<FREQ:6>14.076<GRIDSQUARE:4>JN35<MODE:4>JT65<RST_RCVD:3>-19<RST_SENT:3>-11<QSO_DATE:8>20110422<TIME_ON:6>052501<TIME_OFF:6>053359<TX_PWR:1>3<STATION_CALLSIGN:6>VK3ACF<MY_GRIDSQUARE:6>qf22lb<eor>
<CALL:6:S>W4ABC> ...
*/
void ADIF::init(QString const& filename)
{
_filename = filename;
_data.clear();
}
QString ADIF::extractField(QString const& record, QString const& fieldName) const
{
qsizetype fieldNameIndex = record.indexOf ('<' + fieldName + ':', 0, Qt::CaseInsensitive);
if (fieldNameIndex >=0)
{
qsizetype closingBracketIndex = record.indexOf('>',fieldNameIndex);
qsizetype fieldLengthIndex = record.indexOf(':',fieldNameIndex); // find the size delimiter
qsizetype dataTypeIndex = -1;
if (fieldLengthIndex >= 0)
{
dataTypeIndex = record.indexOf(':',fieldLengthIndex+1); // check for a second : indicating there is a data type
if (dataTypeIndex > closingBracketIndex)
dataTypeIndex = -1; // second : was found but it was beyond the closing >
}
if ((closingBracketIndex > fieldNameIndex) && (fieldLengthIndex > fieldNameIndex) && (fieldLengthIndex< closingBracketIndex))
{
qsizetype fieldLengthCharCount = closingBracketIndex - fieldLengthIndex -1;
if (dataTypeIndex >= 0)
fieldLengthCharCount -= 2; // data type indicator is always a colon followed by a single character
QString fieldLengthString = record.mid(fieldLengthIndex+1,fieldLengthCharCount);
int fieldLength = fieldLengthString.toInt();
if (fieldLength > 0)
{
QString field = record.mid(closingBracketIndex+1,fieldLength);
return field;
}
}
}
return "";
}
void ADIF::load()
{
_data.clear();
QFile inputFile(_filename);
if (inputFile.open(QIODevice::ReadOnly))
{
QTextStream in(&inputFile);
QString buffer;
bool pre_read {false};
qsizetype end_position {-1};
// skip optional header record
do
{
buffer += in.readLine () + '\n';
if (buffer.startsWith (QChar {'<'})) // denotes no header
{
pre_read = true;
}
else
{
end_position = buffer.indexOf ("<EOH>", 0, Qt::CaseInsensitive);
}
}
while (!in.atEnd () && !pre_read && end_position < 0);
if (!pre_read) // found header
{
buffer.remove (0, end_position + 5);
}
while (buffer.size () || !in.atEnd ())
{
do
{
end_position = buffer.indexOf ("<EOR>", 0, Qt::CaseInsensitive);
if (!in.atEnd () && end_position < 0)
{
buffer += in.readLine () + '\n';
}
}
while (!in.atEnd () && end_position < 0);
qsizetype record_length {end_position >= 0 ? end_position + 5 : -1};
auto record = buffer.left (record_length).trimmed ();
auto next_record = buffer.indexOf (QChar {'<'}, record_length);
buffer.remove (0, next_record >=0 ? next_record : buffer.size ());
record = record.mid (record.indexOf (QChar {'<'}));
add (extractField (record, "CALL")
, extractField (record, "BAND")
, extractField (record, "MODE")
, extractField (record, "SUBMODE")
, extractField (record, "GRIDSQUARE")
, extractField (record, "QSO_DATE")
, extractField (record, "NAME")
, extractField (record, "COMMENT")
);
}
inputFile.close ();
}
}
void ADIF::add(QString const& call, QString const& band, QString const& mode, QString const& submode, QString const &grid, QString const& date, QString const& name, QString const& comment)
{
QSO q;
q.call = call;
q.band = band;
q.mode = mode;
q.submode = submode;
q.grid = grid;
q.date = date;
q.name = name;
q.comment = comment;
if (q.call.size ())
{
_data.insert(q.call,q);
// qCDebug(adif_js8) << "Added as worked:" << call << band << mode << date;
}
}
// return true if in the log same band
bool ADIF::match(QString const& call, QString const& band) const
{
QList<QSO> qsos = _data.values(call);
if (qsos.size()>0)
{
QSO q;
foreach(q,qsos)
{
if ( (band.compare(q.band,Qt::CaseInsensitive) == 0)
|| (band=="")
|| (q.band==""))
{
return true;
}
}
}
return false;
}
QList<ADIF::QSO> ADIF::find(QString const& call) const
{
return _data.values(call);
}
QList<QString> ADIF::getCallList() const
{
QList<QString> p;
QMultiHash<QString,QSO>::const_iterator i = _data.constBegin();
while (i != _data.constEnd())
{
p << i.key();
++i;
}
return p;
}
qsizetype ADIF::getCount() const
{
return _data.size();
}
QByteArray ADIF::QSOToADIF(QString const& hisCall, QString const& hisGrid, QString const& mode, QString const& submode
, QString const& rptSent, QString const& rptRcvd, QDateTime const& dateTimeOn
, QDateTime const& dateTimeOff, QString const& band, QString const& comments
, QString const& name, QString const& strDialFreq, QString const& m_myCall
, QString const& m_myGrid, QString const& operator_call, QMap<QString, QVariant> const &additionalFields)
{
QString t;
t = "<call:" + QString::number(hisCall.length()) + ">" + hisCall;
t += " <gridsquare:" + QString::number(hisGrid.length()) + ">" + hisGrid;
t += " <mode:" + QString::number(mode.length()) + ">" + mode;
if(!submode.isEmpty()){
t += " <submode:" + QString::number(submode.length()) + ">" + submode;
}
t += " <rst_sent:" + QString::number(rptSent.length()) + ">" + rptSent;
t += " <rst_rcvd:" + QString::number(rptRcvd.length()) + ">" + rptRcvd;
t += " <qso_date:8>" + dateTimeOn.date().toString("yyyyMMdd");
t += " <time_on:6>" + dateTimeOn.time().toString("hhmmss");
t += " <qso_date_off:8>" + dateTimeOff.date().toString("yyyyMMdd");
t += " <time_off:6>" + dateTimeOff.time().toString("hhmmss");
t += " <band:" + QString::number(band.length()) + ">" + band;
t += " <freq:" + QString::number(strDialFreq.length()) + ">" + strDialFreq;
t += " <station_callsign:" + QString::number(m_myCall.length()) + ">" +
m_myCall;
t += " <my_gridsquare:" + QString::number(m_myGrid.length()) + ">" +
m_myGrid;
if (comments != "")
t += " <comment:" + QString::number(comments.length()) +
">" + comments;
if (name != "")
t += " <name:" + QString::number(name.length()) +
">" + name;
if (operator_call!="")
t+=" <operator:" + QString::number(operator_call.length()) +
">" + operator_call;
foreach(auto key, additionalFields.keys()){
auto k = key.toUpper();
auto value = additionalFields[k].toString();
if(ADIF_FIELDS.contains(k)){
t += QString(" <%1:%2>%3").arg(k).arg(value.length()).arg(value);
} else {
t += QString(" <APP_JS8CALL_%1:%2>%3").arg(k).arg(value.length()).arg(value);
}
}
return t.toLatin1 ();
}
// open ADIF file and append the QSO details. Return true on success
bool ADIF::addQSOToFile(QByteArray const& ADIF_record)
{
QFile f2(_filename);
if (!f2.open(QIODevice::Text | QIODevice::Append))
return false;
else
{
QTextStream out(&f2);
if (f2.size()==0)
out << "JS8Call ADIF Export<eoh>" << Qt::endl; // new file
out << ADIF_record << " <eor>" << Qt::endl;
out.flush();
flushFileBuffer(f2);
f2.close();
}
return true;
}
Q_LOGGING_CATEGORY(adif_js8, "adif.js8", QtWarningMsg)