Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
V1.XX.X
[QMS-902] Add basic HiDPI support
[QMS-902] Add HiDPI support
[QMS-963] Add action to move a map to the top in map list
[QMS-1105] Fix: Context menu shortcuts not shown (macOS)
[QMS-1111] Add percentage values to track range
Expand Down
1 change: 0 additions & 1 deletion src/qmapshack/canvas/IDrawContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ void IDrawContext::run() {
// map render objects to buffer structure
currentBuffer.zoomFactor = zoomFactor;
currentBuffer.scale = scale;
currentBuffer.pixelRatio = pixelRatio;
currentBuffer.ref1 = ref1;
currentBuffer.ref2 = ref2;
currentBuffer.ref3 = ref3;
Expand Down
1 change: 0 additions & 1 deletion src/qmapshack/canvas/IDrawContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class IDrawContext : public QThread {
int zoomLevels; //< the number of zoom levels
QPointF zoomFactor{1.0, 1.0}; //< the zoomfactor used to draw the canvas
QPointF scale{1.0, 1.0}; //< the scale of the global viewport
qreal pixelRatio = 1.0; //< device pixel ratio of the canvas

QPointF ref1; //< top left corner
QPointF ref2; //< top right corner
Expand Down
10 changes: 6 additions & 4 deletions src/qmapshack/canvas/IDrawObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,25 @@ void IDrawObject::drawTileLQ(const QImage& img, QPolygonF& l, QPainter& p, IDraw
// finally translate, scale, rotate and draw tile
p.save();
p.translate(l[0]);
p.scale(w / img.width(), h / img.height());
auto imgSize = img.deviceIndependentSize();
p.scale(w / imgSize.width(), h / imgSize.height());
p.rotate(a);
p.drawImage(0, 0, img);
p.restore();
}

void IDrawObject::drawTileHQ(const QImage& img, QPolygonF& l, QPainter& p, IDrawContext& context,
const CProj& proj) const {
auto imgSize = img.deviceIndependentSize();
// the sub-tiles need a sensible size
// if they get too small there will be too much
// rounding effects.
qint32 nStepsX = 8;
qint32 nStepsY = 8;
if (img.width() / nStepsX < 32) {
if (imgSize.width() / nStepsX < 32) {
nStepsX = 4;
}
if (img.height() / nStepsY < 32) {
if (imgSize.height() / nStepsY < 32) {
nStepsY = 4;
}

Expand Down Expand Up @@ -161,7 +163,7 @@ void IDrawObject::drawTileHQ(const QImage& img, QPolygonF& l, QPainter& p, IDraw
// canvas using the view's projection
context.convertRad2Px(quads);

QRectF rect(0, 0, img.width() / nStepsX, img.height() / nStepsY);
QRectF rect(0, 0, imgSize.width() / nStepsX, imgSize.height() / nStepsY);
const qreal rw = rect.width();
const qreal rh = rect.height();

Expand Down
4 changes: 2 additions & 2 deletions src/qmapshack/dem/CDemVRT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ void CDemVRT::draw(IDrawContext::buffer_t& buf) {
}

// use bufferScale (and therefore the zoom level) and the pixel scale of the DEM to caluclate a downsampling factor
qreal buf_scale_x = qAbs(bufferScale.x() / xscale);
qreal buf_scale_y = qAbs(bufferScale.y() / yscale);
qreal buf_scale_x = qAbs(bufferScale.x() / xscale / buf.image.devicePixelRatio());
qreal buf_scale_y = qAbs(bufferScale.y() / yscale / buf.image.devicePixelRatio());
// <1 would mean GDAL does upscaling which is pointless
if (buf_scale_x < 1.0) {
buf_scale_x = 1.0;
Expand Down
2 changes: 1 addition & 1 deletion src/qmapshack/map/CMapGEMF.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ void CMapGEMF::draw(IDrawContext::buffer_t& buf) {
x2 = 180 * DEG_TO_RAD;
}

QPointF s1 = buf.scale * buf.zoomFactor / buf.pixelRatio;
QPointF s1 = buf.scale * buf.zoomFactor;
qreal d = NOFLOAT;
quint32 z = MAX_ZOOM_LEVEL;

Expand Down
2 changes: 1 addition & 1 deletion src/qmapshack/map/CMapJNX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ void CMapJNX::draw(IDrawContext::buffer_t& buf) /* override */
break;
}

qint32 level = scale2level(bufferScale.x() / (5 * buf.pixelRatio), mapFile);
qint32 level = scale2level(bufferScale.x() / 5, mapFile);

// no scalable level found, draw bounding box of map
// derive maps corner coordinate
Expand Down
2 changes: 1 addition & 1 deletion src/qmapshack/map/CMapRMAP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ void CMapRMAP::draw(IDrawContext::buffer_t& buf) /* override */
return;
}

level_t& level = findBestLevel(bufferScale / buf.pixelRatio);
level_t& level = findBestLevel(bufferScale);

// convert top left and bottom right point of buffer to local coord. system
QPointF p1 = buf.ref1;
Expand Down
148 changes: 78 additions & 70 deletions src/qmapshack/map/CMapTMS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,45 @@
#include "helpers/CDraw.h"
#include "map/CMapDraw.h"
#include "map/cache/CDiskCache.h"
#include "units/IUnit.h"

inline int lon2tile(double lon, int z) { return (int)(qRound(256 * (lon + 180.0) / 360.0 * qPow(2.0, z))); }

inline int lat2tile(double lat, int z) {
return (int)(qRound(256 * (1.0 - log(qTan(lat * M_PI / 180.0) + 1.0 / qCos(lat * M_PI / 180.0)) / M_PI) / 2.0 *
qPow(2.0, z)));
}

inline double tile2lon(int x, int z) { return x / qPow(2.0, z) * 360.0 - 180; }

inline double tile2lat(int y, int z) {
double n = M_PI - 2.0 * M_PI * y / qPow(2.0, z);
return 180.0 / M_PI * qAtan(0.5 * (exp(n) - exp(-n)));
}
namespace {
// extend of web mercator per direction in (projected) meters
// meaning the web mercator projection goes from -C to +C in both axis
constexpr qreal C = 20037508.34;

// convert from web mercator meters to tile indices
QPoint toTile(const QPointF& p, int z) {
int n = (1 << z); // n tiles across the map
// normalize into [0,1]
qreal x = (1 + p.x() / C) / 2;
qreal y = (1 - p.y() / C) / 2;

// tile that contains p
int x_tile = x * n;
int y_tile = y * n;
// clamp to valid range
x_tile = std::clamp(x_tile, 0, n - 1);
y_tile = std::clamp(y_tile, 0, n - 1);

return QPoint(x_tile, y_tile);
};

// get web mercator meters for the NW corner of a tile
QPointF fromTile(const QPoint& p, int z) {
int n = (1 << z);
// get normalized again
qreal x = static_cast<qreal>(p.x()) / n;
qreal y = static_cast<qreal>(p.y()) / n;

return QPointF((2 * x - 1) * C, (2 * y - 1) * -C);
};

// euclidian modulo
int eucmod(int a, int b) {
int r = a % b;
return r >= 0 ? r : r + std::abs(b);
};
} // namespace

CMapTMS::CMapTMS(const QString& filename, CMapDraw* parent) : IMapOnline(parent) {
qDebug() << "------------------------------";
Expand Down Expand Up @@ -106,6 +130,7 @@ CMapTMS::CMapTMS(const QString& filename, CMapDraw* parent) : IMapOnline(parent)
layers[idx].script = xmlLayer.namedItem("Script").toElement().text();
layers[idx].minZoomLevel = minZoomLevel;
layers[idx].maxZoomLevel = maxZoomLevel;
layers[idx].tileSizePx = 256;

layers[idx].strUrl.replace("{z}", "%1", Qt::CaseInsensitive);
layers[idx].strUrl.replace("{x}", "%2", Qt::CaseInsensitive);
Expand Down Expand Up @@ -293,89 +318,72 @@ void CMapTMS::draw(IDrawContext::buffer_t& buf) /* override */
return;
}

// get pixel offset of top left buffer corner
QPointF pp = buf.ref1;
map->convertRad2Px(pp);
// get the top-left and bottom-right corners into web mercator
auto r1 = buf.ref1;
auto r3 = buf.ref3;
proj.transform(r1, PJ_INV);
proj.transform(r3, PJ_INV);
QPointF pt1(r1.x(), r1.y());
QPointF pt2(r3.x(), r3.y());

// start to draw the map
QPainter p(&buf.image);
USE_ANTI_ALIASING(p, true);
p.setOpacity(getOpacity() / 100.0);
QPointF pp = buf.ref1;
map->convertRad2Px(pp);
p.translate(-pp);

// calculate maximum viewport
qreal x1 = buf.ref1.x() < buf.ref4.x() ? buf.ref1.x() : buf.ref4.x();
qreal y1 = buf.ref1.y() > buf.ref2.y() ? buf.ref1.y() : buf.ref2.y();

qreal x2 = buf.ref2.x() > buf.ref3.x() ? buf.ref2.x() : buf.ref3.x();
qreal y2 = buf.ref3.y() < buf.ref4.y() ? buf.ref3.y() : buf.ref4.y();

if (x1 < -180.0 * DEG_TO_RAD) {
x1 = -180 * DEG_TO_RAD;
}
if (x2 > 180.0 * DEG_TO_RAD) {
x2 = 180 * DEG_TO_RAD;
}

// draw layers
for (const layer_t& layer : std::as_const(layers)) {
for (auto& layer : layers) {
if (!layer.enabled) {
continue;
}

qint32 z = 20;
QPointF s1 = buf.scale * buf.zoomFactor / buf.pixelRatio;
qreal d = NOFLOAT;

for (qint32 i = layer.minZoomLevel; i < 21; i++) {
qreal s2 = 0.055 * (1 << i);
if (qAbs(s2 - s1.x()) < d) {
z = i;
d = qAbs(s2 - s1.x());
}
}

// calculate zoom level
qreal width = std::remainder(pt2.x() - pt1.x(), C);
qint32 z = std::round(std::log2(2 * C / (width / buf.image.width() * layer.tileSizePx)));
z = qMax(z, layer.minZoomLevel);
if (z > layer.maxZoomLevel) {
continue;
}
int n = (1 << z);

z = 21 - z;

qint32 row1, row2, col1, col2;
// tile indices of the two corners and number of tiles between them
auto t1 = toTile(pt1, z);
auto t2 = toTile(pt2, z);
auto dim = t2 - t1;
dim = {eucmod(dim.x(), n), dim.y()};

col1 = lon2tile(x1 * RAD_TO_DEG, z) / 256;
col2 = lon2tile(x2 * RAD_TO_DEG, z) / 256;
row1 = lat2tile(y1 * RAD_TO_DEG, z) / 256;
row2 = lat2tile(y2 * RAD_TO_DEG, z) / 256;

// qDebug() << col1 << col2 << row1 << row2 << (col2 - col1) << (row2 - row1) << ((col2 - col1) * (row2 -
// row1));

// start to request tiles. draw tiles in cache, queue urls of tile yet to be requested
for (qint32 row = row1; row <= row2; row++) {
for (qint32 col = col1; col <= col2; col++) {
QString url = createUrl(layer, col, row, z);
// qDebug() << url;
for (int i = 0; i <= dim.y(); ++i) {
for (int j = 0; j <= dim.x(); ++j) {
int x = eucmod(t1.x() + j, n);
int y = t1.y() + i;
QString url = createUrl(layer, x, y, z);

if (diskCache->contains(url)) {
QImage img;
diskCache->restore(url, img);

img.setDevicePixelRatio(buf.image.devicePixelRatio());
if (img.width() != layer.tileSizePx) {
// we got a tile with a different size then expected
// (which is normal for the first tile we get from e.g. a HiDPI source)
// remember it's size and request a redraw
layer.tileSizePx = img.width();
map->emitSigCanvasUpdate();
}

// TODO throw away drawTile and handle drawing in a sane way
// then we'll also get wraparound on the antimeridian
// (this function is ready but it's impossible with drawTile)
QPolygonF l;

qreal xx1 = tile2lon(col, z) * DEG_TO_RAD;
qreal yy1 = tile2lat(row, z) * DEG_TO_RAD;
qreal xx2 = tile2lon(col + 1, z) * DEG_TO_RAD;
qreal yy2 = tile2lat(row + 1, z) * DEG_TO_RAD;

l << QPointF(xx1, yy1) << QPointF(xx2, yy1) << QPointF(xx2, yy2) << QPointF(xx1, yy2);
l << fromTile({x, y}, z) << fromTile({x + 1, y}, z) << fromTile({x + 1, y + 1}, z) << fromTile({x, y + 1}, z);
proj.transform(l, PJ_FWD);
drawTile(img, l, p);
} else {
urlQueue << url;
}
}
}

emit sigQueueChanged();
}
}
3 changes: 2 additions & 1 deletion src/qmapshack/map/CMapTMS.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ class CMapTMS : public IMapOnline {
QString createUrl(const layer_t& layer, int x, int y, int z);

struct layer_t {
layer_t() : enabled(true), minZoomLevel(0), maxZoomLevel(0) {}
layer_t() : enabled(true), minZoomLevel(0), maxZoomLevel(0), tileSizePx(256) {}
bool enabled;
qint32 minZoomLevel;
qint32 maxZoomLevel;
qint32 tileSizePx;
QString title;
QString strUrl;
QString script;
Expand Down
4 changes: 2 additions & 2 deletions src/qmapshack/map/CMapVRT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ bool CMapVRT::computeSourceWindow(const IDrawContext::buffer_t& buf, const QPoin
sourceWindow_t& window) const {
// use bufferScale (and therefore the zoom level) and the pixel scale of the map to calculate a downsampling
// factor; <1 would mean GDAL does upscaling which is pointless
window.bufScaleX = qMax(1.0, qAbs(bufferScale.x() / xscale));
window.bufScaleY = qMax(1.0, qAbs(bufferScale.y() / yscale));
window.bufScaleX = qMax(1.0, qAbs(bufferScale.x() / xscale) / buf.image.devicePixelRatio());
window.bufScaleY = qMax(1.0, qAbs(bufferScale.y() / yscale) / buf.image.devicePixelRatio());

// corners of the area we shall draw, converted from the canvas projection into the
// map's own pixel coordinate space
Expand Down
19 changes: 17 additions & 2 deletions src/qmapshack/map/CMapWMTS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ void CMapWMTS::draw(IDrawContext::buffer_t& buf) /* override */
QRectF viewport(QPointF(x1, y1) * RAD_TO_DEG, QPointF(x2, y2) * RAD_TO_DEG);

// draw layers
for (const layer_t& layer : std::as_const(layers)) {
for (layer_t& layer : layers) {
if (!layer.boundingBox.intersects(viewport) || !layer.enabled) {
continue;
}
Expand All @@ -410,7 +410,12 @@ void CMapWMTS::draw(IDrawContext::buffer_t& buf) /* override */
const QStringList& keys = tileset.tilematrix.keys();
for (const QString& key : keys) {
const tilematrix_t& tilematrix = tileset.tilematrix[key];
qreal s2 = tilematrix.scale * 0.28e-3;
// Effective ground resolution of a served tile pixel. For spec-compliant
// layers tileScale is 1.0 and this is just the declared pixel span. For
// HiDPI servers that serve tiles larger than their TileMatrixSet declares,
// dividing by tileScale selects a coarser matrix so the bigger tile still
// maps 1:1 onto the (physical) buffer pixels.
qreal s2 = tilematrix.scale * 0.28e-3 / layer.tileScale;

if (qAbs(s2 - s1.x()) < d) {
tileMatrixId = key;
Expand Down Expand Up @@ -486,6 +491,16 @@ void CMapWMTS::draw(IDrawContext::buffer_t& buf) /* override */
QImage img;
diskCache->restore(url, img);

// Detect HiDPI servers that serve tiles larger (or smaller) than the
// TileWidth declared in their TileMatrixSet. Learn the real ratio from
// the fetched tile and request a redraw so a matching matrix is picked
// and the tile is drawn 1:1 to physical pixels (sharp, with the HiDPI
// text-size advantage on scaled displays).
if (tilematrix.tileWidth > 0 && img.width() != qRound(tilematrix.tileWidth * layer.tileScale)) {
layer.tileScale = qreal(img.width()) / tilematrix.tileWidth;
map->emitSigCanvasUpdate();
}

QPolygonF l;

qreal xx1 = col * (xscale * tilematrix.tileWidth) + tilematrix.topLeft.x();
Expand Down
10 changes: 10 additions & 0 deletions src/qmapshack/map/CMapWMTS.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ class CMapWMTS : public IMapOnline {
QRectF boundingBox;
QString resourceURL;
QMap<QString, limit_t> limits;

/**
Ratio of the actually served tile size to the TileWidth declared in the
TileMatrixSet. Some HiDPI servers violate the WMTS spec by reusing a
standard TileMatrixSet (e.g. TileWidth 256) while serving larger tiles
(e.g. 512px). The ratio is learned from the first fetched tile and used
to pick a matching (coarser) matrix so an over-sized tile still maps 1:1
onto the physical buffer pixels. 1.0 means the server is spec-compliant.
*/
qreal tileScale = 1.0;
};

QList<layer_t> layers;
Expand Down