Skip to content
Permalink
Browse files

[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
  • Loading branch information
nyalldawson committed Dec 28, 2018
1 parent 0f7d8c0 commit 6eb49feddc7e371fe0c0ae90ad08106aceac353d
@@ -127,6 +127,7 @@ Constructor for QgsLayoutItemMapOverview.
:param name: friendly display name for overview
:param map: QgsLayoutItemMap the overview is attached to
%End
~QgsLayoutItemMapOverview();

virtual void draw( QPainter *painter );

@@ -216,6 +217,17 @@ Sets whether the extent of the map is forced to center on the overview
%Docstring
Reconnects signals for overview map, so that overview correctly follows changes to source
map's extent.
%End

QgsVectorLayer *asMapLayer();
%Docstring
Returns a vector layer to render as part of the QgsLayoutItemMap render, containing
a feature representing the overview extent (and with an appropriate renderer set matching
the overview's frameSymbol() ).

Ownership of the layer remain with the overview item.

.. versionadded:: 3.6
%End

public slots:
@@ -26,22 +26,29 @@
#include "qgsreadwritecontext.h"
#include "qgslayoututils.h"
#include "qgsexception.h"
#include "qgsvectorlayer.h"
#include "qgssinglesymbolrenderer.h"

#include <QPainter>

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

QgsLayoutItemMapOverview::~QgsLayoutItemMapOverview() = default;

void QgsLayoutItemMapOverview::createDefaultFrameSymbol()
{
QgsStringMap properties;
properties.insert( QStringLiteral( "color" ), QStringLiteral( "255,0,0,75" ) );
properties.insert( QStringLiteral( "style" ), QStringLiteral( "solid" ) );
properties.insert( QStringLiteral( "style_border" ), QStringLiteral( "no" ) );
mFrameSymbol.reset( QgsFillSymbol::createSimple( properties ) );

mExtentLayer->setRenderer( new QgsSingleSymbolRenderer( mFrameSymbol->clone() ) );
}

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

QgsVectorLayer *QgsLayoutItemMapOverview::asMapLayer()
{
if ( !mEnabled || !mFrameMap || !mMap || !mMap->layout() )
{
return nullptr;
}

const QgsLayoutItemMap *overviewFrameMap = linkedMap();
if ( !overviewFrameMap )
{
return nullptr;
}

//get polygon for other overview frame map's extent (use visibleExtentPolygon as it accounts for map rotation)
QPolygonF otherExtent = overviewFrameMap->visibleExtentPolygon();
QgsGeometry g = QgsGeometry::fromQPolygonF( otherExtent );

if ( overviewFrameMap->crs() != mMap->crs() )
{
// reproject extent
QgsCoordinateTransform ct( overviewFrameMap->crs(),
mMap->crs(), mLayout->project() );
g = g.densifyByCount( 20 );
try
{
g.transform( ct );
}
catch ( QgsCsException & )
{
}
}

//get current map's extent as a QPolygonF
QPolygonF thisExtent = mMap->visibleExtentPolygon();
QgsGeometry thisGeom = QgsGeometry::fromQPolygonF( thisExtent );
//intersect the two
QgsGeometry intersectExtent = thisGeom.intersection( g );

mExtentLayer->setBlendMode( mBlendMode );

static_cast< QgsSingleSymbolRenderer * >( mExtentLayer->renderer() )->setSymbol( mFrameSymbol->clone() );
mExtentLayer->dataProvider()->truncate();
mExtentLayer->setCrs( mMap->crs() );

if ( mInverted )
{
intersectExtent = thisGeom.difference( intersectExtent );
}

QgsFeature f;
f.setGeometry( intersectExtent );
mExtentLayer->dataProvider()->addFeature( f );

return mExtentLayer.get();
}

void QgsLayoutItemMapOverview::setFrameSymbol( QgsFillSymbol *symbol )
{
mFrameSymbol.reset( symbol );
@@ -128,6 +128,7 @@ class CORE_EXPORT QgsLayoutItemMapOverview : public QgsLayoutItemMapItem
* \param map QgsLayoutItemMap the overview is attached to
*/
QgsLayoutItemMapOverview( const QString &name, QgsLayoutItemMap *map );
~QgsLayoutItemMapOverview() override;

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

/**
* Returns a vector layer to render as part of the QgsLayoutItemMap render, containing
* a feature representing the overview extent (and with an appropriate renderer set matching
* the overview's frameSymbol() ).
*
* Ownership of the layer remain with the overview item.
*
* \since QGIS 3.6
*/
QgsVectorLayer *asMapLayer();

public slots:

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

std::unique_ptr< QgsVectorLayer > mExtentLayer;

//! Creates default overview symbol
void createDefaultFrameSymbol();

@@ -25,7 +25,10 @@
QgsVectorLayer,
QgsLayout,
QgsProject,
QgsMultiBandColorRenderer)
QgsMultiBandColorRenderer,
QgsFillSymbol,
QgsSingleSymbolRenderer,
QgsCoordinateReferenceSystem)

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

def testOverviewMapBlend(self):
overviewMap = QgsLayoutItemMap(self.layout)
@@ -115,7 +118,7 @@ def testOverviewMapBlend(self):
myTestResult, myMessage = checker.testLayout()
self.report += checker.report()
self.layout.removeLayoutItem(overviewMap)
assert myTestResult, myMessage
self.assertTrue(myTestResult, myMessage)

def testOverviewMapInvert(self):
overviewMap = QgsLayoutItemMap(self.layout)
@@ -135,7 +138,7 @@ def testOverviewMapInvert(self):
myTestResult, myMessage = checker.testLayout()
self.report += checker.report()
self.layout.removeLayoutItem(overviewMap)
assert myTestResult, myMessage
self.assertTrue(myTestResult, myMessage)

def testOverviewMapCenter(self):
overviewMap = QgsLayoutItemMap(self.layout)
@@ -156,7 +159,63 @@ def testOverviewMapCenter(self):
myTestResult, myMessage = checker.testLayout()
self.report += checker.report()
self.layout.removeLayoutItem(overviewMap)
assert myTestResult, myMessage
self.assertTrue(myTestResult, myMessage)

def testAsMapLayer(self):
l = QgsLayout(QgsProject.instance())
l.initializeDefaults()
map = QgsLayoutItemMap(l)
map.attemptSetSceneRect(QRectF(20, 20, 200, 100))
l.addLayoutItem(map)

overviewMap = QgsLayoutItemMap(l)
overviewMap.attemptSetSceneRect(QRectF(20, 130, 70, 70))
l.addLayoutItem(overviewMap)
# zoom in
myRectangle = QgsRectangle(96, -152, 160, -120)
map.setExtent(myRectangle)
myRectangle2 = QgsRectangle(0, -256, 256, 0)
overviewMap.setExtent(myRectangle2)
overviewMap.overview().setLinkedMap(map)

layer = overviewMap.overview().asMapLayer()
self.assertIsNotNone(layer)
self.assertTrue(layer.isValid())
self.assertEqual([f.geometry().asWkt() for f in layer.getFeatures()], ['Polygon ((96 -120, 160 -120, 160 -152, 96 -152, 96 -120))'])

# check that layer has correct renderer
fill_symbol = QgsFillSymbol.createSimple({'color': '#00ff00', 'outline_color': '#ff0000', 'outline_width': '10'})
overviewMap.overview().setFrameSymbol(fill_symbol)
layer = overviewMap.overview().asMapLayer()
self.assertIsInstance(layer.renderer(), QgsSingleSymbolRenderer)
self.assertEqual(layer.renderer().symbol().symbolLayer(0).properties()['color'], '0,255,0,255')
self.assertEqual(layer.renderer().symbol().symbolLayer(0).properties()['outline_color'], '255,0,0,255')

# test layer blend mode
self.assertEqual(layer.blendMode(), QPainter.CompositionMode_SourceOver)
overviewMap.overview().setBlendMode(QPainter.CompositionMode_Clear)
layer = overviewMap.overview().asMapLayer()
self.assertEqual(layer.blendMode(), QPainter.CompositionMode_Clear)

# should have no effect
overviewMap.setMapRotation(45)
layer = overviewMap.overview().asMapLayer()
self.assertEqual([f.geometry().asWkt() for f in layer.getFeatures()], ['Polygon ((96 -120, 160 -120, 160 -152, 96 -152, 96 -120))'])

map.setMapRotation(15)
layer = overviewMap.overview().asMapLayer()
self.assertEqual([f.geometry().asWkt(0) for f in layer.getFeatures()], ['Polygon ((93 -129, 155 -112, 163 -143, 101 -160, 93 -129))'])

# with reprojection
map.setCrs(QgsCoordinateReferenceSystem('EPSG:3875'))
layer = overviewMap.overview().asMapLayer()
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))'])

map.setCrs(overviewMap.crs())
# with invert
overviewMap.overview().setInverted(True)
layer = overviewMap.overview().asMapLayer()
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))'])


if __name__ == '__main__':

0 comments on commit 6eb49fe

Please sign in to comment.
You can’t perform that action at this time.