Skip to content

Commit

Permalink
Improve QgsDistanceArea API to make it easier to determine units
Browse files Browse the repository at this point in the history
for returned distances, add unit tests
  • Loading branch information
nyalldawson committed Feb 5, 2016
1 parent cb7d0fb commit 99cef42
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 28 deletions.
55 changes: 48 additions & 7 deletions python/core/qgsdistancearea.sip
Expand Up @@ -50,7 +50,15 @@ class QgsDistanceArea
void setSourceAuthId( const QString& authid );

//! returns source spatial reference system
long sourceCrs() const;
//! @deprecated use sourceCrsId() instead
long sourceCrs() const /Deprecated/;

/** Returns the QgsCoordinateReferenceSystem::srsid() for the CRS used during calculations.
* @see setSourceCrs()
* @note added in QGIS 2.14
*/
long sourceCrsId() const;

//! What sort of coordinate system is being used?
bool geographic() const;

Expand Down Expand Up @@ -105,22 +113,46 @@ class QgsDistanceArea

/** Measures the length of a geometry.
* @param geometry geometry to measure
* @returns length of geometry. For geometry collections, non curve geometries will be ignored
* @returns length of geometry. For geometry collections, non curve geometries will be ignored. The units for the
* returned distance can be retrieved by calling lengthUnits().
* @note added in QGIS 2.12
* @see lengthUnits()
* @see measureArea()
* @see measurePerimeter()
*/
double measureLength( const QgsGeometry* geometry ) const;

//! measures perimeter of polygon
double measurePerimeter( const QgsGeometry* geometry ) const;
/** Measures the perimeter of a polygon geometry.
* @param geometry geometry to measure
* @returns perimeter of geometry. For geometry collections, any non-polygon geometries will be ignored. The units for the
* returned perimeter can be retrieved by calling lengthUnits().
* @note added in QGIS 2.12
* @see lengthUnits()
* @see measureArea()
* @see measurePerimeter()
*/
double measurePerimeter( const QgsGeometry *geometry ) const;

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

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

/** Returns the units of distance for length calculations made by this object.
* @note added in QGIS 2.14
*/
QGis::UnitType lengthUnits() const;

//! measures polygon area
double measurePolygon( const QList<QgsPoint>& points ) const;

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

/** Takes a length measurement calculated by this QgsDistanceArea object and converts it to a
* different distance unit.
* @param length length value calculated by this class to convert. It is assumed that the length
* was calculated by this class, ie that its unit of length is equal lengthUnits().
* @param toUnits distance unit to convert measurement to
* @returns converted distance
*/
double convertLengthMeasurement( double length, QGis::UnitType toUnits ) const;

protected:
//! measures line distance, line points are extracted from WKB
// @note available in python bindings
// const unsigned char* measureLine( const unsigned char* feature, double* area, bool hasZptr = false ) const;
//! measures polygon area and perimeter, vertices are extracted from WKB
// @note available in python bindings
// @note not available in python bindings
// const unsigned char* measurePolygon( const unsigned char* feature, double* area, double* perimeter, bool hasZptr = false ) const;

