vkf_trajectory.f90 Source File


This file depends on

sourcefile~~vkf_trajectory.f90~~EfferentGraph sourcefile~vkf_trajectory.f90 vkf_trajectory.f90 sourcefile~vkf_math.f90 vkf_math.f90 sourcefile~vkf_trajectory.f90->sourcefile~vkf_math.f90

Files dependent on this one

sourcefile~~vkf_trajectory.f90~~AfferentGraph sourcefile~vkf_trajectory.f90 vkf_trajectory.f90 sourcefile~vkf_artists.f90 vkf_artists.f90 sourcefile~vkf_artists.f90->sourcefile~vkf_trajectory.f90 sourcefile~vkf_frames.f90 vkf_frames.f90 sourcefile~vkf_artists.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_follower.f90 vkf_follower.f90 sourcefile~vkf_follower.f90->sourcefile~vkf_trajectory.f90 sourcefile~vkf_frames.f90->sourcefile~vkf_trajectory.f90 sourcefile~vkf_frames.f90->sourcefile~vkf_follower.f90 sourcefile~vkf_handles.f90 vkf_handles.f90 sourcefile~vkf_handles.f90->sourcefile~vkf_trajectory.f90 sourcefile~vkf_handles.f90->sourcefile~vkf_artists.f90 sourcefile~vkf_handles.f90->sourcefile~vkf_frames.f90 sourcefile~vkframes.f90 vkframes.f90 sourcefile~vkframes.f90->sourcefile~vkf_trajectory.f90 sourcefile~vkframes.f90->sourcefile~vkf_artists.f90 sourcefile~vkframes.f90->sourcefile~vkf_follower.f90 sourcefile~vkframes.f90->sourcefile~vkf_frames.f90 sourcefile~vkframes.f90->sourcefile~vkf_handles.f90 sourcefile~vkf_hud.f90 vkf_hud.f90 sourcefile~vkframes.f90->sourcefile~vkf_hud.f90 sourcefile~vkf_line_segments.f90 vkf_line_segments.f90 sourcefile~vkframes.f90->sourcefile~vkf_line_segments.f90 sourcefile~vkf_model.f90 vkf_model.f90 sourcefile~vkframes.f90->sourcefile~vkf_model.f90 sourcefile~vkf_shadow.f90 vkf_shadow.f90 sourcefile~vkframes.f90->sourcefile~vkf_shadow.f90 sourcefile~vkf_shapes.f90 vkf_shapes.f90 sourcefile~vkframes.f90->sourcefile~vkf_shapes.f90 sourcefile~vkf_sphere.f90 vkf_sphere.f90 sourcefile~vkframes.f90->sourcefile~vkf_sphere.f90 sourcefile~vkf_terrain.f90 vkf_terrain.f90 sourcefile~vkframes.f90->sourcefile~vkf_terrain.f90 sourcefile~vkf_view.f90 vkf_view.f90 sourcefile~vkframes.f90->sourcefile~vkf_view.f90 sourcefile~vkf_window.f90 vkf_window.f90 sourcefile~vkframes.f90->sourcefile~vkf_window.f90 sourcefile~vkf_hud.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_line_segments.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_model.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_shadow.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_shadow.f90->sourcefile~vkf_sphere.f90 sourcefile~vkf_shadow.f90->sourcefile~vkf_terrain.f90 sourcefile~vkf_shapes.f90->sourcefile~vkf_artists.f90 sourcefile~vkf_shapes.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_sphere.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_terrain.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_view.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_window.f90->sourcefile~vkf_frames.f90 sourcefile~vkf_window.f90->sourcefile~vkf_handles.f90 sourcefile~vkf_window.f90->sourcefile~vkf_hud.f90 sourcefile~vkf_window.f90->sourcefile~vkf_view.f90

Source Code

