From 4158e674cce8d27c361e447f79340377d357f66f Mon Sep 17 00:00:00 2001
From: Daniel Allen <daniel.allen@trakkasystems.com>
Date: Thu, 7 Sep 2023 16:18:07 -0700
Subject: [PATCH 1/5] Fix for loading TFS layers based on min/max levels
 available in the source data without explicit levels set in the TFSLayer
 configuration in osgEarth.

---
 src/osgEarth/TFS.cpp | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/osgEarth/TFS.cpp b/src/osgEarth/TFS.cpp
index 4c42116fe4..7fe0efc8e9 100644
--- a/src/osgEarth/TFS.cpp
+++ b/src/osgEarth/TFS.cpp
@@ -224,6 +224,19 @@ TFSFeatureSource::openImplementation()
         {
             fp->geoInterp() = options().geoInterp().get();
         }
+
+        // If not overridden by the layer, use the TFS min/max levels
+        // Otherwise, only level 0 will ever be loaded
+        if (!options().minLevel().isSet())
+        {
+            setMinLevel(fp->getFirstLevel());
+        }
+
+        if (!options().maxLevel().isSet())
+        {
+            setMaxLevel(fp->getMaxLevel());
+        }
+
     }
     else
     {

From e2969c7168474132caad95779439e050d75af343 Mon Sep 17 00:00:00 2001
From: Daniel Allen <daniel.allen@trakkasystems.com>
Date: Mon, 25 Sep 2023 20:59:39 -0700
Subject: [PATCH 2/5] Added option of allowing multi-touch drag in combination
 with pinch/twist gestures. This feels more natural if you allow
 multi-touch-drag to be pan or drag.

---
 src/osgEarth/EarthManipulator     |  5 +++++
 src/osgEarth/EarthManipulator.cpp | 19 +++++++++++++------
 2 files changed, 18 insertions(+), 6 deletions(-)

diff --git a/src/osgEarth/EarthManipulator b/src/osgEarth/EarthManipulator
index fc8d0fee5b..ee42efbd36 100644
--- a/src/osgEarth/EarthManipulator
+++ b/src/osgEarth/EarthManipulator
@@ -514,6 +514,10 @@ namespace osgEarth { namespace Util
             void setZoomToMouse(bool value) { _zoomToMouse = value; }
             bool getZoomToMouse() const { return _zoomToMouse; }
 
+            /** Allow multitouch drag events to be combined with pinch/twist in the same gesture */
+            void setAllowTouchDragCombos(bool value) { _allowTouchDragCombos = value; }
+            bool getAllowTouchDragCombos() const { return _allowTouchDragCombos; }
+
         private:
 
             friend class EarthManipulator;
@@ -561,6 +565,7 @@ namespace osgEarth { namespace Util
             double _throwDecayRate;
 
             bool _zoomToMouse;
+            bool _allowTouchDragCombos;
         };
 
     public:
diff --git a/src/osgEarth/EarthManipulator.cpp b/src/osgEarth/EarthManipulator.cpp
index 0aa6c888f6..0ea433a3fc 100644
--- a/src/osgEarth/EarthManipulator.cpp
+++ b/src/osgEarth/EarthManipulator.cpp
@@ -297,7 +297,8 @@ _terrainAvoidanceEnabled        ( true ),
 _terrainAvoidanceMinDistance    ( 1.0 ),
 _throwingEnabled                ( false ),
 _throwDecayRate                 ( 0.05 ),
-_zoomToMouse                    ( false )
+_zoomToMouse                    ( false ),
+_allowTouchDragCombos           ( false )
 {
     //NOP
 }
@@ -329,7 +330,8 @@ _terrainAvoidanceEnabled( rhs._terrainAvoidanceEnabled ),
 _terrainAvoidanceMinDistance( rhs._terrainAvoidanceMinDistance ),
 _throwingEnabled( rhs._throwingEnabled ),
 _throwDecayRate( rhs._throwDecayRate ),
-_zoomToMouse( rhs._zoomToMouse )
+_zoomToMouse( rhs._zoomToMouse ),
+_allowTouchDragCombos( rhs._allowTouchDragCombos )
 {
     //NOP
 }
@@ -2188,10 +2190,13 @@ EarthManipulator::parseTouchEvents( TouchEvents& output )
 
         // Threshold in pixels for determining if a two finger drag happened.
         float dragThres = 1.0f;
+        bool doMultiDrag = ( osg::equivalent(vec0.x(), vec1.x(), dragThres) &&
+                             osg::equivalent(vec0.y(), vec1.y(), dragThres) ) ||
+                           _settings->getAllowTouchDragCombos();
 
-        // now see if that corresponds to any touch events:
-        if (osg::equivalent(vec0.x(), vec1.x(), dragThres) &&
-            osg::equivalent(vec0.y(), vec1.y(), dragThres))
+        // First check if we should be doing the multidrag event, either because
+        // it was a pure drag, or because we allow it in combination with pinch/twise
+        if( doMultiDrag )
         {
             // two-finger drag.
             output.push_back(TouchEvent());
@@ -2200,7 +2205,9 @@ EarthManipulator::parseTouchEvents( TouchEvents& output )
             ev._dx = 0.5 * (dx[0] + dx[1]) * sens;
             ev._dy = 0.5 * (dy[0] + dy[1]) * sens;
         }
-        else
+
+        // If it wasn't a drag event, or if we allow drag combinations, do the pinch/twist
+        if( !doMultiDrag || _settings->getAllowTouchDragCombos() )
         {
             // otherwise it's a pinch and/or a zoom.  You can do them together.
             if (fabs(deltaDistance) > (1.0 * 0.0005 / sens))

From b6dc7d128a1d6f788dba2f38d7fdc20bc1c89f05 Mon Sep 17 00:00:00 2001
From: Daniel Allen <daniel.allen@trakkasystems.com>
Date: Tue, 26 Sep 2023 18:40:36 -0700
Subject: [PATCH 3/5] Added support for "pixel-size-on-screen" rendering to
 feature model / tiled feature model.

---
 src/osgEarth/FeatureDisplayLayout       | 24 ++++++++++++++++++++++++
 src/osgEarth/FeatureDisplayLayout.cpp   |  9 +++++++++
 src/osgEarth/FeatureModelGraph.cpp      | 16 ++++++++++++++--
 src/osgEarth/SimplePager                |  4 ++++
 src/osgEarth/SimplePager.cpp            |  9 +++++++--
 src/osgEarth/TiledFeatureModelLayer     |  4 ++++
 src/osgEarth/TiledFeatureModelLayer.cpp | 10 ++++++++++
 7 files changed, 72 insertions(+), 4 deletions(-)

diff --git a/src/osgEarth/FeatureDisplayLayout b/src/osgEarth/FeatureDisplayLayout
index 9fbf278747..18f522b5af 100644
--- a/src/osgEarth/FeatureDisplayLayout
+++ b/src/osgEarth/FeatureDisplayLayout
@@ -116,6 +116,27 @@ namespace osgEarth
          optional<float>& minRange() { return _minRange; }
          const optional<float>& minRange() const { return _minRange; }
 
+        /**
+         * Whether features are loaded based on range.  If not, pixels on screen
+         * is used.  Default = range
+         */
+         optional<bool>& useRange() { return _useRange; }
+         const optional<bool>& useRange() const { return _useRange; }
+
+        /**
+         * When using pixel size on screen instead of range, this specifies the maximum
+         * number of pixels at which to display the feature at a given LOD
+         */
+         optional<float>& maxPixels() { return _maxPixels;}
+         const optional<float>& maxPixels() const { return _maxPixels;}
+
+        /**
+         * When using pixel size on screen instead of range, this specifies the minimum
+         * number of pixels at which to display the feature at a given LOD
+         */
+         optional<float>& minPixels() { return _minPixels; }
+         const optional<float>& minPixels() const { return _minPixels; }
+
         /**
          * Whether to crop geometry to fit within the cell extents when chopping
          * a feature level up into grid cells. By default, this is false, meaning 
@@ -184,6 +205,9 @@ namespace osgEarth
         optional<float> _tileSizeFactor;
         optional<float> _minRange;
         optional<float> _maxRange;
+        optional<float> _minPixels;
+        optional<float> _maxPixels;
+        optional<bool>  _useRange;
         optional<bool>  _cropFeatures;
         optional<float> _priorityOffset;
         optional<float> _priorityScale;
diff --git a/src/osgEarth/FeatureDisplayLayout.cpp b/src/osgEarth/FeatureDisplayLayout.cpp
index ff1fc288ec..47c51af3eb 100644
--- a/src/osgEarth/FeatureDisplayLayout.cpp
+++ b/src/osgEarth/FeatureDisplayLayout.cpp
@@ -71,6 +71,9 @@ FeatureDisplayLayout::FeatureDisplayLayout( const Config& conf ) :
 _tileSizeFactor( 3.5f ),
 _minRange      ( 0.0f ),
 _maxRange      ( 0.0f ),
+_minPixels     ( 0.0f ),
+_maxPixels     ( 0.0f ),
+_useRange      ( true ),
 _cropFeatures  ( false ),
 _priorityOffset( 0.0f ),
 _priorityScale ( 1.0f ),
@@ -91,6 +94,9 @@ FeatureDisplayLayout::fromConfig( const Config& conf )
     conf.get( "min_expiry_time",  _minExpiryTime );
     conf.get( "min_range",        _minRange );
     conf.get( "max_range",        _maxRange );
+    conf.get( "min_pixels",       _minPixels);
+    conf.get( "max_pixels",       _maxPixels);
+    conf.get( "use_range",        _useRange);
     conf.get("paged", _paged);
     ConfigSet children = conf.children( "level" );
     for( ConfigSet::const_iterator i = children.begin(); i != children.end(); ++i )
@@ -109,6 +115,9 @@ FeatureDisplayLayout::getConfig() const
     conf.set( "min_expiry_time",  _minExpiryTime );
     conf.set( "min_range",        _minRange );
     conf.set( "max_range",        _maxRange );
+    conf.set( "min_pixels",       _minPixels );
+    conf.set( "max_pixels",       _maxPixels );
+    conf.set( "use_range",        _useRange );
     conf.set("paged", _paged);
     for( Levels::const_iterator i = _levels.begin(); i != _levels.end(); ++i )
         conf.add( i->second.getConfig() );
diff --git a/src/osgEarth/FeatureModelGraph.cpp b/src/osgEarth/FeatureModelGraph.cpp
index f1ea530b45..e043bcdc6d 100644
--- a/src/osgEarth/FeatureModelGraph.cpp
+++ b/src/osgEarth/FeatureModelGraph.cpp
@@ -269,8 +269,20 @@ namespace
         p->setLoadFunction(func);
         p->setCenter(bs.center());
         p->setRadius(bs.radius());
-        p->setMinRange(minRange);
-        p->setMaxRange(maxRange);
+        if( layout.useRange().get() )
+        {    
+            p->setMinRange(minRange);
+            p->setMaxRange(maxRange);
+        }
+        else
+        {
+            if( layout.minPixels().isSet() )
+                p->setMinPixels(layout.minPixels().get());
+
+            if( layout.maxPixels().isSet() )
+                p->setMaxPixels(layout.maxPixels().get());
+        }
+
         p->setPriorityScale(layout.priorityScale().get());
         p->setSceneGraphCallbacks(sgCallbacks);
         return p;
diff --git a/src/osgEarth/SimplePager b/src/osgEarth/SimplePager
index 830049dc0e..d796964959 100644
--- a/src/osgEarth/SimplePager
+++ b/src/osgEarth/SimplePager
@@ -65,6 +65,8 @@ namespace osgEarth { namespace Util
         float getPriorityOffset() const { return _priorityOffset; }
         void setPriorityOffset(float value) { _priorityOffset = value; }
 
+        void setMinPixel(float value) { _minPixel = value; _useRange = false; }
+
         void setEnableCancelation(bool value);
         bool getEnableCancelation() const;
 
@@ -120,6 +122,8 @@ namespace osgEarth { namespace Util
 
         bool _additive;
         double _rangeFactor;
+        bool _useRange;
+        float _minPixel;
         unsigned int _minLevel;
         unsigned int _maxLevel;
         osg::ref_ptr<const Profile> _profile;
diff --git a/src/osgEarth/SimplePager.cpp b/src/osgEarth/SimplePager.cpp
index de8ebe8ba0..e59bc7eff0 100644
--- a/src/osgEarth/SimplePager.cpp
+++ b/src/osgEarth/SimplePager.cpp
@@ -23,6 +23,8 @@ SimplePager::SimplePager(const osgEarth::Map* map, const osgEarth::Profile* prof
 _map(map),
 _profile( profile ),
 _rangeFactor( 6.0 ),
+_useRange(true),
+_minPixel(0.0),
 _additive(false),
 _minLevel(0),
 _maxLevel(30),
@@ -235,8 +237,11 @@ SimplePager::createPagedNode(const TileKey& key, ProgressCallback* progress)
         loadRange = (float)(tileRadius * _rangeFactor);
         pagedNode->setRefinePolicy(_additive ? REFINE_ADD : REFINE_REPLACE);
     }
-
-    pagedNode->setMaxRange(loadRange);
+    
+    if (_useRange)
+        pagedNode->setMaxRange(loadRange);
+    else
+        pagedNode->setMinPixels(_minPixel);
 
     //OE_INFO << "PagedNode2: key="<<key.str()<<" hasChildren=" << hasChildren << ", range=" << loadRange << std::endl;
 
diff --git a/src/osgEarth/TiledFeatureModelLayer b/src/osgEarth/TiledFeatureModelLayer
index 2476328570..16961b764d 100644
--- a/src/osgEarth/TiledFeatureModelLayer
+++ b/src/osgEarth/TiledFeatureModelLayer
@@ -52,6 +52,7 @@ namespace osgEarth
             Options(const ConfigOptions& options);
             OE_OPTION_LAYER(FeatureSource, featureSource);
             OE_OPTION(bool, additive);
+            OE_OPTION(float, minPixel);
             virtual Config getConfig() const;
         protected: // LayerOptions
             virtual void mergeConfig(const Config& conf);        
@@ -79,6 +80,9 @@ namespace osgEarth
         void setAdditive(const bool& value);
         const bool& getAdditive() const;
 
+        void setMinPixel(const float& value);
+        const float& getMinPixel() const;
+
         //! Forces a rebuild on this FeatureModelLayer.
         void dirty();    
 
diff --git a/src/osgEarth/TiledFeatureModelLayer.cpp b/src/osgEarth/TiledFeatureModelLayer.cpp
index 4c273f9ae3..4754427ea3 100644
--- a/src/osgEarth/TiledFeatureModelLayer.cpp
+++ b/src/osgEarth/TiledFeatureModelLayer.cpp
@@ -48,8 +48,11 @@ GeometryCompilerOptions(options)
 void TiledFeatureModelLayer::Options::fromConfig(const Config& conf)
 {
     additive().setDefault(false);
+    minPixel().setDefault(512.0);
 
     conf.get("additive", additive());
+    conf.get("min_pixel", minPixel());
+
     featureSource().get(conf, "features");
 }
 
@@ -65,6 +68,7 @@ TiledFeatureModelLayer::Options::getConfig() const
     conf.merge(gcConf);
 
     conf.set("additive", additive());
+    conf.set("min_pixel", minPixel());
 
     featureSource().set(conf, "features");
 
@@ -82,6 +86,7 @@ void TiledFeatureModelLayer::Options::mergeConfig(const Config& conf)
 OE_LAYER_PROPERTY_IMPL(TiledFeatureModelLayer, bool, AlphaBlending, alphaBlending);
 OE_LAYER_PROPERTY_IMPL(TiledFeatureModelLayer, bool, EnableLighting, enableLighting);
 OE_LAYER_PROPERTY_IMPL(TiledFeatureModelLayer, bool, Additive, additive);
+OE_LAYER_PROPERTY_IMPL(TiledFeatureModelLayer, float, MinPixel, minPixel);
 
 TiledFeatureModelLayer::~TiledFeatureModelLayer()
 {
@@ -267,6 +272,11 @@ TiledFeatureModelLayer::create()
             fmg->setOwnerName(getName());
             fmg->setFilterChain(chain.get());
             fmg->setAdditive(*_options->additive());
+            if (_options->minPixel().isSet())
+            {
+                fmg->setMinPixel(_options->minPixel().get());
+            }
+
             fmg->build();
 
             _root->removeChildren(0, _root->getNumChildren());

From fa60127884ffa6e403e1ac2c2df52f91cb2dd28b Mon Sep 17 00:00:00 2001
From: Daniel Allen <daniel.allen@trakkasystems.com>
Date: Tue, 10 Oct 2023 21:22:53 -0700
Subject: [PATCH 4/5] When moving the map via Drag or Zoom-to-Mouse, ensure the
 center point is clamped to the terrain. The basic logic was borrowed from the
 pan command.

---
 src/osgEarth/EarthManipulator.cpp | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/src/osgEarth/EarthManipulator.cpp b/src/osgEarth/EarthManipulator.cpp
index 0ea433a3fc..61ccb96a43 100644
--- a/src/osgEarth/EarthManipulator.cpp
+++ b/src/osgEarth/EarthManipulator.cpp
@@ -2696,6 +2696,9 @@ EarthManipulator::zoom( double dx, double dy, osg::View* in_view )
     if ( !camera )
         return;
 
+    if ( !recalculateCenterFromLookVector() )
+        return;
+
     // reset the "remembered start location" if we're just starting a continuous zoom
     static osg::Vec3d zero(0,0,0);
     if (_last_action._type != ACTION_ZOOM)
@@ -3375,6 +3378,9 @@ EarthManipulator::drag(double dx, double dy, osg::View* theView)
     if ( !camera )
         return;
 
+    if ( !recalculateCenterFromLookVector() )
+        return;
+
     osg::Matrix viewMat = camera->getViewMatrix();
     osg::Matrix viewMatInv = camera->getInverseViewMatrix();
     if (!_ga_t1.valid())

From 6dd2569a248e85b21af423f6097757ec10858132 Mon Sep 17 00:00:00 2001
From: Daniel Allen <daniel.allen@trakkasystems.com>
Date: Tue, 16 Jul 2024 14:48:45 -0700
Subject: [PATCH 5/5] Changes from scott to allow loading dynamic plugins for
 geometry labels.

---
 src/osgEarth/Filter               |  547 +++++++------
 src/osgEarth/Filter.cpp           |  842 +++++++++++---------
 src/osgEarth/GeometryCompiler.cpp | 1221 +++++++++++++++--------------
 3 files changed, 1385 insertions(+), 1225 deletions(-)

diff --git a/src/osgEarth/Filter b/src/osgEarth/Filter
index 0e1e737095..73f4608915 100644
--- a/src/osgEarth/Filter
+++ b/src/osgEarth/Filter
@@ -1,239 +1,308 @@
-/* -*-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.
- *
- * 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 OSGEARTHFEATURES_FILTER_H
-#define OSGEARTHFEATURES_FILTER_H 1
-
-#include <osgEarth/Common>
-#include <osgEarth/Feature>
-#include <osgEarth/FilterContext>
-#include <osgEarth/GeoData>
-#include <osg/Matrixd>
-#include <list>
-
-
-namespace osgEarth { namespace Util
-{
-    using namespace osgEarth;
-
-    /**
-     * Base class for a filter.
-     */
-    class OSGEARTH_EXPORT Filter : public osg::Object
-    {
-    public:
-        META_Object(osgEarth, Filter);
-
-    protected:
-        virtual ~Filter();
-        Filter() : osg::Object() { }
-        Filter(const Filter& rhs, const osg::CopyOp& copyop) : osg::Object(rhs, copyop) { }
-    };
-
-    /**
-     * Base class for feature filters.
-     */
-    class OSGEARTH_EXPORT FeatureFilter : public Filter
-    {
-    public:
-        /**
-         * Push a list of features through the filter.
-         */
-        virtual FilterContext push( FeatureList& input, FilterContext& context ) =0;
-
-        /**
-         * Optionally initialize the filter.
-         */
-        virtual Status initialize(const osgDB::Options* readOptions) { return Status::OK(); }
-
-        /**
-         * Serialize this FeatureFilter
-         */
-        virtual Config getConfig() const { return Config(); }
-
-        /**
-         * Called when the FeatureFilter is added to the map.
-         */
-        virtual void addedToMap(const class Map*);
-
-    protected:
-        FeatureFilter() { }
-        FeatureFilter(const FeatureFilter& rhs, const osg::CopyOp& c) : Filter(rhs, c) { }
-        virtual ~FeatureFilter();
-    };
-
-    //! Vector of feature filters (ref counted)
-    class OSGEARTH_EXPORT FeatureFilterChain : 
-        public osg::Referenced,
-        public osg::MixinVector<osg::ref_ptr<FeatureFilter> >
-    {
-    public:
-        static FeatureFilterChain* create(
-            const std::vector<ConfigOptions>& filters,
-            const osgDB::Options* readOptions);
-
-        const Status& getStatus() const { return _status; }
-
-    private:
-        Status _status;
-    };
-
-    /**
-     * A Factory that can create a FeatureFilter from a Config
-     */
-    class OSGEARTH_EXPORT FeatureFilterFactory : public osg::Referenced
-    {
-    public:
-        virtual FeatureFilter* create( const Config& conf ) = 0;
-    };    
-
-    typedef std::list< osg::ref_ptr< FeatureFilterFactory > > FeatureFilterFactoryList;
-
-    /**
-     * A registry of FeatureFilter plugins
-     */
-    class OSGEARTH_EXPORT FeatureFilterRegistry : public osg::Referenced
-    {         
-    public:
-        /**
-         * The singleton instance of the factory
-         */
-        static FeatureFilterRegistry* instance();
-
-        /*
-         * Adds a new FeatureFilterFactory to the list
-         */
-        void add( FeatureFilterFactory* factory );
-
-        /**
-         * Creates a FeatureFilter with the registered plugins from the given Config
-         */
-        FeatureFilter* create(const Config& conf, const osgDB::Options* dbo);
-
-    protected:
-        FeatureFilterRegistry();
-        FeatureFilterFactoryList _factories;
-    };
-
-    template<class T>
-    struct SimpleFeatureFilterFactory : public FeatureFilterFactory
-    {
-        SimpleFeatureFilterFactory(const std::string& key):_key(key){}
-
-        virtual FeatureFilter* create(const Config& conf)
-        {
-            if (conf.key() == _key) return new T(conf);            
-            return 0;
-        }
-
-        std::string _key;
-    };
-
-    template<class T>
-    struct RegisterFeatureFilterProxy
-    {
-        RegisterFeatureFilterProxy( T* factory) { FeatureFilterRegistry::instance()->add( factory ); }
-        RegisterFeatureFilterProxy() { FeatureFilterRegistry::instance()->add( new T ); }
-    };
-
-#define OSGEARTH_REGISTER_FEATUREFILTER( CLASSNAME )\
-    extern "C" void osgearth_featurefilter_##KEY(void) {} \
-    static osgEarth::RegisterFeatureFilterProxy<CLASSNAME> s_osgEarthRegisterFeatureFilterProxy_##CLASSNAME;
-    
-#define USE_OSGEARTH_FEATUREFILTER( KEY ) \
-    extern "C" void osgearth_featurefilter_##KEY(void); \
-    static osgDB::PluginFunctionProxy proxy_osgearth_featurefilter_##KEY(osgearth_featurefilter_##KEY);
-
-#define OSGEARTH_REGISTER_SIMPLE_FEATUREFILTER( KEY, CLASSNAME)\
-    extern "C" void osgearth_simple_featurefilter_##KEY(void) {} \
-    static osgEarth::RegisterFeatureFilterProxy< osgEarth::SimpleFeatureFilterFactory<CLASSNAME> > s_osgEarthRegisterFeatureFilterProxy_##CLASSNAME##KEY(new osgEarth::SimpleFeatureFilterFactory<CLASSNAME>(#KEY));
-    
-#define USE_OSGEARTH_SIMPLE_FEATUREFILTER( KEY ) \
-    extern "C" void osgearth_simple_featurefilter_##KEY(void); \
-    static osgDB::PluginFunctionProxy proxy_osgearth_simple_featurefilter_##KEY(osgearth_simple_featurefilter_##KEY);
-    
-
-    //--------------------------------------------------------------------
-
-    class OSGEARTH_EXPORT FeatureFilterDriver : public osgDB::ReaderWriter
-    {
-    protected:
-        const ConfigOptions& getConfigOptions(const osgDB::Options* options) const;
-    };
-
-    /**
-     * Base class for a filter that converts features into an osg Node.
-     */
-    class OSGEARTH_EXPORT FeaturesToNodeFilter : public Filter
-    {
-    public:
-        virtual osg::Node* push( FeatureList& input, FilterContext& context ) =0;
-
-    public:
-        const osg::Matrixd& local2world() const { return _local2world; }
-        const osg::Matrixd& world2local() const { return _world2local; }
-                        
-    protected:
-
-        virtual ~FeaturesToNodeFilter();
-
-        // computes the matricies required to localizer/delocalize double-precision coords
-        void computeLocalizers( const FilterContext& context );
-        void computeLocalizers( const FilterContext& context, const osgEarth::GeoExtent &extent, osg::Matrixd &out_w2l, osg::Matrixd &out_l2w );
-
-        /** Parents the node with a localizer group if necessary */
-        osg::Node*  delocalize( osg::Node* node ) const;
-        osg::Node*  delocalize( osg::Node* node, const osg::Matrixd &local2World ) const;
-        osg::Group* delocalizeAsGroup( osg::Node* node ) const;
-        osg::Group* delocalizeAsGroup( osg::Node* node, const osg::Matrixd &local2World ) const;
-        osg::Group* createDelocalizeGroup() const;
-        osg::Group* createDelocalizeGroup( const osg::Matrixd &local2World) const;
-
-        void transformAndLocalize(
-            const std::vector<osg::Vec3d>& input,
-            const SpatialReference*        inputSRS,
-            osg::Vec3Array*                output,
-            const SpatialReference*        outputSRS,
-            const osg::Matrixd&            world2local,
-            bool                           toECEF );
-
-        void transformAndLocalize(
-            const std::vector<osg::Vec3d>& input,
-            const SpatialReference*        inputSRS,
-            osg::Vec3Array*                out_verts,
-            osg::Vec3Array*                out_normals,
-            const SpatialReference*        outputSRS,
-            const osg::Matrixd&            world2local,
-            bool                           toECEF );
-
-        void transformAndLocalize(
-            const osg::Vec3d&              input,
-            const SpatialReference*        inputSRS,
-            osg::Vec3d&                    output,
-            const SpatialReference*        outputSRS,
-            const osg::Matrixd&            world2local,
-            bool                           toECEF );
-
-        void applyPointSymbology(osg::StateSet*, const class PointSymbol*);
-
-        osg::Matrixd _world2local, _local2world;   // for coordinate localization
-    };
-
-} }
-
-#endif // OSGEARTHFEATURES_FILTER_H
+/* -*-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.
+ *
+ * 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 OSGEARTHFEATURES_FILTER_H
+#define OSGEARTHFEATURES_FILTER_H 1
+
+#include <osgEarth/Common>
+#include <osgEarth/Feature>
+#include <osgEarth/FilterContext>
+#include <osgEarth/GeoData>
+#include <osg/Matrixd>
+#include <list>
+
+
+namespace osgEarth { namespace Util
+{
+    using namespace osgEarth;
+
+    /**
+     * Base class for a filter.
+     */
+    class OSGEARTH_EXPORT Filter : public osg::Object
+    {
+    public:
+        META_Object(osgEarth, Filter);
+
+    protected:
+        virtual ~Filter();
+        Filter() : osg::Object() { }
+        Filter(const Filter& rhs, const osg::CopyOp& copyop) : osg::Object(rhs, copyop) { }
+    };
+
+    /**
+     * Base class for feature filters.
+     */
+    class OSGEARTH_EXPORT FeatureFilter : public Filter
+    {
+    public:
+        /**
+         * Push a list of features through the filter.
+         */
+        virtual FilterContext push( FeatureList& input, FilterContext& context ) =0;
+
+        /**
+         * Optionally initialize the filter.
+         */
+        virtual Status initialize(const osgDB::Options* readOptions) { return Status::OK(); }
+
+        /**
+         * Serialize this FeatureFilter
+         */
+        virtual Config getConfig() const { return Config(); }
+
+    protected:
+        FeatureFilter() { }
+        FeatureFilter(const FeatureFilter& rhs, const osg::CopyOp& c) : Filter(rhs, c) { }
+        virtual ~FeatureFilter();
+    };
+
+    //! Vector of feature filters (ref counted)
+    class OSGEARTH_EXPORT FeatureFilterChain : 
+        public osg::Referenced,
+        public osg::MixinVector<osg::ref_ptr<FeatureFilter> >
+    {
+    public:
+        static FeatureFilterChain* create(
+            const std::vector<ConfigOptions>& filters,
+            const osgDB::Options* readOptions);
+
+        const Status& getStatus() const { return _status; }
+
+    private:
+        Status _status;
+    };
+
+    /**
+     * A Factory that can create a FeatureFilter from a Config
+     */
+    class OSGEARTH_EXPORT FeatureFilterFactory : public osg::Referenced
+    {
+    public:
+        virtual FeatureFilter* create( const Config& conf ) = 0;
+    };    
+
+    typedef std::list< osg::ref_ptr< FeatureFilterFactory > > FeatureFilterFactoryList;
+
+    /**
+     * A registry of FeatureFilter plugins
+     */
+    class OSGEARTH_EXPORT FeatureFilterRegistry : public osg::Referenced
+    {         
+    public:
+        /**
+         * The singleton instance of the factory
+         */
+        static FeatureFilterRegistry* instance();
+
+        /*
+         * Adds a new FeatureFilterFactory to the list
+         */
+        void add( FeatureFilterFactory* factory );
+
+        /**
+         * Creates a FeatureFilter with the registered plugins from the given Config
+         */
+        FeatureFilter* create(const Config& conf, const osgDB::Options* dbo);
+
+    protected:
+        FeatureFilterRegistry();
+        FeatureFilterFactoryList _factories;
+    };
+
+    template<class T>
+    struct SimpleFeatureFilterFactory : public FeatureFilterFactory
+    {
+        SimpleFeatureFilterFactory(const std::string& key):_key(key){}
+
+        virtual FeatureFilter* create(const Config& conf)
+        {
+            if (conf.key() == _key) return new T(conf);            
+            return 0;
+        }
+
+        std::string _key;
+    };
+
+    template<class T>
+    struct RegisterFeatureFilterProxy
+    {
+        RegisterFeatureFilterProxy( T* factory) { FeatureFilterRegistry::instance()->add( factory ); }
+        RegisterFeatureFilterProxy() { FeatureFilterRegistry::instance()->add( new T ); }
+    };
+
+#define OSGEARTH_REGISTER_FEATUREFILTER( CLASSNAME )\
+    extern "C" void osgearth_featurefilter_##KEY(void) {} \
+    static osgEarth::RegisterFeatureFilterProxy<CLASSNAME> s_osgEarthRegisterFeatureFilterProxy_##CLASSNAME;
+    
+#define USE_OSGEARTH_FEATUREFILTER( KEY ) \
+    extern "C" void osgearth_featurefilter_##KEY(void); \
+    static osgDB::PluginFunctionProxy proxy_osgearth_featurefilter_##KEY(osgearth_featurefilter_##KEY);
+
+#define OSGEARTH_REGISTER_SIMPLE_FEATUREFILTER( KEY, CLASSNAME)\
+    extern "C" void osgearth_simple_featurefilter_##KEY(void) {} \
+    static osgEarth::RegisterFeatureFilterProxy< osgEarth::SimpleFeatureFilterFactory<CLASSNAME> > s_osgEarthRegisterFeatureFilterProxy_##CLASSNAME##KEY(new osgEarth::SimpleFeatureFilterFactory<CLASSNAME>(#KEY));
+    
+#define USE_OSGEARTH_SIMPLE_FEATUREFILTER( KEY ) \
+    extern "C" void osgearth_simple_featurefilter_##KEY(void); \
+    static osgDB::PluginFunctionProxy proxy_osgearth_simple_featurefilter_##KEY(osgearth_simple_featurefilter_##KEY);
+    
+
+    //--------------------------------------------------------------------
+
+    class OSGEARTH_EXPORT FeatureFilterDriver : public osgDB::ReaderWriter
+    {
+    protected:
+        const ConfigOptions& getConfigOptions(const osgDB::Options* options) const;
+    };
+
+    /**
+     * Base class for a filter that converts features into an osg Node.
+     */
+    class OSGEARTH_EXPORT FeaturesToNodeFilter : public Filter
+    {
+    public:
+        virtual osg::Node* push( FeatureList& input, FilterContext& context ) =0;
+
+    public:
+        const osg::Matrixd& local2world() const { return _local2world; }
+        const osg::Matrixd& world2local() const { return _world2local; }
+                        
+    protected:
+
+        virtual ~FeaturesToNodeFilter();
+
+        // computes the matricies required to localizer/delocalize double-precision coords
+        void computeLocalizers( const FilterContext& context );
+        void computeLocalizers( const FilterContext& context, const osgEarth::GeoExtent &extent, osg::Matrixd &out_w2l, osg::Matrixd &out_l2w );
+
+        /** Parents the node with a localizer group if necessary */
+        osg::Node*  delocalize( osg::Node* node ) const;
+        osg::Node*  delocalize( osg::Node* node, const osg::Matrixd &local2World ) const;
+        osg::Group* delocalizeAsGroup( osg::Node* node ) const;
+        osg::Group* delocalizeAsGroup( osg::Node* node, const osg::Matrixd &local2World ) const;
+        osg::Group* createDelocalizeGroup() const;
+        osg::Group* createDelocalizeGroup( const osg::Matrixd &local2World) const;
+
+        void transformAndLocalize(
+            const std::vector<osg::Vec3d>& input,
+            const SpatialReference*        inputSRS,
+            osg::Vec3Array*                output,
+            const SpatialReference*        outputSRS,
+            const osg::Matrixd&            world2local,
+            bool                           toECEF );
+
+        void transformAndLocalize(
+            const std::vector<osg::Vec3d>& input,
+            const SpatialReference*        inputSRS,
+            osg::Vec3Array*                out_verts,
+            osg::Vec3Array*                out_normals,
+            const SpatialReference*        outputSRS,
+            const osg::Matrixd&            world2local,
+            bool                           toECEF );
+
+        void transformAndLocalize(
+            const osg::Vec3d&              input,
+            const SpatialReference*        inputSRS,
+            osg::Vec3d&                    output,
+            const SpatialReference*        outputSRS,
+            const osg::Matrixd&            world2local,
+            bool                           toECEF );
+
+        void applyPointSymbology(osg::StateSet*, const class PointSymbol*);
+
+        osg::Matrixd _world2local, _local2world;   // for coordinate localization
+    };
+
+    class OSGEARTH_EXPORT FeaturesToNodeFilterFactory : public osg::Referenced
+    {
+    public:
+        virtual FeaturesToNodeFilter* create(const Config& conf, osgEarth::Style style) = 0;
+    };
+
+    typedef std::list<osg::ref_ptr<FeaturesToNodeFilterFactory>> FeaturesToNodeFilterFactoryList;
+
+    class OSGEARTH_EXPORT FeaturesToNodeFilterRegistry : public osg::Referenced
+    {
+    public:
+        static FeaturesToNodeFilterRegistry* instance();
+
+        void add( FeaturesToNodeFilterFactory* factory );
+        FeaturesToNodeFilter* create(
+         const Config& conf,
+         const osgDB::Options* dbo,
+         osgEarth::Style style);
+
+    protected:
+        FeaturesToNodeFilterRegistry();
+        FeaturesToNodeFilterFactoryList _factories;
+    };
+
+    template<class T>
+    struct SimpleFeaturesToNodeFilterFactory : public FeaturesToNodeFilterFactory
+    {
+        SimpleFeaturesToNodeFilterFactory(const std::string& key) : _key(key) {}
+
+        virtual FeaturesToNodeFilter* create(const Config& conf, osgEarth::Style style)
+        {
+            if (conf.key() == _key) return new T(style);
+            return 0;
+        }
+
+        std::string _key;
+    };
+
+    template<class T>
+    struct RegisterFeaturesToNodeFilterProxy
+    {
+        RegisterFeaturesToNodeFilterProxy( T* factory)
+        {
+           FeaturesToNodeFilterRegistry::instance()->add( factory );
+        }
+
+        RegisterFeaturesToNodeFilterProxy()
+        {
+           FeaturesToNodeFilterRegistry::instance()->add( new T );
+        }
+    };
+
+#define OSGEARTH_REGISTER_FEATURESTONODEFILTER( CLASSNAME )\
+    extern "C" void osgearth_featurestonodefilter_##KEY(void) {} \
+    static osgEarth::RegisterFeaturesToNodeFilterProxy<CLASSNAME> s_osgEarthRegisterFeaturesToNodeFilterProxy_##CLASSNAME;
+
+#define USE_OSGEARTH_FEATURESTONODEFILTER( KEY ) \
+    extern "C" void osgearth_featurestonodefilter_##KEY(void); \
+    static osgDB::PluginFunctionProxy proxy_osgearth_featurestonodefilter_##KEY(osgearth_featurestonodefilter_##KEY);
+
+#define OSGEARTH_REGISTER_SIMPLE_FEATURESTONODEFILTER( KEY, CLASSNAME)\
+    extern "C" void osgearth_simple_featurestonodefilter_##KEY(void) {} \
+    static osgEarth::RegisterFeaturesToNodeFilterProxy< osgEarth::SimpleFeaturesToNodeFilterFactory<CLASSNAME> > s_osgEarthRegisterFeaturesToNodeFilterProxy_##CLASSNAME##KEY(new osgEarth::SimpleFeaturesToNodeFilterFactory<CLASSNAME>(#KEY));
+
+#define USE_OSGEARTH_SIMPLE_FEATURESTONODEFILTER( KEY ) \
+    extern "C" void osgearth_simple_featurestonodefilter_##KEY(void); \
+    static osgDB::PluginFunctionProxy proxy_osgearth_simple_featurestonodefilter_##KEY(osgearth_simple_featurestonodefilter_##KEY);
+
+    class OSGEARTH_EXPORT FeaturesToNodeFilterDriver : public osgDB::ReaderWriter
+    {
+    protected:
+        const ConfigOptions& getConfigOptions(const osgDB::Options* options) const;
+    };
+
+} }
+
+#endif // OSGEARTHFEATURES_FILTER_H
diff --git a/src/osgEarth/Filter.cpp b/src/osgEarth/Filter.cpp
index 36ae8318c2..b8c33044ad 100644
--- a/src/osgEarth/Filter.cpp
+++ b/src/osgEarth/Filter.cpp
@@ -1,380 +1,462 @@
-/* -*-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.
- *
- * 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/>
- */
-#include <osgEarth/Filter>
-#include <osgEarth/FilterContext>
-#include <osgEarth/LineSymbol>
-#include <osgEarth/PointSymbol>
-#include <osgEarth/ECEF>
-#include <osgEarth/Registry>
-#include <osgEarth/GLUtils>
-#include <osgEarth/VirtualProgram>
-#include <osg/MatrixTransform>
-#include <osgDB/ReadFile>
-
-using namespace osgEarth;
-using namespace osgEarth::Util;
-
-/********************************************************************************/
-Filter::~Filter()
-{
-}
-
-/********************************************************************************/
-FeatureFilter::~FeatureFilter()
-{
-}
-
-void FeatureFilter::addedToMap(const class Map*)
-{
-}
-
-/********************************************************************************/
-
-#undef LC
-#define LC "[FeatureFilterChain] "
-
-FeatureFilterChain*
-FeatureFilterChain::create(const std::vector<ConfigOptions>& filters, const osgDB::Options* readOptions)
-{
-    // Create and initialize the filters.
-    FeatureFilterChain* chain = NULL;
-
-    for(unsigned i=0; i<filters.size(); ++i)
-    {
-        const ConfigOptions& conf = filters[i];
-        FeatureFilter* filter = FeatureFilterRegistry::instance()->create( conf.getConfig(), 0L );
-        if ( filter )
-        {
-            if (chain == NULL)
-                chain = new FeatureFilterChain();
-
-            chain->push_back( filter );
-            Status s = filter->initialize(readOptions);
-            if (s.isError())
-            {
-                chain->_status = s;
-                OE_WARN << LC << "Filter problem: " << filter->getName() << " : " << s.message() << std::endl;
-                break;
-            }
-        }
-    }
-
-    return chain;
-}
-
-/********************************************************************************/
-        
-#undef  LC
-#define LC "[FeatureFilterRegistry] "
-
-FeatureFilterRegistry::FeatureFilterRegistry()
-{
-}
-
-FeatureFilterRegistry*
-FeatureFilterRegistry::instance()
-{
-    // OK to be in the local scope since this gets called at static init time
-    // by the OSGEARTH_REGISTER_ANNOTATION macro
-    static FeatureFilterRegistry* s_singleton =0L;
-    static Threading::Mutex    s_singletonMutex(OE_MUTEX_NAME);
-
-    if ( !s_singleton )
-    {
-        Threading::ScopedMutexLock lock(s_singletonMutex);
-        if ( !s_singleton )
-        {
-            s_singleton = new FeatureFilterRegistry();
-        }
-    }
-    return s_singleton;
-}
-
-void
-FeatureFilterRegistry::add( FeatureFilterFactory* factory )
-{
-    _factories.push_back( factory );
-}
-
-#define FEATURE_FILTER_OPTIONS_TAG "__osgEarth::FeatureFilterOptions"
-
-FeatureFilter*
-FeatureFilterRegistry::create(const Config& conf, const osgDB::Options* dbo)
-{
-    const std::string& driver = conf.key();
-
-    osg::ref_ptr<FeatureFilter> result;
-
-    for (FeatureFilterFactoryList::iterator itr = _factories.begin(); result == 0L && itr != _factories.end(); itr++)
-    {
-        result = itr->get()->create( conf );
-    }
-
-    if ( !result.valid() )
-    {
-        // not found; try to load from plugin.
-        if ( driver.empty() )
-        {
-            OE_WARN << LC << "ILLEGAL- no driver set for feature filter" << std::endl;
-            return 0L;
-        }
-
-        ConfigOptions options(conf);
-
-        osg::ref_ptr<osgDB::Options> dbopt = Registry::instance()->cloneOrCreateOptions(dbo);
-        dbopt->setPluginData( FEATURE_FILTER_OPTIONS_TAG, (void*)&options );
-
-        std::string driverExt = std::string( ".osgearth_featurefilter_" ) + driver;
-        osg::ref_ptr<osg::Object> object = osgDB::readRefObjectFile( driverExt, dbopt.get() );
-        result = dynamic_cast<FeatureFilter*>( object.release() );
-    }
-
-    if ( !result.valid() )
-    {
-        OE_WARN << LC << "Failed to load FeatureFilter driver \"" << driver << "\"" << std::endl;
-    }
-
-    return result.release();
-} 
-
-const ConfigOptions&
-FeatureFilterDriver::getConfigOptions(const osgDB::Options* options) const
-{
-    static ConfigOptions s_default;
-    const void* data = options->getPluginData(FEATURE_FILTER_OPTIONS_TAG);
-    return data ? *static_cast<const ConfigOptions*>(data) : s_default;
-}
-
-/********************************************************************************/
-
-#undef  LC
-#define LC "[FeaturesToNodeFilter] "
-
-FeaturesToNodeFilter::~FeaturesToNodeFilter()
-{
-    //nop
-}
-
-void
-FeaturesToNodeFilter::computeLocalizers( const FilterContext& context )
-{
-    computeLocalizers(context, context.extent().get(), _world2local, _local2world);
-}
-
-void
-FeaturesToNodeFilter::computeLocalizers( const FilterContext& context, const osgEarth::GeoExtent &extent, osg::Matrixd &out_w2l, osg::Matrixd &out_l2w )
-{
-    if ( context.isGeoreferenced() )
-    {
-        bool ecef = context.getOutputSRS()->isGeographic();
-
-        if (ecef)
-        {
-
-            const SpatialReference* geogSRS = context.getOutputSRS()->getGeographicSRS();
-            GeoExtent geodExtent = extent.transform( geogSRS );
-            if ( geodExtent.width() < 180.0 )
-            {
-                osg::Vec3d centroid, centroidECEF;
-                geodExtent.getCentroid( centroid.x(), centroid.y() );
-                geogSRS->transform( centroid, geogSRS->getGeocentricSRS(), centroidECEF );
-                geogSRS->getGeocentricSRS()->createLocalToWorld( centroidECEF, out_l2w );
-                out_w2l.invert( out_l2w );
-            }
-        }
-
-        else // projected
-        {
-            if ( extent.isValid() )
-            {
-                osg::Vec3d centroid;
-                extent.getCentroid(centroid.x(), centroid.y());
-
-                extent.getSRS()->transform(
-                    centroid,
-                    context.getSession()->getMapSRS(),
-                    centroid );
-
-                out_w2l.makeTranslate( -centroid );
-                out_l2w.invert( out_w2l );
-            }
-        }
-    }
-}
-
-void
-FeaturesToNodeFilter::transformAndLocalize(const std::vector<osg::Vec3d>& input,
-                                           const SpatialReference*        inputSRS,
-                                           osg::Vec3Array*                output,
-                                           const SpatialReference*        outputSRS,
-                                           const osg::Matrixd&            world2local,
-                                           bool                           toECEF )
-{
-    output->reserve( output->size() + input.size() );
-
-    if ( toECEF )
-    {
-        ECEF::transformAndLocalize( input, inputSRS, output, outputSRS, world2local );
-    }
-    else if ( inputSRS )
-    {
-        std::vector<osg::Vec3d> temp( input );
-        inputSRS->transform( temp, outputSRS );
-
-        for( std::vector<osg::Vec3d>::const_iterator i = temp.begin(); i != temp.end(); ++i )
-        {
-            output->push_back( (*i) * world2local );
-        }
-    }
-    else
-    {
-        for( std::vector<osg::Vec3d>::const_iterator i = input.begin(); i != input.end(); ++i )
-        {
-            output->push_back( (*i) * world2local );
-        }
-    }
-}
-
-
-
-void
-FeaturesToNodeFilter::transformAndLocalize(const std::vector<osg::Vec3d>& input,
-                                           const SpatialReference*        inputSRS,
-                                           osg::Vec3Array*                output_verts,
-                                           osg::Vec3Array*                output_normals,
-                                           const SpatialReference*        outputSRS,
-                                           const osg::Matrixd&            world2local,
-                                           bool                           toECEF )
-{
-    // pre-allocate enough space (performance)
-    output_verts->reserve( output_verts->size() + input.size() );
-
-    if ( output_normals )
-        output_normals->reserve( output_verts->size() );
-
-    if ( toECEF )
-    {
-        ECEF::transformAndLocalize( input, inputSRS, output_verts, output_normals, outputSRS, world2local );
-    }
-    else if ( inputSRS )
-    {
-        std::vector<osg::Vec3d> temp( input );
-        inputSRS->transform( temp, outputSRS );
-
-        for( std::vector<osg::Vec3d>::const_iterator i = temp.begin(); i != temp.end(); ++i )
-        {
-            output_verts->push_back( (*i) * world2local );
-            if ( output_normals )
-                output_normals->push_back( osg::Vec3(0,0,1) );
-        }
-    }
-    else
-    {
-        for( std::vector<osg::Vec3d>::const_iterator i = input.begin(); i != input.end(); ++i )
-        {
-            output_verts->push_back( (*i) * world2local );
-            if ( output_normals )
-                output_normals->push_back( osg::Vec3(0,0,1) );
-        }
-    }
-}
-
-
-
-
-void
-FeaturesToNodeFilter::transformAndLocalize(const osg::Vec3d&              input,
-                                           const SpatialReference*        inputSRS,
-                                           osg::Vec3d&                    output,
-                                           const SpatialReference*        outputSRS,
-                                           const osg::Matrixd&            world2local,
-                                           bool                           toECEF )
-{
-    if ( toECEF && inputSRS && outputSRS )
-    {
-        ECEF::transformAndLocalize( input, inputSRS, output, outputSRS, world2local );
-    }
-    else if ( inputSRS )
-    {
-        inputSRS->transform( input, outputSRS, output );
-        output = output * world2local;
-    }
-    else
-    {
-        output = input * world2local;
-    }
-}
-
-
-osg::Node*
-FeaturesToNodeFilter::delocalize( osg::Node* node ) const
-{
-    return delocalize(node, _local2world);
-}
-
-osg::Node*
-FeaturesToNodeFilter::delocalize( osg::Node* node, const osg::Matrixd &local2world) const
-{
-    if ( !local2world.isIdentity() ) 
-        return delocalizeAsGroup( node, local2world );
-    else
-        return node;
-}
-
-osg::Group*
-FeaturesToNodeFilter::delocalizeAsGroup( osg::Node* node ) const
-{
-    return delocalizeAsGroup( node, _local2world );
-}
-
-osg::Group*
-FeaturesToNodeFilter::delocalizeAsGroup( osg::Node* node, const osg::Matrixd &local2world ) const
-{
-    osg::Group* group = createDelocalizeGroup(local2world);
-    if ( node )
-        group->addChild( node );
-    return group;
-}
-
-osg::Group*
-FeaturesToNodeFilter::createDelocalizeGroup() const
-{
-    return createDelocalizeGroup( _local2world );
-}
-
-osg::Group*
-FeaturesToNodeFilter::createDelocalizeGroup( const osg::Matrixd &local2world ) const
-{
-    osg::Group* group = local2world.isIdentity() ?
-        new osg::Group() :
-        new osg::MatrixTransform( local2world );
-
-    return group;
-}
-
-void 
-FeaturesToNodeFilter::applyPointSymbology(osg::StateSet*     stateset, 
-                                          const PointSymbol* point)
-{
-    if ( point )
-    {
-        float size = osg::maximum( 0.1f, *point->size() );
-        GLUtils::setPointSize(stateset, size, 1);
-    }
-}
+/* -*-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.
+ *
+ * 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/>
+ */
+#include <osgEarth/Filter>
+#include <osgEarth/FilterContext>
+#include <osgEarth/LineSymbol>
+#include <osgEarth/PointSymbol>
+#include <osgEarth/ECEF>
+#include <osgEarth/Registry>
+#include <osgEarth/GLUtils>
+#include <osgEarth/VirtualProgram>
+#include <osg/MatrixTransform>
+#include <osgDB/ReadFile>
+
+using namespace osgEarth;
+using namespace osgEarth::Util;
+
+/********************************************************************************/
+Filter::~Filter()
+{
+}
+
+/********************************************************************************/
+FeatureFilter::~FeatureFilter()
+{
+}
+
+/********************************************************************************/
+
+#undef LC
+#define LC "[FeatureFilterChain] "
+
+FeatureFilterChain*
+FeatureFilterChain::create(const std::vector<ConfigOptions>& filters, const osgDB::Options* readOptions)
+{
+    // Create and initialize the filters.
+    FeatureFilterChain* chain = NULL;
+
+    for(unsigned i=0; i<filters.size(); ++i)
+    {
+        const ConfigOptions& conf = filters[i];
+        FeatureFilter* filter = FeatureFilterRegistry::instance()->create( conf.getConfig(), 0L );
+        if ( filter )
+        {
+            if (chain == NULL)
+                chain = new FeatureFilterChain();
+
+            chain->push_back( filter );
+            Status s = filter->initialize(readOptions);
+            if (s.isError())
+            {
+                chain->_status = s;
+                OE_WARN << LC << "Filter problem: " << filter->getName() << " : " << s.message() << std::endl;
+                break;
+            }
+        }
+    }
+
+    return chain;
+}
+
+/********************************************************************************/
+        
+#undef  LC
+#define LC "[FeatureFilterRegistry] "
+
+FeatureFilterRegistry::FeatureFilterRegistry()
+{
+}
+
+FeatureFilterRegistry*
+FeatureFilterRegistry::instance()
+{
+    // OK to be in the local scope since this gets called at static init time
+    // by the OSGEARTH_REGISTER_ANNOTATION macro
+    static FeatureFilterRegistry* s_singleton =0L;
+    static Threading::Mutex    s_singletonMutex(OE_MUTEX_NAME);
+
+    if ( !s_singleton )
+    {
+        Threading::ScopedMutexLock lock(s_singletonMutex);
+        if ( !s_singleton )
+        {
+            s_singleton = new FeatureFilterRegistry();
+        }
+    }
+    return s_singleton;
+}
+
+void
+FeatureFilterRegistry::add( FeatureFilterFactory* factory )
+{
+    _factories.push_back( factory );
+}
+
+#define FEATURE_FILTER_OPTIONS_TAG "__osgEarth::FeatureFilterOptions"
+
+FeatureFilter*
+FeatureFilterRegistry::create(const Config& conf, const osgDB::Options* dbo)
+{
+    const std::string& driver = conf.key();
+
+    osg::ref_ptr<FeatureFilter> result;
+
+    for (FeatureFilterFactoryList::iterator itr = _factories.begin(); result == 0L && itr != _factories.end(); itr++)
+    {
+        result = itr->get()->create( conf );
+    }
+
+    if ( !result.valid() )
+    {
+        // not found; try to load from plugin.
+        if ( driver.empty() )
+        {
+            OE_WARN << LC << "ILLEGAL- no driver set for feature filter" << std::endl;
+            return 0L;
+        }
+
+        ConfigOptions options(conf);
+
+        osg::ref_ptr<osgDB::Options> dbopt = Registry::instance()->cloneOrCreateOptions(dbo);
+        dbopt->setPluginData( FEATURE_FILTER_OPTIONS_TAG, (void*)&options );
+
+        std::string driverExt = std::string( ".osgearth_featurefilter_" ) + driver;
+        osg::ref_ptr<osg::Object> object = osgDB::readRefObjectFile( driverExt, dbopt.get() );
+        result = dynamic_cast<FeatureFilter*>( object.release() );
+    }
+
+    if ( !result.valid() )
+    {
+        OE_WARN << LC << "Failed to load FeatureFilter driver \"" << driver << "\"" << std::endl;
+    }
+
+    return result.release();
+} 
+
+const ConfigOptions&
+FeatureFilterDriver::getConfigOptions(const osgDB::Options* options) const
+{
+    static ConfigOptions s_default;
+    const void* data = options->getPluginData(FEATURE_FILTER_OPTIONS_TAG);
+    return data ? *static_cast<const ConfigOptions*>(data) : s_default;
+}
+
+/********************************************************************************/
+
+#undef  LC
+#define LC "[FeaturesToNodeFilter] "
+
+FeaturesToNodeFilter::~FeaturesToNodeFilter()
+{
+    //nop
+}
+
+void
+FeaturesToNodeFilter::computeLocalizers( const FilterContext& context )
+{
+    computeLocalizers(context, context.extent().get(), _world2local, _local2world);
+}
+
+void
+FeaturesToNodeFilter::computeLocalizers( const FilterContext& context, const osgEarth::GeoExtent &extent, osg::Matrixd &out_w2l, osg::Matrixd &out_l2w )
+{
+    if ( context.isGeoreferenced() )
+    {
+        bool ecef = context.getOutputSRS()->isGeographic();
+
+        if (ecef)
+        {
+
+            const SpatialReference* geogSRS = context.getOutputSRS()->getGeographicSRS();
+            GeoExtent geodExtent = extent.transform( geogSRS );
+            if ( geodExtent.width() < 180.0 )
+            {
+                osg::Vec3d centroid, centroidECEF;
+                geodExtent.getCentroid( centroid.x(), centroid.y() );
+                geogSRS->transform( centroid, geogSRS->getGeocentricSRS(), centroidECEF );
+                geogSRS->getGeocentricSRS()->createLocalToWorld( centroidECEF, out_l2w );
+                out_w2l.invert( out_l2w );
+            }
+        }
+
+        else // projected
+        {
+            if ( extent.isValid() )
+            {
+                osg::Vec3d centroid;
+                extent.getCentroid(centroid.x(), centroid.y());
+
+                extent.getSRS()->transform(
+                    centroid,
+                    context.getSession()->getMapSRS(),
+                    centroid );
+
+                out_w2l.makeTranslate( -centroid );
+                out_l2w.invert( out_w2l );
+            }
+        }
+    }
+}
+
+void
+FeaturesToNodeFilter::transformAndLocalize(const std::vector<osg::Vec3d>& input,
+                                           const SpatialReference*        inputSRS,
+                                           osg::Vec3Array*                output,
+                                           const SpatialReference*        outputSRS,
+                                           const osg::Matrixd&            world2local,
+                                           bool                           toECEF )
+{
+    output->reserve( output->size() + input.size() );
+
+    if ( toECEF )
+    {
+        ECEF::transformAndLocalize( input, inputSRS, output, outputSRS, world2local );
+    }
+    else if ( inputSRS )
+    {
+        std::vector<osg::Vec3d> temp( input );
+        inputSRS->transform( temp, outputSRS );
+
+        for( std::vector<osg::Vec3d>::const_iterator i = temp.begin(); i != temp.end(); ++i )
+        {
+            output->push_back( (*i) * world2local );
+        }
+    }
+    else
+    {
+        for( std::vector<osg::Vec3d>::const_iterator i = input.begin(); i != input.end(); ++i )
+        {
+            output->push_back( (*i) * world2local );
+        }
+    }
+}
+
+
+
+void
+FeaturesToNodeFilter::transformAndLocalize(const std::vector<osg::Vec3d>& input,
+                                           const SpatialReference*        inputSRS,
+                                           osg::Vec3Array*                output_verts,
+                                           osg::Vec3Array*                output_normals,
+                                           const SpatialReference*        outputSRS,
+                                           const osg::Matrixd&            world2local,
+                                           bool                           toECEF )
+{
+    // pre-allocate enough space (performance)
+    output_verts->reserve( output_verts->size() + input.size() );
+
+    if ( output_normals )
+        output_normals->reserve( output_verts->size() );
+
+    if ( toECEF )
+    {
+        ECEF::transformAndLocalize( input, inputSRS, output_verts, output_normals, outputSRS, world2local );
+    }
+    else if ( inputSRS )
+    {
+        std::vector<osg::Vec3d> temp( input );
+        inputSRS->transform( temp, outputSRS );
+
+        for( std::vector<osg::Vec3d>::const_iterator i = temp.begin(); i != temp.end(); ++i )
+        {
+            output_verts->push_back( (*i) * world2local );
+            if ( output_normals )
+                output_normals->push_back( osg::Vec3(0,0,1) );
+        }
+    }
+    else
+    {
+        for( std::vector<osg::Vec3d>::const_iterator i = input.begin(); i != input.end(); ++i )
+        {
+            output_verts->push_back( (*i) * world2local );
+            if ( output_normals )
+                output_normals->push_back( osg::Vec3(0,0,1) );
+        }
+    }
+}
+
+
+
+
+void
+FeaturesToNodeFilter::transformAndLocalize(const osg::Vec3d&              input,
+                                           const SpatialReference*        inputSRS,
+                                           osg::Vec3d&                    output,
+                                           const SpatialReference*        outputSRS,
+                                           const osg::Matrixd&            world2local,
+                                           bool                           toECEF )
+{
+    if ( toECEF && inputSRS && outputSRS )
+    {
+        ECEF::transformAndLocalize( input, inputSRS, output, outputSRS, world2local );
+    }
+    else if ( inputSRS )
+    {
+        inputSRS->transform( input, outputSRS, output );
+        output = output * world2local;
+    }
+    else
+    {
+        output = input * world2local;
+    }
+}
+
+
+osg::Node*
+FeaturesToNodeFilter::delocalize( osg::Node* node ) const
+{
+    return delocalize(node, _local2world);
+}
+
+osg::Node*
+FeaturesToNodeFilter::delocalize( osg::Node* node, const osg::Matrixd &local2world) const
+{
+    if ( !local2world.isIdentity() ) 
+        return delocalizeAsGroup( node, local2world );
+    else
+        return node;
+}
+
+osg::Group*
+FeaturesToNodeFilter::delocalizeAsGroup( osg::Node* node ) const
+{
+    return delocalizeAsGroup( node, _local2world );
+}
+
+osg::Group*
+FeaturesToNodeFilter::delocalizeAsGroup( osg::Node* node, const osg::Matrixd &local2world ) const
+{
+    osg::Group* group = createDelocalizeGroup(local2world);
+    if ( node )
+        group->addChild( node );
+    return group;
+}
+
+osg::Group*
+FeaturesToNodeFilter::createDelocalizeGroup() const
+{
+    return createDelocalizeGroup( _local2world );
+}
+
+osg::Group*
+FeaturesToNodeFilter::createDelocalizeGroup( const osg::Matrixd &local2world ) const
+{
+    osg::Group* group = local2world.isIdentity() ?
+        new osg::Group() :
+        new osg::MatrixTransform( local2world );
+
+    return group;
+}
+
+void 
+FeaturesToNodeFilter::applyPointSymbology(osg::StateSet*     stateset, 
+                                          const PointSymbol* point)
+{
+    if ( point )
+    {
+        float size = osg::maximum( 0.1f, *point->size() );
+        GLUtils::setPointSize(stateset, size, 1);
+    }
+}
+
+/********************************************************************************/
+
+#undef  LC
+#define LC "[FeaturesToNodeFilterRegistry] "
+
+FeaturesToNodeFilterRegistry::FeaturesToNodeFilterRegistry() : _factories()
+{
+}
+
+FeaturesToNodeFilterRegistry*
+FeaturesToNodeFilterRegistry::instance()
+{
+    // OK to be in the local scope since this gets called at static init time
+    // by the OSGEARTH_REGISTER_ANNOTATION macro
+    static FeaturesToNodeFilterRegistry* s_singleton =0L;
+    static Threading::Mutex    s_singletonMutex(OE_MUTEX_NAME);
+
+    if ( !s_singleton )
+    {
+        Threading::ScopedMutexLock lock(s_singletonMutex);
+        if ( !s_singleton )
+        {
+            s_singleton = new FeaturesToNodeFilterRegistry();
+        }
+    }
+    return s_singleton;
+}
+
+void
+FeaturesToNodeFilterRegistry::add( FeaturesToNodeFilterFactory* factory )
+{
+    _factories.push_back( factory );
+}
+
+#define FEATURES_TO_NODE_FILTER_OPTIONS_TAG "__osgEarth::FeaturesToNodeFilterOptions"
+
+FeaturesToNodeFilter*
+FeaturesToNodeFilterRegistry::create(
+   const Config& conf,
+   const osgDB::Options* dbo,
+   osgEarth::Style style)
+{
+    const std::string& driver = conf.key();
+
+    osg::ref_ptr<FeaturesToNodeFilter> result;
+
+    for (FeaturesToNodeFilterFactoryList::iterator itr = _factories.begin(); result == 0L && itr != _factories.end(); itr++)
+    {
+        result = itr->get()->create(conf, style);
+    }
+
+    if ( !result.valid() )
+    {
+        // not found; try to load from plugin.
+        if ( driver.empty() )
+        {
+            OE_WARN << LC << "ILLEGAL- no driver set for features to node filter" << std::endl;
+            return 0L;
+        }
+
+        ConfigOptions options(conf);
+
+        osg::ref_ptr<osgDB::Options> dbopt = Registry::instance()->cloneOrCreateOptions(dbo);
+        dbopt->setPluginData( FEATURES_TO_NODE_FILTER_OPTIONS_TAG, (void*)&options );
+
+        std::string driverExt = std::string( ".osgearth_featurestonodefilter_" ) + driver;
+        osg::ref_ptr<osg::Object> object = osgDB::readRefObjectFile( driverExt, dbopt.get() );
+        result = dynamic_cast<FeaturesToNodeFilter*>( object.release() );
+    }
+
+    if ( !result.valid() )
+    {
+        OE_WARN << LC << "Failed to load FeaturesToNodeFilter driver \"" << driver << "\"" << std::endl;
+    }
+
+    return result.release();
+}
+
+const ConfigOptions&
+FeaturesToNodeFilterDriver::getConfigOptions(const osgDB::Options* options) const
+{
+    static ConfigOptions s_default;
+    const void* data = options->getPluginData(FEATURES_TO_NODE_FILTER_OPTIONS_TAG);
+    return data ? *static_cast<const ConfigOptions*>(data) : s_default;
+}
diff --git a/src/osgEarth/GeometryCompiler.cpp b/src/osgEarth/GeometryCompiler.cpp
index f7400ed5e8..c3ac7ded5a 100644
--- a/src/osgEarth/GeometryCompiler.cpp
+++ b/src/osgEarth/GeometryCompiler.cpp
@@ -1,606 +1,615 @@
-/* -*-c++-*- */
-/* osgEarth - Dynamic map generation toolkit 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.
- *
- * 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/>
- */
-#include "GeometryCompiler"
-
-#include <osgEarth/BuildGeometryFilter>
-#include <osgEarth/BuildTextFilter>
-#include <osgEarth/AltitudeFilter>
-#include <osgEarth/CentroidFilter>
-#include <osgEarth/ExtrudeGeometryFilter>
-#include <osgEarth/ScatterFilter>
-#include <osgEarth/SubstituteModelFilter>
-#include <osgEarth/TessellateOperator>
-#include <osgEarth/Session>
-
-#include <osgEarth/Utils>
-#include <osgEarth/CullingUtils>
-#include <osgEarth/Registry>
-#include <osgEarth/Capabilities>
-#include <osgEarth/ShaderGenerator>
-#include <osgEarth/ShaderUtils>
-#include <osgEarth/Utils>
-#include <osgEarth/Metrics>
-
-#include <osg/MatrixTransform>
-#include <osg/Timer>
-#include <osg/KdTree>
-#include <osgDB/WriteFile>
-#include <osgUtil/Optimizer>
-
-#include <cstdlib>
-
-#define LC "[GeometryCompiler] "
-
-using namespace osgEarth;
-using namespace osgEarth::Util;
-using namespace osgEarth::Util;
-
-//#define PROFILING 1
-
-//-----------------------------------------------------------------------
-
-GeometryCompilerOptions GeometryCompilerOptions::s_defaults(true);
-
-void
-GeometryCompilerOptions::setDefaults(const GeometryCompilerOptions& defaults)
-{
-   s_defaults = defaults;
-}
-
-// defaults.
-GeometryCompilerOptions::GeometryCompilerOptions(bool stockDefaults) :
-_maxGranularity_deg    ( 10.0 ),
-_mergeGeometry         ( true ),
-_clustering            ( false ),
-_instancing            ( true ),
-_ignoreAlt             ( false ),
-_shaderPolicy          ( SHADERPOLICY_GENERATE ),
-_geoInterp             ( GEOINTERP_GREAT_CIRCLE ),
-_optimizeStateSharing  ( true ),
-_optimize              ( false ),
-_optimizeVertexOrdering( true ),
-_validate              ( false ),
-_maxPolyTilingAngle    ( 45.0f ),
-_useOSGTessellator     ( false )
-{
-    //nop
-}
-
-//-----------------------------------------------------------------------
-
-GeometryCompilerOptions::GeometryCompilerOptions(const ConfigOptions& conf) :
-_maxGranularity_deg    ( s_defaults.maxGranularity().value() ),
-_mergeGeometry         ( s_defaults.mergeGeometry().value() ),
-_clustering            ( s_defaults.clustering().value() ),
-_instancing            ( s_defaults.instancing().value() ),
-_ignoreAlt             ( s_defaults.ignoreAltitudeSymbol().value() ),
-_shaderPolicy          ( s_defaults.shaderPolicy().value() ),
-_geoInterp             ( s_defaults.geoInterp().value() ),
-_optimizeStateSharing  ( s_defaults.optimizeStateSharing().value() ),
-_optimize              ( s_defaults.optimize().value() ),
-_optimizeVertexOrdering( s_defaults.optimizeVertexOrdering().value() ),
-_validate              ( s_defaults.validate().value() ),
-_maxPolyTilingAngle    ( s_defaults.maxPolygonTilingAngle().value() ),
-_useOSGTessellator     (s_defaults.useOSGTessellator().value())
-{
-    fromConfig(conf.getConfig());
-}
-
-void
-GeometryCompilerOptions::fromConfig( const Config& conf )
-{
-    conf.get( "max_granularity",  _maxGranularity_deg );
-    conf.get( "merge_geometry",   _mergeGeometry );
-    conf.get( "clustering",       _clustering );
-    conf.get( "instancing",       _instancing );
-    conf.get( "feature_name",     _featureNameExpr );
-    conf.get( "ignore_altitude",  _ignoreAlt );
-    conf.get( "geo_interpolation", "great_circle", _geoInterp, GEOINTERP_GREAT_CIRCLE );
-    conf.get( "geo_interpolation", "rhumb_line",   _geoInterp, GEOINTERP_RHUMB_LINE );
-    conf.get( "optimize_state_sharing", _optimizeStateSharing );
-    conf.get( "optimize", _optimize );
-    conf.get( "optimize_vertex_ordering", _optimizeVertexOrdering);
-    conf.get( "validate", _validate );
-    conf.get( "max_polygon_tiling_angle", _maxPolyTilingAngle );
-    conf.get( "use_osg_tessellator", _useOSGTessellator);
-
-    conf.get( "shader_policy", "disable",  _shaderPolicy, SHADERPOLICY_DISABLE );
-    conf.get( "shader_policy", "inherit",  _shaderPolicy, SHADERPOLICY_INHERIT );
-    conf.get( "shader_policy", "generate", _shaderPolicy, SHADERPOLICY_GENERATE );
-}
-
-Config
-GeometryCompilerOptions::getConfig() const
-{
-    Config conf;
-    conf.set( "max_granularity",  _maxGranularity_deg );
-    conf.set( "merge_geometry",   _mergeGeometry );
-    conf.set( "clustering",       _clustering );
-    conf.set( "instancing",       _instancing );
-    conf.set( "feature_name",     _featureNameExpr );
-    conf.set( "ignore_altitude",  _ignoreAlt );
-    conf.set( "geo_interpolation", "great_circle", _geoInterp, GEOINTERP_GREAT_CIRCLE );
-    conf.set( "geo_interpolation", "rhumb_line",   _geoInterp, GEOINTERP_RHUMB_LINE );
-    conf.set( "optimize_state_sharing", _optimizeStateSharing );
-    conf.set( "optimize", _optimize );
-    conf.set( "optimize_vertex_ordering", _optimizeVertexOrdering);
-    conf.set( "validate", _validate );
-    conf.set( "max_polygon_tiling_angle", _maxPolyTilingAngle );
-    conf.set( "use_osg_tessellator", _useOSGTessellator);
-
-    conf.set( "shader_policy", "disable",  _shaderPolicy, SHADERPOLICY_DISABLE );
-    conf.set( "shader_policy", "inherit",  _shaderPolicy, SHADERPOLICY_INHERIT );
-    conf.set( "shader_policy", "generate", _shaderPolicy, SHADERPOLICY_GENERATE );
-    return conf;
-}
-
-
-//-----------------------------------------------------------------------
-
-GeometryCompiler::GeometryCompiler()
-{
-    //nop
-}
-
-GeometryCompiler::GeometryCompiler( const GeometryCompilerOptions& options ) :
-_options( options )
-{
-    //nop
-}
-
-osg::Node*
-GeometryCompiler::compile(Geometry*             geometry,
-                          const Style&          style,
-                          const FilterContext&  context)
-{
-    osg::ref_ptr<Feature> f = new Feature(geometry, 0L); // no SRS!
-    return compile(f.get(), style, context);
-}
-
-osg::Node*
-GeometryCompiler::compile(Geometry*             geometry,
-                          const Style&          style)
-{
-    osg::ref_ptr<Feature> f = new Feature(geometry, 0L); // no SRS!
-    return compile(f.get(), style, FilterContext(0L) );
-}
-
-osg::Node*
-GeometryCompiler::compile(Geometry*             geometry,
-                          const FilterContext&  context)
-{
-    return compile( geometry, Style(), context );
-}
-
-osg::Node*
-GeometryCompiler::compile(Feature*              feature,
-                          const Style&          style,
-                          const FilterContext&  context)
-{
-    FeatureList workingSet;
-    workingSet.push_back(feature);
-    return compile(workingSet, style, context);
-}
-
-osg::Node*
-GeometryCompiler::compile(Feature*              feature,
-                          const FilterContext&  context)
-{
-    return compile(feature, *feature->style(), context);
-}
-
-osg::Node*
-GeometryCompiler::compile(FeatureCursor*        cursor,
-                          const Style&          style,
-                          const FilterContext&  context)
-
-{
-    // start by making a working copy of the feature set
-    FeatureList workingSet;
-    cursor->fill( workingSet );
-
-    return compile(workingSet, style, context);
-}
-
-osg::Node*
-GeometryCompiler::compile(FeatureList&          workingSet,
-                          const Style&          style,
-                          const FilterContext&  context)
-{
-    OE_PROFILING_ZONE;
-
-#ifdef PROFILING
-    osg::Timer_t p_start = osg::Timer::instance()->tick();
-    unsigned p_features = workingSet.size();
-#endif
-
-    // for debugging/validation.
-    std::vector<std::string> history;
-    bool trackHistory = (_options.validate() == true);
-
-    osg::ref_ptr<osg::Group> resultGroup = new osg::Group();
-
-    // create a filter context that will track feature data through the process
-    FilterContext sharedCX = context;
-
-    if ( !sharedCX.extent().isSet() && sharedCX.profile() )
-    {
-        sharedCX.extent() = sharedCX.profile()->getExtent();
-    }
-
-    // ref_ptr's to hold defaults in case we need them.
-    osg::ref_ptr<PointSymbol>   defaultPoint;
-    osg::ref_ptr<LineSymbol>    defaultLine;
-    osg::ref_ptr<PolygonSymbol> defaultPolygon;
-
-    // go through the Style and figure out which filters to use.
-    const PointSymbol*     point     = style.get<PointSymbol>();
-    const LineSymbol*      line      = style.get<LineSymbol>();
-    const PolygonSymbol*   polygon   = style.get<PolygonSymbol>();
-    const ExtrusionSymbol* extrusion = style.get<ExtrusionSymbol>();
-    const AltitudeSymbol*  altitude  = style.get<AltitudeSymbol>();
-    const TextSymbol*      text      = style.get<TextSymbol>();
-    const IconSymbol*      icon      = style.get<IconSymbol>();
-    const ModelSymbol*     model     = style.get<ModelSymbol>();
-    const RenderSymbol*    render    = style.get<RenderSymbol>();
-
-    // Perform tessellation first.
-    if ( line )
-    {
-        if ( line->tessellation().isSet() )
-        {
-            TessellateOperator filter;
-            filter.setNumPartitions( *line->tessellation() );
-            filter.setDefaultGeoInterp( _options.geoInterp().get() );
-            sharedCX = filter.push( workingSet, sharedCX );
-            if ( trackHistory ) history.push_back( "tessellation" );
-        }
-        else if ( line->tessellationSize().isSet() )
-        {
-            TessellateOperator filter;
-            filter.setMaxPartitionSize( *line->tessellationSize() );
-            filter.setDefaultGeoInterp( _options.geoInterp().get() );
-            sharedCX = filter.push( workingSet, sharedCX );
-            if ( trackHistory ) history.push_back( "tessellationSize" );
-        }
-    }
-
-    // if the style was empty, use some defaults based on the geometry type of the
-    // first feature.
-    if ( !point && !line && !polygon && !extrusion && !text && !model && !icon && workingSet.size() > 0 )
-    {
-        Feature* first = workingSet.begin()->get();
-        Geometry* geom = first->getGeometry();
-        if ( geom )
-        {
-            switch( geom->getComponentType() )
-            {
-            case Geometry::TYPE_LINESTRING:
-            case Geometry::TYPE_RING:
-                defaultLine = new LineSymbol();
-                line = defaultLine.get();
-                break;
-            case Geometry::TYPE_POINT:
-            case Geometry::TYPE_POINTSET:
-                defaultPoint = new PointSymbol();
-                point = defaultPoint.get();
-                break;
-            case Geometry::TYPE_POLYGON:
-                defaultPolygon = new PolygonSymbol();
-                polygon = defaultPolygon.get();
-                break;
-            case Geometry::TYPE_MULTI:
-            case Geometry::TYPE_UNKNOWN:
-                break;
-            }
-        }
-    }
-
-    // resample the geometry if necessary:
-    if (_options.resampleMode().isSet())
-    {
-        ResampleFilter resample;
-        resample.resampleMode() = *_options.resampleMode();
-        if (_options.resampleMaxLength().isSet())
-        {
-            resample.maxLength() = *_options.resampleMaxLength();
-        }
-        sharedCX = resample.push( workingSet, sharedCX );
-        if ( trackHistory ) history.push_back( "resample" );
-    }
-
-    // check whether we need to do elevation clamping:
-    bool altRequired =
-        _options.ignoreAltitudeSymbol() != true &&
-        altitude && (
-            altitude->clamping() != AltitudeSymbol::CLAMP_NONE ||
-            altitude->verticalOffset().isSet() ||
-            altitude->verticalScale().isSet() ||
-            altitude->script().isSet() );
-
-    // instance substitution (replaces marker)
-    if ( model )
-    {
-        const InstanceSymbol* instance = (const InstanceSymbol*)model;
-
-        // use a separate filter context since we'll be munging the data
-        FilterContext localCX = sharedCX;
-
-        if ( trackHistory ) history.push_back( "model");
-
-        if ( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM   ||
-             instance->placement() == InstanceSymbol::PLACEMENT_INTERVAL )
-        {
-            ScatterFilter scatter;
-            scatter.setDensity( *instance->density() );
-            scatter.setRandom( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM );
-            scatter.setRandomSeed( *instance->randomSeed() );
-            localCX = scatter.push( workingSet, localCX );
-            if ( trackHistory ) history.push_back( "scatter" );
-        }
-        else if ( instance->placement() == InstanceSymbol::PLACEMENT_CENTROID )
-        {
-            CentroidFilter centroid;
-            localCX = centroid.push( workingSet, localCX );
-            if ( trackHistory ) history.push_back( "centroid" );
-        }
-
-        if ( altRequired )
-        {
-            AltitudeFilter clamp;
-            clamp.setPropertiesFromStyle( style );
-            localCX = clamp.push( workingSet, localCX );
-            if ( trackHistory ) history.push_back( "altitude" );
-        }
-
-        SubstituteModelFilter sub( style );
-
-        // activate clustering
-        sub.setClustering( *_options.clustering() );
-
-        // activate draw-instancing
-        sub.setUseDrawInstanced( *_options.instancing() );
-
-        // activate feature naming
-        if ( _options.featureName().isSet() )
-            sub.setFeatureNameExpr( *_options.featureName() );
-
-
-        osg::Node* node = sub.push( workingSet, localCX );
-        if ( node )
-        {
-            if ( trackHistory ) history.push_back( "substitute" );
-
-            resultGroup->addChild( node );
-        }
-    }
-
-    // extruded geometry
-    if ( extrusion )
-    {
-        if ( altRequired )
-        {
-            AltitudeFilter clamp;
-            clamp.setPropertiesFromStyle( style );
-            sharedCX = clamp.push( workingSet, sharedCX );
-            if ( trackHistory ) history.push_back( "altitude" );
-            altRequired = false;
-        }
-
-        ExtrudeGeometryFilter extrude;
-        extrude.setStyle( style );
-
-        // apply per-feature naming if requested.
-        if ( _options.featureName().isSet() )
-            extrude.setFeatureNameExpr( *_options.featureName() );
-
-        if (_options.mergeGeometry().isSet())
-            extrude.setMergeGeometry(*_options.mergeGeometry());
-        //else if (_options.optimize() == true)
-        //    extrude.setMergeGeometry(false);
-            
-
-        osg::Node* node = extrude.push( workingSet, sharedCX );
-        if ( node )
-        {
-            if ( trackHistory ) history.push_back( "extrude" );
-            resultGroup->addChild( node );
-        }
-
-    }
-
-    // simple geometry
-    else if ( point || line || polygon )
-    {
-        if ( altRequired )
-        {
-            AltitudeFilter clamp;
-            clamp.setPropertiesFromStyle( style );
-            sharedCX = clamp.push( workingSet, sharedCX );
-            if ( trackHistory ) history.push_back( "altitude" );
-            altRequired = false;
-        }
-
-        BuildGeometryFilter filter( style );
-
-        filter.maxGranularity() = *_options.maxGranularity();
-        filter.geoInterp()      = *_options.geoInterp();
-        filter.useOSGTessellator() = *_options.useOSGTessellator();
-
-        if (_options.maxPolygonTilingAngle().isSet())
-            filter.maxPolygonTilingAngle() = *_options.maxPolygonTilingAngle();
-
-        if ( _options.featureName().isSet() )
-            filter.featureName() = *_options.featureName();
-
-        if (_options.optimizeVertexOrdering().isSet())
-            filter.optimizeVertexOrdering() = *_options.optimizeVertexOrdering();
-
-        if (render && render->maxCreaseAngle().isSet())
-            filter.maxCreaseAngle() = render->maxCreaseAngle().get();
-
-        osg::Node* node = filter.push( workingSet, sharedCX );
-        if ( node )
-        {
-            if ( trackHistory ) history.push_back( "geometry" );
-            resultGroup->addChild( node );
-        }
-    }
-
-    if ( text || icon )
-    {
-        // Only clamp annotation types when the technique is
-        // explicity set to MAP. Otherwise, the annotation subsystem
-        // will automatically use SCENE clamping.
-        bool altRequiredForAnnotations =
-            altRequired &&
-            altitude->technique().isSetTo(altitude->TECHNIQUE_MAP);
-
-        if ( altRequiredForAnnotations )
-        {
-            AltitudeFilter clamp;
-            clamp.setPropertiesFromStyle( style );
-            sharedCX = clamp.push( workingSet, sharedCX );
-            if ( trackHistory ) history.push_back( "altitude" );
-            altRequired = false;
-        }
-
-        BuildTextFilter filter( style );
-        osg::Node* node = filter.push( workingSet, sharedCX );
-        if ( node )
-        {
-            if ( trackHistory ) history.push_back( "text" );
-            resultGroup->addChild( node );
-        }
-    }
-
-    if (Registry::capabilities().supportsGLSL())
-    {
-        ShaderPolicy shaderPolicy = _options.shaderPolicy().get();
-
-        if (shaderPolicy == SHADERPOLICY_GENERATE)
-        {
-            // no ss cache because we will optimize later.
-            Registry::shaderGenerator().run(
-                resultGroup.get(),
-                "GeometryCompiler shadergen" );
-        }
-        else if (shaderPolicy == SHADERPOLICY_DISABLE )
-        {
-            resultGroup->getOrCreateStateSet()->setAttributeAndModes(
-                new osg::Program(),
-                osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE );
-
-            if ( trackHistory ) history.push_back( "no shaders" );
-        }
-    }
-
-    // Optimize stateset sharing.
-    if ( _options.optimizeStateSharing() == true )
-    {
-        // Common state set cache?
-        osg::ref_ptr<StateSetCache> sscache;
-        if ( sharedCX.getSession() )
-        {
-            // with a shared cache, don't combine statesets. They may be
-            // in the live graph
-            sscache = sharedCX.getSession()->getStateSetCache();
-            sscache->consolidateStateAttributes( resultGroup.get() );
-        }
-        else
-        {
-            // isolated: perform full optimization
-            sscache = new StateSetCache();
-            sscache->optimize( resultGroup.get() );
-        }
-
-        if ( trackHistory ) history.push_back( "share state" );
-    }
-
-#if 0 // never do this, let the filters do it.
-    if ( _options.optimize() == true )
-    {
-        OE_DEBUG << LC << "optimize begin" << std::endl;
-
-        // Run the optimizer on the resulting graph
-        int optimizations =
-            osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS |
-            osgUtil::Optimizer::REMOVE_REDUNDANT_NODES |
-            osgUtil::Optimizer::COMBINE_ADJACENT_LODS |
-            osgUtil::Optimizer::SHARE_DUPLICATE_STATE |
-            //osgUtil::Optimizer::MERGE_GEOMETRY |
-            osgUtil::Optimizer::CHECK_GEOMETRY |
-            osgUtil::Optimizer::MERGE_GEODES |
-            osgUtil::Optimizer::STATIC_OBJECT_DETECTION;
-
-        osgUtil::Optimizer opt;
-        opt.optimize(resultGroup.get(), optimizations);
-
-        osgUtil::Optimizer::MergeGeometryVisitor mg;
-        mg.setTargetMaximumNumberOfVertices(Registry::instance()->getMaxNumberOfVertsPerDrawable());
-        resultGroup->accept(mg);
-
-        OE_DEBUG << LC << "optimize complete" << std::endl;
-
-        if ( trackHistory ) history.push_back( "optimize" );
-    }
-#endif
-
-    //test: dump the tile to disk
-    //OE_WARN << "Writing GC node file to out.osgt..." << std::endl;
-    //osgDB::writeNodeFile( *(resultGroup.get()), "out.osgt" );
-
-#ifdef PROFILING
-    static double totalTime = 0.0;
-    static Threading::Mutex totalTimeMutex;
-    osg::Timer_t p_end = osg::Timer::instance()->tick();
-    double t = osg::Timer::instance()->delta_s(p_start, p_end);
-    totalTimeMutex.lock();
-    totalTime += t;
-    totalTimeMutex.unlock();
-    OE_INFO << LC
-        << "features = " << p_features
-        << ", time = " << t << " s.  cummulative = "
-        << totalTime << " s."
-        << std::endl;
-#endif
-
-
-    if ( _options.validate() == true )
-    {
-        OE_NOTICE << LC << "-- Start Debugging --\n";
-        std::stringstream buf;
-        buf << "HISTORY ";
-        for(std::vector<std::string>::iterator h = history.begin(); h != history.end(); ++h)
-            buf << ".. " << *h;
-        OE_NOTICE << LC << buf.str() << "\n";
-        osgEarth::GeometryValidator validator;
-        resultGroup->accept(validator);
-        OE_NOTICE << LC << "-- End Debugging --\n";
-    }
-
-    // Build kdtrees to increase intersection speed.
-    if (osgDB::Registry::instance()->getKdTreeBuilder())
-    {
-        osg::ref_ptr< osg::KdTreeBuilder > kdTreeBuilder = osgDB::Registry::instance()->getKdTreeBuilder()->clone();
-        resultGroup->accept(*kdTreeBuilder.get());
-    }
-
-    return resultGroup.release();
-}
+/* -*-c++-*- */
+/* osgEarth - Dynamic map generation toolkit 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.
+ *
+ * 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/>
+ */
+#include "GeometryCompiler"
+
+#include <osgEarth/BuildGeometryFilter>
+#include <osgEarth/BuildTextFilter>
+#include <osgEarth/AltitudeFilter>
+#include <osgEarth/CentroidFilter>
+#include <osgEarth/ExtrudeGeometryFilter>
+#include <osgEarth/ScatterFilter>
+#include <osgEarth/SubstituteModelFilter>
+#include <osgEarth/TessellateOperator>
+#include <osgEarth/Session>
+
+#include <osgEarth/Utils>
+#include <osgEarth/CullingUtils>
+#include <osgEarth/Registry>
+#include <osgEarth/Capabilities>
+#include <osgEarth/ShaderGenerator>
+#include <osgEarth/ShaderUtils>
+#include <osgEarth/Utils>
+#include <osgEarth/Metrics>
+
+#include <osg/MatrixTransform>
+#include <osg/Timer>
+#include <osg/KdTree>
+#include <osgDB/WriteFile>
+#include <osgUtil/Optimizer>
+
+#include <cstdlib>
+
+#define LC "[GeometryCompiler] "
+
+using namespace osgEarth;
+using namespace osgEarth::Util;
+using namespace osgEarth::Util;
+
+//#define PROFILING 1
+
+//-----------------------------------------------------------------------
+
+GeometryCompilerOptions GeometryCompilerOptions::s_defaults(true);
+
+void
+GeometryCompilerOptions::setDefaults(const GeometryCompilerOptions& defaults)
+{
+   s_defaults = defaults;
+}
+
+// defaults.
+GeometryCompilerOptions::GeometryCompilerOptions(bool stockDefaults) :
+_maxGranularity_deg    ( 10.0 ),
+_mergeGeometry         ( true ),
+_clustering            ( false ),
+_instancing            ( true ),
+_ignoreAlt             ( false ),
+_shaderPolicy          ( SHADERPOLICY_GENERATE ),
+_geoInterp             ( GEOINTERP_GREAT_CIRCLE ),
+_optimizeStateSharing  ( true ),
+_optimize              ( false ),
+_optimizeVertexOrdering( true ),
+_validate              ( false ),
+_maxPolyTilingAngle    ( 45.0f ),
+_useOSGTessellator     ( false )
+{
+    //nop
+}
+
+//-----------------------------------------------------------------------
+
+GeometryCompilerOptions::GeometryCompilerOptions(const ConfigOptions& conf) :
+_maxGranularity_deg    ( s_defaults.maxGranularity().value() ),
+_mergeGeometry         ( s_defaults.mergeGeometry().value() ),
+_clustering            ( s_defaults.clustering().value() ),
+_instancing            ( s_defaults.instancing().value() ),
+_ignoreAlt             ( s_defaults.ignoreAltitudeSymbol().value() ),
+_shaderPolicy          ( s_defaults.shaderPolicy().value() ),
+_geoInterp             ( s_defaults.geoInterp().value() ),
+_optimizeStateSharing  ( s_defaults.optimizeStateSharing().value() ),
+_optimize              ( s_defaults.optimize().value() ),
+_optimizeVertexOrdering( s_defaults.optimizeVertexOrdering().value() ),
+_validate              ( s_defaults.validate().value() ),
+_maxPolyTilingAngle    ( s_defaults.maxPolygonTilingAngle().value() ),
+_useOSGTessellator     (s_defaults.useOSGTessellator().value())
+{
+    fromConfig(conf.getConfig());
+}
+
+void
+GeometryCompilerOptions::fromConfig( const Config& conf )
+{
+    conf.get( "max_granularity",  _maxGranularity_deg );
+    conf.get( "merge_geometry",   _mergeGeometry );
+    conf.get( "clustering",       _clustering );
+    conf.get( "instancing",       _instancing );
+    conf.get( "feature_name",     _featureNameExpr );
+    conf.get( "ignore_altitude",  _ignoreAlt );
+    conf.get( "geo_interpolation", "great_circle", _geoInterp, GEOINTERP_GREAT_CIRCLE );
+    conf.get( "geo_interpolation", "rhumb_line",   _geoInterp, GEOINTERP_RHUMB_LINE );
+    conf.get( "optimize_state_sharing", _optimizeStateSharing );
+    conf.get( "optimize", _optimize );
+    conf.get( "optimize_vertex_ordering", _optimizeVertexOrdering);
+    conf.get( "validate", _validate );
+    conf.get( "max_polygon_tiling_angle", _maxPolyTilingAngle );
+    conf.get( "use_osg_tessellator", _useOSGTessellator);
+
+    conf.get( "shader_policy", "disable",  _shaderPolicy, SHADERPOLICY_DISABLE );
+    conf.get( "shader_policy", "inherit",  _shaderPolicy, SHADERPOLICY_INHERIT );
+    conf.get( "shader_policy", "generate", _shaderPolicy, SHADERPOLICY_GENERATE );
+}
+
+Config
+GeometryCompilerOptions::getConfig() const
+{
+    Config conf;
+    conf.set( "max_granularity",  _maxGranularity_deg );
+    conf.set( "merge_geometry",   _mergeGeometry );
+    conf.set( "clustering",       _clustering );
+    conf.set( "instancing",       _instancing );
+    conf.set( "feature_name",     _featureNameExpr );
+    conf.set( "ignore_altitude",  _ignoreAlt );
+    conf.set( "geo_interpolation", "great_circle", _geoInterp, GEOINTERP_GREAT_CIRCLE );
+    conf.set( "geo_interpolation", "rhumb_line",   _geoInterp, GEOINTERP_RHUMB_LINE );
+    conf.set( "optimize_state_sharing", _optimizeStateSharing );
+    conf.set( "optimize", _optimize );
+    conf.set( "optimize_vertex_ordering", _optimizeVertexOrdering);
+    conf.set( "validate", _validate );
+    conf.set( "max_polygon_tiling_angle", _maxPolyTilingAngle );
+    conf.set( "use_osg_tessellator", _useOSGTessellator);
+
+    conf.set( "shader_policy", "disable",  _shaderPolicy, SHADERPOLICY_DISABLE );
+    conf.set( "shader_policy", "inherit",  _shaderPolicy, SHADERPOLICY_INHERIT );
+    conf.set( "shader_policy", "generate", _shaderPolicy, SHADERPOLICY_GENERATE );
+    return conf;
+}
+
+
+//-----------------------------------------------------------------------
+
+GeometryCompiler::GeometryCompiler()
+{
+    //nop
+}
+
+GeometryCompiler::GeometryCompiler( const GeometryCompilerOptions& options ) :
+_options( options )
+{
+    //nop
+}
+
+osg::Node*
+GeometryCompiler::compile(Geometry*             geometry,
+                          const Style&          style,
+                          const FilterContext&  context)
+{
+    osg::ref_ptr<Feature> f = new Feature(geometry, 0L); // no SRS!
+    return compile(f.get(), style, context);
+}
+
+osg::Node*
+GeometryCompiler::compile(Geometry*             geometry,
+                          const Style&          style)
+{
+    osg::ref_ptr<Feature> f = new Feature(geometry, 0L); // no SRS!
+    return compile(f.get(), style, FilterContext(0L) );
+}
+
+osg::Node*
+GeometryCompiler::compile(Geometry*             geometry,
+                          const FilterContext&  context)
+{
+    return compile( geometry, Style(), context );
+}
+
+osg::Node*
+GeometryCompiler::compile(Feature*              feature,
+                          const Style&          style,
+                          const FilterContext&  context)
+{
+    FeatureList workingSet;
+    workingSet.push_back(feature);
+    return compile(workingSet, style, context);
+}
+
+osg::Node*
+GeometryCompiler::compile(Feature*              feature,
+                          const FilterContext&  context)
+{
+    return compile(feature, *feature->style(), context);
+}
+
+osg::Node*
+GeometryCompiler::compile(FeatureCursor*        cursor,
+                          const Style&          style,
+                          const FilterContext&  context)
+
+{
+    // start by making a working copy of the feature set
+    FeatureList workingSet;
+    cursor->fill( workingSet );
+
+    return compile(workingSet, style, context);
+}
+
+osg::Node*
+GeometryCompiler::compile(FeatureList&          workingSet,
+                          const Style&          style,
+                          const FilterContext&  context)
+{
+    OE_PROFILING_ZONE;
+
+#ifdef PROFILING
+    osg::Timer_t p_start = osg::Timer::instance()->tick();
+    unsigned p_features = workingSet.size();
+#endif
+
+    // for debugging/validation.
+    std::vector<std::string> history;
+    bool trackHistory = (_options.validate() == true);
+
+    osg::ref_ptr<osg::Group> resultGroup = new osg::Group();
+
+    // create a filter context that will track feature data through the process
+    FilterContext sharedCX = context;
+
+    if ( !sharedCX.extent().isSet() && sharedCX.profile() )
+    {
+        sharedCX.extent() = sharedCX.profile()->getExtent();
+    }
+
+    // ref_ptr's to hold defaults in case we need them.
+    osg::ref_ptr<PointSymbol>   defaultPoint;
+    osg::ref_ptr<LineSymbol>    defaultLine;
+    osg::ref_ptr<PolygonSymbol> defaultPolygon;
+
+    // go through the Style and figure out which filters to use.
+    const PointSymbol*     point     = style.get<PointSymbol>();
+    const LineSymbol*      line      = style.get<LineSymbol>();
+    const PolygonSymbol*   polygon   = style.get<PolygonSymbol>();
+    const ExtrusionSymbol* extrusion = style.get<ExtrusionSymbol>();
+    const AltitudeSymbol*  altitude  = style.get<AltitudeSymbol>();
+    const TextSymbol*      text      = style.get<TextSymbol>();
+    const IconSymbol*      icon      = style.get<IconSymbol>();
+    const ModelSymbol*     model     = style.get<ModelSymbol>();
+    const RenderSymbol*    render    = style.get<RenderSymbol>();
+
+    // Perform tessellation first.
+    if ( line )
+    {
+        if ( line->tessellation().isSet() )
+        {
+            TessellateOperator filter;
+            filter.setNumPartitions( *line->tessellation() );
+            filter.setDefaultGeoInterp( _options.geoInterp().get() );
+            sharedCX = filter.push( workingSet, sharedCX );
+            if ( trackHistory ) history.push_back( "tessellation" );
+        }
+        else if ( line->tessellationSize().isSet() )
+        {
+            TessellateOperator filter;
+            filter.setMaxPartitionSize( *line->tessellationSize() );
+            filter.setDefaultGeoInterp( _options.geoInterp().get() );
+            sharedCX = filter.push( workingSet, sharedCX );
+            if ( trackHistory ) history.push_back( "tessellationSize" );
+        }
+    }
+
+    // if the style was empty, use some defaults based on the geometry type of the
+    // first feature.
+    if ( !point && !line && !polygon && !extrusion && !text && !model && !icon && workingSet.size() > 0 )
+    {
+        Feature* first = workingSet.begin()->get();
+        Geometry* geom = first->getGeometry();
+        if ( geom )
+        {
+            switch( geom->getComponentType() )
+            {
+            case Geometry::TYPE_LINESTRING:
+            case Geometry::TYPE_RING:
+                defaultLine = new LineSymbol();
+                line = defaultLine.get();
+                break;
+            case Geometry::TYPE_POINT:
+            case Geometry::TYPE_POINTSET:
+                defaultPoint = new PointSymbol();
+                point = defaultPoint.get();
+                break;
+            case Geometry::TYPE_POLYGON:
+                defaultPolygon = new PolygonSymbol();
+                polygon = defaultPolygon.get();
+                break;
+            case Geometry::TYPE_MULTI:
+            case Geometry::TYPE_UNKNOWN:
+                break;
+            }
+        }
+    }
+
+    // resample the geometry if necessary:
+    if (_options.resampleMode().isSet())
+    {
+        ResampleFilter resample;
+        resample.resampleMode() = *_options.resampleMode();
+        if (_options.resampleMaxLength().isSet())
+        {
+            resample.maxLength() = *_options.resampleMaxLength();
+        }
+        sharedCX = resample.push( workingSet, sharedCX );
+        if ( trackHistory ) history.push_back( "resample" );
+    }
+
+    // check whether we need to do elevation clamping:
+    bool altRequired =
+        _options.ignoreAltitudeSymbol() != true &&
+        altitude && (
+            altitude->clamping() != AltitudeSymbol::CLAMP_NONE ||
+            altitude->verticalOffset().isSet() ||
+            altitude->verticalScale().isSet() ||
+            altitude->script().isSet() );
+
+    // instance substitution (replaces marker)
+    if ( model )
+    {
+        const InstanceSymbol* instance = (const InstanceSymbol*)model;
+
+        // use a separate filter context since we'll be munging the data
+        FilterContext localCX = sharedCX;
+
+        if ( trackHistory ) history.push_back( "model");
+
+        if ( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM   ||
+             instance->placement() == InstanceSymbol::PLACEMENT_INTERVAL )
+        {
+            ScatterFilter scatter;
+            scatter.setDensity( *instance->density() );
+            scatter.setRandom( instance->placement() == InstanceSymbol::PLACEMENT_RANDOM );
+            scatter.setRandomSeed( *instance->randomSeed() );
+            localCX = scatter.push( workingSet, localCX );
+            if ( trackHistory ) history.push_back( "scatter" );
+        }
+        else if ( instance->placement() == InstanceSymbol::PLACEMENT_CENTROID )
+        {
+            CentroidFilter centroid;
+            localCX = centroid.push( workingSet, localCX );
+            if ( trackHistory ) history.push_back( "centroid" );
+        }
+
+        if ( altRequired )
+        {
+            AltitudeFilter clamp;
+            clamp.setPropertiesFromStyle( style );
+            localCX = clamp.push( workingSet, localCX );
+            if ( trackHistory ) history.push_back( "altitude" );
+        }
+
+        SubstituteModelFilter sub( style );
+
+        // activate clustering
+        sub.setClustering( *_options.clustering() );
+
+        // activate draw-instancing
+        sub.setUseDrawInstanced( *_options.instancing() );
+
+        // activate feature naming
+        if ( _options.featureName().isSet() )
+            sub.setFeatureNameExpr( *_options.featureName() );
+
+
+        osg::Node* node = sub.push( workingSet, localCX );
+        if ( node )
+        {
+            if ( trackHistory ) history.push_back( "substitute" );
+
+            resultGroup->addChild( node );
+        }
+    }
+
+    // extruded geometry
+    if ( extrusion )
+    {
+        if ( altRequired )
+        {
+            AltitudeFilter clamp;
+            clamp.setPropertiesFromStyle( style );
+            sharedCX = clamp.push( workingSet, sharedCX );
+            if ( trackHistory ) history.push_back( "altitude" );
+            altRequired = false;
+        }
+
+        ExtrudeGeometryFilter extrude;
+        extrude.setStyle( style );
+
+        // apply per-feature naming if requested.
+        if ( _options.featureName().isSet() )
+            extrude.setFeatureNameExpr( *_options.featureName() );
+
+        if (_options.mergeGeometry().isSet())
+            extrude.setMergeGeometry(*_options.mergeGeometry());
+        //else if (_options.optimize() == true)
+        //    extrude.setMergeGeometry(false);
+            
+
+        osg::Node* node = extrude.push( workingSet, sharedCX );
+        if ( node )
+        {
+            if ( trackHistory ) history.push_back( "extrude" );
+            resultGroup->addChild( node );
+        }
+
+    }
+
+    // simple geometry
+    else if ( point || line || polygon )
+    {
+        if ( altRequired )
+        {
+            AltitudeFilter clamp;
+            clamp.setPropertiesFromStyle( style );
+            sharedCX = clamp.push( workingSet, sharedCX );
+            if ( trackHistory ) history.push_back( "altitude" );
+            altRequired = false;
+        }
+
+        BuildGeometryFilter filter( style );
+
+        filter.maxGranularity() = *_options.maxGranularity();
+        filter.geoInterp()      = *_options.geoInterp();
+        filter.useOSGTessellator() = *_options.useOSGTessellator();
+
+        if (_options.maxPolygonTilingAngle().isSet())
+            filter.maxPolygonTilingAngle() = *_options.maxPolygonTilingAngle();
+
+        if ( _options.featureName().isSet() )
+            filter.featureName() = *_options.featureName();
+
+        if (_options.optimizeVertexOrdering().isSet())
+            filter.optimizeVertexOrdering() = *_options.optimizeVertexOrdering();
+
+        if (render && render->maxCreaseAngle().isSet())
+            filter.maxCreaseAngle() = render->maxCreaseAngle().get();
+
+        osg::Node* node = filter.push( workingSet, sharedCX );
+        if ( node )
+        {
+            if ( trackHistory ) history.push_back( "geometry" );
+            resultGroup->addChild( node );
+        }
+    }
+
+    if ( text || icon )
+    {
+        // Only clamp annotation types when the technique is
+        // explicity set to MAP. Otherwise, the annotation subsystem
+        // will automatically use SCENE clamping.
+        bool altRequiredForAnnotations =
+            altRequired &&
+            altitude->technique().isSetTo(altitude->TECHNIQUE_MAP);
+
+        if ( altRequiredForAnnotations )
+        {
+            AltitudeFilter clamp;
+            clamp.setPropertiesFromStyle( style );
+            sharedCX = clamp.push( workingSet, sharedCX );
+            if ( trackHistory ) history.push_back( "altitude" );
+            altRequired = false;
+        }
+
+        std::string ftnf_driver = style.getSymbol<TextSymbol>()->provider().get();
+        Config ftnf_cfg(ftnf_driver);
+        osg::ref_ptr<FeaturesToNodeFilter> filter = FeaturesToNodeFilterRegistry::instance()->create(
+            ftnf_cfg,
+            0L,
+            style);
+
+        if (!filter)
+           filter = new BuildTextFilter( style );
+
+        osg::Node* node = filter->push( workingSet, sharedCX );
+        if ( node )
+        {
+            if ( trackHistory ) history.push_back( "text" );
+            resultGroup->addChild( node );
+        }
+    }
+
+    if (Registry::capabilities().supportsGLSL())
+    {
+        ShaderPolicy shaderPolicy = _options.shaderPolicy().get();
+
+        if (shaderPolicy == SHADERPOLICY_GENERATE)
+        {
+            // no ss cache because we will optimize later.
+            Registry::shaderGenerator().run(
+                resultGroup.get(),
+                "GeometryCompiler shadergen" );
+        }
+        else if (shaderPolicy == SHADERPOLICY_DISABLE )
+        {
+            resultGroup->getOrCreateStateSet()->setAttributeAndModes(
+                new osg::Program(),
+                osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE );
+
+            if ( trackHistory ) history.push_back( "no shaders" );
+        }
+    }
+
+    // Optimize stateset sharing.
+    if ( _options.optimizeStateSharing() == true )
+    {
+        // Common state set cache?
+        osg::ref_ptr<StateSetCache> sscache;
+        if ( sharedCX.getSession() )
+        {
+            // with a shared cache, don't combine statesets. They may be
+            // in the live graph
+            sscache = sharedCX.getSession()->getStateSetCache();
+            sscache->consolidateStateAttributes( resultGroup.get() );
+        }
+        else
+        {
+            // isolated: perform full optimization
+            sscache = new StateSetCache();
+            sscache->optimize( resultGroup.get() );
+        }
+
+        if ( trackHistory ) history.push_back( "share state" );
+    }
+
+#if 0 // never do this, let the filters do it.
+    if ( _options.optimize() == true )
+    {
+        OE_DEBUG << LC << "optimize begin" << std::endl;
+
+        // Run the optimizer on the resulting graph
+        int optimizations =
+            osgUtil::Optimizer::FLATTEN_STATIC_TRANSFORMS |
+            osgUtil::Optimizer::REMOVE_REDUNDANT_NODES |
+            osgUtil::Optimizer::COMBINE_ADJACENT_LODS |
+            osgUtil::Optimizer::SHARE_DUPLICATE_STATE |
+            //osgUtil::Optimizer::MERGE_GEOMETRY |
+            osgUtil::Optimizer::CHECK_GEOMETRY |
+            osgUtil::Optimizer::MERGE_GEODES |
+            osgUtil::Optimizer::STATIC_OBJECT_DETECTION;
+
+        osgUtil::Optimizer opt;
+        opt.optimize(resultGroup.get(), optimizations);
+
+        osgUtil::Optimizer::MergeGeometryVisitor mg;
+        mg.setTargetMaximumNumberOfVertices(Registry::instance()->getMaxNumberOfVertsPerDrawable());
+        resultGroup->accept(mg);
+
+        OE_DEBUG << LC << "optimize complete" << std::endl;
+
+        if ( trackHistory ) history.push_back( "optimize" );
+    }
+#endif
+
+    //test: dump the tile to disk
+    //OE_WARN << "Writing GC node file to out.osgt..." << std::endl;
+    //osgDB::writeNodeFile( *(resultGroup.get()), "out.osgt" );
+
+#ifdef PROFILING
+    static double totalTime = 0.0;
+    static Threading::Mutex totalTimeMutex;
+    osg::Timer_t p_end = osg::Timer::instance()->tick();
+    double t = osg::Timer::instance()->delta_s(p_start, p_end);
+    totalTimeMutex.lock();
+    totalTime += t;
+    totalTimeMutex.unlock();
+    OE_INFO << LC
+        << "features = " << p_features
+        << ", time = " << t << " s.  cummulative = "
+        << totalTime << " s."
+        << std::endl;
+#endif
+
+
+    if ( _options.validate() == true )
+    {
+        OE_NOTICE << LC << "-- Start Debugging --\n";
+        std::stringstream buf;
+        buf << "HISTORY ";
+        for(std::vector<std::string>::iterator h = history.begin(); h != history.end(); ++h)
+            buf << ".. " << *h;
+        OE_NOTICE << LC << buf.str() << "\n";
+        osgEarth::GeometryValidator validator;
+        resultGroup->accept(validator);
+        OE_NOTICE << LC << "-- End Debugging --\n";
+    }
+
+    // Build kdtrees to increase intersection speed.
+    if (osgDB::Registry::instance()->getKdTreeBuilder())
+    {
+        osg::ref_ptr< osg::KdTreeBuilder > kdTreeBuilder = osgDB::Registry::instance()->getKdTreeBuilder()->clone();
+        resultGroup->accept(*kdTreeBuilder.get());
+    }
+
+    return resultGroup.release();
+}