From be9670529ec50ee96fe80bfc074dfb15493cdd4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beat=20K=C3=BCng?= Date: Mon, 10 Jul 2017 19:17:40 +0200 Subject: [PATCH] AirMap: cleanup/refactoring + login + upload flight --- src/MissionManager/AirMapManager.cc | 481 ++++++++++++++++++++++++++++++------ src/MissionManager/AirMapManager.h | 211 ++++++++++++++-- 2 files changed, 597 insertions(+), 95 deletions(-) diff --git a/src/MissionManager/AirMapManager.cc b/src/MissionManager/AirMapManager.cc index d5c1c02..e029b11 100644 --- a/src/MissionManager/AirMapManager.cc +++ b/src/MissionManager/AirMapManager.cc @@ -1,6 +1,6 @@ /**************************************************************************** * - * (c) 2009-2016 QGROUNDCONTROL PROJECT + * (c) 2017 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. @@ -14,6 +14,7 @@ #include "SettingsManager.h" #include "AppSettings.h" #include "QGCQGeoCoordinate.h" +#include "QGCApplication.h" #include #include @@ -24,56 +25,226 @@ QGC_LOGGING_CATEGORY(AirMapManagerLog, "AirMapManagerLog") -AirMapManager::AirMapManager(QGCApplication* app, QGCToolbox* toolbox) - : QGCTool(app, toolbox) + +AirMapLogin::AirMapLogin(QNetworkAccessManager& networkManager, const QString& APIKey) + : _networkManager(networkManager), _APIKey(APIKey) { - _updateTimer.setInterval(2000); - _updateTimer.setSingleShot(true); - connect(&_updateTimer, &QTimer::timeout, this, &AirMapManager::_updateToROI); } -AirMapManager::~AirMapManager() +void AirMapLogin::setCredentials(const QString& clientID, const QString& userName, const QString& password) { + logout(); + _clientID = clientID; + _userName = userName; + _password = password; +} + +void AirMapLogin::login() +{ + if (isLoggedIn() || _isLoginInProgress) { + return; + } + _isLoginInProgress = true; + + QUrlQuery postData; + postData.addQueryItem(QStringLiteral("grant_type"), "password"); + postData.addQueryItem(QStringLiteral("client_id"), _clientID); + postData.addQueryItem(QStringLiteral("connection"), "Username-Password-Authentication"); + postData.addQueryItem(QStringLiteral("username"), _userName); + postData.addQueryItem(QStringLiteral("password"), _password); + postData.addQueryItem(QStringLiteral("scope"), "openid offline_access"); + postData.addQueryItem(QStringLiteral("device"), "test"); + + QUrl url(QStringLiteral("https://sso.airmap.io/oauth/ro")); + _post(url, postData.toString(QUrl::FullyEncoded).toUtf8()); } -void AirMapManager::setROI(QGeoCoordinate& center, double radiusMeters) +void AirMapLogin::_requestFinished(void) { - _roiCenter = center; - _roiRadius = radiusMeters; - _updateTimer.start(); + QNetworkReply* reply = qobject_cast(QObject::sender()); + _isLoginInProgress = false; + + QByteArray responseBytes = reply->readAll(); + QJsonParseError parseError; + QJsonDocument responseJson = QJsonDocument::fromJson(responseBytes, &parseError); + if (parseError.error != QJsonParseError::NoError) { + QNetworkReply::NetworkError networkError = QNetworkReply::NetworkError::UnknownNetworkError; + emit loginFailure(networkError, "", ""); + return; + } + QJsonObject rootObject = responseJson.object(); + + // When an error occurs we still end up here + if (reply->error() != QNetworkReply::NoError) { + QJsonValue errorDescription = rootObject.value("error_description"); + QString serverError = ""; + if (errorDescription.isString()) { + serverError = errorDescription.toString(); + } + emit loginFailure(reply->error(), reply->errorString(), serverError); + return; + } + + _JWTToken = rootObject["id_token"].toString(); + + if (_JWTToken == "") { // make sure we got a token + QNetworkReply::NetworkError networkError = QNetworkReply::NetworkError::AuthenticationRequiredError; + emit loginFailure(networkError, "", ""); + return; + } + + emit loginSuccess(); } -void AirMapManager::_get(QUrl url) +void AirMapLogin::_requestError(QNetworkReply::NetworkError code) { - QNetworkRequest request(url); + Q_UNUSED(code); + // handled in _requestFinished() +} - //qDebug() << url.toString(QUrl::FullyEncoded); - //qDebug() << "Settings API key: " << _toolbox->settingsManager()->appSettings()->airMapKey()->rawValueString(); +void AirMapLogin::_post(QUrl url, const QByteArray& postData) +{ + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - request.setRawHeader("X-API-Key", _toolbox->settingsManager()->appSettings()->airMapKey()->rawValueString().toUtf8()); + request.setRawHeader("X-API-Key", _APIKey.toUtf8()); QNetworkProxy tProxy; tProxy.setType(QNetworkProxy::DefaultProxy); _networkManager.setProxy(tProxy); - QNetworkReply* networkReply = _networkManager.get(request); + QNetworkReply* networkReply = _networkManager.post(request, postData); + if (!networkReply) { + QNetworkReply::NetworkError networkError = QNetworkReply::NetworkError::UnknownNetworkError; + emit loginFailure(networkError, "", ""); + return; + } + + connect(networkReply, &QNetworkReply::finished, this, &AirMapLogin::_requestFinished); + connect(networkReply, static_cast(&QNetworkReply::error), this, &AirMapLogin::_requestError); +} + + +AirMapNetworking::AirMapNetworking(SharedData& networkingData) + : _networkingData(networkingData) +{ +} + +void AirMapNetworking::post(QUrl url, const QByteArray& postData, bool isJsonData, bool requiresLogin) +{ + QNetworkRequest request(url); + if (isJsonData) { + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=utf-8"); + } else { + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + } + + request.setRawHeader("X-API-Key", _networkingData.airmapAPIKey.toUtf8()); + + if (requiresLogin) { + if (_networkingData.login.isLoggedIn()) { + request.setRawHeader("Authorization", (QString("Bearer ")+_networkingData.login.JWTToken()).toUtf8()); + } else { + connect(&_networkingData.login, &AirMapLogin::loginSuccess, this, &AirMapNetworking::_loginSuccess); + connect(&_networkingData.login, &AirMapLogin::loginFailure, this, &AirMapNetworking::_loginFailure); + _pendingRequest.type = RequestType::POST; + _pendingRequest.url = url; + _pendingRequest.postData = postData; + _pendingRequest.isJsonData = isJsonData; + _pendingRequest.requiresLogin = requiresLogin; + _networkingData.login.login(); + return; + } + } + + QNetworkProxy tProxy; + tProxy.setType(QNetworkProxy::DefaultProxy); + _networkingData.networkManager.setProxy(tProxy); + + QNetworkReply* networkReply = _networkingData.networkManager.post(request, postData); if (!networkReply) { - // FIXME - qWarning() << "QNetworkAccessManager::get failed"; + QNetworkReply::NetworkError networkError = QNetworkReply::NetworkError::UnknownNetworkError; + emit error(networkError, "", ""); return; } - connect(networkReply, &QNetworkReply::finished, this, &AirMapManager::_getFinished); - connect(networkReply, static_cast(&QNetworkReply::error), this, &AirMapManager::_getError); + connect(networkReply, &QNetworkReply::finished, this, &AirMapNetworking::_requestFinished); } -void AirMapManager::_getFinished(void) +void AirMapNetworking::_loginSuccess() +{ + disconnect(&_networkingData.login, &AirMapLogin::loginSuccess, this, &AirMapNetworking::_loginSuccess); + disconnect(&_networkingData.login, &AirMapLogin::loginFailure, this, &AirMapNetworking::_loginFailure); + + if (_pendingRequest.type == RequestType::GET) { + get(_pendingRequest.url, _pendingRequest.requiresLogin); + } else if (_pendingRequest.type == RequestType::POST) { + post(_pendingRequest.url, _pendingRequest.postData, _pendingRequest.isJsonData, _pendingRequest.requiresLogin); + } + _pendingRequest.type = RequestType::None; +} +void AirMapNetworking::_loginFailure(QNetworkReply::NetworkError networkError, const QString& errorString, const QString& serverErrorMessage) +{ + disconnect(&_networkingData.login, &AirMapLogin::loginSuccess, this, &AirMapNetworking::_loginSuccess); + disconnect(&_networkingData.login, &AirMapLogin::loginFailure, this, &AirMapNetworking::_loginFailure); + emit error(networkError, errorString, serverErrorMessage); +} + +void AirMapNetworking::get(QUrl url, bool requiresLogin) +{ + QNetworkRequest request(url); + + request.setRawHeader("X-API-Key", _networkingData.airmapAPIKey.toUtf8()); + + if (requiresLogin) { + if (_networkingData.login.isLoggedIn()) { + request.setRawHeader("Authorization", (QString("Bearer ")+_networkingData.login.JWTToken()).toUtf8()); + } else { + connect(&_networkingData.login, &AirMapLogin::loginSuccess, this, &AirMapNetworking::_loginSuccess); + connect(&_networkingData.login, &AirMapLogin::loginFailure, this, &AirMapNetworking::_loginFailure); + _pendingRequest.type = RequestType::GET; + _pendingRequest.url = url; + _pendingRequest.requiresLogin = requiresLogin; + _networkingData.login.login(); + return; + } + } + + QNetworkProxy tProxy; + tProxy.setType(QNetworkProxy::DefaultProxy); + _networkingData.networkManager.setProxy(tProxy); + + QNetworkReply* networkReply = _networkingData.networkManager.get(request); + if (!networkReply) { + QNetworkReply::NetworkError networkError = QNetworkReply::NetworkError::UnknownNetworkError; + emit error(networkError, "", ""); + return; + } + + connect(networkReply, &QNetworkReply::finished, this, &AirMapNetworking::_requestFinished); +} + +void AirMapNetworking::_requestFinished(void) { QNetworkReply* reply = qobject_cast(QObject::sender()); - // When an error occurs we still end up here. So bail out in those cases. + // When an error occurs we still end up here if (reply->error() != QNetworkReply::NoError) { + QByteArray responseBytes = reply->readAll(); + + QJsonParseError parseError; + QJsonDocument responseJson = QJsonDocument::fromJson(responseBytes, &parseError); + QJsonObject rootObject = responseJson.object(); + QString serverError = ""; + if (rootObject.contains("data")) { // eg. in case of a conflict message + serverError = rootObject["data"].toObject()["message"].toString(); + } else if (rootObject.contains("error_description")) { // eg. login failure + serverError = rootObject["error_description"].toString(); + } else if (rootObject.contains("message")) { // eg. api key failure + serverError = rootObject["message"].toString(); + } + emit error(reply->error(), reply->errorString(), serverError); return; } @@ -81,56 +252,33 @@ void AirMapManager::_getFinished(void) QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute); if (!redirectionTarget.isNull()) { QUrl redirectUrl = reply->url().resolved(redirectionTarget.toUrl()); - _get(redirectUrl); + get(redirectUrl); reply->deleteLater(); return; } - qDebug() << "_getFinished"; QByteArray responseBytes = reply->readAll(); - //qDebug() << responseBytes; QJsonParseError parseError; QJsonDocument responseJson = QJsonDocument::fromJson(responseBytes, &parseError); - // FIXME: Error handling - qDebug() << responseJson.isNull() << parseError.errorString(); - //qDebug().noquote() << responseJson.toJson(); - - _parseAirspaceJson(responseJson); + if (parseError.error != QJsonParseError::NoError) { + qCWarning(AirMapManagerLog) << "JSON parse error:" << parseError.errorString(); + } + emit finished(parseError, responseJson); } -void AirMapManager::_getError(QNetworkReply::NetworkError code) -{ - QNetworkReply* reply = qobject_cast(QObject::sender()); - - QString errorMsg; - - if (code == QNetworkReply::OperationCanceledError) { - errorMsg = "Download cancelled"; - - } else if (code == QNetworkReply::ContentNotFoundError) { - errorMsg = "Error: File Not Found"; - - } else { - errorMsg = QString("Error during download. Error: (%1, %2)").arg(code).arg(reply->errorString()); - } - if (_state == State::RetrieveItems) { - if (--_numAwaitingItems == 0) { - _state = State::Idle; - // TODO: handle properly - } - } - // FIXME - qWarning() << errorMsg; +AirspaceRestrictionManager::AirspaceRestrictionManager(AirMapNetworking::SharedData& sharedData) + : _networking(sharedData) +{ + connect(&_networking, &AirMapNetworking::finished, this, &AirspaceRestrictionManager::_parseAirspaceJson); + connect(&_networking, &AirMapNetworking::error, this, &AirspaceRestrictionManager::_error); } -void AirMapManager::_updateToROI(void) +void AirspaceRestrictionManager::updateROI(const QGeoCoordinate& center, double radiusMeters) { if (_state != State::Idle) { - qDebug() << "Error: state not idle (yet)"; - // restart timer? return; } @@ -175,24 +323,20 @@ void AirMapManager::_updateToROI(void) QUrlQuery airspaceQuery; - airspaceQuery.addQueryItem(QStringLiteral("latitude"), QString::number(_roiCenter.latitude(), 'f', 10)); - airspaceQuery.addQueryItem(QStringLiteral("longitude"), QString::number(_roiCenter.longitude(), 'f', 10)); + airspaceQuery.addQueryItem(QStringLiteral("latitude"), QString::number(center.latitude(), 'f', 10)); + airspaceQuery.addQueryItem(QStringLiteral("longitude"), QString::number(center.longitude(), 'f', 10)); airspaceQuery.addQueryItem(QStringLiteral("weather"), QStringLiteral("true")); - airspaceQuery.addQueryItem(QStringLiteral("buffer"), QString::number(_roiRadius, 'f', 0)); + airspaceQuery.addQueryItem(QStringLiteral("buffer"), QString::number(radiusMeters, 'f', 0)); QUrl airMapAirspaceUrl(QStringLiteral("https://api.airmap.com/status/alpha/point")); airMapAirspaceUrl.setQuery(airspaceQuery); _state = State::RetrieveList; - _get(airMapAirspaceUrl); + _networking.get(airMapAirspaceUrl); } -void AirMapManager::_parseAirspaceJson(const QJsonDocument& airspaceDoc) +void AirspaceRestrictionManager::_parseAirspaceJson(QJsonParseError parseError, QJsonDocument airspaceDoc) { - if (!airspaceDoc.isObject()) { - // FIXME - return; - } QJsonObject rootObject = airspaceDoc.object(); @@ -204,13 +348,13 @@ void AirMapManager::_parseAirspaceJson(const QJsonDocument& airspaceDoc) for (int i=0; i< advisoriesArray.count(); i++) { const QJsonObject& advisoryObject = advisoriesArray[i].toObject(); QString advisoryId(advisoryObject["id"].toString()); - qDebug() << "Adivsory id: " << advisoryId; + qCDebug(AirMapManagerLog) << "Advisory id: " << advisoryId; advisorySet.insert(advisoryId); } for (const auto& advisoryId : advisorySet) { QUrl url(QStringLiteral("https://api.airmap.com/airspace/v2/")+advisoryId); - _get(url); + _networking.get(url); } _numAwaitingItems = advisorySet.size(); _state = State::RetrieveItems; @@ -219,16 +363,16 @@ void AirMapManager::_parseAirspaceJson(const QJsonDocument& airspaceDoc) case State::RetrieveItems: { - qDebug() << "got item"; const QJsonArray& airspaceArray = rootObject["data"].toArray(); - for (int i=0; i< airspaceArray.count(); i++) { + for (int i = 0; i < airspaceArray.count(); i++) { const QJsonObject& airspaceObject = airspaceArray[i].toObject(); QString airspaceType(airspaceObject["type"].toString()); QString airspaceId(airspaceObject["id"].toString()); QString airspaceName(airspaceObject["name"].toString()); const QJsonObject& airspaceGeometry(airspaceObject["geometry"].toObject()); QString geometryType(airspaceGeometry["type"].toString()); - qDebug() << "Airspace ID:" << airspaceId << "name:" << airspaceName << "type:" << airspaceType << "geometry:" << geometryType; + qCDebug(AirMapManagerLog) << "Airspace ID:" << airspaceId << "name:" << airspaceName << "type:" << airspaceType << "geometry:" << geometryType; + if (geometryType == "Polygon") { const QJsonArray& airspaceCoordinates(airspaceGeometry["coordinates"].toArray()[0].toArray()); @@ -242,13 +386,13 @@ void AirMapManager::_parseAirspaceJson(const QJsonDocument& airspaceDoc) list.clearAndDeleteContents(); _nextPolygonList.append(new PolygonAirspaceRestriction(polygon)); } else { - //FIXME - qDebug() << errorString; + //TODO + qWarning() << errorString; } } else { // TODO: are there any circles? - qDebug() << "Unknown geometry type:" << geometryType; + qWarning() << "Unknown geometry type:" << geometryType; } } @@ -269,7 +413,7 @@ void AirMapManager::_parseAirspaceJson(const QJsonDocument& airspaceDoc) } break; default: - qDebug() << "Error: wrong state"; + qCDebug(AirMapManagerLog) << "Error: wrong state"; break; } @@ -288,6 +432,193 @@ void AirMapManager::_parseAirspaceJson(const QJsonDocument& airspaceDoc) // } } +void AirspaceRestrictionManager::_error(QNetworkReply::NetworkError code, const QString& errorString, + const QString& serverErrorMessage) +{ + qCWarning(AirMapManagerLog) << "AirspaceRestrictionManager::_error" << code << serverErrorMessage; + + if (_state == State::RetrieveItems) { + if (--_numAwaitingItems == 0) { + _state = State::Idle; + // TODO: handle properly: update _polygonList... + } + } else { + _state = State::Idle; + } + emit networkError(code, errorString, serverErrorMessage); +} + +AirMapFlightManager::AirMapFlightManager(AirMapNetworking::SharedData& sharedData) + : _networking(sharedData) +{ + connect(&_networking, &AirMapNetworking::finished, this, &AirMapFlightManager::_parseJson); + connect(&_networking, &AirMapNetworking::error, this, &AirMapFlightManager::_error); +} + +void AirMapFlightManager::createFlight(const QList& missionItems) +{ + if (!_networking.getLogin().hasCredentials()) { + qCDebug(AirMapManagerLog) << "Login Credentials not set: will not send flight"; + return; + } + + if (_state != State::Idle) { + qCWarning(AirMapManagerLog) << "AirMapFlightManager::createFlight: State not idle"; + return; + } + + QList flight; + + // get the flight trajectory + for(const auto &item : missionItems) { + switch(item->command()) { + case MAV_CMD_NAV_WAYPOINT: + case MAV_CMD_NAV_LAND: + case MAV_CMD_NAV_TAKEOFF: + // TODO: others too? + { + // TODO: handle different coordinate frames? + double lat = item->param5(); + double lon = item->param6(); + double alt = item->param7(); + flight.append(QGeoCoordinate(lat, lon, alt)); + } + break; + default: + break; + } + } + if (flight.empty()) { + return; + } + + qCDebug(AirMapManagerLog) << "uploading flight"; + + QJsonObject root; + root.insert("latitude", QJsonValue::fromVariant(flight[0].latitude())); + root.insert("longitude", QJsonValue::fromVariant(flight[0].longitude())); + QJsonObject geometryObject; + geometryObject.insert("type", "LineString"); + QJsonArray coordinatesArray; + for (const auto& coord : flight) { + QJsonArray coordinate; + coordinate.push_back(coord.longitude()); + coordinate.push_back(coord.latitude()); + coordinatesArray.push_back(coordinate); + } + geometryObject.insert("coordinates", coordinatesArray); + + root.insert("geometry", geometryObject); + + root.insert("public", QJsonValue::fromVariant(true)); + root.insert("notify", QJsonValue::fromVariant(true)); + + _state = State::FlightUpload; + + QUrl url(QStringLiteral("https://api.airmap.com/flight/v2/path")); + + //qCDebug(AirMapManagerLog) << root; + _networking.post(url, QJsonDocument(root).toJson(), true, true); +} + + +void AirMapFlightManager::_parseJson(QJsonParseError parseError, QJsonDocument doc) +{ + + QJsonObject rootObject = doc.object(); + + switch(_state) { + case State::FlightUpload: + { + qDebug() << "flight uploaded:" << rootObject; + const QJsonObject& dataObject = rootObject["data"].toObject(); + _currentFlightId = dataObject["id"].toString(); + qCDebug(AirMapManagerLog) << "Got Flight ID:" << _currentFlightId; + _state = State::Idle; + } + break; + default: + qCDebug(AirMapManagerLog) << "Error: wrong state"; + break; + } +} + +void AirMapFlightManager::_error(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage) +{ + qCWarning(AirMapManagerLog) << "AirMapFlightManager::_error" << code << serverErrorMessage; + _state = State::Idle; + emit networkError(code, errorString, serverErrorMessage); +} + +AirMapManager::AirMapManager(QGCApplication* app, QGCToolbox* toolbox) + : QGCTool(app, toolbox), _airspaceRestrictionManager(_networkingData), _flightManager(_networkingData) +{ + _updateTimer.setInterval(2000); + _updateTimer.setSingleShot(true); + connect(&_updateTimer, &QTimer::timeout, this, &AirMapManager::_updateToROI); + connect(&_airspaceRestrictionManager, &AirspaceRestrictionManager::networkError, this, &AirMapManager::_networkError); + connect(&_flightManager, &AirMapFlightManager::networkError, this, &AirMapManager::_networkError); +} + +AirMapManager::~AirMapManager() +{ + +} + +void AirMapManager::setToolbox(QGCToolbox* toolbox) +{ + QGCTool::setToolbox(toolbox); + + _networkingData.airmapAPIKey = toolbox->settingsManager()->appSettings()->airMapKey()->rawValueString(); + + // TODO: set login credentials from config + QString clientID = ""; + QString userName = ""; + QString password = ""; + _networkingData.login.setCredentials(clientID, userName, password); +} + +void AirMapManager::setROI(QGeoCoordinate& center, double radiusMeters) +{ + _roiCenter = center; + _roiRadius = radiusMeters; + _updateTimer.start(); +} + +void AirMapManager::_updateToROI(void) +{ + if (!hasAPIKey()) { + qCDebug(AirMapManagerLog) << "API key not set. Not updating Airspace"; + return; + } + _airspaceRestrictionManager.updateROI(_roiCenter, _roiRadius); +} + +void AirMapManager::_networkError(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage) +{ + QString errorDetails = ""; + if (code == QNetworkReply::NetworkError::ContentOperationNotPermittedError) { + errorDetails = " (invalid API key?)"; + } else if (code == QNetworkReply::NetworkError::AuthenticationRequiredError) { + errorDetails = " (authentication failure)"; + } + if (serverErrorMessage.length() > 0) { + // the errorString is a bit verbose and redundant with serverErrorMessage. So we just show the server error. + qgcApp()->showMessage(QString("AirMap error%1. Response from Server: %2").arg(errorDetails).arg(serverErrorMessage)); + } else { + qgcApp()->showMessage(QString("AirMap error: %1%2").arg(errorString).arg(errorDetails)); + } +} + +void AirMapManager::createFlight(const QList& missionItems) +{ + if (!hasAPIKey()) { + qCDebug(AirMapManagerLog) << "API key not set. Will not create a flight"; + return; + } + _flightManager.createFlight(missionItems); +} + AirspaceRestriction::AirspaceRestriction(QObject* parent) : QObject(parent) { diff --git a/src/MissionManager/AirMapManager.h b/src/MissionManager/AirMapManager.h index 914835c..05b5b3b 100644 --- a/src/MissionManager/AirMapManager.h +++ b/src/MissionManager/AirMapManager.h @@ -1,6 +1,6 @@ /**************************************************************************** * - * (c) 2009-2016 QGROUNDCONTROL PROJECT + * (c) 2017 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. @@ -13,6 +13,7 @@ #include "QGCToolbox.h" #include "QGCLoggingCategory.h" #include "QmlObjectListModel.h" +#include "MissionItem.h" #include #include @@ -62,6 +63,180 @@ private: }; +class AirMapLogin : public QObject +{ + Q_OBJECT +public: + /** + * @param networkManager + * @param APIKey AirMap API key: this is stored as a reference, and thus must live as long as this object does + */ + AirMapLogin(QNetworkAccessManager& networkManager, const QString& APIKey); + + void setCredentials(const QString& clientID, const QString& userName, const QString& password); + + /** + * check if the credentials are set (not necessarily valid) + */ + bool hasCredentials() const { return _userName != "" && _password != ""; } + + void login(); + void logout() { _JWTToken = ""; } + + /** get the JWT token. Empty if user not logged in */ + const QString& JWTToken() const { return _JWTToken; } + + bool isLoggedIn() const { return _JWTToken != ""; } + +signals: + void loginSuccess(); + void loginFailure(QNetworkReply::NetworkError error, const QString& errorString, const QString& serverErrorMessage); + +private slots: + void _requestFinished(void); + void _requestError(QNetworkReply::NetworkError code); + +private: + void _post(QUrl url, const QByteArray& postData); + + QNetworkAccessManager& _networkManager; + + bool _isLoginInProgress = false; + QString _JWTToken = ""; ///< JWT login token: empty when not logged in + const QString& _APIKey; + + // login credentials + QString _clientID; + QString _userName; + QString _password; +}; + +class AirMapNetworking : public QObject +{ + Q_OBJECT +public: + + struct SharedData { + SharedData() : login(networkManager, airmapAPIKey) {} + + QNetworkAccessManager networkManager; + QString airmapAPIKey; + + AirMapLogin login; + }; + + AirMapNetworking(SharedData& networkingData); + + /** + * send a GET request + * @param url + * @param requiresLogin set to true if the user needs to be logged in for the request + */ + void get(QUrl url, bool requiresLogin = false); + + /** + * send a POST request + * @param url + * @param postData + * @param isJsonData if true, content type is set to JSON, form data otherwise + * @param requiresLogin set to true if the user needs to be logged in for the request + */ + void post(QUrl url, const QByteArray& postData, bool isJsonData = false, bool requiresLogin = false); + + const QString& JWTLoginToken() const { return _networkingData.login.JWTToken(); } + + const AirMapLogin& getLogin() const { return _networkingData.login; } + +signals: + /// signal when the request finished (get or post). All requests are assumed to return JSON. + void finished(QJsonParseError parseError, QJsonDocument document); + void error(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage); + +private slots: + void _loginSuccess(); + void _loginFailure(QNetworkReply::NetworkError networkError, const QString& errorString, const QString& serverErrorMessage); + void _requestFinished(void); +private: + SharedData& _networkingData; + + enum class RequestType { + None, + GET, + POST + }; + struct PendingRequest { + RequestType type = RequestType::None; + QUrl url; + QByteArray postData; + bool isJsonData; + bool requiresLogin; + }; + PendingRequest _pendingRequest; +}; + + +/// class to download polygons from AirMap +class AirspaceRestrictionManager : public QObject +{ + Q_OBJECT +public: + AirspaceRestrictionManager(AirMapNetworking::SharedData& sharedData); + + void updateROI(const QGeoCoordinate& center, double radiusMeters); + + QmlObjectListModel* polygonRestrictions(void) { return &_polygonList; } + QmlObjectListModel* circularRestrictions(void) { return &_circleList; } + +signals: + void networkError(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage); +private slots: + void _parseAirspaceJson(QJsonParseError parseError, QJsonDocument airspaceDoc); + void _error(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage); +private: + + enum class State { + Idle, + RetrieveList, + RetrieveItems, + }; + + State _state = State::Idle; + int _numAwaitingItems = 0; + AirMapNetworking _networking; + + QmlObjectListModel _polygonList; + QmlObjectListModel _circleList; + QList _nextPolygonList; + QList _nextcircleList; +}; + +/// class to upload a flight +class AirMapFlightManager : public QObject +{ + Q_OBJECT +public: + AirMapFlightManager(AirMapNetworking::SharedData& sharedData); + + /// Send flight path to AirMap + void createFlight(const QList& missionItems); + +signals: + void networkError(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage); +private slots: + void _parseJson(QJsonParseError parseError, QJsonDocument doc); + void _error(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage); +private: + enum class State { + Idle, + FlightUpload, + }; + + State _state = State::Idle; + AirMapNetworking _networking; + QString _currentFlightId; +}; + + /// AirMap server communication support. class AirMapManager : public QGCTool { @@ -76,34 +251,30 @@ public: /// @param radiusMeters Radius in meters around center which is of interest void setROI(QGeoCoordinate& center, double radiusMeters); - QmlObjectListModel* polygonRestrictions(void) { return &_polygonList; } - QmlObjectListModel* circularRestrictions(void) { return &_circleList; } - + /// Send flight path to AirMap + void createFlight(const QList& missionItems); + + + QmlObjectListModel* polygonRestrictions(void) { return _airspaceRestrictionManager.polygonRestrictions(); } + QmlObjectListModel* circularRestrictions(void) { return _airspaceRestrictionManager.circularRestrictions(); } + + void setToolbox(QGCToolbox* toolbox) override; + private slots: - void _getFinished(void); - void _getError(QNetworkReply::NetworkError code); void _updateToROI(void); + void _networkError(QNetworkReply::NetworkError code, const QString& errorString, const QString& serverErrorMessage); private: - void _get(QUrl url); - void _parseAirspaceJson(const QJsonDocument& airspaceDoc); + bool hasAPIKey() const { return _networkingData.airmapAPIKey != ""; } - enum class State { - Idle, - RetrieveList, - RetrieveItems, - }; + AirMapNetworking::SharedData _networkingData; + AirspaceRestrictionManager _airspaceRestrictionManager; + AirMapFlightManager _flightManager; - State _state = State::Idle; - int _numAwaitingItems = 0; QGeoCoordinate _roiCenter; double _roiRadius; - QNetworkAccessManager _networkManager; + QTimer _updateTimer; - QmlObjectListModel _polygonList; - QmlObjectListModel _circleList; - QList _nextPolygonList; - QList _nextcircleList; }; #endif