QGIS API Documentation 4.3.0-Master (45633be667c)
Loading...
Searching...
No Matches
qgsalgorithmxyztiles.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsalgorithmxyztiles.cpp
3 ---------------------
4 begin : August 2023
5 copyright : (C) 2023 by Alexander Bruy
6 email : alexander dot bruy at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19
20#include <algorithm>
21#include <atomic>
22
24#include "qgsimageoperation.h"
25#include "qgslayertree.h"
26#include "qgslayertreelayer.h"
27#include "qgsmaplayerutils.h"
29#include "qgstiles.h"
30
31#include <QBuffer>
32#include <QDir>
33#include <QQueue>
34#include <QSemaphore>
35#include <QSet>
36#include <QString>
37#include <QThread>
38#include <QThreadPool>
39#include <QWaitCondition>
40
41using namespace Qt::StringLiterals;
42
44
45
46namespace
47{
48 int tile2tms( const int y, const int zoom )
49 {
50 double n = std::pow( 2, zoom );
51 return ( int ) std::floor( n - y - 1 );
52 }
53} //namespace
54
55void MetaTile::addTile( const int row, const int col, Tile tileToAdd, const QgsRectangle &extent )
56{
57 tiles.insert( QPair<int, int>( row, col ), tileToAdd );
58 if ( row >= rows )
59 {
60 rows = row + 1;
61 }
62 if ( col >= cols )
63 {
64 cols = col + 1;
65 }
66 mExtent.combineExtentWith( extent );
67}
68
69
70namespace
71{
72 QList<MetaTile> getMetatiles(
73 const QgsTileMatrix &z0Matrix, const QgsRectangle &contentsExtent, int zoom, long long &tileCount, int metaTileSize, const QList<QgsTileExtentMaxZoomRegion> &maxZoomRegions, int defaultMaxZoom
74 )
75 {
76 const QgsTileMatrix targetZoomMatrix = QgsTileMatrix::fromTileMatrix( zoom, z0Matrix );
77
78 const long long matrixWidth = targetZoomMatrix.matrixWidth();
79 const long long matrixHeight = targetZoomMatrix.matrixHeight();
80
81 const QgsRectangle tileMatrixSetExtent = targetZoomMatrix.extent();
82 const double tileWidthUnits = tileMatrixSetExtent.width() / matrixWidth;
83 const double tileHeightUnits = tileMatrixSetExtent.height() / matrixHeight;
84
85 int minX = static_cast<int>( std::floor( ( contentsExtent.xMinimum() - tileMatrixSetExtent.xMinimum() ) / tileWidthUnits ) );
86 int maxX = static_cast<int>( std::ceil( ( contentsExtent.xMaximum() - tileMatrixSetExtent.xMinimum() ) / tileWidthUnits ) - 1 );
87 int minY = static_cast<int>( std::floor( ( tileMatrixSetExtent.yMaximum() - contentsExtent.yMaximum() ) / tileHeightUnits ) );
88 int maxY = static_cast<int>( std::ceil( ( tileMatrixSetExtent.yMaximum() - contentsExtent.yMinimum() ) / tileHeightUnits ) - 1 );
89
90 minX = std::clamp<int>( minX, 0, static_cast<int>( matrixWidth - 1 ) );
91 maxX = std::clamp<int>( maxX, 0, static_cast<int>( matrixWidth - 1 ) );
92 minY = std::clamp<int>( minY, 0, static_cast<int>( matrixHeight - 1 ) );
93 maxY = std::clamp<int>( maxY, 0, static_cast<int>( matrixHeight - 1 ) );
94
95 // IMPORTANT - we have to snap the target extent out to multiples of the metatile!
96 const int totalMetaColumns = static_cast<int>( std::ceil( static_cast<double>( maxX - minX + 1 ) / metaTileSize ) );
97 const int totalMetaRows = static_cast<int>( std::ceil( static_cast<double>( maxY - minY + 1 ) / metaTileSize ) );
98 tileCount = 0;
99 QList<MetaTile> metaTiles;
100 for ( int metaTileColumn = 0; metaTileColumn < totalMetaColumns; ++metaTileColumn )
101 {
102 const int startI = metaTileColumn * metaTileSize;
103 const int endI = std::min( startI + metaTileSize - 1, maxX - minX );
104 const int startX = minX + startI;
105 const int endX = minX + endI;
106
107 for ( int metaTileRow = 0; metaTileRow < totalMetaRows; ++metaTileRow )
108 {
109 const int startJ = metaTileRow * metaTileSize;
110 const int endJ = std::min( startJ + metaTileSize - 1, maxY - minY );
111 const int startY = minY + startJ;
112 const int endY = minY + endJ;
113
114 const double metaTileMinX = tileMatrixSetExtent.xMinimum() + startX * tileWidthUnits;
115 const double metaTileMaxX = tileMatrixSetExtent.xMinimum() + ( endX + 1 ) * tileWidthUnits;
116 const double metaTileMaxY = tileMatrixSetExtent.yMaximum() - startY * tileHeightUnits;
117 const double metaTileMinY = tileMatrixSetExtent.yMaximum() - ( endY + 1 ) * tileHeightUnits;
118 const QgsRectangle metaTileExtent( metaTileMinX, metaTileMaxY, metaTileMaxX, metaTileMinY );
119
120 // check if there's any zoom overrides in place for this metatile
121 if ( !maxZoomRegions.isEmpty() )
122 {
123 const int metaTileMaxZoom = QgsProcessingParameterTileExtentMaxZoomList::maxZoomForTile( metaTileExtent, maxZoomRegions, defaultMaxZoom );
124
125 if ( metaTileMaxZoom < zoom )
126 {
127 // don't need this zoom level for this metatile
128 continue;
129 }
130 }
131
132 // build the actual metatile
133 MetaTile metaTile;
134 for ( int x = startX, i = startI; x <= endX; ++x, ++i )
135 {
136 for ( int y = startY, j = startJ; y <= endY; ++y, ++j )
137 {
138 const double tileMinX = tileMatrixSetExtent.xMinimum() + x * tileWidthUnits;
139 const double tileMaxX = tileMinX + tileWidthUnits;
140 const double tileMaxY = tileMatrixSetExtent.yMaximum() - y * tileHeightUnits;
141 const double tileMinY = tileMaxY - tileHeightUnits;
142
143 const QgsRectangle tileExtent( tileMinX, tileMinY, tileMaxX, tileMaxY );
144
145 metaTile.addTile( i % metaTileSize, j % metaTileSize, Tile( x, y, zoom ), tileExtent );
146 tileCount++;
147 }
148 }
149
150 if ( !metaTile.tiles.isEmpty() )
151 {
152 metaTiles.append( metaTile );
153 }
154 }
155 }
156
157 return metaTiles;
158 }
159} //namespace
160
170template<typename T> class PendingTilesToWriteQueue
171{
172 public:
176 void push( const QList<T> &tiles )
177 {
178 if ( tiles.isEmpty() )
179 return;
180
181 QMutexLocker locker( &mMutex );
182 for ( const T &tile : tiles )
183 {
184 mQueue.enqueue( tile );
185 }
186 mNotEmpty.wakeOne();
187 }
188
196 bool popBatch( QList<T> &batch, int maxBatchSize, unsigned long timeoutMs = 500 )
197 {
198 QMutexLocker locker( &mMutex );
199
200 // wait until there is at least one item or all rendering is finished
201 while ( mQueue.isEmpty() && !mFinished )
202 {
203 mNotEmpty.wait( &mMutex );
204 }
205
206 if ( mQueue.isEmpty() )
207 return false;
208
209 // collect up to to maxBatchSize, unless we finish rendering or timeout before that happens
210 while ( mQueue.size() < maxBatchSize && !mFinished )
211 {
212 if ( !mNotEmpty.wait( &mMutex, timeoutMs ) )
213 {
214 // timeout exceeded
215 break;
216 }
217 }
218
219 while ( !mQueue.isEmpty() && batch.size() < maxBatchSize )
220 {
221 batch.append( mQueue.dequeue() );
222 }
223 return true;
224 }
225
229 void setFinished()
230 {
231 QMutexLocker locker( &mMutex );
232 mFinished = true;
233 mNotEmpty.wakeAll();
234 }
235
236 private:
237 QQueue<T> mQueue;
238 mutable QMutex mMutex;
239 QWaitCondition mNotEmpty;
240 bool mFinished = false;
241};
242
243//
244// QgsXyzTilesBaseAlgorithm
245//
246
247QString QgsXyzTilesBaseAlgorithm::group() const
248{
249 return QObject::tr( "Raster tools" );
250}
251
252QString QgsXyzTilesBaseAlgorithm::groupId() const
253{
254 return u"rastertools"_s;
255}
256
257Qgis::ProcessingAlgorithmFlags QgsXyzTilesBaseAlgorithm::flags() const
258{
260}
261
262void QgsXyzTilesBaseAlgorithm::createCommonParameters()
263{
264 auto extentParam = std::make_unique<QgsProcessingParameterExtent>( u"EXTENT"_s, QObject::tr( "Extent" ) );
265 extentParam->setHelp( QObject::tr( "Spatial extent of the area for tile generation." ) );
266 addParameter( extentParam.release() );
267
268 auto minZoomParam = std::make_unique<QgsProcessingParameterNumber>( u"ZOOM_MIN"_s, QObject::tr( "Minimum zoom" ), Qgis::ProcessingNumberParameterType::Integer, 12, false, 0, 25 );
269 minZoomParam->setHelp(
270 QObject::tr(
271 "Minimum zoom level for generated tiles (0–25). Lower zoom levels cover broader geographic areas "
272 "at lower spatial resolution. Must be less than or equal to the maximum zoom level."
273 )
274 );
275 addParameter( minZoomParam.release() );
276
277 auto maxZoomParam = std::make_unique<QgsProcessingParameterNumber>( u"ZOOM_MAX"_s, QObject::tr( "Maximum zoom" ), Qgis::ProcessingNumberParameterType::Integer, 12, false, 0, 25 );
278 maxZoomParam->setHelp(
279 QObject::tr(
280 "Maximum zoom level for generated tiles (0–25). Higher zoom levels capture finer map details "
281 "and higher resolution, but exponentially increase total tile count, storage requirements, and rendering time. "
282 "Must be greater than or equal to the minimum zoom level."
283 )
284 );
285 addParameter( maxZoomParam.release() );
286
287 auto dpiParam = std::make_unique<QgsProcessingParameterNumber>( u"DPI"_s, QObject::tr( "DPI" ), Qgis::ProcessingNumberParameterType::Integer, 96, false, 48, 600 );
288 dpiParam->setHelp( QObject::tr( "Output resolution in DPI for rendered map content." ) );
289 dpiParam->setFlags( dpiParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
290 addParameter( dpiParam.release() );
291
292 auto bgColorParam = std::make_unique<QgsProcessingParameterColor>( u"BACKGROUND_COLOR"_s, QObject::tr( "Background color" ), QColor( Qt::transparent ), true, true );
293 bgColorParam->setHelp( QObject::tr( "Background color used when rendering map tiles." ) );
294 addParameter( bgColorParam.release() );
295
296 auto antialiasParam = std::make_unique<QgsProcessingParameterBoolean>( u"ANTIALIAS"_s, QObject::tr( "Enable antialiasing" ), true );
297 antialiasParam->setHelp( QObject::tr( "Controls whether antialiasing is applied during tile rendering." ) );
298 antialiasParam->setFlags( antialiasParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
299 addParameter( antialiasParam.release() );
300
301 auto tileFormatParam = std::make_unique<QgsProcessingParameterEnum>( u"TILE_FORMAT"_s, QObject::tr( "Tile format" ), QStringList { u"PNG"_s, u"JPG"_s, u"WEBP"_s }, false, 0 );
302 tileFormatParam->setHelp( QObject::tr( "Output image format for the rendered tiles." ) );
303 addParameter( tileFormatParam.release() );
304
305 auto qualityParam = std::make_unique<QgsProcessingParameterNumber>( u"QUALITY"_s, QObject::tr( "Quality (JPG only)" ), Qgis::ProcessingNumberParameterType::Integer, 75, false, 1, 100 );
306 qualityParam->setHelp( QObject::tr( "Image quality percentage used when tile format is set to JPG (1–100)." ) );
307 qualityParam->setFlags( qualityParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
308 addParameter( qualityParam.release() );
309
310 auto metaTileSizeParam = std::make_unique<QgsProcessingParameterNumber>( u"METATILESIZE"_s, QObject::tr( "Metatile size" ), Qgis::ProcessingNumberParameterType::Integer, 4, false, 1, 20 );
311 metaTileSizeParam->setHelp( QObject::tr( "Size of metatiles (in tile units) used during rendering." ) );
312 metaTileSizeParam->setFlags( metaTileSizeParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
313 addParameter( metaTileSizeParam.release() );
314
315 auto skipEmptyTilesParam = std::make_unique<QgsProcessingParameterBoolean>( u"SKIP_EMPTY_TILES"_s, QObject::tr( "Skip empty tiles" ), false );
316 skipEmptyTilesParam->setHelp( QObject::tr( "If set, completely empty tiles will be skipped." ) );
317 skipEmptyTilesParam->setFlags( skipEmptyTilesParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
318 addParameter( skipEmptyTilesParam.release() );
319
320 auto tileExtentMaxZoomParam = std::make_unique<QgsProcessingParameterTileExtentMaxZoomList>( u"TILE_MAX_ZOOM_EXTENTS"_s, QObject::tr( "Maximum zoom extents" ), QVariant(), true );
321 tileExtentMaxZoomParam->setHelp( QObject::tr( "Allows overriding the maximum zoom level for specific geographic regions, so that more detailed tiles can be created for important areas." ) );
322 tileExtentMaxZoomParam->setFlags( tileExtentMaxZoomParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
323 addParameter( tileExtentMaxZoomParam.release() );
324}
325
326void QgsXyzTilesBaseAlgorithm::createTileMatrixParameters()
327{
328 auto crsParam = std::make_unique<QgsProcessingParameterCrs>( u"TARGET_CRS"_s, QObject::tr( "Target CRS" ), QVariant( u"EPSG:3857"_s ) );
329 crsParam->setFlags( crsParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
330 crsParam->setHelp( QObject::tr( "Coordinate reference system used for the output tile matrix set." ) );
331 addParameter( crsParam.release() );
332
333 auto z0ExtentParam = std::make_unique<QgsProcessingParameterExtent>( u"Z0_EXTENT"_s, QObject::tr( "Zoom 0 extent" ) );
334 z0ExtentParam->setFlags( z0ExtentParam->flags() | Qgis::ProcessingParameterFlag::Advanced | Qgis::ProcessingParameterFlag::Optional );
335 z0ExtentParam->setHelp(
336 QObject::tr(
337 "Extent of zoom level 0 in target CRS coordinates. Required when target CRS is not EPSG:3857. If the target CRS is EPSG:3857 then this parameter will be ignored and a standard Web Mercator "
338 "tile matrix will be used instead."
339 )
340 );
341 addParameter( z0ExtentParam.release() );
342
343 auto z0WidthParam = std::make_unique<QgsProcessingParameterNumber>( u"Z0_MATRIX_WIDTH"_s, QObject::tr( "Zoom 0 matrix width" ), Qgis::ProcessingNumberParameterType::Integer, 1, false, 1 );
344 z0WidthParam->setFlags( z0WidthParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
345 z0WidthParam->setHelp( QObject::tr( "Number of tile columns at zoom level 0 (matrix width)." ) );
346 addParameter( z0WidthParam.release() );
347
348 auto z0HeightParam = std::make_unique<QgsProcessingParameterNumber>( u"Z0_MATRIX_HEIGHT"_s, QObject::tr( "Zoom 0 matrix height" ), Qgis::ProcessingNumberParameterType::Integer, 1, false, 1 );
349 z0HeightParam->setFlags( z0HeightParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
350 z0HeightParam->setHelp( QObject::tr( "Number of tile rows at zoom level 0 (matrix height)." ) );
351 addParameter( z0HeightParam.release() );
352}
353
354bool QgsXyzTilesBaseAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
355{
356 Q_UNUSED( feedback );
357
358 QgsProject *project = context.project();
359
360 mExpressionContext = context.expressionContext();
361
362 const QList<QgsLayerTreeLayer *> projectLayers = project->layerTreeRoot()->findLayers();
363 QSet<QString> visibleLayers;
364 for ( const QgsLayerTreeLayer *layer : projectLayers )
365 {
366 if ( layer->isVisible() )
367 {
368 visibleLayers << layer->layer()->id();
369 }
370 }
371
372 const QList<QgsMapLayer *> renderLayers = project->layerTreeRoot()->layerOrder();
373 for ( QgsMapLayer *layer : renderLayers )
374 {
375 if ( visibleLayers.contains( layer->id() ) )
376 {
377 QgsMapLayer *clonedLayer = layer->clone();
378 clonedLayer->moveToThread( nullptr );
379 mLayers << clonedLayer;
380 }
381 }
382
383 if ( parameters.contains( u"TILE_WIDTH"_s ) )
384 {
385 mTileWidth = parameterAsInt( parameters, u"TILE_WIDTH"_s, context );
386 }
387
388 if ( parameters.contains( u"TILE_HEIGHT"_s ) )
389 {
390 mTileHeight = parameterAsInt( parameters, u"TILE_HEIGHT"_s, context );
391 }
392
393 if ( parameters.contains( u"TARGET_CRS"_s ) )
394 {
395 mTargetCrs = parameterAsCrs( parameters, u"TARGET_CRS"_s, context );
396 }
397 else
398 {
399 mTargetCrs = QgsCoordinateReferenceSystem( u"EPSG:3857"_s );
400 }
401
402 int z0MatrixWidth = 1;
403 if ( parameters.contains( u"Z0_MATRIX_WIDTH"_s ) )
404 {
405 z0MatrixWidth = parameterAsInt( parameters, u"Z0_MATRIX_WIDTH"_s, context );
406 }
407
408 int z0MatrixHeight = 1;
409 if ( parameters.contains( u"Z0_MATRIX_HEIGHT"_s ) )
410 {
411 z0MatrixHeight = parameterAsInt( parameters, u"Z0_MATRIX_HEIGHT"_s, context );
412 }
413
414 mTileGenerationRegion = parameterAsExtent( parameters, u"EXTENT"_s, context, mTargetCrs );
415
416 const QgsRectangle userZ0Extent = parameterAsExtent( parameters, u"Z0_EXTENT"_s, context, mTargetCrs );
417 QgsRectangle z0Extent;
418 if ( mTargetCrs.authid() == "EPSG:3857"_L1 )
419 {
420 if ( !userZ0Extent.isEmpty() )
421 {
422 feedback->pushWarning( QObject::tr( "Tiles are being generated in EPSG:3857, so a standard Web Mercator tile matrix will be used. The manual zoom 0 extent will be ignored." ) );
423 }
424 z0Extent = QgsRectangle( -MERC_MAX, -MERC_MAX, MERC_MAX, MERC_MAX );
425 }
426 else
427 {
428 if ( userZ0Extent.isEmpty() )
429 {
430 throw QgsProcessingException( QObject::tr( "Zoom 0 extent must be specified when using a custom target CRS (%1)" ).arg( mTargetCrs.userFriendlyIdentifier() ) );
431 }
432 z0Extent = userZ0Extent;
433 const double rawPixelXSize = z0Extent.width() / ( static_cast<double>( z0MatrixWidth ) * mTileWidth );
434 const double rawPixelYSize = z0Extent.height() / ( static_cast<double>( z0MatrixHeight ) * mTileHeight );
435
436 // tile matrix extents MUST be square! Expanding the tile matrix set extent around its center to force this.
437 if ( !qgsDoubleNear( rawPixelXSize, rawPixelYSize, 1e-9 ) )
438 {
439 const double isotropicPixelSize = std::max( rawPixelXSize, rawPixelYSize );
440 const double requiredWidth = isotropicPixelSize * z0MatrixWidth * mTileWidth;
441 const double requiredHeight = isotropicPixelSize * z0MatrixHeight * mTileHeight;
442
443 const QgsPointXY center = z0Extent.center();
444 z0Extent = QgsRectangle( center.x() - requiredWidth / 2.0, center.y() - requiredHeight / 2.0, center.x() + requiredWidth / 2.0, center.y() + requiredHeight / 2.0 );
445 }
446 }
447
448 mZ0matrix = QgsTileMatrix::fromCustomDef( 0, mTargetCrs, QgsPointXY( z0Extent.xMinimum(), z0Extent.yMaximum() ), z0Extent.width() / z0MatrixWidth, z0MatrixWidth, z0MatrixHeight );
449
450 QgsCoordinateTransform src2Wgs = QgsCoordinateTransform( mTargetCrs, QgsCoordinateReferenceSystem( u"EPSG:4326"_s ), context.transformContext() );
452 try
453 {
454 mWgs84Extent = src2Wgs.transformBoundingBox( mTileGenerationRegion );
455 }
456 catch ( QgsCsException & )
457 {
458 throw QgsProcessingException( QObject::tr( "Could not transform the extent into WGS84" ) );
459 }
460
461 mMinZoom = parameterAsInt( parameters, u"ZOOM_MIN"_s, context );
462 mMaxZoom = parameterAsInt( parameters, u"ZOOM_MAX"_s, context );
463 if ( mMaxZoom < mMinZoom )
464 {
465 throw QgsProcessingException( QObject::tr( "Maximum zoom (%1) must be ≥ minimum zoom (%2)" ).arg( mMaxZoom ).arg( mMinZoom ) );
466 }
467
468 mMaxZoomLimitIncludingOverrides = mMaxZoom;
469 const QList<QgsTileExtentMaxZoomRegion> rawRegions
470 = qgis::down_cast< const QgsProcessingParameterTileExtentMaxZoomList *>( parameterDefinition( u"TILE_MAX_ZOOM_EXTENTS"_s ) )->parameterAsRegionList( parameters.value( u"TILE_MAX_ZOOM_EXTENTS"_s ), context );
471 if ( !rawRegions.isEmpty() )
472 {
473 // convert all override regions to target crs once in advance
474 for ( const QgsTileExtentMaxZoomRegion &region : rawRegions )
475 {
476 QgsTileExtentMaxZoomRegion targetRegion = region;
477 try
478 {
479 QgsCoordinateTransform ct( region.extent.crs(), mTargetCrs, context.transformContext() );
480 ct.setBallparkTransformsAreAppropriate( true );
481 targetRegion.extent = QgsReferencedRectangle( ct.transformBoundingBox( region.extent ), mTargetCrs );
482 mMaxZoomRegions.append( targetRegion );
483 mMaxZoomLimitIncludingOverrides = std::max( mMaxZoomLimitIncludingOverrides, region.maxZoom );
484 }
485 catch ( QgsCsException & )
486 {
487 continue;
488 }
489 }
490 }
491
492 mDpi = parameterAsInt( parameters, u"DPI"_s, context );
493 mBackgroundColor = parameterAsColor( parameters, u"BACKGROUND_COLOR"_s, context );
494 mAntialias = parameterAsBool( parameters, u"ANTIALIAS"_s, context );
495 mSkipEmptyTiles = parameterAsBool( parameters, u"SKIP_EMPTY_TILES"_s, context );
496 switch ( parameterAsEnum( parameters, u"TILE_FORMAT"_s, context ) )
497 {
498 case 0:
499 mTileFormat = u"PNG"_s;
500 break;
501 case 1:
502 mTileFormat = u"JPG"_s;
503 break;
504 case 2:
505 mTileFormat = u"WEBP"_s;
506 break;
507 default:
508 mTileFormat = u"PNG"_s;
509 break;
510 }
511
512 mJpgQuality = mTileFormat != "PNG"_L1 ? parameterAsInt( parameters, u"QUALITY"_s, context ) : -1;
513 mMetaTileSize = parameterAsInt( parameters, u"METATILESIZE"_s, context );
514 mThreadsNumber = context.maximumThreads();
515 mTransformContext = context.transformContext();
516 mEllipsoid = context.ellipsoid();
517
518 if ( ( mTileFormat != "PNG"_L1 && mTileFormat != "WEBP"_L1 ) && mBackgroundColor.alpha() != 255 )
519 {
520 feedback->pushWarning(
521 QObject::tr( "A semi-transparent background color was set, but the JPG format only supports fully opaque colors. The background color setting will be ignored. Please use a fully opaque background color instead." )
522 );
523 }
524
525 mScaleMethod = project->scaleMethod();
526
527 return true;
528}
529
530void QgsXyzTilesBaseAlgorithm::checkLayersUsagePolicy( QgsProcessingFeedback *feedback )
531{
532 if ( mTotalMetaTiles > MAXIMUM_OPENSTREETMAP_TILES_FETCH )
533 {
534 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
535 {
537 {
538 // Prevent bulk downloading of tiles from openstreetmap.org as per OSMF tile usage policy
539 feedback->pushFormattedMessage(
540 QObject::tr( "Layer %1 will be skipped as the algorithm leads to bulk downloading behavior which is prohibited by the %2OpenStreetMap Foundation tile usage policy%3" )
541 .arg( layer->name(), u"<a href=\"https://operations.osmfoundation.org/policies/tiles/\">"_s, u"</a>"_s ),
542 QObject::tr( "Layer %1 will be skipped as the algorithm leads to bulk downloading behavior which is prohibited by the %2OpenStreetMap Foundation tile usage policy%3" )
543 .arg( layer->name(), QString(), QString() )
544 );
545 mLayers.removeAll( layer );
546 delete layer;
547 }
548 }
549 }
550}
551
552std::optional< QgsMapSettings > QgsXyzTilesBaseAlgorithm::mapSettingsForTile( const MetaTile &metaTile ) const
553{
554 QgsMapSettings settings;
555 settings.setExtent( metaTile.mExtent );
556 settings.setDestinationCrs( mTargetCrs );
558 settings.setOutputImageFormat( QImage::Format_ARGB32_Premultiplied );
559 settings.setTransformContext( mTransformContext );
560 settings.setEllipsoid( mEllipsoid );
561 settings.setLayers( mLayers );
562 settings.setOutputDpi( mDpi );
563 settings.setFlag( Qgis::MapSettingsFlag::Antialiasing, mAntialias );
568 settings.setScaleMethod( mScaleMethod );
569 if ( mTileFormat == "PNG"_L1 || mTileFormat == "WEBP"_L1 || mBackgroundColor.alpha() == 255 )
570 {
571 settings.setBackgroundColor( mBackgroundColor );
572 }
573 QSize size( mTileWidth * metaTile.rows, mTileHeight * metaTile.cols );
574 settings.setOutputSize( size );
575
576 QgsLabelingEngineSettings labelingSettings = settings.labelingEngineSettings();
577 labelingSettings.setFlag( Qgis::LabelingFlag::UsePartialCandidates, false );
578 settings.setLabelingEngineSettings( labelingSettings );
579
580 QgsExpressionContext exprContext = mExpressionContext;
582 settings.setExpressionContext( exprContext );
583
584 return settings;
585}
586
587void QgsXyzTilesBaseAlgorithm::startJobs( QgsProcessingFeedback *feedback )
588{
589 while ( mRendererJobs.size() < mThreadsNumber && !mMetaTiles.empty() )
590 {
591 if ( feedback->isCanceled() )
592 break;
593
594 MetaTile metaTile = mMetaTiles.takeFirst();
595 const std::optional<QgsMapSettings> settings = mapSettingsForTile( metaTile );
596 if ( !settings.has_value() )
597 {
598 mProcessedMetaTiles++;
599 feedback->setProgress( 100.0 * mProcessedMetaTiles / mTotalMetaTiles );
600 continue;
601 }
602
604 mRendererJobs.insert( job, metaTile );
605
606 QObject::connect( job, &QgsMapRendererJob::finished, mJobOwner, [this, feedback, job]() {
607 const MetaTile tile = mRendererJobs.take( job );
608 const QImage renderedImage = job->renderedImage();
609 job->deleteLater();
610
611 mProcessedMetaTiles++;
612 feedback->setProgress( 100.0 * mProcessedMetaTiles / mTotalMetaTiles );
613
614 processMetaTile( tile, renderedImage, feedback );
615
616 if ( !feedback->isCanceled() )
617 {
618 startJobs( feedback );
619 }
620 checkPipelineFinished( feedback );
621 } );
622
623 job->start();
624 }
625
626 checkPipelineFinished( feedback );
627}
628
629void QgsXyzTilesBaseAlgorithm::checkPipelineFinished( QgsProcessingFeedback *feedback )
630{
631 if ( feedback->isCanceled() || ( mMetaTiles.isEmpty() && mRendererJobs.isEmpty() && mActivePostProcessingTasks == 0 ) )
632 {
633 if ( mEventLoop )
634 {
635 mEventLoop->exit();
636 }
637 }
638}
639
640//
641// QgsXyzTilesDirectoryAlgorithm
642//
643
644QString QgsXyzTilesDirectoryAlgorithm::name() const
645{
646 return u"tilesxyzdirectory"_s;
647}
648
649QString QgsXyzTilesDirectoryAlgorithm::displayName() const
650{
651 return QObject::tr( "Generate XYZ tiles (Directory)" );
652}
653
654QStringList QgsXyzTilesDirectoryAlgorithm::tags() const
655{
656 return QObject::tr( "tiles,xyz,tms,directory" ).split( ',' );
657}
658
659QString QgsXyzTilesDirectoryAlgorithm::shortHelpString() const
660{
661 return QObject::tr(
662 "This algorithm generates XYZ raster tiles from the current project and saves them as individual image files in a structured directory hierarchy ({z}/{x}/{y}.png or .jpg).\n\n"
663 "All visible map layers from the project will be rendered into tiles across the specified extent and zoom range.\n\n"
664 "Optionally, a standalone Leaflet HTML file can be generated for instant web previewing of the tiles."
665 );
666}
667
668QString QgsXyzTilesDirectoryAlgorithm::shortDescription() const
669{
670 return QObject::tr( "Generates XYZ tiles from the project as a structured directory hierarchy ({z}/{x}/{y}.png/jpg)." );
671}
672
673QgsXyzTilesDirectoryAlgorithm *QgsXyzTilesDirectoryAlgorithm::createInstance() const
674{
675 return new QgsXyzTilesDirectoryAlgorithm();
676}
677
678void QgsXyzTilesDirectoryAlgorithm::initAlgorithm( const QVariantMap & )
679{
680 createCommonParameters();
681 createTileMatrixParameters();
682
683 auto tileWidthParam = std::make_unique<QgsProcessingParameterNumber>( u"TILE_WIDTH"_s, QObject::tr( "Tile width" ), Qgis::ProcessingNumberParameterType::Integer, 256, false, 1, 4096 );
684 tileWidthParam->setHelp( QObject::tr( "Width of each tile image in pixels." ) );
685 addParameter( tileWidthParam.release() );
686
687 auto tileHeightParam = std::make_unique<QgsProcessingParameterNumber>( u"TILE_HEIGHT"_s, QObject::tr( "Tile height" ), Qgis::ProcessingNumberParameterType::Integer, 256, false, 1, 4096 );
688 tileHeightParam->setHelp( QObject::tr( "Height of each tile image in pixels." ) );
689 addParameter( tileHeightParam.release() );
690
691 auto tmsParam = std::make_unique<QgsProcessingParameterBoolean>( u"TMS_CONVENTION"_s, QObject::tr( "Use inverted tile Y axis (TMS convention)" ), false );
692 tmsParam->setHelp( QObject::tr( "Inverts the Y tile coordinate naming convention to follow TMS format." ) );
693 addParameter( tmsParam.release() );
694
695 auto titleParam = std::make_unique<QgsProcessingParameterString>( u"HTML_TITLE"_s, QObject::tr( "Leaflet HTML output title" ), QVariant(), false, true );
696 titleParam->setFlags( titleParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
697 titleParam->setHelp( QObject::tr( "Title displayed in the generated Leaflet HTML web viewer." ) );
698 addParameter( titleParam.release() );
699
700 auto attributionParam = std::make_unique<QgsProcessingParameterString>( u"HTML_ATTRIBUTION"_s, QObject::tr( "Leaflet HTML output attribution" ), QVariant(), false, true );
701 attributionParam->setFlags( attributionParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
702 attributionParam->setHelp( QObject::tr( "Attribution text displayed in the generated Leaflet HTML web viewer." ) );
703 addParameter( attributionParam.release() );
704
705 auto osmParam = std::make_unique<QgsProcessingParameterBoolean>( u"HTML_OSM"_s, QObject::tr( "Include OpenStreetMap basemap in Leaflet HTML output" ), false );
706 osmParam->setFlags( osmParam->flags() | Qgis::ProcessingParameterFlag::Advanced );
707 osmParam->setHelp( QObject::tr( "Includes an OpenStreetMap background layer in the generated Leaflet HTML viewer." ) );
708 addParameter( osmParam.release() );
709
710 auto outputDirParam = std::make_unique<QgsProcessingParameterFolderDestination>( u"OUTPUT_DIRECTORY"_s, QObject::tr( "Output directory" ) );
711 outputDirParam->setHelp( QObject::tr( "Destination folder where the generated directory structure and tile files will be stored." ) );
712 addParameter( outputDirParam.release() );
713
714 auto outputHtmlParam = std::make_unique<QgsProcessingParameterFileDestination>( u"OUTPUT_HTML"_s, QObject::tr( "Output HTML (Leaflet)" ), QObject::tr( "HTML files (*.html)" ), QVariant(), true );
715 outputHtmlParam->setHelp( QObject::tr( "Destination file path for the optional Leaflet HTML web map preview." ) );
716 addParameter( outputHtmlParam.release() );
717
718 addOutput( new QgsProcessingOutputRasterLayer( u"OUTPUT_LAYER"_s, QObject::tr( "Output tiles as raster layer" ) ) );
719}
720
721QVariantMap QgsXyzTilesDirectoryAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
722{
723 QGS_MARK_ALGORITHM_SOURCE
724
725 const bool tms = parameterAsBoolean( parameters, u"TMS_CONVENTION"_s, context );
726 const QString title = parameterAsString( parameters, u"HTML_TITLE"_s, context );
727 const QString attribution = parameterAsString( parameters, u"HTML_ATTRIBUTION"_s, context );
728 const bool useOsm = parameterAsBoolean( parameters, u"HTML_OSM"_s, context );
729 QString outputDir = parameterAsString( parameters, u"OUTPUT_DIRECTORY"_s, context );
730 const QString outputHtml = parameterAsString( parameters, u"OUTPUT_HTML"_s, context );
731
732 mOutputDir = outputDir;
733 mTms = tms;
734
735 long long totalTiles = 0;
736 mTotalMetaTiles = 0;
737 for ( int z = mMinZoom; z <= mMaxZoomLimitIncludingOverrides; z++ )
738 {
739 if ( feedback->isCanceled() )
740 break;
741
742 long long tileCount = 0;
743 mMetaTiles += getMetatiles( mZ0matrix, mTileGenerationRegion, z, tileCount, mMetaTileSize, mMaxZoomRegions, mMaxZoom );
744 feedback->pushInfo( QObject::tr( "%1 metatiles (%2 tiles) will be created for zoom level %3" ).arg( mMetaTiles.size() - mTotalMetaTiles ).arg( tileCount ).arg( z ) );
745 mTotalMetaTiles = mMetaTiles.size();
746 totalTiles += tileCount;
747 }
748 if ( mTotalMetaTiles == 0 )
749 {
750 throw QgsProcessingException( QObject::tr( "No metatiles will be created -- please check the extent and zoom limits" ) );
751 }
752
753 feedback->pushInfo( QObject::tr( "A total of %1 metatiles (%2 tiles) will be created" ).arg( mTotalMetaTiles ).arg( totalTiles ) );
754
755 checkLayersUsagePolicy( feedback );
756
757 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
758 {
759 layer->moveToThread( QThread::currentThread() );
760 }
761
762 doExport( feedback );
763
764 qDeleteAll( mLayers );
765 mLayers.clear();
766
767 if ( mSkipEmptyTiles )
768 {
769 feedback->pushInfo( QObject::tr( "Wrote %1 total tiles, skipped %2 empty tiles" ).arg( mTilesWritten.load() ).arg( mEmptyTiles.load() ) );
770 }
771
772 QVariantMap results;
773 results.insert( u"OUTPUT_DIRECTORY"_s, outputDir );
774
775 if ( !outputHtml.isEmpty() )
776 {
777 const QString osm = QStringLiteral(
778 "var osm_layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png',"
779 "{minZoom: %1, maxZoom: %2, attribution: '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors'}).addTo(map);"
780 )
781 .arg( mMinZoom )
782 .arg( mMaxZoomLimitIncludingOverrides );
783
784 const QString addOsm = useOsm ? osm : QString();
785 const QString tmsConvention = tms ? u"true"_s : u"false"_s;
786 const QString attr = attribution.isEmpty() ? u"Created by QGIS"_s : attribution;
787 const QString tileSource = u"'file:///%1/{z}/{x}/{y}.%2'"_s.arg( outputDir.replace( "\\", "/" ).toHtmlEscaped(), mTileFormat.toLower() );
788
789 const QString html = QStringLiteral(
790 "<!DOCTYPE html><html><head><title>%1</title><meta charset=\"utf-8\"/>"
791 "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
792 "<link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css\""
793 "integrity=\"sha384-sHL9NAb7lN7rfvG5lfHpm643Xkcjzp4jFvuavGOndn6pjVqS6ny56CAt3nsEVT4H\""
794 "crossorigin=\"\"/>"
795 "<script src=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js\""
796 "integrity=\"sha384-cxOPjt7s7Iz04uaHJceBmS+qpjv2JkIHNVcuOrM+YHwZOmJGBXI00mdUXEq65HTH\""
797 "crossorigin=\"\"></script>"
798 "<style type=\"text/css\">body {margin: 0;padding: 0;} html, body, #map{width: 100%;height: 100%;}</style></head>"
799 "<body><div id=\"map\"></div><script>"
800 "var map = L.map('map', {attributionControl: false}).setView([%2, %3], %4);"
801 "L.control.attribution({prefix: false}).addTo(map);"
802 "%5"
803 "var tilesource_layer = L.tileLayer(%6, {minZoom: %7, maxZoom: %8, tms: %9, attribution: '%10'}).addTo(map);"
804 "</script></body></html>"
805 )
806 .arg( title.isEmpty() ? u"Leaflet preview"_s : title )
807 .arg( mWgs84Extent.center().y() )
808 .arg( mWgs84Extent.center().x() )
809 .arg( ( mMaxZoom + mMinZoom ) / 2 )
810 .arg( addOsm, tileSource )
811 .arg( mMinZoom )
812 .arg( mMaxZoomLimitIncludingOverrides )
813 .arg( tmsConvention, attr );
814
815 QFile htmlFile( outputHtml );
816 if ( !htmlFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
817 {
818 throw QgsProcessingException( QObject::tr( "Could not open html file %1" ).arg( outputHtml ) );
819 }
820 QTextStream fout( &htmlFile );
821 fout << html;
822
823 results.insert( u"OUTPUT_HTML"_s, outputHtml );
824 }
825
826 // try to load the result as a raster layer
827 if ( !feedback->isCanceled() )
828 {
829 const QString layerUri
830 = u"type=xyz&url=file:///%1/%7Bz%7D/%7Bx%7D/%7By%7D.%2&zmax=%3&zmin=%4"_s.arg( outputDir.replace( "\\", "/" ).toHtmlEscaped(), mTileFormat.toLower() ).arg( mMaxZoom ).arg( mMinZoom );
831 auto layer = std::make_unique<QgsRasterLayer>( layerUri, "OUTPUT_LAYER", u"wms"_s );
832 if ( !layer->isValid() )
833 {
834 feedback->reportError( QObject::tr( "Failed to open XYZ directory as a raster layer" ) );
835 }
836 const QString layerId = layer->id();
837 const QgsProcessingContext::LayerDetails details( layer->name(), context.project(), u"OUTPUT_LAYER"_s, QgsProcessingUtils::LayerHint::Raster );
838 details.setOutputLayerName( layer.get() );
839 context.addLayerToLoadOnCompletion( layerId, details );
840 context.temporaryLayerStore()->addMapLayer( layer.release() );
841 results.insert( u"OUTPUT_LAYER"_s, layerId );
842 }
843
844 return results;
845}
846
847void QgsXyzTilesDirectoryAlgorithm::processMetaTile( const MetaTile &metaTile, const QImage &renderedImage, QgsProcessingFeedback *feedback )
848{
849 mActivePostProcessingTasks++;
850
851 mPostProcessingPool->start( [this, metaTile, renderedImage, feedback]() {
852 const bool testEmptyTilesUsingAlpha0 = mTileFormat != "JPG"_L1 && mBackgroundColor.alpha() == 0;
853 long long localWritten = 0;
854 long long localEmpty = 0;
855
856 QSet<QString> createdDirs;
857
858 for ( auto it = metaTile.tiles.constBegin(); it != metaTile.tiles.constEnd(); ++it )
859 {
860 if ( feedback->isCanceled() )
861 break;
862
863 const QPair<int, int> tm = it.key();
864 const QImage tileImage = renderedImage.copy( mTileWidth * tm.first, mTileHeight * tm.second, mTileWidth, mTileHeight );
865 const bool skipTile = mSkipEmptyTiles && ( testEmptyTilesUsingAlpha0 ? QgsImageOperation::isBlankImage( tileImage ) : QgsImageOperation::isSingleColor( tileImage, mBackgroundColor ) );
866 if ( skipTile )
867 {
868 localEmpty++;
869 continue;
870 }
871
872 const Tile tile = it.value();
873 const QString dirPath = u"%1/%2/%3"_s.arg( mOutputDir ).arg( tile.z ).arg( tile.x );
874 if ( !createdDirs.contains( dirPath ) )
875 {
876 QDir().mkpath( dirPath );
877 createdDirs.insert( dirPath );
878 }
879
880 const int y = mTms ? tile2tms( tile.y, tile.z ) : tile.y;
881 const QString filePath = u"%1/%2.%3"_s.arg( dirPath ).arg( y ).arg( mTileFormat.toLower() );
882 tileImage.save( filePath, mTileFormat.toStdString().c_str(), mJpgQuality );
883
884 localWritten++;
885 }
886
887 mTilesWritten += localWritten;
888 mEmptyTiles += localEmpty;
889
890 mActivePostProcessingTasks--;
891 checkPipelineFinished( feedback );
892 } );
893}
894
895void QgsXyzTilesDirectoryAlgorithm::doExport( QgsProcessingFeedback *feedback )
896{
897 mPostProcessingPool = std::make_unique<QThreadPool>();
898 mPostProcessingPool->setMaxThreadCount( std::max( 1, mThreadsNumber ) );
899
900 QEventLoop loop;
901 mEventLoop = &loop;
902 mJobOwner.reset( new QObject() );
903
904 startJobs( feedback );
905
906 if ( !mMetaTiles.isEmpty() || !mRendererJobs.isEmpty() || mActivePostProcessingTasks.load() > 0 )
907 {
908 loop.exec();
909 }
910
911 for ( auto it = mRendererJobs.constBegin(); it != mRendererJobs.constEnd(); it++ )
912 {
913 it.key()->cancel();
914 it.key()->deleteLater();
915 }
916 mRendererJobs.clear();
917
918 mPostProcessingPool->waitForDone();
919 mPostProcessingPool.reset();
920 mEventLoop = nullptr;
921}
922
923
924//
925// QgsXyzTilesMbtilesAlgorithm
926//
927
928QString QgsXyzTilesMbtilesAlgorithm::name() const
929{
930 return u"tilesxyzmbtiles"_s;
931}
932
933QString QgsXyzTilesMbtilesAlgorithm::displayName() const
934{
935 return QObject::tr( "Generate XYZ tiles (MBTiles)" );
936}
937
938QStringList QgsXyzTilesMbtilesAlgorithm::tags() const
939{
940 return QObject::tr( "tiles,xyz,tms,mbtiles" ).split( ',' );
941}
942
943QString QgsXyzTilesMbtilesAlgorithm::shortHelpString() const
944{
945 return QObject::tr(
946 "This algorithm generates XYZ raster tiles from the current project and packages them into a single, portable MBTiles (SQLite) database file.\n\n"
947 "All visible map layers from the project will be rendered into tiles across the specified extent and zoom range."
948 );
949}
950
951QString QgsXyzTilesMbtilesAlgorithm::shortDescription() const
952{
953 return QObject::tr( "Generates XYZ tiles from the project as a single, portable MBTiles (SQLite) database." );
954}
955
956QgsXyzTilesMbtilesAlgorithm *QgsXyzTilesMbtilesAlgorithm::createInstance() const
957{
958 return new QgsXyzTilesMbtilesAlgorithm();
959}
960
961void QgsXyzTilesMbtilesAlgorithm::initAlgorithm( const QVariantMap & )
962{
963 createCommonParameters();
964 addParameter( new QgsProcessingParameterFileDestination( u"OUTPUT_FILE"_s, QObject::tr( "Output" ), QObject::tr( "MBTiles files (*.mbtiles *.MBTILES)" ) ) );
965
966 addOutput( new QgsProcessingOutputRasterLayer( u"OUTPUT_LAYER"_s, QObject::tr( "Output MBTiles raster layer" ) ) );
967}
968
969QVariantMap QgsXyzTilesMbtilesAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
970{
971 QGS_MARK_ALGORITHM_SOURCE
972
973 const QString outputFile = parameterAsString( parameters, u"OUTPUT_FILE"_s, context );
974 if ( QFile::exists( outputFile ) )
975 {
976 feedback->pushWarning( QObject::tr( "Removing existing file '%1'" ).arg( QDir::toNativeSeparators( outputFile ) ) );
977 if ( !QFile( outputFile ).remove() )
978 {
979 throw QgsProcessingException( QObject::tr( "Could not remove existing file '%1'" ).arg( QDir::toNativeSeparators( outputFile ) ) );
980 }
981 }
982
983 mMbtilesWriter = std::make_unique<QgsMbTiles>( outputFile );
984 // use deferred index creation, as we'll be writing 1000s of tiles and don't want to update
985 // the index after every one
986 if ( !mMbtilesWriter->create( true ) )
987 {
988 throw QgsProcessingException( QObject::tr( "Failed to create MBTiles file %1: %2" ).arg( outputFile, mMbtilesWriter->lastError() ) );
989 }
990 mMbtilesWriter->setMetadataValue( u"format"_s, mTileFormat.toLower() );
991 mMbtilesWriter->setMetadataValue( u"name"_s, QFileInfo( outputFile ).baseName() );
992 mMbtilesWriter->setMetadataValue( u"description"_s, QFileInfo( outputFile ).baseName() );
993 mMbtilesWriter->setMetadataValue( u"version"_s, u"1.1"_s );
994 mMbtilesWriter->setMetadataValue( u"type"_s, u"overlay"_s );
995 mMbtilesWriter->setMetadataValue( u"minzoom"_s, QString::number( mMinZoom ) );
996 mMbtilesWriter->setMetadataValue( u"maxzoom"_s, QString::number( mMaxZoomLimitIncludingOverrides ) );
997 QString boundsStr = QString( u"%1,%2,%3,%4"_s ).arg( mWgs84Extent.xMinimum() ).arg( mWgs84Extent.yMinimum() ).arg( mWgs84Extent.xMaximum() ).arg( mWgs84Extent.yMaximum() );
998 mMbtilesWriter->setMetadataValue( u"bounds"_s, boundsStr );
999
1000 long long totalTiles = 0;
1001 mTotalMetaTiles = 0;
1002 for ( int z = mMinZoom; z <= mMaxZoomLimitIncludingOverrides; z++ )
1003 {
1004 if ( feedback->isCanceled() )
1005 break;
1006
1007 long long tileCount = 0;
1008 mMetaTiles += getMetatiles( mZ0matrix, mTileGenerationRegion, z, tileCount, mMetaTileSize, mMaxZoomRegions, mMaxZoom );
1009 feedback->pushInfo( QObject::tr( "%1 metatiles (%2 tiles) will be created for zoom level %3" ).arg( mMetaTiles.size() - mTotalMetaTiles ).arg( tileCount ).arg( z ) );
1010 mTotalMetaTiles = mMetaTiles.size();
1011 totalTiles += tileCount;
1012 }
1013 if ( mTotalMetaTiles == 0 )
1014 {
1015 throw QgsProcessingException( QObject::tr( "No metatiles will be created -- please check the extent and zoom limits" ) );
1016 }
1017
1018 feedback->pushInfo( QObject::tr( "A total of %1 metatiles (%2 tiles) will be created" ).arg( mTotalMetaTiles ).arg( totalTiles ) );
1019
1020 checkLayersUsagePolicy( feedback );
1021
1022 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
1023 {
1024 layer->moveToThread( QThread::currentThread() );
1025 }
1026
1027 doExport( feedback );
1028
1029 qDeleteAll( mLayers );
1030 mLayers.clear();
1031
1032 if ( !feedback->isCanceled() )
1033 {
1034 mMbtilesWriter->finalize();
1035 }
1036
1037 if ( mSkipEmptyTiles )
1038 {
1039 feedback->pushInfo( QObject::tr( "Wrote %1 total tiles, skipped %2 empty tiles" ).arg( mTilesWritten.load() ).arg( mEmptyTiles.load() ) );
1040 }
1041 QVariantMap results;
1042 results.insert( u"OUTPUT_FILE"_s, outputFile );
1043
1044 // try to load the result as a raster layer
1045 if ( !feedback->isCanceled() )
1046 {
1047 auto layer = std::make_unique<QgsRasterLayer>( outputFile, "OUTPUT_LAYER", u"gdal"_s );
1048 if ( !layer->isValid() )
1049 {
1050 feedback->reportError( QObject::tr( "Failed to open MBTiles file as a raster layer" ) );
1051 }
1052 const QString layerId = layer->id();
1053 const QgsProcessingContext::LayerDetails details( layer->name(), context.project(), u"OUTPUT_LAYER"_s, QgsProcessingUtils::LayerHint::Raster );
1054 details.setOutputLayerName( layer.get() );
1055 context.addLayerToLoadOnCompletion( layerId, details );
1056 context.temporaryLayerStore()->addMapLayer( layer.release() );
1057 results.insert( u"OUTPUT_LAYER"_s, layerId );
1058 }
1059
1060 return results;
1061}
1062
1063void QgsXyzTilesMbtilesAlgorithm::processMetaTile( const MetaTile &metaTile, const QImage &renderedImg, QgsProcessingFeedback *feedback )
1064{
1065 mActivePostProcessingTasks++;
1066
1067 mPostProcessingPool->start( [this, feedback, metaTile, renderedImg]() {
1068 const bool testEmptyTilesUsingAlpha0 = mTileFormat != "JPG"_L1 && mBackgroundColor.alpha() == 0;
1069 long long localWritten = 0;
1070 long long localEmpty = 0;
1071
1072 QList<QgsMbTiles::TileData> metatileTiles;
1073 metatileTiles.reserve( metaTile.tiles.size() );
1074
1075 for ( auto it = metaTile.tiles.constBegin(); it != metaTile.tiles.constEnd(); ++it )
1076 {
1077 if ( feedback->isCanceled() )
1078 break;
1079
1080 const QPair<int, int> tm = it.key();
1081
1082 const QImage tileImage = renderedImg.copy( mTileWidth * tm.first, mTileHeight * tm.second, mTileWidth, mTileHeight );
1083
1084 const bool skipTile = mSkipEmptyTiles && ( testEmptyTilesUsingAlpha0 ? QgsImageOperation::isBlankImage( tileImage ) : QgsImageOperation::isSingleColor( tileImage, mBackgroundColor ) );
1085 if ( skipTile )
1086 {
1087 localEmpty++;
1088 continue;
1089 }
1090
1091 QByteArray bytes;
1092 QBuffer buffer( &bytes );
1093 buffer.open( QIODevice::WriteOnly );
1094 tileImage.save( &buffer, mTileFormat.toStdString().c_str(), mJpgQuality );
1095
1096 const Tile tile = it.value();
1097 const int tileY = tile2tms( tile.y, tile.z );
1098 metatileTiles.append( { tile.z, tile.x, tileY, bytes } );
1099 localWritten++;
1100 }
1101
1102 mWriteQueue->push( metatileTiles );
1103
1104 mTilesWritten += localWritten;
1105 mEmptyTiles += localEmpty;
1106
1107 mActivePostProcessingTasks--;
1108 checkPipelineFinished( feedback );
1109 } );
1110}
1111
1112void QgsXyzTilesMbtilesAlgorithm::doExport( QgsProcessingFeedback *feedback )
1113{
1114 mPostProcessingPool = std::make_unique<QThreadPool>();
1115 mPostProcessingPool->setMaxThreadCount( std::max( 1, mThreadsNumber ) );
1116
1117 PendingTilesToWriteQueue< QgsMbTiles::TileData > queue;
1118 mWriteQueue = &queue;
1119
1120 QThread *dbThread = QThread::create( [this, &queue]() {
1121 QList<QgsMbTiles::TileData> batch;
1122 constexpr int BATCH_SIZE = 10000;
1123 batch.reserve( BATCH_SIZE );
1124 while ( queue.popBatch( batch, BATCH_SIZE ) )
1125 {
1126 mMbtilesWriter->setTileData( batch );
1127 batch.clear();
1128 }
1129 } );
1130 dbThread->start();
1131
1132 QEventLoop loop;
1133 mEventLoop = &loop;
1134 mJobOwner.reset( new QObject() );
1135
1136 startJobs( feedback );
1137
1138 if ( !mMetaTiles.isEmpty() || !mRendererJobs.isEmpty() || mActivePostProcessingTasks.load() > 0 )
1139 {
1140 loop.exec();
1141 }
1142
1143 for ( auto it = mRendererJobs.constBegin(); it != mRendererJobs.constEnd(); it++ )
1144 {
1145 it.key()->cancel();
1146 it.key()->deleteLater();
1147 }
1148 mRendererJobs.clear();
1149
1150 mPostProcessingPool->waitForDone();
1151 mPostProcessingPool.reset();
1152
1153 queue.setFinished();
1154 dbThread->wait();
1155 delete dbThread;
1156
1157 mWriteQueue = nullptr;
1158 mEventLoop = nullptr;
1159}
1160
1161
1162//
1163// QgsXyzTilesGpkgAlgorithm
1164//
1165
1166QString QgsXyzTilesGpkgAlgorithm::name() const
1167{
1168 return u"tilesxyzgpkg"_s;
1169}
1170
1171QString QgsXyzTilesGpkgAlgorithm::displayName() const
1172{
1173 return QObject::tr( "Generate XYZ tiles (GeoPackage)" );
1174}
1175
1176QStringList QgsXyzTilesGpkgAlgorithm::tags() const
1177{
1178 return QObject::tr( "tiles,xyz,geopackage,gpkg,raster" ).split( ',' );
1179}
1180
1181QString QgsXyzTilesGpkgAlgorithm::shortDescription() const
1182{
1183 return QObject::tr( "Renders maps to XYZ raster tiles into a GeoPackage raster tile database." );
1184}
1185
1186QString QgsXyzTilesGpkgAlgorithm::shortHelpString() const
1187{
1188 return QObject::tr(
1189 "This algorithm generates XYZ raster tiles from the current project and saves them into an OGC GeoPackage file.\n\n"
1190 "All visible map layers from the project will be rendered into tiles across the specified extent and zoom range."
1191 );
1192}
1193
1194QgsXyzTilesGpkgAlgorithm *QgsXyzTilesGpkgAlgorithm::createInstance() const
1195{
1196 return new QgsXyzTilesGpkgAlgorithm();
1197}
1198
1199void QgsXyzTilesGpkgAlgorithm::initAlgorithm( const QVariantMap & )
1200{
1201 createCommonParameters();
1202 createTileMatrixParameters();
1203
1204 auto tileWidthParam = std::make_unique<QgsProcessingParameterNumber>( u"TILE_WIDTH"_s, QObject::tr( "Tile width" ), Qgis::ProcessingNumberParameterType::Integer, 256, false, 1, 4096 );
1205 tileWidthParam->setHelp( QObject::tr( "Width of each tile image in pixels." ) );
1206 addParameter( tileWidthParam.release() );
1207
1208 auto tileHeightParam = std::make_unique<QgsProcessingParameterNumber>( u"TILE_HEIGHT"_s, QObject::tr( "Tile height" ), Qgis::ProcessingNumberParameterType::Integer, 256, false, 1, 4096 );
1209 tileHeightParam->setHelp( QObject::tr( "Height of each tile image in pixels." ) );
1210 addParameter( tileHeightParam.release() );
1211
1212 addParameter( new QgsProcessingParameterFileDestination( u"OUTPUT_FILE"_s, QObject::tr( "Output file" ), QObject::tr( "GeoPackage files (*.gpkg *.GPKG)" ) ) );
1213
1214 addOutput( new QgsProcessingOutputRasterLayer( u"OUTPUT_LAYER"_s, QObject::tr( "Output tiles as raster layer" ) ) );
1215}
1216
1217QVariantMap QgsXyzTilesGpkgAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
1218{
1219 QGS_MARK_ALGORITHM_SOURCE
1220
1221 const QString outputFile = parameterAsString( parameters, u"OUTPUT_FILE"_s, context );
1222
1223 if ( QFile::exists( outputFile ) )
1224 {
1225 feedback->pushWarning( QObject::tr( "Removing existing file '%1'" ).arg( QDir::toNativeSeparators( outputFile ) ) );
1226 if ( !QFile( outputFile ).remove() )
1227 {
1228 throw QgsProcessingException( QObject::tr( "Could not remove existing file '%1'" ).arg( QDir::toNativeSeparators( outputFile ) ) );
1229 }
1230 }
1231
1232 mGpkgWriter = std::make_unique<QgsGeoPackageTiles>( outputFile );
1233
1234 if ( !mGpkgWriter->create( mZ0matrix, mTileGenerationRegion, mMinZoom, mMaxZoomLimitIncludingOverrides, mTileWidth, mTileHeight ) )
1235 {
1236 throw QgsProcessingException( QObject::tr( "Failed to create GeoPackage file %1: %2" ).arg( outputFile, mGpkgWriter->lastError() ) );
1237 }
1238
1239 long long totalTiles = 0;
1240 mTotalMetaTiles = 0;
1241 for ( int z = mMinZoom; z <= mMaxZoomLimitIncludingOverrides; z++ )
1242 {
1243 if ( feedback->isCanceled() )
1244 break;
1245
1246 long long tileCount = 0;
1247 mMetaTiles += getMetatiles( mZ0matrix, mTileGenerationRegion, z, tileCount, mMetaTileSize, mMaxZoomRegions, mMaxZoom );
1248 feedback->pushInfo( QObject::tr( "%1 metatiles (%2 tiles) will be created for zoom level %3" ).arg( mMetaTiles.size() - mTotalMetaTiles ).arg( tileCount ).arg( z ) );
1249 mTotalMetaTiles = mMetaTiles.size();
1250 totalTiles += tileCount;
1251 }
1252 if ( mTotalMetaTiles == 0 )
1253 {
1254 throw QgsProcessingException( QObject::tr( "No metatiles will be created -- please check the extent and zoom limits" ) );
1255 }
1256 feedback->pushInfo( QObject::tr( "A total of %1 metatiles (%2 tiles) will be created" ).arg( mTotalMetaTiles ).arg( totalTiles ) );
1257
1258 checkLayersUsagePolicy( feedback );
1259
1260 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
1261 {
1262 layer->moveToThread( QThread::currentThread() );
1263 }
1264 mJobOwner.reset( new QObject() );
1265
1266 doExport( feedback );
1267
1268 qDeleteAll( mLayers );
1269 mLayers.clear();
1270
1271 if ( !feedback->isCanceled() )
1272 {
1273 mGpkgWriter->finalize();
1274 }
1275
1276 mGpkgWriter->close();
1277
1278 if ( mSkipEmptyTiles )
1279 {
1280 feedback->pushInfo( QObject::tr( "Wrote %1 total tiles, skipped %2 empty tiles" ).arg( mTilesWritten.load() ).arg( mEmptyTiles.load() ) );
1281 }
1282
1283 QVariantMap results;
1284
1285 // try to load the result as a raster layer
1286 if ( !feedback->isCanceled() )
1287 {
1288 auto layer = std::make_unique<QgsRasterLayer>( outputFile, "OUTPUT_LAYER", u"gdal"_s );
1289 if ( !layer->isValid() )
1290 {
1291 feedback->reportError( QObject::tr( "Failed to open GeoPackage file as a raster layer" ) );
1292 }
1293 const QString layerId = layer->id();
1294 const QgsProcessingContext::LayerDetails details( layer->name(), context.project(), u"OUTPUT_LAYER"_s, QgsProcessingUtils::LayerHint::Raster );
1295 details.setOutputLayerName( layer.get() );
1296 context.addLayerToLoadOnCompletion( layerId, details );
1297 context.temporaryLayerStore()->addMapLayer( layer.release() );
1298 results.insert( u"OUTPUT_LAYER"_s, layerId );
1299 }
1300
1301 results.insert( u"OUTPUT_FILE"_s, outputFile );
1302 return results;
1303}
1304
1305void QgsXyzTilesGpkgAlgorithm::processMetaTile( const MetaTile &metaTile, const QImage &renderedImg, QgsProcessingFeedback *feedback )
1306{
1307 mActivePostProcessingTasks++;
1308 mPostProcessingPool->start( [this, feedback, metaTile, renderedImg]() {
1309 const bool testEmptyTilesUsingAlpha0 = mTileFormat != "JPG"_L1 && mBackgroundColor.alpha() == 0;
1310 long long localWritten = 0;
1311 long long localEmpty = 0;
1312
1313 QList<QgsGeoPackageTiles::TileData> metatileTiles;
1314 metatileTiles.reserve( metaTile.tiles.size() );
1315
1316 for ( auto it = metaTile.tiles.constBegin(); it != metaTile.tiles.constEnd(); ++it )
1317 {
1318 if ( feedback->isCanceled() )
1319 break;
1320
1321 const QPair<int, int> tm = it.key();
1322
1323 const QImage tileImage = renderedImg.copy( mTileWidth * tm.first, mTileHeight * tm.second, mTileWidth, mTileHeight );
1324
1325 const bool skipTile = mSkipEmptyTiles && ( testEmptyTilesUsingAlpha0 ? QgsImageOperation::isBlankImage( tileImage ) : QgsImageOperation::isSingleColor( tileImage, mBackgroundColor ) );
1326 if ( skipTile )
1327 {
1328 localEmpty++;
1329 continue;
1330 }
1331
1332 QByteArray bytes;
1333 QBuffer buffer( &bytes );
1334 buffer.open( QIODevice::WriteOnly );
1335 tileImage.save( &buffer, mTileFormat.toStdString().c_str(), mJpgQuality );
1336
1337 const Tile tile = it.value();
1338 // GeoPackage uses tile.y directly (Top-Left origin)
1339 metatileTiles.append( { tile.z, tile.x, tile.y, bytes } );
1340 localWritten++;
1341 }
1342
1343 mWriteQueue->push( metatileTiles );
1344
1345 mTilesWritten += localWritten;
1346 mEmptyTiles += localEmpty;
1347
1348 mActivePostProcessingTasks--;
1349 checkPipelineFinished( feedback );
1350 } );
1351}
1352
1353void QgsXyzTilesGpkgAlgorithm::doExport( QgsProcessingFeedback *feedback )
1354{
1355 mPostProcessingPool = std::make_unique<QThreadPool>();
1356 mPostProcessingPool->setMaxThreadCount( std::max( 1, mThreadsNumber ) );
1357
1358 PendingTilesToWriteQueue<QgsGeoPackageTiles::TileData> queue;
1359 mWriteQueue = &queue;
1360
1361 QThread *dbThread = QThread::create( [this, &queue]() {
1362 QList<QgsGeoPackageTiles::TileData> batch;
1363 constexpr int BATCH_SIZE = 10000;
1364 batch.reserve( BATCH_SIZE );
1365 while ( queue.popBatch( batch, BATCH_SIZE ) )
1366 {
1367 mGpkgWriter->setTileData( batch );
1368 batch.clear();
1369 }
1370 } );
1371 dbThread->start();
1372
1373 QEventLoop loop;
1374 mEventLoop = &loop;
1375 mJobOwner.reset( new QObject() );
1376
1377 startJobs( feedback );
1378
1379 if ( !mMetaTiles.isEmpty() || !mRendererJobs.isEmpty() || mActivePostProcessingTasks.load() > 0 )
1380 {
1381 loop.exec();
1382 }
1383
1384 for ( auto *j : mRendererJobs.keys() )
1385 {
1386 j->cancel();
1387 j->deleteLater();
1388 }
1389 mRendererJobs.clear();
1390
1391 mPostProcessingPool->waitForDone();
1392 mPostProcessingPool.reset();
1393
1394 queue.setFinished();
1395 dbThread->wait();
1396 delete dbThread;
1397
1398 mWriteQueue = nullptr;
1399 mEventLoop = nullptr;
1400}
1401
1402
@ Default
Allow raster-based rendering in situations where it is required for correct rendering or where it wil...
Definition qgis.h:2897
@ UsePartialCandidates
Whether to use also label candidates that are partially outside of the map view.
Definition qgis.h:3044
QFlags< ProcessingAlgorithmFlag > ProcessingAlgorithmFlags
Flags indicating how and when an algorithm operates and should be exposed to users.
Definition qgis.h:3828
@ Export
Renderer used for printing or exporting to a file.
Definition qgis.h:3665
@ RequiresProject
The algorithm requires that a valid QgsProject is available from the processing context in order to e...
Definition qgis.h:3815
@ Advanced
Parameter is an advanced parameter which should be hidden from users by default.
Definition qgis.h:4006
@ Optional
Parameter is optional.
Definition qgis.h:4008
@ RenderMapTile
Draw map such that there are no problems between adjacent tiles.
Definition qgis.h:2918
@ UseRenderingOptimization
Enable vector simplification and other rendering optimizations.
Definition qgis.h:2915
@ Antialiasing
Enable anti-aliasing for map rendering.
Definition qgis.h:2910
@ HighQualityImageTransforms
Enable high quality image transformations, which results in better appearance of scaled or rotated ra...
Definition qgis.h:2925
Represents a coordinate reference system (CRS).
Handles coordinate transforms between two coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const
Transforms a rectangle from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
static QgsExpressionContextScope * mapSettingsScope(const QgsMapSettings &mapSettings)
Creates a new scope which contains variables and functions relating to a QgsMapSettings object.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:56
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:65
Contains operations and filters which apply to QImages.
static bool isBlankImage(const QImage &image)
Tests whether an image is completely blank, i.e.
Stores global configuration for labeling engine.
void setFlag(Qgis::LabelingFlag f, bool enabled=true)
Sets whether a particual flag is enabled.
QList< QgsLayerTreeLayer * > findLayers() const
Find all layer nodes.
Layer tree node points to a map layer.
QList< QgsMapLayer * > layerOrder() const
The order in which layers will be rendered on the canvas.
QgsMapLayer * addMapLayer(QgsMapLayer *layer, bool takeOwnership=true)
Add a layer to the store.
static bool isOpenStreetMapLayer(QgsMapLayer *layer)
Returns true if the layer is served by OpenStreetMap server.
Base class for all map layer types.
Definition qgsmaplayer.h:83
virtual QgsMapLayer * clone() const =0
Returns a new instance equivalent to this one except for the id which is still unique.
void finished()
emitted when asynchronous rendering is finished (or canceled).
void start()
Start the rendering job and immediately return.
Job implementation that renders everything sequentially in one thread.
QImage renderedImage() override
Gets a preview/resulting image.
Contains configuration for rendering maps.
const QgsLabelingEngineSettings & labelingEngineSettings() const
Returns the global configuration of the labeling engine.
void setLayers(const QList< QgsMapLayer * > &layers)
Sets the list of layers to render in the map.
bool setEllipsoid(const QString &ellipsoid)
Sets the ellipsoid by its acronym.
void setScaleMethod(Qgis::ScaleCalculationMethod method)
Sets the method to use for scale calculations for the map.
void setOutputDpi(double dpi)
Sets the dpi (dots per inch) used for conversion between real world units (e.g.
void setOutputImageFormat(QImage::Format format)
sets format of internal QImage
void setRendererUsage(Qgis::RendererUsage rendererUsage)
Sets the rendering usage.
void setRasterizedRenderingPolicy(Qgis::RasterizedRenderingPolicy policy)
Sets the policy controlling when rasterisation of content during renders is permitted.
void setExtent(const QgsRectangle &rect, bool magnified=true)
Sets the coordinates of the rectangle which should be rendered.
void setExpressionContext(const QgsExpressionContext &context)
Sets the expression context.
void setLabelingEngineSettings(const QgsLabelingEngineSettings &settings)
Sets the global configuration of the labeling engine.
void setTransformContext(const QgsCoordinateTransformContext &context)
Sets the coordinate transform context, which stores various information regarding which datum transfo...
void setOutputSize(QSize size)
Sets the size of the resulting map image, in pixels.
void setBackgroundColor(const QColor &color)
Sets the background color of the map.
void setFlag(Qgis::MapSettingsFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected).
void setDestinationCrs(const QgsCoordinateReferenceSystem &crs)
Sets the destination crs (coordinate reference system) for the map render.
Represents a 2D point.
Definition qgspointxy.h:62
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
virtual Qgis::ProcessingAlgorithmFlags flags() const
Returns the flags indicating how and when the algorithm operates and should be exposed to users.
Details for layers to load into projects.
Contains information about the context in which a processing algorithm is executed.
QgsExpressionContext & expressionContext()
Returns the expression context.
QgsCoordinateTransformContext transformContext() const
Returns the coordinate transform context.
void addLayerToLoadOnCompletion(const QString &layer, const QgsProcessingContext::LayerDetails &details)
Adds a layer to load (by ID or datasource) into the canvas upon completion of the algorithm or model.
QgsProject * project() const
Returns the project in which the algorithm is being executed.
QString ellipsoid() const
Returns the ellipsoid to use for distance and area calculations.
QgsMapLayerStore * temporaryLayerStore()
Returns a reference to the layer store used for storing temporary layers during algorithm execution.
int maximumThreads() const
Returns the (optional) number of threads to use when running algorithms.
Custom exception class for processing related exceptions.
Base class for providing feedback from a processing algorithm.
virtual void pushInfo(const QString &info)
Pushes a general informational message from the algorithm.
virtual void pushWarning(const QString &warning)
Pushes a warning informational message from the algorithm.
virtual void pushFormattedMessage(const QString &html, const QString &text)
Pushes a pre-formatted message from the algorithm.
virtual void reportError(const QString &error, bool fatalError=false)
Reports that the algorithm encountered an error while executing.
A raster layer output for processing algorithms.
A generic file based destination parameter, for specifying the destination path for a file (non-map l...
static int maxZoomForTile(const QgsRectangle &tileExtent, const QList< QgsTileExtentMaxZoomRegion > &regions, int defaultMaxZoom)
Evaluates the effective maximum zoom level for a given tileExtent.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
Definition qgsproject.h:114
QgsLayerTree * layerTreeRoot() const
Returns pointer to the root (invisible) node of the project's layer tree.
Qgis::ScaleCalculationMethod scaleMethod
Definition qgsproject.h:136
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
QgsPointXY center
A QgsRectangle with associated coordinate reference system.
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition qgstiles.h:171
QgsRectangle extent() const
Returns extent of the tile matrix.
Definition qgstiles.h:243
int matrixWidth() const
Returns number of columns of the tile matrix.
Definition qgstiles.h:217
int matrixHeight() const
Returns number of rows of the tile matrix.
Definition qgstiles.h:220
static QgsTileMatrix fromTileMatrix(int zoomLevel, const QgsTileMatrix &tileMatrix)
Returns a tile matrix based on another one.
Definition qgstiles.cpp:64
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:35
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7693
#define MAXIMUM_OPENSTREETMAP_TILES_FETCH
Represents a spatial region paired with a custom maximum zoom level.
QgsReferencedRectangle extent
Bounding box extent and associated CRS.