/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
* Copyright 2020 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.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*
* 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/>
*/
#ifndef OSGEARTH_SCREEN_SPACE_LAYOUT_CALLOUT_H
#define OSGEARTH_SCREEN_SPACE_LAYOUT_CALLOUT_H 1

#include <osgEarth/ScreenSpaceLayoutImpl>

// https://github.com/JCash/voronoi
#define JC_VORONOI_IMPLEMENTATION
#define JCV_REAL_TYPE double
#define JCV_ATAN2 atan2
#define JCV_FLT_MAX 1.7976931348623157E+308
#include "jc_voronoi.h"

// whether to use the rotated axis-aligned bounding box method for
// calculating a Voronoi site centroid (as opposed to a simple centroid)
#define USE_ROTATED_AABB_CENTROID_METHOD 1


namespace osgEarth { namespace Internal
{
    using namespace osgEarth;

    struct jcv_point_comparator {
        bool operator()(const jcv_point& a, const jcv_point& b) const {
            if (a.x < b.x) return true;
            else if (a.x > b.x) return false;
            else return a.y < b.y;
        }
    };

    /**
    * Custom RenderLeaf sorting algorithm for deconflicting drawables
    * as callouts.
    */
    struct CalloutImplementation : public osgUtil::RenderBin::SortCallback
    {
        struct BBox
        {
            BBox() : LL(DBL_MAX,DBL_MAX), UR(-DBL_MAX,-DBL_MAX) { }
            void expandToInclude(double x, double y) {
                LL.x() = osg::minimum(LL.x(), x);
                UR.x() = osg::maximum(UR.x(), x);
                LL.y() = osg::minimum(LL.y(), y);
                UR.y() = osg::maximum(UR.y(), y);
            }
            osg::Vec2d LL, UR;
        };

        struct Element
        {
            osg::Drawable* _drawable;
            osgEarth::Text* _text;
            osgUtil::RenderLeaf* _leaf;
            osg::ref_ptr<osg::RefMatrix> _matrix;
            unsigned _frame;
            osg::Vec3d _anchor;
            osg::Vec3d _offsetVector;
            double _offsetLength;
            unsigned _leaderLineIndex;
            bool _declutter;
            int _numFramesSinceMove;

            Element();
            bool operator < (const Element&) const; // comparator
            void move(float dir);
        };

        using Elements = std::map<osg::Drawable*, Element>;

        struct CameraLocal
        {
            CameraLocal();
            const osg::Camera* _camera;      // camera associated with the data structure
            unsigned _frame;
            osg::Matrix _scalebias;          // scale bias matrix for the viewport
            Elements _elements;
            osg::Matrix _vpm;
            bool _vpmChanged;
            osg::ref_ptr<LineDrawable> _leaders;
            bool _leadersDirty;
            osg::Vec4f _leaderColor;
            std::vector<jcv_point> _points;
            // TODO: use unordered_map?
            std::map<jcv_point, Element*, jcv_point_comparator> _lookup;
            osg::ref_ptr<LineDrawable> _voronoi;

            void initDebug();
        };

        ScreenSpaceLayoutContext* _context;
        DeclutterSortFunctor* _customSortFunctor;
        PerObjectFastMap<const osg::Camera*, CameraLocal> _cameraLocal;
        bool _resetWhenViewChanges;

        //! Constructor
        CalloutImplementation(ScreenSpaceLayoutContext* context, DeclutterSortFunctor* f);

        //! Override from SortCallback
        void sortImplementation(osgUtil::RenderBin*) override;

        void push(osgUtil::RenderLeaf* leaf, CameraLocal& local);

        void sort(CameraLocal& local);
    };

    CalloutImplementation::Element::Element() :
        _drawable(NULL),
        _text(NULL),
        _frame(0u),
        _leaderLineIndex(INT_MAX),
        _offsetVector(0,1,0),
        _leaf(NULL),
        _offsetLength(0.0),
        _declutter(false),
        _numFramesSinceMove(0)
    {
        //nop
    }

    bool CalloutImplementation::Element::operator<(const Element& rhs) const
    {
        return ((intptr_t)_drawable < (intptr_t)rhs._drawable);
    }

