添加一个地球显示和管理类
#pragma once
#include <QObject.h>
#include <osg/Node>
#include <osgViewer/Viewer>
#include <osgGA/TrackballManipulator>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
#include <osgViewer/ViewerEventHandlers>
#include <osgEarthUtil/EarthManipulator>
#include <osgEarth/ImageLayer>
#include <osg/BlendFunc>
#include <osgEarthUtil/Sky>
#include <osgEarth/NodeUtils>
#include "callback/IEarthMap.h"
class QTimer;
#define EarthMapMgr() (EarthMapManager::getInstance())
class EarthMapManager :public QObject, public IEarthMap {
Q_OBJECT
private:
EarthMapManager();
static EarthMapManager* _instance;
public:
~EarthMapManager();
static EarthMapManager* getInstance();
//创建地球
void createEarth(int w,int h);
//获取三维显示的Widget
QWidget* get3DMapWidget();
//获取三维显示的Viewer
osg::ref_ptr< osgViewer::Viewer>&getViewer() {
return _viewer;
}
//获取根节点
osg::ref_ptr<osg::Group>&getRoot() {
return _root;
}
//获取地球节点
osg::ref_ptr<osgEarth::MapNode>&getMapNode() {
return _mapNode;
}
//获取特征节点
osg::ref_ptr<osg::Group>&getFeatureNode() {
return _featureRoot;
}
//获取天空节点
osg::ref_ptr<osgEarth::Util::SkyNode>&getSkyNode() {
return _skyNode;
}
//添加模型
osg::ref_ptr<osg::MatrixTransform> add3DModel(std::string nodePath, double lon, double lat, double alt);
//设置模型的位置
void setModelPos(const osg::ref_ptr<osg::MatrixTransform>& node, double latitude, double longitude, double height);
//平滑的移动到地球的某个位置,作为中心点
void gotoViewpoint(double height, double latitude, double longitude);
//设置地球位置的中心店
void setViewpoint(double height, double latitude, double longitude);
private slots:
void slotTimeOut();
private:
void get3DEarthCenter()override;
void initMouseIntersection();
private:
QTimer* m_timer = nullptr;
osg::ref_ptr< osgViewer::Viewer> _viewer;
osg::ref_ptr<osg::Group> _root;
osg::ref_ptr<osgEarth::MapNode> _mapNode;
osg::ref_ptr<osg::Group> _featureRoot;
osg::ref_ptr<osgEarth::Util::SkyNode> _skyNode;
};
#include "EarthMapManager.h"
#include <QTimer>
#include <osgQt/GraphicsWindowQt>
#include <osg/MatrixTransform>
#include "EarthManipulator.h"
#include "CommonUse/GlobalUse.h"
#include "InterfaceImpl/IMap2DUpdate.h"
#include "Entity3D/Entity3DManager.h"
#include "MouseIntersection.h"
EarthMapManager* EarthMapManager::_instance = new EarthMapManager;
EarthMapManager::EarthMapManager() {
}
EarthMapManager::~EarthMapManager() {
}
EarthMapManager* EarthMapManager::getInstance() {
return _instance;
}
void EarthMapManager::createEarth(int w, int h) {
osg::DisplaySettings* ds = osg::DisplaySettings::instance().get();
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
traits->windowDecoration = false;
traits->x = 0;
traits->y = 0;
traits->width = w;
traits->height = h;
traits->doubleBuffer = true;
traits->samples = 16;
osg::ref_ptr<osg::Camera> camera = new osg::Camera;
camera->setGraphicsContext(new osgQt::GraphicsWindowQt(traits.get()));
camera->setClearColor(osg::Vec4(0.2, 0.2, 0.6, 1.0));
camera->setViewport(new osg::Viewport(0, 0, traits->width, traits->height));
camera->setProjectionMatrixAsPerspective(30.0f,
static_cast<double>(traits->width) / static_cast<double>(traits->height),
1.0f,
10000.0f);
_viewer = new osgViewer::Viewer();
_viewer->setCamera(camera);
_viewer->getCamera()->setGraphicsContext(camera->getGraphicsContext());
osg::ref_ptr<osgViewer::StatsHandler> statsHand = new osgViewer::StatsHandler;
_viewer->addEventHandler(statsHand);
EarthManipulator* earthManipulator = new EarthManipulator();
earthManipulator->setEarthCallback(this);
_viewer->setCameraManipulator(earthManipulator);
_viewer->setThreadingModel(osgViewer::ViewerBase::ThreadingModel::SingleThreaded);
_root = new osg::Group;
_viewer->setSceneData(_root);
std::string earthPath = (GlobalUseInst()->loadExeDir() + QString("Map/3D/ericSimple1.earth")).toStdString();
osg::ref_ptr<osg::Node> earthFile = osgDB::readNodeFile(earthPath);
_root->addChild(earthFile);
_mapNode = osgEarth::MapNode::findMapNode(earthFile);
_featureRoot = new osg::Group;
_mapNode->addChild(_featureRoot);
_skyNode = osgEarth::Util::SkyNode::create(_mapNode);
_skyNode->attach(_viewer, 0);
if (_mapNode->getNumParents() > 0) {
osgEarth::insertGroup(_skyNode, _mapNode->getParent(0));
}
//添加经纬度显示界面
initMouseIntersection();
m_timer = new QTimer;
QObject::connect(m_timer, SIGNAL(timeout()), this, SLOT(slotTimeOut()));
m_timer->start();
}
QWidget* EarthMapManager::get3DMapWidget() {
osgQt::GraphicsWindowQt* gw1 = dynamic_cast<osgQt::GraphicsWindowQt*>(_viewer->getCamera()->getGraphicsContext());
return gw1->getGLWidget();
}
osg::ref_ptr<osg::MatrixTransform> EarthMapManager::add3DModel(std::string nodePath, double lon, double lat, double alt) {
//添加模型
osg::ref_ptr<osg::Node> nodeFile = osgDB::readNodeFile(nodePath);
osg::ref_ptr<osg::MatrixTransform> nodeMt = new osg::MatrixTransform;
nodeMt->addChild(nodeFile);
_root->addChild(nodeMt);
osg::Matrix m;
_mapNode->getMapSRS()->getEllipsoid()->computeLocalToWorldTransformFromLatLongHeight(
osg::DegreesToRadians(lat),
osg::DegreesToRadians(lon),
alt,
m);
//m.scale(10.0, 10.0, 10.0);
nodeMt->setMatrix(m);
nodeMt->addDescription("model-3d");
nodeMt->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::ON);
nodeMt->getOrCreateStateSet()->setMode(GL_RESCALE_NORMAL, osg::StateAttribute::ON);
return nodeMt;
}
void EarthMapManager::setModelPos(const osg::ref_ptr<osg::MatrixTransform>& node, double latitude, double longitude, double height) {
osg::Matrix m;
_mapNode->getMapSRS()->getEllipsoid()
->computeLocalToWorldTransformFromLatLongHeight(osg::DegreesToRadians(latitude),
osg::DegreesToRadians(longitude),
height,
m);
node->setMatrix(m);
}
void EarthMapManager::gotoViewpoint(double height, double latitude, double longitude) {
EarthManipulator* em = dynamic_cast<EarthManipulator*>(_viewer->getCameraManipulator());
if (em == NULL) {
return;
}
osgEarth::GeoPoint geoP(_mapNode->getMapSRS(), longitude, latitude, height, osgEarth::AltitudeMode::ALTMODE_ABSOLUTE);
osgEarth::Viewpoint vp;
vp.focalPoint() = geoP;
vp.setRange(2.4078e+06);
em->setViewpoint(vp, 3.0);
}
void EarthMapManager::setViewpoint(double height, double latitude, double longitude) {
EarthManipulator* em = dynamic_cast<EarthManipulator*>(_viewer->getCameraManipulator());
if (em == NULL) {
std::cout << "error in EarthFun::gotoViewpoint: The EarthManipulator ptr is NULL";
return;
}
osgEarth::GeoPoint geoP(_mapNode->getMapSRS(), longitude, latitude, height, osgEarth::AltitudeMode::ALTMODE_ABSOLUTE);
osgEarth::Viewpoint vp;
vp.focalPoint() = geoP;
vp.setRange(2.4078e+06);
em->setViewpoint(vp, 0.0);
}
void EarthMapManager::slotTimeOut() {
_viewer->frame();
}
void EarthMapManager::get3DEarthCenter() {
EarthManipulator* em = dynamic_cast<EarthManipulator*>(_viewer->getCameraManipulator());
if (em == NULL) {
return;
}
osgEarth::Viewpoint vp = em->getViewpoint();
GlobalUseInst()->load2DMapUpdate()->setMapCenter(vp.focalPoint()->x(), vp.focalPoint()->y());
}
void EarthMapManager::initMouseIntersection() {
osg::ref_ptr<MouseIntersection> mouseInter;
mouseInter = new MouseIntersection;
_viewer->addEventHandler(mouseInter);
_root->addChild(mouseInter->getLabel());
}
earth file
<map name="gdal_interp" type="geocentric" version="2">
<image name="world-top-tif" driver="gdal">
<url>world-cctv-all-geo.tif</url>
</image>
<elevation name="readymap_elevation" driver="gdal">
<url>bohai-elevation.tif</url>
</elevation>
<feature_model name="world_boundaries">
<features name="world" driver="ogr">
<url>./shp/world.shp</url>
<build_spatial_index>true</build_spatial_index>
</features>
<styles>
<style type="text/css">
world {
stroke: #ffff00;
stroke-width: 2px;
altitude-clamping: terrain-drape;
}
</style>
</styles>
</feature_model>
</map>
EarthManipulator.h
#ifndef EARTHMANIPULATOR_H
#define EARTHMANIPULATOR_H
#include <osgEarthUtil/Common>
#include <osgEarth/Common>
#include <osgEarth/Viewpoint>
#include <osgEarth/GeoData>
#include <osgEarth/Revisioning>
#include <osgEarth/Terrain>
#include <osgEarth/MapNode>
#include <osg/Timer>
#include <osg/ArgumentParser>
#include <osgGA/CameraManipulator>
#include <map>
#include <list>
#include <utility>
using namespace osgEarth;
class IEarthMap;
/**
* A programmable manipulator suitable for use with geospatial terrains.
*
* You can use the "Settings" class to define custom input device bindings
* and navigation parameters. Create one or more of these and call
* applySettings() to "program" the manipulator at runtime.
*/
class EarthManipulator : public osgGA::CameraManipulator {
public:
/** Bindable manipulator actions. */
enum ActionType {
ACTION_NULL,
ACTION_HOME,
ACTION_GOTO,
ACTION_PAN,
ACTION_PAN_LEFT,
ACTION_PAN_RIGHT,
ACTION_PAN_UP,
ACTION_PAN_DOWN,
ACTION_ROTATE,
ACTION_ROTATE_LEFT,
ACTION_ROTATE_RIGHT,
ACTION_ROTATE_UP,
ACTION_ROTATE_DOWN,
ACTION_ZOOM,
ACTION_ZOOM_IN,
ACTION_ZOOM_OUT,
ACTION_EARTH_DRAG
};
/** Vector of action types */
typedef std::vector<ActionType> ActionTypeVector;
/** Bindable event types. */
enum EventType {
EVENT_MOUSE_DOUBLE_CLICK = osgGA::GUIEventAdapter::DOUBLECLICK,
EVENT_MOUSE_DRAG = osgGA::GUIEventAdapter::DRAG,
EVENT_KEY_DOWN = osgGA::GUIEventAdapter::KEYDOWN,
EVENT_SCROLL = osgGA::GUIEventAdapter::SCROLL,
EVENT_MOUSE_CLICK = osgGA::GUIEventAdapter::USER << 1,
EVENT_MULTI_DRAG = osgGA::GUIEventAdapter::USER << 2, // drag with 2 fingers
EVENT_MULTI_PINCH = osgGA::GUIEventAdapter::USER << 3, // pinch with 2 fingers
EVENT_MULTI_TWIST = osgGA::GUIEventAdapter::USER << 4 // drag 2 fingers in different directions
};
/** Bindable mouse buttons. */
enum MouseEvent {
MOUSE_LEFT_BUTTON = osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON,
MOUSE_MIDDLE_BUTTON = osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON,
MOUSE_RIGHT_BUTTON = osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON
};
/** Action options. Certain options are only meaningful to certain Actions.
See the bind* documentation for information. */
enum ActionOptionType {
OPTION_SCALE_X, // Sensitivity multiplier for horizontal input movements
OPTION_SCALE_Y, // Sensitivity multiplier for vertical input movements
OPTION_CONTINUOUS, // Whether to act as long as the button or key is depressed
OPTION_SINGLE_AXIS, // If true, only operate on one axis at a time (the one with the larger value)
OPTION_GOTO_RANGE_FACTOR, // for ACTION_GOTO, multiply the Range by this factor (to zoom in/out)
OPTION_DURATION // Time it takes to complete the action (in seconds)
};
/** Tethering options **/
enum TetherMode {
TETHER_CENTER, // The camera will follow the center of the node.
TETHER_CENTER_AND_ROTATION, // The camera will follow the node and all rotations made by the node
TETHER_CENTER_AND_HEADING // The camera will follow the node and only follow heading rotation
};
/** Camera projection matrix type **/
enum CameraProjection {
PROJ_PERSPECTIVE,
PROJ_ORTHOGRAPHIC
};
struct ActionOption {
ActionOption() {}
ActionOption(int option, bool value) : _option(option), _bool_value(value) {}
ActionOption(int option, int value) : _option(option), _int_value(value) {}
ActionOption(int option, double value) : _option(option), _dbl_value(value) {}
int option() const { return _option; }
bool boolValue() const { return _bool_value; }
int intValue() const { return _int_value; }
double doubleValue() const { return _dbl_value; }
private:
int _option;
union {
bool _bool_value;
int _int_value;
double _dbl_value;
};
};
struct ActionOptions : public std::vector<ActionOption> {
void add(int option, bool value) { push_back(ActionOption(option, value)); }
void add(int option, int value) { push_back(ActionOption(option, value)); }
void add(int option, double value) { push_back(ActionOption(option, value)); }
};
protected:
struct InputSpec {
InputSpec(int event_type, int input_mask, int modkey_mask)
: _event_type(event_type), _input_mask(input_mask), _modkey_mask(modkey_mask) {
}
InputSpec(const InputSpec& rhs)
: _event_type(rhs._event_type), _input_mask(rhs._input_mask), _modkey_mask(rhs._modkey_mask) {
}
bool operator == (const InputSpec& rhs) const {
return _event_type == rhs._event_type &&
_input_mask == rhs._input_mask &&
((_modkey_mask | osgGA::GUIEventAdapter::MODKEY_NUM_LOCK) == (rhs._modkey_mask | osgGA::GUIEventAdapter::MODKEY_NUM_LOCK));
}
inline bool operator < (const InputSpec& rhs) const {
if (_event_type < rhs._event_type) return true;
else if (_event_type > rhs._event_type) return false;
else if (_input_mask < rhs._input_mask) return true;
else if (_input_mask > rhs._input_mask) return false;
else return (_modkey_mask < rhs._modkey_mask);
}
int _event_type;
int _input_mask;
int _modkey_mask;
};
typedef std::list<InputSpec> InputSpecs;
enum Direction {
DIR_NA,
DIR_LEFT,
DIR_RIGHT,
DIR_UP,
DIR_DOWN
};
struct Action {
Action(ActionType type = ACTION_NULL);
Action(ActionType type, const ActionOptions& options);
Action(const Action& rhs);
ActionType _type;
Direction _dir;
ActionOptions _options;
bool getBoolOption(int option, bool defaultValue) const;
int getIntOption(int option, int defaultValue) const;
double getDoubleOption(int option, double defaultValue) const;
private:
void init();
};
void dumpActionInfo(const Action& action, osg::NotifySeverity level) const;
static Action NullAction;
public:
class Settings : public osg::Referenced, public Revisioned {
public:
// construct with default settings
Settings();
// copy ctor
Settings(const Settings& rhs);
/** dtor */
virtual ~Settings() {}
// look for settings in an AP
void apply(osg::ArgumentParser& args);
/**
* Assigns behavior to the action of dragging the mouse while depressing one or
* more mouse buttons and modifier keys.
*
* @param action
* The EarthManipulator::ActionType value to which to bind this mouse
* input specification.
*
* @param button_mask
* Mask of osgGA::GUIEventAdapter::MouseButtonMask values
*
* @param modkey_mask (default = 0L)
* A mask of osgGA::GUIEventAdapter::ModKeyMask values defining a modifier key
* combination to associate with the action.
*
* @param options
* Action options. Valid options are:
* OPTION_CONTINUOUS, OPTION_SCALE_X, OPTION_SCALE_Y
*/
void bindMouse(
ActionType action, int button_mask,
int modkey_mask = 0L,
const ActionOptions& options = ActionOptions());
/**
* Assigns a bevahior to the action of clicking one or more mouse buttons.
*
* @param action
* The EarthManipulator::ActionType value to which to bind this mouse click
* input specification.
*
* @param button_mask
* Mask of osgGA::GUIEventAdapter::MouseButtonMask values
*
* @param modkey_mask (default = 0L)
* A mask of osgGA::GUIEventAdapter::ModKeyMask values defining a modifier key
* combination to associate with the action.
*
* @param options
* Action options. Valid options are:
* OPTION_GOTO_RANGE_FACTOR, OPTION_DURATION
*/
void bindMouseClick(
ActionType action, int button_mask,
int modkey_mask = 0L,
const ActionOptions& options = ActionOptions());
/**
* Assigns a bevahior to the action of double-clicking one or more mouse buttons.
*
* @param action
* The EarthManipulator::ActionType value to which to bind this double-click
* input specification.
*
* @param button_mask
* Mask of osgGA::GUIEventAdapter::MouseButtonMask values
*
* @param modkey_mask (default = 0L)
* A mask of osgGA::GUIEventAdapter::ModKeyMask values defining a modifier key
* combination to associate with the action.
*
* @param options
* Action options. Valid options are:
* OPTION_GOTO_RANGE_FACTOR, OPTION_DURATION
*/
void bindMouseDoubleClick(
ActionType action, int button_mask,
int modkey_mask = 0L,
const ActionOptions& options = ActionOptions());
/**
* Assigns a bevahior to the action of depressing a key.
*
* @param action
* The EarthManipulator::ActionType value to which to bind this key
* input specification.
*
* @param key
* A osgGA::GUIEventAdapter::KeySymbol value
*
* @param modkey_mask (default = 0L)
* A mask of osgGA::GUIEventAdapter::ModKeyMask values defining a modifier key
* combination to associate with the action.
*
* @param options
* Action options. Valid options are:
* OPTION_CONTINUOUS
*/
void bindKey(
ActionType action, int key,
int modkey_mask = 0L,
const ActionOptions& options = ActionOptions());
/**
* Assigns a bevahior to operation of the mouse's scroll wheel.
*
* @param action
* The EarthManipulator::ActionType value to which to bind this scroll
* input specification.
*
* @param scrolling_motion
* A osgGA::GUIEventAdapter::ScrollingMotion value
*
* @param modkey_mask (default = 0L)
* A mask of osgGA::GUIEventAdapter::ModKeyMask values defining a modifier key
* combination to associate with the action.
*
* @param options
* Action options. Valid options are:
* OPTION_SCALE_Y, OPTION_DURATION
*/
void bindScroll(
ActionType action, int scrolling_motion,
int modkey_mask = 0L,
const ActionOptions& options = ActionOptions());
void bindPinch(
ActionType action, const ActionOptions& = ActionOptions());
void bindTwist(
ActionType action, const ActionOptions& = ActionOptions());
void bindMultiDrag(
ActionType action, const ActionOptions& = ActionOptions());
/**
* Sets an overall mouse sensitivity factor.
*
* @param value
* A scale factor to apply to mouse readings.
* 1.0 = default; < 1.0 = less sensitive; > 1.0 = more sensitive.
*/
void setMouseSensitivity(double value) { _mouse_sens = value; }
/**
* Gets the overall mouse sensitivity scale factor. Default = 1.0.
*/
double getMouseSensitivity() const { return _mouse_sens; }
/**
* Sets an overall touch sensitivity factor.
*
* @param value
* A scale factor to apply to mouse readings.
* 0.005 = default; < 0.005 = less sensitive; > 0.005 = more sensitive.
*/
void setTouchSensitivity(double value) { _touch_sens = value; }
/**
* Gets the overall touch sensitivity scale factor. Default = 1.0.
*/
double getTouchSensitivity() const { return _touch_sens; }
/**
* Sets the keyboard action sensitivity factor. This applies to navigation actions
* that are bound to keyboard events. For example, you may bind the LEFT arrow to
* the ACTION_PAN_LEFT action; this factor adjusts how much panning will occur during
* each frame that the key is depressed.
*
* @param value
* A scale factor to apply to keyboard-controller navigation.
* 1.0 = default; < 1.0 = less sensitive; > 1.0 = more sensitive.
*/
void setKeyboardSensitivity(double value) { _keyboard_sens = value; }
/**
* Gets the keyboard action sensitivity scale factor. Default = 1.0.
*/
double getKeyboardSensitivity() const { return _keyboard_sens; }
/**
* Sets the scroll-wheel sensitivity factor. This applies to navigation actions
* that are bound to scrolling events. For example, you may bind the scroll wheel to
* the ACTION_ZOOM_IN action; this factor adjusts how much zooming will occur each time
* you click the scroll wheel.
*
* @param value
* A scale factor to apply to scroll-wheel-controlled navigation.
* 1.0 = default; < 1.0 = less sensitive; > 1.0 = more sensitive.
*/
void setScrollSensitivity(double value) { _scroll_sens = value; }
/**
* Gets the scroll wheel sensetivity scale factor. Default = 1.0.
*/
double getScrollSensitivity() const { return _scroll_sens; }
/**
* When set to true, prevents simultaneous control of pitch and azimuth.
*
* Usually you can alter pitch and azimuth at the same time. When this flag
* is set, you can only control one at a time - if you start slewing the azimuth of the camera,
* the pitch stays locked until you stop moving and then start slewing the pitch.
*
* Default = false.
*/
void setSingleAxisRotation(bool value) { _single_axis_rotation = value; }
/**
* Gets whether simultaneous control over pitch and azimuth is disabled.
* Default = false.
*/
bool getSingleAxisRotation() const { return _single_axis_rotation; }
/**
* Sets whether to lock in a camera heading when performing panning operations (i.e.,
* changing the focal point).
*/
void setLockAzimuthWhilePanning(bool value) { _lock_azim_while_panning = value; }
/**
* Gets true if the manipulator should lock in a camera heading when performing panning
* operations (i.e. changing the focal point.)
*/
bool getLockAzimuthWhilePanning() const { return _lock_azim_while_panning; }
/**
* Sets the minimum and maximum allowable local camera pitch, in degrees.
*
* By "local" we mean relative to the tangent plane passing through the focal point on
* the surface of the terrain.
*
* Defaults are: Min = -90, Max = -10.
*/
void setMinMaxPitch(double min_pitch, double max_pitch);
/** Gets the minimum allowable local pitch, in degrees. */
double getMinPitch() const { return _min_pitch; }
/** Gets the maximum allowable local pitch, in degrees. */
double getMaxPitch() const { return _max_pitch; }
/** Gets the max x offset in world coordinates */
double getMaxXOffset() const { return _max_x_offset; }
/** Gets the max y offset in world coordinates */
double getMaxYOffset() const { return _max_y_offset; }
/** Gets the minimum distance from the focal point in world coordinates */
double getMinDistance() const { return _min_distance; }
/** Gets the maximum distance from the focal point in world coordinates */
double getMaxDistance() const { return _max_distance; }
/** Sets the min and max distance from the focal point in world coordinates */
void setMinMaxDistance(double min_distance, double max_distance);
/**
* Sets the maximum allowable offsets for the x and y camera offsets in world coordinates
*/
void setMaxOffset(double max_x_offset, double max_y_offset);
/** Mode used for tethering to a node. */
void setTetherMode(TetherMode value) { _tether_mode = value; }
TetherMode getTetherMode() const { return _tether_mode; }
/** Access to the list of Actions that will automatically break a tether */
ActionTypeVector& getBreakTetherActions() { return _breakTetherActions; }
const ActionTypeVector& getBreakTetherActions() const { return _breakTetherActions; }
/** Whether a setViewpoint transition whould "arc" */
void setArcViewpointTransitions(bool value);
bool getArcViewpointTransitions() const { return _arc_viewpoints; }
/** Activates auto-duration for transitioned viewpoints. */
void setAutoViewpointDurationEnabled(bool value);
bool getAutoViewpointDurationEnabled() const { return _auto_vp_duration; }
void setAutoViewpointDurationLimits(double minSeconds, double maxSeconds);
void getAutoViewpointDurationLimits(double& out_minSeconds, double& out_maxSeconds) const {
out_minSeconds = _min_vp_duration_s;
out_maxSeconds = _max_vp_duration_s;
}
/** Whether to automatically adjust an orthographic camera so that it "tracks" the last known FOV and Aspect Ratio. */
bool getOrthoTracksPerspective() const { return _orthoTracksPerspective; }
void setOrthoTracksPerspective(bool value) { _orthoTracksPerspective = value; }
/** Whether or not to keep the camera from going through the terrain surface */
bool getTerrainAvoidanceEnabled() const { return _terrainAvoidanceEnabled; }
void setTerrainAvoidanceEnabled(bool value) { _terrainAvoidanceEnabled = value; }
/** Minimum range for terrain avoidance checks in world coordinates */
double getTerrainAvoidanceMinimumDistance() const { return _terrainAvoidanceMinDistance; }
void setTerrainAvoidanceMinimumDistance(double minDistance) { _terrainAvoidanceMinDistance = minDistance; }
void setThrowingEnabled(bool throwingEnabled) { _throwingEnabled = throwingEnabled; }
bool getThrowingEnabled() const { return _throwingEnabled; }
void setThrowDecayRate(double throwDecayRate) { _throwDecayRate = osg::clampBetween(throwDecayRate, 0.0, 1.0); }
double getThrowDecayRate() const { return _throwDecayRate; }
private:
friend class EarthManipulator;
typedef std::pair<InputSpec, Action> ActionBinding;
typedef std::map<InputSpec, Action> ActionBindings;
// Gets the action bound to the provided input specification, or NullAction if there is
// to matching binding.
const Action& getAction(int event_type, int input_mask, int modkey_mask) const;
void expandSpec(const InputSpec& input, InputSpecs& output) const;
void bind(const InputSpec& spec, const Action& action);
private:
ActionBindings _bindings;
bool _single_axis_rotation;
bool _lock_azim_while_panning;
double _mouse_sens;
double _keyboard_sens;
double _scroll_sens;
double _touch_sens;
double _min_pitch;
double _max_pitch;
double _max_x_offset;
double _max_y_offset;
double _min_distance;
double _max_distance;
TetherMode _tether_mode;
ActionTypeVector _breakTetherActions;
bool _arc_viewpoints;
bool _auto_vp_duration;
double _min_vp_duration_s, _max_vp_duration_s;
bool _orthoTracksPerspective;
bool _terrainAvoidanceEnabled;
double _terrainAvoidanceMinDistance;
bool _throwingEnabled;
double _throwDecayRate;
};
public:
EarthManipulator();
EarthManipulator(osg::ArgumentParser& args);
EarthManipulator(const EarthManipulator& rhs);
/**
* Applies a new settings object to the manipulator, which takes effect immediately.
*/
void applySettings(Settings* settings);
/**
* Gets a handle on the current manipulator settings object.
*/
Settings* getSettings() const;
/**
* Gets the current camera position.
*/
Viewpoint getViewpoint() const;
/**
* Sets the camera position, optionally moving it there over time.
*/
virtual void setViewpoint(const Viewpoint& vp, double duration_s = 0.0);
/**
* Cancels a call to setViewpoint that resulted in an ongoing transition OR
* attachment to a node.
*/
void clearViewpoint();
/**
* Sets the viewpoint to activate when performing the ACTION_HOME action.
*/
void setHomeViewpoint(const Viewpoint& vp, double duration_s = 0.0);
/**
* Whether the manipulator is performing a viewpoint transition.
*/
bool isSettingViewpoint() const;
/**
* Whether the view is tethered to a node.
*/
bool isTethering() const;
/**
* Sets a callback to be invoked upon a tether or tether break
*/
class TetherCallback : public osg::Referenced {
public:
virtual void operator()(osg::Node* tetherNode) {}
protected:
virtual ~TetherCallback() {}
};
void setTetherCallback(TetherCallback* cb) { _tetherCallback = cb; }
TetherCallback* getTetherCallback() { return _tetherCallback.get(); }
/**
* Post-camera-update callback; use to access the camera position after
* the call to updateCamera (for frame synchronization)
*/
class UpdateCameraCallback : public osg::Referenced {
public:
virtual void onUpdateCamera(const osg::Camera*) {}
protected:
virtual ~UpdateCameraCallback() {}
};
void setUpdateCameraCallback(UpdateCameraCallback* cb) { _updateCameraCallback = cb; }
UpdateCameraCallback* getUpdateCameraCallback() { return _updateCameraCallback.get(); }
/**
* Move the focal point of the camera using deltas (normalized screen coords).
*/
virtual void pan(double dx, double dy);
/**
* Rotate the camera (dx = azimuth, dy = pitch) using deltas (radians).
*/
virtual void rotate(double dx, double dy);
/**
* Zoom the camera using deltas (dy only)
*/
virtual void zoom(double dx, double dy);
/**
* Drag the earth using deltas
*/
virtual void drag(double dx, double dy, osg::View* view);
/**
* Converts screen coordinates (relative to the view's viewpoint) to world
* coordinates. Note, this method will use the mask set by setTraversalMask().
*
* @param x, y
* Viewport coordinates
* @param view
* View for which to calculate world coordinates
* @param out_coords
* Output world coordinates (only valid if the method returns true)
*/
bool screenToWorld(float x, float y, osg::View* view, osg::Vec3d& out_coords) const;
/**
* Gets the distance from the focal point in world coordiantes
*/
double getDistance() const { return _distance; }
/**
* Sets the distance from the focal point in world coordinates.
*
* The incoming distance value will be clamped within the valid range specified by the settings.
*/
void setDistance(double distance);
/**
* Gets the rotation of the manipulator. Note: This rotation is in addition to the rotation needed to center the view on the focal point.
*/
const osg::Quat& getRotation() { return _rotation; }
/**
* Sets the rotation of the manipulator. Note: This rotation is in addition to the rotation needed to center the view on the focal point.
*/
void setRotation(const osg::Quat& rotation) { _rotation = rotation; }
/**
* Gets the traversal node mask used to find root MapNode and CoordinateSystemNodes. Default is 0x1.
*/
osg::Node::NodeMask getFindNodeTraversalMask() { return _findNodeTraversalMask; }
/**
* Sets the traversal node mask used to find root MapNode and CoordinateSystemNode. Default is 0x1.
* Use this method if you change MapNode or CoordinateSystemNode mask and want manipulator to work with them correctly.
*/
void setFindNodeTraversalMask(const osg::Node::NodeMask & nodeMask) { _findNodeTraversalMask = nodeMask; }
/**
* Expressly set the initial vertical FOV.
* If the manipulator detects that the camera has switched from persective
* to orthographic projection, it will use the last know VFOV of the perspective
* projection to match the zoom level in orthographic mode. However, if you start
* in orthographic mode, it doesn't have this information; you can provide it
* with this method.
*/
void setInitialVFOV(double vfov);
/**
* The last detected VFOV of a perspective camera (or the initial FOV if started in ortho)
*/
double getLastKnownVFOV() const { return _lastKnownVFOV; }
/**
* Assigns a NodeVisitor to use when OSG calls updateCamera() at the end of
* the update traversal. This is useful if you have a Transform subclass that
* overrides Transform::computeLocalToWorldMatrix and needs access to a
* NodeVisitor.
*/
void setUpdateCameraNodeVisitor(osg::NodeVisitor* nv);
public: // osgGA::CameraManipulator
virtual const char* className() const { return "EarthManipulator"; }
/** set the position of the matrix manipulator using a 4x4 Matrix.*/
virtual void setByMatrix(const osg::Matrixd& matrix);
/** set the position of the matrix manipulator using a 4x4 Matrix.*/
virtual void setByInverseMatrix(const osg::Matrixd& matrix) { setByMatrix(osg::Matrixd::inverse(matrix)); }
/** get the position of the manipulator as 4x4 Matrix.*/
virtual osg::Matrixd getMatrix() const;
/** get the position of the manipulator as a inverse matrix of the manipulator, typically used as a model view matrix.*/
virtual osg::Matrixd getInverseMatrix() const;
/** update the camera with the current values from this manipulator. Overloaded to support tethering, this method is
called by Viewer or ComppositeViewer at the end of the update traversal. */
virtual void updateCamera(osg::Camera& camera);
// Gets the stereo convergance mode.
virtual osgUtil::SceneView::FusionDistanceMode getFusionDistanceMode() const { return osgUtil::SceneView::USE_FUSION_DISTANCE_VALUE; }
// Gets the stereo convergance distance.
virtual float getFusionDistanceValue() const { return _distance; }
// Attach a node to the manipulator.
virtual void setNode(osg::Node*);
// Gets the node to which this manipulator is attached.
virtual osg::Node* getNode();
// Move the camera to the default position.
virtual void home(double /*unused*/);
virtual void home(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& us);
// Start/restart the manipulator.
virtual void init(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& us);
// handle events, return true if handled, false otherwise.
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& us);
// Get the keyboard and mouse usage of this manipulator.
virtual void getUsage(osg::ApplicationUsage& usage) const;
virtual void computeHomePosition();
// react to a tile-added event from the Terrain
virtual void handleTileAdded(const TileKey& key, osg::Node* tile, TerrainCallbackContext& context);
// returns the absolute Euler angles composited from the composite rotation matrix.
void getCompositeEulerAngles(double* out_azim, double* out_pitch = 0L) const;
protected:
virtual ~EarthManipulator();
bool intersect(const osg::Vec3d& start, const osg::Vec3d& end, osg::Vec3d& intersection, osg::Vec3d& normal) const;
bool intersectLookVector(osg::Vec3d& eye, osg::Vec3d& out_target, osg::Vec3d& up) const;
// resets the mouse event stack and pushes the provided event.
void resetMouse(osgGA::GUIActionAdapter& aa, bool flushEventStack = true);
// Reset the internal event stack.
void flushMouseEventStack();
// Add the current mouse osgGA::GUIEvent to internal stack.
void addMouseEvent(const osgGA::GUIEventAdapter& ea);
// sets the camera position by doing a "look at" calculation, converting the center
// point into a look vector and intersecting the terrain.
void setByLookAt(const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up);
// sets the camera position by doing a "look at" calculation, but takes the target point
// "as-is" and does not try to find an intersection.
void setByLookAtRaw(const osg::Vec3d& eye, const osg::Vec3d& target, const osg::Vec3d& up);
// checks to see whether the mouse is "moving".
bool isMouseMoving();
// This sets the camera's roll based on your location on the globe.
void recalculateRoll();
// Gets the matrix without a pre-MapNode transform (i.e., map world space)
osg::Matrixd getWorldMatrix() const;
// Gets the inverse matrix without a pre-MapNode transform (i.e., map world space)
osg::Matrixd getWorldInverseMatrix() const;
protected:
enum TaskType {
TASK_NONE,
TASK_PAN,
TASK_ROTATE,
TASK_ZOOM
};
struct Task : public osg::Referenced {
Task() : _type(TASK_NONE) {}
void set(TaskType type, double dx, double dy, double duration, double now) {
_type = type; _dx = dx; _dy = dy; _duration_s = duration; _time_last_service = now;
}
TaskType _type;
double _dx, _dy;
double _duration_s;
double _time_last_service;
};
// "ticks" the resident Task, which allows for multi-frame animation of navigation
// movements.
bool serviceTask();
// returns the Euler Angles baked into _rotation, the local frame's rotation quaternion.
void getEulerAngles(const osg::Quat& quat, double* azim, double* pitch) const;
// Makes a quaternion from an azimuth and pitch.
osg::Quat getQuaternion(double azim, double pitch) const;
/**
* Fire a ray from the current eyepoint along the current look vector,
* intersect the terrain at the closest point, and reset the matrix parameters
* based on that point.
*/
bool recalculateCenterFromLookVector();
void recalculateCenter(const osg::CoordinateFrame& frame);
osg::Matrixd getRotation(const osg::Vec3d& center) const;
osg::Quat computeCenterRotation(const osg::Vec3d& center) const;
void updateTether(double t);
void updateSetViewpoint();
bool isMouseClick(const osgGA::GUIEventAdapter* mouse_up_event) const;
void applyOptionsToDeltas(const Action& action, double& dx, double& dy);
void configureDefaultSettings();
void reinitialize();
bool established();
// sets the new center (focal) point and recalculates it's L2W matrix.
void setCenter(const osg::Vec3d& center);
// creates a "local-to-world" transform relative to the input point.
bool createLocalCoordFrame(const osg::Vec3d& worldPos, osg::CoordinateFrame& out_frame) const;
// returns an ActionType that would be initiated by the OSG UI event
ActionType getActionTypeForEvent(const osgGA::GUIEventAdapter& ea) const;
public:
void recalculateCenter() { recalculateCenter(_centerLocalToWorld); }
const GeoPoint& centerMap() const { return _centerMap; }
protected:
typedef osgGA::GUIEventAdapter::TouchData::TouchPoint TouchPoint;
typedef std::vector<TouchPoint> MultiTouchPoint; // one per ID (finger/touchpoint)
typedef std::deque<MultiTouchPoint> MultiTouchPointQueue;
MultiTouchPointQueue _touchPointQueue;
struct TouchEvent {
TouchEvent() : _mbmask(0) {}
EventType _eventType;
unsigned _mbmask;
float _dx, _dy;
};
typedef std::vector<TouchEvent> TouchEvents;
void addTouchEvents(const osgGA::GUIEventAdapter& ea);
bool parseTouchEvents(TouchEvents& ev);
// Applies an action using the raw input parameters.
bool handleAction(const Action& action, double dx, double dy, double duration);
virtual bool handleMouseAction(const Action& action, osg::View* view);
virtual bool handleMouseClickAction(const Action& action);
virtual bool handleKeyboardAction(const Action& action, double duration_s = DBL_MAX);
virtual bool handleScrollAction(const Action& action, double duration_s = DBL_MAX);
virtual bool handlePointAction(const Action& type, float mx, float my, osg::View* view);
virtual void handleContinuousAction(const Action& action, osg::View* view);
virtual void handleMovementAction(const ActionType& type, double dx, double dy, osg::View* view);
protected:
// makeshift "stack" of the last 2 incoming events.
osg::ref_ptr<const osgGA::GUIEventAdapter> _ga_t1;
osg::ref_ptr<const osgGA::GUIEventAdapter> _ga_t0;
osg::ref_ptr<const osgGA::GUIEventAdapter> _mouse_down_event;
bool _pushed;
osg::observer_ptr<osg::Node> _node;
osg::observer_ptr<MapNode> _mapNode;
osg::ref_ptr<const osgEarth::SpatialReference> _srs;
double _time_s_now;
bool _thrown;
double _throw_dx;
double _throw_dy;
double _dx;
double _dy;
// The world coordinate of the Viewpoint focal point.
osg::Vec3d _center;
GeoPoint _centerMap;
double _centerHeight;
// local2world matrix for the center point.
osg::CoordinateFrame _centerLocalToWorld;
// Rotation offset to _rotation when tethering.
osg::Quat _tetherRotation;
// The initial offset applied to the tether rotation when orientation-tethering begins.
// This is usually just the inverse of the first-calculated _tetherRotation.
optional<osg::Quat> _tetherRotationOffset;
// The rotation (heading and pitch) of the camera in the
// earth-local frame defined by _centerRotation.
osg::Quat _rotation;
// The rotation that makes the camera look down on the focal
// point on the earth. This is equivalent to a rotation by
// latitude, longitude.
osg::Quat _centerRotation;
// distance from camera to center of rotation.
double _distance;
// XYZ offsets of the focal point in the local tangent plane coordinate system
// of the focal point.
osg::Vec3d _posOffset;
// XY offsets (left/right, down/up) of the focal point in the plane normal to
// the view heading.
osg::Vec2d _viewOffset;
osg::Vec3d _previousUp;
osg::ref_ptr<Task> _task;
osg::Timer_t _time_last_frame;
bool _continuous;
double _continuous_dx;
double _continuous_dy;
double _last_continuous_action_time;
double _single_axis_x;
double _single_axis_y;
// the "pending" viewpoint is only used to enable setting the
// viewpoint before the frame loop starts
optional<Viewpoint> _pendingViewpoint;
Duration _pendingViewpointDuration;
optional<Viewpoint> _setVP0; // Starting viewpoint
optional<Viewpoint> _setVP1; // Final viewpoint
optional<Duration> _setVPStartTime; // Time of call to setViewpoint
Duration _setVPDuration; // Transition time for setViewpoint
double _setVPAccel, _setVPAccel2; // Acceleration factors for setViewpoint
double _setVPArcHeight; // Altitude arcing height for setViewpoint
osg::Quat _tetherRotationVP0; // saves _tetherRotation at the start of a transition
osg::Quat _tetherRotationVP1; // target _tetherRotation if not tethered
TetherMode _lastTetherMode;
osg::Matrix _mapNodeFrame, _mapNodeFrameInverse;
// returns "t", the parametric coefficient of a timed transition. 1=finished.
double setViewpointFrame(double time_s);
void setLookAt(const osg::Vec3d& center, double azim, double pitch, double range, const osg::Vec3d& posoffset);
void resetLookAt();
void collapseTetherRotationIntoRotation();
unsigned _frameCount;
osg::ref_ptr<Settings> _settings;
osgEarth::optional<Viewpoint> _homeViewpoint;
double _homeViewpointDuration;
Action _last_action;
EventType _last_event;
double _time_s_last_event;
double _lastKnownVFOV;
/** updates a camera to switch between prospective and ortho. */
void updateProjection(osg::Camera* eventCamera);
// Support snappy transition when the pointer leaves and
// returns to earth during a drag
osg::Vec3d _lastPointOnEarth;
osg::ref_ptr< TerrainCallback > _terrainCallback;
// Traversal mask used in established and dtor methods to find MapNode and CoordinateSystemNode
osg::Node::NodeMask _findNodeTraversalMask;
osg::ref_ptr<TetherCallback> _tetherCallback;
osg::ref_ptr<UpdateCameraCallback> _updateCameraCallback;
bool _userWillCallUpdateCamera;
osg::observer_ptr<osg::NodeVisitor> _updateCameraNodeVisitor;
void collisionDetect();
void ctor_init();
public:
void setEarthCallback(IEarthMap* earthMap) {
_earthMap = earthMap;
}
private:
IEarthMap* _earthMap = nullptr;
};
#endif // EARTHMANIPULATOR_H
EarthManipulator.cpp
/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2019 Pelican Mapping
* http://osgearth.org
*
* osgEarth is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#undef min
#undef max
#include <EarthManipulator.h>
#include <osgEarth/GeoMath>
#include <osgEarth/TerrainEngineNode>
#include <osgViewer/View>
#include "callback/IEarthMap.h"
#define LC "[EarthManip] "
using namespace osgEarth::Util;
using namespace osgEarth;
//------------------------------------------------------------------------
namespace
{
// a reasonable approximation of cosine interpolation
double
smoothStepInterp(double t) {
return (t*t)*(3.0 - 2.0*t);
}
// rough approximation of pow(x,y)
double
powFast(double x, double y) {
return x / (x + y - y * x);
}
// accel/decel curve (a < 0 => decel)
double
accelerationInterp(double t, double a) {
return a == 0.0 ? t : a > 0.0 ? powFast(t, a) : 1.0 - powFast(1.0 - t, -a);
}
// normalized linear intep
osg::Vec3d nlerp(const osg::Vec3d& a, const osg::Vec3d& b, double t) {
double am = a.length(), bm = b.length();
osg::Vec3d c = a * (1.0 - t) + b * t;
c.normalize();
c *= (1.0 - t)*am + t * bm;
return c;
}
// linear interp
osg::Vec3d lerp(const osg::Vec3d& a, const osg::Vec3d& b, double t) {
return a * (1.0 - t) + b * t;
}
osg::Matrix computeLocalToWorld(osg::Node* node) {
osg::Matrix m;
if (node) {
osg::NodePathList nodePaths = node->getParentalNodePaths();
if (nodePaths.size() > 0) {
osg::NodePath p;
unsigned start = 0;
for (unsigned i = 0; i < nodePaths[0].size(); ++i) {
if (dynamic_cast<MapNode*>(nodePaths[0][i]) != 0L) {
start = i;
break;
}
}
for (unsigned i = start; i < nodePaths[0].size(); ++i)
p.push_back(nodePaths[0][i]);
m = osg::computeLocalToWorld(p);
//m = osg::computeLocalToWorld( nodePaths[0] );
}
else {
osg::Transform* t = dynamic_cast<osg::Transform*>(node);
if (t) {
t->computeLocalToWorldMatrix(m, 0L);
}
}
}
return m;
}
osg::Vec3d computeWorld(osg::Node* node) {
return node ? osg::Vec3d(0, 0, 0) * computeLocalToWorld(node) : osg::Vec3d(0, 0, 0);
}
double normalizeAzimRad(double input)
{
if (fabs(input) > 2 * osg::PI)
input = fmod(input, 2 * osg::PI);
if (input < -osg::PI) input += osg::PI*2.0;
if (input > osg::PI) input -= osg::PI*2.0;
return input;
}
// This replaces OSG's osg::computeLocalToWorld() function with one that
// passes your own NodeVisitor to the Transform::computeLocalToWorldMatrix()
// method. (We cannot subclass OSG's visitor because it's private.) This
// exists for users that have custom Transform subclasses that override
// Transform::computeLocalToWorldMatrix and need access to the NodeVisitor.
struct ComputeLocalToWorld : osg::NodeVisitor
{
osg::Matrix _matrix;
osg::NodeVisitor* _nv;
ComputeLocalToWorld(osg::NodeVisitor* nv) : _nv(nv) { }
void accumulate(const osg::NodePath& path)
{
if (path.empty()) return;
unsigned j = path.size();
for (osg::NodePath::const_reverse_iterator i = path.rbegin();
i != path.rend();
++i, --j)
{
const osg::Camera* cam = dynamic_cast<const osg::Camera*>(*i);
if (cam)
{
if (cam->getReferenceFrame() != osg::Transform::RELATIVE_RF || cam->getParents().empty())
break;
}
else if (dynamic_cast<MapNode*>(*i))
{
break;
}
}
for (; j < path.size(); ++j)
{
const_cast<osg::Node*>(path[j])->accept(*this);
}
}
void apply(osg::Transform& transform)
{
transform.computeLocalToWorldMatrix(_matrix, _nv);
}
};
}
//------------------------------------------------------------------------
namespace
{
// Callback that notifies the manipulator whenever the terrain changes
// around its center point.
struct ManipTerrainCallback : public TerrainCallback
{
ManipTerrainCallback(EarthManipulator* manip) : _manip(manip) { }
void onTileAdded(const TileKey& key, osg::Node* graph, TerrainCallbackContext& context)
{
osg::ref_ptr<EarthManipulator> safe;
if (_manip.lock(safe))
{
safe->handleTileAdded(key, graph, context);
}
}
osg::observer_ptr<EarthManipulator> _manip;
};
}
EarthManipulator::Action::Action(ActionType type, const ActionOptions& options) :
_type(type),
_options(options)
{
init();
}
EarthManipulator::Action::Action(ActionType type) :
_type(type)
{
init();
}
void
EarthManipulator::Action::init()
{
_dir =
_type == ACTION_PAN_LEFT || _type == ACTION_ROTATE_LEFT ? DIR_LEFT :
_type == ACTION_PAN_RIGHT || _type == ACTION_ROTATE_RIGHT ? DIR_RIGHT :
_type == ACTION_PAN_UP || _type == ACTION_ROTATE_UP || _type == ACTION_ZOOM_IN ? DIR_UP :
_type == ACTION_PAN_DOWN || _type == ACTION_ROTATE_DOWN || _type == ACTION_ZOOM_OUT ? DIR_DOWN :
DIR_NA;
}
EarthManipulator::Action::Action(const Action& rhs) :
_type(rhs._type),
_dir(rhs._dir),
_options(rhs._options)
{
//nop
}
bool
EarthManipulator::Action::getBoolOption(int option, bool defaultValue) const
{
for (ActionOptions::const_iterator i = _options.begin(); i != _options.end(); i++) {
if (i->option() == option)
return i->boolValue();
}
return defaultValue;
}
int
EarthManipulator::Action::getIntOption(int option, int defaultValue) const
{
for (ActionOptions::const_iterator i = _options.begin(); i != _options.end(); i++) {
if (i->option() == option)
return i->intValue();
}
return defaultValue;
}
double
EarthManipulator::Action::getDoubleOption(int option, double defaultValue) const
{
for (ActionOptions::const_iterator i = _options.begin(); i != _options.end(); i++) {
if (i->option() == option)
return i->doubleValue();
}
return defaultValue;
}
/****************************************************************************/
EarthManipulator::Action EarthManipulator::NullAction(EarthManipulator::ACTION_NULL);
static std::string s_actionNames[] = {
"null",
"home",
"goto",
"pan",
"pan-left",
"pan-right",
"pan-up",
"pan-down",
"rotate",
"rotate-left",
"rotate-right",
"rotate-up",
"rotate-down",
"zoom",
"zoom-in",
"zoom-out",
"earth-drag"
};
static std::string s_actionOptionNames[] = {
"scale-x",
"scale-y",
"continuous",
"single-axis",
"goto-range-factor",
"duration"
};
static short s_actionOptionTypes[] = { 1, 1, 0, 0, 1, 1 }; // 0=bool, 1=double
//------------------------------------------------------------------------
EarthManipulator::Settings::Settings() :
osg::Referenced(),
Revisioned(),
_single_axis_rotation(false),
_lock_azim_while_panning(true),
_mouse_sens(1.0),
_touch_sens(0.005),
_keyboard_sens(1.0),
_scroll_sens(1.0),
_min_pitch(-89.9),
_max_pitch(-1.0),
_max_x_offset(0.0),
_max_y_offset(0.0),
_min_distance(1.0),
_max_distance(DBL_MAX),
_tether_mode(TETHER_CENTER),
_arc_viewpoints(true),
_auto_vp_duration(false),
_min_vp_duration_s(3.0),
_max_vp_duration_s(8.0),
_orthoTracksPerspective(true),
_terrainAvoidanceEnabled(true),
_terrainAvoidanceMinDistance(1.0),
_throwingEnabled(false),
_throwDecayRate(0.05)
{
//NOP
}
EarthManipulator::Settings::Settings(const EarthManipulator::Settings& rhs) :
osg::Referenced(rhs),
Revisioned(rhs),
_bindings(rhs._bindings),
_single_axis_rotation(rhs._single_axis_rotation),
_lock_azim_while_panning(rhs._lock_azim_while_panning),
_mouse_sens(rhs._mouse_sens),
_touch_sens(rhs._touch_sens),
_keyboard_sens(rhs._keyboard_sens),
_scroll_sens(rhs._scroll_sens),
_min_pitch(rhs._min_pitch),
_max_pitch(rhs._max_pitch),
_max_x_offset(rhs._max_x_offset),
_max_y_offset(rhs._max_y_offset),
_min_distance(rhs._min_distance),
_max_distance(rhs._max_distance),
_tether_mode(rhs._tether_mode),
_arc_viewpoints(rhs._arc_viewpoints),
_auto_vp_duration(rhs._auto_vp_duration),
_min_vp_duration_s(rhs._min_vp_duration_s),
_max_vp_duration_s(rhs._max_vp_duration_s),
_orthoTracksPerspective(rhs._orthoTracksPerspective),
_breakTetherActions(rhs._breakTetherActions),
_terrainAvoidanceEnabled(rhs._terrainAvoidanceEnabled),
_terrainAvoidanceMinDistance(rhs._terrainAvoidanceMinDistance),
_throwingEnabled(rhs._throwingEnabled),
_throwDecayRate(rhs._throwDecayRate)
{
//NOP
}
void
EarthManipulator::Settings::apply(osg::ArgumentParser& args)
{
bool boolval;
double doubleval;
if (args.read("--manip-terrain-avoidance", boolval))
setTerrainAvoidanceEnabled(boolval);
if (args.read("--manip-terrain-avoidance-min-distance", doubleval))
setTerrainAvoidanceMinimumDistance(doubleval);
if (args.read("--manip-min-distance", doubleval))
setMinMaxDistance(doubleval, _max_distance);
if (args.read("--manip-max-distance", doubleval))
setMinMaxDistance(_min_distance, doubleval);
if (args.read("--manip-min-pitch", doubleval))
setMinMaxPitch(doubleval, _max_pitch);
if (args.read("--manip-max-pitch", doubleval))
setMinMaxPitch(_min_pitch, doubleval);
}
#define HASMODKEY( W, V ) (( W & V ) == V )
// expands one input spec into many if necessary, to deal with modifier key combos.
void
EarthManipulator::Settings::expandSpec(const InputSpec& input, InputSpecs& output) const
{
int e = input._event_type;
int i = input._input_mask;
int m = input._modkey_mask;
if (HASMODKEY(m, osgGA::GUIEventAdapter::MODKEY_CTRL))
{
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_LEFT_CTRL), output);
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_RIGHT_CTRL), output);
}
else if (HASMODKEY(m, osgGA::GUIEventAdapter::MODKEY_ALT))
{
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_LEFT_ALT), output);
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_RIGHT_ALT), output);
}
else if (HASMODKEY(m, osgGA::GUIEventAdapter::MODKEY_SHIFT))
{
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_LEFT_SHIFT), output);
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_RIGHT_SHIFT), output);
}
else if (HASMODKEY(m, osgGA::GUIEventAdapter::MODKEY_META))
{
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_LEFT_META), output);
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_RIGHT_META), output);
}
else if (HASMODKEY(m, osgGA::GUIEventAdapter::MODKEY_HYPER))
{
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_LEFT_HYPER), output);
expandSpec(InputSpec(e, i, m & ~osgGA::GUIEventAdapter::MODKEY_RIGHT_HYPER), output);
}
//Always add the input so if we are dealing with a windowing system like QT that just sends MODKEY_CTRL it will still work.
output.push_back(input);
}
void
EarthManipulator::Settings::bind(const InputSpec& spec, const Action& action)
{
InputSpecs specs;
expandSpec(spec, specs);
for (InputSpecs::const_iterator i = specs.begin(); i != specs.end(); i++)
{
_bindings[*i] = action;
}
}
void
EarthManipulator::Settings::bindMouse(ActionType actionType,
int button_mask, int modkey_mask,
const ActionOptions& options)
{
bind(
InputSpec(osgGA::GUIEventAdapter::DRAG, button_mask, modkey_mask),
Action(actionType, options));
}
void
EarthManipulator::Settings::bindMouseClick(ActionType action,
int button_mask, int modkey_mask,
const ActionOptions& options)
{
bind(
InputSpec(EVENT_MOUSE_CLICK, button_mask, modkey_mask),
Action(action, options));
}
void
EarthManipulator::Settings::bindMouseDoubleClick(ActionType action,
int button_mask, int modkey_mask,
const ActionOptions& options)
{
bind(
InputSpec(EVENT_MOUSE_DOUBLE_CLICK, button_mask, modkey_mask),
Action(action, options));
}
void
EarthManipulator::Settings::bindKey(ActionType action,
int key, int modkey_mask,
const ActionOptions& options)
{
bind(
InputSpec(osgGA::GUIEventAdapter::KEYDOWN, key, modkey_mask),
Action(action, options));
}
void
EarthManipulator::Settings::bindScroll(ActionType action, int scrolling_motion,
int modkey_mask, const ActionOptions& options)
{
bind(
InputSpec(osgGA::GUIEventAdapter::SCROLL, scrolling_motion, modkey_mask),
Action(action, options));
}
void
EarthManipulator::Settings::bindPinch(ActionType action, const ActionOptions& options)
{
bind(
InputSpec(EarthManipulator::EVENT_MULTI_PINCH, 0, 0),
Action(action, options));
}
void
EarthManipulator::Settings::bindTwist(ActionType action, const ActionOptions& options)
{
bind(
InputSpec(EarthManipulator::EVENT_MULTI_TWIST, 0, 0),
Action(action, options));
}
void
EarthManipulator::Settings::bindMultiDrag(ActionType action, const ActionOptions& options)
{
bind(
InputSpec(EarthManipulator::EVENT_MULTI_DRAG, 0, 0),
Action(action, options));
}
const EarthManipulator::Action&
EarthManipulator::Settings::getAction(int event_type, int input_mask, int modkey_mask) const
{
//Build the input spec but remove the numlock and caps lock from the modkey mask. On Linux these seem to be passed in as part of the modkeymask
//if they are on. So if you bind an action like SCROLL to a modkey mask of 0 or a modkey mask of ctrl it will never match the spec exactly b/c
//the modkey mask also includes capslock and numlock.
InputSpec spec(event_type, input_mask, modkey_mask & ~osgGA::GUIEventAdapter::MODKEY_NUM_LOCK & ~osgGA::GUIEventAdapter::MODKEY_CAPS_LOCK);
ActionBindings::const_iterator i = _bindings.find(spec);
return i != _bindings.end() ? i->second : NullAction;
}
void
EarthManipulator::Settings::setMinMaxPitch(double min_pitch, double max_pitch)
{
_min_pitch = osg::clampBetween(min_pitch, -89.9, 89.0);
_max_pitch = osg::clampBetween(max_pitch, min_pitch, 89.0);
dirty();
}
void
EarthManipulator::Settings::setMaxOffset(double max_x_offset, double max_y_offset)
{
_max_x_offset = osg::clampAbove(max_x_offset, 0.0);
_max_y_offset = osg::clampAbove(max_y_offset, 0.0);
dirty();
}
void
EarthManipulator::Settings::setMinMaxDistance(double min_distance, double max_distance)
{
_min_distance = min_distance;
_max_distance = max_distance;
dirty();
}
void
EarthManipulator::Settings::setArcViewpointTransitions(bool value)
{
_arc_viewpoints = value;
dirty();
}
void
EarthManipulator::Settings::setAutoViewpointDurationEnabled(bool value)
{
_auto_vp_duration = value;
dirty();
}
void
EarthManipulator::Settings::setAutoViewpointDurationLimits(double minSeconds, double maxSeconds)
{
_min_vp_duration_s = osg::clampAbove(minSeconds, 0.0);
_max_vp_duration_s = osg::clampAbove(maxSeconds, _min_vp_duration_s);
dirty();
}
/************************************************************************/
void
EarthManipulator::ctor_init()
{
_last_action = ACTION_NULL;
_last_event = EVENT_MOUSE_DOUBLE_CLICK;
_time_s_last_event = 0.0;
_frameCount = 0;
_findNodeTraversalMask = 0x01;
_time_s_now = 0.0;
_centerHeight = 0.0;
_time_last_frame = 0.0;
_continuous_dx = 0;
_continuous_dy = 0;
_last_continuous_action_time = 0.0;
_single_axis_x = 0;
_single_axis_y = 0;
_setVPAccel = 0;
_setVPAccel2 = 0;
_lastTetherMode = TETHER_CENTER;
_homeViewpointDuration = 0;
_lastKnownVFOV = 30.0;
_userWillCallUpdateCamera = false;
}
EarthManipulator::EarthManipulator() :
osgGA::CameraManipulator()
{
ctor_init();
reinitialize();
configureDefaultSettings();
if (_settings.valid())
_lastTetherMode = _settings->getTetherMode();
}
EarthManipulator::EarthManipulator(osg::ArgumentParser& args) :
osgGA::CameraManipulator()
{
ctor_init();
reinitialize();
configureDefaultSettings();
if (_settings.valid())
_lastTetherMode = _settings->getTetherMode();
getSettings()->apply(args);
}
EarthManipulator::EarthManipulator(const EarthManipulator& rhs) :
osgGA::CameraManipulator(rhs),
_last_action(ACTION_NULL),
_last_event(EVENT_MOUSE_DOUBLE_CLICK),
_time_s_last_event(0.0),
_frameCount(0),
_settings(new Settings(*rhs.getSettings())),
_findNodeTraversalMask(rhs._findNodeTraversalMask)
{
reinitialize();
}
EarthManipulator::~EarthManipulator()
{
osg::ref_ptr<MapNode> mapNode = _mapNode;
if (mapNode.valid() && _terrainCallback && mapNode->getTerrain())
{
mapNode->getTerrain()->removeTerrainCallback(_terrainCallback.get());
}
}
void
EarthManipulator::configureDefaultSettings()
{
_settings = new Settings();
// install default action bindings:
ActionOptions options;
_settings->bindKey(ACTION_HOME, osgGA::GUIEventAdapter::KEY_Space);
_settings->bindMouse(ACTION_PAN, osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON);
//_settings->bindMouse( ACTION_EARTH_DRAG, osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON );
// zoom as you hold the right button:
options.clear();
options.add(OPTION_CONTINUOUS, true);
_settings->bindMouse(ACTION_ZOOM, osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON, 0L, options);
// rotate with either the middle button or the left+right buttons:
_settings->bindMouse(ACTION_ROTATE, osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON);
_settings->bindMouse(ACTION_ROTATE, osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON | osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON);
// zoom with the scroll wheel:
_settings->bindScroll(ACTION_ZOOM_IN, osgGA::GUIEventAdapter::SCROLL_DOWN);
_settings->bindScroll(ACTION_ZOOM_OUT, osgGA::GUIEventAdapter::SCROLL_UP);
// pan around with arrow keys:
_settings->bindKey(ACTION_PAN_LEFT, osgGA::GUIEventAdapter::KEY_Left);
_settings->bindKey(ACTION_PAN_RIGHT, osgGA::GUIEventAdapter::KEY_Right);
_settings->bindKey(ACTION_PAN_UP, osgGA::GUIEventAdapter::KEY_Up);
_settings->bindKey(ACTION_PAN_DOWN, osgGA::GUIEventAdapter::KEY_Down);
// double click the left button to zoom in on a point:
options.clear();
options.add(OPTION_GOTO_RANGE_FACTOR, 0.4);
_settings->bindMouseDoubleClick(ACTION_GOTO, osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON, 0L, options);
// double click the right button (or CTRL-left button) to zoom out to a point
options.clear();
options.add(OPTION_GOTO_RANGE_FACTOR, 2.5);
_settings->bindMouseDoubleClick(ACTION_GOTO, osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON, 0L, options);
_settings->bindMouseDoubleClick(ACTION_GOTO, osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON, osgGA::GUIEventAdapter::MODKEY_CTRL, options);
// map multi-touch pinch to a discrete zoom
options.clear();
_settings->bindPinch(ACTION_ZOOM, options);
options.clear();
_settings->bindTwist(ACTION_ROTATE, options);
_settings->bindMultiDrag(ACTION_ROTATE, options);
//_settings->setThrowingEnabled( false );
_settings->setLockAzimuthWhilePanning(true);
}
void
EarthManipulator::applySettings(Settings* settings)
{
if (settings)
{
_settings = settings;
}
else
{
configureDefaultSettings();
}
_task->_type = TASK_NONE;
flushMouseEventStack();
// apply new pitch restrictions
double old_pitch;
getEulerAngles(_rotation, 0L, &old_pitch);
double new_pitch = osg::clampBetween(old_pitch, _settings->getMinPitch(), _settings->getMaxPitch());
setDistance(_distance);
if (new_pitch != old_pitch)
{
Viewpoint vp = getViewpoint();
vp.pitch() = new_pitch;
setViewpoint(vp);
}
}
EarthManipulator::Settings*
EarthManipulator::getSettings() const
{
return _settings.get();
}
void
EarthManipulator::reinitialize()
{
_distance = 1.0;
_viewOffset.set(0, 0);
_posOffset.set(0, 0, 0);
_thrown = false;
_dx = 0.0;
_dy = 0.0;
_throw_dx = 0.0;
_throw_dy = 0.0;
_continuous = false;
_task = new Task();
_last_action = ACTION_NULL;
_srs = 0L;
_pendingViewpoint.unset();
_setVP0.unset();
_setVP1.unset();
_lastPointOnEarth.set(0.0, 0.0, 0.0);
_setVPArcHeight = 0.0;
_pushed = false;
}
bool
EarthManipulator::established()
{
if (_srs.valid() && _mapNode.valid() && _node.valid())
return true;
// lock down the observed node:
osg::ref_ptr<osg::Node> safeNode;
if (!_node.lock(safeNode))
return false;
// find a map node or fail:
_mapNode = osgEarth::MapNode::findMapNode(safeNode.get());
if (!_mapNode.valid())
return false;
// resetablish the terrain callback on the map node:
if (_terrainCallback.valid() && _mapNode->getTerrain())
{
_mapNode->getTerrain()->removeTerrainCallback(_terrainCallback.get());
}
_terrainCallback = new ManipTerrainCallback(this);
if (_mapNode->getTerrain())
_mapNode->getTerrain()->addTerrainCallback(_terrainCallback.get());
// Cache the SRS.
_srs = _mapNode->getMapSRS();
// Set the home viewpoint if necessary.
if (!_homeViewpoint.isSet())
{
if (_pendingViewpoint.isSet())
{
setHomeViewpoint(_pendingViewpoint.get(), _pendingViewpointDuration.as(Units::SECONDS));
}
else if (_srs->isGeographic())
{
Viewpoint vp;
vp.focalPoint() = GeoPoint(_srs.get(), -90.0, 0, 0, ALTMODE_ABSOLUTE);
vp.heading()->set(0.0, Units::DEGREES);
vp.pitch()->set(-89.0, Units::DEGREES);
vp.range()->set(_srs->getEllipsoid()->getRadiusEquator() * 3.0, Units::METERS);
vp.positionOffset()->set(0, 0, 0);
setHomeViewpoint(vp);
}
else
{
Viewpoint vp;
const Profile* profile = _mapNode->getMap()->getProfile();
vp.focalPoint() = GeoPoint(_srs.get(), profile->getExtent().getCentroid(), ALTMODE_ABSOLUTE);
vp.heading()->set(0.0, Units::DEGREES);
vp.pitch()->set(-90.0, Units::DEGREES);
vp.range()->set(safeNode->getBound().radius()*2.0, Units::METERS);
vp.positionOffset()->set(0, 0, 0);
setHomeViewpoint(vp);
}
}
if (!_pendingViewpoint.isSet())
{
setViewpoint(_homeViewpoint.get(), _homeViewpointDuration);
}
else
{
setViewpoint(_pendingViewpoint.get(), _pendingViewpointDuration.as(Units::SECONDS));
}
// Clear out any pending viewpoint.
_pendingViewpoint.unset();
return true;
}
void
EarthManipulator::handleTileAdded(const TileKey& key, osg::Node* graph, TerrainCallbackContext& context)
{
// Only do collision avoidance if it's enabled, we're not tethering and
// we're not in the middle of setting a viewpoint.
if (getSettings()->getTerrainAvoidanceEnabled() &&
!isTethering() &&
!isSettingViewpoint())
{
const GeoPoint& pt = centerMap();
if (key.getExtent().contains(pt.x(), pt.y()))
{
recalculateCenterFromLookVector();
collisionDetect();
}
}
}
bool
EarthManipulator::createLocalCoordFrame(const osg::Vec3d& worldPos, osg::CoordinateFrame& out_frame) const
{
if (_srs.valid())
{
osg::Vec3d mapPos;
_srs->transformFromWorld(worldPos, mapPos);
_srs->createLocalToWorld(mapPos, out_frame);
}
return _srs.valid();
}
void
EarthManipulator::setCenter(const osg::Vec3d& worldPos)
{
_center = worldPos;
createLocalCoordFrame(worldPos, _centerLocalToWorld);
if (_srs.valid())
{
_centerMap.fromWorld(_srs.get(), worldPos);
}
// cache the "last known" focal point height so we can use it as a
// backup if necessary.
_centerHeight = _srs->isGeographic() ? _center.length() : _center.z();
}
void
EarthManipulator::setNode(osg::Node* node)
{
// you can only set the node if it has not already been set, OR if you are setting
// it to NULL. (So to change it, you must first set it to NULL.) This is to prevent
// OSG from overwriting the node after you have already set on manually.
if (node == 0L || !_node.valid())
{
_node = node;
_mapNode = 0L;
_srs = 0L;
reinitialize();
established();
}
}
osg::Node*
EarthManipulator::getNode()
{
return _node.get();
}
osg::Matrixd
EarthManipulator::getRotation(const osg::Vec3d& point) const
{
//The look vector will be going directly from the eye point to the point on the earth,
//so the look vector is simply the up vector at the center point
osg::CoordinateFrame cf;
createLocalCoordFrame(point, cf);
osg::Vec3d lookVector = -getUpVector(cf);
osg::Vec3d side;
//Force the side vector to be orthogonal to north
osg::Vec3d worldUp(0, 0, 1);
double dot = osg::absolute(worldUp * lookVector);
if (osg::equivalent(dot, 1.0))
{
//We are looking nearly straight down the up vector, so use the Y vector for world up instead
worldUp = osg::Vec3d(0, 1, 0);
}
side = lookVector ^ worldUp;
osg::Vec3d up = side ^ lookVector;
up.normalize();
//We want a very slight offset
double offset = 1e-6;
return osg::Matrixd::lookAt(point - (lookVector * offset), point, up);
}
osg::Quat
EarthManipulator::computeCenterRotation(const osg::Vec3d& point) const
{
return getRotation(point).getRotate().inverse();
}
Viewpoint
EarthManipulator::getViewpoint() const
{
Viewpoint vp;
// Tethering? Use the tether viewpoint.
if (isTethering() && _setVP1.isSet())
{
vp = _setVP1.get();
osg::ref_ptr<osg::Node> node = new osg::Node;
if (vp.getNode(node))
vp.focalPoint()->fromWorld(_srs.get(), computeWorld(node));
else
vp.focalPoint().unset();
}
// Transitioning? Capture the last calculated intermediate position.
else if (isSettingViewpoint())
{
vp.focalPoint()->fromWorld(_srs.get(), _center);
}
// If we are stationary:
else
{
vp.focalPoint()->fromWorld(_srs.get(), _center);
}
// Always update the local offsets.
double localAzim, localPitch;
getEulerAngles(_rotation, &localAzim, &localPitch);
vp.heading() = Angle(localAzim, Units::RADIANS).to(Units::DEGREES);
vp.pitch() = Angle(localPitch, Units::RADIANS).to(Units::DEGREES);
vp.range()->set(_distance, Units::METERS);
if (_posOffset.x() != 0.0 || _posOffset.y() != 0.0 || _posOffset.z() != 0.0)
{
vp.positionOffset()->set(_posOffset);
}
return vp;
}
void
EarthManipulator::setViewpoint(const Viewpoint& vp, double duration_seconds)
{
// If the manip is not set up, save the viewpoint for later.
if (!established())
{
_pendingViewpoint = vp;
_pendingViewpointDuration.set(duration_seconds, Units::SECONDS);
}
else
{
// Save any existing tether node so we can properly invoke the callback.
osg::ref_ptr<osg::Node> oldEndNode;
if (isTethering() && _tetherCallback.valid())
_setVP1->getNode(oldEndNode);
// starting viewpoint; all fields will be set:
_setVP0 = getViewpoint();
// ending viewpoint
_setVP1 = vp;
// Reset the tethering offset quat.
_tetherRotationVP0 = _tetherRotation;
_tetherRotationVP1 = osg::Quat();
// Fill in any missing end-point data with defaults matching the current camera setup.
// Then all fields are guaranteed to contain usable data during transition.
double defPitch, defAzim;
getEulerAngles(_rotation, &defAzim, &defPitch);
if (!_setVP1->heading().isSet())
_setVP1->heading() = Angle(defAzim, Units::RADIANS);
if (!_setVP1->pitch().isSet())
_setVP1->pitch() = Angle(defPitch, Units::RADIANS);
if (!_setVP1->range().isSet())
_setVP1->range() = Distance(_distance, Units::METERS);
if (!_setVP1->nodeIsSet() && !_setVP1->focalPoint().isSet())
{
osg::ref_ptr<osg::Node> safeNode;
if (_setVP0->getNode(safeNode))
_setVP1->setNode(safeNode.get());
else
_setVP1->focalPoint() = _setVP0->focalPoint().get();
}
_setVPDuration.set(osg::maximum(duration_seconds, 0.0), Units::SECONDS);
OE_DEBUG << LC << "setViewpoint:\n"
<< " from " << _setVP0->toString() << "\n"
<< " to " << _setVP1->toString() << "\n";
// access the new tether node if it exists:
osg::ref_ptr<osg::Node> endNode;
_setVP1->getNode(endNode);
// Timed transition, we need to calculate some things:
if (duration_seconds > 0.0)
{
// Start point is the current manipulator center:
osg::Vec3d startWorld;
osg::ref_ptr<osg::Node> startNode;
startWorld = _setVP0->getNode(startNode) ? computeWorld(startNode.get()) : _center;
_setVPStartTime.unset();
// End point is the world coordinates of the target viewpoint:
osg::Vec3d endWorld;
if (endNode.valid())
endWorld = computeWorld(endNode.get());
else
_setVP1->focalPoint()->transform(_srs.get()).toWorld(endWorld);
// calculate an acceleration factor based on the Z differential.
_setVPArcHeight = 0.0;
double range0 = _setVP0->range()->as(Units::METERS);
double range1 = _setVP1->range()->as(Units::METERS);
double pitch0 = _setVP0->pitch()->as(Units::RADIANS);
double pitch1 = _setVP1->pitch()->as(Units::RADIANS);
double h0 = range0 * sin(-pitch0);
double h1 = range1 * sin(-pitch1);
double dh = (h1 - h0);
// calculate the total distance the focal point will travel and derive an arc height:
double de = (endWorld - startWorld).length();
// maximum height during viewpoint transition
if (_settings->getArcViewpointTransitions())
{
_setVPArcHeight = osg::maximum(de - fabs(dh), 0.0);
}
// calculate acceleration coefficients
if (_setVPArcHeight > 0.0)
{
// if we're arcing, we need separate coefficients for the up and down stages
double h_apex = 2.0*(h0 + h1) + _setVPArcHeight;
double dh2_up = fabs(h_apex - h0) / 100000.0;
_setVPAccel = log10(dh2_up);
double dh2_down = fabs(h_apex - h1) / 100000.0;
_setVPAccel2 = -log10(dh2_down);
}
else
{
// on arc => simple unidirectional acceleration:
double dh2 = (h1 - h0) / 100000.0;
_setVPAccel = fabs(dh2) <= 1.0 ? 0.0 : dh2 > 0.0 ? log10(dh2) : -log10(-dh2);
if (fabs(_setVPAccel) < 1.0) _setVPAccel = 0.0;
}
// Adjust the duration if necessary.
if (_settings->getAutoViewpointDurationEnabled())
{
double maxDistance = _srs->getEllipsoid()->getRadiusEquator();
double ratio = osg::clampBetween(de / maxDistance, 0.0, 1.0);
ratio = accelerationInterp(ratio, -4.5);
double minDur, maxDur;
_settings->getAutoViewpointDurationLimits(minDur, maxDur);
_setVPDuration.set(minDur + ratio * (maxDur - minDur), Units::SECONDS);
}
}
else
{
// Immediate transition? Just do it now.
_setVPStartTime->set(_time_s_now, Units::SECONDS);
setViewpointFrame(_time_s_now);
}
// Fire a tether callback if required.
if (_tetherCallback.valid())
{
// starting a tether to a NEW node:
if (isTethering() && oldEndNode.get() != endNode.get())
(*_tetherCallback)(endNode.get());
// breaking a tether:
else if (!isTethering() && oldEndNode.valid())
(*_tetherCallback)(0L);
}
}
// reset other global state flags.
_thrown = false;
_task->_type = TASK_NONE;
}
// returns "t" [0..1], the interpolation coefficient.
double
EarthManipulator::setViewpointFrame(double time_s)
{
if (!_setVPStartTime.isSet())
{
_setVPStartTime->set(time_s, Units::SECONDS);
return 0.0;
}
else
{
// Start point is the current manipulator center:
osg::Vec3d startWorld;
osg::ref_ptr<osg::Node> startNode;
if (_setVP0->getNode(startNode))
startWorld = computeWorld(startNode.get());
else
_setVP0->focalPoint()->transform(_srs.get()).toWorld(startWorld);
// End point is the world coordinates of the target viewpoint:
osg::Vec3d endWorld;
osg::ref_ptr<osg::Node> endNode;
if (_setVP1->getNode(endNode))
endWorld = computeWorld(endNode.get());
else
_setVP1->focalPoint()->transform(_srs.get()).toWorld(endWorld);
// Remaining time is the full duration minus the time since initiation:
double elapsed = time_s - _setVPStartTime->as(Units::SECONDS);
double duration = _setVPDuration.as(Units::SECONDS);
double t = osg::minimum(1.0, duration > 0.0 ? elapsed / duration : 1.0);
double tp = t;
if (_setVPArcHeight > 0.0)
{
if (tp <= 0.5)
{
double t2 = 2.0*tp;
tp = 0.5*t2;
}
else
{
double t2 = 2.0*(tp - 0.5);
tp = 0.5 + (0.5*t2);
}
// the more smoothsteps you do, the more pronounced the fade-in/out effect
tp = smoothStepInterp(tp);
}
else if (t > 0.0)
{
tp = smoothStepInterp(tp);
}
osg::Vec3d newCenter =
_srs->isGeographic() ? nlerp(startWorld, endWorld, tp) : lerp(startWorld, endWorld, tp);
// Calculate the delta-heading, and make sure we are going in the shortest direction:
Angle d_azim = _setVP1->heading().get() - _setVP0->heading().get();
if (d_azim.as(Units::RADIANS) > osg::PI)
d_azim = d_azim - Angle(2.0*osg::PI, Units::RADIANS);
else if (d_azim.as(Units::RADIANS) < -osg::PI)
d_azim = d_azim + Angle(2.0*osg::PI, Units::RADIANS);
double newAzim = _setVP0->heading()->as(Units::RADIANS) + tp * d_azim.as(Units::RADIANS);
// Calculate the new pitch:
Angle d_pitch = _setVP1->pitch().get() - _setVP0->pitch().get();
double newPitch = _setVP0->pitch()->as(Units::RADIANS) + tp * d_pitch.as(Units::RADIANS);
// Calculate the new range:
Distance d_range = _setVP1->range().get() - _setVP0->range().get();
double newRange =
_setVP0->range()->as(Units::METERS) +
d_range.as(Units::METERS)*tp + sin(osg::PI*tp)*_setVPArcHeight;
// Calculate the offsets
osg::Vec3d offset0 = _setVP0->positionOffset().getOrUse(osg::Vec3d(0, 0, 0));
osg::Vec3d offset1 = _setVP1->positionOffset().getOrUse(osg::Vec3d(0, 0, 0));
osg::Vec3d newOffset = offset0 + (offset1 - offset0)*tp;
// Activate.
setLookAt(newCenter, newAzim, newPitch, newRange, newOffset);
// interpolate tether rotation:
_tetherRotation.slerp(tp, _tetherRotationVP0, _tetherRotationVP1);
// At t=1 the transition is complete.
if (t >= 1.0)
{
_setVP0.unset();
// If this was a transition into a tether, keep the endpoint around so we can
// continue tracking it.
if (!isTethering())
{
_setVP1.unset();
}
}
return tp;
}
}
void
EarthManipulator::setLookAt(const osg::Vec3d& center,
double azim,
double pitch,
double range,
const osg::Vec3d& posOffset)
{
setCenter(center);
setDistance(range);
_previousUp = getUpVector(_centerLocalToWorld);
_centerRotation = computeCenterRotation(center);
_posOffset = posOffset;
azim = normalizeAzimRad(azim);
pitch = osg::clampBetween(
pitch,
osg::DegreesToRadians(_settings->getMinPitch()),
osg::DegreesToRadians(_settings->getMaxPitch()));
_rotation = getQuaternion(azim, pitch);
}
void
EarthManipulator::resetLookAt()
{
double pitch;
getEulerAngles(_rotation, 0L, &pitch);
double maxPitch = osg::DegreesToRadians(-10.0);
if (pitch > maxPitch)
rotate(0.0, -(pitch - maxPitch));
osg::Vec3d eye = getWorldMatrix().getTrans();
// calculate the center point in front of the eye. The reference frame here
// is the view plane of the camera.
osg::Matrix m(_rotation * _centerRotation);
recalculateCenter(m);
double newDistance = (eye - _center).length();
setDistance(newDistance);
_posOffset.set(0, 0, 0);
_viewOffset.set(0, 0);
_tetherRotation = osg::Quat();
_tetherRotationVP0 = osg::Quat();
_tetherRotationVP1 = osg::Quat();
}
bool
EarthManipulator::isSettingViewpoint() const
{
return _setVP0.isSet() && _setVP1.isSet();
}
void
EarthManipulator::clearViewpoint()
{
bool breakingTether = isTethering();
// Cancel any ongoing transition or tethering:
_setVP0.unset();
_setVP1.unset();
// Restore the matrix values in a neutral state.
recalculateCenterFromLookVector();
//resetLookAt();
// Fire the callback to indicate a tethering break.
if (_tetherCallback.valid() && breakingTether)
(*_tetherCallback)(0L);
}
bool
EarthManipulator::isTethering() const
{
// True if setViewpoint() was called and the viewpoint has a node.
return _setVP1.isSet() && _setVP1->nodeIsSet();
}
void EarthManipulator::collisionDetect()
{
if (!getSettings()->getTerrainAvoidanceEnabled() ||
!_srs.valid())
{
return;
}
// The camera has changed, so make sure we aren't under the ground.
osg::Vec3d eye = getWorldMatrix().getTrans();
osg::CoordinateFrame eyeCoordFrame;
createLocalCoordFrame(eye, eyeCoordFrame);
osg::Vec3d eyeUp = getUpVector(eyeCoordFrame);
// Try to intersect the terrain with a vector going straight up and down.
double r = osg::minimum(_srs->getEllipsoid()->getRadiusEquator(), _srs->getEllipsoid()->getRadiusPolar());
osg::Vec3d ip, normal;
if (intersect(eye + eyeUp * r, eye - eyeUp * r, ip, normal))
{
double eps = _settings->getTerrainAvoidanceMinimumDistance();
// Now determine if the point is above the ground or not
osg::Vec3d v0 = eyeUp;
v0.normalize();
osg::Vec3d v1 = eye - (ip + eyeUp * eps);
v1.normalize();
// save rotation so we can restore it later - the setraw method
// may alter it and we don't want that.
osg::Quat rotation = _rotation;
//osg::Vec3d adjVector = normal;
osg::Vec3d adjVector = eyeUp;
if (v0 * v1 <= 0)
{
setByLookAtRaw(ip + adjVector * eps, _center, eyeUp);
_rotation = rotation;
}
//OE_INFO << "hit at " << ip.x() << ", " << ip.y() << ", " << ip.z() << "\n";
}
}
bool
EarthManipulator::intersect(const osg::Vec3d& start, const osg::Vec3d& end, osg::Vec3d& intersection, osg::Vec3d& normal) const
{
osg::ref_ptr<MapNode> mapNode;
if (_mapNode.lock(mapNode) && mapNode->getTerrainEngine())
{
osg::ref_ptr<osgUtil::LineSegmentIntersector> lsi = NULL;
lsi = new osgUtil::LineSegmentIntersector(start, end);
osgUtil::IntersectionVisitor iv(lsi.get());
iv.setTraversalMask(_intersectTraversalMask);
mapNode->getTerrainEngine()->accept(iv);
if (lsi->containsIntersections())
{
intersection = lsi->getIntersections().begin()->getWorldIntersectPoint();
normal = lsi->getIntersections().begin()->getWorldIntersectNormal();
return true;
}
}
return false;
}
bool
EarthManipulator::intersectLookVector(osg::Vec3d& out_eye,
osg::Vec3d& out_target,
osg::Vec3d& out_up) const
{
bool success = false;
osg::ref_ptr<MapNode> mapNode;
if (_mapNode.lock(mapNode) && mapNode->getTerrainEngine())
{
double R = _centerHeight;
getWorldInverseMatrix().getLookAt(out_eye, out_target, out_up, 1.0);
osg::Vec3d look = out_target - out_eye;
osg::ref_ptr<osgUtil::LineSegmentIntersector> lsi =
new osgUtil::LineSegmentIntersector(out_eye, out_eye + look * 1e8);
lsi->setIntersectionLimit(lsi->LIMIT_NEAREST);
osgUtil::IntersectionVisitor iv(lsi.get());
iv.setTraversalMask(_intersectTraversalMask);
mapNode->getTerrainEngine()->accept(iv);
if (lsi->containsIntersections())
{
out_target = lsi->getIntersections().begin()->getWorldIntersectPoint();
if (!_srs->isGeographic() || GeoMath::isPointVisible(out_eye, out_target, R))
{
success = true;
}
}
if (!success)
{
// backup plan: intersect spheroid (if geocentric) or base plane (if projected)
if (_srs->isGeographic())
{
osg::Vec3d i0, i1;
unsigned hits = GeoMath::interesectLineWithSphere(out_eye, out_eye + look * 1e8, R, i0, i1);
if (hits > 0)
{
// Need to check not only for intersection, but also visibility
// over the horizon.
// one hit? look vec is tangent to sphere, or camera is underground
if (hits == 1 && GeoMath::isPointVisible(out_eye, i0, R))
{
out_target = i0;
success = true;
}
// two hits? look vec intersects sphere (in two places)
else if (hits == 2)
{
// select the closest hit
out_target = (out_eye - i0).length2() < (out_eye - i1).length2() ? i0 : i1;
if (GeoMath::isPointVisible(out_eye, out_target, R))
{
success = true;
}
}
}
}
else // !_srs->isGeographic()
{
osg::Vec3d i0;
osg::Plane zup(0, 0, 1, 0);
unsigned hits = GeoMath::intersectLineWithPlane(out_eye, out_eye + look, zup, i0);
if (hits > 0)
{
out_target = i0;
success = true;
}
}
}
}
return success;
}
void
EarthManipulator::home(double unused)
{
handleAction(ACTION_HOME, 0, 0, 0);
}
void
EarthManipulator::home(const osgGA::GUIEventAdapter&, osgGA::GUIActionAdapter& us)
{
home(0.0);
us.requestRedraw();
}
void
EarthManipulator::computeHomePosition()
{
if (getNode())
{
const osg::BoundingSphere& boundingSphere = getNode()->getBound();
osg::Vec3d eye =
boundingSphere._center +
osg::Vec3(0.0, -3.5f * boundingSphere._radius, boundingSphere._radius * 0.0001);
setHomePosition(
eye,
boundingSphere._center,
osg::Vec3d(0, 0, 1),
_autoComputeHomePosition);
}
}
void
EarthManipulator::init(const osgGA::GUIEventAdapter&, osgGA::GUIActionAdapter&)
{
flushMouseEventStack();
}
void
EarthManipulator::getUsage(osg::ApplicationUsage& usage) const
{
}
void
EarthManipulator::resetMouse(osgGA::GUIActionAdapter& aa, bool flushEventStack)
{
if (flushEventStack)
flushMouseEventStack();
aa.requestContinuousUpdate(false);
_thrown = false;
_continuous = false;
_single_axis_x = 1.0;
_single_axis_y = 1.0;
_lastPointOnEarth.set(0.0, 0.0, 0.0);
}
// Camera updates get called AFTER the scene gets its update traversal. So, if you have
// tethering enabled (or some other feature that tracks scene graph nodes), this will
// update the camera after the scene graph. This is important in order to maintain
// frame coherency and prevent "jitter".
//
// The reason we install/uninstall instead of just leaving it there is so we can
// support OSG's "ON_DEMAND" frame scheme, which disables itself is there are any
// update callbacks in the scene graph.
void
EarthManipulator::updateProjection(osg::Camera* eventCamera)
{
// check to see if we need to install a new camera callback:
if (eventCamera)
{
// update the projection matrix if necessary
osg::Viewport* vp = eventCamera->getViewport();
if (vp)
{
const osg::Matrixd& proj = eventCamera->getProjectionMatrix();
bool isOrtho = osg::equivalent(proj(3, 3), 1.0);
// For a perspective camera, remember the last known VFOV. We will need it if we
// detect a switch to orthographic.
if (!isOrtho)
{
double vfov, ar, zn, zf;
if (eventCamera->getProjectionMatrixAsPerspective(vfov, ar, zn, zf))
{
_lastKnownVFOV = vfov;
}
}
// For an orthographic camera, convert the distance and remembered VFOV
// into proper x/y extents to simulate "zoom".
else if (_settings->getOrthoTracksPerspective())
{
// need to update the ortho projection matrix to reflect the camera distance.
double ar = vp->width() / vp->height();
double y = _distance * tan(0.5*osg::DegreesToRadians(_lastKnownVFOV));
double x = y * ar;
#if 0 // TODO: derive the pixel offsets and re-instate them.
// apply the offsets:
double px = 0.0, py = 0.0;
const osg::Vec2s& p = _settings->getCameraFrustumOffsets();
if (p.x() != 0 || p.y() != 0)
{
px = (2.0*x*(double)-p.x()) / (double)vp->width();
py = (2.0*y*(double)-p.y()) / (double)vp->height();
}
double ignore, N, F;
proj.getOrtho(ignore, ignore, ignore, ignore, N, F);
eventCamera->setProjectionMatrixAsOrtho(px - x, px + x, py - y, py + y, N, F);
#else
double ignore, N, F;
proj.getOrtho(ignore, ignore, ignore, ignore, N, F);
eventCamera->setProjectionMatrixAsOrtho(-x, +x, -y, +y, N, F);
#endif
OE_DEBUG << "ORTHO: "
<< "ar = " << ar << ", width=" << vp->width() << ", height=" << vp->height()
<< ", dist = " << _distance << ", vfov=" << _lastKnownVFOV
<< ", X = " << x << ", Y = " << y
<< std::endl;
}
}
}
}
bool
EarthManipulator::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
bool handled = false;
// first order of business: make sure the CSN is established.
if (!established())
return false;
osg::View* view = aa.asView();
//double time_s_now = osg::Timer::instance()->time_s();
_time_s_now = view->getFrameStamp()->getReferenceTime();
if (ea.getEventType() == osgGA::GUIEventAdapter::FRAME)
{
if (_node.valid())
{
// Update the mapnode reference frame. This is the transformation
// between the Camera and the MapNode. Since all computations are done
// in map-world space (e.g., ECEF) we need to then introduce this transform
// at the very end (in getInverseMatrix()) to apply the final view matrix.
{
osg::ref_ptr<MapNode> mapNode;
if (_mapNode.lock(mapNode))
{
_mapNodeFrame = osg::computeLocalToWorld(mapNode->getParentalNodePaths()[0]);
_mapNodeFrameInverse.invert(_mapNodeFrame);
}
}
if (_pendingViewpoint.isSet())
{
setViewpoint(_pendingViewpoint.get(), _pendingViewpointDuration.as(Units::SECONDS));
_pendingViewpoint.unset();
aa.requestRedraw();
}
else if (isSettingViewpoint() && !isTethering())
{
if (_frameCount < 2)
_setVPStartTime->set(_time_s_now, Units::SECONDS);
}
if (_thrown)
{
double decayFactor = 1.0 - _settings->getThrowDecayRate();
_throw_dx = osg::absolute(_throw_dx) > osg::absolute(_dx * 0.01) ? _throw_dx * decayFactor : 0.0;
_throw_dy = osg::absolute(_throw_dy) > osg::absolute(_dy * 0.01) ? _throw_dy * decayFactor : 0.0;
if (_throw_dx == 0.0 && _throw_dy == 0.0)
_thrown = false;
else
handleMovementAction(_last_action._type, _throw_dx, _throw_dy, aa.asView());
}
aa.requestContinuousUpdate(isSettingViewpoint() || _thrown || _pushed);
if (_continuous)
{
handleContinuousAction(_last_action, aa.asView());
aa.requestRedraw();
}
else
{
_continuous_dx = 0.0;
_continuous_dy = 0.0;
}
if (_task.valid() && _task->_type != TASK_NONE)
{
bool stillRunning = serviceTask();
if (stillRunning)
{
aa.requestContinuousUpdate(true);
}
else
{
// turn off the continuous, but we still need one last redraw
// to process the final state.
aa.requestContinuousUpdate(false);
aa.requestRedraw();
}
}
}
_frameCount++;
return false;
}
// the camera manipulator runs last after any other event handlers. So bail out
// if the incoming event has already been handled by another handler.
if (ea.getHandled())
{
return false;
}
// form the current Action based on the event type:
Action action = ACTION_NULL;
// if tethering is active, check to see whether the incoming event
// will break the tether.
if (isTethering() && ea.getEventType() != ea.FRAME)
{
const ActionTypeVector& atv = _settings->getBreakTetherActions();
if (atv.size() > 0)
{
EventType eventType = (EventType)ea.getEventType();
int buttonMask = ea.getButtonMask();
int modKeyMask = ea.getModKeyMask();
if (eventType == osgGA::GUIEventAdapter::RELEASE && isMouseClick(&ea))
{
eventType = EVENT_MOUSE_CLICK;
buttonMask = _mouse_down_event->getButtonMask();
modKeyMask = _mouse_down_event->getModKeyMask();
}
const Action& action = _settings->getAction(eventType, buttonMask, modKeyMask);
if (std::find(atv.begin(), atv.end(), action._type) != atv.end())
{
clearViewpoint();
}
}
}
if (ea.isMultiTouchEvent())
{
// not a mouse event; clear the mouse queue.
resetMouse(aa, false);
// queue up a touch event set and figure out the current state:
addTouchEvents(ea);
TouchEvents te;
if (parseTouchEvents(te))
{
for (TouchEvents::iterator i = te.begin(); i != te.end(); ++i)
{
action = _settings->getAction(i->_eventType, i->_mbmask, 0);
if (action._type != ACTION_NULL)
{
_last_event = i->_eventType;
// here we adjust for action scale, global sensitivy
double dx = i->_dx, dy = i->_dy;
dx *= _settings->getMouseSensitivity();
dy *= _settings->getMouseSensitivity();
applyOptionsToDeltas(action, dx, dy);
_dx = dx;
_dy = dy;
if (action._type == ACTION_GOTO)
handlePointAction(action, ea.getX(), ea.getY(), view);
else
handleMovementAction(action._type, dx, dy, view);
aa.requestRedraw();
}
}
handled = true;
}
else
{
// The only multitouch event we want passed on if not handled is a release
handled = ea.getEventType() != osgGA::GUIEventAdapter::RELEASE;
// if a new push occurs we want to reset the dx/dy values to stop/prevent throwing
if (ea.getEventType() == osgGA::GUIEventAdapter::PUSH)
_dx = _dy = 0.0;
}
}
if (!handled)
{
// not a touch event; clear the touch queue.
//_touchPointQueue.clear();
switch (ea.getEventType())
{
case osgGA::GUIEventAdapter::PUSH:
_pushed = true;
resetMouse(aa);
addMouseEvent(ea);
_mouse_down_event = &ea;
aa.requestRedraw();
handled = true;
break;
case osgGA::GUIEventAdapter::RELEASE:
_pushed = false;
if (_continuous)
{
// bail out of continuous mode if necessary:
_continuous = false;
aa.requestContinuousUpdate(false);
}
else
{
action = _last_action;
_throw_dx = fabs(_dx) > 0.01 ? _dx : 0.0;
_throw_dy = fabs(_dy) > 0.01 ? _dy : 0.0;
if (_settings->getThrowingEnabled() && (_time_s_now - _time_s_last_event < 0.05) && (_throw_dx != 0.0 || _throw_dy != 0.0))
{
_thrown = true;
aa.requestRedraw();
aa.requestContinuousUpdate(true);
}
else if (isMouseClick(&ea))
{
addMouseEvent(ea);
if (_mouse_down_event)
{
action = _settings->getAction(EVENT_MOUSE_CLICK, _mouse_down_event->getButtonMask(), _mouse_down_event->getModKeyMask());
if (handlePointAction(action, ea.getX(), ea.getY(), aa.asView()))
aa.requestRedraw();
}
resetMouse(aa);
}
else
{
resetMouse(aa);
addMouseEvent(ea);
}
}
handled = true;
//拖动三维地球之后,二维地图同时也跟着一块移动
if (_earthMap != nullptr) {
_earthMap->get3DEarthCenter();
}
break;
case osgGA::GUIEventAdapter::DOUBLECLICK:
// bail out of continuous mode if necessary:
_continuous = false;
_pushed = false;
addMouseEvent(ea);
if (_mouse_down_event)
{
action = _settings->getAction(ea.getEventType(), _mouse_down_event->getButtonMask(), _mouse_down_event->getModKeyMask());
if (handlePointAction(action, ea.getX(), ea.getY(), aa.asView()))
aa.requestRedraw();
resetMouse(aa);
handled = true;
}
break;
case osgGA::GUIEventAdapter::MOVE: // MOVE not currently bindable
//NOP
break;
case osgGA::GUIEventAdapter::DRAG:
{
if (_pushed)
{
action = _settings->getAction(ea.getEventType(), ea.getButtonMask(), ea.getModKeyMask());
addMouseEvent(ea);
bool wasContinuous = _continuous;
_continuous = action.getBoolOption(OPTION_CONTINUOUS, false);
if (handleMouseAction(action, aa.asView()))
aa.requestRedraw();
if (_continuous && !wasContinuous)
_last_continuous_action_time = _time_s_now;
aa.requestContinuousUpdate(_continuous);
_thrown = false;
handled = true;
}
}
break;
case osgGA::GUIEventAdapter::KEYDOWN:
if (ea.getKey() < osgGA::GUIEventAdapter::KEY_Shift_L)
{
resetMouse(aa);
action = _settings->getAction(ea.getEventType(), ea.getKey(), ea.getModKeyMask());
if (handleKeyboardAction(action))
aa.requestRedraw();
handled = true;
}
break;
case osgGA::GUIEventAdapter::KEYUP:
resetMouse(aa);
_task->_type = TASK_NONE;
handled = true;
break;
case osgGA::GUIEventAdapter::SCROLL:
resetMouse(aa);
addMouseEvent(ea);
action = _settings->getAction(ea.getEventType(), ea.getScrollingMotion(), ea.getModKeyMask());
if (handleScrollAction(action, action.getDoubleOption(OPTION_DURATION, 0.2)))
aa.requestRedraw();
handled = true;
break;
default: break;
}
}
// if a new task was started, request continuous updates.
if (_task.valid() && _task->_type != TASK_NONE)
{
aa.requestContinuousUpdate(true);
}
if (handled && action._type != ACTION_NULL)
{
_last_action = action;
_time_s_last_event = _time_s_now;
}
return handled;
}
namespace
{
/// Helper class for generating NodePathList.
class CollectAllParentPaths : public osg::NodeVisitor
{
public:
CollectAllParentPaths(const osg::Node* haltTraversalAtNode = 0) :
osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_PARENTS),
_haltTraversalAtNode(haltTraversalAtNode)
{
// This is the same as the osg::CollectParentPaths visitor (which is not exported)
// except for this node mask override to ensure that it will even traverse nodes hidden via node mask.
setNodeMaskOverride(~0);
}
virtual void apply(osg::Node& node)
{
if (node.getNumParents() == 0 || &node == _haltTraversalAtNode)
{
_nodePaths.push_back(getNodePath());
}
else
{
traverse(node);
}
}
const osg::Node* _haltTraversalAtNode;
osg::NodePath _nodePath;
osg::NodePathList _nodePaths;
};
}
osg::NodePathList getAllParentalNodePaths(osg::Node* node, osg::Node* haltTraversalAtNode = 0)
{
CollectAllParentPaths cpp(haltTraversalAtNode);
node->accept(cpp);
return cpp._nodePaths;
}
void
EarthManipulator::updateTether(double t)
{
// Initial transition is complete, so update the camera for tether.
osg::ref_ptr<osg::Node> node;
if (_setVP1->getNode(node))
{
// We use our getAllParentalNodePaths function instead of tether_node->getParentalNodePaths() so that we can
// ensure even nodes hidden via a node mask are traversed.
// Establish the reference frame for the target node. If this fails, bail out.
osg::Matrix L2W;
osg::NodePathList nodePaths = getAllParentalNodePaths(node.get());
if (nodePaths.empty())
return;
osg::ref_ptr<osg::NodeVisitor> nv;
_updateCameraNodeVisitor.lock(nv);
ComputeLocalToWorld computeL2W(nv.get());
computeL2W.accumulate(nodePaths[0]);
L2W = computeL2W._matrix;
if (!L2W.valid())
return;
// If we just called setViewpointFrame, no need to calculate the center again.
if (!isSettingViewpoint())
{
setCenter(osg::Vec3d(0, 0, 0) * L2W);
_centerRotation = computeCenterRotation(_center);
_previousUp = getUpVector(_centerLocalToWorld);
};
if (_settings->getTetherMode() == TETHER_CENTER)
{
if (_lastTetherMode == TETHER_CENTER_AND_ROTATION)
{
// level out the camera so we don't leave the camera is weird state.
osg::Matrixd localToFrame(L2W*osg::Matrixd::inverse(_centerLocalToWorld));
double azim = atan2(-localToFrame(0, 1), localToFrame(0, 0));
_tetherRotation.makeRotate(-azim, 0.0, 0.0, 1.0);
}
}
else
{
// remove any scaling introduced by the model
double sx = 1.0 / sqrt(L2W(0, 0)*L2W(0, 0) + L2W(1, 0)*L2W(1, 0) + L2W(2, 0)*L2W(2, 0));
double sy = 1.0 / sqrt(L2W(0, 1)*L2W(0, 1) + L2W(1, 1)*L2W(1, 1) + L2W(2, 1)*L2W(2, 1));
double sz = 1.0 / sqrt(L2W(0, 2)*L2W(0, 2) + L2W(1, 2)*L2W(1, 2) + L2W(2, 2)*L2W(2, 2));
L2W = L2W * osg::Matrixd::scale(sx, sy, sz);
if (_settings->getTetherMode() == TETHER_CENTER_AND_HEADING)
{
// Back out the tetheree's rotation, then discard all but the heading component:
osg::Matrixd localToFrame(L2W*osg::Matrixd::inverse(_centerLocalToWorld));
double azim = atan2(-localToFrame(0, 1), localToFrame(0, 0));
osg::Quat finalTetherRotation;
finalTetherRotation.makeRotate(-azim, 0.0, 0.0, 1.0);
_tetherRotation.slerp(t, _tetherRotationVP0, finalTetherRotation);
}
// Track all rotations
else if (_settings->getTetherMode() == TETHER_CENTER_AND_ROTATION)
{
osg::Quat finalTetherRotation;
finalTetherRotation = L2W.getRotate() * _centerRotation.inverse();
_tetherRotation.slerp(t, _tetherRotationVP0, finalTetherRotation);
}
}
_lastTetherMode = _settings->getTetherMode();
}
}
bool
EarthManipulator::serviceTask()
{
if (_task.valid() && _task->_type != TASK_NONE)
{
double dt = _time_s_now - _task->_time_last_service;
if (dt > 0.0)
{
// cap the DT so we don't exceed the expected delta.
dt = osg::clampBelow(dt, _task->_duration_s);
switch (_task->_type)
{
case TASK_PAN:
pan(dt * _task->_dx, dt * _task->_dy);
break;
case TASK_ROTATE:
rotate(dt * _task->_dx, dt * _task->_dy);
break;
case TASK_ZOOM:
zoom(dt * _task->_dx, dt * _task->_dy);
break;
default: break;
}
_task->_duration_s -= dt;
_task->_time_last_service = _time_s_now;
if (_task->_duration_s <= 0.0)
{
_task->_type = TASK_NONE;
}
}
}
// returns true if the task is still running.
return _task.valid() && _task->_type != TASK_NONE;
}
bool
EarthManipulator::isMouseClick(const osgGA::GUIEventAdapter* mouse_up_event) const
{
if (mouse_up_event == NULL || _mouse_down_event == NULL) return false;
static const float velocity = 0.1f;
float dx = mouse_up_event->getXnormalized() - _mouse_down_event->getXnormalized();
float dy = mouse_up_event->getYnormalized() - _mouse_down_event->getYnormalized();
float len = sqrtf(dx*dx + dy * dy);
float dt = mouse_up_event->getTime() - _mouse_down_event->getTime();
return len < dt * velocity;
}
void
EarthManipulator::flushMouseEventStack()
{
_ga_t1 = NULL;
_ga_t0 = NULL;
//_touchPointQueue.clear();
}
void
EarthManipulator::addMouseEvent(const osgGA::GUIEventAdapter& ea)
{
_ga_t1 = _ga_t0;
_ga_t0 = &ea;
//_touchPointQueue.clear();
}
void
EarthManipulator::addTouchEvents(const osgGA::GUIEventAdapter& ea)
{
_ga_t1 = _ga_t0;
_ga_t0 = &ea;
// first, push the old event to the back of the queue.
while (_touchPointQueue.size() > 1)
_touchPointQueue.pop_front();
// queue any new events.
if (ea.isMultiTouchEvent())
{
osgGA::GUIEventAdapter::TouchData* data = ea.getTouchData();
_touchPointQueue.push_back(MultiTouchPoint());
MultiTouchPoint& ev = _touchPointQueue.back();
for (unsigned i = 0; i < data->getNumTouchPoints(); ++i)
{
osgGA::GUIEventAdapter::TouchData::TouchPoint tp = data->get(i);
ev.push_back(tp);
}
}
}
bool
EarthManipulator::parseTouchEvents(TouchEvents& output)
{
double sens = this->getSettings()->getTouchSensitivity();
if (_touchPointQueue.size() == 2)
{
if (_touchPointQueue[0].size() == 2 && // two fingers
_touchPointQueue[1].size() == 2) // two fingers
{
MultiTouchPoint& p0 = _touchPointQueue[0];
MultiTouchPoint& p1 = _touchPointQueue[1];
if (p0[0].phase != osgGA::GUIEventAdapter::TOUCH_ENDED &&
p1[0].phase != osgGA::GUIEventAdapter::TOUCH_ENDED &&
p0[1].phase == osgGA::GUIEventAdapter::TOUCH_MOVED &&
p1[1].phase == osgGA::GUIEventAdapter::TOUCH_MOVED)
{
// gather information about what happened:
float dx[2], dy[2];
for (int i = 0; i < 2; ++i)
{
dx[i] = p1[i].x - p0[i].x;
dy[i] = p1[i].y - p0[i].y;
}
osg::Vec2f vec0 = osg::Vec2f(p0[1].x, p0[1].y) - osg::Vec2f(p0[0].x, p0[0].y);
osg::Vec2f vec1 = osg::Vec2f(p1[1].x, p1[1].y) - osg::Vec2f(p1[0].x, p1[0].y);
float deltaDistance = vec1.length() - vec0.length();
float angle[2];
angle[0] = atan2(p0[0].y - p0[1].y, p0[0].x - p0[1].x);
angle[1] = atan2(p1[0].y - p1[1].y, p1[0].x - p1[1].x);
float da = angle[1] - angle[0];
// Threshold in pixels for determining if a two finger drag happened.
float dragThres = 1.0f;
// now see if that corresponds to any touch events:
if (osg::equivalent(vec0.x(), vec1.x(), dragThres) &&
osg::equivalent(vec0.y(), vec1.y(), dragThres))
{
// two-finger drag.
output.push_back(TouchEvent());
TouchEvent& ev = output.back();
ev._eventType = EVENT_MULTI_DRAG;
ev._dx = 0.5 * (dx[0] + dx[1]) * sens;
ev._dy = 0.5 * (dy[0] + dy[1]) * sens;
}
else
{
// otherwise it's a pinch and/or a zoom. You can do them together.
if (fabs(deltaDistance) > (1.0 * 0.0005 / sens))
{
// distance between the fingers changed: a pinch.
output.push_back(TouchEvent());
TouchEvent& ev = output.back();
ev._eventType = EVENT_MULTI_PINCH;
ev._dx = 0.0, ev._dy = deltaDistance * -sens;
}
if (fabs(da) > (0.01 * 0.0005 / sens))
{
// angle between vectors changed: a twist.
output.push_back(TouchEvent());
TouchEvent& ev = output.back();
ev._eventType = EVENT_MULTI_TWIST;
ev._dx = da;
//ev._dy = 0.5 * (dy[0]+dy[1]) * _touch_sens;
ev._dy = 0.0;
}
}
}
}
else if (_touchPointQueue[0].size() >= 1 && // one finger
_touchPointQueue[1].size() >= 1) // one finger
{
MultiTouchPoint& p0 = _touchPointQueue[0];
MultiTouchPoint& p1 = _touchPointQueue[1];
if (p1[0].tapCount == 2)
{
// double tap
output.push_back(TouchEvent());
TouchEvent& ev = output.back();
ev._eventType = EVENT_MOUSE_DOUBLE_CLICK;
ev._mbmask = osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
ev._dx = 0.0;
ev._dy = 0.0;
}
else if ((p0[0].phase != osgGA::GUIEventAdapter::TOUCH_ENDED &&
p1[0].phase == osgGA::GUIEventAdapter::TOUCH_MOVED))
{
output.push_back(TouchEvent());
TouchEvent& ev = output.back();
ev._eventType = EVENT_MOUSE_DRAG;
ev._mbmask = osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON;
ev._dx = (p1[0].x - p0[0].x) * sens;
ev._dy = (p1[0].y - p0[0].y) * sens;
}
}
}
return output.size() > 0;
}
void
EarthManipulator::setByMatrix(const osg::Matrixd& matrix)
{
if (!established())
return;
osg::Vec3d lookVector(-matrix(2, 0), -matrix(2, 1), -matrix(2, 2));
osg::Vec3d eye(matrix(3, 0), matrix(3, 1), matrix(3, 2));
_centerRotation = computeCenterRotation(_center);
osg::ref_ptr<osg::Node> safeNode = _node.get();
if (!safeNode.valid())
{
setCenter(eye + lookVector);
setDistance(lookVector.length());
_rotation = matrix.getRotate().inverse() * _centerRotation.inverse();
return;
}
// need to reintersect with the terrain
const osg::BoundingSphere& bs = safeNode->getBound();
float distance = (eye - bs.center()).length() + safeNode->getBound().radius();
osg::Vec3d start_segment = eye;
osg::Vec3d end_segment = eye + lookVector * distance;
osg::Vec3d ip, normal;
bool hitFound = false;
if (intersect(start_segment, end_segment, ip, normal))
{
setCenter(ip);
_centerRotation = computeCenterRotation(_center);
setDistance((eye - ip).length());
osg::Matrixd rotation_matrix = osg::Matrixd::translate(0.0, 0.0, -_distance)*
matrix*
osg::Matrixd::translate(-_center);
_rotation = rotation_matrix.getRotate() * _centerRotation.inverse();
hitFound = true;
}
if (!hitFound)
{
osg::CoordinateFrame eyeCoordFrame;
createLocalCoordFrame(eye, eyeCoordFrame);
osg::Vec3d eyeUp = getUpVector(eyeCoordFrame);
if (intersect(eye + eyeUp * distance, eye - eyeUp * distance, ip, normal))
{
setCenter(ip);
_centerRotation = computeCenterRotation(_center);
setDistance((eye - ip).length());
_rotation.set(0, 0, 0, 1);
hitFound = true;
}
}
//osg::CoordinateFrame coordinateFrame;
//createLocalCoordFrame( _center, coordinateFrame );
_previousUp = getUpVector(_centerLocalToWorld);
recalculateRoll();
//recalculateLocalPitchAndAzimuth();
collisionDetect();
}
osg::Matrixd
EarthManipulator::getMatrix() const
{
return getWorldMatrix() * _mapNodeFrame;
}
osg::Matrixd
EarthManipulator::getWorldMatrix() const
{
return osg::Matrixd::translate(_viewOffset.x(), _viewOffset.y(), _distance) *
osg::Matrixd::rotate(_rotation) *
osg::Matrixd::rotate(_tetherRotation) *
osg::Matrixd::translate(_posOffset) *
osg::Matrixd::rotate(_centerRotation) *
osg::Matrixd::translate(_center);
}
osg::Matrixd
EarthManipulator::getInverseMatrix() const
{
return _mapNodeFrameInverse * getWorldInverseMatrix();
}
osg::Matrixd
EarthManipulator::getWorldInverseMatrix() const
{
return osg::Matrixd::translate(-_center)*
osg::Matrixd::rotate(_centerRotation.inverse()) *
osg::Matrixd::translate(-_posOffset) *
osg::Matrixd::rotate(_tetherRotation.inverse()) *
osg::Matrixd::rotate(_rotation.inverse()) *
osg::Matrixd::translate(-_viewOffset.x(), -_viewOffset.y(), -_distance);
}
void
EarthManipulator::updateCamera(osg::Camera& camera)
{
// time exactly now
double now = osg::Timer::instance()->time_s();
// interpolation through a setViewpoint, if applicable
double t = 1.0;
// Update a viewpoint transition:
if (isSettingViewpoint())
{
t = setViewpointFrame(now);
}
// Update a camera tethered to a node:
if (isTethering())
{
updateTether(t);
}
// Update an ortho/perspective projection tracking:
updateProjection(&camera);
osgGA::CameraManipulator::updateCamera(camera);
// Invoke the callback once the camera's been udpated for the ensuing render:
if (_updateCameraCallback.valid())
{
_updateCameraCallback->onUpdateCamera(&camera);
}
}
void
EarthManipulator::setUpdateCameraNodeVisitor(osg::NodeVisitor* nv)
{
_updateCameraNodeVisitor = nv;
}
void
EarthManipulator::setByLookAt(const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up)
{
osg::ref_ptr<osg::Node> safeNode;
if (_node.lock(safeNode))
{
// compute rotation matrix
osg::Vec3d lv(center - eye);
setDistance(lv.length());
setCenter(center);
bool hitFound = false;
double distance = lv.length();
double maxDistance = distance + 2 * (eye - safeNode->getBound().center()).length();
osg::Vec3d farPosition = eye + lv * (maxDistance / distance);
osg::Vec3d endPoint = center;
for (int i = 0;
!hitFound && i < 2;
++i, endPoint = farPosition)
{
// compute the intersection with the scene.
osg::Vec3d ip, normal;
if (intersect(eye, endPoint, ip, normal))
{
setCenter(ip);
setDistance((ip - eye).length());
hitFound = true;
}
}
}
// note LookAt = inv(CF)*inv(RM)*inv(T) which is equivalent to:
// inv(R) = CF*LookAt.
osg::Matrixd rotation_matrix = osg::Matrixd::lookAt(eye, center, up);
_centerRotation = computeCenterRotation(_center);// getRotation( _center ).getRotate().inverse();
_rotation = rotation_matrix.getRotate().inverse() * _centerRotation.inverse();
_previousUp = getUpVector(_centerLocalToWorld);
_posOffset.set(0, 0, 0);
_viewOffset.set(0, 0);
recalculateRoll();
}
void
EarthManipulator::setByLookAtRaw(const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up)
{
// compute rotation matrix
osg::Vec3d lv(center - eye);
setDistance(lv.length());
setCenter(center);
_posOffset.set(0, 0, 0);
_viewOffset.set(0, 0);
osg::Matrixd rotation_matrix = osg::Matrixd::lookAt(eye, center, up);
_centerRotation = computeCenterRotation(_center); // getRotation(_center).getRotate().inverse();
_rotation = rotation_matrix.getRotate().inverse() * _centerRotation.inverse();
_previousUp = getUpVector(_centerLocalToWorld);
recalculateRoll();
}
bool
EarthManipulator::recalculateCenterFromLookVector()
{
// just re-applying the lookat parameters will calculate a new coordinate
// frame based on a look-at intersection.
osg::Vec3d eye, target, up;
bool intersected = intersectLookVector(eye, target, up);
if (intersected)
{
setByLookAtRaw(eye, target, up);
}
return intersected;
}
void
EarthManipulator::recalculateCenter(const osg::CoordinateFrame& frame)
{
osg::ref_ptr<osg::Node> node;
if (_node.lock(node))
{
// need to reintersect with the terrain
double ilen = node->getBound().radius()*0.25f;
osg::Vec3d up = getUpVector(frame);
osg::Vec3d ip1;
osg::Vec3d ip2;
osg::Vec3d normal;
bool hit_ip1 = intersect(_center - up * ilen * 0.1, _center + up * ilen, ip1, normal);
bool hit_ip2 = intersect(_center + up * ilen * 0.1, _center - up * ilen, ip2, normal);
if (hit_ip1)
{
if (hit_ip2)
{
setCenter((_center - ip1).length2() < (_center - ip2).length2() ? ip1 : ip2);
}
else
{
setCenter(ip1);
}
}
else if (hit_ip2)
{
setCenter(ip2);
}
}
}
void
EarthManipulator::pan(double dx, double dy)
{
if (!isTethering())
{
// to pan, we need a focus point on the terrain:
if (!recalculateCenterFromLookVector())
return;
double scale = -0.3f*_distance;
osg::Matrixd rotation_matrix;
rotation_matrix.makeRotate(_rotation * _centerRotation);
// compute look vector.
osg::Vec3d lookVector = -getUpVector(rotation_matrix);
osg::Vec3d sideVector = getSideVector(rotation_matrix);
osg::Vec3d upVector = getFrontVector(rotation_matrix);
osg::Vec3d localUp = _previousUp;
osg::Vec3d forwardVector = localUp ^ sideVector;
sideVector = forwardVector ^ localUp;
forwardVector.normalize();
sideVector.normalize();
osg::Vec3d dv = forwardVector * (dy*scale) + sideVector * (dx*scale);
// save the previous CF so we can do azimuth locking:
osg::CoordinateFrame oldCenterLocalToWorld = _centerLocalToWorld;
// move the center point
double len = _center.length();
osg::Vec3d newCenter = _center + dv;
if (_srs->isGeographic())
{
// in geocentric, ensure that it doesn't change length.
newCenter.normalize();
newCenter *= len;
}
setCenter(newCenter);
if (_settings->getLockAzimuthWhilePanning())
{
// in azimuth-lock mode, _centerRotation maintains a consistent north vector
_centerRotation = computeCenterRotation(_center);
}
else
{
// otherwise, we need to rotate _centerRotation manually.
osg::Vec3d new_localUp = getUpVector(_centerLocalToWorld);
osg::Quat pan_rotation;
pan_rotation.makeRotate(localUp, new_localUp);
if (!pan_rotation.zeroRotation())
{
_centerRotation = _centerRotation * pan_rotation;
_previousUp = new_localUp;
}
}
}
else
{
double scale = _distance;
// Panning in tether mode changes the focal view offsets.
_viewOffset.x() -= dx * scale;
_viewOffset.y() -= dy * scale;
//Clamp values within range
_viewOffset.x() = osg::clampBetween(_viewOffset.x(), -_settings->getMaxXOffset(), _settings->getMaxXOffset());
_viewOffset.y() = osg::clampBetween(_viewOffset.y(), -_settings->getMaxYOffset(), _settings->getMaxYOffset());
}
collisionDetect();
}
void
EarthManipulator::rotate(double dx, double dy)
{
// clamp the local pitch delta; never allow the pitch to hit -90.
double minp = osg::DegreesToRadians(osg::clampAbove(_settings->getMinPitch(), -89.9));
double maxp = osg::DegreesToRadians(osg::clampBelow(_settings->getMaxPitch(), 89.9));
#if 0
OE_NOTICE << LC
<< "LocalPitch=" << osg::RadiansToDegrees(_local_pitch)
<< ", dy=" << osg::RadiansToDegrees(dy)
<< ", dy+lp=" << osg::RadiansToDegrees(_local_pitch + dy)
<< ", limits=" << osg::RadiansToDegrees(minp) << "," << osg::RadiansToDegrees(maxp)
<< std::endl;
#endif
// clamp pitch range:
double oldPitch;
getEulerAngles(_rotation, 0L, &oldPitch);
if (dy + oldPitch > maxp || dy + oldPitch < minp)
dy = 0;
osg::Matrix rotation_matrix;
rotation_matrix.makeRotate(_rotation);
osg::Vec3d lookVector = -getUpVector(rotation_matrix);
osg::Vec3d sideVector = getSideVector(rotation_matrix);
osg::Vec3d upVector = getFrontVector(rotation_matrix);
osg::Vec3d localUp(0.0f, 0.0f, 1.0f);
osg::Vec3d forwardVector = localUp ^ sideVector;
sideVector = forwardVector ^ localUp;
forwardVector.normalize();
sideVector.normalize();
osg::Quat rotate_elevation;
rotate_elevation.makeRotate(dy, sideVector);
osg::Quat rotate_azim;
rotate_azim.makeRotate(-dx, localUp);
_rotation = _rotation * rotate_elevation * rotate_azim;
collisionDetect();
}
void
EarthManipulator::zoom(double dx, double dy)
{
// in normal (non-tethered mode) we need a valid zoom point.
if (!isTethering())
{
recalculateCenterFromLookVector();
}
double scale = 1.0f + dy;
setDistance(_distance * scale);
collisionDetect();
}
bool
EarthManipulator::screenToWorld(float x, float y, osg::View* theView, osg::Vec3d& out_coords) const
{
osgViewer::View* view = dynamic_cast<osgViewer::View*>(theView);
if (!view || !view->getCamera())
return false;
osg::ref_ptr<MapNode> mapNode;
if (!_mapNode.lock(mapNode) || !mapNode->getTerrain())
return false;
return mapNode->getTerrain()->getWorldCoordsUnderMouse(view, x, y, out_coords);
}
void
EarthManipulator::setDistance(double distance)
{
_distance = osg::clampBetween(distance, _settings->getMinDistance(), _settings->getMaxDistance());
}
void
EarthManipulator::setInitialVFOV(double vfov)
{
_lastKnownVFOV = vfov;
}
void
EarthManipulator::dumpActionInfo(const EarthManipulator::Action& action, osg::NotifySeverity level) const
{
osgEarth::notify(level) << "action: " << s_actionNames[action._type] << "; options: ";
for (ActionOptions::const_iterator i = action._options.begin(); i != action._options.end(); ++i)
{
const ActionOption& option = *i;
std::string val;
if (s_actionOptionTypes[option.option()] == 0)
val = option.boolValue() ? "true" : "false";
else
val = toString<double>(option.doubleValue());
osgEarth::notify(level)
<< s_actionOptionNames[option.option()] << "=" << val << ", ";
}
osgEarth::notify(level) << std::endl;
}
void
EarthManipulator::handleMovementAction(const ActionType& type, double dx, double dy, osg::View* view)
{
switch (type)
{
case ACTION_PAN:
pan(dx, dy);
break;
case ACTION_ROTATE:
// in "single axis" mode, zero out one of the deltas.
if (_continuous && _settings->getSingleAxisRotation())
{
if (::fabs(dx) > ::fabs(dy))
dy = 0.0;
else
dx = 0.0;
}
rotate(dx, dy);
break;
case ACTION_ZOOM:
zoom(dx, dy);
break;
case ACTION_EARTH_DRAG:
if (_thrown)
pan(dx*0.5, dy*0.5); //TODO: create proper drag throwing instead of panning trick
else
drag(dx, dy, view);
break;
default:break;
}
}
bool
EarthManipulator::handlePointAction(const Action& action, float mx, float my, osg::View* view)
{
//Exit early if the action is null
if (action._type == ACTION_NULL)
return true;
osg::Vec3d point;
if (screenToWorld(mx, my, view, point))
{
switch (action._type)
{
case ACTION_GOTO:
{
Viewpoint here = getViewpoint();
here.focalPoint()->fromWorld(_srs.get(), point);
//osg::Vec3d pointVP;
//here.getSRS()->transformFromWorld(point, pointVP);
//OE_NOTICE << "X=" << pointVP.x() << ", Y=" << pointVP.y() << std::endl;
// here.setFocalPoint( pointVP );
double duration_s = action.getDoubleOption(OPTION_DURATION, 1.0);
double range_factor = action.getDoubleOption(OPTION_GOTO_RANGE_FACTOR, 1.0);
here.range() = here.range().get() * range_factor;
//here.setRange( here.getRange() * range_factor );
setViewpoint(here, duration_s);
}
break;
default:
break;
}
}
return true;
}
void
EarthManipulator::handleContinuousAction(const Action& action, osg::View* view)
{
double t_factor = (_time_s_now - _last_continuous_action_time) / 0.016666666;
_last_continuous_action_time = _time_s_now;
handleMovementAction(action._type, _continuous_dx * t_factor, _continuous_dy * t_factor, view);
}
void
EarthManipulator::applyOptionsToDeltas(const Action& action, double& dx, double& dy)
{
dx *= action.getDoubleOption(OPTION_SCALE_X, 1.0);
dy *= action.getDoubleOption(OPTION_SCALE_Y, 1.0);
if (action.getBoolOption(OPTION_SINGLE_AXIS, false) == true)
{
if (osg::absolute(dx) > osg::absolute(dy))
dy = 0.0;
else
dx = 0.0;
}
}
bool
EarthManipulator::handleMouseAction(const Action& action, osg::View* view)
{
// return if less then two events have been added.
if (_ga_t0.get() == NULL || _ga_t1.get() == NULL) return false;
//if ( osgEarth::getNotifyLevel() > osg::INFO )
// dumpActionInfo( action, osg::DEBUG_INFO );
double dx = _ga_t0->getXnormalized() - _ga_t1->getXnormalized();
double dy = _ga_t0->getYnormalized() - _ga_t1->getYnormalized();
// return if there is no movement.
if (dx == 0 && dy == 0) return false;
// here we adjust for action scale, global sensitivy
dx *= _settings->getMouseSensitivity();
dy *= _settings->getMouseSensitivity();
applyOptionsToDeltas(action, dx, dy);
// in "continuous" mode, we accumulate the deltas each frame - thus
// the deltas act more like speeds.
if (_continuous)
{
_continuous_dx += dx * 0.01;
_continuous_dy += dy * 0.01;
}
else
{
_dx = dx;
_dy = dy;
handleMovementAction(action._type, dx, dy, view);
}
return true;
}
bool
EarthManipulator::handleMouseClickAction(const Action& action)
{
//TODO.
return false;
}
bool
EarthManipulator::handleKeyboardAction(const Action& action, double duration)
{
double dx = 0, dy = 0;
switch (action._dir)
{
case DIR_LEFT: dx = 1; break;
case DIR_RIGHT: dx = -1; break;
case DIR_UP: dy = -1; break;
case DIR_DOWN: dy = 1; break;
default: break;
}
dx *= _settings->getKeyboardSensitivity();
dy *= _settings->getKeyboardSensitivity();
applyOptionsToDeltas(action, dx, dy);
return handleAction(action, dx, dy, duration);
}
bool
EarthManipulator::handleScrollAction(const Action& action, double duration)
{
const double scrollFactor = 1.5;
double dx = 0, dy = 0;
switch (action._dir)
{
case DIR_LEFT: dx = 1; break;
case DIR_RIGHT: dx = -1; break;
case DIR_UP: dy = 1; break;//鼠标滚轮时间
case DIR_DOWN: dy = -1; break;
default: break;
}
dx *= scrollFactor * _settings->getScrollSensitivity();
dy *= scrollFactor * _settings->getScrollSensitivity();
applyOptionsToDeltas(action, dx, dy);
return handleAction(action, dx, dy, duration);
}
bool
EarthManipulator::handleAction(const Action& action, double dx, double dy, double duration)
{
bool handled = true;
//if ( osgEarth::getNotifyLevel() > osg::INFO )
// dumpActionInfo( action, osg::DEBUG_INFO );
//OE_NOTICE << "action=" << action << ", dx=" << dx << ", dy=" << dy << std::endl;
switch (action._type)
{
case ACTION_HOME:
if (_homeViewpoint.isSet())
{
setViewpoint(_homeViewpoint.value(), _homeViewpointDuration);
}
break;
case ACTION_PAN:
case ACTION_PAN_LEFT:
case ACTION_PAN_RIGHT:
case ACTION_PAN_UP:
case ACTION_PAN_DOWN:
_task->set(TASK_PAN, dx, dy, duration, _time_s_now);
break;
case ACTION_ROTATE:
case ACTION_ROTATE_LEFT:
case ACTION_ROTATE_RIGHT:
case ACTION_ROTATE_UP:
case ACTION_ROTATE_DOWN:
_task->set(TASK_ROTATE, dx, dy, duration, _time_s_now);
break;
case ACTION_ZOOM:
case ACTION_ZOOM_IN:
case ACTION_ZOOM_OUT:
_task->set(TASK_ZOOM, dx, dy, duration, _time_s_now);
break;
default:
handled = false;
}
return handled;
}
void
EarthManipulator::recalculateRoll()
{
osg::Matrixd rotation_matrix;
rotation_matrix.makeRotate(_centerRotation);
osg::Vec3d lookVector = -getUpVector(rotation_matrix);
osg::Vec3d upVector = getFrontVector(rotation_matrix);
osg::Vec3d localUp = getUpVector(_centerLocalToWorld);
osg::Vec3d sideVector = lookVector ^ localUp;
if (sideVector.length() < 0.1)
{
//OE_INFO<<"Side vector short "<<sideVector.length()<<std::endl;
sideVector = upVector ^ localUp;
sideVector.normalize();
}
osg::Vec3d newUpVector = sideVector ^ lookVector;
newUpVector.normalize();
osg::Quat rotate_roll;
rotate_roll.makeRotate(upVector, newUpVector);
if (!rotate_roll.zeroRotation())
{
_centerRotation = _centerRotation * rotate_roll;
}
}
void
EarthManipulator::getCompositeEulerAngles(double* out_azim, double* out_pitch) const
{
osg::Matrix m = getWorldMatrix() * osg::Matrixd::inverse(_centerLocalToWorld);
osg::Vec3d look = -getUpVector(m);
osg::Vec3d up = getFrontVector(m);
look.normalize();
up.normalize();
if (out_azim)
{
if (look.z() < -0.9)
*out_azim = atan2(up.x(), up.y());
else if (look.z() > 0.9)
*out_azim = atan2(-up.x(), -up.y());
else
*out_azim = atan2(look.x(), look.y());
*out_azim = normalizeAzimRad(*out_azim);
}
if (out_pitch)
{
*out_pitch = asin(look.z());
}
}
// Extracts azim and pitch from a quaternion that does not contain any roll.
void
EarthManipulator::getEulerAngles(const osg::Quat& q, double* out_azim, double* out_pitch) const
{
osg::Matrix m(q);
osg::Vec3d look = -getUpVector(m);
osg::Vec3d up = getFrontVector(m);
look.normalize();
up.normalize();
if (out_azim)
{
if (look.z() < -0.9)
*out_azim = atan2(up.x(), up.y());
else if (look.z() > 0.9)
*out_azim = atan2(-up.x(), -up.y());
else
*out_azim = atan2(look.x(), look.y());
*out_azim = normalizeAzimRad(*out_azim);
}
if (out_pitch)
{
*out_pitch = asin(look.z());
}
}
osg::Quat
EarthManipulator::getQuaternion(double azim, double pitch) const
{
osg::Quat azim_q(azim, osg::Vec3d(0, 0, 1));
osg::Quat pitch_q(-pitch - osg::PI_2, osg::Vec3d(1, 0, 0));
osg::Matrix newRot = osg::Matrixd(azim_q * pitch_q);
return osg::Matrixd::inverse(newRot).getRotate();
//TODO: simplify this old code..
}
void
EarthManipulator::collapseTetherRotationIntoRotation()
{
// fetch the composite rotation angles (_rotation and _tetherRotation):
double azim, pitch;
getCompositeEulerAngles(&azim, &pitch); // TODO replace with getEulerAngles(_rotation*_tetherRotation, ...)
pitch = osg::clampBetween(
pitch,
osg::DegreesToRadians(_settings->getMinPitch()),
osg::DegreesToRadians(_settings->getMaxPitch()));
_rotation = getQuaternion(azim, pitch);
_tetherRotation = osg::Quat();
_tetherRotationOffset.unset();
}
void
EarthManipulator::setHomeViewpoint(const Viewpoint& vp, double duration_s)
{
_homeViewpoint = vp;
_homeViewpointDuration = duration_s;
}
namespace // Utility functions for drag()
{
// Find the point on a line, specified by p1 and v, closest to another
// point.
osg::Vec3d closestPtOnLine(const osg::Vec3d& p1, const osg::Vec3d& v,
const osg::Vec3d& p)
{
double u = (p - p1) * v / v.length2();
return p1 + v * u;
}
// Intersection of line and plane
bool findIntersectionWithPlane(const osg::Vec3d& normal, const osg::Vec3d& pt,
const osg::Vec3d& p1, const osg::Vec3d& v,
osg::Vec3d& result)
{
double denom = normal * v;
if (osg::equivalent(0, denom))
return false;
double u = normal * (pt - p1) / denom;
result = p1 + v * u;
return true;
}
// Circle of intersection of two spheres. The circle is in the plane
// normal to the line between the centers.
bool sphereInterection(const osg::Vec3d& p0, double r0,
const osg::Vec3d& p1, double r1,
osg::Vec3d& resultCenter, double& r)
{
using namespace osg;
Vec3d ptvec = (p1 - p0);
double d = ptvec.normalize();
if (d > r0 + r1)
return false; // spheres are too far apart
else if (d < fabs(r0 - r1))
return false; // One sphere is contained in the other
else if (equivalent(0, d) && equivalent(r0, r1))
{
resultCenter = p0;
r = r0;
return true; // circles are coincident.
}
// distance from p0 to the line through the interection points
double a = (r0 * r0 - r1 * r1 + d * d) / (2 * d);
// distance from bisection of that line to the intersections
resultCenter = p0 + ptvec * a;
r = sqrt(r0 * r0 - a * a);
return true;
}
// Find a point on the sphere (center, radius) through which the tangent
// through pt passes. The point lies in the plane defined by
//the line pt->center and ray.
osg::Vec3d calcTangentPoint(const osg::Vec3d& pt, const osg::Vec3d& center,
double radius, const osg::Vec3d& ray)
{
using namespace osg;
// new sphere with center at midpoint between pt and input sphere
Vec3d center2 = (pt + center) / 2.0;
double radius2 = (pt - center2).length();
Vec3d resCtr;
double resRad;
// Use Thales' theorem, which states that a triangle inscribed in
// a circle, with two points on a diameter of the circle and the
// third on the circle, is a right triangle. Since one endpoint is
// the center of the original sphere (the earth) and the other is
// pt, we can get our tangent from that.
bool valid = sphereInterection(center, radius, center2, radius2, resCtr,
resRad);
if (!valid)
return Vec3d(0.0, 0.0, 0.0);
// Get the tangent point that lies in the plane of the ray and the
// center line. The sequence of cross products gives us the point
// that is closest to the ray, rather than the one on the other
// side of the sphere.
Vec3d toCenter = center - pt;
toCenter.normalize();
Vec3d normal = ray ^ toCenter;
normal.normalize();
Vec3d radial = toCenter ^ normal;
radial = radial * resRad;
Vec3d result = resCtr + radial;
return result;
}
// Calculate a pointer click in eye coordinates
osg::Vec3d getWindowPoint(osgViewer::View* view, float x, float y)
{
float local_x, local_y;
const osg::Camera* camera
= view->getCameraContainingPosition(x, y, local_x, local_y);
if (!camera)
camera = view->getCamera();
osg::Matrix winMat;
if (camera->getViewport())
winMat = camera->getViewport()->computeWindowMatrix();
osg::Matrix projMat = camera->getProjectionMatrix();
// ray from eye through pointer in camera coordinate system goes
// from origin through transformed pointer coordinates
osg::Matrix win2camera = projMat * winMat;
win2camera.invert(win2camera);
osg::Vec4d winpt4 = osg::Vec4d(x, y, 0.0, 1.0) * win2camera;
winpt4 = winpt4 / winpt4.w();
return osg::Vec3d(winpt4.x(), winpt4.y(), winpt4.z());
}
// Decompose _center and _centerRotation into a longitude rotation
// and a latitude rotation + translation in the longitudinal plane.
void decomposeCenter(const osg::Vec3d& center, const osg::Quat& centerRotation,
osg::Matrix& Me, osg::Matrix& Mlon)
{
using namespace osg;
Mlon.makeIdentity();
Matrix Mtotal(centerRotation);
Mtotal.setTrans(center);
// Use the X axis to determine longitude rotation. Due to the
// OpenGL camera rotation, this axis will be the Y axis of the
// longitude matrix.
Mlon(1, 0) = Mtotal(0, 0); Mlon(1, 1) = Mtotal(0, 1);
// X axis is rotated 90 degrees, obviously
Mlon(0, 0) = Mlon(1, 1); Mlon(0, 1) = -Mlon(1, 0);
Matrix MlonInv = Matrixd::inverse(Mlon);
Me = Mtotal * MlonInv;
}
osg::Matrixd rotateAroundPoint(const osg::Vec3d& pt, double theta,
const osg::Vec3d& axis)
{
return (osg::Matrixd::translate(pt)
* osg::Matrixd::rotate(theta, axis)
* osg::Matrixd::translate(pt * -1.0));
}
}
// Theory of operation for the manipulator drag motion
//
// The mouse drag is transformed to a vector on the surface of the
// earth i.e., in the surface plane at the start of the drag. This is
// treated as a displacement along the arc of a great circle. The
// earth will be rotated by the equivalent rotation around the axis of
// the circle. However, the manipulator controls the camera, not the
// earth, so the camera's placement matrix (inverse view matrix)
// should be rotated by the inverse of the calculated
// rotation. EarthManipulator represents the placement matrix as the
// concatenation of 4 transformations: distance from focal point,
// local heading and pitch, rotation to frame of focal point, focal
// point. To change the camera placement we rotate the frame rotation
// (_centerRotation) and focal point (_center).
//
// When the start or end drag click is not on the earth, we choose the
// nearest tangent point on the earth to the ray from the eye and
// proceed.
void
EarthManipulator::drag(double dx, double dy, osg::View* theView)
{
osgViewer::View* view = dynamic_cast<osgViewer::View*>(theView);
if (!view)
return;
const osg::Vec3d zero(0.0, 0.0, 0.0);
if (_last_action._type != ACTION_EARTH_DRAG)
_lastPointOnEarth = zero;
double radiusEquator = _srs.valid() ? _srs->getEllipsoid()->getRadiusEquator() : 6378137.0;
float x = _ga_t0->getX(), y = _ga_t0->getY();
float local_x, local_y;
const osg::Camera* camera = view->getCameraContainingPosition(x, y, local_x, local_y);
if (!camera)
camera = view->getCamera();
if (!camera)
return;
osg::Matrix viewMat = camera->getViewMatrix();
osg::Matrix viewMatInv = camera->getInverseViewMatrix();
if (!_ga_t1.valid())
return;
osg::Vec3d worldStartDrag;
// drag start in camera coordinate system.
bool onEarth = screenToWorld(_ga_t1->getX(), _ga_t1->getY(), view, worldStartDrag);
if (onEarth)
{
if (_lastPointOnEarth == zero)
_lastPointOnEarth = worldStartDrag;
else
worldStartDrag = _lastPointOnEarth;
}
else if (_srs->isGeographic())
{
if (_lastPointOnEarth != zero)
{
worldStartDrag = _lastPointOnEarth;
}
else if (_srs.valid())
{
const osg::Vec3d startWinPt = getWindowPoint(view, _ga_t1->getX(), _ga_t1->getY());
const osg::Vec3d startDrag = calcTangentPoint(
zero, zero * viewMat, radiusEquator,
startWinPt);
worldStartDrag = startDrag * viewMatInv;
}
}
else
return;
// ray from eye through pointer in camera coordinate system goes
// from origin through transformed pointer coordinates
const osg::Vec3d winpt = getWindowPoint(view, x, y);
// Find new point to which startDrag has been moved
osg::Vec3d worldEndDrag;
osg::Quat worldRot;
bool endOnEarth = screenToWorld(x, y, view, worldEndDrag);
if (endOnEarth)
{
// OE_WARN << "end drag: " << worldEndDrag << "\n";
}
else
{
osg::Vec3d earthOrigin = zero * viewMat;
const osg::Vec3d endDrag = calcTangentPoint(zero, earthOrigin, radiusEquator, winpt);
worldEndDrag = endDrag * viewMatInv;
//OE_INFO << "tangent: " << worldEndDrag << "\n";
}
if (_srs->isGeographic())
{
worldRot.makeRotate(worldStartDrag, worldEndDrag);
// Move the camera by the inverse rotation
osg::Quat cameraRot = worldRot.conj();
// Derive manipulator parameters from the camera matrix. We
// can't use _center, _centerRotation, and _rotation directly
// from the manipulator because they may have been updated
// already this frame while the camera view matrix,
// used to do the terrain intersection, has not. This happens
// when several mouse movement events arrive in a frame. there
// will be bad stuttering artifacts if we use the updated
// manipulator parameters.
osg::Matrixd Mmanip = osg::Matrixd::translate(-_viewOffset.x(), -_viewOffset.y(), -_distance) * viewMatInv;
osg::Vec3d center = Mmanip.getTrans();
osg::Quat centerRotation = computeCenterRotation(center);
osg::Matrixd Mrotation = (Mmanip * osg::Matrixd::translate(center * -1)
* osg::Matrixd::rotate(centerRotation.inverse()));
osg::Matrixd Me = osg::Matrixd::rotate(centerRotation)
* osg::Matrixd::translate(center) * osg::Matrixd::rotate(cameraRot);
// In order for the Viewpoint settings to make sense, the
// inverse camera matrix must not have a roll component, which
// implies that its x axis remains parallel to the
// z = 0 plane. The strategy for doing that is different if
// the azimuth is locked.
// Additionally, the part of the camera rotation defined by
// _centerRotation must be oriented with the local frame of
// _center on the ellipsoid. For the purposes of the drag
// motion this is nearly identical to the frame obtained by
// the trackball motion, so we just fix it up at the end.
if (_settings->getLockAzimuthWhilePanning())
{
// The camera needs to be rotated that _centerRotation
// is a rotation only around the global Z axis and the
// camera frame X axis. We don't change _rotation, so that
// azimuth and pitch will stay constant, but the drag must
// still be correct i.e., the point dragged must remain
// under the cursor. Therefore the rotation must be around the
// point that was dragged, worldEndDrag.
//
// Rotate Me so that its x axis is parallel to the z=0
// plane.
// Find cone with worldEndDrag->center axis and x
// axis of coordinate frame as generator of the conical
// surface.
osg::Vec3d coneAxis = worldEndDrag * -1;
coneAxis.normalize();
osg::Vec3d xAxis(Me(0, 0), Me(0, 1), Me(0, 2));
// Center of disk: project xAxis onto coneAxis
double diskDist = xAxis * coneAxis;
osg::Vec3d P1 = coneAxis * diskDist;
// Basis of disk equation:
// p = P1 + R * r * cos(theta) + S * r * sin(theta)
osg::Vec3d R = xAxis - P1;
osg::Vec3d S = R ^ coneAxis;
double r = R.normalize();
S.normalize();
// Solve for angle that rotates xAxis into z = 0 plane.
// soln to 0 = P1.z + r cos(theta) R.z + r sin(theta) S.z
double temp1 = r * (osg::square(S.z()) + osg::square(R.z()));
if (osg::equivalent(temp1, 0.0))
return;
double radical = r * temp1 - osg::square(P1.z());
if (radical < 0)
return;
double temp2 = R.z() * sqrt(radical) / temp1;
double temp3 = S.z() * P1.z() / temp1;
double sin1 = temp2 + temp3;
double sin2 = temp2 - temp3;
double theta1 = DBL_MAX;
double theta2 = DBL_MAX;
osg::Matrixd cm1, cm2;
if (fabs(sin1) <= 1.0)
{
theta1 = -asin(sin1);
osg::Matrixd m = rotateAroundPoint(worldEndDrag, -theta1, coneAxis);
cm1 = Me * m;
}
if (fabs(sin2) <= 1.0)
{
theta2 = asin(sin2);
osg::Matrix m = rotateAroundPoint(worldEndDrag, -theta2, coneAxis);
cm2 = Me * m;
}
if (theta1 == DBL_MAX && theta2 == DBL_MAX)
return;
osg::Matrixd* CameraMat = 0;
if (theta1 != DBL_MAX && cm1(1, 2) >= 0.0)
CameraMat = &cm1;
else if (theta2 != DBL_MAX && cm2(1, 2) >= 0.0)
CameraMat = &cm2;
else
return;
setCenter(CameraMat->getTrans());
}
else
{
// The camera matrix must be rotated around the local Z axis so
// that the X axis is parallel to the global z = 0
// plane. Then, _rotation is rotated by the inverse
// rotation to preserve the total transformation.
double theta = atan2(-Me(0, 2), Me(1, 2));
double s = sin(theta), c = cos(theta);
if (c * Me(1, 2) - s * Me(0, 2) < 0.0)
{
s = -s;
c = -c;
}
osg::Matrixd m(c, s, 0, 0,
-s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
osg::Matrixd CameraMat = m * Me;
setCenter(CameraMat.getTrans());
// It's not necessary to include the translation
// component, but it's useful for debugging.
osg::Matrixd headMat
= (osg::Matrixd::translate(_viewOffset.x(), _viewOffset.y(), _distance)
* Mrotation);
headMat = headMat * osg::Matrixd::inverse(m);
_rotation = headMat.getRotate();
//recalculateLocalPitchAndAzimuth();
}
_centerRotation = computeCenterRotation(_center);
_previousUp = getUpVector(_centerLocalToWorld);
}
else
{
// This is obviously not correct.
setCenter(_center + (worldStartDrag - worldEndDrag));
}
}
aaa