mirror of
https://github.com/saitohirga/WSJT-X.git
synced 2024-11-28 23:28:49 -05:00
4b4f65eb9f
Some logging applications export the BAND ADIF field with uppercase characters. This change makes sure that the internal worked before lookup indexes use uppercase throughout when fields that can come from external sources are stored.
410 lines
12 KiB
C++
410 lines
12 KiB
C++
#include "AD1CCty.hpp"
|
|
|
|
#include <string>
|
|
#include <stdexcept>
|
|
#include <algorithm>
|
|
#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/lambda/lambda.hpp>
|
|
#include <boost/lexical_cast.hpp>
|
|
#include <QString>
|
|
#include <QStandardPaths>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QDebug>
|
|
#include <QDebugStateSaver>
|
|
#include "Radio.hpp"
|
|
#include "pimpl_impl.hpp"
|
|
|
|
#include "moc_AD1CCty.cpp"
|
|
|
|
using namespace boost::multi_index;
|
|
|
|
namespace
|
|
{
|
|
auto const file_name = "cty.dat";
|
|
}
|
|
|
|
struct entity
|
|
{
|
|
using Continent = AD1CCty::Continent;
|
|
|
|
explicit entity (int id
|
|
, QString const& name
|
|
, bool WAE_only
|
|
, int CQ_zone
|
|
, int ITU_zone
|
|
, Continent continent
|
|
, float latitude
|
|
, float longtitude
|
|
, int UTC_offset
|
|
, QString const& primary_prefix)
|
|
: id_ {id}
|
|
, name_ {name}
|
|
, WAE_only_ {WAE_only}
|
|
, CQ_zone_ {CQ_zone}
|
|
, ITU_zone_ {ITU_zone}
|
|
, continent_ {continent}
|
|
, lat_ {latitude}
|
|
, long_ {longtitude}
|
|
, UTC_offset_ {UTC_offset}
|
|
, primary_prefix_ {primary_prefix}
|
|
{
|
|
}
|
|
|
|
int id_;
|
|
QString name_;
|
|
bool WAE_only_; // DARC WAE only, not valid for ARRL awards
|
|
int CQ_zone_;
|
|
int ITU_zone_;
|
|
Continent continent_;
|
|
float lat_; // degrees + is North
|
|
float long_; // degrees + is West
|
|
int UTC_offset_; // seconds
|
|
QString primary_prefix_;
|
|
};
|
|
|
|
#if !defined (QT_NO_DEBUG_STREAM)
|
|
QDebug operator << (QDebug dbg, entity const& e)
|
|
{
|
|
QDebugStateSaver saver {dbg};
|
|
dbg.nospace () << "entity("
|
|
<< e.id_ << ", "
|
|
<< e.name_ << ", "
|
|
<< e.WAE_only_ << ", "
|
|
<< e.CQ_zone_ << ", "
|
|
<< e.ITU_zone_ << ", "
|
|
<< e.continent_ << ", "
|
|
<< e.lat_ << ", "
|
|
<< e.long_ << ", "
|
|
<< (e.UTC_offset_ / (60. * 60.)) << ", "
|
|
<< e.primary_prefix_ << ')';
|
|
return dbg;
|
|
}
|
|
#endif
|
|
|
|
// tags
|
|
struct id {};
|
|
struct primary_prefix {};
|
|
|
|
// hash operation for QString object instances
|
|
struct hash_QString
|
|
{
|
|
std::size_t operator () (QString const& qs) const
|
|
{
|
|
return qHash (qs);
|
|
}
|
|
};
|
|
|
|
// set with hashed unique index that allow for efficient lookup of
|
|
// entity by internal id
|
|
typedef multi_index_container<
|
|
entity,
|
|
indexed_by<
|
|
hashed_unique<tag<id>, member<entity, int, &entity::id_> >,
|
|
hashed_unique<tag<primary_prefix>, member<entity, QString, &entity::primary_prefix_>, hash_QString> >
|
|
> entities_type;
|
|
|
|
struct prefix
|
|
{
|
|
explicit prefix (QString const& prefix, bool exact_match_only, int entity_id)
|
|
: prefix_ {prefix}
|
|
, exact_ {exact_match_only}
|
|
, entity_id_ {entity_id}
|
|
{
|
|
}
|
|
|
|
// extract key which is the prefix ignoring the trailing override
|
|
// information
|
|
QString prefix_key () const
|
|
{
|
|
auto const& prefix = prefix_.toStdString ();
|
|
return QString::fromStdString (prefix.substr (0, prefix.find_first_of ("({[<~")));
|
|
}
|
|
|
|
QString prefix_; // call or prefix with optional
|
|
// trailing override information
|
|
bool exact_;
|
|
int entity_id_;
|
|
};
|
|
|
|
#if !defined (QT_NO_DEBUG_STREAM)
|
|
QDebug operator << (QDebug dbg, prefix const& p)
|
|
{
|
|
QDebugStateSaver saver {dbg};
|
|
dbg.nospace () << "prefix("
|
|
<< p.prefix_ << ", "
|
|
<< p.exact_ << ", "
|
|
<< p.entity_id_ << ')';
|
|
return dbg;
|
|
}
|
|
#endif
|
|
|
|
// set with ordered unique index that allow for efficient
|
|
// determination of entity and entity overrides for a call or call
|
|
// prefix
|
|
typedef multi_index_container<
|
|
prefix,
|
|
indexed_by<
|
|
ordered_unique<const_mem_fun<prefix, QString, &prefix::prefix_key> > >
|
|
> prefixes_type;
|
|
|
|
class AD1CCty::impl final
|
|
{
|
|
public:
|
|
explicit impl ()
|
|
{
|
|
}
|
|
|
|
Record fixup (QString call, prefix const& p) const
|
|
{
|
|
call = call.toUpper ();
|
|
using entity_by_id = entities_type::index<id>::type;
|
|
entity_by_id::iterator e; // iterator into entity set
|
|
|
|
//
|
|
// deal with special rules that cty.dat does not cope with
|
|
//
|
|
if (call.startsWith ("KG4") && call.size () != 5 && call.size () != 3)
|
|
{
|
|
// KG4 2x1 and 2x3 calls that map to Gitmo are mainland US not Gitmo
|
|
e = entities_.project<id> (entities_.get<primary_prefix> ().find ("K"));
|
|
}
|
|
else
|
|
{
|
|
e = entities_.get<id> ().find (p.entity_id_);
|
|
}
|
|
|
|
Record result;
|
|
result.continent = e->continent_;
|
|
result.CQ_zone = e->CQ_zone_;
|
|
result.ITU_zone = e->ITU_zone_;
|
|
result.entity_name = e->name_;
|
|
result.WAE_only = e->WAE_only_;
|
|
result.latitude = e->lat_;
|
|
result.longtitude = e->long_;
|
|
result.UTC_offset = e->UTC_offset_;
|
|
result.primary_prefix = e->primary_prefix_;
|
|
|
|
// check for overrides
|
|
bool ok1 {true}, ok2 {true}, ok3 {true}, ok4 {true}, ok5 {true};
|
|
QString value;
|
|
if (override_value (p.prefix_, '(', ')', value)) result.CQ_zone = value.toInt (&ok1);
|
|
if (override_value (p.prefix_, '[', ']', value)) result.ITU_zone = value.toInt (&ok2);
|
|
if (override_value (p.prefix_, '<', '>', value))
|
|
{
|
|
auto const& fix = value.split ('/');
|
|
result.latitude = fix[0].toFloat (&ok3);
|
|
result.longtitude = fix[1].toFloat (&ok4);
|
|
}
|
|
if (override_value (p.prefix_, '{', '}', value)) result.continent = continent (value);
|
|
if (override_value (p.prefix_, '~', '~', value)) result.UTC_offset = static_cast<int> (value.toFloat (&ok5) * 60 * 60);
|
|
if (!(ok1 && ok2 && ok3 && ok4 && ok5))
|
|
{
|
|
throw std::domain_error {"Invalid number in cty.dat for override of " + p.prefix_.toStdString ()};
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static bool override_value (QString const& s, QChar lb, QChar ub, QString& v)
|
|
{
|
|
auto pos = s.indexOf (lb);
|
|
if (pos >= 0)
|
|
{
|
|
v = s.mid (pos + 1, s.indexOf (ub, pos + 1) - pos - 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QString path_;
|
|
entities_type entities_;
|
|
prefixes_type prefixes_;
|
|
};
|
|
|
|
AD1CCty::Record::Record ()
|
|
: continent {Continent::UN}
|
|
, CQ_zone {0}
|
|
, ITU_zone {0}
|
|
, WAE_only {false}
|
|
, latitude {NAN}
|
|
, longtitude {NAN}
|
|
, UTC_offset {0}
|
|
{
|
|
}
|
|
|
|
#if !defined (QT_NO_DEBUG_STREAM)
|
|
QDebug operator << (QDebug dbg, AD1CCty::Record const& r)
|
|
{
|
|
QDebugStateSaver saver {dbg};
|
|
dbg.nospace () << "AD1CCty::Record("
|
|
<< r.continent << ", "
|
|
<< r.CQ_zone << ", "
|
|
<< r.ITU_zone << ", "
|
|
<< r.entity_name << ", "
|
|
<< r.WAE_only << ", "
|
|
<< r.latitude << ", "
|
|
<< r.longtitude << ", "
|
|
<< (r.UTC_offset / (60. * 60.)) << ", "
|
|
<< r.primary_prefix << ')';
|
|
return dbg;
|
|
}
|
|
#endif
|
|
|
|
auto AD1CCty::continent (QString const& continent_id) -> Continent
|
|
{
|
|
Continent continent;
|
|
if ("AF" == continent_id)
|
|
{
|
|
continent = Continent::AF;
|
|
}
|
|
else if ("AN" == continent_id)
|
|
{
|
|
continent = Continent::AN;
|
|
}
|
|
else if ("AS" == continent_id)
|
|
{
|
|
continent = Continent::AS;
|
|
}
|
|
else if ("EU" == continent_id)
|
|
{
|
|
continent = Continent::EU;
|
|
}
|
|
else if ("NA" == continent_id)
|
|
{
|
|
continent = Continent::NA;
|
|
}
|
|
else if ("OC" == continent_id)
|
|
{
|
|
continent = Continent::OC;
|
|
}
|
|
else if ("SA" == continent_id)
|
|
{
|
|
continent = Continent::SA;
|
|
}
|
|
else
|
|
{
|
|
throw std::domain_error {"Invalid continent id: " + continent_id.toStdString ()};
|
|
}
|
|
return continent;
|
|
}
|
|
|
|
char const * AD1CCty::continent (Continent c)
|
|
{
|
|
switch (c)
|
|
{
|
|
case Continent::AF: return "AF";
|
|
case Continent::AN: return "AN";
|
|
case Continent::AS: return "AS";
|
|
case Continent::EU: return "EU";
|
|
case Continent::NA: return "NA";
|
|
case Continent::OC: return "OC";
|
|
case Continent::SA: return "SA";
|
|
default: return "UN";
|
|
}
|
|
}
|
|
|
|
AD1CCty::AD1CCty ()
|
|
{
|
|
QDir dataPath {QStandardPaths::writableLocation (QStandardPaths::DataLocation)};
|
|
m_->path_ = dataPath.exists (file_name)
|
|
? dataPath.absoluteFilePath (file_name) // user override
|
|
: QString {":/"} + file_name; // or original in the resources FS
|
|
QFile file {m_->path_};
|
|
if (file.open (QFile::ReadOnly))
|
|
{
|
|
int entity_id = 0;
|
|
int line_number {0};
|
|
QTextStream in {&file};
|
|
while (!in.atEnd ())
|
|
{
|
|
auto const& entity_line = in.readLine ();
|
|
++line_number;
|
|
if (!in.atEnd ())
|
|
{
|
|
auto const& entity_parts = entity_line.split (':');
|
|
if (entity_parts.size () >= 8)
|
|
{
|
|
auto primary_prefix = entity_parts[7].trimmed ();
|
|
bool WAE_only {false};
|
|
if (primary_prefix.startsWith ('*'))
|
|
{
|
|
primary_prefix = primary_prefix.mid (1);
|
|
WAE_only = true;
|
|
}
|
|
bool ok1, ok2, ok3, ok4, ok5;
|
|
m_->entities_.emplace (++entity_id
|
|
, entity_parts[0].trimmed ()
|
|
, WAE_only
|
|
, entity_parts[1].trimmed ().toInt (&ok1)
|
|
, entity_parts[2].trimmed ().toInt (&ok2)
|
|
, continent (entity_parts[3].trimmed ())
|
|
, entity_parts[4].trimmed ().toFloat (&ok3)
|
|
, entity_parts[5].trimmed ().toFloat (&ok4)
|
|
, static_cast<int> (entity_parts[6].trimmed ().toFloat (&ok5) * 60 * 60)
|
|
, primary_prefix);
|
|
if (!(ok1 && ok2 && ok3 && ok4 && ok5))
|
|
{
|
|
throw std::domain_error {"Invalid number in cty.dat line " + boost::lexical_cast<std::string> (line_number)};
|
|
}
|
|
QString line;
|
|
QString detail;
|
|
do
|
|
{
|
|
in.readLineInto (&line);
|
|
++line_number;
|
|
} while (detail += line, !detail.endsWith (';'));
|
|
for (auto prefix : detail.left (detail.size () - 1).split (','))
|
|
{
|
|
prefix = prefix.trimmed ();
|
|
bool exact {false};
|
|
if (prefix.startsWith ('='))
|
|
{
|
|
prefix = prefix.mid (1);
|
|
exact = true;
|
|
}
|
|
m_->prefixes_.emplace (prefix, exact, entity_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AD1CCty::~AD1CCty ()
|
|
{
|
|
}
|
|
|
|
auto AD1CCty::lookup (QString const& call) const -> Record
|
|
{
|
|
auto const& exact_search = call.toUpper ();
|
|
if (!(exact_search.endsWith ("/MM") || exact_search.endsWith ("/AM")))
|
|
{
|
|
auto search_prefix = Radio::effective_prefix (exact_search);
|
|
if (search_prefix != exact_search)
|
|
{
|
|
auto p = m_->prefixes_.find (exact_search);
|
|
if (p != m_->prefixes_.end () && p->exact_)
|
|
{
|
|
return m_->fixup (call, *p);
|
|
}
|
|
}
|
|
while (search_prefix.size ())
|
|
{
|
|
auto p = m_->prefixes_.find (search_prefix);
|
|
if (p != m_->prefixes_.end ())
|
|
{
|
|
if (!p->exact_ || call.size () == search_prefix.size ())
|
|
{
|
|
return m_->fixup (call, *p);
|
|
}
|
|
}
|
|
search_prefix = search_prefix.left (search_prefix.size () - 1);
|
|
}
|
|
}
|
|
return Record {};
|
|
}
|