Skip to content

Commit fb78285

Browse files
committedOct 18, 2016
[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 b2b6675 commit fb78285

15 files changed

+478
-10
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
@@ -25,6 +25,7 @@
2525
%Include qgsaggregatecalculator.sip
2626
%Include qgsattributeaction.sip
2727
%Include qgsattributetableconfig.sip
28+
%Include qgsbearingutils.sip
2829
%Include qgsbrowsermodel.sip
2930
%Include qgsclipper.sip
3031
%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 );
@@ -280,6 +287,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged(
280287
mPicture->setRotationMap( -1 );
281288
mPictureRotationSpinBox->setEnabled( true );
282289
mComposerMapComboBox->setEnabled( false );
290+
mNorthTypeComboBox->setEnabled( false );
291+
mPictureRotationOffsetSpinBox->setEnabled( false );
283292
mPicture->setPictureRotation( mPictureRotationSpinBox->value() );
284293
}
285294
else
@@ -288,6 +297,8 @@ void QgsComposerPictureWidget::on_mRotationFromComposerMapCheckBox_stateChanged(
288297
int mapId = map ? map->id() : -1;
289298
mPicture->setRotationMap( mapId );
290299
mPictureRotationSpinBox->setEnabled( false );
300+
mNorthTypeComboBox->setEnabled( true );
301+
mPictureRotationOffsetSpinBox->setEnabled( true );
291302
mComposerMapComboBox->setEnabled( true );
292303
}
293304
mPicture->endCommand();
@@ -335,6 +346,8 @@ void QgsComposerPictureWidget::setGuiElementValues()
335346
mPictureLineEdit->blockSignals( true );
336347
mComposerMapComboBox->blockSignals( true );
337348
mRotationFromComposerMapCheckBox->blockSignals( true );
349+
mNorthTypeComboBox->blockSignals( true );
350+
mPictureRotationOffsetSpinBox->blockSignals( true );
338351
mResizeModeComboBox->blockSignals( true );
339352
mAnchorPointComboBox->blockSignals( true );
340353
mFillColorButton->blockSignals( true );
@@ -355,13 +368,19 @@ void QgsComposerPictureWidget::setGuiElementValues()
355368
mRotationFromComposerMapCheckBox->setCheckState( Qt::Checked );
356369
mPictureRotationSpinBox->setEnabled( false );
357370
mComposerMapComboBox->setEnabled( true );
371+
mNorthTypeComboBox->setEnabled( true );
372+
mPictureRotationOffsetSpinBox->setEnabled( true );
358373
}
359374
else
360375
{
361376
mRotationFromComposerMapCheckBox->setCheckState( Qt::Unchecked );
362377
mPictureRotationSpinBox->setEnabled( true );
363378
mComposerMapComboBox->setEnabled( false );
379+
mNorthTypeComboBox->setEnabled( false );
380+
mPictureRotationOffsetSpinBox->setEnabled( false );
364381
}
382+
mNorthTypeComboBox->setCurrentIndex( mNorthTypeComboBox->findData( mPicture->northMode() ) );
383+
mPictureRotationOffsetSpinBox->setValue( mPicture->northOffset() );
365384

366385
mResizeModeComboBox->setCurrentIndex(( int )mPicture->resizeMode() );
367386
//disable picture rotation for non-zoom modes
@@ -389,6 +408,8 @@ void QgsComposerPictureWidget::setGuiElementValues()
389408
mPictureRotationSpinBox->blockSignals( false );
390409
mPictureLineEdit->blockSignals( false );
391410
mComposerMapComboBox->blockSignals( false );
411+
mNorthTypeComboBox->blockSignals( false );
412+
mPictureRotationOffsetSpinBox->blockSignals( false );
392413
mResizeModeComboBox->blockSignals( false );
393414
mAnchorPointComboBox->blockSignals( false );
394415
mFillColorButton->blockSignals( false );
@@ -665,6 +686,22 @@ void QgsComposerPictureWidget::on_mOutlineWidthSpinBox_valueChanged( double d )
665686
mPicture->update();
666687
}
667688

