// SPDX-FileCopyrightText: 2020 Julian Smith
// SPDX-License-Identifier: GPL-2.0-or-later

/*
Implementation of 'step' view system.

The basic idea here is calculate view position and direction by a series
of explicit steps. Steps can move to the origin of the user aircraft or a
multiplayer aircraft, or modify the current position by a fixed vector (e.g.
to move from aircraft origin to the pilot's eyepoint), or rotate the current
direction by a fixed transformation etc. We can also have a step that sets the
direction to point to a previously-calculated target position.

This is similar to what is already done by FlightGear's existing View code, but
making the individual steps explicit gives us more flexibility.

The dynamic nature of step views allows view cloning with composite-viewer.

We also allow views to be defined and created at runtime instead of being
hard-coded in *-set.xml files. For example this makes it possible to define a
view from the user's aircraft's pilot to the centre of a multiplayer aircraft
(or to a multiplayer aircraft's pilot).
*/

#include "sview.hxx"

#include <Main/fg_props.hxx>
#include <Main/globals.hxx>
#include <Scenery/scenery.hxx>
#include <Viewer/FGEventHandler.hxx>
#include <Viewer/renderer.hxx>
#include <Viewer/WindowBuilder.hxx>
#include <Viewer/WindowSystemAdapter.hxx>

#include <simgear/debug/logstream.hxx>
#include <simgear/math/SGMath.hxx>
#include <simgear/props/props.hxx>
#include <simgear/props/props_io.hxx>
#include <simgear/scene/util/OsgMath.hxx>
#include <simgear/scene/viewer/Compositor.hxx>

#include <osg/CameraView>
#include <osg/GraphicsContext>
#include <osgViewer/CompositeViewer>

static const double pi = 3.141592653589793238463;

static std::ostream& operator << (std::ostream& out, const osg::Vec3f& vec)
{
    return out << "Vec3f{"
            << " x=" << vec._v[0]
            << " y=" << vec._v[1]
            << " z=" << vec._v[2]
            << "}";
}

static std::ostream& operator << (std::ostream& out, const osg::Quat& quat)
{
    return out << "Quat{"
            << " x=" << quat._v[0]
            << " y=" << quat._v[1]
            << " z= " << quat._v[2]
            << " w=" << quat._v[3]
            << "}";
}

static std::ostream& operator << (std::ostream& out, const osg::Matrixd& matrix)
{
    osg::Vec3f  translation;
    osg::Quat   rotation;
    osg::Vec3f  scale;
    osg::Quat   so;
    matrix.decompose(translation, rotation, scale, so);
    return out << "Matrixd {"
            << " translation=" << translation
            << " rotation=" << rotation
            << " scale=" << scale
            << " so=" << so
            << "}";
}


/* A position and direction. Repeatedly modified by SviewStep::evaluate() when
calculating final camera position/orientation. */
struct SviewPosDir
{
    SviewPosDir()
    :
    heading(0),
    pitch(0),
    roll(0),
    target_is_set(false)
    {
    }

    SGGeod  position;
    double  heading;
    double  pitch;
    double  roll;

    SGGeod  target;
    bool    target_is_set;

    /* The final position and direction, in a form suitable for setting an
    osg::Camera's view matrix. */
    SGVec3d position2;
    SGQuatd direction2;

    /* If a step sets either/both of these to non-zero, the view will alter
    zoom to accommodate the required field of views (in degreea). */
    double fov_h = 0;
    double fov_v = 0;

    friend std::ostream& operator<< (std::ostream& out, const SviewPosDir& posdir)
    {
        out << "SviewPosDir {"
                << " position=" << posdir.position
                << " heading=" << posdir.heading
                << " pitch=" << posdir.pitch
                << " roll=" << posdir.roll
                << " target=" << posdir.target
                << " position2=" << posdir.position2
                << " direction2=" << posdir.direction2
                << "}"
                ;
        return out;
    }
};


/* Generic damping support. Improved version of class in view.hxx and view.cxx.

We model dx/dt = (target-current)/damping_time, so damping_time is time for
value to change by a factor of e. */
struct Damping {

    /* If m_wrap_max is non-zero, we wrap to ensure values are always between 0
    and m_wrap_max. E.g. use m_wrap_max=360 for an angle in degrees. */
    Damping(double damping_time, double wrap_max=0, double current=0)
    :
    m_damping_time(damping_time),
    m_current(current),
    m_wrap_max(wrap_max)
    {}

    /* Updates and returns new smoothed value. */
    double  update(double dt, double target)
    {
        const double e = 2.718281828459045;
        double delta = target - m_current;
        if (m_wrap_max) {
            if (delta < -m_wrap_max/2)  delta += m_wrap_max;
            if (delta >= m_wrap_max/2)  delta -= m_wrap_max;
        }
        m_current = target - delta * pow(e, -dt/m_damping_time);
        if (m_wrap_max) {
            if (m_current < 0)  m_current += m_wrap_max;
            if (m_current >= m_wrap_max)  m_current -= m_wrap_max;
        }
        return m_current;
    }

    /* Forces current value to be <current>. */
    double reset(double current)
    {
        m_current = current;
        return m_current;
    }

    private:
        double  m_damping_time;
        double  m_current;
        double  m_wrap_max;
};

/* Abstract base class for a single view step. A view step modifies a
SviewPosDir, e.g. translating the position and/or rotating the direction. */
struct SviewStep
{
    /* Updates <posdir>. */
    virtual void evaluate(SviewPosDir& posdir, double dt=0) = 0;

    /* Modify view angle. */
    virtual void mouse_drag(double delta_x_deg, double delta_y_deg)
    {
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStep>";
    }

    virtual ~SviewStep() {}

    std::string m_description;

    friend std::ostream& operator<< (std::ostream& out, const SviewStep& step)
    {
        out << ' ' << typeid(step).name();
        step.stream(out);
        return out;
    }
};

struct Callsign
/* Info about aircraft with a particular callsign. "" means the user
aircraft. We use the user aircraft if specified callsign does not exist.

update() must be called before m_root is dereferenced. */
{
    Callsign(const std::string& callsign)
    :
    m_callsign(callsign)
    {
    }

    bool update()
    /* Returns true if we have changed m_root. */
    {
        if (m_callsign == "") {
            if (m_root) return false;
        }
        else if (m_root && m_root->getStringValue("callsign") == m_callsign) {
            return false;
        }
        m_root = nullptr;
        if (m_callsign != "") {
            /* Find multiplayer with matching callsign. */
            SGPropertyNode* c = globals->get_props()->getNode("ai/models/callsigns")->getNode(m_callsign);
            if (c) {
                int i = c->getIntValue();
                m_root = globals->get_props()->getNode("ai/models/multiplayer", i);
                assert(m_root);
            }
        }
        if (!m_root) {
            /* Default to user aircraft. */
            m_root = globals->get_props();
        }
        return true;
    }

    std::string         m_callsign;
    SGPropertyNode_ptr m_root;
};

/* A step that sets position to aircraft origin and direction to aircraft's
longitudal axis. */
struct SviewStepAircraft : SviewStep
{
    SviewStepAircraft(const std::string& callsign)
    :
    m_callsign(callsign)
    {
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        if (m_callsign.update()) {
            m_longitude     = m_callsign.m_root->getNode("position/longitude-deg");
            m_latitude      = m_callsign.m_root->getNode("position/latitude-deg");
            m_altitude      = m_callsign.m_root->getNode("position/altitude-ft");
            m_heading       = m_callsign.m_root->getNode("orientation/true-heading-deg");
            m_pitch         = m_callsign.m_root->getNode("orientation/pitch-deg");
            m_roll          = m_callsign.m_root->getNode("orientation/roll-deg");
        }
        posdir.position = SGGeod::fromDegFt(
                m_longitude->getDoubleValue(),
                m_latitude->getDoubleValue(),
                m_altitude->getDoubleValue()
                );

        posdir.heading  = m_heading->getDoubleValue();
        posdir.pitch    = m_pitch->getDoubleValue();
        posdir.roll     = m_roll->getDoubleValue();
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepAircraft:" + m_callsign.m_callsign + ">";
    }

private:
    Callsign            m_callsign;

    SGPropertyNode_ptr  m_longitude;
    SGPropertyNode_ptr  m_latitude;
    SGPropertyNode_ptr  m_altitude;

