QGIS API Documentation 3.39.0-Master (47f7b3a4989)
Loading...
Searching...
No Matches
qgselevationprofilecanvas.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgselevationprofilecanvas.cpp
3 -----------------
4 begin : March 2022
5 copyright : (C) 2022 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7***************************************************************************/
8
9
10/***************************************************************************
11 * *
12 * This program is free software; you can redistribute it and/or modify *
13 * it under the terms of the GNU General Public License as published by *
14 * the Free Software Foundation; either version 2 of the License, or *
15 * (at your option) any later version. *
16 * *
17 ***************************************************************************/
18
19#include "qgsapplication.h"
22#include "qgsplotcanvasitem.h"
23#include "qgsprofilerequest.h"
25#include "qgscurve.h"
27#include "qgsterrainprovider.h"
29#include "qgsprofilerenderer.h"
30#include "qgspoint.h"
31#include "qgsgeos.h"
32#include "qgsplot.h"
33#include "qgsnumericformat.h"
35#include "qgsprofilesnapping.h"
37#include "qgsscreenhelper.h"
38#include "qgsfillsymbol.h"
39#include "qgslinesymbol.h"
41
42#include <QWheelEvent>
43#include <QTimer>
44#include <QPalette>
45
47class QgsElevationProfilePlotItem : public Qgs2DPlot, public QgsPlotCanvasItem
48{
49 public:
50
51 QgsElevationProfilePlotItem( QgsElevationProfileCanvas *canvas )
52 : QgsPlotCanvasItem( canvas )
53 {
54 setYMinimum( 0 );
55 setYMaximum( 100 );
56
58 }
59
60 void setRenderer( QgsProfilePlotRenderer *renderer )
61 {
62 mRenderer = renderer;
63 }
64
65 void updateRect()
66 {
67 mRect = mCanvas->rect();
68 setSize( mRect.size() );
69
70 prepareGeometryChange();
71 setPos( mRect.topLeft() );
72
73 mImage = QImage();
74 mCachedImages.clear();
75 mPlotArea = QRectF();
76 update();
77 }
78
79 void updatePlot()
80 {
81 mImage = QImage();
82 mCachedImages.clear();
83 mPlotArea = QRectF();
84 update();
85 }
86
87 bool redrawResults( const QString &sourceId )
88 {
89 auto it = mCachedImages.find( sourceId );
90 if ( it == mCachedImages.end() )
91 return false;
92
93 mCachedImages.erase( it );
94 mImage = QImage();
95 return true;
96 }
97
98 QRectF boundingRect() const override
99 {
100 return mRect;
101 }
102
103 QString distanceSuffix() const
104 {
105 switch ( mDistanceUnit )
106 {
116 return QStringLiteral( " %1" ).arg( QgsUnitTypes::toAbbreviatedString( mDistanceUnit ) );
117
119 return QObject::tr( "°" );
121 return QString();
122 }
124 }
125
126 void setXAxisUnits( Qgis::DistanceUnit unit )
127 {
128 mDistanceUnit = unit;
129 xAxis().setLabelSuffix( distanceSuffix() );
130 update();
131 }
132
133 QRectF plotArea()
134 {
135 if ( !mPlotArea.isNull() )
136 return mPlotArea;
137
138 // force immediate recalculation of plot area
139 QgsRenderContext context;
140 if ( !scene()->views().isEmpty() )
141 context.setScaleFactor( scene()->views().at( 0 )->logicalDpiX() / 25.4 );
142
144 mPlotArea = interiorPlotArea( context );
145 return mPlotArea;
146 }
147
148 QgsProfilePoint canvasPointToPlotPoint( QPointF point )
149 {
150 const QRectF area = plotArea();
151 if ( !area.contains( point.x(), point.y() ) )
152 return QgsProfilePoint();
153
154 const double distance = ( point.x() - area.left() ) / area.width() * ( xMaximum() - xMinimum() ) * mXScaleFactor + xMinimum() * mXScaleFactor;
155 const double elevation = ( area.bottom() - point.y() ) / area.height() * ( yMaximum() - yMinimum() ) + yMinimum();
156 return QgsProfilePoint( distance, elevation );
157 }
158
159 QgsPointXY plotPointToCanvasPoint( const QgsProfilePoint &point )
160 {
161 if ( point.distance() < xMinimum() * mXScaleFactor || point.distance() > xMaximum()* mXScaleFactor || point.elevation() < yMinimum() || point.elevation() > yMaximum() )
162 return QgsPointXY();
163
164 const QRectF area = plotArea();
165
166 const double x = ( point.distance() - xMinimum() * mXScaleFactor ) / ( ( xMaximum() - xMinimum() ) * mXScaleFactor ) * ( area.width() ) + area.left();
167 const double y = area.bottom() - ( point.elevation() - yMinimum() ) / ( yMaximum() - yMinimum() ) * ( area.height() );
168 return QgsPointXY( x, y );
169 }
170
171 void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override
172 {
173 mPlotArea = plotArea;
174
175 if ( !mRenderer )
176 return;
177
178 const double pixelRatio = !scene()->views().empty() ? scene()->views().at( 0 )->devicePixelRatioF() : 1;
179
180 const QStringList sourceIds = mRenderer->sourceIds();
181 for ( const QString &source : sourceIds )
182 {
183 QImage plot;
184 auto it = mCachedImages.constFind( source );
185 if ( it != mCachedImages.constEnd() )
186 {
187 plot = it.value();
188 }
189 else
190 {
191 plot = mRenderer->renderToImage( plotArea.width() * pixelRatio,
192 plotArea.height() * pixelRatio, xMinimum() * mXScaleFactor, xMaximum() * mXScaleFactor, yMinimum(), yMaximum(), source, pixelRatio );
193 plot.setDevicePixelRatio( pixelRatio );
194 mCachedImages.insert( source, plot );
195 }
196 rc.painter()->drawImage( QPointF( plotArea.left(),
197 plotArea.top() ), plot );
198 }
199 }
200
201 void paint( QPainter *painter ) override
202 {
203 // cache rendering to an image, so we don't need to redraw the plot
204 if ( !mImage.isNull() )
205 {
206 painter->drawImage( QPointF( 0, 0 ), mImage );
207 }
208 else
209 {
210 const double pixelRatio = !scene()->views().empty() ? scene()->views().at( 0 )->devicePixelRatioF() : 1;
211 mImage = QImage( mRect.width() * pixelRatio, mRect.height() * pixelRatio, QImage::Format_ARGB32_Premultiplied );
212 mImage.setDevicePixelRatio( pixelRatio );
213 mImage.fill( Qt::transparent );
214
215 QPainter imagePainter( &mImage );
216 imagePainter.setRenderHint( QPainter::Antialiasing, true );
218 rc.setDevicePixelRatio( pixelRatio );
219
220 const double mapUnitsPerPixel = ( xMaximum() - xMinimum() ) * mXScaleFactor / plotArea().width();
221 rc.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
222
225
227 render( rc );
228 imagePainter.end();
229
230 painter->drawImage( QPointF( 0, 0 ), mImage );
231 }
232 }
233
234 QgsProject *mProject = nullptr;
235 double mXScaleFactor = 1.0;
236
238
239 private:
240
241 QImage mImage;
242
243 QMap< QString, QImage > mCachedImages;
244
245 QRectF mRect;
246 QRectF mPlotArea;
247 QgsProfilePlotRenderer *mRenderer = nullptr;
248};
249
250class QgsElevationProfileCrossHairsItem : public QgsPlotCanvasItem
251{
252 public:
253
254 QgsElevationProfileCrossHairsItem( QgsElevationProfileCanvas *canvas, QgsElevationProfilePlotItem *plotItem )
255 : QgsPlotCanvasItem( canvas )
256 , mPlotItem( plotItem )
257 {
258 }
259
260 void updateRect()
261 {
262 mRect = mCanvas->rect();
263
264 prepareGeometryChange();
265 setPos( mRect.topLeft() );
266 update();
267 }
268
269 void setPoint( const QgsProfilePoint &point )
270 {
271 mPoint = point;
272 update();
273 }
274
275 QRectF boundingRect() const override
276 {
277 return mRect;
278 }
279
280 void paint( QPainter *painter ) override
281 {
282 const QgsPointXY crossHairPlotPoint = mPlotItem->plotPointToCanvasPoint( mPoint );
283 if ( crossHairPlotPoint.isEmpty() )
284 return;
285
286 painter->save();
287 painter->setBrush( Qt::NoBrush );
288 QPen crossHairPen;
289 crossHairPen.setCosmetic( true );
290 crossHairPen.setWidthF( 1 );
291 crossHairPen.setStyle( Qt::DashLine );
292 crossHairPen.setCapStyle( Qt::FlatCap );
293 const QPalette scenePalette = mPlotItem->scene()->palette();
294 QColor penColor = scenePalette.color( QPalette::ColorGroup::Active, QPalette::Text );
295 penColor.setAlpha( 150 );
296 crossHairPen.setColor( penColor );
297 painter->setPen( crossHairPen );
298 painter->drawLine( QPointF( mPlotItem->plotArea().left(), crossHairPlotPoint.y() ), QPointF( mPlotItem->plotArea().right(), crossHairPlotPoint.y() ) );
299 painter->drawLine( QPointF( crossHairPlotPoint.x(), mPlotItem->plotArea().top() ), QPointF( crossHairPlotPoint.x(), mPlotItem->plotArea().bottom() ) );
300
301 // also render current point text
302 QgsNumericFormatContext numericContext;
303
304 const QString xCoordinateText = mPlotItem->xAxis().numericFormat()->formatDouble( mPoint.distance() / mPlotItem->mXScaleFactor, numericContext )
305 + mPlotItem->distanceSuffix();
306
307 const QString yCoordinateText = mPlotItem->yAxis().numericFormat()->formatDouble( mPoint.elevation(), numericContext );
308
309 QFont font;
310 const QFontMetrics fm( font );
311 const double height = fm.capHeight();
312 const double xWidth = fm.horizontalAdvance( xCoordinateText );
313 const double yWidth = fm.horizontalAdvance( yCoordinateText );
314 const double textAxisMargin = fm.horizontalAdvance( ' ' );
315
316 QPointF xCoordOrigin;
317 QPointF yCoordOrigin;
318
319 if ( mPoint.distance() < ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5 * mPlotItem->mXScaleFactor )
320 {
321 if ( mPoint.elevation() < ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5 )
322 {
323 // render x coordinate on right top (left top align)
324 xCoordOrigin = QPointF( crossHairPlotPoint.x() + textAxisMargin, mPlotItem->plotArea().top() + height + textAxisMargin );
325 // render y coordinate on right top (right bottom align)
326 yCoordOrigin = QPointF( mPlotItem->plotArea().right() - yWidth - textAxisMargin, crossHairPlotPoint.y() - textAxisMargin );
327 }
328 else
329 {
330 // render x coordinate on right bottom (left bottom align)
331 xCoordOrigin = QPointF( crossHairPlotPoint.x() + textAxisMargin, mPlotItem->plotArea().bottom() - textAxisMargin );
332 // render y coordinate on right bottom (right top align)
333 yCoordOrigin = QPointF( mPlotItem->plotArea().right() - yWidth - textAxisMargin, crossHairPlotPoint.y() + height + textAxisMargin );
334 }
335 }
336 else
337 {
338 if ( mPoint.elevation() < ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5 )
339 {
340 // render x coordinate on left top (right top align)
341 xCoordOrigin = QPointF( crossHairPlotPoint.x() - xWidth - textAxisMargin, mPlotItem->plotArea().top() + height + textAxisMargin );
342 // render y coordinate on left top (left bottom align)
343 yCoordOrigin = QPointF( mPlotItem->plotArea().left() + textAxisMargin, crossHairPlotPoint.y() - textAxisMargin );
344 }
345 else
346 {
347 // render x coordinate on left bottom (right bottom align)
348 xCoordOrigin = QPointF( crossHairPlotPoint.x() - xWidth - textAxisMargin, mPlotItem->plotArea().bottom() - textAxisMargin );
349 // render y coordinate on left bottom (left top align)
350 yCoordOrigin = QPointF( mPlotItem->plotArea().left() + textAxisMargin, crossHairPlotPoint.y() + height + textAxisMargin );
351 }
352 }
353
354 // semi opaque background color brush
355 QColor backgroundColor = mPlotItem->chartBackgroundSymbol()->color();
356 backgroundColor.setAlpha( 220 );
357 painter->setBrush( QBrush( backgroundColor ) );
358 painter->setPen( Qt::NoPen );
359 painter->drawRect( QRectF( xCoordOrigin.x() - textAxisMargin + 1, xCoordOrigin.y() - textAxisMargin - height + 1, xWidth + 2 * textAxisMargin - 2, height + 2 * textAxisMargin - 2 ) );
360 painter->drawRect( QRectF( yCoordOrigin.x() - textAxisMargin + 1, yCoordOrigin.y() - textAxisMargin - height + 1, yWidth + 2 * textAxisMargin - 2, height + 2 * textAxisMargin - 2 ) );
361
362 painter->setBrush( Qt::NoBrush );
363 painter->setPen( scenePalette.color( QPalette::ColorGroup::Active, QPalette::Text ) );
364
365 painter->drawText( xCoordOrigin, xCoordinateText );
366 painter->drawText( yCoordOrigin, yCoordinateText );
367 painter->restore();
368 }
369
370 private:
371
372 QRectF mRect;
373 QgsProfilePoint mPoint;
374 QgsElevationProfilePlotItem *mPlotItem = nullptr;
375};
377
378
380 : QgsPlotCanvas( parent )
381{
382 mScreenHelper = new QgsScreenHelper( this );
383
384 mPlotItem = new QgsElevationProfilePlotItem( this );
385
386 // follow system color scheme by default
387 setBackgroundColor( QColor() );
388
389 mCrossHairsItem = new QgsElevationProfileCrossHairsItem( this, mPlotItem );
390 mCrossHairsItem->setZValue( 100 );
391 mCrossHairsItem->hide();
392
393 // updating the profile plot is deferred on a timer, so that we don't trigger it too often
394 mDeferredRegenerationTimer = new QTimer( this );
395 mDeferredRegenerationTimer->setSingleShot( true );
396 mDeferredRegenerationTimer->stop();
397 connect( mDeferredRegenerationTimer, &QTimer::timeout, this, &QgsElevationProfileCanvas::startDeferredRegeneration );
398
399 mDeferredRedrawTimer = new QTimer( this );
400 mDeferredRedrawTimer->setSingleShot( true );
401 mDeferredRedrawTimer->stop();
402 connect( mDeferredRedrawTimer, &QTimer::timeout, this, &QgsElevationProfileCanvas::startDeferredRedraw );
403
404}
405
407{
408 if ( mCurrentJob )
409 {
410 mPlotItem->setRenderer( nullptr );
411 mCurrentJob->deleteLater();
412 mCurrentJob = nullptr;
413 }
414}
415
417{
418 if ( mCurrentJob )
419 {
420 mPlotItem->setRenderer( nullptr );
421 disconnect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
422 mCurrentJob->cancelGeneration();
423 mCurrentJob->deleteLater();
424 mCurrentJob = nullptr;
425 }
426}
427
429{
430 const double dxPercent = dx / mPlotItem->plotArea().width();
431 const double dyPercent = dy / mPlotItem->plotArea().height();
432
433 // these look backwards, but we are dragging the paper, not the view!
434 const double dxPlot = - dxPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
435 const double dyPlot = dyPercent * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
436
437 // no need to handle axis scale lock here, we aren't changing scales
438 mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
439 mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
440 mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
441 mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
442
443 refineResults();
444
445 mPlotItem->updatePlot();
446 emit plotAreaChanged();
447}
448
450{
451 if ( !mPlotItem->plotArea().contains( x, y ) )
452 return;
453
454 const double newCenterX = mPlotItem->xMinimum() + ( x - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
455 const double newCenterY = mPlotItem->yMinimum() + ( mPlotItem->plotArea().bottom() - y ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
456
457 const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
458 const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;
459
460 // no need to handle axis scale lock here, we aren't changing scales
461 mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
462 mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
463 mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
464 mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
465
466 refineResults();
467
468 mPlotItem->updatePlot();
469 emit plotAreaChanged();
470}
471
473{
474 scalePlot( factor, factor );
475 emit plotAreaChanged();
476}
477
478QgsProfileSnapContext QgsElevationProfileCanvas::snapContext() const
479{
480 const double toleranceInPixels = QFontMetrics( font() ).horizontalAdvance( ' ' );
481 const double xToleranceInPlotUnits = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) * toleranceInPixels;
482 const double yToleranceInPlotUnits = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * toleranceInPixels;
483
484 QgsProfileSnapContext context;
485 context.maximumSurfaceDistanceDelta = 2 * xToleranceInPlotUnits;
486 context.maximumSurfaceElevationDelta = 10 * yToleranceInPlotUnits;
487 context.maximumPointDistanceDelta = 4 * xToleranceInPlotUnits;
488 context.maximumPointElevationDelta = 4 * yToleranceInPlotUnits;
489 context.displayRatioElevationVsDistance = ( ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) )
490 / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) );
491
492 return context;
493}
494
495QgsProfileIdentifyContext QgsElevationProfileCanvas::identifyContext() const
496{
497 const double toleranceInPixels = QFontMetrics( font() ).horizontalAdvance( ' ' );
498 const double xToleranceInPlotUnits = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) * toleranceInPixels;
499 const double yToleranceInPlotUnits = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * toleranceInPixels;
500
502 context.maximumSurfaceDistanceDelta = 2 * xToleranceInPlotUnits;
503 context.maximumSurfaceElevationDelta = 10 * yToleranceInPlotUnits;
504 context.maximumPointDistanceDelta = 4 * xToleranceInPlotUnits;
505 context.maximumPointElevationDelta = 4 * yToleranceInPlotUnits;
506 context.displayRatioElevationVsDistance = ( ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) )
507 / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) );
508
509 context.project = mProject;
510
511 return context;
512}
513
514void QgsElevationProfileCanvas::setupLayerConnections( QgsMapLayer *layer, bool isDisconnect )
515{
516 if ( isDisconnect )
517 {
518 disconnect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileGenerationPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged );
519 disconnect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileRenderingPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged );
520 disconnect( layer, &QgsMapLayer::dataChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
521 }
522 else
523 {
524 connect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileGenerationPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged );
525 connect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileRenderingPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged );
526 connect( layer, &QgsMapLayer::dataChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
527 }
528
529 switch ( layer->type() )
530 {
532 {
533 QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer );
534 if ( isDisconnect )
535 {
536 disconnect( vl, &QgsVectorLayer::featureAdded, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
537 disconnect( vl, &QgsVectorLayer::featureDeleted, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
538 disconnect( vl, &QgsVectorLayer::geometryChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
539 disconnect( vl, &QgsVectorLayer::attributeValueChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
540 }
541 else
542 {
543 connect( vl, &QgsVectorLayer::featureAdded, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
544 connect( vl, &QgsVectorLayer::featureDeleted, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
545 connect( vl, &QgsVectorLayer::geometryChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
546 connect( vl, &QgsVectorLayer::attributeValueChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
547 }
548 break;
549 }
558 break;
559 }
560}
561
562void QgsElevationProfileCanvas::adjustRangeForAxisScaleLock( double &xMinimum, double &xMaximum, double &yMinimum, double &yMaximum ) const
563{
564 // ensures that we always "zoom out" to match horizontal/vertical scales
565 const double horizontalScale = ( xMaximum - xMinimum ) / mPlotItem->plotArea().width();
566 const double verticalScale = ( yMaximum - yMinimum ) / mPlotItem->plotArea().height();
567 if ( horizontalScale > verticalScale )
568 {
569 const double height = horizontalScale * mPlotItem->plotArea().height();
570 const double deltaHeight = ( yMaximum - yMinimum ) - height;
571 yMinimum += deltaHeight / 2;
572 yMaximum -= deltaHeight / 2;
573 }
574 else
575 {
576 const double width = verticalScale * mPlotItem->plotArea().width();
577 const double deltaWidth = ( ( xMaximum - xMinimum ) - width );
578 xMinimum += deltaWidth / 2;
579 xMaximum -= deltaWidth / 2;
580 }
581}
582
584{
585 return mDistanceUnit;
586}
587
589{
590 mDistanceUnit = unit;
591 const double oldMin = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
592 const double oldMax = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor;
593 mPlotItem->mXScaleFactor = QgsUnitTypes::fromUnitToUnitFactor( mDistanceUnit, mCrs.mapUnits() );
594 mPlotItem->setXAxisUnits( mDistanceUnit );
595 mPlotItem->setXMinimum( oldMin / mPlotItem->mXScaleFactor );
596 mPlotItem->setXMaximum( oldMax / mPlotItem->mXScaleFactor );
597 mPlotItem->updatePlot();
598}
599
601{
602 if ( !color.isValid() )
603 {
604 QPalette customPalette = qApp->palette();
605 const QColor baseColor = qApp->palette().color( QPalette::ColorRole::Base );
606 const QColor windowColor = qApp->palette().color( QPalette::ColorRole::Window );
607 customPalette.setColor( QPalette::ColorRole::Base, windowColor );
608 customPalette.setColor( QPalette::ColorRole::Window, baseColor );
609 setPalette( customPalette );
610 scene()->setPalette( customPalette );
611 }
612 else
613 {
614 // build custom palette
615 const bool isDarkTheme = color.lightnessF() < 0.5;
616 QPalette customPalette = qApp->palette();
617 customPalette.setColor( QPalette::ColorRole::Window, color );
618 if ( isDarkTheme )
619 {
620 customPalette.setColor( QPalette::ColorRole::Text, QColor( 255, 255, 255 ) );
621 customPalette.setColor( QPalette::ColorRole::Base, color.lighter( 120 ) );
622 }
623 else
624 {
625 customPalette.setColor( QPalette::ColorRole::Text, QColor( 0, 0, 0 ) );
626 customPalette.setColor( QPalette::ColorRole::Base, color.darker( 120 ) );
627 }
628
629 setPalette( customPalette );
630 scene()->setPalette( customPalette );
631 }
632
633 updateChartFromPalette();
634}
635
637{
638 return mLockAxisScales;
639}
640
642{
643 mLockAxisScales = lock;
644 if ( mLockAxisScales )
645 {
646 double xMinimum = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
647 double xMaximum = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor;
648 double yMinimum = mPlotItem->yMinimum();
649 double yMaximum = mPlotItem->yMaximum();
650 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
651 mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
652 mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
653 mPlotItem->setYMinimum( yMinimum );
654 mPlotItem->setYMaximum( yMaximum );
655
656 refineResults();
657 mPlotItem->updatePlot();
658 emit plotAreaChanged();
659 }
660}
661
663{
664 if ( !mCurrentJob || !mSnappingEnabled )
665 return QgsPointXY();
666
667 const QgsProfilePoint plotPoint = canvasPointToPlotPoint( point );
668
669 const QgsProfileSnapResult snappedPoint = mCurrentJob->snapPoint( plotPoint, snapContext() );
670 if ( !snappedPoint.isValid() )
671 return QgsPointXY();
672
673 return plotPointToCanvasPoint( snappedPoint.snappedPoint );
674}
675
676void QgsElevationProfileCanvas::scalePlot( double xFactor, double yFactor )
677{
678 if ( mLockAxisScales )
679 yFactor = xFactor;
680
681 const double currentWidth = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor;
682 const double currentHeight = mPlotItem->yMaximum() - mPlotItem->yMinimum();
683
684 const double newWidth = currentWidth / xFactor;
685 const double newHeight = currentHeight / yFactor;
686
687 const double currentCenterX = ( mPlotItem->xMinimum() + mPlotItem->xMaximum() ) * 0.5 * mPlotItem->mXScaleFactor;
688 const double currentCenterY = ( mPlotItem->yMinimum() + mPlotItem->yMaximum() ) * 0.5;
689
690 double xMinimum = currentCenterX - newWidth * 0.5;
691 double xMaximum = currentCenterX + newWidth * 0.5;
692 double yMinimum = currentCenterY - newHeight * 0.5;
693 double yMaximum = currentCenterY + newHeight * 0.5;
694 if ( mLockAxisScales )
695 {
696 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
697 }
698
699 mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
700 mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
701 mPlotItem->setYMinimum( yMinimum );
702 mPlotItem->setYMaximum( yMaximum );
703
704 refineResults();
705 mPlotItem->updatePlot();
706 emit plotAreaChanged();
707}
708
710{
711 const QRectF intersected = rect.intersected( mPlotItem->plotArea() );
712
713 double minX = ( intersected.left() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
714 double maxX = ( intersected.right() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
715 double minY = ( mPlotItem->plotArea().bottom() - intersected.bottom() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
716 double maxY = ( mPlotItem->plotArea().bottom() - intersected.top() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
717
718 if ( mLockAxisScales )
719 {
720 adjustRangeForAxisScaleLock( minX, maxX, minY, maxY );
721 }
722
723 mPlotItem->setXMinimum( minX / mPlotItem->mXScaleFactor );
724 mPlotItem->setXMaximum( maxX / mPlotItem->mXScaleFactor );
725 mPlotItem->setYMinimum( minY );
726 mPlotItem->setYMaximum( maxY );
727
728 refineResults();
729 mPlotItem->updatePlot();
730 emit plotAreaChanged();
731}
732
733void QgsElevationProfileCanvas::wheelZoom( QWheelEvent *event )
734{
735 //get mouse wheel zoom behavior settings
736 QgsSettings settings;
737 double zoomFactor = settings.value( QStringLiteral( "qgis/zoom_factor" ), 2 ).toDouble();
738 bool reverseZoom = settings.value( QStringLiteral( "qgis/reverse_wheel_zoom" ), false ).toBool();
739 bool zoomIn = reverseZoom ? event->angleDelta().y() < 0 : event->angleDelta().y() > 0;
740
741 // "Normal" mouse have an angle delta of 120, precision mouses provide data faster, in smaller steps
742 zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 120.0 * std::fabs( event->angleDelta().y() );
743
744 if ( event->modifiers() & Qt::ControlModifier )
745 {
746 //holding ctrl while wheel zooming results in a finer zoom
747 zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 20.0;
748 }
749
750 //calculate zoom scale factor
751 double scaleFactor = ( zoomIn ? 1 / zoomFactor : zoomFactor );
752
753 QRectF viewportRect = mPlotItem->plotArea();
754
755 if ( viewportRect.contains( event->position() ) )
756 {
757 //adjust view center
758 const double oldCenterX = 0.5 * ( mPlotItem->xMaximum() + mPlotItem->xMinimum() );
759 const double oldCenterY = 0.5 * ( mPlotItem->yMaximum() + mPlotItem->yMinimum() );
760
761 const double eventPosX = ( event->position().x() - viewportRect.left() ) / viewportRect.width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
762 const double eventPosY = ( viewportRect.bottom() - event->position().y() ) / viewportRect.height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
763
764 const double newCenterX = eventPosX + ( ( oldCenterX - eventPosX ) * scaleFactor );
765 const double newCenterY = eventPosY + ( ( oldCenterY - eventPosY ) * scaleFactor );
766
767 const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
768 const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;
769
770 // don't need to handle axis scale lock here, we are always changing axis by the same scale
771 mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
772 mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
773 mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
774 mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
775 }
776
777 //zoom plot
778 if ( zoomIn )
779 {
780 scalePlot( zoomFactor );
781 }
782 else
783 {
784 scalePlot( 1 / zoomFactor );
785 }
786 emit plotAreaChanged();
787}
788
790{
792 if ( e->isAccepted() )
793 {
794 mCrossHairsItem->hide();
795 return;
796 }
797
798 QgsProfilePoint plotPoint = canvasPointToPlotPoint( e->pos() );
799 if ( mCurrentJob && mSnappingEnabled && !plotPoint.isEmpty() )
800 {
801 const QgsProfileSnapResult snapResult = mCurrentJob->snapPoint( plotPoint, snapContext() );
802 if ( snapResult.isValid() )
803 plotPoint = snapResult.snappedPoint;
804 }
805
806 if ( plotPoint.isEmpty() )
807 {
808 mCrossHairsItem->hide();
809 }
810 else
811 {
812 mCrossHairsItem->setPoint( plotPoint );
813 mCrossHairsItem->show();
814 }
815 emit canvasPointHovered( e->pos(), plotPoint );
816}
817
819{
820 return mPlotItem->plotArea();
821}
822
824{
825 if ( !mProject || !profileCurve() )
826 return;
827
828 if ( mCurrentJob )
829 {
830 mPlotItem->setRenderer( nullptr );
831 disconnect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
832 mCurrentJob->deleteLater();
833 mCurrentJob = nullptr;
834 }
835
836 QgsProfileRequest request( profileCurve()->clone() );
837 request.setCrs( mCrs );
838 request.setTolerance( mTolerance );
839 request.setTransformContext( mProject->transformContext() );
840 request.setTerrainProvider( mProject->elevationProperties()->terrainProvider() ? mProject->elevationProperties()->terrainProvider()->clone() : nullptr );
841 QgsExpressionContext context;
844 request.setExpressionContext( context );
845
846 const QList< QgsMapLayer * > layersToGenerate = layers();
847 QList< QgsAbstractProfileSource * > sources;
848 const QList< QgsAbstractProfileSource * > registrySources = QgsApplication::profileSourceRegistry()->profileSources();
849 sources.reserve( layersToGenerate.size() + registrySources.size() );
850
851 sources << registrySources;
852 for ( QgsMapLayer *layer : layersToGenerate )
853 {
854 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
855 sources.append( source );
856 }
857
858 mCurrentJob = new QgsProfilePlotRenderer( sources, request );
859 connect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
860
861 QgsProfileGenerationContext generationContext;
862 generationContext.setDpi( mScreenHelper->screenDpi() );
863 generationContext.setMaximumErrorMapUnits( MAX_ERROR_PIXELS * ( mProfileCurve->length() ) / mPlotItem->plotArea().width() );
864 generationContext.setMapUnitsPerDistancePixel( mProfileCurve->length() / mPlotItem->plotArea().width() );
865 mCurrentJob->setContext( generationContext );
866
867 mCurrentJob->startGeneration();
868 mPlotItem->setRenderer( mCurrentJob );
869
870 emit activeJobCountChanged( 1 );
871}
872
874{
875 mZoomFullWhenJobFinished = true;
876}
877
878void QgsElevationProfileCanvas::generationFinished()
879{
880 if ( !mCurrentJob )
881 return;
882
883 emit activeJobCountChanged( 0 );
884
885 if ( mZoomFullWhenJobFinished )
886 {
887 // we only zoom full for the initial generation
888 mZoomFullWhenJobFinished = false;
889 zoomFull();
890 }
891 else
892 {
893 // here we should invalidate cached results only for the layers which have been refined
894
895 // and if no layers are being refeined, don't invalidate anything
896
897 mPlotItem->updatePlot();
898 }
899
900 if ( mForceRegenerationAfterCurrentJobCompletes )
901 {
902 mForceRegenerationAfterCurrentJobCompletes = false;
903 mCurrentJob->invalidateAllRefinableSources();
904 scheduleDeferredRegeneration();
905 }
906}
907
908void QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged()
909{
910 // TODO -- handle nicely when existing job is in progress
911 if ( !mCurrentJob || mCurrentJob->isActive() )
912 return;
913
914 QgsMapLayerElevationProperties *properties = qobject_cast< QgsMapLayerElevationProperties * >( sender() );
915 if ( !properties )
916 return;
917
918 if ( QgsMapLayer *layer = qobject_cast< QgsMapLayer * >( properties->parent() ) )
919 {
920 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
921 {
922 if ( mCurrentJob->invalidateResults( source ) )
923 scheduleDeferredRegeneration();
924 }
925 }
926}
927
928void QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged()
929{
930 // TODO -- handle nicely when existing job is in progress
931 if ( !mCurrentJob || mCurrentJob->isActive() )
932 return;
933
934 QgsMapLayerElevationProperties *properties = qobject_cast< QgsMapLayerElevationProperties * >( sender() );
935 if ( !properties )
936 return;
937
938 if ( QgsMapLayer *layer = qobject_cast< QgsMapLayer * >( properties->parent() ) )
939 {
940 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
941 {
942 mCurrentJob->replaceSource( source );
943 }
944 if ( mPlotItem->redrawResults( layer->id() ) )
945 scheduleDeferredRedraw();
946 }
947}
948
949void QgsElevationProfileCanvas::regenerateResultsForLayer()
950{
951 if ( QgsMapLayer *layer = qobject_cast< QgsMapLayer * >( sender() ) )
952 {
953 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
954 {
955 if ( mCurrentJob->invalidateResults( source ) )
956 scheduleDeferredRegeneration();
957 }
958 }
959}
960
961void QgsElevationProfileCanvas::scheduleDeferredRegeneration()
962{
963 if ( !mDeferredRegenerationScheduled )
964 {
965 mDeferredRegenerationTimer->start( 1 );
966 mDeferredRegenerationScheduled = true;
967 }
968}
969
970void QgsElevationProfileCanvas::scheduleDeferredRedraw()
971{
972 if ( !mDeferredRedrawScheduled )
973 {
974 mDeferredRedrawTimer->start( 1 );
975 mDeferredRedrawScheduled = true;
976 }
977}
978
979void QgsElevationProfileCanvas::startDeferredRegeneration()
980{
981 if ( mCurrentJob && !mCurrentJob->isActive() )
982 {
983 emit activeJobCountChanged( 1 );
984 mCurrentJob->regenerateInvalidatedResults();
985 }
986 else if ( mCurrentJob )
987 {
988 mForceRegenerationAfterCurrentJobCompletes = true;
989 }
990
991 mDeferredRegenerationScheduled = false;
992}
993
994void QgsElevationProfileCanvas::startDeferredRedraw()
995{
996 mPlotItem->update();
997 mDeferredRedrawScheduled = false;
998}
999
1000void QgsElevationProfileCanvas::refineResults()
1001{
1002 if ( mCurrentJob )
1003 {
1005 context.setDpi( mScreenHelper->screenDpi() );
1006 const double plotDistanceRange = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor;
1007 const double plotElevationRange = mPlotItem->yMaximum() - mPlotItem->yMinimum();
1008 const double plotDistanceUnitsPerPixel = plotDistanceRange / mPlotItem->plotArea().width();
1009
1010 // we round the actual desired map error down to just one significant figure, to avoid tiny differences
1011 // as the plot is panned
1012 const double targetMaxErrorInMapUnits = MAX_ERROR_PIXELS * plotDistanceUnitsPerPixel;
1013 const double factor = std::pow( 10.0, 1 - std::ceil( std::log10( std::fabs( targetMaxErrorInMapUnits ) ) ) );
1014 const double roundedErrorInMapUnits = std::floor( targetMaxErrorInMapUnits * factor ) / factor;
1015 context.setMaximumErrorMapUnits( roundedErrorInMapUnits );
1016
1017 context.setMapUnitsPerDistancePixel( plotDistanceUnitsPerPixel );
1018
1019 // for similar reasons we round the minimum distance off to multiples of the maximum error in map units
1020 const double distanceMin = std::floor( ( mPlotItem->xMinimum() * mPlotItem->mXScaleFactor - plotDistanceRange * 0.05 ) / context.maximumErrorMapUnits() ) * context.maximumErrorMapUnits();
1021 context.setDistanceRange( QgsDoubleRange( std::max( 0.0, distanceMin ),
1022 mPlotItem->xMaximum() * mPlotItem->mXScaleFactor + plotDistanceRange * 0.05 ) );
1023
1024 context.setElevationRange( QgsDoubleRange( mPlotItem->yMinimum() - plotElevationRange * 0.05,
1025 mPlotItem->yMaximum() + plotElevationRange * 0.05 ) );
1026 mCurrentJob->setContext( context );
1027 }
1028 scheduleDeferredRegeneration();
1029}
1030
1031void QgsElevationProfileCanvas::updateChartFromPalette()
1032{
1033 const QPalette chartPalette = palette();
1034 setBackgroundBrush( QBrush( chartPalette.color( QPalette::ColorRole::Base ) ) );
1035 {
1036 QgsTextFormat textFormat = mPlotItem->xAxis().textFormat();
1037 textFormat.setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::Text ) );
1038 mPlotItem->xAxis().setTextFormat( textFormat );
1039 mPlotItem->yAxis().setTextFormat( textFormat );
1040 }
1041 {
1042 std::unique_ptr< QgsFillSymbol > chartFill( mPlotItem->chartBackgroundSymbol()->clone() );
1043 chartFill->setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Window ) );
1044 mPlotItem->setChartBackgroundSymbol( chartFill.release() );
1045 }
1046 {
1047 std::unique_ptr< QgsFillSymbol > chartBorder( mPlotItem->chartBorderSymbol()->clone() );
1048 chartBorder->setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text ) );
1049 mPlotItem->setChartBorderSymbol( chartBorder.release() );
1050 }
1051 {
1052 std::unique_ptr< QgsLineSymbol > chartMajorSymbol( mPlotItem->xAxis().gridMajorSymbol()->clone() );
1053 QColor c = chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text );
1054 c.setAlpha( 150 );
1055 chartMajorSymbol->setColor( c );
1056 mPlotItem->xAxis().setGridMajorSymbol( chartMajorSymbol->clone() );
1057 mPlotItem->yAxis().setGridMajorSymbol( chartMajorSymbol.release() );
1058 }
1059 {
1060 std::unique_ptr< QgsLineSymbol > chartMinorSymbol( mPlotItem->xAxis().gridMinorSymbol()->clone() );
1061 QColor c = chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text );
1062 c.setAlpha( 50 );
1063 chartMinorSymbol->setColor( c );
1064 mPlotItem->xAxis().setGridMinorSymbol( chartMinorSymbol->clone() );
1065 mPlotItem->yAxis().setGridMinorSymbol( chartMinorSymbol.release() );
1066 }
1067 mPlotItem->updatePlot();
1068}
1069
1071{
1072 if ( !mPlotItem->plotArea().contains( point.x(), point.y() ) )
1073 return QgsProfilePoint();
1074
1075 return mPlotItem->canvasPointToPlotPoint( point );
1076}
1077
1079{
1080 return mPlotItem->plotPointToCanvasPoint( point );
1081}
1082
1084{
1085 mProject = project;
1086 mPlotItem->mProject = project;
1087}
1088
1093
1095{
1096 mProfileCurve.reset( curve );
1097}
1098
1100{
1101 return mProfileCurve.get();
1102}
1103
1105{
1106 mTolerance = tolerance;
1107}
1108
1113
1114void QgsElevationProfileCanvas::setLayers( const QList<QgsMapLayer *> &layers )
1115{
1116 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
1117 {
1118 setupLayerConnections( layer, true );
1119 }
1120
1121 // filter list, removing null layers and invalid layers
1122 auto filteredList = layers;
1123 filteredList.erase( std::remove_if( filteredList.begin(), filteredList.end(),
1124 []( QgsMapLayer * layer )
1125 {
1126 return !layer || !layer->isValid();
1127 } ), filteredList.end() );
1128
1129 mLayers = _qgis_listRawToQPointer( filteredList );
1130 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
1131 {
1132 setupLayerConnections( layer, false );
1133 }
1134}
1135
1136QList<QgsMapLayer *> QgsElevationProfileCanvas::layers() const
1137{
1138 return _qgis_listQPointerToRaw( mLayers );
1139}
1140
1142{
1144
1145 if ( mLockAxisScales )
1146 {
1147 double xMinimum = mPlotItem->xMinimum();
1148 double xMaximum = mPlotItem->xMaximum();
1149 double yMinimum = mPlotItem->yMinimum();
1150 double yMaximum = mPlotItem->yMaximum();
1151 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
1152 mPlotItem->setXMinimum( xMinimum );
1153 mPlotItem->setXMaximum( xMaximum );
1154 mPlotItem->setYMinimum( yMinimum );
1155 mPlotItem->setYMaximum( yMaximum );
1156 }
1157
1158 mPlotItem->updateRect();
1159 mCrossHairsItem->updateRect();
1160}
1161
1163{
1164 QgsPlotCanvas::paintEvent( event );
1165
1166 if ( !mFirstDrawOccurred )
1167 {
1168 // on first show we need to update the visible rect of the plot. (Not sure why this doesn't work in showEvent, but it doesn't).
1169 mFirstDrawOccurred = true;
1170 mPlotItem->updateRect();
1171 mCrossHairsItem->updateRect();
1172 }
1173}
1174
1176{
1177 if ( !mPlotItem->plotArea().contains( point.x(), point.y() ) )
1178 return QgsPoint();
1179
1180 if ( !mProfileCurve )
1181 return QgsPoint();
1182
1183 const double dx = point.x() - mPlotItem->plotArea().left();
1184
1185 const double distanceAlongPlotPercent = dx / mPlotItem->plotArea().width();
1186 double distanceAlongCurveLength = distanceAlongPlotPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
1187
1188 std::unique_ptr< QgsPoint > mapXyPoint( mProfileCurve->interpolatePoint( distanceAlongCurveLength ) );
1189 if ( !mapXyPoint )
1190 return QgsPoint();
1191
1192 const double mapZ = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * ( mPlotItem->plotArea().bottom() - point.y() ) + mPlotItem->yMinimum();
1193
1194 return QgsPoint( mapXyPoint->x(), mapXyPoint->y(), mapZ );
1195}
1196
1198{
1199 if ( !mProfileCurve )
1200 return QgsPointXY();
1201
1202 QgsGeos geos( mProfileCurve.get() );
1203 QString error;
1204 const double distanceAlongCurve = geos.lineLocatePoint( point, &error );
1205
1206 const double distanceAlongCurveOnPlot = distanceAlongCurve - mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
1207 const double distanceAlongCurvePercent = distanceAlongCurveOnPlot / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor );
1208 const double distanceAlongPlotRect = distanceAlongCurvePercent * mPlotItem->plotArea().width();
1209
1210 const double canvasX = mPlotItem->plotArea().left() + distanceAlongPlotRect;
1211
1212 double canvasY = 0;
1213 if ( std::isnan( point.z() ) || point.z() < mPlotItem->yMinimum() )
1214 {
1215 canvasY = mPlotItem->plotArea().top();
1216 }
1217 else if ( point.z() > mPlotItem->yMaximum() )
1218 {
1219 canvasY = mPlotItem->plotArea().bottom();
1220 }
1221 else
1222 {
1223 const double yPercent = ( point.z() - mPlotItem->yMinimum() ) / ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
1224 canvasY = mPlotItem->plotArea().bottom() - mPlotItem->plotArea().height() * yPercent;
1225 }
1226
1227 return QgsPointXY( canvasX, canvasY );
1228}
1229
1231{
1232 if ( !mCurrentJob )
1233 return;
1234
1235 const QgsDoubleRange zRange = mCurrentJob->zRange();
1236
1237 double yMinimum = 0;
1238 double yMaximum = 0;
1239
1240 if ( zRange.upper() < zRange.lower() )
1241 {
1242 // invalid range, e.g. no features found in plot!
1243 yMinimum = 0;
1244 yMaximum = 10;
1245 }
1246 else if ( qgsDoubleNear( zRange.lower(), zRange.upper(), 0.0000001 ) )
1247 {
1248 // corner case ... a zero height plot! Just pick an arbitrary +/- 5 height range.
1249 yMinimum = zRange.lower() - 5;
1250 yMaximum = zRange.lower() + 5;
1251 }
1252 else
1253 {
1254 // add 5% margin to height range
1255 const double margin = ( zRange.upper() - zRange.lower() ) * 0.05;
1256 yMinimum = zRange.lower() - margin;
1257 yMaximum = zRange.upper() + margin;
1258 }
1259
1260 const double profileLength = profileCurve()->length();
1261 double xMinimum = 0;
1262 // just 2% margin to max distance -- any more is overkill and wasted space
1263 double xMaximum = profileLength * 1.02;
1264
1265 if ( mLockAxisScales )
1266 {
1267 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
1268 }
1269
1270 mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
1271 mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
1272 mPlotItem->setYMinimum( yMinimum );
1273 mPlotItem->setYMaximum( yMaximum );
1274
1275 refineResults();
1276 mPlotItem->updatePlot();
1277 emit plotAreaChanged();
1278}
1279
1280void QgsElevationProfileCanvas::setVisiblePlotRange( double minimumDistance, double maximumDistance, double minimumElevation, double maximumElevation )
1281{
1282 if ( mLockAxisScales )
1283 {
1284 adjustRangeForAxisScaleLock( minimumDistance, maximumDistance, minimumElevation, maximumElevation );
1285 }
1286
1287 mPlotItem->setYMinimum( minimumElevation );
1288 mPlotItem->setYMaximum( maximumElevation );
1289 mPlotItem->setXMinimum( minimumDistance / mPlotItem->mXScaleFactor );
1290 mPlotItem->setXMaximum( maximumDistance / mPlotItem->mXScaleFactor );
1291 refineResults();
1292 mPlotItem->updatePlot();
1293 emit plotAreaChanged();
1294}
1295
1297{
1298 return QgsDoubleRange( mPlotItem->xMinimum() * mPlotItem->mXScaleFactor, mPlotItem->xMaximum() * mPlotItem->mXScaleFactor );
1299}
1300
1302{
1303 return QgsDoubleRange( mPlotItem->yMinimum(), mPlotItem->yMaximum() );
1304}
1305
1307{
1308 return *mPlotItem;
1309}
1310
1312class QgsElevationProfilePlot : public Qgs2DPlot
1313{
1314 public:
1315
1316 QgsElevationProfilePlot( QgsProfilePlotRenderer *renderer )
1317 : mRenderer( renderer )
1318 {
1319 }
1320
1321 void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override
1322 {
1323 if ( !mRenderer )
1324 return;
1325
1326 rc.painter()->translate( plotArea.left(), plotArea.top() );
1327 mRenderer->render( rc, plotArea.width(), plotArea.height(), xMinimum() * mXScale, xMaximum() * mXScale, yMinimum(), yMaximum() );
1328 rc.painter()->translate( -plotArea.left(), -plotArea.top() );
1329 }
1330
1331 double mXScale = 1;
1332
1333 private:
1334
1335 QgsProfilePlotRenderer *mRenderer = nullptr;
1336};
1338
1339void QgsElevationProfileCanvas::render( QgsRenderContext &context, double width, double height, const Qgs2DPlot &plotSettings )
1340{
1341 if ( !mCurrentJob )
1342 return;
1343
1346
1347 QgsElevationProfilePlot profilePlot( mCurrentJob );
1348
1349 // quick and nasty way to transfer settings from another plot class -- in future we probably want to improve this, but let's let the API settle first...
1350 QDomDocument doc;
1351 QDomElement elem = doc.createElement( QStringLiteral( "plot" ) );
1352 QgsReadWriteContext rwContext;
1353 plotSettings.writeXml( elem, doc, rwContext );
1354 profilePlot.readXml( elem, rwContext );
1355
1356 profilePlot.mXScale = mPlotItem->mXScaleFactor;
1357 profilePlot.xAxis().setLabelSuffix( mPlotItem->xAxis().labelSuffix() );
1358 profilePlot.xAxis().setLabelSuffixPlacement( mPlotItem->xAxis().labelSuffixPlacement() );
1359
1360 profilePlot.setSize( QSizeF( width, height ) );
1361 profilePlot.render( context );
1362}
1363
1364QVector<QgsProfileIdentifyResults> QgsElevationProfileCanvas::identify( QPointF point )
1365{
1366 if ( !mCurrentJob )
1367 return {};
1368
1369 const QgsProfilePoint plotPoint = canvasPointToPlotPoint( point );
1370
1371 return mCurrentJob->identify( plotPoint, identifyContext() );
1372}
1373
1374QVector<QgsProfileIdentifyResults> QgsElevationProfileCanvas::identify( const QRectF &rect )
1375{
1376 if ( !mCurrentJob )
1377 return {};
1378
1379 const QgsProfilePoint topLeftPlotPoint = canvasPointToPlotPoint( rect.topLeft() );
1380 const QgsProfilePoint bottomRightPlotPoint = canvasPointToPlotPoint( rect.bottomRight() );
1381
1382 double distance1 = topLeftPlotPoint.distance();
1383 double distance2 = bottomRightPlotPoint.distance();
1384 if ( distance2 < distance1 )
1385 std::swap( distance1, distance2 );
1386
1387 double elevation1 = topLeftPlotPoint.elevation();
1388 double elevation2 = bottomRightPlotPoint.elevation();
1389 if ( elevation2 < elevation1 )
1390 std::swap( elevation1, elevation2 );
1391
1392 return mCurrentJob->identify( QgsDoubleRange( distance1, distance2 ), QgsDoubleRange( elevation1, elevation2 ), identifyContext() );
1393}
1394
1396{
1397 setProfileCurve( nullptr );
1398 mPlotItem->setRenderer( nullptr );
1399 mPlotItem->updatePlot();
1400}
1401
1403{
1404 mSnappingEnabled = enabled;
1405}
@ FirstAndLastLabels
Place suffix after the first and last label values only.
DistanceUnit
Units of distance.
Definition qgis.h:4363
@ Feet
Imperial feet.
@ Centimeters
Centimeters.
@ Millimeters
Millimeters.
@ Miles
Terrestrial miles.
@ Unknown
Unknown distance unit.
@ Yards
Imperial yards.
@ Degrees
Degrees, for planar geographic CRS distance measurements.
@ Inches
Inches (since QGIS 3.32)
@ NauticalMiles
Nautical miles.
@ Kilometers
Kilometers.
@ Group
Composite group layer. Added in QGIS 3.24.
@ Plugin
Plugin based layer.
@ TiledScene
Tiled scene layer. Added in QGIS 3.34.
@ Annotation
Contains freeform, georeferenced annotations. Added in QGIS 3.16.
@ Vector
Vector layer.
@ VectorTile
Vector tile layer. Added in QGIS 3.14.
@ Mesh
Mesh layer. Added in QGIS 3.2.
@ Raster
Raster layer.
@ PointCloud
Point cloud layer. Added in QGIS 3.18.
Base class for 2-dimensional plot/chart/graphs.
Definition qgsplot.h:278
void calculateOptimisedIntervals(QgsRenderContext &context)
Automatically sets the grid and label intervals to optimal values for display in the given render con...
Definition qgsplot.cpp:611
double yMaximum() const
Returns the maximum value of the y axis.
Definition qgsplot.h:390
bool writeXml(QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context) const override
Writes the plot's properties into an XML element.
Definition qgsplot.cpp:177
QgsPlotAxis & xAxis()
Returns a reference to the plot's x axis.
Definition qgsplot.h:404
void setSize(QSizeF size)
Sets the overall size of the plot (including titles and over components which sit outside the plot ar...
Definition qgsplot.cpp:491
double xMaximum() const
Returns the maximum value of the x axis.
Definition qgsplot.h:376
void render(QgsRenderContext &context)
Renders the plot.
Definition qgsplot.cpp:229
void setYMaximum(double maximum)
Sets the maximum value of the y axis.
Definition qgsplot.h:397
double xMinimum() const
Returns the minimum value of the x axis.
Definition qgsplot.h:348
double yMinimum() const
Returns the minimum value of the y axis.
Definition qgsplot.h:362
QRectF interiorPlotArea(QgsRenderContext &context) const
Returns the area of the plot which corresponds to the actual plot content (excluding all titles and o...
Definition qgsplot.cpp:496
void setYMinimum(double minimum)
Sets the minimum value of the y axis.
Definition qgsplot.h:369
virtual void renderContent(QgsRenderContext &context, const QRectF &plotArea)
Renders the plot content.
Definition qgsplot.cpp:479
virtual double length() const
Returns the planar, 2-dimensional length of the geometry.
Interface for classes which can generate elevation profiles.
virtual QgsAbstractTerrainProvider * clone() const =0
Creates a clone of the provider and returns the new object.
static QgsProfileSourceRegistry * profileSourceRegistry()
Returns registry of available profile source implementations.
This class represents a coordinate reference system (CRS).
Abstract base class for curved geometry type.
Definition qgscurve.h:35
QgsRange which stores a range of double values.
Definition qgsrange.h:231
A canvas for elevation profiles.
QgsDoubleRange visibleElevationRange() const
Returns the elevation range currently visible in the plot.
QgsCurve * profileCurve() const
Returns the profile curve.
void setTolerance(double tolerance)
Sets the profile tolerance (in crs() units).
void setLockAxisScales(bool lock)
Sets whether the distance and elevation scales are locked to each other.
void setProfileCurve(QgsCurve *curve)
Sets the profile curve.
void zoomToRect(const QRectF &rect) override
Zooms the plot to the specified rect in canvas units.
void activeJobCountChanged(int count)
Emitted when the number of active background jobs changes.
QgsElevationProfileCanvas(QWidget *parent=nullptr)
Constructor for QgsElevationProfileCanvas, with the specified parent widget.
void scalePlot(double factor) override
Scales the plot by a specified scale factor.
void paintEvent(QPaintEvent *event) override
QgsDoubleRange visibleDistanceRange() const
Returns the distance range currently visible in the plot.
void cancelJobs() override
Cancel any rendering job, in a blocking way.
QgsCoordinateReferenceSystem crs() const override
Returns the coordinate reference system (CRS) for map coordinates used by the canvas.
void clear()
Clears the current profile.
void setDistanceUnit(Qgis::DistanceUnit unit)
Sets the distance unit used by the canvas.
QgsProfilePoint canvasPointToPlotPoint(QPointF point) const
Converts a canvas point to the equivalent plot point.
void setBackgroundColor(const QColor &color)
Sets the background color to use for the profile canvas.
QgsPointXY plotPointToCanvasPoint(const QgsProfilePoint &point) const
Converts a plot point to the equivalent canvas point.
QgsPoint toMapCoordinates(const QgsPointXY &point) const override
Converts a point on the canvas to the associated map coordinate.
bool lockAxisScales() const
Returns true if the distance and elevation scales are locked to each other.
void setVisiblePlotRange(double minimumDistance, double maximumDistance, double minimumElevation, double maximumElevation)
Sets the visible area of the plot.
void canvasPointHovered(const QgsPointXY &point, const QgsProfilePoint &profilePoint)
Emitted when the mouse hovers over the specified point (in canvas coordinates).
void render(QgsRenderContext &context, double width, double height, const Qgs2DPlot &plotSettings)
Renders a portion of the profile using the specified render context.
QgsPointXY snapToPlot(QPoint point) override
Snap a canvas point to the plot.
void setProject(QgsProject *project)
Sets the project associated with the profile.
QList< QgsMapLayer * > layers() const
Returns the list of layers included in the profile.
void resizeEvent(QResizeEvent *event) override
void centerPlotOn(double x, double y) override
Centers the plot on the plot point corresponding to x, y in canvas units.
const Qgs2DPlot & plot() const
Returns a reference to the 2D plot used by the widget.
void wheelZoom(QWheelEvent *event) override
Zoom plot from a mouse wheel event.
void refresh() override
Triggers a complete regeneration of the profile, causing the profile extraction to perform in the bac...
Qgis::DistanceUnit distanceUnit() const
Returns the distance unit used by the canvas.
double tolerance() const
Returns the tolerance of the profile (in crs() units).
void mouseMoveEvent(QMouseEvent *e) override
void panContentsBy(double dx, double dy) override
Pans the plot contents by dx, dy in canvas units.
void invalidateCurrentPlotExtent()
Invalidates the current plot extent, which means that the visible plot area will be recalculated and ...
QgsPointXY toCanvasCoordinates(const QgsPoint &point) const override
Converts a point in map coordinates to the associated canvas point.
void setCrs(const QgsCoordinateReferenceSystem &crs)
Sets the crs associated with the canvas' map coordinates.
void setLayers(const QList< QgsMapLayer * > &layers)
Sets the list of layers to include in the profile.
void zoomFull()
Zooms to the full extent of the profile.
void setSnappingEnabled(bool enabled)
Sets whether snapping of cursor points is enabled.
QVector< QgsProfileIdentifyResults > identify(QPointF point)
Identify results visible at the specified plot point.
QRectF plotArea() const
Returns the interior rectangle representing the surface of the plot, in canvas coordinates.
static QgsExpressionContextScope * projectScope(const QgsProject *project)
Creates a new scope which contains variables and functions relating to a QGIS project.
static QgsExpressionContextScope * globalScope()
Creates a new scope which contains variables and functions relating to the global QGIS context.
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.
Does vector analysis using the geos library and handles import, export, exception handling*.
Definition qgsgeos.h:137
Base class for storage of map layer elevation properties.
void profileGenerationPropertyChanged()
Emitted when any of the elevation properties which relate solely to generation of elevation profiles ...
void profileRenderingPropertyChanged()
Emitted when any of the elevation properties which relate solely to presentation of elevation results...
Base class for all map layer types.
Definition qgsmaplayer.h:75
QString id
Definition qgsmaplayer.h:78
Qgis::LayerType type
Definition qgsmaplayer.h:85
void dataChanged()
Data of layer changed.
virtual QgsMapLayerElevationProperties * elevationProperties()
Returns the layer's elevation properties.
Perform transforms between map coordinates and device coordinates.
A context for numeric formats.
void setLabelSuffixPlacement(Qgis::PlotAxisSuffixPlacement placement)
Sets the placement for the axis label suffixes.
Definition qgsplot.cpp:129
void setLabelSuffix(const QString &suffix)
Sets the axis label suffix.
Definition qgsplot.cpp:119
An abstract class for items that can be placed on a QgsPlotCanvas.
virtual void paint(QPainter *painter)=0
Paints the item.
Plot canvas is a class for displaying interactive 2d charts and plots.
bool event(QEvent *e) override
void plotAreaChanged()
Emitted whenever the visible area of the plot is changed.
void mouseMoveEvent(QMouseEvent *e) override
void resizeEvent(QResizeEvent *e) override
A class to represent a 2D point.
Definition qgspointxy.h:60
double y
Definition qgspointxy.h:64
double x
Definition qgspointxy.h:63
bool isEmpty() const
Returns true if the geometry is empty.
Definition qgspointxy.h:243
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:49
double z
Definition qgspoint.h:54
Encapsulates the context in which an elevation profile is to be generated.
double maximumErrorMapUnits() const
Returns the maximum allowed error in the generated result, in profile curve map units.
void setDpi(double dpi)
Sets the dpi (dots per inch) for the profie, to be used in size conversions.
void setMaximumErrorMapUnits(double error)
Sets the maximum allowed error in the generated result, in profile curve map units.
void setDistanceRange(const QgsDoubleRange &range)
Sets the range of distances to include in the generation.
void setElevationRange(const QgsDoubleRange &range)
Sets the range of elevations to include in the generation.
void setMapUnitsPerDistancePixel(double units)
Sets the number of map units per pixel in the distance dimension.
Encapsulates the context of identifying profile results.
double maximumPointElevationDelta
Maximum allowed snapping delta for the elevation values when identifying a point.
double maximumPointDistanceDelta
Maximum allowed snapping delta for the distance values when identifying a point.
QgsProject * project
Associated project.
double displayRatioElevationVsDistance
Display ratio of elevation vs distance units.
double maximumSurfaceDistanceDelta
Maximum allowed snapping delta for the distance values when identifying a continuous elevation surfac...
double maximumSurfaceElevationDelta
Maximum allowed snapping delta for the elevation values when identifying a continuous elevation surfa...
Generates and renders elevation profile plots.
QgsProfileSnapResult snapPoint(const QgsProfilePoint &point, const QgsProfileSnapContext &context)
Snap a point to the results.
void regenerateInvalidatedResults()
Starts a background regeneration of any invalidated results and immediately returns.
void invalidateAllRefinableSources()
Invalidates previous results from all refinable sources.
void cancelGeneration()
Stop the generation job - does not return until the job has terminated.
void startGeneration()
Start the generation job and immediately return.
QgsDoubleRange zRange() const
Returns the limits of the retrieved elevation values.
QVector< QgsProfileIdentifyResults > identify(const QgsProfilePoint &point, const QgsProfileIdentifyContext &context)
Identify results visible at the specified profile point.
bool isActive() const
Returns true if the generation job is currently running in background.
bool invalidateResults(QgsAbstractProfileSource *source)
Invalidates the profile results from the source with matching ID.
void replaceSource(QgsAbstractProfileSource *source)
Replaces the existing source with matching ID.
void setContext(const QgsProfileGenerationContext &context)
Sets the context in which the profile generation will occur.
void generationFinished()
Emitted when the profile generation is finished (or canceled).
Encapsulates a point on a distance-elevation profile.
double elevation() const
Returns the elevation of the point.
double distance() const
Returns the distance of the point.
bool isEmpty() const
Returns true if the point is empty.
Encapsulates properties and constraints relating to fetching elevation profiles from different source...
QgsProfileRequest & setExpressionContext(const QgsExpressionContext &context)
Sets the expression context used to evaluate expressions.
QgsProfileRequest & setTransformContext(const QgsCoordinateTransformContext &context)
Sets the transform context, for use when transforming coordinates from a source to the request's crs(...
QgsProfileRequest & setTerrainProvider(QgsAbstractTerrainProvider *provider)
Sets the terrain provider.
QgsProfileRequest & setTolerance(double tolerance)
Sets the tolerance of the request (in crs() units).
QgsProfileRequest & setCrs(const QgsCoordinateReferenceSystem &crs)
Sets the desired Coordinate Reference System (crs) for the profile.
Encapsulates the context of snapping a profile point.
double maximumPointDistanceDelta
Maximum allowed snapping delta for the distance values when snapping to a point.
double maximumSurfaceElevationDelta
Maximum allowed snapping delta for the elevation values when snapping to a continuous elevation surfa...
double maximumPointElevationDelta
Maximum allowed snapping delta for the elevation values when snapping to a point.
double maximumSurfaceDistanceDelta
Maximum allowed snapping delta for the distance values when snapping to a continuous elevation surfac...
double displayRatioElevationVsDistance
Display ratio of elevation vs distance units.
Encapsulates results of snapping a profile point.
bool isValid() const
Returns true if the result is a valid point.
QgsProfilePoint snappedPoint
Snapped point.
QList< QgsAbstractProfileSource * > profileSources() const
Returns a list of registered profile sources.
QgsAbstractTerrainProvider * terrainProvider()
Returns the project's terrain provider.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
Definition qgsproject.h:107
const QgsProjectElevationProperties * elevationProperties() const
Returns the project's elevation properties, which contains the project's elevation related settings.
QgsCoordinateTransformContext transformContext
Definition qgsproject.h:113
T lower() const
Returns the lower bound of the range.
Definition qgsrange.h:78
T upper() const
Returns the upper bound of the range.
Definition qgsrange.h:85
The class is used as a container of context for various read/write operations on other objects.
Contains information about the context of a rendering operation.
void setScaleFactor(double factor)
Sets the scaling factor for the render to convert painter units to physical sizes.
void setDevicePixelRatio(float ratio)
Sets the device pixel ratio.
QPainter * painter()
Returns the destination QPainter for the render operation.
QgsExpressionContext & expressionContext()
Gets the expression context.
void setMapToPixel(const QgsMapToPixel &mtp)
Sets the context's map to pixel transform, which transforms between map coordinates and device coordi...
static QgsRenderContext fromQPainter(QPainter *painter)
Creates a default render context given a pixel based QPainter destination.
A utility class for dynamic handling of changes to screen properties.
double screenDpi() const
Returns the current screen DPI for the screen that the parent widget appears on.
This class is a composition of two QSettings instances:
Definition qgssettings.h:64
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Container for all settings relating to text rendering.
void setColor(const QColor &color)
Sets the color that text will be rendered in.
static Q_INVOKABLE double fromUnitToUnitFactor(Qgis::DistanceUnit fromUnit, Qgis::DistanceUnit toUnit)
Returns the conversion factor between the specified distance units.
static Q_INVOKABLE QString toAbbreviatedString(Qgis::DistanceUnit unit)
Returns a translated abbreviation representing a distance unit.
Represents a vector layer which manages a vector based data sets.
void attributeValueChanged(QgsFeatureId fid, int idx, const QVariant &value)
Emitted whenever an attribute value change is done in the edit buffer.
void featureAdded(QgsFeatureId fid)
Emitted when a new feature has been added to the layer.
void featureDeleted(QgsFeatureId fid)
Emitted when a feature has been deleted.
void geometryChanged(QgsFeatureId fid, const QgsGeometry &geometry)
Emitted whenever a geometry change is done in the edit buffer.
Contains geos related utilities and functions.
Definition qgsgeos.h:75
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define BUILTIN_UNREACHABLE
Definition qgis.h:6119
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5465
const QgsCoordinateReferenceSystem & crs