1
0
mirror of https://github.com/tumic0/GPXSee.git synced 2025-06-26 19:19:16 +02:00

Prefer the map DEM for track/waypoints elevation if present

This commit is contained in:
2024-07-17 01:57:10 +02:00
parent be75165088
commit b6b64065fb
40 changed files with 84 additions and 72 deletions

View File

@ -1,188 +0,0 @@
/*
WARNING: This code uses internal Qt API - the QZipReader class for reading
ZIP files - and things may break if Qt changes the API. For Qt5 this is not
a problem as we can "see the future" now and there are no changes in all
the supported Qt5 versions up to the last one (5.15). In Qt6 the class
might change or even disappear in the future, but this is very unlikely
as there were no changes for several years and The Qt Company's policy
is: "do not invest any resources into any desktop related stuff unless
absolutely necessary". There is an issue (QTBUG-3897) since the year 2009 to
include the ZIP reader into the public API, which aptly illustrates the
effort The Qt Company is willing to make about anything desktop related...
*/
#include <QtEndian>
#include <QDir>
#include <QFile>
#include <QRegularExpression>
#include <QLocale>
#include <private/qzipreader_p.h>
#include "rectc.h"
#include "dem.h"
static unsigned int isqrt(unsigned int x)
{
unsigned int r = 0;
while ((r + 1) * (r + 1) <= x)
r++;
return r;
}
static double interpolate(double dx, double dy, double p0, double p1, double p2,
double p3)
{
return p0 * (1.0 - dx) * (1.0 - dy) + p1 * dx * (1.0 - dy)
+ p2 * dy * (1.0 - dx) + p3 * dx * dy;
}
static double value(int col, int row, int samples, const QByteArray &data)
{
int pos = ((samples - 1 - row) * samples + col) * 2;
qint16 val = qFromBigEndian(*((const qint16*)(data.constData() + pos)));
return (val == -32768) ? NAN : val;
}
QMutex DEM::_lock;
DEM::Entry::Entry(const QByteArray &data) : _data(data)
{
_samples = isqrt(_data.size() / 2);
}
QString DEM::Tile::latStr() const
{
const char ns = (_lat >= 0) ? 'N' : 'S';
return QString("%1%2").arg(ns).arg(qAbs(_lat), 2, 10, QChar('0'));
}
QString DEM::Tile::lonStr() const
{
const char ew = (_lon >= 0) ? 'E' : 'W';
return QString("%1%2").arg(ew).arg(qAbs(_lon), 3, 10, QChar('0'));
}
QString DEM::Tile::fileName() const
{
return QString("%1%2.hgt").arg(latStr(), lonStr());
}
QString DEM::_dir;
DEM::TileCache DEM::_data;
void DEM::setCacheSize(int size)
{
_data.setMaxCost(size);
}
void DEM::setDir(const QString &path)
{
_dir = path;
}
void DEM::clearCache()
{
_data.clear();
}
double DEM::height(const Coordinates &c, const Entry *e)
{
if (!e->samples())
return NAN;
double lat = (c.lat() - floor(c.lat())) * (e->samples() - 1);
double lon = (c.lon() - floor(c.lon())) * (e->samples() - 1);
int row = (int)lat;
int col = (int)lon;
double p0 = value(col, row, e->samples(), e->data());
double p1 = value(col + 1, row, e->samples(), e->data());
double p2 = value(col, row + 1, e->samples(), e->data());
double p3 = value(col + 1, row + 1, e->samples(), e->data());
return interpolate(lon - col, lat - row, p0, p1, p2, p3);
}
DEM::Entry *DEM::loadTile(const Tile &tile)
{
QString fileName(tile.fileName());
QString path(QDir(_dir).absoluteFilePath(fileName));
QString zipPath(path + ".zip");
if (QFileInfo::exists(zipPath)) {
QZipReader zip(zipPath, QIODevice::ReadOnly);
return new Entry(zip.fileData(fileName));
} else {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning("%s: %s", qPrintable(file.fileName()),
qPrintable(file.errorString()));
return new Entry();
} else
return new Entry(file.readAll());
}
}
double DEM::elevation(const Coordinates &c)
{
if (_dir.isEmpty())
return NAN;
Tile tile(floor(c.lon()), floor(c.lat()));
Entry *e = _data.object(tile);
double ele;
if (!e) {
e = loadTile(tile);
ele = height(c, e);
_data.insert(tile, e, e->data().size() / 1024);
} else
ele = height(c, e);
return ele;
}
QList<Area> DEM::tiles()
{
static const QRegularExpression re(
"^([NS])([0-9]{2})([EW])([0-9]{3})(\\.hgt|\\.hgt\\.zip)$");
QDir dir(_dir);
QFileInfoList files(dir.entryInfoList(QDir::Files | QDir::Readable));
QLocale l(QLocale::system());
QList<Area> list;
for (int i = 0; i < files.size(); i++) {
QRegularExpressionMatch match(re.match(files.at(i).fileName()));
if (!match.hasMatch())
continue;
int lat = match.captured(2).toInt();
int lon = match.captured(4).toInt();
if (match.captured(1) == "S")
lat = -lat;
if (match.captured(3) == "W")
lon = -lon;
Area area(RectC(Coordinates(lon, lat + 1), Coordinates(lon + 1, lat)));
area.setName(files.at(i).baseName());
area.setDescription(files.at(i).suffix().toUpper() + ", "
+ l.formattedDataSize(files.at(i).size()));
area.setStyle(PolygonStyle(QColor(0xFF, 0, 0, 0x40),
QColor(0xFF, 0, 0, 0x80), 2));
list.append(area);
}
return list;
}
#ifndef QT_NO_DEBUG
QDebug operator<<(QDebug dbg, const DEM::Tile &tile)
{
dbg.nospace() << "Tile(" << tile.fileName() << ")";
return dbg.space();
}
#endif // QT_NO_DEBUG