    SGPropertyNode_ptr  m_heading;
    SGPropertyNode_ptr  m_pitch;
    SGPropertyNode_ptr  m_roll;
};


/* Moves position by fixed vector, does not change direction.

E.g. for moving from aircraft origin to pilot viewpoint. */
struct SviewStepMove : SviewStep
{
    SviewStepMove(double forward, double up, double right)
    :
    m_offset(-right, -up, -forward)
    {
        SG_LOG(SG_VIEW, SG_INFO, "forward=" << forward << " up=" << up << " right=" << right);
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        /* These calculations are copied from View::recalcLookFrom(). */

        /* The rotation rotating from the earth centered frame to the horizontal
        local frame. */
        SGQuatd hlOr = SGQuatd::fromLonLat(posdir.position);

        /* The rotation from the horizontal local frame to the basic view
        orientation. */
        SGQuatd hlToBody = SGQuatd::fromYawPitchRollDeg(posdir.heading, posdir.pitch, posdir.roll);

        /* Compute the eyepoints orientation and position wrt the earth
        centered frame - that is global coordinates. */
        SGQuatd ec2body = hlOr * hlToBody;

        /* The cartesian position of the basic view coordinate. */
        SGVec3d position = SGVec3d::fromGeod(posdir.position);

        /* This is rotates the x-forward, y-right, z-down coordinate system the
        where simulation runs into the OpenGL camera system with x-right, y-up,
        z-back. */
        SGQuatd q(-0.5, -0.5, 0.5, 0.5);

        position += (ec2body * q).backTransform(m_offset);
        posdir.position = SGGeod::fromCart(position);
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepMove>" << m_offset;
    }

    private:
    SGVec3d m_offset;
};

/* Modifies heading, pitch and roll by fixed amounts; does not change
position.

E.g. can be used to preserve direction (relative to aircraft) of Helicopter
view at the time it was cloned. */
struct SviewStepRotate : SviewStep
{
    SviewStepRotate(
            double heading,
            double pitch,
            double roll,
            double damping_heading = 0,
            double damping_pitch = 0,
            double damping_roll = 0
            )
    :
    m_heading(heading),
    m_pitch(pitch),
    m_roll(roll),
    m_damping_heading(damping_heading, 360 /*m_wrap_max*/),
    m_damping_pitch(damping_pitch, 360 /*m_wrap_max*/),
    m_damping_roll(damping_roll, 360 /*m_wrap_max*/)
    {
        SG_LOG(SG_VIEW, SG_INFO, "heading=" << heading << " pitch=" << pitch << " roll=" << roll);
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        posdir.heading  = m_damping_heading.update(dt, posdir.heading + m_heading);
        posdir.pitch    = m_damping_pitch.update(dt, posdir.pitch + m_pitch);
        posdir.roll     = m_damping_roll.update(dt, posdir.roll + m_roll);
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepRotate>"
                << ' ' << m_heading
                << ' ' << m_pitch
                << ' ' << m_roll
                ;
    }

    private:
    double  m_heading;
    double  m_pitch;
    double  m_roll;
    Damping m_damping_heading;
    Damping m_damping_pitch;
    Damping m_damping_roll;
};

/* Modify a view's heading and pitch in response to mouse dragging. */
struct SviewStepMouseDrag : SviewStep
{
    SviewStepMouseDrag(double heading_scale, double pitch_scale)
    :
    m_heading_scale(heading_scale),
    m_pitch_scale(pitch_scale)
    {}

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        posdir.heading += m_heading;
        posdir.pitch += m_pitch;
    }

    void mouse_drag(double delta_x_deg, double delta_y_deg) override
    {
        m_heading += m_heading_scale * delta_x_deg;
        m_pitch += m_pitch_scale * delta_y_deg;
    }

    double m_heading_scale;
    double m_pitch_scale;
    double m_heading = 0;
    double m_pitch = 0;
};

/* Multiply heading, pitch and roll by constants. */
struct SviewStepDirectionMultiply : SviewStep
{
    SviewStepDirectionMultiply(double heading=0, double pitch=0, double roll=0)
    :
    m_heading(heading),
    m_pitch(pitch),
    m_roll(roll)
    {
        SG_LOG(SG_VIEW, SG_INFO, "heading=" << heading << " pitch=" << pitch << " roll=" << roll);
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        posdir.heading *= m_heading;
        posdir.pitch *= m_pitch;
        posdir.roll *= m_roll;
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepDirectionMultiply>"
                << ' ' << m_heading
                << ' ' << m_pitch
                << ' ' << m_roll
                ;
    }

    private:
    double  m_heading;
    double  m_pitch;
    double  m_roll;
};

/* Copies current position to posdir.target. Used by SviewEyeTarget()
to make current position be available as target later on, e.g. by
SviewStepFinal with to_target=true. */
struct SviewStepCopyToTarget : SviewStep
{
    void evaluate(SviewPosDir& posdir, double dt) override
    {
        assert(!posdir.target_is_set);
        posdir.target = posdir.position;
        posdir.target_is_set = true;
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepCopyToTarget>";
    }
};

/* Move position to nearest tower. */
struct SviewStepNearestTower : SviewStep
{
    SviewStepNearestTower(const std::string& callsign)
    :
    m_callsign(callsign)
    {
        m_description = "Nearest tower";
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        if (m_callsign.update()) {
            m_latitude = m_callsign.m_root->getNode("sim/tower/latitude-deg", true /*create*/);
            m_longitude = m_callsign.m_root->getNode("sim/tower/longitude-deg", true /*create*/);
            m_altitude = m_callsign.m_root->getNode("sim/tower/altitude-ft", true /*create*/);
        }
        posdir.position = SGGeod::fromDegFt(
                m_longitude->getDoubleValue(),
                m_latitude->getDoubleValue(),
                m_altitude->getDoubleValue()
                );
        posdir.heading = 0;
        posdir.pitch = 0;
        posdir.roll = 0;
        SG_LOG(SG_VIEW, SG_BULK, "moved posdir.position to: " << posdir.position);
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepNearestTower:" + m_callsign.m_callsign + ">";
    }

    Callsign            m_callsign;
    SGPropertyNode_ptr  m_latitude;
    SGPropertyNode_ptr  m_longitude;
    SGPropertyNode_ptr  m_altitude;
};


/*
Converts posdir's eye position/direction to global cartesian
position/quarternion direction in position2 and direction2, which will be used
to set the camera parameters.

If posdir.target_is_set is true (i.e. posdir.target is valid), we also change
the direction to point at the target, using the original direction to determine
the 'up' vector.
*/
struct SviewStepFinal : SviewStep
{
    SviewStepFinal()
    {
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        /* See View::recalcLookFrom(). */

        /* The rotation rotating from the earth centered frame to the horizontal
        local frame. */
        SGQuatd eye_position_direction = SGQuatd::fromLonLat(posdir.position);

        /* The rotation from the horizontal local frame to the basic view
        orientation. */
        SGQuatd eye_local_direction = SGQuatd::fromYawPitchRollDeg(posdir.heading, posdir.pitch, posdir.roll);

        /* The cartesian position of the basic view coordinate. */
        SGVec3d eye_position = SGVec3d::fromGeod(posdir.position);

        /* Compute the eye direction in global coordinates. */
        SGQuatd eye_direction = eye_position_direction * eye_local_direction;

        if (posdir.target_is_set)
        {
            /* Rotate eye direction to point at posdir.target. */

            SGVec3d target_position = SGVec3d::fromGeod(posdir.target);

            /* add target offsets to at_position...
            Compute the eyepoints orientation and position wrt the earth centered
            frame - that is global coordinates _absolute_view_pos = eye_position; */

            /* the view direction. */
            SGVec3d eye_to_target_direction = normalize(target_position - eye_position);

            /* the up direction. */
            SGVec3d up = eye_direction.backTransform(SGVec3d(0, 0, -1));

            /* rotate -dir to the 2-th unit vector
            rotate up to 1-th unit vector
            Note that this matches the OpenGL camera coordinate system with
            x-right, y-up, z-back. */
            posdir.direction2 = SGQuatd::fromRotateTo(-eye_to_target_direction, 2, up, 1);
        }
        else
        {
            /* This is rotates the x-forward, y-right, z-down coordinate system the
            where simulation runs into the OpenGL camera system with x-right, y-up,
            z-back. */
            SGQuatd q(-0.5, -0.5, 0.5, 0.5);

            posdir.direction2 = eye_direction * q;
        }

        posdir.position2 = eye_position;
    }

