2018-11-07 12:49:45 -05:00
|
|
|
#include "FoxLog.hpp"
|
|
|
|
|
2018-11-11 21:06:44 -05:00
|
|
|
#include <stdexcept>
|
|
|
|
#include <utility>
|
2018-11-07 18:55:15 -05:00
|
|
|
#include <QString>
|
2018-11-07 12:49:45 -05:00
|
|
|
#include <QDateTime>
|
|
|
|
#include <QSqlDatabase>
|
|
|
|
#include <QSqlTableModel>
|
|
|
|
#include <QSqlRecord>
|
|
|
|
#include <QSqlError>
|
|
|
|
#include <QSqlQuery>
|
2018-12-30 07:35:41 -05:00
|
|
|
#include <QTextStream>
|
2018-11-07 12:49:45 -05:00
|
|
|
#include <QDebug>
|
2023-04-25 22:38:47 -04:00
|
|
|
#include "Logger.hpp"
|
2018-12-30 07:35:41 -05:00
|
|
|
#include "Configuration.hpp"
|
2018-11-11 21:06:44 -05:00
|
|
|
#include "qt_db_helpers.hpp"
|
2018-11-07 12:49:45 -05:00
|
|
|
#include "pimpl_impl.hpp"
|
|
|
|
|
|
|
|
class FoxLog::impl final
|
|
|
|
: public QSqlTableModel
|
|
|
|
{
|
2019-06-06 07:56:25 -04:00
|
|
|
Q_OBJECT
|
|
|
|
|
2018-11-07 12:49:45 -05:00
|
|
|
public:
|
2018-12-30 07:35:41 -05:00
|
|
|
impl (Configuration const * configuration);
|
2018-11-11 21:06:44 -05:00
|
|
|
|
2019-05-03 05:21:50 -04:00
|
|
|
QVariant data (QModelIndex const& index, int role) const
|
|
|
|
{
|
|
|
|
auto value = QSqlTableModel::data (index, role);
|
2019-12-08 13:37:35 -05:00
|
|
|
if (index.column () == fieldIndex ("when") && Qt::DisplayRole == role)
|
2019-05-03 05:21:50 -04:00
|
|
|
{
|
2019-12-08 13:37:35 -05:00
|
|
|
QLocale locale;
|
|
|
|
value = locale.toString (QDateTime::fromMSecsSinceEpoch (value.toULongLong () * 1000ull, Qt::UTC), locale.dateFormat (QLocale::ShortFormat) + " hh:mm:ss");
|
2019-05-03 05:21:50 -04:00
|
|
|
}
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2018-12-30 07:35:41 -05:00
|
|
|
Configuration const * configuration_;
|
2018-11-11 21:06:44 -05:00
|
|
|
QSqlQuery mutable dupe_query_;
|
2018-12-30 07:35:41 -05:00
|
|
|
QSqlQuery mutable export_query_;
|
2023-04-25 22:38:47 -04:00
|
|
|
// queries for rates
|
|
|
|
QSqlQuery mutable rate_n_query_;
|
|
|
|
QSqlQuery mutable rate60m_query_;
|
|
|
|
QString rate();
|
2018-11-07 12:49:45 -05:00
|
|
|
};
|
|
|
|
|
2019-06-06 07:56:25 -04:00
|
|
|
#include "FoxLog.moc"
|
|
|
|
|
2019-11-18 11:45:16 -05:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
QString const fox_log_ddl {
|
|
|
|
"CREATE %1 TABLE fox_log%2 ("
|
|
|
|
" id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"
|
|
|
|
" \"when\" DATETIME NOT NULL,"
|
|
|
|
" call VARCHAR(20) NOT NULL,"
|
|
|
|
" grid VARCHAR(4),"
|
|
|
|
" report_sent VARCHAR(3),"
|
|
|
|
" report_rcvd VARCHAR(3),"
|
|
|
|
" band VARCHAR(6) NOT NULL"
|
|
|
|
")"
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-12-30 07:35:41 -05:00
|
|
|
FoxLog::impl::impl (Configuration const * configuration)
|
|
|
|
: configuration_ {configuration}
|
2018-11-07 12:49:45 -05:00
|
|
|
{
|
|
|
|
if (!database ().tables ().contains ("fox_log"))
|
|
|
|
{
|
|
|
|
QSqlQuery query;
|
2018-11-11 21:06:44 -05:00
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
2019-11-18 11:45:16 -05:00
|
|
|
fox_log_ddl.arg ("").arg (""));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
QSqlQuery query;
|
|
|
|
// query to check if table has a unique constraint
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
"SELECT COUNT(*)"
|
|
|
|
" FROM sqlite_master"
|
|
|
|
" WHERE"
|
|
|
|
" type = 'index' AND tbl_name = 'fox_log'");
|
|
|
|
query.next ();
|
|
|
|
if (query.value (0).toInt ())
|
|
|
|
{
|
|
|
|
// update to new schema with no dupe disallowing unique
|
|
|
|
// constraint
|
|
|
|
database ().transaction ();
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
fox_log_ddl.arg ("TEMPORARY").arg ("_backup"));
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
"INSERT INTO fox_log_backup SELECT * from fox_log");
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
"DROP TABLE fox_log");
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
fox_log_ddl.arg ("").arg (""));
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
"INSERT INTO fox_log SELECT * from fox_log_backup");
|
|
|
|
SQL_error_check (query, static_cast<bool (QSqlQuery::*) (QString const&)> (&QSqlQuery::exec),
|
|
|
|
"DROP TABLE fox_log_backup");
|
|
|
|
database ().commit ();
|
|
|
|
}
|
2018-11-07 12:49:45 -05:00
|
|
|
}
|
|
|
|
|
2018-11-11 21:06:44 -05:00
|
|
|
SQL_error_check (dupe_query_, &QSqlQuery::prepare,
|
2019-01-21 08:35:18 -05:00
|
|
|
"SELECT "
|
|
|
|
" COUNT(*) "
|
|
|
|
" FROM "
|
|
|
|
" fox_log "
|
|
|
|
" WHERE "
|
|
|
|
" call = :call "
|
|
|
|
" AND band = :band");
|
2018-11-11 21:06:44 -05:00
|
|
|
|
2018-12-30 07:35:41 -05:00
|
|
|
SQL_error_check (export_query_, &QSqlQuery::prepare,
|
2019-01-21 08:35:18 -05:00
|
|
|
"SELECT "
|
|
|
|
" band"
|
|
|
|
" , \"when\""
|
|
|
|
" , call"
|
|
|
|
" , grid"
|
|
|
|
" , report_sent"
|
|
|
|
" , report_rcvd "
|
|
|
|
" FROM "
|
|
|
|
" fox_log "
|
|
|
|
" ORDER BY "
|
|
|
|
" \"when\"");
|
2018-12-30 07:35:41 -05:00
|
|
|
|
2023-04-25 22:38:47 -04:00
|
|
|
SQL_error_check (rate_n_query_, &QSqlQuery::prepare,
|
|
|
|
"SELECT "
|
|
|
|
" \"when\""
|
|
|
|
" FROM "
|
|
|
|
" fox_log "
|
|
|
|
" ORDER BY "
|
|
|
|
" \"when\" DESC"
|
2023-04-25 23:08:33 -04:00
|
|
|
" LIMIT :limitn"
|
2023-04-25 22:38:47 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
SQL_error_check (rate60m_query_, &QSqlQuery::prepare,
|
|
|
|
"SELECT "
|
|
|
|
" COUNT() "
|
|
|
|
" FROM "
|
|
|
|
" fox_log "
|
|
|
|
" where \"when\" > :one_hour_ago"
|
|
|
|
" ORDER BY "
|
|
|
|
" \"when\" DESC"
|
|
|
|
);
|
2018-12-01 21:30:32 -05:00
|
|
|
setEditStrategy (QSqlTableModel::OnFieldChange);
|
2018-11-07 12:49:45 -05:00
|
|
|
setTable ("fox_log");
|
2018-11-11 23:00:55 -05:00
|
|
|
setHeaderData (fieldIndex ("when"), Qt::Horizontal, tr ("Date & Time(UTC)"));
|
2018-11-07 12:49:45 -05:00
|
|
|
setHeaderData (fieldIndex ("call"), Qt::Horizontal, tr ("Call"));
|
|
|
|
setHeaderData (fieldIndex ("grid"), Qt::Horizontal, tr ("Grid"));
|
|
|
|
setHeaderData (fieldIndex ("report_sent"), Qt::Horizontal, tr ("Sent"));
|
|
|
|
setHeaderData (fieldIndex ("report_rcvd"), Qt::Horizontal, tr ("Rcvd"));
|
|
|
|
setHeaderData (fieldIndex ("band"), Qt::Horizontal, tr ("Band"));
|
2018-12-06 00:41:16 -05:00
|
|
|
|
|
|
|
// This descending order by time is important, it makes the view
|
|
|
|
// place the latest row at the top, without this the model/view
|
|
|
|
// interactions are both sluggish and unhelpful.
|
|
|
|
setSort (fieldIndex ("when"), Qt::DescendingOrder);
|
|
|
|
|
2018-11-11 21:06:44 -05:00
|
|
|
SQL_error_check (*this, &QSqlTableModel::select);
|
2018-11-07 12:49:45 -05:00
|
|
|
}
|
|
|
|
|
2023-04-25 22:38:47 -04:00
|
|
|
QString FoxLog::rate()
|
|
|
|
{
|
2023-05-03 22:31:35 -04:00
|
|
|
return QString("Last 10: %1, Last 100: %2, Last 60m: %3").arg(QString::number(this->rate_last_n(10),'f',0),
|
2023-04-25 23:08:33 -04:00
|
|
|
QString::number(this->rate_last_n(100),'f',0),
|
2023-04-25 22:38:47 -04:00
|
|
|
QString::number(this->rate_60m()));
|
|
|
|
}
|
|
|
|
|
2018-12-30 07:35:41 -05:00
|
|
|
FoxLog::FoxLog (Configuration const * configuration)
|
|
|
|
: m_ {configuration}
|
2018-11-07 12:49:45 -05:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
FoxLog::~FoxLog ()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2018-11-25 17:19:41 -05:00
|
|
|
QSqlTableModel * FoxLog::model ()
|
2018-11-07 12:49:45 -05:00
|
|
|
{
|
|
|
|
return &*m_;
|
|
|
|
}
|
|
|
|
|
2018-11-11 21:06:44 -05:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
void set_value_maybe_null (QSqlRecord& record, QString const& name, QString const& value)
|
|
|
|
{
|
|
|
|
if (value.size ())
|
|
|
|
{
|
|
|
|
record.setValue (name, value);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
record.setNull (name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-07 12:49:45 -05:00
|
|
|
bool FoxLog::add_QSO (QDateTime const& when, QString const& call, QString const& grid
|
2018-11-12 13:26:05 -05:00
|
|
|
, QString const& report_sent, QString const& report_received
|
2018-11-07 12:49:45 -05:00
|
|
|
, QString const& band)
|
|
|
|
{
|
|
|
|
auto record = m_->record ();
|
2018-11-22 20:18:39 -05:00
|
|
|
if (!when.isNull ())
|
|
|
|
{
|
|
|
|
record.setValue ("when", when.toMSecsSinceEpoch () / 1000);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
record.setNull ("when");
|
|
|
|
}
|
|
|
|
set_value_maybe_null (record, "call", call);
|
2018-11-11 21:06:44 -05:00
|
|
|
set_value_maybe_null (record, "grid", grid);
|
|
|
|
set_value_maybe_null (record, "report_sent", report_sent);
|
|
|
|
set_value_maybe_null (record, "report_rcvd", report_received);
|
2018-11-22 20:18:39 -05:00
|
|
|
set_value_maybe_null (record, "band", band);
|
2018-12-01 21:30:32 -05:00
|
|
|
if (m_->isDirty ())
|
|
|
|
{
|
|
|
|
m_->revert (); // discard any uncommitted changes
|
|
|
|
}
|
2018-12-06 00:41:16 -05:00
|
|
|
m_->setEditStrategy (QSqlTableModel::OnManualSubmit);
|
|
|
|
ConditionalTransaction transaction {*m_};
|
2018-11-25 17:19:41 -05:00
|
|
|
auto ok = m_->insertRecord (-1, record);
|
|
|
|
if (ok)
|
2018-11-07 12:49:45 -05:00
|
|
|
{
|
2018-12-06 00:41:16 -05:00
|
|
|
ok = transaction.submit (false);
|
2018-11-07 12:49:45 -05:00
|
|
|
}
|
2018-12-06 00:41:16 -05:00
|
|
|
m_->setEditStrategy (QSqlTableModel::OnFieldChange);
|
2018-11-25 17:19:41 -05:00
|
|
|
return ok;
|
2018-11-07 12:49:45 -05:00
|
|
|
}
|
2018-11-11 21:06:44 -05:00
|
|
|
|
|
|
|
bool FoxLog::dupe (QString const& call, QString const& band) const
|
|
|
|
{
|
|
|
|
m_->dupe_query_.bindValue (":call", call);
|
|
|
|
m_->dupe_query_.bindValue (":band", band);
|
|
|
|
SQL_error_check (m_->dupe_query_, static_cast<bool (QSqlQuery::*) ()> (&QSqlQuery::exec));
|
|
|
|
m_->dupe_query_.next ();
|
|
|
|
return m_->dupe_query_.value (0).toInt ();
|
|
|
|
}
|
|
|
|
|
|
|
|
void FoxLog::reset ()
|
|
|
|
{
|
2019-01-21 08:35:18 -05:00
|
|
|
// synchronize model
|
|
|
|
while (m_->canFetchMore ()) m_->fetchMore ();
|
2018-11-11 23:00:55 -05:00
|
|
|
if (m_->rowCount ())
|
|
|
|
{
|
2018-11-25 17:19:41 -05:00
|
|
|
m_->setEditStrategy (QSqlTableModel::OnManualSubmit);
|
2018-11-11 23:00:55 -05:00
|
|
|
ConditionalTransaction transaction {*m_};
|
|
|
|
SQL_error_check (*m_, &QSqlTableModel::removeRows, 0, m_->rowCount (), QModelIndex {});
|
|
|
|
transaction.submit ();
|
2018-11-25 17:19:41 -05:00
|
|
|
m_->select (); // to refresh views
|
2018-12-01 21:30:32 -05:00
|
|
|
m_->setEditStrategy (QSqlTableModel::OnFieldChange);
|
2018-11-11 23:00:55 -05:00
|
|
|
}
|
2018-11-11 21:06:44 -05:00
|
|
|
}
|
2018-12-30 07:35:41 -05:00
|
|
|
|
2023-04-25 22:38:47 -04:00
|
|
|
int FoxLog::rate_60m()
|
|
|
|
{
|
|
|
|
int rate60m = 0;
|
|
|
|
qlonglong const& one_hour_ago = QDateTime::currentDateTime().addSecs(-3600).toMSecsSinceEpoch () / 1000;
|
|
|
|
|
|
|
|
// query the 60m rate
|
|
|
|
m_->rate60m_query_.bindValue (":one_hour_ago", one_hour_ago);
|
|
|
|
SQL_error_check (m_->rate60m_query_, static_cast<bool (QSqlQuery::*) ()> (&QSqlQuery::exec));
|
|
|
|
m_->rate60m_query_.next ();
|
|
|
|
rate60m = m_->rate60m_query_.value (0).toLongLong();
|
|
|
|
return rate60m;
|
|
|
|
//
|
|
|
|
}
|
|
|
|
|
|
|
|
double FoxLog::rate_last_n(int n)
|
|
|
|
{
|
|
|
|
double rate_interval = 0;
|
|
|
|
|
|
|
|
qlonglong const& secs_now = QDateTime::currentDateTime().toMSecsSinceEpoch () / 1000;
|
|
|
|
|
|
|
|
// get last n or up to n
|
2023-04-25 23:08:33 -04:00
|
|
|
m_->rate_n_query_.bindValue (":limitn", n);
|
2023-04-25 22:38:47 -04:00
|
|
|
SQL_error_check (m_->rate_n_query_, static_cast<bool (QSqlQuery::*) ()> (&QSqlQuery::exec));
|
|
|
|
|
|
|
|
m_->rate_n_query_.next();
|
|
|
|
if (!m_->rate_n_query_.isValid()) {
|
|
|
|
LOG_ERROR(QString("rate_n result is not valid. Last error %1").arg(m_->rate_n_query_.lastError().text()));
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
// size / (time_now - time_of_first)
|
|
|
|
m_->rate_n_query_.last();
|
|
|
|
rate_interval = secs_now - m_->rate_n_query_.value (0).toLongLong ();
|
2023-04-28 10:59:38 -04:00
|
|
|
if (rate_interval == 0) return 0.0;
|
2023-04-25 22:38:47 -04:00
|
|
|
|
|
|
|
m_->rate_n_query_.first(); // count the records
|
2023-04-25 23:08:33 -04:00
|
|
|
|
2023-04-25 22:38:47 -04:00
|
|
|
int size = 1;
|
|
|
|
while (m_->rate_n_query_.next() && m_->rate_n_query_.isValid()) size++;
|
|
|
|
return (size/rate_interval) * 3600;
|
|
|
|
}
|
|
|
|
|
2018-12-30 07:35:41 -05:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
struct ADIF_field
|
|
|
|
{
|
|
|
|
explicit ADIF_field (QString const& name, QString const& value)
|
|
|
|
: name_ {name}
|
|
|
|
, value_ {value}
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
QString name_;
|
|
|
|
QString value_;
|
|
|
|
};
|
|
|
|
|
|
|
|
QTextStream& operator << (QTextStream& os, ADIF_field const& field)
|
|
|
|
{
|
|
|
|
if (field.value_.size ())
|
|
|
|
{
|
|
|
|
os << QString {"<%1:%2>%3 "}.arg (field.name_).arg (field.value_.size ()).arg (field.value_);
|
|
|
|
}
|
|
|
|
return os;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void FoxLog::export_qsos (QTextStream& out) const
|
|
|
|
{
|
|
|
|
out << "WSJT-X FT8 DXpedition Mode Fox Log\n<eoh>";
|
|
|
|
|
|
|
|
SQL_error_check (m_->export_query_, static_cast<bool (QSqlQuery::*) ()> (&QSqlQuery::exec));
|
|
|
|
auto record = m_->export_query_.record ();
|
|
|
|
auto band_index = record.indexOf ("band");
|
|
|
|
auto when_index = record.indexOf ("when");
|
|
|
|
auto call_index = record.indexOf ("call");
|
|
|
|
auto grid_index = record.indexOf ("grid");
|
|
|
|
auto sent_index = record.indexOf ("report_sent");
|
|
|
|
auto rcvd_index = record.indexOf ("report_rcvd");
|
|
|
|
while (m_->export_query_.next ())
|
|
|
|
{
|
|
|
|
auto when = QDateTime::fromMSecsSinceEpoch (m_->export_query_.value (when_index).toULongLong () * 1000ull, Qt::UTC);
|
|
|
|
out << '\n'
|
|
|
|
<< ADIF_field {"band", m_->export_query_.value (band_index).toString ()}
|
|
|
|
<< ADIF_field {"mode", "FT8"}
|
|
|
|
<< ADIF_field {"qso_date", when.toString ("yyyyMMdd")}
|
|
|
|
<< ADIF_field {"time_on", when.toString ("hhmmss")}
|
|
|
|
<< ADIF_field {"call", m_->export_query_.value (call_index).toString ()}
|
|
|
|
<< ADIF_field {"gridsquare", m_->export_query_.value (grid_index).toString ()}
|
|
|
|
<< ADIF_field {"rst_sent", m_->export_query_.value (sent_index).toString ()}
|
|
|
|
<< ADIF_field {"rst_rcvd", m_->export_query_.value (rcvd_index).toString ()}
|
|
|
|
<< ADIF_field {"station_callsign", m_->configuration_->my_callsign ()}
|
|
|
|
<< ADIF_field {"my_gridsquare", m_->configuration_->my_grid ()}
|
|
|
|
<< ADIF_field {"operator", m_->configuration_->opCall ()}
|
|
|
|
<< "<eor>";
|
|
|
|
}
|
2020-06-13 11:04:41 -04:00
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
|
2018-12-30 07:35:41 -05:00
|
|
|
out << endl;
|
2020-06-13 11:04:41 -04:00
|
|
|
#else
|
|
|
|
out << Qt::endl;
|
|
|
|
#endif
|
2018-12-30 07:35:41 -05:00
|
|
|
}
|