QGIS API Documentation 3.39.0-Master (47f7b3a4989)
Loading...
Searching...
No Matches
qgselevationcontrollerwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgselevationcontrollerwidget.cpp
3 ---------------
4 begin : March 2024
5 copyright : (C) 2024 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "qgsrangeslider.h"
20#include "qgsrange.h"
21#include "qgsproject.h"
23#include "qgsapplication.h"
24#include "qgsdoublespinbox.h"
25
26#include <QVBoxLayout>
27#include <QHBoxLayout>
28#include <QToolButton>
29#include <QEvent>
30#include <QMouseEvent>
31#include <QMenu>
32#include <QPainterPath>
33#include <QLabel>
34
36 : QWidget( parent )
37{
38 QVBoxLayout *vl = new QVBoxLayout();
39 vl->setContentsMargins( 0, 0, 0, 0 );
40
41 mConfigureButton = new QToolButton();
42 mConfigureButton->setPopupMode( QToolButton::InstantPopup );
43 mConfigureButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/settings.svg" ) ) );
44 QHBoxLayout *hl = new QHBoxLayout();
45 hl->setContentsMargins( 0, 0, 0, 0 );
46 hl->addWidget( mConfigureButton );
47 hl->addStretch();
48 vl->addLayout( hl );
49 mMenu = new QMenu( this );
50 mConfigureButton->setMenu( mMenu );
51
52 mSettingsAction = new QgsElevationControllerSettingsAction( mMenu );
53 mMenu->addAction( mSettingsAction );
54 mInvertDirectionAction = new QAction( tr( "Invert Direction" ), this );
55 mInvertDirectionAction->setCheckable( true );
56 mMenu->addAction( mInvertDirectionAction );
57
58 mSettingsAction->sizeSpin()->clear();
59 connect( mSettingsAction->sizeSpin(), qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, [this]( double size )
60 {
61 setFixedRangeSize( size < 0 ? -1 : size );
62 } );
63
64 mMenu->addSeparator();
65
66 mSlider = new QgsRangeSlider( Qt::Vertical );
67 mSlider->setFlippedDirection( true );
68 mSlider->setRangeLimits( 0, 100000 );
69 mSliderLabels = new QgsElevationControllerLabels();
70
71 QHBoxLayout *hlSlider = new QHBoxLayout();
72 hlSlider->setContentsMargins( 0, 0, 0, 0 );
73 hlSlider->setSpacing( 2 );
74 hlSlider->addWidget( mSlider );
75 hlSlider->addWidget( mSliderLabels, 1 );
76 hlSlider->addStretch();
77 vl->addLayout( hlSlider );
78
79 setCursor( Qt::ArrowCursor );
80
81 setLayout( vl );
82 updateWidgetMask();
83
85 // if project doesn't have a range, just default to ANY range!
86 setRangeLimits( projectRange.isInfinite() ? QgsDoubleRange( 0, 100 ) : projectRange );
87 connect( QgsProject::instance()->elevationProperties(), &QgsProjectElevationProperties::elevationRangeChanged, this, [this]( const QgsDoubleRange & range )
88 {
89 if ( !range.isInfinite() )
91 } );
92
93 connect( mSlider, &QgsRangeSlider::rangeChanged, this, [this]( int, int )
94 {
95 if ( mBlockSliderChanges )
96 return;
97
98 emit rangeChanged( range() );
99 mSliderLabels->setRange( range() );
100 } );
101
102 connect( mInvertDirectionAction, &QAction::toggled, this, [this]()
103 {
104 mSlider->setFlippedDirection( !mInvertDirectionAction->isChecked() );
105 mSliderLabels->setInverted( mInvertDirectionAction->isChecked() );
106
107 emit invertedChanged( mInvertDirectionAction->isChecked() );
108 } );
109
110 // default initial value to full range
112 mSliderLabels->setRange( rangeLimits() );
113}
114
116{
117 QWidget::resizeEvent( event );
118 updateWidgetMask();
119}
120
122{
123 // if the current slider range is just the current range, but snapped to the slider precision, then losslessly return the current range
124 const int snappedLower = static_cast< int >( std::floor( mCurrentRange.lower() * mSliderPrecision ) );
125 const int snappedUpper = static_cast< int >( std::ceil( mCurrentRange.upper() * mSliderPrecision ) );
126 if ( snappedLower == mSlider->lowerValue() && snappedUpper == mSlider->upperValue() )
127 return mCurrentRange;
128
129 const QgsDoubleRange sliderRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision );
130 if ( mFixedRangeSize >= 0 )
131 {
132 // adjust range so that it has exactly the fixed width (given slider int precision the slider range
133 // will not have the exact fixed width)
134 if ( sliderRange.upper() + mFixedRangeSize <= mRangeLimits.upper() )
135 return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeSize );
136 else
137 return QgsDoubleRange( sliderRange.upper() - mFixedRangeSize, sliderRange.upper() );
138 }
139 else
140 {
141 return sliderRange;
142 }
143}
144
146{
147 return mRangeLimits;
148}
149
154
156{
157 return mMenu;
158}
159
161{
162 if ( range == mCurrentRange )
163 return;
164
165 mCurrentRange = range;
166 mBlockSliderChanges = true;
167 mSlider->setRange( static_cast< int >( std::floor( range.lower() * mSliderPrecision ) ),
168 static_cast< int >( std::ceil( range.upper() * mSliderPrecision ) ) );
169 mBlockSliderChanges = false;
170 emit rangeChanged( range );
171
172 mSliderLabels->setRange( mCurrentRange );
173}
174
176{
177 if ( limits.isInfinite() )
178 return;
179
180 mRangeLimits = limits;
181
182 const double limitRange = limits.upper() - limits.lower();
183
184 // pick a reasonable slider precision, given that the slider operates in integer values only
185 mSliderPrecision = std::max( 1000, mSlider->height() ) / limitRange;
186
187 mBlockSliderChanges = true;
188 mSlider->setRangeLimits( static_cast< int >( std::floor( limits.lower() * mSliderPrecision ) ),
189 static_cast< int >( std::ceil( limits.upper() * mSliderPrecision ) ) );
190
191 // clip current range to fit limits
192 const double newCurrentLower = std::max( mCurrentRange.lower(), limits.lower() );
193 const double newCurrentUpper = std::min( mCurrentRange.upper(), limits.upper() );
194 const bool rangeHasChanged = newCurrentLower != mCurrentRange.lower() || newCurrentUpper != mCurrentRange.upper();
195
196 mSlider->setRange( static_cast< int >( std::floor( newCurrentLower * mSliderPrecision ) ),
197 static_cast< int >( std::ceil( newCurrentUpper * mSliderPrecision ) ) );
198 mCurrentRange = QgsDoubleRange( newCurrentLower, newCurrentUpper );
199 mBlockSliderChanges = false;
200 if ( rangeHasChanged )
201 emit rangeChanged( mCurrentRange );
202
203 mSliderLabels->setLimits( mRangeLimits );
204}
205
206void QgsElevationControllerWidget::updateWidgetMask()
207{
208 // we want mouse events from this widgets children to be caught, but events
209 // on the widget itself to be ignored and passed to underlying widgets which are NOT THE DIRECT
210 // PARENT of this widget.
211 // this is definitively *****NOT***** possible with event filters, by overriding mouse events, or
212 // with the WA_TransparentForMouseEvents attribute
213
214 QRegion reg( frameGeometry() );
215 reg -= QRegion( geometry() );
216 reg += childrenRegion();
217 setMask( reg );
218}
219
221{
222 return mFixedRangeSize;
223}
224
226{
227 if ( size == mFixedRangeSize )
228 return;
229
230 mFixedRangeSize = size;
231 if ( mFixedRangeSize < 0 )
232 {
233 mSlider->setFixedRangeSize( -1 );
234 }
235 else
236 {
237 mSlider->setFixedRangeSize( static_cast< int >( std::round( mFixedRangeSize * mSliderPrecision ) ) );
238 }
239 if ( mFixedRangeSize != mSettingsAction->sizeSpin()->value() )
240 mSettingsAction->sizeSpin()->setValue( mFixedRangeSize );
241 emit fixedRangeSizeChanged( mFixedRangeSize );
242}
243
245{
246 mInvertDirectionAction->setChecked( inverted );
247}
248
249void QgsElevationControllerWidget::setSignificantElevations( const QList<double> &elevations )
250{
251 mSliderLabels->setSignificantElevations( elevations );
252}
253
254//
255// QgsElevationControllerLabels
256//
258QgsElevationControllerLabels::QgsElevationControllerLabels( QWidget *parent )
259 : QWidget( parent )
260{
261 // Drop the default widget font size by a couple of points
262 QFont smallerFont = font();
263 int fontSize = smallerFont.pointSize();
264#ifdef Q_OS_WIN
265 fontSize = std::max( fontSize - 1, 8 ); // bit less on windows, due to poor rendering of small point sizes
266#else
267 fontSize = std::max( fontSize - 2, 7 );
268#endif
269 smallerFont.setPointSize( fontSize );
270 setFont( smallerFont );
271
272 const QFontMetrics fm( smallerFont );
273 setMinimumWidth( fm.horizontalAdvance( '0' ) * 5 );
274 setAttribute( Qt::WA_TransparentForMouseEvents );
275}
276
277void QgsElevationControllerLabels::paintEvent( QPaintEvent * )
278{
279 QStyleOptionSlider styleOption;
280 styleOption.initFrom( this );
281
282 const QRect sliderRect = style()->subControlRect( QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle, this );
283 const int sliderHeight = sliderRect.height();
284
285 QFont f = font();
286 const QFontMetrics fm( f );
287
288 const int left = rect().left() + 2;
289
290 const double limitRange = mLimits.upper() - mLimits.lower();
291 const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange;
292 const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange;
293 const int lowerY = !mInverted
294 ? ( std::min( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ),
295 rect().bottom() - fm.descent() ) )
296 : ( std::max( static_cast< int >( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * lowerFraction - fm.descent() ) ),
297 rect().top() + fm.ascent() ) );
298 const int upperY = !mInverted ?
299 ( std::max( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ),
300 rect().top() + fm.ascent() ) )
301 : ( std::min( static_cast< int >( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * upperFraction + fm.ascent() ) ),
302 rect().bottom() - fm.descent() ) );
303
304 const bool lowerIsCloseToLimit = !mInverted
305 ? ( lowerY + fm.height() > rect().bottom() - fm.descent() )
306 : ( lowerY - fm.height() < rect().top() + fm.ascent() ) ;
307 const bool upperIsCloseToLimit = !mInverted
308 ? ( upperY - fm.height() < rect().top() + fm.ascent() )
309 : ( upperY + fm.height() > rect().bottom() - fm.descent() ) ;
310 const bool lowerIsCloseToUpperLimit = !mInverted
311 ? ( lowerY - fm.height() < rect().top() + fm.ascent() )
312 : ( lowerY + fm.height() > rect().bottom() - fm.descent() );
313
314 QLocale locale;
315
316 QPainterPath path;
317
318 for ( double value : std::as_const( mSignificantElevations ) )
319 {
320 const double valueFraction = ( value - mLimits.lower() ) / limitRange;
321 const double verticalCenter = !mInverted
322 ? ( std::min( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ),
323 rect().bottom() - fm.descent() ) )
324 : ( std::max( static_cast< int >( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ),
325 rect().top() + fm.ascent() ) );
326
327 const bool valueIsCloseToLower = verticalCenter + fm.height() > lowerY && verticalCenter - fm.height() < lowerY;
328 if ( valueIsCloseToLower )
329 continue;
330
331 const bool valueIsCloseToUpper = verticalCenter + fm.height() > upperY && verticalCenter - fm.height() < upperY;
332 if ( valueIsCloseToUpper )
333 continue;
334
335 const bool valueIsCloseToLowerLimit = !mInverted
336 ? ( verticalCenter + fm.height() > rect().bottom() - fm.descent() )
337 : ( verticalCenter - fm.height() < rect().top() + fm.ascent() ) ;
338 if ( valueIsCloseToLowerLimit )
339 continue;
340
341 const bool valueIsCloseToUpperLimit = !mInverted
342 ? ( verticalCenter - fm.height() < rect().top() + fm.ascent() )
343 : ( verticalCenter + fm.height() > rect().bottom() - fm.descent() ) ;
344 if ( valueIsCloseToUpperLimit )
345 continue;
346
347 path.addText( left, verticalCenter, f, locale.toString( value ) );
348 }
349
350 if ( mLimits.lower() > std::numeric_limits< double >::lowest() )
351 {
352 if ( lowerIsCloseToLimit )
353 {
354 f.setBold( true );
355 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
356 }
357 else
358 {
359 f.setBold( true );
360 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
361 f.setBold( false );
362 path.addText( left, !mInverted ? ( rect().bottom() - fm.descent() ) : ( rect().top() + fm.ascent() ), f, locale.toString( mLimits.lower() ) );
363 }
364 }
365
366 if ( mLimits.upper() < std::numeric_limits< double >::max() )
367 {
368 if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) )
369 {
370 if ( !lowerIsCloseToUpperLimit )
371 {
372 f.setBold( false );
373 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
374 }
375 }
376 else
377 {
378 if ( upperIsCloseToLimit )
379 {
380 f.setBold( true );
381 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
382 }
383 else
384 {
385 f.setBold( true );
386 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
387 f.setBold( false );
388 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
389 }
390 }
391 }
392
393 QPainter p( this );
394 p.setRenderHint( QPainter::Antialiasing, true );
395 const QColor bufferColor = palette().color( QPalette::Window );
396 const QColor textColor = palette().color( QPalette::WindowText );
397 QPen pen( bufferColor );
398 pen.setJoinStyle( Qt::RoundJoin );
399 pen.setCapStyle( Qt::RoundCap );
400 pen.setWidthF( 4 );
401 p.setPen( pen );
402 p.setBrush( Qt::NoBrush );
403 p.drawPath( path );
404 p.setPen( Qt::NoPen );
405 p.setBrush( QBrush( textColor ) );
406 p.drawPath( path );
407 p.end();
408}
409
410void QgsElevationControllerLabels::setLimits( const QgsDoubleRange &limits )
411{
412 if ( limits == mLimits )
413 return;
414
415 const QFontMetrics fm( font() );
416 const int maxChars = std::max( QLocale().toString( std::floor( limits.lower() ) ).length(),
417 QLocale().toString( std::floor( limits.upper() ) ).length() ) + 3;
418 setMinimumWidth( fm.horizontalAdvance( '0' ) * maxChars );
419
420 mLimits = limits;
421 update();
422}
423
424void QgsElevationControllerLabels::setRange( const QgsDoubleRange &range )
425{
426 if ( range == mRange )
427 return;
428
429 mRange = range;
430 update();
431}
432
433void QgsElevationControllerLabels::setInverted( bool inverted )
434{
435 if ( inverted == mInverted )
436 return;
437
438 mInverted = inverted;
439 update();
440}
441
442void QgsElevationControllerLabels::setSignificantElevations( const QList<double> &elevations )
443{
444 if ( elevations == mSignificantElevations )
445 return;
446
447 mSignificantElevations = elevations;
448 update();
449}
450
451//
452// QgsElevationControllerSettingsAction
453//
454
455QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent )
456 : QWidgetAction( parent )
457{
458 QGridLayout *gLayout = new QGridLayout();
459 gLayout->setContentsMargins( 3, 2, 3, 2 );
460
461 QLabel *label = new QLabel( tr( "Fixed Range Size" ) );
462 gLayout->addWidget( label, 0, 0 );
463
464 mSizeSpin = new QgsDoubleSpinBox();
465 mSizeSpin->setDecimals( 4 );
466 mSizeSpin->setMinimum( -1.0 );
467 mSizeSpin->setMaximum( 999999999.0 );
468 mSizeSpin->setClearValue( -1, tr( "Not set" ) );
469 mSizeSpin->setKeyboardTracking( false );
470 mSizeSpin->setToolTip( tr( "Limit elevation range to a fixed size" ) );
471
472 gLayout->addWidget( mSizeSpin, 0, 1 );
473
474 QWidget *w = new QWidget();
475 w->setLayout( gLayout );
476 setDefaultWidget( w );
477}
478
479QgsDoubleSpinBox *QgsElevationControllerSettingsAction::sizeSpin()
480{
481 return mSizeSpin;
482}
483
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.
Definition qgsrange.h:231
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition qgsrange.h:285
The QgsSpinBox is a spin box with a clear button that will set the value to the defined clear value.
double fixedRangeSize() const
Returns the fixed range size, or -1 if no fixed size is set.
QgsRangeSlider * slider()
Returns a reference to the slider component of the widget.
void setSignificantElevations(const QList< double > &elevations)
Sets a list of significant elevations to highlight in the widget.
void resizeEvent(QResizeEvent *event) override
void setRange(const QgsDoubleRange &range)
Sets the current visible range for the widget.
QgsDoubleRange rangeLimits() const
Returns the limits of the elevation range which can be selected by the widget.
void setInverted(bool inverted)
Sets whether the elevation slider should be inverted.
void setFixedRangeSize(double size)
Sets the fixed range size.
void invertedChanged(bool inverted)
Emitted when the elevation filter slider is inverted.
void fixedRangeSizeChanged(double size)
Emitted when the fixed range size is changed from the widget.
void setRangeLimits(const QgsDoubleRange &limits)
Sets the limits of the elevation range which can be selected by the widget.
void rangeChanged(const QgsDoubleRange &range)
Emitted when the visible range from the widget is changed.
QgsElevationControllerWidget(QWidget *parent=nullptr)
Constructor for QgsElevationControllerWidget, with the specified parent widget.
QgsDoubleRange range() const
Returns the current visible range from the widget.
QMenu * menu()
Returns a reference to the widget's configuration menu, which can be used to add actions to the menu.
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.
A slider control with two interactive endpoints, for interactive selection of a range of values.
void setRangeLimits(int minimum, int maximum)
Sets the minimum and maximum range limits for values allowed in the widget.
int upperValue() const
Returns the upper value for the range selected in the widget.
void rangeChanged(int minimum, int maximum)
Emitted when the range selected in the widget is changed.
void setFlippedDirection(bool flipped)
Sets whether the slider has its values flipped.
void setRange(int lower, int upper)
Sets the current range selected in the widget.
int lowerValue() const
Returns the lower value for the range selected in the widget.
void setFixedRangeSize(int size)
Sets the slider's fixed range size.
T lower() const
Returns the lower bound of the range.
Definition qgsrange.h:78
T upper() const
Returns the upper bound of the range.
Definition qgsrange.h:85
int ANALYSIS_EXPORT lower(int n, int i)
Lower function.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:5465