/* -*-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 */ #ifndef OSGEARTH_SCREEN_SPACE_LAYOUT_CALLOUT_H #define OSGEARTH_SCREEN_SPACE_LAYOUT_CALLOUT_H 1 #include // 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 _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; 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 _leaders; bool _leadersDirty; osg::Vec4f _leaderColor; std::vector _points; // TODO: use unordered_map? std::map _lookup; osg::ref_ptr _voronoi; void initDebug(); }; ScreenSpaceLayoutContext* _context; DeclutterSortFunctor* _customSortFunctor; PerObjectFastMap _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 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(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(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; ip]; 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 > _ortho2D; osg::ref_ptr _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& 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"<_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