OsgEearh 中的 FeatureEditor的实现原理

时间:2022-02-09 23:45:07

先来看看FeatureEditor的用法:

    const osgEarth::SpatialReference* mapSRS = mapNode->getMapSRS();

    osgEarth::Symbology::Style geomStyle;
    geomStyle.getOrCreate<osgEarth::LineSymbol>()->stroke()->color() = osgEarth::Symbology::Color::Cyan;
    geomStyle.getOrCreate<osgEarth::LineSymbol>()->stroke()->width() = 5.0f;
    geomStyle.getOrCreate<osgEarth::LineSymbol>()->tessellationSize() = 75000;
    geomStyle.getOrCreate<osgEarth::AltitudeSymbol>()->clamping() = osgEarth::AltitudeSymbol::CLAMP_TO_TERRAIN;
    geomStyle.getOrCreate<osgEarth::AltitudeSymbol>()->technique() = osgEarth::AltitudeSymbol::TECHNIQUE_DRAPE;

    osg::ref_ptr<osgEarth::Symbology::Polygon> polygon = new osgEarth::Symbology::Polygon();
    polygon->push_back(osg::Vec3d(0, 40, 0));
    polygon->push_back(osg::Vec3d(-60, 40, 0));
    polygon->push_back(osg::Vec3d(-60, 60, 0));
    polygon->push_back(osg::Vec3d(0, 60, 0));

    osg::ref_ptr<osgEarth::Features::Feature> feature = new osgEarth::Features::Feature(polygon, mapSRS);
    osg::ref_ptr<osgEarth::Annotation::FeatureNode> featureNode = new osgEarth::Annotation::FeatureNode(feature, geomStyle);
    geometryGroup->addChild(featureNode);
    osg::ref_ptr<osgEarth::Annotation::FeatureEditor> editor = new osgEarth::Annotation::FeatureEditor(featureNode);
    mapNode->addChild(editor);

FeatureNode作为参数来构造出一个FeatureEditor,然后将该FeatureEditor添加到MapNode中,即可实现通过鼠标拖动对FeatureNode的编辑功能。那么FeatureEditor是如何做到的呢?

OsgEearh 中的 FeatureEditor的实现原理

先看看FeatureEditor的继承图

OsgEearh 中的 FeatureEditor的实现原理

FeatureEditor继承自AnnotationEditorAnnotationEditor继承自Group,看看AnnotationEditor的代码

class OSGEARTHANNO_EXPORT AnnotationEditor : public osg::Group
    {
    protected:
        AnnotationEditor();

        virtual ~AnnotationEditor() { }
    };
AnnotationEditor::AnnotationEditor() :
osg::Group()
{
    // editor geometry should always be visible.
    osg::StateSet* stateSet = this->getOrCreateStateSet();
    stateSet->setMode(GL_DEPTH_TEST, 0);
    stateSet->setRenderBinDetails(99, "RenderBin");
}

AnnotationEditor这个类并没有做太多的事情,仅仅是在构造函数中设置了一下渲染状态而已。


接着看看FeatureEditor这个类

class OSGEARTHANNO_EXPORT FeatureEditor : public AnnotationEditor
    {
    public:
         /**
         * Constructs a new FeatureEditor
         * @param featureNode
         *      The FeatureNode to edit         
         */
        FeatureEditor( FeatureNode* featureNode );

        /**
         *Gets the color of the draggers when they are selected
         */
        const osg::Vec4f& getPickColor() const;

        /**
         *Sets the color of the draggers when they are selected
         */
        void setPickColor( const osg::Vec4f& pickColor );

        /**
         *Gets the color of the draggers
         */
        const osg::Vec4f& getColor() const;

        /**
         *Sets the color of the draggers
         */
        void setColor( const osg::Vec4f& color );


        /**
         *Gets the dragger size
         */
        float getSize() const;

        /**
         *Sets the dragger size
         */
        void setSize( float size );


    protected:
        void init();

        osg::Vec4f _pickColor;
        osg::Vec4f _color;
        float _size;

        osg::ref_ptr< FeatureNode > _featureNode;        
    };

