Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[profile tool] Add option to lock the horizontal/vertical scale to
matching scales

When activated, this option ensures that the horizontal and vertical
scales are always kept equal (so that eg a 45 degree slope will appear
as a 45 degree slope in the profile)
  • Loading branch information
nyalldawson committed May 20, 2023
1 parent 5bc7b6f commit 7c7d5dc
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 16 deletions.
Expand Up @@ -204,6 +204,24 @@ Converts a canvas point to the equivalent plot point.
Converts a plot point to the equivalent canvas point.

.. seealso:: :py:func:`canvasPointToPlotPoint`
%End

bool lockAxisScales() const;
%Docstring
Returns ``True`` if the distance and elevation scales are locked to each other.

.. seealso:: :py:func:`setLockAxisScales`

.. versionadded:: 3.32
%End

void setLockAxisScales( bool lock );
%Docstring
Sets whether the distance and elevation scales are locked to each other.

.. seealso:: :py:func:`lockAxisScales`

.. versionadded:: 3.32
%End

signals:
Expand Down
15 changes: 15 additions & 0 deletions src/app/elevation/qgselevationprofilewidget.cpp
Expand Up @@ -59,6 +59,7 @@
const QgsSettingsEntryDouble *QgsElevationProfileWidget::settingTolerance = new QgsSettingsEntryDouble( QStringLiteral( "tolerance" ), QgsSettingsTree::sTreeElevationProfile, 0.1, QStringLiteral( "Tolerance distance for elevation profile plots" ), Qgis::SettingsOptions(), 0 );

const QgsSettingsEntryBool *QgsElevationProfileWidget::settingShowLayerTree = new QgsSettingsEntryBool( QStringLiteral( "show-layer-tree" ), QgsSettingsTree::sTreeElevationProfile, true, QStringLiteral( "Whether the layer tree should be shown for elevation profile plots" ) );
const QgsSettingsEntryBool *QgsElevationProfileWidget::settingLockAxis = new QgsSettingsEntryBool( QStringLiteral( "lock-axis-ratio" ), QgsSettingsTree::sTreeElevationProfile, false, QStringLiteral( "Whether the the distance and elevation axis scales are locked to each other" ) );

QgsElevationProfileWidget::QgsElevationProfileWidget( const QString &name )
: QWidget( nullptr )
Expand All @@ -82,6 +83,8 @@ QgsElevationProfileWidget::QgsElevationProfileWidget( const QString &name )
connect( mCanvas, &QgsElevationProfileCanvas::activeJobCountChanged, this, &QgsElevationProfileWidget::onTotalPendingJobsCountChanged );
connect( mCanvas, &QgsElevationProfileCanvas::canvasPointHovered, this, &QgsElevationProfileWidget::onCanvasPointHovered );

mCanvas->setLockAxisScales( settingLockAxis->value() );

mPanTool = new QgsPlotToolPan( mCanvas );

mLayerTreeView = new QgsAppElevationProfileLayerTreeView( mLayerTree.get() );
Expand Down Expand Up @@ -235,6 +238,12 @@ QgsElevationProfileWidget::QgsElevationProfileWidget( const QString &name )
// Options Menu
mOptionsMenu = new QMenu( this );

mLockRatioAction = new QAction( tr( "Lock Distance/Elevation Scales" ), this );
mLockRatioAction->setCheckable( true );
mLockRatioAction->setChecked( settingLockAxis->value( ) );
connect( mLockRatioAction, &QAction::toggled, this, &QgsElevationProfileWidget::axisScaleLockToggled );
mOptionsMenu->addAction( mLockRatioAction );

mSettingsAction = new QgsElevationProfileWidgetSettingsAction( mOptionsMenu );

mSettingsAction->toleranceSpinBox()->setValue( settingTolerance->value() );
Expand Down Expand Up @@ -641,6 +650,12 @@ void QgsElevationProfileWidget::nudgeCurve( Qgis::BufferSide side )
setProfileCurve( nudgedCurve, false );
}