    virtual void stream(std::ostream& out) const
    {
        out << " <SviewStepFinal>";
    }
};


/* Change angle and field of view so that we can see an aircraft and the ground
immediately below it. */
struct SviewStepAGL : SviewStep
{
    SviewStepAGL(const std::string& callsign, double damping_time)
    :
    m_callsign(callsign),
    relative_height_ground_damping(damping_time)
    {
    }

    void evaluate(SviewPosDir& posdir, double dt) override
    {
        if (m_callsign.update()) {
            m_chase_distance = -25;
            if (m_callsign.m_callsign == "") {
                m_chase_distance = globals->get_props()->getDoubleValue("/sim/chase-distance-m", m_chase_distance);
            }
            else {
                m_chase_distance = m_callsign.m_root->getDoubleValue("set/sim/chase-distance-m", m_chase_distance);
            }
        }
        assert(posdir.target_is_set);
        double _fov_user_deg = 30;
        double _configFOV_deg = 30;
        /* Some aircraft appear to have elevation that is slightly below ground
        level when on the ground, e.g. SenecaII, which makes get_elevation_m()
        fail. So we pass a slightly incremented elevation. */
        double                      ground_altitude = 0;
        const simgear::BVHMaterial* material = NULL;
        SGGeod                      target0 = posdir.target;
        SGGeod                      target_plus = posdir.target;
        target_plus.setElevationM(target_plus.getElevationM() + 1);
        bool ok = globals->get_scenery()->get_elevation_m(target_plus, ground_altitude, &material);
        if (ok) {
            m_ground_altitude = ground_altitude;
        }
        else {
            /* get_elevation_m() can fail if scenery has been un-cached, which
            appears to happen quite often with remote multiplayer aircraft, so
            we preserve the previous ground altitude to give some consistency
            and avoid confusing zooming when switching between views. */
            ground_altitude = m_ground_altitude;
        }

        double    h_distance = SGGeodesy::distanceM(posdir.position, posdir.target);
        if (h_distance == 0) {
            /* Not sure this should ever happen, but we need to cope with this
            here otherwise we'll get divide-by-zero. */
            return;
        }
        /* Find vertical region we want to be able to see. */
        double  relative_height_target = posdir.target.getElevationM() - posdir.position.getElevationM();
        double  relative_height_ground = ground_altitude - posdir.position.getElevationM();

        /* We expand the field of view so that it hopefully shows the whole
        aircraft and a little more of the ground.

        We use chase-distance as a crude measure of the aircraft's
        size. There doesn't seem to be any more definitive information.

        We damp our measure of ground level, to avoid the view jumping around
        if an aircraft flies over buildings. */

        relative_height_ground -= 2;

        double    aircraft_size_vertical = fabs(m_chase_distance) * 0.3;
        double    aircraft_size_horizontal = fabs(m_chase_distance) * 0.9;

        double    relative_height_target_plus = relative_height_target + aircraft_size_vertical;
        double    relative_height_ground_ = relative_height_ground;
        relative_height_ground = relative_height_ground_damping.update(dt, relative_height_ground);
        if (relative_height_ground > relative_height_target) {
            /* Damping of relative_height_ground can result in it being
            temporarily above the aircraft, so we ensure the aircraft is
            visible. */
            relative_height_ground = relative_height_ground_damping.reset(relative_height_ground_);
        }

        /* Not implemented yet: apply scaling from user field of view
        setting, altering only relative_height_ground so that the aircraft
        is always in view. */
        {
            double  delta = relative_height_target_plus - relative_height_ground;
            delta *= (_fov_user_deg / _configFOV_deg);
            relative_height_ground = relative_height_target_plus - delta;
        }

        double  angle_v_target = atan(relative_height_target_plus / h_distance);
        double  angle_v_ground = atan(relative_height_ground / h_distance);

        /* The target we want to use is determined by the midpoint of the two
        angles we've calculated. */
        double  angle_v_mid = (angle_v_target + angle_v_ground) / 2;
        double posdir_target_old_elevation = posdir.target.getElevationM();
        posdir.target.setElevationM(posdir.position.getElevationM() + h_distance * tan(angle_v_mid));

        /* Set required vertical field of view. We use fabs to avoid
        things being upside down if target is below ground level (e.g. new
        multiplayer aircraft are briefly at -9999ft). */
        double  fov_v = fabs(angle_v_target - angle_v_ground);

        /* Set required horizontal field of view so that we can see entire
        horizontal extent of the aircraft (assuming worst case where
        airplane is horizontal and square-on to viewer). */
        double  fov_h = 2 * atan(aircraft_size_horizontal / 2 / h_distance);

        posdir.fov_v = fov_v * 180 / pi;
        posdir.fov_h = fov_h * 180 / pi;

        bool verbose = false;
        if (0) {
            static time_t t0 = 0;
            time_t t = time(NULL);
            if (0 && t - t0 >= 3) {
                t0 = t;
                verbose = true;
            }
        }
        if (verbose) SG_LOG(SG_VIEW, SG_ALERT, ""
                << " target0=" << target0
                << " fov_v=" << fov_v * 180/pi
                << " ground_altitude=" << ground_altitude
                << " relative_height_target_plus=" << relative_height_target_plus
                << " h_distance=" << h_distance
                << " relative_height_ground=" << relative_height_ground
                << " m_chase_distance=" << m_chase_distance
                << " angle_v_mid=" << angle_v_mid * 180/pi
                << " posdir_target_old_elevation=" << posdir_target_old_elevation
                << " posdir.target=" << posdir.target
                );
    }

    double      m_chase_distance;
    Callsign    m_callsign;
    double      m_ground_altitude = 0;
    Damping     relative_height_ground_damping;
};

/* A step that takes the SviewPosDir's eye and target positions and treats
them as local and remote points respectively. We choose an eye position and
direction such that the local and remote points are both visible, with the
original eye position in the foreground at a fixed distance. */
struct SviewStepDouble : SviewStep
{
    SviewStepDouble()
    {
        m_local_chase_distance = 25;
        m_angle_rad = 15 * pi / 180;
    }

