-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathAnimationController2.py
442 lines (374 loc) · 19.5 KB
/
AnimationController2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
'''
Created on 28 de ene. de 2016
@author: IHC
'''
# Standard libs:
import time
import traceback
import threading
from datetime import timedelta
# QGIS /PyQt libs:
from qgis.utils import iface
from qgis.core import QgsMessageLog, QgsProject, Qgis
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot, Qt
# Our libs:
from .Animation2 import AnimationData
from . import AnimationOtherLayerManager
from .AnimationLayer import AnimationLayer
from ..utilities.LayerLegendGroupifier import LayerGroupifier
from ..providersmanagers.wcs.WCSBatchDownloadUtil import WCSDownloadWorkerThread
from ..providersmanagers.wms.WMSBatchDownloadUtil import WMSDownloadWorkerThread
class Controller(QObject):
"""Manages all operations required to handle animation operations.
a) Load time-aware data from any of several supported providers.
b) Create a single or multiple layer animation.
c) Play, stop, find frames.
After creating this object, whatever is calling it should
provide it with a number of AnimationLayer objects through
its setUpAnimation() method, which accepts a list of them.
It will then read each layer's getAnimationData() and
getAnimationLegendGroups() methods, and if no data is defined
in -both- of them, it'll attempt to find a controller suitable
to download and generate that information, to create a fully
interpretable AnimationLayer object. This controller can, at
this point, handle WMS/WCS data download, and load any proper
AnimationLayer object.
After performing the AnimationLayer load (either by downloading
required data or not because it had all the data already prepared),
updateTimeRange() will be called to update the oldest and newest
time which will be played. This allows us to handle the animation
as a time series instead of a simple frame-by-frame based animation.
Through the method setTimeDeltaPerFrame() we can tell the animator
how much map time will pass between frame updates, and
setTimeDeviationTolerance() will define how much deviation from
that exact time we allow when looking for the map to draw in
QGIS interface.
Several signals provide for status updates while preparing or
playing back the animation.
"""
animationPlaybackEnd = pyqtSignal()
animatorReady = pyqtSignal()
externalAnimationLoaded = pyqtSignal(object)#Must be an AnimationLayer like object
animationGenerationStepDone = pyqtSignal()
newFrameShown = pyqtSignal(tuple) #Emitted when a new 'time frame' is shown, with the time tag as parameter
#Self-signal so the group creation will be run on main thread
#even after being requested by async ops.
createLayerGroupsOnMainThread = pyqtSignal(AnimationLayer, str)
maxDownloadThreads = 3
lock = threading.RLock()
groupAssignmentLock = threading.RLock()
errorSignal = pyqtSignal(str)
statusSignal = pyqtSignal(str)
def __init__(self, parent=None):
"""Constructor."""
#TODO: Remove debug
#import sys
super(Controller, self).__init__(parent)
self.paused = True
self.playbackSpeed = 500
self.timer = QTimer()
self.timer.timeout.connect(self.onNextFrameRequested)
self.createLayerGroupsOnMainThread.connect(self.createLayerGroup)
iface.mapCanvas().setParallelRenderingEnabled(True)
self.timeDeviationTolerance = None
self.timeDeltaPerFrame = None
self.initialize()
def initialize(self):
self.animationBeginTime = None
self.animationEndTime = None
self.threadsInUse = {} #Dictionary which contains all the 'worker objects' and their assocciated threads.
#in {workerObject : threadObject} form so we can cancel and so stuff with both.
self.framesShown = []
self.animationElements = []
self.animationGroups = [] #QGIS UI layer groups
self.nextFrame = None #next time to be requested
self.paused = True
self.maxProgress = 0
self.layersToBeGrouped = 0
self.errors = False
self.maxAnimationGroupsToBeCreated = 0
self.layersAddedToGroups = 0
#
# Methods to assist in playback
#
def play(self):
if self.animationGroups is not None and len(self.animationGroups) != 0:
self.timer.start(self.playbackSpeed)
self.paused = False
def pause(self):
self.paused = True
self.timer.stop()
def togglePlay(self):
if self.paused:
self.play()
else:
self.pause()
def getNumberOfFrames(self):
try:
timeRangeInDelta = abs(self.animationEndTime - self.animationBeginTime)
return int( (timeRangeInDelta.total_seconds()/self.timeDeltaPerFrame.total_seconds()))
except (AttributeError, ZeroDivisionError, TypeError):
# QgsMessageLog.logMessage(traceback.format_exc(), "THREDDS Explorer", QgsMessageLog.CRITICAL )
return 0
def onNextFrameRequested(self):
if self.nextFrame is None or self.nextFrame < self.animationBeginTime:
self.nextFrame = self.animationBeginTime
elif self.nextFrame > self.animationEndTime:
#print("====END====")
#print(self.nextFrame)
#print(self.animationEndTime)
#print("====END====")
self.pause()
self.nextFrame = self.animationBeginTime
self.animationPlaybackEnd.emit()
return
self.framesShown = []
for animation in self.animationElements:
try:
layer = animation.getFrameByTime(self.nextFrame,
self.timeDeviationTolerance)
try:
#iface.legendInterface().setLayerVisible(layer, True)
QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(True)
#QgsProject.instance().layerTreeRoot().findLayer(layer.id()).setItemVisibilityChecked(True)
except RuntimeError:
#Will happen if the animator attempts to set as visible
#a no longer existing layer (i.e. if the user removes
#that layer or group from the legend interface).
self.pause()
self.errorSignal.emit("A layer for this animation was not found.\nWas it removed?"\
" Please, click\nagain on prepare animation to fix this issue.")
QgsMessageLog.logMessage(traceback.format_exc(), "THREDDS Explorer", Qgis.Critical )
self.framesShown.append(layer)
except KeyError:
#QgsMessageLog.logMessage(traceback.format_exc(), "THREDDS Explorer", QgsMessageLog.CRITICAL )
iface.messageBar().pushMessage("THREDDS Explorer", "Connection error.", level=Qgis.Critical)
continue
#print("MAP SEARCH FINISHED")
try:
nextFrameToBeDisplayedIndex = \
abs((self.animationBeginTime - self.nextFrame).total_seconds())/self.timeDeltaPerFrame.total_seconds()
except TypeError:
#Will happen if this animation is reset while running.
self.pause()
return
#print("next frame to show int: "+str(nextFrameToBeDisplayedIndex))
self.newFrameShown.emit((nextFrameToBeDisplayedIndex, str(self.nextFrame)))
self.nextFrame = self.nextFrame + self.timeDeltaPerFrame
def setCurrentFrame(self, intFramePosition):
"""Sets the animation in the specified frame."""
timeDelta = timedelta(seconds=intFramePosition * self.timeDeltaPerFrame.total_seconds())
self.nextFrame = self.animationBeginTime + timeDelta
if self.paused:
#If the animation is paused, we will manually
#trigger a new layer draw.
self.onNextFrameRequested()
def setFrameRate(self, millisecondsPerFrame):
self.playbackSpeed = millisecondsPerFrame
if not self.paused:
#If the animation is running, we will stop and restart it
#so it uses our new framerate.
self.pause()
self.play()
def setTimeDeviationTolerance(self, timedeltaMaxDeviation):
"""Sets the maximum allowed variation between the expected
time to be shown to the user and the actual one stored.
This means, if we are playing at 15 min / second, and
we have a tolerance of 5 minutes, when the animation
controller requests the map for 15:00, the closest
map available for that time will be shown within the
set tolerance. This might be a map "set" between
14:55 and 15:05.
:param timedeltaMaxDeviation: max time deviation or None for exact match.
:type timedeltaMaxDeviation: datetime.timedelta
"""
self.timeDeviationTolerance = timedeltaMaxDeviation
#print("Tolerance = "+str(self.timeDeviationTolerance))
def setTimeDeltaPerFrame(self, timeDelta):
self.timeDeltaPerFrame = timeDelta
def isPlaying(self):
return not self.paused
#
# Methods to create the animation elements and manage them
#
def setUpAnimation(self, animationLayerList):
"""Set animation up."""
if not animationLayerList:
raise AttributeError("Invalid data provided.")
self.initialize()
self.animationLayerObjectList = animationLayerList
wcsLayers, wmsLayers, otherLayers = [], [], []
for animationLayer in animationLayerList:
# If the layer has both animationData and legendGroups ready,
# it is ready to be used, no matter its origin, which should
# be through a controller which generates an AnimationLayer.
# Other supported layers missing those attributes should be handled separately.
# TODO: Refactor this WMS/WCS layer fabrication through an external controller, as TESEO one, sort of...
if animationLayer.getAnimationData() and animationLayer.getAnimationLegendGroups():
otherLayers.append(animationLayer)
elif animationLayer.getService() == "WMS":
wmsLayers.append(animationLayer)
elif animationLayer.getService() == "WCS":
wcsLayers.append(animationLayer)
# The max amount of steps which will be reported to progress
# bars or such things will be one operation per WCS animationLayer
# retrieval attempt (begin, end), and four per WMS animationLayer
# retrieval attempt (two per range check, one to begin
# retrieving the animationLayer itself, one when it finishes)
self.maxProgress = 2*sum([len(x.getTimes()) for x in wcsLayers]) \
+ 3*sum([len(x.getTimes()) for x in wmsLayers])
for animationLayer in otherLayers:
if animationLayer.getAnimationLegendGroups():
self.animationGroups.append(animationLayer.getAnimationLegendGroups())
self.animationElements.append(animationLayer.getAnimationData())
self.updateTimeRange(animationLayer.getAnimationData().frameData.keys())
self.maxAnimationGroupsToBeCreated = len(wmsLayers) + len(wcsLayers) + len(otherLayers)
if len(wmsLayers):
threading.Thread(target=self.createMultipleWMSAnimationElements, args=(wmsLayers,)).start()
if len(wcsLayers):
threading.Thread(target=self.createMultipleWCSAnimationElements, args=(wcsLayers,)).start()
# If no layers must be downloaded, it's over and ready:
if not wmsLayers and not wcsLayers:
#print("READY WITHOUT LAYERS")
self.animatorCreated()
return
self.statusSignal.emit("Downloading layers...")
def cancelLoad(self):
try:
for item in self.threadsInUse.keys():
item.requestCancellation()
#print("ITEM ALIVE: "+str((self.threadsInUse[item]).isAlive()))
self.threadsInUse.pop(item)
self.statusSignal.emit("Operation cancelled.")
except AttributeError:
pass
def animatorCreated(self):
#print("ANIMATOR CREATED --------------")
self.animatorReady.emit()
if self.errors == True :
self.errorSignal.emit("An error occured during the download.\nThe animation may have gaps.")
def getMaxProgressValue(self):
"""This will report the maximum number of operations/steps which will be done
by this controller when attempting to prepare an animation.
"""
return self.maxProgress
def createMultipleWCSAnimationElements(self, animationLayerList):
"""
:param animationLayerList: list of AnimationLayer to be created and managed by this controller.
:type animationLayerList: [AnimationLayer]
"""
for item in animationLayerList:
worker = WCSDownloadWorkerThread(item.getMapObject().getWCS().getCapabilitiesURL(),
item.getTimes(),
item.getLayerName(),
item.getBBOX(),
parent=self)
baseLayerDictionary = worker.getLayerDict()
animData = AnimationData(item.getLayerName(), baseLayerDictionary)
item.setAnimationData(animData)
worker.WCSFrameStartsDownload.connect(self.animationGenerationStepDone.emit, Qt.DirectConnection)
worker.WCSFrameFinishedDownload.connect(self.animationGenerationStepDone.emit, Qt.DirectConnection)
worker.WCSProcessdone.connect(self.BatchWorkerThreadDone)
worker.WCSMapDownloadFail.connect(self.WorkerThreadDownloadError)
thread = threading.Thread(target=worker.run)
self.threadsInUse[worker] = thread
thread.start()
def createMultipleWMSAnimationElements(self, animationLayerList):
"""
:param animationLayerList: list of AnimationLayer to be created and managed by this controller.
:type animationLayerList: [AnimationLayer]
"""
for item in animationLayerList:
worker = WMSDownloadWorkerThread(
capabilitiesURL=item.getMapObject().getWMS(),
times=item.getTimes(),
layerName=item.getLayerName(),
style=item.getStyle(),
bbox=item.getBBOX(),
parent=self)
baseLayerDictionary = worker.getLayerDict()
animData = AnimationData(item.getLayerName()+"_"+item.getStyle(), baseLayerDictionary)
item.setAnimationData(animData)
worker.WMSFrameStartsDownload.connect(self.animationGenerationStepDone.emit, Qt.DirectConnection)
worker.WMSFrameFinishedDownload.connect(self.animationGenerationStepDone.emit, Qt.DirectConnection)
worker.WMSSingleValueRangeProcessed.connect(self.animationGenerationStepDone.emit, Qt.DirectConnection)
worker.WMSprocessdone.connect(self.BatchWorkerThreadDone)
worker.WMSMapDownloadFail.connect(self.WorkerThreadDownloadError)
thread = threading.Thread(target = worker.run)
self.threadsInUse[worker] = thread
thread.start()
@pyqtSlot(dict, QObject)
def BatchWorkerThreadDone(self, layerDict, workerObject):
"""
:param animationLayerObject: The previously provided AnimationLayer object
for this animated map in createMultipleWMS/WCSAnimationElements.
:type animationLayerObject: AnimationLayer
:param layerDict: The dictionary holding the layers in the form timestamp : layer
It is already held inside animationLayerObject, but the signal
is emitted with a reference to it in case the AnimationLayer
object was not provided to the batch downloader (i.e. in the static
viewer case).
:type layerDict: dict
:param workerObject: The thread object
:type workerObject: QObject
"""
try:
self.threadsInUse.pop(workerObject) #We have to keep our list clean.
#We find what animationLayer object this dictionary
#is from
animationLayerObject = \
([x for x in self.animationLayerObjectList if x.getAnimationData().frameData == layerDict])[0]
self.initializeAnimator(animationLayerObject)
groupName = animationLayerObject.getMapObject().getName()+"-"+animationLayerObject.getLayerName()
threading.Thread(target = self.addPendingLayerGroupRequest,
args = (animationLayerObject,
groupName)).start()
except KeyError:
#Might happen if a thread is cancelled in the last frame download,
#as it'll already have been queued for removal from the threadsInUse dict
pass
@pyqtSlot(int, str)
def WorkerThreadDownloadError(self, numberOfFailedDownloads, layerName):
self.errorSignal.emit("Warning: "+str(numberOfFailedDownloads)+" frames failed to be downloaded from layer \n"
+layerName+". The resulting animation\nmay have gaps.")
def initializeAnimator(self, animationLayerObject):
animationData = animationLayerObject.getAnimationData()
self.animationElements.append(animationData)
self.updateTimeRange(animationData.frameData.keys()) #We update the list of times covered by our frames
def addExternalTimeLayer(self):
self.addLayerMenu = AnimationOtherLayerManager.AnimationOtherLayerManager(self)
self.addLayerMenu.animationLayerCreated.connect(self.addNewExternalAnimatedLayer)
self.addLayerMenu.show()
@pyqtSlot(object)
def addNewExternalAnimatedLayer(self, animationLayer):
self.externalAnimationLoaded.emit(animationLayer)
def updateTimeRange(self, newElements):
"""Will append the given list of times to the current ones available
for animation, remove any duplicates, and sort them.
"""
orderedElements = sorted(newElements)
if newElements == None or len(newElements) == 0:
return
if self.animationBeginTime is None \
or self.animationBeginTime > orderedElements[0]:
self.animationBeginTime = orderedElements[0]
if self.animationEndTime is None \
or self.animationEndTime < orderedElements[len(orderedElements)-1]:
self.animationEndTime = orderedElements[len(orderedElements)-1]
@pyqtSlot(AnimationLayer,str)
def addPendingLayerGroupRequest(self, animationLayerObject, groupName):
while len(self.threadsInUse.keys()) > 0:
time.sleep(0.5)
self.createLayerGroupsOnMainThread.emit(animationLayerObject, groupName)
def createLayerGroup(self, animationLayerObject, groupName):
layerList = animationLayerObject.getAnimationData().frameData.values()
groupifier = LayerGroupifier(layerList, groupName)
groupifier.statusSignal.connect(self.statusSignal)
groupifier.groupifyComplete.connect(self._newLegendGroupReady)
#We assign the generated group reference to this animationLayer object
animationLayerObject.setAnimationLegendGroups(groupifier.getGeneratedGroup())
groupifier.groupify()
def _newLegendGroupReady(self, qgsGroupObject):
self.animationGroups.append(qgsGroupObject)
if len(self.animationGroups) == self.maxAnimationGroupsToBeCreated:
self.animatorCreated()