Skip to content

Commit 6eb49fe

Browse files
committedJan 2, 2019
[layouts] Add method to get overview item extent as a vector layer
The layer contains a single feature representing the linked map extent, and set to render using the overview's symbol
1 parent 0f7d8c0 commit 6eb49fe

File tree

4 files changed

+153
-5
lines changed

4 files changed

+153
-5
lines changed
 

‎python/core/auto_generated/layout/qgslayoutitemmapoverview.sip.in

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Constructor for QgsLayoutItemMapOverview.
127127
:param name: friendly display name for overview
128128
:param map: QgsLayoutItemMap the overview is attached to
129129
%End
130+
~QgsLayoutItemMapOverview();
130131

131132
virtual void draw( QPainter *painter );
132133

@@ -216,6 +217,17 @@ Sets whether the extent of the map is forced to center on the overview
216217
%Docstring
217218
Reconnects signals for overview map, so that overview correctly follows changes to source
218219
map's extent.
220+
%End
221+
222+
QgsVectorLayer *asMapLayer();
223+
%Docstring
224+
Returns a vector layer to render as part of the QgsLayoutItemMap render, containing
225+
a feature representing the overview extent (and with an appropriate renderer set matching
226+
the overview's frameSymbol() ).
227+
228+
Ownership of the layer remain with the overview item.
229+
230+
.. versionadded:: 3.6
219231
%End
220232

221233
public slots:

‎src/core/layout/qgslayoutitemmapoverview.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,29 @@
2626
#include "qgsreadwritecontext.h"
2727
#include "qgslayoututils.h"
2828
#include "qgsexception.h"
29+
#include "qgsvectorlayer.h"
30+
#include "qgssinglesymbolrenderer.h"
2931

3032
#include <QPainter>
3133

3234
QgsLayoutItemMapOverview::QgsLayoutItemMapOverview( const QString &name, QgsLayoutItemMap *map )
3335
: QgsLayoutItemMapItem( name, map )
36+
, mExtentLayer( qgis::make_unique< QgsVectorLayer >( QStringLiteral( "Polygon?crs=EPSG:4326" ), QStringLiteral( "overview" ), QStringLiteral( "memory" ) ) )
3437
{
3538
createDefaultFrameSymbol();
3639
}
3740

41+
QgsLayoutItemMapOverview::~QgsLayoutItemMapOverview() = default;
42+
3843
void QgsLayoutItemMapOverview::createDefaultFrameSymbol()
3944
{
4045
QgsStringMap properties;
4146
properties.insert( QStringLiteral( "color" ), QStringLiteral( "255,0,0,75" ) );
4247
properties.insert( QStringLiteral( "style" ), QStringLiteral( "solid" ) );
4348
properties.insert( QStringLiteral( "style_border" ), QStringLiteral( "no" ) );
4449
mFrameSymbol.reset( QgsFillSymbol::createSimple( properties ) );
50+
51+
mExtentLayer->setRenderer( new QgsSingleSymbolRenderer( mFrameSymbol->clone() ) );
4552
}
4653

4754
void QgsLayoutItemMapOverview::draw( QPainter *painter )
@@ -245,6 +252,62 @@ void QgsLayoutItemMapOverview::connectSignals()
245252
}
246253
}
247254

