Skip to content

Commit 99cef42

Browse files
committed
Improve QgsDistanceArea API to make it easier to determine units
for returned distances, add unit tests
1 parent cb7d0fb commit 99cef42

8 files changed

+201
-28
lines changed

python/core/qgsdistancearea.sip

+48-7
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@ class QgsDistanceArea
5050
void setSourceAuthId( const QString& authid );
5151

5252
//! returns source spatial reference system
53-
long sourceCrs() const;
53+
//! @deprecated use sourceCrsId() instead
54+
long sourceCrs() const /Deprecated/;
55+
56+
/** Returns the QgsCoordinateReferenceSystem::srsid() for the CRS used during calculations.
57+
* @see setSourceCrs()
58+
* @note added in QGIS 2.14
59+
*/
60+
long sourceCrsId() const;
61+
5462
//! What sort of coordinate system is being used?
5563
bool geographic() const;
5664

@@ -105,22 +113,46 @@ class QgsDistanceArea
105113

106114
/** Measures the length of a geometry.
107115
* @param geometry geometry to measure
108-
* @returns length of geometry. For geometry collections, non curve geometries will be ignored
116+
* @returns length of geometry. For geometry collections, non curve geometries will be ignored. The units for the
117+
* returned distance can be retrieved by calling lengthUnits().
109118
* @note added in QGIS 2.12
119+
* @see lengthUnits()
110120
* @see measureArea()
111121
* @see measurePerimeter()
112122
*/
113123
double measureLength( const QgsGeometry* geometry ) const;
114124

115-
//! measures perimeter of polygon
116-
double measurePerimeter( const QgsGeometry* geometry ) const;
125+
/** Measures the perimeter of a polygon geometry.
126+
* @param geometry geometry to measure
127+
* @returns perimeter of geometry. For geometry collections, any non-polygon geometries will be ignored. The units for the
128+
* returned perimeter can be retrieved by calling lengthUnits().
129+
* @note added in QGIS 2.12
130+
* @see lengthUnits()
131+
* @see measureArea()
132+
* @see measurePerimeter()
133+
*/
134+
double measurePerimeter( const QgsGeometry *geometry ) const;
117135

118-
//! measures line
136+
/** Measures the length of a line with multiple segments.
137+
* @param points list of points in line
138+
* @returns length of line. The units for the returned length can be retrieved by calling lengthUnits().
139+
* @see lengthUnits()
140+
*/
119141
double measureLine( const QList<QgsPoint>& points ) const;
120142

121-
//! measures line with one segment
143+
/** Measures length of a line with one segment.
144+
* @param p1 start of line
145+
* @param p2 end of line
146+
* @returns distance between points. The units for the returned distance can be retrieved by calling lengthUnits().
147+
* @see lengthUnits()
148+
*/
122149
double measureLine( const QgsPoint& p1, const QgsPoint& p2 ) const;
123150

151+
/** Returns the units of distance for length calculations made by this object.
152+
* @note added in QGIS 2.14
153+
*/
154+
QGis::UnitType lengthUnits() const;
155+
124156
//! measures polygon area
125157
double measurePolygon( const QList<QgsPoint>& points ) const;
126158

@@ -132,12 +164,21 @@ class QgsDistanceArea
132164
//! Helper for conversion between physical units
133165
void convertMeasurement( double &measure /In,Out/, QGis::UnitType &measureUnits /In,Out/, QGis::UnitType displayUnits, bool isArea ) const;
134166

167+
/** Takes a length measurement calculated by this QgsDistanceArea object and converts it to a
168+
* different distance unit.
169+
* @param length length value calculated by this class to convert. It is assumed that the length
170+
* was calculated by this class, ie that its unit of length is equal lengthUnits().
171+
* @param toUnits distance unit to convert measurement to
172+
* @returns converted distance
173+
*/
174+
double convertLengthMeasurement( double length, QGis::UnitType toUnits ) const;
175+
135176
protected:
136177
//! measures line distance, line points are extracted from WKB
137178
// @note available in python bindings
138179
// const unsigned char* measureLine( const unsigned char* feature, double* area, bool hasZptr = false ) const;
139180
//! measures polygon area and perimeter, vertices are extracted from WKB
140-
// @note available in python bindings
181+
// @note not available in python bindings
141182
// const unsigned char* measurePolygon( const unsigned char* feature, double* area, double* perimeter, bool hasZptr = false ) const;
142183

