Skip to content

Commit

Permalink
Add more granular feedback to Extract Labels algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Jan 10, 2022
1 parent 286f79e commit 40ba7b0
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 28 deletions.
Expand Up @@ -159,6 +159,7 @@ Does not take ownership of the object.




int renderingTime() const;
%Docstring
Returns the total time it took to finish the job (in milliseconds).
Expand Down
89 changes: 82 additions & 7 deletions src/analysis/processing/qgsalgorithmextractlabels.cpp
Expand Up @@ -25,6 +25,7 @@
#include "qgsscalecalculator.h"
#include "qgstextlabelfeature.h"
#include "qgsnullsymbolrenderer.h"
#include "qgsprocessingfeedback.h"

#include "pal/feature.h"
#include "pal/pointset.h"
Expand Down Expand Up @@ -122,11 +123,11 @@ QgsExtractLabelsAlgorithm *QgsExtractLabelsAlgorithm::createInstance() const
class ExtractLabelSink : public QgsLabelSink
{
public:
ExtractLabelSink( QMap<QString, QString> mapLayerNames, QgsProcessingFeedback *feedback )
ExtractLabelSink( const QMap<QString, QString> &mapLayerNames, QgsProcessingFeedback *feedback )
: mMapLayerNames( mapLayerNames )
, mFeedback( feedback )
{
};
}

void drawLabel( const QString &layerId, QgsRenderContext &context, pal::LabelPosition *label, const QgsPalLayerSettings &settings ) override
{
Expand Down Expand Up @@ -175,7 +176,7 @@ class ExtractLabelSink : public QgsLabelSink
const QString labelText = QgsPalLabeling::splitToLines( labelFeature->text( -1 ),
labelSettings.wrapChar,
labelSettings.autoWrapLength,
labelSettings.useMaxLineLengthForAutoWrap ).join( "\n" );
labelSettings.useMaxLineLengthForAutoWrap ).join( '\n' );

QString labelAlignment;
if ( dataDefinedValues.contains( QgsPalLayerSettings::MultiLineAlignment ) )
Expand Down Expand Up @@ -307,7 +308,7 @@ class ExtractLabelSink : public QgsLabelSink
feature.setAttributes( attributes );
feature.setGeometry( geometry );
features << feature;
};
}

QList<QgsFeature> features;

Expand Down Expand Up @@ -389,7 +390,7 @@ QVariantMap QgsExtractLabelsAlgorithm::processAlgorithm( const QVariantMap &para

QgsNullPaintDevice nullPaintDevice;
nullPaintDevice.setOutputSize( imageSize );
nullPaintDevice.setOutputDpi( dpi );
nullPaintDevice.setOutputDpi( static_cast< int >( std::round( dpi ) ) );
QPainter painter( &nullPaintDevice );

QgsMapRendererCustomPainterJob renderJob( mapSettings, &painter );
Expand All @@ -398,20 +399,92 @@ QVariantMap QgsExtractLabelsAlgorithm::processAlgorithm( const QVariantMap &para

feedback->pushInfo( QObject::tr( "Extracting labels" ) );

QgsProcessingMultiStepFeedback multiStepFeedback( 10, feedback );
multiStepFeedback.setCurrentStep( 0 );

QEventLoop loop;
QObject::connect( feedback, &QgsFeedback::canceled, &renderJob, &QgsMapRendererCustomPainterJob::cancel );
QObject::connect( &renderJob, &QgsMapRendererJob::renderingLayersFinished, feedback, [feedback]() { feedback->pushInfo( QObject::tr( "Calculating label placement" ) ); } );
QObject::connect( &renderJob, &QgsMapRendererJob::layerRenderingStarted, feedback, [this, feedback]( const QString & layerId ) { feedback->pushInfo( QObject::tr( "Collecting labels for %1" ).arg( mMapLayerNames.value( layerId ) ) ); } );
int labelsCollectedFromLayers = 0;
QObject::connect( &renderJob, &QgsMapRendererJob::layerRenderingStarted, feedback, [this, &multiStepFeedback, &labelsCollectedFromLayers]( const QString & layerId )
{
multiStepFeedback.pushInfo( QObject::tr( "Collecting labelled features from %1" ).arg( mMapLayerNames.value( layerId ) ) );
multiStepFeedback.setProgress( 100.0 * static_cast< double >( labelsCollectedFromLayers ) / mMapLayers.size() );
labelsCollectedFromLayers++;
} );

QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::labelRegistrationAboutToBegin, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setCurrentStep( 1 );
multiStepFeedback.pushInfo( QObject::tr( "Registering labels" ) );
} );

QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::providerRegistrationAboutToBegin, &multiStepFeedback, [this, &multiStepFeedback]( QgsAbstractLabelProvider * provider )
{
multiStepFeedback.setCurrentStep( 2 );
if ( !provider->layerId().isEmpty() )
{
multiStepFeedback.pushInfo( QObject::tr( "Adding labels from %1" ).arg( mMapLayerNames.value( provider->layerId() ) ) );
}
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::candidateCreationAboutToBegin, &multiStepFeedback, [this, &multiStepFeedback]( QgsAbstractLabelProvider * provider )
{
multiStepFeedback.setCurrentStep( 3 );
if ( !provider->layerId().isEmpty() )
{
multiStepFeedback.pushInfo( QObject::tr( "Generating label placement candidates for %1" ).arg( mMapLayerNames.value( provider->layerId() ) ) );
}
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::obstacleCostingAboutToBegin, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setCurrentStep( 4 );
multiStepFeedback.setProgressText( QObject::tr( "Calculating obstacle costs" ) );
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::calculatingConflictsAboutToBegin, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setCurrentStep( 5 );
multiStepFeedback.setProgressText( QObject::tr( "Calculating label conflicts" ) );
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::finalizingCandidatesAboutToBegin, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setCurrentStep( 6 );
multiStepFeedback.setProgressText( QObject::tr( "Finalizing candidates" ) );
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::reductionAboutToBegin, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setCurrentStep( 7 );
multiStepFeedback.setProgressText( QObject::tr( "Reducing problem" ) );
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::solvingPlacementAboutToBegin, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setCurrentStep( 8 );
multiStepFeedback.setProgressText( QObject::tr( "Determining optimal label placements" ) );
} );
QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::solvingPlacementFinished, &multiStepFeedback, [&multiStepFeedback]()
{
multiStepFeedback.setProgressText( QObject::tr( "Labeling complete" ) );
} );

QObject::connect( renderJob.labelingEngineFeedback(), &QgsLabelingEngineFeedback::progressChanged, &multiStepFeedback, [&multiStepFeedback]( double progress )
{
multiStepFeedback.setProgress( progress );
} );

QObject::connect( &renderJob, &QgsMapRendererJob::finished, &loop, [&loop]() { loop.exit(); } );
renderJob.start();
loop.exec();

qDeleteAll( mMapLayers );
mMapLayers.clear();

