Export animations via PyQGIS / PyQGIS でアニメーションを自動エクスポート

This snippet loads a delimited text layer (CSV) from a file then exports the animation to the specified directory.
(The following script is modified from my actual code and I didn't check if it is working.)

The following snippet works for QGIS 3.22.0 (or later), which incorporates [my patch](https://github.com/qgis/QGIS/pull/44315).

~~As of QGIS 3.16.4, exporting with decorations is not possible. 
To do so, I modified the QGIS codebase and re-built it by my own: https://github.com/keigoi/QGIS/tree/script_decorations_3_16_4 and it seems working (the part `a.decorations = iface.activeDecorations()`). Building instructions is [here](https://github.com/qgis/QGIS/blob/master/INSTALL.md).~~

QGIS (pyqgis) で自動的にアニメーションをエクスポートしたかったのだがそういうスクリプトの例を見つけられなかったので自分で書いた。
ググラビリティを上げるために英語でもメモを残しておく。

```python

# Add a delimited text layer from a CSV with fields "time,lat,lng"
def addLayer(fileuri, name, stylefile):

    # specify the CSV format as a part of URI
    fileuri += "?encoding=UTF-8&delimiter=,&xField=lng&yField=lat&crs=epsg:4326"

    # create a delimited text layer
    layer=QgsVectorLayer(fileuri, name, "delimitedtext")

    # load the style from a file
    layer.loadNamedStyle(stylefile)

    # set up the temporarl properties
    p = layer.temporalProperties()
    p.setMode(QgsVectorLayerTemporalProperties.ModeFeatureDateTimeInstantFromField)
    p.setStartField('time')

    # set event duration length to 10 minutes (for example)
    p.setDurationUnits(QgsUnitTypes.TemporalUnit.TemporalMinutes)
    p.setFixedDuration(10)

    p.setIsActive(True)

    # check if layer is valid
    if not layer.isValid():
        print ("Layer not loaded")

    # add the layer to the project
    QgsProject.instance().addMapLayer(layer)

    lid = layer.id()

    # set visibility to False (if you want)
    # QgsProject.instance().layerTreeRoot().findLayer(lid).setItemVisibilityChecked(False)

    return layer

# export an animation 
def exportAnimation(outdir, extent, start, end, duration_mins, imageSize):
    # get map settings from the current canvas (seems mandatory?)
    s = iface.mapCanvas().mapSettings()
    s.setExtent(extent)
    s.setOutputSize(imageSize)

    a = QgsTemporalUtils.AnimationExportSettings()
    duration = QgsInterval()
    duration.setMinutes(duration_mins)

    a.frameDuration = duration
    a.outputDirectory = outdir
    a.fileNameTemplate = "####.png"

    begin=QDateTime.fromString(start, "yyyy-MM-dd HH:mm:ss")
    end=QDateTime.fromString(end, "yyyy-MM-dd HH:mm:ss")
    range_= QgsDateTimeRange(begin=begin,end=end)
    a.animationRange = range_

    # if you need decorations (seems unsupported for now), 
    # try building QGIS at https://github.com/keigoi/QGIS/tree/script_decorations_3_16_4
    # then do:
    # a.decorations = iface.activeDecorations()
    
    # start export
    QgsTemporalUtils.exportAnimation(s, a)
    

def eventloop():
    renderer = QgsMapRendererParallelJob(iface.mapCanvas().mapSettings())
    event_loop = QEventLoop()
    renderer.finished.connect(event_loop.quit)
    renderer.start()
    event_loop.exec_()
    
fileuri = "file:///Users/keigoi/test.csv"
name = 'my layer'
stylefile = '/Users/keigoi/magma2.qml'

l1 = addLayer(fileuri, name, stylefile)

# wait for QGIS to refresh
eventloop()

# output directory
outdir = "/Users/keigoi/outanim"

# size on the map (picked from the export animation dialog)
extent = QgsRectangle(15229993.1766,4183800.0296,15253400.5239,4194500.9741)

# the size of the image in pixels
imgsize = QSize(2048,1080)

# start and end time of the animation
# BUG: range interpreted 30 min later ?
start = "2018-12-31 23:30:00"
end = "2019-01-02 00:00:00"

# duration of a single frame
duration_mins = 30

exportAnimation(
    outdir, 
    extent,
    start,
    end,
    duration_mins,
    imgsize
)

# remove the layer
lroot = QgsProject.instance().layerTreeRoot()
lroot.removeLayer(lroot.findLayer(l1.id()).layer())