39#include <QPainterPath>
44#include "moc_qgselevationcontrollerwidget.cpp"
46using namespace Qt::StringLiterals;
51 QVBoxLayout *vl =
new QVBoxLayout();
52 vl->setContentsMargins( 0, 0, 0, 0 );
54 mConfigureButton =
new QToolButton();
55 mConfigureButton->setPopupMode( QToolButton::InstantPopup );
57 QHBoxLayout *hl =
new QHBoxLayout();
58 hl->setContentsMargins( 0, 0, 0, 0 );
59 hl->addWidget( mConfigureButton );
62 mMenu =
new QMenu(
this );
63 mConfigureButton->setMenu( mMenu );
65 mSettingsAction =
new QgsElevationControllerSettingsAction( mMenu );
66 mMenu->addAction( mSettingsAction );
68 mInvertDirectionAction =
new QAction( tr(
"Invert Direction" ),
this );
69 mInvertDirectionAction->setCheckable(
true );
70 mMenu->addAction( mInvertDirectionAction );
72 QMenu *limitsMenu = mSettingsAction->limitsMenu();
74 QAction *limitsFromProjectRangeAction = limitsMenu->addAction( tr(
"Project Elevation Range" ) );
75 connect( limitsFromProjectRangeAction, &QAction::triggered,
this, [
this] {
77 if (
range.isInfinite() )
85 QAction *limitsFromProjectLayersAction = limitsMenu->addAction( tr(
"Project Layers" ) );
86 connect( limitsFromProjectLayersAction, &QAction::triggered,
this, [
this] { setLimitsFromLayers(
QgsProject::instance()->mapLayers().values() ); } );
88 QAction *limitsFromCurrentLayerAction = limitsMenu->addAction( tr(
"Current Layer" ) );
89 connect( limitsFromCurrentLayerAction, &QAction::triggered,
this, [
this] {
91 setLimitsFromLayers( { mMapCanvas->currentLayer() } );
95 connect( limitsMenu, &QMenu::aboutToShow,
this, [
this, limitsFromProjectRangeAction, limitsFromProjectLayersAction, limitsFromCurrentLayerAction] {
96 limitsFromProjectRangeAction->setEnabled( !
QgsProject::instance()->elevationProperties()->elevationRange().isInfinite() );
99 limitsFromProjectLayersAction->setEnabled( std::any_of( projectLayers.begin(), projectLayers.end(), [](
QgsMapLayer *layer ) { return layerHasElevation( layer ); } ) );
101 limitsFromCurrentLayerAction->setEnabled( mMapCanvas && layerHasElevation( mMapCanvas->currentLayer() ) );
106 connect( mSettingsAction, &QgsElevationControllerSettingsAction::fullRangeRequested,
this, [
this] {
112 mSlider->setFlippedDirection(
true );
113 mSlider->setRangeLimits( 0, 100000 );
114 mSliderLabels =
new QgsElevationControllerLabels();
116 QHBoxLayout *hlSlider =
new QHBoxLayout();
117 hlSlider->setContentsMargins( 0, 0, 0, 0 );
118 hlSlider->setSpacing( 2 );
119 hlSlider->addWidget( mSlider );
120 hlSlider->addWidget( mSliderLabels, 1 );
121 hlSlider->addStretch();
122 vl->addLayout( hlSlider );
124 setCursor( Qt::ArrowCursor );
133 if ( !
range.isInfinite() )
138 if ( mBlockSliderChanges )
145 mBlockSliderChanges++;
146 mSlider->setRange(
static_cast<int>( std::floor( snapped.
lower() * mSliderPrecision ) ),
static_cast<int>( std::ceil( snapped.
upper() * mSliderPrecision ) ) );
147 mBlockSliderChanges--;
149 if ( snapped == mCurrentRange )
152 mCurrentRange = snapped;
154 mSliderLabels->setRange( mCurrentRange );
157 connect( mMenu, &QMenu::aboutToShow,
this, [
this]() {
158 mSettingsAction->setLimits( mRangeLimits );
159 mSettingsAction->updateRangeSize(
range().upper() -
range().lower() );
162 connect( mInvertDirectionAction, &QAction::toggled,
this, [
this](
bool inverted ) {
163 mSlider->setFlippedDirection( !inverted );
164 mSliderLabels->setInverted( inverted );
176 QWidget::resizeEvent( event );
182 return mCurrentRange;
211 if ( mMapCanvas &&
QgsProject::instance()->elevationProperties()->elevationRange().isInfinite() )
212 setLimitsFromLayers( mMapCanvas->layers(
true ) );
215bool QgsElevationControllerWidget::layerHasElevation(
QgsMapLayer *layer )
220void QgsElevationControllerWidget::setLimitsFromLayers(
const QList<QgsMapLayer *> &layers )
223 const QgsTemporaryCursorOverride waitCursor( Qt::WaitCursor );
228void QgsElevationControllerWidget::setLimitsFromRange(
const QgsDoubleRange &range )
241 if ( newRange == mCurrentRange )
244 mCurrentRange = newRange;
245 mBlockSliderChanges =
true;
246 mSlider->setRange(
static_cast<int>( std::floor( mCurrentRange.lower() * mSliderPrecision ) ),
static_cast<int>( std::ceil( mCurrentRange.upper() * mSliderPrecision ) ) );
247 mBlockSliderChanges =
false;
250 mSliderLabels->setRange( mCurrentRange );
258 mRangeLimits = limits;
259 mSettingsAction->setLimits( mRangeLimits );
261 const double limitRange = limits.
upper() - limits.
lower();
264 mSliderPrecision = std::max( 1000, mSlider->height() ) / limitRange;
269 mSnapDecimals = mSnapInterval > 0 ? std::max( 0, -
static_cast<int>( std::floor( std::log10( mSnapInterval ) ) ) ) : 0;
271 mBlockSliderChanges =
true;
272 mSlider->setRangeLimits(
static_cast<int>( std::floor( limits.
lower() * mSliderPrecision ) ),
static_cast<int>( std::ceil( limits.
upper() * mSliderPrecision ) ) );
275 if ( mFixedRangeSize >= 0 )
276 mSlider->setFixedRangeSize(
static_cast<int>( std::round( mFixedRangeSize * mSliderPrecision ) ) );
279 double newCurrentLower = std::max( mCurrentRange.lower(), limits.
lower() );
280 double newCurrentUpper = std::min( mCurrentRange.upper(), limits.
upper() );
281 if ( mFixedRangeSize >= 0 )
284 const QgsDoubleRange fitted = fixedSizeRangeFrom( newCurrentLower );
285 newCurrentLower = fitted.
lower();
286 newCurrentUpper = fitted.
upper();
288 const bool rangeHasChanged = newCurrentLower != mCurrentRange.lower() || newCurrentUpper != mCurrentRange.upper();
290 mSlider->setRange(
static_cast<int>( std::floor( newCurrentLower * mSliderPrecision ) ),
static_cast<int>( std::ceil( newCurrentUpper * mSliderPrecision ) ) );
291 mCurrentRange =
QgsDoubleRange( newCurrentLower, newCurrentUpper );
292 mBlockSliderChanges =
false;
293 if ( rangeHasChanged )
298 mSettingsAction->updateRangeSize( mCurrentRange.upper() - mCurrentRange.lower() );
301 mSliderLabels->setLimits( mRangeLimits );
302 mSliderLabels->setRange( mCurrentRange );
307 const double lower = snapValue(
range.
lower() );
310 if ( mFixedRangeSize >= 0 )
311 return fixedSizeRangeFrom( lower );
316double QgsElevationControllerWidget::snapValue(
double value )
const
318 if ( mSnapInterval <= 0 )
319 return std::clamp( value, mRangeLimits.lower(), mRangeLimits.upper() );
321 double snapped = std::round( value / mSnapInterval ) * mSnapInterval;
322 if ( mSnapDecimals > 0 )
325 snapped =
qgsRound( snapped, mSnapDecimals );
329 for (
double elevation : mSignificantElevations )
331 if ( std::fabs( elevation - value ) < std::fabs( snapped - value ) )
335 return std::clamp( snapped, mRangeLimits.lower(), mRangeLimits.upper() );
340 return QgsDoubleRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision );
343QgsDoubleRange QgsElevationControllerWidget::fixedSizeRangeFrom(
double lower )
const
345 double upper =
lower + mFixedRangeSize;
346 if ( upper > mRangeLimits.upper() )
348 upper = mRangeLimits.upper();
349 lower = std::max( upper - mFixedRangeSize, mRangeLimits.lower() );
352 return QgsDoubleRange( lower, upper );
355void QgsElevationControllerWidget::updateWidgetMask()
363 QRegion reg( frameGeometry() );
364 reg -= QRegion( geometry() );
365 reg += childrenRegion();
371 return mFixedRangeSize;
376 if ( size == mFixedRangeSize )
379 mFixedRangeSize = size;
380 if ( mFixedRangeSize < 0 )
382 mSlider->setFixedRangeSize( -1 );
386 mSlider->setFixedRangeSize(
static_cast<int>( std::round( mFixedRangeSize * mSliderPrecision ) ) );
389 mSettingsAction->setFixedRangeSize( mFixedRangeSize );
396 mInvertDirectionAction->setChecked( inverted );
401 mSignificantElevations = elevations;
402 mSliderLabels->setSignificantElevations( elevations );
409QgsElevationControllerLabels::QgsElevationControllerLabels( QWidget *parent )
413 QFont smallerFont = font();
414 int fontSize = smallerFont.pointSize();
416 fontSize = std::max( fontSize - 1, 8 );
418 fontSize = std::max( fontSize - 2, 7 );
420 smallerFont.setPointSize( fontSize );
421 setFont( smallerFont );
423 const QFontMetrics fm( smallerFont );
424 setMinimumWidth( fm.horizontalAdvance(
'0' ) * 5 );
425 setAttribute( Qt::WA_TransparentForMouseEvents );
428void QgsElevationControllerLabels::paintEvent( QPaintEvent * )
430 QStyleOptionSlider styleOption;
431 styleOption.initFrom(
this );
433 const QRect sliderRect = style()->subControlRect( QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle,
this );
434 const int sliderHeight = sliderRect.height();
437 const QFontMetrics fm( f );
439 const int left = rect().left() + 2;
441 const double limitRange = mLimits.upper() - mLimits.lower();
442 const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange;
443 const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange;
446 ? ( std::min(
static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ), rect().bottom() - fm.descent() ) )
447 : ( std::max( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * lowerFraction - fm.descent() ) ), rect().top() + fm.ascent() ) );
450 ? ( std::max(
static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ), rect().top() + fm.ascent() ) )
451 : ( std::min(
static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * upperFraction + fm.ascent() ) ), rect().bottom() - fm.descent() ) );
453 const bool lowerIsCloseToLimit = !mInverted ? ( lowerY + fm.height() > rect().bottom() - fm.descent() ) : ( lowerY - fm.height() < rect().top() + fm.ascent() );
454 const bool upperIsCloseToLimit = !mInverted ? ( upperY - fm.height() < rect().top() + fm.ascent() ) : ( upperY + fm.height() > rect().bottom() - fm.descent() );
455 const bool lowerIsCloseToUpperLimit = !mInverted ? ( lowerY - fm.height() < rect().top() + fm.ascent() ) : ( lowerY + fm.height() > rect().bottom() - fm.descent() );
461 for (
double value : std::as_const( mSignificantElevations ) )
463 const double valueFraction = ( value - mLimits.lower() ) / limitRange;
464 const double verticalCenter
466 ? ( std::min(
static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ), rect().bottom() - fm.descent() ) )
467 : ( std::max( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ), rect().top() + fm.ascent() ) );
469 const bool valueIsCloseToLower = verticalCenter + fm.height() > lowerY && verticalCenter - fm.height() < lowerY;
470 if ( valueIsCloseToLower )
473 const bool valueIsCloseToUpper = verticalCenter + fm.height() > upperY && verticalCenter - fm.height() < upperY;
474 if ( valueIsCloseToUpper )
477 const bool valueIsCloseToLowerLimit = !mInverted ? ( verticalCenter + fm.height() > rect().bottom() - fm.descent() ) : ( verticalCenter - fm.height() < rect().top() + fm.ascent() );
478 if ( valueIsCloseToLowerLimit )
481 const bool valueIsCloseToUpperLimit = !mInverted ? ( verticalCenter - fm.height() < rect().top() + fm.ascent() ) : ( verticalCenter + fm.height() > rect().bottom() - fm.descent() );
482 if ( valueIsCloseToUpperLimit )
485 path.addText( left, verticalCenter, f, locale.toString( value ) );
488 if ( mLimits.lower() > std::numeric_limits<double>::lowest() )
490 if ( lowerIsCloseToLimit )
493 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
498 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
500 path.addText( left, !mInverted ? ( rect().bottom() - fm.descent() ) : ( rect().top() + fm.ascent() ), f, locale.toString( mLimits.lower() ) );
504 if ( mLimits.upper() < std::numeric_limits<double>::max() )
506 if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) )
508 if ( !lowerIsCloseToUpperLimit )
511 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
516 if ( upperIsCloseToLimit )
519 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
524 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
526 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
532 p.setRenderHint( QPainter::Antialiasing,
true );
533 const QColor bufferColor = palette().color( QPalette::Window );
534 const QColor textColor = palette().color( QPalette::WindowText );
535 QPen pen( bufferColor );
536 pen.setJoinStyle( Qt::RoundJoin );
537 pen.setCapStyle( Qt::RoundCap );
540 p.setBrush( Qt::NoBrush );
542 p.setPen( Qt::NoPen );
543 p.setBrush( QBrush( textColor ) );
548void QgsElevationControllerLabels::setLimits(
const QgsDoubleRange &limits )
550 if ( limits == mLimits )
553 const QFontMetrics fm( font() );
554 const int maxChars = std::max( QLocale().toString( std::floor( limits.
lower() ) ).length(), QLocale().toString( std::floor( limits.
upper() ) ).length() ) + 3;
555 setMinimumWidth( fm.horizontalAdvance(
'0' ) * maxChars );
561void QgsElevationControllerLabels::setRange(
const QgsDoubleRange &range )
563 if ( range == mRange )
570void QgsElevationControllerLabels::setInverted(
bool inverted )
572 if ( inverted == mInverted )
575 mInverted = inverted;
579void QgsElevationControllerLabels::setSignificantElevations(
const QList<double> &elevations )
581 if ( elevations == mSignificantElevations )
584 mSignificantElevations = elevations;
592QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent )
593 : QWidgetAction( parent )
594 , mMenu( qobject_cast<QMenu *>( parent ) )
596 QGridLayout *gLayout =
new QGridLayout();
597 gLayout->setContentsMargins( 3, 2, 3, 2 );
599 QLabel *limitsLabel =
new QLabel( tr(
"Limits" ) );
600 limitsLabel->setToolTip( tr(
"Lowest and highest elevations which can be selected with the slider" ) );
601 gLayout->addWidget( limitsLabel, 0, 0 );
603 mLowerSpin =
new QgsDoubleSpinBox();
604 mLowerSpin->setDecimals( 4 );
605 mLowerSpin->setMinimum( -999999999.0 );
606 mLowerSpin->setMaximum( 999999999.0 );
607 mLowerSpin->setShowClearButton(
false );
608 mLowerSpin->setKeyboardTracking(
false );
609 mLowerSpin->setToolTip( tr(
"Lowest elevation which can be selected" ) );
611 gLayout->addWidget( mLowerSpin, 0, 1 );
613 QLabel *toLabel =
new QLabel( tr(
"to" ) );
614 gLayout->addWidget( toLabel, 0, 2 );
616 mUpperSpin =
new QgsDoubleSpinBox();
617 mUpperSpin->setDecimals( 4 );
618 mUpperSpin->setMinimum( -999999999.0 );
619 mUpperSpin->setMaximum( 999999999.0 );
620 mUpperSpin->setShowClearButton(
false );
621 mUpperSpin->setKeyboardTracking(
false );
622 mUpperSpin->setToolTip( tr(
"Highest elevation which can be selected" ) );
624 gLayout->addWidget( mUpperSpin, 0, 3 );
626 mLimitsButton =
new QToolButton();
628 mLimitsButton->setPopupMode( QToolButton::InstantPopup );
629 mLimitsButton->setToolTip( tr(
"Take the elevation limits from the project or the layers" ) );
630 mLimitsMenu =
new QMenu( mLimitsButton );
631 mLimitsButton->setMenu( mLimitsMenu );
633 gLayout->addWidget( mLimitsButton, 0, 4 );
635 const QString rangeToolTip = tr(
636 "Size of the elevation range shown in the map, i.e. the difference between "
637 "the upper and the lower handle of the slider. Lock it to keep this size while "
638 "moving the slider, or clear it to show the full range."
641 QLabel *rangeLabel =
new QLabel( tr(
"Range" ) );
642 rangeLabel->setToolTip( rangeToolTip );
643 gLayout->addWidget( rangeLabel, 1, 0 );
645 mSizeSpin =
new QgsDoubleSpinBox();
646 mSizeSpin->setDecimals( 4 );
647 mSizeSpin->setMinimum( 0.0 );
648 mSizeSpin->setMaximum( 999999999.0 );
649 mSizeSpin->setClearValue( 0, tr(
"Full range" ) );
650 mSizeSpin->setKeyboardTracking(
false );
651 mSizeSpin->setToolTip( rangeToolTip );
653 gLayout->addWidget( mSizeSpin, 1, 1, 1, 3 );
655 mLockButton =
new QToolButton();
657 mLockButton->setCheckable(
true );
658 mLockButton->setToolTip( tr(
"Lock the elevation range to a fixed size" ) );
660 gLayout->addWidget( mLockButton, 1, 4 );
663 gLayout->setColumnStretch( 1, 1 );
664 gLayout->setColumnStretch( 3, 1 );
666 const auto emitLimits = [
this] { emit limitsChanged( QgsDoubleRange( mLowerSpin->value(), mUpperSpin->value() ) ); };
667 connect( mLowerSpin, qOverload<double>( &QgsDoubleSpinBox::valueChanged ),
this, emitLimits );
668 connect( mUpperSpin, qOverload<double>( &QgsDoubleSpinBox::valueChanged ),
this, emitLimits );
670 connect( mSizeSpin, qOverload<double>( &QgsDoubleSpinBox::valueChanged ),
this, [
this](
double size ) {
671 if ( mSizeSpin->isCleared() )
674 emit fullRangeRequested();
676 else if ( mLockButton->isChecked() )
681 connect( mLockButton, &QToolButton::toggled,
this, [
this](
bool locked ) { emit
fixedRangeSizeChanged( locked ? mSizeSpin->value() : -1 ); } );
683 QWidget *w =
new QWidget();
684 w->setLayout( gLayout );
688 w->installEventFilter(
this );
689 const QList<QWidget *> children = w->findChildren<QWidget *>();
690 for ( QWidget *child : children )
691 child->installEventFilter(
this );
693 connect(
this, &QAction::hovered,
this, &QgsElevationControllerSettingsAction::onHover );
695 setDefaultWidget( w );
698QMenu *QgsElevationControllerSettingsAction::limitsMenu()
703void QgsElevationControllerSettingsAction::setLimits(
const QgsDoubleRange &limits )
705 const QSignalBlocker blockLower( mLowerSpin );
706 const QSignalBlocker blockUpper( mUpperSpin );
708 mLowerSpin->setMaximum( limits.
upper() );
709 mUpperSpin->setMinimum( limits.
lower() );
710 mLowerSpin->setValue( limits.
lower() );
711 mUpperSpin->setValue( limits.
upper() );
714void QgsElevationControllerSettingsAction::setFixedRangeSize(
double size )
717 const QSignalBlocker blockLock( mLockButton );
718 mLockButton->setChecked( size >= 0 );
723 const QSignalBlocker blockSpin( mSizeSpin );
724 mSizeSpin->setValue( size );
728void QgsElevationControllerSettingsAction::updateRangeSize(
double size )
731 if ( mLockButton->isChecked() )
734 const QSignalBlocker blockSpin( mSizeSpin );
735 mSizeSpin->setValue( size );
738bool QgsElevationControllerSettingsAction::eventFilter( QObject *watched, QEvent *event )
740 switch ( event->type() )
746 case QEvent::KeyPress:
748 const QKeyEvent *keyEvent =
static_cast<QKeyEvent *
>( event );
749 if ( keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter )
755 spin = qobject_cast<QgsDoubleSpinBox *>( watched->parent() );
759 spin->interpretText();
760 if ( spin == mSizeSpin && !mSizeSpin->isCleared() )
761 mLockButton->setChecked(
true );
771 return QWidgetAction::eventFilter( watched, event );
774void QgsElevationControllerSettingsAction::onHover()
779 if ( mSuppressRecurse || !mMenu )
782 mSuppressRecurse =
true;
783 mMenu->setActiveAction(
this );
784 mSuppressRecurse =
false;
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
QgsRange which stores a range of double values.
bool isInfinite() const
Returns true if the range consists of all possible values.
The QgsSpinBox is a spin box with a clear button that will set the value to the defined clear value.
static QgsDoubleRange calculateZRangeForLayers(const QList< QgsMapLayer * > &layers)
Calculates the elevation range for the specified layers.
Map canvas is a class for displaying all GIS data types on a canvas.
virtual bool hasElevation() const
Returns true if the layer has an elevation or z component.
Base class for all map layer types.
virtual QgsMapLayerElevationProperties * elevationProperties()
Returns the layer's elevation properties.
static QgsDoubleRange roundedRange(const QgsDoubleRange &range)
Expands a range outward to round values, e.g.
static double roundingInterval(double span, int divisions=10)
Returns a round interval, a power of ten, which splits the given span into at least divisions parts.
QgsDoubleRange elevationRange() const
Returns the project's elevation range, which indicates the upper and lower elevation limits associate...
void elevationRangeChanged(const QgsDoubleRange &range)
Emitted when the project's elevation is changed.
static QgsProject * instance()
Returns the QgsProject singleton instance.
const QgsProjectElevationProperties * elevationProperties() const
Returns the project's elevation properties, which contains the project's elevation related settings.
QMap< QString, QgsMapLayer * > mapLayers(const bool validOnly=false) const
Returns a map of all registered layers by layer ID.
A slider control with two interactive endpoints, for interactive selection of a range of values.
void rangeChanged(int minimum, int maximum)
Emitted when the range selected in the widget is changed.
T lower() const
Returns the lower bound of the range.
T upper() const
Returns the upper bound of the range.
int ANALYSIS_EXPORT lower(int n, int i)
Lower function.
double qgsRound(double number, int places)
Returns a double number, rounded (as close as possible) to the specified number of places.