    void
    CalloutImplementation::Element::move(float dir)
    {
        // rotate little more than 1/4 turn:
        const double rotation = osg::PI / 16; //1.7; //osg::PI / 32; //1.6;
        const osg::Quat q(dir*rotation, osg::Vec3d(0, 0, 1));
        _offsetVector = q * _offsetVector;
    }

    CalloutImplementation::CameraLocal::CameraLocal() :
        _camera(0),
        _frame(0),
        _vpmChanged(true),
        _leadersDirty(false),
        _leaderColor(1,1,1,1)
    {
        _leaders = new LineDrawable(GL_LINES);
        _leaders->setCullingActive(false);
        _leaders->setDataVariance(osg::Object::DYNAMIC);
        _leaders->setColor(_leaderColor);
        _leaders->setLineSmooth(true);
        GLUtils::setLighting(_leaders->getOrCreateStateSet(), osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);
    }

    void CalloutImplementation::CameraLocal::initDebug()
    {
        _voronoi = new LineDrawable(GL_LINES);
        _voronoi->setCullingActive(false);
        _voronoi->setDataVariance(osg::Object::DYNAMIC);
        _voronoi->setColor(osg::Vec4f(1,0,0,1));
        _voronoi->setLineWidth(1.0f);
        GLUtils::setLighting(_voronoi->getOrCreateStateSet(), osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED);
    }

    CalloutImplementation::CalloutImplementation(ScreenSpaceLayoutContext* context, DeclutterSortFunctor* f) :
        _context(context),
        _customSortFunctor(f),
        _resetWhenViewChanges(false)
    {
        //nop
    }

    namespace {
        inline float signOf(float x) {
            return x<0.0f? -1.0f: x>0.0f? 1.0f : 0.0f;
        }
    }

    void CalloutImplementation::push(osgUtil::RenderLeaf* leaf, CameraLocal& local)
    {
        static Element prototype;
        const osg::Vec3d zero(0,0,0);

        osg::Drawable* drawable = leaf->_drawable.get();

        std::pair<Elements::iterator,bool> result = local._elements.insert(std::make_pair(drawable,prototype));
        Element& element = result.first->second;

        bool isNew =
            (result.second == true) ||
            (local._frame - element._frame > 1);

        element._frame = local._frame;
        element._leaf = leaf;

        osg::Vec3d offset;
        const ScreenSpaceLayoutData* layoutData = dynamic_cast<const ScreenSpaceLayoutData*>(drawable->getUserData());
        if (layoutData)
        {
            offset = osg::Vec3d(layoutData->getPixelOffset().x(), layoutData->getPixelOffset().y(), 0.0);
        }

        if (element._drawable == NULL)
        {
            element._drawable = drawable;
            element._text = dynamic_cast<osgEarth::Text*>(drawable);
            element._drawable->setDataVariance(osg::Object::DYNAMIC);
            element._declutter = true;
        }

        if (layoutData)
        {
            // FLT_MAX priority => don't declutter this element.
            // Fixed position => don't move the element
            if (layoutData->getPriority() == FLT_MAX ||
                layoutData->getFixed())
            {
                element._declutter = false;
            }
            else
            {
                element._declutter = (element._drawable != NULL);
            }
        }

        element._anchor =
            zero *
            (*leaf->_modelview) *
            (*leaf->_projection) *
            local._scalebias;

        const osg::Viewport* vp = local._camera->getViewport();

        if (isNew)
        {
            element._offsetVector = element._anchor - osg::Vec3d(0.5*vp->width(), 0.5*vp->height(), 0.0);
            element._offsetVector.normalize();
        }
        else if (_resetWhenViewChanges && local._vpmChanged)
        {
            element._offsetVector = element._anchor - osg::Vec3d(0.5*vp->width(), 0.5*vp->height(), 0.0);
            element._offsetVector.normalize();
        }

        if (element._leaderLineIndex == INT_MAX)
        {
            element._leaderLineIndex = local._leaders->getNumVerts();
            local._leaders->pushVertex(osg::Vec3f());
            local._leaders->pushVertex(osg::Vec3f());
            local._leadersDirty = true;
        }

        // each one needs a unique mvm
        leaf->_modelview = new osg::RefMatrix();

        if (element._declutter && ScreenSpaceLayout::globallyEnabled)
        {
            double leaderLen = osg::minimum((double)_context->_options.leaderLineMaxLength().get(), element._offsetLength);
            osg::Vec3d pos = element._anchor + element._offsetVector*leaderLen;

            // alignment of text relative to the leader line
            const float margin = 5.0f;
            osg::Vec3d alignment;
            const osg::BoundingBox& bb = element._drawable->getBoundingBox();
            alignment.x() = element._offsetVector.x() * 0.5*(bb.xMax()-bb.xMin()) + margin*signOf(element._offsetVector.x());
            alignment.y() = element._offsetVector.y() * 0.5*(bb.yMax()-bb.yMin()) + margin*signOf(element._offsetVector.y());

            leaf->_modelview->makeTranslate(pos + alignment);

            // and update the leader line endpoints
            if (element._anchor != local._leaders->getVertex(element._leaderLineIndex))
            {
                local._leaders->setVertex(element._leaderLineIndex, element._anchor);
            }
            if (pos != local._leaders->getVertex(element._leaderLineIndex + 1))
            {
                local._leaders->setVertex(element._leaderLineIndex + 1, pos);
            }

            // and update the colors of the leader line
            if (element._text)
            {
                osg::Vec4f color =
                    _context->_options.leaderLineColor().isSet() ? _context->_options.leaderLineColor().get() :
                    element._text->getDrawMode() & osgText::Text::BOUNDINGBOX ? element._text->getBoundingBoxColor() :
                    local._leaderColor;

                local._leaders->setColor(element._leaderLineIndex, color);
                local._leaders->setColor(element._leaderLineIndex + 1, color);
            }
        }
        else
        {
            leaf->_modelview->makeTranslate(element._anchor + offset);
        }
    }

