From d919d8f7763e81a90759b340825f664e01836c22 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 4 Aug 2025 11:17:18 +1000 Subject: [PATCH] [api] Set specific distance:elevation ratio for elevation profile canvas --- .../qgselevationprofilecanvas.sip.in | 29 +++++++++++ .../qgselevationprofilecanvas.sip.in | 29 +++++++++++ .../elevation/qgselevationprofilecanvas.cpp | 35 +++++++++++-- src/gui/elevation/qgselevationprofilecanvas.h | 24 +++++++++ .../python/test_qgselevationprofilecanvas.py | 50 +++++++++++++++++++ 5 files changed, 164 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in b/python/PyQt6/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in index d63ab4f94b3..e5711ba2964 100644 --- a/python/PyQt6/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in +++ b/python/PyQt6/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in @@ -235,6 +235,35 @@ Sets whether the distance and elevation scales are locked to each other. .. seealso:: :py:func:`lockAxisScales` .. versionadded:: 3.32 +%End + + double axisScaleRatio() const; +%Docstring +Returns the current ratio of horizontal (distance) to vertical +(elevation) scale for the plot. + +.. seealso:: :py:func:`setAxisScaleRatio` + +.. versionadded:: 4.0 +%End + + void setAxisScaleRatio( double scale ); +%Docstring +Sets the ratio of horizontal (distance) to vertical (elevation) scale +for the plot. + +E.g. a ``scale`` of 3 indicates a ratio of 3:1 for distance vs +elevation, whereas a scale of 0.3333 indicates a ratio of 1:3 for +distance vs elevation. + +This will immediately update the visible plot area to match the +specified scale. + +.. seealso:: :py:func:`axisScaleRatio` + +.. seealso:: :py:func:`setLockAxisScales` + +.. versionadded:: 4.0 %End Qgis::DistanceUnit distanceUnit() const; diff --git a/python/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in b/python/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in index d63ab4f94b3..e5711ba2964 100644 --- a/python/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in +++ b/python/gui/auto_generated/elevation/qgselevationprofilecanvas.sip.in @@ -235,6 +235,35 @@ Sets whether the distance and elevation scales are locked to each other. .. seealso:: :py:func:`lockAxisScales` .. versionadded:: 3.32 +%End + + double axisScaleRatio() const; +%Docstring +Returns the current ratio of horizontal (distance) to vertical +(elevation) scale for the plot. + +.. seealso:: :py:func:`setAxisScaleRatio` + +.. versionadded:: 4.0 +%End + + void setAxisScaleRatio( double scale ); +%Docstring +Sets the ratio of horizontal (distance) to vertical (elevation) scale +for the plot. + +E.g. a ``scale`` of 3 indicates a ratio of 3:1 for distance vs +elevation, whereas a scale of 0.3333 indicates a ratio of 1:3 for +distance vs elevation. + +This will immediately update the visible plot area to match the +specified scale. + +.. seealso:: :py:func:`axisScaleRatio` + +.. seealso:: :py:func:`setLockAxisScales` + +.. versionadded:: 4.0 %End Qgis::DistanceUnit distanceUnit() const; diff --git a/src/gui/elevation/qgselevationprofilecanvas.cpp b/src/gui/elevation/qgselevationprofilecanvas.cpp index 97dfafc8e13..fec3d90e981 100644 --- a/src/gui/elevation/qgselevationprofilecanvas.cpp +++ b/src/gui/elevation/qgselevationprofilecanvas.cpp @@ -605,16 +605,19 @@ void QgsElevationProfileCanvas::adjustRangeForAxisScaleLock( double &xMinimum, d // ensures that we always "zoom out" to match horizontal/vertical scales const double horizontalScale = ( xMaximum - xMinimum ) / mPlotItem->plotArea().width(); const double verticalScale = ( yMaximum - yMinimum ) / mPlotItem->plotArea().height(); - if ( horizontalScale > verticalScale ) + + const double currentRatio = horizontalScale / verticalScale; + + if ( currentRatio <= mLockedAxisScale ) { - const double height = horizontalScale * mPlotItem->plotArea().height(); + const double height = horizontalScale * mPlotItem->plotArea().height() / mLockedAxisScale; const double deltaHeight = ( yMaximum - yMinimum ) - height; yMinimum += deltaHeight / 2; yMaximum -= deltaHeight / 2; } else { - const double width = verticalScale * mPlotItem->plotArea().width(); + const double width = verticalScale * mPlotItem->plotArea().width() * mLockedAxisScale; const double deltaWidth = ( ( xMaximum - xMinimum ) - width ); xMinimum += deltaWidth / 2; xMaximum -= deltaWidth / 2; @@ -700,6 +703,32 @@ void QgsElevationProfileCanvas::setLockAxisScales( bool lock ) } } +double QgsElevationProfileCanvas::axisScaleRatio() const +{ + const double horizontalScale = ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) * mPlotItem->mXScaleFactor / mPlotItem->plotArea().width(); + const double verticalScale = ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) / mPlotItem->plotArea().height(); + return horizontalScale / verticalScale; +} + +void QgsElevationProfileCanvas::setAxisScaleRatio( double scale ) +{ + mLockedAxisScale = scale; + + double xMinimum = mPlotItem->xMinimum() * mPlotItem->mXScaleFactor; + double xMaximum = mPlotItem->xMaximum() * mPlotItem->mXScaleFactor; + double yMinimum = mPlotItem->yMinimum(); + double yMaximum = mPlotItem->yMaximum(); + adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum ); + mPlotItem->setXMinimum( xMinimum / mPlotItem->mXScaleFactor ); + mPlotItem->setXMaximum( xMaximum / mPlotItem->mXScaleFactor ); + mPlotItem->setYMinimum( yMinimum ); + mPlotItem->setYMaximum( yMaximum ); + + refineResults(); + mPlotItem->updatePlot(); + emit plotAreaChanged(); +} + QgsPointXY QgsElevationProfileCanvas::snapToPlot( QPoint point ) { if ( !mCurrentJob || !mSnappingEnabled ) diff --git a/src/gui/elevation/qgselevationprofilecanvas.h b/src/gui/elevation/qgselevationprofilecanvas.h index f57d4e7020d..231e75e76ed 100644 --- a/src/gui/elevation/qgselevationprofilecanvas.h +++ b/src/gui/elevation/qgselevationprofilecanvas.h @@ -239,6 +239,29 @@ class GUI_EXPORT QgsElevationProfileCanvas : public QgsPlotCanvas */ void setLockAxisScales( bool lock ); + /** + * Returns the current ratio of horizontal (distance) to vertical (elevation) scale + * for the plot. + * + * \see setAxisScaleRatio() + * \since QGIS 4.0 + */ + double axisScaleRatio() const; + + /** + * Sets the ratio of horizontal (distance) to vertical (elevation) scale for the plot. + * + * E.g. a \a scale of 3 indicates a ratio of 3:1 for distance vs elevation, whereas a scale + * of 0.3333 indicates a ratio of 1:3 for distance vs elevation. + * + * This will immediately update the visible plot area to match the specified scale. + * + * \see axisScaleRatio() + * \see setLockAxisScales() + * \since QGIS 4.0 + */ + void setAxisScaleRatio( double scale ); + /** * Returns the distance unit used by the canvas. * @@ -340,6 +363,7 @@ class GUI_EXPORT QgsElevationProfileCanvas : public QgsPlotCanvas QgsScreenHelper *mScreenHelper = nullptr; bool mLockAxisScales = false; + double mLockedAxisScale = 1; QgsCoordinateReferenceSystem mCrs; QgsProject *mProject = nullptr; diff --git a/tests/src/python/test_qgselevationprofilecanvas.py b/tests/src/python/test_qgselevationprofilecanvas.py index c23f8d92331..4faf0ae8a70 100644 --- a/tests/src/python/test_qgselevationprofilecanvas.py +++ b/tests/src/python/test_qgselevationprofilecanvas.py @@ -225,6 +225,56 @@ class TestQgsElevationProfileCanvas(QgisTestCase): canvas.wheelEvent(wheel_event) self.assertEqual(tool.events[-1].type(), QEvent.Type.Wheel) + def test_ratio(self): + """ + Test axis scale ratio logic + """ + canvas = QgsElevationProfileCanvas() + canvas.setCrs(QgsCoordinateReferenceSystem("EPSG:4326")) + canvas.setFrameStyle(0) + # make a 2:1 canvas, to make the maths easier! + canvas.resize(800, 400) + canvas.setProject(QgsProject.instance()) + canvas.show() + self.assertEqual(canvas.width(), 800) + self.assertEqual(canvas.height(), 400) + + canvas.setVisiblePlotRange(100, 200, 50, 150) + # showing 100m distance, 100m elevation + # distance:elevation ratio is 1:2, as canvas is twice as wide as high + self.assertAlmostEqual(canvas.axisScaleRatio(), 0.5, 1) + + canvas.setVisiblePlotRange(100, 300, 50, 150) + # showing 200m distance, 100m elevation + # distance:elevation ratio is 1:1, as canvas is twice as wide as high + self.assertAlmostEqual(canvas.axisScaleRatio(), 1.0, 1) + + canvas.setVisiblePlotRange(100, 500, 50, 150) + # showing 400m distance, 100m elevation + # distance:elevation ratio is 2:1, as canvas is twice as wide as high + self.assertAlmostEqual(canvas.axisScaleRatio(), 2.0, delta=0.1) + + canvas.setAxisScaleRatio(0.5) + self.assertAlmostEqual(canvas.axisScaleRatio(), 0.5, 1) + self.assertAlmostEqual(canvas.visibleDistanceRange().lower(), 248.024, 1) + self.assertAlmostEqual(canvas.visibleDistanceRange().upper(), 351.976, 1) + self.assertAlmostEqual(canvas.visibleElevationRange().lower(), 50.0, 1) + self.assertAlmostEqual(canvas.visibleElevationRange().upper(), 150.0, 1) + + canvas.setAxisScaleRatio(1.0) + self.assertAlmostEqual(canvas.axisScaleRatio(), 1.0, 1) + self.assertAlmostEqual(canvas.visibleDistanceRange().lower(), 248.024, 1) + self.assertAlmostEqual(canvas.visibleDistanceRange().upper(), 351.976, 1) + self.assertAlmostEqual(canvas.visibleElevationRange().lower(), 75.0, 1) + self.assertAlmostEqual(canvas.visibleElevationRange().upper(), 125.0, 1) + + canvas.setAxisScaleRatio(2.0) + self.assertAlmostEqual(canvas.axisScaleRatio(), 2.0, 1) + self.assertAlmostEqual(canvas.visibleDistanceRange().lower(), 248.024, 1) + self.assertAlmostEqual(canvas.visibleDistanceRange().upper(), 351.976, 1) + self.assertAlmostEqual(canvas.visibleElevationRange().lower(), 87.5, 1) + self.assertAlmostEqual(canvas.visibleElevationRange().upper(), 112.5, 1) + if __name__ == "__main__": unittest.main()