View File

@ -1,79 +0,0 @@
#ifndef DEM_H
#define DEM_H
#include <QString>
#include <QCache>
#include <QByteArray>
#include <QMutex>
#include "common/hash.h"
#include "data/area.h"
class Coordinates;
class DEM
{
public:
class Tile {
public:
Tile(int lon, int lat) : _lon(lon), _lat(lat) {}
int lon() const {return _lon;}
int lat() const {return _lat;}
QString lonStr() const;
QString latStr() const;
QString fileName() const;
bool operator==(const Tile &other) const
{
return (_lon == other._lon && _lat == other._lat);
}
private:
int _lon, _lat;
};
static void setCacheSize(int size);
static void setDir(const QString &path);
static void clearCache();
static double elevation(const Coordinates &c);
static void lock() {_lock.lock();}
static void unlock() {_lock.unlock();}
static QList<Area> tiles();
private:
class Entry {
public:
Entry() : _samples(0) {}
Entry(const QByteArray &data);
const QByteArray &data() const {return _data;}
int samples() const {return _samples;}
private:
unsigned int _samples;
QByteArray _data;
};
typedef QCache<DEM::Tile, Entry> TileCache;
static double height(const Coordinates &c, const Entry *e);
static Entry *loadTile(const Tile &tile);
static QString _dir;
static TileCache _data;
static QMutex _lock;
};
inline HASH_T qHash(const DEM::Tile &tile)
{
return (qHash(tile.lon()) ^ qHash(tile.lat()));
}
#ifndef QT_NO_DEBUG
QDebug operator<<(QDebug dbg, const DEM::Tile &tile);
#endif // QT_NO_DEBUG
#endif // DEM_H

View File

