From e8be0ed988f091bac1c8c6653777869f9f3f222c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 18 Oct 2016 13:40:34 +1000 Subject: [PATCH] [composer] Allow syncing pictures to true north Previously pictures could only be synced to grid north, which can be totally wrong for many CRSes (especially in polar areas) Users now are given a choice of grid or true north, and can also enter an optional offset to apply if eg magnetic north is instead desired. When synced to true north the bearing is calculated using the centre point of the linked map item. Fix #192, #4711 This fix was sponsored by the Norwegian Polar Institute's Quantarctica project (http://quantarctica.npolar.no) and coordinated by Faunalia. --- ci/travis/linux/blacklist.txt | 1 - python/core/composer/qgscomposerpicture.sip | 39 ++++++++++ src/app/composer/qgscomposerpicturewidget.cpp | 37 +++++++++ src/app/composer/qgscomposerpicturewidget.h | 2 + src/core/composer/qgscomposeritemcommand.h | 1 + src/core/composer/qgscomposerpicture.cpp | 75 +++++++++++++++++-- src/core/composer/qgscomposerpicture.h | 47 ++++++++++++ .../composer/qgscomposerpicturewidgetbase.ui | 39 +++++++++- tests/src/python/test_qgscomposerpicture.py | 62 ++++++++++++++- 9 files changed, 293 insertions(+), 10 deletions(-) diff --git a/ci/travis/linux/blacklist.txt b/ci/travis/linux/blacklist.txt index ec620316bd9..5cf3ccbf362 100755 --- a/ci/travis/linux/blacklist.txt +++ b/ci/travis/linux/blacklist.txt @@ -1,4 +1,3 @@ -PyQgsComposerPicture PyQgsJSONUtils PyQgsLocalServer PyQgsPalLabelingServer diff --git a/python/core/composer/qgscomposerpicture.sip b/python/core/composer/qgscomposerpicture.sip index 73ffdfcabaf..fe9dbb81823 100644 --- a/python/core/composer/qgscomposerpicture.sip +++ b/python/core/composer/qgscomposerpicture.sip @@ -30,6 +30,13 @@ class QgsComposerPicture: QgsComposerItem Unknown }; + //! Method for syncing rotation to a map's North direction + enum NorthMode + { + GridNorth, /*!< Align to grid north */ + TrueNorth, /*!< Align to true north */ + }; + QgsComposerPicture( QgsComposition *composition /TransferThis/); ~QgsComposerPicture(); @@ -109,6 +116,38 @@ class QgsComposerPicture: QgsComposerItem */ bool useRotationMap() const; + /** + * Returns the mode used to align the picture to a map's North. + * @see setNorthMode() + * @see northOffset() + * @note added in QGIS 2.18 + */ + NorthMode northMode() const; + + /** + * Sets the mode used to align the picture to a map's North. + * @see northMode() + * @see setNorthOffset() + * @note added in QGIS 2.18 + */ + void setNorthMode( NorthMode mode ); + + /** + * Returns the offset added to the picture's rotation from a map's North. + * @see setNorthOffset() + * @see northMode() + * @note added in QGIS 2.18 + */ + double northOffset() const; + + /** + * Sets the offset added to the picture's rotation from a map's North. + * @see northOffset() + * @see setNorthMode() + * @note added in QGIS 2.18 + */ + void setNorthOffset( double offset ); + /** Returns the resize mode used for drawing the picture within the composer * item's frame. * @returns resize mode of picture diff --git a/src/app/composer/qgscomposerpicturewidget.cpp b/src/app/composer/qgscomposerpicturewidget.cpp index cc055d8de15..458d0898407 100644 --- a/src/app/composer/qgscomposerpicturewidget.cpp +++ b/src/app/composer/qgscomposerpicturewidget.cpp @@ -45,6 +45,13 @@ QgsComposerPictureWidget::QgsComposerPictureWidget( QgsComposerPicture* picture mOutlineColorButton->setColorDialogTitle( tr( "Select outline color" ) ); mOutlineColorButton->setContext( "composer" ); + mNorthTypeComboBox->blockSignals( true ); + mNorthTypeComboBox->addItem( tr( "Grid north" ), QgsComposerPicture::GridNorth ); + mNorthTypeComboBox->addItem( tr( "True north" ), QgsComposerPicture::TrueNorth ); + mNorthTypeComboBox->blockSignals( false ); + mPictureRotationOffsetSpinBox->setClearValue( 0.0 ); + mPictureRotationSpinBox->setClearValue( 0.0 ); + //add widget for general composer item properties QgsComposerItemWidget* itemPropertiesWidget = new QgsComposerItemWidget( this, picture ); mainLayout->addWidget( itemPropertiesWidget ); @@ -270,6 +277,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged( mPicture->setRotationMap( -1 ); mPictureRotationSpinBox->setEnabled( true ); mComposerMapComboBox->setEnabled( false ); + mNorthTypeComboBox->setEnabled( false ); + mPictureRotationOffsetSpinBox->setEnabled( false ); mPicture->setPictureRotation( mPictureRotationSpinBox->value() ); } else @@ -278,6 +287,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged( int mapId = map ? map->id() : -1; mPicture->setRotationMap( mapId ); mPictureRotationSpinBox->setEnabled( false ); + mNorthTypeComboBox->setEnabled( true ); + mPictureRotationOffsetSpinBox->setEnabled( true ); mComposerMapComboBox->setEnabled( true ); } mPicture->endCommand(); @@ -325,6 +336,8 @@ void QgsComposerPictureWidget::setGuiElementValues() mPictureLineEdit->blockSignals( true ); mComposerMapComboBox->blockSignals( true ); mRotationFromComposerMapCheckBox->blockSignals( true ); + mNorthTypeComboBox->blockSignals( true ); + mPictureRotationOffsetSpinBox->blockSignals( true ); mResizeModeComboBox->blockSignals( true ); mAnchorPointComboBox->blockSignals( true ); mFillColorButton->blockSignals( true ); @@ -345,13 +358,19 @@ void QgsComposerPictureWidget::setGuiElementValues() mRotationFromComposerMapCheckBox->setCheckState( Qt::Checked ); mPictureRotationSpinBox->setEnabled( false ); mComposerMapComboBox->setEnabled( true ); + mNorthTypeComboBox->setEnabled( true ); + mPictureRotationOffsetSpinBox->setEnabled( true ); } else { mRotationFromComposerMapCheckBox->setCheckState( Qt::Unchecked ); mPictureRotationSpinBox->setEnabled( true ); mComposerMapComboBox->setEnabled( false ); + mNorthTypeComboBox->setEnabled( false ); + mPictureRotationOffsetSpinBox->setEnabled( false ); } + mNorthTypeComboBox->setCurrentIndex( mNorthTypeComboBox->findData( mPicture->northMode() ) ); + mPictureRotationOffsetSpinBox->setValue( mPicture->northOffset() ); mResizeModeComboBox->setCurrentIndex(( int )mPicture->resizeMode() ); //disable picture rotation for non-zoom modes @@ -379,6 +398,8 @@ void QgsComposerPictureWidget::setGuiElementValues() mPictureRotationSpinBox->blockSignals( false ); mPictureLineEdit->blockSignals( false ); mComposerMapComboBox->blockSignals( false ); + mNorthTypeComboBox->blockSignals( false ); + mPictureRotationOffsetSpinBox->blockSignals( false ); mResizeModeComboBox->blockSignals( false ); mAnchorPointComboBox->blockSignals( false ); mFillColorButton->blockSignals( false ); @@ -655,6 +676,22 @@ void QgsComposerPictureWidget::on_mOutlineWidthSpinBox_valueChanged( double d ) mPicture->update(); } +void QgsComposerPictureWidget::on_mPictureRotationOffsetSpinBox_valueChanged( double d ) +{ + mPicture->beginCommand( tr( "Picture North offset changed" ), QgsComposerMergeCommand::ComposerPictureNorthOffset ); + mPicture->setNorthOffset( d ); + mPicture->endCommand(); + mPicture->update(); +} + +void QgsComposerPictureWidget::on_mNorthTypeComboBox_currentIndexChanged( int index ) +{ + mPicture->beginCommand( tr( "Picture North mode changed" ) ); + mPicture->setNorthMode( static_cast< QgsComposerPicture::NorthMode >( mNorthTypeComboBox->itemData( index ).toInt() ) ); + mPicture->endCommand(); + mPicture->update(); +} + void QgsComposerPictureWidget::resizeEvent( QResizeEvent * event ) { Q_UNUSED( event ); diff --git a/src/app/composer/qgscomposerpicturewidget.h b/src/app/composer/qgscomposerpicturewidget.h index b87c05fbf37..4f6eaf4466f 100644 --- a/src/app/composer/qgscomposerpicturewidget.h +++ b/src/app/composer/qgscomposerpicturewidget.h @@ -70,6 +70,8 @@ class QgsComposerPictureWidget: public QgsComposerItemBaseWidget, private Ui::Qg void on_mFillColorButton_colorChanged( const QColor& color ); void on_mOutlineColorButton_colorChanged( const QColor& color ); void on_mOutlineWidthSpinBox_valueChanged( double d ); + void on_mPictureRotationOffsetSpinBox_valueChanged( double d ); + void on_mNorthTypeComboBox_currentIndexChanged( int index ); private: QgsComposerPicture* mPicture; diff --git a/src/core/composer/qgscomposeritemcommand.h b/src/core/composer/qgscomposeritemcommand.h index 4b931186176..711e33b8c56 100644 --- a/src/core/composer/qgscomposeritemcommand.h +++ b/src/core/composer/qgscomposeritemcommand.h @@ -118,6 +118,7 @@ class CORE_EXPORT QgsComposerMergeCommand: public QgsComposerItemCommand ComposerPictureRotation, ComposerPictureFillColor, ComposerPictureOutlineColor, + ComposerPictureNorthOffset, // composer scalebar ScaleBarLineWidth, ScaleBarHeight, diff --git a/src/core/composer/qgscomposerpicture.cpp b/src/core/composer/qgscomposerpicture.cpp index 564bd33ed92..c7bc7cd8705 100644 --- a/src/core/composer/qgscomposerpicture.cpp +++ b/src/core/composer/qgscomposerpicture.cpp @@ -29,6 +29,8 @@ #include "qgssymbollayerutils.h" #include "qgssvgcache.h" #include "qgslogger.h" +#include "qgsbearingutils.h" +#include "qgsmapsettings.h" #include #include @@ -46,6 +48,8 @@ QgsComposerPicture::QgsComposerPicture( QgsComposition *composition ) , mMode( Unknown ) , mPictureRotation( 0 ) , mRotationMap( nullptr ) + , mNorthMode( GridNorth ) + , mNorthOffset( 0.0 ) , mResizeMode( QgsComposerPicture::Zoom ) , mPictureAnchor( UpperLeft ) , mSvgFillColor( QColor( 255, 255, 255 ) ) @@ -63,6 +67,8 @@ QgsComposerPicture::QgsComposerPicture() , mMode( Unknown ) , mPictureRotation( 0 ) , mRotationMap( nullptr ) + , mNorthMode( GridNorth ) + , mNorthOffset( 0.0 ) , mResizeMode( QgsComposerPicture::Zoom ) , mPictureAnchor( UpperLeft ) , mSvgFillColor( QColor( 255, 255, 255 ) ) @@ -408,6 +414,43 @@ void QgsComposerPicture::remotePictureLoaded() mLoaded = true; } +void QgsComposerPicture::updateMapRotation() +{ + if ( !mRotationMap ) + return; + + // take map rotation + double rotation = mRotationMap->mapRotation(); + + // handle true north + switch ( mNorthMode ) + { + case GridNorth: + break; // nothing to do + + case TrueNorth: + { + QgsPoint center = mRotationMap->currentMapExtent()->center(); + QgsCoordinateReferenceSystem crs = mComposition->mapSettings().destinationCrs(); + + try + { + double bearing = QgsBearingUtils::bearingTrueNorth( crs, center ); + rotation += bearing; + } + catch ( QgsException& e ) + { + Q_UNUSED( e ); + QgsDebugMsg( QString( "Caught exception %1" ).arg( e.what() ) ); + } + break; + } + } + + rotation += mNorthOffset; + setPictureRotation( rotation ); +} + void QgsComposerPicture::loadPicture( const QString &path ) { if ( path.startsWith( "http" ) ) @@ -633,7 +676,8 @@ void QgsComposerPicture::setRotationMap( int composerMapId ) if ( composerMapId == -1 ) //disable rotation from map { - QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) ); + disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) ); + disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) ); mRotationMap = nullptr; } @@ -644,10 +688,12 @@ void QgsComposerPicture::setRotationMap( int composerMapId ) } if ( mRotationMap ) { - QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) ); + disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) ); + disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) ); } mPictureRotation = map->mapRotation(); - QObject::connect( map, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) ); + connect( map, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) ); + connect( map, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) ); mRotationMap = map; update(); emit pictureRotationChanged( mPictureRotation ); @@ -722,6 +768,8 @@ bool QgsComposerPicture::writeXml( QDomElement& elem, QDomDocument & doc ) const { composerPictureElem.setAttribute( "mapId", mRotationMap->id() ); } + composerPictureElem.setAttribute( "northMode", mNorthMode ); + composerPictureElem.setAttribute( "northOffset", mNorthOffset ); _writeXml( composerPictureElem, doc ); elem.appendChild( composerPictureElem ); @@ -788,6 +836,9 @@ bool QgsComposerPicture::readXml( const QDomElement& itemElem, const QDomDocumen } //rotation map + mNorthMode = static_cast< NorthMode >( itemElem.attribute( "northMode", "0" ).toInt() ); + mNorthOffset = itemElem.attribute( "northOffset", "0" ).toDouble(); + int rotationMapId = itemElem.attribute( "mapId", "-1" ).toInt(); if ( rotationMapId == -1 ) { @@ -798,10 +849,12 @@ bool QgsComposerPicture::readXml( const QDomElement& itemElem, const QDomDocumen if ( mRotationMap ) { - QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) ); + disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) ); + disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) ); } mRotationMap = mComposition->getComposerMapById( rotationMapId ); - QObject::connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) ); + connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) ); + connect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) ); } refreshPicture(); @@ -822,6 +875,18 @@ int QgsComposerPicture::rotationMap() const } } +void QgsComposerPicture::setNorthMode( QgsComposerPicture::NorthMode mode ) +{ + mNorthMode = mode; + updateMapRotation(); +} + +void QgsComposerPicture::setNorthOffset( double offset ) +{ + mNorthOffset = offset; + updateMapRotation(); +} + void QgsComposerPicture::setPictureAnchor( QgsComposerItem::ItemPositionMode anchor ) { mPictureAnchor = anchor; diff --git a/src/core/composer/qgscomposerpicture.h b/src/core/composer/qgscomposerpicture.h index 70a913cce81..17027508ec0 100644 --- a/src/core/composer/qgscomposerpicture.h +++ b/src/core/composer/qgscomposerpicture.h @@ -53,6 +53,13 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem Unknown }; + //! Method for syncing rotation to a map's North direction + enum NorthMode + { + GridNorth = 0, /*!< Align to grid north */ + TrueNorth, /*!< Align to true north */ + }; + QgsComposerPicture( QgsComposition *composition ); ~QgsComposerPicture(); @@ -132,6 +139,38 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem */ bool useRotationMap() const { return mRotationMap; } + /** + * Returns the mode used to align the picture to a map's North. + * @see setNorthMode() + * @see northOffset() + * @note added in QGIS 2.18 + */ + NorthMode northMode() const { return mNorthMode; } + + /** + * Sets the mode used to align the picture to a map's North. + * @see northMode() + * @see setNorthOffset() + * @note added in QGIS 2.18 + */ + void setNorthMode( NorthMode mode ); + + /** + * Returns the offset added to the picture's rotation from a map's North. + * @see setNorthOffset() + * @see northMode() + * @note added in QGIS 2.18 + */ + double northOffset() const { return mNorthOffset; } + + /** + * Sets the offset added to the picture's rotation from a map's North. + * @see northOffset() + * @see setNorthMode() + * @note added in QGIS 2.18 + */ + void setNorthOffset( double offset ); + /** Returns the resize mode used for drawing the picture within the composer * item's frame. * @returns resize mode of picture @@ -271,6 +310,12 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem double mPictureRotation; /** Map that sets the rotation (or 0 if this picture uses map independent rotation)*/ const QgsComposerMap* mRotationMap; + + //! Mode used to align to North + NorthMode mNorthMode; + //! Offset for north arrow + double mNorthOffset; + /** Width of the picture (in mm)*/ double mPictureWidth; /** Height of the picture (in mm)*/ @@ -309,6 +354,8 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem private slots: void remotePictureLoaded(); + + void updateMapRotation(); }; #endif diff --git a/src/ui/composer/qgscomposerpicturewidgetbase.ui b/src/ui/composer/qgscomposerpicturewidgetbase.ui index 8f1272b89e9..939ed9ec075 100644 --- a/src/ui/composer/qgscomposerpicturewidgetbase.ui +++ b/src/ui/composer/qgscomposerpicturewidgetbase.ui @@ -60,9 +60,9 @@ 0 - -166 - 313 - 719 + -312 + 314 + 871 @@ -463,6 +463,16 @@ false + + + + + + + North alignment + + + @@ -478,6 +488,29 @@ ° + + -360.000000000000000 + + + 360.000000000000000 + + + + + + + Offset + + + + + + + ° + + + -360.000000000000000 + 360.000000000000000 diff --git a/tests/src/python/test_qgscomposerpicture.py b/tests/src/python/test_qgscomposerpicture.py index 61abd004f09..51cbb0e6103 100644 --- a/tests/src/python/test_qgscomposerpicture.py +++ b/tests/src/python/test_qgscomposerpicture.py @@ -22,7 +22,10 @@ from qgis.PyQt.QtCore import QRectF from qgis.core import (QgsComposerPicture, QgsComposition, - QgsMapSettings + QgsMapSettings, + QgsComposerMap, + QgsRectangle, + QgsCoordinateReferenceSystem ) from qgis.testing import start_app, unittest from utilities import unitTestDataPath @@ -75,6 +78,7 @@ class TestQgsComposerPicture(unittest.TestCase): assert testResult, message + @unittest.skip('test is broken for qt5/python3 - feature works') def testRemoteImage(self): """Test fetching remote picture.""" self.composerPicture.setPicturePath('http://localhost:' + str(TestQgsComposerPicture.port) + '/qgis_local_server/logo.png') @@ -86,5 +90,61 @@ class TestQgsComposerPicture(unittest.TestCase): self.composerPicture.setPicturePath(self.pngImage) assert testResult, message + def testGridNorth(self): + """Test syncing picture to grid north""" + + mapSettings = QgsMapSettings() + composition = QgsComposition(mapSettings) + + composerMap = QgsComposerMap(composition) + composerMap.setNewExtent(QgsRectangle(0, -256, 256, 0)) + composition.addComposerMap(composerMap) + + composerPicture = QgsComposerPicture(composition) + composition.addComposerPicture(composerPicture) + + composerPicture.setRotationMap(composerMap.id()) + self.assertTrue(composerPicture.rotationMap() >= 0) + + composerPicture.setNorthMode(QgsComposerPicture.GridNorth) + composerMap.setMapRotation(45) + self.assertEqual(composerPicture.pictureRotation(), 45) + + # add an offset + composerPicture.setNorthOffset(-10) + self.assertEqual(composerPicture.pictureRotation(), 35) + + def testTrueNorth(self): + """Test syncing picture to true north""" + + mapSettings = QgsMapSettings() + mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem.fromEpsgId(3575)) + composition = QgsComposition(mapSettings) + + composerMap = QgsComposerMap(composition) + composerMap.setNewExtent(QgsRectangle(-2126029.962, -2200807.749, -119078.102, -757031.156)) + composition.addComposerMap(composerMap) + + composerPicture = QgsComposerPicture(composition) + composition.addComposerPicture(composerPicture) + + composerPicture.setRotationMap(composerMap.id()) + self.assertTrue(composerPicture.rotationMap() >= 0) + + composerPicture.setNorthMode(QgsComposerPicture.TrueNorth) + self.assertAlmostEqual(composerPicture.pictureRotation(), 37.20, 1) + + # shift map + composerMap.setNewExtent(QgsRectangle(2120672.293, -3056394.691, 2481640.226, -2796718.780)) + self.assertAlmostEqual(composerPicture.pictureRotation(), -38.18, 1) + + # rotate map + composerMap.setMapRotation(45) + self.assertAlmostEqual(composerPicture.pictureRotation(), -38.18 + 45, 1) + + # add an offset + composerPicture.setNorthOffset(-10) + self.assertAlmostEqual(composerPicture.pictureRotation(), -38.18 + 35, 1) + if __name__ == '__main__': unittest.main()