Skip to content

Commit 89cc645

Browse files
authored
Merge pull request #3618 from nyalldawson/true_north
[composer] Allow syncing pictures to true north (fixes #192)
2 parents 2835cad + e8be0ed commit 89cc645

16 files changed

+471
-10
lines changed

ci/travis/linux/blacklist.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
PyQgsComposerPicture
21
PyQgsJSONUtils
32
PyQgsLocalServer
43
PyQgsPalLabelingServer

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

@@ -109,6 +116,38 @@ class QgsComposerPicture: QgsComposerItem
109116
*/
110117
bool useRotationMap() const;
111118

119+
/**
120+
* Returns the mode used to align the picture to a map's North.
121+
* @see setNorthMode()
122+
* @see northOffset()
123+
* @note added in QGIS 2.18
124+
*/
125+
NorthMode northMode() const;
126+
127+
/**
128+
* Sets the mode used to align the picture to a map's North.
129+
* @see northMode()
130+
* @see setNorthOffset()
131+
* @note added in QGIS 2.18
132+
*/
133+
void setNorthMode( NorthMode mode );
134+
135+
/**
136+
* Returns the offset added to the picture's rotation from a map's North.
137+
* @see setNorthOffset()
138+
* @see northMode()
139+
* @note added in QGIS 2.18
140+
*/
141+
double northOffset() const;
142+
143+
/**
144+
* Sets the offset added to the picture's rotation from a map's North.
145+
* @see northOffset()
146+
* @see setNorthMode()
147+
* @note added in QGIS 2.18
148+
*/
149+
void setNorthOffset( double offset );
150+
112151
/** Returns the resize mode used for drawing the picture within the composer
113152
* item's frame.
114153
* @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 qgsaggregatecalculator.sip
2222
%Include qgsattributetableconfig.sip
2323
%Include qgsattributeeditorelement.sip
24+
%Include qgsbearingutils.sip
2425
%Include qgsbrowsermodel.sip
2526
%Include qgsclipper.sip
2627
%Include qgscolorramp.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
@@ -45,6 +45,13 @@ QgsComposerPictureWidget::QgsComposerPictureWidget( QgsComposerPicture* picture
4545
mOutlineColorButton->setColorDialogTitle( tr( "Select outline color" ) );
4646
mOutlineColorButton->setContext( "composer" );
4747

48+
mNorthTypeComboBox->blockSignals( true );
49+
mNorthTypeComboBox->addItem( tr( "Grid north" ), QgsComposerPicture::GridNorth );
50+
mNorthTypeComboBox->addItem( tr( "True north" ), QgsComposerPicture::TrueNorth );
51+
mNorthTypeComboBox->blockSignals( false );
52+
mPictureRotationOffsetSpinBox->setClearValue( 0.0 );
53+
mPictureRotationSpinBox->setClearValue( 0.0 );
54+
4855
//add widget for general composer item properties
4956
QgsComposerItemWidget* itemPropertiesWidget = new QgsComposerItemWidget( this, picture );
5057
mainLayout->addWidget( itemPropertiesWidget );
@@ -270,6 +277,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged(
270277
mPicture->setRotationMap( -1 );
271278
mPictureRotationSpinBox->setEnabled( true );
272279
mComposerMapComboBox->setEnabled( false );
280+
mNorthTypeComboBox->setEnabled( false );
281+
mPictureRotationOffsetSpinBox->setEnabled( false );
273282
mPicture->setPictureRotation( mPictureRotationSpinBox->value() );
274283
}
275284
else
@@ -278,6 +287,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged(
278287
int mapId = map ? map->id() : -1;
279288
mPicture->setRotationMap( mapId );
280289
mPictureRotationSpinBox->setEnabled( false );
290+
mNorthTypeComboBox->setEnabled( true );
291+
mPictureRotationOffsetSpinBox->setEnabled( true );
281292
mComposerMapComboBox->setEnabled( true );
282293
}
283294
mPicture->endCommand();
@@ -325,6 +336,8 @@ void QgsComposerPictureWidget::setGuiElementValues()
325336
mPictureLineEdit->blockSignals( true );
326337
mComposerMapComboBox->blockSignals( true );
327338
mRotationFromComposerMapCheckBox->blockSignals( true );
339+
mNorthTypeComboBox->blockSignals( true );
340+
mPictureRotationOffsetSpinBox->blockSignals( true );
328341
mResizeModeComboBox->blockSignals( true );
329342
mAnchorPointComboBox->blockSignals( true );
330343
mFillColorButton->blockSignals( true );
@@ -345,13 +358,19 @@ void QgsComposerPictureWidget::setGuiElementValues()
345358
mRotationFromComposerMapCheckBox->setCheckState( Qt::Checked );
346359
mPictureRotationSpinBox->setEnabled( false );
347360
mComposerMapComboBox->setEnabled( true );
361+
mNorthTypeComboBox->setEnabled( true );
362+
mPictureRotationOffsetSpinBox->setEnabled( true );
348363
}
349364
else
350365
{
351366
mRotationFromComposerMapCheckBox->setCheckState( Qt::Unchecked );
352367
mPictureRotationSpinBox->setEnabled( true );
353368
mComposerMapComboBox->setEnabled( false );
369+
mNorthTypeComboBox->setEnabled( false );
370+
mPictureRotationOffsetSpinBox->setEnabled( false );
354371
}
372+
mNorthTypeComboBox->setCurrentIndex( mNorthTypeComboBox->findData( mPicture->northMode() ) );
373+
mPictureRotationOffsetSpinBox->setValue( mPicture->northOffset() );
355374

356375
mResizeModeComboBox->setCurrentIndex(( int )mPicture->resizeMode() );
357376
//disable picture rotation for non-zoom modes
@@ -379,6 +398,8 @@ void QgsComposerPictureWidget::setGuiElementValues()
379398
mPictureRotationSpinBox->blockSignals( false );
380399
mPictureLineEdit->blockSignals( false );
381400
mComposerMapComboBox->blockSignals( false );
401+
mNorthTypeComboBox->blockSignals( false );
402+
mPictureRotationOffsetSpinBox->blockSignals( false );
382403
mResizeModeComboBox->blockSignals( false );
383404
mAnchorPointComboBox->blockSignals( false );
384405
mFillColorButton->blockSignals( false );
@@ -655,6 +676,22 @@ void QgsComposerPictureWidget::on_mOutlineWidthSpinBox_valueChanged( double d )
655676
mPicture->update();
656677
}
657678

679+
void QgsComposerPictureWidget::on_mPictureRotationOffsetSpinBox_valueChanged( double d )
680+
{
681+
mPicture->beginCommand( tr( "Picture North offset changed" ), QgsComposerMergeCommand::ComposerPictureNorthOffset );
682+
mPicture->setNorthOffset( d );
683+
mPicture->endCommand();
684+
mPicture->update();
685+
}
686+
687+
void QgsComposerPictureWidget::on_mNorthTypeComboBox_currentIndexChanged( int index )
688+
{
689+
mPicture->beginCommand( tr( "Picture North mode changed" ) );
690+
mPicture->setNorthMode( static_cast< QgsComposerPicture::NorthMode >( mNorthTypeComboBox->itemData( index ).toInt() ) );
691+
mPicture->endCommand();
692+
mPicture->update();
693+
}
694+
658695
void QgsComposerPictureWidget::resizeEvent( QResizeEvent * event )
659696
{
660697
Q_UNUSED( event );

src/app/composer/qgscomposerpicturewidget.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ class QgsComposerPictureWidget: public QgsComposerItemBaseWidget, private Ui::Qg
7070
void on_mFillColorButton_colorChanged( const QColor& color );
7171
void on_mOutlineColorButton_colorChanged( const QColor& color );
7272
void on_mOutlineWidthSpinBox_valueChanged( double d );
73+
void on_mPictureRotationOffsetSpinBox_valueChanged( double d );
74+
void on_mNorthTypeComboBox_currentIndexChanged( int index );
7375

7476
private:
7577
QgsComposerPicture* mPicture;

src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ SET(QGIS_CORE_SRCS
8585
qgsaggregatecalculator.cpp
8686
qgsattributetableconfig.cpp
8787
qgsattributeeditorelement.cpp
88+
qgsbearingutils.cpp
8889
qgsbrowsermodel.cpp
8990
qgscachedfeatureiterator.cpp
9091
qgscacheindex.cpp
@@ -600,6 +601,7 @@ SET(QGIS_CORE_HDRS
600601
qgsannotation.h
601602
qgsattributetableconfig.h
602603
qgsattributeeditorelement.h
604+
qgsbearingutils.h
603605
qgscachedfeatureiterator.h
604606
qgscacheindex.h
605607
qgscacheindexfeatureid.h

src/core/composer/qgscomposeritemcommand.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class CORE_EXPORT QgsComposerMergeCommand: public QgsComposerItemCommand
118118
ComposerPictureRotation,
119119
ComposerPictureFillColor,
120120
ComposerPictureOutlineColor,
121+
ComposerPictureNorthOffset,
121122
// composer scalebar
122123
ScaleBarLineWidth,
123124
ScaleBarHeight,

src/core/composer/qgscomposerpicture.cpp

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
#include "qgssymbollayerutils.h"
3030
#include "qgssvgcache.h"
3131
#include "qgslogger.h"
32+
#include "qgsbearingutils.h"
33+
#include "qgsmapsettings.h"
3234

3335
#include <QDomDocument>
3436
#include <QDomElement>
@@ -46,6 +48,8 @@ QgsComposerPicture::QgsComposerPicture( QgsComposition *composition )
4648
, mMode( Unknown )
4749
, mPictureRotation( 0 )
4850
, mRotationMap( nullptr )
51+
, mNorthMode( GridNorth )
52+
, mNorthOffset( 0.0 )
4953
, mResizeMode( QgsComposerPicture::Zoom )
5054
, mPictureAnchor( UpperLeft )
5155
, mSvgFillColor( QColor( 255, 255, 255 ) )
@@ -63,6 +67,8 @@ QgsComposerPicture::QgsComposerPicture()
6367
, mMode( Unknown )
6468
, mPictureRotation( 0 )
6569
, mRotationMap( nullptr )
70+
, mNorthMode( GridNorth )
71+
, mNorthOffset( 0.0 )
6672
, mResizeMode( QgsComposerPicture::Zoom )
6773
, mPictureAnchor( UpperLeft )
6874
, mSvgFillColor( QColor( 255, 255, 255 ) )
@@ -408,6 +414,43 @@ void QgsComposerPicture::remotePictureLoaded()
408414
mLoaded = true;
409415
}
410416

417+
void QgsComposerPicture::updateMapRotation()
418+
{
419+
if ( !mRotationMap )
420+
return;
421+
422+
// take map rotation
423+
double rotation = mRotationMap->mapRotation();
424+
425+
// handle true north
426+
switch ( mNorthMode )
427+
{
428+
case GridNorth:
429+
break; // nothing to do
430+
431+
case TrueNorth:
432+
{
433+
QgsPoint center = mRotationMap->currentMapExtent()->center();
434+
QgsCoordinateReferenceSystem crs = mComposition->mapSettings().destinationCrs();
435+
436+
try
437+
{
438+
double bearing = QgsBearingUtils::bearingTrueNorth( crs, center );
439+
rotation += bearing;
440+
}
441+
catch ( QgsException& e )
442+
{
443+
Q_UNUSED( e );
444+
QgsDebugMsg( QString( "Caught exception %1" ).arg( e.what() ) );
445+
}
446+
break;
447+
}
448+
}
449+
450+
rotation += mNorthOffset;
451+
setPictureRotation( rotation );
452+
}
453+
411454
void QgsComposerPicture::loadPicture( const QString &path )
412455
{
413456
if ( path.startsWith( "http" ) )
@@ -633,7 +676,8 @@ void QgsComposerPicture::setRotationMap( int composerMapId )
633676

634677
if ( composerMapId == -1 ) //disable rotation from map
635678
{
636-
QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) );
679+
disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
680+
disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
637681
mRotationMap = nullptr;
638682
}
639683

@@ -644,10 +688,12 @@ void QgsComposerPicture::setRotationMap( int composerMapId )
644688
}
645689
if ( mRotationMap )
646690
{
647-
QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) );
691+
disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
692+
disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
648693
}
649694
mPictureRotation = map->mapRotation();
650-
QObject::connect( map, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setPictureRotation( double ) ) );
695+
connect( map, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
696+
connect( map, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
651697
mRotationMap = map;
652698
update();
653699
emit pictureRotationChanged( mPictureRotation );
@@ -722,6 +768,8 @@ bool QgsComposerPicture::writeXml( QDomElement& elem, QDomDocument & doc ) const
722768
{
723769
composerPictureElem.setAttribute( "mapId", mRotationMap->id() );
724770
}
771+
composerPictureElem.setAttribute( "northMode", mNorthMode );
772+
composerPictureElem.setAttribute( "northOffset", mNorthOffset );
725773

726774
_writeXml( composerPictureElem, doc );
727775
elem.appendChild( composerPictureElem );
@@ -788,6 +836,9 @@ bool QgsComposerPicture::readXml( const QDomElement& itemElem, const QDomDocumen
788836
}
789837

790838
//rotation map
839+
mNorthMode = static_cast< NorthMode >( itemElem.attribute( "northMode", "0" ).toInt() );
840+
mNorthOffset = itemElem.attribute( "northOffset", "0" ).toDouble();
841+
791842
int rotationMapId = itemElem.attribute( "mapId", "-1" ).toInt();
792843
if ( rotationMapId == -1 )
793844
{
@@ -798,10 +849,12 @@ bool QgsComposerPicture::readXml( const QDomElement& itemElem, const QDomDocumen
798849

799850
if ( mRotationMap )
800851
{
801-
QObject::disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) );
852+
disconnect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
853+
disconnect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
802854
}
803855
mRotationMap = mComposition->getComposerMapById( rotationMapId );
804-
QObject::connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( setRotation( double ) ) );
856+
connect( mRotationMap, SIGNAL( mapRotationChanged( double ) ), this, SLOT( updateMapRotation() ) );
857+
connect( mRotationMap, SIGNAL( extentChanged() ), this, SLOT( updateMapRotation() ) );
805858
}
806859

807860
refreshPicture();
@@ -822,6 +875,18 @@ int QgsComposerPicture::rotationMap() const
822875
}
823876
}
824877

878+
void QgsComposerPicture::setNorthMode( QgsComposerPicture::NorthMode mode )
879+
{
880+
mNorthMode = mode;
881+
updateMapRotation();
882+
}
883+
884+
void QgsComposerPicture::setNorthOffset( double offset )
885+
{
886+
mNorthOffset = offset;
887+
updateMapRotation();
888+
}
889+
825890
void QgsComposerPicture::setPictureAnchor( QgsComposerItem::ItemPositionMode anchor )
826891
{
827892
mPictureAnchor = anchor;

0 commit comments

Comments
 (0)