Terrain.cc 7.98 KB
Newer Older
1 2 3 4 5 6 7 8 9 10
/****************************************************************************
 *
 *   (c) 2009-2016 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
 *
 * QGroundControl is licensed according to the terms in the file
 * COPYING.md in the root of the source code directory.
 *
 ****************************************************************************/

#include "Terrain.h"
11
#include "QGCMapEngine.h"
12
#include "QGeoMapReplyQGC.h"
13 14 15 16 17 18 19 20 21

#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkProxy>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
Andreas Bircher's avatar
Andreas Bircher committed
22
#include <QtLocation/private/qgeotilespec_p.h>
23

Andreas Bircher's avatar
Andreas Bircher committed
24 25
QGC_LOGGING_CATEGORY(TerrainLog, "TerrainLog")

Andreas Bircher's avatar
Andreas Bircher committed
26 27 28 29
QMutex                          ElevationProvider::_tilesMutex;
QHash<QString, TerrainTile>     ElevationProvider::_tiles;
QStringList                     ElevationProvider::_downloadQueue;

30 31 32 33 34 35
ElevationProvider::ElevationProvider(QObject* parent)
    : QObject(parent)
{

}

Andreas Bircher's avatar
Andreas Bircher committed
36
bool ElevationProvider::queryTerrainDataPoints(const QList<QGeoCoordinate>& coordinates)
37
{
Andreas Bircher's avatar
Andreas Bircher committed
38
    if (_state != State::Idle || coordinates.length() == 0) {
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
        return false;
    }

    QUrlQuery query;
    QString points = "";
    for (const auto& coordinate : coordinates) {
        points += QString::number(coordinate.latitude(), 'f', 10) + ","
                + QString::number(coordinate.longitude(), 'f', 10) + ",";
    }
    points = points.mid(0, points.length() - 1); // remove the last ','

    query.addQueryItem(QStringLiteral("points"), points);
    QUrl url(QStringLiteral("https://api.airmap.com/elevation/stage/srtm1/ele"));
    url.setQuery(query);

    QNetworkRequest request(url);

    QNetworkProxy tProxy;
    tProxy.setType(QNetworkProxy::DefaultProxy);
    _networkManager.setProxy(tProxy);

    QNetworkReply* networkReply = _networkManager.get(request);
    if (!networkReply) {
        return false;
    }

    connect(networkReply, &QNetworkReply::finished, this, &ElevationProvider::_requestFinished);

    _state = State::Downloading;
    return true;
}

Andreas Bircher's avatar
Andreas Bircher committed
71 72 73 74 75 76 77 78 79
bool ElevationProvider::queryTerrainData(const QList<QGeoCoordinate>& coordinates)
{
    if (_state != State::Idle || coordinates.length() == 0) {
        return false;
    }

    QList<float> altitudes;
    bool needToDownload = false;
    foreach (QGeoCoordinate coordinate, coordinates) {
80
        QString tileHash = _getTileHash(coordinate);
Andreas Bircher's avatar
Andreas Bircher committed
81
        _tilesMutex.lock();
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
        if (!_tiles.contains(tileHash)) {
            qCDebug(TerrainLog) << "Need to download tile " << tileHash;

            if (!_downloadQueue.contains(tileHash)) {
                // Schedule the fetch task

                QNetworkRequest request = getQGCMapEngine()->urlFactory()->getTileURL(UrlFactory::AirmapElevation, QGCMapEngine::long2elevationTileX(coordinate.longitude(), 1), QGCMapEngine::lat2elevationTileY(coordinate.latitude(), 1), 1, &_networkManager);
                QGeoTileSpec spec;
                spec.setX(QGCMapEngine::long2elevationTileX(coordinate.longitude(), 1));
                spec.setY(QGCMapEngine::lat2elevationTileY(coordinate.latitude(), 1));
                spec.setZoom(1);
                spec.setMapId(UrlFactory::AirmapElevation);
                QGeoTiledMapReplyQGC* reply = new QGeoTiledMapReplyQGC(&_networkManager, request, spec);
                connect(reply, &QGeoTiledMapReplyQGC::finished, this, &ElevationProvider::_fetchedTile);
                connect(reply, &QGeoTiledMapReplyQGC::aborted, this, &ElevationProvider::_fetchedTile);

                _downloadQueue.append(tileHash);
            }
Andreas Bircher's avatar
Andreas Bircher committed
100 101 102
            needToDownload = true;
        } else {
            if (!needToDownload) {
103 104
                if (_tiles[tileHash].isIn(coordinate)) {
                    altitudes.push_back(_tiles[tileHash].elevation(coordinate));
Andreas Bircher's avatar
Andreas Bircher committed
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
                } else {
                    qCDebug(TerrainLog) << "Error: coordinate not in tile region";
                    altitudes.push_back(-1.0);
                }
            }
        }
        _tilesMutex.unlock();
    }

    if (!needToDownload) {
        qCDebug(TerrainLog) << "All altitudes taken from cached data";
        emit terrainData(true, altitudes);
        _coordinates.clear();
        return true;
    }

    _coordinates = coordinates;

123
    return false;
124 125
}

