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