/** * @file ADIF.cpp * @brief Implementation of the ADIF class for handling ADIF log files. */ #include "ADIF.h" #include #include #include #include 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", }; /* W1XT20m14.076DM33JT65-21-142011042204171204243541st JT65A QSO. Him: mag loop 20WVK3ACFqf22lb IK1SOW20m14.076JN35JT65-19-11201104220525010533593VK3ACFqf22lb 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("", 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("", 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 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::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 ADIF::getCallList() const { QList p; QMultiHash::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 const &additionalFields) { QString t; t = "" + hisCall; t += " " + hisGrid; t += " " + mode; if (!submode.isEmpty()) { t += " " + submode; } t += " " + rptSent; t += " " + rptRcvd; t += " " + dateTimeOn.date().toString("yyyyMMdd"); t += " " + dateTimeOn.time().toString("hhmmss"); t += " " + dateTimeOff.date().toString("yyyyMMdd"); t += " " + dateTimeOff.time().toString("hhmmss"); t += " " + band; t += " " + strDialFreq; t += " " + m_myCall; t += " " + m_myGrid; if (comments != "") t += " " + comments; if (name != "") t += " " + name; if (operator_call != "") t += " " + 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(" %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" << Qt::endl; // new file out << ADIF_record << " " << Qt::endl; out.flush(); flushFileBuffer(f2); f2.close(); } return true; } Q_LOGGING_CATEGORY(adif_js8, "adif.js8", QtWarningMsg)