diff --git a/qml/SettingsView.qml b/qml/SettingsView.qml index 2216a42..139f4a6 100644 --- a/qml/SettingsView.qml +++ b/qml/SettingsView.qml @@ -76,10 +76,6 @@ Page { console.log("Receive Key") qrz.receiveKey() } - - if(rbSwitch.checked) { - rb.checkPermissions(); - } } function apiKeyOk() { @@ -103,10 +99,11 @@ Page { Connections{ target: rb - onLocatorDone: { - console.log("LOCATOR DONE") - gridsquare.text = locator + onLocatorDone: function(locator) { + console.log("LOCATOR DONE"); + gridsquare.text = locator; saveSettings(); + rb.stopPositioning(); } } @@ -199,8 +196,7 @@ Page { Button { text : qsTr("Lookup") onClicked: { - rb.checkPermissions(); - //gridsquare.text = rb.getLocator(); + rb.tryStartPositioning(gridsquare.text) } } } diff --git a/qt5/qml/SettingsView.qml b/qt5/qml/SettingsView.qml index 03d73cc..8e6ecd0 100644 --- a/qt5/qml/SettingsView.qml +++ b/qt5/qml/SettingsView.qml @@ -76,10 +76,6 @@ Page { console.log("Receive Key") qrz.receiveKey() } - - if(rbSwitch.checked) { - rb.checkPermissions(); - } } function apiKeyOk() { @@ -103,10 +99,11 @@ Page { Connections{ target: rb - onLocatorDone: { - console.log("LOCATOR DONE") - gridsquare.text = locator + onLocatorDone: function(locator) { + console.log("LOCATOR DONE"); + gridsquare.text = locator; saveSettings(); + rb.stopPositioning(); } } @@ -199,8 +196,7 @@ Page { Button { text : qsTr("Lookup") onClicked: { - rb.checkPermissions(); - //gridsquare.text = rb.getLocator(); + rb.tryStartPositioning(gridsquare.text); } } } diff --git a/src/repeatermodel.cpp b/src/repeatermodel.cpp index 8b0c6aa..7df8ced 100644 --- a/src/repeatermodel.cpp +++ b/src/repeatermodel.cpp @@ -1,7 +1,20 @@ #include "repeatermodel.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "cacheddata.h" #include "maidenhead.h" @@ -11,60 +24,29 @@ #endif -rbManager::rbManager(QObject *parent) - : QAbstractListModel(parent) -{ - nm = nullptr; - initialized = false; -} - -rbManager::~rbManager() -{ - if(nm != nullptr) { - delete nm; - } -} +/** + * Key behaviors + * + * Construction -> empty model, empty locator, invalid cood + * tryStartPositioning() -> check permissions, start positioning if granted, + * (optionally) init locator + * positionUpdated() -> update coord and locator (if moved more than threshold), + * emit locatorDone on actual change, + * refresh database on actual change or if empty + * getLocator() -> return current locator, init from settings if unknown + * getRepeaters() -> request update of repeater list (cache, network) + * refreshModel() -> update filtered database from full data and current position + */ -void rbManager::init() -{ - if(initialized == false) { - qDebug() << "INIT"; - locator = ""; - initialized = true; - source = QGeoPositionInfoSource::createDefaultSource(this); +rbManager::rbManager(QObject *parent) + : QAbstractListModel(parent) +{} - connect(source, - SIGNAL(positionUpdated(QGeoPositionInfo)), - this, - SLOT(positionUpdated(QGeoPositionInfo))); +rbManager::~rbManager() = default; - source->setUpdateInterval(60*1000); - source->startUpdates(); - } -} -void rbManager::checkPermissions() -{ -#ifdef CLO_HAVE_QPERMISSION - QLocationPermission preciselocationPermission; - preciselocationPermission.setAccuracy(QLocationPermission::Precise); - - if(qApp->checkPermission(preciselocationPermission) != Qt::PermissionStatus::Granted) { - qApp->requestPermission(preciselocationPermission, this, [this](const QPermission &permission) { - if (permission.status() == Qt::PermissionStatus::Granted) { - qDebug() << "PRECISE PERMISSION GRANTED"; - init(); - } else { - qDebug() << "PRECISE PERMISSION NOT GRANTED"; - } - }); - } else { - qDebug() << "PRECISE PERMISSION ALREADY GRANTED"; - init(); - } -#endif -} +// Qt list model interface int rbManager::rowCount(const QModelIndex &parent) const { @@ -107,69 +89,163 @@ QHash rbManager::roleNames() const return roles; } -void rbManager::positionUpdated(const QGeoPositionInfo &info) + +// Positioning + +void rbManager::tryStartPositioning() { - qDebug() << "Position updated: " << info; - locator = maidenhead::fromGeoCoordinate(info.coordinate()); - emit locatorDone(locator); - getRepeaters(); + tryStartPositioning(locator); } -void rbManager::getRepeaters() +void rbManager::tryStartPositioning(const QString& oldLocator) { - checkPermissions(); - if (!nm) - { - nm = new QNetworkAccessManager(this); - connect(nm, - SIGNAL(finished(QNetworkReply*)), - this, - SLOT(parseNetworkResponse(QNetworkReply*))); + locator = oldLocator; + coord = maidenhead::toGeoCoordinate(locator); + +#ifdef CLO_HAVE_QPERMISSION + QLocationPermission preciselocationPermission; + preciselocationPermission.setAccuracy(QLocationPermission::Precise); + + if(qApp->checkPermission(preciselocationPermission) != Qt::PermissionStatus::Granted) { + qApp->requestPermission(preciselocationPermission, this, [this](const QPermission &permission) { + if (permission.status() == Qt::PermissionStatus::Granted) { + qDebug() << "PRECISE PERMISSION GRANTED"; + startPositioning(); + } else { + qDebug() << "PRECISE PERMISSION NOT GRANTED"; + } + }); } + + qDebug() << "PRECISE PERMISSION ALREADY GRANTED"; +#else + qDebug() << "PRECISE PERMISSION NOT IMPLEMENTED"; +#endif + startPositioning(); +} + +void rbManager::startPositioning() +{ + if (source) + return; - //QString url = "https://www.repeaterbook.com/api/exportROW.php?country="+country; - QString url = "https://hearham.com/api/repeaters/v1"; - nm->get(QNetworkRequest(QUrl(url))); + source = QGeoPositionInfoSource::createDefaultSource(this); + if (source) + { + qDebug("INIT positioning, source: %s", qUtf8Printable(source->sourceName())); + connect(source, &QGeoPositionInfoSource::positionUpdated, this, &rbManager::positionUpdated); + source->setUpdateInterval(60*1000); + source->startUpdates(); + } else { + qDebug("INIT positioning failed"); + } } -bool rbManager::filter(double rLat, double rLon, double radius) +void rbManager::stopPositioning() { - QGeoCircle circ(coord, 1000.0 * radius); - QGeoCoordinate repeater(rLat,rLon); - return circ.contains(repeater); + if (source) + { + source->stopUpdates(); + source->deleteLater(); + source = nullptr; + } } -double rbManager::distance(double rLat, double rLon) +void rbManager::positionUpdated(const QGeoPositionInfo &info) { - QGeoCoordinate repeater(rLat,rLon); - return repeater.distanceTo(coord)/1000.0; + qDebug() << "Position updated: " << info; + if (!coord.isValid() || coord.distanceTo(info.coordinate()) >= 2) // threshold: 2 m + { + coord = info.coordinate(); + auto new_locator = maidenhead::fromGeoCoordinate(coord); + if (new_locator != locator) + { + locator = new_locator; + emit locatorDone(locator); + getRepeaters(); + } + else if(database.isEmpty()) + { + getRepeaters(); + } + } } QString rbManager::getLocator() { + if (locator.isEmpty()) + { + const auto gridsquare = settings.value("gridsquare").toString(); + const auto approximate_coord = maidenhead::toGeoCoordinate(gridsquare); + if (approximate_coord.isValid()) + { + coord = approximate_coord; + locator = gridsquare; + emit locatorDone(locator); + } + } return locator; } -void rbManager::parseNetworkResponse(QNetworkReply *nreply) // from getRepeaters + +// Repeater list initialization and refresh + +void rbManager::getRepeaters() { - const QByteArray rawJson = nreply->readAll(); - const QJsonDocument jsonResponse = QJsonDocument::fromJson(rawJson); + QByteArray cachedData; + const auto max_age = 24*3600; // 24 h + if (cachedData::restore("repeaters", cachedData, max_age)) + { + refreshModel(cachedData); + return; + } - const QJsonArray jsonRepeaters = jsonResponse.array(); + // Use network. + if (!nm) + { + nm = new QNetworkAccessManager(this); + connect(nm, &QNetworkAccessManager::finished, this, &rbManager::parseNetworkResponse); + } + else if (request_timer.isValid() && request_timer.elapsed() < 60*1000 /*milliseconds*/) + { + return; // rate limiting + } + QNetworkRequest request(QUrl("http://hearham.com/api/repeaters/v1")); + request.setHeader(QNetworkRequest::UserAgentHeader, "CloudLogOffline/" GIT_VERSION); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + nm->get(request); +} +void rbManager::parseNetworkResponse(QNetworkReply* nreply) +{ + request_timer.invalidate(); + const QByteArray rawData = nreply->readAll(); + if (refreshModel(rawData)) + cachedData::save("repeaters", rawData); +} + +bool rbManager::refreshModel(const QByteArray& rawData) +{ + const QJsonDocument jsonResponse = QJsonDocument::fromJson(rawData); + if (jsonResponse.isNull()) + return false; + beginResetModel(); database.clear(); double radius = settings.value("rbRadius").toDouble(); if (radius <= 0.0) radius = 20.0; - + const QGeoCircle neighborhood(coord, 1000.0 * radius); + + const QJsonArray jsonRepeaters = jsonResponse.array(); for (const auto& jsonRepeater : jsonRepeaters) { const QJsonObject repeater = jsonRepeater.toObject(); double rlat = repeater["latitude"].toDouble(); double rlon = repeater["longitude"].toDouble(); + auto repeaterCoord = QGeoCoordinate(rlat, rlon); - if(filter(rlat, rlon, radius)) { + if (neighborhood.contains(repeaterCoord)) { qDebug() << "Found: " << repeater["callsign"].toString(); relais r; r.call = repeater["callsign"].toString(); @@ -177,7 +253,7 @@ void rbManager::parseNetworkResponse(QNetworkReply *nreply) // from getRepeaters r.lat = QString::number(rlat); r.lon = QString::number(rlon); r.shift = QString::number(repeater["offset"].toDouble()/1000.0/1000.0); - r.distance = distance(rlat, rlon); + r.distance = coord.distanceTo(repeaterCoord) / 1000.0; r.tone = repeater["decode"].toString(); r.city = repeater["city"].toString().split(QLatin1Char(','))[0]; r.modes = repeater["mode"].toString(); @@ -190,4 +266,5 @@ void rbManager::parseNetworkResponse(QNetworkReply *nreply) // from getRepeaters }); endResetModel(); + return true; } diff --git a/src/repeatermodel.h b/src/repeatermodel.h index 80198d4..d4a9f79 100644 --- a/src/repeatermodel.h +++ b/src/repeatermodel.h @@ -1,20 +1,22 @@ #ifndef RBMODEL_H #define RBMODEL_H +#include +#include +#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include #include +QT_BEGIN_NAMESPACE +class QGeoPositionInfo; +class QGeoPositionInfoSource; +class QNetworkAccessManager; +class QNetworkReply; +QT_END_NAMESPACE + + struct relais { QString call; QString lat; @@ -46,36 +48,41 @@ class rbManager : public QAbstractListModel public: rbManager(QObject *parent = nullptr); ~rbManager(); + + // Qt list model interface int rowCount(const QModelIndex &parent) const; QVariant data(const QModelIndex &index, int role) const; QHash roleNames() const; - + + // Positioning and locator interface signals: void locatorDone(const QString &locator); public slots: QString getLocator(); void getRepeaters(); - void checkPermissions(); - + void tryStartPositioning(); + void tryStartPositioning(const QString& oldLocator); + void stopPositioning(); + private: - QGeoPositionInfoSource *source; - QNetworkAccessManager *nm; - - QString country; - QString locator; - QGeoCoordinate coord; - bool initialized; - bool filter(double rLat, double rLon, double radius); - double distance(double rLat, double rLon); - void init(); - - QList database; - + void startPositioning(); + + bool refreshModel(const QByteArray &rawData); + private Q_SLOTS: void positionUpdated(const QGeoPositionInfo &info); void parseNetworkResponse(QNetworkReply* nreply); - + +private: + QList database; + QString locator; + QGeoCoordinate coord; + + QGeoPositionInfoSource *source = nullptr; + QNetworkAccessManager *nm = nullptr; + QElapsedTimer request_timer; + protected: QSettings settings; };