multiStepFeedback.setCurrentStep( 9 );
feedback->pushInfo( QObject::tr( "Writing %n label(s) to output layer", "", labelSink.features.count() ) );
const double step = !labelSink.features.empty() ? 100.0 / labelSink.features.count() : 1;
long long index = -1;
for ( QgsFeature &feature : labelSink.features )
{
index++;
multiStepFeedback.setProgress( step * index );
if ( feedback->isCanceled() )
break;

Expand Down Expand Up @@ -498,7 +571,9 @@ bool QgsExtractLabelsAlgorithm::prepareAlgorithm( const QVariantMap &parameters,
{
QList<QgsMapLayer *> layers;
QgsLayerTree *root = context.project()->layerTreeRoot();
for ( QgsLayerTreeLayer *nodeLayer : root->findLayers() )
const QList<QgsLayerTreeLayer *> layerTreeLayers = root->findLayers();
layers.reserve( layerTreeLayers.size() );
for ( QgsLayerTreeLayer *nodeLayer : layerTreeLayers )
{
QgsMapLayer *layer = nodeLayer->layer();
if ( nodeLayer->isVisible() && root->layerOrder().contains( layer ) )
Expand Down
21 changes: 19 additions & 2 deletions src/core/labeling/qgslabelingengine.cpp
Expand Up @@ -268,6 +268,11 @@ void QgsLabelingEngine::processProvider( QgsAbstractLabelProvider *provider, Qgs

void QgsLabelingEngine::registerLabels( QgsRenderContext &context )
{
QgsLabelingEngineFeedback *feedback = qobject_cast< QgsLabelingEngineFeedback * >( context.feedback() );

if ( feedback )
feedback->emit labelRegistrationAboutToBegin();

const QgsLabelingEngineSettings &settings = mMapSettings.labelingEngineSettings();

mPal = std::make_unique< pal::Pal >();
Expand All @@ -279,15 +284,27 @@ void QgsLabelingEngine::registerLabels( QgsRenderContext &context )
mPal->setPlacementVersion( settings.placementVersion() );

// for each provider: get labels and register them in PAL
const double step = !mProviders.empty() ? 100.0 / mProviders.size() : 1;
int index = 0;
for ( QgsAbstractLabelProvider *provider : std::as_const( mProviders ) )
{
if ( feedback )
{
feedback->emit providerRegistrationAboutToBegin( provider );
feedback->setProgress( index * step );
}
index++;
std::unique_ptr< QgsExpressionContextScopePopper > layerScopePopper;
if ( provider->layerExpressionContextScope() )
{
layerScopePopper = std::make_unique< QgsExpressionContextScopePopper >( context.expressionContext(), new QgsExpressionContextScope( *provider->layerExpressionContextScope() ) );
}
processProvider( provider, context, *mPal );
if ( feedback )
feedback->emit providerRegistrationFinished( provider );
}
if ( feedback )
feedback->emit labelRegistrationFinished();
}

void QgsLabelingEngine::solve( QgsRenderContext &context )
Expand Down Expand Up @@ -351,7 +368,7 @@ void QgsLabelingEngine::solve( QgsRenderContext &context )
// do the labeling itself
try
{
mProblem = mPal->extractProblem( extent, mapBoundaryGeom );
mProblem = mPal->extractProblem( extent, mapBoundaryGeom, context );
}
catch ( std::exception &e )
{
Expand Down Expand Up @@ -395,7 +412,7 @@ void QgsLabelingEngine::solve( QgsRenderContext &context )
}

// find the solution
mLabels = mPal->solveProblem( mProblem.get(),
mLabels = mPal->solveProblem( mProblem.get(), context,
settings.testFlag( QgsLabelingEngineSettings::UseAllLabels ),
settings.testFlag( QgsLabelingEngineSettings::DrawUnplacedLabels ) || settings.testFlag( QgsLabelingEngineSettings::CollectUnplacedLabels ) ? &mUnlabeled : nullptr );

Expand Down
104 changes: 103 additions & 1 deletion src/core/labeling/qgslabelingengine.h
Expand Up @@ -24,6 +24,7 @@
#include "qgspallabeling.h"
#include "qgslabelingenginesettings.h"
#include "qgslabeling.h"
#include "qgsfeedback.h"

class QgsLabelingEngine;
class QgsLabelingResults;
Expand Down Expand Up @@ -202,6 +203,108 @@ class CORE_EXPORT QgsAbstractLabelProvider
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsAbstractLabelProvider::Flags )


/**
* \ingroup core
* \brief QgsFeedback subclass for granular reporting of labeling engine progress.
* \note not available in Python bindings
* \since QGIS 3.24
*/
class CORE_EXPORT QgsLabelingEngineFeedback : public QgsFeedback
{
Q_OBJECT

public:

/**
* Constructor for QgsLabelingEngineFeedback, with the specified \a parent object.
*/
QgsLabelingEngineFeedback( QObject *parent SIP_TRANSFERTHIS = nullptr )
: QgsFeedback( parent )
{}

signals:

/**
* Emitted when the label registration is about to begin.
*/
void labelRegistrationAboutToBegin();

/**
* Emitted when the label registration has completed for all providers.
*/
void labelRegistrationFinished();

/**
* Emitted when the label registration is about to begin for a \a provider.
*/
void providerRegistrationAboutToBegin( QgsAbstractLabelProvider *provider );

/**
* Emitted when the label registration has completed for a \a provider.
*/
void providerRegistrationFinished( QgsAbstractLabelProvider *provider );

/**
* Emitted when the label candidate creation is about to begin for a \a provider.
*/
void candidateCreationAboutToBegin( QgsAbstractLabelProvider *provider );

/**
* Emitted when the label candidate creation has completed for a \a provider.
*/
void candidateCreationFinished( QgsAbstractLabelProvider *provider );

/**
* Emitted when the obstacle costing is about to begin.
*/
void obstacleCostingAboutToBegin();

/**
* Emitted when the obstacle costing has completed.
*/
void obstacleCostingFinished();

/**
* Emitted when the conflict handling step is about to begin.
*/
void calculatingConflictsAboutToBegin();

/**
* Emitted when the conflict handling step has completed.
*/
void calculatingConflictsFinished();

/**
* Emitted when the label candidates are about to be finalized.
*/
void finalizingCandidatesAboutToBegin();

/**
* Emitted when the label candidates are finalized.
*/
void finalizingCandidatesFinished();

/**
* Emitted when the candidate reduction step is about to begin.
*/
void reductionAboutToBegin();

/**
* Emitted when the candidate reduction step is finished.
*/
void reductionFinished();

/**
* Emitted when the problem solving step is about to begin.
*/
void solvingPlacementAboutToBegin();

/**
* Emitted when the problem solving step is finished.
*/
void solvingPlacementFinished();
};

/**
* \ingroup core
* \brief The QgsLabelingEngine class provides map labeling functionality.
Expand Down Expand Up @@ -298,7 +401,6 @@ class CORE_EXPORT QgsLabelingEngine
* Runs the label registration step.
*
* Must be called by subclasses prior to solve() and drawLabels()
*
* \since QGIS 3.10
*/
void registerLabels( QgsRenderContext &context );
Expand Down
7 changes: 7 additions & 0 deletions src/core/maprenderer/qgsmaprendererjob.cpp
Expand Up @@ -132,6 +132,7 @@ bool LayerRenderJob::imageCanBeComposed() const
QgsMapRendererJob::QgsMapRendererJob( const QgsMapSettings &settings )
: mSettings( settings )
, mRenderedItemResults( std::make_unique< QgsRenderedItemResults >( settings.extent() ) )
, mLabelingEngineFeedback( new QgsLabelingEngineFeedback( this ) )
{}

QgsMapRendererJob::~QgsMapRendererJob() = default;
Expand Down Expand Up @@ -173,6 +174,11 @@ void QgsMapRendererJob::setCache( QgsMapRendererCache *cache )
mCache = cache;
}

QgsLabelingEngineFeedback *QgsMapRendererJob::labelingEngineFeedback()
{
return mLabelingEngineFeedback;
}

QHash<QgsMapLayer *, int> QgsMapRendererJob::perLayerRenderingTime() const
{
QHash<QgsMapLayer *, int> result;
Expand Down Expand Up @@ -771,6 +777,7 @@ LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabe
job.context = QgsRenderContext::fromMapSettings( mSettings );
job.context.setPainter( painter );
job.context.setLabelingEngine( labelingEngine2 );
job.context.setFeedback( mLabelingEngineFeedback );

QgsRectangle r1 = mSettings.visibleExtent();
r1.grow( mSettings.extentBuffer() );
Expand Down
11 changes: 11 additions & 0 deletions src/core/maprenderer/qgsmaprendererjob.h
Expand Up @@ -368,6 +368,16 @@ class CORE_EXPORT QgsMapRendererJob : public QObject SIP_ABSTRACT
*/
void setLabelSink( QgsLabelSink *sink ) { mLabelSink = sink; } SIP_SKIP

/**
* Returns the associated labeling engine feedback object.
*
* Callers can connect to the signals in this object to receive granular progress reports during the labeling steps.
*
* \note Not available in Python bindings
* \since QGIS 3.24
*/
QgsLabelingEngineFeedback *labelingEngineFeedback() SIP_SKIP;

/**
* Returns the total time it took to finish the job (in milliseconds).
* \see perLayerRenderingTime()
Expand Down Expand Up @@ -595,6 +605,7 @@ class CORE_EXPORT QgsMapRendererJob : public QObject SIP_ABSTRACT
virtual void startPrivate() = 0;

QgsLabelSink *mLabelSink = nullptr;
QgsLabelingEngineFeedback *mLabelingEngineFeedback = nullptr;

};

Expand Down

0 comments on commit 40ba7b0

Please sign in to comment.