/* -*-c++-*- */
/* osgEarth - Geospatial SDK for OpenSceneGraph
 * Copyright 2018 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.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * 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_IMGUI_SCENE_GRAPH_GUI
#define OSGEARTH_IMGUI_SCENE_GRAPH_GUI

#include "ImGui"
#include <osgEarth/AnnotationUtils>
#include <osgEarth/TerrainEngineNode>
#include <osgEarth/PagedNode>
#include <osgEarth/MetadataNode>
#include <osgEarth/Viewpoint>
#include <osgEarth/ViewFitter>
#include <osgEarth/ThreeDTilesLayer>
#include <osgEarth/EarthManipulator>
#include <osgEarth/StringUtils>
#include <osgDB/FileUtils>
#include <osgDB/ReadFile>
#include <osgDB/WriteFile>
#include <osg/TextureBuffer>

#include <osg/io_utils>
#include <osg/PolygonMode>

#if defined(__has_include)
#if __has_include(<third_party/portable-file-dialogs/portable-file-dialogs.h>)
#include <third_party/portable-file-dialogs/portable-file-dialogs.h>
#define HAS_PFD
#endif
#endif

struct PrepareForWriting : public osg::NodeVisitor
{
    PrepareForWriting() : osg::NodeVisitor()
    {
        setTraversalMode(TRAVERSE_ALL_CHILDREN);
        setNodeMaskOverride(~0);
    }

    void apply(osg::Node& node)
    {
        apply(node.getStateSet());
        applyUserData(node);
        traverse(node);
    }

    void apply(osg::Drawable& drawable)
    {
        apply(drawable.getStateSet());
        applyUserData(drawable);

        osg::Geometry* geom = drawable.asGeometry();
        if (geom)
            apply(geom);
    }

    void apply(osg::Geometry* geom)
    {
        // This detects any NULL vertex attribute arrays and then populates them.
        // Do this because a NULL VAA will crash the OSG serialization reader (osg 3.4.0)
        osg::Geometry::ArrayList& arrays = geom->getVertexAttribArrayList();
        for (osg::Geometry::ArrayList::iterator i = arrays.begin(); i != arrays.end(); ++i)
        {
            if (i->get() == 0L)
            {
                *i = new osg::FloatArray();
                i->get()->setBinding(osg::Array::BIND_OFF);
            }
        }

        // Get rid of any kdtree since osg can't serialize it.
        geom->setShape(nullptr);
    }

    void apply(osg::StateSet* ss)
    {
        if (!ss) return;

        osg::StateSet::AttributeList& a0 = ss->getAttributeList();
        for (osg::StateSet::AttributeList::iterator i = a0.begin(); i != a0.end(); ++i)
        {
            osg::StateAttribute* sa = i->second.first.get();
            applyUserData(*sa);
        }

        // Disable the texture image-unref feature so we can share the resource
        // across cached tiles.
        osg::StateSet::TextureAttributeList& a = ss->getTextureAttributeList();
        for (osg::StateSet::TextureAttributeList::iterator i = a.begin(); i != a.end(); ++i)
        {
            osg::StateSet::AttributeList& b = *i;
            for (osg::StateSet::AttributeList::iterator j = b.begin(); j != b.end(); ++j)
            {
                osg::StateAttribute* sa = j->second.first.get();
                if (sa)
                {
                    osg::Texture* tex = dynamic_cast<osg::Texture*>(sa);
                    if (tex)
                    {
                        tex->setUnRefImageDataAfterApply(false);
                    }
                    else
                    {
                        applyUserData(*sa);
                    }
                }
            }
        }

        applyUserData(*ss);
    }

    void applyUserData(osg::Object& object)
    {
        object.setUserDataContainer(0L);
    }
};

struct WriteExternalImages : public osgEarth::TextureAndImageVisitor
{
    std::string _destinationPath;

    WriteExternalImages(const std::string& destinationPath)
        : TextureAndImageVisitor(),
        _destinationPath(destinationPath)
    {
        setTraversalMode(TRAVERSE_ALL_CHILDREN);
        setNodeMaskOverride(~0L);
    }

    void apply(osg::Texture & tex)
    {
        if (dynamic_cast<osg::TextureBuffer*>(&tex) != 0L)
        {
            // skip texture buffers, they need no prep and
            // will be inlined as long as they have a write hint
            // set to STORE_INLINE.
        }
        else
        {
            osgEarth::TextureAndImageVisitor::apply(tex);
        }
    }

    void apply(osg::Image & image)
    {
        std::string path = image.getFileName();
        if (path.empty())
        {
            OE_WARN << "ERROR image with blank filename.\n";
            return;
        }

        std::string format = "dds";
        unsigned int hash = osgEarth::hashString(path);
        std::string relativeName = osgEarth::Util::Stringify() << "images/" <<  hash << "." << format;
        std::string filename = osgDB::concatPaths(_destinationPath, relativeName);
        image.setFileName(relativeName);
        image.setWriteHint(osg::Image::EXTERNAL_FILE);
        osg::ref_ptr < osgDB::Options > options = new osgDB::Options;
        options->setOptionString("ddsNoAutoFlipWrite");
        osgDB::makeDirectoryForFile(filename);
        osgDB::writeImageFile(image, filename, options.get());
    }
};

namespace osgEarth
{
    namespace GUI
    {
        using namespace osgEarth;
        using namespace osgEarth::Util;


        struct ArrayStats
        {
            unsigned int arrayCount = 0;
            unsigned int elementCount = 0;
            unsigned int size = 0;
        };

        struct MemoryStats
        {
            void reset()
            {
                arrayStats.clear();
                totalDataSize = 0;
                totalGeometries = 0;
                totalPrimitiveSets = 0;
                computed = false;
            }

            void addArray(osg::Drawable::AttributeTypes index, const osg::Array* array)
            {
                if (array && array->getNumElements() > 0)
                {
                    arrayStats[index].arrayCount += 1;
                    arrayStats[index].elementCount += array->getNumElements();;
                    arrayStats[index].size += array->getTotalDataSize();
                    totalDataSize += array->getTotalDataSize();                 
                }
                computed = true;
            }

            void addGeometry(const osg::Geometry& geometry)
            {
                if (geometry.getVertexArray())
                {
                    addArray(osg::Geometry::VERTICES, geometry.getVertexArray());
                }

                if (geometry.getNormalArray())
                {
                    addArray(osg::Geometry::NORMALS, geometry.getNormalArray());
                }

                if (geometry.getColorArray())
                {
                    addArray(osg::Geometry::COLORS, geometry.getColorArray());
                }

                if (geometry.getSecondaryColorArray())
                {
                    addArray(osg::Geometry::SECONDARY_COLORS, geometry.getSecondaryColorArray());
                }

                if (geometry.getFogCoordArray())
                {
                    addArray(osg::Geometry::FOG_COORDS, geometry.getFogCoordArray());
                }

                const auto& texCoordArrays = geometry.getTexCoordArrayList();
                for (unsigned int i = 0; i < texCoordArrays.size(); ++i)
                {
                    if (texCoordArrays[i].valid())
                    {
                        addArray((osg::Drawable::AttributeTypes)(osg::Geometry::TEXTURE_COORDS_0 + i), texCoordArrays[i].get());
                    }
                }

                const osg::Geometry::ArrayList& arrays = geometry.getVertexAttribArrayList();
                for (unsigned int i = 0; i < arrays.size(); ++i)
                {
                    if (arrays[i].valid())
                    {
                        addArray((osg::Drawable::AttributeTypes)i, arrays[i].get());
                    }
                }

                ++totalGeometries;

                totalPrimitiveSets += geometry.getNumPrimitiveSets();
            }

            std::map< osg::Drawable::AttributeTypes, ArrayStats > arrayStats;
            unsigned int totalDataSize = 0;
            unsigned int totalGeometries = 0;
            unsigned int totalPrimitiveSets = 0;
            bool computed = false;
        };

        class ComputeMemoryStatsVisitor : public osg::NodeVisitor
        {
        public:
            ComputeMemoryStatsVisitor():
                osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN)
            {
            }

            virtual void apply( osg::Geometry& geometry ) override
            {
                stats.addGeometry(geometry);
            }

            MemoryStats stats;
        };

        template<typename T>
        std::string printArrayValue(const T* array)
        {
            std::stringstream buf;
            unsigned int size = array->size();
            for (unsigned i = 0; i < size; ++i)
            {
                if (i > 0)
                {
                    buf << ", ";
                }
                buf << (*array)[i];
            }
            return buf.str();
        }

        static const char* arrayBindingToString(osg::Array::Binding binding)
        {
            switch (binding)
            {
            case osg::Array::BIND_PER_VERTEX:
                return "BIND_PER_VERTEX";
            case osg::Array::BIND_OVERALL:
                return "BIND_OVERALL";
            case osg::Array::BIND_PER_PRIMITIVE_SET:
                return "BIND_PER_PRIMITIVE_SET";
            case osg::Array::BIND_OFF:
                return "BIND_OFF";
            default:
                return "BIND_UNDEFINED";
            }
        }

        static const char* attributeTypeToString(osg::Drawable::AttributeType attributeType)
        {
            switch (attributeType)
            {
            case osg::Drawable::VERTICES:
                return "VERTICES";
            case osg::Drawable::WEIGHTS:
                return "WEIGHTS";
            case osg::Drawable::NORMALS:
                return "NORMALS";
            case osg::Drawable::COLORS:
                return "COLORS";
            case osg::Drawable::SECONDARY_COLORS:
                return "SECONDARY_COLORS";
            case osg::Drawable::FOG_COORDS:
                return "FOG_COORDS";
            case osg::Drawable::ATTRIBUTE_6:
                return "ATTRIBUTE_6";
            case osg::Drawable::ATTRIBUTE_7:
                return "ATTRIBUTE_7";
            case osg::Drawable::TEXTURE_COORDS_0:
                return "TEXTURE_COORDS_0";
            case osg::Drawable::TEXTURE_COORDS_1:
                return "TEXTURE_COORDS_1";
            case osg::Drawable::TEXTURE_COORDS_2:
                return "TEXTURE_COORDS_2";
            case osg::Drawable::TEXTURE_COORDS_3:
                return "TEXTURE_COORDS_3";
            case osg::Drawable::TEXTURE_COORDS_4:
                return "TEXTURE_COORDS_4";
            case osg::Drawable::TEXTURE_COORDS_5:
                return "TEXTURE_COORDS_5";
            case osg::Drawable::TEXTURE_COORDS_6:
                return "TEXTURE_COORDS_6";
            case osg::Drawable::TEXTURE_COORDS_7:
                return "TEXTURE_COORDS_7";
            default:
                return "Uknown Array";
            }
        }

        template<typename T>
        void printArrayTablePretty(const std::string& name, const T* array)
        {
            if (!array) return;
            static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_SizingFixedFit;

            const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing();

            // When using ScrollX or ScrollY we need to specify a size for our table container!
            // Otherwise by default the table will fit all available space, like a BeginChild() call.
            ImGui::Text(typeid(*array).name());
            ImGui::Text("Binding %s", arrayBindingToString(array->getBinding()));
            ImGui::Text("Size: %dkb", (unsigned int)(array->getTotalDataSize() / 1024.0f));
            ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 8);
            if (ImGui::BeginTable(name.c_str(), 2, flags, outer_size))
            {
                ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
                ImGui::TableSetupColumn("Index", ImGuiTableColumnFlags_None);
                ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_None);
                ImGui::TableHeadersRow();

                ImGuiListClipper clipper;
                clipper.Begin(array->size());
                while (clipper.Step())
                {
                    for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
                    {
                        ImGui::TableNextRow();

                        ImGui::TableSetColumnIndex(0);
                        ImGui::Text("%d", i);

                        ImGui::TableSetColumnIndex(1);

                        std::stringstream val;
                        val << (*array)[i];
                        ImGui::Text(val.str().c_str());
                    }
                }
                ImGui::EndTable();
            }
        }

        inline void printArrayTable(const osg::Array* array)
        {
            switch (array->getType())
            {
            case osg::Array::ByteArrayType:
                printArrayTablePretty("Data", static_cast<const osg::ByteArray*>(array));
                break;
            case osg::Array::ShortArrayType:
                printArrayTablePretty("Data", static_cast<const osg::ShortArray*>(array));
                break;
            case osg::Array::IntArrayType:
                printArrayTablePretty("Data", static_cast<const osg::IntArray*>(array));
                break;
            case osg::Array::UByteArrayType:
                printArrayTablePretty("Data", static_cast<const osg::UByteArray*>(array));
                break;
            case osg::Array::UShortArrayType:
                printArrayTablePretty("Data", static_cast<const osg::UShortArray*>(array));
                break;
            case osg::Array::UIntArrayType:
                printArrayTablePretty("Data", static_cast<const osg::UIntArray*>(array));
                break;
            case osg::Array::FloatArrayType:
                printArrayTablePretty("Data", static_cast<const osg::FloatArray*>(array));
                break;
            case osg::Array::DoubleArrayType:
                printArrayTablePretty("Data", static_cast<const osg::DoubleArray*>(array));
                break;
            case osg::Array::Vec2bArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2bArray*>(array));
                break;
            case osg::Array::Vec3bArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3bArray*>(array));
                break;
            case osg::Array::Vec4bArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4bArray*>(array));
                break;
            case osg::Array::Vec2sArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2sArray*>(array));
                break;
            case osg::Array::Vec3sArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3sArray*>(array));
                break;
            case osg::Array::Vec4sArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4sArray*>(array));
                break;
            case osg::Array::Vec2iArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2iArray*>(array));
                break;
            case osg::Array::Vec3iArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3iArray*>(array));
                break;
            case osg::Array::Vec4iArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4iArray*>(array));
                break;
#if 0
            case osg::Array::Vec2ubArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2ubArray*>(array));
                break;
            case osg::Array::Vec3ubArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3ubArray*>(array));
                break;
#endif
            case osg::Array::Vec4ubArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4ubArray*>(array));
                break;
#if 0
            case osg::Array::Vec2usArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2usArray*>(array));
                break;
            case osg::Array::Vec3usArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3usArray*>(array));
                break;
            case osg::Array::Vec4usArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4usArray*>(array));
                break;
            case osg::Array::Vec2uiArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2uiArray*>(array));
                break;
            case osg::Array::Vec3uiArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3uiArray*>(array));
                break;
            case osg::Array::Vec4uiArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4uiArray*>(array));
                break;
#endif
            case osg::Array::Vec2ArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2Array*>(array));
                break;
            case osg::Array::Vec3ArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3Array*>(array));
                break;
            case osg::Array::Vec4ArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4Array*>(array));
                break;
            case osg::Array::Vec2dArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec2dArray*>(array));
                break;
            case osg::Array::Vec3dArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec3dArray*>(array));
                break;
            case osg::Array::Vec4dArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Vec4dArray*>(array));
                break;
#if 0
            case osg::Array::MatrixArrayType:
                printArrayTablePretty("Data", static_cast<const osg::MatrixfArray*>(array));
                break;
            case osg::Array::MatrixdArrayType:
                printArrayTablePretty("Data", static_cast<const osg::MatrixdArray*>(array));
                break;
            case osg::Array::QuatArrayType:
                printArrayTablePretty("Data", static_cast<const osg::QuatArray*>(array));
                break;
#endif
            case osg::Array::UInt64ArrayType:
                printArrayTablePretty("Data", static_cast<const osg::UInt64Array*>(array));
                break;
            case osg::Array::Int64ArrayType:
                printArrayTablePretty("Data", static_cast<const osg::Int64Array*>(array));
                break;
            default:
                ImGui::Text("Unknown array type");
                break;
            }
        }



        static std::string printUniformValue(osg::Uniform* uniform)
        {
            if (uniform->getFloatArray()) return printArrayValue(uniform->getFloatArray());
            if (uniform->getDoubleArray()) return printArrayValue(uniform->getDoubleArray());
            if (uniform->getIntArray()) return printArrayValue(uniform->getIntArray());
            if (uniform->getUIntArray()) return printArrayValue(uniform->getUIntArray());
            if (uniform->getUInt64Array()) return printArrayValue(uniform->getUInt64Array());
            if (uniform->getInt64Array()) return printArrayValue(uniform->getInt64Array());

            return "";
        }


        class SceneGraphGUI : public BaseGUI
        {
        public:

            osg::ref_ptr<osg::Node> getSelectedNode()
            {
                if (!_selectedNodePath.empty())
                {
                    return _selectedNodePath.back();
                }
                return nullptr;
            }

            const osg::RefNodePath& getSelectedNodePath()
            {
                return _selectedNodePath;
            }

            void setSelectedNodePath(const osg::NodePath& nodePath)
            {
                _selectedNodePath.clear();
                for (auto itr = nodePath.begin(); itr != nodePath.end(); ++itr)
                {
                    _selectedNodePath.push_back(*itr);
                }
                _boundsDirty = true;
                _memoryStats.reset();
            }

            struct SelectNodeHandler : public osgGA::GUIEventHandler
            {
                SceneGraphGUI* _owner;

                SelectNodeHandler(SceneGraphGUI* owner) :
                    _owner(owner)
                {
                }

                bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
                {
                    osgViewer::View* view = static_cast<osgViewer::View*>(aa.asView());

                    if (ea.getEventType() == ea.PUSH && ea.getButton() == ea.LEFT_MOUSE_BUTTON && ea.getModKeyMask() & osgGA::GUIEventAdapter::MODKEY_CTRL)
                    {
                        float w = 5.0;
                        float h = 5.0;

                        float x = ea.getX();
                        float y = ea.getY();

                        osg::ref_ptr< osgUtil::PolytopeIntersector> picker = new osgUtil::PolytopeIntersector(osgUtil::Intersector::WINDOW, x - w, y - h, x + w, y + h);
                        picker->setIntersectionLimit(osgUtil::Intersector::LIMIT_NEAREST);
                        osgUtil::IntersectionVisitor iv(picker.get());
                        // This is a hack, but we set the node mask to something other than 1 << 2 which is what the "selected bounds" node mask is set to to avoid picking it.
                        // We should rework this later into something more formal so we can add widget type nodes that aren't generally interacted with.
                        iv.setTraversalMask((1 << 1));
                        view->getCamera()->accept(iv);
                        if (picker->containsIntersections())
                        {
                            osg::NodePath nodePath = picker->getIntersections().begin()->nodePath;
                            nodePath.push_back(picker->getIntersections().begin()->drawable.get());
                            _owner->setSelectedNodePath(nodePath);

                        }
                    }
                    return false;
                }
            };

            class SceneHierarchyVisitor : public osg::NodeVisitor
            {
            public:
                SceneGraphGUI* _owner;

                SceneHierarchyVisitor(SceneGraphGUI* owner) :
                    osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN),
                    _owner(owner)
                {
                    setNodeMaskOverride(~0);
                }

                void apply(osg::Node& node)
                {
                    // Non groups act as leaf nodes
                    std::string label = getLabel(node);
                    ImGuiTreeNodeFlags node_flags = base_flags;
                    node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
                    if (_owner->getSelectedNode() == &node)
                    {
                        node_flags |= ImGuiTreeNodeFlags_Selected;
                    }

                    bool nodeOff = node.getNodeMask() == 0u;
                    if (nodeOff)
                    {
                        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
                    }
                    ImGui::TreeNodeEx(&node, node_flags, label.c_str());
                    if (nodeOff)
                    {
                        ImGui::PopStyleColor();
                    }
                    if (ImGui::IsItemClicked())
                    {
                        _owner->setSelectedNodePath(getNodePath());
                    }
                    if (ImGui::BeginDragDropTarget())
                    {
                        if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("TEXTURE"))
                        {
                            osg::Texture2D* droppedTexture = *(osg::Texture2D**)payload->Data;
                            if (droppedTexture)
                            {
                                node.getOrCreateStateSet()->setTextureAttributeAndModes(0, droppedTexture, osg::StateAttribute::ON);
                            }
                        }
                        ImGui::EndDragDropTarget();
                    }
                }

                void apply(osg::Group& node)
                {
                    std::string label = getLabel(node);
                    ImGuiTreeNodeFlags node_flags = base_flags;
                    if (_owner->getSelectedNode() == &node)
                    {
                        node_flags |= ImGuiTreeNodeFlags_Selected;
                    }

                    if (isInSelectedNodePath(&node))
                    {
                        ImGui::SetNextItemOpen(true, ImGuiCond_Once);
                    }

                    bool nodeOff = node.getNodeMask() == 0u;
                    if (nodeOff)
                    {
                        ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]);
                    }

                    bool node_open = ImGui::TreeNodeEx(&node, node_flags, label.c_str());
                    if (nodeOff)
                    {
                        ImGui::PopStyleColor();
                    }
                    if (ImGui::IsItemClicked())
                    {
                        _owner->setSelectedNodePath(getNodePath());
                    }
                    if (ImGui::BeginDragDropTarget())
                    {
                        if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("NODE"))
                        {
                            osg::Node* droppedNode = *(osg::Node**)payload->Data;
                            if (droppedNode)
                            {
                                node.addChild(droppedNode);
                            }
                        }
                        else if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("TEXTURE"))
                        {
                            osg::Texture2D* droppedTexture = *(osg::Texture2D**)payload->Data;
                            if (droppedTexture)
                            {
                                node.getOrCreateStateSet()->setTextureAttributeAndModes(0, droppedTexture, osg::StateAttribute::ON);
                            }
                        }
                        ImGui::EndDragDropTarget();
                    }

                    if (node_open)
                    {
                        traverse(node);
                        ImGui::TreePop();
                    }
                }

                std::string getLabel(osg::Node& node)
                {
                    std::stringstream buf;
                    buf << node.getName() << " [" << typeid(node).name() << "]";

                    osg::Group* group = node.asGroup();
                    if (group)
                    {
                        buf << " (" << group->getNumChildren() << ")";
                    }

                    return buf.str();
                }

                bool isInSelectedNodePath(osg::Node* node)
                {
                    auto selectedNodePaths = _owner->getSelectedNodePath();

                    for (unsigned int i = 0; i < selectedNodePaths.size(); i++)
                    {
                        if (node == selectedNodePaths[i].get())
                        {
                            return true;
                        }
                    }
                    return false;
                }

                ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanAvailWidth;
            };

            SceneGraphGUI() :
                BaseGUI("Scene Graph Inspector"),
                _installedSelectNodeHandler(false),
                _selectedBounds(0),
                _node(nullptr)
            {
            }

            void draw(osg::RenderInfo& ri) override
            {
                if (!isVisible())
                    return;

                if (ImGui::Begin(name(), visible()))
                {
                    if (_node == nullptr)
                        _node = ri.getCurrentCamera();

                    if (!_mapNode.valid())
                        _mapNode = osgEarth::findTopMostNodeOfType<MapNode>(ri.getCurrentCamera());

                    if (!_installedSelectNodeHandler)
                    {
                        auto view = dynamic_cast<osgViewer::View*>(ri.getView());
                        view->addEventHandler(new SelectNodeHandler(this));
                        _installedSelectNodeHandler = true;
                    }

                    auto size = ImGui::GetWindowSize();

                    if (ImGui::CollapsingHeader("Scene"))
                    {
                        // Change the size based on whether the properties panel is
                        ImGui::BeginChild("Scene", ImVec2(0, getSelectedNode() && _propertiesExpanded ? 0.6f * size.y : 0.90f * size.y), false, ImGuiWindowFlags_HorizontalScrollbar);

                        SceneHierarchyVisitor v(this);
                        _node->accept(v);                        

                        ImGui::EndChild();
                    }

                    if (getSelectedNode())
                    {
                        _propertiesExpanded = ImGui::CollapsingHeader("Properties", ImGuiTreeNodeFlags_DefaultOpen);
                        if (_propertiesExpanded)
                        {
                            ImGui::BeginChild("Properties");
                            auto view = dynamic_cast<osgViewer::View*>(ri.getView());
                            auto manip = dynamic_cast<EarthManipulator*>(view->getCameraManipulator());
                            properties(getSelectedNode(), ri, manip, _mapNode.get());
                            ImGui::EndChild();
                        }
                    }
                    ImGui::End();
                }
                // If they closed the tool deselect the node.
                if (!isVisible() && getSelectedNode())
                {
                    osg::NodePath empty;
                    setSelectedNodePath(empty);
                }

                if (_boundsDirty)
                {
                    updateBoundsDebug(_mapNode.get());
                }
            }

            void updateBoundsDebug(MapNode* mapNode)
            {
                if (_selectedBounds)
                {
                    mapNode->removeChild(_selectedBounds);
                    _selectedBounds = nullptr;
                }

                auto selectedNode = getSelectedNode();

                if (selectedNode)
                {
                    osg::RefNodePath refNodePath = getSelectedNodePath();
                    osg::NodePath nodePath;
                    for (unsigned int i = 0; i < refNodePath.size(); ++i)
                    {
                        nodePath.push_back(refNodePath[i].get());
                    }

                    if (nodePath.back()->asTransform())
                    {
                        nodePath.pop_back();
                    }

                    osg::Matrixd localToWorld = osg::computeLocalToWorld(nodePath);

                    osg::Drawable* drawable = selectedNode->asDrawable();
                    if (drawable)
                    {
                        osg::BoundingBoxd bb = drawable->getBoundingBox();
                        if (bb.valid())
                        {
                            _selectedBounds = new osg::MatrixTransform;
                            _selectedBounds->setName("Bounds");
                            _selectedBounds->setMatrix(localToWorld);

                            osg::MatrixTransform* center = new osg::MatrixTransform;
                            center->addChild(AnnotationUtils::createBoundingBox(bb, Color::Yellow));
                            _selectedBounds->addChild(center);

                            _selectedBounds->setNodeMask(1 << 2);
                            mapNode->addChild(_selectedBounds);
                        }
                    }
                    else
                    {
                        osg::BoundingSphered bs = osg::BoundingSphered(selectedNode->getBound().center(), selectedNode->getBound().radius());
                        if (bs.radius() > 0)
                        {
                            _selectedBounds = new osg::MatrixTransform;
                            _selectedBounds->setName("Bounds");
                            _selectedBounds->setMatrix(localToWorld);

                            osg::MatrixTransform* center = new osg::MatrixTransform;
                            center->setMatrix(osg::Matrix::translate(bs.center()));
                            center->addChild(AnnotationUtils::createSphere(selectedNode->getBound().radius(), Color::Yellow));
                            _selectedBounds->addChild(center);

                            _selectedBounds->getOrCreateStateSet()->setAttributeAndModes(new osg::PolygonMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE), 1);
                            _selectedBounds->setNodeMask(1 << 2);
                            mapNode->addChild(_selectedBounds);
                        }
                    }
                }

                _boundsDirty = false;
            }


            void properties(osg::Node* node, osg::RenderInfo& ri, EarthManipulator* manip, MapNode* mapNode)
            {
                if (manip && ImGui::Button("Zoom to"))
                {
                    osg::NodePath nodePath = node->getParentalNodePaths()[0];
                    osg::Matrixd localToWorld = osg::computeLocalToWorld(nodePath);

                    osg::BoundingSphered bs(node->getBound().center(), node->getBound().radius());
                    if (bs.valid())
                    {
                        bs.center() += localToWorld.getTrans();
                        osg::Vec3d c = bs.center();
                        double r = bs.radius();
                        const SpatialReference* mapSRS = mapNode->getMap()->getSRS();

                        std::vector<GeoPoint> points;
                        GeoPoint p;
                        p.fromWorld(mapSRS, osg::Vec3d(c.x() + r, c.y(), c.z())); points.push_back(p);
                        p.fromWorld(mapSRS, osg::Vec3d(c.x() - r, c.y(), c.z())); points.push_back(p);
                        p.fromWorld(mapSRS, osg::Vec3d(c.x(), c.y() + r, c.z())); points.push_back(p);
                        p.fromWorld(mapSRS, osg::Vec3d(c.x(), c.y() - r, c.z())); points.push_back(p);
                        p.fromWorld(mapSRS, osg::Vec3d(c.x(), c.y(), c.z() + r)); points.push_back(p);
                        p.fromWorld(mapSRS, osg::Vec3d(c.x(), c.y(), c.z() - r)); points.push_back(p);

                        ViewFitter fitter(mapNode->getMap()->getSRS(), ri.getCurrentCamera());
                        Viewpoint vp;
                        if (fitter.createViewpoint(points, vp))
                        {
                            manip->setViewpoint(vp, 2.0);
                        }
                    }
                }

#ifdef HAS_PFD
                if (ImGui::Button("Export"))
                {
                    auto f = pfd::save_file("Save file", pfd::path::home());
                    if (!f.result().empty())
                    {
                        std::string filename = f.result();
                        osgDB::makeDirectoryForFile(filename);

                        PrepareForWriting prepare;
                        node->accept(prepare);
                        std::string path = osgDB::getFilePath(filename);

                        // Maybe make this optional
                        WriteExternalImages write(path);
                        node->accept(write);

                        osg::ref_ptr< osgDB::Options > writeOptions = new osgDB::Options;
                        osgDB::writeNodeFile(*node, filename, writeOptions.get());
                    }
                }

                osg::Group* group = node->asGroup();
                if (group)
                {
                    if (ImGui::Button("Add Node"))
                    {
                        auto f = pfd::open_file("Choose files to read", pfd::path::home(),
                            { "All Files", "*" },
                            pfd::opt::multiselect);

                        for (auto const& name : f.result())
                        {
                            osg::ref_ptr< osg::Node > node = osgDB::readNodeFile(name);
                            if (node.valid())
                            {
                                Registry::instance()->shaderGenerator().run(node.get());
                                group->addChild(node.get());
                            }
                        }
                    }
                }
#endif

                bool visible = node->getNodeMask() != 0;
                if (ImGui::Checkbox("Visible", &visible))
                {
                    node->setNodeMask(visible);
                }

                if (ImGui::TreeNode("General"))
                {
                    ImGui::Text("Type %s", typeid(*node).name());
                    ImGui::Text("Name %s", node->getName().c_str());

                    osg::Drawable* drawable = node->asDrawable();
                    if (drawable)
                    {
                        osg::BoundingBox bbox = drawable->getBoundingBox();
                        double width = bbox.xMax() - bbox.xMin();
                        double depth = bbox.yMax() - bbox.yMin();
                        double height = bbox.zMax() - bbox.zMin();

                        ImGui::Text("Bounding box (local) center=(%.3f, %.3f, %.3f) dimensions=%.3f x %.3f %.3f", bbox.center().x(), bbox.center().y(), bbox.center().z(), width, depth, height);
                    }
                    else
                    {
                        osg::BoundingSphere bs = node->getBound();
                        ImGui::Text("Bounding sphere (local) center=(%.3f, %.3f, %.3f) radius=%.3f", bs.center().x(), bs.center().y(), bs.center().z(), bs.radius());
                    }

                    ImGui::Text("Reference count %d", node->referenceCount());

                    ImGui::TreePop();
                }

                if (ImGui::TreeNode("Memory Usage"))
                {
                    if (_memoryStats.computed)
                    {
                        ImGui::Text("Geometry Count: %d", _memoryStats.totalGeometries);
                        ImGui::Text("PrimitiveSets: %d", _memoryStats.totalPrimitiveSets);
                        ImGui::Text("Total Data Size %dkb", (unsigned int)(_memoryStats.totalDataSize / 1024.0f));                       
                        if (ImGui::BeginTable("Memory", 4, ImGuiTableFlags_Borders))
                        {
                            ImGui::TableSetupColumn("Attribute");
                            ImGui::TableSetupColumn("Array Count");
                            ImGui::TableSetupColumn("Element Count");
                            ImGui::TableSetupColumn("Size");
                            ImGui::TableHeadersRow();

                            for (auto& a : _memoryStats.arrayStats)
                            {
                                ImGui::TableNextColumn(); ImGui::Text(attributeTypeToString((osg::Drawable::AttributeType)a.first));
                                ImGui::TableNextColumn(); ImGui::Text("%d", a.second.arrayCount);
                                ImGui::TableNextColumn(); ImGui::Text("%d", a.second.elementCount);
                                ImGui::TableNextColumn(); ImGui::Text("%dkb", (unsigned int)(a.second.size / 1024.0f));
                            }
                            ImGui::EndTable();
                        }                        
                    }

                    if (ImGui::Button("Compute"))
                    {                        
                        ComputeMemoryStatsVisitor visitor;
                        getSelectedNode()->accept(visitor);
                        _memoryStats = visitor.stats;
                    }

                    ImGui::TreePop();
                }

                osg::MatrixTransform* matrixTransform = dynamic_cast<osg::MatrixTransform*>(node);
                if (matrixTransform)
                {
                    if (ImGui::TreeNode("Transform"))
                    {
                        bool dirty = false;
                        osg::Matrix matrix = matrixTransform->getMatrix();
                        osg::Vec3d translate = matrixTransform->getMatrix().getTrans();
                        if (ImGui::InputScalarN("Translation", ImGuiDataType_Double, translate._v, 3))
                        {
                            dirty = true;
                        }

                        osg::Vec3d scale = matrixTransform->getMatrix().getScale();
                        if (ImGui::InputScalarN("Scale", ImGuiDataType_Double, scale._v, 3))
                        {
                            dirty = true;
                        }
                        osg::Quat rot = matrixTransform->getMatrix().getRotate();
                        if (ImGui::InputScalarN("Rotation", ImGuiDataType_Double, rot._v, 4))
                        {
                            dirty = true;
                        }

                        if (dirty)
                        {
                            osg::Matrix newMatrix = osg::Matrix::translate(translate) * osg::Matrix::rotate(rot) * osg::Matrixd::scale(scale);
                            matrixTransform->setMatrix(newMatrix);
                        }
                        ImGui::TreePop();
                    }
                }

                osgEarth::PagedNode2* pagedNode2 = dynamic_cast<osgEarth::PagedNode2*>(node);
                if (pagedNode2)
                {
                    if (ImGui::TreeNode("PagedNode"))
                    {
                        ImGui::Text("Min range %.3f", pagedNode2->getMinRange());
                        ImGui::Text("Max range %.3f", pagedNode2->getMaxRange());
                        ImGui::TreePop();
                    }
                }

                osgEarth::Contrib::ThreeDTiles::ThreeDTileNode* threeDTiles = dynamic_cast<osgEarth::Contrib::ThreeDTiles::ThreeDTileNode*>(node);
                if (threeDTiles)
                {
                    if (ImGui::TreeNode("3D Tiles"))
                    {
                        std::string content = threeDTiles->getTile()->content()->getJSON().toStyledString();
                        ImGui::Text("Content");
                        ImGui::TextWrapped(content.c_str());
                    }
                }

                osg::Camera* cam = dynamic_cast<osg::Camera*>(node);
                if (cam)
                {
                    if (ImGui::TreeNode("osg::Camera"))
                    {
                        const std::string order[3] = { "PRE_RENDER", "NESTED_RENDER", "POST_RENDER" };
                        const std::string proj[2] = { "PERSP", "ORTHO" };
                        ImGui::Text("Render order: %s", order[(int)cam->getRenderOrder()].c_str());
                        ImGui::Text("Projection: %s", proj[ProjectionMatrix::isOrtho(cam->getProjectionMatrix()) ? 1 : 0].c_str());
                        ImGui::Text("Cull mask: %0x%.8X", cam->getCullMask());
                        ImGui::Text("Gfx context: %0x%.16X", (std::uintptr_t)cam->getGraphicsContext());
                        ImGui::Text("Gfx context ID: %d", cam->getGraphicsContext()->getState()->getContextID());
                        ImGui::TreePop();
                    }
                }

                osgEarth::MetadataNode* metadata = dynamic_cast<osgEarth::MetadataNode*>(node);
                if (metadata)
                {
                    if (ImGui::TreeNode("Metadata"))
                    {
                        static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_SizingFixedFit;

                        const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing();

                        // When using ScrollX or ScrollY we need to specify a size for our table container!
                        // Otherwise by default the table will fit all available space, like a BeginChild() call.
                        ImGui::Text("Features %d", metadata->getNumFeatures());
                        ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 8);
                        if (ImGui::BeginTable("Features", 4, flags, outer_size))
                        {
                            ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
                            ImGui::TableSetupColumn("Index", ImGuiTableColumnFlags_None);
                            ImGui::TableSetupColumn("Feature ID", ImGuiTableColumnFlags_None);
                            ImGui::TableSetupColumn("Object ID", ImGuiTableColumnFlags_None);
                            ImGui::TableSetupColumn("Visible", ImGuiTableColumnFlags_None);
                            ImGui::TableHeadersRow();

                            ImGuiListClipper clipper;
                            clipper.Begin(metadata->getNumFeatures());
                            while (clipper.Step())
                            {
                                for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
                                {
                                    ImGui::PushID(i);

                                    // TODO:  Add feature properties
                                    ImGui::TableNextRow();

                                    ImGui::TableSetColumnIndex(0);
                                    ImGui::Text("%d", i);

                                    ImGui::TableSetColumnIndex(1);
                                    ImGui::Text("%I64d", metadata->getFeature(i)->getFID());

                                    ImGui::TableSetColumnIndex(2);
                                    ImGui::Text("%I64d", metadata->getObjectID(i));

                                    ImGui::TableSetColumnIndex(3);
                                    bool visible = metadata->getVisible(i);
                                    if (ImGui::Checkbox("", &visible))
                                    {
                                        metadata->setVisible(i, visible);
                                    }

#if 0
                                    ImGui::TableSetColumnIndex(3);
                                    auto feature = metadata->getFeature(i);
                                    if (feature)
                                    {
                                        std::stringstream buf;
                                        for (auto& attr : feature->getAttrs())
                                        {
                                            buf << attr.first << "=" << attr.second.getString() << std::endl;
                                        }
                                        //std::string geojson = feature->getGeoJSON();
                                        ImGui::Text(buf.str().c_str());
                                    }
                                    else
                                    {
                                        ImGui::Text("Null feature");
                                    }
#endif

                                    ImGui::PopID();
                                }
                            }
                            ImGui::EndTable();
                        }

                        ImGui::TreePop();
                    }
                }

                osg::StateSet* stateset = node->getStateSet();
                if (stateset)
                {
                    if (ImGui::TreeNode("State Set"))
                    {
                        if (!stateset->getTextureAttributeList().empty() && ImGui::TreeNode("Textures"))
                        {
                            unsigned int textureWidth = 50;
                            for (unsigned int i = 0; i < stateset->getTextureAttributeList().size(); ++i)
                            {
                                osg::Texture2D* texture = dynamic_cast<osg::Texture2D*>(stateset->getTextureAttribute(i, osg::StateAttribute::TEXTURE));

                                if (texture)
                                {
                                    ImGui::PushID(texture);
                                    ImGui::BeginGroup();
                                    ImGui::Text("Unit %d", i);
                                    ImGui::Text("Name %s", texture->getName().c_str());
                                    ImGuiUtil::Texture(texture, ri, textureWidth);
                                    ImGui::EndGroup();
                                }
                            }
                            ImGui::TreePop();
                        }

                        VirtualProgram* virtualProgram = VirtualProgram::get(stateset);
                        if (virtualProgram && ImGui::TreeNode("Shaders"))
                        {
                            VirtualProgram::ShaderMap shaderMap;
                            virtualProgram->getShaderMap(shaderMap);
                            for (auto& s : shaderMap)
                            {
                                ImGui::Text("Name %s", s.second._shader->getName().c_str());
                                ImGui::Text("Location %s", FunctionLocationToString(s.second._shader->getLocation()));
                                ImGui::TextWrapped(s.second._shader->getShaderSource().c_str());
                                ImGui::Separator();
                            }
                            ImGui::TreePop();
                        }

                        if (!stateset->getUniformList().empty())
                        {
                            if (ImGui::TreeNode("Uniforms"))
                            {
                                if (ImGui::BeginTable("Uniforms", 3, ImGuiTableFlags_Borders))
                                {
                                    for (auto& u : stateset->getUniformList())
                                    {

                                        osg::Uniform* uniform = dynamic_cast<osg::Uniform*>(u.second.first.get());
                                        if (uniform)
                                        {
                                            // Name
                                            ImGui::TableNextColumn(); ImGui::Text(u.first.c_str());
                                            // Type
                                            ImGui::TableNextColumn(); ImGui::Text(osg::Uniform::getTypename(uniform->getType()));
                                            // Value
                                            ImGui::TableNextColumn(); ImGui::Text(printUniformValue(uniform).c_str());
                                        }
                                    }
                                    ImGui::EndTable();
                                }
                            }
                        }

                        ImGui::TreePop();
                    }
                }

                osg::Geometry* geometry = dynamic_cast<osg::Geometry*>(node);
                if (geometry && ImGui::TreeNode("Geometry Properties"))
                {
                    if (geometry->getVertexArray())
                    {
                        if (ImGui::TreeNode("verts", "VERTICES %d", geometry->getVertexArray()->getNumElements()))
                        {
                            printArrayTable(geometry->getVertexArray());
                            ImGui::TreePop();
                        }
                    }

                    if (geometry->getColorArray())
                    {
                        if (ImGui::TreeNode("colors", "COLORS %d", geometry->getColorArray()->getNumElements()))
                        {
                            printArrayTable(geometry->getColorArray());
                            ImGui::TreePop();
                        }
                    }

                    if (geometry->getNormalArray())
                    {
                        if (ImGui::TreeNode("normals", "NORMALS %d", geometry->getNormalArray()->getNumElements()))
                        {
                            printArrayTable(geometry->getNormalArray());
                            ImGui::TreePop();
                        }
                    }

                    auto& texCoordArrays = geometry->getTexCoordArrayList();
                    for (unsigned int i = 0; i < texCoordArrays.size(); ++i)
                    {
                        if (texCoordArrays[i].valid())
                        {
                            if (ImGui::TreeNode("texcoords", "TexCoords Unit %d %d", i, texCoordArrays[i]->getNumElements()))
                            {
                                printArrayTable(texCoordArrays[i].get());
                                ImGui::TreePop();
                            }
                        }
                    }

                    // Vetex attrib arrays
                    osg::Geometry::ArrayList& arrays = geometry->getVertexAttribArrayList();
                    bool validVertexAttributeArray = false;
                    for (unsigned int i = 0; i < arrays.size(); ++i)
                    {
                        if (arrays[i].valid())
                        {
                            validVertexAttributeArray = true;
                            break;
                        }
                    }

                    if (validVertexAttributeArray)
                    {

                        ImGui::Text("Vertex Attributes");
                        ImGui::Separator();
                        for (unsigned int i = 0; i < arrays.size(); ++i)
                        {
                            if (!arrays[i].valid() && arrays[i]->getBinding() != osg::Array::BIND_OFF) continue;

                            const char* arrayName = attributeTypeToString((osg::Drawable::AttributeType)i);

                            if (ImGui::TreeNode(arrayName, "%s %d", arrayName, arrays[i]->getNumElements()))
                            {
                                printArrayTable(arrays[i].get());
                                ImGui::TreePop();
                            }
                        }
                    }

                    for (auto &p : geometry->getPrimitiveSetList())
                    {
                        ImGui::Text("%s Mode=%s Primitives=%d Instances=%d", typeid(*p.get()).name(), GLModeToString(p->getMode()), p->getNumPrimitives(), p->getNumInstances());
                    }
                }
            }

            const char* GLModeToString(GLenum mode)
            {
                switch (mode)
                {
                case(GL_POINTS): return "GL_POINTS";
                case(GL_LINES): return "GL_LINES";
                case(GL_TRIANGLES): return "GL_TRIANGLES";
                case(GL_QUADS): return "GL_QUADS";
                case(GL_LINE_STRIP): return "GL_LINE_STRIP";
                case(GL_LINE_LOOP): return "GL_LINE_LOOP";
                case(GL_TRIANGLE_STRIP): return "GL_TRIANGLE_STRIP";
                case(GL_TRIANGLE_FAN): return "GL_TRIANGLE_FAN";
                case(GL_QUAD_STRIP): return "GL_QUAD_STRIP";
                case(GL_PATCHES): return "GL_PATCHES";
                case(GL_POLYGON): return "GL_POLYGON";
                default: return "Unknown";
                }
            }

            const char* FunctionLocationToString(VirtualProgram::FunctionLocation location)
            {
                using namespace osgEarth::ShaderComp;

                switch (location)
                {
                case LOCATION_VERTEX_MODEL: return "LOCATION_VERTEX_MODEL";
                case LOCATION_VERTEX_VIEW: return "LOCATION_VERTEX_VIEW";
                case LOCATION_VERTEX_CLIP: return "LOCATION_VERTEX_CLIP";
                case LOCATION_TESS_CONTROL: return "LOCATION_TESS_CONTROL";
                case LOCATION_TESS_EVALUATION: return "LOCATION_TESS_EVALUATION";
                case LOCATION_GEOMETRY: return "LOCATION_GEOMETRY";
                case LOCATION_FRAGMENT_COLORING: return "LOCATION_FRAGMENT_COLORING";
                case LOCATION_FRAGMENT_LIGHTING: return "LOCATION_FRAGMENT_LIGHTING";
                case LOCATION_FRAGMENT_OUTPUT: return "LOCATION_FRAGMENT_OUTPUT";
                case LOCATION_UNDEFINED: return "LOCATION_UNDEFINED";
                default: return "Undefined";
                }
            }

            bool _installedSelectNodeHandler;
            osg::MatrixTransform* _selectedBounds;
            osg::observer_ptr<osg::Node> _node;
            osg::observer_ptr<MapNode> _mapNode;
            osg::RefNodePath _selectedNodePath;
            bool _propertiesExpanded = true;
            bool _boundsDirty = false;
            MemoryStats _memoryStats;
        };
    }
}

#endif // OSGEARTH_IMGUI_SCENE_GRAPH_GUI