    void CalloutImplementation::sort(CameraLocal& local)
    {
        if (local._elements.empty())
            return;

        const osg::Viewport* vp = local._camera->getViewport();

        local._points.clear();
        local._lookup.clear();

        jcv_rect bounds;
        const double K = 0;
        bounds.min.x = vp->x() + K, bounds.min.y = vp->y() + K;
        bounds.max.x = vp->x() + vp->width() - K * 2, bounds.max.y = vp->y() + vp->height() - K * 2;

        for (Elements::reverse_iterator i = local._elements.rbegin();
            i != local._elements.rend();
            ++i)
        {
            Element& element = i->second;

            if (element._leaf != NULL && element._declutter)
            {
                // Hm, not sure what this is doing
                //if (element._frame == element._frame)
                if (element._frame == local._frame)
                {
                    // i.e., visible
                    element._leaf->_depth = 1.0f;

                    if (element._anchor.x() >= bounds.min.x &&
                        element._anchor.x() <= bounds.max.x &&
                        element._anchor.y() >= bounds.min.y &&
                        element._anchor.y() <= bounds.max.y)
                    {
                        jcv_point back{ element._anchor.x(), element._anchor.y() };
                        local._points.emplace_back(back);
                        local._lookup[back] = &element;
                    }
                }
            }
        }

        if (local._points.empty())
            return;

        // Use a Voronoi diagram to find the best location for each callout.
        // Use the "visual center" of the point's site as the label location.
        jcv_diagram diagram;
        ::memset(&diagram, 0, sizeof(jcv_diagram));
        jcv_diagram_generate(local._points.size(), &local._points[0], &bounds, &diagram);

        if (local._voronoi.valid())
        {
            local._voronoi->clear();
        }

#ifdef USE_ROTATED_AABB_CENTROID_METHOD
        osg::Quat q;
#endif

        const jcv_site* sites = jcv_diagram_get_sites(&diagram);
        for(int i=0; i<diagram.numsites; ++i)
        {
            const jcv_site* site = &sites[i];

            Element* element = local._lookup[site->p];
            if (element)
            {
#ifdef USE_ROTATED_AABB_CENTROID_METHOD
                // This centroid-finding method calculates an axis-aligned bounding box formed by
                // first rotating the site so that its longest edge is axis-aligned. This seems to
                // provide a better centroid in some cases, esp for trianglar sites.

                // first find the longest edge in the site:
                const jcv_graphedge* edge = NULL;
                const jcv_graphedge* longestEdge = NULL;

                jcv_real maxLen2 = 0;
                for(edge = site->edges; edge; edge = edge->next)
                {
                    jcv_real dx = edge->pos[1].x - edge->pos[0].x;
                    jcv_real dy = edge->pos[1].y - edge->pos[0].y;
                    jcv_real len2 = dx*dx + dy*dy;
                    if (len2 > maxLen2)
                    {
                        maxLen2 = len2;
                        longestEdge = edge;
                    }
                }

                // now compute a rotation that transforms the longest edge so that
                // it is parallel with x=0
                osg::Vec3d v(longestEdge->pos[1].x-longestEdge->pos[0].x, longestEdge->pos[1].y-longestEdge->pos[0].y, 0);
                q.makeRotate(v, osg::Vec3d(0,1,0));

                // now calculate an axis-aligned bounding box for the rotated site:
                BBox bb;
                const jcv_graphedge* lastEdge = NULL;
                osg::Vec3d p0, p1;
                for(edge = site->edges; edge; edge = edge->next)
                {
                    p0.set(edge->pos[0].x, edge->pos[0].y, 0);
                    p1.set(edge->pos[1].x, edge->pos[1].y, 0);

                    if (local._voronoi.valid())
                    {
                        local._voronoi->pushVertex(p0);
                        local._voronoi->pushVertex(p1);
                    }

                    // rotate the point into the aabb frame and incorporate it
                    // into the bounding box.
                    p0 = q*p0;
                    bb.expandToInclude(p0.x(), p0.y());
                }

                // finally take the centroid and transform it back into the original frame:
                osg::Vec3d centroid(
                    bb.LL.x() + 0.5*(bb.UR.x()-bb.LL.x()),
                    bb.LL.y() + 0.5*(bb.UR.y()-bb.LL.y()),
                    0);

                centroid = q.inverse()*centroid;

#else
                // This centroid-finding method is the typical bounding-box centroid.
                BBox bb;

                const jcv_graphedge* edge = site->edges;
                while(edge)
                {
                    bb.expandToInclude(edge->pos[0].x, edge->pos[0].y);

                    if (local._voronoi.valid())
                    {
                        local._voronoi->pushVertex(osg::Vec3(edge->pos[0].x, edge->pos[0].y, 0));
                        local._voronoi->pushVertex(osg::Vec3(edge->pos[1].x, edge->pos[1].y, 0));
                    }

                    edge = edge->next;
                }

                osg::Vec3d centroid(
                    bb.LL.x() + 0.5*(bb.UR.x()-bb.LL.x()),
                    bb.LL.y() + 0.5*(bb.UR.y()-bb.LL.y()),
                    0);
#endif

                osg::Vec3d prevOffset = element->_offsetVector;
                osg::Vec3d newOffset = centroid - element->_anchor;
                double newOffsetLength = newOffset.length();
                newOffset.normalize();

                // Try to avoid moving the offsetVector too drastically per frame.
                // If the label still needs to move after 60 frames go ahead and move it.
                // This helps for fast moving entities or if you are moving the camera quickly.
                double dot = newOffset * prevOffset;
                if (osg::absolute(dot) < 0.9 && element->_numFramesSinceMove < 60)
                {
                    element->_numFramesSinceMove++;
                }
                else
                {
                    element->_offsetVector = newOffset;
                    element->_numFramesSinceMove = 0;
                    element->_offsetLength = newOffsetLength;
                    element->_offsetVector.normalize();
                }
            }
        }
        jcv_diagram_free(&diagram);

        if (local._voronoi.valid())
        {
            local._voronoi->finish();
        }
    }