    SviewStepDouble(SGPropertyNode* config)
    {
        m_local_chase_distance = config->getDoubleValue("chase-distance");
        m_angle_rad = config->getDoubleValue("angle") * pi / 180;
    }
    void evaluate(SviewPosDir& posdir, double dt) override
    {
        /*
        We choose eye position so that we show the local aircraft a fixed
        amount below the view midpoint, and the remote aircraft the same fixed
        amount above the view midpoint.

        L: middle of local aircraft.
        R: middle of remote aircraft.
        E: desired eye-point

           ----             R
         /      \
        E        |
        |    L   | .................... H (horizon)
        |        |
         \      /
           ----

        We require that:

            EL is local aircraft's chase-distance so that local aircraft is in
            perfect view.

            Angle LER is fixed to give good view of both aircraft in
            window. (Should be related to the vertical angular size of the
            window, but at the moment we use a fixed value.)

        We need to calculate angle RLE, and add to HLR, in order to find
        position of E (eye) relative to L. Then for view pitch we use midpoint
        of angle of ER and angle EL, so that local and remote aircraft are
        symmetrically below and above the centre of the view.

        We find angle RLE by using cosine rule twice in the triangle RLE:
            ER^2 = EL^2 + LR^2 - 2*EL*LR*cos(RLE)
            LR^2 = ER^2 + EL^2 - 2*ER*EL*cos(LER)

        Wen end up with a quadratic for ER with solution:
            ER = EL * cos(LER) + sqrt(LR^2 - EL^22*sin(LER)^2)
            (We discard the -sqrt because it ends up with ER being negative.)

        and:
            cos(RLE) = (LR^2 + LE^2 - ER^2) / (2*LE*LR)

        So we can find RLE using acos().
        */
        bool debug = false;
        static time_t t0 = 0;
        time_t t = time(NULL);
        if (0 && t - t0 > 3) {
            t0 = t;
            debug = true;
        }

        assert(posdir.target_is_set);
        SviewPosDir posdir_remote = posdir;
        SviewPosDir posdir_local = posdir;

        posdir_local.target = posdir_local.position;

        if (debug) {
            SG_LOG(SG_VIEW, SG_ALERT, " posdir       =" << posdir);
            SG_LOG(SG_VIEW, SG_ALERT, " posdir_local =" << posdir_local);
            SG_LOG(SG_VIEW, SG_ALERT, " posdir_remote=" << posdir_remote);
        }

        /* Create cartesian coordinates so we can calculate distance <lr>. */
        SGVec3d local_pos = SGVec3d::fromGeod(posdir_local.target);
        SGVec3d remote_pos = SGVec3d::fromGeod(posdir_remote.target);
        double lr = sqrt(distSqr(local_pos, remote_pos));

        /* Desired angle between local and remote aircraft in final view. */
        double ler = m_angle_rad;

        /* Distance of eye from local aircraft. */
        double le = m_local_chase_distance;

        /* Find <er>, the distance of eye from remote aircraft. Have to be
        careful to cope when there is no solution if remote is too close, and
        choose the +ve sqrt(). */
        double er_root_term = lr*lr - le*le*sin(ler)*sin(ler);
        if (er_root_term < 0) {
            /* This can happen if LR is too small, i.e. remote aircraft too
            close to local aircraft. */
            er_root_term = 0;
        }
        double er = le * cos(ler) + sqrt(er_root_term);

        /* Now find rle, angle at local aircraft between vector to remote
        aircraft and vector to desired eye position. Again we have to cope when
        a real solution is not possible. */
        double cos_rle = (lr*lr + le*le - er*er) / (2*le*lr);
        if (cos_rle > 1) cos_rle = 1;
        if (cos_rle < -1) cos_rle = -1;
        double rle = acos(cos_rle);
        double rle_deg = rle * 180 / pi;

        /* Now find the actual eye position. We do this by calculating heading
        and pitch from local aircraft L to eye position E, then using a
        temporary SviewStepMove. */
        double lr_vertical = posdir_remote.target.getElevationM() - posdir_local.target.getElevationM();
        double lr_horizontal = SGGeodesy::distanceM(posdir_local.target, posdir_remote.target);
        double hlr = atan2(lr_vertical, lr_horizontal);
        posdir_local.heading = SGGeodesy::courseDeg(
                posdir_local.target,
                posdir_remote.target
                );
        posdir_local.pitch = (hlr + rle) * 180 / pi;
        posdir_local.roll = 0;
        auto move = SviewStepMove(le, 0, 0);
        move.evaluate(posdir_local, 0 /*dt*/);

        /* At this point, posdir_local.position is eye position. We make
        posdir_local.pitch point from this eye position to halfway between the
        remote and local aircraft. */
        double er_vertical = posdir_remote.target.getElevationM()
                - posdir_local.position.getElevationM();
        double her = asin(er_vertical / er);
        double hel = (hlr + rle) - pi;                   // codespell:ignore hel
        posdir_local.pitch = (her + hel) / 2 * 180 / pi; // codespell:ignore hel
        posdir = posdir_local;

        /* Need to ensure that SviewStepFinal will not rotate the view to point
        at posdir.target. */
        posdir.target_is_set = false;

        if (debug) {
            SG_LOG(SG_VIEW, SG_ALERT, ""
                    << " lr=" << lr
                    << " ler=" << ler
                    << " er_root_term=" << er_root_term
                    << " er=" << er
                    << " cos_rle=" << cos_rle
                    << " rle_deg=" << rle_deg
                    << " lr_vertical=" << lr_vertical
                    << " lr_horizontal=" << lr_horizontal
                    << " hlr=" << hlr
                    << " posdir_local=" << posdir_local
                    << " posdir_remote=" << posdir_remote
                    );
        }
    }

    double m_local_chase_distance;
    double m_angle_rad;
};

/* Contains a list of SviewStep's that are used to evaluate a SviewPosDir
(position and orientation)
*/
struct SviewSteps
{
    void add_step(std::shared_ptr<SviewStep> step)
    {
        m_steps.push_back(step);
    }

    void add_step(SviewStep* step)
    {
        return add_step(std::shared_ptr<SviewStep>(step));
    }

    void evaluate(SviewPosDir& posdir, double dt, bool debug=false)
    {
        if (debug) SG_LOG(SG_VIEW, SG_ALERT, "evaluating m_name=" << m_name);
        for (auto step: m_steps) {
            step->evaluate(posdir, dt);
            if (debug) SG_LOG(SG_VIEW, SG_ALERT, "posdir=" << posdir);
        }
    };

    std::string m_name;
    std::vector<std::shared_ptr<SviewStep>>   m_steps;

    friend std::ostream& operator << (std::ostream& out, const SviewSteps& viewpos)
    {
        out << viewpos.m_name << " (" << viewpos.m_steps.size() << ")";
        for (auto step: viewpos.m_steps) {
            out << " " << *step;
        }
        return out;
    }
};


/* Base class for views.
*/
struct SviewView
{
    SviewView(osgViewer::View* osg_view)
    :
    m_osg_view(osg_view)
    {
        s_id += 1;
    }

    /* Description that also includes integer identifier. */
    const std::string  description2()
    {
        char    buffer[32];
        snprintf(buffer, sizeof(buffer), "[%i] ", s_id);
        return buffer + description();
    }

    /* Description of this view, used in window title etc. */
    virtual const std::string description() = 0;

    virtual ~SviewView()
    {
        if (!m_osg_view) {
            return;
        }
        osgViewer::ViewerBase* viewer_base = m_osg_view->getViewerBase();
        auto composite_viewer = dynamic_cast<osgViewer::CompositeViewer*>(viewer_base);
        assert(composite_viewer);
        for (unsigned i=0; i<composite_viewer->getNumViews(); ++i) {
            osgViewer::View* view = composite_viewer->getView(i);
            SG_LOG(SG_VIEW, SG_DEBUG, "composite_viewer view i=" << i << " view=" << view);
        }
        SG_LOG(SG_VIEW, SG_DEBUG, "removing m_osg_view=" << m_osg_view);
        composite_viewer->stopThreading();
        composite_viewer->removeView(m_osg_view);
        composite_viewer->startThreading();
    }

    /* Returns false if window has been closed. */
    virtual bool update(double dt) = 0;

    virtual void mouse_drag(double delta_x_deg, double delta_y_deg) = 0;

    /* Sets this view's camera position/orientation from <posdir>. */
    void posdir_to_view(SviewPosDir posdir)
    {
        /* FGViewMgr::update(). */
        osg::Vec3d  position    = toOsg(posdir.position2);
        osg::Quat   orientation = toOsg(posdir.direction2);

        osg::Camera* camera = m_osg_view->getCamera();
        osg::Matrix old_m = camera->getViewMatrix();
        /* This calculation is copied from CameraGroup::update(). */
        const osg::Matrix new_m(
                osg::Matrix::translate(-position)
                * osg::Matrix::rotate(orientation.inverse())
                );
        SG_LOG(SG_VIEW, SG_BULK, "old_m: " << old_m);
        SG_LOG(SG_VIEW, SG_BULK, "new_m: " << new_m);
        camera->setViewMatrix(new_m);

        if (posdir.fov_v || posdir.fov_h) {
            /* Update zoom to accommodate required vertical/horizontal field of
            views. */
            double fovy;
            double aspect_ratio;
            double zNear;
            double zFar;
            camera->getProjectionMatrixAsPerspective(fovy, aspect_ratio, zNear, zFar);
            if (posdir.fov_v) {
                camera->setProjectionMatrixAsPerspective(
                        posdir.fov_v,
                        aspect_ratio,
                        zNear,
                        zFar
                        );
                if (posdir.fov_h) {
                    /* Increase fov if necessary so that we include both
                    fov_h and fov_v. */
                    double aspect_ratio;
                    camera->getProjectionMatrixAsPerspective(
                            fovy,
                            aspect_ratio,
                            zNear,
                            zFar
                            );
                    if (fovy * aspect_ratio < posdir.fov_h) {
                        camera->setProjectionMatrixAsPerspective(
                                posdir.fov_v * posdir.fov_h / (fovy * aspect_ratio),
                                aspect_ratio,
                                zNear,
                                zFar
                                );
                    }
                }
            }
            else {
                camera->setProjectionMatrixAsPerspective(
                        posdir.fov_h / aspect_ratio,
                        aspect_ratio,
                        zNear,
                        zFar
                        );
            }
        }
    }