/**
Expand Down
23 changes: 11 additions & 12 deletions src/app/qgsmeasuredialog.cpp
Expand Up @@ -141,9 +141,7 @@ void QgsMeasureDialog::mouseMove( QgsPoint &point )

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

QGis::UnitType displayUnits;
// Meters or feet?
convertMeasurement( d, displayUnits, false );
d = convertLength( d, mDisplayUnits );

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

QGis::UnitType displayUnits;
// Meters or feet?
convertMeasurement( d, displayUnits, false );
d = convertLength( d, mDisplayUnits );

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

QGis::UnitType newDisplayUnits;
convertMeasurement( distance, newDisplayUnits, false );
return QgsDistanceArea::textUnit( distance, mDecimalPlaces, newDisplayUnits, false, baseUnit );
distance = convertLength( distance, mDisplayUnits );
return QgsDistanceArea::textUnit( distance, mDecimalPlaces, mDisplayUnits, false, baseUnit );
}

QString QgsMeasureDialog::formatArea( double area )
Expand Down Expand Up @@ -329,9 +324,8 @@ void QgsMeasureDialog::updateUi()
p2 = *it;
if ( !b )
{
double d = mDa.measureLine( p1, p2 );
QGis::UnitType dummyUnits;
convertMeasurement( d, dummyUnits, false );
double d = mDa.measureLine( p1, p2 );
d = convertLength( d, mDisplayUnits );

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

double QgsMeasureDialog::convertLength( double length, QGis::UnitType toUnit )
{
return mDa.convertLengthMeasurement( length, toUnit );
}


void QgsMeasureDialog::reject()
{
Expand Down
2 changes: 2 additions & 0 deletions src/app/qgsmeasuredialog.h
Expand Up @@ -85,6 +85,8 @@ class APP_EXPORT QgsMeasureDialog : public QDialog, private Ui::QgsMeasureBase
//! Converts the measurement, depending on settings in options and current transformation
void convertMeasurement( double &measure, QGis::UnitType &u, bool isArea );

double convertLength( double length, QGis::UnitType toUnit );

double mTotal;

//! indicates whether we're measuring distances or areas
Expand Down
18 changes: 18 additions & 0 deletions src/core/qgsdistancearea.cpp
Expand Up @@ -534,6 +534,10 @@ double QgsDistanceArea::measureLine( const QgsPoint& p1, const QgsPoint& p2, QGi
return result;
}

QGis::UnitType QgsDistanceArea::lengthUnits() const
{
return willUseEllipsoid() ? QGis::Meters : mCoordTransform->sourceCrs().mapUnits();
}

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

double QgsDistanceArea::convertLengthMeasurement( double length, QGis::UnitType toUnits ) const
{
// get the conversion factor between the specified units
QGis::UnitType measureUnits = lengthUnits();
double factorUnits = QgsUnitTypes::fromUnitToUnitFactor( measureUnits, toUnits );

double result = length * factorUnits;
QgsDebugMsg( QString( "Converted length of %1 %2 to %3 %4" ).arg( length )
.arg( QgsUnitTypes::toString( measureUnits ) )
.arg( result )
.arg( QgsUnitTypes::toString( toUnits ) ) );
return result;
}

53 changes: 46 additions & 7 deletions src/core/qgsdistancearea.h
Expand Up @@ -83,7 +83,16 @@ class CORE_EXPORT QgsDistanceArea
void setSourceAuthId( const QString& authid );

//! returns source spatial reference system
long sourceCrs() const { return mCoordTransform->sourceCrs().srsid(); }
//! @deprecated use sourceCrsId() instead
// TODO QGIS 3.0 - make sourceCrs() return QgsCoordinateReferenceSystem
Q_DECL_DEPRECATED long sourceCrs() const { return mCoordTransform->sourceCrs().srsid(); }

/** Returns the QgsCoordinateReferenceSystem::srsid() for the CRS used during calculations.
* @see setSourceCrs()
* @note added in QGIS 2.14
*/
long sourceCrsId() const { return mCoordTransform->sourceCrs().srsid(); }

//! What sort of coordinate system is being used?
bool geographic() const { return mCoordTransform->sourceCrs().geographicFlag(); }

Expand Down Expand Up @@ -138,23 +147,38 @@ class CORE_EXPORT QgsDistanceArea

/** Measures the length of a geometry.
* @param geometry geometry to measure
* @returns length of geometry. For geometry collections, non curve geometries will be ignored
* @returns length of geometry. For geometry collections, non curve geometries will be ignored. The units for the
* returned distance can be retrieved by calling lengthUnits().
* @note added in QGIS 2.12
* @see lengthUnits()
* @see measureArea()
* @see measurePerimeter()
*/
double measureLength( const QgsGeometry* geometry ) const;

//! measures perimeter of polygon
/** Measures the perimeter of a polygon geometry.
* @param geometry geometry to measure
* @returns perimeter of geometry. For geometry collections, any non-polygon geometries will be ignored. The units for the
* returned perimeter can be retrieved by calling lengthUnits().
* @note added in QGIS 2.12
* @see lengthUnits()
* @see measureArea()
* @see measurePerimeter()
*/
double measurePerimeter( const QgsGeometry *geometry ) const;

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

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

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

/** Returns the units of distance for length calculations made by this object.
* @note added in QGIS 2.14
*/
QGis::UnitType lengthUnits() const;

//! measures polygon area
double measurePolygon( const QList<QgsPoint>& points ) const;

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

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

/** Takes a length measurement calculated by this QgsDistanceArea object and converts it to a
* different distance unit.
* @param length length value calculated by this class to convert. It is assumed that the length
* was calculated by this class, ie that its unit of length is equal lengthUnits().
* @param toUnits distance unit to convert measurement to
* @returns converted distance
*/
double convertLengthMeasurement( double length, QGis::UnitType toUnits ) const;