    // runs in CULL thread after culling completes
    void CalloutImplementation::sortImplementation(osgUtil::RenderBin* bin)
    {
        const ScreenSpaceLayoutOptions& options = _context->_options;

        bin->copyLeavesFromStateGraphListToRenderLeafList();

        osgUtil::RenderBin::RenderLeafList& leaves = bin->getRenderLeafList();

        // first, sort the leaves:
        if (_customSortFunctor && ScreenSpaceLayout::globallyEnabled)
        {
            // if there's a custom sorting function installed
            std::sort(leaves.begin(), leaves.end(), SortContainer(*_customSortFunctor));
        }
        else if (options.sortByDistance() == true)
        {
            // default behavior:
            std::sort(leaves.begin(), leaves.end(), SortFrontToBackPreservingGeodeTraversalOrder());
        }

        // nothing to sort? bail out
        if (leaves.empty())
            return;

        // access the per-camera persistent data:
        osg::Camera* cam = bin->getStage()->getCamera();

        // bail out if this camera is a master camera with no GC
        // (e.g., in a multi-screen layout)
        if (cam == NULL || (cam->getGraphicsContext() == NULL && !cam->isRenderToTextureCamera()))
            return;

        CameraLocal& local = _cameraLocal.get(cam);
        local._camera = cam;

        static osg::Vec4f invisible(1,0,0,0);
        local._leaders->setColor(invisible);
        local._leaders->setLineWidth(_context->_options.leaderLineWidth().get());

        if (_context->_debug && !local._voronoi.valid() && ScreenSpaceLayout::globallyEnabled)
            local.initDebug();

        osg::GraphicsContext* gc = cam->getGraphicsContext();
        local._frame = gc && gc->getState() && gc->getState()->getFrameStamp() ?
            gc->getState()->getFrameStamp()->getFrameNumber() : 0u;

        const osg::Viewport* vp = cam->getViewport();
        local._scalebias =
            osg::Matrix::translate(1, 1, 1) *
            osg::Matrix::scale(0.5*vp->width(), 0.5*vp->height(), 0.5) *
            osg::Matrix::translate(vp->x(), vp->y(), 0.0);

        for(osgUtil::RenderBin::RenderLeafList::iterator i = leaves.begin();
            i != leaves.end();
            ++i)
        {
            push(*i, local);
        }

        if (ScreenSpaceLayout::globallyEnabled)
        {
            sort(local);
        }

        // clear out the RenderLeaf pointers (since they change each frame)
        for (Elements::iterator i = local._elements.begin();
            i != local._elements.end();
            ++i)
        {
            Element& element = i->second;
            element._leaf = NULL;
        }
    }