这个类的代码也比较简单,只包含四个成员变量,两个color,一个size,以及该Editor所要编辑的FeatureNode

FeatureEditor::FeatureEditor( FeatureNode* featureNode):
_featureNode( featureNode ),
_color(osg::Vec4(0.0f, 1.0f, 0.0f, 1.0f)),
_pickColor(osg::Vec4(1.0f, 1.0f, 0.0f, 1.0f)),
_size( 5.0f )
{
    init();
}

构造函数初始化了四个成员变量,然后调用init()函数。

void
FeatureEditor::init()
{
    removeChildren( 0, getNumChildren() );

    Feature* feature = _featureNode->getFeatures().front().get();
    //Create a dragger for each point
    for (unsigned int i = 0; i < feature->getGeometry()->size(); i  )
    {
        SphereDragger* dragger = new SphereDragger( _featureNode->getMapNode() );
        dragger->setColor( _color );
        dragger->setPickColor( _pickColor );
        dragger->setSize( _size );
        dragger->setPosition(GeoPoint(feature->getSRS(),  (*feature->getGeometry())[i].x(),  (*feature->getGeometry())[i].y()));
        dragger->addPositionChangedCallback(new MoveFeatureDraggerCallback( _featureNode.get(), i) );

        addChild(dragger);        
    }
}

正如官方所给的注释:这个函数为geometry中的每个point都创建了一个draggerdragger又是个什么东东?这个我们现在还不知道,接下来我们的内容就是研究dragger,在此处,我们主要需要注意这四句代码,此时不懂不要紧,看完后面,就会串起来了。

void
FeatureEditor::init()
{
    for (unsigned int i = 0; i < feature->getGeometry()->size(); i  )
    {
        //构造函数
        SphereDragger* dragger = new SphereDragger( _featureNode->getMapNode() );
        //根据point的位置来设置dragger的位置
        dragger->setPosition(GeoPoint(feature->getSRS(),  (*feature->getGeometry())[i].x(),  (*feature->getGeometry())[i].y()));
        //为dragger添加回调
        dragger->addPositionChangedCallback(new MoveFeatureDraggerCallback( _featureNode.get(), i) );
        //将dragger添加到场景中
        addChild(dragger);        
    }
}


下面来看看dragger

OsgEearh 中的 FeatureEditor的实现原理

dragger继承自GeoPositionNode,对于GeoPositionNode,我们不需要知道太多,GeoPositionNode继承自Group,它的特殊之处在于它具有地理信息(毕竟人家叫 Geo Positon嘛),它提供了一个setPosition接口,我们可以通过这个接口来设置GeoPositionNode的位置。

/**
     * Dragger is a handle you can use to control things in the scene.
     * You drag it around with the mouse and it fires PositionChangedCallback‘s
     * that you can listen to to repond to.
     */
    class OSGEARTHANNO_EXPORT Dragger : public GeoPositionNode
    {
    public:
        /**
        * Callback that is fired when the position changes
        */
        struct PositionChangedCallback : public osg::Referenced
        {
        public:
            virtual void onPositionChanged(const Dragger* sender, const osgEarth::GeoPoint& position) {};
            virtual ~PositionChangedCallback() { }
        };

        typedef std::list< osg::ref_ptr<PositionChangedCallback> > PositionChangedCallbackList;

        enum DragMode
        {
          DRAGMODE_HORIZONTAL,
          DRAGMODE_VERTICAL
        };

        Dragger( MapNode* mapNode, int modKeyMask=0, const DragMode& defaultMode=DRAGMODE_HORIZONTAL );

        /** dtor */
        virtual ~Dragger();

        /** Sets the map position of the dragger, optionally firing a PositionChanged event. */
        void setPosition(const osgEarth::GeoPoint& position, bool fireEvents);

        /** Drag mode */
        void setDefaultDragMode(const DragMode& mode) { _defaultMode = mode; }
        DragMode& getDefaultDragMode() { return _defaultMode; }

        /** Add a callback that runs whenever the user moves the dragger */
        void addPositionChangedCallback( PositionChangedCallback* callback );

        /** Remove a callback. */
        void removePositionChangedCallback( PositionChangedCallback* callback );


    public: // GeoPositionNode

        virtual void setPosition(const GeoPoint& point);

    public: // osg::Node

        virtual void traverse(osg::NodeVisitor& nv);

    protected:
        virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa);
        void firePositionChanged();
        
        bool _dragging;
        bool _hovered;
        PositionChangedCallbackList _callbacks;

        osg::ref_ptr<  osgManipulator::LineProjector >  _projector;
        osgManipulator::PointerInfo  _pointer;
        osg::Vec3d _startProjectedPoint;
        bool _elevationDragging;
        int _modKeyMask;
        DragMode _defaultMode;
        double _verticalMinimum;
    };