    osgViewer::View*                    m_osg_view = nullptr;
    simgear::compositor::Compositor*    m_compositor = nullptr;

    bool                                m_mouse_button2;
    double                              m_mouse_x = 0;
    double                              m_mouse_y = 0;

    static int s_id;
};

/* Converts legacy damping values (e.g. at-model-heading-damping) to a damping
time suitable for use by our Damping class. */
static double legacy_damping_time(double damping)
{
    if (damping <= 0)  return 0;
    return 1 / damping;
}

int SviewView::s_id = 0;

/* A view defined by a series of steps that defines an eye and a target. Used
for clones of main window views. */
struct SviewViewEyeTarget : SviewView
{
    SviewViewEyeTarget(osgViewer::View* view, SGPropertyNode* config)
    :
    SviewView(view)
    {
        if (config->getStringValue("type") == "legacy")
        {
            /* Legacy view. */
            std::string callsign = config->getStringValue("callsign");
            std::string callsign_desc = (callsign == "") ? "" : ": " + callsign;
            SG_LOG(SG_VIEW, SG_INFO, "callsign=" << callsign);

            if (config->getBoolValue("view/config/eye-fixed")) {
                SG_LOG(SG_VIEW, SG_INFO, "eye-fixed");
                m_steps.m_name = std::string() + "legacy tower" + callsign_desc;

                if (config->getStringValue("view/type") == "lookat") {
                    /* E.g. Tower view or Tower view AGL. */

                    /* Add a step to move to centre of aircraft. target offsets appear
                    to have reversed sign compared to what we require. */
                    m_steps.add_step(new SviewStepAircraft(callsign));
                    m_steps.add_step(new SviewStepMove(
                            -config->getDoubleValue("view/config/target-z-offset-m"),
                            -config->getDoubleValue("view/config/target-y-offset-m"),
                            -config->getDoubleValue("view/config/target-x-offset-m")
                            ));

                    /* Add a step to set pitch and roll to zero, otherwise view
                    from tower (as calculated by SviewStepFinal) rolls/pitches
                    with aircraft. */
                    m_steps.add_step(new SviewStepDirectionMultiply(
                            1 /* heading */,
                            0 /* pitch */,
                            0 /* roll */
                            ));

                    /* Current position is the target, so add a step that copies it to
                    SviewPosDir.target. */
                    m_steps.add_step(new SviewStepCopyToTarget);

                    /* Added steps to set .m_eye up so that it looks from the nearest
                    tower. */
                    m_steps.add_step(new SviewStepNearestTower(callsign));

                    if (config->getBoolValue("view/config/lookat-agl")) {
                        double damping = config->getDoubleValue("view/config/lookat-agl-damping");
                        double damping_time = log(10) * legacy_damping_time(damping);
                        SG_LOG(SG_VIEW, SG_DEBUG, "lookat-agl");
                        m_steps.add_step(new SviewStepAGL(callsign, damping_time));
                    }

                    m_steps.add_step(new SviewStepFinal);

                    /* Would be nice to add a step that moves towards the
                    target a little, like we do with Tower view look from. But
                    simply adding a SviewStepMove doesn't work because the
                    preceding SviewStepFinal has finalised the view angle etc.
                    */
                }
                else {
                    /* E.g. Tower view look from. */
                    m_steps.add_step(new SviewStepNearestTower(callsign));

                    /* Looks like Tower view look from's heading-offset is reversed. */
                    m_steps.add_step(new SviewStepRotate(
                            -globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg"),
                            globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg"),
                            globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg")
                            ));
                    /* Move forward a little as though one was walking towards
                    the window inside the tower; this might improve view of
                    aircraft near the tower on the ground. Ideally each tower
                    would have a 'diameter' property, but for now we just use
                    a hard-coded value. Also it would be nice to make this
                    movement not change the height. */
                    m_steps.add_step(new SviewStepMove(1, 0, 0));

                    m_steps.add_step(new SviewStepMouseDrag(
                            1 /*mouse_heading_scale*/,
                            1 /*mouse_pitch_scale*/
                            ));
                    m_steps.add_step(new SviewStepFinal);
                }
            }
            else {
                SG_LOG(SG_VIEW, SG_INFO, "not eye-fixed");
                /* E.g. Pilot view and Helicopter/Chase views. */

                SGPropertyNode* global_sim_view = globals->get_props()
                        ->getNode("sim/view", config->getIntValue("view-number-raw"));

                if (global_sim_view->getStringValue("type") == "lookat") {
                    /* E.g. Helicopter view and Chase views. */
                    m_steps.m_name = std::string() + "legacy helicopter/chase" + callsign_desc;
                    m_steps.add_step(new SviewStepAircraft(callsign));

                    /* Move to centre of aircraft. config/target-z-offset-m
                    seems to use +ve to indicate movement backwards relative
                    to the aircraft, so we need to negate the value we pass to
                    SviewStepMove(). */
                    m_steps.add_step(new SviewStepMove(
                            -config->getDoubleValue("view/config/target-z-offset-m"),
                            -config->getDoubleValue("view/config/target-y-offset-m"),
                            -config->getDoubleValue("view/config/target-x-offset-m")
                            ));

                    /* Add a step that crudely preserves or don't preserve
                    aircraft's heading, pitch and roll; this enables us to mimic
                    Helicopter and Chase views. In theory we should evaluate the
                    specified paths, but in practise we only need to multiply
                    current values by 0 or 1. */
                    assert(global_sim_view);
                    m_steps.add_step(new SviewStepDirectionMultiply(
                            global_sim_view->getStringValue("config/eye-heading-deg-path")[0] ? 1 : 0,
                            global_sim_view->getStringValue("config/eye-pitch-deg-path")[0] ? 1 : 0,
                            global_sim_view->getStringValue("config/eye-roll-deg-path")[0] ? 1 : 0
                            ));

                    /* Add a step that applies the current view rotation. We
                    use -ve heading and pitch because in the legacy
                    viewing system's Helicopter view etc, increments to
                    heading-offset-deg and pitch-offset-deg actually correspond
                    to a decreasing actual heading and pitch values. */
                    double  damping_heading = legacy_damping_time(config->getDoubleValue("view/config/at-model-heading-damping"));
                    double  damping_pitch = legacy_damping_time(config->getDoubleValue("view/config/at-model-pitch-damping"));
                    double  damping_roll = legacy_damping_time(config->getDoubleValue("view/config/at-model-roll-damping"));

                    m_steps.add_step(new SviewStepRotate(
                            -globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg"),
                            -globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg"),
                            globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg"),
                            damping_heading,
                            damping_pitch,
                            damping_roll
                            ));

                    m_steps.add_step(new SviewStepMouseDrag(
                            1 /*mouse_heading_scale*/,
                            -1 /*mouse_pitch_scale*/
                            ));

                    /* Set current position as target. This isn't actually
                    necessary for this view because the direction implied by
                    heading/pitch/roll will still point to the centre of the
                    aircraft after the following steps. But it allows double
                    views to work better - they will use the centre of the
                    aircraft instead of the eye position. */
                    m_steps.add_step(new SviewStepCopyToTarget);

                    /* Add step that moves eye away from aircraft.
                    config/z-offset-m defaults to /sim/chase-distance-m (see
                    fgdata:defaults.xml) which is -ve, e.g. -25m. */
                    m_steps.add_step(new SviewStepMove(
                            config->getDoubleValue("view/config/z-offset-m"),
                            -config->getDoubleValue("view/config/y-offset-m"),
                            config->getDoubleValue("view/config/x-offset-m")
                            ));

                    /* Finally add a step that converts
                    lat,lon,height,heading,pitch,roll into SGVec3d position and
                    SGQuatd orientation. */
                    m_steps.add_step(new SviewStepFinal);
                }
                else {
                    /* E.g. pilot view.

                    Add steps to move to the pilot's eye position.

                    config/z-offset-m seems to be +ve when moving backwards
                    relative to the aircraft, so we need to negate the value we
                    pass to SviewStepMove(). */
                    m_steps.m_name = std::string() + "legacy pilot" + callsign_desc;
                    m_steps.add_step(new SviewStepAircraft(callsign));
                    m_steps.add_step(new SviewStepMove(
                            -config->getDoubleValue("view/config/z-offset-m"),
                            -config->getDoubleValue("view/config/y-offset-m"),
                            -config->getDoubleValue("view/config/x-offset-m")
                            ));

                    double current_heading_offset = globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg");
                    double current_pitch_offset   = globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg");
                    double current_roll_offset    = globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg");
                    /*double default_heading_offset = config->getDoubleValue("view/config/heading-offset-deg");
                    double default_pitch_offset   = config->getDoubleValue("view/config/pitch-offset-deg");
                    double default_roll_offset    = config->getDoubleValue("view/config/roll-offset-deg");*/

                    /* Apply final rotation. */
                    m_steps.add_step(new SviewStepRotate(
                            current_heading_offset,
                            current_pitch_offset,
                            current_roll_offset
                            ));

                    m_steps.add_step(new SviewStepMouseDrag(
                            1 /*mouse_heading_scale*/,
                            1 /*mouse_pitch_scale*/
                            ));
                    m_steps.add_step(new SviewStepFinal);
                    SG_LOG(SG_VIEW, SG_INFO, "m_steps=" << m_steps);
                }
            }
        }
        else
        {
            /* New-style Sview specification, where each step is specified for
            us. */
            simgear::PropertyList steps = config->getChildren("step");
            if (steps.empty()) {
                throw std::runtime_error(std::string() + "No steps specified");
            }
            for (SGPropertyNode* step: steps) {
                std::string type = step->getStringValue("type");
                if (0) {}
                else if (type == "aircraft") {
                    std::string callsign = step->getStringValue("callsign");
                    m_steps.add_step(new SviewStepAircraft(callsign));
                    if (callsign != "") {
                        m_steps.m_name += " callsign=" + callsign;
                    }
                }
                else if (type == "move") {
                    m_steps.add_step(new SviewStepMove(
                            step->getDoubleValue("forward"),
                            step->getDoubleValue("up"),
                            step->getDoubleValue("right")
                            ));
                }
                else if (type == "direction-multiply") {
                    m_steps.add_step(new SviewStepDirectionMultiply(
                            step->getDoubleValue("heading"),
                            step->getDoubleValue("pitch"),
                            step->getDoubleValue("roll")
                            ));
               }
               else if (type == "copy-to-target") {
                    m_steps.add_step(new SviewStepCopyToTarget);
                }
                else if (type == "nearest-tower") {
                    std::string callsign = step->getStringValue("callsign");
                    m_steps.add_step(new SviewStepNearestTower(callsign));
                    m_steps.m_name += " tower";
                    if (callsign != "") m_steps.m_name += " callsign=" + callsign;
                }
                else if (type == "rotate") {
                    m_steps.add_step(new SviewStepRotate(
                            step->getDoubleValue("heading"),
                            step->getDoubleValue("pitch"),
                            step->getDoubleValue("roll"),
                            step->getDoubleValue("damping-heading"),
                            step->getDoubleValue("damping-pitch"),
                            step->getDoubleValue("damping-roll")
                            ));
                }
                else if (type == "rotate-current-view") {
                    m_steps.add_step(new SviewStepRotate(
                            globals->get_props()->getDoubleValue("heading"),
                            globals->get_props()->getDoubleValue("pitch"),
                            globals->get_props()->getDoubleValue("roll")
                            ));
                }
                else if (type == "mouse-drag")
                {
                    m_steps.add_step(new SviewStepMouseDrag(
                            step->getDoubleValue("heading-scale", 1),
                            step->getDoubleValue("pitch-scale", 1)
                            ));
                }
                else if (type == "double") {
                    m_steps.add_step(new SviewStepDouble(step));
                    m_steps.m_name += " double";
                }
                else if (type == "agl") {
                    std::string callsign = step->getStringValue("callsign");
                    double damping_time = step->getDoubleValue("damping-time");
                    m_steps.add_step(new SviewStepAGL(callsign, damping_time));
                }
                else {
                    throw std::runtime_error(std::string() + "Unrecognised step name: '" + type + "'");
                }
            }
            m_steps.add_step(new SviewStepFinal);
        }
        SG_LOG(SG_VIEW, SG_DEBUG, "m_steps=" << m_steps);
    }

