DYT/Tool/OpenSceneGraph-3.6.5/include/osgEarth/ScreenSpaceLayoutCallout

773 lines
27 KiB
Plaintext
Raw Permalink Normal View History

2024-12-24 23:49:36 +00:00
/* -*-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