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