Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #52096 from joto/symbol-levels-plus
  • Loading branch information
m-kuhn committed May 8, 2023
2 parents 5837820 + 28a9b11 commit 4720209
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 41 deletions.
126 changes: 85 additions & 41 deletions src/core/vector/qgsvectorlayerrenderer.cpp
Expand Up @@ -544,7 +544,28 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
{
const bool isMainRenderer = renderer == mRenderer;

QHash< QgsSymbol *, QList<QgsFeature> > features; // key = symbol, value = array of features
// We need to figure out in which order all the features should be rendered.
// Ordering is based on (a) a "level" which is determined by the configured
// feature rendering order" and (b) the symbol level. The "level" is
// determined by the values of the attributes defined in the feature
// rendering order settings. Each time the attribute(s) have a new distinct
// value, a new empty QHash is added to the "features" list. This QHash is
// then filled by mappings from the symbol to a list of all the features
// that should be rendered by that symbol.
//
// If orderBy is not enabled, this list will only ever contain a single
// element.
QList<QHash< QgsSymbol *, QList<QgsFeature> >> features;

// We have at least one "level" for the features.
features.push_back( {} );

QSet<int> orderByAttributeIdx;
if ( renderer->orderByEnabled() )
{
orderByAttributeIdx = renderer->orderBy().usedAttributeIndices( mSource->fields() );
}

QgsRenderContext &context = *renderContext();

QgsSingleSymbolRenderer *selRenderer = nullptr;
Expand Down Expand Up @@ -572,6 +593,7 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q

// 1. fetch features
QgsFeature fet;
QVector<QVariant> prevValues; // previous values of ORDER BY attributes
while ( fit.nextFeature( fet ) )
{
if ( context.renderingStopped() )
Expand All @@ -595,13 +617,33 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
continue;
}

if ( renderer->orderByEnabled() )
{
QVector<QVariant> currentValues;
for ( auto const idx : orderByAttributeIdx )
{
currentValues.push_back( fet.attribute( idx ) );
}
if ( prevValues.empty() )
{
prevValues = std::move( currentValues );
}
else if ( currentValues != prevValues )
{
// Current values of ORDER BY attributes are different than previous
// values of these attributes. Start a new level.
prevValues = std::move( currentValues );
features.push_back( {} );
}
}

if ( !context.testFlag( Qgis::RenderContextFlag::SkipSymbolRendering ) )
{
if ( !features.contains( sym ) )
if ( !features.back().contains( sym ) )
{
features.insert( sym, QList<QgsFeature>() );
features.back().insert( sym, QList<QgsFeature>() );
}
features[sym].append( fet );
features.back()[sym].append( fet );
}

// new labeling engine
Expand Down Expand Up @@ -637,7 +679,7 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q

scopePopper.reset();

if ( features.empty() )
if ( features.back().empty() )
{
// nothing to draw
stopRenderer( renderer, selRenderer );
Expand Down Expand Up @@ -666,52 +708,54 @@ void QgsVectorLayerRenderer::drawRendererLevels( QgsFeatureRenderer *renderer, Q
context.setFeatureClipGeometry( mClipFeatureGeom );

// 2. draw features in correct order
for ( int l = 0; l < levels.count(); l++ )
for ( auto &featureLists : features )
{
QgsSymbolLevel &level = levels[l];
for ( int i = 0; i < level.count(); i++ )
for ( int l = 0; l < levels.count(); l++ )
{
QgsSymbolLevelItem &item = level[i];
if ( !features.contains( item.symbol() ) )
const QgsSymbolLevel &level = levels[l];
for ( int i = 0; i < level.count(); i++ )
{
QgsDebugMsg( QStringLiteral( "level item's symbol not found!" ) );
continue;
}
int layer = item.layer();
QList<QgsFeature> &lst = features[item.symbol()];
QList<QgsFeature>::iterator fit;
for ( fit = lst.begin(); fit != lst.end(); ++fit )
{
if ( context.renderingStopped() )
const QgsSymbolLevelItem &item = level[i];
if ( !featureLists.contains( item.symbol() ) )
{
stopRenderer( renderer, selRenderer );
return;
QgsDebugMsg( QStringLiteral( "level item's symbol not found!" ) );
continue;
}
const int layer = item.layer();
const QList<QgsFeature> &lst = featureLists[item.symbol()];
for ( auto fit = lst.begin(); fit != lst.end(); ++fit )
{
if ( context.renderingStopped() )
{
stopRenderer( renderer, selRenderer );
return;
}

bool sel = isMainRenderer && context.showSelection() && mSelectedFeatureIds.contains( fit->id() );
// maybe vertex markers should be drawn only during the last pass...
bool drawMarker = isMainRenderer && ( mDrawVertexMarkers && context.drawEditingInformation() && ( !mVertexMarkerOnlyForSelection || sel ) );

if ( ! mNoSetLayerExpressionContext )
context.expressionContext().setFeature( *fit );
const bool sel = isMainRenderer && context.showSelection() && mSelectedFeatureIds.contains( fit->id() );
// maybe vertex markers should be drawn only during the last pass...
const bool drawMarker = isMainRenderer && ( mDrawVertexMarkers && context.drawEditingInformation() && ( !mVertexMarkerOnlyForSelection || sel ) );

try
{
renderer->renderFeature( *fit, context, layer, sel, drawMarker );
if ( ! mNoSetLayerExpressionContext )
context.expressionContext().setFeature( *fit );

// as soon as first feature is rendered, we can start showing layer updates.
// but if we are blocking render updates (so that a previously cached image is being shown), we wait
// at most e.g. 3 seconds before we start forcing progressive updates.
if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
try
{
mReadyToCompose = true;
renderer->renderFeature( *fit, context, layer, sel, drawMarker );

// as soon as first feature is rendered, we can start showing layer updates.
// but if we are blocking render updates (so that a previously cached image is being shown), we wait
// at most e.g. 3 seconds before we start forcing progressive updates.
if ( !mBlockRenderUpdates || mElapsedTimer.elapsed() > MAX_TIME_TO_USE_CACHED_PREVIEW_IMAGE )
{
mReadyToCompose = true;
}
}
catch ( const QgsCsException &cse )
{
Q_UNUSED( cse )
QgsDebugMsg( QStringLiteral( "Failed to transform a point while drawing a feature with ID '%1'. Ignoring this feature. %2" )
.arg( fet.id() ).arg( cse.what() ) );
}
}
catch ( const QgsCsException &cse )
{
Q_UNUSED( cse )
QgsDebugMsg( QStringLiteral( "Failed to transform a point while drawing a feature with ID '%1'. Ignoring this feature. %2" )
.arg( fet.id() ).arg( cse.what() ) );
}
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/src/core/CMakeLists.txt
Expand Up @@ -84,6 +84,7 @@ set(TESTS
testqgsjsonutils.cpp
testqgslabelingengine.cpp
testqgslayerdefinition.cpp
testqgslayeredsymbollevel.cpp
testqgslayertree.cpp
testqgslayout.cpp
testqgslayoutatlas.cpp
Expand Down
144 changes: 144 additions & 0 deletions tests/src/core/testqgslayeredsymbollevel.cpp
@@ -0,0 +1,144 @@
/***************************************************************************
testqgslayeredsymbollevel.cpp
--------------------------------------
Date : April 2016
Copyright : (C) 2016 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 "qgstest.h"
#include <QObject>
#include <QString>
#include <QStringList>
#include <QApplication>
#include <QFileInfo>
#include <QDir>
#include <QDesktopServices>

//qgis includes...
#include <qgsmapsettings.h>
#include <qgsmaplayer.h>
#include <qgsvectorlayer.h>
#include <qgsapplication.h>
#include <qgsproviderregistry.h>
#include <qgsproject.h>
#include <qgssinglesymbolrenderer.h>
#include <qgsfeaturerequest.h>
#include <qgslinesymbol.h>
#include <qgslinesymbollayer.h>
#include <qgssymbol.h>
#include "qgslayoutitemmap.h"
#include "qgsmultirenderchecker.h"
#include "qgsexpressioncontextutils.h"
#include "qgslayout.h"

/**
* \ingroup UnitTests
* This is a unit test for layered symbol level rendering.
*/
class TestQgsLayeredSymbolLevel : public QgsTest
{
Q_OBJECT
public:
TestQgsLayeredSymbolLevel() : QgsTest( QStringLiteral( "Layered Symbol Level Rendering Tests" ) ) {}

private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.

void render();

private:
bool imageCheck( const QString &type );
QgsMapSettings mMapSettings;
QgsVectorLayer *mpRoadsLayer = nullptr;
QString mTestDataDir;
};


void TestQgsLayeredSymbolLevel::initTestCase()
{
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();
QgsApplication::showSettings();

//create some objects that will be used in all tests...
const QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
mTestDataDir = myDataDir + '/';

//
//create a roads layer that will be used in all tests...
//
const QString myRoadsFileName = mTestDataDir + "layered_roads.shp";
const QFileInfo myRoadsFileInfo( myRoadsFileName );
mpRoadsLayer = new QgsVectorLayer( myRoadsFileInfo.filePath(),
myRoadsFileInfo.completeBaseName(), QStringLiteral( "ogr" ) );

QgsVectorSimplifyMethod simplifyMethod;
simplifyMethod.setSimplifyHints( QgsVectorSimplifyMethod::NoSimplification );
mpRoadsLayer->setSimplifyMethod( simplifyMethod );

mMapSettings.setLayers( QList<QgsMapLayer *>() << mpRoadsLayer );

}
void TestQgsLayeredSymbolLevel::cleanupTestCase()
{
delete mpRoadsLayer;

QgsApplication::exitQgis();
}

void TestQgsLayeredSymbolLevel::render()
{
QgsSimpleLineSymbolLayer *lineSymbolLayer0 = new QgsSimpleLineSymbolLayer;
lineSymbolLayer0->setColor( Qt::black );
lineSymbolLayer0->setWidth( 3 );

QgsSimpleLineSymbolLayer *lineSymbolLayer1 = new QgsSimpleLineSymbolLayer;
lineSymbolLayer1->setColor( Qt::yellow );
lineSymbolLayer1->setWidth( 2 );

QgsLineSymbol *lineSymbol = new QgsLineSymbol( QgsSymbolLayerList() << lineSymbolLayer0 << lineSymbolLayer1 );

QgsSingleSymbolRenderer *renderer = new QgsSingleSymbolRenderer( lineSymbol );
mpRoadsLayer->setRenderer( renderer );

renderer->setUsingSymbolLevels( true );
QVERIFY( imageCheck( "with_levels_no_layers" ) );

renderer->setOrderBy( QgsFeatureRequest::OrderBy() << QgsFeatureRequest::OrderByClause( "layer", false ) );
renderer->setOrderByEnabled( true );
QVERIFY( imageCheck( "with_levels_with_layers" ) );

renderer->setUsingSymbolLevels( false );
QVERIFY( imageCheck( "no_levels_with_layers" ) );
}

bool TestQgsLayeredSymbolLevel::imageCheck( const QString &testType )
{
//use the QgsRenderChecker test utility class to
//ensure the rendered output matches our control image
mMapSettings.setExtent( mpRoadsLayer->extent() );
mMapSettings.setOutputSize( QSize( 400, 400 ) );
mMapSettings.setOutputDpi( 96 );
QgsExpressionContext context;
context << QgsExpressionContextUtils::mapSettingsScope( mMapSettings );
mMapSettings.setExpressionContext( context );
QgsMultiRenderChecker myChecker;
myChecker.setControlPathPrefix( QStringLiteral( "layered_symbol_levels" ) );
myChecker.setControlName( "expected_" + testType );
myChecker.setMapSettings( mMapSettings );
const bool myResultFlag = myChecker.runTest( testType, 0 );
mReport += myChecker.report();
return myResultFlag;
}

QGSTEST_MAIN( TestQgsLayeredSymbolLevel )
#include "testqgslayeredsymbollevel.moc"
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/testdata/layered_roads.cpg
@@ -0,0 +1 @@
UTF-8
Binary file added tests/testdata/layered_roads.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions tests/testdata/layered_roads.prj
@@ -0,0 +1 @@
PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]
Binary file added tests/testdata/layered_roads.shp
Binary file not shown.
Binary file added tests/testdata/layered_roads.shx
Binary file not shown.

0 comments on commit 4720209

Please sign in to comment.