/* osgEarth
* Copyright 2025 Pelican Mapping
* MIT License
*/
#pragma once

#include <osgEarth/ScreenSpaceLayoutImpl>
#include <osgEarth/Text>
#include <osgEarth/LineDrawable>
#include <osgEarth/GLUtils>

// 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
//#define USE_WIDEST_SEGMENT_CENTROID_METHOD 1

#undef FADE_UNIFORM_NAME
#define FADE_UNIFORM_NAME "oe_declutter_fade"

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;
        }
    };
    struct WidthResult
    {
        osg::Vec3f left;
        osg::Vec3f right;
        osg::Vec3f center;
        float width;
    };

    WidthResult widestXSegment(const std::vector<osg::Vec3f>& poly)
    {
        const float EPS = 1e-6f;

        WidthResult best;
        best.left = osg::Vec3f(0.0f, 0.0f, 0.0f);
        best.right = osg::Vec3f(0.0f, 0.0f, 0.0f);
        best.center = osg::Vec3f(0.0f, 0.0f, 0.0f);
        best.width = -1.0f;

        for (std::size_t py = 0; py < poly.size(); ++py)
        {
            const float y = poly[py].y();

            std::vector<float> xs;

            for (std::size_t i = 0; i < poly.size(); ++i)
            {
                const osg::Vec3f& a = poly[i];
                const osg::Vec3f& b = poly[(i + 1) % poly.size()];

                // Skip horizontal edges
                if (std::fabs(a.y() - b.y()) < EPS)
                    continue;

                const float ymin = std::min(a.y(), b.y());
                const float ymax = std::max(a.y(), b.y());

                // Half-open interval avoids double-counting vertices
                if (y + EPS >= ymin && y < ymax - EPS)
                {
                    const float t = (y - a.y()) / (b.y() - a.y());
                    const float x = a.x() + t * (b.x() - a.x());
                    xs.push_back(x);
                }
            }

            // Include vertices lying exactly on this Y
            for (std::size_t i = 0; i < poly.size(); ++i)
            {
                if (std::fabs(poly[i].y() - y) < EPS)
                    xs.push_back(poly[i].x());
            }

            if (xs.size() < 2)
                continue;

            std::vector<float>::const_iterator minIt =
                std::min_element(xs.begin(), xs.end());

            std::vector<float>::const_iterator maxIt =
                std::max_element(xs.begin(), xs.end());

            const float x0 = *minIt;
            const float x1 = *maxIt;
            const float w = x1 - x0;

            if (w > best.width)
            {
                best.left = osg::Vec3f(x0, y, 0);
                best.right = osg::Vec3f(x1, y, 0);
                best.center = osg::Vec3f((x0 + x1) * 0.5f, y, 0);
                best.width = w;
            }
        }

        return best;
    }

    /**
    * 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;
            osg::Vec3d _pendingOffsetVector;
            double _pendingOffsetLength;
            unsigned _leaderLineIndex;
            bool _declutter;
            int _numFramesSinceMove;
            bool _pendingOffsetValid;

            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;
            osg::Vec3d _dprscale;

            void initDebug();
        };

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

        //! 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),
        _leaf(NULL),
        _frame(0u),
        _offsetVector(0,1,0),
        _offsetLength(0.0),
        _pendingOffsetVector(0,1,0),
        _pendingOffsetLength(0.0),
        _leaderLineIndex(INT_MAX),
        _declutter(false),
        _numFramesSinceMove(0),
        _pendingOffsetValid(false)
    {
        //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(0.3, 0.3, 0.3, 1)); // 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)
    {
        //nop
    }

    namespace {
        inline double clampTo(double x, double minValue, double maxValue) {
            return osg::maximum(minValue, osg::minimum(maxValue, x));
        }
    }

    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();
            element._pendingOffsetValid = false;
            element._numFramesSinceMove = 0;
        }
        else if (_resetWhenViewChanges && local._vpmChanged)
        {
            element._offsetVector = element._anchor - osg::Vec3d(0.5*vp->width(), 0.5*vp->height(), 0.0);
            element._offsetVector.normalize();
            element._pendingOffsetValid = false;
            element._numFramesSinceMove = 0;
        }

        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;
            osg::BoundingBox box = element._drawable->getBoundingBox();

            // scale the bb by the dpr
            double hw = 0.5 * (box.xMax() - box.xMin());
            double hh = 0.5 * (box.yMax() - box.yMin());
            box.xMin() -= (hw * (local._dprscale.x() - 1.0));
            box.xMax() += (hw * (local._dprscale.x() - 1.0));
            box.yMin() -= (hh * (local._dprscale.y() - 1.0));
            box.yMax() += (hh * (local._dprscale.y() - 1.0));

            const osg::Vec3d boxCenter(
                0.5*(box.xMin() + box.xMax()),
                0.5*(box.yMin() + box.yMax()),
                0.0);
            const double halfWidth = 0.5*(box.xMax() - box.xMin());
            const double halfHeight = 0.5*(box.yMax() - box.yMin());
            const double absX = osg::absolute(element._offsetVector.x());
            const double absY = osg::absolute(element._offsetVector.y());
            const double tx = absX > 1e-6 ? halfWidth / absX : DBL_MAX;
            const double ty = absY > 1e-6 ? halfHeight / absY : DBL_MAX;
            alignment = element._offsetVector * (osg::minimum(tx, ty) + margin) - boxCenter;

            leaf->_modelview->makeTranslate(pos + alignment);
            leaf->_modelview->preMultScale(local._dprscale);

            // 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);
            leaf->_modelview->preMultScale(local._dprscale);
        }
    }

    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;

        struct SiteCluster
        {
            SiteCluster(const osg::Vec2d& in_center, Element* in_element) :
                center(in_center)
            {
                elements.push_back(in_element);
            }

            osg::Vec2d center;
            std::vector<Element*> elements;
        };

        const double clusterDistance = 4.0;
        const double clusterDistance2 = clusterDistance * clusterDistance;
        std::vector<SiteCluster> siteClusters;

        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)
                    {
                        osg::Vec2d anchor(element._anchor.x(), element._anchor.y());
                        bool addedToCluster = false;

                        for (SiteCluster& cluster : siteClusters)
                        {
                            const double dx = anchor.x() - cluster.center.x();
                            const double dy = anchor.y() - cluster.center.y();
                            if (dx * dx + dy * dy <= clusterDistance2)
                            {
                                const double count = static_cast<double>(cluster.elements.size());
                                cluster.center.x() = (cluster.center.x() * count + anchor.x()) / (count + 1.0);
                                cluster.center.y() = (cluster.center.y() * count + anchor.y()) / (count + 1.0);
                                cluster.elements.push_back(&element);
                                addedToCluster = true;
                                break;
                            }
                        }

                        if (!addedToCluster)
                        {
                            siteClusters.emplace_back(anchor, &element);
                        }
                    }
                }
            }
        }

        for (SiteCluster& cluster : siteClusters)
        {
            std::sort(cluster.elements.begin(), cluster.elements.end(),
                [](const Element* lhs, const Element* rhs)
                {
                    return (intptr_t)lhs->_drawable < (intptr_t)rhs->_drawable;
                });

            if (cluster.elements.size() == 1)
            {
                Element* element = cluster.elements.front();
                jcv_point site{ element->_anchor.x(), element->_anchor.y() };
                local._points.emplace_back(site);
                local._lookup[site] = element;
            }
            else
            {
                const double minX = bounds.min.x + 0.001;
                const double maxX = bounds.max.x - 0.001;
                const double minY = bounds.min.y + 0.001;
                const double maxY = bounds.max.y - 0.001;
                const double radius = clusterDistance;

                for (std::size_t k = 0; k < cluster.elements.size(); ++k)
                {
                    const double angle = (2.0 * osg::PI * static_cast<double>(k)) / static_cast<double>(cluster.elements.size());
                    jcv_point site{
                        clampTo(cluster.center.x() + std::cos(angle) * radius, minX, maxX),
                        clampTo(cluster.center.y() + std::sin(angle) * radius, minY, maxY) };
                    local._points.emplace_back(site);
                    local._lookup[site] = cluster.elements[k];
                }
            }
        }

        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)
            {
#if defined(USE_WIDEST_SEGMENT_CENTROID_METHOD)

                // This centroid-finding method calculates the widest horizontal segment in the site,
                // and uses the midpoint of that segment as the centroid. This seems to provide a
                // better centroid for long skinny sites.
                std::vector<osg::Vec3f> poly;
                const jcv_graphedge* edge = site->edges;
                while (edge)
                {
                    poly.emplace_back(edge->pos[0].x, edge->pos[0].y, 0);

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

                    edge = edge->next;
                }
                WidthResult r = widestXSegment(poly);
                osg::Vec3d centroid(r.center.x(), r.center.y(), 0);

#elif defined(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();
                if (newOffsetLength == 0.0)
                    continue;
                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 (dot < 0.9)
                {
                    const double pendingDot = element->_pendingOffsetValid ?
                        newOffset * element->_pendingOffsetVector :
                        -1.0;

                    if (!element->_pendingOffsetValid || pendingDot < 0.98)
                    {
                        element->_pendingOffsetVector = newOffset;
                        element->_pendingOffsetLength = newOffsetLength;
                        element->_pendingOffsetValid = true;
                        element->_numFramesSinceMove = 1;
                    }
                    else
                    {
                        element->_pendingOffsetVector = newOffset;
                        element->_pendingOffsetLength = newOffsetLength;
                        element->_numFramesSinceMove++;
                    }

                    if (element->_numFramesSinceMove >= 60)
                    {
                        element->_offsetVector = element->_pendingOffsetVector;
                        element->_offsetLength = element->_pendingOffsetLength;
                        element->_pendingOffsetValid = false;
                        element->_numFramesSinceMove = 0;
                    }
                }
                else
                {
                    element->_offsetVector = prevOffset * 0.85 + newOffset * 0.15;
                    element->_numFramesSinceMove = 0;
                    element->_offsetLength = newOffsetLength;
                    element->_pendingOffsetValid = false;
                    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);

        auto dpr = Registry::instance()->getDevicePixelRatio(cam->getGraphicsContext());
        local._dprscale.set(dpr, dpr, 1.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();
            }
        }
    };
} }