void QgsElevationProfileWidget::axisScaleLockToggled( bool active )
{
settingLockAxis->setValue( active );
mCanvas->setLockAxisScales( active );
}

void QgsElevationProfileWidget::createOrUpdateRubberBands( )
{
if ( !mRubberBand )
Expand Down
3 changes: 3 additions & 0 deletions src/app/elevation/qgselevationprofilewidget.h
Expand Up @@ -70,6 +70,7 @@ class QgsElevationProfileWidget : public QWidget

static const QgsSettingsEntryDouble *settingTolerance;
static const QgsSettingsEntryBool *settingShowLayerTree;
static const QgsSettingsEntryBool *settingLockAxis;

QgsElevationProfileWidget( const QString &name );
~QgsElevationProfileWidget();
Expand Down Expand Up @@ -104,6 +105,7 @@ class QgsElevationProfileWidget : public QWidget
void nudgeLeft();
void nudgeRight();
void nudgeCurve( Qgis::BufferSide side );
void axisScaleLockToggled( bool active );

private:
QgsElevationProfileCanvas *mCanvas = nullptr;
Expand All @@ -123,6 +125,7 @@ class QgsElevationProfileWidget : public QWidget
QAction *mCaptureCurveFromFeatureAction = nullptr;
QAction *mNudgeLeftAction = nullptr;
QAction *mNudgeRightAction = nullptr;
QAction *mLockRatioAction = nullptr;

QgsDockableWidgetHelper *mDockableWidgetHelper = nullptr;
std::unique_ptr< QgsMapToolProfileCurve > mCaptureCurveMapTool;
Expand Down
3 changes: 3 additions & 0 deletions src/core/plot/qgsplot.cpp
Expand Up @@ -423,6 +423,9 @@ QRectF Qgs2DPlot::interiorPlotArea( QgsRenderContext &context ) const

void Qgs2DPlot::calculateOptimisedIntervals( QgsRenderContext &context )
{
if ( !mSize.isValid() )
return;

// aim for about 40% coverage of label text to available space
constexpr double IDEAL_WIDTH = 0.4;
constexpr double TOLERANCE = 0.04;
Expand Down
119 changes: 103 additions & 16 deletions src/gui/elevation/qgselevationprofilecanvas.cpp
Expand Up @@ -372,6 +372,7 @@ void QgsElevationProfileCanvas::panContentsBy( double dx, double dy )
const double dxPlot = - dxPercent * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() );
const double dyPlot = dyPercent * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() );

// no need to handle axis scale lock here, we aren't changing scales
mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
Expand All @@ -394,6 +395,7 @@ void QgsElevationProfileCanvas::centerPlotOn( double x, double y )
const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;

// no need to handle axis scale lock here, we aren't changing scales
mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
Expand Down Expand Up @@ -494,6 +496,53 @@ void QgsElevationProfileCanvas::setupLayerConnections( QgsMapLayer *layer, bool
}
}

void QgsElevationProfileCanvas::adjustRangeForAxisScaleLock( double &xMinimum, double &xMaximum, double &yMinimum, double &yMaximum ) const
{
// 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 height = horizontalScale * mPlotItem->plotArea().height();
const double deltaHeight = ( yMaximum - yMinimum ) - height;
yMinimum += deltaHeight / 2;
yMaximum -= deltaHeight / 2;
}
else
{
const double width = verticalScale * mPlotItem->plotArea().width();
const double deltaWidth = ( xMaximum - xMinimum ) - width;
xMinimum += deltaWidth / 2;
xMaximum -= deltaWidth / 2;
}
}

bool QgsElevationProfileCanvas::lockAxisScales() const
{
return mLockAxisScales;
}

void QgsElevationProfileCanvas::setLockAxisScales( bool lock )
{
mLockAxisScales = lock;
if ( mLockAxisScales )
{
double xMinimum = mPlotItem->xMinimum();
double xMaximum = mPlotItem->xMaximum();
double yMinimum = mPlotItem->yMinimum();
double yMaximum = mPlotItem->yMaximum();
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
mPlotItem->setXMinimum( xMinimum );
mPlotItem->setXMaximum( xMaximum );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );

refineResults();
mPlotItem->updatePlot();
emit plotAreaChanged();
}
}