    /* Construct a double view using two step sequences. */
    SviewViewEyeTarget(
            osgViewer::View* view,
            SGPropertyNode* config,
            SviewSteps& a,
            SviewSteps& b
            )
    :
    SviewView(view)
    {
        std::string type = config->getStringValue("type");
        if (type.empty()) {
            throw std::runtime_error("double-type not specified");
        }
        if (type == "last_pair" || type == "last_pair_double")
        {
            /* Copy steps from <b> that will set .target. */
            for (auto step: b.m_steps)
            {
                if (0
                        || dynamic_cast<SviewStepCopyToTarget*>(step.get())
                        || dynamic_cast<SviewStepFinal*>(step.get())
                        )
                {
                    break;
                }
                m_steps.add_step(step);
            }
            m_steps.add_step(new SviewStepCopyToTarget);

            /* Copy steps from <a> that will set .position. */
            for (auto step: a.m_steps)
            {
                if (0
                        || dynamic_cast<SviewStepCopyToTarget*>(step.get())
                        || dynamic_cast<SviewStepFinal*>(step.get())
                        )
                {
                    break;
                }
                m_steps.add_step(step);
            }

            if (type == "last_pair_double")
            {
                /* We need a final SviewStepDouble step. */
                m_steps.add_step(new SviewStepDouble);
                m_steps.m_name = std::string() + " double: " + a.m_name + " - " + b.m_name;
            }
            else
            {
                m_steps.m_name = std::string() + " pair: " + a.m_name + " - " + b.m_name;
            }
            m_steps.add_step(new SviewStepFinal);
            SG_LOG(SG_VIEW, SG_INFO, "    m_steps:" << m_steps);
        }
        else {
            throw std::runtime_error(std::string("Unrecognised double view: ") + type);
        }

        /* Disable our mouse_drag() method - doesn't make sense for double views. */
        m_mouse_drag = false;
        SG_LOG(SG_VIEW, SG_DEBUG, "m_steps=" << m_steps);
    }

    const std::string description() override
    {
        return m_steps.m_name;
    }

    bool update(double dt) override
    {
        bool valid = m_osg_view->getCamera()->getGraphicsContext()->valid();
        SG_LOG(SG_VIEW, SG_BULK, "valid=" << valid);
        if (!valid) return false;

        SviewPosDir posdir;
        bool debug = false;
        if (m_debug) {
            time_t t = time(NULL) / 10;
            if (t != m_debug_time) {
                m_debug_time = t;
                debug = true;
            }
        }
        if (debug) SG_LOG(SG_VIEW, SG_INFO, "evaluating m_steps:");
        m_steps.evaluate(posdir, dt, debug);

        posdir_to_view(posdir);
        return true;
    }

    void mouse_drag(double delta_x_deg, double delta_y_deg) override
    {
        if (!m_mouse_drag)  return;
        for (auto step: m_steps.m_steps) {
            step->mouse_drag(delta_x_deg, delta_y_deg);
        }
    }

    SviewSteps          m_steps;
    bool                m_mouse_drag = true;
    bool                m_debug = false;
    time_t              m_debug_time = 0;
};


/* All our view windows. */
static std::vector<std::shared_ptr<SviewView>>  s_views;

/* Recent views, for use by SviewAddLastPair(). */
static std::deque<std::shared_ptr<SviewViewEyeTarget>>    s_recent_views;


