1 /***************************************************************************
2   qgs3danimationwidget.cpp
3   --------------------------------------
4   Date                 : July 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 "qgs3danimationwidget.h"
17 
18 #include "qgs3danimationsettings.h"
19 #include "qgsapplication.h"
20 #include "qgscameracontroller.h"
21 #include "qgs3danimationexportdialog.h"
22 #include "qgs3dmapsettings.h"
23 #include "qgsoffscreen3dengine.h"
24 #include "qgs3dmapscene.h"
25 #include "qgs3dutils.h"
26 #include "qgsfeedback.h"
27 
28 #include <QInputDialog>
29 #include <QMessageBox>
30 #include <QTimer>
31 #include <QProgressDialog>
32 
Qgs3DAnimationWidget(QWidget * parent)33 Qgs3DAnimationWidget::Qgs3DAnimationWidget( QWidget *parent )
34   : QWidget( parent )
35 {
36   setupUi( this );
37 
38   btnAddKeyframe->setIcon( QIcon( QgsApplication::iconPath( "symbologyAdd.svg" ) ) );
39   btnRemoveKeyframe->setIcon( QIcon( QgsApplication::iconPath( "symbologyRemove.svg" ) ) );
40   btnEditKeyframe->setIcon( QIcon( QgsApplication::iconPath( "symbologyEdit.svg" ) ) );
41   btnPlayPause->setIcon( QIcon( QgsApplication::iconPath( "mTaskRunning.svg" ) ) );
42   btnDuplicateKeyframe->setIcon( QIcon( QgsApplication::iconPath( "mActionEditCopy.svg" ) ) );
43   btnExportAnimation->setIcon( QIcon( QgsApplication::iconPath( "mActionFileSave.svg" ) ) );
44   cboKeyframe->addItem( tr( "<none>" ) );
45 
46   mAnimationTimer = new QTimer( this );
47   mAnimationTimer->setInterval( 10 );
48   connect( mAnimationTimer, &QTimer::timeout, this, &Qgs3DAnimationWidget::onAnimationTimer );
49 
50   connect( btnAddKeyframe, &QToolButton::clicked, this, &Qgs3DAnimationWidget::onAddKeyframe );
51   connect( btnRemoveKeyframe, &QToolButton::clicked, this, &Qgs3DAnimationWidget::onRemoveKeyframe );
52   connect( btnEditKeyframe, &QToolButton::clicked, this, &Qgs3DAnimationWidget::onEditKeyframe );
53   connect( btnDuplicateKeyframe, &QToolButton::clicked, this, &Qgs3DAnimationWidget::onDuplicateKeyframe );
54   connect( btnExportAnimation, &QToolButton::clicked, this, &Qgs3DAnimationWidget::onExportAnimation );
55   connect( cboInterpolation, qgis::overload<int>::of( &QComboBox::currentIndexChanged ), this, &Qgs3DAnimationWidget::onInterpolationChanged );
56 
57   btnPlayPause->setCheckable( true );
58   connect( btnPlayPause, &QToolButton::clicked, this, &Qgs3DAnimationWidget::onPlayPause );
59 
60   connect( sliderTime, &QSlider::valueChanged, this, &Qgs3DAnimationWidget::onSliderValueChanged );
61 
62   connect( cboKeyframe, qgis::overload<int>::of( &QComboBox::currentIndexChanged ), this, &Qgs3DAnimationWidget::onKeyframeChanged );
63 }
64 
65 Qgs3DAnimationWidget::~Qgs3DAnimationWidget() = default;
66 
67 
setCameraController(QgsCameraController * cameraController)68 void Qgs3DAnimationWidget::setCameraController( QgsCameraController *cameraController )
69 {
70   mCameraController = cameraController;
71   connect( mCameraController, &QgsCameraController::cameraChanged, this, &Qgs3DAnimationWidget::onCameraChanged );
72 }
73 
74 
setAnimation(const Qgs3DAnimationSettings & animSettings)75 void Qgs3DAnimationWidget::setAnimation( const Qgs3DAnimationSettings &animSettings )
76 {
77   whileBlocking( cboInterpolation )->setCurrentIndex( animSettings.easingCurve().type() );
78 
79   // initialize GUI from the given animation
80   cboKeyframe->clear();
81   cboKeyframe->addItem( tr( "<none>" ) );
82   for ( const Qgs3DAnimationSettings::Keyframe &keyframe : animSettings.keyFrames() )
83   {
84     cboKeyframe->addItem( QStringLiteral( "%1 s" ).arg( keyframe.time ) );
85     int lastIndex = cboKeyframe->count() - 1;
86     cboKeyframe->setItemData( lastIndex, QVariant::fromValue<Qgs3DAnimationSettings::Keyframe>( keyframe ), Qt::UserRole + 1 );
87   }
88 
89   initializeController( animSettings );
90 }
91 
initializeController(const Qgs3DAnimationSettings & animSettings)92 void Qgs3DAnimationWidget::initializeController( const Qgs3DAnimationSettings &animSettings )
93 {
94   mAnimationSettings.reset( new Qgs3DAnimationSettings( animSettings ) );
95 
96   sliderTime->setMaximum( animSettings.duration() * 100 );
97 }
98 
animation() const99 Qgs3DAnimationSettings Qgs3DAnimationWidget::animation() const
100 {
101   Qgs3DAnimationSettings animSettings;
102   animSettings.setEasingCurve( QEasingCurve( ( QEasingCurve::Type ) cboInterpolation->currentIndex() ) );
103   Qgs3DAnimationSettings::Keyframes keyframes;
104   for ( int i = 1; i < cboKeyframe->count(); ++i )
105   {
106     Qgs3DAnimationSettings::Keyframe kf;
107     kf = cboKeyframe->itemData( i, Qt::UserRole + 1 ).value<Qgs3DAnimationSettings::Keyframe>();
108     keyframes << kf;
109   }
110   animSettings.setKeyframes( keyframes );
111   return animSettings;
112 }
113 
setDefaultAnimation()114 void Qgs3DAnimationWidget::setDefaultAnimation()
115 {
116   Qgs3DAnimationSettings animSettings;
117   Qgs3DAnimationSettings::Keyframes kf;
118   Qgs3DAnimationSettings::Keyframe f1, f2;
119   f1.time = 0;
120   f1.point = mCameraController->lookingAtPoint();
121   f1.dist = mCameraController->distance();
122   f1.pitch = mCameraController->pitch();
123   f1.yaw = mCameraController->yaw();
124 
125   f2.time = 5;
126   f2.point = f1.point;
127   f2.dist = f1.dist * 2;
128   f2.pitch = f1.pitch;
129   f2.yaw = f1.yaw;
130 
131   kf << f1 << f2;
132   animSettings.setKeyframes( kf );
133 
134   setAnimation( animSettings );
135 }
136 
setEditControlsEnabled(bool enabled)137 void Qgs3DAnimationWidget::setEditControlsEnabled( bool enabled )
138 {
139   cboKeyframe->setEnabled( enabled );
140   btnAddKeyframe->setEnabled( enabled );
141   cboInterpolation->setEnabled( enabled );
142 }
143 
setMap(Qgs3DMapSettings * map)144 void Qgs3DAnimationWidget::setMap( Qgs3DMapSettings *map )
145 {
146   mMap = map;
147 }
148 
onPlayPause()149 void Qgs3DAnimationWidget::onPlayPause()
150 {
151   if ( mAnimationTimer->isActive() )
152   {
153     mAnimationTimer->stop();
154     setEditControlsEnabled( true );
155   }
156   else
157   {
158     if ( sliderTime->value() >= sliderTime->maximum() )
159     {
160       sliderTime->setValue( 0 );
161     }
162 
163     cboKeyframe->setCurrentIndex( 0 ); // unset active keyframe
164     setEditControlsEnabled( false );
165     mAnimationTimer->start();
166   }
167 }
168 
onAnimationTimer()169 void Qgs3DAnimationWidget::onAnimationTimer()
170 {
171   if ( sliderTime->value() >= sliderTime->maximum() )
172   {
173     if ( mLoopingCheckBox->isChecked() )
174       sliderTime->setValue( 0 );
175     else
176     {
177       // stop playback
178       onPlayPause();
179       btnPlayPause->setChecked( false );
180     }
181   }
182   else
183   {
184     sliderTime->setValue( sliderTime->value() + 1 );
185   }
186 }
187 
onExportAnimation()188 void Qgs3DAnimationWidget::onExportAnimation()
189 {
190   if ( !mMap || !mAnimationSettings )
191     QMessageBox::warning( this, tr( "Export Animation" ), tr( "Unable to export 3D animation" ) );
192 
193   Qgs3DAnimationExportDialog dialog;
194   if ( dialog.exec() == QDialog::Accepted )
195   {
196     QgsFeedback progressFeedback;
197 
198     QProgressDialog progressDialog( tr( "Exporting frames..." ), tr( "Abort" ), 0, 100, this );
199     progressDialog.setWindowModality( Qt::WindowModal );
200     QString error;
201 
202     connect( &progressFeedback, &QgsFeedback::progressChanged, this,
203              [&progressDialog, &progressFeedback]
204     {
205       progressDialog.setValue( static_cast<int>( progressFeedback.progress() ) );
206       QCoreApplication::processEvents();
207     } );
208 
209     connect( &progressDialog, &QProgressDialog::canceled, &progressFeedback, &QgsFeedback::cancel );
210 
211     bool success = Qgs3DUtils::exportAnimation(
212                      animation(),
213                      *mMap,
214                      dialog.fps(),
215                      dialog.outputDirectory(),
216                      dialog.fileNameExpression(),
217                      dialog.frameSize(),
218                      error,
219                      &progressFeedback );
220 
221     progressDialog.hide();
222     if ( !success )
223     {
224       QMessageBox::warning( this, tr( "Export Animation" ), error );
225       return;
226     }
227   }
228 }
229 
230 
onSliderValueChanged()231 void Qgs3DAnimationWidget::onSliderValueChanged()
232 {
233   // make sure we do not have an active keyframe
234   if ( cboKeyframe->currentIndex() != 0 )
235     cboKeyframe->setCurrentIndex( 0 );
236 
237   Qgs3DAnimationSettings::Keyframe kf = mAnimationSettings->interpolate( sliderTime->value() / 100. );
238   mCameraController->setLookingAtPoint( kf.point, kf.dist, kf.pitch, kf.yaw );
239 }
240 
onCameraChanged()241 void Qgs3DAnimationWidget::onCameraChanged()
242 {
243   if ( cboKeyframe->currentIndex() <= 0 )
244     return;
245 
246   // update keyframe's camera position/rotation
247   int i = cboKeyframe->currentIndex();
248   Qgs3DAnimationSettings::Keyframe kf = cboKeyframe->itemData( i, Qt::UserRole + 1 ).value<Qgs3DAnimationSettings::Keyframe>();
249   kf.point = mCameraController->lookingAtPoint();
250   kf.dist = mCameraController->distance();
251   kf.pitch = mCameraController->pitch();
252   kf.yaw = mCameraController->yaw();
253   cboKeyframe->setItemData( i, QVariant::fromValue<Qgs3DAnimationSettings::Keyframe>( kf ), Qt::UserRole + 1 );
254 
255   initializeController( animation() );
256 }
257 
onKeyframeChanged()258 void Qgs3DAnimationWidget::onKeyframeChanged()
259 {
260   bool hasKeyframe = cboKeyframe->currentIndex() > 0;
261   btnRemoveKeyframe->setEnabled( hasKeyframe );
262   btnEditKeyframe->setEnabled( hasKeyframe );
263   btnDuplicateKeyframe->setEnabled( hasKeyframe );
264 
265   if ( !hasKeyframe )
266     return;
267 
268   // jump to the camera view of the keyframe
269   Qgs3DAnimationSettings::Keyframe kf = cboKeyframe->itemData( cboKeyframe->currentIndex(), Qt::UserRole + 1 ).value<Qgs3DAnimationSettings::Keyframe>();
270 
271   whileBlocking( sliderTime )->setValue( kf.time * 100 );
272   mCameraController->setLookingAtPoint( kf.point, kf.dist, kf.pitch, kf.yaw );
273 }
274 
findIndexForKeyframe(float time)275 int Qgs3DAnimationWidget::findIndexForKeyframe( float time )
276 {
277   int newIndex = 0;
278   for ( const Qgs3DAnimationSettings::Keyframe &keyframe : mAnimationSettings->keyFrames() )
279   {
280     if ( keyframe.time > time )
281       break;
282     newIndex++;
283   }
284   return newIndex;
285 }
286 
askForKeyframeTime(float defaultTime,bool * ok)287 float Qgs3DAnimationWidget::askForKeyframeTime( float defaultTime, bool *ok )
288 {
289   double t = QInputDialog::getDouble( this, tr( "Keyframe time" ), tr( "Keyframe time [seconds]:" ), defaultTime, 0, 9999, 2, ok );
290   if ( !*ok )
291     return 0;
292 
293   // figure out position of this keyframe
294   for ( const Qgs3DAnimationSettings::Keyframe &keyframe : mAnimationSettings->keyFrames() )
295   {
296     if ( keyframe.time == t )
297     {
298       QMessageBox::warning( this, tr( "Keyframe time" ), tr( "There is already a keyframe at the given time" ) );
299       *ok = false;
300       return 0;
301     }
302   }
303 
304   *ok = true;
305   return t;
306 }
307 
onAddKeyframe()308 void Qgs3DAnimationWidget::onAddKeyframe()
309 {
310   bool ok;
311   float t = askForKeyframeTime( sliderTime->value() / 100., &ok );
312   if ( !ok )
313     return;
314 
315   int index = findIndexForKeyframe( t );
316 
317   Qgs3DAnimationSettings::Keyframe kf;
318   kf.time = t;
319   kf.point = mCameraController->lookingAtPoint();
320   kf.dist = mCameraController->distance();
321   kf.pitch = mCameraController->pitch();
322   kf.yaw = mCameraController->yaw();
323 
324   cboKeyframe->insertItem( index + 1, QStringLiteral( "%1 s" ).arg( kf.time ) );
325   cboKeyframe->setItemData( index + 1, QVariant::fromValue<Qgs3DAnimationSettings::Keyframe>( kf ), Qt::UserRole + 1 );
326 
327   initializeController( animation() );
328 
329   cboKeyframe->setCurrentIndex( index + 1 );
330 }
331 
onRemoveKeyframe()332 void Qgs3DAnimationWidget::onRemoveKeyframe()
333 {
334   int index = cboKeyframe->currentIndex();
335   if ( index <= 0 )
336     return;
337 
338   cboKeyframe->setCurrentIndex( 0 );
339   cboKeyframe->removeItem( index );
340 
341   initializeController( animation() );
342 }
343 
onEditKeyframe()344 void Qgs3DAnimationWidget::onEditKeyframe()
345 {
346   int index = cboKeyframe->currentIndex();
347   if ( index <= 0 )
348     return;
349 
350   Qgs3DAnimationSettings::Keyframe kf = cboKeyframe->itemData( index, Qt::UserRole + 1 ).value<Qgs3DAnimationSettings::Keyframe>();
351 
352   bool ok;
353   float t = askForKeyframeTime( kf.time, &ok );
354   if ( !ok )
355     return;
356 
357   cboKeyframe->setCurrentIndex( 0 );
358   cboKeyframe->removeItem( index );
359 
360   initializeController( animation() );
361 
362   // figure out position of this keyframe
363   int newIndex = findIndexForKeyframe( t );
364 
365   kf.time = t;
366 
367   cboKeyframe->insertItem( newIndex + 1, QStringLiteral( "%1 s" ).arg( kf.time ) );
368   cboKeyframe->setItemData( newIndex + 1, QVariant::fromValue<Qgs3DAnimationSettings::Keyframe>( kf ), Qt::UserRole + 1 );
369 
370   initializeController( animation() );
371 
372   cboKeyframe->setCurrentIndex( newIndex + 1 );
373 }
374 
onDuplicateKeyframe()375 void Qgs3DAnimationWidget::onDuplicateKeyframe()
376 {
377   int index = cboKeyframe->currentIndex();
378   if ( index <= 0 )
379     return;
380 
381   Qgs3DAnimationSettings::Keyframe kf = cboKeyframe->itemData( index, Qt::UserRole + 1 ).value<Qgs3DAnimationSettings::Keyframe>();
382 
383   bool ok;
384   float t = askForKeyframeTime( kf.time, &ok );
385   if ( !ok )
386     return;
387 
388   // figure out position of this keyframe
389   int newIndex = findIndexForKeyframe( t );
390 
391   kf.time = t;
392 
393   cboKeyframe->insertItem( newIndex + 1, QStringLiteral( "%1 s" ).arg( kf.time ) );
394   cboKeyframe->setItemData( newIndex + 1, QVariant::fromValue<Qgs3DAnimationSettings::Keyframe>( kf ), Qt::UserRole + 1 );
395 
396   initializeController( animation() );
397 
398   cboKeyframe->setCurrentIndex( newIndex + 1 );
399 }
400 
onInterpolationChanged()401 void Qgs3DAnimationWidget::onInterpolationChanged()
402 {
403   initializeController( animation() );
404 
405   if ( cboKeyframe->currentIndex() <= 0 )
406     onSliderValueChanged();
407 }
408