dragger的代码有一丢丢长,为求直观,上面的代码中我删除了其中一部分代码。

/**
     * Dragger is a handle you can use to control things in the scene.
     * You drag it around with the mouse and it fires PositionChangedCallback‘s
     * that you can listen to to repond to.
     */

官方给的注释是:可以利用dragger来control scene中的对象,当你用鼠标拖动dragger时,会触发PositionChangedCallback回调。

还记得之前FeatureEditor中init函数中的这句代码么?

//为dragger添加回调
dragger->addPositionChangedCallback(new MoveFeatureDraggerCallback( _featureNode.get(), i) );

我们先不管MoveFeatureDraggerCallback具体是什么,只需要知道它继承自PositionChangedCallback(PostionChangedCallback的代码在上面的代码里),PositionChangedCallback这个函数包含一个虚函数:

virtual void onPositionChanged(const Dragger* sender, const osgEarth::GeoPoint& position) {};

显然MoveFeatureChangedCallback对这个函数进行了重写,重写的内容具体是什么,我们先不管,先将注意力放在addPositionChangedCallback这个函数上。

void Dragger::addPositionChangedCallback( PositionChangedCallback* callback )
{
    _callbacks.push_back( callback );
}

这个函数的功能也很直观,将PositionChangedCallback添加进一个列表中,这个列表何时被用到?

void Dragger::firePositionChanged()
{
    for( PositionChangedCallbackList::iterator i = _callbacks.begin(); i != _callbacks.end(); i   )
    {
        i->get()->onPositionChanged(this, getPosition());
    }
}

firePositonChanged函数遍历列表中的PositionChangedCallback对象,调用其onPositionChanged函数。firePositionChanged函数何时被调用?

void Dragger::setPosition(const GeoPoint& position, bool fireEvents)
{
    GeoPositionNode::setPosition( position );
    if ( fireEvents )
        firePositionChanged();
}

再看看handle函数。handle函数是用来处理gui事件的,在handle函数中,firePositionChanged()setPosition()多次出现过。

