[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.
This commit is contained in:
Nyall Dawson 2016-10-18 13:40:34 +10:00
parent fb860fb618
commit e8be0ed988
9 changed files with 293 additions and 10 deletions

View File

@ -1,4 +1,3 @@
PyQgsComposerPicture
PyQgsJSONUtils
PyQgsLocalServer
PyQgsPalLabelingServer

View File

@ -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

View File

@ -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 );

View File

@ -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;

View File

@ -118,6 +118,7 @@ class CORE_EXPORT QgsComposerMergeCommand: public QgsComposerItemCommand
ComposerPictureRotation,
ComposerPictureFillColor,
ComposerPictureOutlineColor,
ComposerPictureNorthOffset,
// composer scalebar
ScaleBarLineWidth,
ScaleBarHeight,

View File

@ -29,6 +29,8 @@
#include "qgssymbollayerutils.h"
#include "qgssvgcache.h"
#include "qgslogger.h"
#include "qgsbearingutils.h"
#include "qgsmapsettings.h"
#include <QDomDocument>
#include <QDomElement>
@ -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;

View File

@ -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

View File

@ -60,9 +60,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>-166</y>
<width>313</width>
<height>719</height>
<y>-312</y>
<width>314</width>
<height>871</height>
</rect>
</property>
<layout class="QVBoxLayout" name="mainLayout">
@ -463,6 +463,16 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="0,1">
<item row="2" column="1">
<widget class="QComboBox" name="mNorthTypeComboBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>North alignment</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="mRotationFromComposerMapCheckBox">
<property name="text">
@ -478,6 +488,29 @@
<property name="suffix">
<string> °</string>
</property>
<property name="minimum">
<double>-360.000000000000000</double>
</property>
<property name="maximum">
<double>360.000000000000000</double>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Offset</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QgsDoubleSpinBox" name="mPictureRotationOffsetSpinBox">
<property name="suffix">
<string> °</string>
</property>
<property name="minimum">
<double>-360.000000000000000</double>
</property>
<property name="maximum">
<double>360.000000000000000</double>
</property>

View File

@ -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()