126 127 128 129 130 131 132 133 134 135 136 137
void ElevationProvider::_requestFinished()
{
    QNetworkReply* reply = qobject_cast<QNetworkReply*>(QObject::sender());
    QList<float> altitudes;
    _state = State::Idle;

    // 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);
Andreas Bircher's avatar
Andreas Bircher committed
138
        qCDebug(TerrainLog) << "Received " <<  responseJson;
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        emit terrainData(false, altitudes);
        return;
    }

    QByteArray responseBytes = reply->readAll();

    QJsonParseError parseError;
    QJsonDocument responseJson = QJsonDocument::fromJson(responseBytes, &parseError);
    if (parseError.error != QJsonParseError::NoError) {
        emit terrainData(false, altitudes);
        return;
    }
    QJsonObject rootObject = responseJson.object();
    QString status = rootObject["status"].toString();
    if (status == "success") {
        const QJsonArray& dataArray = rootObject["data"].toArray();
        for (int i = 0; i < dataArray.count(); i++) {
            altitudes.push_back(dataArray[i].toDouble());
        }

        emit terrainData(true, altitudes);
    }
    emit terrainData(false, altitudes);
}
Andreas Bircher's avatar
Andreas Bircher committed
163

164
void ElevationProvider::_fetchedTile()
Andreas Bircher's avatar
Andreas Bircher committed
165
{
166
    QGeoTiledMapReplyQGC* reply = qobject_cast<QGeoTiledMapReplyQGC*>(QObject::sender());
Andreas Bircher's avatar
Andreas Bircher committed
167

Andreas Bircher's avatar
Andreas Bircher committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
    if (!reply) {
        qCDebug(TerrainLog) << "Elevation tile fetched but invalid reply data type.";
        return;
    }

    // remove from download queue
    QGeoTileSpec spec = reply->tileSpec();
    QString hash = QGCMapEngine::getTileHash(UrlFactory::AirmapElevation, spec.x(), spec.y(), spec.zoom());
    _tilesMutex.lock();
    if (_downloadQueue.contains(hash)) {
        _downloadQueue.removeOne(hash);
    }
    _tilesMutex.unlock();

    // handle potential errors
    if (reply->error() != QGeoTiledMapReply::NoError) {
        if (reply->error() == QGeoTiledMapReply::CommunicationError) {
            qCDebug(TerrainLog) << "Elevation tile fetching returned communication error. " << reply->errorString();
186
        } else {
Andreas Bircher's avatar
Andreas Bircher committed
187
            qCDebug(TerrainLog) << "Elevation tile fetching returned error. " << reply->errorString();
188
        }
Andreas Bircher's avatar
Andreas Bircher committed
189 190 191 192 193 194
        reply->deleteLater();
        return;
    }
    if (!reply->isFinished()) {
        qCDebug(TerrainLog) << "Error in fetching elevation tile. Not finished. " << reply->errorString();
        reply->deleteLater();
Andreas Bircher's avatar
Andreas Bircher committed
195 196 197
        return;
    }

Andreas Bircher's avatar
Andreas Bircher committed
198
    // parse received data and insert into hash table
199
    QByteArray responseBytes = reply->mapImageData();
Andreas Bircher's avatar
Andreas Bircher committed
200 201 202 203

    QJsonParseError parseError;
    QJsonDocument responseJson = QJsonDocument::fromJson(responseBytes, &parseError);
    if (parseError.error != QJsonParseError::NoError) {
204 205 206
        qCDebug(TerrainLog) << "Could not parse terrain tile " << parseError.errorString();
        qCDebug(TerrainLog) << responseBytes;
        reply->deleteLater();
Andreas Bircher's avatar
Andreas Bircher committed
207 208 209
        return;
    }

210 211
    TerrainTile* terrainTile = new TerrainTile(responseJson);
    if (terrainTile->isValid()) {
Andreas Bircher's avatar
Andreas Bircher committed
212
        _tilesMutex.lock();
Andreas Bircher's avatar
Andreas Bircher committed
213
        if (!_tiles.contains(hash)) {
Andreas Bircher's avatar
Andreas Bircher committed
214
            _tiles.insert(hash, *terrainTile);
Andreas Bircher's avatar
Andreas Bircher committed
215
        } else {
216
            delete terrainTile;
Andreas Bircher's avatar
Andreas Bircher committed
217
        }
218
        _tilesMutex.unlock();
Andreas Bircher's avatar
Andreas Bircher committed
219 220
    } else {
        qCDebug(TerrainLog) << "Received invalid tile";
Andreas Bircher's avatar
Andreas Bircher committed
221
    }
222
    reply->deleteLater();
Andreas Bircher's avatar
Andreas Bircher committed
223 224 225

    // now try to query the data again
    queryTerrainData(_coordinates);
Andreas Bircher's avatar
Andreas Bircher committed
226 227
}

228
QString ElevationProvider::_getTileHash(const QGeoCoordinate& coordinate)
Andreas Bircher's avatar
Andreas Bircher committed
229
{
230 231
    QString ret = QGCMapEngine::getTileHash(UrlFactory::AirmapElevation, QGCMapEngine::long2elevationTileX(coordinate.longitude(), 1), QGCMapEngine::lat2elevationTileY(coordinate.latitude(), 1), 1);
    qCDebug(TerrainLog) << "Computing unique tile hash for " << coordinate << ret;
Andreas Bircher's avatar
Andreas Bircher committed
232 233 234

    return ret;
}