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