From fe8b7bea2129647148896846b360683c7acd09dd Mon Sep 17 00:00:00 2001
From: Tobias Dammers <tdammers@gmail.com>
Date: Sat, 15 Jan 2022 18:20:35 +0100
Subject: [PATCH] Marker pins

---
 src/Model/modelmgr.cxx            |  41 +++++++---
 src/Scenery/CMakeLists.txt        |   2 +
 src/Scenery/marker.cxx            | 119 ++++++++++++++++++++++++++++++
 src/Scenery/marker.hxx            |  16 ++++
 src/Scripting/NasalPositioned.cxx |   3 +
 5 files changed, 171 insertions(+), 10 deletions(-)
 create mode 100644 src/Scenery/marker.cxx
 create mode 100644 src/Scenery/marker.hxx

diff --git a/src/Model/modelmgr.cxx b/src/Model/modelmgr.cxx
index 3a4d3599c..65dcceed4 100644
--- a/src/Model/modelmgr.cxx
+++ b/src/Model/modelmgr.cxx
@@ -24,8 +24,10 @@
 
 #include <Main/fg_props.hxx>
 #include <Scenery/scenery.hxx>
+#include <Scenery/marker.hxx>
 
 #include <osg/ProxyNode>
+#include <osgText/String>
 
 #include "modelmgr.hxx"
 
@@ -121,6 +123,8 @@ FGModelMgr::add_model (SGPropertyNode * node)
         SG_LOG(SG_AIRCRAFT, SG_WARN, "add_model called with empty path");
         return;
     }
+
+    const std::string internal_model{node->getStringValue("internal-model", "external")};
  
   osg::Node *object;
 
@@ -128,21 +132,38 @@ FGModelMgr::add_model (SGPropertyNode * node)
   instance->loaded_node = node->addChild("loaded");
   instance->loaded_node->setBoolValue(false);
     