    /**
    * Custom draw routine for our declutter render bin.
    */
    struct CalloutDraw : public osgUtil::RenderBin::DrawCallback
    {
        ScreenSpaceLayoutContext*                 _context;
        PerThread< osg::ref_ptr<osg::RefMatrix> > _ortho2D;
        osg::ref_ptr<osg::Uniform>                _fade;
        CalloutImplementation*                    _sortCallback;

        struct RunningState
        {
            RunningState() : lastFade(-1.0f), lastPCP(NULL) { }
            float lastFade;
            const osg::Program::PerContextProgram* lastPCP;
        };

        /**
        * Constructs the decluttering draw callback.
        * @param context A shared context among all decluttering objects.
        */
        CalloutDraw(ScreenSpaceLayoutContext* context)
            : _context(context)
            , _sortCallback(NULL)
        {
            // create the fade uniform.
            _fade = new osg::Uniform(osg::Uniform::FLOAT, FADE_UNIFORM_NAME);
            _fade->set(1.0f);
        }

        /**
        * Draws a bin. Most of this code is copied from osgUtil::RenderBin::drawImplementation.
        * The modifications are (a) skipping code to render child bins, (b) setting a bin-global
        * projection matrix in orthographic space, and (c) calling our custom "renderLeaf()" method
        * instead of RenderLeaf::render()
        */
        void drawImplementation(osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous)
        {
            osg::State& state = *renderInfo.getState();

            unsigned int numToPop = (previous ? osgUtil::StateGraph::numToPop(previous->_parent) : 0);
            if (numToPop > 1) --numToPop;
            unsigned int insertStateSetPosition = state.getStateSetStackSize() - numToPop;

            if (bin->getStateSet())
            {
                state.insertStateSet(insertStateSetPosition, bin->getStateSet());
            }

            // apply a window-space projection matrix.
            const osg::Viewport* vp = renderInfo.getCurrentCamera()->getViewport();
            if (vp)
            {
                osg::ref_ptr<osg::RefMatrix>& m = _ortho2D.get();
                if (!m.valid())
                    m = new osg::RefMatrix();

                //m->makeOrtho2D( vp->x(), vp->x()+vp->width()-1, vp->y(), vp->y()+vp->height()-1 );
                m->makeOrtho(vp->x(), vp->x() + vp->width() - 1, vp->y(), vp->y() + vp->height() - 1, -1000, 1000);
                state.applyProjectionMatrix(m.get());
            }

            // initialize the fading uniform
            RunningState rs;

            // render the list
            osgUtil::RenderBin::RenderLeafList& leaves = bin->getRenderLeafList();

            for (osgUtil::RenderBin::RenderLeafList::reverse_iterator rlitr = leaves.rbegin();
                rlitr != leaves.rend();
                ++rlitr)
            {
                osgUtil::RenderLeaf* rl = *rlitr;
                //if (rl->_depth > 0.0f)
                {
                    renderLeaf(rl, renderInfo, previous, rs);
                    previous = rl;
                }
            }

            if (bin->getStateSet())
            {
                state.removeStateSet(insertStateSetPosition);
            }

            if (ScreenSpaceLayout::globallyEnabled)
            {
                // the leader lines
                CalloutImplementation::CameraLocal& local = _sortCallback->_cameraLocal.get(renderInfo.getCurrentCamera());

                if (local._leadersDirty)
                {
                    local._leaders->dirty();
                    local._leadersDirty = false;
                }

                renderInfo.getState()->applyModelViewMatrix(osg::Matrix::identity());

                if (local._leaders->getUseGPU())
                {
                    renderInfo.getState()->pushStateSet(local._leaders->getGPUStateSet());
                }

                renderInfo.getState()->pushStateSet(local._leaders->getStateSet());

                renderInfo.getState()->apply();

                glDepthFunc(GL_ALWAYS);
                glDepthMask(GL_FALSE);
                glEnable(GL_BLEND);

                if (renderInfo.getState()->getUseModelViewAndProjectionUniforms())
                    renderInfo.getState()->applyModelViewAndProjectionUniformsIfRequired();

                local._leaders->draw(renderInfo);

                renderInfo.getState()->popStateSet();
                if (local._leaders->getUseGPU())
                {
                    renderInfo.getState()->popStateSet();
                }

                // the debug diagram
                if (local._voronoi.valid())
                {
                    local._voronoi->draw(renderInfo);
                }
            }
        }

