Skip to content

Commit 13afc10

Browse files
committed
[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. (cherry-picked from 89cc645)
1 parent 71bfa1d commit 13afc10

15 files changed

+475
-7
lines changed

python/core/composer/qgscomposerpicture.sip

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ class QgsComposerPicture: QgsComposerItem
3030
Unknown
3131
};
3232

33+
//! Method for syncing rotation to a map's North direction
34+
enum NorthMode
35+
{
36+
GridNorth, /*!< Align to grid north */
37+
TrueNorth, /*!< Align to true north */
38+
};
39+
3340
QgsComposerPicture( QgsComposition *composition /TransferThis/);
3441
~QgsComposerPicture();
3542

@@ -132,6 +139,38 @@ class QgsComposerPicture: QgsComposerItem
132139
*/
133140
bool useRotationMap() const;
134141

142+
/**
143+
* Returns the mode used to align the picture to a map's North.
144+
* @see setNorthMode()
145+
* @see northOffset()
146+
* @note added in QGIS 2.18
147+
*/
148+
NorthMode northMode() const;
149+
150+
/**
151+
* Sets the mode used to align the picture to a map's North.
152+
* @see northMode()
153+
* @see setNorthOffset()
154+
* @note added in QGIS 2.18
155+
*/
156+
void setNorthMode( NorthMode mode );
157+
158+
/**
159+
* Returns the offset added to the picture's rotation from a map's North.
160+
* @see setNorthOffset()
161+
* @see northMode()
162+
* @note added in QGIS 2.18
163+
*/
164+
double northOffset() const;
165+
166+
/**
167+
* Sets the offset added to the picture's rotation from a map's North.
168+
* @see northOffset()
169+
* @see setNorthMode()
170+
* @note added in QGIS 2.18
171+
*/
172+
void setNorthOffset( double offset );
173+
135174
/** Returns the resize mode used for drawing the picture within the composer
136175
* item's frame.
137176
* @returns resize mode of picture

python/core/core.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
%Include qgsannotation.sip
2222
%Include qgsapplication.sip
2323
%Include qgsattributeaction.sip
24+
%Include qgsbearingutils.sip
2425
%Include qgsbrowsermodel.sip
2526
%Include qgsclipper.sip
2627
%Include qgscolorscheme.sip

python/core/qgsbearingutils.sip

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* \class QgsBearingUtils
3+
* \ingroup core
4+
* Utilities for calculating bearings and directions.
5+
* \note Added in version 2.18
6+
*/
7+
class QgsBearingUtils
8+
{
9+
%TypeHeaderCode
10+
#include <qgsbearingutils.h>
11+
%End
12+
public:
13+
14+
/**
15+
* Returns the direction to true north from a specified point and for a specified
16+
* coordinate reference system. The returned value is in degrees clockwise from
17+
* vertical. An exception will be thrown if the bearing could not be calculated.
18+
*/
19+
static double bearingTrueNorth( const QgsCoordinateReferenceSystem& crs,
20+
const QgsPoint& point );
21+
};