143184
/**

src/app/qgsmeasuredialog.cpp

+11-12
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,7 @@ void QgsMeasureDialog::mouseMove( QgsPoint &point )
141141

142142
editTotal->setText( formatDistance( mTotal + d ) );
143143

144-
QGis::UnitType displayUnits;
145-
// Meters or feet?
146-
convertMeasurement( d, displayUnits, false );
144+
d = convertLength( d, mDisplayUnits );
147145

148146
// Set moving
149147
QTreeWidgetItem *item = mTable->topLevelItem( mTable->topLevelItemCount() - 1 );
@@ -211,9 +209,7 @@ void QgsMeasureDialog::removeLastPoint()
211209
mTotal = mDa.measureLine( mTool->points() );
212210
editTotal->setText( formatDistance( mTotal + d ) );
213211

214-
QGis::UnitType displayUnits;
215-
// Meters or feet?
216-
convertMeasurement( d, displayUnits, false );
212+
d = convertLength( d, mDisplayUnits );
217213

218214
QTreeWidgetItem *item = mTable->topLevelItem( mTable->topLevelItemCount() - 1 );
219215
item->setText( 0, QLocale::system().toString( d, 'f', mDecimalPlaces ) );
@@ -252,9 +248,8 @@ QString QgsMeasureDialog::formatDistance( double distance )
252248
QSettings settings;
253249
bool baseUnit = settings.value( "/qgis/measure/keepbaseunit", false ).toBool();
254250

255-
QGis::UnitType newDisplayUnits;
256-
convertMeasurement( distance, newDisplayUnits, false );
257-
return QgsDistanceArea::textUnit( distance, mDecimalPlaces, newDisplayUnits, false, baseUnit );
251+
distance = convertLength( distance, mDisplayUnits );
252+
return QgsDistanceArea::textUnit( distance, mDecimalPlaces, mDisplayUnits, false, baseUnit );
258253
}
259254

260255
QString QgsMeasureDialog::formatArea( double area )
@@ -329,9 +324,8 @@ void QgsMeasureDialog::updateUi()
329324
p2 = *it;
330325
if ( !b )
331326
{
332-
double d = mDa.measureLine( p1, p2 );
333-
QGis::UnitType dummyUnits;
334-
convertMeasurement( d, dummyUnits, false );
327+
double d = mDa.measureLine( p1, p2 );
328+
d = convertLength( d, mDisplayUnits );
335329

336330
QTreeWidgetItem *item = new QTreeWidgetItem( QStringList( QLocale::system().toString( d, 'f', mDecimalPlaces ) ) );
337331
item->setTextAlignment( 0, Qt::AlignRight );
@@ -361,6 +355,11 @@ void QgsMeasureDialog::convertMeasurement( double &measure, QGis::UnitType &u, b
361355
u = myUnits;
362356
}
363357

358+
double QgsMeasureDialog::convertLength( double length, QGis::UnitType toUnit )
359+
{
360+
return mDa.convertLengthMeasurement( length, toUnit );
361+
}
362+
364363

365364
void QgsMeasureDialog::reject()
366365
{

src/app/qgsmeasuredialog.h

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ class APP_EXPORT QgsMeasureDialog : public QDialog, private Ui::QgsMeasureBase
8585
//! Converts the measurement, depending on settings in options and current transformation
8686
void convertMeasurement( double &measure, QGis::UnitType &u, bool isArea );
8787

88+
double convertLength( double length, QGis::UnitType toUnit );
89+
8890
double mTotal;
8991

9092
//! indicates whether we're measuring distances or areas

src/core/qgsdistancearea.cpp

+18
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,10 @@ double QgsDistanceArea::measureLine( const QgsPoint& p1, const QgsPoint& p2, QGi
534534
return result;
535535
}
536536

537+
QGis::UnitType QgsDistanceArea::lengthUnits() const
538+
{
539+
return willUseEllipsoid() ? QGis::Meters : mCoordTransform->sourceCrs().mapUnits();
540+
}
537541

538542
const unsigned char *QgsDistanceArea::measurePolygon( const unsigned char* feature, double* area, double* perimeter, bool hasZptr ) const
539543
{
@@ -1118,3 +1122,17 @@ void QgsDistanceArea::convertMeasurement( double &measure, QGis::UnitType &measu
11181122
measureUnits = displayUnits;
11191123
}
11201124

1125+
double QgsDistanceArea::convertLengthMeasurement( double length, QGis::UnitType toUnits ) const
1126+
{
1127+
// get the conversion factor between the specified units
1128+
QGis::UnitType measureUnits = lengthUnits();
1129+
double factorUnits = QgsUnitTypes::fromUnitToUnitFactor( measureUnits, toUnits );
1130+
1131+
double result = length * factorUnits;
1132+
QgsDebugMsg( QString( "Converted length of %1 %2 to %3 %4" ).arg( length )
1133+
.arg( QgsUnitTypes::toString( measureUnits ) )
1134+
.arg( result )
1135+
.arg( QgsUnitTypes::toString( toUnits ) ) );
1136+
return result;
1137+
}
1138+

src/core/qgsdistancearea.h

+46-7
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,16 @@ class CORE_EXPORT QgsDistanceArea
8383
void setSourceAuthId( const QString& authid );
8484

8585
//! returns source spatial reference system
86-
long sourceCrs() const { return mCoordTransform->sourceCrs().srsid(); }
86+
//! @deprecated use sourceCrsId() instead
87+
// TODO QGIS 3.0 - make sourceCrs() return QgsCoordinateReferenceSystem
88+
Q_DECL_DEPRECATED long sourceCrs() const { return mCoordTransform->sourceCrs().srsid(); }
89+
90+
/** Returns the QgsCoordinateReferenceSystem::srsid() for the CRS used during calculations.
91+
* @see setSourceCrs()
92+
* @note added in QGIS 2.14
93+
*/
94+
long sourceCrsId() const { return mCoordTransform->sourceCrs().srsid(); }
95+
8796
//! What sort of coordinate system is being used?
8897
bool geographic() const { return mCoordTransform->sourceCrs().geographicFlag(); }
8998

