Added support for ENC atlases (catalogues)
[GPXSee.git] / src / map / encatlas.cpp
blob8107a83d1fefc19294248c444979d578323a10a9
1 #include <QPainter>
2 #include <QPixmapCache>
3 #include "common/wgs84.h"
4 #include "GUI/format.h"
5 #include "rectd.h"
6 #include "pcs.h"
7 #include "encjob.h"
8 #include "encatlas.h"
10 using namespace ENC;
12 #define TILE_SIZE 512
14 Range ENCAtlas::zooms(IntendedUsage usage)
16 switch (usage) {
17 case Overview:
18 return Range(6, 7);
19 case General:
20 return Range(8, 9);
21 case Coastal:
22 return Range(10, 11);
23 case Approach:
24 return Range(12, 13);
25 case Harbour:
26 return Range(14, 18);
27 case Berthing:
28 return Range(19, 19);
30 case River:
31 return Range(12, 17);
32 case RiverHarbour:
33 return Range(18, 18);
34 case RiverBerthing:
35 return Range(19, 19);
36 default:
37 return Range(0, 19);
41 ENCAtlas::IntendedUsage ENCAtlas::usage(const QString &path)
43 QFileInfo fi(path);
44 QString basename(fi.baseName());
46 if (basename.size() != 8)
47 return Unknown;
48 int iu = basename.at(2).digitValue();
49 if (iu < 1 || iu > 9)
50 return Unknown;
52 return (IntendedUsage)iu;
55 bool ENCAtlas::processRecord(const ISO8211::Record &record, QByteArray &file,
56 RectC &bounds)
58 if (record.size() < 2)
59 return false;
61 const ENC::ISO8211::Field &f = record.at(1);
62 const QByteArray &ba = f.tag();
64 if (ba == "CATD") {
65 QByteArray FILE, IMPL;
67 if (!f.subfield("IMPL", &IMPL))
68 return false;
69 if (!f.subfield("FILE", &FILE))
70 return false;
72 if (IMPL == "BIN" && FILE.endsWith("000")) {
73 QByteArray SLAT, WLON, NLAT, ELON;
75 if (!f.subfield("SLAT", &SLAT))
76 return false;
77 if (!f.subfield("WLON", &WLON))
78 return false;
79 if (!f.subfield("NLAT", &NLAT))
80 return false;
81 if (!f.subfield("ELON", &ELON))
82 return false;
84 bool ok1, ok2, ok3, ok4;
85 bounds = RectC(Coordinates(WLON.toDouble(&ok1), NLAT.toDouble(&ok2)),
86 Coordinates(ELON.toDouble(&ok3), SLAT.toDouble(&ok4)));
87 if (!(ok1 && ok2 && ok3 && ok4))
88 return false;
90 file = FILE.replace('\\', '/');
92 return true;
96 return false;
99 void ENCAtlas::addMap(const QDir &dir, const QByteArray &file,
100 const RectC &bounds)
102 QString path(dir.absoluteFilePath(file));
103 if (!QFileInfo::exists(path)) {
104 qWarning("%s: No such map file", qPrintable(path));
105 return;
107 if (!bounds.isValid()) {
108 qWarning("%s: Invalid map bounds", qPrintable(path));
109 return;
112 IntendedUsage iu = usage(path);
113 auto it = _data.find(iu);
114 if (it == _data.end())
115 it = _data.insert(iu, new AtlasData(_cache, _lock));
117 it.value()->addMap(bounds, path);
119 _name = "ENC (" + Format::coordinates(bounds.center(), DecimalDegrees) + ")";
120 _llBounds |= bounds;
123 ENCAtlas::ENCAtlas(const QString &fileName, QObject *parent)
124 : Map(fileName, parent), _projection(PCS::pcs(3857)),
125 _tileRatio(1.0), _zoom(0), _valid(false)
127 QDir dir(QFileInfo(fileName).absoluteDir());
128 ISO8211 ddf(fileName);
129 ISO8211::Record record;
130 QByteArray file;
131 RectC bounds;
133 if (!ddf.readDDR()) {
134 _errorString = ddf.errorString();
135 return;
137 while (ddf.readRecord(record)) {
138 if (processRecord(record, file, bounds))
139 addMap(dir, file, bounds);
141 if (!ddf.errorString().isNull()) {
142 _errorString = ddf.errorString();
143 return;
146 if (_data.isEmpty()) {
147 _errorString = "No usable ENC map found";
148 return;
151 _usage = _data.firstKey();
152 _zoom = zooms(_usage).min();
153 updateTransform();
155 _cache.setMaxCost(10);
157 _valid = true;
160 ENCAtlas::~ENCAtlas()
162 qDeleteAll(_data);
165 void ENCAtlas::load(const Projection &in, const Projection &out,
166 qreal deviceRatio, bool hidpi)
168 Q_UNUSED(in);
169 Q_UNUSED(hidpi);
171 _tileRatio = deviceRatio;
172 _projection = out;
174 QPixmapCache::clear();
177 void ENCAtlas::unload()
179 cancelJobs(true);
181 _cache.clear();
184 int ENCAtlas::zoomFit(const QSize &size, const RectC &rect)
186 if (rect.isValid()) {
187 RectD pr(rect, _projection, 10);
189 for (auto it = _data.cbegin(); it != _data.cend(); ++it) {
190 Range z(zooms(it.key()));
192 _usage = it.key();
193 _zoom = z.min();
195 for (int i = z.min() + 1; i <= z.max(); i++) {
196 Transform t(transform(i));
197 QRectF r(t.proj2img(pr.topLeft()), t.proj2img(pr.bottomRight()));
199 if (size.width() < r.width() || size.height() < r.height()) {
200 updateTransform();
201 return _zoom;
204 _zoom = i;
207 } else {
208 IntendedUsage usage(_data.lastKey());
209 _usage = usage;
210 _zoom = zooms(usage).max();
213 updateTransform();
215 return _zoom;
218 int ENCAtlas::zoomIn()
220 cancelJobs(false);
222 if (_zoom + 1 <= zooms(_usage).max())
223 _zoom++;
224 else {
225 auto it = _data.find(_usage);
226 if (++it != _data.end()) {
227 _usage = it.key();
228 _zoom = zooms(it.key()).min();
232 updateTransform();
234 return _zoom;
237 int ENCAtlas::zoomOut()
239 cancelJobs(false);
241 if (_zoom - 1 >= zooms(_usage).min())
242 _zoom--;
243 else {
244 auto it = _data.find(_usage);
245 if (it != _data.begin()) {
246 --it;
247 _usage = it.key();
248 _zoom = zooms(it.key()).max();
252 updateTransform();
254 return _zoom;
257 void ENCAtlas::setZoom(int zoom)
259 _zoom = zoom;
260 updateTransform();
263 Transform ENCAtlas::transform(int zoom) const
265 int z = zoom + Util::log2i(TILE_SIZE);
267 double scale = _projection.isGeographic()
268 ? 360.0 / (1<<z) : (2.0 * M_PI * WGS84_RADIUS) / (1<<z);
269 PointD topLeft(_projection.ll2xy(_llBounds.topLeft()));
270 return Transform(ReferencePoint(PointD(0, 0), topLeft),
271 PointD(scale, scale));
274 void ENCAtlas::updateTransform()
276 _transform = transform(_zoom);
278 RectD prect(_llBounds, _projection);
279 _bounds = QRectF(_transform.proj2img(prect.topLeft()),
280 _transform.proj2img(prect.bottomRight()));
283 bool ENCAtlas::isRunning(int zoom, const QPoint &xy) const
285 for (int i = 0; i < _jobs.size(); i++) {
286 const QList<ENC::RasterTile> &tiles = _jobs.at(i)->tiles();
287 for (int j = 0; j < tiles.size(); j++) {
288 const ENC::RasterTile &mt = tiles.at(j);
289 if (mt.zoom() == zoom && mt.xy() == xy)
290 return true;
294 return false;
297 void ENCAtlas::runJob(ENCJob *job)
299 _jobs.append(job);
301 connect(job, &ENCJob::finished, this, &ENCAtlas::jobFinished);
302 job->run();
305 void ENCAtlas::removeJob(ENCJob *job)
307 _jobs.removeOne(job);
308 job->deleteLater();
311 void ENCAtlas::jobFinished(ENCJob *job)
313 const QList<ENC::RasterTile> &tiles = job->tiles();
315 for (int i = 0; i < tiles.size(); i++) {
316 const ENC::RasterTile &mt = tiles.at(i);
317 if (mt.isValid())
318 QPixmapCache::insert(key(mt.zoom(), mt.xy()), mt.pixmap());
321 removeJob(job);
323 emit tilesLoaded();
326 void ENCAtlas::cancelJobs(bool wait)
328 for (int i = 0; i < _jobs.size(); i++)
329 _jobs.at(i)->cancel(wait);
332 QString ENCAtlas::key(int zoom, const QPoint &xy) const
334 return path() + "-" + QString::number(zoom) + "_"
335 + QString::number(xy.x()) + "_" + QString::number(xy.y());
338 void ENCAtlas::draw(QPainter *painter, const QRectF &rect, Flags flags)
340 Q_UNUSED(flags);
341 QPointF tl(floor(rect.left() / TILE_SIZE) * TILE_SIZE,
342 floor(rect.top() / TILE_SIZE) * TILE_SIZE);
343 QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y());
344 int width = ceil(s.width() / TILE_SIZE);
345 int height = ceil(s.height() / TILE_SIZE);
347 QList<RasterTile> tiles;
349 for (int i = 0; i < width; i++) {
350 for (int j = 0; j < height; j++) {
351 QPoint ttl(tl.x() + i * TILE_SIZE, tl.y() + j * TILE_SIZE);
352 if (isRunning(_zoom, ttl))
353 continue;
355 QPixmap pm;
356 if (QPixmapCache::find(key(_zoom, ttl), &pm))
357 painter->drawPixmap(ttl, pm);
358 else
359 tiles.append(RasterTile(_projection, _transform,
360 _data.value(_usage), _zoom, zooms(_usage),
361 QRect(ttl, QSize(TILE_SIZE, TILE_SIZE)), _tileRatio));
365 if (!tiles.isEmpty()) {
366 if (flags & Map::Block) {
367 QFuture<void> future = QtConcurrent::map(tiles, &RasterTile::render);
368 future.waitForFinished();
370 for (int i = 0; i < tiles.size(); i++) {
371 const RasterTile &mt = tiles.at(i);
372 const QPixmap &pm = mt.pixmap();
373 painter->drawPixmap(mt.xy(), pm);
374 QPixmapCache::insert(key(mt.zoom(), mt.xy()), pm);
376 } else
377 runJob(new ENCJob(tiles));
381 Map *ENCAtlas::create(const QString &path, bool *isDir)
383 if (isDir)
384 *isDir = true;
386 return new ENCAtlas(path);