From ddbdcf8ab1196e859642b1083e43a1bcd2ae2e14 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 14 Feb 2016 21:04:41 +1100 Subject: [PATCH] Fix project unit confusion (pt 2): add project distance unit setting Adds a new option in project properties to set the units used for distance measurements. This setting defaults to the units set in QGIS options, but can then be overridden for specific projects. The setting is respected for length and perimeter calculations in: - Attribute table field update bar - Field calculator calculations - Identify tool derived length and perimeter values - Default unit shown in measure dialog Also adds unit tests to ensure that length and perimeter calculated by attribute table update bar, field calculator and identify tool are consistent wrt ellipsoidal calculations and distance units. (refs #13209, #12939, #2402, #4857, #4252) --- python/core/qgsproject.sip | 5 + python/gui/qgsmaptoolidentify.sip | 13 ++ src/app/qgsattributetabledialog.cpp | 2 + src/app/qgsattributetabledialog.h | 2 + src/app/qgsfieldcalculator.cpp | 1 + src/app/qgsfieldcalculator.h | 2 + src/app/qgsmaptoolidentifyaction.cpp | 5 + src/app/qgsmaptoolidentifyaction.h | 1 + src/app/qgsmeasuredialog.cpp | 6 +- src/app/qgsprojectproperties.cpp | 19 +- src/core/qgsproject.cpp | 19 ++ src/core/qgsproject.h | 5 + src/gui/qgsmaptoolidentify.cpp | 28 ++- src/gui/qgsmaptoolidentify.h | 10 + src/ui/qgsprojectpropertiesbase.ui | 37 ++-- tests/src/app/CMakeLists.txt | 31 ++- tests/src/app/testqgsattributetable.cpp | 119 +++++++++++ tests/src/app/testqgsfieldcalculator.cpp | 130 ++++++++++++ .../src/app/testqgsmaptoolidentifyaction.cpp | 188 ++++++++++++++++++ tests/src/core/testqgsproject.cpp | 29 +++ 20 files changed, 611 insertions(+), 41 deletions(-) create mode 100644 tests/src/app/testqgsattributetable.cpp create mode 100644 tests/src/app/testqgsfieldcalculator.cpp create mode 100644 tests/src/app/testqgsmaptoolidentifyaction.cpp diff --git a/python/core/qgsproject.sip b/python/core/qgsproject.sip index 0840715a117..46e209a2491 100644 --- a/python/core/qgsproject.sip +++ b/python/core/qgsproject.sip @@ -248,6 +248,11 @@ class QgsProject : QObject /** Convenience function to query topological editing status */ bool topologicalEditing() const; + /** Convenience function to query default distance measurement units for project. + * @note added in QGIS 2.14 + */ + QGis::UnitType distanceUnits() const; + /** Return project's home path @return home path of project (or QString::null if not set) */ QString homePath() const; diff --git a/python/gui/qgsmaptoolidentify.sip b/python/gui/qgsmaptoolidentify.sip index 13df5069845..d5b63900913 100644 --- a/python/gui/qgsmaptoolidentify.sip +++ b/python/gui/qgsmaptoolidentify.sip @@ -107,4 +107,17 @@ class QgsMapToolIdentify : QgsMapTool bool identifyRasterLayer( QList *results, QgsRasterLayer *layer, QgsPoint point, const QgsRectangle& viewExtent, double mapUnitsPerPixel ); bool identifyVectorLayer( QList *results, QgsVectorLayer *layer, const QgsPoint& point ); + + private: + + //! Private helper + virtual void convertMeasurement( QgsDistanceArea &calc, double &measure, QGis::UnitType &u, bool isArea ); + + /** Transforms the measurements of derived attributes in the desired units*/ + virtual QGis::UnitType displayUnits(); + + /** Desired units for distance display. + * @note added in QGIS 2.14 + */ + virtual QGis::UnitType displayDistanceUnits(); }; diff --git a/src/app/qgsattributetabledialog.cpp b/src/app/qgsattributetabledialog.cpp index ea53b7075bc..ce9f8fc21a5 100644 --- a/src/app/qgsattributetabledialog.cpp +++ b/src/app/qgsattributetabledialog.cpp @@ -366,6 +366,7 @@ void QgsAttributeTableDialog::runFieldCalculation( QgsVectorLayer* layer, const QgsExpression exp( expression ); exp.setGeomCalculator( *myDa ); + exp.setDistanceUnits( QgsProject::instance()->distanceUnits() ); bool useGeometry = exp.needsGeometry(); QgsFeatureRequest request( mMainView->masterModel()->request() ); @@ -816,6 +817,7 @@ void QgsAttributeTableDialog::setFilterExpression( const QString& filterString ) QApplication::setOverrideCursor( Qt::WaitCursor ); filterExpression.setGeomCalculator( myDa ); + filterExpression.setDistanceUnits( QgsProject::instance()->distanceUnits() ); QgsFeatureRequest request( mMainView->masterModel()->request() ); request.setSubsetOfAttributes( filterExpression.referencedColumns(), mLayer->fields() ); if ( !fetchGeom ) diff --git a/src/app/qgsattributetabledialog.h b/src/app/qgsattributetabledialog.h index a4794ac98df..1dd1cf786f2 100644 --- a/src/app/qgsattributetabledialog.h +++ b/src/app/qgsattributetabledialog.h @@ -223,6 +223,8 @@ class APP_EXPORT QgsAttributeTableDialog : public QDialog, private Ui::QgsAttrib QgsRubberBand* mRubberBand; QgsSearchWidgetWrapper* mCurrentSearchWidgetWrapper; + + friend class TestQgsAttributeTable; }; diff --git a/src/app/qgsfieldcalculator.cpp b/src/app/qgsfieldcalculator.cpp index 00c7a009ff4..99cdc36dc5a 100644 --- a/src/app/qgsfieldcalculator.cpp +++ b/src/app/qgsfieldcalculator.cpp @@ -164,6 +164,7 @@ void QgsFieldCalculator::accept() QString calcString = builder->expressionText(); QgsExpression exp( calcString ); exp.setGeomCalculator( myDa ); + exp.setDistanceUnits( QgsProject::instance()->distanceUnits() ); QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() diff --git a/src/app/qgsfieldcalculator.h b/src/app/qgsfieldcalculator.h index 1a92fd653d8..921371dee96 100644 --- a/src/app/qgsfieldcalculator.h +++ b/src/app/qgsfieldcalculator.h @@ -71,6 +71,8 @@ class APP_EXPORT QgsFieldCalculator: public QDialog, private Ui::QgsFieldCalcula /** Idx of changed attribute*/ int mAttributeId; + + friend class TestQgsFieldCalculator; }; #endif // QGSFIELDCALCULATOR_H diff --git a/src/app/qgsmaptoolidentifyaction.cpp b/src/app/qgsmaptoolidentifyaction.cpp index 81497c69688..cdd47060e10 100644 --- a/src/app/qgsmaptoolidentifyaction.cpp +++ b/src/app/qgsmaptoolidentifyaction.cpp @@ -193,6 +193,11 @@ QGis::UnitType QgsMapToolIdentifyAction::displayUnits() return ok ? unit : QGis::Meters; } +QGis::UnitType QgsMapToolIdentifyAction::displayDistanceUnits() +{ + return QgsProject::instance()->distanceUnits(); +} + void QgsMapToolIdentifyAction::handleCopyToClipboard( QgsFeatureStore & featureStore ) { QgsDebugMsg( QString( "features count = %1" ).arg( featureStore.features().size() ) ); diff --git a/src/app/qgsmaptoolidentifyaction.h b/src/app/qgsmaptoolidentifyaction.h index cf84f41b802..3e05ca3b7de 100644 --- a/src/app/qgsmaptoolidentifyaction.h +++ b/src/app/qgsmaptoolidentifyaction.h @@ -81,6 +81,7 @@ class APP_EXPORT QgsMapToolIdentifyAction : public QgsMapToolIdentify QgsIdentifyResultsDialog *resultsDialog(); virtual QGis::UnitType displayUnits() override; + virtual QGis::UnitType displayDistanceUnits() override; }; diff --git a/src/app/qgsmeasuredialog.cpp b/src/app/qgsmeasuredialog.cpp index dd94212ca9d..18b8dcb8364 100644 --- a/src/app/qgsmeasuredialog.cpp +++ b/src/app/qgsmeasuredialog.cpp @@ -54,9 +54,7 @@ QgsMeasureDialog::QgsMeasureDialog( QgsMeasureTool* tool, Qt::WindowFlags f ) mUnitsCombo->addItem( QgsUnitTypes::toString( QGis::Feet ), QGis::Feet ); mUnitsCombo->addItem( QgsUnitTypes::toString( QGis::Degrees ), QGis::Degrees ); mUnitsCombo->addItem( QgsUnitTypes::toString( QGis::NauticalMiles ), QGis::NauticalMiles ); - QSettings settings; - QString units = settings.value( "/qgis/measure/displayunits", QgsUnitTypes::encodeUnit( QGis::Meters ) ).toString(); - mUnitsCombo->setCurrentIndex( mUnitsCombo->findData( QgsUnitTypes::decodeDistanceUnit( units ) ) ); + mUnitsCombo->setCurrentIndex( mUnitsCombo->findData( QgsProject::instance()->distanceUnits() ) ); updateSettings(); @@ -78,7 +76,7 @@ void QgsMeasureDialog::updateSettings() mDecimalPlaces = settings.value( "/qgis/measure/decimalplaces", "3" ).toInt(); mCanvasUnits = mTool->canvas()->mapUnits(); // Configure QgsDistanceArea - mDisplayUnits = static_cast< QGis::UnitType >( mUnitsCombo->itemData( mUnitsCombo->currentIndex() ).toInt() ); + mDisplayUnits = QgsProject::instance()->distanceUnits(); mDa.setSourceCrs( mTool->canvas()->mapSettings().destinationCrs().srsid() ); mDa.setEllipsoid( QgsProject::instance()->readEntry( "Measure", "/Ellipsoid", GEO_NONE ) ); // Only use ellipsoidal calculation when project wide transformation is enabled. diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index b051143e24f..7b2595ff12e 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -54,6 +54,7 @@ #include "qgslayertreegroup.h" #include "qgslayertreelayer.h" #include "qgslayertreemodel.h" +#include "qgsunittypes.h" #include "qgsmessagelog.h" @@ -84,6 +85,12 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas* mapCanvas, QWidget *pa mCoordinateDisplayComboBox->addItem( tr( "Degrees, minutes" ), DegreesMinutes ); mCoordinateDisplayComboBox->addItem( tr( "Degrees, minutes, seconds" ), DegreesMinutesSeconds ); + mDistanceUnitsCombo->addItem( tr( "Meters" ), QGis::Meters ); + mDistanceUnitsCombo->addItem( tr( "Feet" ), QGis::Feet ); + mDistanceUnitsCombo->addItem( tr( "Nautical miles" ), QGis::NauticalMiles ); + mDistanceUnitsCombo->addItem( tr( "Degrees" ), QGis::Degrees ); + mDistanceUnitsCombo->addItem( tr( "Map units" ), QGis::UnknownUnit ); + connect( buttonBox->button( QDialogButtonBox::Apply ), SIGNAL( clicked() ), this, SLOT( apply() ) ); connect( this, SIGNAL( accepted() ), this, SLOT( apply() ) ); connect( projectionSelector, SIGNAL( sridSelected( QString ) ), this, SLOT( srIdUpdated() ) ); @@ -157,6 +164,8 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas* mapCanvas, QWidget *pa else mCoordinateDisplayComboBox->setCurrentIndex( mCoordinateDisplayComboBox->findData( DecimalDegrees ) ); + mDistanceUnitsCombo->setCurrentIndex( mDistanceUnitsCombo->findData( QgsProject::instance()->distanceUnits() ) ); + //get the color selections and set the button color accordingly int myRedInt = QgsProject::instance()->readNumEntry( "Gui", "/SelectionColorRedPart", 255 ); int myGreenInt = QgsProject::instance()->readNumEntry( "Gui", "/SelectionColorGreenPart", 255 ); @@ -779,6 +788,9 @@ void QgsProjectProperties::apply() // Announce that we may have a new display precision setting emit displayPrecisionChanged(); + QGis::UnitType distanceUnits = static_cast< QGis::UnitType >( mDistanceUnitsCombo->itemData( mDistanceUnitsCombo->currentIndex() ).toInt() ); + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( distanceUnits ) ); + QgsProject::instance()->writeEntry( "Paths", "/Absolute", cbxAbsolutePath->currentIndex() == 0 ); if ( mEllipsoidList.at( mEllipsoidIndex ).acronym.startsWith( "PARAMETER" ) ) @@ -1139,7 +1151,6 @@ void QgsProjectProperties::showProjectionsTab() void QgsProjectProperties::on_cbxProjectionEnabled_toggled( bool onFlyEnabled ) { - QString measureOnFlyState = tr( "Measure tool (CRS transformation: %1)" ); if ( !onFlyEnabled ) { // reset projection to default @@ -1162,8 +1173,6 @@ void QgsProjectProperties::on_cbxProjectionEnabled_toggled( bool onFlyEnabled ) // unset ellipsoid mEllipsoidIndex = 0; - - btnGrpMeasureEllipsoid->setTitle( measureOnFlyState.arg( tr( "OFF" ) ) ); } else { @@ -1172,8 +1181,6 @@ void QgsProjectProperties::on_cbxProjectionEnabled_toggled( bool onFlyEnabled ) mLayerSrsId = projectionSelector->selectedCrsId(); } projectionSelector->setSelectedCrsId( mProjectSrsId ); - - btnGrpMeasureEllipsoid->setTitle( measureOnFlyState.arg( tr( "ON" ) ) ); } srIdUpdated(); @@ -1230,7 +1237,7 @@ void QgsProjectProperties::updateGuiForMapUnits( QGis::UnitType units ) else { //make sure map units option is shown in coordinate display combo - QString mapUnitString = tr( "Map units (%1)" ).arg( QGis::tr( units ) ); + QString mapUnitString = tr( "Map units (%1)" ).arg( QgsUnitTypes::toString( units ) ); if ( idx < 0 ) { mCoordinateDisplayComboBox->insertItem( 0, mapUnitString, MapUnits ); diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index 8818f6dc18d..6995736a552 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -36,6 +36,7 @@ #include "qgsvectorlayer.h" #include "qgsvisibilitypresetcollection.h" #include "qgslayerdefinition.h" +#include "qgsunittypes.h" #include #include @@ -45,6 +46,7 @@ #include #include #include +#include #ifdef Q_OS_UNIX #include @@ -452,6 +454,10 @@ void QgsProject::clear() writeEntry( "PositionPrecision", "/DecimalPlaces", 2 ); writeEntry( "Paths", "/Absolute", false ); + //copy default distance units to project + QSettings s; + writeEntry( "Measurement", "/DistanceUnits", s.value( "/qgis/measure/displayunits" ).toString() ); + setDirty( false ); } @@ -2058,6 +2064,19 @@ bool QgsProject::topologicalEditing() const return ( QgsProject::instance()->readNumEntry( "Digitizing", "/TopologicalEditing", 0 ) > 0 ); } +QGis::UnitType QgsProject::distanceUnits() const +{ + QString distanceUnitString = QgsProject::instance()->readEntry( "Measurement", "/DistanceUnits", QString() ); + if ( !distanceUnitString.isEmpty() ) + return QgsUnitTypes::decodeDistanceUnit( distanceUnitString ); + + //fallback to QGIS default measurement unit + QSettings s; + bool ok = false; + QGis::UnitType type = QgsUnitTypes::decodeDistanceUnit( s.value( "/qgis/measure/displayunits" ).toString(), &ok ); + return ok ? type : QGis::Meters; +} + void QgsProjectBadLayerDefaultHandler::handleBadLayers( const QList& /*layers*/, const QDomDocument& /*projectDom*/ ) { // just ignore any bad layers diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h index 9b01ff892b9..3b8897815a8 100644 --- a/src/core/qgsproject.h +++ b/src/core/qgsproject.h @@ -293,6 +293,11 @@ class CORE_EXPORT QgsProject : public QObject /** Convenience function to query topological editing status */ bool topologicalEditing() const; + /** Convenience function to query default distance measurement units for project. + * @note added in QGIS 2.14 + */ + QGis::UnitType distanceUnits() const; + /** Return project's home path @return home path of project (or QString::null if not set) */ QString homePath() const; diff --git a/src/gui/qgsmaptoolidentify.cpp b/src/gui/qgsmaptoolidentify.cpp index 3a128d54405..dc4b62bf29c 100644 --- a/src/gui/qgsmaptoolidentify.cpp +++ b/src/gui/qgsmaptoolidentify.cpp @@ -369,9 +369,8 @@ QMap< QString, QString > QgsMapToolIdentify::featureDerivedAttributes( QgsFeatur if ( geometryType == QGis::Line ) { double dist = calc.measureLength( feature->constGeometry() ); - QGis::UnitType myDisplayUnits; - convertMeasurement( calc, dist, myDisplayUnits, false ); - QString str = calc.textUnit( dist, 3, myDisplayUnits, false ); // dist and myDisplayUnits are out params + dist = calc.convertLengthMeasurement( dist, displayDistanceUnits() ); + QString str = formatDistance( dist ); derivedAttributes.insert( tr( "Length" ), str ); const QgsCurveV2* curve = dynamic_cast< const QgsCurveV2* >( feature->constGeometry()->geometry() ); @@ -399,13 +398,15 @@ QMap< QString, QString > QgsMapToolIdentify::featureDerivedAttributes( QgsFeatur else if ( geometryType == QGis::Polygon ) { double area = calc.measureArea( feature->constGeometry() ); - double perimeter = calc.measurePerimeter( feature->constGeometry() ); + QGis::UnitType myDisplayUnits; convertMeasurement( calc, area, myDisplayUnits, true ); // area and myDisplayUnits are out params QString str = calc.textUnit( area, 3, myDisplayUnits, true ); derivedAttributes.insert( tr( "Area" ), str ); - convertMeasurement( calc, perimeter, myDisplayUnits, false ); // perimeter and myDisplayUnits are out params - str = calc.textUnit( perimeter, 3, myDisplayUnits, false ); + + double perimeter = calc.measurePerimeter( feature->constGeometry() ); + perimeter = calc.convertLengthMeasurement( perimeter, displayDistanceUnits() ); + str = formatDistance( perimeter ); derivedAttributes.insert( tr( "Perimeter" ), str ); str = QLocale::system().toString( feature->constGeometry()->geometry()->nCoordinates() ); @@ -640,7 +641,7 @@ bool QgsMapToolIdentify::identifyRasterLayer( QList *results, Qg void QgsMapToolIdentify::convertMeasurement( QgsDistanceArea &calc, double &measure, QGis::UnitType &u, bool isArea ) { - // Helper for converting between meters and feet + // Helper for converting between units // The parameter &u is out only... // Get the canvas units @@ -655,6 +656,19 @@ QGis::UnitType QgsMapToolIdentify::displayUnits() return mCanvas->mapUnits(); } +QGis::UnitType QgsMapToolIdentify::displayDistanceUnits() +{ + return mCanvas->mapUnits(); +} + +QString QgsMapToolIdentify::formatDistance( double distance ) +{ + QSettings settings; + bool baseUnit = settings.value( "/qgis/measure/keepbaseunit", false ).toBool(); + + return QgsDistanceArea::textUnit( distance, 3, displayDistanceUnits(), false, baseUnit ); +} + void QgsMapToolIdentify::formatChanged( QgsRasterLayer *layer ) { QgsDebugMsg( "Entered" ); diff --git a/src/gui/qgsmaptoolidentify.h b/src/gui/qgsmaptoolidentify.h index 20ca5be9674..543b247cdaa 100644 --- a/src/gui/qgsmaptoolidentify.h +++ b/src/gui/qgsmaptoolidentify.h @@ -163,6 +163,16 @@ class GUI_EXPORT QgsMapToolIdentify : public QgsMapTool /** Transforms the measurements of derived attributes in the desired units*/ virtual QGis::UnitType displayUnits(); + /** Desired units for distance display. + * @note added in QGIS 2.14 + */ + virtual QGis::UnitType displayDistanceUnits(); + + /** Format a distance into a suitable string for display to the user + * @note added in QGIS 2.14 + */ + QString formatDistance( double distance ); + QMap< QString, QString > featureDerivedAttributes( QgsFeature *feature, QgsMapLayer *layer, const QgsPoint& layerPoint = QgsPoint() ); /** Adds details of the closest vertex to derived attributes diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index 64be00fdcab..986ddaa74c1 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -471,13 +471,13 @@ - Measure tool + Measurements projgeneral - + Ellipsoid @@ -485,29 +485,39 @@ - + + + + + + + + + + + + + + + + Units for distance measurement + + + + Semi-major - - - - - - - + Semi-minor - - - @@ -2528,6 +2538,7 @@ cmbEllipsoid leSemiMajor leSemiMinor + mDistanceUnitsCombo mCoordinateDisplayComboBox radAutomatic radManual diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index 68484077d40..46a101d7f9b 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -3,17 +3,23 @@ # the UI file won't be wrapped! INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_SOURCE_DIR}/src/core - ${CMAKE_SOURCE_DIR}/src/core/auth - ${CMAKE_SOURCE_DIR}/src/core/composer - ${CMAKE_SOURCE_DIR}/src/core/geometry - ${CMAKE_SOURCE_DIR}/src/core/raster - ${CMAKE_SOURCE_DIR}/src/core/symbology-ng - ${CMAKE_BINARY_DIR}/src/ui - ${CMAKE_SOURCE_DIR}/src/gui - ${CMAKE_SOURCE_DIR}/src/python - ${CMAKE_SOURCE_DIR}/src/app - ${CMAKE_SOURCE_DIR}/src/app/pluginmanager + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core/auth + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core/composer + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core/geometry + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core/raster + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/core/symbology-ng + ${CMAKE_CURRENT_BINARY_DIR}/../../../src/ui + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/editorwidgets + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/editorwidgets/core + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/attributetable + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/symbology-ng + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/gui/raster + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/python + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/app + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/app/pluginmanager + ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/ui ) INCLUDE_DIRECTORIES(SYSTEM ${QT_INCLUDE_DIR} @@ -96,3 +102,6 @@ ENDMACRO (ADD_QGIS_TEST) # Tests: ADD_QGIS_TEST(qgisappclipboard testqgisappclipboard.cpp) +ADD_QGIS_TEST(attributetabletest testqgsattributetable.cpp) +ADD_QGIS_TEST(fieldcalculatortest testqgsfieldcalculator.cpp) +ADD_QGIS_TEST(maptoolidentifyaction testqgsmaptoolidentifyaction.cpp) diff --git a/tests/src/app/testqgsattributetable.cpp b/tests/src/app/testqgsattributetable.cpp new file mode 100644 index 00000000000..e8b0291420d --- /dev/null +++ b/tests/src/app/testqgsattributetable.cpp @@ -0,0 +1,119 @@ +/*************************************************************************** + testqgsattributetable.cpp + ------------------------- + Date : 2016-02-14 + Copyright : (C) 2016 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include +#include "qgisapp.h" +#include "qgsapplication.h" +#include "qgsvectorlayer.h" +#include "qgsfeature.h" +#include "qgsgeometry.h" +#include "qgsvectordataprovider.h" +#include "qgsattributetabledialog.h" +#include "qgsproject.h" +#include "qgsmapcanvas.h" +#include "qgsunittypes.h" + +/** \ingroup UnitTests + * This is a unit test for the attribute table dialog + */ +class TestQgsAttributeTable : public QObject +{ + Q_OBJECT + public: + TestQgsAttributeTable(); + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + void testFieldCalculation(); + + private: + QgisApp * mQgisApp; +}; + +TestQgsAttributeTable::TestQgsAttributeTable() + : mQgisApp( nullptr ) +{ + +} + +//runs before all tests +void TestQgsAttributeTable::initTestCase() +{ + qDebug() << "TestQgisAppClipboard::initTestCase()"; + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + mQgisApp = new QgisApp(); +} + +//runs after all tests +void TestQgsAttributeTable::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsAttributeTable::testFieldCalculation() +{ + //test field calculation + + //create a temporary layer + QScopedPointer< QgsVectorLayer> tempLayer( new QgsVectorLayer( "LineString?crs=epsg:3111&field=pk:int&field=col1:double", "vl", "memory" ) ); + QVERIFY( tempLayer->isValid() ); + QgsFeature f1( tempLayer->dataProvider()->fields(), 1 ); + f1.setAttribute( "pk", 1 ); + f1.setAttribute( "col1", 0.0 ); + QgsPolyline line3111; + line3111 << QgsPoint( 2484588, 2425722 ) << QgsPoint( 2482767, 2398853 ); + f1.setGeometry( QgsGeometry::fromPolyline( line3111 ) ); + tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 ); + + // set project CRS and ellipsoid + QgisApp::instance()->mapCanvas()->setCrsTransformEnabled( true ); + QgsCoordinateReferenceSystem srs( 3111, QgsCoordinateReferenceSystem::EpsgCrsId ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSProj4String", srs.toProj4() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSID", ( int ) srs.srsid() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCrs", srs.authid() ); + QgsProject::instance()->writeEntry( "Measure", "/Ellipsoid", QString( "WGS84" ) ); + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Meters ) ); + + // run length calculation + QScopedPointer< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.data() ) ); + tempLayer->startEditing(); + dlg->runFieldCalculation( tempLayer.data(), "col1", "$length" ); + tempLayer->commitChanges(); + // check result + QgsFeatureIterator fit = tempLayer->dataProvider()->getFeatures(); + QgsFeature f; + QVERIFY( fit.nextFeature( f ) ); + double expected = 26932.156; + QVERIFY( qgsDoubleNear( f.attribute( "col1" ).toDouble(), expected, 0.001 ) ); + + // change project length unit, check calculation respects unit + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Feet ) ); + QScopedPointer< QgsAttributeTableDialog > dlg2( new QgsAttributeTableDialog( tempLayer.data() ) ); + tempLayer->startEditing(); + dlg2->runFieldCalculation( tempLayer.data(), "col1", "$length" ); + tempLayer->commitChanges(); + // check result + fit = tempLayer->dataProvider()->getFeatures(); + QVERIFY( fit.nextFeature( f ) ); + expected = 88360.0918635; + QVERIFY( qgsDoubleNear( f.attribute( "col1" ).toDouble(), expected, 0.001 ) ); +} + +QTEST_MAIN( TestQgsAttributeTable ) +#include "testqgsattributetable.moc" diff --git a/tests/src/app/testqgsfieldcalculator.cpp b/tests/src/app/testqgsfieldcalculator.cpp new file mode 100644 index 00000000000..c84a04cb565 --- /dev/null +++ b/tests/src/app/testqgsfieldcalculator.cpp @@ -0,0 +1,130 @@ +/*************************************************************************** + testqgsfieldcalculator.cpp + -------------------------- + Date : 2016-02-14 + Copyright : (C) 2016 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include +#include "qgisapp.h" +#include "qgsapplication.h" +#include "qgsvectorlayer.h" +#include "qgsfeature.h" +#include "qgsgeometry.h" +#include "qgsvectordataprovider.h" +#include "qgsfieldcalculator.h" +#include "qgsproject.h" +#include "qgsmapcanvas.h" +#include "qgsunittypes.h" + +/** \ingroup UnitTests + * This is a unit test for the field calculator + */ +class TestQgsFieldCalculator : public QObject +{ + Q_OBJECT + public: + TestQgsFieldCalculator(); + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + void testLengthCalculations(); + + private: + QgisApp * mQgisApp; +}; + +TestQgsFieldCalculator::TestQgsFieldCalculator() + : mQgisApp( nullptr ) +{ + +} + +//runs before all tests +void TestQgsFieldCalculator::initTestCase() +{ + qDebug() << "TestQgisAppClipboard::initTestCase()"; + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + mQgisApp = new QgisApp(); +} + +//runs after all tests +void TestQgsFieldCalculator::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsFieldCalculator::testLengthCalculations() +{ + //test length calculation respects ellipsoid and project distance units + + //create a temporary layer + QScopedPointer< QgsVectorLayer> tempLayer( new QgsVectorLayer( "LineString?crs=epsg:3111&field=pk:int&field=col1:double", "vl", "memory" ) ); + QVERIFY( tempLayer->isValid() ); + QgsFeature f1( tempLayer->dataProvider()->fields(), 1 ); + f1.setAttribute( "pk", 1 ); + f1.setAttribute( "col1", 0.0 ); + QgsPolyline line3111; + line3111 << QgsPoint( 2484588, 2425722 ) << QgsPoint( 2482767, 2398853 ); + f1.setGeometry( QgsGeometry::fromPolyline( line3111 ) ); + tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 ); + + // set project CRS and ellipsoid + QgisApp::instance()->mapCanvas()->setCrsTransformEnabled( true ); + QgsCoordinateReferenceSystem srs( 3111, QgsCoordinateReferenceSystem::EpsgCrsId ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSProj4String", srs.toProj4() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSID", ( int ) srs.srsid() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCrs", srs.authid() ); + QgsProject::instance()->writeEntry( "Measure", "/Ellipsoid", QString( "WGS84" ) ); + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Meters ) ); + + // run length calculation + tempLayer->startEditing(); + QScopedPointer< QgsFieldCalculator > calc( new QgsFieldCalculator( tempLayer.data() ) ); + + // this next part is fragile, and may need to be modified if the dialog changes: + calc->mUpdateExistingGroupBox->setChecked( true ); + calc->mExistingFieldComboBox->setCurrentIndex( 1 ); + calc->builder->setExpressionText( "$length" ); + calc->accept(); + + tempLayer->commitChanges(); + + // check result + QgsFeatureIterator fit = tempLayer->dataProvider()->getFeatures(); + QgsFeature f; + QVERIFY( fit.nextFeature( f ) ); + double expected = 26932.156; + QVERIFY( qgsDoubleNear( f.attribute( "col1" ).toDouble(), expected, 0.001 ) ); + + // change project length unit, check calculation respects unit + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Feet ) ); + tempLayer->startEditing(); + QScopedPointer< QgsFieldCalculator > calc2( new QgsFieldCalculator( tempLayer.data() ) ); + calc2->mUpdateExistingGroupBox->setChecked( true ); + calc2->mExistingFieldComboBox->setCurrentIndex( 1 ); + calc2->builder->setExpressionText( "$length" ); + calc2->accept(); + tempLayer->commitChanges(); + // check result + fit = tempLayer->dataProvider()->getFeatures(); + QVERIFY( fit.nextFeature( f ) ); + expected = 88360.0918635; + QVERIFY( qgsDoubleNear( f.attribute( "col1" ).toDouble(), expected, 0.001 ) ); + +} + +QTEST_MAIN( TestQgsFieldCalculator ) +#include "testqgsfieldcalculator.moc" diff --git a/tests/src/app/testqgsmaptoolidentifyaction.cpp b/tests/src/app/testqgsmaptoolidentifyaction.cpp new file mode 100644 index 00000000000..d914a771f93 --- /dev/null +++ b/tests/src/app/testqgsmaptoolidentifyaction.cpp @@ -0,0 +1,188 @@ +/*************************************************************************** + testqgsmaptoolidentifyaction.cpp + -------------------------------- + Date : 2016-02-14 + Copyright : (C) 2016 by Nyall Dawson + Email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include +#include "qgsapplication.h" +#include "qgsvectorlayer.h" +#include "qgsfeature.h" +#include "qgsgeometry.h" +#include "qgsvectordataprovider.h" +#include "qgsproject.h" +#include "qgsmapcanvas.h" +#include "qgsunittypes.h" +#include "qgsmaptoolidentifyaction.h" + +class TestQgsMapToolIdentifyAction : public QObject +{ + Q_OBJECT + public: + TestQgsMapToolIdentifyAction() + : canvas( 0 ) + {} + + private slots: + void initTestCase(); // will be called before the first testfunction is executed. + void cleanupTestCase(); // will be called after the last testfunction was executed. + void init(); // will be called before each testfunction is executed. + void cleanup(); // will be called after every testfunction. + void lengthCalculation(); //test calculation of derived length attributes + void perimeterCalculation(); //test calculation of derived perimeter attribute + + private: + QgsMapCanvas* canvas; +}; + +void TestQgsMapToolIdentifyAction::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + // Set up the QSettings environment + QCoreApplication::setOrganizationName( "QGIS" ); + QCoreApplication::setOrganizationDomain( "qgis.org" ); + QCoreApplication::setApplicationName( "QGIS-TEST" ); + + QgsApplication::showSettings(); +} + +void TestQgsMapToolIdentifyAction::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsMapToolIdentifyAction::init() +{ + canvas = new QgsMapCanvas(); +} + +void TestQgsMapToolIdentifyAction::cleanup() +{ + delete canvas; +} + +void TestQgsMapToolIdentifyAction::lengthCalculation() +{ + QSettings s; + s.setValue( "/qgis/measure/keepbaseunit", true ); + + //create a temporary layer + QScopedPointer< QgsVectorLayer> tempLayer( new QgsVectorLayer( "LineString?crs=epsg:3111&field=pk:int&field=col1:double", "vl", "memory" ) ); + QVERIFY( tempLayer->isValid() ); + QgsFeature f1( tempLayer->dataProvider()->fields(), 1 ); + f1.setAttribute( "pk", 1 ); + f1.setAttribute( "col1", 0.0 ); + QgsPolyline line3111; + line3111 << QgsPoint( 2484588, 2425722 ) << QgsPoint( 2482767, 2398853 ); + f1.setGeometry( QgsGeometry::fromPolyline( line3111 ) ); + tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 ); + + // set project CRS and ellipsoid + QgsCoordinateReferenceSystem srs( 3111, QgsCoordinateReferenceSystem::EpsgCrsId ); + canvas->setCrsTransformEnabled( true ); + canvas->setDestinationCrs( srs ); + canvas->setExtent( f1.geometry()->boundingBox() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSProj4String", srs.toProj4() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSID", ( int ) srs.srsid() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCrs", srs.authid() ); + QgsProject::instance()->writeEntry( "Measure", "/Ellipsoid", QString( "WGS84" ) ); + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Meters ) ); + + QgsPoint mapPoint = canvas->getCoordinateTransform()->transform( 2484588, 2425722 ); + + QScopedPointer< QgsMapToolIdentifyAction > action( new QgsMapToolIdentifyAction( canvas ) ); + QList result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.data() ); + QCOMPARE( result.length(), 1 ); + QString derivedLength = result.at( 0 ).mDerivedAttributes[tr( "Length" )]; + double length = derivedLength.remove( ',' ).split( ' ' ).at( 0 ).toDouble(); + QVERIFY( qgsDoubleNear( length, 26932.2, 0.1 ) ); + + //check that project units are respected + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Feet ) ); + result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.data() ); + QCOMPARE( result.length(), 1 ); + derivedLength = result.at( 0 ).mDerivedAttributes[tr( "Length" )]; + length = derivedLength.remove( ',' ).split( ' ' ).at( 0 ).toDouble(); + QVERIFY( qgsDoubleNear( length, 88360.1, 0.1 ) ); + + //test unchecked "keep base units" setting + s.setValue( "/qgis/measure/keepbaseunit", false ); + result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.data() ); + QCOMPARE( result.length(), 1 ); + derivedLength = result.at( 0 ).mDerivedAttributes[tr( "Length" )]; + length = derivedLength.remove( ',' ).split( ' ' ).at( 0 ).toDouble(); + QVERIFY( qgsDoubleNear( length, 16.735, 0.001 ) ); +} + +void TestQgsMapToolIdentifyAction::perimeterCalculation() +{ + QSettings s; + s.setValue( "/qgis/measure/keepbaseunit", true ); + + //create a temporary layer + QScopedPointer< QgsVectorLayer> tempLayer( new QgsVectorLayer( "Polygon?crs=epsg:3111&field=pk:int&field=col1:double", "vl", "memory" ) ); + QVERIFY( tempLayer->isValid() ); + QgsFeature f1( tempLayer->dataProvider()->fields(), 1 ); + f1.setAttribute( "pk", 1 ); + f1.setAttribute( "col1", 0.0 ); + QgsPolyline polygonRing3111; + polygonRing3111 << QgsPoint( 2484588, 2425722 ) << QgsPoint( 2482767, 2398853 ) << QgsPoint( 2520109, 2397715 ) << QgsPoint( 2520792, 2425494 ) << QgsPoint( 2484588, 2425722 ); + QgsPolygon polygon3111; + polygon3111 << polygonRing3111; + f1.setGeometry( QgsGeometry::fromPolygon( polygon3111 ) ); + tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 ); + + // set project CRS and ellipsoid + QgsCoordinateReferenceSystem srs( 3111, QgsCoordinateReferenceSystem::EpsgCrsId ); + canvas->setCrsTransformEnabled( true ); + canvas->setDestinationCrs( srs ); + canvas->setExtent( f1.geometry()->boundingBox() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSProj4String", srs.toProj4() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCRSID", ( int ) srs.srsid() ); + QgsProject::instance()->writeEntry( "SpatialRefSys", "/ProjectCrs", srs.authid() ); + QgsProject::instance()->writeEntry( "Measure", "/Ellipsoid", QString( "WGS84" ) ); + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Meters ) ); + + QgsPoint mapPoint = canvas->getCoordinateTransform()->transform( 2484588, 2425722 ); + + QScopedPointer< QgsMapToolIdentifyAction > action( new QgsMapToolIdentifyAction( canvas ) ); + QList result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.data() ); + QCOMPARE( result.length(), 1 ); + QString derivedPerimeter = result.at( 0 ).mDerivedAttributes[tr( "Perimeter" )]; + double perimeter = derivedPerimeter.remove( ',' ).split( ' ' ).at( 0 ).toDouble(); + QCOMPARE( perimeter, 128289.074 ); + + //check that project units are respected + QgsProject::instance()->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::Feet ) ); + result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.data() ); + QCOMPARE( result.length(), 1 ); + derivedPerimeter = result.at( 0 ).mDerivedAttributes[tr( "Perimeter" )]; + perimeter = derivedPerimeter.remove( ',' ).split( ' ' ).at( 0 ).toDouble(); + QVERIFY( qgsDoubleNear( perimeter, 420896.0, 0.1 ) ); + + //test unchecked "keep base units" setting + s.setValue( "/qgis/measure/keepbaseunit", false ); + result = action->identify( mapPoint.x(), mapPoint.y(), QList() << tempLayer.data() ); + QCOMPARE( result.length(), 1 ); + derivedPerimeter = result.at( 0 ).mDerivedAttributes[tr( "Perimeter" )]; + perimeter = derivedPerimeter.remove( ',' ).split( ' ' ).at( 0 ).toDouble(); + QVERIFY( qgsDoubleNear( perimeter, 79.715, 0.001 ) ); +} + + +QTEST_MAIN( TestQgsMapToolIdentifyAction ) +#include "testqgsmaptoolidentifyaction.moc" + + + + diff --git a/tests/src/core/testqgsproject.cpp b/tests/src/core/testqgsproject.cpp index 00dd82ffd51..72aba6ec9c0 100644 --- a/tests/src/core/testqgsproject.cpp +++ b/tests/src/core/testqgsproject.cpp @@ -17,6 +17,7 @@ #include #include +#include "qgsunittypes.h" class TestQgsProject : public QObject @@ -29,6 +30,7 @@ class TestQgsProject : public QObject void cleanup();// will be called after every testfunction. void testReadPath(); + void testProjectUnits(); }; void TestQgsProject::init() @@ -43,6 +45,11 @@ void TestQgsProject::cleanup() void TestQgsProject::initTestCase() { // Runs once before any tests are run + + // Set up the QSettings environment + QCoreApplication::setOrganizationName( "QGIS" ); + QCoreApplication::setOrganizationDomain( "qgis.org" ); + QCoreApplication::setApplicationName( "QGIS-TEST" ); } @@ -76,6 +83,28 @@ void TestQgsProject::testReadPath() } +void TestQgsProject::testProjectUnits() +{ + //test setting and retrieving project units + + //first set a default QGIS distance unit + QSettings s; + s.setValue( "/qgis/measure/displayunits", QgsUnitTypes::encodeUnit( QGis::Feet ) ); + + QgsProject* prj = QgsProject::instance(); + // new project should inherit QGIS default distance unit + prj->clear(); + QCOMPARE( prj->distanceUnits(), QGis::Feet ); + + //changing default QGIS unit should not affect existing project + s.setValue( "/qgis/measure/displayunits", QgsUnitTypes::encodeUnit( QGis::NauticalMiles ) ); + QCOMPARE( prj->distanceUnits(), QGis::Feet ); + + //test setting new units for project + prj->writeEntry( "Measurement", "/DistanceUnits", QgsUnitTypes::encodeUnit( QGis::NauticalMiles ) ); + QCOMPARE( prj->distanceUnits(), QGis::NauticalMiles ); +} + QTEST_MAIN( TestQgsProject ) #include "testqgsproject.moc"