/* Returns an independent property tree that defines an Sview that is a copy of
the current view. */
static SGPropertyNode_ptr SviewConfigForCurrentView()
{
    SGPropertyNode_ptr  config = new SGPropertyNode;
    int view_number_raw = globals->get_props()->getIntValue("/sim/current-view/view-number-raw");
    config->setIntValue("view-number-raw", view_number_raw);
    SGPropertyNode* global_view = globals->get_props()->getNode("/sim/view", view_number_raw /*index*/, true /*create*/);
    std::string root_path = global_view->getStringValue("config/root");   /* "" or /ai/models/multiplayer[]. */
    SGPropertyNode* root = globals->get_props()->getNode(root_path);
    std::string callsign = root->getStringValue("callsign");

    config->setStringValue("type", "legacy");
    config->setStringValue("callsign", callsign);
    SGPropertyNode* config_view = config->getNode("view", true /*create*/);
    if (callsign == "") {
        /* User aircraft. */
        copyProperties(globals->get_props()->getNode("/sim/view", view_number_raw), config_view);
    }
    else {
        /* Multiplayer aircraft. */
        copyProperties(root->getNode("set/sim/view", view_number_raw), config_view);
    }

    config->setDoubleValue(
            "direction-delta/heading",
            globals->get_props()->getDoubleValue("sim/current-view/heading-offset-deg")
            );
    config->setDoubleValue(
            "direction-delta/pitch",
            globals->get_props()->getDoubleValue("sim/current-view/pitch-offset-deg")
            );
    config->setDoubleValue(
            "direction-delta/roll",
            globals->get_props()->getDoubleValue("sim/current-view/roll-offset-deg")
            );

    config->setDoubleValue("zoom-delta", 1);

    SG_LOG(SG_VIEW, SG_INFO, "returning:\n" << writePropertiesInline(config, true /*write_all*/));
    return config;
}


void SviewPush()
{
    if (s_recent_views.size() >= 2) {
        s_recent_views.pop_front();
    }
    /* Make a dummy view whose eye and target members will copy the current
    view. */
    SGPropertyNode_ptr config = SviewConfigForCurrentView();
    std::shared_ptr<SviewViewEyeTarget> v(new SviewViewEyeTarget(nullptr /*view*/, config));
    s_recent_views.push_back(v);
    SG_LOG(SG_VIEW, SG_DEBUG, "Have pushed view: " << v);
}


void SviewUpdate(double dt)
{
    bool verbose = 0;
    for (size_t i=0; i<s_views.size(); /* incremented in loop*/) {
        if (verbose) {
            SG_LOG(SG_VIEW, SG_INFO, "updating i=" << i
                    << ": " << s_views[i]->m_osg_view
                    << ' ' << s_views[i]->m_compositor
                    );
        }
        bool valid = s_views[i]->update(dt);
        if (valid) {
            const osg::Matrix& view_matrix = s_views[i]->m_osg_view->getCamera()->getViewMatrix();
            const osg::Matrix& projection_matrix = s_views[i]->m_osg_view->getCamera()->getProjectionMatrix();
            s_views[i]->m_compositor->update(view_matrix, projection_matrix);
            i += 1;
        }
        else {
            /* Window has been closed.

            This doesn't work reliably with OpenSceneGraph-3.4. We seem to
            sometimes get valid=false when a different window from s_views[i]
            is closed. And sometimes OSG seems to end up sending a close-window
            even to the main window when the user has closed an sview window.
            */
            auto view_it = s_views.begin() + i;
            SG_LOG(SG_VIEW, SG_INFO, "deleting SviewView i=" << i << ": " << (*view_it)->description2());
            for (size_t j=0; j<s_views.size(); ++j) {
                SG_LOG(SG_VIEW, SG_INFO, "    " << j
                        << ": " << s_views[j]->m_osg_view
                        << ' ' << s_views[j]->m_compositor
                        );
            }
            s_views.erase(view_it);

            for (size_t j=0; j<s_views.size(); ++j) {
                SG_LOG(SG_VIEW, SG_INFO, "    " << j
                        << ": " << s_views[j]->m_osg_view
                        << ' ' << s_views[j]->m_compositor
                        );
            }
            verbose = true;
        }
    }
}

void SviewClear()
{
    s_views.clear();
}


/* These are set by SViewSetCompositorParams(). */
static osg::ref_ptr<simgear::SGReaderWriterOptions> s_compositor_options;
static std::string s_compositor_path;


struct EventHandler : osgGA::EventHandler
{
    bool handle(osgGA::Event* event, osg::Object* object, osg::NodeVisitor* nv) override
    {
        osgGA::GUIEventAdapter* ea = event->asGUIEventAdapter();
        if (ea) {
            osgGA::GUIEventAdapter::EventType et = ea->getEventType();
            if (et != osgGA::GUIEventAdapter::FRAME) {
                SG_LOG(SG_GENERAL, SG_BULK, "sview event handler called. ea->getEventType()=" << ea->getEventType());
            }
        }
        else {
            SG_LOG(SG_GENERAL, SG_BULK, "sview event handler called...");
        }
        return true;
    }
};

#include "Viewer/FGEventHandler.hxx"

