From c1a1c3425bf8bbe45d7049d650dd8987a243ab1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20T=C5=AFma?= Date: Sat, 2 Mar 2019 16:51:14 +0100 Subject: [PATCH] Added support for (raster) RMap maps --- gpxsee.pro | 6 +- src/map/maplist.cpp | 8 +- src/map/rmap.cpp | 354 ++++++++++++++++++++++++++++++++++++++++++++ src/map/rmap.h | 62 ++++++++ 4 files changed, 426 insertions(+), 4 deletions(-) create mode 100644 src/map/rmap.cpp create mode 100644 src/map/rmap.h diff --git a/gpxsee.pro b/gpxsee.pro index dad3eb24..9bddf20b 100644 --- a/gpxsee.pro +++ b/gpxsee.pro @@ -158,7 +158,8 @@ HEADERS += src/common/config.h \ src/data/polygon.h \ src/data/area.h \ src/map/obliquestereographic.h \ - src/GUI/coordinatesitem.h + src/GUI/coordinatesitem.h \ + src/map/rmap.h SOURCES += src/main.cpp \ src/common/coordinates.cpp \ src/common/rectc.cpp \ @@ -273,7 +274,8 @@ SOURCES += src/main.cpp \ src/data/dem.cpp \ src/data/polygon.cpp \ src/map/obliquestereographic.cpp \ - src/GUI/coordinatesitem.cpp + src/GUI/coordinatesitem.cpp \ + src/map/rmap.cpp greaterThan(QT_MAJOR_VERSION, 4) { HEADERS += src/data/geojsonparser.h diff --git a/src/map/maplist.cpp b/src/map/maplist.cpp index ad79f83e..9fc2b9d2 100644 --- a/src/map/maplist.cpp +++ b/src/map/maplist.cpp @@ -7,6 +7,7 @@ #include "geotiffmap.h" #include "mapsource.h" #include "mbtilesmap.h" +#include "rmap.h" #include "maplist.h" @@ -58,6 +59,8 @@ bool MapList::loadFile(const QString &path, bool *atlas, bool dir) map = new GeoTIFFMap(path, this); else if (suffix == "mbtiles") map = new MBTilesMap(path, this); + else if (suffix == "rmap") + map = new RMap(path, this); else map = new OziMap(path, this); @@ -114,12 +117,13 @@ QString MapList::formats() { return tr("Supported files") - + " (*.jnx *.map *.mbtiles *.tar *.tba *.tif *.tiff *.xml);;" + + " (*.jnx *.map *.mbtiles *.rmap *.tar *.tba *.tif *.tiff *.xml);;" + tr("MBTiles maps") + " (*.mbtiles);;" + tr("Garmin JNX maps") + " (*.jnx);;" + tr("OziExplorer maps") + " (*.map);;" + tr("TrekBuddy maps/atlases") + " (*.tar *.tba);;" + tr("GeoTIFF images") + " (*.tif *.tiff);;" + + tr("RMap maps") + " (*.rmap);;" + tr("Online map sources") + " (*.xml)"; } @@ -127,6 +131,6 @@ QStringList MapList::filter() { QStringList filter; filter << "*.jnx" << "*.map" << "*.tba" << "*.tar" << "*.xml" << "*.tif" - << "*.tiff" << "*.mbtiles"; + << "*.tiff" << "*.mbtiles" << "*.rmap"; return filter; } diff --git a/src/map/rmap.cpp b/src/map/rmap.cpp new file mode 100644 index 00000000..bb662563 --- /dev/null +++ b/src/map/rmap.cpp @@ -0,0 +1,354 @@ +#include +#include +#include +#include +#include "common/rectc.h" +#include "common/wgs84.h" +#include "transform.h" +#include "utm.h" +#include "pcs.h" +#include "rectd.h" +#include "rmap.h" + + +#define MAGIC "CompeGPSRasterImage" +#define strsize(str) (sizeof(str) - 1) + +class CalibrationPoint { +public: + CalibrationPoint() {} + CalibrationPoint(PointD xy, PointD pp) : _xy(xy), _pp(pp) {} + CalibrationPoint(PointD xy, Coordinates c) : _xy(xy), _ll(c) {} + + ReferencePoint rp(const Projection &projection) const + { + return (_pp.isNull()) + ? ReferencePoint(_xy, projection.ll2xy(_ll)) + : ReferencePoint(_xy, _pp); + } + +private: + PointD _xy; + PointD _pp; + Coordinates _ll; +}; + +static CalibrationPoint parseCalibrationPoint(const QString &str) +{ + QStringList fields(str.split(",")); + if (fields.size() != 5) + return CalibrationPoint(); + + bool ret1, ret2; + PointD xy(fields.at(0).toDouble(&ret1), fields.at(1).toDouble(&ret2)); + if (!ret1 || !ret2) + return CalibrationPoint(); + PointD pp(fields.at(3).toDouble(&ret1), fields.at(4).toDouble(&ret2)); + if (!ret1 || !ret2) + return CalibrationPoint(); + + return (fields.at(2) == "A") + ? CalibrationPoint(xy, Coordinates(pp.x(), pp.y())) + : CalibrationPoint(xy, pp); +} + +static Projection parseProjection(const QString &str, const GCS *gcs) +{ + QStringList fields(str.split(",")); + if (fields.isEmpty()) + return Projection(); + bool ret; + int id = fields.at(0).toDouble(&ret); + if (!ret) + return Projection(); + PCS pcs; + int zone; + + switch (id) { + case 0: + if (fields.size() < 4) + return Projection(); + zone = fields.at(2).toInt(&ret); + if (!ret) + return Projection(); + pcs = PCS(gcs, 9807, UTM::setup(zone), 9001); + return Projection(&pcs); + case 1: + return Projection(gcs); + case 2: + pcs = PCS(gcs, 1024, Projection::Setup(), 9001); + return Projection(&pcs); + default: + return Projection(); + } +} + +bool RMap::parseIMP(const QByteArray &data) +{ + QStringList lines = QString(data).split("\r\n"); + QList cp; + const GCS *gcs = 0; + QString projection, datum; + + for (int i = 0; i < lines.count(); i++) { + const QString &line = lines.at(i); + + if (line.startsWith("Projection=")) + projection = line.split("=").at(1); + else if (line.startsWith("Datum=")) + datum = line.split("=").at(1); + else if (line.startsWith("P0=") || line.startsWith("P1=") + || line.startsWith("P2=") || line.startsWith("P3=")) { + QString point(line.split("=").at(1)); + cp.append(parseCalibrationPoint(point)); + } + } + + if (!(gcs = GCS::gcs(datum))) { + _errorString = datum + ": invalid datum"; + return false; + } + _projection = parseProjection(projection, gcs); + if (!_projection.isValid()) { + _errorString = projection + ": invalid projection"; + return false; + } + + QList rp; + for (int i = 0; i < cp.size(); i++) + rp.append(cp.at(i).rp(_projection)); + + _transform = Transform(rp); + if (!_transform.isValid()) { + _errorString = _transform.errorString(); + return false; + } + + return true; +} + +bool RMap::ok(const QDataStream &stream) +{ + if (stream.status() != QDataStream::Ok) { + _errorString = "Invalid/corrupted RMap file"; + return false; + } + + return true; +} + +bool RMap::seek(QFile &file, quint64 offset) +{ + if (!file.seek(offset)) { + _errorString = "Invalid/corrupted RMap file"; + return false; + } + + return true; +} + +RMap::RMap(const QString &fileName, QObject *parent) + : Map(parent), _fileName(fileName), _zoom(0), _valid(false) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + _errorString = file.errorString(); + return; + } + + QDataStream stream(&file); + stream.setByteOrder(QDataStream::LittleEndian); + + char magic[strsize(MAGIC)]; + if (stream.readRawData(magic, sizeof(magic)) != sizeof(magic) + || memcmp(MAGIC, magic, sizeof(magic))) { + _errorString = "Not a raster RMap file"; + return; + } + + quint32 tmp, width, height; + stream >> tmp >> tmp >> tmp; + stream >> width >> height; + QSize imageSize(width, -height); + stream >> tmp >> tmp; + stream >> width >> height; + _tileSize = QSize(width, height); + quint64 IMPOffset; + stream >> IMPOffset; + stream >> tmp; + qint32 zoomCount; + stream >> zoomCount; + if (!ok(stream)) + return; + + QVector zoomOffsets(zoomCount); + for (int i = 0; i < zoomCount; i++) + stream >> zoomOffsets[i]; + if (!ok(stream)) + return; + + for (int i = 0; i < zoomOffsets.size(); i++) { + _zooms.append(Zoom()); + Zoom &zoom = _zooms.last(); + + if (!seek(file, zoomOffsets.at(i))) + return; + + quint32 width, height; + stream >> width >> height; + zoom.size = QSize(width, -height); + stream >> width >> height; + zoom.dim = QSize(width, height); + zoom.scale = QPointF((qreal)zoom.size.width() / (qreal)imageSize.width(), + (qreal)zoom.size.height() / (qreal)imageSize.height()); + if (!ok(stream)) + return; + + zoom.tiles.resize(zoom.dim.width() * zoom.dim.height()); + for (int j = 0; j < zoom.tiles.size(); j++) + stream >> zoom.tiles[j]; + if (!ok(stream)) + return; + } + + if (!seek(file, IMPOffset)) + return; + quint32 IMPSize; + stream >> tmp >> IMPSize; + if (!ok(stream)) + return; + + QByteArray IMP(IMPSize + 1, 0); + stream.readRawData(IMP.data(), IMP.size()); + _valid = parseIMP(IMP); +} + +QString RMap::name() const +{ + QFileInfo fi(_fileName); + return fi.baseName(); +} + +QRectF RMap::bounds() +{ + return QRectF(QPointF(0, 0), _zooms.at(_zoom).size); +} + +int RMap::zoomFit(const QSize &size, const RectC &rect) +{ + if (!rect.isValid()) + _zoom = 0; + else { + RectD prect(rect, _projection); + QRectF sbr(_transform.proj2img(prect.topLeft()), + _transform.proj2img(prect.bottomRight())); + + for (int i = 0; i < _zooms.size(); i++) { + _zoom = i; + const Zoom &z = _zooms.at(i); + if (sbr.size().width() * z.scale.x() <= size.width() + && sbr.size().height() * z.scale.y() <= size.height()) + break; + } + } + + return _zoom; +} + +int RMap::zoomIn() +{ + _zoom = qMax(_zoom - 1, 0); + return _zoom; +} + +int RMap::zoomOut() +{ + _zoom = qMin(_zoom + 1, _zooms.size() - 1); + return _zoom; +} + +QPointF RMap::ll2xy(const Coordinates &c) +{ + const QPointF &scale = _zooms.at(_zoom).scale; + QPointF p(_transform.proj2img(_projection.ll2xy(c))); + return QPointF(p.x() * scale.x(), p.y() * scale.y()); +} + +Coordinates RMap::xy2ll(const QPointF &p) +{ + const QPointF &scale = _zooms.at(_zoom).scale; + return _projection.xy2ll(_transform.img2proj(QPointF(p.x() / scale.x(), + p.y() / scale.y()))); +} + +void RMap::load() +{ + _file.setFileName(_fileName); + _file.open(QIODevice::ReadOnly); +} + +void RMap::unload() +{ + _file.close(); +} + +QPixmap RMap::tile(int x, int y) +{ + const Zoom &zoom = _zooms.at(_zoom); + + qint32 index = y / _tileSize.height() * zoom.dim.width() + + x / _tileSize.width(); + if (index > zoom.tiles.size()) + return QPixmap(); + + quint64 offset = zoom.tiles.at(index); + if (!_file.seek(offset)) + return QPixmap(); + QDataStream stream(&_file); + stream.setByteOrder(QDataStream::LittleEndian); + quint32 tag, len; + stream >> tag >> len; + if (stream.status() != QDataStream::Ok) + return QPixmap(); + + QByteArray ba; + ba.resize(len); + if (stream.readRawData(ba.data(), ba.size()) != ba.size()) + return QPixmap(); + + QImage img(QImage::fromData(ba)); + return QPixmap::fromImage(img); +} + +void RMap::draw(QPainter *painter, const QRectF &rect, Flags flags) +{ + Q_UNUSED(flags); + + QSizeF ts(_tileSize.width(), _tileSize.height()); + QPointF tl(floor(rect.left() / ts.width()) * ts.width(), + floor(rect.top() / ts.height()) * ts.height()); + + QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y()); + for (int i = 0; i < ceil(s.width() / ts.width()); i++) { + for (int j = 0; j < ceil(s.height() / ts.height()); j++) { + int x = round(tl.x() + i * _tileSize.width()); + int y = round(tl.y() + j * _tileSize.height()); + + QPixmap pixmap; + QString key = _fileName + "/" + QString::number(_zoom) + "_" + + QString::number(x) + "_" + QString::number(y); + if (!QPixmapCache::find(key, &pixmap)) { + pixmap = tile(x, y); + if (!pixmap.isNull()) + QPixmapCache::insert(key, pixmap); + } + + if (pixmap.isNull()) + qWarning("%s: error loading tile image", qPrintable(key)); + else { + QPointF tp(tl.x() + i * ts.width(), tl.y() + j * ts.height()); + painter->drawPixmap(tp, pixmap); + } + } + } +} diff --git a/src/map/rmap.h b/src/map/rmap.h new file mode 100644 index 00000000..20b3785c --- /dev/null +++ b/src/map/rmap.h @@ -0,0 +1,62 @@ +#ifndef RMAP_H +#define RMAP_H + +#include "map.h" +#include "transform.h" +#include "projection.h" + +class RMap : public Map +{ + Q_OBJECT + +public: + RMap(const QString &fileName, QObject *parent = 0); + + QString name() const; + + QRectF bounds(); + + int zoom() const {return _zoom;} + void setZoom(int zoom) {_zoom = zoom;} + int zoomFit(const QSize &size, const RectC &rect); + int zoomIn(); + int zoomOut(); + + QPointF ll2xy(const Coordinates &c); + Coordinates xy2ll(const QPointF &p); + + void load(); + void unload(); + + void draw(QPainter *painter, const QRectF &rect, Flags flags); + + bool isValid() const {return _valid;} + QString errorString() const {return _errorString;} + +private: + struct Zoom { + QSize size; + QSize dim; + QPointF scale; + QVector tiles; + }; + + bool ok(const QDataStream &stream); + bool seek(QFile &file, quint64 offset); + bool parseIMP(const QByteArray &data); + QPixmap tile(int x, int y); + + QList _zooms; + Projection _projection; + Transform _transform; + QSize _tileSize; + QFile _file; + + QString _fileName; + int _zoom; + + bool _valid; + QString _errorString; +}; + +#endif // RMAP_H