mirror of
				https://github.com/saitohirga/WSJT-X.git
				synced 2025-10-31 13:10:19 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			464 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			464 lines
		
	
	
		
			13 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 <QRegularExpression>
 | |
| #include "Configuration.hpp"
 | |
| #include "Radio.hpp"
 | |
| #include "pimpl_impl.hpp"
 | |
| #include "Logger.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:
 | |
|   using entity_by_id = entities_type::index<id>::type;
 | |
| 
 | |
|   explicit impl (Configuration const * configuration)
 | |
|     : configuration_ {configuration}
 | |
|   {
 | |
|   }
 | |
| 
 | |
|   QString get_cty_path(const Configuration *configuration);
 | |
|   void load_cty(QFile &file);
 | |
| 
 | |
|   entity_by_id::iterator lookup_entity (QString call, prefix const& p) const
 | |
|   {
 | |
|     call = call.toUpper ();
 | |
|     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
 | |
|         return entities_.project<id> (entities_.get<primary_prefix> ().find ("K"));
 | |
|       }
 | |
|     else
 | |
|       {
 | |
|         return entities_.get<id> ().find (p.entity_id_);
 | |
|       }
 | |
|   }
 | |
| 
 | |
|   Record fixup (prefix const& p, entity const& e) const
 | |
|   {
 | |
|     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;
 | |
|   }
 | |
| 
 | |
|   Configuration const * configuration_;
 | |
|   QString path_;
 | |
|   QString cty_version_;
 | |
|   QString cty_version_date_;
 | |
| 
 | |
|   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";
 | |
|     }
 | |
| }
 | |
| 
 | |
| QString AD1CCty::impl::get_cty_path(Configuration const * configuration)
 | |
| {
 | |
|   QDir dataPath {QStandardPaths::writableLocation (QStandardPaths::DataLocation)};
 | |
|   auto path = dataPath.exists (file_name)
 | |
|               ? dataPath.absoluteFilePath (file_name) // user override
 | |
|               : configuration->data_dir ().absoluteFilePath (file_name); // or original
 | |
|   return path;
 | |
| }
 | |
| 
 | |
| void AD1CCty::impl::load_cty(QFile &file)
 | |
| {
 | |
|   QRegularExpression version_pattern{R"(VER\d{8})"};
 | |
|   int entity_id = 0;
 | |
|   int line_number{0};
 | |
| 
 | |
|   entities_.clear();
 | |
|   prefixes_.clear();
 | |
|   cty_version_ = QString{};
 | |
|   cty_version_date_ = QString{};
 | |
| 
 | |
|   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;
 | |
|         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;
 | |
|             // match version pattern to prefix
 | |
|             if (version_pattern.match(prefix).hasMatch())
 | |
|             {
 | |
|               cty_version_date_ = prefix;
 | |
|             }
 | |
|           }
 | |
|           prefixes_.emplace(prefix, exact, entity_id);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| AD1CCty::AD1CCty (Configuration const * configuration)
 | |
|   : m_ {configuration}
 | |
| {
 | |
|   Q_ASSERT (configuration);
 | |
|   // TODO: G4WJS - consider doing the following asynchronously to
 | |
|   // speed up startup. Not urgent as it takes less than 0.5s on a Core
 | |
|   // i7 reading BIG CTY.DAT.
 | |
|   AD1CCty::reload (configuration);
 | |
|   }
 | |
| 
 | |
| void AD1CCty::reload(Configuration const * configuration)
 | |
| {
 | |
|   m_->path_ = m_->impl::get_cty_path(configuration);
 | |
|   QFile file {m_->path_};
 | |
| 
 | |
|   LOG_INFO(QString{"Loading CTY.DAT from %1"}.arg (m_->path_));
 | |
| 
 | |
|   if (file.open (QFile::ReadOnly))
 | |
|   {
 | |
|     m_->impl::load_cty(file);
 | |
|     m_->cty_version_ = AD1CCty::lookup("VERSION").entity_name;
 | |
|     Q_EMIT cty_loaded(m_->cty_version_);
 | |
|     LOG_INFO(QString{"Loaded CTY.DAT version %1, %2"}.arg (m_->cty_version_date_).arg (m_->cty_version_));
 | |
|   }
 | |
| }
 | |
| 
 | |
| 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 (*p, *m_->lookup_entity (call, *p));
 | |
|             }
 | |
|         }
 | |
|       while (search_prefix.size ())
 | |
|         {
 | |
|           auto p = m_->prefixes_.find (search_prefix);
 | |
|           if (p != m_->prefixes_.end ())
 | |
|             {
 | |
|               impl::entity_by_id::iterator e = m_->lookup_entity (call, *p);
 | |
|               // always lookup WAE entities, we substitute them later in displaytext.cpp if "Include extra WAE entites" is not selected
 | |
|               if (!p->exact_ || call.size () == search_prefix.size ())
 | |
|                 {
 | |
|                   return m_->fixup (*p, *e);
 | |
|                 }
 | |
|             }
 | |
|           search_prefix = search_prefix.left (search_prefix.size () - 1);
 | |
|         }
 | |
|     }
 | |
|   return Record {};
 | |
| }
 | |
| auto AD1CCty::version () const -> QString
 | |
| {
 | |
|   return m_->cty_version_date_;
 | |
| }
 |