QGIS API Documentation  3.27.0-Master (597e8eebd4)
qgstiles.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgstiles.cpp
3  --------------------------------------
4  Date : March 2020
5  Copyright : (C) 2020 by Martin Dobias
6  Email : wonder dot sk at gmail dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
16 #include "qgstiles.h"
17 
18 #include "qgslogger.h"
20 #include "qgssettings.h"
21 #include "qgsrendercontext.h"
22 
24 {
25  constexpr double z0xMin = -20037508.3427892;
26  constexpr double z0yMax = 20037508.3427892;
27 
28  return fromCustomDef( zoomLevel, QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ), QgsPointXY( z0xMin, z0yMax ), 2 * z0yMax );
29 }
30 
32  const QgsPointXY &z0TopLeftPoint, double z0Dimension, int z0MatrixWidth, int z0MatrixHeight )
33 {
34  // Square extent calculation
35  double z0xMin = z0TopLeftPoint.x();
36  double z0yMax = z0TopLeftPoint.y();
37  double z0xMax = z0xMin + z0MatrixWidth * z0Dimension;
38  double z0yMin = z0yMax - z0MatrixHeight * z0Dimension;
39 
40  // Constant for scale denominator calculation
41  constexpr double TILE_SIZE = 256.0;
42  constexpr double PIXELS_TO_M = 2.8 / 10000.0; // WMS/WMTS define "standardized rendering pixel size" as 0.28mm
44  // Scale denominator calculation
45  const double scaleDenom0 = ( z0Dimension / TILE_SIZE ) * ( unitToMeters / PIXELS_TO_M );
46 
47  int numTiles = static_cast<int>( pow( 2, zoomLevel ) ); // assuming we won't ever go over 30 zoom levels
48 
49  QgsTileMatrix tm;
50  tm.mCrs = crs;
51  tm.mZoomLevel = zoomLevel;
52  tm.mMatrixWidth = z0MatrixWidth * numTiles;
53  tm.mMatrixHeight = z0MatrixHeight * numTiles;
54  tm.mTileXSpan = ( z0xMax - z0xMin ) / tm.mMatrixWidth;
55  tm.mTileYSpan = ( z0yMax - z0yMin ) / tm.mMatrixHeight;
56  tm.mExtent = QgsRectangle( z0xMin, z0yMin, z0xMax, z0yMax );
57  tm.mScaleDenom = scaleDenom0 / pow( 2, zoomLevel );
58  return tm;
59 }
60 
61 QgsTileMatrix QgsTileMatrix::fromTileMatrix( const int zoomLevel, const QgsTileMatrix &tileMatrix )
62 {
63  QgsTileMatrix tm;
64  int numTiles = static_cast<int>( pow( 2, zoomLevel ) ); // assuming we won't ever go over 30 zoom levels
65  int aZoomLevel = tileMatrix.zoomLevel();
66  int aNumTiles = static_cast<int>( pow( 2, aZoomLevel ) );
67  int aMatrixWidth = tileMatrix.matrixWidth();
68  int aMatrixHeight = tileMatrix.matrixHeight();
69  QgsRectangle aExtent = tileMatrix.extent();
70  tm.mCrs = tileMatrix.crs();
71  tm.mZoomLevel = zoomLevel;
72  tm.mMatrixWidth = aMatrixWidth * numTiles / aNumTiles;
73  tm.mMatrixHeight = aMatrixHeight * numTiles / aNumTiles;
74  tm.mTileXSpan = aExtent.width() / tm.mMatrixWidth;
75  tm.mTileYSpan = aExtent.height() / tm.mMatrixHeight;
76  tm.mExtent = aExtent;
77  tm.mScaleDenom = tileMatrix.scale() * pow( 2, aZoomLevel ) / pow( 2, zoomLevel );
78  return tm;
79 }
80 
82 {
83  double xMin = mExtent.xMinimum() + mTileXSpan * id.column();
84  double xMax = xMin + mTileXSpan;
85  double yMax = mExtent.yMaximum() - mTileYSpan * id.row();
86  double yMin = yMax - mTileYSpan;
87  return QgsRectangle( xMin, yMin, xMax, yMax );
88 }
89 
91 {
92  double x = mExtent.xMinimum() + mTileXSpan * id.column() + mTileXSpan / 2;
93  double y = mExtent.yMaximum() - mTileYSpan * id.row() - mTileYSpan / 2;
94  return QgsPointXY( x, y );
95 }
96 
98 {
99  double x0 = std::clamp( r.xMinimum(), mExtent.xMinimum(), mExtent.xMaximum() );
100  double y0 = std::clamp( r.yMinimum(), mExtent.yMinimum(), mExtent.yMaximum() );
101  double x1 = std::clamp( r.xMaximum(), mExtent.xMinimum(), mExtent.xMaximum() );
102  double y1 = std::clamp( r.yMaximum(), mExtent.yMinimum(), mExtent.yMaximum() );
103  if ( x0 >= x1 || y0 >= y1 )
104  return QgsTileRange(); // nothing to display
105 
106  double tileX1 = ( x0 - mExtent.xMinimum() ) / mTileXSpan;
107  double tileX2 = ( x1 - mExtent.xMinimum() ) / mTileXSpan;
108  double tileY1 = ( mExtent.yMaximum() - y1 ) / mTileYSpan;
109  double tileY2 = ( mExtent.yMaximum() - y0 ) / mTileYSpan;
110 
111  QgsDebugMsgLevel( QStringLiteral( "Tile range of edges [%1,%2] - [%3,%4]" ).arg( tileX1 ).arg( tileY1 ).arg( tileX2 ).arg( tileY2 ), 2 );
112 
113  // figure out tile range from zoom
114  int startColumn = std::clamp( static_cast<int>( floor( tileX1 ) ), 0, mMatrixWidth - 1 );
115  int endColumn = std::clamp( static_cast<int>( floor( tileX2 ) ), 0, mMatrixWidth - 1 );
116  int startRow = std::clamp( static_cast<int>( floor( tileY1 ) ), 0, mMatrixHeight - 1 );
117  int endRow = std::clamp( static_cast<int>( floor( tileY2 ) ), 0, mMatrixHeight - 1 );
118  return QgsTileRange( startColumn, endColumn, startRow, endRow );
119 }
120 
121 QPointF QgsTileMatrix::mapToTileCoordinates( const QgsPointXY &mapPoint ) const
122 {
123  double dx = mapPoint.x() - mExtent.xMinimum();
124  double dy = mExtent.yMaximum() - mapPoint.y();
125  return QPointF( dx / mTileXSpan, dy / mTileYSpan );
126 }
127 
128 //
129 // QgsTileMatrixSet
130 //
131 
133 {
134  return mTileMatrices.isEmpty();
135 }
136 
137 void QgsTileMatrixSet::addGoogleCrs84QuadTiles( int minimumZoom, int maximumZoom )
138 {
139  if ( maximumZoom < minimumZoom )
140  std::swap( minimumZoom, maximumZoom );
141 
142  for ( int zoom = minimumZoom; zoom <= maximumZoom; ++zoom )
143  {
145  }
146 }
147 
149 {
150  return mTileMatrices.value( zoom );
151 }
152 
154 {
155  mTileMatrices.insert( matrix.zoomLevel(), matrix );
156 }
157 
159 {
160  int res = -1;
161  for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
162  {
163  if ( res == -1 || it->zoomLevel() < res )
164  res = it->zoomLevel();
165  }
166  return res;
167 }
168 
170 {
171  int res = -1;
172  for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
173  {
174  if ( res == -1 || it->zoomLevel() > res )
175  res = it->zoomLevel();
176  }
177  return res;
178 }
179 
180 void QgsTileMatrixSet::dropMatricesOutsideZoomRange( int minimumZoom, int maximumZoom )
181 {
182  for ( auto it = mTileMatrices.begin(); it != mTileMatrices.end(); )
183  {
184  if ( it->zoomLevel() < minimumZoom || it->zoomLevel() > maximumZoom )
185  {
186  it = mTileMatrices.erase( it );
187  }
188  else
189  {
190  ++it;
191  }
192  }
193 }
194 
196 {
197  if ( mTileMatrices.empty() )
199 
200  return mTileMatrices.value( minimumZoom() ).crs();
201 }
202 
203 double QgsTileMatrixSet::scaleToZoom( double scale ) const
204 {
205  int zoomUnder = -1;
206  int zoomOver = -1;
207  double scaleUnder = 0;
208  double scaleOver = 0;
209 
210  switch ( mScaleToTileZoomMethod )
211  {
213  {
214  // TODO: it seems that map scale is double (is that because of high-dpi screen?)
215  // (this TODO was taken straight from QgsVectorTileUtils::scaleToZoom!)
216  scale *= 2;
217  break;
218  }
220  break;
221  }
222 
223  for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
224  {
225  if ( it->scale() > scale && ( zoomUnder == -1 || zoomUnder < it->zoomLevel() ) )
226  {
227  zoomUnder = it->zoomLevel();
228  scaleUnder = it->scale();
229  }
230  if ( it->scale() < scale && ( zoomOver == -1 || zoomOver > it->zoomLevel() ) )
231  {
232  zoomOver = it->zoomLevel();
233  scaleOver = it->scale();
234  }
235  }
236 
237  if ( zoomUnder < 0 )
238  return zoomOver;
239  if ( zoomOver < 0 )
240  return zoomUnder;
241  else
242  return ( scaleUnder - scale ) / ( scaleUnder - scaleOver ) * ( zoomOver - zoomUnder ) + zoomUnder;
243 }
244 
245 int QgsTileMatrixSet::scaleToZoomLevel( double scale ) const
246 {
247  int tileZoom = 0;
248  switch ( mScaleToTileZoomMethod )
249  {
251  tileZoom = static_cast<int>( round( scaleToZoom( scale ) ) );
252  break;
254  tileZoom = static_cast<int>( floor( scaleToZoom( scale ) ) );
255  break;
256  }
257 
258  return std::clamp( tileZoom, minimumZoom(), maximumZoom() );
259 }
260 
262 {
263  return calculateTileScaleForMap( context.rendererScale(),
265  context.mapExtent(),
266  context.outputSize(),
267  context.painter()->device()->logicalDpiX() );
268 }
269 
270 double QgsTileMatrixSet::calculateTileScaleForMap( double actualMapScale, const QgsCoordinateReferenceSystem &mapCrs, const QgsRectangle &mapExtent, const QSize mapSize, const double mapDpi ) const
271 {
272  switch ( mScaleToTileZoomMethod )
273  {
275  return actualMapScale;
276 
278  if ( mapCrs.isGeographic() )
279  {
280  // ESRI calculates the scale for geographic CRS ***ALWAYS*** at the equator, regardless of map extent!
281  // see https://support.esri.com/en/technical-article/000007211, https://gis.stackexchange.com/questions/33270/how-does-arcmap-calculate-scalebar-inside-a-wgs84-layout
282  constexpr double METERS_PER_DEGREE = M_PI / 180.0 * 6378137;
283  constexpr double INCHES_PER_METER = 39.370078;
284  const double mapWidthInches = mapExtent.width() * METERS_PER_DEGREE * INCHES_PER_METER;
285 
286  double scale = mapWidthInches * mapDpi / static_cast< double >( mapSize.width() );
287 
288  // Note: I **think** there's also some magic which ESRI applies when rendering tiles ON SCREEN,
289  // which may be something like adjusting the scale based on the ratio between the map DPI and 96 DPI,
290  // e.g. scale *= mapDpi / 96.0;
291  // BUT the same adjustment isn't applied when exporting maps. This needs further investigation!
292 
293  return scale;
294  }
295  else
296  {
297  return actualMapScale;
298  }
299  }
301 }
302 
303 bool QgsTileMatrixSet::readXml( const QDomElement &element, QgsReadWriteContext & )
304 {
305  mTileMatrices.clear();
306 
307  mScaleToTileZoomMethod = qgsEnumKeyToValue( element.attribute( QStringLiteral( "scaleToZoomMethod" ) ), Qgis::ScaleToTileZoomLevelMethod::MapBox );
308 
309  const QDomNodeList children = element.childNodes();
310  for ( int i = 0; i < children.size(); i++ )
311  {
312  const QDomElement matrixElement = children.at( i ).toElement();
313 
314  QgsTileMatrix matrix;
315  matrix.mZoomLevel = matrixElement.attribute( QStringLiteral( "zoomLevel" ) ).toInt();
316  matrix.mMatrixWidth = matrixElement.attribute( QStringLiteral( "matrixWidth" ) ).toInt();
317  matrix.mMatrixHeight = matrixElement.attribute( QStringLiteral( "matrixHeight" ) ).toInt();
318  matrix.mExtent = QgsRectangle(
319  matrixElement.attribute( QStringLiteral( "xMin" ) ).toDouble(),
320  matrixElement.attribute( QStringLiteral( "yMin" ) ).toDouble(),
321  matrixElement.attribute( QStringLiteral( "xMax" ) ).toDouble(),
322  matrixElement.attribute( QStringLiteral( "yMax" ) ).toDouble()
323  );
324 
325  matrix.mScaleDenom = matrixElement.attribute( QStringLiteral( "scale" ) ).toDouble();
326  matrix.mTileXSpan = matrixElement.attribute( QStringLiteral( "tileXSpan" ) ).toDouble();
327  matrix.mTileYSpan = matrixElement.attribute( QStringLiteral( "tileYSpan" ) ).toDouble();
328  matrix.mCrs.readXml( matrixElement );
329  addMatrix( matrix );
330  }
331  return true;
332 }
333 
334 QDomElement QgsTileMatrixSet::writeXml( QDomDocument &document, const QgsReadWriteContext & ) const
335 {
336  QDomElement setElement = document.createElement( QStringLiteral( "matrixSet" ) );
337  setElement.setAttribute( QStringLiteral( "scaleToZoomMethod" ), qgsEnumValueToKey( mScaleToTileZoomMethod ) );
338 
339  for ( auto it = mTileMatrices.constBegin(); it != mTileMatrices.constEnd(); ++it )
340  {
341  QDomElement matrixElement = document.createElement( QStringLiteral( "matrix" ) );
342  matrixElement.setAttribute( QStringLiteral( "zoomLevel" ), it->zoomLevel() );
343  matrixElement.setAttribute( QStringLiteral( "matrixWidth" ), it->matrixWidth() );
344  matrixElement.setAttribute( QStringLiteral( "matrixHeight" ), it->matrixHeight() );
345 
346  matrixElement.setAttribute( QStringLiteral( "xMin" ), qgsDoubleToString( it->mExtent.xMinimum() ) );
347  matrixElement.setAttribute( QStringLiteral( "xMax" ), qgsDoubleToString( it->mExtent.xMaximum() ) );
348  matrixElement.setAttribute( QStringLiteral( "yMin" ), qgsDoubleToString( it->mExtent.yMinimum() ) );
349  matrixElement.setAttribute( QStringLiteral( "yMax" ), qgsDoubleToString( it->mExtent.yMaximum() ) );
350 
351  matrixElement.setAttribute( QStringLiteral( "scale" ), qgsDoubleToString( it->scale() ) );
352  matrixElement.setAttribute( QStringLiteral( "tileXSpan" ), qgsDoubleToString( it->mTileXSpan ) );
353  matrixElement.setAttribute( QStringLiteral( "tileYSpan" ), qgsDoubleToString( it->mTileYSpan ) );
354 
355  it->crs().writeXml( matrixElement, document );
356  setElement.appendChild( matrixElement );
357  }
358  return setElement;
359 }
@ Esri
No scale doubling, always rounds down when matching to available tile levels.
@ MapBox
Uses a scale doubling approach to account for hi-DPI tiles, and rounds to the nearest tile level for ...
This class represents a coordinate reference system (CRS).
bool readXml(const QDomNode &node)
Restores state from the given DOM node.
Q_GADGET QgsUnitTypes::DistanceUnit mapUnits
QgsCoordinateReferenceSystem destinationCrs() const
Returns the destination coordinate reference system, which the transform will transform coordinates t...
A class to represent a 2D point.
Definition: qgspointxy.h:59
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
The class is used as a container of context for various read/write operations on other objects.
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:183
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
double height() const SIP_HOLDGIL
Returns the height of the rectangle.
Definition: qgsrectangle.h:230
double width() const SIP_HOLDGIL
Returns the width of the rectangle.
Definition: qgsrectangle.h:223
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
double rendererScale() const
Returns the renderer map scale.
QSize outputSize() const
Returns the size of the resulting rendered image, in pixels.
QgsRectangle mapExtent() const
Returns the original extent of the map being rendered.
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
virtual QDomElement writeXml(QDomDocument &document, const QgsReadWriteContext &context) const
Writes the set to an XML element.
Definition: qgstiles.cpp:334
void addGoogleCrs84QuadTiles(int minimumZoom=0, int maximumZoom=14)
Adds tile matrices corresponding to the standard web mercator/GoogleCRS84Quad setup.
Definition: qgstiles.cpp:137
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system associated with the tiles.
Definition: qgstiles.cpp:195
double scaleForRenderContext(const QgsRenderContext &context) const
Calculates the correct scale to use for the tiles when rendered using the specified render context.
Definition: qgstiles.cpp:261
int minimumZoom() const
Returns the minimum zoom level for tiles present in the set.
Definition: qgstiles.cpp:158
int scaleToZoomLevel(double scale) const
Finds the best fitting (integer) zoom level given a map scale denominator.
Definition: qgstiles.cpp:245
double scaleToZoom(double scale) const
Calculates a fractional zoom level given a map scale denominator.
Definition: qgstiles.cpp:203
int maximumZoom() const
Returns the maximum zoom level for tiles present in the set.
Definition: qgstiles.cpp:169
QgsTileMatrix tileMatrix(int zoom) const
Returns the tile matrix corresponding to the specified zoom.
Definition: qgstiles.cpp:148
virtual bool readXml(const QDomElement &element, QgsReadWriteContext &context)
Reads the set from an XML element.
Definition: qgstiles.cpp:303
void addMatrix(const QgsTileMatrix &matrix)
Adds a matrix to the set.
Definition: qgstiles.cpp:153
double calculateTileScaleForMap(double actualMapScale, const QgsCoordinateReferenceSystem &mapCrs, const QgsRectangle &mapExtent, const QSize mapSize, const double mapDpi) const
Calculates the correct scale to use for the tiles when rendered using the specified map properties.
Definition: qgstiles.cpp:270
void dropMatricesOutsideZoomRange(int minimumZoom, int maximumZoom)
Deletes any existing matrices which fall outside the zoom range specified by minimumZoom to maximumZo...
Definition: qgstiles.cpp:180
bool isEmpty() const
Returns true if the matrix set is empty.
Definition: qgstiles.cpp:132
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition: qgstiles.h:108
QgsRectangle tileExtent(QgsTileXYZ id) const
Returns extent of the given tile in this matrix.
Definition: qgstiles.cpp:81
QPointF mapToTileCoordinates(const QgsPointXY &mapPoint) const
Returns row/column coordinates (floating point number) from the given point in map coordinates.
Definition: qgstiles.cpp:121
QgsTileRange tileRangeFromExtent(const QgsRectangle &mExtent) const
Returns tile range that fully covers the given extent.
Definition: qgstiles.cpp:97
static QgsTileMatrix fromWebMercator(int zoomLevel)
Returns a tile matrix for the usual web mercator.
Definition: qgstiles.cpp:23
QgsRectangle extent() const
Returns extent of the tile matrix.
Definition: qgstiles.h:163
int matrixWidth() const
Returns number of columns of the tile matrix.
Definition: qgstiles.h:157
QgsCoordinateReferenceSystem crs() const
Returns the crs of the tile matrix.
Definition: qgstiles.h:131
double scale() const
Returns scale denominator of the tile matrix.
Definition: qgstiles.h:170
QgsPointXY tileCenter(QgsTileXYZ id) const
Returns center of the given tile in this matrix.
Definition: qgstiles.cpp:90
int matrixHeight() const
Returns number of rows of the tile matrix.
Definition: qgstiles.h:160
static QgsTileMatrix fromTileMatrix(int zoomLevel, const QgsTileMatrix &tileMatrix)
Returns a tile matrix based on another one.
Definition: qgstiles.cpp:61
int zoomLevel() const
Returns the zoom level of the tile matrix.
Definition: qgstiles.h:146
static QgsTileMatrix fromCustomDef(int zoomLevel, const QgsCoordinateReferenceSystem &crs, const QgsPointXY &z0TopLeftPoint, double z0Dimension, int z0MatrixWidth=1, int z0MatrixHeight=1)
Returns a tile matrix for a specific CRS, top left point, zoom level 0 dimension in CRS units.
Definition: qgstiles.cpp:31
Range of tiles in a tile matrix to be rendered.
Definition: qgstiles.h:71
Stores coordinates of a tile in a tile matrix set.
Definition: qgstiles.h:38
@ DistanceMeters
Meters.
Definition: qgsunittypes.h:69
static Q_INVOKABLE double fromUnitToUnitFactor(QgsUnitTypes::DistanceUnit fromUnit, QgsUnitTypes::DistanceUnit toUnit)
Returns the conversion factor between the specified distance units.
T qgsEnumKeyToValue(const QString &key, const T &defaultValue, bool tryValueAsKey=true, bool *returnOk=nullptr)
Returns the value corresponding to the given key of an enum.
Definition: qgis.h:2454
QString qgsDoubleToString(double a, int precision=17)
Returns a string representation of a double.
Definition: qgis.h:2199
QString qgsEnumValueToKey(const T &value, bool *returnOk=nullptr)
Returns the value for the given key of an enum.
Definition: qgis.h:2435
#define BUILTIN_UNREACHABLE
Definition: qgis.h:2902
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
const QgsCoordinateReferenceSystem & crs