QGIS API Documentation 3.43.0-Master (2366440f66a)
qgsvectortilelayerrenderer.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsvectortilelayerrenderer.cpp
3 --------------------------------------
4 Date : March 2020
5 Copyright : (C) 2020 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
18#include "qgsfeedback.h"
19#include "qgslogger.h"
21#include "qgsvectortilelayer.h"
22#include "qgsvectortileloader.h"
23#include "qgsvectortileutils.h"
24#include "qgslabelingengine.h"
26#include "qgsmapclippingutils.h"
27#include "qgsrendercontext.h"
29#include "qgstextrenderer.h"
30#include "qgsruntimeprofiler.h"
31#include "qgsapplication.h"
32
33#include <QElapsedTimer>
34#include <QThread>
35
37 : QgsMapLayerRenderer( layer->id(), &context )
38 , mLayerName( layer->name() )
39 , mDataProvider( qgis::down_cast< const QgsVectorTileDataProvider* >( layer->dataProvider() )->clone() )
40 , mRenderer( layer->renderer()->clone() )
41 , mLayerBlendMode( layer->blendMode() )
42 , mDrawTileBoundaries( layer->isTileBorderRenderingEnabled() )
43 , mLabelsEnabled( layer->labelsEnabled() )
44 , mFeedback( new QgsFeedback )
45 , mSelectedFeatures( layer->selectedFeatures() )
46 , mLayerOpacity( layer->opacity() )
47 , mTileMatrixSet( layer->tileMatrixSet() )
48 , mEnableProfile( context.flags() & Qgis::RenderContextFlag::RecordProfile )
49{
50 QElapsedTimer timer;
51 timer.start();
52
53 if ( QgsLabelingEngine *engine = context.labelingEngine() )
54 {
55 if ( layer->labelsEnabled() )
56 {
57 mLabelProvider = layer->labeling()->provider( layer );
58 if ( mLabelProvider )
59 {
60 engine->addProvider( mLabelProvider );
61 }
62 }
63 }
64
66
67 mDataProvider->moveToThread( nullptr );
68
69 mPreparationTime = timer.elapsed();
70}
71
73
75{
76 std::unique_ptr< QgsScopedRuntimeProfile > profile;
77 if ( mEnableProfile )
78 {
79 profile = std::make_unique< QgsScopedRuntimeProfile >( mLayerName, QStringLiteral( "rendering" ), layerId() );
80 if ( mPreparationTime > 0 )
81 QgsApplication::profiler()->record( QObject::tr( "Create renderer" ), mPreparationTime / 1000.0, QStringLiteral( "rendering" ) );
82 }
83
85
86 if ( ctx.renderingStopped() )
87 return false;
88
89 std::unique_ptr< QgsScopedRuntimeProfile > preparingProfile;
90 if ( mEnableProfile )
91 {
92 preparingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Preparing render" ), QStringLiteral( "rendering" ) );
93 }
94
95 mDataProvider->moveToThread( QThread::currentThread() );
96
97 const QgsScopedQPainterState painterState( ctx.painter() );
98
99 if ( !mClippingRegions.empty() )
100 {
101 bool needsPainterClipPath = false;
102 const QPainterPath path = QgsMapClippingUtils::calculatePainterClipRegion( mClippingRegions, *renderContext(), Qgis::LayerType::VectorTile, needsPainterClipPath );
103 if ( needsPainterClipPath )
104 renderContext()->painter()->setClipPath( path, Qt::IntersectClip );
105 }
106
107 QElapsedTimer tTotal;
108 tTotal.start();
109
110 const double tileRenderScale = mTileMatrixSet.scaleForRenderContext( ctx );
111
112 QgsRectangle extent = ctx.extent();
113 if ( extent.isMaximal() )
114 {
115 // invalid extent. We don't want to fetch ALL tiles at the zoom level!
116 // This has occurred because the map renderer is being pessimistic and thinks a worst-case
117 // scenario is acceptable when it failed to transform the map extent to the layer's crs.
118 // But while that's permissible eg for a local raster layer, it's entirely inappropriate
119 // for vector tiles!
120 // So let's try and determine a MINIMAL extent for the layer, avoiding the pessimism used
121 // in the generic map renderer code
122 QgsCoordinateTransform layerToMapTransform = ctx.coordinateTransform();
123 layerToMapTransform.setAllowFallbackTransforms( true );
124 layerToMapTransform.setBallparkTransformsAreAppropriate( true );
125
126 QgsRectangle extentInMapCrs = ctx.mapExtent();
127 try
128 {
129 extent = layerToMapTransform.transformBoundingBox( extentInMapCrs, Qgis::TransformDirection::Reverse );
130 }
131 catch ( QgsCsException &cs )
132 {
133 QgsDebugError( QStringLiteral( "Could not transform map extent to layer extent -- cannot calculate valid extent for vector tiles, aborting: %1" ).arg( cs.what() ) );
134 return false;
135 }
136 }
137
138 QgsDebugMsgLevel( QStringLiteral( "Vector tiles rendering extent: " ) + extent.toString( -1 ), 2 );
139 QgsDebugMsgLevel( QStringLiteral( "Vector tiles map scale 1 : %1" ).arg( tileRenderScale ), 2 );
140
141 mTileZoomToFetch = mTileMatrixSet.scaleToZoomLevel( tileRenderScale );
142 QgsDebugMsgLevel( QStringLiteral( "Vector tiles zoom level: %1" ).arg( mTileZoomToFetch ), 2 );
143 mTileZoomToRender = mTileMatrixSet.scaleToZoomLevel( tileRenderScale, false );
144 QgsDebugMsgLevel( QStringLiteral( "Render zoom level: %1" ).arg( mTileZoomToRender ), 2 );
145
146 mTileMatrix = mTileMatrixSet.tileMatrix( mTileZoomToFetch );
147
148 mTileRange = mTileMatrix.tileRangeFromExtent( extent );
149 QgsDebugMsgLevel( QStringLiteral( "Vector tiles range X: %1 - %2 Y: %3 - %4 (%5 tiles total)" )
150 .arg( mTileRange.startColumn() ).arg( mTileRange.endColumn() )
151 .arg( mTileRange.startRow() ).arg( mTileRange.endRow() ).arg( mTileRange.count() ), 2 );
152
153 // view center is used to sort the order of tiles for fetching and rendering
154 const QPointF viewCenter = mTileMatrix.mapToTileCoordinates( extent.center() );
155
156 if ( !mTileRange.isValid() )
157 {
158 QgsDebugMsgLevel( QStringLiteral( "Vector tiles - outside of range" ), 2 );
159 return true; // nothing to do
160 }
161
162 preparingProfile.reset();
163 std::unique_ptr< QgsScopedRuntimeProfile > renderingProfile;
164 if ( mEnableProfile )
165 {
166 renderingProfile = std::make_unique< QgsScopedRuntimeProfile >( QObject::tr( "Rendering" ), QStringLiteral( "rendering" ) );
167 }
168
169 std::unique_ptr<QgsVectorTileLoader> asyncLoader;
170 QList<QgsVectorTileRawData> rawTiles;
171 if ( !mDataProvider->supportsAsync() )
172 {
173 QElapsedTimer tFetch;
174 tFetch.start();
175 rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mDataProvider.get(), mTileMatrixSet, viewCenter, mTileRange, mTileZoomToFetch, mFeedback.get() );
176 QgsDebugMsgLevel( QStringLiteral( "Tile fetching time: %1" ).arg( tFetch.elapsed() / 1000. ), 2 );
177 QgsDebugMsgLevel( QStringLiteral( "Fetched tiles: %1" ).arg( rawTiles.count() ), 2 );
178 }
179 else
180 {
181 asyncLoader.reset( new QgsVectorTileLoader( mDataProvider.get(), mTileMatrixSet, mTileRange, mTileZoomToFetch, viewCenter, mFeedback.get(), renderContext()->rendererUsage() ) );
182 QObject::connect( asyncLoader.get(), &QgsVectorTileLoader::tileRequestFinished, asyncLoader.get(), [this]( const QgsVectorTileRawData & rawTile )
183 {
184 QgsDebugMsgLevel( QStringLiteral( "Got tile asynchronously: " ) + rawTile.id.toString(), 2 );
185 if ( !rawTile.data.isEmpty() )
186 decodeAndDrawTile( rawTile );
187 } );
188 }
189
190 if ( ctx.renderingStopped() )
191 return false;
192
193 // add @zoom_level and @vector_tile_zoom variables which can be used in styling
194 QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Tiles" ) ); // will be deleted by popper
195 scope->setVariable( QStringLiteral( "zoom_level" ), mTileZoomToRender, true );
196 scope->setVariable( QStringLiteral( "vector_tile_zoom" ), mTileMatrixSet.scaleToZoom( tileRenderScale ), true );
197 const QgsExpressionContextScopePopper popper( ctx.expressionContext(), scope );
198
199 mRenderer->startRender( *renderContext(), mTileZoomToRender, mTileRange );
200
201 // Draw background style if present
202 mRenderer->renderBackground( ctx );
203
204 QMap<QString, QSet<QString> > requiredFields = mRenderer->usedAttributes( ctx );
205
206 if ( mLabelProvider )
207 {
208 const QMap<QString, QSet<QString> > requiredFieldsLabeling = mLabelProvider->usedAttributes( ctx, mTileZoomToRender );
209 for ( auto it = requiredFieldsLabeling.begin(); it != requiredFieldsLabeling.end(); ++it )
210 {
211 requiredFields[it.key()].unite( it.value() );
212 }
213 }
214
215 for ( auto it = requiredFields.constBegin(); it != requiredFields.constEnd(); ++it )
216 mPerLayerFields[it.key()] = QgsVectorTileUtils::makeQgisFields( it.value() );
217
218 mRequiredLayers = mRenderer->requiredLayers( ctx, mTileZoomToRender );
219
220 if ( mLabelProvider )
221 {
222 mLabelProvider->setFields( mPerLayerFields );
223 QSet<QString> attributeNames; // we don't need this - already got referenced columns in provider constructor
224 if ( !mLabelProvider->prepare( ctx, attributeNames ) )
225 {
226 ctx.labelingEngine()->removeProvider( mLabelProvider );
227 mLabelProvider = nullptr; // provider is deleted by the engine
228 }
229 else
230 {
231 mRequiredLayers.unite( mLabelProvider->requiredLayers( ctx, mTileZoomToRender ) );
232 }
233 }
234
235 if ( !mDataProvider->supportsAsync() )
236 {
237 for ( const QgsVectorTileRawData &rawTile : std::as_const( rawTiles ) )
238 {
239 if ( ctx.renderingStopped() )
240 break;
241
242 decodeAndDrawTile( rawTile );
243 }
244 }
245 else
246 {
247 // Block until tiles are fetched and rendered. If the rendering gets canceled at some point,
248 // the async loader will catch the signal, abort requests and return from downloadBlocking()
249 asyncLoader->downloadBlocking();
250 if ( !asyncLoader->error().isEmpty() )
251 mErrors.append( asyncLoader->error() );
252 }
253
254 // Register labels features when all tiles are fetched to ensure consistent labeling
255 if ( mLabelProvider )
256 {
257 for ( const auto &tile : mTileDataMap )
258 {
259 mLabelProvider->registerTileFeatures( tile, ctx );
260 }
261 }
262
264 mRenderer->renderSelectedFeatures( mSelectedFeatures, ctx );
265
266 mRenderer->stopRender( ctx );
267
268 QgsDebugMsgLevel( QStringLiteral( "Total time for decoding: %1" ).arg( mTotalDecodeTime / 1000. ), 2 );
269 QgsDebugMsgLevel( QStringLiteral( "Drawing time: %1" ).arg( mTotalDrawTime / 1000. ), 2 );
270 QgsDebugMsgLevel( QStringLiteral( "Total time: %1" ).arg( tTotal.elapsed() / 1000. ), 2 );
271
272 return !ctx.renderingStopped();
273}
274
276{
278 return false;
279
280 if ( !qgsDoubleNear( mLayerOpacity, 1.0 ) )
281 return true;
282
283 if ( mLayerBlendMode != QPainter::CompositionMode_SourceOver )
284 return true;
285
286 return false;
287}
288
289void QgsVectorTileLayerRenderer::decodeAndDrawTile( const QgsVectorTileRawData &rawTile )
290{
292
293 QgsDebugMsgLevel( QStringLiteral( "Drawing tile " ) + rawTile.id.toString(), 2 );
294
295 QElapsedTimer tLoad;
296 tLoad.start();
297
298 // currently only MVT encoding supported
299 QgsVectorTileMVTDecoder decoder( mTileMatrixSet );
300 if ( !decoder.decode( rawTile ) )
301 {
302 QgsDebugMsgLevel( QStringLiteral( "Failed to parse raw tile data! " ) + rawTile.id.toString(), 2 );
303 return;
304 }
305
306 if ( ctx.renderingStopped() )
307 return;
308
310
311 QgsVectorTileRendererData tile( rawTile.id );
312 tile.setRenderZoomLevel( mTileZoomToRender );
313
314 tile.setFields( mPerLayerFields );
315 tile.setFeatures( decoder.layerFeatures( mPerLayerFields, ct, &mRequiredLayers ) );
316
317 try
318 {
319 tile.setTilePolygon( QgsVectorTileUtils::tilePolygon( rawTile.id, ct, mTileMatrixSet.tileMatrix( rawTile.id.zoomLevel() ), ctx.mapToPixel() ) );
320 }
321 catch ( QgsCsException & )
322 {
323 QgsDebugMsgLevel( QStringLiteral( "Failed to generate tile polygon " ) + rawTile.id.toString(), 2 );
324 return;
325 }
326
327 mTotalDecodeTime += tLoad.elapsed();
328
329 // calculate tile polygon in screen coordinates
330
331 if ( ctx.renderingStopped() )
332 return;
333
334 {
335 // set up clipping so that rendering does not go behind tile's extent
336 const QgsScopedQPainterState savePainterState( ctx.painter() );
337 // we have to intersect with any existing painter clip regions, or we risk overwriting valid clip
338 // regions setup outside of the vector tile renderer (e.g. layout map clip region)
339 ctx.painter()->setClipRegion( QRegion( tile.tilePolygon() ), Qt::IntersectClip );
340
341 QElapsedTimer tDraw;
342 tDraw.start();
343
344 mRenderer->renderTile( tile, ctx );
345 mTotalDrawTime += tDraw.elapsed();
346 }
347
348 // Store tile for later use
349 if ( mLabelProvider )
350 mTileDataMap.insert( tile.id().toString(), tile );
351
352 if ( mDrawTileBoundaries )
353 {
354 const QgsScopedQPainterState savePainterState( ctx.painter() );
355 ctx.painter()->setClipping( false );
356
357 QPen pen( Qt::red );
358 pen.setWidth( 3 );
359 QBrush brush( QColor( 255, 0, 0, 40 ), Qt::BrushStyle::Dense3Pattern );
360
361 ctx.painter()->setPen( pen );
362 ctx.painter()->setBrush( brush );
363 ctx.painter()->drawPolygon( tile.tilePolygon() );
364#if 1
365 QgsTextFormat format;
366 format.setColor( QColor( 255, 0, 0 ) );
367 format.buffer().setEnabled( true );
368
369 QgsTextRenderer::drawText( QRectF( QPoint( 0, 0 ), ctx.outputSize() ).intersected( tile.tilePolygon().boundingRect() ),
370 0, Qgis::TextHorizontalAlignment::Center, { tile.id().toString() },
372#endif
373 }
374}
Provides global constants and enumerations for use throughout the application.
Definition qgis.h:54
@ VectorTile
Vector tile layer. Added in QGIS 3.14.
@ DrawSelection
Whether vector selections should be shown in the rendered map.
@ UseAdvancedEffects
Enable layer opacity and blending effects.
@ VerticalCenter
Center align.
@ Reverse
Reverse/inverse transform (from destination to source)
static QgsRuntimeProfiler * profiler()
Returns the application runtime profiler.
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.
void setAllowFallbackTransforms(bool allowed)
Sets whether "ballpark" fallback transformations can be used in the case that the specified coordinat...
Custom exception class for Coordinate Reference System related exceptions.
QString what() const
RAII class to pop scope from an expression context on destruction.
Single scope for storing variables and functions for use within a QgsExpressionContext.
void setVariable(const QString &name, const QVariant &value, bool isStatic=false)
Convenience method for setting a variable in the context scope by name name and value.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
Provides map labeling functionality.
void removeProvider(QgsAbstractLabelProvider *provider)
Remove provider if the provider's initialization failed. Provider instance is deleted.
static QPainterPath calculatePainterClipRegion(const QList< QgsMapClippingRegion > &regions, const QgsRenderContext &context, Qgis::LayerType layerType, bool &shouldClip)
Returns a QPainterPath representing the intersection of clipping regions from context which should be...
static QList< QgsMapClippingRegion > collectClippingRegionsForLayer(const QgsRenderContext &context, const QgsMapLayer *layer)
Collects the list of map clipping regions from a context which apply to a map layer.
Base class for utility classes that encapsulate information necessary for rendering of map layers.
QString layerId() const
Gets access to the ID of the layer rendered by this class.
QgsRenderContext * renderContext()
Returns the render context associated with the renderer.
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 truncated to the sp...
bool isMaximal() const
Test if the rectangle is the maximal possible rectangle.
QgsPointXY center
Contains information about the context of a rendering operation.
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
QSize outputSize() const
Returns the size of the resulting rendered image, in pixels.
const QgsRectangle & extent() const
When rendering a map layer, calling this method returns the "clipping" extent for the layer (in the l...
QgsRectangle mapExtent() const
Returns the original extent of the map being rendered.
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
bool renderingStopped() const
Returns true if the rendering operation has been stopped and any ongoing rendering should be canceled...
QgsLabelingEngine * labelingEngine() const
Gets access to new labeling engine (may be nullptr).
QgsCoordinateTransform coordinateTransform() const
Returns the current coordinate transform for the context.
Qgis::RenderContextFlags flags() const
Returns combination of flags used for rendering.
void record(const QString &name, double time, const QString &group="startup", const QString &id=QString())
Manually adds a profile event with the given name and total time (in seconds).
Scoped object for saving and restoring a QPainter object's state.
void setEnabled(bool enabled)
Sets whether the text buffer will be drawn.
Container for all settings relating to text rendering.
void setColor(const QColor &color)
Sets the color that text will be rendered in.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
double scaleForRenderContext(const QgsRenderContext &context) const
Calculates the correct scale to use for the tiles when rendered using the specified render context.
Definition qgstiles.cpp:299
double scaleToZoom(double scale) const
Calculates a fractional zoom level given a map scale denominator.
Definition qgstiles.cpp:226
QgsTileMatrix tileMatrix(int zoom) const
Returns the tile matrix corresponding to the specified zoom.
Definition qgstiles.cpp:156
int scaleToZoomLevel(double scale, bool clamp=true) const
Finds the best fitting (integer) zoom level given a map scale denominator.
Definition qgstiles.cpp:283
QPointF mapToTileCoordinates(const QgsPointXY &mapPoint) const
Returns row/column coordinates (floating point number) from the given point in map coordinates.
Definition qgstiles.cpp:121
QgsTileRange tileRangeFromExtent(const QgsRectangle &mExtent) const
Returns tile range that fully covers the given extent.
Definition qgstiles.cpp:97
int endColumn() const
Returns index of the last column in the range.
Definition qgstiles.h:110
int endRow() const
Returns index of the last row in the range.
Definition qgstiles.h:114
int startRow() const
Returns index of the first row in the range.
Definition qgstiles.h:112
int startColumn() const
Returns index of the first column in the range.
Definition qgstiles.h:108
bool isValid() const
Returns whether the range is valid (when all row/column numbers are not negative)
Definition qgstiles.h:105
int count() const
Returns the total number of tiles in the range.
Definition qgstiles.h:121
QString toString() const
Returns tile coordinates in a formatted string.
Definition qgstiles.h:56
int zoomLevel() const
Returns tile's zoom level (Z)
Definition qgstiles.h:53
virtual bool prepare(QgsRenderContext &context, QSet< QString > &attributeNames)
Prepare for registration of features.
Base class for vector tile layer data providers.
virtual QMap< QString, QSet< QString > > usedAttributes(const QgsRenderContext &context, int tileZoom) const =0
Returns field names for each sub-layer that are required for labeling.
virtual void setFields(const QMap< QString, QgsFields > &perLayerFields)=0
Sets fields for each sub-layer.
virtual QSet< QString > requiredLayers(QgsRenderContext &context, int tileZoom) const
Returns a list of the layers required for labeling.
virtual void registerTileFeatures(const QgsVectorTileRendererData &tile, QgsRenderContext &context)=0
Registers label features for given tile to the labeling engine.
virtual QgsVectorTileLabelProvider * provider(QgsVectorTileLayer *layer) const SIP_SKIP
Factory for label provider implementation.
bool forceRasterRender() const override
Returns true if the renderer must be rendered to a raster paint device (e.g.
~QgsVectorTileLayerRenderer() override
QgsVectorTileLayerRenderer(QgsVectorTileLayer *layer, QgsRenderContext &context)
Creates the renderer. Always called from main thread, should copy whatever necessary from the layer.
virtual bool render() override
Do the rendering (based on data stored in the class).
Implements a map layer that is dedicated to rendering of vector tiles.
bool labelsEnabled() const
Returns whether the layer contains labels which are enabled and should be drawn.
QgsVectorTileLabeling * labeling() const
Returns currently assigned labeling.
The loader class takes care of loading raw vector tile data from a tile source.
void tileRequestFinished(const QgsVectorTileRawData &rawTile)
Emitted when a tile request has finished. If a tile request has failed, the returned raw tile byte ar...
static QList< QgsVectorTileRawData > blockingFetchTileRawData(const QgsVectorTileDataProvider *provider, const QgsTileMatrixSet &tileMatrixSet, const QPointF &viewCenter, const QgsTileRange &range, int zoomLevel, QgsFeedback *feedback=nullptr, Qgis::RendererUsage usage=Qgis::RendererUsage::Unknown)
Returns raw tile data for the specified range of tiles. Blocks the caller until all tiles are fetched...
Responsible for decoding raw tile data written with Mapbox Vector Tiles encoding.
Keeps track of raw tile data from one or more sources that need to be decoded.
QgsTileXYZ id
Tile position in tile matrix set.
Contains decoded features of a single vector tile and any other data necessary for rendering of it.
static QPolygon tilePolygon(QgsTileXYZ id, const QgsCoordinateTransform &ct, const QgsTileMatrix &tm, const QgsMapToPixel &mtp)
Returns polygon (made by four corners of the tile) in screen coordinates.
static QgsFields makeQgisFields(const QSet< QString > &flds)
Returns QgsFields instance based on the set of field names.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:6302
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41
#define QgsDebugError(str)
Definition qgslogger.h:40