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