689+
void QgsComposerPictureWidget::on_mPictureRotationOffsetSpinBox_valueChanged( double d )
690+
{
691+
mPicture->beginCommand( tr( "Picture North offset changed" ), QgsComposerMergeCommand::ComposerPictureNorthOffset );
692+
mPicture->setNorthOffset( d );
693+
mPicture->endCommand();
694+
mPicture->update();
695+
}
696+
697+
void QgsComposerPictureWidget::on_mNorthTypeComboBox_currentIndexChanged( int index )
698+
{
699+
mPicture->beginCommand( tr( "Picture North mode changed" ) );
700+
mPicture->setNorthMode( static_cast< QgsComposerPicture::NorthMode >( mNorthTypeComboBox->itemData( index ).toInt() ) );
701+
mPicture->endCommand();
702+
mPicture->update();
703+
}
704+
668705
void QgsComposerPictureWidget::resizeEvent( QResizeEvent * event )
669706
{
670707
Q_UNUSED( event );

‎src/app/composer/qgscomposerpicturewidget.h

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

7678
private:
7779
QgsComposerPicture* mPicture;

‎src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ SET(QGIS_CORE_SRCS
8181
qgsactionmanager.cpp
8282
qgsaggregatecalculator.cpp
8383
qgsattributetableconfig.cpp
84+
qgsbearingutils.cpp
8485
qgsbrowsermodel.cpp
8586
qgscachedfeatureiterator.cpp
8687
qgscacheindex.cpp
@@ -613,6 +614,7 @@ SET(QGIS_CORE_HDRS
613614
qgsannotation.h
614615
qgsattributetableconfig.h
615616
qgsattributeaction.h
617+
qgsbearingutils.h
616618
qgscachedfeatureiterator.h
617619
qgscacheindex.h
618620
qgscacheindexfeatureid.h

‎src/core/composer/qgscomposeritemcommand.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class CORE_EXPORT QgsComposerMergeCommand: public QgsComposerItemCommand
109109
LegendRasterBorderWidth,
110110
//composer picture
111111
ComposerPictureRotation,
112+
ComposerPictureNorthOffset,
112113
// composer scalebar
113114
ScaleBarLineWidth,
114115
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;

‎src/core/composer/qgscomposerpicture.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
5353
Unknown
5454
};
5555

56+
//! Method for syncing rotation to a map's North direction
57+
enum NorthMode
58+
{
59+
GridNorth = 0, /*!< Align to grid north */
60+
TrueNorth, /*!< Align to true north */
61+
};
62+
5663
QgsComposerPicture( QgsComposition *composition );
5764
~QgsComposerPicture();
5865

@@ -155,6 +162,38 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
155162
*/
156163
bool useRotationMap() const { return mRotationMap; }
157164

165+
/**
166+
* Returns the mode used to align the picture to a map's North.
167+
* @see setNorthMode()
168+
* @see northOffset()
169+
* @note added in QGIS 2.18
170+
*/
171+
NorthMode northMode() const { return mNorthMode; }
172+
173+
/**
174+
* Sets the mode used to align the picture to a map's North.
175+
* @see northMode()
176+
* @see setNorthOffset()
177+
* @note added in QGIS 2.18
178+
*/
179+
void setNorthMode( NorthMode mode );
180+
181+
/**
182+
* Returns the offset added to the picture's rotation from a map's North.
183+
* @see setNorthOffset()
184+
* @see northMode()
185+
* @note added in QGIS 2.18
186+
*/
187+
double northOffset() const { return mNorthOffset; }
188+
189+
/**
190+
* Sets the offset added to the picture's rotation from a map's North.
191+
* @see northOffset()
192+
* @see setNorthMode()
193+
* @note added in QGIS 2.18
194+
*/
195+
void setNorthOffset( double offset );
196+
158197
/** Returns the resize mode used for drawing the picture within the composer
159198
* item's frame.
160199
* @returns resize mode of picture
@@ -366,6 +405,12 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
366405
double mPictureRotation;
367406
/** Map that sets the rotation (or 0 if this picture uses map independent rotation)*/
368407
const QgsComposerMap* mRotationMap;
408+
409+
//! Mode used to align to North
410+
NorthMode mNorthMode;
411+
//! Offset for north arrow
412+
double mNorthOffset;
413+
369414
/** Width of the picture (in mm)*/
370415
double mPictureWidth;
371416
/** Height of the picture (in mm)*/
@@ -404,6 +449,8 @@ class CORE_EXPORT QgsComposerPicture: public QgsComposerItem
404449
private slots:
405450

406451
void remotePictureLoaded();
452+
453+
void updateMapRotation();
407454
};
408455

409456
#endif

‎src/core/qgsbearingutils.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/***************************************************************************
2+
qgsbearingutils.cpp
3+
-------------------
4+
begin : October 2016
5+
copyright : (C) 2016 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#include "qgsbearingutils.h"
19+
#include "qgscoordinatereferencesystem.h"
20+
#include "qgspoint.h"
21+
#include "qgscoordinatetransform.h"
22+
#include "qgsexception.h"
23+
24+
double QgsBearingUtils::bearingTrueNorth( const QgsCoordinateReferenceSystem &crs, const QgsPoint &point )
25+
{
26+
// step 1 - transform point into WGS84 geographic crs
27+
QgsCoordinateReferenceSystem destCrs;
28+
destCrs.createFromOgcWmsCrs( "EPSG:4326" );
29+
QgsCoordinateTransform transform( crs, destCrs );
30+
31+
if ( !transform.isInitialised() )
32+
{
33+
//raise
34+
throw QgsException( QObject::tr( "Could not create transform to calculate true north" ) );
35+
}
36+
37+
if ( transform.isShortCircuited() )
38+
return 0.0;
39+
40+
QgsPoint p1 = transform.transform( point );
41+
42+
// shift point a tiny bit north
43+
QgsPoint p2 = p1;
44+
p2.setY( p2.y() + 0.000001 );
45+
46+
//transform back
47+
QgsPoint p3 = transform.transform( p2, QgsCoordinateTransform::ReverseTransform );
48+
49+
// find bearing from point to p3
50+
return point.azimuth( p3 );
51+
}

‎src/core/qgsbearingutils.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/***************************************************************************
2+
qgsbearingutils.h
3+
-----------------
4+
begin : October 2016
5+
copyright : (C) 2016 by Nyall Dawson
6+
email : nyall dot dawson at gmail dot com
7+
***************************************************************************/
8+
9+
/***************************************************************************
10+
* *
11+
* This program is free software; you can redistribute it and/or modify *
12+
* it under the terms of the GNU General Public License as published by *
13+
* the Free Software Foundation; either version 2 of the License, or *
14+
* (at your option) any later version. *
15+
* *
16+
***************************************************************************/
17+
18+
#ifndef QGSBEARINGUTILS_H
19+
#define QGSBEARINGUTILS_H
20+
21+
class QgsCoordinateReferenceSystem;
22+
class QgsPoint;
23+
24+
25+
/**
26+
* \class QgsBearingUtils
27+
* \ingroup core
28+
* Utilities for calculating bearings and directions.
29+
* \note Added in version 2.18
30+
*/
31+
class CORE_EXPORT QgsBearingUtils
32+
{
33+
public:
34+
35+
/**
36+
* Returns the direction to true north from a specified point and for a specified
37+
* coordinate reference system. The returned value is in degrees clockwise from
38+
* vertical. An exception will be thrown if the bearing could not be calculated.
39+
*/
40+
static double bearingTrueNorth( const QgsCoordinateReferenceSystem& crs,
41+
const QgsPoint& point );
42+
43+
};
44+
45+
#endif //QGSBEARINGUTILS_H

‎src/ui/composer/qgscomposerpicturewidgetbase.ui

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@
6060
<property name="geometry">
6161
<rect>
6262
<x>0</x>
63-
<y>-166</y>
64-
<width>313</width>
65-
<height>719</height>
63+
<y>-312</y>
64+
<width>314</width>
65+
<height>871</height>
6666
</rect>
6767
</property>
6868
<layout class="QVBoxLayout" name="mainLayout">
@@ -463,6 +463,16 @@
463463
<bool>false</bool>
464464
</property>
465465
<layout class="QGridLayout" name="gridLayout_2" columnstretch="0,1">
466+
<item row="2" column="1">
467+
<widget class="QComboBox" name="mNorthTypeComboBox"/>
468+
</item>
469+
<item row="2" column="0">
470+
<widget class="QLabel" name="label_7">
471+
<property name="text">
472+
<string>North alignment</string>
473+
</property>
474+
</widget>
475+
</item>
466476
<item row="1" column="0">
467477
<widget class="QCheckBox" name="mRotationFromComposerMapCheckBox">
468478
<property name="text">
@@ -478,6 +488,29 @@
478488
<property name="suffix">
479489
<string> °</string>
480490
</property>
491+
<property name="minimum">
492+
<double>-360.000000000000000</double>
493+
</property>
494+
<property name="maximum">
495+
<double>360.000000000000000</double>
496+
</property>
497+
</widget>
498+
</item>
499+
<item row="3" column="0">
500+
<widget class="QLabel" name="label_8">
501+
<property name="text">
502+
<string>Offset</string>
503+
</property>
504+
</widget>
505+
</item>
506+
<item row="3" column="1">
507+
<widget class="QgsDoubleSpinBox" name="mPictureRotationOffsetSpinBox">
508+
<property name="suffix">
509+
<string> °</string>
510+
</property>
511+
<property name="minimum">
512+
<double>-360.000000000000000</double>
513+
</property>
481514
<property name="maximum">
482515
<double>360.000000000000000</double>
483516
</property>

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ADD_PYTHON_TEST(PyQgsAttributeFormEditorWidget test_qgsattributeformeditorwidget
1717
ADD_PYTHON_TEST(PyQgsAttributeTableConfig test_qgsattributetableconfig.py)
1818
ADD_PYTHON_TEST(PyQgsAttributeTableModel test_qgsattributetablemodel.py)
1919
#ADD_PYTHON_TEST(PyQgsAuthenticationSystem test_qgsauthsystem.py)
20+
ADD_PYTHON_TEST(PyQgsBearingUtils test_qgsbearingutils.py)
2021
ADD_PYTHON_TEST(PyQgsBlendModes test_qgsblendmodes.py)
2122
ADD_PYTHON_TEST(PyQgsCategorizedSymbolRendererV2 test_qgscategorizedsymbolrendererv2.py)
2223
ADD_PYTHON_TEST(PyQgsColorButtonV2 test_qgscolorbuttonv2.py)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsBearingUtils.
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '18/10/2016'
11+
__copyright__ = 'Copyright 2016, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis # switch sip api
16+
17+
from qgis.core import (QgsBearingUtils,
18+
QgsCoordinateReferenceSystem,
19+
QgsPoint
20+
)
21+
22+
from qgis.testing import (start_app,
23+
unittest
24+
)
25+
26+
27+
start_app()
28+
29+
30+
class TestQgsBearingUtils(unittest.TestCase):
31+
32+
def testTrueNorth(self):
33+
""" test calculating bearing to true north"""
34+
35+
# short circuit - already a geographic crs
36+
crs = QgsCoordinateReferenceSystem()
37+
crs.createFromOgcWmsCrs('EPSG:4326')
38+
self.assertEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(0, 0)), 0)
39+
self.assertEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(44, 0)), 0)
40+
self.assertEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(44, -43)), 0)
41+
self.assertEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(44, 43)), 0)
42+
43+
self.assertEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(44, 200)), 0)
44+
self.assertEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(44, -200)), 0)
45+
46+
# no short circuit
47+
crs.createFromOgcWmsCrs('EPSG:3111')
48+
self.assertAlmostEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(2508807, 2423425)), 0.06, 2)
49+
50+
# try a south-up crs
51+
crs.createFromOgcWmsCrs('EPSG:2053')
52+
self.assertAlmostEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(29, -27.55)), -180.0, 1)
53+
54+
# try a north pole crs
55+
crs.createFromOgcWmsCrs('EPSG:3575')
56+
self.assertAlmostEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(-780770, 652329)), 129.9, 1)
57+
self.assertAlmostEqual(QgsBearingUtils.bearingTrueNorth(crs, QgsPoint(513480, 873173)), -149.5, 1)
58+
59+
if __name__ == '__main__':
60+
unittest.main()