QgsPointXY QgsElevationProfileCanvas::snapToPlot( QPoint point )
{
if ( !mCurrentJob || !mSnappingEnabled )
Expand All @@ -510,6 +559,9 @@ QgsPointXY QgsElevationProfileCanvas::snapToPlot( QPoint point )

void QgsElevationProfileCanvas::scalePlot( double xFactor, double yFactor )
{
if ( mLockAxisScales )
yFactor = xFactor;

const double currentWidth = mPlotItem->xMaximum() - mPlotItem->xMinimum();
const double currentHeight = mPlotItem->yMaximum() - mPlotItem->yMinimum();

Expand All @@ -519,10 +571,19 @@ void QgsElevationProfileCanvas::scalePlot( double xFactor, double yFactor )
const double currentCenterX = ( mPlotItem->xMinimum() + mPlotItem->xMaximum() ) * 0.5;
const double currentCenterY = ( mPlotItem->yMinimum() + mPlotItem->yMaximum() ) * 0.5;

mPlotItem->setXMinimum( currentCenterX - newWidth * 0.5 );
mPlotItem->setXMaximum( currentCenterX + newWidth * 0.5 );
mPlotItem->setYMinimum( currentCenterY - newHeight * 0.5 );
mPlotItem->setYMaximum( currentCenterY + newHeight * 0.5 );
double xMinimum = currentCenterX - newWidth * 0.5;
double xMaximum = currentCenterX + newWidth * 0.5;
double yMinimum = currentCenterY - newHeight * 0.5;
double yMaximum = currentCenterY + newHeight * 0.5;
if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
}

mPlotItem->setXMinimum( xMinimum );
mPlotItem->setXMaximum( xMaximum );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );

refineResults();
mPlotItem->updatePlot();
Expand All @@ -533,10 +594,15 @@ void QgsElevationProfileCanvas::zoomToRect( const QRectF &rect )
{
const QRectF intersected = rect.intersected( mPlotItem->plotArea() );

const double minX = ( intersected.left() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
const double maxX = ( intersected.right() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
const double minY = ( mPlotItem->plotArea().bottom() - intersected.bottom() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
const double maxY = ( mPlotItem->plotArea().bottom() - intersected.top() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
double minX = ( intersected.left() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
double maxX = ( intersected.right() - mPlotItem->plotArea().left() ) / mPlotItem->plotArea().width() * ( mPlotItem->xMaximum() - mPlotItem->xMinimum() ) + mPlotItem->xMinimum();
double minY = ( mPlotItem->plotArea().bottom() - intersected.bottom() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();
double maxY = ( mPlotItem->plotArea().bottom() - intersected.top() ) / mPlotItem->plotArea().height() * ( mPlotItem->yMaximum() - mPlotItem->yMinimum() ) + mPlotItem->yMinimum();

if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( minX, maxX, minY, maxY );
}

mPlotItem->setXMinimum( minX );
mPlotItem->setXMaximum( maxX );
Expand Down Expand Up @@ -585,6 +651,7 @@ void QgsElevationProfileCanvas::wheelZoom( QWheelEvent *event )
const double dxPlot = newCenterX - ( mPlotItem->xMaximum() + mPlotItem->xMinimum() ) * 0.5;
const double dyPlot = newCenterY - ( mPlotItem->yMaximum() + mPlotItem->yMinimum() ) * 0.5;

// don't need to handle axis scale lock here, we are always changing axis by the same scale
mPlotItem->setXMinimum( mPlotItem->xMinimum() + dxPlot );
mPlotItem->setXMaximum( mPlotItem->xMaximum() + dxPlot );
mPlotItem->setYMinimum( mPlotItem->yMinimum() + dyPlot );
Expand Down Expand Up @@ -995,30 +1062,45 @@ void QgsElevationProfileCanvas::zoomFull()

const QgsDoubleRange zRange = mCurrentJob->zRange();

double xMinimum = mPlotItem->xMinimum();
double xMaximum = mPlotItem->xMaximum();
double yMinimum = mPlotItem->yMinimum();
double yMaximum = mPlotItem->yMaximum();

if ( zRange.upper() < zRange.lower() )
{
// invalid range, e.g. no features found in plot!
mPlotItem->setYMinimum( 0 );
mPlotItem->setYMaximum( 10 );
yMinimum = 0;
yMaximum = 10;
}
else if ( qgsDoubleNear( zRange.lower(), zRange.upper(), 0.0000001 ) )
{
// corner case ... a zero height plot! Just pick an arbitrary +/- 5 height range.
mPlotItem->setYMinimum( zRange.lower() - 5 );
mPlotItem->setYMaximum( zRange.lower() + 5 );
yMinimum = zRange.lower() - 5;
yMaximum = zRange.lower() + 5;
}
else
{
// add 5% margin to height range
const double margin = ( zRange.upper() - zRange.lower() ) * 0.05;
mPlotItem->setYMinimum( zRange.lower() - margin );
mPlotItem->setYMaximum( zRange.upper() + margin );
yMinimum = zRange.lower() - margin;
yMaximum = zRange.upper() + margin;
}

const double profileLength = profileCurve()->length();
mPlotItem->setXMinimum( 0 );
xMinimum = 0;
// just 2% margin to max distance -- any more is overkill and wasted space
mPlotItem->setXMaximum( profileLength * 1.02 );
xMaximum = profileLength * 1.02;

if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( xMinimum, xMaximum, yMinimum, yMaximum );
}

mPlotItem->setXMinimum( xMinimum );
mPlotItem->setXMaximum( xMaximum );
mPlotItem->setYMinimum( yMinimum );
mPlotItem->setYMaximum( yMaximum );

refineResults();
mPlotItem->updatePlot();
Expand All @@ -1027,6 +1109,11 @@ void QgsElevationProfileCanvas::zoomFull()

void QgsElevationProfileCanvas::setVisiblePlotRange( double minimumDistance, double maximumDistance, double minimumElevation, double maximumElevation )
{
if ( mLockAxisScales )
{
adjustRangeForAxisScaleLock( minimumDistance, maximumDistance, minimumElevation, maximumElevation );
}

mPlotItem->setYMinimum( minimumElevation );
mPlotItem->setYMaximum( maximumElevation );
mPlotItem->setXMinimum( minimumDistance );
Expand Down
20 changes: 20 additions & 0 deletions src/gui/elevation/qgselevationprofilecanvas.h
Expand Up @@ -224,6 +224,22 @@ class GUI_EXPORT QgsElevationProfileCanvas : public QgsPlotCanvas
*/
QgsPointXY plotPointToCanvasPoint( const QgsProfilePoint &point ) const;

/**
* Returns TRUE if the distance and elevation scales are locked to each other.
*
* \see setLockAxisScales()
* \since QGIS 3.32
*/
bool lockAxisScales() const;

/**
* Sets whether the distance and elevation scales are locked to each other.
*
* \see lockAxisScales()
* \since QGIS 3.32
*/
void setLockAxisScales( bool lock );

signals:

/**
Expand Down Expand Up @@ -274,8 +290,12 @@ class GUI_EXPORT QgsElevationProfileCanvas : public QgsPlotCanvas

void setupLayerConnections( QgsMapLayer *layer, bool isDisconnect );

void adjustRangeForAxisScaleLock( double &xMinimum, double &xMaximum, double &yMinimum, double &yMaximum ) const;

QgsScreenHelper *mScreenHelper = nullptr;

bool mLockAxisScales = false;

QgsCoordinateReferenceSystem mCrs;
QgsProject *mProject = nullptr;

Expand Down

0 comments on commit 7c7d5dc

Please sign in to comment.