diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 2f29ad786..09f9e4099 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -246,7 +246,7 @@ jobs:
-S ../ \
-B ./
- - name: build APK
+ - name: Build APK
env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_HOST: darwin-x86_64
@@ -271,7 +271,7 @@ jobs:
path: ${{ github.workspace }}/merginmaps-${{ env.INPUT_VERSION_CODE }}.apk
name: Mergin Maps ${{ env.INPUT_VERSION_CODE }} APK [v7 + v8a]
- - name: build AAB
+ - name: Build AAB
if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }}
env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
@@ -298,4 +298,3 @@ jobs:
with:
path: ${{ github.workspace }}/merginmaps-${{ env.INPUT_VERSION_CODE }}.aab
name: Mergin Maps ${{ env.INPUT_VERSION_CODE }} AAB
-
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index b96c77c27..cf9201375 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -27,6 +27,7 @@ set(MM_SRCS
maptools/abstractmaptool.cpp
maptools/recordingmaptool.cpp
maptools/splittingmaptool.cpp
+ maptools/measurementmaptool.cpp
ios/iosimagepicker.cpp
ios/iosutils.cpp
position/providers/abstractpositionprovider.cpp
@@ -109,6 +110,7 @@ set(MM_HDRS
maptools/abstractmaptool.h
maptools/recordingmaptool.h
maptools/splittingmaptool.h
+ maptools/measurementmaptool.h
ios/iosimagepicker.h
ios/iosutils.h
position/providers/abstractpositionprovider.h
diff --git a/app/icons/CloseShape.svg b/app/icons/CloseShape.svg
new file mode 100644
index 000000000..594a3a78e
--- /dev/null
+++ b/app/icons/CloseShape.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/icons/Measure.svg b/app/icons/Measure.svg
new file mode 100644
index 000000000..0666bf957
--- /dev/null
+++ b/app/icons/Measure.svg
@@ -0,0 +1,7 @@
+
diff --git a/app/icons/icons.qrc b/app/icons/icons.qrc
index c3f553a97..3f88e0ec6 100644
--- a/app/icons/icons.qrc
+++ b/app/icons/icons.qrc
@@ -97,5 +97,7 @@
ZoomToProject.svg
StakeOut.svg
Student.svg
+ Measure.svg
+ CloseShape.svg
diff --git a/app/images/NeutralMMSymbol.svg b/app/images/NeutralMMSymbol.svg
new file mode 100644
index 000000000..e31c406a2
--- /dev/null
+++ b/app/images/NeutralMMSymbol.svg
@@ -0,0 +1,23 @@
+
diff --git a/app/images/images.qrc b/app/images/images.qrc
index ac0474d90..b0b772a30 100644
--- a/app/images/images.qrc
+++ b/app/images/images.qrc
@@ -23,6 +23,7 @@
ExternalGpsGreen.svg
NegativeMMSymbol.svg
PositiveMMSymbol.svg
+ NeutralMMSymbol.svg
CloseAccount.svg
Attention.svg
Bubble.svg
diff --git a/app/inpututils.cpp b/app/inpututils.cpp
index 5b40e14c0..218c024e1 100644
--- a/app/inpututils.cpp
+++ b/app/inpututils.cpp
@@ -168,25 +168,48 @@ QString InputUtils::formatNumber( const double number, int precision )
return QString::number( number, 'f', precision );
}
-QString InputUtils::formatDistanceInProjectUnit( const double distanceInMeters, int precision, Qgis::DistanceUnit destUnit )
+QString InputUtils::formatDistanceInProjectUnit( const double distanceInMeters, int precision, QgsProject *project )
{
- Qgis::DistanceUnit distUnit = destUnit;
+ if ( !project )
+ return QString();
+
+ return InputUtils::formatDistanceHelper( distanceInMeters, precision, project->distanceUnits() );
+}
- if ( distUnit == Qgis::DistanceUnit::Unknown )
+QString InputUtils::formatDistanceHelper( const double distanceInMeters, int precision, Qgis::DistanceUnit destUnit )
+{
+ if ( destUnit == Qgis::DistanceUnit::Unknown )
{
- distUnit = QgsProject::instance()->distanceUnits();
+ destUnit = Qgis::DistanceUnit::Meters;
}
- if ( distUnit == Qgis::DistanceUnit::Unknown )
+ const double factor = QgsUnitTypes::fromUnitToUnitFactor( Qgis::DistanceUnit::Meters, destUnit );
+ const double distance = distanceInMeters * factor;
+ const QString abbreviation = QgsUnitTypes::toAbbreviatedString( destUnit );
+
+ return QString( "%L1 %2" ).arg( QString::number( distance, 'f', precision ), abbreviation );
+}
+
+QString InputUtils::formatAreaInProjectUnit( const double areaInSquareMeters, int precision, QgsProject *project )
+{
+ if ( !project )
+ return QString();
+
+ return InputUtils::formatAreaHelper( areaInSquareMeters, precision, project->areaUnits() );
+}
+
+QString InputUtils::formatAreaHelper( const double areaInSquareMeters, int precision, Qgis::AreaUnit destUnit )
+{
+ if ( destUnit == Qgis::AreaUnit::Unknown )
{
- return QString::number( distanceInMeters, 'f', precision );
+ destUnit = Qgis::AreaUnit::SquareMeters;
}
- double factor = QgsUnitTypes::fromUnitToUnitFactor( Qgis::DistanceUnit::Meters, distUnit );
- double distance = distanceInMeters * factor;
- QString abbreviation = QgsUnitTypes::toAbbreviatedString( distUnit );
+ const double factor = QgsUnitTypes::fromUnitToUnitFactor( Qgis::AreaUnit::SquareMeters, destUnit );
+ const double area = areaInSquareMeters * factor;
+ const QString abbreviation = QgsUnitTypes::toAbbreviatedString( destUnit );
- return QString( "%1 %2" ).arg( QString::number( distance, 'f', precision ), abbreviation );
+ return QString( "%L1 %2" ).arg( QString::number( area, 'f', precision ), abbreviation );
}
QString InputUtils::formatDateTimeDiff( const QDateTime &tMin, const QDateTime &tMax )
@@ -2194,3 +2217,8 @@ bool InputUtils::openLink( const QString &homePath, const QString &link )
return true;
}
+
+double InputUtils::pixelDistanceBetween( const QPointF &p1, const QPointF &p2 )
+{
+ return std::hypot( p1.x() - p2.x(), p1.y() - p2.y() );
+}
diff --git a/app/inpututils.h b/app/inpututils.h
index 2ba463900..83be81f39 100644
--- a/app/inpututils.h
+++ b/app/inpututils.h
@@ -75,7 +75,12 @@ class InputUtils: public QObject
Q_INVOKABLE QString getFileName( const QString &filePath );
Q_INVOKABLE QString formatProjectName( const QString &fullProjectName );
Q_INVOKABLE QString formatNumber( const double number, int precision = 1 );
- Q_INVOKABLE QString formatDistanceInProjectUnit( const double distanceInMeters, int precision = 1, Qgis::DistanceUnit destUnit = Qgis::DistanceUnit::Unknown );
+ Q_INVOKABLE QString formatDistanceInProjectUnit( const double distanceInMeters, int precision, QgsProject *project );
+ Q_INVOKABLE QString formatAreaInProjectUnit( const double areaInSquareMeters, int precision, QgsProject *project );
+
+ static QString formatDistanceHelper( const double distanceInMeters, int precision, Qgis::DistanceUnit destUnit );
+ static QString formatAreaHelper( const double areaInSquareMeters, int precision, Qgis::AreaUnit destUnit );
+
Q_INVOKABLE void setExtentToFeature( const FeatureLayerPair &pair, InputMapSettings *mapSettings );
/**
@@ -579,6 +584,11 @@ class InputUtils: public QObject
*/
static QVector qgisProfilerLog();
+ /**
+ * Calculates the Euclidean distance between two pixel points
+ */
+ static double pixelDistanceBetween( const QPointF &p1, const QPointF &p2 );
+
public slots:
void onQgsLogMessageReceived( const QString &message, const QString &tag, Qgis::MessageLevel level );
diff --git a/app/main.cpp b/app/main.cpp
index d0c35cd24..2ff044e83 100644
--- a/app/main.cpp
+++ b/app/main.cpp
@@ -114,6 +114,7 @@
#include "maptools/abstractmaptool.h"
#include "maptools/recordingmaptool.h"
#include "maptools/splittingmaptool.h"
+#include "maptools/measurementmaptool.h"
#include "layer/layertreemodel.h"
#include "layer/layertreemodelpixmapprovider.h"
@@ -348,6 +349,7 @@ void initDeclarative()
qmlRegisterUncreatableType< AbstractMapTool >( "mm", 1, 0, "AbstractMapTool", "Instantiate one of child map tools instead" );
qmlRegisterType< RecordingMapTool >( "mm", 1, 0, "RecordingMapTool" );
qmlRegisterType< SplittingMapTool >( "mm", 1, 0, "SplittingMapTool" );
+ qmlRegisterType< MeasurementMapTool >( "mm", 1, 0, "MeasurementMapTool" );
}
void addQmlImportPath( QQmlEngine &engine )
diff --git a/app/maptools/abstractmaptool.cpp b/app/maptools/abstractmaptool.cpp
index dd8b25636..ee81f1dc0 100644
--- a/app/maptools/abstractmaptool.cpp
+++ b/app/maptools/abstractmaptool.cpp
@@ -26,6 +26,10 @@ void AbstractMapTool::setMapSettings( InputMapSettings *newMapSettings )
{
if ( mMapSettings == newMapSettings )
return;
+
+ emit onAboutToChangeMapSettings();
+
mMapSettings = newMapSettings;
+
emit mapSettingsChanged( mMapSettings );
}
diff --git a/app/maptools/abstractmaptool.h b/app/maptools/abstractmaptool.h
index ec6a9ca99..e6eba3532 100644
--- a/app/maptools/abstractmaptool.h
+++ b/app/maptools/abstractmaptool.h
@@ -30,11 +30,10 @@ class AbstractMapTool : public QObject
void setMapSettings( InputMapSettings *newMapSettings );
signals:
-
+ void onAboutToChangeMapSettings();
void mapSettingsChanged( InputMapSettings *mapSettings );
private:
-
InputMapSettings *mMapSettings = nullptr;
};
diff --git a/app/maptools/measurementmaptool.cpp b/app/maptools/measurementmaptool.cpp
new file mode 100644
index 000000000..dcaad908b
--- /dev/null
+++ b/app/maptools/measurementmaptool.cpp
@@ -0,0 +1,319 @@
+/***************************************************************************
+ * *
+ * 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 "measurementmaptool.h"
+
+MeasurementMapTool::MeasurementMapTool( QObject *parent )
+ : AbstractMapTool{ parent }
+{
+ connect( this, &AbstractMapTool::onAboutToChangeMapSettings, this, &MeasurementMapTool::resetMapSettings );
+ connect( this, &AbstractMapTool::mapSettingsChanged, this, &MeasurementMapTool::updateMapSettings );
+}
+
+MeasurementMapTool::~MeasurementMapTool() = default;
+
+void MeasurementMapTool::addPoint()
+{
+ if ( mapSettings() )
+ {
+ QgsPoint transformedPoint = mapSettings()->screenToCoordinate( mCrosshairPoint );
+
+ if ( !mPoints.empty() && transformedPoint == mPoints.back() )
+ return;
+
+ mPoints.push_back( transformedPoint );
+ rebuildGeometry();
+ }
+}
+
+void MeasurementMapTool::removePoint()
+{
+ if ( !mPoints.isEmpty() && mapSettings() )
+ {
+ mPoints.pop_back();
+ checkCanCloseShape();
+ rebuildGeometry();
+ updateDistance();
+ }
+}
+
+void MeasurementMapTool::updateDistance()
+{
+ if ( mPoints.isEmpty() || !mapSettings() )
+ {
+ setLengthWithGuideline( 0.0 );
+ return;
+ }
+
+ checkCanCloseShape();
+
+ QgsPoint lastPoint = mPoints.last();
+ QgsPoint transformedCrosshairPoint = mapSettings()->screenToCoordinate( mCrosshairPoint );
+
+ double calculatedLength = mPerimeter + mDistanceArea.measureLine( transformedCrosshairPoint, lastPoint );
+ setLengthWithGuideline( calculatedLength );
+}
+
+void MeasurementMapTool::checkCanCloseShape()
+{
+ if ( !mRecordedGeometry.isEmpty() && mapSettings() && mPoints.count() < 3 )
+ {
+ setCanCloseShape( false );
+ return;
+ }
+
+ QgsPoint firstPoint = mPoints.first();
+ QPointF firstPointScreen = mapSettings()->coordinateToScreen( firstPoint );
+ double distanceToFirstPoint = InputUtils::pixelDistanceBetween( mCrosshairPoint, firstPointScreen );
+ setCanCloseShape( distanceToFirstPoint <= CLOSE_THRESHOLD );
+}
+
+void MeasurementMapTool::finalizeMeasurement( bool closeShapeClicked )
+{
+ if ( mPoints.count() < 2 || !mapSettings() )
+ return;
+
+ QList pointList;
+ for ( const QgsPoint &point : mPoints )
+ pointList.append( QgsPointXY( point.x(), point.y() ) );
+
+ QgsGeometry geometry;
+ double perimeter = 0.0;
+
+ if ( closeShapeClicked && mCanCloseShape )
+ {
+ geometry = QgsGeometry::fromPolygonXY( QList>() << pointList );
+ perimeter = mDistanceArea.measurePerimeter( geometry );
+ setArea( mDistanceArea.measureArea( geometry ) );
+ setCanCloseShape( false );
+ }
+ else
+ {
+ geometry = QgsGeometry::fromPolylineXY( pointList );
+ perimeter = mDistanceArea.measureLength( geometry );
+ }
+
+ setRecordedGeometry( geometry );
+ setPerimeter( perimeter );
+ setMeasurementFinalized( true );
+}
+
+void MeasurementMapTool::resetMeasurement()
+{
+ mPoints.clear();
+
+ setPerimeter( 0.0 );
+ setArea( 0.0 );
+ setLengthWithGuideline( 0.0 );
+ setCanCloseShape( false );
+ setMeasurementFinalized( false );
+
+ rebuildGeometry();
+}
+
+void MeasurementMapTool::rebuildGeometry()
+{
+ if ( !mapSettings() )
+ return;
+
+ QgsGeometry geometry;
+
+ QgsMultiPoint *existingVertices = new QgsMultiPoint();
+ mExistingVertices.set( existingVertices );
+
+ if ( mPoints.count() > 0 )
+ {
+ geometry = QgsGeometry::fromPolyline( mPoints );
+
+ for ( const QgsPoint &point : mPoints )
+ {
+ existingVertices->addGeometry( point.clone() );
+ }
+
+ double perimeter = mDistanceArea.measureLength( geometry );
+ setPerimeter( perimeter );
+ setCanUndo( true );
+ }
+ else
+ {
+ setCanUndo( false );
+ }
+
+ // If we have more two or more points, "Done" button will be enabled
+ bool hasValidGeometry = !mRecordedGeometry.isEmpty() && mPoints.count() >= 2;
+ setIsValidGeometry( hasValidGeometry );
+
+ emit existingVerticesChanged( mExistingVertices );
+ setRecordedGeometry( geometry );
+}
+
+void MeasurementMapTool::resetMapSettings()
+{
+ InputMapSettings *currentMapSettings = mapSettings();
+
+ if ( currentMapSettings )
+ {
+ disconnect( currentMapSettings );
+ }
+}
+
+void MeasurementMapTool::updateMapSettings( InputMapSettings *newMapSettings )
+{
+ AbstractMapTool::setMapSettings( newMapSettings );
+
+ InputMapSettings *updatedMapSettings = mapSettings();
+
+ if ( updatedMapSettings )
+ {
+ connect( updatedMapSettings, &InputMapSettings::extentChanged, this, &MeasurementMapTool::updateDistance );
+
+ mDistanceArea.setEllipsoid( updatedMapSettings->project()->ellipsoid() );
+ mDistanceArea.setSourceCrs( updatedMapSettings->destinationCrs(), updatedMapSettings->transformContext() );
+ }
+}
+
+const QgsGeometry &MeasurementMapTool::recordedGeometry() const
+{
+ return mRecordedGeometry;
+}
+
+QgsGeometry MeasurementMapTool::existingVertices() const
+{
+ return mExistingVertices;
+}
+
+void MeasurementMapTool::setExistingVertices( const QgsGeometry &vertices )
+{
+ if ( mExistingVertices.equals( vertices ) )
+ return;
+
+ mExistingVertices = vertices;
+ emit existingVerticesChanged( mExistingVertices );
+}
+
+double MeasurementMapTool::area() const
+{
+ return mArea;
+}
+
+double MeasurementMapTool::perimeter() const
+{
+ return mPerimeter;
+}
+
+QPointF MeasurementMapTool::crosshairPoint() const
+{
+ return mCrosshairPoint;
+}
+
+double MeasurementMapTool::lengthWithGuideline() const
+{
+ return mLengthWithGuideline;
+}
+
+bool MeasurementMapTool::canUndo() const
+{
+ return mCanUndo;
+}
+
+void MeasurementMapTool::setCanUndo( bool newCanUndo )
+{
+ if ( mCanUndo == newCanUndo )
+ return;
+
+ mCanUndo = newCanUndo;
+ emit canUndoChanged( mCanUndo );
+}
+
+bool MeasurementMapTool::canCloseShape() const
+{
+ return mCanCloseShape;
+}
+
+void MeasurementMapTool::setCanCloseShape( bool newCanCloseShape )
+{
+ if ( mCanCloseShape == newCanCloseShape )
+ return;
+
+ mCanCloseShape = newCanCloseShape;
+ emit canCloseShapeChanged( mCanCloseShape );
+}
+
+bool MeasurementMapTool::isValidGeometry() const
+{
+ return mIsValidGeometry;
+}
+
+void MeasurementMapTool::setIsValidGeometry( bool hasValidGeometry )
+{
+ if ( mIsValidGeometry != hasValidGeometry )
+ {
+ mIsValidGeometry = hasValidGeometry;
+ emit isValidGeometryChanged( hasValidGeometry );
+ }
+}
+
+bool MeasurementMapTool::measurementFinalized() const
+{
+ return mMeasurementFinalized;
+}
+
+void MeasurementMapTool::setMeasurementFinalized( bool newMeasurementFinalized )
+{
+ if ( mMeasurementFinalized == newMeasurementFinalized )
+ return;
+
+ mMeasurementFinalized = newMeasurementFinalized;
+ emit measurementFinalizedChanged( mMeasurementFinalized );
+}
+
+void MeasurementMapTool::setRecordedGeometry( const QgsGeometry &newRecordedGeometry )
+{
+ if ( mRecordedGeometry.equals( newRecordedGeometry ) )
+ return;
+
+ mRecordedGeometry = newRecordedGeometry;
+ emit recordedGeometryChanged( mRecordedGeometry );
+}
+
+void MeasurementMapTool::setLengthWithGuideline( const double &lengthWithGuideline )
+{
+ if ( mLengthWithGuideline == lengthWithGuideline )
+ return;
+
+ mLengthWithGuideline = lengthWithGuideline;
+ emit lengthWithGuidelineChanged( lengthWithGuideline );
+}
+
+void MeasurementMapTool::setArea( const double &area )
+{
+ if ( mArea == area )
+ return;
+
+ mArea = area;
+ emit areaChanged( area );
+}
+
+void MeasurementMapTool::setPerimeter( const double &perimeter )
+{
+ if ( mPerimeter == perimeter )
+ return;
+
+ mPerimeter = perimeter;
+ emit perimeterChanged( perimeter );
+}
+
+void MeasurementMapTool::setCrosshairPoint( const QPointF &point )
+{
+ if ( mCrosshairPoint == point )
+ return;
+
+ mCrosshairPoint = point;
+ emit crosshairPointChanged( mCrosshairPoint );
+}
diff --git a/app/maptools/measurementmaptool.h b/app/maptools/measurementmaptool.h
new file mode 100644
index 000000000..1d041b203
--- /dev/null
+++ b/app/maptools/measurementmaptool.h
@@ -0,0 +1,139 @@
+/***************************************************************************
+ * *
+ * 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. *
+ * *
+ ***************************************************************************/
+
+#ifndef MEASUREMENTMAPTOOL_H
+#define MEASUREMENTMAPTOOL_H
+
+#include "abstractmaptool.h"
+#include
+#include "qgsdistancearea.h"
+#include "qgsgeometry.h"
+#include "inpututils.h"
+#include "qgspolygon.h"
+#include "qgsgeometry.h"
+#include "qgsvectorlayer.h"
+#include "qgsmultipoint.h"
+
+const double CLOSE_THRESHOLD = 10.0; // in pixels
+
+class MeasurementMapTool : public AbstractMapTool
+{
+ Q_OBJECT
+
+ Q_PROPERTY( QgsGeometry recordedGeometry READ recordedGeometry WRITE setRecordedGeometry NOTIFY recordedGeometryChanged )
+ Q_PROPERTY( QgsGeometry existingVertices READ existingVertices WRITE setExistingVertices NOTIFY existingVerticesChanged )
+ Q_PROPERTY( QPointF crosshairPoint READ crosshairPoint WRITE setCrosshairPoint NOTIFY crosshairPointChanged )
+
+ Q_PROPERTY( double lengthWithGuideline READ lengthWithGuideline WRITE setLengthWithGuideline NOTIFY lengthWithGuidelineChanged )
+ Q_PROPERTY( double perimeter READ perimeter WRITE setPerimeter NOTIFY perimeterChanged )
+ Q_PROPERTY( double area READ area WRITE setArea NOTIFY areaChanged )
+
+ Q_PROPERTY( bool canUndo READ canUndo WRITE setCanUndo NOTIFY canUndoChanged )
+ Q_PROPERTY( bool canCloseShape READ canCloseShape WRITE setCanCloseShape NOTIFY canCloseShapeChanged )
+ Q_PROPERTY( bool isValidGeometry READ isValidGeometry WRITE setIsValidGeometry NOTIFY isValidGeometryChanged )
+ Q_PROPERTY( bool measurementFinalized READ measurementFinalized WRITE setMeasurementFinalized NOTIFY measurementFinalizedChanged )
+
+ public:
+ explicit MeasurementMapTool( QObject *parent = nullptr );
+ virtual ~MeasurementMapTool() override;
+
+ /**
+ * Adds point to the end of the recorded geometry; updates recordedGeometry afterwards
+ * Passed point needs to be in map CRS
+ */
+ Q_INVOKABLE void addPoint();
+
+ /**
+ * Removes last point from recorded geometry if there is at least one point
+ * Updates recordedGeometry afterwards
+ */
+ Q_INVOKABLE void removePoint();
+
+ /**
+ * Finalizes measurement by forming a polygon if "Close shape" button was clicked
+ * and there are at least 3 points; otherwise, if "Done" button was clicked, forms a polyline.
+ * Updates recorded geometry, calculates perimeter, and calculates the area if it's a polygon.
+ */
+ Q_INVOKABLE void finalizeMeasurement( bool closeShapeClicked );
+
+ /**
+ * Repeats measurement process.
+ * Clears all recorded points and rebuilds the geometry.
+ */
+ Q_INVOKABLE void resetMeasurement();
+
+ // Getter and Setters
+ double lengthWithGuideline() const;
+ void setLengthWithGuideline( const double &length );
+
+ double perimeter() const;
+ void setPerimeter( const double &perimeter );
+
+ double area() const;
+ void setArea( const double &area );
+
+ QPointF crosshairPoint() const;
+ void setCrosshairPoint( const QPointF &point );
+
+ bool canUndo() const;
+ void setCanUndo( bool newCanUndo );
+
+ bool canCloseShape() const;
+ void setCanCloseShape( bool newCanCloseShape );
+
+ bool isValidGeometry() const;
+ void setIsValidGeometry( bool hasValidGeometry );
+
+ bool measurementFinalized() const;
+ void setMeasurementFinalized( bool newMeasurementFinalized );
+
+ const QgsGeometry &recordedGeometry() const;
+ void setRecordedGeometry( const QgsGeometry &newRecordedGeometry );
+
+ QgsGeometry existingVertices() const;
+ void setExistingVertices( const QgsGeometry &vertices );
+
+ void resetMapSettings();
+ void updateMapSettings( InputMapSettings *newMapSettings );
+
+ signals:
+ void lengthWithGuidelineChanged( const double &lengthWithGuideline );
+ void perimeterChanged( const double &perimeter );
+ void areaChanged( const double &area );
+ void canUndoChanged( bool canUndo );
+ void canCloseShapeChanged( bool canUndo );
+ void measurementFinalizedChanged( bool measurementFinalized );
+ void recordedGeometryChanged( const QgsGeometry &recordedGeometry );
+ void existingVerticesChanged( const QgsGeometry &vertices );
+ void crosshairPointChanged( const QPointF &crosshairPoint );
+ void isValidGeometryChanged( bool canFinalize );
+
+ protected:
+ void rebuildGeometry();
+ void checkCanCloseShape();
+
+ public slots:
+ void updateDistance();
+
+ private:
+ QVector mPoints;
+ QgsGeometry mRecordedGeometry;
+ QgsGeometry mExistingVertices;
+ QgsDistanceArea mDistanceArea;
+ QPointF mCrosshairPoint;
+ double mLengthWithGuideline = 0;
+ double mPerimeter = 0;
+ double mArea = 0;
+ bool mCanUndo = false;
+ bool mCanCloseShape = false;
+ bool mIsValidGeometry = false;
+ bool mMeasurementFinalized = false;
+};
+
+#endif // MEASUREMENTMAPTOOL_H
diff --git a/app/mmstyle.h b/app/mmstyle.h
index 7d65b0e91..16587bb81 100644
--- a/app/mmstyle.h
+++ b/app/mmstyle.h
@@ -168,6 +168,8 @@ class MMStyle: public QObject
Q_PROPERTY( QUrl streamingIcon READ streamingIcon CONSTANT )
Q_PROPERTY( QUrl redrawGeometryIcon READ redrawGeometryIcon CONSTANT )
Q_PROPERTY( QUrl cloudIcon READ cloudIcon CONSTANT )
+ Q_PROPERTY( QUrl measurementToolIcon READ measurementToolIcon CONSTANT )
+ Q_PROPERTY( QUrl closeShapeIcon READ closeShapeIcon CONSTANT )
// Filled Icons - for visualizing of selected item in toolbar
Q_PROPERTY( QUrl projectsFilledIcon READ projectsFilledIcon CONSTANT )
@@ -215,6 +217,7 @@ class MMStyle: public QObject
Q_PROPERTY( QUrl externalGpsRedImage READ externalGpsRedImage CONSTANT )
Q_PROPERTY( QUrl negativeMMSymbolImage READ negativeMMSymbolImage CONSTANT )
Q_PROPERTY( QUrl positiveMMSymbolImage READ positiveMMSymbolImage CONSTANT )
+ Q_PROPERTY( QUrl neutralMMSymbolImage READ neutralMMSymbolImage CONSTANT )
Q_PROPERTY( QUrl closeAccountImage READ closeAccountImage CONSTANT )
Q_PROPERTY( QUrl attentionImage READ attentionImage CONSTANT )
Q_PROPERTY( QUrl blueInfoImage READ blueInfoImage CONSTANT )
@@ -276,7 +279,9 @@ class MMStyle: public QObject
// Page
Q_PROPERTY( double pageMargins READ number20 CONSTANT ) // distance between screen edge and components
+ Q_PROPERTY( double spacing2 READ number2 CONSTANT )
Q_PROPERTY( double spacing5 READ number5 CONSTANT )
+ Q_PROPERTY( double spacing10 READ number10 CONSTANT )
Q_PROPERTY( double spacing12 READ number12 CONSTANT ) // distance between page header, page content and page footer
Q_PROPERTY( double spacing20 READ number20 CONSTANT )
Q_PROPERTY( double spacing30 READ number30 CONSTANT )
@@ -422,6 +427,8 @@ class MMStyle: public QObject
QUrl moreVerticalIcon() {return QUrl( "qrc:/MoreVertical.svg" );}
QUrl morePhotosIcon() {return QUrl( "qrc:/MorePhotos.svg" );}
QUrl mouthIcon() {return QUrl( "qrc:/Mouth.svg" );}
+ QUrl measurementToolIcon() {return QUrl( "qrc:/Measure.svg" );}
+ QUrl closeShapeIcon() {return QUrl( "qrc:/CloseShape.svg" );}
QUrl naturalResourcesIcon() {return QUrl( "qrc:/NaturalResources.svg" );}
QUrl nextIcon() {return QUrl( "qrc:/Next.svg" );}
QUrl otherIcon() {return QUrl( "qrc:/Other.svg" );}
@@ -498,6 +505,7 @@ class MMStyle: public QObject
QUrl externalGpsRedImage() {return QUrl( "qrc:/images/ExternalGpsRed.svg" );}
QUrl negativeMMSymbolImage() {return QUrl( "qrc:/images/NegativeMMSymbol.svg" );}
QUrl positiveMMSymbolImage() {return QUrl( "qrc:/images/PositiveMMSymbol.svg" );}
+ QUrl neutralMMSymbolImage() {return QUrl( "qrc:/images/NeutralMMSymbol.svg" );}
QUrl closeAccountImage() {return QUrl( "qrc:/images/CloseAccount.svg" );}
QUrl attentionImage() {return QUrl( "qrc:/images/Attention.svg" );}
QUrl blueInfoImage() {return QUrl( "qrc:/images/BlueInfo.svg" );}
diff --git a/app/qml/CMakeLists.txt b/app/qml/CMakeLists.txt
index bf848bd61..ccf99a8d5 100644
--- a/app/qml/CMakeLists.txt
+++ b/app/qml/CMakeLists.txt
@@ -116,6 +116,7 @@ set(MM_QML
gps/MMGpsDataDrawer.qml
gps/MMPositionProviderPage.qml
gps/MMStakeoutDrawer.qml
+ gps/MMMeasureDrawer.qml
gps/components/MMGpsDataText.qml
inputs/MMComboboxInput.qml
inputs/MMPasswordInput.qml
@@ -135,8 +136,10 @@ set(MM_QML
map/MMSplittingTools.qml
map/MMStakeoutTools.qml
map/MMRecordingTools.qml
+ map/MMMeasurementTools.qml
map/components/MMHidingBox.qml
map/components/MMCrosshair.qml
+ map/components/MMMeasureCrosshair.qml
map/components/MMMapHidingLabel.qml
map/components/MMMapButton.qml
map/components/MMMapLabel.qml
diff --git a/app/qml/components/MMButton.qml b/app/qml/components/MMButton.qml
index 631a26b3e..3cce97fc9 100644
--- a/app/qml/components/MMButton.qml
+++ b/app/qml/components/MMButton.qml
@@ -15,8 +15,10 @@ Button {
id: root
enum Types { Primary, Secondary, Tertiary }
+ enum Sizes { Small, Regular }
property int type: MMButton.Types.Primary
+ property int size: MMButton.Sizes.Regular
property color fontColor: {
if ( type === MMButton.Types.Primary ) return __style.forestColor
@@ -161,10 +163,10 @@ Button {
state: "default"
implicitHeight: root.type === MMButton.Types.Tertiary ? buttonContent.height : buttonContent.height + topPadding + bottomPadding
- implicitWidth: row.paintedChildrenWidth + 2 * __style.margin20
+ implicitWidth: row.paintedChildrenWidth + 2 * ( root.size === MMButton.Sizes.Small ? __style.margin16 : __style.margin20 )
- topPadding: root.type === MMButton.Types.Tertiary ? 0 : 11 * __dp
- bottomPadding: root.type === MMButton.Types.Tertiary ? 0 : 11 * __dp
+ topPadding: ( root.type === MMButton.Types.Tertiary ) ? 0 : 11 * __dp
+ bottomPadding: ( root.type === MMButton.Types.Tertiary ) ? 0 : 11 * __dp
rightPadding: 0
leftPadding: 0
@@ -177,14 +179,21 @@ Button {
id: row
property real paintedChildrenWidth: buttonIconLeft.paintedWidth + buttonContent.implicitWidth + buttonIconRight.paintedWidth + spacing
- property real maxWidth: parent.width - 2 * __style.margin20
+ property real maxWidth: parent.width - 2 * ( root.size === MMButton.Sizes.Small ? __style.margin16 : __style.margin20 )
x: ( parent.width - width ) / 2
width: Math.min( paintedChildrenWidth, maxWidth )
height: Math.max( buttonContent.paintedHeight, buttonIconRight.height )
- spacing: buttonIconRight.visible || buttonIconLeft.visible ? __style.spacing12 : 0
+ spacing: {
+ if ( ( root.size === MMButton.Sizes.Small ) )
+ return __style.spacing2;
+ else if ( ( buttonIconRight.visible || buttonIconLeft.visible ) )
+ return __style.spacing12;
+ else
+ return 0;
+ }
MMIcon {
id: buttonIconLeft
diff --git a/app/qml/components/MMDrawerHeader.qml b/app/qml/components/MMDrawerHeader.qml
index 8d5a5cfc2..b40c2ce1e 100644
--- a/app/qml/components/MMDrawerHeader.qml
+++ b/app/qml/components/MMDrawerHeader.qml
@@ -22,7 +22,9 @@ Rectangle {
property font titleFont: __style.t3
property bool hasCloseButton: true
+
property alias closeButton: closeBtn
+ property alias topLeftItemContent: topLeftButtonGroup.children
color: __style.transparentColor
@@ -31,14 +33,26 @@ Rectangle {
implicitHeight: 60 * __dp
implicitWidth: ApplicationWindow.window?.width ?? 0
+ Item {
+ id: topLeftButtonGroup
+
+ width: childrenRect.width
+ height: parent.height
+ }
+
Text {
- // If the close button is visible, we need to properly center the text
- property real margin: internal.closeBtnRealWidth + internal.headerSpacing + __style.pageMargins
+ property real leftMarginShift: {
+ return Math.max( internal.closeBtnRealWidth, topLeftButtonGroup.width ) + internal.headerSpacing + __style.pageMargins
+ }
+
+ property real rightMarginShift: {
+ return Math.max( internal.closeBtnRealWidth, topLeftButtonGroup.width ) + internal.headerSpacing + __style.pageMargins
+ }
anchors {
fill: parent
- leftMargin: margin
- rightMargin: margin
+ leftMargin: leftMarginShift
+ rightMargin: rightMarginShift
}
text: root.title
diff --git a/app/qml/gps/MMMeasureDrawer.qml b/app/qml/gps/MMMeasureDrawer.qml
new file mode 100644
index 000000000..75910e175
--- /dev/null
+++ b/app/qml/gps/MMMeasureDrawer.qml
@@ -0,0 +1,119 @@
+/***************************************************************************
+ * *
+ * 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. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import QtQuick.Layouts
+import QtQuick.Controls
+import Qt5Compat.GraphicalEffects
+import QtQuick.Shapes
+
+import mm 1.0 as MM
+
+import "../components"
+import "../map/components"
+import "./components" as MMGpsComponents
+
+MMDrawer {
+ id: root
+
+ property var mapTool
+
+ readonly property alias panelHeight: root.height
+
+ property bool canCloseShape: mapTool?.canCloseShape ?? false
+ property bool canUndo: mapTool?.canUndo ?? false
+ property bool isValidGeometry: mapTool?.isValidGeometry ?? false
+ property bool measurementFinalized: mapTool?.measurementFinalized ?? false
+
+ property string perimeter: mapTool?.perimeter ?? 0
+ property string area: mapTool?.area ?? 0
+ property bool isPolygon: area > 0
+
+ signal measureFinished()
+
+ Component.onCompleted: root.open()
+
+ modal: false
+ interactive: false
+ closePolicy: Popup.CloseOnEscape
+
+ dropShadow: true
+
+ onClosed: root.measureFinished()
+
+ Behavior on implicitHeight {
+ PropertyAnimation { properties: "implicitHeight"; easing.type: Easing.InOutQuad }
+ }
+
+ drawerHeader.title: qsTr( "Measure" )
+
+ drawerHeader.topLeftItemContent: MMButton {
+ type: MMButton.Types.Primary
+ text: measurementFinalized ? qsTr( "Repeat" ) : qsTr( "Undo" )
+ iconSourceLeft: measurementFinalized ? __style.syncIcon : __style.undoIcon
+ bgndColor: __style.lightGreenColor
+ size: MMButton.Sizes.Small
+ enabled: measurementFinalized || canUndo
+
+ anchors {
+ left: parent.left
+ leftMargin: __style.pageMargins + __style.safeAreaLeft
+ verticalCenter: parent.verticalCenter
+ }
+
+ onClicked: measurementFinalized ? root.mapTool.resetMeasurement() : root.mapTool.removePoint()
+ }
+
+ drawerContent: Column {
+ id: mainColumn
+
+ width: parent.width
+ spacing: __style.margin10
+
+ Row {
+ width: parent.width
+
+ MMGpsComponents.MMGpsDataText{
+ width: ( parent.width + parent.spacing ) / 2
+
+ title: measurementFinalized && root.isPolygon ? qsTr( "Perimeter" ) : qsTr( "Length" ) //Perimeter only if its a polygon
+ value: __inputUtils.formatDistanceInProjectUnit( root.perimeter, 1, __activeProject.qgsProject )
+ }
+
+ MMGpsComponents.MMGpsDataText{
+ width: ( parent.width + parent.spacing ) / 2
+
+ title: qsTr( "Area" )
+ value: __inputUtils.formatAreaInProjectUnit( root.area, 1, __activeProject.qgsProject )
+ alignmentRight: true
+ visible: measurementFinalized && root.isPolygon
+ }
+ }
+
+ Row {
+ width: parent.width
+ spacing: __style.margin12
+ visible: !root.measurementFinalized
+
+ MMButton {
+ text: root.canCloseShape ? qsTr( "Close shape" ) : qsTr( "Add point" )
+ iconSourceLeft: canCloseShape ? __style.closeShapeIcon : __style.plusIcon
+ onClicked: canCloseShape ? root.mapTool.finalizeMeasurement( true ) : root.mapTool.addPoint()
+ }
+
+ MMButton {
+ type: MMButton.Types.Secondary
+ text: qsTr( "Done" )
+ iconSourceLeft: __style.doneCircleIcon
+ enabled: root.isValidGeometry
+ onClicked: root.mapTool.finalizeMeasurement( false )
+ }
+ }
+ }
+}
diff --git a/app/qml/gps/MMStakeoutDrawer.qml b/app/qml/gps/MMStakeoutDrawer.qml
index 918436976..61f6ca354 100644
--- a/app/qml/gps/MMStakeoutDrawer.qml
+++ b/app/qml/gps/MMStakeoutDrawer.qml
@@ -97,7 +97,6 @@ MMDrawer {
Row {
width: parent.width
- spacing: __style.margi8
MMGpsComponents.MMGpsDataText{
width: ( parent.width + parent.spacing ) / 2
@@ -110,7 +109,7 @@ MMDrawer {
width: ( parent.width + parent.spacing ) / 2
title: qsTr( "Distance" )
- value: remainingDistance >= 0 ?__inputUtils.formatDistanceInProjectUnit( remainingDistance, 2 ) : qsTr( "N/A" )
+ value: remainingDistance >= 0 ?__inputUtils.formatDistanceInProjectUnit( remainingDistance, 2, __activeProject.qgsProject ) : qsTr( "N/A" )
alignmentRight: true
}
}
diff --git a/app/qml/main.qml b/app/qml/main.qml
index 822e7bd41..d6b7ea116 100644
--- a/app/qml/main.qml
+++ b/app/qml/main.qml
@@ -158,6 +158,10 @@ ApplicationWindow {
// if stakeout panel is opened
return stakeoutPanelLoader.item.panelHeight - mapToolbar.height
}
+ else if ( measurePanelLoader.active )
+ {
+ return measurePanelLoader.item.panelHeight - mapToolbar.height
+ }
else if ( formsStackManager.takenVerticalSpace > 0 )
{
// if feature preview panel is opened
@@ -219,6 +223,11 @@ ApplicationWindow {
stakeoutPanelLoader.item.targetPair = pair
}
+ onMeasureStarted: function( pair ) {
+ measurePanelLoader.active = true
+ measurePanelLoader.focus = true
+ }
+
onLocalChangesPanelRequested: {
stateManager.state = "projects"
projectController.openChangesPanel( __activeProject.projectFullName(), true )
@@ -326,6 +335,12 @@ ApplicationWindow {
}
}
+ MMToolbarButton {
+ text: qsTr("Measure")
+ iconSource: __style.measurementToolIcon
+ onClicked: map.measure()
+ }
+
MMToolbarButton {
text: qsTr("Local changes")
iconSource: __style.localChangesIcon
@@ -596,6 +611,32 @@ ApplicationWindow {
}
}
+ Loader {
+ id: measurePanelLoader
+
+ focus: true
+ active: false
+ asynchronous: true
+
+ sourceComponent: measurePanelComponent
+ }
+
+ Component {
+ id: measurePanelComponent
+
+ MMMeasureDrawer {
+ id: measurePanel
+
+ width: window.width
+ mapTool: map.mapToolComponent
+
+ onMeasureFinished: {
+ measurePanelLoader.active = false
+ map.finishMeasure()
+ }
+ }
+ }
+
MMFormStackController {
id: formsStackManager
diff --git a/app/qml/map/MMMapController.qml b/app/qml/map/MMMapController.qml
index a1c8c7fb4..43f76d8af 100644
--- a/app/qml/map/MMMapController.qml
+++ b/app/qml/map/MMMapController.qml
@@ -35,6 +35,10 @@ Item {
property bool isStreaming: recordingToolsLoader.active ? recordingToolsLoader.item.recordingMapTool.recordingType === MM.RecordingMapTool.StreamMode : false
property bool centeredToGPS: false
+ property var mapToolComponent: {
+ measurementToolsLoader.active ? measurementToolsLoader.item.mapTool : null
+ }
+
property MM.PositionTrackingManager trackingManager: tracking.item?.manager ?? null
signal featureIdentified( var pair )
@@ -60,6 +64,8 @@ Item {
signal stakeoutStarted( var pair )
signal accuracyButtonClicked()
+ signal measureStarted()
+
signal localChangesPanelRequested()
signal openTrackingPanel()
@@ -86,6 +92,9 @@ Item {
State {
name: "stakeout"
},
+ State {
+ name: "measure"
+ },
State {
name: "inactive" // ignores touch input
}
@@ -143,6 +152,13 @@ Item {
break
}
+ case "measure": {
+ root.showInfoTextMessage( qsTr( "Add points to measure distance, close the shape to measure area" ) )
+ root.hideHighlight()
+ root.measureStarted()
+ break
+ }
+
case "inactive": {
break
}
@@ -381,6 +397,17 @@ Item {
sourceComponent: splittingToolsComponent
}
+ Loader {
+ id: measurementToolsLoader
+
+ anchors.fill: mapCanvas
+
+ asynchronous: true
+ active: root.state === "measure"
+
+ sourceComponent: measurementToolsComponent
+ }
+
// map available content within safe area
Item {
anchors {
@@ -497,10 +524,10 @@ Item {
anchors.bottom: parent.bottom
- anchors.bottomMargin: root.state === "stakeout" ? root.mapExtentOffset : 0
+ anchors.bottomMargin: root.state === "stakeout" || root.state === "measure" ? root.mapExtentOffset : 0
visible: {
- if ( root.state === "stakeout" )
+ if ( root.state === "stakeout" || root.state === "measure" )
return true
else
return root.mapExtentOffset > 0 ? false : true
@@ -962,6 +989,18 @@ Item {
}
}
+ Component {
+ id: measurementToolsComponent
+
+ MMMeasurementTools {
+ anchors.fill: parent
+
+ map: mapCanvas
+ positionMarkerComponent: positionMarker
+ onFinishMeasurement: root.finishMeasure()
+ }
+ }
+
Component {
id: splittingToolsComponent
@@ -1111,6 +1150,10 @@ Item {
state = "stakeout"
}
+ function measure() {
+ state = "measure"
+ }
+
function toggleStreaming() {
// start/stop the streaming mode
if ( recordingToolsLoader.active ) {
@@ -1127,6 +1170,10 @@ Item {
root.centeredToGPS = internal.centeredToGPSBeforeStakeout
}
+ function finishMeasure() {
+ state = "view"
+ }
+
function centerToPair( pair ) {
__inputUtils.setExtentToFeature( pair, mapCanvas.mapSettings )
}
diff --git a/app/qml/map/MMMeasurementTools.qml b/app/qml/map/MMMeasurementTools.qml
new file mode 100644
index 000000000..e4a785961
--- /dev/null
+++ b/app/qml/map/MMMeasurementTools.qml
@@ -0,0 +1,101 @@
+/***************************************************************************
+ * *
+ * 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. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import QtQuick.Shapes
+
+import mm 1.0 as MM
+
+import "../components"
+import "./components"
+import "../gps"
+import "../dialogs"
+
+Item {
+ id: root
+
+ required property MMMapCanvas map
+ required property MMPositionMarker positionMarkerComponent
+
+ property var mapTool: mapTool
+
+ signal finishMeasurement()
+
+ MM.MeasurementMapTool {
+ id: mapTool
+
+ mapSettings: root.map.mapSettings
+ crosshairPoint: crosshair.screenPoint
+ }
+
+ MM.GuidelineController {
+ id: guidelineController
+
+ allowed: !mapTool.measurementFinalized
+ mapSettings: root.map.mapSettings
+ crosshairPosition: crosshair.screenPoint
+ realGeometry: mapTool.recordedGeometry
+ }
+
+ MMHighlight {
+ id: guideline
+
+ height: root.map.height
+ width: root.map.width
+
+ markerColor: __style.deepOceanColor
+ lineColor: __style.deepOceanColor
+ lineStrokeStyle: ShapePath.DashLine
+ lineWidth: MMHighlight.LineWidths.Narrow
+
+ mapSettings: root.map.mapSettings
+ geometry: guidelineController.guidelineGeometry
+ }
+
+ MMHighlight {
+ id: highlight
+
+ height: map.height
+ width: map.width
+
+ markerColor: __style.deepOceanColor
+ lineColor: __style.deepOceanColor
+ lineWidth: MMHighlight.LineWidths.Narrow
+
+ mapSettings: root.map.mapSettings
+ geometry: mapTool.recordedGeometry
+ }
+
+ MMHighlight {
+ id: existingVerticesHighlight
+
+ height: root.map.height
+ width: root.map.width
+
+ mapSettings: root.map.mapSettings
+ geometry: mapTool.existingVertices
+
+ markerType: MMHighlight.MarkerTypes.Circle
+ markerSize: MMHighlight.MarkerSizes.Bigger
+ }
+
+ MMMeasureCrosshair {
+ id: crosshair
+
+ anchors.fill: parent
+ qgsProject: __activeProject.qgsProject
+ mapSettings: root.map.mapSettings
+ visible: !mapTool.measurementFinalized
+
+ text: __inputUtils.formatDistanceInProjectUnit( mapTool.lengthWithGuideline, 1, __activeProject.qgsProject )
+ canCloseShape: mapTool.canCloseShape
+
+ onCloseShapeClicked: root.mapTool.finalizeMeasurement( true )
+ }
+}
diff --git a/app/qml/map/components/MMCrosshair.qml b/app/qml/map/components/MMCrosshair.qml
index a9c7ec465..82892ff25 100644
--- a/app/qml/map/components/MMCrosshair.qml
+++ b/app/qml/map/components/MMCrosshair.qml
@@ -28,6 +28,9 @@ Item {
property real outerSize: 60 * __dp
property real innerDotSize: 10 * __dp
+ property alias crosshairForeground: crosshairForeground
+ property alias snapUtils: snapUtils
+
MM.SnapUtils {
id: snapUtils
diff --git a/app/qml/map/components/MMMapHidingLabel.qml b/app/qml/map/components/MMMapHidingLabel.qml
index 7a0ab3720..63941982d 100644
--- a/app/qml/map/components/MMMapHidingLabel.qml
+++ b/app/qml/map/components/MMMapHidingLabel.qml
@@ -24,12 +24,14 @@ MMHidingBox {
root.visible = false
}
- height: __style.row40
+ height: text.lineCount * __style.row40
timerInterval: 10000
fadeOutDuration: 1000
// Text
Text {
+ id: text
+
height: parent.height
width: parent.width - 2 * __style.pageMargins
anchors.verticalCenter: parent.verticalCenter
@@ -40,6 +42,8 @@ MMHidingBox {
font: __style.t3
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
+ maximumLineCount: 3
+ wrapMode: Text.WordWrap
elide: Text.ElideRight
}
}
diff --git a/app/qml/map/components/MMMeasureCrosshair.qml b/app/qml/map/components/MMMeasureCrosshair.qml
new file mode 100644
index 000000000..3102def00
--- /dev/null
+++ b/app/qml/map/components/MMMeasureCrosshair.qml
@@ -0,0 +1,106 @@
+/***************************************************************************
+ * *
+ * 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. *
+ * *
+ ***************************************************************************/
+
+import QtQuick
+import Qt5Compat.GraphicalEffects
+
+import mm 1.0 as MM
+import "../../components"
+
+Item {
+ id: root
+
+ required property string text
+ /*required*/ property var qgsProject
+ /*required*/ property var mapSettings
+
+ property bool canCloseShape: false
+ property bool shouldUseSnapping: false
+
+ property point center: Qt.point( root.width / 2, root.height / 2 )
+
+ property var recordPoint: crosshair.recordPoint
+
+ property point screenPoint: crosshair.snapUtils.snapped && __activeLayer.vectorLayer ? __inputUtils.transformPointToScreenCoordinates(__activeLayer.vectorLayer.crs, mapSettings, recordPoint) : center
+
+ property real outerSize: 60 * __dp
+ property real innerDotSize: 10 * __dp
+
+ implicitWidth: row.width
+ implicitHeight: __style.mapItemHeight
+
+ signal closeShapeClicked
+
+ MMCrosshair {
+ id: crosshair
+
+ anchors.fill: parent
+ qgsProject: __activeProject.qgsProject
+ mapSettings: root.mapSettings
+ }
+
+ Rectangle {
+ y: crosshair.crosshairForeground.y + crosshair.crosshairForeground.height + __style.spacing2
+ x: crosshair.crosshairForeground.x - ( ( width - crosshair.crosshairForeground.width ) / 2 )
+
+ width: Math.max( root.outerSize - __style.spacing10 , row.width )
+ height: root.outerSize * 0.6
+ radius: root.height / 2
+ color: textBg.color
+
+ layer.enabled: true
+ layer.effect: MMShadow {}
+
+ Row {
+ id: row
+
+ anchors.centerIn: parent
+ leftPadding: 8 * __dp
+ rightPadding: leftPadding
+ spacing: 4 * __dp
+ height: parent.height
+
+ MMIcon {
+ id: icon
+ anchors.verticalCenter: parent.verticalCenter
+ source: root.canCloseShape ? __style.closeShapeIcon : ""
+ size: root.canCloseShape ? __style.icon24 : 0
+ }
+
+ Rectangle {
+ id: textBg
+ property real spacing: __style.spacing5
+ anchors.verticalCenter: parent.verticalCenter
+ color: root.canCloseShape ? __style.grassColor : __style.forestColor
+ height: text.height + spacing
+ width: text.width + 3 * spacing
+ radius: height / 2
+
+ MMText {
+ id: text
+
+ property real textSurroundingItemsWidth: textBg.spacing + icon.width + row.spacing + 2 * row.leftPadding
+
+ width: ( implicitWidth + textSurroundingItemsWidth ) > root.maxWidth ? root.maxWidth - textSurroundingItemsWidth : implicitWidth
+ anchors.centerIn: parent
+ color: root.canCloseShape ? __style.forestColor: __style.polarColor
+ text: root.canCloseShape ? qsTr( "Close shape" ) : root.text
+ font: __style.t3
+ elide: Text.ElideRight
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: root.canCloseShape
+ onClicked: root.closeShapeClicked()
+ }
+ }
+}
diff --git a/app/test/testmaptools.cpp b/app/test/testmaptools.cpp
index 3b359a399..c90fb4de2 100644
--- a/app/test/testmaptools.cpp
+++ b/app/test/testmaptools.cpp
@@ -29,6 +29,7 @@
#include "snaputils.h"
#include "maptools/splittingmaptool.h"
#include "maptools/recordingmaptool.h"
+#include "maptools/measurementmaptool.h"
#include "featurelayerpair.h"
#include "streamingintervaltype.h"
@@ -276,6 +277,87 @@ void TestMapTools::testRecording()
delete recordTool;
}
+void TestMapTools::testMeasuring()
+{
+ MeasurementMapTool *measurementTool = new MeasurementMapTool();
+
+ InputMapCanvasMap canvas;
+ InputMapSettings *ms = canvas.mapSettings();
+
+ QgsProject *project = TestUtils::loadPlanesTestProject();
+ QVERIFY( project && !project->homePath().isEmpty() );
+
+ setupMapSettings( ms, project, QgsRectangle( -10, -10, 10, 10 ), QSize( 600, 600 ) );
+
+ measurementTool->setMapSettings( ms );
+
+ QgsDistanceArea distanceArea;
+ distanceArea.setEllipsoid( ms->project()->ellipsoid() );
+ distanceArea.setSourceCrs( ms->destinationCrs(), ms->transformContext() );
+
+ QPointF crosshairPoint1 = ms->coordinateToScreen( QgsPoint( 0, 0 ) );
+ measurementTool->setCrosshairPoint( crosshairPoint1 );
+ measurementTool->addPoint();
+
+ measurementTool->updateDistance();
+ QCOMPARE( measurementTool->lengthWithGuideline(), 0.0 );
+
+ QPointF crosshairPoint2 = ms->coordinateToScreen( QgsPoint( 0, 1 ) );
+ measurementTool->setCrosshairPoint( crosshairPoint2 );
+ measurementTool->addPoint();
+
+ measurementTool->updateDistance();
+
+ QList points;
+ points.append( QgsPointXY( 0, 0 ) );
+ points.append( QgsPointXY( 0, 1 ) );
+
+ QgsGeometry lineGeometry = QgsGeometry::fromPolylineXY( points );
+ double expectedPerimeter = distanceArea.measureLength( lineGeometry );
+ QCOMPARE( measurementTool->perimeter(), expectedPerimeter );
+
+ QPointF crosshairPoint3 = ms->coordinateToScreen( QgsPoint( 1, 1 ) );
+ measurementTool->setCrosshairPoint( crosshairPoint3 );
+ measurementTool->addPoint();
+
+ measurementTool->updateDistance();
+ points.append( QgsPointXY( 1, 1 ) );
+ lineGeometry = QgsGeometry::fromPolylineXY( points );
+ expectedPerimeter = distanceArea.measureLength( lineGeometry );
+ QCOMPARE( measurementTool->perimeter(), expectedPerimeter );
+
+ measurementTool->finalizeMeasurement( false );
+
+ QVERIFY( measurementTool->recordedGeometry().wkbType() == Qgis::WkbType::LineString );
+
+ measurementTool->resetMeasurement();
+
+ measurementTool->setCrosshairPoint( crosshairPoint1 );
+ measurementTool->addPoint();
+
+ measurementTool->setCrosshairPoint( crosshairPoint2 );
+ measurementTool->addPoint();
+
+ measurementTool->setCrosshairPoint( crosshairPoint3 );
+ measurementTool->addPoint();
+
+ measurementTool->setCrosshairPoint( crosshairPoint1 );
+ measurementTool->updateDistance();
+
+ QVERIFY( measurementTool->canCloseShape() );
+
+ measurementTool->finalizeMeasurement( true );
+
+ QVERIFY( measurementTool->recordedGeometry().wkbType() == Qgis::WkbType::Polygon );
+
+ QgsGeometry polygonGeometry = QgsGeometry::fromPolygonXY( QList>() << points );
+ double expectedArea = distanceArea.measureArea( polygonGeometry );
+ QCOMPARE( measurementTool->area(), expectedArea );
+
+ delete project;
+ delete measurementTool;
+}
+
void TestMapTools::testExistingVertices()
{
RecordingMapTool mapTool;
diff --git a/app/test/testmaptools.h b/app/test/testmaptools.h
index deb24e0c4..e06c748aa 100644
--- a/app/test/testmaptools.h
+++ b/app/test/testmaptools.h
@@ -29,6 +29,7 @@ class TestMapTools : public QObject
void testSnapping();
void testSplitting();
void testRecording();
+ void testMeasuring();
void testExistingVertices();
void testMidSegmentVertices();
diff --git a/app/test/testutilsfunctions.cpp b/app/test/testutilsfunctions.cpp
index 603d8e319..564b8458b 100644
--- a/app/test/testutilsfunctions.cpp
+++ b/app/test/testutilsfunctions.cpp
@@ -856,35 +856,93 @@ void TestUtilsFunctions::testParsePositionUpdates()
}
}
-void TestUtilsFunctions::testFormatDistanceInDistanceUnit()
+void TestUtilsFunctions::testFormatDistanceInProjectUnit()
{
- QString dist2str = mUtils->formatDistanceInProjectUnit( 1222.234, 2, Qgis::DistanceUnit::Meters );
+ QgsProject *project = QgsProject::instance();
+ QVERIFY( project != nullptr );
+
+ // Set the project distance units to meters
+ project->setDistanceUnits( Qgis::DistanceUnit::Meters );
+
+ QString dist2str = mUtils->formatDistanceInProjectUnit( 1222.234, 2, project );
QVERIFY( dist2str == "1222.23 m" );
- dist2str = mUtils->formatDistanceInProjectUnit( 1222.234, 1, Qgis::DistanceUnit::Meters );
+ dist2str = mUtils->formatDistanceInProjectUnit( 1222.234, 1, project );
QVERIFY( dist2str == "1222.2 m" );
- dist2str = mUtils->formatDistanceInProjectUnit( 1222.234, 0, Qgis::DistanceUnit::Meters );
+ dist2str = mUtils->formatDistanceInProjectUnit( 1222.234, 0, project );
QVERIFY( dist2str == "1222 m" );
- dist2str = mUtils->formatDistanceInProjectUnit( 700.22, 1, Qgis::DistanceUnit::Meters );
+ dist2str = mUtils->formatDistanceInProjectUnit( 700.22, 1, project );
QVERIFY( dist2str == "700.2 m" );
- dist2str = mUtils->formatDistanceInProjectUnit( 0.22, 0, Qgis::DistanceUnit::Meters );
+ dist2str = mUtils->formatDistanceInProjectUnit( 0.22, 0, project );
QVERIFY( dist2str == "0 m" );
- dist2str = mUtils->formatDistanceInProjectUnit( -0.22, 0, Qgis::DistanceUnit::Meters );
+ dist2str = mUtils->formatDistanceInProjectUnit( -0.22, 0, project );
QVERIFY( dist2str == "-0 m" );
- dist2str = mUtils->formatDistanceInProjectUnit( 1.222234, 2, Qgis::DistanceUnit::Kilometers );
+ // Change project distance units to kilometers
+ project->setDistanceUnits( Qgis::DistanceUnit::Kilometers );
+
+ dist2str = mUtils->formatDistanceInProjectUnit( 1.222234, 2, project );
QVERIFY( dist2str == "0.00 km" );
- dist2str = mUtils->formatDistanceInProjectUnit( 6000, 1, Qgis::DistanceUnit::Feet );
+ // Change project distance units to feet
+ project->setDistanceUnits( Qgis::DistanceUnit::Feet );
+
+ dist2str = mUtils->formatDistanceInProjectUnit( 6000, 1, project );
QVERIFY( dist2str == "19685.0 ft" );
- dist2str = mUtils->formatDistanceInProjectUnit( 5, 1, Qgis::DistanceUnit::Feet );
+ dist2str = mUtils->formatDistanceInProjectUnit( 5, 1, project );
QVERIFY( dist2str == "16.4 ft" );
- dist2str = mUtils->formatDistanceInProjectUnit( 7000, 1, Qgis::DistanceUnit::Feet );
+ dist2str = mUtils->formatDistanceInProjectUnit( 7000, 1, project );
QVERIFY( dist2str == "22965.9 ft" );
}
+
+
+void TestUtilsFunctions::testFormatAreaInProjectUnit()
+{
+ QgsProject *project = QgsProject::instance();
+ QVERIFY( project != nullptr );
+
+ // Set project area units to square meters
+ project->setAreaUnits( Qgis::AreaUnit::SquareMeters );
+
+ QString area2str = mUtils->formatAreaInProjectUnit( 1500.234, 2, project );
+ QVERIFY( area2str == "1500.23 m²" );
+
+ area2str = mUtils->formatAreaInProjectUnit( 1500.234, 1, project );
+ QVERIFY( area2str == "1500.2 m²" );
+
+ area2str = mUtils->formatAreaInProjectUnit( 1500.234, 0, project );
+ QVERIFY( area2str == "1500 m²" );
+
+ area2str = mUtils->formatAreaInProjectUnit( 500.22, 1, project );
+ QVERIFY( area2str == "500.2 m²" );
+
+ area2str = mUtils->formatAreaInProjectUnit( 0.22, 0, project );
+ QVERIFY( area2str == "0 m²" );
+
+ area2str = mUtils->formatAreaInProjectUnit( -0.22, 0, project );
+ QVERIFY( area2str == "-0 m²" );
+
+ // Change project area units to square kilometers
+ project->setAreaUnits( Qgis::AreaUnit::SquareKilometers );
+
+ area2str = mUtils->formatAreaInProjectUnit( 1.222234, 2, project );
+ QVERIFY( area2str == "0.00 km²" );
+
+ // Change project area units to acres
+ project->setAreaUnits( Qgis::AreaUnit::Acres );
+
+ area2str = mUtils->formatAreaInProjectUnit( 6000, 1, project );
+ QVERIFY( area2str == "1.5 ac" );
+
+ area2str = mUtils->formatAreaInProjectUnit( 5, 1, project );
+ QVERIFY( area2str == "0.0 ac" );
+
+ area2str = mUtils->formatAreaInProjectUnit( 7000, 1, project );
+ QVERIFY( area2str == "1.7 ac" );
+}
diff --git a/app/test/testutilsfunctions.h b/app/test/testutilsfunctions.h
index f74cf140f..75b497c78 100644
--- a/app/test/testutilsfunctions.h
+++ b/app/test/testutilsfunctions.h
@@ -48,7 +48,8 @@ class TestUtilsFunctions: public QObject
void testInvalidGeometryWarning();
void testAttribution();
void testParsePositionUpdates();
- void testFormatDistanceInDistanceUnit();
+ void testFormatDistanceInProjectUnit();
+ void testFormatAreaInProjectUnit();
private:
void testFormatDuration( const QDateTime &t0, qint64 diffSecs, const QString &expectedResult );
diff --git a/gallery/qml/pages/ButtonsPage.qml b/gallery/qml/pages/ButtonsPage.qml
index 97fabbb3e..4e0bc636e 100644
--- a/gallery/qml/pages/ButtonsPage.qml
+++ b/gallery/qml/pages/ButtonsPage.qml
@@ -87,8 +87,24 @@ ScrollView {
iconSourceLeft: __style.uploadIcon
}
- MMButton {
- text: "Primary flex width (no witdth set)"
+ Row {
+ width: parent.width
+ spacing: 20
+
+ MMButton {
+ text: "Primary flex width (no witdth set)"
+ }
+
+ MMButton {
+ text: "Small Primary"
+ size: MMButton.Sizes.Small
+ }
+
+ MMButton {
+ text: "Small Primary with Icon"
+ iconSourceLeft: __style.syncIcon
+ size: MMButton.Sizes.Small
+ }
}
MMListSpacer { height: __style.margin20 }
@@ -142,11 +158,29 @@ ScrollView {
iconSourceRight: __style.arrowLinkRightIcon
}
- MMButton {
- type: MMButton.Types.Secondary
- text: "Secondary flex width (no witdth set)"
- iconSourceLeft: __style.uploadIcon
- iconSourceRight: __style.uploadIcon
+ Row {
+ width: parent.width
+ spacing: 20
+
+ MMButton {
+ type: MMButton.Types.Secondary
+ text: "Secondary flex width (no witdth set)"
+ iconSourceLeft: __style.uploadIcon
+ iconSourceRight: __style.uploadIcon
+ }
+
+ MMButton {
+ text: "Small Secondary"
+ type: MMButton.Types.Secondary
+ size: MMButton.Sizes.Small
+ }
+
+ MMButton {
+ text: "Small Secondary with Icon"
+ type: MMButton.Types.Secondary
+ iconSourceLeft: __style.syncIcon
+ size: MMButton.Sizes.Small
+ }
}
MMListSpacer { height: __style.margin20 }
@@ -200,11 +234,29 @@ ScrollView {
iconSourceRight: __style.arrowLinkRightIcon
}
- MMButton {
- type: MMButton.Types.Tertiary
- text: "Tertiary flex width (no witdth set)"
- fontColor: __style.nightColor
- iconSourceLeft: __style.globeIcon
+ Row {
+ width: parent.width
+ spacing: 20
+
+ MMButton {
+ type: MMButton.Types.Tertiary
+ text: "Tertiary flex width (no witdth set)"
+ fontColor: __style.nightColor
+ iconSourceLeft: __style.globeIcon
+ }
+
+ MMButton {
+ text: "Small Tertiary"
+ type: MMButton.Types.Tertiary
+ size: MMButton.Sizes.Small
+ }
+
+ MMButton {
+ text: "Small Tertiary with Icon"
+ type: MMButton.Types.Tertiary
+ iconSourceLeft: __style.syncIcon
+ size: MMButton.Sizes.Small
+ }
}
}
}
diff --git a/gallery/qml/pages/IconsPage.qml b/gallery/qml/pages/IconsPage.qml
index a9d56b6dd..999d34c26 100644
--- a/gallery/qml/pages/IconsPage.qml
+++ b/gallery/qml/pages/IconsPage.qml
@@ -76,6 +76,8 @@ ScrollView {
GalleryComponents.IconLine { name: "morePhotosIcon"; source: __style.morePhotosIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
GalleryComponents.IconLine { name: "plusIcon"; source: __style.plusIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
GalleryComponents.IconLine { name: "positionTrackingIcon"; source: __style.positionTrackingIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
+ GalleryComponents.IconLine { name: "measurementToolIcon"; source: __style.measurementToolIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
+ GalleryComponents.IconLine { name: "closeShapeIcon"; source: __style.closeShapeIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
GalleryComponents.IconLine { name: "qrCodeIcon"; source: __style.qrCodeIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
GalleryComponents.IconLine { name: "satelliteIcon"; source: __style.satelliteIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }
GalleryComponents.IconLine { name: "searchIcon"; source: __style.searchIcon; showRect: checkboxBorder.checked; invertColors: checkboxColor.checked }