QGIS/src/gui/elevation/qgselevationprofilecanvas.cpp
Simon Lopez 1c2c2b3252 Add QgsMapLayer::profileSource
This commit adds a virtual method to QgsMapLayer to enable the creation of custom
elevation profiles for any type of layer.
The methods returns a pointer to a helper class of type
QgsAbstractProfileSource which is a factory for profile generators
(cf. documentation for QgsAbstractProfileSource).
Existing layers that derive from QgsAbstractProfileSource just override
this method returning a *this* pointer.
As the method is *sipified* it can be used in python to declare custom
profile source for classes derived from PluginLayer (ownership of the
QgsAbstractProfileSource must then be managed on the python side).
2025-10-01 12:04:04 +00:00

1451 lines
52 KiB
C++

/***************************************************************************
qgselevationprofilecanvas.cpp
-----------------
begin : March 2022
copyright : (C) 2022 by Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsapplication.h"
#include "qgselevationprofilecanvas.h"
#include "moc_qgselevationprofilecanvas.cpp"
#include "qgsmaplayerlistutils_p.h"
#include "qgsplotcanvasitem.h"
#include "qgsprofilerequest.h"
#include "qgsabstractprofilesource.h"
#include "qgscurve.h"
#include "qgsprojectelevationproperties.h"
#include "qgsterrainprovider.h"
#include "qgsabstractprofilegenerator.h"
#include "qgsprofilerenderer.h"
#include "qgspoint.h"
#include "qgsgeos.h"
#include "qgsplot.h"
#include "qgsnumericformat.h"
#include "qgsexpressioncontextutils.h"
#include "qgsprofilesnapping.h"
#include "qgsmaplayerelevationproperties.h"
#include "qgsscreenhelper.h"
#include "qgsfillsymbol.h"
#include "qgsprofilesourceregistry.h"
#include <QWheelEvent>
#include <QTimer>
#include <QPalette>
///@cond PRIVATE
class QgsElevationProfilePlotItem : public Qgs2DPlot, public QgsPlotCanvasItem
{
public:
QgsElevationProfilePlotItem( QgsElevationProfileCanvas *canvas )
: QgsPlotCanvasItem( canvas )
{
setYMinimum( 0 );
setYMaximum( 100 );
xAxis().setLabelSuffixPlacement( Qgis::PlotAxisSuffixPlacement::FirstAndLastLabels );
}
void setRenderer( QgsProfilePlotRenderer *renderer )
{
mRenderer = renderer;
}
void updateRect()
{
mRect = mCanvas->rect();
setSize( mRect.size() );
prepareGeometryChange();
setPos( mRect.topLeft() );
mImage = QImage();
mCachedImages.clear();
mPlotArea = QRectF();
update();
}
void updatePlot()
{
mImage = QImage();
mCachedImages.clear();
mPlotArea = QRectF();
update();
}
bool redrawResults( const QString &sourceId )
{
auto it = mCachedImages.find( sourceId );
if ( it == mCachedImages.end() )
return false;
mCachedImages.erase( it );
mImage = QImage();
return true;
}
QRectF boundingRect() const override
{
return mRect;
}
QString distanceSuffix() const
{
switch ( mDistanceUnit )
{
case Qgis::DistanceUnit::Meters:
case Qgis::DistanceUnit::Kilometers:
case Qgis::DistanceUnit::Feet:
case Qgis::DistanceUnit::NauticalMiles:
case Qgis::DistanceUnit::Yards:
case Qgis::DistanceUnit::Miles:
case Qgis::DistanceUnit::Centimeters:
case Qgis::DistanceUnit::Millimeters:
case Qgis::DistanceUnit::Inches:
case Qgis::DistanceUnit::ChainsInternational:
case Qgis::DistanceUnit::ChainsBritishBenoit1895A:
case Qgis::DistanceUnit::ChainsBritishBenoit1895B:
case Qgis::DistanceUnit::ChainsBritishSears1922Truncated:
case Qgis::DistanceUnit::ChainsBritishSears1922:
case Qgis::DistanceUnit::ChainsClarkes:
case Qgis::DistanceUnit::ChainsUSSurvey:
case Qgis::DistanceUnit::FeetBritish1865:
case Qgis::DistanceUnit::FeetBritish1936:
case Qgis::DistanceUnit::FeetBritishBenoit1895A:
case Qgis::DistanceUnit::FeetBritishBenoit1895B:
case Qgis::DistanceUnit::FeetBritishSears1922Truncated:
case Qgis::DistanceUnit::FeetBritishSears1922:
case Qgis::DistanceUnit::FeetClarkes:
case Qgis::DistanceUnit::FeetGoldCoast:
case Qgis::DistanceUnit::FeetIndian:
case Qgis::DistanceUnit::FeetIndian1937:
case Qgis::DistanceUnit::FeetIndian1962:
case Qgis::DistanceUnit::FeetIndian1975:
case Qgis::DistanceUnit::FeetUSSurvey:
case Qgis::DistanceUnit::LinksInternational:
case Qgis::DistanceUnit::LinksBritishBenoit1895A:
case Qgis::DistanceUnit::LinksBritishBenoit1895B:
case Qgis::DistanceUnit::LinksBritishSears1922Truncated:
case Qgis::DistanceUnit::LinksBritishSears1922:
case Qgis::DistanceUnit::LinksClarkes:
case Qgis::DistanceUnit::LinksUSSurvey:
case Qgis::DistanceUnit::YardsBritishBenoit1895A:
case Qgis::DistanceUnit::YardsBritishBenoit1895B:
case Qgis::DistanceUnit::YardsBritishSears1922Truncated:
case Qgis::DistanceUnit::YardsBritishSears1922:
case Qgis::DistanceUnit::YardsClarkes:
case Qgis::DistanceUnit::YardsIndian:
case Qgis::DistanceUnit::YardsIndian1937:
case Qgis::DistanceUnit::YardsIndian1962:
case Qgis::DistanceUnit::YardsIndian1975:
case Qgis::DistanceUnit::MilesUSSurvey:
case Qgis::DistanceUnit::Fathoms:
case Qgis::DistanceUnit::MetersGermanLegal:
return QStringLiteral( " %1" ).arg( QgsUnitTypes::toAbbreviatedString( mDistanceUnit ) );
case Qgis::DistanceUnit::Degrees:
return QObject::tr( "°" );
case Qgis::DistanceUnit::Unknown:
return QString();
}
BUILTIN_UNREACHABLE
}
void setXAxisUnits( Qgis::DistanceUnit unit )
{
mDistanceUnit = unit;
xAxis().setLabelSuffix( distanceSuffix() );
update();
}
QRectF plotArea()
{
if ( !mPlotArea.isNull() )
return mPlotArea;
// force immediate recalculation of plot area
QgsRenderContext context;
if ( !scene()->views().isEmpty() )
context.setScaleFactor( scene()->views().at( 0 )->logicalDpiX() / 25.4 );
calculateOptimisedIntervals( context );
mPlotArea = interiorPlotArea( context );
return mPlotArea;
}
QgsProfilePoint canvasPointToPlotPoint( QPointF point )
{
const QRectF area = plotArea();
if ( !area.contains( point.x(), point.y() ) )
return QgsProfilePoint();
const double distance = ( point.x() - area.left() ) / area.width() * ( xMaximum() - xMinimum() ) * mXScaleFactor + xMinimum() * mXScaleFactor;
const double elevation = ( area.bottom() - point.y() ) / area.height() * ( yMaximum() - yMinimum() ) + yMinimum();
return QgsProfilePoint( distance, elevation );
}
QgsPointXY plotPointToCanvasPoint( const QgsProfilePoint &point )
{
if ( point.distance() < xMinimum() * mXScaleFactor || point.distance() > xMaximum() * mXScaleFactor || point.elevation() < yMinimum() || point.elevation() > yMaximum() )
return QgsPointXY();
const QRectF area = plotArea();
const double x = ( point.distance() - xMinimum() * mXScaleFactor ) / ( ( xMaximum() - xMinimum() ) * mXScaleFactor ) * ( area.width() ) + area.left();
const double y = area.bottom() - ( point.elevation() - yMinimum() ) / ( yMaximum() - yMinimum() ) * ( area.height() );
return QgsPointXY( x, y );
}
void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override
{
mPlotArea = plotArea;
if ( !mRenderer )
return;
const double pixelRatio = !scene()->views().empty() ? scene()->views().at( 0 )->devicePixelRatioF() : 1;
const QStringList sourceIds = mRenderer->sourceIds();
for ( const QString &source : sourceIds )
{
QImage plot;
auto it = mCachedImages.constFind( source );
if ( it != mCachedImages.constEnd() )
{
plot = it.value();
}
else
{
plot = mRenderer->renderToImage( plotArea.width() * pixelRatio, plotArea.height() * pixelRatio, xMinimum() * mXScaleFactor, xMaximum() * mXScaleFactor, yMinimum(), yMaximum(), source, pixelRatio );
plot.setDevicePixelRatio( pixelRatio );
mCachedImages.insert( source, plot );
}
rc.painter()->drawImage( QPointF( plotArea.left(), plotArea.top() ), plot );
}
}
void paint( QPainter *painter ) override
{
// cache rendering to an image, so we don't need to redraw the plot
if ( !mImage.isNull() )
{
painter->drawImage( QPointF( 0, 0 ), mImage );
}
else
{
const double pixelRatio = !scene()->views().empty() ? scene()->views().at( 0 )->devicePixelRatioF() : 1;
mImage = QImage( mRect.width() * pixelRatio, mRect.height() * pixelRatio, QImage::Format_ARGB32_Premultiplied );
mImage.setDevicePixelRatio( pixelRatio );
mImage.fill( Qt::transparent );
QPainter imagePainter( &mImage );
imagePainter.setRenderHint( QPainter::Antialiasing, true );
QgsRenderContext rc = QgsRenderContext::fromQPainter( &imagePainter );
rc.setDevicePixelRatio( pixelRatio );
const double mapUnitsPerPixel = ( xMaximum() - xMinimum() ) * mXScaleFactor / plotArea().width();
rc.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
rc.expressionContext().appendScope( QgsExpressionContextUtils::globalScope() );
rc.expressionContext().appendScope( QgsExpressionContextUtils::projectScope( mProject ) );
calculateOptimisedIntervals( rc );
render( rc );
imagePainter.end();
painter->drawImage( QPointF( 0, 0 ), mImage );
}
}
void setSubsectionsSymbol( QgsLineSymbol *symbol )
{
if ( mRenderer )
{
mRenderer->setSubsectionsSymbol( symbol );
updatePlot();
}
}
QgsProject *mProject = nullptr;
double mXScaleFactor = 1.0;
Qgis::DistanceUnit mDistanceUnit = Qgis::DistanceUnit::Unknown;
private:
QImage mImage;
QMap<QString, QImage> mCachedImages;
QRectF mRect;
QRectF mPlotArea;
QgsProfilePlotRenderer *mRenderer = nullptr;
};
class QgsElevationProfileCrossHairsItem : public QgsPlotCanvasItem
{
public:
QgsElevationProfileCrossHairsItem( QgsElevationProfileCanvas *canvas, QgsElevationProfilePlotItem *plotItem )
: QgsPlotCanvasItem( canvas )
, mPlotItem( plotItem )
{
}
void updateRect()
{
mRect = mCanvas->rect();
prepareGeometryChange();
setPos( mRect.topLeft() );
update();
}
void setPoint( const QgsProfilePoint &point )
{
mPoint = point;
update();
}
QRectF boundingRect() const override
{
return mRect;
}
void paint( QPainter *painter ) override
{
const QgsPointXY crossHairPlotPoint = mPlotItem->plotPointToCanvasPoint( mPoint );
if ( crossHairPlotPoint.isEmpty() )
return;
painter->save();
painter->setBrush( Qt::NoBrush );
QPen crossHairPen;
crossHairPen.setCosmetic( true );
crossHairPen.setWidthF( 1 );
crossHairPen.setStyle( Qt::DashLine );
crossHairPen.setCapStyle( Qt::FlatCap );
const QPalette scenePalette = mPlotItem->scene()->palette();
QColor penColor = scenePalette.color( QPalette::ColorGroup::Active, QPalette::Text );
penColor.setAlpha( 150 );
crossHairPen.setColor( penColor );
painter->setPen( crossHairPen );
painter->drawLine( QPointF( mPlotItem->plotArea().left(), crossHairPlotPoint.y() ), QPointF( mPlotItem->plotArea().right(), crossHairPlotPoint.y() ) );
painter->drawLine( QPointF( crossHairPlotPoint.x(), mPlotItem->plotArea().top() ), QPointF( crossHairPlotPoint.x(), mPlotItem->plotArea().bottom() ) );
// also render current point text
QgsNumericFormatContext numericContext;
const QString xCoordinateText = mPlotItem->xAxis().numericFormat()->formatDouble( mPoint.distance() / mPlotItem->mXScaleFactor, numericContext )
+ mPlotItem->distanceSuffix();
const QString yCoordinateText = mPlotItem->yAxis().numericFormat()->formatDouble( mPoint.elevation(), numericContext );
QFont font;
const QFontMetrics fm( font );
const double height = fm.capHeight();
const double xWidth = fm.horizontalAdvance( xCoordinateText );
const double yWidth = fm.horizontalAdvance( yCoordinateText );
const double textAxisMargin = fm.horizontalAdvance( ' ' );
QPointF xCoordOrigin;
QPointF yCoordOrigin;
if ( mPoint.distance() < ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5 * mPlotItem->mXScaleFactor )
{
if ( mPoint.elevation() < ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5 )
{
// render x coordinate on right top (left top align)
xCoordOrigin = QPointF( crossHairPlotPoint.x() + textAxisMargin, mPlotItem->plotArea().top() + height + textAxisMargin );
// render y coordinate on right top (right bottom align)
yCoordOrigin = QPointF( mPlotItem->plotArea().right() - yWidth - textAxisMargin, crossHairPlotPoint.y() - textAxisMargin );
}
else
{
// render x coordinate on right bottom (left bottom align)
xCoordOrigin = QPointF( crossHairPlotPoint.x() + textAxisMargin, mPlotItem->plotArea().bottom() - textAxisMargin );
// render y coordinate on right bottom (right top align)
yCoordOrigin = QPointF( mPlotItem->plotArea().right() - yWidth - textAxisMargin, crossHairPlotPoint.y() + height + textAxisMargin );
}
}
else
{
if ( mPoint.elevation() < ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5 )
{
// render x coordinate on left top (right top align)
xCoordOrigin = QPointF( crossHairPlotPoint.x() - xWidth - textAxisMargin, mPlotItem->plotArea().top() + height + textAxisMargin );
// render y coordinate on left top (left bottom align)
yCoordOrigin = QPointF( mPlotItem->plotArea().left() + textAxisMargin, crossHairPlotPoint.y() - textAxisMargin );
}
else
{
// render x coordinate on left bottom (right bottom align)
xCoordOrigin = QPointF( crossHairPlotPoint.x() - xWidth - textAxisMargin, mPlotItem->plotArea().bottom() - textAxisMargin );
// render y coordinate on left bottom (left top align)
yCoordOrigin = QPointF( mPlotItem->plotArea().left() + textAxisMargin, crossHairPlotPoint.y() + height + textAxisMargin );
}
}
// semi opaque background color brush
QColor backgroundColor = mPlotItem->chartBackgroundSymbol()->color();
backgroundColor.setAlpha( 220 );
painter->setBrush( QBrush( backgroundColor ) );
painter->setPen( Qt::NoPen );
painter->drawRect( QRectF( xCoordOrigin.x() - textAxisMargin + 1, xCoordOrigin.y() - textAxisMargin - height + 1, xWidth + 2 * textAxisMargin - 2, height + 2 * textAxisMargin - 2 ) );
painter->drawRect( QRectF( yCoordOrigin.x() - textAxisMargin + 1, yCoordOrigin.y() - textAxisMargin - height + 1, yWidth + 2 * textAxisMargin - 2, height + 2 * textAxisMargin - 2 ) );
painter->setBrush( Qt::NoBrush );
painter->setPen( scenePalette.color( QPalette::ColorGroup::Active, QPalette::Text ) );
painter->drawText( xCoordOrigin, xCoordinateText );
painter->drawText( yCoordOrigin, yCoordinateText );
painter->restore();
}
private:
QRectF mRect;
QgsProfilePoint mPoint;
QgsElevationProfilePlotItem *mPlotItem = nullptr;
};
///@endcond PRIVATE
QgsElevationProfileCanvas::QgsElevationProfileCanvas( QWidget *parent )
: QgsPlotCanvas( parent )
{
mScreenHelper = new QgsScreenHelper( this );
mPlotItem = new QgsElevationProfilePlotItem( this );
// follow system color scheme by default
setBackgroundColor( QColor() );
mCrossHairsItem = new QgsElevationProfileCrossHairsItem( this, mPlotItem );
mCrossHairsItem->setZValue( 100 );
mCrossHairsItem->hide();
// updating the profile plot is deferred on a timer, so that we don't trigger it too often
mDeferredRegenerationTimer = new QTimer( this );
mDeferredRegenerationTimer->setSingleShot( true );
mDeferredRegenerationTimer->stop();
connect( mDeferredRegenerationTimer, &QTimer::timeout, this, &QgsElevationProfileCanvas::startDeferredRegeneration );
mDeferredRedrawTimer = new QTimer( this );
mDeferredRedrawTimer->setSingleShot( true );
mDeferredRedrawTimer->stop();
connect( mDeferredRedrawTimer, &QTimer::timeout, this, &QgsElevationProfileCanvas::startDeferredRedraw );
}
QgsElevationProfileCanvas::~QgsElevationProfileCanvas()
{
if ( mCurrentJob )
{
mPlotItem->setRenderer( nullptr );
mCurrentJob->deleteLater();
mCurrentJob = nullptr;
}
}
void QgsElevationProfileCanvas::cancelJobs()
{
if ( mCurrentJob )
{
mPlotItem->setRenderer( nullptr );
disconnect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
mCurrentJob->cancelGeneration();
mCurrentJob->deleteLater();
mCurrentJob = nullptr;
}
}
void QgsElevationProfileCanvas::panContentsBy( double dx, double dy )
{
const double dxPercent = dx / mPlotItem->plotArea().width();
const double dyPercent = dy / mPlotItem->plotArea().height();
// these look backwards, but we are dragging the paper, not the view!
const double dxPlot = -dxPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
const double dyPlot = dyPercent * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
// no need to handle axis scale lock here, we aren't changing scales
mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
void QgsElevationProfileCanvas::centerPlotOn( double x, double y )
{
if ( !mPlotItem->plotArea().contains( x, y ) )
return;
const double newCenterX = mPlotItem->xMinimum() + ( x - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
const double newCenterY = mPlotItem->yMinimum() + ( mPlotItem->plotArea().bottom() - y ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;
// no need to handle axis scale lock here, we aren't changing scales
mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
void QgsElevationProfileCanvas::scalePlot( double factor )
{
scalePlot( factor, factor );
emit plotAreaChanged();
}
QgsProfileSnapContext QgsElevationProfileCanvas::snapContext() const
{
const double toleranceInPixels = QFontMetrics( font() ).horizontalAdvance( ' ' );
const double xToleranceInPlotUnits = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) * toleranceInPixels;
const double yToleranceInPlotUnits = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * toleranceInPixels;
QgsProfileSnapContext context;
context.maximumSurfaceDistanceDelta = 2 * xToleranceInPlotUnits;
context.maximumSurfaceElevationDelta = 10 * yToleranceInPlotUnits;
context.maximumPointDistanceDelta = 4 * xToleranceInPlotUnits;
context.maximumPointElevationDelta = 4 * yToleranceInPlotUnits;
context.displayRatioElevationVsDistance = ( ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) )
/ ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) );
return context;
}
QgsProfileIdentifyContext QgsElevationProfileCanvas::identifyContext() const
{
const double toleranceInPixels = QFontMetrics( font() ).horizontalAdvance( ' ' );
const double xToleranceInPlotUnits = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) * toleranceInPixels;
const double yToleranceInPlotUnits = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * toleranceInPixels;
QgsProfileIdentifyContext context;
context.maximumSurfaceDistanceDelta = 2 * xToleranceInPlotUnits;
context.maximumSurfaceElevationDelta = 10 * yToleranceInPlotUnits;
context.maximumPointDistanceDelta = 4 * xToleranceInPlotUnits;
context.maximumPointElevationDelta = 4 * yToleranceInPlotUnits;
context.displayRatioElevationVsDistance = ( ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) )
/ ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / ( mPlotItem->plotArea().width() ) );
context.project = mProject;
return context;
}
void QgsElevationProfileCanvas::setupLayerConnections( QgsMapLayer *layer, bool isDisconnect )
{
if ( isDisconnect )
{
disconnect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileGenerationPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged );
disconnect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileRenderingPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged );
disconnect( layer, &QgsMapLayer::dataChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
}
else
{
connect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileGenerationPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged );
connect( layer->elevationProperties(), &QgsMapLayerElevationProperties::profileRenderingPropertyChanged, this, &QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged );
connect( layer, &QgsMapLayer::dataChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
}
switch ( layer->type() )
{
case Qgis::LayerType::Vector:
{
QgsVectorLayer *vl = qobject_cast<QgsVectorLayer *>( layer );
if ( isDisconnect )
{
disconnect( vl, &QgsVectorLayer::featureAdded, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
disconnect( vl, &QgsVectorLayer::featureDeleted, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
disconnect( vl, &QgsVectorLayer::geometryChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
disconnect( vl, &QgsVectorLayer::attributeValueChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
}
else
{
connect( vl, &QgsVectorLayer::featureAdded, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
connect( vl, &QgsVectorLayer::featureDeleted, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
connect( vl, &QgsVectorLayer::geometryChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
connect( vl, &QgsVectorLayer::attributeValueChanged, this, &QgsElevationProfileCanvas::regenerateResultsForLayer );
}
break;
}
case Qgis::LayerType::Raster:
case Qgis::LayerType::Plugin:
case Qgis::LayerType::Mesh:
case Qgis::LayerType::VectorTile:
case Qgis::LayerType::Annotation:
case Qgis::LayerType::PointCloud:
case Qgis::LayerType::Group:
case Qgis::LayerType::TiledScene:
break;
}
}
void QgsElevationProfileCanvas::adjustRangeForAxisScaleLock( double &xMinimum, double &xMaximum, double &yMinimum, double &yMaximum ) const
{
// ensures that we always "zoom out" to match horizontal/vertical scales
const double horizontalScale = ( xMaximum - xMinimum ) / mPlotItem->plotArea().width();
const double verticalScale = ( yMaximum - yMinimum ) / mPlotItem->plotArea().height();
if ( horizontalScale > verticalScale )
{
const double height = horizontalScale * mPlotItem->plotArea().height();
const double deltaHeight = ( yMaximum - yMinimum ) - height;
yMinimum += deltaHeight / 2;
yMaximum -= deltaHeight / 2;
}
else
{
const double width = verticalScale * mPlotItem->plotArea().width();
const double deltaWidth = ( ( xMaximum - xMinimum ) - width );
xMinimum += deltaWidth / 2;
xMaximum -= deltaWidth / 2;
}
}
Qgis::DistanceUnit QgsElevationProfileCanvas::distanceUnit() const
{
return mDistanceUnit;
}
void QgsElevationProfileCanvas::setDistanceUnit( Qgis::DistanceUnit unit )
{
mDistanceUnit = unit;
const double oldMin = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
const double oldMax = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor;
mPlotItem->mXScaleFactor = QgsUnitTypes::fromUnitToUnitFactor( mDistanceUnit, mCrs.mapUnits() );
mPlotItem->setXAxisUnits( mDistanceUnit );
mPlotItem->setXMinimum( oldMin / mPlotItem->mXScaleFactor );
mPlotItem->setXMaximum( oldMax / mPlotItem->mXScaleFactor );
mPlotItem->updatePlot();
}
void QgsElevationProfileCanvas::setBackgroundColor( const QColor &color )
{
if ( !color.isValid() )
{
QPalette customPalette = qApp->palette();
const QColor baseColor = qApp->palette().color( QPalette::ColorRole::Base );
const QColor windowColor = qApp->palette().color( QPalette::ColorRole::Window );
customPalette.setColor( QPalette::ColorRole::Base, windowColor );
customPalette.setColor( QPalette::ColorRole::Window, baseColor );
setPalette( customPalette );
scene()->setPalette( customPalette );
}
else
{
// build custom palette
const bool isDarkTheme = color.lightnessF() < 0.5;
QPalette customPalette = qApp->palette();
customPalette.setColor( QPalette::ColorRole::Window, color );
if ( isDarkTheme )
{
customPalette.setColor( QPalette::ColorRole::Text, QColor( 255, 255, 255 ) );
customPalette.setColor( QPalette::ColorRole::Base, color.lighter( 120 ) );
}
else
{
customPalette.setColor( QPalette::ColorRole::Text, QColor( 0, 0, 0 ) );
customPalette.setColor( QPalette::ColorRole::Base, color.darker( 120 ) );
}
setPalette( customPalette );
scene()->setPalette( customPalette );
}
updateChartFromPalette();
}
bool QgsElevationProfileCanvas::lockAxisScales() const
{
return mLockAxisScales;
}
void QgsElevationProfileCanvas::setLockAxisScales( bool lock )
{
mLockAxisScales = lock;
if ( mLockAxisScales )
{
double xMinimum = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
double xMaximum = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor;
double yMinimum = mPlotItem->yMinimum();
double yMaximum = mPlotItem->yMaximum();
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
}
QgsPointXY QgsElevationProfileCanvas::snapToPlot( QPoint point )
{
if ( !mCurrentJob || !mSnappingEnabled )
return QgsPointXY();
const QgsProfilePoint plotPoint = canvasPointToPlotPoint( point );
const QgsProfileSnapResult snappedPoint = mCurrentJob->snapPoint( plotPoint, snapContext() );
if ( !snappedPoint.isValid() )
return QgsPointXY();
return plotPointToCanvasPoint( snappedPoint.snappedPoint );
}
void QgsElevationProfileCanvas::scalePlot( double xFactor, double yFactor )
{
if ( mLockAxisScales )
yFactor = xFactor;
const double currentWidth = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor;
const double currentHeight = mPlotItem->yMaximum() - mPlotItem->yMinimum();
const double newWidth = currentWidth / xFactor;
const double newHeight = currentHeight / yFactor;
const double currentCenterX = ( mPlotItem->xMinimum() + mPlotItem->xMaximum() ) * 0.5 * mPlotItem->mXScaleFactor;
const double currentCenterY = ( mPlotItem->yMinimum() + mPlotItem->yMaximum() ) * 0.5;
double xMinimum = currentCenterX - newWidth * 0.5;
double xMaximum = currentCenterX + newWidth * 0.5;
double yMinimum = currentCenterY - newHeight * 0.5;
double yMaximum = currentCenterY + newHeight * 0.5;
if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
}
mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
void QgsElevationProfileCanvas::zoomToRect( const QRectF &rect )
{
const QRectF intersected = rect.intersected( mPlotItem->plotArea() );
double minX = ( intersected.left() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
double maxX = ( intersected.right() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
double minY = ( mPlotItem->plotArea().bottom() - intersected.bottom() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
double maxY = ( mPlotItem->plotArea().bottom() - intersected.top() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( minX, maxX, minY, maxY );
}
mPlotItem->setXMinimum( minX / mPlotItem->mXScaleFactor );
mPlotItem->setXMaximum( maxX / mPlotItem->mXScaleFactor );
mPlotItem->setYMinimum( minY );
mPlotItem->setYMaximum( maxY );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
void QgsElevationProfileCanvas::wheelZoom( QWheelEvent *event )
{
//get mouse wheel zoom behavior settings
QgsSettings settings;
double zoomFactor = settings.value( QStringLiteral( "qgis/zoom_factor" ), 2 ).toDouble();
bool reverseZoom = settings.value( QStringLiteral( "qgis/reverse_wheel_zoom" ), false ).toBool();
bool zoomIn = reverseZoom ? event->angleDelta().y() < 0 : event->angleDelta().y() > 0;
// "Normal" mouse have an angle delta of 120, precision mouses provide data faster, in smaller steps
zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 120.0 * std::fabs( event->angleDelta().y() );
if ( event->modifiers() & Qt::ControlModifier )
{
//holding ctrl while wheel zooming results in a finer zoom
zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 20.0;
}
//calculate zoom scale factor
double scaleFactor = ( zoomIn ? 1 / zoomFactor : zoomFactor );
QRectF viewportRect = mPlotItem->plotArea();
if ( viewportRect.contains( event->position() ) )
{
//adjust view center
const double oldCenterX = 0.5 * ( mPlotItem->xMaximum() + mPlotItem->xMinimum() );
const double oldCenterY = 0.5 * ( mPlotItem->yMaximum() + mPlotItem->yMinimum() );
const double eventPosX = ( event->position().x() - viewportRect.left() ) / viewportRect.width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
const double eventPosY = ( viewportRect.bottom() - event->position().y() ) / viewportRect.height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
const double newCenterX = eventPosX + ( ( oldCenterX - eventPosX ) * scaleFactor );
const double newCenterY = eventPosY + ( ( oldCenterY - eventPosY ) * scaleFactor );
const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;
// don't need to handle axis scale lock here, we are always changing axis by the same scale
mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
mPlotItem->setYMaximum( mPlotItem->yMaximum() + dyPlot );
}
//zoom plot
if ( zoomIn )
{
scalePlot( zoomFactor );
}
else
{
scalePlot( 1 / zoomFactor );
}
emit plotAreaChanged();
}
void QgsElevationProfileCanvas::mouseMoveEvent( QMouseEvent *e )
{
QgsPlotCanvas::mouseMoveEvent( e );
if ( e->isAccepted() )
{
mCrossHairsItem->hide();
return;
}
QgsProfilePoint plotPoint = canvasPointToPlotPoint( e->pos() );
if ( mCurrentJob && mSnappingEnabled && !plotPoint.isEmpty() )
{
const QgsProfileSnapResult snapResult = mCurrentJob->snapPoint( plotPoint, snapContext() );
if ( snapResult.isValid() )
plotPoint = snapResult.snappedPoint;
}
if ( plotPoint.isEmpty() )
{
mCrossHairsItem->hide();
}
else
{
mCrossHairsItem->setPoint( plotPoint );
mCrossHairsItem->show();
}
emit canvasPointHovered( e->pos(), plotPoint );
}
QRectF QgsElevationProfileCanvas::plotArea() const
{
return mPlotItem->plotArea();
}
void QgsElevationProfileCanvas::refresh()
{
if ( !mProject || !profileCurve() )
return;
cancelJobs();
QgsProfileRequest request( profileCurve()->clone() );
request.setCrs( mCrs );
request.setTolerance( mTolerance );
request.setTransformContext( mProject->transformContext() );
request.setTerrainProvider( mProject->elevationProperties()->terrainProvider() ? mProject->elevationProperties()->terrainProvider()->clone() : nullptr );
QgsExpressionContext context;
context.appendScope( QgsExpressionContextUtils::globalScope() );
context.appendScope( QgsExpressionContextUtils::projectScope( mProject ) );
request.setExpressionContext( context );
const QList<QgsMapLayer *> layersToGenerate = layers();
QList<QgsAbstractProfileSource *> sources;
const QList<QgsAbstractProfileSource *> registrySources = QgsApplication::profileSourceRegistry()->profileSources();
sources.reserve( layersToGenerate.size() + registrySources.size() );
sources << registrySources;
for ( QgsMapLayer *layer : layersToGenerate )
{
if ( QgsAbstractProfileSource *source = layer->profileSource() )
sources.append( source );
}
mCurrentJob = new QgsProfilePlotRenderer( sources, request );
connect( mCurrentJob, &QgsProfilePlotRenderer::generationFinished, this, &QgsElevationProfileCanvas::generationFinished );
QgsProfileGenerationContext generationContext;
generationContext.setDpi( mScreenHelper->screenDpi() );
generationContext.setMaximumErrorMapUnits( MAX_ERROR_PIXELS * ( mProfileCurve->length() ) / mPlotItem->plotArea().width() );
generationContext.setMapUnitsPerDistancePixel( mProfileCurve->length() / mPlotItem->plotArea().width() );
mCurrentJob->setContext( generationContext );
if ( mSubsectionsSymbol )
{
mCurrentJob->setSubsectionsSymbol( mSubsectionsSymbol->clone() );
}
mCurrentJob->startGeneration();
mPlotItem->setRenderer( mCurrentJob );
emit activeJobCountChanged( 1 );
}
void QgsElevationProfileCanvas::invalidateCurrentPlotExtent()
{
mZoomFullWhenJobFinished = true;
}
void QgsElevationProfileCanvas::generationFinished()
{
if ( !mCurrentJob )
return;
emit activeJobCountChanged( 0 );
if ( mZoomFullWhenJobFinished )
{
// we only zoom full for the initial generation
mZoomFullWhenJobFinished = false;
zoomFull();
}
else
{
// here we should invalidate cached results only for the layers which have been refined
// and if no layers are being refeined, don't invalidate anything
mPlotItem->updatePlot();
}
if ( mForceRegenerationAfterCurrentJobCompletes )
{
mForceRegenerationAfterCurrentJobCompletes = false;
mCurrentJob->invalidateAllRefinableSources();
scheduleDeferredRegeneration();
}
}
void QgsElevationProfileCanvas::onLayerProfileGenerationPropertyChanged()
{
// TODO -- handle nicely when existing job is in progress
if ( !mCurrentJob || mCurrentJob->isActive() )
return;
QgsMapLayerElevationProperties *properties = qobject_cast<QgsMapLayerElevationProperties *>( sender() );
if ( !properties )
return;
if ( QgsMapLayer *layer = qobject_cast<QgsMapLayer *>( properties->parent() ) )
{
if ( QgsAbstractProfileSource *source = layer->profileSource() )
{
if ( mCurrentJob->invalidateResults( source ) )
scheduleDeferredRegeneration();
}
}
}
void QgsElevationProfileCanvas::onLayerProfileRendererPropertyChanged()
{
// TODO -- handle nicely when existing job is in progress
if ( !mCurrentJob || mCurrentJob->isActive() )
return;
QgsMapLayerElevationProperties *properties = qobject_cast<QgsMapLayerElevationProperties *>( sender() );
if ( !properties )
return;
if ( QgsMapLayer *layer = qobject_cast<QgsMapLayer *>( properties->parent() ) )
{
if ( QgsAbstractProfileSource *source = layer->profileSource() )
{
mCurrentJob->replaceSource( source );
}
if ( mPlotItem->redrawResults( layer->id() ) )
scheduleDeferredRedraw();
}
}
void QgsElevationProfileCanvas::regenerateResultsForLayer()
{
if ( !mCurrentJob )
return;
if ( QgsMapLayer *layer = qobject_cast<QgsMapLayer *>( sender() ) )
{
if ( QgsAbstractProfileSource *source = layer->profileSource() )
{
if ( mCurrentJob->invalidateResults( source ) )
scheduleDeferredRegeneration();
}
}
}
void QgsElevationProfileCanvas::scheduleDeferredRegeneration()
{
if ( !mDeferredRegenerationScheduled )
{
mDeferredRegenerationTimer->start( 1 );
mDeferredRegenerationScheduled = true;
}
}
void QgsElevationProfileCanvas::scheduleDeferredRedraw()
{
if ( !mDeferredRedrawScheduled )
{
mDeferredRedrawTimer->start( 1 );
mDeferredRedrawScheduled = true;
}
}
void QgsElevationProfileCanvas::startDeferredRegeneration()
{
if ( mCurrentJob && !mCurrentJob->isActive() )
{
emit activeJobCountChanged( 1 );
mCurrentJob->regenerateInvalidatedResults();
}
else if ( mCurrentJob )
{
mForceRegenerationAfterCurrentJobCompletes = true;
}
mDeferredRegenerationScheduled = false;
}
void QgsElevationProfileCanvas::startDeferredRedraw()
{
mPlotItem->update();
mDeferredRedrawScheduled = false;
}
void QgsElevationProfileCanvas::refineResults()
{
if ( mCurrentJob )
{
QgsProfileGenerationContext context;
context.setDpi( mScreenHelper->screenDpi() );
const double plotDistanceRange = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor;
const double plotElevationRange = mPlotItem->yMaximum() - mPlotItem->yMinimum();
const double plotDistanceUnitsPerPixel = plotDistanceRange / mPlotItem->plotArea().width();
// we round the actual desired map error down to just one significant figure, to avoid tiny differences
// as the plot is panned
const double targetMaxErrorInMapUnits = MAX_ERROR_PIXELS * plotDistanceUnitsPerPixel;
const double factor = std::pow( 10.0, 1 - std::ceil( std::log10( std::fabs( targetMaxErrorInMapUnits ) ) ) );
const double roundedErrorInMapUnits = std::floor( targetMaxErrorInMapUnits * factor ) / factor;
context.setMaximumErrorMapUnits( roundedErrorInMapUnits );
context.setMapUnitsPerDistancePixel( plotDistanceUnitsPerPixel );
// for similar reasons we round the minimum distance off to multiples of the maximum error in map units
const double distanceMin = std::floor( ( mPlotItem->xMinimum() * mPlotItem->mXScaleFactor - plotDistanceRange * 0.05 ) / context.maximumErrorMapUnits() ) * context.maximumErrorMapUnits();
context.setDistanceRange( QgsDoubleRange( std::max( 0.0, distanceMin ), mPlotItem->xMaximum() * mPlotItem->mXScaleFactor + plotDistanceRange * 0.05 ) );
context.setElevationRange( QgsDoubleRange( mPlotItem->yMinimum() - plotElevationRange * 0.05, mPlotItem->yMaximum() + plotElevationRange * 0.05 ) );
mCurrentJob->setContext( context );
}
scheduleDeferredRegeneration();
}
void QgsElevationProfileCanvas::updateChartFromPalette()
{
const QPalette chartPalette = palette();
setBackgroundBrush( QBrush( chartPalette.color( QPalette::ColorRole::Base ) ) );
{
QgsTextFormat textFormat = mPlotItem->xAxis().textFormat();
textFormat.setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::Text ) );
mPlotItem->xAxis().setTextFormat( textFormat );
mPlotItem->yAxis().setTextFormat( textFormat );
}
{
std::unique_ptr<QgsFillSymbol> chartFill( mPlotItem->chartBackgroundSymbol()->clone() );
chartFill->setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Window ) );
mPlotItem->setChartBackgroundSymbol( chartFill.release() );
}
{
std::unique_ptr<QgsFillSymbol> chartBorder( mPlotItem->chartBorderSymbol()->clone() );
chartBorder->setColor( chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text ) );
mPlotItem->setChartBorderSymbol( chartBorder.release() );
}
{
std::unique_ptr<QgsLineSymbol> chartMajorSymbol( mPlotItem->xAxis().gridMajorSymbol()->clone() );
QColor c = chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text );
c.setAlpha( 150 );
chartMajorSymbol->setColor( c );
mPlotItem->xAxis().setGridMajorSymbol( chartMajorSymbol->clone() );
mPlotItem->yAxis().setGridMajorSymbol( chartMajorSymbol.release() );
}
{
std::unique_ptr<QgsLineSymbol> chartMinorSymbol( mPlotItem->xAxis().gridMinorSymbol()->clone() );
QColor c = chartPalette.color( QPalette::ColorGroup::Active, QPalette::ColorRole::Text );
c.setAlpha( 50 );
chartMinorSymbol->setColor( c );
mPlotItem->xAxis().setGridMinorSymbol( chartMinorSymbol->clone() );
mPlotItem->yAxis().setGridMinorSymbol( chartMinorSymbol.release() );
}
mPlotItem->updatePlot();
}
QgsProfilePoint QgsElevationProfileCanvas::canvasPointToPlotPoint( QPointF point ) const
{
if ( !mPlotItem->plotArea().contains( point.x(), point.y() ) )
return QgsProfilePoint();
return mPlotItem->canvasPointToPlotPoint( point );
}
QgsPointXY QgsElevationProfileCanvas::plotPointToCanvasPoint( const QgsProfilePoint &point ) const
{
return mPlotItem->plotPointToCanvasPoint( point );
}
void QgsElevationProfileCanvas::setProject( QgsProject *project )
{
mProject = project;
mPlotItem->mProject = project;
}
void QgsElevationProfileCanvas::setCrs( const QgsCoordinateReferenceSystem &crs )
{
mCrs = crs;
}
void QgsElevationProfileCanvas::setProfileCurve( QgsCurve *curve )
{
mProfileCurve.reset( curve );
}
QgsCurve *QgsElevationProfileCanvas::profileCurve() const
{
return mProfileCurve.get();
}
void QgsElevationProfileCanvas::setTolerance( double tolerance )
{
mTolerance = tolerance;
}
QgsCoordinateReferenceSystem QgsElevationProfileCanvas::crs() const
{
return mCrs;
}
void QgsElevationProfileCanvas::setLayers( const QList<QgsMapLayer *> &layers )
{
for ( QgsMapLayer *layer : std::as_const( mLayers ) )
{
setupLayerConnections( layer, true );
}
// filter list, removing null layers and invalid layers
auto filteredList = layers;
filteredList.erase( std::remove_if( filteredList.begin(), filteredList.end(), []( QgsMapLayer *layer ) {
return !layer || !layer->isValid();
} ),
filteredList.end() );
mLayers = _qgis_listRawToQPointer( filteredList );
for ( QgsMapLayer *layer : std::as_const( mLayers ) )
{
setupLayerConnections( layer, false );
}
}
QList<QgsMapLayer *> QgsElevationProfileCanvas::layers() const
{
return _qgis_listQPointerToRaw( mLayers );
}
void QgsElevationProfileCanvas::resizeEvent( QResizeEvent *event )
{
QgsPlotCanvas::resizeEvent( event );
if ( mLockAxisScales )
{
double xMinimum = mPlotItem->xMinimum();
double xMaximum = mPlotItem->xMaximum();
double yMinimum = mPlotItem->yMinimum();
double yMaximum = mPlotItem->yMaximum();
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
mPlotItem->setXMinimum( xMinimum );
mPlotItem->setXMaximum( xMaximum );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );
}
mPlotItem->updateRect();
mCrossHairsItem->updateRect();
}
void QgsElevationProfileCanvas::paintEvent( QPaintEvent *event )
{
QgsPlotCanvas::paintEvent( event );
if ( !mFirstDrawOccurred )
{
// 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).
mFirstDrawOccurred = true;
mPlotItem->updateRect();
mCrossHairsItem->updateRect();
}
}
QgsPoint QgsElevationProfileCanvas::toMapCoordinates( const QgsPointXY &point ) const
{
if ( !mPlotItem->plotArea().contains( point.x(), point.y() ) )
return QgsPoint();
if ( !mProfileCurve )
return QgsPoint();
const double dx = point.x() - mPlotItem->plotArea().left();
const double distanceAlongPlotPercent = dx / mPlotItem->plotArea().width();
double distanceAlongCurveLength = distanceAlongPlotPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor + mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
std::unique_ptr<QgsPoint> mapXyPoint( mProfileCurve->interpolatePoint( distanceAlongCurveLength ) );
if ( !mapXyPoint )
return QgsPoint();
const double mapZ = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / ( mPlotItem->plotArea().height() ) * ( mPlotItem->plotArea().bottom() - point.y() ) + mPlotItem->yMinimum();
return QgsPoint( mapXyPoint->x(), mapXyPoint->y(), mapZ );
}
QgsPointXY QgsElevationProfileCanvas::toCanvasCoordinates( const QgsPoint &point ) const
{
if ( !mProfileCurve )
return QgsPointXY();
QgsGeos geos( mProfileCurve.get() );
QString error;
const double distanceAlongCurve = geos.lineLocatePoint( point, &error );
const double distanceAlongCurveOnPlot = distanceAlongCurve - mPlotItem->xMinimum() * mPlotItem->mXScaleFactor;
const double distanceAlongCurvePercent = distanceAlongCurveOnPlot / ( ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor );
const double distanceAlongPlotRect = distanceAlongCurvePercent * mPlotItem->plotArea().width();
const double canvasX = mPlotItem->plotArea().left() + distanceAlongPlotRect;
double canvasY = 0;
if ( std::isnan( point.z() ) || point.z() < mPlotItem->yMinimum() )
{
canvasY = mPlotItem->plotArea().top();
}
else if ( point.z() > mPlotItem->yMaximum() )
{
canvasY = mPlotItem->plotArea().bottom();
}
else
{
const double yPercent = ( point.z() - mPlotItem->yMinimum() ) / ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );
canvasY = mPlotItem->plotArea().bottom() - mPlotItem->plotArea().height() * yPercent;
}
return QgsPointXY( canvasX, canvasY );
}
void QgsElevationProfileCanvas::zoomFull()
{
if ( !mCurrentJob )
return;
const QgsDoubleRange zRange = mCurrentJob->zRange();
double yMinimum = 0;
double yMaximum = 0;
if ( zRange.upper() < zRange.lower() )
{
// invalid range, e.g. no features found in plot!
yMinimum = 0;
yMaximum = 10;
}
else if ( qgsDoubleNear( zRange.lower(), zRange.upper(), 0.0000001 ) )
{
// corner case ... a zero height plot! Just pick an arbitrary +/- 5 height range.
yMinimum = zRange.lower() - 5;
yMaximum = zRange.lower() + 5;
}
else
{
// add 5% margin to height range
const double margin = ( zRange.upper() - zRange.lower() ) * 0.05;
yMinimum = zRange.lower() - margin;
yMaximum = zRange.upper() + margin;
}
const double profileLength = profileCurve()->length();
double xMinimum = 0;
// just 2% margin to max distance -- any more is overkill and wasted space
double xMaximum = profileLength * 1.02;
if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
}
mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor );
mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
void QgsElevationProfileCanvas::setVisiblePlotRange( double minimumDistance, double maximumDistance, double minimumElevation, double maximumElevation )
{
if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( minimumDistance, maximumDistance, minimumElevation, maximumElevation );
}
mPlotItem->setYMinimum( minimumElevation );
mPlotItem->setYMaximum( maximumElevation );
mPlotItem->setXMinimum( minimumDistance / mPlotItem->mXScaleFactor );
mPlotItem->setXMaximum( maximumDistance / mPlotItem->mXScaleFactor );
refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
QgsDoubleRange QgsElevationProfileCanvas::visibleDistanceRange() const
{
return QgsDoubleRange( mPlotItem->xMinimum() * mPlotItem->mXScaleFactor, mPlotItem->xMaximum() * mPlotItem->mXScaleFactor );
}
QgsDoubleRange QgsElevationProfileCanvas::visibleElevationRange() const
{
return QgsDoubleRange( mPlotItem->yMinimum(), mPlotItem->yMaximum() );
}
const Qgs2DPlot &QgsElevationProfileCanvas::plot() const
{
return *mPlotItem;
}
///@cond PRIVATE
class QgsElevationProfilePlot : public Qgs2DPlot
{
public:
QgsElevationProfilePlot( QgsProfilePlotRenderer *renderer )
: mRenderer( renderer )
{
}
void renderContent( QgsRenderContext &rc, const QRectF &plotArea ) override
{
if ( !mRenderer )
return;
rc.painter()->translate( plotArea.left(), plotArea.top() );
mRenderer->render( rc, plotArea.width(), plotArea.height(), xMinimum() * mXScale, xMaximum() * mXScale, yMinimum(), yMaximum() );
rc.painter()->translate( -plotArea.left(), -plotArea.top() );
}
double mXScale = 1;
private:
QgsProfilePlotRenderer *mRenderer = nullptr;
};
///@endcond PRIVATE
void QgsElevationProfileCanvas::render( QgsRenderContext &context, double width, double height, const Qgs2DPlot &plotSettings )
{
if ( !mCurrentJob )
return;
context.expressionContext().appendScope( QgsExpressionContextUtils::globalScope() );
context.expressionContext().appendScope( QgsExpressionContextUtils::projectScope( mProject ) );
QgsElevationProfilePlot profilePlot( mCurrentJob );
// 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...
QDomDocument doc;
QDomElement elem = doc.createElement( QStringLiteral( "plot" ) );
QgsReadWriteContext rwContext;
plotSettings.writeXml( elem, doc, rwContext );
profilePlot.readXml( elem, rwContext );
profilePlot.mXScale = mPlotItem->mXScaleFactor;
profilePlot.xAxis().setLabelSuffix( mPlotItem->xAxis().labelSuffix() );
profilePlot.xAxis().setLabelSuffixPlacement( mPlotItem->xAxis().labelSuffixPlacement() );
profilePlot.setSize( QSizeF( width, height ) );
profilePlot.render( context );
}
QVector<QgsProfileIdentifyResults> QgsElevationProfileCanvas::identify( QPointF point )
{
if ( !mCurrentJob )
return {};
const QgsProfilePoint plotPoint = canvasPointToPlotPoint( point );
return mCurrentJob->identify( plotPoint, identifyContext() );
}
QVector<QgsProfileIdentifyResults> QgsElevationProfileCanvas::identify( const QRectF &rect )
{
if ( !mCurrentJob )
return {};
const QgsProfilePoint topLeftPlotPoint = canvasPointToPlotPoint( rect.topLeft() );
const QgsProfilePoint bottomRightPlotPoint = canvasPointToPlotPoint( rect.bottomRight() );
double distance1 = topLeftPlotPoint.distance();
double distance2 = bottomRightPlotPoint.distance();
if ( distance2 < distance1 )
std::swap( distance1, distance2 );
double elevation1 = topLeftPlotPoint.elevation();
double elevation2 = bottomRightPlotPoint.elevation();
if ( elevation2 < elevation1 )
std::swap( elevation1, elevation2 );
return mCurrentJob->identify( QgsDoubleRange( distance1, distance2 ), QgsDoubleRange( elevation1, elevation2 ), identifyContext() );
}
void QgsElevationProfileCanvas::clear()
{
setProfileCurve( nullptr );
cancelJobs();
mPlotItem->updatePlot();
}
void QgsElevationProfileCanvas::setSnappingEnabled( bool enabled )
{
mSnappingEnabled = enabled;
}
void QgsElevationProfileCanvas::setSubsectionsSymbol( QgsLineSymbol *symbol )
{
mSubsectionsSymbol.reset( symbol );
std::unique_ptr<QgsLineSymbol> plotItemSymbol( mSubsectionsSymbol ? mSubsectionsSymbol->clone() : nullptr );
mPlotItem->setSubsectionsSymbol( plotItemSymbol.release() );
}