QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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"
40
41#include <QWheelEvent>
42#include <QTimer>
43#include <QPalette>
44
46class QgsElevationProfilePlotItem : public Qgs2DPlot, public QgsPlotCanvasItem
47{
48 public:
49
50 QgsElevationProfilePlotItem( QgsElevationProfileCanvas *canvas )
51 : QgsPlotCanvasItem( canvas )
52 {
53 setYMinimum( 0 );
54 setYMaximum( 100 );
55
57 }
58
59 void setRenderer( QgsProfilePlotRenderer *renderer )
60 {
61 mRenderer = renderer;
62 }
63
64 void updateRect()
65 {
66 mRect = mCanvas->rect();
67 setSize( mRect.size() );
68
69 prepareGeometryChange();
70 setPos( mRect.topLeft() );
71
72 mImage = QImage();
73 mCachedImages.clear();
74 mPlotArea = QRectF();
75 update();
76 }
77
78 void updatePlot()
79 {
80 mImage = QImage();
81 mCachedImages.clear();
82 mPlotArea = QRectF();
83 update();
84 }
85
86 bool redrawResults( const QString &sourceId )
87 {
88 auto it = mCachedImages.find( sourceId );
89 if ( it == mCachedImages.end() )
90 return false;
91
92 mCachedImages.erase( it );
93 mImage = QImage();
94 return true;
95 }
96
97 QRectF boundingRect() const override
98 {
99 return mRect;
100 }
101
102 QString distanceSuffix() const
103 {
104 switch ( mDistanceUnit )
105 {
115 return QStringLiteral( " %1" ).arg( QgsUnitTypes::toAbbreviatedString( mDistanceUnit ) );
116
118 return QObject::tr( "°" );
120 return QString();
121 }
123 }
124
125 void setXAxisUnits( Qgis::DistanceUnit unit )
126 {
127 mDistanceUnit = unit;
128 xAxis().setLabelSuffix( distanceSuffix() );
129 update();
130 }
131
132 QRectF plotArea()
133 {
134 if ( !mPlotArea.isNull() )
135 return mPlotArea;
136
137 // force immediate recalculation of plot area
138 QgsRenderContext context;
139 if ( !scene()->views().isEmpty() )
140 context.setScaleFactor( scene()->views().at( 0 )->logicalDpiX() / 25.4 );
141
143 mPlotArea = interiorPlotArea( context );
144 return mPlotArea;
145 }
146
147 QgsProfilePoint canvasPointToPlotPoint( QPointF point )
148 {
149 const QRectF area = plotArea();
150 if ( !area.contains( point.x(), point.y() ) )
151 return QgsProfilePoint();
152
153 const double distance = ( point.x() - area.left() ) / area.width() * ( xMaximum() - xMinimum() ) * mXScaleFactor + xMinimum() * mXScaleFactor;
154 const double elevation = ( area.bottom() - point.y() ) / area.height() * ( yMaximum() - yMinimum() ) + yMinimum();
155 return QgsProfilePoint( distance, elevation );
156 }
157
158 QgsPointXY plotPointToCanvasPoint( const QgsProfilePoint &point )
159 {
160 if ( point.distance() < xMinimum() * mXScaleFactor || point.distance() > xMaximum()* mXScaleFactor || point.elevation() < yMinimum() || point.elevation() > yMaximum() )
161 return QgsPointXY();
162
163 const QRectF area = plotArea();
164
165 const double x = ( point.distance() - xMinimum() * mXScaleFactor ) / ( ( xMaximum() - xMinimum() ) * mXScaleFactor ) * ( area.width() ) + area.left();
166 const double y = area.bottom() - ( point.elevation() - yMinimum() ) / ( yMaximum() - yMinimum() ) * ( area.height() );
167 return QgsPointXY( x, y );
168 }
169
170 void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override
171 {
172 mPlotArea = plotArea;
173
174 if ( !mRenderer )
175 return;
176
177 const double pixelRatio = !scene()->views().empty() ? scene()->views().at( 0 )->devicePixelRatioF() : 1;
178
179 const QStringList sourceIds = mRenderer->sourceIds();
180 for ( const QString &source : sourceIds )
181 {
182 QImage plot;
183 auto it = mCachedImages.constFind( source );
184 if ( it != mCachedImages.constEnd() )
185 {
186 plot = it.value();
187 }
188 else
189 {
190 plot = mRenderer->renderToImage( plotArea.width() * pixelRatio,
191 plotArea.height() * pixelRatio, xMinimum() * mXScaleFactor, xMaximum() * mXScaleFactor, yMinimum(), yMaximum(), source, pixelRatio );
192 plot.setDevicePixelRatio( pixelRatio );
193 mCachedImages.insert( source, plot );
194 }
195 rc.painter()->drawImage( QPointF( plotArea.left(),
196 plotArea.top() ), plot );
197 }
198 }
199
200 void paint( QPainter *painter ) override
201 {
202 // cache rendering to an image, so we don't need to redraw the plot
203 if ( !mImage.isNull() )
204 {
205 painter->drawImage( QPointF( 0, 0 ), mImage );
206 }
207 else
208 {
209 const double pixelRatio = !scene()->views().empty() ? scene()->views().at( 0 )->devicePixelRatioF() : 1;
210 mImage = QImage( mRect.width() * pixelRatio, mRect.height() * pixelRatio, QImage::Format_ARGB32_Premultiplied );
211 mImage.setDevicePixelRatio( pixelRatio );
212 mImage.fill( Qt::transparent );
213
214 QPainter imagePainter( &mImage );
215 imagePainter.setRenderHint( QPainter::Antialiasing, true );
217 rc.setDevicePixelRatio( pixelRatio );
218
219 const double mapUnitsPerPixel = ( xMaximum() - xMinimum() ) * mXScaleFactor / plotArea().width();
220 rc.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
221
224
226 render( rc );
227 imagePainter.end();
228
229 painter->drawImage( QPointF( 0, 0 ), mImage );
230 }
231 }
232
233 QgsProject *mProject = nullptr;
234 double mXScaleFactor = 1.0;
235
237
238 private:
239
240 QImage mImage;
241
242 QMap< QString, QImage > mCachedImages;
243
244 QRectF mRect;
245 QRectF mPlotArea;
246 QgsProfilePlotRenderer *mRenderer = nullptr;
247};
248
249class QgsElevationProfileCrossHairsItem : public QgsPlotCanvasItem
250{
251 public:
252
253 QgsElevationProfileCrossHairsItem( QgsElevationProfileCanvas *canvas, QgsElevationProfilePlotItem *plotItem )
254 : QgsPlotCanvasItem( canvas )
255 , mPlotItem( plotItem )
256 {
257 }
258
259 void updateRect()
260 {
261 mRect = mCanvas->rect();
262
263 prepareGeometryChange();
264 setPos( mRect.topLeft() );
265 update();
266 }
267
268 void setPoint( const QgsProfilePoint &point )
269 {
270 mPoint = point;
271 update();
272 }
273
274 QRectF boundingRect() const override
275 {
276 return mRect;
277 }
278
279 void paint( QPainter *painter ) override
280 {
281 const QgsPointXY crossHairPlotPoint = mPlotItem->plotPointToCanvasPoint( mPoint );
282 if ( crossHairPlotPoint.isEmpty() )
283 return;
284
285 painter->save();
286 painter->setBrush( Qt::NoBrush );
287 QPen crossHairPen;
288 crossHairPen.setCosmetic( true );
289 crossHairPen.setWidthF( 1 );
290 crossHairPen.setStyle( Qt::DashLine );
291 crossHairPen.setCapStyle( Qt::FlatCap );
292 const QPalette scenePalette = mPlotItem->scene()->palette();
293 QColor penColor = scenePalette.color( QPalette::ColorGroup::Active, QPalette::Text );
294 penColor.setAlpha( 150 );
295 crossHairPen.setColor( penColor );
296 painter->setPen( crossHairPen );
297 painter->drawLine( QPointF( mPlotItem->plotArea().left(), crossHairPlotPoint.y() ), QPointF( mPlotItem->plotArea().right(), crossHairPlotPoint.y() ) );
298 painter->drawLine( QPointF( crossHairPlotPoint.x(), mPlotItem->plotArea().top() ), QPointF( crossHairPlotPoint.x(), mPlotItem->plotArea().bottom() ) );
299
300 // also render current point text
301 QgsNumericFormatContext numericContext;
302
303 const QString xCoordinateText = mPlotItem->xAxis().numericFormat()->formatDouble( mPoint.distance() / mPlotItem->mXScaleFactor, numericContext )
304 + mPlotItem->distanceSuffix();
305
306 const QString yCoordinateText = mPlotItem->yAxis().numericFormat()->formatDouble( mPoint.elevation(), numericContext );
307
308 QFont font;
309 const QFontMetrics fm( font );
310 const double height = fm.capHeight();
311 const double xWidth = fm.horizontalAdvance( xCoordinateText );
312 const double yWidth = fm.horizontalAdvance( yCoordinateText );
313 const double textAxisMargin = fm.horizontalAdvance( ' ' );
314
315 QPointF xCoordOrigin;
316 QPointF yCoordOrigin;
317
318 if ( mPoint.distance() < ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5 * mPlotItem->mXScaleFactor )
319 {
320 if ( mPoint.elevation() < ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5 )
321 {
322 // render x coordinate on right top (left top align)
323 xCoordOrigin = QPointF( crossHairPlotPoint.x() + textAxisMargin, mPlotItem->plotArea().top() + height + textAxisMargin );
324 // render y coordinate on right top (right bottom align)
325 yCoordOrigin = QPointF( mPlotItem->plotArea().right() - yWidth - textAxisMargin, crossHairPlotPoint.y() - textAxisMargin );
326 }
327 else
328 {
329 // render x coordinate on right bottom (left bottom align)
330 xCoordOrigin = QPointF( crossHairPlotPoint.x() + textAxisMargin, mPlotItem->plotArea().bottom() - textAxisMargin );
331 // render y coordinate on right bottom (right top align)
332 yCoordOrigin = QPointF( mPlotItem->plotArea().right() - yWidth - textAxisMargin, crossHairPlotPoint.y() + height + textAxisMargin );
333 }
334 }
335 else
336 {
337 if ( mPoint.elevation() < ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5 )
338 {
339 // render x coordinate on left top (right top align)
340 xCoordOrigin = QPointF( crossHairPlotPoint.x() - xWidth - textAxisMargin, mPlotItem->plotArea().top() + height + textAxisMargin );
341 // render y coordinate on left top (left bottom align)
342 yCoordOrigin = QPointF( mPlotItem->plotArea().left() + textAxisMargin, crossHairPlotPoint.y() - textAxisMargin );
343 }
344 else
345 {
346 // render x coordinate on left bottom (right bottom align)
347 xCoordOrigin = QPointF( crossHairPlotPoint.x() - xWidth - textAxisMargin, mPlotItem->plotArea().bottom() - textAxisMargin );
348 // render y coordinate on left bottom (left top align)
349 yCoordOrigin = QPointF( mPlotItem->plotArea().left() + textAxisMargin, crossHairPlotPoint.y() + height + textAxisMargin );
350 }
351 }
352
353 // semi opaque background color brush
354 QColor backgroundColor = mPlotItem->chartBackgroundSymbol()->color();
355 backgroundColor.setAlpha( 220 );
356 painter->setBrush( QBrush( backgroundColor ) );
357 painter->setPen( Qt::NoPen );
358 painter->drawRect( QRectF( xCoordOrigin.x() - textAxisMargin + 1, xCoordOrigin.y() - textAxisMargin - height + 1, xWidth + 2 * textAxisMargin - 2, height + 2 * textAxisMargin - 2 ) );
359 painter->drawRect( QRectF( yCoordOrigin.x() - textAxisMargin + 1, yCoordOrigin.y() - textAxisMargin - height + 1, yWidth + 2 * textAxisMargin - 2, height + 2 * textAxisMargin - 2 ) );
360
361 painter->setBrush( Qt::NoBrush );
362 painter->setPen( scenePalette.color( QPalette::ColorGroup::Active, QPalette::Text ) );
363
364 painter->drawText( xCoordOrigin, xCoordinateText );
365 painter->drawText( yCoordOrigin, yCoordinateText );
366 painter->restore();
367 }
368
369 private:
370
371 QRectF mRect;
372 QgsProfilePoint mPoint;
373 QgsElevationProfilePlotItem *mPlotItem = nullptr;
374};
376
377
379 : QgsPlotCanvas( parent )
380{
381 mScreenHelper = new QgsScreenHelper( this );
382
383 mPlotItem = new QgsElevationProfilePlotItem( this );
384
385 // follow system color scheme by default
386 setBackgroundColor( QColor() );
387
388 mCrossHairsItem = new QgsElevationProfileCrossHairsItem( this, mPlotItem );
389 mCrossHairsItem->setZValue( 100 );
390 mCrossHairsItem->hide();
391
392 // updating the profile plot is deferred on a timer, so that we don't trigger it too often
393 mDeferredRegenerationTimer = new QTimer( this );
394 mDeferredRegenerationTimer->setSingleShot( true );
395 mDeferredRegenerationTimer->stop();
396 connect( mDeferredRegenerationTimer, &QTimer::timeout, this, &QgsElevationProfileCanvas::startDeferredRegeneration );
397
398 mDeferredRedrawTimer = new QTimer( this );
399 mDeferredRedrawTimer->setSingleShot( true );
400 mDeferredRedrawTimer->stop();
401 connect( mDeferredRedrawTimer, &QTimer::timeout, this, &QgsElevationProfileCanvas::startDeferredRedraw );
402
403}
404
406{
407 if ( mCurrentJob )
408 {
409 mPlotItem->setRenderer( nullptr );
410 mCurrentJob->deleteLater();
411 mCurrentJob = nullptr;
412 }
413}
414
416{
417 if ( mCurrentJob )
418 {
419 mPlotItem->setRenderer( nullptr );
420 disconnect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
421 mCurrentJob->cancelGeneration();
422 mCurrentJob->deleteLater();
423 mCurrentJob = nullptr;
424 }
425}
426
428{
429 const double dxPercent = dx / mPlotItem->plotArea().width();
430 const double dyPercent = dy / mPlotItem->plotArea().height();
431
432 // these look backwards, but we are dragging the paper, not the view!
433 const double dxPlot = - dxPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
434 const double dyPlot = dyPercent * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
435
436 // no need to handle axis scale lock here, we aren't changing scales
437 mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
438 mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
439 mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
440 mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
441
442 refineResults();
443
444 mPlotItem->updatePlot();
445 emit plotAreaChanged();
446}
447
449{
450 if ( !mPlotItem->plotArea().contains( x, y ) )
451 return;
452
453 const double newCenterX = mPlotItem->xMinimum() + ( x - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
454 const double newCenterY = mPlotItem->yMinimum() + ( mPlotItem->plotArea().bottom() - y ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
455
456 const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
457 const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;
458
459 // no need to handle axis scale lock here, we aren't changing scales
460 mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
461 mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
462 mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
463 mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
464
465 refineResults();
466
467 mPlotItem->updatePlot();
468 emit plotAreaChanged();
469}
470
472{
473 scalePlot( factor, factor );
474 emit plotAreaChanged();
475}
476
477QgsProfileSnapContext QgsElevationProfileCanvas::snapContext() const
478{
479 const double toleranceInPixels = QFontMetrics( font() ).horizontalAdvance( ' ' );
480 const double xToleranceInPlotUnits = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) * toleranceInPixels;
481 const double yToleranceInPlotUnits = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * toleranceInPixels;
482
483 QgsProfileSnapContext context;
484 context.maximumSurfaceDistanceDelta = 2 * xToleranceInPlotUnits;
485 context.maximumSurfaceElevationDelta = 10 * yToleranceInPlotUnits;
486 context.maximumPointDistanceDelta = 4 * xToleranceInPlotUnits;
487 context.maximumPointElevationDelta = 4 * yToleranceInPlotUnits;
488 context.displayRatioElevationVsDistance = ( ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) )
489 / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) );
490
491 return context;
492}
493
494QgsProfileIdentifyContext QgsElevationProfileCanvas::identifyContext() const
495{
496 const double toleranceInPixels = QFontMetrics( font() ).horizontalAdvance( ' ' );
497 const double xToleranceInPlotUnits = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) * toleranceInPixels;
498 const double yToleranceInPlotUnits = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * toleranceInPixels;
499
501 context.maximumSurfaceDistanceDelta = 2 * xToleranceInPlotUnits;
502 context.maximumSurfaceElevationDelta = 10 * yToleranceInPlotUnits;
503 context.maximumPointDistanceDelta = 4 * xToleranceInPlotUnits;
504 context.maximumPointElevationDelta = 4 * yToleranceInPlotUnits;
505 context.displayRatioElevationVsDistance = ( ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) )
506 / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) );
507
508 context.project = mProject;
509
510 return context;
511}
512
513void QgsElevationProfileCanvas::setupLayerConnections( QgsMapLayer *layer, bool isDisconnect )
514{
515 if ( isDisconnect )
516 {
517 disconnect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileGenerationPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged );
518 disconnect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileRenderingPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged );
519 disconnect( layer, &QgsMapLayer::dataChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
520 }
521 else
522 {
523 connect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileGenerationPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged );
524 connect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileRenderingPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged );
525 connect( layer, &QgsMapLayer::dataChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
526 }
527
528 switch ( layer->type() )
529 {
531 {
532 QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer );
533 if ( isDisconnect )
534 {
535 disconnect( vl, &QgsVectorLayer::featureAdded, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
536 disconnect( vl, &QgsVectorLayer::featureDeleted, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
537 disconnect( vl, &QgsVectorLayer::geometryChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
538 disconnect( vl, &QgsVectorLayer::attributeValueChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
539 }
540 else
541 {
542 connect( vl, &QgsVectorLayer::featureAdded, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
543 connect( vl, &QgsVectorLayer::featureDeleted, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
544 connect( vl, &QgsVectorLayer::geometryChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
545 connect( vl, &QgsVectorLayer::attributeValueChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
546 }
547 break;
548 }
557 break;
558 }
559}
560
561void QgsElevationProfileCanvas::adjustRangeForAxisScaleLock( double &xMinimum, double &xMaximum, double &yMinimum, double &yMaximum ) const
562{
563 // ensures that we always "zoom out" to match horizontal/vertical scales
564 const double horizontalScale = ( xMaximum - xMinimum ) / mPlotItem->plotArea().width();
565 const double verticalScale = ( yMaximum - yMinimum ) / mPlotItem->plotArea().height();
566 if ( horizontalScale > verticalScale )
567 {
568 const double height = horizontalScale * mPlotItem->plotArea().height();
569 const double deltaHeight = ( yMaximum - yMinimum ) - height;
570 yMinimum += deltaHeight / 2;
571 yMaximum -= deltaHeight / 2;
572 }
573 else
574 {
575 const double width = verticalScale * mPlotItem->plotArea().width();
576 const double deltaWidth = ( ( xMaximum - xMinimum ) - width );
577 xMinimum += deltaWidth / 2;
578 xMaximum -= deltaWidth / 2;
579 }
580}
581
583{
584 return mDistanceUnit;
585}
586
588{
589 mDistanceUnit = unit;
590 const double oldMin = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
591 const double oldMax = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor;
592 mPlotItem->mXScaleFactor = QgsUnitTypes::fromUnitToUnitFactor( mDistanceUnit, mCrs.mapUnits() );
593 mPlotItem->setXAxisUnits( mDistanceUnit );
594 mPlotItem->setXMinimum( oldMin / mPlotItem->mXScaleFactor );
595 mPlotItem->setXMaximum( oldMax / mPlotItem->mXScaleFactor );
596 mPlotItem->updatePlot();
597}
598
600{
601 if ( !color.isValid() )
602 {
603 QPalette customPalette = qApp->palette();
604 const QColor baseColor = qApp->palette().color( QPalette::ColorRole::Base );
605 const QColor windowColor = qApp->palette().color( QPalette::ColorRole::Window );
606 customPalette.setColor( QPalette::ColorRole::Base, windowColor );
607 customPalette.setColor( QPalette::ColorRole::Window, baseColor );
608 setPalette( customPalette );
609 scene()->setPalette( customPalette );
610 }
611 else
612 {
613 // build custom palette
614 const bool isDarkTheme = color.lightnessF() < 0.5;
615 QPalette customPalette = qApp->palette();
616 customPalette.setColor( QPalette::ColorRole::Window, color );
617 if ( isDarkTheme )
618 {
619 customPalette.setColor( QPalette::ColorRole::Text, QColor( 255, 255, 255 ) );
620 customPalette.setColor( QPalette::ColorRole::Base, color.lighter( 120 ) );
621 }
622 else
623 {
624 customPalette.setColor( QPalette::ColorRole::Text, QColor( 0, 0, 0 ) );
625 customPalette.setColor( QPalette::ColorRole::Base, color.darker( 120 ) );
626 }
627
628 setPalette( customPalette );
629 scene()->setPalette( customPalette );
630 }
631
632 updateChartFromPalette();
633}
634
636{
637 return mLockAxisScales;
638}
639
641{
642 mLockAxisScales = lock;
643 if ( mLockAxisScales )
644 {
645 double xMinimum = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
646 double xMaximum = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor;
647 double yMinimum = mPlotItem->yMinimum();
648 double yMaximum = mPlotItem->yMaximum();
649 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
650 mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
651 mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
652 mPlotItem->setYMinimum( yMinimum );
653 mPlotItem->setYMaximum( yMaximum );
654
655 refineResults();
656 mPlotItem->updatePlot();
657 emit plotAreaChanged();
658 }
659}
660
662{
663 if ( !mCurrentJob || !mSnappingEnabled )
664 return QgsPointXY();
665
666 const QgsProfilePoint plotPoint = canvasPointToPlotPoint( point );
667
668 const QgsProfileSnapResult snappedPoint = mCurrentJob->snapPoint( plotPoint, snapContext() );
669 if ( !snappedPoint.isValid() )
670 return QgsPointXY();
671
672 return plotPointToCanvasPoint( snappedPoint.snappedPoint );
673}
674
675void QgsElevationProfileCanvas::scalePlot( double xFactor, double yFactor )
676{
677 if ( mLockAxisScales )
678 yFactor = xFactor;
679
680 const double currentWidth = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor;
681 const double currentHeight = mPlotItem->yMaximum() - mPlotItem->yMinimum();
682
683 const double newWidth = currentWidth / xFactor;
684 const double newHeight = currentHeight / yFactor;
685
686 const double currentCenterX = ( mPlotItem->xMinimum() + mPlotItem->xMaximum() ) * 0.5 * mPlotItem->mXScaleFactor;
687 const double currentCenterY = ( mPlotItem->yMinimum() + mPlotItem->yMaximum() ) * 0.5;
688
689 double xMinimum = currentCenterX - newWidth * 0.5;
690 double xMaximum = currentCenterX + newWidth * 0.5;
691 double yMinimum = currentCenterY - newHeight * 0.5;
692 double yMaximum = currentCenterY + newHeight * 0.5;
693 if ( mLockAxisScales )
694 {
695 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
696 }
697
698 mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
699 mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
700 mPlotItem->setYMinimum( yMinimum );
701 mPlotItem->setYMaximum( yMaximum );
702
703 refineResults();
704 mPlotItem->updatePlot();
705 emit plotAreaChanged();
706}
707
709{
710 const QRectF intersected = rect.intersected( mPlotItem->plotArea() );
711
712 double minX = ( intersected.left() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
713 double maxX = ( intersected.right() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
714 double minY = ( mPlotItem->plotArea().bottom() - intersected.bottom() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
715 double maxY = ( mPlotItem->plotArea().bottom() - intersected.top() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
716
717 if ( mLockAxisScales )
718 {
719 adjustRangeForAxisScaleLock( minX, maxX, minY, maxY );
720 }
721
722 mPlotItem->setXMinimum( minX / mPlotItem->mXScaleFactor );
723 mPlotItem->setXMaximum( maxX / mPlotItem->mXScaleFactor );
724 mPlotItem->setYMinimum( minY );
725 mPlotItem->setYMaximum( maxY );
726
727 refineResults();
728 mPlotItem->updatePlot();
729 emit plotAreaChanged();
730}
731
732void QgsElevationProfileCanvas::wheelZoom( QWheelEvent *event )
733{
734 //get mouse wheel zoom behavior settings
735 QgsSettings settings;
736 double zoomFactor = settings.value( QStringLiteral( "qgis/zoom_factor" ), 2 ).toDouble();
737 bool reverseZoom = settings.value( QStringLiteral( "qgis/reverse_wheel_zoom" ), false ).toBool();
738 bool zoomIn = reverseZoom ? event->angleDelta().y() < 0 : event->angleDelta().y() > 0;
739
740 // "Normal" mouse have an angle delta of 120, precision mouses provide data faster, in smaller steps
741 zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 120.0 * std::fabs( event->angleDelta().y() );
742
743 if ( event->modifiers() & Qt::ControlModifier )
744 {
745 //holding ctrl while wheel zooming results in a finer zoom
746 zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 20.0;
747 }
748
749 //calculate zoom scale factor
750 double scaleFactor = ( zoomIn ? 1 / zoomFactor : zoomFactor );
751
752 QRectF viewportRect = mPlotItem->plotArea();
753
754 if ( viewportRect.contains( event->position() ) )
755 {
756 //adjust view center
757 const double oldCenterX = 0.5 * ( mPlotItem->xMaximum() + mPlotItem->xMinimum() );
758 const double oldCenterY = 0.5 * ( mPlotItem->yMaximum() + mPlotItem->yMinimum() );
759
760 const double eventPosX = ( event->position().x() - viewportRect.left() ) / viewportRect.width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
761 const double eventPosY = ( viewportRect.bottom() - event->position().y() ) / viewportRect.height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
762
763 const double newCenterX = eventPosX + ( ( oldCenterX - eventPosX ) * scaleFactor );
764 const double newCenterY = eventPosY + ( ( oldCenterY - eventPosY ) * scaleFactor );
765
766 const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
767 const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;
768
769 // don't need to handle axis scale lock here, we are always changing axis by the same scale
770 mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
771 mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
772 mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
773 mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
774 }
775
776 //zoom plot
777 if ( zoomIn )
778 {
779 scalePlot( zoomFactor );
780 }
781 else
782 {
783 scalePlot( 1 / zoomFactor );
784 }
785 emit plotAreaChanged();
786}
787
789{
791 if ( e->isAccepted() )
792 {
793 mCrossHairsItem->hide();
794 return;
795 }
796
797 QgsProfilePoint plotPoint = canvasPointToPlotPoint( e->pos() );
798 if ( mCurrentJob && mSnappingEnabled && !plotPoint.isEmpty() )
799 {
800 const QgsProfileSnapResult snapResult = mCurrentJob->snapPoint( plotPoint, snapContext() );
801 if ( snapResult.isValid() )
802 plotPoint = snapResult.snappedPoint;
803 }
804
805 if ( plotPoint.isEmpty() )
806 {
807 mCrossHairsItem->hide();
808 }
809 else
810 {
811 mCrossHairsItem->setPoint( plotPoint );
812 mCrossHairsItem->show();
813 }
814 emit canvasPointHovered( e->pos(), plotPoint );
815}
816
818{
819 return mPlotItem->plotArea();
820}
821
823{
824 if ( !mProject || !profileCurve() )
825 return;
826
827 if ( mCurrentJob )
828 {
829 mPlotItem->setRenderer( nullptr );
830 disconnect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
831 mCurrentJob->deleteLater();
832 mCurrentJob = nullptr;
833 }
834
835 QgsProfileRequest request( profileCurve()->clone() );
836 request.setCrs( mCrs );
837 request.setTolerance( mTolerance );
838 request.setTransformContext( mProject->transformContext() );
839 request.setTerrainProvider( mProject->elevationProperties()->terrainProvider() ? mProject->elevationProperties()->terrainProvider()->clone() : nullptr );
840 QgsExpressionContext context;
843 request.setExpressionContext( context );
844
845 const QList< QgsMapLayer * > layersToGenerate = layers();
846 QList< QgsAbstractProfileSource * > sources;
847 sources.reserve( layersToGenerate .size() );
848 for ( QgsMapLayer *layer : layersToGenerate )
849 {
850 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
851 sources.append( source );
852 }
853
854 mCurrentJob = new QgsProfilePlotRenderer( sources, request );
855 connect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
856
857 QgsProfileGenerationContext generationContext;
858 generationContext.setDpi( mScreenHelper->screenDpi() );
859 generationContext.setMaximumErrorMapUnits( MAX_ERROR_PIXELS * ( mProfileCurve->length() ) / mPlotItem->plotArea().width() );
860 generationContext.setMapUnitsPerDistancePixel( mProfileCurve->length() / mPlotItem->plotArea().width() );
861 mCurrentJob->setContext( generationContext );
862
863 mCurrentJob->startGeneration();
864 mPlotItem->setRenderer( mCurrentJob );
865
866 emit activeJobCountChanged( 1 );
867}
868
870{
871 mZoomFullWhenJobFinished = true;
872}
873
874void QgsElevationProfileCanvas::generationFinished()
875{
876 if ( !mCurrentJob )
877 return;
878
879 emit activeJobCountChanged( 0 );
880
881 if ( mZoomFullWhenJobFinished )
882 {
883 // we only zoom full for the initial generation
884 mZoomFullWhenJobFinished = false;
885 zoomFull();
886 }
887 else
888 {
889 // here we should invalidate cached results only for the layers which have been refined
890
891 // and if no layers are being refeined, don't invalidate anything
892
893 mPlotItem->updatePlot();
894 }
895
896 if ( mForceRegenerationAfterCurrentJobCompletes )
897 {
898 mForceRegenerationAfterCurrentJobCompletes = false;
899 mCurrentJob->invalidateAllRefinableSources();
900 scheduleDeferredRegeneration();
901 }
902}
903
904void QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged()
905{
906 // TODO -- handle nicely when existing job is in progress
907 if ( !mCurrentJob || mCurrentJob->isActive() )
908 return;
909
910 QgsMapLayerElevationProperties *properties = qobject_cast< QgsMapLayerElevationProperties * >( sender() );
911 if ( !properties )
912 return;
913
914 if ( QgsMapLayer *layer = qobject_cast< QgsMapLayer * >( properties->parent() ) )
915 {
916 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
917 {
918 if ( mCurrentJob->invalidateResults( source ) )
919 scheduleDeferredRegeneration();
920 }
921 }
922}
923
924void QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged()
925{
926 // TODO -- handle nicely when existing job is in progress
927 if ( !mCurrentJob || mCurrentJob->isActive() )
928 return;
929
930 QgsMapLayerElevationProperties *properties = qobject_cast< QgsMapLayerElevationProperties * >( sender() );
931 if ( !properties )
932 return;
933
934 if ( QgsMapLayer *layer = qobject_cast< QgsMapLayer * >( properties->parent() ) )
935 {
936 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
937 {
938 mCurrentJob->replaceSource( source );
939 }
940 if ( mPlotItem->redrawResults( layer->id() ) )
941 scheduleDeferredRedraw();
942 }
943}
944
945void QgsElevationProfileCanvas::regenerateResultsForLayer()
946{
947 if ( QgsMapLayer *layer = qobject_cast< QgsMapLayer * >( sender() ) )
948 {
949 if ( QgsAbstractProfileSource *source = dynamic_cast< QgsAbstractProfileSource * >( layer ) )
950 {
951 if ( mCurrentJob->invalidateResults( source ) )
952 scheduleDeferredRegeneration();
953 }
954 }
955}
956
957void QgsElevationProfileCanvas::scheduleDeferredRegeneration()
958{
959 if ( !mDeferredRegenerationScheduled )
960 {
961 mDeferredRegenerationTimer->start( 1 );
962 mDeferredRegenerationScheduled = true;
963 }
964}
965
966void QgsElevationProfileCanvas::scheduleDeferredRedraw()
967{
968 if ( !mDeferredRedrawScheduled )
969 {
970 mDeferredRedrawTimer->start( 1 );
971 mDeferredRedrawScheduled = true;
972 }
973}
974
975void QgsElevationProfileCanvas::startDeferredRegeneration()
976{
977 if ( mCurrentJob && !mCurrentJob->isActive() )
978 {
979 emit activeJobCountChanged( 1 );
980 mCurrentJob->regenerateInvalidatedResults();
981 }
982 else if ( mCurrentJob )
983 {
984 mForceRegenerationAfterCurrentJobCompletes = true;
985 }
986
987 mDeferredRegenerationScheduled = false;
988}
989
990void QgsElevationProfileCanvas::startDeferredRedraw()
991{
992 mPlotItem->update();
993 mDeferredRedrawScheduled = false;
994}
995
996void QgsElevationProfileCanvas::refineResults()
997{
998 if ( mCurrentJob )
999 {
1001 context.setDpi( mScreenHelper->screenDpi() );
1002 const double plotDistanceRange = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor;
1003 const double plotElevationRange = mPlotItem->yMaximum() - mPlotItem->yMinimum();
1004 const double plotDistanceUnitsPerPixel = plotDistanceRange / mPlotItem->plotArea().width();
1005
1006 // we round the actual desired map error down to just one significant figure, to avoid tiny differences
1007 // as the plot is panned
1008 const double targetMaxErrorInMapUnits = MAX_ERROR_PIXELS * plotDistanceUnitsPerPixel;
1009 const double factor = std::pow( 10.0, 1 - std::ceil( std::log10( std::fabs( targetMaxErrorInMapUnits ) ) ) );
1010 const double roundedErrorInMapUnits = std::floor( targetMaxErrorInMapUnits * factor ) / factor;
1011 context.setMaximumErrorMapUnits( roundedErrorInMapUnits );
1012
1013 context.setMapUnitsPerDistancePixel( plotDistanceUnitsPerPixel );
1014
1015 // for similar reasons we round the minimum distance off to multiples of the maximum error in map units
1016 const double distanceMin = std::floor( ( mPlotItem->xMinimum() * mPlotItem->mXScaleFactor - plotDistanceRange * 0.05 ) / context.maximumErrorMapUnits() ) * context.maximumErrorMapUnits();
1017 context.setDistanceRange( QgsDoubleRange( std::max( 0.0, distanceMin ),
1018 mPlotItem->xMaximum() * mPlotItem->mXScaleFactor + plotDistanceRange * 0.05 ) );
1019
1020 context.setElevationRange( QgsDoubleRange( mPlotItem->yMinimum() - plotElevationRange * 0.05,
1021 mPlotItem->yMaximum() + plotElevationRange * 0.05 ) );
1022 mCurrentJob->setContext( context );
1023 }
1024 scheduleDeferredRegeneration();
1025}
1026
1027void QgsElevationProfileCanvas::updateChartFromPalette()
1028{
1029 const QPalette chartPalette = palette();
1030 setBackgroundBrush( QBrush( chartPalette.color( QPalette::ColorRole::Base ) ) );
1031 {
1032 QgsTextFormat textFormat = mPlotItem->xAxis().textFormat();
1033 textFormat.setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::Text ) );
1034 mPlotItem->xAxis().setTextFormat( textFormat );
1035 mPlotItem->yAxis().setTextFormat( textFormat );
1036 }
1037 {
1038 std::unique_ptr< QgsFillSymbol > chartFill( mPlotItem->chartBackgroundSymbol()->clone() );
1039 chartFill->setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Window ) );
1040 mPlotItem->setChartBackgroundSymbol( chartFill.release() );
1041 }
1042 {
1043 std::unique_ptr< QgsFillSymbol > chartBorder( mPlotItem->chartBorderSymbol()->clone() );
1044 chartBorder->setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text ) );
1045 mPlotItem->setChartBorderSymbol( chartBorder.release() );
1046 }
1047 {
1048 std::unique_ptr< QgsLineSymbol > chartMajorSymbol( mPlotItem->xAxis().gridMajorSymbol()->clone() );
1049 QColor c = chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text );
1050 c.setAlpha( 150 );
1051 chartMajorSymbol->setColor( c );
1052 mPlotItem->xAxis().setGridMajorSymbol( chartMajorSymbol->clone() );
1053 mPlotItem->yAxis().setGridMajorSymbol( chartMajorSymbol.release() );
1054 }
1055 {
1056 std::unique_ptr< QgsLineSymbol > chartMinorSymbol( mPlotItem->xAxis().gridMinorSymbol()->clone() );
1057 QColor c = chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text );
1058 c.setAlpha( 50 );
1059 chartMinorSymbol->setColor( c );
1060 mPlotItem->xAxis().setGridMinorSymbol( chartMinorSymbol->clone() );
1061 mPlotItem->yAxis().setGridMinorSymbol( chartMinorSymbol.release() );
1062 }
1063 mPlotItem->updatePlot();
1064}
1065
1067{
1068 if ( !mPlotItem->plotArea().contains( point.x(), point.y() ) )
1069 return QgsProfilePoint();
1070
1071 return mPlotItem->canvasPointToPlotPoint( point );
1072}
1073
1075{
1076 return mPlotItem->plotPointToCanvasPoint( point );
1077}
1078
1080{
1081 mProject = project;
1082 mPlotItem->mProject = project;
1083}
1084
1086{
1087 mCrs = crs;
1088}
1089
1091{
1092 mProfileCurve.reset( curve );
1093}
1094
1096{
1097 return mProfileCurve.get();
1098}
1099
1101{
1102 mTolerance = tolerance;
1103}
1104
1106{
1107 return mCrs;
1108}
1109
1110void QgsElevationProfileCanvas::setLayers( const QList<QgsMapLayer *> &layers )
1111{
1112 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
1113 {
1114 setupLayerConnections( layer, true );
1115 }
1116
1117 // filter list, removing null layers and invalid layers
1118 auto filteredList = layers;
1119 filteredList.erase( std::remove_if( filteredList.begin(), filteredList.end(),
1120 []( QgsMapLayer * layer )
1121 {
1122 return !layer || !layer->isValid();
1123 } ), filteredList.end() );
1124
1125 mLayers = _qgis_listRawToQPointer( filteredList );
1126 for ( QgsMapLayer *layer : std::as_const( mLayers ) )
1127 {
1128 setupLayerConnections( layer, false );
1129 }
1130}
1131
1132QList<QgsMapLayer *> QgsElevationProfileCanvas::layers() const
1133{
1134 return _qgis_listQPointerToRaw( mLayers );
1135}
1136
1138{
1140
1141 if ( mLockAxisScales )
1142 {
1143 double xMinimum = mPlotItem->xMinimum();
1144 double xMaximum = mPlotItem->xMaximum();
1145 double yMinimum = mPlotItem->yMinimum();
1146 double yMaximum = mPlotItem->yMaximum();
1147 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
1148 mPlotItem->setXMinimum( xMinimum );
1149 mPlotItem->setXMaximum( xMaximum );
1150 mPlotItem->setYMinimum( yMinimum );
1151 mPlotItem->setYMaximum( yMaximum );
1152 }
1153
1154 mPlotItem->updateRect();
1155 mCrossHairsItem->updateRect();
1156}
1157
1159{
1160 QgsPlotCanvas::paintEvent( event );
1161
1162 if ( !mFirstDrawOccurred )
1163 {
1164 // 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).
1165 mFirstDrawOccurred = true;
1166 mPlotItem->updateRect();
1167 mCrossHairsItem->updateRect();
1168 }
1169}
1170
1172{
1173 if ( !mPlotItem->plotArea().contains( point.x(), point.y() ) )
1174 return QgsPoint();
1175
1176 if ( !mProfileCurve )
1177 return QgsPoint();
1178
1179 const double dx = point.x() - mPlotItem->plotArea().left();
1180
1181 const double distanceAlongPlotPercent = dx / mPlotItem->plotArea().width();
1182 double distanceAlongCurveLength = distanceAlongPlotPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
1183
1184 std::unique_ptr< QgsPoint > mapXyPoint( mProfileCurve->interpolatePoint( distanceAlongCurveLength ) );
1185 if ( !mapXyPoint )
1186 return QgsPoint();
1187
1188 const double mapZ = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * ( mPlotItem->plotArea().bottom() - point.y() ) + mPlotItem->yMinimum();
1189
1190 return QgsPoint( mapXyPoint->x(), mapXyPoint->y(), mapZ );
1191}
1192
1194{
1195 if ( !mProfileCurve )
1196 return QgsPointXY();
1197
1198 QgsGeos geos( mProfileCurve.get() );
1199 QString error;
1200 const double distanceAlongCurve = geos.lineLocatePoint( point, &error );
1201
1202 const double distanceAlongCurveOnPlot = distanceAlongCurve - mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
1203 const double distanceAlongCurvePercent = distanceAlongCurveOnPlot / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor );
1204 const double distanceAlongPlotRect = distanceAlongCurvePercent * mPlotItem->plotArea().width();
1205
1206 const double canvasX = mPlotItem->plotArea().left() + distanceAlongPlotRect;
1207
1208 double canvasY = 0;
1209 if ( std::isnan( point.z() ) || point.z() < mPlotItem->yMinimum() )
1210 {
1211 canvasY = mPlotItem->plotArea().top();
1212 }
1213 else if ( point.z() > mPlotItem->yMaximum() )
1214 {
1215 canvasY = mPlotItem->plotArea().bottom();
1216 }
1217 else
1218 {
1219 const double yPercent = ( point.z() - mPlotItem->yMinimum() ) / ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
1220 canvasY = mPlotItem->plotArea().bottom() - mPlotItem->plotArea().height() * yPercent;
1221 }
1222
1223 return QgsPointXY( canvasX, canvasY );
1224}
1225
1227{
1228 if ( !mCurrentJob )
1229 return;
1230
1231 const QgsDoubleRange zRange = mCurrentJob->zRange();
1232
1233 double yMinimum = 0;
1234 double yMaximum = 0;
1235
1236 if ( zRange.upper() < zRange.lower() )
1237 {
1238 // invalid range, e.g. no features found in plot!
1239 yMinimum = 0;
1240 yMaximum = 10;
1241 }
1242 else if ( qgsDoubleNear( zRange.lower(), zRange.upper(), 0.0000001 ) )
1243 {
1244 // corner case ... a zero height plot! Just pick an arbitrary +/- 5 height range.
1245 yMinimum = zRange.lower() - 5;
1246 yMaximum = zRange.lower() + 5;
1247 }
1248 else
1249 {
1250 // add 5% margin to height range
1251 const double margin = ( zRange.upper() - zRange.lower() ) * 0.05;
1252 yMinimum = zRange.lower() - margin;
1253 yMaximum = zRange.upper() + margin;
1254 }
1255
1256 const double profileLength = profileCurve()->length();
1257 double xMinimum = 0;
1258 // just 2% margin to max distance -- any more is overkill and wasted space
1259 double xMaximum = profileLength * 1.02;
1260
1261 if ( mLockAxisScales )
1262 {
1263 adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
1264 }
1265
1266 mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
1267 mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
1268 mPlotItem->setYMinimum( yMinimum );
1269 mPlotItem->setYMaximum( yMaximum );
1270
1271 refineResults();
1272 mPlotItem->updatePlot();
1273 emit plotAreaChanged();
1274}
1275
1276void QgsElevationProfileCanvas::setVisiblePlotRange( double minimumDistance, double maximumDistance, double minimumElevation, double maximumElevation )
1277{
1278 if ( mLockAxisScales )
1279 {
1280 adjustRangeForAxisScaleLock( minimumDistance, maximumDistance, minimumElevation, maximumElevation );
1281 }
1282
1283 mPlotItem->setYMinimum( minimumElevation );
1284 mPlotItem->setYMaximum( maximumElevation );
1285 mPlotItem->setXMinimum( minimumDistance / mPlotItem->mXScaleFactor );
1286 mPlotItem->setXMaximum( maximumDistance / mPlotItem->mXScaleFactor );
1287 refineResults();
1288 mPlotItem->updatePlot();
1289 emit plotAreaChanged();
1290}
1291
1293{
1294 return QgsDoubleRange( mPlotItem->xMinimum() * mPlotItem->mXScaleFactor, mPlotItem->xMaximum() * mPlotItem->mXScaleFactor );
1295}
1296
1298{
1299 return QgsDoubleRange( mPlotItem->yMinimum(), mPlotItem->yMaximum() );
1300}
1301
1303{
1304 return *mPlotItem;
1305}
1306
1308class QgsElevationProfilePlot : public Qgs2DPlot
1309{
1310 public:
1311
1312 QgsElevationProfilePlot( QgsProfilePlotRenderer *renderer )
1313 : mRenderer( renderer )
1314 {
1315 }
1316
1317 void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override
1318 {
1319 if ( !mRenderer )
1320 return;
1321
1322 rc.painter()->translate( plotArea.left(), plotArea.top() );
1323 mRenderer->render( rc, plotArea.width(), plotArea.height(), xMinimum() * mXScale, xMaximum() * mXScale, yMinimum(), yMaximum() );
1324 rc.painter()->translate( -plotArea.left(), -plotArea.top() );
1325 }
1326
1327 double mXScale = 1;
1328
1329 private:
1330
1331 QgsProfilePlotRenderer *mRenderer = nullptr;
1332};
1334
1335void QgsElevationProfileCanvas::render( QgsRenderContext &context, double width, double height, const Qgs2DPlot &plotSettings )
1336{
1337 if ( !mCurrentJob )
1338 return;
1339
1342
1343 QgsElevationProfilePlot profilePlot( mCurrentJob );
1344
1345 // 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...
1346 QDomDocument doc;
1347 QDomElement elem = doc.createElement( QStringLiteral( "plot" ) );
1348 QgsReadWriteContext rwContext;
1349 plotSettings.writeXml( elem, doc, rwContext );
1350 profilePlot.readXml( elem, rwContext );
1351
1352 profilePlot.mXScale = mPlotItem->mXScaleFactor;
1353 profilePlot.xAxis().setLabelSuffix( mPlotItem->xAxis().labelSuffix() );
1354 profilePlot.xAxis().setLabelSuffixPlacement( mPlotItem->xAxis().labelSuffixPlacement() );
1355
1356 profilePlot.setSize( QSizeF( width, height ) );
1357 profilePlot.render( context );
1358}
1359
1360QVector<QgsProfileIdentifyResults> QgsElevationProfileCanvas::identify( QPointF point )
1361{
1362 if ( !mCurrentJob )
1363 return {};
1364
1365 const QgsProfilePoint plotPoint = canvasPointToPlotPoint( point );
1366
1367 return mCurrentJob->identify( plotPoint, identifyContext() );
1368}
1369
1370QVector<QgsProfileIdentifyResults> QgsElevationProfileCanvas::identify( const QRectF &rect )
1371{
1372 if ( !mCurrentJob )
1373 return {};
1374
1375 const QgsProfilePoint topLeftPlotPoint = canvasPointToPlotPoint( rect.topLeft() );
1376 const QgsProfilePoint bottomRightPlotPoint = canvasPointToPlotPoint( rect.bottomRight() );
1377
1378 double distance1 = topLeftPlotPoint.distance();
1379 double distance2 = bottomRightPlotPoint.distance();
1380 if ( distance2 < distance1 )
1381 std::swap( distance1, distance2 );
1382
1383 double elevation1 = topLeftPlotPoint.elevation();
1384 double elevation2 = bottomRightPlotPoint.elevation();
1385 if ( elevation2 < elevation1 )
1386 std::swap( elevation1, elevation2 );
1387
1388 return mCurrentJob->identify( QgsDoubleRange( distance1, distance2 ), QgsDoubleRange( elevation1, elevation2 ), identifyContext() );
1389}
1390
1392{
1393 setProfileCurve( nullptr );
1394 mPlotItem->setRenderer( nullptr );
1395 mPlotItem->updatePlot();
1396}
1397
1399{
1400 mSnappingEnabled = enabled;
1401}
@ FirstAndLastLabels
Place suffix after the first and last label values only.
DistanceUnit
Units of distance.
Definition: qgis.h:4124
@ 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.
This class represents a coordinate reference system (CRS).
Q_GADGET Qgis::DistanceUnit mapUnits
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:98
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() const
Returns the layer's unique ID, which is used to access this layer from QgsProject.
Qgis::LayerType type
Definition: qgsmaplayer.h:82
void dataChanged()
Data of layer changed.
virtual QgsMapLayerElevationProperties * elevationProperties()
Returns the layer's elevation properties.
Definition: qgsmaplayer.h:1633
Perform transforms between map coordinates and device coordinates.
Definition: qgsmaptopixel.h:39
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.
Definition: qgsplotcanvas.h:54
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
Q_GADGET 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.
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.
Definition: qgstextformat.h:41
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:36
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:5853
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207
const QgsCoordinateReferenceSystem & crs