@@ -138,23 +147,38 @@ class CORE_EXPORT QgsDistanceArea
138147

139148
/** Measures the length of a geometry.
140149
* @param geometry geometry to measure
141-
* @returns length of geometry. For geometry collections, non curve geometries will be ignored
150+
* @returns length of geometry. For geometry collections, non curve geometries will be ignored. The units for the
151+
* returned distance can be retrieved by calling lengthUnits().
142152
* @note added in QGIS 2.12
153+
* @see lengthUnits()
143154
* @see measureArea()
144155
* @see measurePerimeter()
145156
*/
146157
double measureLength( const QgsGeometry* geometry ) const;
147158

148-
//! measures perimeter of polygon
159+
/** Measures the perimeter of a polygon geometry.
160+
* @param geometry geometry to measure
161+
* @returns perimeter of geometry. For geometry collections, any non-polygon geometries will be ignored. The units for the
162+
* returned perimeter can be retrieved by calling lengthUnits().
163+
* @note added in QGIS 2.12
164+
* @see lengthUnits()
165+
* @see measureArea()
166+
* @see measurePerimeter()
167+
*/
149168
double measurePerimeter( const QgsGeometry *geometry ) const;
150169

151-
//! measures line
170+
/** Measures the length of a line with multiple segments.
171+
* @param points list of points in line
172+
* @returns length of line. The units for the returned length can be retrieved by calling lengthUnits().
173+
* @see lengthUnits()
174+
*/
152175
double measureLine( const QList<QgsPoint>& points ) const;
153176

154-
/** Measures length of line with one segment
177+
/** Measures length of a line with one segment.
155178
* @param p1 start of line
156179
* @param p2 end of line
157-
* @returns distance in meters, or map units if cartesian calculation was performed
180+
* @returns distance between points. The units for the returned distance can be retrieved by calling lengthUnits().
181+
* @see lengthUnits()
158182
*/
159183
double measureLine( const QgsPoint& p1, const QgsPoint& p2 ) const;
160184

@@ -167,6 +191,11 @@ class CORE_EXPORT QgsDistanceArea
167191
*/
168192
double measureLine( const QgsPoint& p1, const QgsPoint& p2, QGis::UnitType& units ) const;
169193

194+
/** Returns the units of distance for length calculations made by this object.
195+
* @note added in QGIS 2.14
196+
*/
197+
QGis::UnitType lengthUnits() const;
198+
170199
//! measures polygon area
171200
double measurePolygon( const QList<QgsPoint>& points ) const;
172201

@@ -176,11 +205,21 @@ class CORE_EXPORT QgsDistanceArea
176205
static QString textUnit( double value, int decimals, QGis::UnitType u, bool isArea, bool keepBaseUnit = false );
177206

178207
//! Helper for conversion between physical units
208+
// TODO QGIS 3.0 - remove this method, as its behaviour is non-intuitive.
179209
void convertMeasurement( double &measure, QGis::UnitType &measureUnits, QGis::UnitType displayUnits, bool isArea ) const;
180210

211+
/** Takes a length measurement calculated by this QgsDistanceArea object and converts it to a
212+
* different distance unit.
213+
* @param length length value calculated by this class to convert. It is assumed that the length
214+
* was calculated by this class, ie that its unit of length is equal lengthUnits().
215+
* @param toUnits distance unit to convert measurement to
216+
* @returns converted distance
217+
*/
218+
double convertLengthMeasurement( double length, QGis::UnitType toUnits ) const;
219+
181220
protected:
182221
//! measures polygon area and perimeter, vertices are extracted from WKB
183-
// @note available in python bindings
222+
// @note not available in python bindings
184223
const unsigned char* measurePolygon( const unsigned char* feature, double* area, double* perimeter, bool hasZptr = false ) const;
185224