@ -1,122 +0,0 @@
#include <QtMath>
#include <QFileInfo>
#include "common/rectc.h"
#include "demloader.h"
static QList<DEM::Tile> tiles(const RectC &rect)
{
QList<DEM::Tile> list;
if (rect.isNull())
return list;
for (int i = qFloor(rect.top()); i >= qFloor(rect.bottom()); i--)
for (int j = qFloor(rect.left()); j <= qFloor(rect.right()); j++)
list.append(DEM::Tile(j, i));
return list;
}
static bool isZip(const QUrl &url)
{
QFileInfo fi(url.fileName());
return (fi.suffix().toLower() == "zip");
}
DEMLoader::DEMLoader(const QString &dir, QObject *parent)
: QObject(parent), _dir(dir)
{
_downloader = new Downloader(this);
connect(_downloader, &Downloader::finished, this, &DEMLoader::finished);
}
int DEMLoader::numTiles(const RectC &rect) const
{
QList<DEM::Tile> tl(tiles(rect));
int cnt = 0;
for (int i = 0; i < tl.size(); i++) {
const DEM::Tile &t = tl.at(i);
QString fn(tileFile(t));
QString zn(fn + ".zip");
if (!(QFileInfo::exists(zn) || QFileInfo::exists(fn)))
cnt++;
}
return cnt;
}
bool DEMLoader::loadTiles(const RectC &rect)
{
QList<DEM::Tile> tl(tiles(rect));
QList<Download> dl;
/* Create the user DEM dir only when a download is requested as it will
override the global DEM dir. */
if (!_dir.mkpath(_dir.absolutePath())) {
qWarning("%s: %s", qPrintable(_dir.canonicalPath()),
"Error creating DEM directory");
return false;
}
DEM::setDir(_dir.path());
for (int i = 0; i < tl.size(); i++) {
const DEM::Tile &t = tl.at(i);
QString fn(tileFile(t));
QString zn(fn + ".zip");
if (!(QFileInfo::exists(zn) || QFileInfo::exists(fn))) {
QUrl url(tileUrl(t));
dl.append(Download(url, isZip(url) ? zn : fn));
}
if (dl.size() > DEM_DOWNLOAD_LIMIT) {
qWarning("DEM download limit (%d) exceeded.", DEM_DOWNLOAD_LIMIT);
return false;
}
}
return _downloader->get(dl, _headers);
}
bool DEMLoader::checkTiles(const RectC &rect) const
{
QList<DEM::Tile> tl(tiles(rect));
for (int i = 0; i < tl.size(); i++) {
const DEM::Tile &t = tl.at(i);
QString fn(tileFile(t));
QString zn(fn + ".zip");
if (!(QFileInfo::exists(zn) || QFileInfo::exists(fn)))
return false;
}
return true;
}
QUrl DEMLoader::tileUrl(const DEM::Tile &tile) const
{
QString url(_url);
url.replace("$lon", tile.lonStr());
url.replace("$lat", tile.latStr());
return QUrl(url);
}
QString DEMLoader::tileFile(const DEM::Tile &tile) const
{
return _dir.absoluteFilePath(tile.fileName());
}
void DEMLoader::setAuthorization(const Authorization &authorization)
{
QList<HTTPHeader> headers;
if (!authorization.isNull())
headers.append(authorization.header());
_headers = headers;
}

View File

@ -1,43 +0,0 @@
#ifndef DEMLOADER_H
#define DEMLOADER_H
#include <QObject>
#include <QDir>
#include "downloader.h"
#include "dem.h"
class RectC;
#define DEM_DOWNLOAD_WARNING 4
#define DEM_DOWNLOAD_LIMIT 1024
class DEMLoader : public QObject
{
Q_OBJECT
public:
DEMLoader(const QString &dir, QObject *parent = 0);
void setUrl(const QString &url) {_url = url;}
void setAuthorization(const Authorization &authorization);
int numTiles(const RectC &rect) const;
bool loadTiles(const RectC &rect);
bool checkTiles(const RectC &rect) const;
const QString &url() const {return _url;}
signals:
void finished();
private:
QUrl tileUrl(const DEM::Tile &tile) const;
QString tileFile(const DEM::Tile &tile) const;
Downloader *_downloader;
QString _url;
QDir _dir;
QList<HTTPHeader> _headers;
};
#endif // DEMLOADER_H

