Fixed broken map scale (ruler) on HiDPI maps
[GPXSee.git] / src / map / aqmmap.cpp
blob4f9b6a45d3cf62e8e1443ab9b69790c0547fe938
1 #include <cctype>
2 #include <QPainter>
3 #include <QPixmapCache>
4 #include <QImageReader>
5 #include <QBuffer>
6 #include <QtConcurrent>
7 #include "common/hash.h"
8 #include "osm.h"
9 #include "tile.h"
10 #include "aqmmap.h"
13 #define MAGIC "FLATPACK1"
15 static bool parseHeader(const QByteArray &data, QString &name)
17 QList<QByteArray> lines = data.split('\n');
19 for (int i = 0; i < lines.count(); i++) {
20 const QByteArray &line = lines.at(i);
22 QList<QByteArray> tokens = line.split('=');
23 if (tokens.size() != 2)
24 continue;
26 QByteArray key(tokens.at(0).trimmed());
27 QByteArray value(tokens.at(1).trimmed());
29 if (key == "name")
30 name = value;
33 return !name.isEmpty();
36 static bool parseLevel(const QByteArray &data, int &zoom, int &tileSize,
37 QRect &rect)
39 int id = -1;
40 int xtsize = 0, ytsize = 0;
41 int xtmin = 0, xtmax = 0, ytmin = 0, ytmax = 0;
42 QList<QByteArray> lines = data.split('\n');
44 for (int i = 0; i < lines.count(); i++) {
45 const QByteArray &line = lines.at(i);
47 QList<QByteArray> tokens = line.split('=');
48 if (tokens.size() != 2)
49 continue;
51 QByteArray key(tokens.at(0).trimmed());
52 QByteArray value(tokens.at(1).trimmed());
53 bool ok = true;
55 if (key == "id")
56 id = value.toInt(&ok);
57 else if (key == "xtsize")
58 xtsize = value.toInt(&ok);
59 else if (key == "ytsize")
60 ytsize = value.toInt(&ok);
61 else if (key == "xtmin")
62 xtmin = value.toInt(&ok);
63 else if (key == "xtmax")
64 xtmax = value.toInt(&ok);
65 else if (key == "ytmin")
66 ytmin = value.toInt(&ok);
67 else if (key == "ytmax")
68 ytmax = value.toInt(&ok);
70 if (!ok)
71 return false;
74 if (xtsize <= 0 || ytsize <= 0 || ytsize != xtsize || id < 0)
75 return false;
77 zoom = id;
78 tileSize = xtsize;
79 rect = QRect(QPoint(xtmin, (1<<zoom) - ytmax /*- 1*/),
80 QPoint(xtmax, (1<<zoom) - ytmin /*- 1*/));
82 return true;
86 bool AQMMap::readSize(size_t &size)
88 char c;
90 size = 0;
92 while (_file.getChar(&c)) {
93 if (isdigit(c))
94 size = size * 10 + c - '0';
95 else if (!c)
96 return true;
97 else
98 return false;
101 return false;
104 bool AQMMap::readString(QByteArray &str)
106 char c;
108 str.clear();
110 while (_file.getChar(&c)) {
111 if (!c)
112 return true;
113 else
114 str.append(c);
117 return false;
120 bool AQMMap::readFile(File &file)
122 if (!readString(file.name))
123 return false;
124 if (!readSize(file.offset))
125 return false;
127 return true;
130 bool AQMMap::readData(QByteArray &data)
132 size_t size;
134 if (!readSize(size))
135 return false;
136 data.resize(size);
137 return _file.read(data.data(), size) == (qint64)size;
140 bool AQMMap::readHeader()
142 size_t hdrSize, numFiles;
143 QByteArray data;
145 if (!readSize(hdrSize))
146 return false;
147 if (!readSize(numFiles))
148 return false;
150 QVector<File> files(numFiles);
151 for (size_t i = 0; i < numFiles; i++) {
152 if (!readFile(files[i]))
153 return false;
156 size_t start = _file.pos();
157 for (int i = 0; i < files.size(); i++)
158 files[i].offset += start;
160 int li = -1;
161 for (int i = 0; i < files.size(); i++) {
162 if (files.at(i).name == "V2HEADER") {
163 if (!(_file.seek(files.at(i).offset) && readData(data)))
164 return false;
165 if (!parseHeader(data, _name))
166 return false;
167 } else if (files.at(i).name == "V2LEVEL") {
168 int zoom, tileSize;
169 QRect bounds;
171 if (!(_file.seek(files.at(i).offset) && readData(data)))
172 return false;
173 if (!parseLevel(data, zoom, tileSize, bounds))
174 return false;
176 if (_bounds.isNull()) {
177 int minX = qMin((1<<zoom) - 1, qMax(0, bounds.left()));
178 int minY = qMin((1<<zoom) - 1, qMax(0, bounds.top()));
179 int maxX = qMin((1<<zoom) - 1, qMax(0, bounds.right())) + 1;
180 int maxY = qMin((1<<zoom) - 1, qMax(0, bounds.bottom())) + 1;
181 Coordinates tl(OSM::tile2ll(QPoint(minX, minY), zoom));
182 tl.rlat() = -tl.lat();
183 Coordinates br(OSM::tile2ll(QPoint(maxX, maxY), zoom));
184 br.rlat() = -br.lat();
185 // Workaround of broken zoom levels 0 and 1 due to numerical
186 // instability
187 tl.rlat() = qMin(tl.lat(), OSM::BOUNDS.top());
188 br.rlat() = qMax(br.lat(), OSM::BOUNDS.bottom());
189 _bounds = RectC(tl, br);
191 _zooms.append(Zoom(zoom, tileSize));
192 } else if (files.at(i).name == "@LEVEL") {
193 li = i;
194 break;
198 if (li < 0)
199 return false;
201 int level = -1;
202 for (int i = li; i < files.size(); i++) {
203 if (files.at(i).name == "@LEVEL")
204 level++;
205 else if (files.at(i).name == "#END")
206 break;
207 else {
208 if (level < 0 || level > _zooms.size() - 1)
209 return false;
211 QList<QByteArray> ba(files.at(i).name.split('_'));
212 if (ba.size() != 2)
213 return false;
214 bool xok, yok;
215 int x = ba.at(0).toInt(&xok);
216 int y = ba.at(1).toInt(&yok);
217 if (!(xok && yok))
218 return false;
219 int zoom = _zooms.at(level).zoom;
220 _zooms[level].tiles.insert(QPoint(x, (1<<zoom) - y /*- 1*/),
221 files.at(i).offset);
225 return true;
228 AQMMap::AQMMap(const QString &fileName, QObject *parent)
229 : Map(fileName, parent), _file(fileName), _zoom(0), _mapRatio(1.0),
230 _valid(false)
232 char magic[sizeof(MAGIC) - 1];
234 if (!_file.open(QIODevice::ReadOnly)) {
235 _errorString = _file.errorString();
236 return;
239 if (_file.read(magic, sizeof(magic)) != sizeof(magic)
240 || memcmp(magic, MAGIC, sizeof(magic))) {
241 _errorString = "Not an AlpineQuest map";
242 return;
245 if (!readHeader()) {
246 _errorString = "AQM file format error";
247 return;
250 _file.close();
252 _valid = true;
255 void AQMMap::load(const Projection &in, const Projection &out,
256 qreal deviceRatio, bool hidpi)
258 Q_UNUSED(in);
259 Q_UNUSED(out);
261 _mapRatio = hidpi ? deviceRatio : 1.0;
262 _file.open(QIODevice::ReadOnly);
265 void AQMMap::unload()
267 _file.close();
270 QRectF AQMMap::bounds()
272 return QRectF(ll2xy(_bounds.topLeft()), ll2xy(_bounds.bottomRight()));
275 int AQMMap::zoomFit(const QSize &size, const RectC &rect)
277 if (!rect.isValid())
278 _zoom = _zooms.size() - 1;
279 else {
280 for (int i = 1; i < _zooms.count(); i++) {
281 _zoom = i;
282 QRect sbr(QPoint(ll2xy(rect.topLeft()).toPoint()),
283 QPoint(ll2xy(rect.bottomRight()).toPoint()));
284 if (sbr.size().width() >= size.width() || sbr.size().height()
285 >= size.height()) {
286 _zoom--;
287 break;
292 return _zoom;
295 qreal AQMMap::resolution(const QRectF &rect)
297 const Zoom &z = _zooms.at(_zoom);
298 return OSM::resolution(rect.center(), z.zoom, tileSize());
301 int AQMMap::zoomIn()
303 _zoom = qMin(_zoom + 1, _zooms.size() - 1);
304 return _zoom;
307 int AQMMap::zoomOut()
309 _zoom = qMax(_zoom - 1, 0);
310 return _zoom;
313 QPointF AQMMap::ll2xy(const Coordinates &c)
315 const Zoom &z = _zooms.at(_zoom);
316 qreal scale = OSM::zoom2scale(z.zoom, z.tileSize);
317 QPointF m = OSM::ll2m(c);
318 return QPointF(m.x() / scale, m.y() / -scale) / _mapRatio;
321 Coordinates AQMMap::xy2ll(const QPointF &p)
323 const Zoom &z = _zooms.at(_zoom);
324 qreal scale = OSM::zoom2scale(z.zoom, z.tileSize);
325 return OSM::m2ll(QPointF(p.x() * scale, -p.y() * scale) * _mapRatio);
328 qreal AQMMap::tileSize() const
330 return (_zooms.at(_zoom).tileSize / _mapRatio);
333 QByteArray AQMMap::tileData(const QPoint &tile)
335 const Zoom &z = _zooms.at(_zoom);
336 QByteArray ba;
338 size_t offset = z.tiles.value(tile);
339 if (!(offset && _file.seek(offset) && readData(ba)))
340 return QByteArray();
342 return ba;
345 void AQMMap::draw(QPainter *painter, const QRectF &rect, Flags flags)
347 Q_UNUSED(flags);
348 const Zoom &z = _zooms.at(_zoom);
349 qreal scale = OSM::zoom2scale(z.zoom, z.tileSize);
350 QPoint tile = OSM::mercator2tile(QPointF(rect.topLeft().x() * scale,
351 -rect.topLeft().y() * scale) * _mapRatio, z.zoom);
352 Coordinates ctl(OSM::tile2ll(tile, z.zoom));
353 QPointF tl(ll2xy(Coordinates(ctl.lon(), -ctl.lat())));
354 QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y());
355 int width = ceil(s.width() / tileSize());
356 int height = ceil(s.height() / tileSize());
358 QList<DataTile> tiles;
360 for (int i = 0; i < width; i++) {
361 for (int j = 0; j < height; j++) {
362 QPixmap pm;
363 QPoint t(tile.x() + i, tile.y() + j);
364 QString key = path() + "-" + QString::number(z.zoom) + "_"
365 + QString::number(t.x()) + "_" + QString::number(t.y());
367 if (QPixmapCache::find(key, &pm)) {
368 QPointF tp(tl.x() + (t.x() - tile.x()) * tileSize(),
369 tl.y() + (t.y() - tile.y()) * tileSize());
370 drawTile(painter, pm, tp);
371 } else
372 tiles.append(DataTile(t, tileData(t), key));
376 QFuture<void> future = QtConcurrent::map(tiles, &DataTile::load);
377 future.waitForFinished();
379 for (int i = 0; i < tiles.size(); i++) {
380 const DataTile &mt = tiles.at(i);
381 QPixmap pm(mt.pixmap());
382 if (pm.isNull())
383 continue;
385 QPixmapCache::insert(mt.key(), pm);
387 QPointF tp(tl.x() + (mt.xy().x() - tile.x()) * tileSize(),
388 tl.y() + (mt.xy().y() - tile.y()) * tileSize());
389 drawTile(painter, pm, tp);
393 void AQMMap::drawTile(QPainter *painter, QPixmap &pixmap, QPointF &tp)
395 pixmap.setDevicePixelRatio(_mapRatio);
396 painter->drawPixmap(tp, pixmap);
399 Map *AQMMap::create(const QString &path, const Projection &proj, bool *isDir)
401 Q_UNUSED(proj);
403 if (isDir)
404 *isDir = false;
406 return new AQMMap(path);
409 #ifndef QT_NO_DEBUG
410 QDebug operator<<(QDebug dbg, const AQMMap::File &file)
412 dbg.nospace() << "File(" << file.name << ", " << file.offset << ")";
413 return dbg.space();
416 QDebug operator<<(QDebug dbg, const AQMMap::Zoom &zoom)
418 dbg.nospace() << "Zoom(" << zoom.zoom << ", " << zoom.tileSize << ", "
419 << zoom.tiles << ")";
420 return dbg.space();
422 #endif // QT_NO_DEBUG