Skip to content

Commit 80ae9ef

Browse files
author
Hugo Mercier
committed
Inverted polygon renderer: add an option to preprocess polygons using an union
1 parent 19041a8 commit 80ae9ef

15 files changed

+329
-40
lines changed

src/core/symbology-ng/qgsinvertedpolygonrenderer.cpp

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
#include <QDomElement>
2929

3030
QgsInvertedPolygonRenderer::QgsInvertedPolygonRenderer( const QgsFeatureRendererV2* subRenderer )
31-
: QgsFeatureRendererV2( "invertedPolygonRenderer" )
31+
: QgsFeatureRendererV2( "invertedPolygonRenderer" ), mPreprocessingEnabled( false )
3232
{
3333
if ( subRenderer ) {
3434
setEmbeddedRenderer( subRenderer );
@@ -154,15 +154,6 @@ bool QgsInvertedPolygonRenderer::renderFeature( QgsFeature& feature, QgsRenderCo
154154
return false;
155155
}
156156

157-
// We build here a "reversed" geometry of all the polygons
158-
//
159-
// The final geometry is a multipolygon F, with :
160-
// * the first polygon of F having the current extent as its exterior ring
161-
// * each polygon's exterior ring is added as interior ring of the first polygon of F
162-
// * each polygon's interior ring is added as new polygons in F
163-
//
164-
// No validity check is done, on purpose, it will be very slow and painting
165-
// operations do not need geometries to be valid
166157
if ( ! mFeaturesCategoryMap.contains(catId) )
167158
{
168159
// the exterior ring must be a square in the destination CRS
@@ -173,7 +164,7 @@ bool QgsInvertedPolygonRenderer::renderFeature( QgsFeature& feature, QgsRenderCo
173164
mFeaturesCategoryMap.insert( catId, cFeat );
174165
}
175166

176-
// update the gometry
167+
// update the geometry
177168
CombinedFeature& cFeat = mFeaturesCategoryMap[catId];
178169
QgsMultiPolygon multi;
179170
QgsGeometry* geom = feature.geometry();
@@ -190,37 +181,69 @@ bool QgsInvertedPolygonRenderer::renderFeature( QgsFeature& feature, QgsRenderCo
190181
multi = geom->asMultiPolygon();
191182
}
192183

193-
for ( int i = 0; i < multi.size(); i++ ) {
194-
// add the exterior ring as interior ring to the first polygon
195-
if ( mTransform ) {
196-
QgsPolyline new_ls;
197-
QgsPolyline& old_ls = multi[i][0];
198-
for ( int k = 0; k < old_ls.size(); k++ ) {
199-
new_ls.append( mTransform->transform( old_ls[k] ) );
200-
}
201-
cFeat.multiPolygon[0].append( new_ls );
184+
if ( mPreprocessingEnabled )
185+
{
186+
// preprocessing
187+
if ( ! cFeat.feature.geometry() )
188+
{
189+
// first feature: add the current geometry
190+
cFeat.feature.setGeometry( new QgsGeometry(*geom) );
202191
}
203192
else
204193
{
205-
cFeat.multiPolygon[0].append( multi[i][0] );
194+
// other features: combine them (union)
195+
QgsGeometry* combined = cFeat.feature.geometry()->combine( geom );
196+
if ( combined && combined->isGeosValid() )
197+
{
198+
cFeat.feature.setGeometry( combined );
199+
}
206200
}
207-
// add interior rings as new polygons
208-
for ( int j = 1; j < multi[i].size(); j++ ) {
209-
QgsPolygon new_poly;
201+
}
202+
else
203+
{
204+
// No preprocessing involved.
205+
// We build here a "reversed" geometry of all the polygons
206+
//
207+
// The final geometry is a multipolygon F, with :
208+
// * the first polygon of F having the current extent as its exterior ring
209+
// * each polygon's exterior ring is added as interior ring of the first polygon of F
210+
// * each polygon's interior ring is added as new polygons in F
211+
//
212+
// No validity check is done, on purpose, it will be very slow and painting
213+
// operations do not need geometries to be valid
214+
215+
for ( int i = 0; i < multi.size(); i++ ) {
216+
// add the exterior ring as interior ring to the first polygon
210217
if ( mTransform ) {
211218
QgsPolyline new_ls;
212-
QgsPolyline& old_ls = multi[i][j];
219+
QgsPolyline& old_ls = multi[i][0];
213220
for ( int k = 0; k < old_ls.size(); k++ ) {
214221
new_ls.append( mTransform->transform( old_ls[k] ) );
215222
}
216-
new_poly.append( new_ls );
223+
cFeat.multiPolygon[0].append( new_ls );
217224
}
218225
else
219226
{
220-
new_poly.append( multi[i][j] );
227+
cFeat.multiPolygon[0].append( multi[i][0] );
221228
}
229+
// add interior rings as new polygons
230+
for ( int j = 1; j < multi[i].size(); j++ ) {
231+
QgsPolygon new_poly;
232+
if ( mTransform ) {
233+
QgsPolyline new_ls;
234+
QgsPolyline& old_ls = multi[i][j];
235+
for ( int k = 0; k < old_ls.size(); k++ ) {
236+
new_ls.append( mTransform->transform( old_ls[k] ) );
237+
}
238+
new_poly.append( new_ls );
239+
}
240+
else
241+
{
242+
new_poly.append( multi[i][j] );
243+
}
222244

223-
cFeat.multiPolygon.append( new_poly );
245+
cFeat.multiPolygon.append( new_poly );
246+
}
224247
}
225248
}
226249
return true;
@@ -239,7 +262,24 @@ void QgsInvertedPolygonRenderer::stopRender( QgsRenderContext& context )
239262
for ( FeatureCategoryMap::iterator cit = mFeaturesCategoryMap.begin(); cit != mFeaturesCategoryMap.end(); ++cit)
240263
{
241264
QgsFeature feat( cit.value().feature );
242-
feat.setGeometry( QgsGeometry::fromMultiPolygon( cit.value().multiPolygon ) );
265+
if ( !mPreprocessingEnabled )
266+
{
267+
// no preprocessing - the final polygon has already been prepared
268+
feat.setGeometry( QgsGeometry::fromMultiPolygon( cit.value().multiPolygon ) );
269+
}
270+
else
271+
{
272+
// preprocessing mode - we still have to invert (using difference)
273+
if ( feat.geometry() )
274+
{
275+
QScopedPointer<QgsGeometry> rect( QgsGeometry::fromPolygon( mExtentPolygon ) );
276+
QgsGeometry *final = rect->difference( feat.geometry() );
277+
if ( final )
278+
{
279+
feat.setGeometry( final );
280+
}
281+
}
282+
}
243283
mSubRenderer->renderFeature( feat, context );
244284
}
245285

@@ -280,12 +320,17 @@ QString QgsInvertedPolygonRenderer::dump() const
280320

281321
QgsFeatureRendererV2* QgsInvertedPolygonRenderer::clone()
282322
{
323+
QgsInvertedPolygonRenderer* newRenderer;
283324
if ( mSubRenderer.isNull() )
284325
{
285-
return new QgsInvertedPolygonRenderer( 0 );
326+
newRenderer = new QgsInvertedPolygonRenderer( 0 );
327+
}
328+
else
329+
{
330+
newRenderer = new QgsInvertedPolygonRenderer( mSubRenderer->clone() );
286331
}
287-
// else
288-
return new QgsInvertedPolygonRenderer( mSubRenderer->clone() );
332+
newRenderer->setPreprocessingEnabled( preprocessingEnabled() );
333+
return newRenderer;
289334
}
290335

291336
QgsFeatureRendererV2* QgsInvertedPolygonRenderer::create( QDomElement& element )
@@ -297,13 +342,15 @@ QgsFeatureRendererV2* QgsInvertedPolygonRenderer::create( QDomElement& element )
297342
{
298343
r->setEmbeddedRenderer( QgsFeatureRendererV2::load( embeddedRendererElem ) );
299344
}
345+
r->setPreprocessingEnabled( element.attribute( "preprocessing", "0" ).toInt() == 1 );
300346
return r;
301347
}
302348

303349
QDomElement QgsInvertedPolygonRenderer::save( QDomDocument& doc )
304350
{
305351
QDomElement rendererElem = doc.createElement( RENDERER_TAG_NAME );
306352
rendererElem.setAttribute( "type", "invertedPolygonRenderer" );
353+
rendererElem.setAttribute( "preprocessing", preprocessingEnabled() ? "1" : "0" );
307354

308355
if ( mSubRenderer )
309356
{

src/core/symbology-ng/qgsinvertedpolygonrenderer.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,16 @@ class CORE_EXPORT QgsInvertedPolygonRenderer : public QgsFeatureRendererV2
104104
*/
105105
const QgsFeatureRendererV2* embeddedRenderer() const;
106106

107+
/** @returns true if the geometries are to be preprocessed (merged with an union) before rendering.*/
108+
bool preprocessingEnabled() const { return mPreprocessingEnabled; }
109+
/**
110+
@param enabled enables or disables the preprocessing.
111+
When enabled, geometries will be merged with an union before being rendered.
112+
It allows to fix some rendering artefacts (when rendering overlapping polygons for instance).
113+
This will involve some CPU-demanding computations and is thus disabled by default.
114+
*/
115+
void setPreprocessingEnabled( bool enabled ) { mPreprocessingEnabled = enabled; }
116+
107117
private:
108118
/** Private copy constructor. @see clone() */
109119
QgsInvertedPolygonRenderer( const QgsInvertedPolygonRenderer& );
@@ -145,6 +155,9 @@ class CORE_EXPORT QgsInvertedPolygonRenderer : public QgsFeatureRendererV2
145155
feature(a_feature),selected(a_selected), drawMarkers(a_drawMarkers), layer(a_layer) {}
146156
};
147157
QList<FeatureDecoration> mFeatureDecorations;
158+
159+
/** whether to preprocess (merge) geometries before rendering*/
160+
bool mPreprocessingEnabled;
148161
};
149162

150163

src/gui/symbology-ng/qgsinvertedpolygonrendererwidget.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ QgsInvertedPolygonRendererWidget::QgsInvertedPolygonRendererWidget( QgsVectorLay
6767
{
6868
// an existing inverted renderer
6969
mRenderer.reset( static_cast<QgsInvertedPolygonRenderer*>(renderer) );
70+
mMergePolygonsCheckBox->blockSignals( true );
71+
mMergePolygonsCheckBox->setCheckState( mRenderer->preprocessingEnabled() ? Qt::Checked : Qt::Unchecked );
72+
mMergePolygonsCheckBox->blockSignals( false );
7073
}
7174

7275
int currentEmbeddedIdx = 0;
@@ -122,11 +125,15 @@ void QgsInvertedPolygonRendererWidget::on_mRendererComboBox_currentIndexChanged(
122125
{
123126
mEmbeddedRendererWidget.reset( m->createRendererWidget( mLayer, mStyle, const_cast<QgsFeatureRendererV2*>(mRenderer->embeddedRenderer())->clone() ) );
124127

125-
if ( mLayout->count() > 1 ) {
128+
if ( mLayout->count() > 2 ) {
126129
// remove the current renderer widget
127-
mLayout->takeAt( 1 );
130+
mLayout->takeAt( 2 );
128131
}
129132
mLayout->addWidget( mEmbeddedRendererWidget.data() );
130133
}
131134
}
132135

136+
void QgsInvertedPolygonRendererWidget::on_mMergePolygonsCheckBox_stateChanged( int state )
137+
{
138+
mRenderer->setPreprocessingEnabled( state == Qt::Checked );
139+
}

src/gui/symbology-ng/qgsinvertedpolygonrendererwidget.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class GUI_EXPORT QgsInvertedPolygonRendererWidget : public QgsRendererV2Widget,
5454

5555
private slots:
5656
void on_mRendererComboBox_currentIndexChanged( int index );
57+
void on_mMergePolygonsCheckBox_stateChanged( int state );
5758
};
5859

5960

src/ui/qgsinvertedpolygonrendererwidgetbase.ui

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>390</width>
10-
<height>79</height>
9+
<width>316</width>
10+
<height>75</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
@@ -28,6 +28,16 @@
2828
</item>
2929
</layout>
3030
</item>
31+
<item>
32+
<widget class="QCheckBox" name="mMergePolygonsCheckBox">
33+
<property name="toolTip">
34+
<string/>
35+
</property>
36+
<property name="text">
37+
<string>Merge polygons before rendering (slow)</string>
38+
</property>
39+
</widget>
40+
</item>
3141
</layout>
3242
</widget>
3343
<resources/>

tests/src/core/testqgsinvertedpolygonrenderer.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class TestQgsInvertedPolygon: public QObject
4747

4848
void singleSubRenderer();
4949
void graduatedSubRenderer();
50+
void preprocess();
5051

5152
private:
5253
bool mTestHasError;
@@ -73,7 +74,7 @@ void TestQgsInvertedPolygon::initTestCase()
7374
//
7475
//create a poly layer that will be used in all tests...
7576
//
76-
QString myPolysFileName = mTestDataDir + "polys.shp";
77+
QString myPolysFileName = mTestDataDir + "polys_overlapping.shp";
7778
QFileInfo myPolyFileInfo( myPolysFileName );
7879
mpPolysLayer = new QgsVectorLayer( myPolyFileInfo.filePath(),
7980
myPolyFileInfo.completeBaseName(), "ogr" );
@@ -115,6 +116,14 @@ void TestQgsInvertedPolygon::graduatedSubRenderer()
115116
QVERIFY( imageCheck( "inverted_polys_graduated" ) );
116117
}
117118

119+
void TestQgsInvertedPolygon::preprocess()
120+
{
121+
// FIXME will have to find some overlapping polygons
122+
mReport += "<h2>Inverted polygon renderer, preprocessing test</h2>\n";
123+
QVERIFY( setQml( "inverted_polys_preprocess.qml" ) );
124+
QVERIFY( imageCheck( "inverted_polys_preprocess" ) );
125+
}
126+
118127
//
119128
// Private helper functions not called directly by CTest
120129
//
0 Bytes
Loading
627 KB
Loading
0 Bytes
Loading

tests/testdata/inverted_polys_graduated.qml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
22
<qgis version="2.3.0-Master" minimumScale="1" maximumScale="1e+08" simplifyDrawingHints="1" minLabelScale="1" maxLabelScale="1e+08" simplifyDrawingTol="1" simplifyMaxScale="1" hasScaleBasedVisibilityFlag="0" simplifyLocal="1" scaleBasedLabelVisibilityFlag="0">
3-
<renderer-v2 type="invertedPolygonRenderer">
4-
<renderer-v2 attr="Name" symbollevels="0" type="categorizedSymbol">
3+
<renderer-v2 preprocessing="0" type="invertedPolygonRenderer">
4+
<renderer-v2 attr="Name" symbollevels="1" type="categorizedSymbol">
55
<categories>
66
<category symbol="0" value="Dam" label="Dam"/>
77
<category symbol="1" value="Lake" label="Lake"/>
@@ -27,7 +27,7 @@
2727
</layer>
2828
</symbol>
2929
<symbol alpha="1" type="fill" name="1">
30-
<layer pass="0" class="ShapeburstFill" locked="0">
30+
<layer pass="1" class="ShapeburstFill" locked="0">
3131
<prop k="blur_radius" v="0"/>
3232
<prop k="color1" v="0,0,255,255"/>
3333
<prop k="color2" v="0,255,0,255"/>

0 commit comments

Comments
 (0)