From 6eb49feddc7e371fe0c0ae90ad08106aceac353d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 28 Dec 2018 12:23:15 +1000 Subject: [PATCH] [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 --- .../layout/qgslayoutitemmapoverview.sip.in | 12 ++++ src/core/layout/qgslayoutitemmapoverview.cpp | 63 +++++++++++++++++ src/core/layout/qgslayoutitemmapoverview.h | 14 ++++ tests/src/python/test_qgslayoutmapoverview.py | 69 +++++++++++++++++-- 4 files changed, 153 insertions(+), 5 deletions(-) diff --git a/python/core/auto_generated/layout/qgslayoutitemmapoverview.sip.in b/python/core/auto_generated/layout/qgslayoutitemmapoverview.sip.in index 814e7be8ba6a..48dd5c9c96e1 100644 --- a/python/core/auto_generated/layout/qgslayoutitemmapoverview.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemmapoverview.sip.in @@ -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: diff --git a/src/core/layout/qgslayoutitemmapoverview.cpp b/src/core/layout/qgslayoutitemmapoverview.cpp index ad30c9ebfbb3..b61ce83eeeeb 100644 --- a/src/core/layout/qgslayoutitemmapoverview.cpp +++ b/src/core/layout/qgslayoutitemmapoverview.cpp @@ -26,15 +26,20 @@ #include "qgsreadwritecontext.h" #include "qgslayoututils.h" #include "qgsexception.h" +#include "qgsvectorlayer.h" +#include "qgssinglesymbolrenderer.h" #include 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; @@ -42,6 +47,8 @@ void QgsLayoutItemMapOverview::createDefaultFrameSymbol() 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 ); diff --git a/src/core/layout/qgslayoutitemmapoverview.h b/src/core/layout/qgslayoutitemmapoverview.h index dbbc12a4475d..ae92d6de8ecb 100644 --- a/src/core/layout/qgslayoutitemmapoverview.h +++ b/src/core/layout/qgslayoutitemmapoverview.h @@ -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(); diff --git a/tests/src/python/test_qgslayoutmapoverview.py b/tests/src/python/test_qgslayoutmapoverview.py index d6ba81c019d6..c3eedb6a6cc9 100644 --- a/tests/src/python/test_qgslayoutmapoverview.py +++ b/tests/src/python/test_qgslayoutmapoverview.py @@ -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__':