mirror of
https://github.com/JS8Call-improved/JS8Call-improved
synced 2026-08-13 17:47:36 -04:00
461 lines
14 KiB
C++
461 lines
14 KiB
C++
/**
|
|
* @file ADIF.cpp
|
|
* @brief Implementation of the ADIF class for handling ADIF log files.
|
|
*/
|
|
#include "ADIF.h"
|
|
|
|
#include <QDateTime>
|
|
#include <QFile>
|
|
#include <QLoggingCategory>
|
|
#include <QTextStream>
|
|
|
|
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> ...
|
|
*/
|
|
|
|
/**
|
|
* @brief Initialize the ADIF instance with the specified filename.
|
|
* @param filename The path to the ADIF log file.
|
|
*/
|
|
void ADIF::init(QString const &filename) {
|
|
_filename = filename;
|
|
_data.clear();
|
|
}
|
|
|
|
/**
|
|
* @brief Extract the value of a specified field from an ADIF record.
|
|
* @param record The ADIF record as a string.
|
|
* @param fieldName The name of the field to extract.
|
|
* @return The extracted field value, or an empty string if not found.
|
|
*/
|
|
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 "";
|
|
}
|
|
|
|
/**
|
|
* @brief Load ADIF records from the specified file into the internal data
|
|
* structure.
|
|
*/
|
|
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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Add a new QSO to the internal data structure.
|
|
* @param call The callsign of the contacted station.
|
|
* @param band The band on which the QSO was made.
|
|
* @param mode The mode of the QSO.
|
|
* @param submode The submode of the QSO.
|
|
* @param grid The grid locator of the contacted station.
|
|
* @param date The date of the QSO.
|
|
* @param name The name of the operator.
|
|
* @param comment Any comments associated with the QSO.
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Check if a callsign and band combination exists in the internal data
|
|
* structure.
|
|
* @param call The callsign to search for.
|
|
* @param band The band to search for.
|
|
* @return True if a matching QSO is found, false otherwise.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Find QSOs associated with a given callsign.
|
|
* @param call The callsign to search for.
|
|
* @return A list of QSOs associated with the callsign.
|
|
*/
|
|
QList<ADIF::QSO> ADIF::find(QString const &call) const {
|
|
return _data.values(call);
|
|
}
|
|
|
|
/**
|
|
* @brief Get a list of all callsigns in the internal data structure.
|
|
* @return A list of callsigns.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @brief Get the count of QSOs in the internal data structure.
|
|
* @return The number of QSOs.
|
|
*/
|
|
qsizetype ADIF::getCount() const { return _data.size(); }
|
|
|
|
/**
|
|
* @brief Convert QSO details into an ADIF record format.
|
|
* @param hisCall The callsign of the contacted station.
|
|
* @param hisGrid The grid locator of the contacted station.
|
|
* @param mode The mode of the QSO.
|
|
* @param submode The submode of the QSO.
|
|
* @param rptSent The report sent.
|
|
* @param rptRcvd The report received.
|
|
* @param dateTimeOn The date and time when the QSO started.
|
|
* @param dateTimeOff The date and time when the QSO ended.
|
|
* @param band The band on which the QSO was made.
|
|
* @param comments Any comments associated with the QSO.
|
|
* @param name The name of the operator.
|
|
* @param strDialFreq The dial frequency as a string.
|
|
* @param m_myCall The station's own callsign.
|
|
* @param m_myGrid The station's own grid locator.
|
|
* @param operator_call The operator's callsign.
|
|
* @param additionalFields A map of additional ADIF fields to include.
|
|
* @return The ADIF record as a QByteArray.
|
|
*/
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @brief Open the ADIF file and append the QSO details.
|
|
* @param ADIF_record The ADIF record to append.
|
|
* @return True on success, false otherwise.
|
|
*/
|
|
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)
|