bool Dragger::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
    if (ea.getHandled()) return false;

    osgViewer::View* view = dynamic_cast<osgViewer::View*>(&aa);
    if (!view) return false;
    if (!getMapNode()) return false;

    if (ea.getEventType() == osgGA::GUIEventAdapter::PUSH)
    {
        IntersectionPicker picker( view, this );
        IntersectionPicker::Hits hits;

        if ( picker.pick( ea.getX(), ea.getY(), hits ) )
        {
            const GeoPoint& position = getPosition();

            _dragging = true;

            //Check for and handle vertical dragging if necessary
            bool pressedAlt = _modKeyMask && (ea.getModKeyMask() & _modKeyMask) > 0;
            _elevationDragging = (_defaultMode == Dragger::DRAGMODE_VERTICAL && !pressedAlt) || (_defaultMode == Dragger::DRAGMODE_HORIZONTAL && pressedAlt);

            if (_elevationDragging)
            {
              _pointer.reset();

              // set movement range
              // TODO: values 0.0 and 300000.0 are rather experimental
              GeoPoint posStart(position.getSRS(), position.x(), position.y(), 0.0, ALTMODE_ABSOLUTE);
              osg::Vec3d posStartXYZ;
              posStart.toWorld(posStartXYZ);

              GeoPoint posEnd(position.getSRS(), position.x(), position.y(), 300000.0, ALTMODE_ABSOLUTE);
              osg::Vec3d posEndXYZ;
              posEnd.toWorld(posEndXYZ);

              _projector->setLine(posStartXYZ, posEndXYZ);

              // set camera
              osgUtil::LineSegmentIntersector::Intersections intersections;
              osg::Node::NodeMask intersectionMask = 0xffffffff;
              osgViewer::View* view = dynamic_cast<osgViewer::View*>(&aa);
              if ( !view )
                  return true;

              if (view->computeIntersections(ea.getX(),ea.getY(),intersections, intersectionMask))
              {
                  for (osgUtil::LineSegmentIntersector::Intersections::iterator hitr = intersections.begin(); hitr != intersections.end();   hitr)
                  {
                      _pointer.addIntersection(hitr->nodePath, hitr->getLocalIntersectPoint());
                  }

                  bool draggerFound = false;
                  for (osgManipulator::PointerInfo::IntersectionList::iterator piit = _pointer._hitList.begin(); piit != _pointer._hitList.end();   piit)
                  {
                      for (osg::NodePath::iterator itr = piit->first.begin(); itr != piit->first.end();   itr)
                      {
                          Dragger* dragger = dynamic_cast<Dragger*>(*itr);
                          if (dragger==this)
                          {
                              draggerFound = true;
                              osg::Camera *rootCamera = view->getCamera();
                              osg::NodePath nodePath = _pointer._hitList.front().first;
                              osg::NodePath::reverse_iterator ritr;
                              for (ritr = nodePath.rbegin(); ritr != nodePath.rend();   ritr)
                              {
                                  osg::Camera* camera = dynamic_cast<osg::Camera*>(*ritr);
                                  if (camera && (camera->getReferenceFrame()!=osg::Transform::RELATIVE_RF || camera->getParents().empty()))
                                  {
                                       rootCamera = camera;
                                       break;
                                  }
                              }
                              _pointer.setCamera(rootCamera);
                              _pointer.setMousePosition(ea.getX(), ea.getY());

                              break;
                          }
                      }

                      if (draggerFound)
                        break;
                  }
              }
            }

            aa.requestRedraw();
            return true;
        }
    }
    else if (ea.getEventType() == osgGA::GUIEventAdapter::RELEASE)
    {
        _elevationDragging = false;

        if ( _dragging )
        {
            _dragging = false;
            firePositionChanged();
        }

        aa.requestRedraw();
    }
    else if (ea.getEventType() == osgGA::GUIEventAdapter::DRAG)
    {
        if (_elevationDragging) 
        {
            _pointer._hitIter = _pointer._hitList.begin();
            _pointer.setMousePosition(ea.getX(), ea.getY());

            if (_projector->project(_pointer, _startProjectedPoint)) 
            {
                const GeoPoint& position = getPosition();

                //Get the absolute mapPoint that they‘ve drug it to.
                GeoPoint projectedPos;
                projectedPos.fromWorld(position.getSRS(), _startProjectedPoint);

                // make sure point is not dragged down below
                // TODO: think of a better solution / HeightAboveTerrain performance issues?
                if (projectedPos.z() >= _verticalMinimum)
                {
                    //If the current position is relative, we need to convert the absolute world point to relative.
                    //If the point is absolute then just emit the absolute point.
                    if (position.altitudeMode() == ALTMODE_RELATIVE)
                    {
                        projectedPos.transformZ(ALTMODE_RELATIVE, getMapNode()->getTerrain());
                    }

                    setPosition( projectedPos );
                    aa.requestRedraw();
                }
            }

            return true;
        }
        
        if (_dragging)
        {
            osg::Vec3d world;
            if ( getMapNode() && getMapNode()->getTerrain()->getWorldCoordsUnderMouse(view, ea.getX(), ea.getY(), world) )
            {
                const GeoPoint& position = getPosition();

                //Get the absolute mapPoint that they‘ve drug it to.
                GeoPoint mapPoint;
                mapPoint.fromWorld( getMapNode()->getMapSRS(), world );

                //If the current position is relative, we need to convert the absolute world point to relative.
                //If the point is absolute then just emit the absolute point.
                if (position.altitudeMode() == ALTMODE_RELATIVE)
                {
                    mapPoint.alt() = position.alt();
                    mapPoint.altitudeMode() = ALTMODE_RELATIVE;
                }

                setPosition( mapPoint );
                aa.requestRedraw();
                return true;
            }
        }
    }   
    else if (ea.getEventType() == osgGA::GUIEventAdapter::MOVE)
    {
        IntersectionPicker picker( view, this );
        IntersectionPicker::Hits hits;

        if ( picker.pick( ea.getX(), ea.getY(), hits ) )
        {
            setHover( true );
        }
        else
        {
            setHover( false );
        }        
        aa.requestRedraw();
    }
    return false;
}