255+
QgsVectorLayer *QgsLayoutItemMapOverview::asMapLayer()
256+
{
257+
if ( !mEnabled || !mFrameMap || !mMap || !mMap->layout() )
258+
{
259+
return nullptr;
260+
}
261+
262+
const QgsLayoutItemMap *overviewFrameMap = linkedMap();
263+
if ( !overviewFrameMap )
264+
{
265+
return nullptr;
266+
}
267+
268+
//get polygon for other overview frame map's extent (use visibleExtentPolygon as it accounts for map rotation)
269+
QPolygonF otherExtent = overviewFrameMap->visibleExtentPolygon();
270+
QgsGeometry g = QgsGeometry::fromQPolygonF( otherExtent );
271+
272+
if ( overviewFrameMap->crs() != mMap->crs() )
273+
{
274+
// reproject extent
275+
QgsCoordinateTransform ct( overviewFrameMap->crs(),
276+
mMap->crs(), mLayout->project() );
277+
g = g.densifyByCount( 20 );
278+
try
279+
{
280+
g.transform( ct );
281+
}
282+
catch ( QgsCsException & )
283+
{
284+
}
285+
}
286+
287+
//get current map's extent as a QPolygonF
288+
QPolygonF thisExtent = mMap->visibleExtentPolygon();
289+
QgsGeometry thisGeom = QgsGeometry::fromQPolygonF( thisExtent );
290+
//intersect the two
291+
QgsGeometry intersectExtent = thisGeom.intersection( g );
292+
293+
mExtentLayer->setBlendMode( mBlendMode );
294+
295+
static_cast< QgsSingleSymbolRenderer * >( mExtentLayer->renderer() )->setSymbol( mFrameSymbol->clone() );
296+
mExtentLayer->dataProvider()->truncate();
297+
mExtentLayer->setCrs( mMap->crs() );
298+
299+
if ( mInverted )
300+
{
301+
intersectExtent = thisGeom.difference( intersectExtent );
302+
}
303+
304+
QgsFeature f;
305+
f.setGeometry( intersectExtent );
306+
mExtentLayer->dataProvider()->addFeature( f );
307+
308+
return mExtentLayer.get();
309+
}
310+
248311
void QgsLayoutItemMapOverview::setFrameSymbol( QgsFillSymbol *symbol )
249312
{
250313
mFrameSymbol.reset( symbol );

‎src/core/layout/qgslayoutitemmapoverview.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ class CORE_EXPORT QgsLayoutItemMapOverview : public QgsLayoutItemMapItem
128128
* \param map QgsLayoutItemMap the overview is attached to
129129
*/
130130
QgsLayoutItemMapOverview( const QString &name, QgsLayoutItemMap *map );
131+
~QgsLayoutItemMapOverview() override;
131132

132133
void draw( QPainter *painter ) override;
133134
bool writeXml( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const override;
@@ -211,6 +212,17 @@ class CORE_EXPORT QgsLayoutItemMapOverview : public QgsLayoutItemMapItem
211212
*/
212213
void connectSignals();
213214

215+
/**
216+
* Returns a vector layer to render as part of the QgsLayoutItemMap render, containing
217+
* a feature representing the overview extent (and with an appropriate renderer set matching
218+
* the overview's frameSymbol() ).
219+
*
220+
* Ownership of the layer remain with the overview item.
221+
*
222+
* \since QGIS 3.6
223+
*/
224+
QgsVectorLayer *asMapLayer();
225+
214226
public slots:
215227

216228
/**
@@ -237,6 +249,8 @@ class CORE_EXPORT QgsLayoutItemMapOverview : public QgsLayoutItemMapItem
237249
//! True if map is centered on overview
238250
bool mCentered = false;
239251

252+
std::unique_ptr< QgsVectorLayer > mExtentLayer;
253+
240254
//! Creates default overview symbol
241255
void createDefaultFrameSymbol();
242256

‎tests/src/python/test_qgslayoutmapoverview.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
QgsVectorLayer,
2626
QgsLayout,
2727
QgsProject,
28-
QgsMultiBandColorRenderer)
28+
QgsMultiBandColorRenderer,
29+
QgsFillSymbol,
30+
QgsSingleSymbolRenderer,
31+
QgsCoordinateReferenceSystem)
2932

3033
from qgis.testing import start_app, unittest
3134
from utilities import unitTestDataPath
@@ -95,7 +98,7 @@ def testOverviewMap(self):
9598
myTestResult, myMessage = checker.testLayout()
9699
self.report += checker.report()
97100
self.layout.removeLayoutItem(overviewMap)
98-
assert myTestResult, myMessage
101+
self.assertTrue(myTestResult, myMessage)
99102

100103
def testOverviewMapBlend(self):
101104
overviewMap = QgsLayoutItemMap(self.layout)
@@ -115,7 +118,7 @@ def testOverviewMapBlend(self):
115118
myTestResult, myMessage = checker.testLayout()
116119
self.report += checker.report()
117120
self.layout.removeLayoutItem(overviewMap)
118-
assert myTestResult, myMessage
121+
self.assertTrue(myTestResult, myMessage)
119122

120123
def testOverviewMapInvert(self):
121124
overviewMap = QgsLayoutItemMap(self.layout)
@@ -135,7 +138,7 @@ def testOverviewMapInvert(self):
135138
myTestResult, myMessage = checker.testLayout()
136139
self.report += checker.report()
137140
self.layout.removeLayoutItem(overviewMap)
138-
assert myTestResult, myMessage
141+
self.assertTrue(myTestResult, myMessage)
139142

140143
def testOverviewMapCenter(self):
141144
overviewMap = QgsLayoutItemMap(self.layout)
@@ -156,7 +159,63 @@ def testOverviewMapCenter(self):
156159
myTestResult, myMessage = checker.testLayout()
157160
self.report += checker.report()
158161
self.layout.removeLayoutItem(overviewMap)
159-
assert myTestResult, myMessage
162+
self.assertTrue(myTestResult, myMessage)
163+
164+
def testAsMapLayer(self):
165+
l = QgsLayout(QgsProject.instance())
166+
l.initializeDefaults()
167+
map = QgsLayoutItemMap(l)
168+
map.attemptSetSceneRect(QRectF(20, 20, 200, 100))
169+
l.addLayoutItem(map)
170+
171+
overviewMap = QgsLayoutItemMap(l)
172+
overviewMap.attemptSetSceneRect(QRectF(20, 130, 70, 70))
173+
l.addLayoutItem(overviewMap)
174+
# zoom in
175+
myRectangle = QgsRectangle(96, -152, 160, -120)
176+
map.setExtent(myRectangle)
177+
myRectangle2 = QgsRectangle(0, -256, 256, 0)
178+
overviewMap.setExtent(myRectangle2)
179+
overviewMap.overview().setLinkedMap(map)
180+
181+
layer = overviewMap.overview().asMapLayer()
182+
self.assertIsNotNone(layer)
183+
self.assertTrue(layer.isValid())
184+
self.assertEqual([f.geometry().asWkt() for f in layer.getFeatures()], ['Polygon ((96 -120, 160 -120, 160 -152, 96 -152, 96 -120))'])
185+
186+
# check that layer has correct renderer
187+
fill_symbol = QgsFillSymbol.createSimple({'color': '#00ff00', 'outline_color': '#ff0000', 'outline_width': '10'})
188+
overviewMap.overview().setFrameSymbol(fill_symbol)
189+
layer = overviewMap.overview().asMapLayer()
190+
self.assertIsInstance(layer.renderer(), QgsSingleSymbolRenderer)
191+
self.assertEqual(layer.renderer().symbol().symbolLayer(0).properties()['color'], '0,255,0,255')
192+
self.assertEqual(layer.renderer().symbol().symbolLayer(0).properties()['outline_color'], '255,0,0,255')
193+
194+
# test layer blend mode
195+
self.assertEqual(layer.blendMode(), QPainter.CompositionMode_SourceOver)
196+
overviewMap.overview().setBlendMode(QPainter.CompositionMode_Clear)
197+
layer = overviewMap.overview().asMapLayer()
198+
self.assertEqual(layer.blendMode(), QPainter.CompositionMode_Clear)
199+
200+
# should have no effect
201+
overviewMap.setMapRotation(45)
202+
layer = overviewMap.overview().asMapLayer()
203+
self.assertEqual([f.geometry().asWkt() for f in layer.getFeatures()], ['Polygon ((96 -120, 160 -120, 160 -152, 96 -152, 96 -120))'])
204+
205+
map.setMapRotation(15)
206+
layer = overviewMap.overview().asMapLayer()
207+
self.assertEqual([f.geometry().asWkt(0) for f in layer.getFeatures()], ['Polygon ((93 -129, 155 -112, 163 -143, 101 -160, 93 -129))'])
208+
209+
# with reprojection
210+
map.setCrs(QgsCoordinateReferenceSystem('EPSG:3875'))
211+
layer = overviewMap.overview().asMapLayer()
212+
self.assertEqual([f.geometry().asWkt(0) for f in layer.getFeatures()], ['Polygon ((93 -129, 96 -128, 99 -127, 102 -126, 105 -126, 108 -125, 111 -124, 114 -123, 116 -123, 119 -122, 122 -121, 125 -120, 128 -119, 131 -119, 134 -118, 137 -117, 140 -116, 143 -115, 146 -115, 149 -114, 152 -113, 155 -112, 155 -114, 156 -115, 156 -117, 156 -118, 157 -120, 157 -121, 158 -123, 158 -124, 158 -126, 159 -127, 159 -128, 160 -130, 160 -131, 160 -133, 161 -134, 161 -136, 161 -137, 162 -139, 162 -140, 163 -142, 163 -143, 160 -144, 157 -145, 154 -146, 151 -146, 148 -147, 145 -148, 142 -149, 140 -149, 137 -150, 134 -151, 131 -152, 128 -153, 125 -153, 122 -154, 119 -155, 116 -156, 113 -157, 110 -157, 107 -158, 104 -159, 101 -160, 101 -158, 100 -157, 100 -155, 100 -154, 99 -152, 99 -151, 98 -149, 98 -148, 98 -146, 97 -145, 97 -144, 96 -142, 96 -141, 96 -139, 95 -138, 95 -136, 95 -135, 94 -133, 94 -132, 93 -130, 93 -129))'])
213+
214+
map.setCrs(overviewMap.crs())
215+
# with invert
216+
overviewMap.overview().setInverted(True)
217+
layer = overviewMap.overview().asMapLayer()
218+
self.assertEqual([f.geometry().asWkt(0) for f in layer.getFeatures()], ['Polygon ((-53 -128, 128 53, 309 -128, 128 -309, -53 -128),(93 -129, 101 -160, 163 -143, 155 -112, 93 -129))'])
160219

161220

162221
if __name__ == '__main__':

0 commit comments

Comments
 (0)
Please sign in to comment.