src/app/composer/qgscomposerpicturewidget.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ QgsComposerPictureWidget::QgsComposerPictureWidget( QgsComposerPicture* picture
4444
mOutlineColorButton->setColorDialogTitle( tr( "Select outline color" ) );
4545
mOutlineColorButton->setContext( "composer" );
4646

47+
mNorthTypeComboBox->blockSignals( true );
48+
mNorthTypeComboBox->addItem( tr( "Grid north" ), QgsComposerPicture::GridNorth );
49+
mNorthTypeComboBox->addItem( tr( "True north" ), QgsComposerPicture::TrueNorth );
50+
mNorthTypeComboBox->blockSignals( false );
51+
mPictureRotationOffsetSpinBox->setClearValue( 0.0 );
52+
mPictureRotationSpinBox->setClearValue( 0.0 );
53+
4754
//add widget for general composer item properties
4855
QgsComposerItemWidget* itemPropertiesWidget = new QgsComposerItemWidget( this, picture );
4956
mainLayout->addWidget( itemPropertiesWidget );
@@ -273,6 +280,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged(
273280
mPicture->setRotationMap( -1 );
274281
mPictureRotationSpinBox->setEnabled( true );
275282
mComposerMapComboBox->setEnabled( false );
283+
mNorthTypeComboBox->setEnabled( false );
284+
mPictureRotationOffsetSpinBox->setEnabled( false );
276285
mPicture->setPictureRotation( mPictureRotationSpinBox->value() );
277286
}
278287
else
@@ -286,6 +295,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged(
286295

287296
mPicture->setRotationMap( composerId );
288297
mPictureRotationSpinBox->setEnabled( false );
298+
mNorthTypeComboBox->setEnabled( true );
299+
mPictureRotationOffsetSpinBox->setEnabled( true );
289300
mComposerMapComboBox->setEnabled( true );
290301
}
291302
mPicture->endCommand();
@@ -388,6 +399,8 @@ void QgsComposerPictureWidget::setGuiElementValues()
388399
mPictureLineEdit->blockSignals( true );
389400
mComposerMapComboBox->blockSignals( true );
390401
mRotationFromComposerMapCheckBox->blockSignals( true );
402+
mNorthTypeComboBox->blockSignals( true );
403+
mPictureRotationOffsetSpinBox->blockSignals( true );
391404
mResizeModeComboBox->blockSignals( true );
392405
mAnchorPointComboBox->blockSignals( true );
393406
mFillColorButton->blockSignals( true );
@@ -410,13 +423,19 @@ void QgsComposerPictureWidget::setGuiElementValues()
410423
{
411424
mComposerMapComboBox->setCurrentIndex( itemId );
412425
}
426+
mNorthTypeComboBox->setEnabled( true );
427+
mPictureRotationOffsetSpinBox->setEnabled( true );
413428
}
414429
else
415430
{
416431
mRotationFromComposerMapCheckBox->setCheckState( Qt::Unchecked );
417432
mPictureRotationSpinBox->setEnabled( true );
418433
mComposerMapComboBox->setEnabled( false );
434+
mNorthTypeComboBox->setEnabled( false );
435+
mPictureRotationOffsetSpinBox->setEnabled( false );
419436
}
437+
mNorthTypeComboBox->setCurrentIndex( mNorthTypeComboBox->findData( mPicture->northMode() ) );
438+
mPictureRotationOffsetSpinBox->setValue( mPicture->northOffset() );
420439

421440
mResizeModeComboBox->setCurrentIndex(( int )mPicture->resizeMode() );
422441
//disable picture rotation for non-zoom modes
@@ -444,6 +463,8 @@ void QgsComposerPictureWidget::setGuiElementValues()
444463
mPictureRotationSpinBox->blockSignals( false );
445464
mPictureLineEdit->blockSignals( false );
446465
mComposerMapComboBox->blockSignals( false );
466+
mNorthTypeComboBox->blockSignals( false );
467+
mPictureRotationOffsetSpinBox->blockSignals( false );
447468
mResizeModeComboBox->blockSignals( false );
448469
mAnchorPointComboBox->blockSignals( false );
449470
mFillColorButton->blockSignals( false );
@@ -726,6 +747,22 @@ void QgsComposerPictureWidget::showEvent( QShowEvent * event )
726747
refreshMapComboBox();
727748
}
728749

