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