QGIS/src/core/layout/qgslayoutitemelevationprofile.cpp
Denis Rouzaud 9efd6af839
[core] Introduce QgsLayerTreeCustomNode and a first use case: Custom Elevation Profile Sources (#62819)
* Introduce QgsLayerTreeCustomNode to handle items in QgsLayerTree which are not layers nor layer groups (e.g., a custom profile source from a web service to the Elevation Profile layer tree); make QgsAbstractProfileSource and its generator classes get an id in the constructor, so that subclasses don't need to deal with source ids (source ids from the generators are already in use, whereas source id from source subclasses will be used to link sources to QgsLayerTreeCustomNodes, and will be used to retrieve sources from source registry)

* [elevation] Add/remove nodes to Elevation's layer tree when registering/unregistering profile sources via QgsProfileSourceRegistry

* [elevation] Add updateCanvasSources() to Elevation Profile widget, so that after changes in the elevation layer tree, the canvas gets an updated list of profile sources, taking profile sources (i.e., from custom nodes) into account. Add QgsProfileSourceRegistry::findSourceById() for getting profile sources by id from the registry.

* [elevation] Allow for setting a display name for custom profile sources (to be displayed as layer tree node name)

* [elevation] Refactor profile source handling in Layout Elevation Profile widget, so that after changes in the elevation layer tree, the canvas gets an updated list of profile sources, taking profile sources (i.e., from custom nodes) into account. Adjust QgsLayerTreeGroup to be able to find layers and custom nodes, as well as order layer and custom nodes inside a group (only direct children).

* [elevation] Profile Source Registry improvements:

 + Avoid registering a source if its id is already registered,
 + New method to unregister a source by id, deprecating the unregister by source object.

* [tests] Add tests for QgsLayerTreeCustomNode

* [tests] add tests for legacy Elevation Profile sources and for legacy Layout Profile items:

 + make sure we keep the API untouched and mark some stuff as legacy (e.g., pure virtual methods in QgsAbstractProfileSource need to have a default implementation and making them pure virtual is postponed until QGIS 5.0),
 + add insertCustomNode(index, node) method to QgsLayerTreeGroup, so that we can set up the custom node before adding it to the layer tree, because if we add it with no custom properties set (e.g., for the elevation profile custom node), it will trigger plot updates but it won't be found due to a still missing custom property.

* [elevation] Make sure legacy layer ordering in canvas, layout item profile, and layer tree are kept (i.e., keep API untouched). Make profile source ordering works in the most expected way: canvas, layout item profile, and layer tree are in the same order, and sources are only reversed right before passing them to renderers. Add unit tests.

* [tests] Custom node for custom Elevation profiles: make sure layout item sources are stored in and read from the project

* Address review on custom nodes for elevation profile's tree view
2025-09-25 13:47:21 +02:00

1203 lines
41 KiB
C++

/***************************************************************************
qgslayoutitemelevationprofile.cpp
---------------------------------
begin : January 2023
copyright : (C) 2023 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 "qgslayoutitemelevationprofile.h"
#include "moc_qgslayoutitemelevationprofile.cpp"
#include "qgslayoutitemregistry.h"
#include "qgslinesymbol.h"
#include "qgsplot.h"
#include "qgslayout.h"
#include "qgsmessagelog.h"
#include "qgsmaplayerlistutils_p.h"
#include "qgscurve.h"
#include "qgsprofilerequest.h"
#include "qgsprojectelevationproperties.h"
#include "qgsterrainprovider.h"
#include "qgsprofilerenderer.h"
#include "qgslayoututils.h"
#include "qgsvectorlayer.h"
#include "qgslayoutrendercontext.h"
#include "qgslayoutreportcontext.h"
#include "qgsprofilesourceregistry.h"
#include "qgssymbollayerutils.h"
#include <QTimer>
#include <memory>
#define CACHE_SIZE_LIMIT 5000
///@cond PRIVATE
class QgsLayoutItemElevationProfilePlot : public Qgs2DXyPlot
{
public:
QgsLayoutItemElevationProfilePlot()
{
}
void setRenderer( QgsProfilePlotRenderer *renderer )
{
// cppcheck-suppress danglingLifetime
mRenderer = renderer;
}
void renderContent( QgsRenderContext &rc, QgsPlotRenderContext &, const QRectF &plotArea, const QgsPlotData & ) override
{
if ( mRenderer )
{
const double distanceMin = xMinimum() * xScale;
const double distanceMax = xMaximum() * xScale;
rc.painter()->translate( plotArea.left(), plotArea.top() );
mRenderer->render( rc, plotArea.width(), plotArea.height(), distanceMin, distanceMax, yMinimum(), yMaximum() );
rc.painter()->translate( -plotArea.left(), -plotArea.top() );
mRenderer->renderSubsectionsIndicator( rc, plotArea, distanceMin, distanceMax, yMinimum(), yMaximum() );
}
}
double xScale = 1;
private:
QgsProfilePlotRenderer *mRenderer = nullptr;
};
///@endcond PRIVATE
QgsLayoutItemElevationProfile::QgsLayoutItemElevationProfile( QgsLayout *layout )
: QgsLayoutItem( layout )
, mPlot( std::make_unique< QgsLayoutItemElevationProfilePlot >() )
{
mBackgroundUpdateTimer = new QTimer( this );
mBackgroundUpdateTimer->setSingleShot( true );
connect( mBackgroundUpdateTimer, &QTimer::timeout, this, &QgsLayoutItemElevationProfile::recreateCachedImageInBackground );
setCacheMode( QGraphicsItem::NoCache );
if ( mLayout )
{
connect( mLayout, &QgsLayout::refreshed, this, &QgsLayoutItemElevationProfile::invalidateCache );
}
connect( this, &QgsLayoutItem::sizePositionChanged, this, [this]
{
invalidateCache();
} );
//default to no background
setBackgroundEnabled( false );
connect( QgsApplication::profileSourceRegistry(), &QgsProfileSourceRegistry::profileSourceRegistered, this, &QgsLayoutItemElevationProfile::setSourcesPrivate );
connect( QgsApplication::profileSourceRegistry(), &QgsProfileSourceRegistry::profileSourceUnregistered, this, &QgsLayoutItemElevationProfile::setSourcesPrivate );
setSourcesPrivate(); // Initialize with registered sources
}
QgsLayoutItemElevationProfile::~QgsLayoutItemElevationProfile()
{
if ( mRenderJob )
{
disconnect( mRenderJob.get(), &QgsProfilePlotRenderer::generationFinished, this, &QgsLayoutItemElevationProfile::profileGenerationFinished );
emit backgroundTaskCountChanged( 0 );
mRenderJob->cancelGeneration(); // blocks
mPainter->end();
}
}
QgsLayoutItemElevationProfile *QgsLayoutItemElevationProfile::create( QgsLayout *layout )
{
return new QgsLayoutItemElevationProfile( layout );
}
int QgsLayoutItemElevationProfile::type() const
{
return QgsLayoutItemRegistry::LayoutElevationProfile;
}
QIcon QgsLayoutItemElevationProfile::icon() const
{
return QgsApplication::getThemeIcon( QStringLiteral( "mLayoutItemElevationProfile.svg" ) );
}
void QgsLayoutItemElevationProfile::refreshDataDefinedProperty( DataDefinedProperty property )
{
const QgsExpressionContext context = createExpressionContext();
bool forceUpdate = false;
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileTolerance || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileTolerance ) ) )
{
double value = mTolerance;
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileTolerance, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile tolerance expression eval error" ) );
}
else
{
mTolerance = value;
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileMinimumDistance || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileMinimumDistance ) ) )
{
double value = mPlot->xMinimum();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileMinimumDistance, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile minimum distance expression eval error" ) );
}
else
{
mPlot->setXMinimum( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileMaximumDistance || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileMaximumDistance ) ) )
{
double value = mPlot->xMaximum();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileMaximumDistance, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile maximum distance expression eval error" ) );
}
else
{
mPlot->setXMaximum( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileMinimumElevation || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileMinimumElevation ) ) )
{
double value = mPlot->yMinimum();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileMinimumElevation, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile minimum elevation expression eval error" ) );
}
else
{
mPlot->setYMinimum( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileMaximumElevation || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileMaximumElevation ) ) )
{
double value = mPlot->yMaximum();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileMaximumElevation, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile maximum elevation expression eval error" ) );
}
else
{
mPlot->setYMaximum( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMajorInterval || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMajorInterval ) ) )
{
double value = mPlot->xAxis().gridIntervalMajor();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMajorInterval, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile distance axis major interval expression eval error" ) );
}
else
{
mPlot->xAxis().setGridIntervalMajor( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMinorInterval || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMinorInterval ) ) )
{
double value = mPlot->xAxis().gridIntervalMinor();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceMinorInterval, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile distance axis minor interval expression eval error" ) );
}
else
{
mPlot->xAxis().setGridIntervalMinor( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceLabelInterval || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceLabelInterval ) ) )
{
double value = mPlot->xAxis().labelInterval();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileDistanceLabelInterval, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile distance axis label interval expression eval error" ) );
}
else
{
mPlot->xAxis().setLabelInterval( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationMajorInterval || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationMajorInterval ) ) )
{
double value = mPlot->yAxis().gridIntervalMajor();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationMajorInterval, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile elevation axis major interval expression eval error" ) );
}
else
{
mPlot->yAxis().setGridIntervalMajor( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationMinorInterval || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationMinorInterval ) ) )
{
double value = mPlot->yAxis().gridIntervalMinor();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationMinorInterval, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile elevation axis minor interval expression eval error" ) );
}
else
{
mPlot->yAxis().setGridIntervalMinor( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationLabelInterval || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationLabelInterval ) ) )
{
double value = mPlot->yAxis().labelInterval();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::ElevationProfileElevationLabelInterval, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile elevation axis label interval expression eval error" ) );
}
else
{
mPlot->yAxis().setLabelInterval( value );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::MarginLeft || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::MarginLeft ) ) )
{
double value = mPlot->margins().left();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::MarginLeft, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile left margin expression eval error" ) );
}
else
{
QgsMargins margins = mPlot->margins();
margins.setLeft( value );
mPlot->setMargins( margins );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::MarginRight || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::MarginRight ) ) )
{
double value = mPlot->margins().right();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::MarginRight, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile right margin expression eval error" ) );
}
else
{
QgsMargins margins = mPlot->margins();
margins.setRight( value );
mPlot->setMargins( margins );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::MarginTop || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::MarginTop ) ) )
{
double value = mPlot->margins().top();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::MarginTop, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile top margin expression eval error" ) );
}
else
{
QgsMargins margins = mPlot->margins();
margins.setTop( value );
mPlot->setMargins( margins );
}
forceUpdate = true;
}
if ( ( property == QgsLayoutObject::DataDefinedProperty::MarginBottom || property == QgsLayoutObject::DataDefinedProperty::AllProperties )
&& ( mDataDefinedProperties.isActive( QgsLayoutObject::DataDefinedProperty::MarginBottom ) ) )
{
double value = mPlot->margins().bottom();
bool ok = false;
value = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::DataDefinedProperty::MarginBottom, context, value, &ok );
if ( !ok )
{
QgsMessageLog::logMessage( tr( "Elevation profile bottom margin expression eval error" ) );
}
else
{
QgsMargins margins = mPlot->margins();
margins.setBottom( value );
mPlot->setMargins( margins );
}
forceUpdate = true;
}
if ( forceUpdate )
{
mCacheInvalidated = true;
refreshItemSize();
update();
}
QgsLayoutItem::refreshDataDefinedProperty( property );
}
QgsLayoutItem::Flags QgsLayoutItemElevationProfile::itemFlags() const
{
return QgsLayoutItem::FlagOverridesPaint | QgsLayoutItem::FlagDisableSceneCaching;
}
bool QgsLayoutItemElevationProfile::requiresRasterization() const
{
return blendMode() != QPainter::CompositionMode_SourceOver;
}
bool QgsLayoutItemElevationProfile::containsAdvancedEffects() const
{
return mEvaluatedOpacity < 1.0;
}
Qgs2DXyPlot *QgsLayoutItemElevationProfile::plot()
{
return mPlot.get();
}
const Qgs2DXyPlot *QgsLayoutItemElevationProfile::plot() const
{
return mPlot.get();
}
QList<QgsMapLayer *> QgsLayoutItemElevationProfile::layers() const
{
return _qgis_listRefToRaw( mLayers );
}
void QgsLayoutItemElevationProfile::setLayers( const QList<QgsMapLayer *> &layers )
{
if ( layers == _qgis_listRefToRaw( mLayers ) )
return;
mLayers = _qgis_listRawToRef( layers );
invalidateCache();
}
QList<QgsAbstractProfileSource *> QgsLayoutItemElevationProfile::sources() const
{
if ( mSources.isEmpty() && !mLayers.isEmpty() )
{
// Legacy: If we have layers, extract their sources and return them.
// We don't set mSources here, because we want the previous check to
// continue failing if only layers are set.
// TODO: Remove in QGIS 5.0.
QList< QgsAbstractProfileSource * > sources;
const QList<QgsMapLayer *> layersToGenerate = layers();
sources.reserve( layersToGenerate.size() );
for ( QgsMapLayer *layer : layersToGenerate )
{
if ( QgsAbstractProfileSource *source = layer->profileSource() )
sources << source;
}
// More legacy: elevation profile layers are in opposite direction
// to what the layer tree requires, and are in the same direction as
// the renderer expects, but since we reverse sources() (i.e., layers)
// before passing them to the renderer, then we need to reverse here first.
std::reverse( sources.begin(), sources.end() );
return sources;
}
return mSources;
}
void QgsLayoutItemElevationProfile::setSources( const QList<QgsAbstractProfileSource *> &sources )
{
mSources = sources;
invalidateCache();
}
void QgsLayoutItemElevationProfile::setProfileCurve( QgsCurve *curve )
{
mCurve.reset( curve );
invalidateCache();
}
QgsCurve *QgsLayoutItemElevationProfile::profileCurve() const
{
return mCurve.get();
}
void QgsLayoutItemElevationProfile::setCrs( const QgsCoordinateReferenceSystem &crs )
{
if ( mCrs == crs )
return;
mCrs = crs;
invalidateCache();
}
QgsCoordinateReferenceSystem QgsLayoutItemElevationProfile::crs() const
{
return mCrs;
}
void QgsLayoutItemElevationProfile::setTolerance( double tolerance )
{
if ( mTolerance == tolerance )
return;
mTolerance = tolerance;
invalidateCache();
}
double QgsLayoutItemElevationProfile::tolerance() const
{
return mTolerance;
}
void QgsLayoutItemElevationProfile::setAtlasDriven( bool enabled )
{
mAtlasDriven = enabled;
}
QgsProfileRequest QgsLayoutItemElevationProfile::profileRequest() const
{
QgsProfileRequest req( mCurve ? mCurve.get()->clone() : nullptr );
req.setCrs( mCrs );
req.setTolerance( mTolerance );
req.setExpressionContext( createExpressionContext() );
if ( mLayout )
{
if ( QgsProject *project = mLayout->project() )
{
req.setTransformContext( project->transformContext() );
if ( QgsAbstractTerrainProvider *provider = project->elevationProperties()->terrainProvider() )
{
req.setTerrainProvider( provider->clone() );
}
}
}
return req;
}
void QgsLayoutItemElevationProfile::paint( QPainter *painter, const QStyleOptionGraphicsItem *itemStyle, QWidget * )
{
if ( !mLayout || !painter || !painter->device() || !mUpdatesEnabled )
{
return;
}
if ( !shouldDrawItem() )
{
return;
}
QRectF thisPaintRect = rect();
if ( qgsDoubleNear( thisPaintRect.width(), 0.0 ) || qgsDoubleNear( thisPaintRect.height(), 0 ) )
return;
if ( mLayout->renderContext().isPreviewRender() )
{
QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( mLayout, painter );
rc.setExpressionContext( createExpressionContext() );
QgsScopedQPainterState painterState( painter );
painter->setClipRect( thisPaintRect );
if ( !mCacheFinalImage || mCacheFinalImage->isNull() )
{
// No initial render available - so draw some preview text alerting user
painter->setBrush( QBrush( QColor( 125, 125, 125, 125 ) ) );
painter->drawRect( thisPaintRect );
painter->setBrush( Qt::NoBrush );
QFont messageFont;
messageFont.setPointSize( 12 );
painter->setFont( messageFont );
painter->setPen( QColor( 255, 255, 255, 255 ) );
painter->drawText( thisPaintRect, Qt::AlignCenter | Qt::AlignHCenter, tr( "Rendering profile" ) );
if (
( mRenderJob && mCacheInvalidated && !mDrawingPreview ) // current job was invalidated - start a new one
||
( !mRenderJob && !mDrawingPreview ) // this is the profiles's very first paint - trigger a cache update
)
{
mPreviewScaleFactor = QgsLayoutUtils::scaleFactorFromItemStyle( itemStyle, painter );
mBackgroundUpdateTimer->start( 1 );
}
}
else
{
if ( mCacheInvalidated && !mDrawingPreview )
{
// cache was invalidated - trigger a background update
mPreviewScaleFactor = QgsLayoutUtils::scaleFactorFromItemStyle( itemStyle, painter );
mBackgroundUpdateTimer->start( 1 );
}
//Background color is already included in cached image, so no need to draw
double imagePixelWidth = mCacheFinalImage->width(); //how many pixels of the image are for the map extent?
double scale = rect().width() / imagePixelWidth;
QgsScopedQPainterState rotatedPainterState( painter );
painter->scale( scale, scale );
painter->setCompositionMode( blendModeForRender() );
painter->drawImage( 0, 0, *mCacheFinalImage );
}
painter->setClipRect( thisPaintRect, Qt::NoClip );
if ( frameEnabled() )
{
QgsLayoutItem::drawFrame( rc );
}
}
else
{
if ( mDrawing )
return;
mDrawing = true;
QPaintDevice *paintDevice = painter->device();
if ( !paintDevice )
return;
QSizeF layoutSize = mLayout->convertToLayoutUnits( sizeWithUnits() );
if ( mLayout->renderContext().flags() & Qgis::LayoutRenderFlag::LosslessImageRendering )
painter->setRenderHint( QPainter::LosslessImageRendering, true );
mPlot->xScale = QgsUnitTypes::fromUnitToUnitFactor( mDistanceUnit, mCrs.mapUnits() );
if ( !qgsDoubleNear( layoutSize.width(), 0.0 ) && !qgsDoubleNear( layoutSize.height(), 0.0 ) )
{
const bool forceVector = mLayout && mLayout->renderContext().rasterizedRenderingPolicy() == Qgis::RasterizedRenderingPolicy::ForceVector;
if ( ( containsAdvancedEffects() || ( blendModeForRender() != QPainter::CompositionMode_SourceOver ) )
&& !forceVector )
{
// rasterize
double destinationDpi = QgsLayoutUtils::scaleFactorFromItemStyle( itemStyle, painter ) * 25.4;
double layoutUnitsInInches = mLayout ? mLayout->convertFromLayoutUnits( 1, Qgis::LayoutUnit::Inches ).length() : 1;
int widthInPixels = static_cast< int >( std::round( boundingRect().width() * layoutUnitsInInches * destinationDpi ) );
int heightInPixels = static_cast< int >( std::round( boundingRect().height() * layoutUnitsInInches * destinationDpi ) );
QImage image = QImage( widthInPixels, heightInPixels, QImage::Format_ARGB32 );
image.fill( Qt::transparent );
image.setDotsPerMeterX( static_cast< int >( std::round( 1000 * destinationDpi / 25.4 ) ) );
image.setDotsPerMeterY( static_cast< int >( std::round( 1000 * destinationDpi / 25.4 ) ) );
double dotsPerMM = destinationDpi / 25.4;
layoutSize *= dotsPerMM; // output size will be in dots (pixels)
QPainter p( &image );
preparePainter( &p );
QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( mLayout, &p );
rc.setExpressionContext( createExpressionContext() );
p.scale( dotsPerMM, dotsPerMM );
if ( hasBackground() )
{
QgsLayoutItem::drawBackground( rc );
}
p.scale( 1.0 / dotsPerMM, 1.0 / dotsPerMM );
const double mapUnitsPerPixel = static_cast<double>( mPlot->xMaximum() - mPlot->xMinimum() ) * mPlot->xScale / layoutSize.width();
rc.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
QList< QgsAbstractProfileSource *> sourcesToRender = sources();
std::reverse( sourcesToRender.begin(), sourcesToRender.end() ); // sources are rendered from bottom to top
QgsProfilePlotRenderer renderer( sourcesToRender, profileRequest() );
std::unique_ptr<QgsLineSymbol> rendererSubSectionsSymbol( subsectionsSymbol() ? subsectionsSymbol()->clone() : nullptr );
renderer.setSubsectionsSymbol( rendererSubSectionsSymbol.release() );
renderer.generateSynchronously();
mPlot->setRenderer( &renderer );
// size must be in pixels, not layout units
mPlot->setSize( layoutSize );
QgsPlotRenderContext plotContext;
mPlot->render( rc, plotContext );
mPlot->setRenderer( nullptr );
p.scale( dotsPerMM, dotsPerMM );
if ( frameEnabled() )
{
QgsLayoutItem::drawFrame( rc );
}
QgsScopedQPainterState painterState( painter );
painter->setCompositionMode( blendModeForRender() );
painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots
painter->drawImage( 0, 0, image );
painter->scale( dotsPerMM, dotsPerMM );
}
else
{
QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( mLayout, painter );
rc.setExpressionContext( createExpressionContext() );
// Fill with background color
if ( hasBackground() )
{
QgsLayoutItem::drawBackground( rc );
}
QgsScopedQPainterState painterState( painter );
QgsScopedQPainterState stagedPainterState( painter );
double dotsPerMM = paintDevice->logicalDpiX() / 25.4;
layoutSize *= dotsPerMM; // output size will be in dots (pixels)
painter->scale( 1 / dotsPerMM, 1 / dotsPerMM ); // scale painter from mm to dots
const double mapUnitsPerPixel = static_cast<double>( mPlot->xMaximum() - mPlot->xMinimum() ) * mPlot->xScale / layoutSize.width();
rc.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
QList< QgsAbstractProfileSource *> sourcesToRender = sources();
std::reverse( sourcesToRender.begin(), sourcesToRender.end() ); // sources are rendered from bottom to top
QgsProfilePlotRenderer renderer( sourcesToRender, profileRequest() );
std::unique_ptr<QgsLineSymbol> rendererSubSectionsSymbol( subsectionsSymbol() ? subsectionsSymbol()->clone() : nullptr );
renderer.setSubsectionsSymbol( rendererSubSectionsSymbol.release() );
// TODO
// we should be able to call renderer.start()/renderer.waitForFinished() here and
// benefit from parallel source generation. BUT
// for some reason the QtConcurrent::map call in start() never triggers
// the actual background thread execution.
// So for now just generate the results one by one
renderer.generateSynchronously();
mPlot->setRenderer( &renderer );
// size must be in pixels, not layout units
mPlot->setSize( layoutSize );
QgsPlotRenderContext plotContext;
mPlot->render( rc, plotContext );
mPlot->setRenderer( nullptr );
painter->setClipRect( thisPaintRect, Qt::NoClip );
if ( frameEnabled() )
{
QgsLayoutItem::drawFrame( rc );
}
}
}
mDrawing = false;
}
}
void QgsLayoutItemElevationProfile::refresh()
{
if ( mAtlasDriven && mLayout && mLayout->reportContext().layer() )
{
if ( QgsVectorLayer *layer = mLayout->reportContext().layer() )
{
mCrs = layer->crs();
}
const QgsGeometry curveGeom( mLayout->reportContext().currentGeometry( mCrs ) );
if ( const QgsAbstractGeometry *geom = curveGeom.constGet() )
{
if ( const QgsCurve *curve = qgsgeometry_cast< const QgsCurve * >( geom->simplifiedTypeRef() ) )
{
mCurve.reset( curve->clone() );
}
}
}
QgsLayoutItem::refresh();
invalidateCache();
}
void QgsLayoutItemElevationProfile::invalidateCache()
{
if ( mDrawing )
return;
mCacheInvalidated = true;
update();
}
void QgsLayoutItemElevationProfile::draw( QgsLayoutItemRenderContext & )
{
}
bool QgsLayoutItemElevationProfile::writePropertiesToElement( QDomElement &layoutProfileElem, QDomDocument &doc, const QgsReadWriteContext &rwContext ) const
{
{
QDomElement plotElement = doc.createElement( QStringLiteral( "plot" ) );
mPlot->writeXml( plotElement, doc, rwContext );
layoutProfileElem.appendChild( plotElement );
}
layoutProfileElem.setAttribute( QStringLiteral( "distanceUnit" ), qgsEnumValueToKey( mDistanceUnit ) );
layoutProfileElem.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
layoutProfileElem.setAttribute( QStringLiteral( "atlasDriven" ), mAtlasDriven ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
if ( mCrs.isValid() )
{
QDomElement crsElem = doc.createElement( QStringLiteral( "crs" ) );
mCrs.writeXml( crsElem, doc );
layoutProfileElem.appendChild( crsElem );
}
if ( mCurve )
{
QDomElement curveElem = doc.createElement( QStringLiteral( "curve" ) );
curveElem.appendChild( doc.createTextNode( mCurve->asWkt( ) ) );
layoutProfileElem.appendChild( curveElem );
}
{
QDomElement layersElement = doc.createElement( QStringLiteral( "layers" ) );
for ( const QgsMapLayerRef &layer : mLayers )
{
QDomElement layerElement = doc.createElement( QStringLiteral( "layer" ) );
layer.writeXml( layerElement, rwContext );
layersElement.appendChild( layerElement );
}
layoutProfileElem.appendChild( layersElement );
}
{
QDomElement sourcesElement = doc.createElement( QStringLiteral( "profileSources" ) );
for ( QgsAbstractProfileSource *source : mSources )
{
if ( source )
{
if ( QgsApplication::profileSourceRegistry()->findSourceById( source->profileSourceId() ) )
{
QDomElement sourceElement = doc.createElement( QStringLiteral( "profileCustomSource" ) );
sourceElement.setAttribute( QStringLiteral( "id" ), source->profileSourceId() );
sourcesElement.appendChild( sourceElement );
}
else if ( auto layer = QgsMapLayerRef( dynamic_cast<QgsMapLayer *>( source ) ) )
{
QDomElement sourceElement = doc.createElement( QStringLiteral( "profileLayerSource" ) );
layer.writeXml( sourceElement, rwContext );
sourcesElement.appendChild( sourceElement );
}
}
}
layoutProfileElem.appendChild( sourcesElement );
}
if ( mSubsectionsSymbol )
{
QDomElement subsectionsElement = doc.createElement( QStringLiteral( "subsections" ) );
const QDomElement symbolElement = QgsSymbolLayerUtils::saveSymbol( QStringLiteral( "subsections" ), mSubsectionsSymbol.get(), doc, rwContext );
subsectionsElement.appendChild( symbolElement );
layoutProfileElem.appendChild( subsectionsElement );
}
return true;
}
bool QgsLayoutItemElevationProfile::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &, const QgsReadWriteContext &context )
{
const QDomElement plotElement = itemElem.firstChildElement( QStringLiteral( "plot" ) );
if ( !plotElement.isNull() )
{
mPlot->readXml( plotElement, context );
}
const QDomNodeList crsNodeList = itemElem.elementsByTagName( QStringLiteral( "crs" ) );
QgsCoordinateReferenceSystem crs;
if ( !crsNodeList.isEmpty() )
{
const QDomElement crsElem = crsNodeList.at( 0 ).toElement();
crs.readXml( crsElem );
}
mCrs = crs;
setDistanceUnit( qgsEnumKeyToValue( itemElem.attribute( QStringLiteral( "distanceUnit" ) ), mCrs.mapUnits() ) );
const QDomNodeList curveNodeList = itemElem.elementsByTagName( QStringLiteral( "curve" ) );
if ( !curveNodeList.isEmpty() )
{
const QDomElement curveElem = curveNodeList.at( 0 ).toElement();
const QgsGeometry curve = QgsGeometry::fromWkt( curveElem.text() );
if ( const QgsCurve *curveGeom = qgsgeometry_cast< const QgsCurve * >( curve.constGet() ) )
{
mCurve.reset( curveGeom->clone() );
}
else
{
mCurve.reset();
}
}
mTolerance = itemElem.attribute( QStringLiteral( "tolerance" ) ).toDouble();
mAtlasDriven = static_cast< bool >( itemElem.attribute( QStringLiteral( "atlasDriven" ), QStringLiteral( "0" ) ).toInt() );
{
mLayers.clear();
const QDomElement layersElement = itemElem.firstChildElement( QStringLiteral( "layers" ) );
QDomElement layerElement = layersElement.firstChildElement( QStringLiteral( "layer" ) );
while ( !layerElement.isNull() )
{
QgsMapLayerRef ref;
ref.readXml( layerElement, context );
ref.resolveWeakly( mLayout->project() );
mLayers.append( ref );
layerElement = layerElement.nextSiblingElement( QStringLiteral( "layer" ) );
}
}
{
mSources.clear();
const QDomElement sourcesElement = itemElem.firstChildElement( QStringLiteral( "profileSources" ) );
QDomElement sourceElement = sourcesElement.firstChildElement();
while ( !sourceElement.isNull() )
{
if ( sourceElement.tagName() == QStringLiteral( "profileCustomSource" ) )
{
const QString sourceId = sourceElement.attribute( QStringLiteral( "id" ) );
if ( QgsAbstractProfileSource *profileSource = QgsApplication::profileSourceRegistry()->findSourceById( sourceId ) )
{
mSources.append( profileSource );
}
}
else if ( sourceElement.tagName() == QStringLiteral( "profileLayerSource" ) )
{
QgsMapLayerRef ref;
ref.readXml( sourceElement, context );
ref.resolveWeakly( mLayout->project() );
if ( ref.get() )
{
mSources.append( ref.get()->profileSource() );
}
}
sourceElement = sourceElement.nextSiblingElement();
}
}
const QDomElement subsectionsElement = itemElem.firstChildElement( QStringLiteral( "subsections" ) );
const QDomElement symbolsElement = subsectionsElement.firstChildElement( QStringLiteral( "symbol" ) );
if ( !symbolsElement.isNull() )
{
std::unique_ptr< QgsLineSymbol > subSectionsSymbol = QgsSymbolLayerUtils::loadSymbol<QgsLineSymbol >( symbolsElement, context );
if ( subSectionsSymbol )
{
setSubsectionsSymbol( subSectionsSymbol.release() );
}
}
return true;
}
void QgsLayoutItemElevationProfile::recreateCachedImageInBackground()
{
if ( mRenderJob )
{
disconnect( mRenderJob.get(), &QgsProfilePlotRenderer::generationFinished, this, &QgsLayoutItemElevationProfile::profileGenerationFinished );
QgsProfilePlotRenderer *oldJob = mRenderJob.release();
QPainter *oldPainter = mPainter.release();
QImage *oldImage = mCacheRenderingImage.release();
connect( oldJob, &QgsProfilePlotRenderer::generationFinished, this, [oldPainter, oldJob, oldImage]
{
oldJob->deleteLater();
delete oldPainter;
delete oldImage;
} );
oldJob->cancelGenerationWithoutBlocking();
}
else
{
mCacheRenderingImage.reset( nullptr );
emit backgroundTaskCountChanged( 1 );
}
Q_ASSERT( !mRenderJob );
Q_ASSERT( !mPainter );
Q_ASSERT( !mCacheRenderingImage );
const QSizeF layoutSize = mLayout->convertToLayoutUnits( sizeWithUnits() );
double widthLayoutUnits = layoutSize.width();
double heightLayoutUnits = layoutSize.height();
int w = static_cast< int >( std::round( widthLayoutUnits * mPreviewScaleFactor ) );
int h = static_cast< int >( std::round( heightLayoutUnits * mPreviewScaleFactor ) );
// limit size of image for better performance
if ( w > 5000 || h > 5000 )
{
if ( w > h )
{
w = 5000;
h = static_cast< int>( std::round( w * heightLayoutUnits / widthLayoutUnits ) );
}
else
{
h = 5000;
w = static_cast< int >( std::round( h * widthLayoutUnits / heightLayoutUnits ) );
}
}
if ( w <= 0 || h <= 0 )
return;
mCacheRenderingImage.reset( new QImage( w, h, QImage::Format_ARGB32 ) );
// set DPI of the image
mCacheRenderingImage->setDotsPerMeterX( static_cast< int >( std::round( 1000 * w / widthLayoutUnits ) ) );
mCacheRenderingImage->setDotsPerMeterY( static_cast< int >( std::round( 1000 * h / heightLayoutUnits ) ) );
//start with empty fill to avoid artifacts
mCacheRenderingImage->fill( Qt::transparent );
if ( hasBackground() )
{
//Initially fill image with specified background color
mCacheRenderingImage->fill( backgroundColor().rgba() );
}
mCacheInvalidated = false;
mPainter.reset( new QPainter( mCacheRenderingImage.get() ) );
QList< QgsAbstractProfileSource *> sourcesToRender = sources();
std::reverse( sourcesToRender.begin(), sourcesToRender.end() ); // sources are rendered from bottom to top
mRenderJob = std::make_unique< QgsProfilePlotRenderer >( sourcesToRender, profileRequest() );
std::unique_ptr<QgsLineSymbol> rendererSubSectionsSymbol( subsectionsSymbol() ? subsectionsSymbol()->clone() : nullptr );
mRenderJob->setSubsectionsSymbol( rendererSubSectionsSymbol.release() );
connect( mRenderJob.get(), &QgsProfilePlotRenderer::generationFinished, this, &QgsLayoutItemElevationProfile::profileGenerationFinished );
mRenderJob->startGeneration();
mDrawingPreview = false;
}
void QgsLayoutItemElevationProfile::profileGenerationFinished()
{
mPlot->setRenderer( mRenderJob.get() );
QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( mLayout, mPainter.get() );
mPlot->xScale = QgsUnitTypes::fromUnitToUnitFactor( mDistanceUnit, mCrs.mapUnits() );
const double mapUnitsPerPixel = static_cast< double >( mPlot->xMaximum() - mPlot->xMinimum() ) * mPlot->xScale /
mCacheRenderingImage->size().width();
rc.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
// size must be in pixels, not layout units
mPlot->setSize( mCacheRenderingImage->size() );
QgsPlotRenderContext plotContext;
mPlot->render( rc, plotContext );
mPlot->setRenderer( nullptr );
mPainter->end();
mRenderJob.reset( nullptr );
mPainter.reset( nullptr );
mCacheFinalImage = std::move( mCacheRenderingImage );
emit backgroundTaskCountChanged( 0 );
update();
emit previewRefreshed();
}
Qgis::DistanceUnit QgsLayoutItemElevationProfile::distanceUnit() const
{
return mDistanceUnit;
}
void QgsLayoutItemElevationProfile::setDistanceUnit( Qgis::DistanceUnit unit )
{
mDistanceUnit = unit;
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:
mPlot->xAxis().setLabelSuffix( QStringLiteral( " %1" ).arg( QgsUnitTypes::toAbbreviatedString( mDistanceUnit ) ) );
break;
case Qgis::DistanceUnit::Degrees:
mPlot->xAxis().setLabelSuffix( QObject::tr( "°" ) );
break;
case Qgis::DistanceUnit::Unknown:
mPlot->xAxis().setLabelSuffix( QString() );
break;
}
}
void QgsLayoutItemElevationProfile::setSubsectionsSymbol( QgsLineSymbol *symbol )
{
mSubsectionsSymbol.reset( symbol );
}
void QgsLayoutItemElevationProfile::setSourcesPrivate()
{
mSources = QgsApplication::profileSourceRegistry()->profileSources();
}