dragger有两种模式,水平模式和垂直模式,这两种模式的差异在handle()中得到了体现。

enum DragMode
        {
          DRAGMODE_HORIZONTAL,
          DRAGMODE_VERTICAL
        };

handle函数的内容我大体说明一下:当鼠标push时,判断是否点击到了dragger上,如果击中的话,判断当前拖动模式是水平模式还是垂直模式,如果是垂直模式的话,就创建一个projector对象,根据dragger当前的位置得到一条直线,将projector的投影对象设为该直线。之后drag时,将鼠标所处位置的世界坐标投影到该直线上。(顺带一提,这部分代码可以参考一下osg中的dragger类的handle函数,两者还是挺相似的,都用到了投影的方法

OsgEearh 中的 FeatureEditor的实现原理

若为水平模式,那就不需要投影了,在drag时,直接根据鼠标的位置,得到地面坐标就ok了。

OsgEearh 中的 FeatureEditor的实现原理

故handle函数的主要作用就是当drag dragger时,利用setPosition函数实时地更新dragger的位置,并触发回调。

最后再回头看看MoveFeatureChangedCallback()这个类。

class MoveFeatureDraggerCallback : public Dragger::PositionChangedCallback
{
public:
    MoveFeatureDraggerCallback(FeatureNode* featureNode, int point):
      _featureNode( featureNode ),      
      _point(point)
      {}

      virtual void onPositionChanged(const Dragger* sender, const osgEarth::GeoPoint& position)
      {
          Feature* feature = _featureNode->getFeatures().front().get();
          (*feature->getGeometry())[_point] =  osg::Vec3d(position.x(), position.y(), 0);
          _featureNode->init();
      }

      osg::ref_ptr< FeatureNode > _featureNode;
      
      int _point;

};

重点关注onPositionChanged这个重写的虚函数,它有两个参数,sender代表调用这个函数的dragger,position表示dragger的位置,函数内容很简单,就是根据dragger的位置来更改geometry中point的位置,最后再重绘geometry。


剖析到此结束,我们总结一下:假设一个geometry有十个point,当我们根据这个geometry创建一个FeatureEditor是,FeatureEditor会创建十个Dragger,每个Dragger的初始位置都是Point所处的位置,当Draggr被移动时,会触发回调,从而改变这个Dragger所绑定的point的位置,以上!

建议再重头看一下上面的剖析过程。OsgEearh 中的 FeatureEditor的实现原理