QGIS – Interpolating Points with QGIS Native Temporal Controller

geometry-generatorqgisspatio-temporal-datatemporal-controllertimemanager

QGIS introdced native temporal controller in QGIS version 3.14, thus replacing the older TimeManger Plugin. However, as of QGIS version 3.26, there are still some functionalities in the plugin that the native controller does not offer: when animating point movement (based on a series of points along a track with a time or datetime attribute), the plugin offered the possibility to interpolate points between two measured features, according to the time elapsed.

The native temporal controller by default is able to show/hide pre-defined points, depending on the frame of the animation. However, it does not offer any option to interpolate points in between – like halfway between two points after half the time is elapsed between the two points showing up.

Is there a way to imitate this bahviour in QGIS time controller: is there a possibility to interpolate points? There should be a possibility to imitate that using geometry generator.

This post is inspired by the comments in this answer: https://gis.stackexchange.com/a/438241/88814

Best Answer

The basic ideas

Interpolating points based on location and time is possible using Geometry generator. The basic idea to interpolate between point N and point N+1 (both with differend values for the datetime attribute arrival_time) is to create a line between both points, render just a substring of it, representing the percentage of the time elapsed at the current frame, and get the end-point of this substring.

Red point, interpolated between the blue points that correspond to the features of the layer: enter image description here

Details of the idea

  1. Create a line connecting each point with the following point, using make_line() from point N to point N+1

  2. Create a substring of this line using line_substring(): the starts at 0 (the start point of the line) and goes up to a certain length. This length corresponds to the percentage of the total time elapsed between the time defined for point N and point N+1.

    Example: if point N appears at 05:07 and N+1 at 07:37, total time between both points is 2:30 (=150 seconds). At 05:57 (= 50 seconds after point N), a third of the total time has elapsed - so the substring should cover the first thrid of the line

    To do so, get the middle of the current frame (mean of @map_start_time and @map_end_time) and culculate how many time has elapsed since the time the current feature appeared (here the attribute arrival_time): subtract arrival_time from the mean. Divide this value by the total length (time) between the arrival_time of the current and the next feature (N and N+1). This is the percentage of the total time between the two feature's appearing on the map that has elapsed during the current frame. So mulitply this percentage with the total length of the line to get the length of the substring that should be rendered.

  3. Create the end-point of the line with end_point()

  4. Set a condition that the line / end point appears only for time values covered by the current frame: in between N and N+1 (and not at every feature).

    Use a case condition with the variables @map_start_time and @map_end_time (defines the start- and end-time of the current animation frame) and the time/datetime attribute from the layer, here arrival_time. If the mean of the current frame (=the middle between @map_start_time and @map_end_time) is smaller then arrival_time of the next feature (N+1), but larger then arrival_time of the current feature, then the current frame shows a point between N and N+1. Only in this case, the line substring's end point should be shown.

What to be aware of: different time zones (attributes vs. variables)

This solution is based on the sample data provided here from the question Animating data with Temporal Controller - When loading the data (csv) to QGIS, each feature comes with an attribute arrival_time, interpreted as Central European Time (CET) on my system. As the variables for the animation like @animation_start_time are in UTC, I had to convert arrival_time to UTC, adding 2 hours: arrival_time + to_interval ('2 hours'). Adapt this value in the expression below.

Time Controller settings

  1. Make time controller settings in layer properties > Temporal tab: check the box Dynamic temporal control and for Configuration, use Redraw Layer Only.

  2. Open Temporal controller panel (no 1 in screenshot below), select the button in the middle Animated temporal navigation and click the icon with two arrows Set to full range to load min/max values for the anmination (no. two in screenshot.

  3. Set the step between each frame, here e.g. 1 second (no. 3). The higher the value, the faster the point moves.

  4. Click the yellow gear icon (no. 4) to change frame rate (frames per second). The higher the value, the faster the point moves.

enter image description here

The expression

Based on a point layer with a datatime attribute called arrival_time, the following expression creates an animated point that "moves", based on interpolated points as described above. Use it in Geometry Generator, make the relevant settings in Temporal controller panel as described above and start the animation by clicking the play button.

The expression to use in geometry generator looks like this. The expression uses the following custom variables:

  • @zone: number of hours to add to change from UTC to local time zone (change this value on line 3 for other time zones)
  • @arrival: changes the attribute value from the field arrival_time to UTC, based on the value defined in variable @zone
  • @next: gets the value for arrival_time attribute of the next feature
  • @mapstart: datetime timecode of the mean of the current animation frame (where in the timeline the animation currently is)
  • @line: creates a line from the current to the next feature (N to N+1)
with_variable (
    'zone',
    2,  -- change this value to add/subtract hours to/from attribute named arrival_time to transform local time zone to UTC
    with_variable(
        'arrival',
        arrival_time + to_interval (@zone  || ' hours'),
        with_variable(
            'next',
            attribute (get_feature_by_id (@layer, $id+1), 'arrival_time'),
            with_variable (
                'mapstart',
                @map_start_time + to_interval ((second (@map_end_time - @map_start_time)/2 ) || ' seconds'),
                with_variable (
                    'line',
                    make_line (
                        $geometry,
                        geometry( get_feature_by_id (@layer, $id+1))
                    ),
                    case 
                    when 
                        @mapstart < @next and @mapstart > arrival_time +  @frame_duration 
                    then 
                        end_point (
                            line_substring (
                                @line,
                                0,
                                length (@line) * 
                                minute ( @map_start_time + (second (@frame_duration )/2)  || ' seconds' - @arrival ) /
                                minute (  @next + to_interval (@zone  || ' hours') - @arrival)
                            )
                        )
                    
                end
                )
            )
        )
    )
)