‎tests/src/python/test_qgscomposerpicture.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222

2323
from qgis.core import (QgsComposerPicture,
2424
QgsComposition,
25-
QgsMapSettings
25+
QgsMapSettings,
26+
QgsComposerMap,
27+
QgsRectangle,
28+
QgsCoordinateReferenceSystem
2629
)
2730
from qgis.testing import start_app, unittest
2831
from utilities import unitTestDataPath
@@ -86,5 +89,63 @@ def testRemoteImage(self):
8689
self.composerPicture.setPicturePath(self.pngImage)
8790
assert testResult, message
8891

92+
def testGridNorth(self):
93+
"""Test syncing picture to grid north"""
94+
95+
mapSettings = QgsMapSettings()
96+
composition = QgsComposition(mapSettings)
97+
98+
composerMap = QgsComposerMap(composition)
99+
composerMap.setNewExtent(QgsRectangle(0, -256, 256, 0))
100+
composition.addComposerMap(composerMap)
101+
102+
composerPicture = QgsComposerPicture(composition)
103+
composition.addComposerPicture(composerPicture)
104+
105+
composerPicture.setRotationMap(composerMap.id())
106+
self.assertTrue(composerPicture.rotationMap() >= 0)
107+
108+
composerPicture.setNorthMode(QgsComposerPicture.GridNorth)
109+
composerMap.setMapRotation(45)
110+
self.assertEqual(composerPicture.pictureRotation(), 45)
111+
112+
# add an offset
113+
composerPicture.setNorthOffset(-10)
114+
self.assertEqual(composerPicture.pictureRotation(), 35)
115+
116+
def testTrueNorth(self):
117+
"""Test syncing picture to true north"""
118+
119+
mapSettings = QgsMapSettings()
120+
crs = QgsCoordinateReferenceSystem()
121+
crs.createFromOgcWmsCrs('EPSG:3575')
122+
mapSettings.setDestinationCrs(crs)
123+
composition = QgsComposition(mapSettings)
124+
125+
composerMap = QgsComposerMap(composition)
126+
composerMap.setNewExtent(QgsRectangle(-2126029.962, -2200807.749, -119078.102, -757031.156))
127+
composition.addComposerMap(composerMap)
128+
129+
composerPicture = QgsComposerPicture(composition)
130+
composition.addComposerPicture(composerPicture)
131+
132+
composerPicture.setRotationMap(composerMap.id())
133+
self.assertTrue(composerPicture.rotationMap() >= 0)
134+
135+
composerPicture.setNorthMode(QgsComposerPicture.TrueNorth)
136+
self.assertAlmostEqual(composerPicture.pictureRotation(), 37.20, 1)
137+
138+
# shift map
139+
composerMap.setNewExtent(QgsRectangle(2120672.293, -3056394.691, 2481640.226, -2796718.780))
140+
self.assertAlmostEqual(composerPicture.pictureRotation(), -38.18, 1)
141+
142+
# rotate map
143+
composerMap.setMapRotation(45)
144+
self.assertAlmostEqual(composerPicture.pictureRotation(), -38.18 + 45, 1)
145+
146+
# add an offset
147+
composerPicture.setNorthOffset(-10)
148+
self.assertAlmostEqual(composerPicture.pictureRotation(), -38.18 + 35, 1)
149+
89150
if __name__ == '__main__':
90151
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.