#!/usr/bin/env python
# -*- coding: utf8 -*-
# *****************************************************************
# **       PTS -- Python Toolkit for working with SKIRT          **
# **       © Astronomical Observatory, Ghent University          **
# *****************************************************************

## \package pts.core.basics.timeline Timeline for moving a perspective instrument through space
#
# The class in this module allows constructing a timeline that defines the properties and motion
# of a perspective instrument through abstract world space. It is typically used to help produce
# a fly-through movie from a sequence of frames generated by the SKIRT perspective instrument.

# -----------------------------------------------------------------

# Import standard modules
import numpy as np

# Import the relevant PTS classes and modules
from ..tools import geometry as g

# -----------------------------------------------------------------

## This class allows constructing a timeline that defines the properties and motion
# of a perspective instrument through abstract world space. It is typically used to help produce
# a fly-through movie from a sequence of frames generated by the SKIRT perspective instrument.
#
# The constructor arguments specify the overall properties of the timeline (such as the duration) and
# of the corresponding movie (such as the frame rate). This defines the total number of frames in the
# timeline. Then there is a set of functions for setting up and adjusting the position, orientation
# and other properties of the perspective instrument at each point in the timeline. This results in
# a distinct set of instrument properties for each frame. Finally there is a function to return this
# sequence of instrument property sets, one for each frame. This can be used to build a movie.
#
# The constructor initializes all frames in the timeline to the same default set of property values.
# Each of the setter functions operates on the current contents of the frame sequence, adjusting the
# instrument properties for some or all of the frames. So in general the order in which functions
# are executed matters.
#
# The constructor also defines global spatial and temporal scales, so that all arguments in the setter
# functions are expressed in abstract length units (alu) and abstract time units (atu).
#
class Timeline:

    ## The constructor constructs a timeline with the specified overall properties and initializes all
    # frames in the timeline to the same set of instrument properties. The arguments are:
    #  - duration: the duration of the timeline (i.e. the length of the movie) in abstract time units.
    #  - timeunit: the duration of an abstract time unit (atu) in real-life movie seconds.
    #  - rate: the number of movie frames per real-life second.
    #  - lengthunit: the length of an abstract length unit in output units (usually parsec).
    #  - shape: the width and height of the viewport in pixels; the pixels are assumed to be square,
    #    so that this argument implicitly defines the viewport aspect ratio as well.
    #
    # The number of frames in the timeline is duration x timeunit x rate. All frames are initialized
    # to the following set of instrument properties, in abstract length units: viewport width = 1,
    # viewport position = (0,0,1), crosshair position = (0,0,0), upwards position = (0,1,0), and
    # focal length = 1. This corresponds to an observer looking down from the positive z-axis,
    # such that the xy-plane is oriented in the usual way.
    #
    def __init__(self, duration=10., timeunit=1., rate=24, lengthunit=1., shape=(500,500)):
        # global properties
        self.nx = int(shape[0])                 # number of viewport pixels in x direction
        self.ny = int(shape[1])                 # number of viewport pixels in y direction
        self.alu = float(lengthunit)            # one abstract length unit in output units
        self.atu = float(timeunit)              # one abstract time unit in seconds
        self.rate = int(rate)                   # number of movie frames per real-life second
        self.fpu = float(self.atu*rate)         # number of (fractional) frames per abstract time unit
        self.n = int(round(self.fpu*duration))  # total number of frames
        # instrument properties for each frame
        self.w = np.ones(self.n)                         # viewport widths
        self.v = np.zeros((self.n,3)); self.v[:,2] = 1.  # viewport positions
        self.c = np.zeros((self.n,3))                    # crosshair positions
        self.u = np.zeros((self.n,3)); self.u[:,1] = 1.  # upwards positions
        self.f = np.ones(self.n)                         # focal lengths

    ## This function returns the number of movie frames per real-life second in the timeline.
    def getrate(self):
        return self.rate

    ## This function returns the shape of the frames in the timeline, in pixels.
    def getshape(self):
        return (self.nx, self.ny)

    ## This function returns a list of duration x timeunit x rate tuples, i.e. one for each frame.
    # Each tuple contains the following information: viewport shape (in pixels), viewport size,
    # viewport position, crosshair position, upwards position, and focal length (all in output units).
    # The coordinates of each item are grouped in tuples, so the structure of the returned list is:
    # [ ((Nx,Ny),(Sx,Sy),(Vx,Vy,Vz),(Cx,Cy,Cz),(Ux,Uy,Uz),Fe) , ... ]
    #
    def getframes(self):
        result = []
        for i in range(self.n):
            frame = ( (self.nx, self.ny),
                      (self.w[i]*self.alu, self.w[i]*self.alu*float(self.ny)/float(self.nx)),
                      (self.v[i,0]*self.alu, self.v[i,1]*self.alu, self.v[i,2]*self.alu),
                      (self.c[i,0]*self.alu, self.c[i,1]*self.alu, self.c[i,2]*self.alu),
                      (self.u[i,0]*self.alu, self.u[i,1]*self.alu, self.u[i,2]*self.alu),
                      self.f[i]*self.alu )
            result += [ frame ]
        return result

    ## This function sets certain instrument properties for all frames in the timeline starting at a
    # specified point to some fixed value. The arguments are:
    #  - time: the first affected point in the timeline, in atu; the default value is 0 (which
    #    means that all frames in the timeline are affected).
    #  - width: the new viewport width in alu; if missing the property remains unchanged.
    #  - viewport: the new viewport position as 3-tuple in alu; if missing the property remains unchanged.
    #  - crosshair: the new crosshair position as 3-tuple in alu; if missing the property remains unchanged.
    #  - upwards: the new upwards position as 3-tuple in alu; if missing the property remains unchanged.
    #  - focal: the new focal length in alu; if missing the property remains unchanged.
    #
    def set(self, time=0., width=None, viewport=None, crosshair=None, upwards=None, focal=None):
        first = np.clip(int(round(self.fpu*time)), 0, self.n)
        if width!=None: self.w[first:] = width
        if viewport!=None: self.v[first:] = viewport
        if crosshair!=None: self.c[first:] = crosshair
        if upwards!=None: self.u[first:] = upwards
        if focal!=None: self.f[first:] = focal

    ## This function gradually moves the viewport position to a new position. Specifically:
    #  - for each frame within the specified time interval, the viewport position is shifted in the
    #    direction of the new position over a distance linearly interpolated between zero
    #    and the distance between the frame's viewport position and the new position;
    #  - for each frame after the specified time interval, the position is shifted over the same offset
    #    as the last frame in the interval;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - position: the new viewport position in alu; the default is the world coordinate origin.
    #
    def glide(self, interval=None, position=(0,0,0)):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        p = np.array(position, np.float)
        # shift the viewport position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            offset = fraction * (p - self.v[i])
            self.v[i] += offset
        # shift the viewport position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                offset = p - self.v[last]
            self.v[last:] += offset

    ## This function gradually advances the viewport position towards the crosshair. Specifically:
    #  - for each frame within the specified time interval, the viewport position is shifted in the
    #    direction of the frame's crosshair position over a distance linearly interpolated between zero
    #    and the specified total distance;
    #  - for each frame after the specified time interval, the position is shifted over the same offset
    #    as the last frame in the interval (i.e. independent of each frame's crosshair position);
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - distance: the total distance in alu the viewport is to be advanced; the default value is 1 alu.
    #
    def advance(self, interval=None, distance=1.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # shift the viewport position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            direction = self.c[i] - self.v[i]
            norm = np.sqrt(np.sum(direction*direction))
            offset = direction * (distance * fraction / norm)
            self.v[i] += offset
        # shift the viewport position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                direction = self.c[last] - self.v[last]
                norm = np.sqrt(np.sum(direction*direction))
                offset = direction * (distance / norm)
            self.v[last:] += offset

    ## This function gradually rotates the viewport position about an axis through the crosshair position,
    # defined as explained below. Specifically:
    #  - for each frame within the specified time interval, the viewport position is rotated about the
    #    axis determined for the frame over an angle linearly interpolated between zero and the specified
    #    total rotation angle;
    #  - for each frame after the specified time interval, the position is rotated about the axis
    #    determined for the last frame in the interval over the total rotation angle;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The rotation axis is always parallel with the viewport plane; by default it is oriented along
    # the viewport's y-axis but this can be adjusted by specifying a nonzero orientation angle.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise
    #    rotation when looking down from the positive rotation axis.
    #  - orientation: the orientation angle in degrees; zero means that the rotation axis has the same
    #    direction as the viewport's y-axis; a positive value indicates a rotation axis tilted clockwise
    #    when looking from the viewport position towards the crosshair position.
    #
    # Implementation note. Define G = V - C as the direction from the crosshair position to the viewport
    # position, and U as the upwards direction. If the orientation angle is zero, the direction R of the
    # rotation axis is given by the intersection of the viewport plane (the plane perpendicular to G) and
    # the projection plane of U on the viewport plane (the plane spanned by U and G). Considering that
    # the intersection of two planes lies in the direction of the cross product of their respective
    # normal vectors, and that the normal vector of a plane spanned by two vectors is given by the
    # cross product of these vectors, it follows that R = G x (U x G). If the orientation angle is
    # nonzero, R must be further rotated about G over the orientation angle.
    #
    def circle(self, interval=None, rotation=90., orientation=0.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # rotate the viewport position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            G = self.v[i] - self.c[i]
            U = self.u[i]
            direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
            point = self.c[i]
            self.v[i] = g.rotate(self.v[i], axispoint=point, axisdirection=direc, angle=fraction*rotation)
        # rotate the viewport position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                G = self.v[last] - self.c[last]
                U = self.u[last]
                direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
                point = self.c[last]
            for i in range(last,self.n):
                self.v[i] = g.rotate(self.v[i], axispoint=point, axisdirection=direc, angle=rotation)

    ## This function gradually rotates the crosshair position about an axis through the viewport position,
    # defined as explained below. Specifically:
    #  - for each frame within the specified time interval, the crosshair position is rotated about the
    #    axis determined for the frame over an angle linearly interpolated between zero and the specified
    #    total rotation angle;
    #  - for each frame after the specified time interval, the position is rotated about the axis
    #    determined for the last frame in the interval over the total rotation angle;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The rotation axis is always parallel with the viewport plane; by default is it oriented along
    # the viewport's y-axis but this can be adjusted by specifying a nonzero orientation angle.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise
    #    rotation when looking down from the positive rotation axis.
    #  - orientation: the orientation angle in degrees; zero means that the rotation axis has the same
    #    direction as the viewport's y-axis; a positive value indicates a rotation axis tilted clockwise
    #    when looking from the viewport position towards the crosshair position.
    #
    # Implementation note: see the implementation note for circle().
    #
    def pan(self, interval=None, rotation=90., orientation=0.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # rotate the crosshair position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            G = self.v[i] - self.c[i]
            U = self.u[i]
            direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
            point = self.v[i]
            self.c[i] = g.rotate(self.c[i], axispoint=point, axisdirection=direc, angle=fraction*rotation)
        # rotate the crosshair position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                G = self.v[last] - self.c[last]
                U = self.u[last]
                direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
                point = self.v[last]
            for i in range(last,self.n):
                self.c[i] = g.rotate(self.c[i], axispoint=point, axisdirection=direc, angle=rotation)

    ## This function gradually rotates the upwards position about the axis from the viewport position
    #  to the crosshair position. Specifically:
    #  - for each frame within the specified time interval, the crosshair position is rotated about the
    #    axis from the viewport position to the crosshair position over an angle linearly interpolated
    #    between zero and the specified total rotation angle;
    #  - for each frame after the specified time interval, the position is rotated about the axis
    #    determined for the last frame in the interval over the total rotation angle;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise
    #    rotation when looking from the viewport position to the crosshair position .
    #
    def roll(self, interval=None, rotation=90.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # rotate the crosshair position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            self.u[i] = g.rotate(self.u[i], axispoint=self.v[i], axisdirection=self.c[i]-self.v[i],
                                 angle=fraction*rotation)
        # rotate the crosshair position for frames beyond the specified range
        for i in range(last,self.n):
            self.u[i] = g.rotate(self.u[i], axispoint=self.v[i], axisdirection=self.c[i]-self.v[i], angle=rotation)

    ## This internal helper function returns a tuple with the first and last frame indices
    # corresponding to a time interval in the timeline, specified as a 2-tuple (start,end) in atu.
    # If only one value is specified it is taken as the start time and the end time defaults to the
    # end of the timeline; if the argument equals None, the interval defaults to the complete timeline.
    #
    def frames_for_interval(self, interval):
        if interval==None:
            first = 0
            last = self.n
        elif len(np.shape(interval))==0:
            first = np.clip(int(round(self.fpu*interval)), 0, self.n)
            last = self.n
        else:
            first = np.clip(int(round(self.fpu*interval[0])), 0, self.n)
            last = np.clip(int(round(self.fpu*interval[1]))+1, first, self.n)
        return (first,last)

# -----------------------------------------------------------------

#!/usr/bin/env python
# -*- coding: utf8 -*-
# *****************************************************************
# **       PTS -- Python Toolkit for working with SKIRT          **
# **       © Astronomical Observatory, Ghent University          **
# *****************************************************************

## \package pts.core.basics.timeline Timeline for moving a perspective instrument through space
#
# The class in this module allows constructing a timeline that defines the properties and motion
# of a perspective instrument through abstract world space. It is typically used to help produce
# a fly-through movie from a sequence of frames generated by the SKIRT perspective instrument.

# -----------------------------------------------------------------

# Import standard modules
import numpy as np

# Import the relevant PTS classes and modules
from ..tools import geometry as g

# -----------------------------------------------------------------

## This class allows constructing a timeline that defines the properties and motion
# of a perspective instrument through abstract world space. It is typically used to help produce
# a fly-through movie from a sequence of frames generated by the SKIRT perspective instrument.
#
# The constructor arguments specify the overall properties of the timeline (such as the duration) and
# of the corresponding movie (such as the frame rate). This defines the total number of frames in the
# timeline. Then there is a set of functions for setting up and adjusting the position, orientation
# and other properties of the perspective instrument at each point in the timeline. This results in
# a distinct set of instrument properties for each frame. Finally there is a function to return this
# sequence of instrument property sets, one for each frame. This can be used to build a movie.
#
# The constructor initializes all frames in the timeline to the same default set of property values.
# Each of the setter functions operates on the current contents of the frame sequence, adjusting the
# instrument properties for some or all of the frames. So in general the order in which functions
# are executed matters.
#
# The constructor also defines global spatial and temporal scales, so that all arguments in the setter
# functions are expressed in abstract length units (alu) and abstract time units (atu).
#
class Timeline:

    ## The constructor constructs a timeline with the specified overall properties and initializes all
    # frames in the timeline to the same set of instrument properties. The arguments are:
    #  - duration: the duration of the timeline (i.e. the length of the movie) in abstract time units.
    #  - timeunit: the duration of an abstract time unit (atu) in real-life movie seconds.
    #  - rate: the number of movie frames per real-life second.
    #  - lengthunit: the length of an abstract length unit in output units (usually parsec).
    #  - shape: the width and height of the viewport in pixels; the pixels are assumed to be square,
    #    so that this argument implicitly defines the viewport aspect ratio as well.
    #
    # The number of frames in the timeline is duration x timeunit x rate. All frames are initialized
    # to the following set of instrument properties, in abstract length units: viewport width = 1,
    # viewport position = (0,0,1), crosshair position = (0,0,0), upwards position = (0,1,0), and
    # focal length = 1. This corresponds to an observer looking down from the positive z-axis,
    # such that the xy-plane is oriented in the usual way.
    #
    def __init__(self, duration=10., timeunit=1., rate=24, lengthunit=1., shape=(500,500)):
        # global properties
        self.nx = int(shape[0])                 # number of viewport pixels in x direction
        self.ny = int(shape[1])                 # number of viewport pixels in y direction
        self.alu = float(lengthunit)            # one abstract length unit in output units
        self.atu = float(timeunit)              # one abstract time unit in seconds
        self.rate = int(rate)                   # number of movie frames per real-life second
        self.fpu = float(self.atu*rate)         # number of (fractional) frames per abstract time unit
        self.n = int(round(self.fpu*duration))  # total number of frames
        # instrument properties for each frame
        self.w = np.ones(self.n)                         # viewport widths
        self.v = np.zeros((self.n,3)); self.v[:,2] = 1.  # viewport positions
        self.c = np.zeros((self.n,3))                    # crosshair positions
        self.u = np.zeros((self.n,3)); self.u[:,1] = 1.  # upwards positions
        self.f = np.ones(self.n)                         # focal lengths

    ## This function returns the number of movie frames per real-life second in the timeline.
    def getrate(self):
        return self.rate

    ## This function returns the shape of the frames in the timeline, in pixels.
    def getshape(self):
        return (self.nx, self.ny)

    ## This function returns a list of duration x timeunit x rate tuples, i.e. one for each frame.
    # Each tuple contains the following information: viewport shape (in pixels), viewport size,
    # viewport position, crosshair position, upwards position, and focal length (all in output units).
    # The coordinates of each item are grouped in tuples, so the structure of the returned list is:
    # [ ((Nx,Ny),(Sx,Sy),(Vx,Vy,Vz),(Cx,Cy,Cz),(Ux,Uy,Uz),Fe) , ... ]
    #
    def getframes(self):
        result = []
        for i in range(self.n):
            frame = ( (self.nx, self.ny),
                      (self.w[i]*self.alu, self.w[i]*self.alu*float(self.ny)/float(self.nx)),
                      (self.v[i,0]*self.alu, self.v[i,1]*self.alu, self.v[i,2]*self.alu),
                      (self.c[i,0]*self.alu, self.c[i,1]*self.alu, self.c[i,2]*self.alu),
                      (self.u[i,0]*self.alu, self.u[i,1]*self.alu, self.u[i,2]*self.alu),
                      self.f[i]*self.alu )
            result += [ frame ]
        return result

    ## This function sets certain instrument properties for all frames in the timeline starting at a
    # specified point to some fixed value. The arguments are:
    #  - time: the first affected point in the timeline, in atu; the default value is 0 (which
    #    means that all frames in the timeline are affected).
    #  - width: the new viewport width in alu; if missing the property remains unchanged.
    #  - viewport: the new viewport position as 3-tuple in alu; if missing the property remains unchanged.
    #  - crosshair: the new crosshair position as 3-tuple in alu; if missing the property remains unchanged.
    #  - upwards: the new upwards position as 3-tuple in alu; if missing the property remains unchanged.
    #  - focal: the new focal length in alu; if missing the property remains unchanged.
    #
    def set(self, time=0., width=None, viewport=None, crosshair=None, upwards=None, focal=None):
        first = np.clip(int(round(self.fpu*time)), 0, self.n)
        if width!=None: self.w[first:] = width
        if viewport!=None: self.v[first:] = viewport
        if crosshair!=None: self.c[first:] = crosshair
        if upwards!=None: self.u[first:] = upwards
        if focal!=None: self.f[first:] = focal

    ## This function gradually moves the viewport position to a new position. Specifically:
    #  - for each frame within the specified time interval, the viewport position is shifted in the
    #    direction of the new position over a distance linearly interpolated between zero
    #    and the distance between the frame's viewport position and the new position;
    #  - for each frame after the specified time interval, the position is shifted over the same offset
    #    as the last frame in the interval;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - position: the new viewport position in alu; the default is the world coordinate origin.
    #
    def glide(self, interval=None, position=(0,0,0)):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        p = np.array(position, np.float)
        # shift the viewport position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            offset = fraction * (p - self.v[i])
            self.v[i] += offset
        # shift the viewport position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                offset = p - self.v[last]
            self.v[last:] += offset

    ## This function gradually advances the viewport position towards the crosshair. Specifically:
    #  - for each frame within the specified time interval, the viewport position is shifted in the
    #    direction of the frame's crosshair position over a distance linearly interpolated between zero
    #    and the specified total distance;
    #  - for each frame after the specified time interval, the position is shifted over the same offset
    #    as the last frame in the interval (i.e. independent of each frame's crosshair position);
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - distance: the total distance in alu the viewport is to be advanced; the default value is 1 alu.
    #
    def advance(self, interval=None, distance=1.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # shift the viewport position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            direction = self.c[i] - self.v[i]
            norm = np.sqrt(np.sum(direction*direction))
            offset = direction * (distance * fraction / norm)
            self.v[i] += offset
        # shift the viewport position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                direction = self.c[last] - self.v[last]
                norm = np.sqrt(np.sum(direction*direction))
                offset = direction * (distance / norm)
            self.v[last:] += offset

    ## This function gradually rotates the viewport position about an axis through the crosshair position,
    # defined as explained below. Specifically:
    #  - for each frame within the specified time interval, the viewport position is rotated about the
    #    axis determined for the frame over an angle linearly interpolated between zero and the specified
    #    total rotation angle;
    #  - for each frame after the specified time interval, the position is rotated about the axis
    #    determined for the last frame in the interval over the total rotation angle;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The rotation axis is always parallel with the viewport plane; by default it is oriented along
    # the viewport's y-axis but this can be adjusted by specifying a nonzero orientation angle.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise
    #    rotation when looking down from the positive rotation axis.
    #  - orientation: the orientation angle in degrees; zero means that the rotation axis has the same
    #    direction as the viewport's y-axis; a positive value indicates a rotation axis tilted clockwise
    #    when looking from the viewport position towards the crosshair position.
    #
    # Implementation note. Define G = V - C as the direction from the crosshair position to the viewport
    # position, and U as the upwards direction. If the orientation angle is zero, the direction R of the
    # rotation axis is given by the intersection of the viewport plane (the plane perpendicular to G) and
    # the projection plane of U on the viewport plane (the plane spanned by U and G). Considering that
    # the intersection of two planes lies in the direction of the cross product of their respective
    # normal vectors, and that the normal vector of a plane spanned by two vectors is given by the
    # cross product of these vectors, it follows that R = G x (U x G). If the orientation angle is
    # nonzero, R must be further rotated about G over the orientation angle.
    #
    def circle(self, interval=None, rotation=90., orientation=0.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # rotate the viewport position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            G = self.v[i] - self.c[i]
            U = self.u[i]
            direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
            point = self.c[i]
            self.v[i] = g.rotate(self.v[i], axispoint=point, axisdirection=direc, angle=fraction*rotation)
        # rotate the viewport position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                G = self.v[last] - self.c[last]
                U = self.u[last]
                direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
                point = self.c[last]
            for i in range(last,self.n):
                self.v[i] = g.rotate(self.v[i], axispoint=point, axisdirection=direc, angle=rotation)

    ## This function gradually rotates the crosshair position about an axis through the viewport position,
    # defined as explained below. Specifically:
    #  - for each frame within the specified time interval, the crosshair position is rotated about the
    #    axis determined for the frame over an angle linearly interpolated between zero and the specified
    #    total rotation angle;
    #  - for each frame after the specified time interval, the position is rotated about the axis
    #    determined for the last frame in the interval over the total rotation angle;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The rotation axis is always parallel with the viewport plane; by default is it oriented along
    # the viewport's y-axis but this can be adjusted by specifying a nonzero orientation angle.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise
    #    rotation when looking down from the positive rotation axis.
    #  - orientation: the orientation angle in degrees; zero means that the rotation axis has the same
    #    direction as the viewport's y-axis; a positive value indicates a rotation axis tilted clockwise
    #    when looking from the viewport position towards the crosshair position.
    #
    # Implementation note: see the implementation note for circle().
    #
    def pan(self, interval=None, rotation=90., orientation=0.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # rotate the crosshair position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            G = self.v[i] - self.c[i]
            U = self.u[i]
            direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
            point = self.v[i]
            self.c[i] = g.rotate(self.c[i], axispoint=point, axisdirection=direc, angle=fraction*rotation)
        # rotate the crosshair position for frames beyond the specified range
        if last!=self.n:
            if first==last:
                G = self.v[last] - self.c[last]
                U = self.u[last]
                direc = g.rotate(g.cross(G, g.cross(U, G)), axisdirection=G, angle=orientation)
                point = self.v[last]
            for i in range(last,self.n):
                self.c[i] = g.rotate(self.c[i], axispoint=point, axisdirection=direc, angle=rotation)

    ## This function gradually rotates the upwards position about the axis from the viewport position
    #  to the crosshair position. Specifically:
    #  - for each frame within the specified time interval, the crosshair position is rotated about the
    #    axis from the viewport position to the crosshair position over an angle linearly interpolated
    #    between zero and the specified total rotation angle;
    #  - for each frame after the specified time interval, the position is rotated about the axis
    #    determined for the last frame in the interval over the total rotation angle;
    #  - for frames before the specified time interval, the position is not affected.
    #
    # The function arguments are:
    #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is
    #    specified it is taken as the start time and the end time defaults to the end of the timeline;
    #    if the argument is missing the interval defaults to the complete timeline.
    #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise
    #    rotation when looking from the viewport position to the crosshair position .
    #
    def roll(self, interval=None, rotation=90.):
        # get the time interval as frame indices
        first,last = self.frames_for_interval(interval)
        # rotate the crosshair position for frames in the specified range
        for i in range(first,last):
            fraction = float(1+i-first)/float(last-first)
            self.u[i] = g.rotate(self.u[i], axispoint=self.v[i], axisdirection=self.c[i]-self.v[i],
                                 angle=fraction*rotation)
        # rotate the crosshair position for frames beyond the specified range
        for i in range(last,self.n):
            self.u[i] = g.rotate(self.u[i], axispoint=self.v[i], axisdirection=self.c[i]-self.v[i], angle=rotation)

    ## This internal helper function returns a tuple with the first and last frame indices
    # corresponding to a time interval in the timeline, specified as a 2-tuple (start,end) in atu.
    # If only one value is specified it is taken as the start time and the end time defaults to the
    # end of the timeline; if the argument equals None, the interval defaults to the complete timeline.
    #
    def frames_for_interval(self, interval):
        if interval==None:
            first = 0
            last = self.n
        elif len(np.shape(interval))==0:
            first = np.clip(int(round(self.fpu*interval)), 0, self.n)
            last = self.n
        else:
            first = np.clip(int(round(self.fpu*interval[0])), 0, self.n)
            last = np.clip(int(round(self.fpu*interval[1]))+1, first, self.n)
        return (first,last)

# -----------------------------------------------------------------

#!/usr/bin/env python # -*- coding: utf8 -*- # ***************************************************************** # **       PTS -- Python Toolkit for working with SKIRT          ** # **       © Astronomical Observatory, Ghent University          ** # ***************************************************************** ## \package pts.core.basics.timeline Timeline for moving a perspective instrument through space # # The class in this module allows constructing a timeline that defines the properties and motion # of a perspective instrument through abstract world space. It is typically used to help produce # a fly-through movie from a sequence of frames generated by the SKIRT perspective instrument. # ----------------------------------------------------------------- # Import standard modules # Import the relevant PTS classes and modules # ----------------------------------------------------------------- ## This class allows constructing a timeline that defines the properties and motion # of a perspective instrument through abstract world space. It is typically used to help produce # a fly-through movie from a sequence of frames generated by the SKIRT perspective instrument. # # The constructor arguments specify the overall properties of the timeline (such as the duration) and # of the corresponding movie (such as the frame rate). This defines the total number of frames in the # timeline. Then there is a set of functions for setting up and adjusting the position, orientation # and other properties of the perspective instrument at each point in the timeline. This results in # a distinct set of instrument properties for each frame. Finally there is a function to return this # sequence of instrument property sets, one for each frame. This can be used to build a movie. # # The constructor initializes all frames in the timeline to the same default set of property values. # Each of the setter functions operates on the current contents of the frame sequence, adjusting the # instrument properties for some or all of the frames. So in general the order in which functions # are executed matters. # # The constructor also defines global spatial and temporal scales, so that all arguments in the setter # functions are expressed in abstract length units (alu) and abstract time units (atu). # ## The constructor constructs a timeline with the specified overall properties and initializes all # frames in the timeline to the same set of instrument properties. The arguments are: #  - duration: the duration of the timeline (i.e. the length of the movie) in abstract time units. #  - timeunit: the duration of an abstract time unit (atu) in real-life movie seconds. #  - rate: the number of movie frames per real-life second. #  - lengthunit: the length of an abstract length unit in output units (usually parsec). #  - shape: the width and height of the viewport in pixels; the pixels are assumed to be square, #    so that this argument implicitly defines the viewport aspect ratio as well. # # The number of frames in the timeline is duration x timeunit x rate. All frames are initialized # to the following set of instrument properties, in abstract length units: viewport width = 1, # viewport position = (0,0,1), crosshair position = (0,0,0), upwards position = (0,1,0), and # focal length = 1. This corresponds to an observer looking down from the positive z-axis, # such that the xy-plane is oriented in the usual way. # # global properties # number of viewport pixels in x direction # number of viewport pixels in y direction # one abstract length unit in output units # one abstract time unit in seconds # number of movie frames per real-life second # number of (fractional) frames per abstract time unit # total number of frames # instrument properties for each frame # viewport widths # viewport positions # crosshair positions # upwards positions # focal lengths ## This function returns the number of movie frames per real-life second in the timeline. ## This function returns the shape of the frames in the timeline, in pixels. ## This function returns a list of duration x timeunit x rate tuples, i.e. one for each frame. # Each tuple contains the following information: viewport shape (in pixels), viewport size, # viewport position, crosshair position, upwards position, and focal length (all in output units). # The coordinates of each item are grouped in tuples, so the structure of the returned list is: # [ ((Nx,Ny),(Sx,Sy),(Vx,Vy,Vz),(Cx,Cy,Cz),(Ux,Uy,Uz),Fe) , ... ] # ## This function sets certain instrument properties for all frames in the timeline starting at a # specified point to some fixed value. The arguments are: #  - time: the first affected point in the timeline, in atu; the default value is 0 (which #    means that all frames in the timeline are affected). #  - width: the new viewport width in alu; if missing the property remains unchanged. #  - viewport: the new viewport position as 3-tuple in alu; if missing the property remains unchanged. #  - crosshair: the new crosshair position as 3-tuple in alu; if missing the property remains unchanged. #  - upwards: the new upwards position as 3-tuple in alu; if missing the property remains unchanged. #  - focal: the new focal length in alu; if missing the property remains unchanged. # ## This function gradually moves the viewport position to a new position. Specifically: #  - for each frame within the specified time interval, the viewport position is shifted in the #    direction of the new position over a distance linearly interpolated between zero #    and the distance between the frame's viewport position and the new position; #  - for each frame after the specified time interval, the position is shifted over the same offset #    as the last frame in the interval; #  - for frames before the specified time interval, the position is not affected. # # The function arguments are: #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is #    specified it is taken as the start time and the end time defaults to the end of the timeline; #    if the argument is missing the interval defaults to the complete timeline. #  - position: the new viewport position in alu; the default is the world coordinate origin. # # get the time interval as frame indices # shift the viewport position for frames in the specified range # shift the viewport position for frames beyond the specified range ## This function gradually advances the viewport position towards the crosshair. Specifically: #  - for each frame within the specified time interval, the viewport position is shifted in the #    direction of the frame's crosshair position over a distance linearly interpolated between zero #    and the specified total distance; #  - for each frame after the specified time interval, the position is shifted over the same offset #    as the last frame in the interval (i.e. independent of each frame's crosshair position); #  - for frames before the specified time interval, the position is not affected. # # The function arguments are: #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is #    specified it is taken as the start time and the end time defaults to the end of the timeline; #    if the argument is missing the interval defaults to the complete timeline. #  - distance: the total distance in alu the viewport is to be advanced; the default value is 1 alu. # # get the time interval as frame indices # shift the viewport position for frames in the specified range # shift the viewport position for frames beyond the specified range ## This function gradually rotates the viewport position about an axis through the crosshair position, # defined as explained below. Specifically: #  - for each frame within the specified time interval, the viewport position is rotated about the #    axis determined for the frame over an angle linearly interpolated between zero and the specified #    total rotation angle; #  - for each frame after the specified time interval, the position is rotated about the axis #    determined for the last frame in the interval over the total rotation angle; #  - for frames before the specified time interval, the position is not affected. # # The rotation axis is always parallel with the viewport plane; by default it is oriented along # the viewport's y-axis but this can be adjusted by specifying a nonzero orientation angle. # # The function arguments are: #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is #    specified it is taken as the start time and the end time defaults to the end of the timeline; #    if the argument is missing the interval defaults to the complete timeline. #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise #    rotation when looking down from the positive rotation axis. #  - orientation: the orientation angle in degrees; zero means that the rotation axis has the same #    direction as the viewport's y-axis; a positive value indicates a rotation axis tilted clockwise #    when looking from the viewport position towards the crosshair position. # # Implementation note. Define G = V - C as the direction from the crosshair position to the viewport # position, and U as the upwards direction. If the orientation angle is zero, the direction R of the # rotation axis is given by the intersection of the viewport plane (the plane perpendicular to G) and # the projection plane of U on the viewport plane (the plane spanned by U and G). Considering that # the intersection of two planes lies in the direction of the cross product of their respective # normal vectors, and that the normal vector of a plane spanned by two vectors is given by the # cross product of these vectors, it follows that R = G x (U x G). If the orientation angle is # nonzero, R must be further rotated about G over the orientation angle. # # get the time interval as frame indices # rotate the viewport position for frames in the specified range # rotate the viewport position for frames beyond the specified range ## This function gradually rotates the crosshair position about an axis through the viewport position, # defined as explained below. Specifically: #  - for each frame within the specified time interval, the crosshair position is rotated about the #    axis determined for the frame over an angle linearly interpolated between zero and the specified #    total rotation angle; #  - for each frame after the specified time interval, the position is rotated about the axis #    determined for the last frame in the interval over the total rotation angle; #  - for frames before the specified time interval, the position is not affected. # # The rotation axis is always parallel with the viewport plane; by default is it oriented along # the viewport's y-axis but this can be adjusted by specifying a nonzero orientation angle. # # The function arguments are: #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is #    specified it is taken as the start time and the end time defaults to the end of the timeline; #    if the argument is missing the interval defaults to the complete timeline. #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise #    rotation when looking down from the positive rotation axis. #  - orientation: the orientation angle in degrees; zero means that the rotation axis has the same #    direction as the viewport's y-axis; a positive value indicates a rotation axis tilted clockwise #    when looking from the viewport position towards the crosshair position. # # Implementation note: see the implementation note for circle(). # # get the time interval as frame indices # rotate the crosshair position for frames in the specified range # rotate the crosshair position for frames beyond the specified range ## This function gradually rotates the upwards position about the axis from the viewport position #  to the crosshair position. Specifically: #  - for each frame within the specified time interval, the crosshair position is rotated about the #    axis from the viewport position to the crosshair position over an angle linearly interpolated #    between zero and the specified total rotation angle; #  - for each frame after the specified time interval, the position is rotated about the axis #    determined for the last frame in the interval over the total rotation angle; #  - for frames before the specified time interval, the position is not affected. # # The function arguments are: #  - interval: a time interval in the timeline, as a 2-tuple (start,end) in atu; if only one value is #    specified it is taken as the start time and the end time defaults to the end of the timeline; #    if the argument is missing the interval defaults to the complete timeline. #  - rotation: the total rotation angle in degrees; a positive value indicates clockwise #    rotation when looking from the viewport position to the crosshair position . # # get the time interval as frame indices # rotate the crosshair position for frames in the specified range # rotate the crosshair position for frames beyond the specified range ## This internal helper function returns a tuple with the first and last frame indices # corresponding to a time interval in the timeline, specified as a 2-tuple (start,end) in atu. # If only one value is specified it is taken as the start time and the end time defaults to the # end of the timeline; if the argument equals None, the interval defaults to the complete timeline. # # -----------------------------------------------------------------