View File

@ -1,302 +0,0 @@
#include <QFile>
#include <QNetworkRequest>
#include <QDir>
#include <QTimerEvent>
#include "common/config.h"
#include "downloader.h"
#if defined(Q_OS_ANDROID)
#define PLATFORM_STR "Android"
#elif defined(Q_OS_LINUX)
#define PLATFORM_STR "Linux"
#elif defined(Q_OS_WIN32)
#define PLATFORM_STR "Windows"
#elif defined(Q_OS_MAC)
#define PLATFORM_STR "OS X"
#elif defined(Q_OS_BSD4)
#define PLATFORM_STR "BSD"
#elif defined(Q_OS_HAIKU)
#define PLATFORM_STR "Haiku"
#else
#define PLATFORM_STR "Unknown"
#endif
#define USER_AGENT \
APP_NAME "/" APP_VERSION " (" PLATFORM_STR "; Qt " QT_VERSION_STR ")"
#define MAX_REDIRECT_LEVEL 5
#define RETRIES 3
#define ATTR_REDIRECT_POLICY QNetworkRequest::RedirectPolicyAttribute
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
#define ATTR_HTTP2_ALLOWED QNetworkRequest::HTTP2AllowedAttribute
#else // QT 5.15
#define ATTR_HTTP2_ALLOWED QNetworkRequest::Http2AllowedAttribute
#endif // QT 5.15
#define TMP_SUFFIX ".download"
// QNetworkReply::errorString() returns bullshit, use our own reporting
static const char *errorString(QNetworkReply::NetworkError error)
{
switch (error) {
case QNetworkReply::ConnectionRefusedError:
return "Connection refused";
case QNetworkReply::RemoteHostClosedError:
return "Connection closed";
case QNetworkReply::HostNotFoundError:
return "Host not found";
case QNetworkReply::TimeoutError:
return "Connection timeout";
case QNetworkReply::OperationCanceledError:
return "Operation canceled";
case QNetworkReply::SslHandshakeFailedError:
return "SSL handshake failed";
case QNetworkReply::TemporaryNetworkFailureError:
return "Temporary network failure";
case QNetworkReply::NetworkSessionFailedError:
return "Network session failed";
case QNetworkReply::BackgroundRequestNotAllowedError:
return "Background request not allowed";
case QNetworkReply::TooManyRedirectsError:
return "Too many redirects";
case QNetworkReply::InsecureRedirectError:
return "Insecure redirect";
case QNetworkReply::ProxyConnectionRefusedError:
return "Proxy connection refused";
case QNetworkReply::ProxyConnectionClosedError:
return "Proxy connection closed";
case QNetworkReply::ProxyNotFoundError:
return "Proxy not found";
case QNetworkReply::ProxyTimeoutError:
return "Proxy timeout error";
case QNetworkReply::ProxyAuthenticationRequiredError:
return "Proxy authentication required";
case QNetworkReply::ContentAccessDenied:
return "Content access denied";
case QNetworkReply::ContentOperationNotPermittedError:
return "Content operation not permitted";
case QNetworkReply::ContentNotFoundError:
return "Content not found";
case QNetworkReply::AuthenticationRequiredError:
return "Authentication required";
case QNetworkReply::ContentReSendError:
return "Content re-send error";
case QNetworkReply::ContentConflictError:
return "Content conflict";
case QNetworkReply::ContentGoneError:
return "Content gone";
case QNetworkReply::InternalServerError:
return "Internal server error";
case QNetworkReply::OperationNotImplementedError:
return "Operation not implemented";
case QNetworkReply::ServiceUnavailableError:
return "Service unavailable";
case QNetworkReply::ProtocolUnknownError:
return "Protocol unknown";
case QNetworkReply::ProtocolInvalidOperationError:
return "Protocol invalid operation";
case QNetworkReply::UnknownNetworkError:
return "Unknown network error";
case QNetworkReply::UnknownProxyError:
return "Unknown proxy error";
case QNetworkReply::UnknownContentError:
return "Unknown content error";
case QNetworkReply::ProtocolFailure:
return "Protocol failure";
case QNetworkReply::UnknownServerError:
return "Unknown server error";
default:
return "Unknown error";
}
}
static QString tmpName(const QString &origName)
{
return origName + TMP_SUFFIX;
}
static QString origName(const QString &tmpName)
{
return tmpName.left(tmpName.size() - (sizeof(TMP_SUFFIX) - 1));
}
Authorization::Authorization(const QString &username, const QString &password)
{
QString concatenated = username + ":" + password;
QByteArray data = concatenated.toLocal8Bit().toBase64();
_header = HTTPHeader("Authorization", "Basic " + data);
}
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
NetworkTimeout::NetworkTimeout(int timeout, QNetworkReply *reply)
: QObject(reply), _timeout(timeout)
{
connect(reply, &QIODevice::readyRead, this, &NetworkTimeout::reset);
_timer.start(timeout * 1000, this);
}
void NetworkTimeout::reset()
{
_timer.start(_timeout * 1000, this);
}
void NetworkTimeout::timerEvent(QTimerEvent *ev)
{
if (!_timer.isActive() || ev->timerId() != _timer.timerId())
return;
QNetworkReply *reply = static_cast<QNetworkReply*>(parent());
if (reply->isRunning())
reply->abort();
_timer.stop();
}
#endif // QT 5.15
QNetworkAccessManager *Downloader::_manager = 0;
int Downloader::_timeout = 30;
bool Downloader::_http2 = true;
bool Downloader::doDownload(const Download &dl, const QList<HTTPHeader> &headers)
{
const QUrl &url = dl.url();
bool userAgent = false;
if (!url.isValid() || !(url.scheme() == QLatin1String("http")
|| url.scheme() == QLatin1String("https"))) {
qWarning("%s: Invalid URL", qPrintable(url.toString()));
return false;
}
if (_errorDownloads.value(url) >= RETRIES)
return false;
if (_currentDownloads.contains(url))
return false;
QNetworkRequest request(url);
request.setMaximumRedirectsAllowed(MAX_REDIRECT_LEVEL);
request.setAttribute(ATTR_REDIRECT_POLICY,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setAttribute(ATTR_HTTP2_ALLOWED, QVariant(_http2));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setTransferTimeout(_timeout * 1000);
#endif // QT 5.15
for (int i = 0; i < headers.size(); i++) {
const HTTPHeader &hdr = headers.at(i);
request.setRawHeader(hdr.key(), hdr.value());
// QByteArray::compare() not available in Qt < 5.12
if (!QString(hdr.key()).compare("User-Agent", Qt::CaseInsensitive))
userAgent = true;
}
if (!userAgent)
request.setRawHeader("User-Agent", USER_AGENT);
QFile *file = new QFile(tmpName(dl.file()));
if (!file->open(QIODevice::WriteOnly)) {
qWarning("%s: %s", qPrintable(file->fileName()),
qPrintable(file->errorString()));
delete file;
_errorDownloads.insert(url, RETRIES);
return false;
}
Q_ASSERT(_manager);
QNetworkReply *reply = _manager->get(request);
file->setParent(reply);
_currentDownloads.insert(url, file);
if (reply->isRunning()) {
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
new NetworkTimeout(_timeout, reply);
#endif // QT 5.15
connect(reply, &QIODevice::readyRead, this, &Downloader::emitReadReady);
connect(reply, &QNetworkReply::finished, this, &Downloader::emitFinished);
} else {
readData(reply);
downloadFinished(reply);
}
return true;
}
void Downloader::emitFinished()
{
downloadFinished(static_cast<QNetworkReply*>(sender()));
}
void Downloader::emitReadReady()
{
readData(static_cast<QNetworkReply*>(sender()));
}
void Downloader::insertError(const QUrl &url, QNetworkReply::NetworkError error)
{
switch (error) {
case QNetworkReply::OperationCanceledError:
case QNetworkReply::TimeoutError:
case QNetworkReply::RemoteHostClosedError:
case QNetworkReply::ConnectionRefusedError:
_errorDownloads.insert(url, _errorDownloads.value(url) + 1);
break;
default:
_errorDownloads.insert(url, RETRIES);
}
}
void Downloader::readData(QNetworkReply *reply)
{
QFile *file = _currentDownloads.value(reply->request().url());
Q_ASSERT(file);
file->write(reply->readAll());
}
void Downloader::downloadFinished(QNetworkReply *reply)
{
QUrl url(reply->request().url());
QNetworkReply::NetworkError error = reply->error();
QFile *file = _currentDownloads.value(reply->request().url());
if (error) {
insertError(url, error);
qWarning("%s: %s", url.toEncoded().constData(), errorString(error));
file->remove();
} else {
file->close();
file->rename(origName(file->fileName()));
}
_currentDownloads.remove(url);
reply->deleteLater();
if (_currentDownloads.isEmpty())
emit finished();
}
bool Downloader::get(const QList<Download> &list,
const QList<HTTPHeader> &headers)
{
bool finishEmitted = false;
for (int i = 0; i < list.count(); i++)
finishEmitted |= doDownload(list.at(i), headers);
return finishEmitted;
}
void Downloader::enableHTTP2(bool enable)
{
Q_ASSERT(_manager);
_http2 = enable;
_manager->clearConnectionCache();
}
#ifndef QT_NO_DEBUG
QDebug operator<<(QDebug dbg, const Download &download)
{
dbg.nospace() << "Download(" << download.url() << "," << download.file()
<< ")";
return dbg.space();
}
#endif // QT_NO_DEBUG

View File

@ -1,103 +0,0 @@
#ifndef DOWNLOADER_H
#define DOWNLOADER_H
#include <QNetworkAccessManager>
#include <QNetworkReply>
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
#include <QBasicTimer>
#endif // QT 5.15
#include <QUrl>
#include <QList>
#include <QHash>
#include "common/kv.h"
class QFile;
typedef KV<QByteArray, QByteArray> HTTPHeader;
class Download
{
public:
Download(const QUrl &url, const QString &file) : _url(url), _file(file) {}
const QUrl &url() const {return _url;}
const QString &file() const {return _file;}
private:
QUrl _url;
QString _file;
};
class Authorization
{
public:
Authorization() {}
Authorization(const QString &username, const QString &password);
const HTTPHeader &header() const {return _header;}
bool isNull() const {return _header.key().isNull();}
private:
HTTPHeader _header;
};
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
class NetworkTimeout : public QObject
{
Q_OBJECT
public:
NetworkTimeout(int timeout, QNetworkReply *reply);
private slots:
void reset();
private:
void timerEvent(QTimerEvent *ev);
QBasicTimer _timer;
int _timeout;
};
#endif // QT 5.15
class Downloader : public QObject
{
Q_OBJECT
public:
Downloader(QObject *parent = 0) : QObject(parent) {}
bool get(const QList<Download> &list, const QList<HTTPHeader> &headers);
void clearErrors() {_errorDownloads.clear();}
static void setNetworkManager(QNetworkAccessManager *manager)
{_manager = manager;}
static void setTimeout(int timeout) {_timeout = timeout;}
static void enableHTTP2(bool enable);
signals:
void finished();
private slots:
void emitFinished();
void emitReadReady();
private:
void insertError(const QUrl &url, QNetworkReply::NetworkError error);
bool doDownload(const Download &dl, const QList<HTTPHeader> &headers);
void downloadFinished(QNetworkReply *reply);
void readData(QNetworkReply *reply);
QHash<QUrl, QFile*> _currentDownloads;
QHash<QUrl, int> _errorDownloads;
static QNetworkAccessManager *_manager;
static int _timeout;
static bool _http2;
};
#ifndef QT_NO_DEBUG
QDebug operator<<(QDebug dbg, const Download &download);
#endif // QT_NO_DEBUG
#endif // DOWNLOADER_H