!> Trajectory data: times with positions, attitudes and optional data vectors, as
!> in OpenFrames' Trajectory.
!>
!> Data are appended: each time may get one position (with [[num_optionals]]
!> optional vectors), one attitude and one color, so there may be fewer
!> positions, attitudes or colors than times (data can arrive separately).
!> Colors are optional (not in OpenFrames): a CurveArtist draws a trajectory
!> with colors in them, blended along each segment between its points'
!> colors (see CurveArtist%set_point_colors). No sorting is done; times
!> are expected to be monotonic, increasing or decreasing. Indices are 1-based.
!>
!> Differences from OpenFrames:
!> * Storage grows by doubling, positions, attitudes and colors separately, so
!>   a trajectory without attitudes (colors) holds no attitude (color) memory.
!>   [[reserve]] sets it for a known size; [[compact]] frees what's unused once a
!>   trajectory is complete (one copy of each array; [[storage_bytes]] says
!>   what is held).
!> * Instead of subscriber callbacks, a trajectory counts its changes
!>   ([[revision]]) and clears ([[generation]]): a reader such as an artist compares
!>   them with what it last saw and updates only what changed (e.g. uploads only
!>   the points appended since).
!> * No locking: vkframes updates and renders on one thread.
!>
!> Between points, a trajectory is linear in time: `point_at(t)` is the point
!> at any time (the first or last point outside its times), and
!> `point_between(k, k2, t)` and `time_fraction(k, k2, t)` the same between
!> two points already found (followers, trace mode's tail, intermediate
!> markers and time handles use them).
module vkf_trajectory

    use, intrinsic :: iso_c_binding, only: c_float
    use, intrinsic :: iso_fortran_env, only: int64
    use vkf_math, only: wp

    implicit none

    private

    !> where a component of a DataSource comes from (OpenFrames' SourceType)
    integer, parameter, public :: SOURCE_ZERO = 0       !! 0
    integer, parameter, public :: SOURCE_TIME = 1       !! the time
    integer, parameter, public :: SOURCE_POSOPT = 2     !! an element of the position or of an optional
    integer, parameter, public :: SOURCE_ATTITUDE = 3   !! an element of the attitude quaternion

    !> results of Trajectory%time_index (as OpenFrames' getTimeIndex)
    integer, parameter, public :: TIME_NOT_FOUND = -2   !! the search did not converge
    integer, parameter, public :: TIME_OUT_OF_RANGE = -1 !! index = 0 (before the first time) or num_times + 1 (after the last)
    integer, parameter, public :: TIME_AT_BOUND = 0     !! index = 1 or num_times (at the first or last time)
    ! a positive result: found, with times(index) <= t < times(index+1) (for increasing times)

    !> Where one component (x, y or z) of a point is taken from, as OpenFrames'
    !> Trajectory::DataSource: `src` is a SOURCE_* constant; for SOURCE_POSOPT,
    !> `element` (1 to dof) of the position (`opt` = 0) or of optional `opt`;
    !> for SOURCE_ATTITUDE, `element` (1 to 4) of the quaternion. The value is
    !> multiplied by `scale`.
    type, public :: DataSource
        integer :: src = SOURCE_ZERO        !! where the component comes from: a SOURCE_* constant
        integer :: element = 1              !! element (1 to dof, or 1 to 4 for SOURCE_ATTITUDE)
        integer :: opt = 0                  !! (0: the position, k: optional k (SOURCE_POSOPT))
        real(wp) :: scale = 1.0_wp          !! factor the value is multiplied by
    end type DataSource

    !> A trajectory (see the module documentation).
    type, public :: Trajectory
        private
        integer :: dof_ = 3                                 !! degrees of freedom of positions and optionals (2 or 3)
        integer :: nopt_ = 0                                !! optionals per position
        integer :: n_times = 0                              !! number of times
        integer :: n_pos = 0                                !! number of positions (<= n_times)
        integer :: n_att = 0                                !! number of attitudes (<= n_times)
        integer :: n_col = 0                                !! number of colors (<= n_times)
        real(wp), allocatable :: t(:)                       !! (capacity): times
        real(wp), allocatable :: posopt(:,:,:)              !! (3, 0:nopt, capacity): position, then optionals
        real(wp), allocatable :: att(:,:)                   !! (4, capacity): quaternions [x, y, z, w]
        real(c_float), allocatable :: col(:,:)              !! (4, capacity): colors [r, g, b, a] (as drawn)
        integer :: revision_ = 0                            !! incremented on every change
        integer :: generation_ = 0                          !! incremented when the data are cleared
    contains
        procedure :: reserve, compact, storage_bytes
        procedure :: clear
        procedure :: set_dof, dof
        procedure :: set_num_optionals, num_optionals
        procedure :: num_times, num_positions, num_attitudes, num_colors, is_empty
        procedure :: add_time, add_position, add_attitude, add_color, set_optional
        generic :: add => add_point, add_points
        procedure, private :: add_point, add_points
        procedure :: time, position, attitude, optional, color
        procedure :: times, positions, attitudes, colors
        procedure :: time_range, time_distance, time_index
        procedure :: point, points, verify_data, num_points
        procedure :: time_fraction, point_between, point_at
        procedure :: revision, generation
    end type Trajectory

    !> The Trajectory constructor: `Trajectory(...)` makes one (see [[new_trajectory]] for its
    !> arguments).
    interface Trajectory
        module procedure new_trajectory
    end interface Trajectory

    !> The DataSource constructor: `DataSource(...)` makes one (see [[new_data_source]] for its
    !> arguments).
    interface DataSource
        module procedure new_data_source
    end interface DataSource

    public :: position_sources, optional_sources

contains

    ! ==================================================================
    ! construction and sizes

    !> A trajectory with `dof` (2 or 3, default 3) degrees of freedom and
    !> `nopt` (default 0) optional vectors per position.
    function new_trajectory(dof, nopt) result(traj)
        integer, intent(in), optional :: dof    !! degrees of freedom (2 or 3, default 3)
        integer, intent(in), optional :: nopt   !! optional vectors per position (default 0)
        type(Trajectory) :: traj
        if (present(dof)) traj%dof_ = max(2, min(3, dof))
        if (present(nopt)) traj%nopt_ = max(0, nopt)
    end function new_trajectory

    !> A data source (see [[DataSource]]); `element` defaults to 1, `opt` to 0,
    !> `scale` to 1.
    pure function new_data_source(src, element, opt, scale) result(s)
        integer, intent(in) :: src                      !! a SOURCE_* constant
        integer, intent(in), optional :: element        !! element (1 to dof, or 1 to 4 for an attitude; default 1)
        integer, intent(in), optional :: opt            !! (0: the position (default), k: optional k)
        real(wp), intent(in), optional :: scale         !! factor the value is multiplied by (default 1)
        type(DataSource) :: s
        s%src = src
        if (present(element)) s%element = element
        if (present(opt)) s%opt = opt
        if (present(scale)) s%scale = scale
    end function new_data_source

    !> The sources of a trajectory's positions: x, y, (z) of the position, and
    !> zero for components beyond `dof`.
    pure function position_sources(dof) result(s)
        integer, intent(in) :: dof      !! degrees of freedom (2 or 3)
        type(DataSource) :: s(3)
        integer :: k
        do k = 1, 3
            if (k <= dof) s(k) = DataSource(SOURCE_POSOPT, element=k)
        end do
    end function position_sources

    !> The sources of optional `opt`'s x, y, (z) (zero beyond `dof`, default
    !> 3), each times `scale` (default 1): e.g. a thrust vector stored per
    !> point, for [[SegmentArtist:set_end_data]] with relative ends.
    pure function optional_sources(opt, dof, scale) result(s)
        integer, intent(in) :: opt      !! the optional (1 to num_optionals)
        integer, intent(in), optional :: dof !! degrees of freedom (default 3)
        real(wp), intent(in), optional :: scale !! factor (default 1)
        type(DataSource) :: s(3)
        integer :: k, d
        d = 3
        if (present(dof)) d = dof
        do k = 1, min(3, d)
            s(k) = DataSource(SOURCE_POSOPT, element=k, opt=opt, scale=scale)
        end do
    end function optional_sources

    !> Reserve memory for `n` points: times, positions (with their optionals)
    !> unless `positions` is false, and attitudes and colors only if
    !> `attitudes` / `colors` is true. Optional: storage grows as needed, but
    !> reserving avoids reallocations for a known size. It never shrinks the
    !> storage (see [[compact]]).
    subroutine reserve(self, n, positions, attitudes, colors)
        class(Trajectory), intent(inout) :: self
        integer, intent(in) :: n                            !! number of points
        logical, intent(in), optional :: positions          !! reserve positions (default true)
        logical, intent(in), optional :: attitudes          !! reserve attitudes (default false)
        logical, intent(in), optional :: colors             !! reserve colors (default false)
        logical :: pos, att, col
        pos = .true.
        att = .false.
        col = .false.
        if (present(positions)) pos = positions
        if (present(attitudes)) att = attitudes
        if (present(colors)) col = colors
        call grow_times(self, n, exact=.true.)
        if (pos) call grow_positions(self, n, exact=.true.)
        if (att) call grow_attitudes(self, n, exact=.true.)
        if (col) call grow_colors(self, n, exact=.true.)
    end subroutine reserve

    !> Free the storage not used: each array is reallocated to the points it
    !> holds (one copy, an array at a time), and arrays holding none are
    !> deallocated. For a trajectory that is complete: growing doubles the
    !> storage, so up to about half of it can be unused. The data and what is
    !> drawn of them don't change; pointers from [[times]], [[positions]], ...
    !> must be fetched again. Points can still be added (storage grows again).
    subroutine compact(self)
        class(Trajectory), intent(inout) :: self
        real(wp), allocatable :: t(:), posopt(:,:,:), att(:,:)
        real(c_float), allocatable :: col(:,:)
        if (allocated(self%t)) then
            if (size(self%t) /= self%n_times) then
                allocate(t(self%n_times))
                t = self%t(1:self%n_times)
                call move_alloc(t, self%t)
            end if
            if (self%n_times == 0) deallocate(self%t)
        end if
        if (allocated(self%posopt)) then
            if (size(self%posopt, 3) /= self%n_pos) then
                allocate(posopt(3, 0:self%nopt_, self%n_pos))
                posopt = self%posopt(:, :, 1:self%n_pos)
                call move_alloc(posopt, self%posopt)
            end if
            if (self%n_pos == 0) deallocate(self%posopt)
        end if
        if (allocated(self%att)) then
            if (size(self%att, 2) /= self%n_att) then
                allocate(att(4, self%n_att))
                att = self%att(:, 1:self%n_att)
                call move_alloc(att, self%att)
            end if
            if (self%n_att == 0) deallocate(self%att)
        end if
        if (allocated(self%col)) then
            if (size(self%col, 2) /= self%n_col) then
                allocate(col(4, self%n_col))
                col = self%col(:, 1:self%n_col)
                call move_alloc(col, self%col)
            end if
            if (self%n_col == 0) deallocate(self%col)
        end if
    end subroutine compact

    !> The memory the trajectory's data take (bytes), used or not: its
    !> storage's capacity (see [[reserve]], [[compact]]).
    pure integer(int64) function storage_bytes(self) result(b)
        class(Trajectory), intent(in) :: self
        b = 0_int64
        if (allocated(self%t)) b = b + storage_size(self%t, int64) / 8 * size(self%t, kind=int64)
        if (allocated(self%posopt)) b = b + storage_size(self%posopt, int64) / 8 * size(self%posopt, kind=int64)
        if (allocated(self%att)) b = b + storage_size(self%att, int64) / 8 * size(self%att, kind=int64)
        if (allocated(self%col)) b = b + storage_size(self%col, int64) / 8 * size(self%col, kind=int64)
    end function storage_bytes

    !> Remove all points (keeping the memory).
    subroutine clear(self)
        class(Trajectory), intent(inout) :: self
        self%n_times = 0
        self%n_pos = 0
        self%n_att = 0
        self%n_col = 0
        self%generation_ = self%generation_ + 1
        self%revision_ = self%revision_ + 1
    end subroutine clear

    !> Set the degrees of freedom (2 or 3) of positions and optionals; clears the trajectory.
    subroutine set_dof(self, dof)
        class(Trajectory), intent(inout) :: self
        integer, intent(in) :: dof                          !! degrees of freedom (2 or 3)
        self%dof_ = max(2, min(3, dof))
        call self%clear()
    end subroutine set_dof

    !> Degrees of freedom (2 or 3) of positions and optionals.
    pure integer function dof(self)
        class(Trajectory), intent(in) :: self
        dof = self%dof_
    end function dof

    !> Set the number of optional vectors per position; clears the trajectory.
    subroutine set_num_optionals(self, nopt)
        class(Trajectory), intent(inout) :: self
        integer, intent(in) :: nopt                         !! optional vectors per position (>= 0)
        if (nopt /= self%nopt_ .and. allocated(self%posopt)) deallocate(self%posopt)
        self%nopt_ = max(0, nopt)
        call self%clear()
    end subroutine set_num_optionals

    !> Number of optional vectors per position.
    pure integer function num_optionals(self)
        class(Trajectory), intent(in) :: self
        num_optionals = self%nopt_
    end function num_optionals

    !> Number of times.
    pure integer function num_times(self)
        class(Trajectory), intent(in) :: self
        num_times = self%n_times
    end function num_times

    !> Number of positions (at most num_times).
    pure integer function num_positions(self)
        class(Trajectory), intent(in) :: self
        num_positions = self%n_pos
    end function num_positions

    !> Number of attitudes (at most num_times).
    pure integer function num_attitudes(self)
        class(Trajectory), intent(in) :: self
        num_attitudes = self%n_att
    end function num_attitudes

    !> Number of colors (at most num_times).
    pure integer function num_colors(self)
        class(Trajectory), intent(in) :: self
        num_colors = self%n_col
    end function num_colors

    !> True if the trajectory has no times.
    pure logical function is_empty(self)
        class(Trajectory), intent(in) :: self
        is_empty = self%n_times == 0
    end function is_empty

    !> Number of changes so far (any added, set or cleared data).
    pure integer function revision(self)
        class(Trajectory), intent(in) :: self
        revision = self%revision_
    end function revision

    !> Number of times the data were cleared (by [[clear]], [[set_dof]] or
    !> [[set_num_optionals]]): while it is unchanged, data were only appended.
    pure integer function generation(self)
        class(Trajectory), intent(in) :: self
        generation = self%generation_
    end function generation

    ! ==================================================================
    ! adding data

    !> Add a time at the end of the time list.
    subroutine add_time(self, t)
        class(Trajectory), intent(inout) :: self
        real(wp), intent(in) :: t                           !! the time
        call grow_times(self, self%n_times + 1)
        self%n_times = self%n_times + 1
        self%t(self%n_times) = t
        self%revision_ = self%revision_ + 1
    end subroutine add_time

    !> Add a position (the first [[dof]] elements of `pos` are used) for the next
    !> time that has none. `ok` is false if every time already has a position.
    subroutine add_position(self, pos, ok)
        class(Trajectory), intent(inout) :: self
        real(wp), intent(in) :: pos(:)          !! position (first [[dof]] elements used)
        logical, intent(out), optional :: ok    !! false if not added
        integer :: d
        if (present(ok)) ok = .false.
        if (self%n_pos >= self%n_times) return
        call grow_positions(self, self%n_pos + 1)
        self%n_pos = self%n_pos + 1
        d = min(self%dof_, size(pos))
        self%posopt(:, :, self%n_pos) = 0.0_wp
        self%posopt(1:d, 0, self%n_pos) = pos(1:d)
        self%revision_ = self%revision_ + 1
        if (present(ok)) ok = .true.
    end subroutine add_position

    !> Add an attitude quaternion [x, y, z, w] for the next time that has none.
    !> `ok` is false if every time already has an attitude.
    subroutine add_attitude(self, q, ok)
        class(Trajectory), intent(inout) :: self
        real(wp), intent(in) :: q(4)            !! quaternion [x, y, z, w]
        logical, intent(out), optional :: ok    !! false if not added
        if (present(ok)) ok = .false.
        if (self%n_att >= self%n_times) return
        call grow_attitudes(self, self%n_att + 1)
        self%n_att = self%n_att + 1
        self%att(:, self%n_att) = q
        self%revision_ = self%revision_ + 1
        if (present(ok)) ok = .true.
    end subroutine add_attitude

    !> Add a color ([r, g, b], alpha 1, or [r, g, b, a], 0 to 1) for the next
    !> time that has none. `ok` is false if every time already has a color.
    subroutine add_color(self, color, ok)
        class(Trajectory), intent(inout) :: self
        real(wp), intent(in) :: color(:)        !! [r, g, b] or [r, g, b, a] (0 to 1)
        logical, intent(out), optional :: ok    !! false if not added
        if (present(ok)) ok = .false.
        if (self%n_col >= self%n_times) return
        call grow_colors(self, self%n_col + 1)
        self%n_col = self%n_col + 1
        call to_float_rgba(self%col(:, self%n_col), color)
        self%revision_ = self%revision_ + 1
        if (present(ok)) ok = .true.
    end subroutine add_color

    !> rgba from [r, g, b] (alpha 1) or [r, g, b, a].
    pure subroutine to_float_rgba(rgba, color)
        real(c_float), intent(out) :: rgba(4)   !! [r, g, b, a]
        real(wp), intent(in) :: color(:)        !! [r, g, b] or [r, g, b, a] (0 to 1)
        integer :: n
        n = min(4, size(color))
        rgba = 1.0_c_float
        rgba(1:n) = real(color(1:n), c_float)
    end subroutine to_float_rgba

    !> Set optional vector `index` (1 to num_optionals) of the most recently
    !> added position. `ok` is false if there is no position or no such optional.
    subroutine set_optional(self, index, v, ok)
        class(Trajectory), intent(inout) :: self
        integer, intent(in) :: index            !! the optional (1 to num_optionals)
        real(wp), intent(in) :: v(:)            !! the vector (its first [[dof]] elements are used)
        logical, intent(out), optional :: ok    !! false if not set
        integer :: d
        if (present(ok)) ok = .false.
        if (index < 1 .or. index > self%nopt_ .or. self%n_pos == 0) return
        d = min(self%dof_, size(v))
        self%posopt(1:d, index, self%n_pos) = v(1:d)
        self%revision_ = self%revision_ + 1
        if (present(ok)) ok = .true.
    end subroutine set_optional

    !> Add a point: a time with an optional position, attitude and color
    !> ([r, g, b] or [r, g, b, a]).
    !>
    !> A circular orbit, a point a minute (with `orbit%add`, the generic name):
    !>
    !> ```fortran
    !> orbit = Trajectory()
    !> do k = 0, 360
    !>     t = 60.0_wp * k
    !>     a = 2.0_wp * pi * t / 5400.0_wp
    !>     call orbit%add(t, pos=7000.0_wp * [cos(a), sin(a), 0.0_wp])
    !> end do
    !> ```
    subroutine add_point(self, t, pos, att, color)
        class(Trajectory), intent(inout) :: self
        real(wp), intent(in) :: t                       !! time
        real(wp), intent(in), optional :: pos(:)        !! position (first [[dof]] elements used)
        real(wp), intent(in), optional :: att(4)        !! attitude quaternion [x, y, z, w]
        real(wp), intent(in), optional :: color(:)      !! [r, g, b] or [r, g, b, a] (0 to 1)
        call self%add_time(t)
        if (present(pos)) call self%add_position(pos)
        if (present(att)) call self%add_attitude(att)
        if (present(color)) call self%add_color(color)
    end subroutine add_point

    !> Add points in bulk: times `t(n)`, with optional positions `pos(dof, n)`,
    !> attitudes `att(4, n)` and colors `color(3 or 4, n)`. Times, positions,
    !> attitudes and colors must all have been complete before (as many of each
    !> as times) for the given ones to line up with `t`.
    !>
    !> Points computed elsewhere, all at once (`times(n)`, `positions(3, n)`):
    !>
    !> ```fortran
    !> call orbit%add(times, pos=positions)
    !> ```
    subroutine add_points(self, t, pos, att, color)
        class(Trajectory), intent(inout) :: self
        real(wp), intent(in) :: t(:)                    !! (n): times
        real(wp), intent(in), optional :: pos(:,:)      !! (dof, n): positions
        real(wp), intent(in), optional :: att(:,:)      !! (4, n): attitude quaternions [x, y, z, w]
        real(wp), intent(in), optional :: color(:,:)    !! (3 or 4, n): colors [r, g, b(, a)] (0 to 1)
        integer :: n, i0, d
        n = size(t)
        i0 = self%n_times
        call grow_times(self, i0 + n)
        self%t(i0+1:i0+n) = t
        self%n_times = i0 + n
        if (present(pos)) then
            n = min(size(pos, 2), self%n_times - self%n_pos)
            d = min(self%dof_, size(pos, 1))
            call grow_positions(self, self%n_pos + n)
            self%posopt(:, :, self%n_pos+1:self%n_pos+n) = 0.0_wp
            self%posopt(1:d, 0, self%n_pos+1:self%n_pos+n) = pos(1:d, 1:n)
            self%n_pos = self%n_pos + n
        end if
        if (present(att)) then
            n = min(size(att, 2), self%n_times - self%n_att)
            call grow_attitudes(self, self%n_att + n)
            self%att(:, self%n_att+1:self%n_att+n) = att(:, 1:n)
            self%n_att = self%n_att + n
        end if
        if (present(color)) then
            n = min(size(color, 2), self%n_times - self%n_col)
            d = min(4, size(color, 1))
            call grow_colors(self, self%n_col + n)
            self%col(:, self%n_col+1:self%n_col+n) = 1.0_c_float
            self%col(1:d, self%n_col+1:self%n_col+n) = real(color(1:d, 1:n), c_float)
            self%n_col = self%n_col + n
        end if
        self%revision_ = self%revision_ + 1
    end subroutine add_points

    ! ==================================================================
    ! reading data

    !> Time i.
    pure real(wp) function time(self, i)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i                            !! the time (1 to num_times)
        time = self%t(i)
    end function time

    !> Position i (z = 0 for a 2-DOF trajectory).
    pure function position(self, i) result(p)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i                            !! the position (1 to num_positions)
        real(wp) :: p(3)
        p = self%posopt(:, 0, i)
    end function position

    !> Attitude i (quaternion [x, y, z, w]).
    pure function attitude(self, i) result(q)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i                            !! the attitude (1 to num_attitudes)
        real(wp) :: q(4)
        q = self%att(:, i)
    end function attitude

    !> Color i ([r, g, b, a]).
    pure function color(self, i) result(c)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i                            !! the color (1 to num_colors)
        real(wp) :: c(4)
        c = real(self%col(:, i), wp)
    end function color

    !> Optional vector k (1 to num_optionals) of position i.
    pure function optional(self, i, k) result(v)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i, k                         !! the position (1 to num_positions); the optional (1 to num_optionals)
        real(wp) :: v(3)
        v = self%posopt(:, k, i)
    end function optional

    !> Pointer to the times (num_times), without copying.
    subroutine times(self, t)
        class(Trajectory), intent(in), target :: self
        real(wp), dimension(:), pointer, intent(out) :: t   !! (num_times): times (null if none)
        if (self%n_times == 0) then
            t => null()
        else
            t => self%t(1:self%n_times)
        end if
    end subroutine times

    !> Pointer to the positions (3, num_positions), without copying.
    subroutine positions(self, p)
        class(Trajectory), intent(in), target :: self
        real(wp), dimension(:,:), pointer, intent(out) :: p !! (3, num_positions): positions (null if none)
        if (self%n_pos == 0) then
            p => null()
        else
            p => self%posopt(:, 0, 1:self%n_pos)
        end if
    end subroutine positions

    !> Pointer to the attitudes (4, num_attitudes), without copying.
    subroutine attitudes(self, q)
        class(Trajectory), intent(in), target :: self
        real(wp), dimension(:,:), pointer, intent(out) :: q !! (4, num_attitudes): quaternions [x, y, z, w] (null if none)
        if (self%n_att == 0) then
            q => null()
        else
            q => self%att(:, 1:self%n_att)
        end if
    end subroutine attitudes

    !> Pointer to the colors (4, num_colors; single precision, as drawn),
    !> without copying.
    subroutine colors(self, c)
        class(Trajectory), intent(in), target :: self
        real(c_float), dimension(:,:), pointer, intent(out) :: c !! (4, num_colors): colors [r, g, b, a] (null if none)
        if (self%n_col == 0) then
            c => null()
        else
            c => self%col(:, 1:self%n_col)
        end if
    end subroutine colors

    !> The first and last times, ordered (t0 <= tf); false if there are no times.
    logical function time_range(self, t0, tf)
        class(Trajectory), intent(in) :: self
        real(wp), intent(out) :: t0, tf                     !! first and last times (t0 <= tf; 0 if no times)
        time_range = self%n_times > 0
        if (.not. time_range) then
            t0 = 0.0_wp
            tf = 0.0_wp
            return
        end if
        t0 = min(self%t(1), self%t(self%n_times))
        tf = max(self%t(1), self%t(self%n_times))
    end function time_range

    !> The "distance" from time t to the time range [t0, tf]: > 0 outside it (to
    !> the nearer end), 0 at its ends, < 0 inside; huge() without times.
    !> (OpenFrames' getTimeDistance.)
    pure real(wp) function time_distance(self, t)
        class(Trajectory), intent(in) :: self
        real(wp), intent(in) :: t                           !! the time
        real(wp) :: t0, tf
        if (self%n_times == 0) then
            time_distance = huge(1.0_wp)
            return
        end if
        t0 = min(self%t(1), self%t(self%n_times))
        tf = max(self%t(1), self%t(self%n_times))
        if (t <= t0) then
            time_distance = t0 - t
        else if (t >= tf) then
            time_distance = t - tf
        else
            time_distance = (t - t0) * (t - tf)
        end if
    end function time_distance

    !> Find the interval of the time list containing t, by bisection (times
    !> increasing or decreasing). Returns a TIME_* code, or the number of
    !> iterations (> 0) when found, with times(index) <= t < times(index+1) for
    !> increasing times (>= and > for decreasing). See the TIME_* constants for
    !> `index` in the other cases.
    integer function time_index(self, t, index) result(status)
        class(Trajectory), intent(in) :: self
        real(wp), intent(in) :: t                       !! the time
        integer, intent(out) :: index                   !! the interval's first index (see the TIME_* constants)
        integer :: low, high, mid, iter, n
        real(wp) :: dir, dist
        n = self%n_times
        if (n == 0) then
            index = 0
            status = TIME_OUT_OF_RANGE
            return
        end if
        dir = merge(1.0_wp, -1.0_wp, self%t(1) <= self%t(n))
        dist = self%time_distance(t)
        if (dist > 0.0_wp) then
            index = merge(0, n + 1, dir * t < dir * self%t(1))
            status = TIME_OUT_OF_RANGE
            return
        else if (dist == 0.0_wp) then
            index = merge(1, n, t == self%t(1))
            status = TIME_AT_BOUND
            return
        end if
        low = 1
        high = n
        do iter = 1, 64
            mid = low + (high - low) / 2
            if (dir * t < dir * self%t(mid)) then
                high = mid
            else
                low = mid
            end if
            if (high - low <= 1) then
                index = low
                status = iter
                return
            end if
        end do
        index = low
        status = TIME_NOT_FOUND
    end function time_index

    !> The fraction of the way in time from point k to point k2 (any two
    !> points: e.g. k + 1, or k - 1 with times running backward) at time t,
    !> kept within 0 to 1 (0 if their times are the same). No checks: k and k2
    !> must be times.
    pure real(wp) function time_fraction(self, k, k2, t) result(frac)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: k, k2                        !! the points (1 to num_times)
        real(wp), intent(in) :: t                           !! the time
        frac = 0.0_wp
        if (self%t(k2) /= self%t(k)) frac = max(0.0_wp, min(1.0_wp, (t - self%t(k)) / (self%t(k2) - self%t(k))))
    end function time_fraction

    !> The point at time t between points k and k2 (components from
    !> `sources`): linear in time, within the two (time_fraction). Point k if
    !> k2 isn't a point (beyond the ends, or more times than data).
    pure function point_between(self, k, k2, t, sources) result(v)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: k                            !! a point (1 to num_points(sources))
        integer, intent(in) :: k2                           !! the other point (e.g. k + 1)
        real(wp), intent(in) :: t                           !! the time
        type(DataSource), intent(in) :: sources(3)          !! where the components come from
        real(wp) :: v(3)
        v = self%point(k, sources)
        if (k2 < 1 .or. k2 > self%num_points(sources)) return
        v = v + self%time_fraction(k, k2, t) * (self%point(k2, sources) - v)
    end function point_between

    !> The point at time t (components from `sources`; default: the
    !> position's), linear in time between the points about it; outside the
    !> times, the first or last point. `ok` (optional) false, and v 0, if there
    !> is no point (or the search for the time failed).
    function point_at(self, t, sources, ok) result(v)
        class(Trajectory), intent(in) :: self
        real(wp), intent(in) :: t                           !! the time
        type(DataSource), intent(in), optional :: sources(3)  !! where the components come from (default: the position)
        logical, intent(out), optional :: ok                !! a point was found
        real(wp) :: v(3)
        type(DataSource) :: src(3)
        integer :: n, i, status
        v = 0.0_wp
        if (present(ok)) ok = .false.
        src = position_sources(self%dof_)
        if (present(sources)) src = sources
        n = min(self%n_times, self%num_points(src))
        if (n == 0) return
        status = self%time_index(t, i)
        if (status == TIME_NOT_FOUND) return
        if (i < 1) then
            v = self%point(1, src)
        else if (i >= n) then
            v = self%point(n, src)
        else
            v = self%point_between(i, i + 1, t, src)
        end if
        if (present(ok)) ok = .true.
    end function point_at

    !> Point i with components from `sources` (OpenFrames' getPoint). No checks:
    !> use [[verify_data]] and [[num_points]] first.
    pure function point(self, i, sources) result(v)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i                            !! the point (1 to num_points(sources))
        type(DataSource), intent(in) :: sources(3)          !! where the components come from
        real(wp) :: v(3)
        integer :: k
        do k = 1, 3
            select case (sources(k)%src)
            case (SOURCE_TIME)
                v(k) = sources(k)%scale * self%t(i)
            case (SOURCE_POSOPT)
                v(k) = sources(k)%scale * self%posopt(sources(k)%element, sources(k)%opt, i)
            case (SOURCE_ATTITUDE)
                v(k) = sources(k)%scale * self%att(sources(k)%element, i)
            case default
                v(k) = 0.0_wp
            end select
        end do
    end function point

    !> Points i1, i1 + stride, ... (up to i2) with components from `sources`, into
    !> v(3, :) (as many columns as points; [[point]] for a range, a whole array
    !> slice per component). No checks: use [[verify_data]] and [[num_points]] first.
    pure subroutine points(self, i1, i2, sources, v, stride)
        class(Trajectory), intent(in) :: self
        integer, intent(in) :: i1, i2                   !! first and last points (1 to num_points(sources))
        type(DataSource), intent(in) :: sources(3)      !! where the components come from
        real(wp), intent(out) :: v(:,:)                 !! (3, number of points)
        integer, intent(in), optional :: stride         !! step between points (default 1)
        integer :: k, s
        s = 1
        if (present(stride)) s = stride
        do k = 1, 3
            associate (src => sources(k))
                select case (src%src)
                case (SOURCE_TIME)
                    v(k, :) = src%scale * self%t(i1:i2:s)
                case (SOURCE_POSOPT)
                    v(k, :) = src%scale * self%posopt(src%element, src%opt, i1:i2:s)
                case (SOURCE_ATTITUDE)
                    v(k, :) = src%scale * self%att(src%element, i1:i2:s)
                case default
                    v(k, :) = 0.0_wp
                end select
            end associate
        end do
    end subroutine points

    !> True if the trajectory has the data the sources refer to (elements and
    !> optionals in range).
    pure logical function verify_data(self, sources)
        class(Trajectory), intent(in) :: self
        type(DataSource), intent(in) :: sources(3)          !! where the components come from
        integer :: k
        verify_data = .true.
        do k = 1, 3
            select case (sources(k)%src)
            case (SOURCE_ZERO, SOURCE_TIME)
            case (SOURCE_POSOPT)
                if (sources(k)%element < 1 .or. sources(k)%element > self%dof_ .or. &
                    sources(k)%opt < 0 .or. sources(k)%opt > self%nopt_) verify_data = .false.
            case (SOURCE_ATTITUDE)
                if (sources(k)%element < 1 .or. sources(k)%element > 4) verify_data = .false.
            case default
                verify_data = .false.
            end select
        end do
    end function verify_data

    !> Number of points available with these sources: the number of times,
    !> limited by the positions or attitudes they use; huge() if all are zero.
    pure integer function num_points(self, sources)
        class(Trajectory), intent(in) :: self
        type(DataSource), intent(in) :: sources(3)          !! where the components come from
        integer :: k
        if (all(sources%src == SOURCE_ZERO)) then
            num_points = huge(0)
            return
        end if
        num_points = self%n_times
        do k = 1, 3
            if (sources(k)%src == SOURCE_POSOPT) num_points = min(num_points, self%n_pos)
            if (sources(k)%src == SOURCE_ATTITUDE) num_points = min(num_points, self%n_att)
        end do
    end function num_points

    ! ==================================================================
    ! storage (amortized doubling)

    !> The capacity to grow to for `needed` points: twice the current, or what is
    !> needed and a quarter more (so that appending after a large bulk add
    !> doesn't copy everything at once); `exact` (reserve): what is needed.
    pure integer function new_capacity(current, needed, exact)
        integer, intent(in) :: current                  !! current capacity
        integer, intent(in) :: needed                   !! points needed
        logical, intent(in), optional :: exact          !! (true: what is needed (default false))
        new_capacity = max(needed + needed / 4, 2 * current, 16)
        if (present(exact)) then
            if (exact) new_capacity = needed
        end if
    end function new_capacity

    !> Grow the times' storage to hold at least `n` (see [[new_capacity]]).
    subroutine grow_times(self, n, exact)
        type(Trajectory), intent(inout) :: self
        integer, intent(in) :: n                            !! times to hold
        logical, intent(in), optional :: exact              !! (true: exactly n (default false))
        real(wp), allocatable :: tmp(:)
        integer :: cap
        cap = 0
        if (allocated(self%t)) cap = size(self%t)
        if (n <= cap) return
        allocate(tmp(new_capacity(cap, n, exact)))
        if (self%n_times > 0) tmp(1:self%n_times) = self%t(1:self%n_times)
        call move_alloc(tmp, self%t)
    end subroutine grow_times

    !> Grow the positions' (and optionals') storage to hold at least `n`.
    subroutine grow_positions(self, n, exact)
        type(Trajectory), intent(inout) :: self
        integer, intent(in) :: n                            !! positions to hold
        logical, intent(in), optional :: exact              !! (true: exactly n (default false))
        real(wp), allocatable :: tmp(:,:,:)
        integer :: cap
        cap = 0
        if (allocated(self%posopt)) cap = size(self%posopt, 3)
        if (n <= cap) return
        allocate(tmp(3, 0:self%nopt_, new_capacity(cap, n, exact)))
        if (self%n_pos > 0) tmp(:, :, 1:self%n_pos) = self%posopt(:, :, 1:self%n_pos)
        call move_alloc(tmp, self%posopt)
    end subroutine grow_positions

    !> Grow the attitudes' storage to hold at least `n`.
    subroutine grow_attitudes(self, n, exact)
        type(Trajectory), intent(inout) :: self
        integer, intent(in) :: n                            !! attitudes to hold
        logical, intent(in), optional :: exact              !! (true: exactly n (default false))
        real(wp), allocatable :: tmp(:,:)
        integer :: cap
        cap = 0
        if (allocated(self%att)) cap = size(self%att, 2)
        if (n <= cap) return
        allocate(tmp(4, new_capacity(cap, n, exact)))
        if (self%n_att > 0) tmp(:, 1:self%n_att) = self%att(:, 1:self%n_att)
        call move_alloc(tmp, self%att)
    end subroutine grow_attitudes

    !> Grow the colors' storage to hold at least `n`.
    subroutine grow_colors(self, n, exact)
        type(Trajectory), intent(inout) :: self
        integer, intent(in) :: n                            !! colors to hold
        logical, intent(in), optional :: exact              !! (true: exactly n (default false))
        real(c_float), allocatable :: tmp(:,:)
        integer :: cap
        cap = 0
        if (allocated(self%col)) cap = size(self%col, 2)
        if (n <= cap) return
        allocate(tmp(4, new_capacity(cap, n, exact)))
        if (self%n_col > 0) tmp(:, 1:self%n_col) = self%col(:, 1:self%n_col)
        call move_alloc(tmp, self%col)
    end subroutine grow_colors

end module vkf_trajectory