2023-09-07 09:31:23 +02:00
|
|
|
#include <QPainter>
|
|
|
|
#include <QPixmapCache>
|
|
|
|
#include "common/wgs84.h"
|
|
|
|
#include "GUI/format.h"
|
|
|
|
#include "rectd.h"
|
|
|
|
#include "pcs.h"
|
|
|
|
#include "encjob.h"
|
|
|
|
#include "encatlas.h"
|
|
|
|
|
|
|
|
using namespace ENC;
|
|
|
|
|
2024-02-22 21:23:37 +01:00
|
|
|
#define EPSILON 1e-6
|
2023-09-07 09:31:23 +02:00
|
|
|
#define TILE_SIZE 512
|
|
|
|
|
|
|
|
Range ENCAtlas::zooms(IntendedUsage usage)
|
|
|
|
{
|
|
|
|
switch (usage) {
|
|
|
|
case Overview:
|
|
|
|
return Range(6, 7);
|
|
|
|
case General:
|
|
|
|
return Range(8, 9);
|
|
|
|
case Coastal:
|
|
|
|
return Range(10, 11);
|
|
|
|
case Approach:
|
|
|
|
return Range(12, 13);
|
|
|
|
case Harbour:
|
|
|
|
return Range(14, 18);
|
|
|
|
case Berthing:
|
|
|
|
return Range(19, 19);
|
|
|
|
|
|
|
|
case River:
|
|
|
|
return Range(12, 17);
|
|
|
|
case RiverHarbour:
|
|
|
|
return Range(18, 18);
|
|
|
|
case RiverBerthing:
|
|
|
|
return Range(19, 19);
|
|
|
|
default:
|
|
|
|
return Range(0, 19);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
ENCAtlas::IntendedUsage ENCAtlas::usage(const QString &path)
|
|
|
|
{
|
|
|
|
QFileInfo fi(path);
|
|
|
|
QString basename(fi.baseName());
|
|
|
|
|
|
|
|
if (basename.size() != 8)
|
|
|
|
return Unknown;
|
|
|
|
int iu = basename.at(2).digitValue();
|
|
|
|
if (iu < 1 || iu > 9)
|
|
|
|
return Unknown;
|
|
|
|
|
|
|
|
return (IntendedUsage)iu;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ENCAtlas::processRecord(const ISO8211::Record &record, QByteArray &file,
|
|
|
|
RectC &bounds)
|
|
|
|
{
|
|
|
|
if (record.size() < 2)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const ENC::ISO8211::Field &f = record.at(1);
|
|
|
|
const QByteArray &ba = f.tag();
|
|
|
|
|
|
|
|
if (ba == "CATD") {
|
|
|
|
QByteArray FILE, IMPL;
|
|
|
|
|
|
|
|
if (!f.subfield("IMPL", &IMPL))
|
|
|
|
return false;
|
|
|
|
if (!f.subfield("FILE", &FILE))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (IMPL == "BIN" && FILE.endsWith("000")) {
|
|
|
|
QByteArray SLAT, WLON, NLAT, ELON;
|
|
|
|
|
|
|
|
if (!f.subfield("SLAT", &SLAT))
|
|
|
|
return false;
|
|
|
|
if (!f.subfield("WLON", &WLON))
|
|
|
|
return false;
|
|
|
|
if (!f.subfield("NLAT", &NLAT))
|
|
|
|
return false;
|
|
|
|
if (!f.subfield("ELON", &ELON))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
bool ok1, ok2, ok3, ok4;
|
|
|
|
bounds = RectC(Coordinates(WLON.toDouble(&ok1), NLAT.toDouble(&ok2)),
|
|
|
|
Coordinates(ELON.toDouble(&ok3), SLAT.toDouble(&ok4)));
|
|
|
|
if (!(ok1 && ok2 && ok3 && ok4))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
file = FILE.replace('\\', '/');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::addMap(const QDir &dir, const QByteArray &file,
|
|
|
|
const RectC &bounds)
|
|
|
|
{
|
|
|
|
QString path(dir.absoluteFilePath(file));
|
|
|
|
if (!QFileInfo::exists(path)) {
|
2025-01-04 00:18:27 +01:00
|
|
|
qWarning("%s: No such map file", qUtf8Printable(path));
|
2023-09-07 09:31:23 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!bounds.isValid()) {
|
2025-01-04 00:18:27 +01:00
|
|
|
qWarning("%s: Invalid map bounds", qUtf8Printable(path));
|
2023-09-07 09:31:23 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
IntendedUsage iu = usage(path);
|
|
|
|
auto it = _data.find(iu);
|
|
|
|
if (it == _data.end())
|
2024-06-09 13:34:13 +02:00
|
|
|
it = _data.insert(iu, new AtlasData(_cache, _cacheLock));
|
2023-09-07 09:31:23 +02:00
|
|
|
|
|
|
|
it.value()->addMap(bounds, path);
|
|
|
|
|
|
|
|
_llBounds |= bounds;
|
|
|
|
}
|
|
|
|
|
|
|
|
ENCAtlas::ENCAtlas(const QString &fileName, QObject *parent)
|
2024-02-12 10:02:19 +01:00
|
|
|
: Map(fileName, parent), _projection(PCS::pcs(3857)), _tileRatio(1.0),
|
|
|
|
_style(0), _zoom(0), _valid(false)
|
2023-09-07 09:31:23 +02:00
|
|
|
{
|
|
|
|
QDir dir(QFileInfo(fileName).absoluteDir());
|
|
|
|
ISO8211 ddf(fileName);
|
|
|
|
ISO8211::Record record;
|
|
|
|
QByteArray file;
|
|
|
|
RectC bounds;
|
|
|
|
|
|
|
|
if (!ddf.readDDR()) {
|
|
|
|
_errorString = ddf.errorString();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
while (ddf.readRecord(record)) {
|
|
|
|
if (processRecord(record, file, bounds))
|
|
|
|
addMap(dir, file, bounds);
|
|
|
|
}
|
|
|
|
if (!ddf.errorString().isNull()) {
|
|
|
|
_errorString = ddf.errorString();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_data.isEmpty()) {
|
|
|
|
_errorString = "No usable ENC map found";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-02-18 07:38:46 +01:00
|
|
|
_name = "ENC (" + Format::coordinates(_llBounds.center(), DecimalDegrees)
|
|
|
|
+ ")";
|
2023-09-07 09:31:23 +02:00
|
|
|
_usage = _data.firstKey();
|
|
|
|
_zoom = zooms(_usage).min();
|
|
|
|
updateTransform();
|
|
|
|
|
|
|
|
_cache.setMaxCost(10);
|
|
|
|
|
|
|
|
_valid = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ENCAtlas::~ENCAtlas()
|
|
|
|
{
|
|
|
|
qDeleteAll(_data);
|
2024-02-12 10:02:19 +01:00
|
|
|
delete _style;
|
2023-09-07 09:31:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::load(const Projection &in, const Projection &out,
|
|
|
|
qreal deviceRatio, bool hidpi)
|
|
|
|
{
|
|
|
|
Q_UNUSED(in);
|
|
|
|
Q_UNUSED(hidpi);
|
|
|
|
|
|
|
|
_tileRatio = deviceRatio;
|
|
|
|
_projection = out;
|
|
|
|
|
2024-02-12 10:02:19 +01:00
|
|
|
Q_ASSERT(!_style);
|
|
|
|
_style = new Style(deviceRatio);
|
|
|
|
|
2023-09-07 09:31:23 +02:00
|
|
|
QPixmapCache::clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::unload()
|
|
|
|
{
|
|
|
|
cancelJobs(true);
|
|
|
|
|
|
|
|
_cache.clear();
|
2024-02-12 10:02:19 +01:00
|
|
|
|
|
|
|
delete _style;
|
|
|
|
_style = 0;
|
2023-09-07 09:31:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
int ENCAtlas::zoomFit(const QSize &size, const RectC &rect)
|
|
|
|
{
|
|
|
|
if (rect.isValid()) {
|
|
|
|
RectD pr(rect, _projection, 10);
|
|
|
|
|
|
|
|
for (auto it = _data.cbegin(); it != _data.cend(); ++it) {
|
|
|
|
Range z(zooms(it.key()));
|
|
|
|
|
|
|
|
_usage = it.key();
|
|
|
|
_zoom = z.min();
|
|
|
|
|
|
|
|
for (int i = z.min() + 1; i <= z.max(); i++) {
|
|
|
|
Transform t(transform(i));
|
|
|
|
QRectF r(t.proj2img(pr.topLeft()), t.proj2img(pr.bottomRight()));
|
|
|
|
|
2024-02-22 21:23:37 +01:00
|
|
|
if (size.width() + EPSILON < r.width()
|
|
|
|
|| size.height() + EPSILON < r.height()) {
|
2023-09-07 09:31:23 +02:00
|
|
|
updateTransform();
|
|
|
|
return _zoom;
|
|
|
|
}
|
|
|
|
|
|
|
|
_zoom = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
IntendedUsage usage(_data.lastKey());
|
|
|
|
_usage = usage;
|
|
|
|
_zoom = zooms(usage).max();
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTransform();
|
|
|
|
|
|
|
|
return _zoom;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ENCAtlas::zoomIn()
|
|
|
|
{
|
|
|
|
cancelJobs(false);
|
|
|
|
|
|
|
|
if (_zoom + 1 <= zooms(_usage).max())
|
|
|
|
_zoom++;
|
|
|
|
else {
|
|
|
|
auto it = _data.find(_usage);
|
|
|
|
if (++it != _data.end()) {
|
|
|
|
_usage = it.key();
|
|
|
|
_zoom = zooms(it.key()).min();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTransform();
|
|
|
|
|
|
|
|
return _zoom;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ENCAtlas::zoomOut()
|
|
|
|
{
|
|
|
|
cancelJobs(false);
|
|
|
|
|
|
|
|
if (_zoom - 1 >= zooms(_usage).min())
|
|
|
|
_zoom--;
|
|
|
|
else {
|
|
|
|
auto it = _data.find(_usage);
|
|
|
|
if (it != _data.begin()) {
|
|
|
|
--it;
|
|
|
|
_usage = it.key();
|
|
|
|
_zoom = zooms(it.key()).max();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTransform();
|
|
|
|
|
|
|
|
return _zoom;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::setZoom(int zoom)
|
|
|
|
{
|
|
|
|
_zoom = zoom;
|
|
|
|
updateTransform();
|
|
|
|
}
|
|
|
|
|
|
|
|
Transform ENCAtlas::transform(int zoom) const
|
|
|
|
{
|
|
|
|
int z = zoom + Util::log2i(TILE_SIZE);
|
|
|
|
|
|
|
|
double scale = _projection.isGeographic()
|
|
|
|
? 360.0 / (1<<z) : (2.0 * M_PI * WGS84_RADIUS) / (1<<z);
|
|
|
|
PointD topLeft(_projection.ll2xy(_llBounds.topLeft()));
|
|
|
|
return Transform(ReferencePoint(PointD(0, 0), topLeft),
|
|
|
|
PointD(scale, scale));
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::updateTransform()
|
|
|
|
{
|
|
|
|
_transform = transform(_zoom);
|
|
|
|
|
|
|
|
RectD prect(_llBounds, _projection);
|
|
|
|
_bounds = QRectF(_transform.proj2img(prect.topLeft()),
|
|
|
|
_transform.proj2img(prect.bottomRight()));
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ENCAtlas::isRunning(int zoom, const QPoint &xy) const
|
|
|
|
{
|
|
|
|
for (int i = 0; i < _jobs.size(); i++) {
|
|
|
|
const QList<ENC::RasterTile> &tiles = _jobs.at(i)->tiles();
|
|
|
|
for (int j = 0; j < tiles.size(); j++) {
|
|
|
|
const ENC::RasterTile &mt = tiles.at(j);
|
|
|
|
if (mt.zoom() == zoom && mt.xy() == xy)
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::runJob(ENCJob *job)
|
|
|
|
{
|
|
|
|
_jobs.append(job);
|
|
|
|
|
|
|
|
connect(job, &ENCJob::finished, this, &ENCAtlas::jobFinished);
|
|
|
|
job->run();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::removeJob(ENCJob *job)
|
|
|
|
{
|
|
|
|
_jobs.removeOne(job);
|
|
|
|
job->deleteLater();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::jobFinished(ENCJob *job)
|
|
|
|
{
|
|
|
|
const QList<ENC::RasterTile> &tiles = job->tiles();
|
|
|
|
|
|
|
|
for (int i = 0; i < tiles.size(); i++) {
|
|
|
|
const ENC::RasterTile &mt = tiles.at(i);
|
2024-02-23 09:45:41 +01:00
|
|
|
if (!mt.pixmap().isNull())
|
2023-09-07 09:31:23 +02:00
|
|
|
QPixmapCache::insert(key(mt.zoom(), mt.xy()), mt.pixmap());
|
|
|
|
}
|
|
|
|
|
|
|
|
removeJob(job);
|
|
|
|
|
|
|
|
emit tilesLoaded();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::cancelJobs(bool wait)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < _jobs.size(); i++)
|
|
|
|
_jobs.at(i)->cancel(wait);
|
|
|
|
}
|
|
|
|
|
|
|
|
QString ENCAtlas::key(int zoom, const QPoint &xy) const
|
|
|
|
{
|
|
|
|
return path() + "-" + QString::number(zoom) + "_"
|
|
|
|
+ QString::number(xy.x()) + "_" + QString::number(xy.y());
|
|
|
|
}
|
|
|
|
|
|
|
|
void ENCAtlas::draw(QPainter *painter, const QRectF &rect, Flags flags)
|
|
|
|
{
|
2024-02-18 15:05:54 +01:00
|
|
|
AtlasData *data = _data.value(_usage);
|
|
|
|
Range zr(zooms(_usage));
|
2023-09-07 09:31:23 +02:00
|
|
|
QPointF tl(floor(rect.left() / TILE_SIZE) * TILE_SIZE,
|
|
|
|
floor(rect.top() / TILE_SIZE) * TILE_SIZE);
|
|
|
|
QSizeF s(rect.right() - tl.x(), rect.bottom() - tl.y());
|
|
|
|
int width = ceil(s.width() / TILE_SIZE);
|
|
|
|
int height = ceil(s.height() / TILE_SIZE);
|
|
|
|
|
|
|
|
QList<RasterTile> tiles;
|
|
|
|
|
|
|
|
for (int i = 0; i < width; i++) {
|
|
|
|
for (int j = 0; j < height; j++) {
|
|
|
|
QPoint ttl(tl.x() + i * TILE_SIZE, tl.y() + j * TILE_SIZE);
|
|
|
|
if (isRunning(_zoom, ttl))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
QPixmap pm;
|
|
|
|
if (QPixmapCache::find(key(_zoom, ttl), &pm))
|
|
|
|
painter->drawPixmap(ttl, pm);
|
|
|
|
else
|
2024-02-12 10:02:19 +01:00
|
|
|
tiles.append(RasterTile(_projection, _transform, _style,
|
2024-02-18 15:05:54 +01:00
|
|
|
data, _zoom, zr, QRect(ttl, QSize(TILE_SIZE, TILE_SIZE)),
|
|
|
|
_tileRatio));
|
2023-09-07 09:31:23 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!tiles.isEmpty()) {
|
|
|
|
if (flags & Map::Block) {
|
|
|
|
QFuture<void> future = QtConcurrent::map(tiles, &RasterTile::render);
|
|
|
|
future.waitForFinished();
|
|
|
|
|
|
|
|
for (int i = 0; i < tiles.size(); i++) {
|
|
|
|
const RasterTile &mt = tiles.at(i);
|
|
|
|
const QPixmap &pm = mt.pixmap();
|
|
|
|
painter->drawPixmap(mt.xy(), pm);
|
|
|
|
QPixmapCache::insert(key(mt.zoom(), mt.xy()), pm);
|
|
|
|
}
|
|
|
|
} else
|
|
|
|
runJob(new ENCJob(tiles));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-14 18:36:03 +02:00
|
|
|
Map *ENCAtlas::create(const QString &path, const Projection &proj, bool *isDir)
|
2023-09-07 09:31:23 +02:00
|
|
|
{
|
2023-09-14 18:36:03 +02:00
|
|
|
Q_UNUSED(proj);
|
|
|
|
|
2023-09-07 09:31:23 +02:00
|
|
|
if (isDir)
|
|
|
|
*isDir = true;
|
|
|
|
|
|
|
|
return new ENCAtlas(path);
|
|
|
|
}
|