1 /***************************************************************************
2   qgslightswidget.cpp
3   --------------------------------------
4   Date                 : November 2018
5   Copyright            : (C) 2018 by Martin Dobias
6   Email                : wonder dot sk at gmail dot com
7  ***************************************************************************
8  *                                                                         *
9  *   This program is free software; you can redistribute it and/or modify  *
10  *   it under the terms of the GNU General Public License as published by  *
11  *   the Free Software Foundation; either version 2 of the License, or     *
12  *   (at your option) any later version.                                   *
13  *                                                                         *
14  ***************************************************************************/
15 
16 #include "qgslightswidget.h"
17 
18 #include "qgs3dmapsettings.h"
19 #include "qgsapplication.h"
20 #include "qgssettings.h"
21 
22 #include <QMessageBox>
23 #include <QMenu>
24 
QgsLightsWidget(QWidget * parent)25 QgsLightsWidget::QgsLightsWidget( QWidget *parent )
26   : QWidget( parent )
27 {
28   setupUi( this );
29 
30   spinPositionX->setClearValue( 0.0 );
31   spinPositionY->setClearValue( 1000.0 );
32   spinPositionZ->setClearValue( 0.0 );
33   spinIntensity->setClearValue( 1.0 );
34   spinA0->setClearValue( 0.0 );
35   spinA1->setClearValue( 0.0 );
36   spinA2->setClearValue( 0.0 );
37   spinDirectionalIntensity->setClearValue( 1.0 );
38 
39   mLightsModel = new QgsLightsModel( this );
40   mLightsListView->setModel( mLightsModel );
41 
42   connect( mLightsListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsLightsWidget::selectedLightChanged );
43 
44   btnAddLight->setIcon( QIcon( QgsApplication::iconPath( "symbologyAdd.svg" ) ) );
45   btnRemoveLight->setIcon( QIcon( QgsApplication::iconPath( "symbologyRemove.svg" ) ) );
46 
47   dialAzimuth->setMaximum( 359 );
48 
49   QMenu *addLightMenu = new QMenu( this );
50   QAction *addPointLight = new QAction( tr( "Point Light" ), addLightMenu );
51   connect( addPointLight, &QAction::triggered, this, &QgsLightsWidget::onAddLight );
52   addLightMenu->addAction( addPointLight );
53 
54   QAction *addDirectionalLight = new QAction( tr( "Directional Light" ), addLightMenu );
55   connect( addDirectionalLight, &QAction::triggered, this, &QgsLightsWidget::onAddDirectionalLight );
56   addLightMenu->addAction( addDirectionalLight );
57 
58   btnAddLight->setMenu( addLightMenu );
59 
60   connect( btnRemoveLight, &QToolButton::clicked, this, &QgsLightsWidget::onRemoveLight );
61 
62   connect( spinPositionX, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
63   connect( spinPositionY, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
64   connect( spinPositionZ, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
65   connect( spinIntensity, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
66   connect( btnColor, &QgsColorButton::colorChanged, this, &QgsLightsWidget::updateCurrentLightParameters );
67   connect( spinA0, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
68   connect( spinA1, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
69   connect( spinA2, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentLightParameters );
70 
71   connect( spinDirectionalIntensity, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::updateCurrentDirectionalLightParameters );
72   connect( btnDirectionalColor, &QgsColorButton::colorChanged, this, &QgsLightsWidget::updateCurrentDirectionalLightParameters );
73 
74   connect( dialAzimuth, &QSlider::valueChanged, [this]( int value ) {spinBoxAzimuth->setValue( ( value + 180 ) % 360 );} );
75   connect( sliderAltitude, &QSlider::valueChanged, spinBoxAltitude, &QgsDoubleSpinBox::setValue );
76   connect( spinBoxAzimuth, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::onDirectionChange );
77   connect( spinBoxAltitude, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, &QgsLightsWidget::onDirectionChange );
78 
79   mLightsListView->selectionModel()->select( mLightsModel->index( 0, 0 ), QItemSelectionModel::ClearAndSelect );
80   selectedLightChanged( mLightsListView->selectionModel()->selection(), QItemSelection() );
81 }
82 
setLights(const QList<QgsPointLightSettings> & pointLights,const QList<QgsDirectionalLightSettings> & directionalLights)83 void QgsLightsWidget::setLights( const QList<QgsPointLightSettings> &pointLights, const QList<QgsDirectionalLightSettings> &directionalLights )
84 {
85   mLightsModel->setPointLights( pointLights );
86   mLightsModel->setDirectionalLights( directionalLights );
87   mLightsListView->selectionModel()->select( mLightsModel->index( 0, 0 ), QItemSelectionModel::ClearAndSelect );
88   selectedLightChanged( mLightsListView->selectionModel()->selection(), QItemSelection() );
89 }
90 
pointLights()91 QList<QgsPointLightSettings> QgsLightsWidget::pointLights()
92 {
93   return mLightsModel->pointLights();
94 }
95 
directionalLights()96 QList<QgsDirectionalLightSettings> QgsLightsWidget::directionalLights()
97 {
98   return mLightsModel->directionalLights();
99 }
100 
selectedLightChanged(const QItemSelection & selected,const QItemSelection &)101 void QgsLightsWidget::selectedLightChanged( const QItemSelection &selected, const QItemSelection & )
102 {
103   if ( selected.empty() )
104   {
105     mStackedWidget->setCurrentIndex( 0 );
106     return;
107   }
108 
109   const QgsLightsModel::LightType lightType = static_cast< QgsLightsModel::LightType >( mLightsModel->data( selected.indexes().at( 0 ), QgsLightsModel::LightTypeRole ).toInt() );
110   const int listIndex = mLightsModel->data( selected.indexes().at( 0 ), QgsLightsModel::LightListIndex ).toInt();
111 
112   switch ( lightType )
113   {
114     case QgsLightsModel::Point:
115       mStackedWidget->setCurrentIndex( 1 );
116       showSettingsForPointLight( mLightsModel->pointLights().at( listIndex ) );
117       break;
118 
119     case QgsLightsModel::Directional:
120       mStackedWidget->setCurrentIndex( 2 );
121       showSettingsForDirectionalLight( mLightsModel->directionalLights().at( listIndex ) );
122       break;
123   }
124 }
125 
showSettingsForPointLight(const QgsPointLightSettings & light)126 void QgsLightsWidget::showSettingsForPointLight( const QgsPointLightSettings &light )
127 {
128   whileBlocking( spinPositionX )->setValue( light.position().x() );
129   whileBlocking( spinPositionY )->setValue( light.position().y() );
130   whileBlocking( spinPositionZ )->setValue( light.position().z() );
131   whileBlocking( btnColor )->setColor( light.color() );
132   whileBlocking( spinIntensity )->setValue( light.intensity() );
133   whileBlocking( spinA0 )->setValue( light.constantAttenuation() );
134   whileBlocking( spinA1 )->setValue( light.linearAttenuation() );
135   whileBlocking( spinA2 )->setValue( light.quadraticAttenuation() );
136 }
137 
showSettingsForDirectionalLight(const QgsDirectionalLightSettings & light)138 void QgsLightsWidget::showSettingsForDirectionalLight( const QgsDirectionalLightSettings &light )
139 {
140   mDirectionX = light.direction().x();
141   mDirectionY = light.direction().y();
142   mDirectionZ = light.direction().z();
143   whileBlocking( btnDirectionalColor )->setColor( light.color() );
144   whileBlocking( spinDirectionalIntensity )->setValue( light.intensity() );
145   setAzimuthAltitude();
146 }
147 
148 
updateCurrentLightParameters()149 void QgsLightsWidget::updateCurrentLightParameters()
150 {
151   const int listIndex = mLightsModel->data( mLightsListView->selectionModel()->selection().indexes().at( 0 ), QgsLightsModel::LightListIndex ).toInt();
152 
153   QgsPointLightSettings light;
154   light.setPosition( QgsVector3D( spinPositionX->value(), spinPositionY->value(), spinPositionZ->value() ) );
155   light.setColor( btnColor->color() );
156   light.setIntensity( spinIntensity->value() );
157   light.setConstantAttenuation( spinA0->value() );
158   light.setLinearAttenuation( spinA1->value() );
159   light.setQuadraticAttenuation( spinA2->value() );
160 
161   mLightsModel->setPointLightSettings( listIndex, light );
162 }
163 
updateCurrentDirectionalLightParameters()164 void QgsLightsWidget::updateCurrentDirectionalLightParameters()
165 {
166   labelX->setText( QString::number( mDirectionX, 'f', 2 ) );
167   labelY->setText( QString::number( mDirectionY, 'f', 2 ) );
168   labelZ->setText( QString::number( mDirectionZ, 'f', 2 ) );
169 
170   const int listIndex = mLightsModel->data( mLightsListView->selectionModel()->selection().indexes().at( 0 ), QgsLightsModel::LightListIndex ).toInt();
171 
172   QgsDirectionalLightSettings light;
173   light.setDirection( QgsVector3D( mDirectionX, mDirectionY, mDirectionZ ) );
174   light.setColor( btnDirectionalColor->color() );
175   light.setIntensity( spinDirectionalIntensity->value() );
176 
177   mLightsModel->setDirectionalLightSettings( listIndex, light );
178 }
179 
onAddLight()180 void QgsLightsWidget::onAddLight()
181 {
182   if ( mLightsModel->pointLights().size() >= 8 )
183   {
184     QMessageBox::warning( this, tr( "Add Light" ), tr( "It is not possible to add more than 8 lights to the scene." ) );
185     return;
186   }
187 
188   const QModelIndex newIndex = mLightsModel->addPointLight( QgsPointLightSettings() );
189   mLightsListView->selectionModel()->select( newIndex, QItemSelectionModel::ClearAndSelect );
190   emit lightsAdded();
191 }
192 
onAddDirectionalLight()193 void QgsLightsWidget::onAddDirectionalLight()
194 {
195   if ( mLightsModel->directionalLights().size() >= 4 )
196   {
197     QMessageBox::warning( this, tr( "Add Directional Light" ), tr( "It is not possible to add more than 4 directional lights to the scene." ) );
198     return;
199   }
200 
201   const QModelIndex newIndex = mLightsModel->addDirectionalLight( QgsDirectionalLightSettings() );
202   mLightsListView->selectionModel()->select( newIndex, QItemSelectionModel::ClearAndSelect );
203   emit lightsAdded();
204 }
205 
onRemoveLight()206 void QgsLightsWidget::onRemoveLight()
207 {
208   const QItemSelection selected = mLightsListView->selectionModel()->selection();
209   if ( selected.empty() )
210   {
211     return;
212   }
213 
214   const int directionalCount = mLightsModel->directionalLights().size();
215   const int pointCount = mLightsModel->pointLights().size();
216 
217   mLightsModel->removeRows( selected.indexes().at( 0 ).row(), 1 );
218 
219   if ( mLightsModel->directionalLights().size() != directionalCount )
220     emit directionalLightsCountChanged( mLightsModel->directionalLights().size() );
221 
222   if ( mLightsModel->rowCount( QModelIndex() ) != directionalCount + pointCount )
223     emit lightsRemoved();
224 }
225 
setAzimuthAltitude()226 void QgsLightsWidget::setAzimuthAltitude()
227 {
228   double azimuthAngle;
229   double altitudeAngle;
230 
231   double horizontalVectorMagnitude = sqrt( mDirectionX * mDirectionX + mDirectionZ * mDirectionZ );
232 
233   if ( horizontalVectorMagnitude == 0 )
234     azimuthAngle = 0;
235   else
236   {
237     azimuthAngle = ( asin( -mDirectionX / horizontalVectorMagnitude ) ) / M_PI * 180;
238     if ( mDirectionZ < 0 )
239       azimuthAngle = 180 - azimuthAngle;
240     azimuthAngle = std::fmod( azimuthAngle + 360.0, 360.0 );
241   }
242 
243   whileBlocking( dialAzimuth )->setValue( int( azimuthAngle + 180 ) % 360 );
244   whileBlocking( spinBoxAzimuth )->setValue( azimuthAngle );
245 
246   if ( horizontalVectorMagnitude == 0 )
247     altitudeAngle = 90;
248   else
249     altitudeAngle = -atan( mDirectionY / horizontalVectorMagnitude ) / M_PI * 180;
250 
251   whileBlocking( spinBoxAltitude )->setValue( altitudeAngle );
252   whileBlocking( sliderAltitude )->setValue( altitudeAngle );
253 
254   updateCurrentDirectionalLightParameters();
255 }
256 
onDirectionChange()257 void QgsLightsWidget::onDirectionChange()
258 {
259   double altitudeValue = spinBoxAltitude->value();
260   double azimuthValue = spinBoxAzimuth->value();
261 
262   double horizontalVectorMagnitude = cos( altitudeValue / 180 * M_PI );
263   mDirectionX = -horizontalVectorMagnitude * sin( azimuthValue / 180 * M_PI );
264   mDirectionZ = horizontalVectorMagnitude * cos( azimuthValue / 180 * M_PI );
265   mDirectionY = -sin( altitudeValue / 180 * M_PI );
266 
267   whileBlocking( sliderAltitude )->setValue( altitudeValue );
268   updateCurrentDirectionalLightParameters();
269 }
270 
271 
272 
273 //
274 // QgsLightsModel
275 //
QgsLightsModel(QObject * parent)276 QgsLightsModel::QgsLightsModel( QObject *parent )
277   : QAbstractListModel( parent )
278 {
279 
280 }
281 
rowCount(const QModelIndex & parent) const282 int QgsLightsModel::rowCount( const QModelIndex &parent ) const
283 {
284   Q_UNUSED( parent )
285   return mPointLights.size() + mDirectionalLights.size();
286 }
287 
data(const QModelIndex & index,int role) const288 QVariant QgsLightsModel::data( const QModelIndex &index, int role ) const
289 {
290   if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) )
291     return QVariant();
292 
293   const LightType lightType = index.row() < mPointLights.size() ? Point : Directional;
294   const int lightListRow = lightType == Point ? index.row() : index.row() - mPointLights.size();
295 
296   switch ( role )
297   {
298     case Qt::DisplayRole:
299     case Qt::ToolTipRole:
300     case Qt::EditRole:
301       switch ( lightType )
302       {
303         case Point:
304           return tr( "Point light %1" ).arg( lightListRow + 1 );
305 
306         case Directional:
307           return tr( "Directional light %1" ).arg( lightListRow + 1 );
308       }
309       break;
310 
311     case LightTypeRole:
312       return lightType;
313 
314     case LightListIndex:
315       return lightListRow;
316 
317     case Qt::DecorationRole:
318       return QgsApplication::getThemeIcon( QStringLiteral( "/mActionHighlightFeature.svg" ) );
319 
320     default:
321       break;
322   }
323   return QVariant();
324 }
325 
removeRows(int row,int count,const QModelIndex & parent)326 bool QgsLightsModel::removeRows( int row, int count, const QModelIndex &parent )
327 {
328   beginRemoveRows( parent, row, row + count - 1 );
329   for ( int i = row + count - 1; i >= row; --i )
330   {
331     const LightType lightType = i < mPointLights.size() ? Point : Directional;
332     const int lightListRow = lightType == Point ? i : i - mPointLights.size();
333 
334     switch ( lightType )
335     {
336       case Point:
337         mPointLights.removeAt( lightListRow );
338         break;
339 
340       case Directional:
341         mDirectionalLights.removeAt( lightListRow );
342         break;
343     }
344   }
345   endRemoveRows();
346   return true;
347 }
348 
setPointLights(const QList<QgsPointLightSettings> & lights)349 void QgsLightsModel::setPointLights( const QList<QgsPointLightSettings> &lights )
350 {
351   beginRemoveRows( QModelIndex(), 0, mPointLights.size() - 1 );
352   mPointLights.clear();
353   endRemoveRows();
354 
355   beginInsertRows( QModelIndex(), 0, lights.size() - 1 );
356   mPointLights = lights;
357   endInsertRows();
358 }
359 
setDirectionalLights(const QList<QgsDirectionalLightSettings> & lights)360 void QgsLightsModel::setDirectionalLights( const QList<QgsDirectionalLightSettings> &lights )
361 {
362   beginRemoveRows( QModelIndex(), mPointLights.size(), mPointLights.size() + mDirectionalLights.size() - 1 );
363   mDirectionalLights.clear();
364   endRemoveRows();
365 
366   beginInsertRows( QModelIndex(), mPointLights.size(), mPointLights.size() + lights.size() - 1 );
367   mDirectionalLights = lights;
368   endInsertRows();
369 }
370 
pointLights() const371 QList<QgsPointLightSettings> QgsLightsModel::pointLights() const
372 {
373   return mPointLights;
374 }
375 
directionalLights() const376 QList<QgsDirectionalLightSettings> QgsLightsModel::directionalLights() const
377 {
378   return mDirectionalLights;
379 }
380 
setPointLightSettings(int index,const QgsPointLightSettings & light)381 void QgsLightsModel::setPointLightSettings( int index, const QgsPointLightSettings &light )
382 {
383   mPointLights[ index ] = light;
384 }
385 
setDirectionalLightSettings(int index,const QgsDirectionalLightSettings & light)386 void QgsLightsModel::setDirectionalLightSettings( int index, const QgsDirectionalLightSettings &light )
387 {
388   mDirectionalLights[ index ] = light;
389 }
390 
addPointLight(const QgsPointLightSettings & light)391 QModelIndex QgsLightsModel::addPointLight( const QgsPointLightSettings &light )
392 {
393   beginInsertRows( QModelIndex(), mPointLights.size(), mPointLights.size() );
394   mPointLights.append( light );
395   endInsertRows();
396 
397   return index( mPointLights.size() - 1 );
398 }
399 
addDirectionalLight(const QgsDirectionalLightSettings & light)400 QModelIndex QgsLightsModel::addDirectionalLight( const QgsDirectionalLightSettings &light )
401 {
402   beginInsertRows( QModelIndex(), mPointLights.size() + mDirectionalLights.size(), mPointLights.size() + mDirectionalLights.size() );
403   mDirectionalLights.append( light );
404   endInsertRows();
405 
406   return index( mPointLights.size() + mDirectionalLights.size() - 1 );
407 }
408