750+
void QgsComposerPictureWidget::on_mPictureRotationOffsetSpinBox_valueChanged( double d )
751+
{
752+
mPicture->beginCommand( tr( "Picture North offset changed" ), QgsComposerMergeCommand::ComposerPictureNorthOffset );
753+
mPicture->setNorthOffset( d );
754+
mPicture->endCommand();
755+
mPicture->update();
756+
}
757+
758+
void QgsComposerPictureWidget::on_mNorthTypeComboBox_currentIndexChanged( int index )
759+
{
760+
mPicture->beginCommand( tr( "Picture North mode changed" ) );
761+
mPicture->setNorthMode( static_cast< QgsComposerPicture::NorthMode >( mNorthTypeComboBox->itemData( index ).toInt() ) );
762+
mPicture->endCommand();
763+
mPicture->update();
764+
}
765+
729766
void QgsComposerPictureWidget::resizeEvent( QResizeEvent * event )
730767
{
731768
Q_UNUSED( event );

src/app/composer/qgscomposerpicturewidget.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ class QgsComposerPictureWidget: public QgsComposerItemBaseWidget, private Ui::Qg
7373
void on_mFillColorButton_colorChanged( const QColor& color );
7474
void on_mOutlineColorButton_colorChanged( const QColor& color );
7575
void on_mOutlineWidthSpinBox_valueChanged( double d );
76+
void on_mPictureRotationOffsetSpinBox_valueChanged( double d );
77+
void on_mNorthTypeComboBox_currentIndexChanged( int index );
7678

7779
private:
7880
QgsComposerPicture* mPicture;

src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ SET(QGIS_CORE_SRCS
7676
qgis.cpp
7777
qgsapplication.cpp
7878
qgsattributeaction.cpp
79+
qgsbearingutils.cpp
7980
qgsbrowsermodel.cpp
8081
qgscachedfeatureiterator.cpp
8182
qgscacheindex.cpp
@@ -583,6 +584,7 @@ SET(QGIS_CORE_HDRS
583584
qgis.h
584585
qgsannotation.h
585586
qgsattributeaction.h
587+
qgsbearingutils.h
586588
qgscachedfeatureiterator.h
587589
qgscacheindex.h
588590
qgscacheindexfeatureid.h

src/core/composer/qgscomposeritemcommand.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class CORE_EXPORT QgsComposerMergeCommand: public QgsComposerItemCommand
105105
LegendRasterBorderWidth,
106106
//composer picture
107107
ComposerPictureRotation,
108+
ComposerPictureNorthOffset,
108109
// composer scalebar
109110
ScaleBarLineWidth,
110111
ScaleBarHeight,

src/core/composer/qgscomposerpicture.cpp

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
#include "qgsnetworkcontentfetcher.h"
2929
#include "qgssymbollayerv2utils.h"
3030
#include "qgssvgcache.h"
31+
#include "qgslogger.h"
32+
#include "qgsbearingutils.h"
33+
#include "qgsmapsettings.h"
34+
3135
#include <QDomDocument>
3236
#include <QDomElement>
3337
#include <QFileInfo>
@@ -44,6 +48,8 @@ QgsComposerPicture::QgsComposerPicture( QgsComposition *composition )
4448
, mMode( Unknown )
4549
, mPictureRotation( 0 )
4650
, mRotationMap( nullptr )
51+
, mNorthMode( GridNorth )
52+
, mNorthOffset( 0.0 )
4753
, mResizeMode( QgsComposerPicture::Zoom )
4854
, mPictureAnchor( UpperLeft )
4955
, mSvgFillColor( QColor( 255, 255, 255 ) )
@@ -61,6 +67,8 @@ QgsComposerPicture::QgsComposerPicture()
6167
, mMode( Unknown )
6268
, mPictureRotation( 0 )
6369
, mRotationMap( nullptr )
70+
, mNorthMode( GridNorth )
71+
, mNorthOffset( 0.0 )
6472
, mResizeMode( QgsComposerPicture::Zoom )
6573
, mPictureAnchor( UpperLeft )
6674
, mSvgFillColor( QColor( 255, 255, 255 ) )
@@ -419,6 +427,43 @@ void QgsComposerPicture::remotePictureLoaded()
419427
mLoaded = true;
420428
}
421429

430+
void QgsComposerPicture::updateMapRotation()
431+
{
432+
if ( !mRotationMap )
433+
return;
434+
435+
// take map rotation
436+
double rotation = mRotationMap->mapRotation();
437+
438+
// handle true north
439+
switch ( mNorthMode )
440+
{
441+
case GridNorth:
442+
break; // nothing to do
443+
444+
case TrueNorth:
445+
{
446+
QgsPoint center = mRotationMap->currentMapExtent()->center();
447+
QgsCoordinateReferenceSystem crs = mComposition->mapSettings().destinationCrs();
448+
449+
try
450+
{
451+
double bearing = QgsBearingUtils::bearingTrueNorth( crs, center );
452+
rotation += bearing;
453+
}
454+
catch ( QgsException& e )
455+
{
456+
Q_UNUSED( e );
457+
QgsDebugMsg( QString( "Caught exception %1" ).arg( e.what() ) );
458+
}
459+
break;
460+
}
461+
}
462+
463+
rotation += mNorthOffset;
464+
setPictureRotation( rotation );
465+
}
466+
422467
void QgsComposerPicture::loadPicture( const QString &path )
423468
{
424469
if ( path.startsWith( "http" ) )
@@ -650,7 +695,8 @@ void QgsComposerPicture::setRotationMap( int composerMapId )
650695

651696
if ( composerMapId == -1 ) //disable rotation from map
652697
{
653-
QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) );
698+
disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
699+
disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
654700
mRotationMap = nullptr;
655701
}
656702

@@ -661,12 +707,14 @@ void QgsComposerPicture::setRotationMap( int composerMapId )
661707
}
662708
if ( mRotationMap )
663709
{
664-
QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) );
710+
disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
711+
disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
665712
}
666713
mPictureRotation = map->mapRotation();
667-
QObject::connect( map, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) );
714+
connect( map, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
715+
connect( map, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
668716
mRotationMap = map;
669-
update();
717+
updateMapRotation();
670718
emit pictureRotationChanged( mPictureRotation );
671719
}
672720

@@ -761,6 +809,8 @@ bool QgsComposerPicture::writeXML( QDomElement& elem, QDomDocument & doc ) const
761809
{
762810
composerPictureElem.setAttribute( "mapId", mRotationMap->id() );
763811
}
812+
composerPictureElem.setAttribute( "northMode", mNorthMode );
813+
composerPictureElem.setAttribute( "northOffset", mNorthOffset );
764814

765815
_writeXML( composerPictureElem, doc );
766816
elem.appendChild( composerPictureElem );
@@ -827,6 +877,9 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen
827877
}
828878