protected:
//! measures polygon area and perimeter, vertices are extracted from WKB
// @note available in python bindings
// @note not available in python bindings
const unsigned char* measurePolygon( const unsigned char* feature, double* area, double* perimeter, bool hasZptr = false ) const;

/**
Expand Down
2 changes: 1 addition & 1 deletion src/gui/qgsmaptoolidentify.cpp
Expand Up @@ -625,7 +625,7 @@ void QgsMapToolIdentify::convertMeasurement( QgsDistanceArea &calc, double &meas
QGis::UnitType myUnits = mCanvas->mapUnits();

calc.convertMeasurement( measure, myUnits, displayUnits(), isArea );
u = myUnits;
u = displayUnits();
}

QGis::UnitType QgsMapToolIdentify::displayUnits()
Expand Down
2 changes: 2 additions & 0 deletions tests/src/core/testqgsdistancearea.cpp
Expand Up @@ -162,7 +162,9 @@ void TestQgsDistanceArea::unit_conversions()
//outputUnit = QGis::Meters;

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

// The print a text unit. This is i18n, so we should ignore the unit
Expand Down
74 changes: 73 additions & 1 deletion tests/src/python/test_qgsdistancearea.py
Expand Up @@ -16,7 +16,10 @@

from qgis.core import (QgsGeometry,
QgsPoint,
QgsDistanceArea
QgsDistanceArea,
QgsCoordinateReferenceSystem,
QGis,
QgsUnitTypes
)

from qgis.testing import (start_app,
Expand All @@ -30,6 +33,19 @@

class TestQgsDistanceArea(unittest.TestCase):

def testCrs(self):
# test setting/getting the source CRS
da = QgsDistanceArea()

# try setting using a crs id
da.setSourceCrs(3452)
self.assertEqual(da.sourceCrsId(), 3452)

# try setting using a CRS object
crs = QgsCoordinateReferenceSystem(3111, QgsCoordinateReferenceSystem.EpsgCrsId)
da.setSourceCrs(crs)
self.assertEqual(da.sourceCrsId(), crs.srsid())

def testMeasureLine(self):
# +-+
# | |
Expand Down Expand Up @@ -137,6 +153,62 @@ def testWillUseEllipsoid(self):
da.setEllipsoidalMode(False)
self.assertFalse(da.willUseEllipsoid())

def testLengthMeasureAndUnits(self):
"""Test a variety of length measurements in different CRS and ellipsoid modes, to check that the
calculated lengths and units are always consistent
"""

da = QgsDistanceArea()
da.setSourceCrs(3452)
da.setEllipsoidalMode(False)
da.setEllipsoid("NONE")
daCRS = QgsCoordinateReferenceSystem()
daCRS.createFromSrsId(da.sourceCrs())

# We check both the measured length AND the units, in case the logic regarding
# ellipsoids and units changes in future
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
units = da.lengthUnits()

print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
assert ((abs(distance - 2.23606797) < 0.00000001 and units == QGis.Degrees) or
(abs(distance - 248.52) < 0.01 and units == QGis.Meters))

da.setEllipsoid("WGS84")
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
units = da.lengthUnits()

print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
assert ((abs(distance - 2.23606797) < 0.00000001 and units == QGis.Degrees) or
(abs(distance - 248.52) < 0.01 and units == QGis.Meters))

da.setEllipsoidalMode(True)
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
units = da.lengthUnits()

print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
# should always be in Meters
self.assertAlmostEqual(distance, 247555.57, delta=0.01)
self.assertEqual(units, QGis.Meters)

# now try with a source CRS which is in feet
da.setSourceCrs(27469)
da.setEllipsoidalMode(False)
# measurement should be in feet
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
units = da.lengthUnits()
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
self.assertAlmostEqual(distance, 2.23606797, delta=0.000001)
self.assertEqual(units, QGis.Feet)

da.setEllipsoidalMode(True)
# now should be in Meters again
distance = da.measureLine(QgsPoint(1, 1), QgsPoint(2, 3))
units = da.lengthUnits()
print "measured {} in {}".format(distance, QgsUnitTypes.toString(units))
self.assertAlmostEqual(distance, 0.67953772, delta=0.000001)
self.assertEqual(units, QGis.Meters)


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

0 comments on commit 99cef42

Please sign in to comment.