186225
/**

src/gui/qgsmaptoolidentify.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ void QgsMapToolIdentify::convertMeasurement( QgsDistanceArea &calc, double &meas
625625
QGis::UnitType myUnits = mCanvas->mapUnits();
626626

627627
calc.convertMeasurement( measure, myUnits, displayUnits(), isArea );
628-
u = myUnits;
628+
u = displayUnits();
629629
}
630630

631631
QGis::UnitType QgsMapToolIdentify::displayUnits()

tests/src/core/testqgsdistancearea.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ void TestQgsDistanceArea::unit_conversions()
162162
//outputUnit = QGis::Meters;
163163

164164
// First, convert from sq.meter to sq.feet
165+
Q_NOWARN_DEPRECATED_PUSH
165166
myDa.convertMeasurement( inputValue, inputUnit, outputUnit, true );
167+
Q_NOWARN_DEPRECATED_POP
166168
QVERIFY( qAbs( inputValue - 107639.1041671 ) <= 0.0000001 );
167169

168170
// The print a text unit. This is i18n, so we should ignore the unit

tests/src/python/test_qgsdistancearea.py

+73-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
from qgis.core import (QgsGeometry,
1818
QgsPoint,
19-
QgsDistanceArea
19+
QgsDistanceArea,
20+
QgsCoordinateReferenceSystem,
21+
QGis,
22+
QgsUnitTypes
2023
)
2124

2225
from qgis.testing import (start_app,
@@ -30,6 +33,19 @@
3033

3134
class TestQgsDistanceArea(unittest.TestCase):
3235

36+
def testCrs(self):
37+
# test setting/getting the source CRS
38+
da = QgsDistanceArea()
39+
40+
# try setting using a crs id
41+
da.setSourceCrs(3452)
42+
self.assertEqual(da.sourceCrsId(), 3452)
43+
44+
# try setting using a CRS object
45+
crs = QgsCoordinateReferenceSystem(3111, QgsCoordinateReferenceSystem.EpsgCrsId)
46+
da.setSourceCrs(crs)
47+
self.assertEqual(da.sourceCrsId(), crs.srsid())
48+
3349
def testMeasureLine(self):
3450
# +-+
3551
# | |
@@ -137,6 +153,62 @@ def testWillUseEllipsoid(self):
137153
da.setEllipsoidalMode(False)
138154
self.assertFalse(da.willUseEllipsoid())
139155

156+
def testLengthMeasureAndUnits(self):
157+
"""Test a variety of length measurements in different CRS and ellipsoid modes, to check that the
158+
calculated lengths and units are always consistent
159+
"""
160+
161+
da = QgsDistanceArea()
162+
da.setSourceCrs(3452)
163+
da.setEllipsoidalMode(False)
164+
da.setEllipsoid("NONE")
165+
daCRS = QgsCoordinateReferenceSystem()
166+
daCRS.createFromSrsId(da.sourceCrs())
167+
168+
# We check both the measured length AND the units, in case the logic regarding
169+
# ellipsoids and units changes in future
170+
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
171+
units = da.lengthUnits()
172+
173+
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
174+
assert ((abs(distance - 2.23606797) < 0.00000001 and units == QGis.Degrees) or
175+
(abs(distance - 248.52) < 0.01 and units == QGis.Meters))
176+
177+
da.setEllipsoid("WGS84")
178+
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
179+
units = da.lengthUnits()
180+
181+
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
182+
assert ((abs(distance - 2.23606797) < 0.00000001 and units == QGis.Degrees) or
183+
(abs(distance - 248.52) < 0.01 and units == QGis.Meters))
184+
185+
da.setEllipsoidalMode(True)
186+
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
187+
units = da.lengthUnits()
188+
189+
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
190+
# should always be in Meters
191+
self.assertAlmostEqual(distance, 247555.57, delta=0.01)
192+
self.assertEqual(units, QGis.Meters)
193+
194+
# now try with a source CRS which is in feet
195+
da.setSourceCrs(27469)
196+
da.setEllipsoidalMode(False)
197+
# measurement should be in feet
198+
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
199+
units = da.lengthUnits()
200+
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
201+
self.assertAlmostEqual(distance, 2.23606797, delta=0.000001)
202+
self.assertEqual(units, QGis.Feet)
203+
204+
da.setEllipsoidalMode(True)
205+
# now should be in Meters again
206+
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
207+
units = da.lengthUnits()
208+
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
209+
self.assertAlmostEqual(distance, 0.67953772, delta=0.000001)
210+
self.assertEqual(units, QGis.Meters)
211+
140212

141213
if __name__ == '__main__':
142214
unittest.main()

0 commit comments

Comments
 (0)