From 86c6fa7b0336d31e2ce8c43ad10a949560b2fd90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20T=C5=AFma?= Date: Thu, 4 Feb 2021 23:22:16 +0100 Subject: [PATCH] Added support for AlpineQuest AQM maps --- gpxsee.pro | 6 +- src/common/config.h | 6 + src/map/aqmmap.cpp | 428 ++++++++++++++++++++++++++++++++++++++++++++ src/map/aqmmap.h | 85 +++++++++ src/map/maplist.cpp | 10 +- 5 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 src/map/aqmmap.cpp create mode 100644 src/map/aqmmap.h diff --git a/gpxsee.pro b/gpxsee.pro index 74dae29b..fa98a308 100644 --- a/gpxsee.pro +++ b/gpxsee.pro @@ -207,7 +207,8 @@ HEADERS += src/common/config.h \ src/GUI/pdfexportdialog.h \ src/GUI/pngexportdialog.h \ src/data/geojsonparser.h \ - src/GUI/timezoneinfo.h + src/GUI/timezoneinfo.h \ + src/map/aqmmap.h SOURCES += src/main.cpp \ src/GUI/axislabelitem.cpp \ @@ -365,7 +366,8 @@ SOURCES += src/main.cpp \ src/data/smlparser.cpp \ src/GUI/pdfexportdialog.cpp \ src/GUI/pngexportdialog.cpp \ - src/data/geojsonparser.cpp + src/data/geojsonparser.cpp \ + src/map/aqmmap.cpp DEFINES += APP_VERSION=\\\"$$VERSION\\\" \ QT_NO_DEPRECATED_WARNINGS diff --git a/src/common/config.h b/src/common/config.h index b2558f92..5c594455 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -2,6 +2,7 @@ #define CONFIG_H #include +#include #define APP_NAME "GPXSee" #define APP_HOMEPAGE "http://www.gpxsee.org" @@ -12,4 +13,9 @@ #define HASH_T size_t #endif // QT6 +inline HASH_T qHash(const QPoint &p) +{ + return ::qHash(p.x()) ^ ::qHash(p.y()); +} + #endif /* CONFIG_H */ diff --git a/src/map/aqmmap.cpp b/src/map/aqmmap.cpp new file mode 100644 index 00000000..af415444 --- /dev/null +++ b/src/map/aqmmap.cpp @@ -0,0 +1,428 @@ +#include +#include +#include +#include +#include +#include +#include "osm.h" +#include "aqmmap.h" + + +#define MAGIC "FLATPACK1" + +class AQTile +{ +public: + AQTile(const QPoint &xy, const QByteArray &data, const QString &key) + : _xy(xy), _data(data), _key(key) {} + + const QPoint &xy() const {return _xy;} + const QString &key() const {return _key;} + const QPixmap &pixmap() const {return _pixmap;} + + void load() {_pixmap.loadFromData(_data);} + +private: + QPoint _xy; + QByteArray _data; + QString _key; + QPixmap _pixmap; +}; + + +static bool parseHeader(const QByteArray &data, QString &name) +{ + QList lines = data.split('\n'); + + for (int i = 0; i < lines.count(); i++) { + const QByteArray &line = lines.at(i); + + QList tokens = line.split('='); + if (tokens.size() != 2) + continue; + + QByteArray key(tokens.at(0).trimmed()); + QByteArray value(tokens.at(1).trimmed()); + + if (key == "name") + name = value; + } + + return !name.isEmpty(); +} + +static bool parseLevel(const QByteArray &data, int &zoom, int &tileSize, + QRect &rect) +{ + int id = -1; + int xtsize = 0, ytsize = 0; + int xtmin = 0, xtmax = 0, ytmin = 0, ytmax = 0; + QList lines = data.split('\n'); + + for (int i = 0; i < lines.count(); i++) { + const QByteArray &line = lines.at(i); + + QList tokens = line.split('='); + if (tokens.size() != 2) + continue; + + QByteArray key(tokens.at(0).trimmed()); + QByteArray value(tokens.at(1).trimmed()); + bool ok = true; + + if (key == "id") + id = value.toInt(&ok); + else if (key == "xtsize") + xtsize = value.toInt(&ok); + else if (key == "ytsize") + ytsize = value.toInt(&ok); + else if (key == "xtmin") + xtmin = value.toInt(&ok); + else if (key == "xtmax") + xtmax = value.toInt(&ok); + else if (key == "ytmin") + ytmin = value.toInt(&ok); + else if (key == "ytmax") + ytmax = value.toInt(&ok); + + if (!ok) + return false; + } + + if (xtsize <= 0 || ytsize <= 0 || ytsize != xtsize || id < 0) + return false; + + zoom = id; + tileSize = xtsize; + rect = QRect(QPoint(xtmin, (1< files(numFiles); + for (size_t i = 0; i < numFiles; i++) { + if (!readFile(files[i])) + return false; + } + + size_t start = _file.pos(); + for (int i = 0; i < files.size(); i++) + files[i].offset += start; + + int li = -1; + for (int i = 0; i < files.size(); i++) { + if (files.at(i).name == "V2HEADER") { + _file.seek(files.at(i).offset); + if (!readData(data)) + return false; + if (!parseHeader(data, _name)) + return false; + } else if (files.at(i).name == "V2LEVEL") { + int zoom, tileSize; + QRect bounds; + + _file.seek(files.at(i).offset); + if (!readData(data)) + return false; + if (!parseLevel(data, zoom, tileSize, bounds)) + return false; + + _bounds = RectC(tile2ll(bounds.topLeft(), zoom), + tile2ll(bounds.bottomRight(), zoom)); + _zooms.append(Zoom(zoom, tileSize)); + } else if (files.at(i).name == "@LEVEL") { + li = i; + break; + } + } + + if (li < 0) + return false; + + int level = -1; + for (int i = li; i < files.size(); i++) { + if (files.at(i).name == "@LEVEL") + level++; + else if (files.at(i).name == "#END") + break; + else { + if (level < 0 || level > _zooms.size() - 1) + return false; + + QList ba(files.at(i).name.split('_')); + if (ba.size() != 2) + return false; + bool xok, yok; + int x = ba.at(0).toInt(&xok); + int y = ba.at(1).toInt(&yok); + if (!(xok && yok)) + return false; + int zoom = _zooms.at(level).zoom; + _zooms[level].tiles.insert(QPoint(x, (1<= size.width() || sbr.size().height() + >= size.height()) { + _zoom--; + break; + } + } + } + + return _zoom; +} + +qreal AQMMap::resolution(const QRectF &rect) +{ + const Zoom &z = _zooms.at(_zoom); + return OSM::resolution(rect.center(), z.zoom, z.tileSize); +} + +int AQMMap::zoomIn() +{ + _zoom = qMin(_zoom + 1, _zooms.size() - 1); + return _zoom; +} + +int AQMMap::zoomOut() +{ + _zoom = qMax(_zoom - 1, 0); + return _zoom; +} + +void AQMMap::setDevicePixelRatio(qreal deviceRatio, qreal mapRatio) +{ + Q_UNUSED(deviceRatio); + _mapRatio = mapRatio; +} + +QPointF AQMMap::ll2xy(const Coordinates &c) +{ + const Zoom &z = _zooms.at(_zoom); + qreal scale = OSM::zoom2scale(z.zoom, z.tileSize); + QPointF m = OSM::ll2m(c); + return QPointF(m.x() / scale, m.y() / -scale) / _mapRatio; +} + +Coordinates AQMMap::xy2ll(const QPointF &p) +{ + const Zoom &z = _zooms.at(_zoom); + qreal scale = OSM::zoom2scale(z.zoom, z.tileSize); + return OSM::m2ll(QPointF(p.x() * scale, -p.y() * scale) * _mapRatio); +} + +qreal AQMMap::tileSize() const +{ + return (_zooms.at(_zoom).tileSize / _mapRatio); +} + +QByteArray AQMMap::tileData(const QPoint &tile) +{ + const Zoom &z = _zooms.at(_zoom); + QByteArray ba; + + size_t offset = z.tiles.value(tile); + if (!offset || !_file.seek(offset) || !readData(ba)) + return QByteArray(); + + return ba; +} + +void AQMMap::draw(QPainter *painter, const QRectF &rect, Flags flags) +{ + Q_UNUSED(flags); + const Zoom &z = _zooms.at(_zoom); + qreal scale = OSM::zoom2scale(z.zoom, z.tileSize); + + QPoint tile = OSM::mercator2tile(QPointF(rect.topLeft().x() * scale, + -rect.topLeft().y() * scale) * _mapRatio, z.zoom); + QPointF tl(floor(rect.left() / tileSize()) + * tileSize(), floor(rect.top() / tileSize()) * tileSize()); + + QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y()); + int width = ceil(s.width() / tileSize()); + int height = ceil(s.height() / tileSize()); + + QList tiles; + + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + QPixmap pm; + QPoint t(tile.x() + i, tile.y() + j); + QString key = path() + "-" + QString::number(z.zoom) + "_" + + QString::number(t.x()) + "_" + QString::number(t.y()); + + if (QPixmapCache::find(key, &pm)) { + QPointF tp(tl.x() + (t.x() - tile.x()) * tileSize(), + tl.y() + (t.y() - tile.y()) * tileSize()); + drawTile(painter, pm, tp); + } else + tiles.append(AQTile(t, tileData(t), key)); + } + } + + QFuture future = QtConcurrent::map(tiles, &AQTile::load); + future.waitForFinished(); + + for (int i = 0; i < tiles.size(); i++) { + const AQTile &mt = tiles.at(i); + QPixmap pm(mt.pixmap()); + if (pm.isNull()) + continue; + QPixmapCache::insert(mt.key(), pm); + + QPointF tp(tl.x() + (mt.xy().x() - tile.x()) * tileSize(), + tl.y() + (mt.xy().y() - tile.y()) * tileSize()); + drawTile(painter, pm, tp); + } +} + +void AQMMap::drawTile(QPainter *painter, QPixmap &pixmap, QPointF &tp) +{ + pixmap.setDevicePixelRatio(_mapRatio); + painter->drawPixmap(tp, pixmap); +} + +#ifndef QT_NO_DEBUG +QDebug operator<<(QDebug dbg, const AQMMap::File &file) +{ + dbg.nospace() << "File(" << file.name << ", " << file.offset << ")"; + return dbg.space(); +} + +QDebug operator<<(QDebug dbg, const AQMMap::Zoom &zoom) +{ + dbg.nospace() << "Zoom(" << zoom.zoom << ", " << zoom.tileSize << ", " + << zoom.tiles << ")"; + return dbg.space(); +} +#endif // QT_NO_DEBUG diff --git a/src/map/aqmmap.h b/src/map/aqmmap.h new file mode 100644 index 00000000..06b3ef4f --- /dev/null +++ b/src/map/aqmmap.h @@ -0,0 +1,85 @@ +#ifndef AQMMAP_H +#define AQMMAP_H + +#include +#include +#include +#include "common/config.h" +#include "map.h" + +class AQMMap : public Map +{ +public: + Q_OBJECT + +public: + AQMMap(const QString &fileName, QObject *parent = 0); + + QString name() const {return _name;} + + QRectF bounds(); + qreal resolution(const QRectF &rect); + + int zoom() const {return _zoom;} + void setZoom(int zoom) {_zoom = zoom;} + int zoomFit(const QSize &size, const RectC &rect); + int zoomIn(); + int zoomOut(); + + void load(); + void unload(); + void setDevicePixelRatio(qreal deviceRatio, qreal mapRatio); + + QPointF ll2xy(const Coordinates &c); + Coordinates xy2ll(const QPointF &p); + + void draw(QPainter *painter, const QRectF &rect, Flags flags); + + bool isValid() const {return _valid;} + QString errorString() const {return _errorString;} + +private: + struct File { + QByteArray name; + size_t offset; + }; + + struct Zoom { + Zoom() : zoom(-1), tileSize(-1) {} + Zoom(int zoom, int tileSize) : zoom(zoom), tileSize(tileSize) {} + + int zoom; + int tileSize; + QHash tiles; + }; + + bool readSize(size_t &size); + bool readString(QByteArray &str); + bool readFile(File &file); + bool readData(QByteArray &data); + bool readHeader(); + + qreal tileSize() const; + QByteArray tileData(const QPoint &tile); + void drawTile(QPainter *painter, QPixmap &pixmap, QPointF &tp); + + friend QDebug operator<<(QDebug dbg, const File &file); + friend QDebug operator<<(QDebug dbg, const Zoom &zoom); + + QString _name; + QFile _file; + QVector _zooms; + int _zoom; + RectC _bounds; + qreal _mapRatio; + + bool _valid; + QString _errorString; +}; + +#ifndef QT_NO_DEBUG +QDebug operator<<(QDebug dbg, const AQMMap::File &file); +QDebug operator<<(QDebug dbg, const AQMMap::Zoom &zoom); +#endif // QT_NO_DEBUG + +#endif // AQMMAP_H diff --git a/src/map/maplist.cpp b/src/map/maplist.cpp index 46021793..598d8cb9 100644 --- a/src/map/maplist.cpp +++ b/src/map/maplist.cpp @@ -12,6 +12,7 @@ #include "IMG/gmap.h" #include "bsbmap.h" #include "kmzmap.h" +#include "aqmmap.h" #include "invalidmap.h" #include "maplist.h" @@ -50,6 +51,8 @@ Map *MapList::loadFile(const QString &path, bool *terminate) map = new BSBMap(path); else if (suffix == "kmz") map = new KMZMap(path); + else if (suffix == "aqm") + map = new AQMMap(path); return map ? map : new InvalidMap(path, "Unknown file format"); } @@ -95,6 +98,7 @@ QString MapList::formats() return qApp->translate("MapList", "Supported files") + " (" + filter().join(" ") + ");;" + + qApp->translate("MapList", "AlpineQuest maps") + " (*.aqm);;" + qApp->translate("MapList", "Garmin IMG maps") + " (*.gmap *.gmapi *.img *.xml);;" + qApp->translate("MapList", "Garmin JNX maps") + " (*.jnx);;" @@ -111,8 +115,8 @@ QString MapList::formats() QStringList MapList::filter() { QStringList filter; - filter << "*.gmap" << "*.gmapi" << "*.img" << "*.jnx" << "*.kap" << "*.kmz" - << "*.map" << "*.mbtiles" << "*.rmap" << "*.rtmap" << "*.tar" << "*.tba" - << "*.tif" << "*.tiff" << "*.xml"; + filter << "*.aqm" << "*.gmap" << "*.gmapi" << "*.img" << "*.jnx" << "*.kap" + << "*.kmz" << "*.map" << "*.mbtiles" << "*.rmap" << "*.rtmap" << "*.tar" + << "*.tba" << "*.tif" << "*.tiff" << "*.xml"; return filter; }