        /**
        * Renders a single leaf. We already applied the projection matrix, so here we only
        * need to apply a modelview matrix that specifies the ortho offset of the drawable.
        *
        * Most of this code is copied from RenderLeaf::draw() -- but I removed all the code
        * dealing with nested bins, since decluttering does not support them.
        */
        void renderLeaf(osgUtil::RenderLeaf* leaf, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous, RunningState& rs)
        {
            osg::State& state = *renderInfo.getState();

            // don't draw this leaf if the abort rendering flag has been set.
            if (state.getAbortRendering())
            {
                //cout << "early abort"<<endl;
                return;
            }

            state.applyModelViewMatrix(leaf->_modelview.get());

            if (previous)
            {
                // apply state if required.
                osgUtil::StateGraph* prev_rg = previous->_parent;
                osgUtil::StateGraph* prev_rg_parent = prev_rg->_parent;
                osgUtil::StateGraph* rg = leaf->_parent;
                if (prev_rg_parent != rg->_parent)
                {
                    osgUtil::StateGraph::moveStateGraph(state, prev_rg_parent, rg->_parent);

                    // send state changes and matrix changes to OpenGL.
                    state.apply(rg->getStateSet());

                }
                else if (rg != prev_rg)
                {
                    // send state changes and matrix changes to OpenGL.
                    state.apply(rg->getStateSet());
                }
            }
            else
            {
                // apply state if required.
                osgUtil::StateGraph::moveStateGraph(state, NULL, leaf->_parent->_parent);

                state.apply(leaf->_parent->getStateSet());
            }

            // if we are using osg::Program which requires OSG's generated uniforms to track
            // modelview and projection matrices then apply them now.
            if (state.getUseModelViewAndProjectionUniforms())
                state.applyModelViewAndProjectionUniformsIfRequired();

            // apply the fading uniform
            const osg::Program::PerContextProgram* pcp = state.getLastAppliedProgramObject();
            if (pcp)
            {
                if (pcp != rs.lastPCP)
                {
                    pcp->apply(*_fade.get());
                }
            }
            rs.lastPCP = pcp;

            // draw the drawable
            leaf->_drawable->draw(renderInfo);

            if (leaf->_dynamic)
            {
                state.decrementDynamicObjectCount();
            }
        }
    };
} }

#endif // OSGEARTH_SCREEN_SPACE_LAYOUT_CALLOUT_H