-  try {
+  if (internal_model == "marker") {
+    std::string label{node->getStringValue("marker/text", "MARKER")};
+    float r = node->getFloatValue("marker/color[0]", 1.0f);
+    float g = node->getFloatValue("marker/color[1]", 1.0f);
+    float b = node->getFloatValue("marker/color[2]", 1.0f);
+    osg::Vec4f color(r, g, b, 1.0f);
+    float font_size = node->getFloatValue("marker/size", 1.0f);
+    float pin_height = node->getFloatValue("marker/height", 1000.0f);
+    float tip_height = node->getFloatValue("marker/tip-height", 0.0f);
+    object = fgCreateMarkerNode(osgText::String(label, osgText::String::ENCODING_UTF8), font_size, pin_height, tip_height, color);
+  }
+  else if (internal_model == "external") {
+    try {
       std::string fullPath = simgear::SGModelLib::findDataFile(model_path);
       if (fullPath.empty()) {
-          SG_LOG(SG_AIRCRAFT, SG_ALERT, "add_model: unable to find model with name '" << model_path << "'");
-          return;
+        SG_LOG(SG_AIRCRAFT, SG_ALERT, "add_model: unable to find model with name '" << model_path << "'");
+        return;
       }
       object = SGModelLib::loadDeferredModel(fullPath, globals->get_props());
-  } catch (const sg_throwable& t) {
-    SG_LOG(SG_AIRCRAFT, SG_ALERT, "Error loading " << model_path << ":\n  "
-        << t.getFormattedMessage() << t.getOrigin());
-    return;
+    } catch (const sg_throwable& t) {
+      SG_LOG(SG_AIRCRAFT, SG_ALERT, "Error loading " << model_path << ":\n  "
+          << t.getFormattedMessage() << t.getOrigin());
+      return;
+    }
   }
-    
-    const std::string modelName{node->getStringValue("name", model_path.c_str())};
-    SG_LOG(SG_AIRCRAFT, SG_INFO, "Adding model " << modelName);
+  else {
+      object = new osg::Node;
+      SG_LOG(SG_AIRCRAFT, SG_WARN, "Unsupported internal-model type " << internal_model);
+  }
+
+  const std::string modelName{node->getStringValue("name", model_path.c_str())};
+  SG_LOG(SG_AIRCRAFT, SG_INFO, "Adding model " << modelName);
 
   SGModelPlacement *model = new SGModelPlacement;
   instance->model = model;
diff --git a/src/Scenery/CMakeLists.txt b/src/Scenery/CMakeLists.txt
index 7a4d0e3f8..aad5b4cab 100644
--- a/src/Scenery/CMakeLists.txt
+++ b/src/Scenery/CMakeLists.txt
@@ -9,6 +9,7 @@ set(SOURCES
 	tilecache.cxx
 	tileentry.cxx
 	tilemgr.cxx
+  marker.cxx
 	)
 
 set(HEADERS
@@ -21,6 +22,7 @@ set(HEADERS
 	tilecache.hxx
 	tileentry.hxx
 	tilemgr.hxx
+  marker.hxx
 	)
 
 flightgear_component(Scenery "${SOURCES}" "${HEADERS}")
diff --git a/src/Scenery/marker.cxx b/src/Scenery/marker.cxx
new file mode 100644
index 000000000..4b56eefa3
--- /dev/null
+++ b/src/Scenery/marker.cxx
@@ -0,0 +1,119 @@
+// marker.cxx - 3D Marker pins in FlightGear
+// Copyright (C) 2022 Tobias Dammers
+// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
+
+#ifdef HAVE_CONFIG_H
+#  include <config.h>
+#endif
+
+#include "Scenery/marker.hxx"
+#include <simgear/scene/util/SGNodeMasks.hxx>
+#include <simgear/scene/util/SGReaderWriterOptions.hxx>
+#include <simgear/scene/material/Effect.hxx>
+#include <simgear/scene/material/EffectGeode.hxx>
+#include <osg/Geometry>
+#include <osg/Material>
+#include <osg/Node>
+#include <osg/Billboard>
+#include <osgText/Text>
+#include <osgText/String>
+#include <osgDB/ReadFile>
+#include <osgDB/WriteFile>
+#include <osgDB/Registry>
+
+osg::Node* fgCreateMarkerNode(const osgText::String& label, float font_size, float pin_height, float tip_height, const osg::Vec4f& color)
+{
+    auto mainNode = new osg::Group;
+
+    auto textNode = new osg::Billboard;
+    mainNode->addChild(textNode);
+
+    textNode->setMode(osg::Billboard::AXIAL_ROT);
+
+    auto text = new osgText::Text;
+    text->setText(label);
+    text->setAlignment(osgText::Text::CENTER_BOTTOM);
+    text->setAxisAlignment(osgText::Text::XZ_PLANE);
+    text->setFont("Fonts/LiberationFonts/LiberationSans-Regular.ttf");
+    // text->setCharacterSizeMode(osgText::Text::OBJECT_COORDS_WITH_MAXIMUM_SCREEN_SIZE_CAPPED_BY_FONT_HEIGHT);
+    text->setCharacterSize(font_size, 1.0f);
+    text->setFontResolution(std::max(32.0f, font_size), std::max(32.0f, font_size));
+    text->setColor(color);
+    text->setPosition(osg::Vec3(0, 0, pin_height));
+    text->setBackdropType(osgText::Text::OUTLINE);
+    text->setBackdropColor(osg::Vec4(0, 0, 0, 0.75));
+    textNode->addDrawable(text);
+
+    float top_spacing = font_size * 0.25;
+
+    if (pin_height - top_spacing > tip_height) {
+        osg::Vec4f solid = color;
+        osg::Vec4f transparent = color;
+        osg::Vec3f nvec(0, 1, 0);
+
+        solid[3] = 1.0f;
+        transparent[3] = 0.0f;
+
+        auto geoNode = new simgear::EffectGeode;
+        mainNode->addChild(geoNode);
+        auto pinGeo = new osg::Geometry;
+        osg::Vec3Array* vtx = new osg::Vec3Array;
+        osg::Vec3Array* nor = new osg::Vec3Array;
+        osg::Vec4Array* rgb = new osg::Vec4Array;
+
+        nor->push_back(nvec);
+
+        vtx->push_back(osg::Vec3f(0, 0, tip_height));
+        rgb->push_back(solid);
+
+        vtx->push_back(osg::Vec3f(-font_size * 0.125, 0, pin_height - top_spacing));
+        rgb->push_back(transparent);
+
+        vtx->push_back(osg::Vec3f(0, 0, tip_height));
+        rgb->push_back(solid);
+
+        vtx->push_back(osg::Vec3f(0, font_size * 0.125, pin_height - top_spacing));
+        rgb->push_back(transparent);
+
+        vtx->push_back(osg::Vec3f(0, 0, tip_height));
+        rgb->push_back(solid);
+
+        vtx->push_back(osg::Vec3f(font_size * 0.125, 0, pin_height - top_spacing));
+        rgb->push_back(transparent);
+
+        vtx->push_back(osg::Vec3f(0, 0, tip_height));
+        rgb->push_back(solid);
+
+        vtx->push_back(osg::Vec3f(0, -font_size * 0.125, pin_height - top_spacing));
+        rgb->push_back(transparent);
+
+        vtx->push_back(osg::Vec3f(0, 0, tip_height));
+        rgb->push_back(solid);
+
+        vtx->push_back(osg::Vec3f(-font_size * 0.125, 0, pin_height - top_spacing));
+        rgb->push_back(transparent);
+
+        pinGeo->setVertexArray(vtx);
+        pinGeo->setColorArray(rgb, osg::Array::BIND_PER_VERTEX);
+        pinGeo->setNormalArray(nor, osg::Array::BIND_OVERALL);
+        pinGeo->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUAD_STRIP, 0, vtx->size()));
+        geoNode->addDrawable(pinGeo);
+
+        auto stateSet = geoNode->getOrCreateStateSet();
+
+        stateSet->setMode(GL_FOG, osg::StateAttribute::OFF);
+        stateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
+        stateSet->setMode(GL_BLEND, osg::StateAttribute::OFF);
+        stateSet->setMode(GL_ALPHA_TEST, osg::StateAttribute::ON);
+        stateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::ON);
+
+        osg::ref_ptr<simgear::SGReaderWriterOptions> opt;
+        opt = simgear::SGReaderWriterOptions::copyOrCreate(osgDB::Registry::instance()->getOptions());
+        simgear::Effect* effect = simgear::makeEffect("Effects/marker-pin", true, opt);
+        if (effect)
+            geoNode->setEffect(effect);
+    }
+
+    mainNode->setNodeMask(~simgear::CASTSHADOW_BIT);
+    return mainNode;
+}
diff --git a/src/Scenery/marker.hxx b/src/Scenery/marker.hxx
new file mode 100644
index 000000000..0ea65981d
--- /dev/null
+++ b/src/Scenery/marker.hxx
@@ -0,0 +1,16 @@
+// marker.hxx - 3D Marker pins in FlightGear
+// Copyright (C) 2022 Tobias Dammers
+// SPDX-License-Identifier: GPL-2.0-or-later OR MIT
+
+#pragma once
+
+namespace osgText {
+    class String;
+}
+
+namespace osg {
+    class Node;
+    class Vec4f;
+}
+
+osg::Node* fgCreateMarkerNode(const osgText::String&, float font_size, float pin_height, float tip_height, const osg::Vec4f& color);
diff --git a/src/Scripting/NasalPositioned.cxx b/src/Scripting/NasalPositioned.cxx
index 3cd636d62..abaa10b90 100644
--- a/src/Scripting/NasalPositioned.cxx
+++ b/src/Scripting/NasalPositioned.cxx
@@ -676,6 +676,9 @@ static naRef f_geodinfo(naContext c, naRef me, int argc, naRef* args)
   const simgear::BVHMaterial *material;
   SGGeod geod = SGGeod::fromDegM(lon, lat, elev);
 
+  if (globals == nullptr)
+    return naNil();
+
   const auto scenery = globals->get_scenery();
   if (scenery == nullptr)
     return naNil();