mirror of
https://github.com/qgis/QGIS.git
synced 2025-12-15 00:07:25 -05:00
Fix calculation of tile zoom levels for ESRI vector tiles when
map is in a geographic CRS
This commit is contained in:
parent
d866bd3ce9
commit
883d31dbc0
@ -9,6 +9,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
class QgsTileXYZ
|
||||
{
|
||||
%Docstring(signature="appended")
|
||||
@ -295,6 +296,25 @@ The zoom level will be linearly interpolated between zoom levels present in the
|
||||
Finds the best fitting (integer) zoom level given a map ``scale`` denominator.
|
||||
|
||||
Values are constrained to the zoom levels between :py:func:`~QgsTileMatrixSet.minimumZoom` and :py:func:`~QgsTileMatrixSet.maximumZoom`.
|
||||
%End
|
||||
|
||||
double scaleForRenderContext( const QgsRenderContext &context ) const;
|
||||
%Docstring
|
||||
Calculates the correct scale to use for the tiles when rendered using the specified render ``context``.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
double calculateTileScaleForMap( double actualMapScale,
|
||||
const QgsCoordinateReferenceSystem &mapCrs,
|
||||
const QgsRectangle &mapExtent,
|
||||
const QSize mapSize,
|
||||
const double mapDpi
|
||||
) const;
|
||||
%Docstring
|
||||
Calculates the correct scale to use for the tiles when rendered using the specified map properties.
|
||||
|
||||
.. versionadded:: 3.26
|
||||
%End
|
||||
|
||||
virtual bool readXml( const QDomElement &element, QgsReadWriteContext &context );
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
#include "qgslogger.h"
|
||||
#include "qgscoordinatereferencesystem.h"
|
||||
#include "qgssettings.h"
|
||||
#include "qgsrendercontext.h"
|
||||
|
||||
QgsTileMatrix QgsTileMatrix::fromWebMercator( int zoomLevel )
|
||||
{
|
||||
@ -257,6 +258,48 @@ int QgsTileMatrixSet::scaleToZoomLevel( double scale ) const
|
||||
return std::clamp( tileZoom, minimumZoom(), maximumZoom() );
|
||||
}
|
||||
|
||||
double QgsTileMatrixSet::scaleForRenderContext( const QgsRenderContext &context ) const
|
||||
{
|
||||
return calculateTileScaleForMap( context.rendererScale(),
|
||||
context.coordinateTransform().destinationCrs(),
|
||||
context.mapExtent(),
|
||||
context.outputSize(),
|
||||
context.painter()->device()->logicalDpiX() );
|
||||
}
|
||||
|
||||
double QgsTileMatrixSet::calculateTileScaleForMap( double actualMapScale, const QgsCoordinateReferenceSystem &mapCrs, const QgsRectangle &mapExtent, const QSize mapSize, const double mapDpi ) const
|
||||
{
|
||||
switch ( mScaleToTileZoomMethod )
|
||||
{
|
||||
case Qgis::ScaleToTileZoomLevelMethod::MapBox:
|
||||
return actualMapScale;
|
||||
|
||||
case Qgis::ScaleToTileZoomLevelMethod::Esri:
|
||||
if ( mapCrs.isGeographic() )
|
||||
{
|
||||
// ESRI calculates the scale for geographic CRS ***ALWAYS*** at the equator, regardless of map extent!
|
||||
// see https://support.esri.com/en/technical-article/000007211, https://gis.stackexchange.com/questions/33270/how-does-arcmap-calculate-scalebar-inside-a-wgs84-layout
|
||||
constexpr double METERS_PER_DEGREE = M_PI / 180.0 * 6378137;
|
||||
constexpr double INCHES_PER_METER = 39.370078;
|
||||
const double mapWidthInches = mapExtent.width() * METERS_PER_DEGREE * INCHES_PER_METER;
|
||||
|
||||
double scale = mapWidthInches * mapDpi / static_cast< double >( mapSize.width() );
|
||||
|
||||
// Note: I **think** there's also some magic which ESRI applies when rendering tiles ON SCREEN,
|
||||
// which may be something like adjusting the scale based on the ratio between the map DPI and 96 DPI,
|
||||
// e.g. scale *= mapDpi / 96.0;
|
||||
// BUT the same adjustment isn't applied when exporting maps. This needs further investigation!
|
||||
|
||||
return scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
return actualMapScale;
|
||||
}
|
||||
}
|
||||
BUILTIN_UNREACHABLE
|
||||
}
|
||||
|
||||
bool QgsTileMatrixSet::readXml( const QDomElement &element, QgsReadWriteContext & )
|
||||
{
|
||||
mTileMatrices.clear();
|
||||
|
||||
@ -24,6 +24,8 @@
|
||||
#include "qgscoordinatereferencesystem.h"
|
||||
#include "qgsreadwritecontext.h"
|
||||
|
||||
class QgsRenderContext;
|
||||
|
||||
/**
|
||||
* \ingroup core
|
||||
* \brief Stores coordinates of a tile in a tile matrix set. Tile matrix is identified
|
||||
@ -289,6 +291,25 @@ class CORE_EXPORT QgsTileMatrixSet
|
||||
*/
|
||||
int scaleToZoomLevel( double scale ) const;
|
||||
|
||||
/**
|
||||
* Calculates the correct scale to use for the tiles when rendered using the specified render \a context.
|
||||
*
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
double scaleForRenderContext( const QgsRenderContext &context ) const;
|
||||
|
||||
/**
|
||||
* Calculates the correct scale to use for the tiles when rendered using the specified map properties.
|
||||
*
|
||||
* \since QGIS 3.26
|
||||
*/
|
||||
double calculateTileScaleForMap( double actualMapScale,
|
||||
const QgsCoordinateReferenceSystem &mapCrs,
|
||||
const QgsRectangle &mapExtent,
|
||||
const QSize mapSize,
|
||||
const double mapDpi
|
||||
) const;
|
||||
|
||||
/**
|
||||
* Reads the set from an XML \a element.
|
||||
*
|
||||
|
||||
@ -82,10 +82,11 @@ bool QgsVectorTileLayerRenderer::render()
|
||||
QElapsedTimer tTotal;
|
||||
tTotal.start();
|
||||
|
||||
const double tileRenderScale = mTileMatrixSet.scaleForRenderContext( ctx );
|
||||
QgsDebugMsgLevel( QStringLiteral( "Vector tiles rendering extent: " ) + ctx.extent().toString( -1 ), 2 );
|
||||
QgsDebugMsgLevel( QStringLiteral( "Vector tiles map scale 1 : %1" ).arg( ctx.rendererScale() ), 2 );
|
||||
QgsDebugMsgLevel( QStringLiteral( "Vector tiles map scale 1 : %1" ).arg( tileRenderScale ), 2 );
|
||||
|
||||
mTileZoom = mTileMatrixSet.scaleToZoomLevel( ctx.rendererScale() );
|
||||
mTileZoom = mTileMatrixSet.scaleToZoomLevel( tileRenderScale );
|
||||
QgsDebugMsgLevel( QStringLiteral( "Vector tiles zoom level: %1" ).arg( mTileZoom ), 2 );
|
||||
|
||||
mTileMatrix = mTileMatrixSet.tileMatrix( mTileZoom );
|
||||
@ -149,7 +150,7 @@ bool QgsVectorTileLayerRenderer::render()
|
||||
// add @zoom_level variable which can be used in styling
|
||||
QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Tiles" ) ); // will be deleted by popper
|
||||
scope->setVariable( QStringLiteral( "zoom_level" ), mTileZoom, true );
|
||||
scope->setVariable( QStringLiteral( "vector_tile_zoom" ), mTileMatrixSet.scaleToZoom( ctx.rendererScale() ), true );
|
||||
scope->setVariable( QStringLiteral( "vector_tile_zoom" ), mTileMatrixSet.scaleToZoom( tileRenderScale ), true );
|
||||
const QgsExpressionContextScopePopper popper( ctx.expressionContext(), scope );
|
||||
|
||||
mRenderer->startRender( *renderContext(), mTileZoom, mTileRange );
|
||||
|
||||
@ -338,12 +338,25 @@ QgsVectorTileBasicLabelingWidget::QgsVectorTileBasicLabelingWidget( QgsVectorTil
|
||||
{
|
||||
connect( mMapCanvas, &QgsMapCanvas::scaleChanged, this, [ = ]( double scale )
|
||||
{
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( scale ) : QgsVectorTileUtils::scaleToZoomLevel( scale, 0, 99 );
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( scale,
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : scale;
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 );
|
||||
mLabelCurrentZoom->setText( tr( "Current zoom: %1" ).arg( zoom ) );
|
||||
if ( mProxyModel )
|
||||
mProxyModel->setCurrentZoom( zoom );
|
||||
} );
|
||||
mLabelCurrentZoom->setText( tr( "Current zoom: %1" ).arg( mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( mMapCanvas->scale() ) : QgsVectorTileUtils::scaleToZoomLevel( mMapCanvas->scale(), 0, 99 ) ) );
|
||||
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : mMapCanvas->scale();
|
||||
mLabelCurrentZoom->setText( tr( "Current zoom: %1" ).arg( mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 ) ) );
|
||||
}
|
||||
|
||||
connect( mCheckVisibleOnly, &QCheckBox::toggled, this, [ = ]( bool filter )
|
||||
@ -378,7 +391,13 @@ void QgsVectorTileBasicLabelingWidget::setLayer( QgsVectorTileLayer *layer )
|
||||
|
||||
if ( mMapCanvas )
|
||||
{
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( mMapCanvas->scale() ) : QgsVectorTileUtils::scaleToZoomLevel( mMapCanvas->scale(), 0, 99 );
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : mMapCanvas->scale();
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 );
|
||||
mProxyModel->setCurrentZoom( zoom );
|
||||
}
|
||||
|
||||
@ -442,7 +461,13 @@ void QgsVectorTileBasicLabelingWidget::editStyleAtIndex( const QModelIndex &prox
|
||||
|
||||
if ( mMapCanvas )
|
||||
{
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( mMapCanvas->scale() ) : QgsVectorTileUtils::scaleToZoomLevel( mMapCanvas->scale(), 0, 99 );
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : mMapCanvas->scale();
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 );
|
||||
QList<QgsExpressionContextScope> scopes = context.additionalExpressionContextScopes();
|
||||
QgsExpressionContextScope tileScope;
|
||||
tileScope.setVariable( "zoom_level", zoom, true );
|
||||
|
||||
@ -340,12 +340,25 @@ QgsVectorTileBasicRendererWidget::QgsVectorTileBasicRendererWidget( QgsVectorTil
|
||||
{
|
||||
connect( mMapCanvas, &QgsMapCanvas::scaleChanged, this, [ = ]( double scale )
|
||||
{
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( scale ) : QgsVectorTileUtils::scaleToZoomLevel( scale, 0, 99 );
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : scale;
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 );
|
||||
mLabelCurrentZoom->setText( tr( "Current zoom: %1" ).arg( zoom ) );
|
||||
if ( mProxyModel )
|
||||
mProxyModel->setCurrentZoom( zoom );
|
||||
} );
|
||||
mLabelCurrentZoom->setText( tr( "Current zoom: %1" ).arg( mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( mMapCanvas->scale() ) : QgsVectorTileUtils::scaleToZoomLevel( mMapCanvas->scale(), 0, 99 ) ) );
|
||||
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : mMapCanvas->scale();
|
||||
mLabelCurrentZoom->setText( tr( "Current zoom: %1" ).arg( mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 ) ) );
|
||||
}
|
||||
|
||||
connect( mCheckVisibleOnly, &QCheckBox::toggled, this, [ = ]( bool filter )
|
||||
@ -380,7 +393,13 @@ void QgsVectorTileBasicRendererWidget::setLayer( QgsVectorTileLayer *layer )
|
||||
|
||||
if ( mMapCanvas )
|
||||
{
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( mMapCanvas->scale() ) : QgsVectorTileUtils::scaleToZoomLevel( mMapCanvas->scale(), 0, 99 );
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : mMapCanvas->scale();
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 );
|
||||
mProxyModel->setCurrentZoom( zoom );
|
||||
}
|
||||
|
||||
@ -446,7 +465,13 @@ void QgsVectorTileBasicRendererWidget::editStyleAtIndex( const QModelIndex &prox
|
||||
|
||||
if ( mMapCanvas )
|
||||
{
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( mMapCanvas->scale() ) : QgsVectorTileUtils::scaleToZoomLevel( mMapCanvas->scale(), 0, 99 );
|
||||
const QgsMapSettings &mapSettings = mMapCanvas->mapSettings();
|
||||
const double tileScale = mVTLayer ? mVTLayer->tileMatrixSet().calculateTileScaleForMap( mMapCanvas->scale(),
|
||||
mapSettings.destinationCrs(),
|
||||
mapSettings.visibleExtent(),
|
||||
mapSettings.outputSize(),
|
||||
mapSettings.outputDpi() ) : mMapCanvas->scale();
|
||||
const int zoom = mVTLayer ? mVTLayer->tileMatrixSet().scaleToZoomLevel( tileScale ) : QgsVectorTileUtils::scaleToZoomLevel( tileScale, 0, 99 );
|
||||
QList<QgsExpressionContextScope> scopes = context.additionalExpressionContextScopes();
|
||||
QgsExpressionContextScope tileScope;
|
||||
tileScope.setVariable( "zoom_level", zoom, true );
|
||||
|
||||
@ -11,6 +11,7 @@ __date__ = '04/03/2022'
|
||||
__copyright__ = 'Copyright 2022, The QGIS Project'
|
||||
|
||||
import qgis # NOQA
|
||||
from qgis.PyQt.QtCore import QSize
|
||||
from qgis.PyQt.QtXml import QDomDocument
|
||||
from qgis.core import (
|
||||
QgsTileXYZ,
|
||||
@ -21,7 +22,8 @@ from qgis.core import (
|
||||
QgsTileMatrixSet,
|
||||
QgsVectorTileMatrixSet,
|
||||
QgsReadWriteContext,
|
||||
Qgis
|
||||
Qgis,
|
||||
QgsRectangle,
|
||||
)
|
||||
from qgis.testing import start_app, unittest
|
||||
|
||||
@ -78,6 +80,12 @@ class TestQgsTiles(unittest.TestCase):
|
||||
self.assertEqual(matrix_set.maximumZoom(), 1)
|
||||
self.assertEqual(matrix_set.crs().authid(), 'EPSG:4326')
|
||||
|
||||
# should not apply any special logic here, and return scales unchanged
|
||||
self.assertEqual(matrix_set.calculateTileScaleForMap(1000, QgsCoordinateReferenceSystem('EPSG:4326'),
|
||||
QgsRectangle(0, 2, 20, 12), QSize(20, 10), 96), 1000)
|
||||
self.assertEqual(matrix_set.calculateTileScaleForMap(1000, QgsCoordinateReferenceSystem('EPSG:3857'),
|
||||
QgsRectangle(0, 2, 20, 12), QSize(20, 10), 96), 1000)
|
||||
|
||||
self.assertEqual(matrix_set.tileMatrix(1).zoomLevel(), 1)
|
||||
# zoom level not present in matrix!
|
||||
self.assertEqual(matrix_set.tileMatrix(99).zoomLevel(), -1)
|
||||
@ -357,6 +365,18 @@ class TestQgsTiles(unittest.TestCase):
|
||||
self.assertFalse(vector_tile_set.fromEsriJson({}))
|
||||
self.assertTrue(vector_tile_set.fromEsriJson(esri_metadata))
|
||||
|
||||
# should not apply any special logic here for non-geographic CRS, and return scales unchanged
|
||||
self.assertEqual(vector_tile_set.calculateTileScaleForMap(1000, QgsCoordinateReferenceSystem('EPSG:3857'),
|
||||
QgsRectangle(0, 2, 20, 12), QSize(20, 10), 96), 1000)
|
||||
|
||||
# for geographic CRS the scale should be calculated using the scale at the equator.
|
||||
# see https://support.esri.com/en/technical-article/000007211,
|
||||
# https://gis.stackexchange.com/questions/33270/how-does-arcmap-calculate-scalebar-inside-a-wgs84-layout
|
||||
self.assertAlmostEqual(vector_tile_set.calculateTileScaleForMap(420735075, QgsCoordinateReferenceSystem('EPSG:4326'),
|
||||
QgsRectangle(0, 2, 20, 12), QSize(2000, 1000), 96), 4207351, 0)
|
||||
self.assertAlmostEqual(vector_tile_set.calculateTileScaleForMap(420735075, QgsCoordinateReferenceSystem('EPSG:4326'),
|
||||
QgsRectangle(0, 62, 20, 72), QSize(2000, 1000), 96), 4207351, 0)
|
||||
|
||||
# we should NOT apply the tile scale doubling hack to ESRI tiles, otherwise our scales
|
||||
# are double what ESRI use for the same tile sets
|
||||
self.assertEqual(vector_tile_set.scaleToTileZoomMethod(), Qgis.ScaleToTileZoomLevelMethod.Esri)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user