std::shared_ptr<SviewView> SviewCreate(SGPropertyNode* config)
{
    assert(config);

    FGRenderer* renderer = globals->get_renderer();
    osgViewer::ViewerBase* viewer_base = renderer->getViewerBase();
    osgViewer::CompositeViewer* composite_viewer = dynamic_cast<osgViewer::CompositeViewer*>(viewer_base);
    if (!composite_viewer) {
        return nullptr;
    }

    osgViewer::View* main_view = renderer->getView();
    osg::Node* scene_data = main_view->getSceneData();

    SG_LOG(SG_GENERAL, SG_DEBUG, "main_view->getNumSlaves()=" << main_view->getNumSlaves());

    osgViewer::View* view = new osgViewer::View();
    flightgear::FGEventHandler* event_handler = globals->get_renderer()->getEventHandler();
    view->addEventHandler(event_handler);

    std::shared_ptr<SviewView>  sview_view;

    std::string type = config->getStringValue("type");
    SG_LOG(SG_VIEW, SG_DEBUG, "type=" << type);
    if (0) {
    }
    else if (type == "current") {
        SGPropertyNode_ptr config2 = SviewConfigForCurrentView();
        copyProperties(config, config2);
        config2->setStringValue("type", "legacy"); /* restore it after copy() sets to "current". */
        sview_view.reset(new SviewViewEyeTarget(view, config2));
    }
    else if (type == "last_pair") {
        if (s_recent_views.size() < 2) {
            SG_LOG(SG_VIEW, SG_ALERT, "Need two pushed views");
            return nullptr;
        }
        auto it = s_recent_views.end();
        std::shared_ptr<SviewViewEyeTarget> target = *(--it);
        std::shared_ptr<SviewViewEyeTarget> eye    = *(--it);
        sview_view.reset(new SviewViewEyeTarget(view, config, eye->m_steps, target->m_steps));
    }
    else if (type == "last_pair_double") {
        if (s_recent_views.size() < 2) {
            SG_LOG(SG_VIEW, SG_ALERT, "Need two pushed views");
            return nullptr;
        }
        auto it = s_recent_views.end();
        std::shared_ptr<SviewViewEyeTarget> remote = *(--it);
        std::shared_ptr<SviewViewEyeTarget> local    = *(--it);
        sview_view.reset(new SviewViewEyeTarget(view, config, local->m_steps, remote->m_steps));
    }
    else {
        SG_LOG(SG_VIEW, SG_ALERT, "config is:\n" << writePropertiesInline(config, true /*write_all*/));
        sview_view.reset(new SviewViewEyeTarget(view, config));
    }

    osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
    osg::ref_ptr<osg::GraphicsContext> gc;

    /* When we implement canvas views, we won't create a new window here. */
    if (1) {
        /* Create a new window. */

        // osg::GraphicsContext::WindowingSystemInterface* wsi = osg::GraphicsContext::getWindowingSystemInterface();
        // assert(wsi);
        flightgear::WindowSystemAdapter* wsa = flightgear::WindowSystemAdapter::getWSA();
        flightgear::GraphicsWindow* main_window = wsa->getGUIWindow();
        assert(main_window); // The GUI window must exist
        osg::ref_ptr<osg::GraphicsContext> main_gc = main_window->gc;
        const osg::GraphicsContext::Traits* main_traits = main_gc->getTraits();

        /* Arbitrary initial position of new window. */
        traits->x = config->getIntValue("window-x", 100);
        traits->y = config->getIntValue("window-y", 100);

        /* We set new window size as fraction of main window. This keeps the
        aspect ratio of new window same as that of the main window which allows
        us to use main window's projection matrix directly. Presumably we could
        calculate a suitable projection matrix to match an arbitrary aspect
        ratio, but i don't know the maths well enough to do this. */
        traits->width  = config->getIntValue("window-width", main_traits->width / 2);
        traits->height = config->getIntValue("window-height", main_traits->height / 2);
        traits->windowDecoration = true;
        traits->doubleBuffer = true;
        traits->sharedContext = 0;

        traits->readDISPLAY();
        if (traits->displayNum < 0) traits->displayNum = 0;
        if (traits->screenNum < 0)  traits->screenNum = 0;

        int bpp = fgGetInt("/sim/rendering/bits-per-pixel");
        int cbits = (bpp <= 16) ?  5 :  8;
        int zbits = (bpp <= 16) ? 16 : 24;
        traits->red = traits->green = traits->blue = cbits;
        traits->depth = zbits;

        traits->mipMapGeneration = true;
        traits->windowName = "Flightgear " + sview_view->description2();
        traits->sampleBuffers = fgGetInt("/sim/rendering/multi-sample-buffers", traits->sampleBuffers);
        traits->samples = fgGetInt("/sim/rendering/multi-samples", traits->samples);
        traits->vsync = fgGetBool("/sim/rendering/vsync-enable", traits->vsync);
        traits->stencil = 8;

        gc = osg::GraphicsContext::createGraphicsContext(traits);
        assert(gc.valid());
    }

    /* need to ensure that the window is cleared make sure that the complete
    window is set the correct colour rather than just the parts of the window
    that are under the camera's viewports. */
    //gc->setClearColor(osg::Vec4f(0.2f,0.2f,0.6f,1.0f));
    //gc->setClearMask(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    view->setSceneData(scene_data);
    view->setDatabasePager(FGScenery::getPagerSingleton());

    /* https://www.mail-archive.com/osg-users@lists.openscenegraph.org/msg29820.html
    Passing (false, false) here seems to cause a hang on startup. */
    view->getDatabasePager()->setUnrefImageDataAfterApplyPolicy(true, false);
    osg::GraphicsContext::createNewContextID();

    osg::Camera* main_camera = main_view->getCamera();
    osg::Camera* camera = view->getCamera();
    camera->setGraphicsContext(gc.get());

    if (0) {
        /* Show the projection matrix. */
        double left;
        double right;
        double bottom;
        double top;
        double zNear;
        double zFar;
        auto projection_matrix = main_camera->getProjectionMatrix();
        bool ok = projection_matrix.getFrustum(left, right, bottom, top, zNear, zFar);
        SG_LOG(SG_GENERAL, SG_ALERT, "projection_matrix:"
                << " ok=" << ok
                << " left=" << left
                << " right=" << right
                << " bottom=" << bottom
                << " top=" << top
                << " zNear=" << zNear
                << " zFar=" << zFar
                );
    }

    camera->setProjectionMatrix(main_camera->getProjectionMatrix());
    camera->setViewMatrix(main_camera->getViewMatrix());
    camera->setCullMask(0xffffffff);
    camera->setCullMaskLeft(0xffffffff);
    camera->setCullMaskRight(0xffffffff);

    /* This appears to avoid unhelpful culling of nearby objects. Though the
    above SG_LOG() says zNear=0.1 zFar=120000, so not sure what's going on. */
    camera->setComputeNearFarMode(osgUtil::CullVisitor::DO_NOT_COMPUTE_NEAR_FAR);

    /*
    from CameraGroup::buildGUICamera():
    camera->setInheritanceMask(osg::CullSettings::ALL_VARIABLES
                               & ~(osg::CullSettings::COMPUTE_NEAR_FAR_MODE
                                   | osg::CullSettings::CULLING_MODE
                                   | osg::CullSettings::CLEAR_MASK
                                   ));
    camera->setCullingMode(osg::CullSettings::NO_CULLING);


    main_viewport seems to be null so this doesn't work.
    osg::Viewport* main_viewport = main_view->getCamera()->getViewport();
    SG_LOG(SG_GENERAL, SG_ALERT, "main_viewport=" << main_viewport);

    osg::Viewport* viewport = new osg::Viewport(*main_viewport);

    view->getCamera()->setViewport(viewport);
    */
    view->getCamera()->setViewport(0, 0, traits->width, traits->height);

    view->setName("Cloned view");

    view->setFrameStamp(composite_viewer->getFrameStamp());

    simgear::compositor::Compositor* compositor = simgear::compositor::Compositor::create(
            view,
            gc,
            view->getCamera()->getViewport(),
            s_compositor_path,
            s_compositor_options
            );

    sview_view->m_compositor = compositor;
    s_views.push_back(sview_view);

    /* stop/start threading:
    https://www.mail-archive.com/osg-users@lists.openscenegraph.org/msg54341.html
    */
    composite_viewer->stopThreading();
    composite_viewer->addView(view);
    composite_viewer->startThreading();

    SG_LOG(SG_GENERAL, SG_DEBUG, "main_view->getNumSlaves()=" << main_view->getNumSlaves());
    SG_LOG(SG_GENERAL, SG_DEBUG, "view->getNumSlaves()=" << view->getNumSlaves());

    SG_LOG(SG_VIEW, SG_DEBUG, "have added extra view. views are now:");
    for (unsigned i=0; i<composite_viewer->getNumViews(); ++i) {
        osgViewer::View* view = composite_viewer->getView(i);
        SG_LOG(SG_VIEW, SG_DEBUG, "composite_viewer view i=" << i << " view=" << view);
    }

    return sview_view;
}

std::shared_ptr<SviewView> SviewCreate(const SGPropertyNode* config)
{
    return SviewCreate(const_cast<SGPropertyNode*>(config));
}

simgear::compositor::Compositor* SviewGetEventViewport(const osgGA::GUIEventAdapter& ea)
{
    const osg::GraphicsContext* gc = ea.getGraphicsContext();
    for (std::shared_ptr<SviewView> sview_view: s_views) {
        if (sview_view->m_compositor->getGraphicsContext() == gc) {
            return sview_view->m_compositor;
        }
    }
    return nullptr;
}

void SViewSetCompositorParams(
        osg::ref_ptr<simgear::SGReaderWriterOptions> options,
        const std::string& compositor_path
        )
{
    s_compositor_options = options;
    s_compositor_path = compositor_path;
}

bool SviewMouseMotion(int x, int y, const osgGA::GUIEventAdapter& ea)
{
    const osg::GraphicsContext* gc = ea.getGraphicsContext();
    std::shared_ptr<SviewView>  sview_view;
    for (auto v: s_views) {
        if (v->m_compositor->getGraphicsContext() == gc) {
            sview_view = v;
            break;
        }
    }
    if (!sview_view) {
        return false;
    }
    double xx;
    double yy;
    if (!flightgear::eventToWindowCoords(&ea, xx, yy)) {
        return false;
    }

    SG_LOG(SG_GENERAL, SG_DEBUG, "sview_view=" << sview_view.get() << " xx=" << xx << " yy=" << yy);
    bool button2 = globals->get_props()->getBoolValue("/devices/status/mice/mouse/button[2]");
    if (button2 && sview_view->m_mouse_button2) {
        /* Button2 drag. */
        double  delta_x = xx - sview_view->m_mouse_x;
        double  delta_y = yy - sview_view->m_mouse_y;
        if (delta_x || delta_y) {
            /* Convert delta (which is mouse movement in pixels) to a change
            in viewing angle in degrees. We do this using the fov and window
            size so that the result is the image moving as though it was being
            panned by the mouse movement. So angle changes are smaller for
            high zoom values or small windows, making high zoom views easy to
            control. */
            osg::Camera* camera = sview_view->m_osg_view->getCamera();
            double fov_y;
            double aspect_ratio;
            double z_near;
            double z_far;
            camera->getProjectionMatrixAsPerspective(fov_y, aspect_ratio, z_near, z_far);
            double fov_x = fov_y * aspect_ratio;

            simgear::compositor::Compositor*    compositor = sview_view->m_compositor;
            osg::Viewport*                      viewport = compositor->getViewport();
            double delta_x_deg = delta_x / viewport->width() * fov_x;
            double delta_y_deg = delta_y / viewport->height() * fov_y;

            /* Scale movement a little to make things more similar to normal
            operation. */
            double scale = 5;
            delta_x_deg *= scale;
            delta_y_deg *= scale;
            sview_view->mouse_drag(delta_x_deg, delta_y_deg);
        }
    }
    sview_view->m_mouse_button2 = button2;
    sview_view->m_mouse_x = xx;
    sview_view->m_mouse_y = yy;

    return true;
}
