WSJT-X/logbook/WorkedBefore.cpp
Bill Somerville ca07d22e89
Add option to highlight un-worked grid fields
This  is a  check box  option  in "Settings->Colors"  rather than  new
    highlighting types so un-worked  field highlighting and un-worked grid
    square highlighting are mutually exclusive. The check box state can be
    changed at  any time, no  log rescanning is necessary,  and subsequent
    decoded message highlighting will be according to the check box state.
2019-08-09 11:25:50 +01:00

676 lines
23 KiB
C++

#include "WorkedBefore.hpp"
#include <functional>
#include <stdexcept>
#include <boost/functional/hash.hpp>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/key_extractors.hpp>
#include <boost/range/iterator_range.hpp>
#include <QtConcurrent/QtConcurrentRun>
#include <QFuture>
#include <QFutureWatcher>
#include <QChar>
#include <QString>
#include <QByteArray>
#include <QStandardPaths>
#include <QDir>
#include <QFileInfo>
#include <QFile>
#include <QTextStream>
#include "Configuration.hpp"
#include "qt_helpers.hpp"
#include "pimpl_impl.hpp"
#include "moc_WorkedBefore.cpp"
using namespace boost::multi_index;
// hash function for QString members in hashed indexes
inline
std::size_t hash_value (QString const& s)
{
return std::hash<QString> {} (s);
}
//
// worked before set element
//
struct worked_entry
{
explicit worked_entry (QString const& call, QString const& grid, QString const& band
, QString const& mode, QString const& country, AD1CCty::Continent continent
, int CQ_zone, int ITU_zone)
: call_ {call}
, grid_ {grid}
, band_ {band}
, mode_ {mode}
, country_ {country}
, continent_ {continent}
, CQ_zone_ {CQ_zone}
, ITU_zone_ {ITU_zone}
{
}
QString call_;
QString grid_;
QString band_;
QString mode_;
QString country_;
AD1CCty::Continent continent_;
int CQ_zone_;
int ITU_zone_;
};
bool operator == (worked_entry const& lhs, worked_entry const& rhs)
{
return
lhs.continent_ == rhs.continent_ // check 1st as it is fast
&& lhs.CQ_zone_ == rhs.CQ_zone_ // ditto
&& lhs.ITU_zone_ == rhs.ITU_zone_ // ditto
&& lhs.call_ == rhs.call_ // check the rest in decreasing
&& lhs.grid_ == rhs.grid_ // domain size order to shortcut
&& lhs.country_ == rhs.country_ // differences as quickly as possible
&& lhs.band_ == rhs.band_
&& lhs.mode_ == rhs.mode_;
}
std::size_t hash_value (worked_entry const& we)
{
std::size_t seed {0};
boost::hash_combine (seed, we.call_);
boost::hash_combine (seed, we.grid_);
boost::hash_combine (seed, we.band_);
boost::hash_combine (seed, we.mode_);
boost::hash_combine (seed, we.country_);
boost::hash_combine (seed, we.continent_);
boost::hash_combine (seed, we.CQ_zone_);
boost::hash_combine (seed, we.ITU_zone_);
return seed;
}
#if !defined (QT_NO_DEBUG_STREAM)
QDebug operator << (QDebug dbg, worked_entry const& e)
{
QDebugStateSaver saver {dbg};
dbg.nospace () << "worked_entry("
<< e.call_ << ", "
<< e.grid_ << ", "
<< e.band_ << ", "
<< e.mode_ << ", "
<< e.country_ << ", "
<< e.continent_ << ", "
<< e.CQ_zone_ << ", "
<< e.ITU_zone_ << ')';
return dbg;
}
#endif
// less then predidate for the Continent enum class, needed for
// ordered indexes
struct Continent_less
{
bool operator () (AD1CCty::Continent lhs, AD1CCty::Continent rhs) const
{
return static_cast<int> (lhs) < static_cast<int> (rhs);
}
};
// index tags
struct call_mode_band {};
struct call_band {};
struct grid_mode_band {};
struct grid_band {};
struct entity_mode_band {};
struct entity_band {};
struct continent_mode_band {};
struct continent_band {};
struct CQ_zone_mode_band {};
struct CQ_zone_band {};
struct ITU_zone_mode_band {};
struct ITU_zone_band {};
// set with multiple ordered unique indexes that allow for optimally
// efficient determination of various categories of worked before
// status
typedef multi_index_container<
worked_entry,
indexed_by<
// basic unordered set constraint - we don't need duplicate worked entries
hashed_unique<identity<worked_entry>>,
//
// The following indexes are used to discover worked before stuff.
//
// They are ordered so as to support partial lookups and
// non-unique because container inserts must be valid for all
// indexes.
//
// call+mode+band
ordered_non_unique<tag<call_mode_band>,
composite_key<worked_entry,
member<worked_entry, QString, &worked_entry::call_>,
member<worked_entry, QString, &worked_entry::mode_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// call+band
ordered_non_unique<tag<call_band>,
composite_key<worked_entry,
member<worked_entry, QString, &worked_entry::call_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// grid+mode+band
ordered_non_unique<tag<grid_mode_band>,
composite_key<worked_entry,
member<worked_entry, QString, &worked_entry::grid_>,
member<worked_entry, QString, &worked_entry::mode_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// grid+band
ordered_non_unique<tag<grid_band>,
composite_key<worked_entry,
member<worked_entry, QString, &worked_entry::grid_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// country+mode+band
ordered_non_unique<tag<entity_mode_band>,
composite_key<worked_entry,
member<worked_entry, QString, &worked_entry::country_>,
member<worked_entry, QString, &worked_entry::mode_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// country+band
ordered_non_unique<tag<entity_band>,
composite_key<worked_entry,
member<worked_entry, QString, &worked_entry::country_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// continent+mode+band
ordered_non_unique<tag<continent_mode_band>,
composite_key<worked_entry,
member<worked_entry, AD1CCty::Continent, &worked_entry::continent_>,
member<worked_entry, QString, &worked_entry::mode_>,
member<worked_entry, QString, &worked_entry::band_> >,
composite_key_compare<Continent_less, std::less<QString>, std::less<QString> > >,
// continent+band
ordered_non_unique<tag<continent_band>,
composite_key<worked_entry,
member<worked_entry, AD1CCty::Continent, &worked_entry::continent_>,
member<worked_entry, QString, &worked_entry::band_> >,
composite_key_compare<Continent_less, std::less<QString> > >,
// CQ-zone+mode+band
ordered_non_unique<tag<CQ_zone_mode_band>,
composite_key<worked_entry,
member<worked_entry, int, &worked_entry::CQ_zone_>,
member<worked_entry, QString, &worked_entry::mode_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// CQ-zone+band
ordered_non_unique<tag<CQ_zone_band>,
composite_key<worked_entry,
member<worked_entry, int, &worked_entry::CQ_zone_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// ITU-zone+mode+band
ordered_non_unique<tag<ITU_zone_mode_band>,
composite_key<worked_entry,
member<worked_entry, int, &worked_entry::ITU_zone_>,
member<worked_entry, QString, &worked_entry::mode_>,
member<worked_entry, QString, &worked_entry::band_> > >,
// ITU-zone+band
ordered_non_unique<tag<ITU_zone_band>,
composite_key<worked_entry,
member<worked_entry, int, &worked_entry::ITU_zone_>,
member<worked_entry, QString, &worked_entry::band_> > > >
> worked_before_database_type;
namespace
{
auto const logFileName = "wsjtx_log.adi";
// Expception class suitable for using with QtConcurrent across
// thread boundaries
class LoaderException final
: public QException
{
public:
LoaderException (std::exception const& e) : error_ {e.what ()} {}
QString error () const {return error_;}
void raise () const override {throw *this;}
LoaderException * clone () const override {return new LoaderException {*this};}
private:
QString error_;
};
QString extractField (QString const& record, QString const& fieldName)
{
int fieldNameIndex = record.indexOf ('<' + fieldName + ':', 0, Qt::CaseInsensitive);
if (fieldNameIndex >=0)
{
int closingBracketIndex = record.indexOf('>',fieldNameIndex);
int fieldLengthIndex = record.indexOf(':',fieldNameIndex); // find the size delimiter
int 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 >
}
else
{
throw LoaderException (std::runtime_error {"Invalid ADIF field " + fieldName.toStdString () + ": " + record.toStdString ()});
}
if (closingBracketIndex > fieldNameIndex && fieldLengthIndex > fieldNameIndex && fieldLengthIndex < closingBracketIndex)
{
int 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)
{
return record.mid(closingBracketIndex+1,fieldLength);
}
}
else
{
throw LoaderException (std::runtime_error {"Malformed ADIF field " + fieldName.toStdString () + ": " + record.toStdString ()});
}
}
return QString {};
}
worked_before_database_type loader (QString const& path, AD1CCty const * prefixes)
{
worked_before_database_type worked;
QFile inputFile {path};
if (inputFile.exists ())
{
if (inputFile.open (QFile::ReadOnly))
{
QTextStream in {&inputFile};
QString buffer;
bool pre_read {false};
int 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
{
if (end_position < 0)
{
throw LoaderException (std::runtime_error {"Invalid ADIF header"});
}
buffer.remove (0, end_position + 5);
}
while (!in.atEnd ())
{
end_position = buffer.indexOf ("<EOR>", 0, Qt::CaseInsensitive);
do
{
if (!in.atEnd () && end_position < 0)
{
buffer += in.readLine () + '\n';
}
}
while ((end_position = buffer.indexOf ("<EOR>", 0, Qt::CaseInsensitive)) < 0 && !in.atEnd ());
if (end_position >= 0) // require valid ADIF record
// with terminator
{
auto record = buffer.left (end_position + 5).trimmed ();
auto next_record = buffer.indexOf (QChar {'<'}, end_position + 5);
buffer.remove (0, next_record >=0 ? next_record : buffer.size ());
record = record.mid (record.indexOf (QChar {'<'}));
auto call = extractField (record, "CALL");
if (call.size ()) // require CALL field before we
// will parse a record
{
auto const& entity = prefixes->lookup (call);
auto mode = extractField (record, "MODE").toUpper ();
if (!mode.size () || "MFSK" == mode)
{
mode = extractField (record, "SUBMODE").toUpper ();
}
worked.emplace (call.toUpper ()
, extractField (record, "GRIDSQUARE").left (4).toUpper () // not interested in 6-digit grids
, extractField (record, "BAND").toUpper ()
, mode
, entity.entity_name
, entity.continent
, entity.CQ_zone
, entity.ITU_zone);
}
}
}
}
else
{
throw LoaderException (std::runtime_error {"Error opening ADIF log file for read: " + inputFile.errorString ().toStdString ()});
}
}
return worked;
}
}
class WorkedBefore::impl final
{
public:
impl (Configuration const * configuration)
: configuration_ {configuration}
, path_ {QDir {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}.absoluteFilePath (logFileName)}
, prefixes_ {configuration}
{
}
void reload ()
{
async_loader_ = QtConcurrent::run (loader, path_, &prefixes_);
loader_watcher_.setFuture (async_loader_);
}
Configuration const * configuration_;
QString path_;
AD1CCty prefixes_;
QFutureWatcher<worked_before_database_type> loader_watcher_;
QFuture<worked_before_database_type> async_loader_;
worked_before_database_type worked_;
};
WorkedBefore::WorkedBefore (Configuration const * configuration)
: m_ {configuration}
{
Q_ASSERT (configuration);
connect (&m_->loader_watcher_, &QFutureWatcher<worked_before_database_type>::finished, [this] () {
QString error;
size_t n {0};
try
{
m_->worked_ = m_->loader_watcher_.result ();
n = m_->worked_.size ();
}
catch (LoaderException const& e)
{
error = e.error ();
}
Q_EMIT finished_loading (n, error);
});
reload ();
}
void WorkedBefore::reload ()
{
m_->reload ();
}
WorkedBefore::~WorkedBefore ()
{
}
QString const& WorkedBefore::path () const
{
return m_->path_;
}
AD1CCty const * WorkedBefore::countries () const
{
return &m_->prefixes_;
}
bool WorkedBefore::add (QString const& call
, QString const& grid
, QString const& band
, QString const& mode
, QByteArray const& ADIF_record)
{
if (call.size ())
{
auto const& entity = m_->prefixes_.lookup (call);
QFile file {m_->path_};
if (!file.open(QIODevice::Text | QIODevice::Append))
{
return false;
}
else
{
QTextStream out {&file};
if (!file.size ())
{
out << "WSJT-X ADIF Export<eoh>" << endl; // new file
}
out << ADIF_record << " <eor>" << endl;
}
m_->worked_.emplace (call.toUpper (), grid.left (4).toUpper (), band.toUpper (), mode.toUpper ()
, entity.entity_name, entity.continent, entity.CQ_zone, entity.ITU_zone);
}
return true;
}
bool WorkedBefore::country_worked (QString const& country, QString const& mode, QString const& band) const
{
if (mode.size ())
{
if (band.size ())
{
return
country.size ()
&& m_->worked_.get<entity_mode_band> ().end ()
!= m_->worked_.get<entity_mode_band> ().find (std::make_tuple (country, mode.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return
country.size ()
&& m_->worked_.get<entity_mode_band> ().end ()
!= m_->worked_.get<entity_mode_band> ().find (std::make_tuple (country, mode.toUpper ()));
}
}
else
{
if (band.size ())
{
return
country.size ()
&& m_->worked_.get<entity_band> ().end ()
!= m_->worked_.get<entity_band> ().find (std::make_tuple (country, band.toUpper ()));
}
else
{
// partial key lookup
return
country.size ()
&& m_->worked_.get<entity_band> ().end ()
!= m_->worked_.get<entity_band> ().find (country);
}
}
}
bool WorkedBefore::grid_worked (QString const& grid, QString const& mode, QString const& band) const
{
auto gridsquare = grid.left (4).toUpper ();
if (m_->configuration_->highlight_only_fields ())
{
// can't use a direct set find operation or a set operation with
// a (CompatibleKey, CompatibleCompare) concept so we must
// partially scan the index
auto range = boost::make_iterator_range (
m_->worked_.get<grid_mode_band> ().lower_bound (gridsquare.left (2))
, m_->worked_.get<grid_mode_band> ().upper_bound (gridsquare.left (2) + "99"));
for (worked_entry const& worked : range)
{
if ((!mode.size () || mode.toUpper () == worked.mode_)
&& (!band.size () || worked.band_ == band.toUpper ()))
{
return true;
}
}
}
else
{
if (mode.size ())
{
if (band.size ())
{
return m_->worked_.get<grid_mode_band> ().end ()
!= m_->worked_.get<grid_mode_band> ().find (std::make_tuple (gridsquare, mode.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<grid_mode_band> ().end ()
!= m_->worked_.get<grid_mode_band> ().find (std::make_tuple (gridsquare, mode.toUpper ()));
}
}
else
{
if (band.size ())
{
return m_->worked_.get<grid_band> ().end ()
!= m_->worked_.get<grid_band> ().find (std::make_tuple (gridsquare, band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<grid_band> ().end ()
!= m_->worked_.get<grid_band> ().find (gridsquare);
}
}
}
return false;
}
bool WorkedBefore::call_worked (QString const& call, QString const& mode, QString const& band) const
{
if (mode.size ())
{
if (band.size ())
{
return m_->worked_.get<call_mode_band> ().end ()
!= m_->worked_.get<call_mode_band> ().find (std::make_tuple (call.toUpper (), mode.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<call_mode_band> ().end ()
!= m_->worked_.get<call_mode_band> ().find (std::make_tuple (call.toUpper (), mode.toUpper ()));
}
}
else
{
if (band.size ())
{
return m_->worked_.get<call_band> ().end ()
!= m_->worked_.get<call_band> ().find (std::make_tuple (call.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<call_band> ().end ()
!= m_->worked_.get<call_band> ().find (std::make_tuple (call.toUpper ()));
}
}
}
bool WorkedBefore::continent_worked (Continent continent, QString const& mode, QString const& band) const
{
if (mode.size ())
{
if (band.size ())
{
return m_->worked_.get<continent_mode_band> ().end ()
!= m_->worked_.get<continent_mode_band> ().find (std::make_tuple (continent, mode.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<continent_mode_band> ().end ()
!= m_->worked_.get<continent_mode_band> ().find (std::make_tuple (continent, mode.toUpper ()));
}
}
else
{
if (band.size ())
{
return m_->worked_.get<continent_band> ().end ()
!= m_->worked_.get<continent_band> ().find (std::make_tuple (continent, band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<continent_band> ().end ()
!= m_->worked_.get<continent_band> ().find (continent);
}
}
}
bool WorkedBefore::CQ_zone_worked (int CQ_zone, QString const& mode, QString const& band) const
{
if (mode.size ())
{
if (band.size ())
{
return m_->worked_.get<CQ_zone_mode_band> ().end ()
!= m_->worked_.get<CQ_zone_mode_band> ().find (std::make_tuple (CQ_zone, mode.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<CQ_zone_mode_band> ().end ()
!= m_->worked_.get<CQ_zone_mode_band> ().find (std::make_tuple (CQ_zone, mode.toUpper ()));
}
}
else
{
if (band.size ())
{
return m_->worked_.get<CQ_zone_band> ().end ()
!= m_->worked_.get<CQ_zone_band> ().find (std::make_tuple (CQ_zone, band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<CQ_zone_band> ().end ()
!= m_->worked_.get<CQ_zone_band> ().find (CQ_zone);
}
}
}
bool WorkedBefore::ITU_zone_worked (int ITU_zone, QString const& mode, QString const& band) const
{
if (mode.size ())
{
if (band.size ())
{
return m_->worked_.get<ITU_zone_mode_band> ().end ()
!= m_->worked_.get<ITU_zone_mode_band> ().find (std::make_tuple (ITU_zone, mode.toUpper (), band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<ITU_zone_mode_band> ().end ()
!= m_->worked_.get<ITU_zone_mode_band> ().find (std::make_tuple (ITU_zone, mode.toUpper ()));
}
}
else
{
if (band.size ())
{
return m_->worked_.get<ITU_zone_band> ().end ()
!= m_->worked_.get<ITU_zone_band> ().find (std::make_tuple (ITU_zone, band.toUpper ()));
}
else
{
// partial key lookup
return m_->worked_.get<ITU_zone_band> ().end ()
!= m_->worked_.get<ITU_zone_band> ().find (ITU_zone);
}
}
}