829879
//rotation map
880+
mNorthMode = static_cast< NorthMode >( itemElem.attribute( "northMode", "0" ).toInt() );
881+
mNorthOffset = itemElem.attribute( "northOffset", "0" ).toDouble();
882+
830883
int rotationMapId = itemElem.attribute( "mapId", "-1" ).toInt();
831884
if ( rotationMapId == -1 )
832885
{
@@ -837,10 +890,12 @@ bool QgsComposerPicture::readXML( const QDomElement& itemElem, const QDomDocumen
837890

838891
if ( mRotationMap )
839892
{
840-
QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) );
893+
disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
894+
disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
841895
}
842896
mRotationMap = mComposition->getComposerMapById( rotationMapId );
843-
QObject::connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) );
897+
connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
898+
connect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
844899
}
845900

846901
refreshPicture();
@@ -861,6 +916,18 @@ int QgsComposerPicture::rotationMap() const
861916
}
862917
}
863918

919+
void QgsComposerPicture::setNorthMode( QgsComposerPicture::NorthMode mode )
920+
{
921+
mNorthMode = mode;
922+
updateMapRotation();
923+
}
924+
925+
void QgsComposerPicture::setNorthOffset( double offset )
926+
{
927+
mNorthOffset = offset;
928+
updateMapRotation();
929+
}
930+
864931
void QgsComposerPicture::setPictureAnchor( QgsComposerItem::ItemPositionMode anchor )
865932
{
866933
mPictureAnchor = anchor;

0 commit comments

Comments
 (0)