[Git][debian-gis-team/mapnik][upstream] New upstream version 4.3.1+ds

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Fri Aug 28 16:01:48 BST 2026



Bas Couwenberg pushed to branch upstream at Debian GIS Project / mapnik


Commits:
407b4b9e by Bas Couwenberg at 2026-08-28T14:15:03+02:00
New upstream version 4.3.1+ds
- - - - -


14 changed files:

- CHANGELOG.md
- CMakeLists.txt
- include/mapnik/expression_evaluator.hpp
- include/mapnik/expression_node_types.hpp
- include/mapnik/feature.hpp
- include/mapnik/feature_style_processor_impl.hpp
- include/mapnik/rule_cache.hpp
- include/mapnik/text/harfbuzz_shaper.hpp
- include/mapnik/util/sort_by.hpp
- include/mapnik/version.hpp
- plugins/input/postgis/postgis_featureset.cpp
- test/unit/core/expressions_test.cpp
- test/unit/renderer/feature_style_processor.cpp
- test/unit/text/shaping.cpp


Changes:

=====================================
CHANGELOG.md
=====================================
@@ -6,6 +6,23 @@ Developers: Please commit along with changes.
 
 For a complete change history, see the git log.
 
+## Mapnik 4.3.1
+
+Released August 28th, 2026
+
+(Packaged from [38c207ee0](https://github.com/mapnik/mapnik/commit/38c207ee0))
+
+- CMake build - enable NDEBUG by default
+- Avoid copying expression attributes e1e6d31808877171d14c74d5974b923cce423595
+- Compare string literals directly d0ed0964ad9d6c23cee6171656b002d1304fff40
+- Hash feature attribute lookups d744e094b451a12b070173547f638b896d1386d8
+- Prioritize common expression nodes e981e4b43ed57aa209175626777735036f97cc88
+- Evaluate filters as booleans d59329887dc708612a670117e562070f15634b63
+- Index rules by filter value 508dd835252b5a523bffef076b6779b0c21e4588
+- Store PostGIS attributes by index 40c36ee0125a7056af617f607bda61e9eb21bf08
+- Correction for "Rendering complex Unicode sequences across multiple fonts in a fontset" PR #4574
+- Upgrade SCons to v4.11.1
+
 ## Mapnik 4.3.0
 
 Released July 24th, 2026


=====================================
CMakeLists.txt
=====================================
@@ -57,6 +57,7 @@ mapnik_option(USE_MULTITHREADED "enables the multithreaded features (threadsafe)
 mapnik_option(USE_NO_ATEXIT "disable atexit" OFF)
 mapnik_option(USE_NO_DLCLOSE "disable dlclose" OFF)
 mapnik_option(USE_DEBUG_OUTPUT "enables some debug messages for development" OFF)
+mapnik_option(USE_NDEBUG "defines NDEBUG for non-Debug builds. Turning this off makes mapbox::variant dispatch non-inlinable and much slower" ON)
 mapnik_option(USE_LOG "enables logging output. See log severity level." OFF)
 # 0 = debug
 # 1 = warn
@@ -295,6 +296,22 @@ if(USE_DEBUG_OUTPUT)
     list(APPEND MAPNIK_COMPILE_DEFS MAPNIK_DEBUG)
 endif()
 
+# NDEBUG does more for mapnik than disable assert(): mapbox::variant tags every
+# visit()/dispatcher::apply()/recursive_wrapper::get() with VARIANT_INLINE, which expands to
+# __attribute__((noinline)) when NDEBUG is not defined. mapnik's expression AST, mapnik::value
+# and all geometry visitation go through mapbox::variant, so an optimised build without NDEBUG
+# cannot inline its own hottest dispatch path (ref #4571).
+#
+# CMake only supplies -DNDEBUG via CMAKE_<LANG>_FLAGS_<CONFIG>, so a build that leaves
+# CMAKE_BUILD_TYPE empty or pins it to None - as Debian's `dh --buildsystem=cmake` does - never
+# gets it. Define it here instead of relying on the configuration name. Multi-config generators
+# pick the configuration at build time, where CMAKE_<LANG>_FLAGS_<CONFIG> already handles this.
+get_property(MAPNIK_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
+string(TOUPPER "${CMAKE_BUILD_TYPE}" MAPNIK_BUILD_TYPE_UPPER)
+if(USE_NDEBUG AND NOT MAPNIK_IS_MULTI_CONFIG AND NOT MAPNIK_BUILD_TYPE_UPPER STREQUAL "DEBUG")
+    list(APPEND MAPNIK_COMPILE_DEFS NDEBUG)
+endif()
+
 if(USE_LOG)
     list(APPEND MAPNIK_COMPILE_DEFS MAPNIK_LOG MAPNIK_DEFAULT_LOG_SEVERITY=${USE_LOG_SEVERITY})
 endif()


=====================================
include/mapnik/expression_evaluator.hpp
=====================================
@@ -31,8 +31,84 @@
 #include <mapnik/util/variant.hpp>
 #include <mapnik/feature.hpp>
 
+// stl
+#include <type_traits>
+
 namespace mapnik {
 
+namespace detail {
+
+template<typename Tag>
+struct ustring_compare
+{
+    static constexpr bool enabled = false;
+};
+
+template<>
+struct ustring_compare<mapnik::tags::equal_to>
+{
+    static constexpr bool enabled = true;
+
+    static bool apply(value_unicode_string const& a, value_unicode_string const& b) { return a == b; }
+
+    template<typename V>
+    static bool apply_value(V const& lhs, value_unicode_string const& rhs)
+    {
+        if (lhs.template is<value_unicode_string>())
+        {
+            return lhs.template get_unchecked<value_unicode_string>() == rhs;
+        }
+        return false;
+    }
+};
+
+template<>
+struct ustring_compare<mapnik::tags::not_equal_to>
+{
+    static constexpr bool enabled = true;
+
+    static bool apply(value_unicode_string const& a, value_unicode_string const& b) { return a != b; }
+
+    // Preserve value's asymmetric null/empty-string comparison.
+    template<typename V>
+    static bool apply_value(V const& lhs, value_unicode_string const& rhs)
+    {
+        if (lhs.template is<value_unicode_string>())
+        {
+            return lhs.template get_unchecked<value_unicode_string>() != rhs;
+        }
+        if (lhs.template is<value_null>())
+        {
+            return !rhs.isEmpty();
+        }
+        return true;
+    }
+};
+
+template<typename Tag>
+struct is_comparison_tag : std::false_type
+{};
+template<>
+struct is_comparison_tag<mapnik::tags::less> : std::true_type
+{};
+template<>
+struct is_comparison_tag<mapnik::tags::less_equal> : std::true_type
+{};
+template<>
+struct is_comparison_tag<mapnik::tags::greater> : std::true_type
+{};
+template<>
+struct is_comparison_tag<mapnik::tags::greater_equal> : std::true_type
+{};
+template<>
+struct is_comparison_tag<mapnik::tags::equal_to> : std::true_type
+{};
+template<>
+struct is_comparison_tag<mapnik::tags::not_equal_to> : std::true_type
+{};
+
+} // namespace detail
+
 template<typename T0, typename T1, typename T2>
 struct evaluate
 {
@@ -45,6 +121,31 @@ struct evaluate
           vars_(v)
     {}
 
+    using attribute_reference = decltype(std::declval<feature_type const&>().get(std::declval<std::string const&>()));
+    static constexpr bool borrowable_attributes =
+      std::is_lvalue_reference_v<attribute_reference> && std::is_same_v<std::decay_t<attribute_reference>, value_type>;
+
+    value_type const* borrow(expr_node const& node) const
+    {
+        if constexpr (borrowable_attributes)
+        {
+            if (node.template is<attribute>())
+            {
+                return &feature_.get(node.template get_unchecked<attribute>().name());
+            }
+        }
+        return nullptr;
+    }
+
+    value_bool eval_to_bool(expr_node const& node) const
+    {
+        if (value_type const* v = borrow(node))
+        {
+            return v->to_bool();
+        }
+        return util::apply_visitor(*this, node).to_bool();
+    }
+
     value_type operator()(value_integer val) const { return val; }
 
     value_type operator()(value_double val) const { return val; }
@@ -74,32 +175,83 @@ struct evaluate
 
     value_type operator()(binary_node<tags::logical_and> const& x) const
     {
-        return (util::apply_visitor(*this, x.left).to_bool()) && (util::apply_visitor(*this, x.right).to_bool());
+        return eval_to_bool(x.left) && eval_to_bool(x.right);
     }
 
     value_type operator()(binary_node<tags::logical_or> const& x) const
     {
-        return (util::apply_visitor(*this, x.left).to_bool()) || (util::apply_visitor(*this, x.right).to_bool());
+        return eval_to_bool(x.left) || eval_to_bool(x.right);
     }
 
     template<typename Tag>
-    value_type operator()(binary_node<Tag> const& x) const
+    bool compare(binary_node<Tag> const& x) const
     {
         typename make_op<Tag>::type operation;
+        value_type const* lhs = borrow(x.left);
+        value_type const* rhs = borrow(x.right);
+
+        if constexpr (detail::ustring_compare<Tag>::enabled)
+        {
+            if (lhs && !rhs && x.right.template is<value_unicode_string>())
+            {
+                return detail::ustring_compare<Tag>::apply_value(
+                  *lhs,
+                  x.right.template get_unchecked<value_unicode_string>());
+            }
+            if (rhs && !lhs && rhs->template is<value_unicode_string>() && x.left.template is<value_unicode_string>())
+            {
+                return detail::ustring_compare<Tag>::apply(x.left.template get_unchecked<value_unicode_string>(),
+                                                           rhs->template get_unchecked<value_unicode_string>());
+            }
+        }
+
+        if (lhs)
+        {
+            if (rhs)
+                return operation(*lhs, *rhs);
+            return operation(*lhs, util::apply_visitor(*this, x.right));
+        }
+        if (rhs)
+            return operation(util::apply_visitor(*this, x.left), *rhs);
         return operation(util::apply_visitor(*this, x.left), util::apply_visitor(*this, x.right));
     }
 
+    template<typename Tag>
+    value_type operator()(binary_node<Tag> const& x) const
+    {
+        if constexpr (detail::is_comparison_tag<Tag>::value)
+        {
+            return value_type(compare(x));
+        }
+        else
+        {
+            typename make_op<Tag>::type operation;
+            value_type const* lhs = borrow(x.left);
+            value_type const* rhs = borrow(x.right);
+            if (lhs)
+            {
+                if (rhs)
+                    return operation(*lhs, *rhs);
+                return operation(*lhs, util::apply_visitor(*this, x.right));
+            }
+            if (rhs)
+                return operation(util::apply_visitor(*this, x.left), *rhs);
+            return operation(util::apply_visitor(*this, x.left), util::apply_visitor(*this, x.right));
+        }
+    }
+
     template<typename Tag>
     value_type operator()(unary_node<Tag> const& x) const
     {
         typename make_op<Tag>::type func;
+        if (value_type const* v = borrow(x.expr))
+        {
+            return func(*v);
+        }
         return func(util::apply_visitor(*this, x.expr));
     }
 
-    value_type operator()(unary_node<tags::logical_not> const& x) const
-    {
-        return !(util::apply_visitor(*this, x.expr).to_bool());
-    }
+    value_type operator()(unary_node<tags::logical_not> const& x) const { return !eval_to_bool(x.expr); }
 
     value_type operator()(regex_match_node const& x) const
     {
@@ -130,6 +282,56 @@ struct evaluate
     variable_type const& vars_;
 };
 
+// Evaluates an expression for its truth value without materialising intermediate values.
+template<typename T0, typename T1, typename T2>
+struct evaluate_boolean
+{
+    using feature_type = T0;
+    using value_type = T1;
+    using variable_type = T2;
+    using result_type = value_bool;
+
+    explicit evaluate_boolean(feature_type const& f, variable_type const& v)
+        : eval_(f, v)
+    {}
+
+    result_type apply(expr_node const& node) const
+    {
+        if (value_type const* v = eval_.borrow(node))
+        {
+            return v->to_bool();
+        }
+        return util::apply_visitor(*this, node);
+    }
+
+    result_type operator()(binary_node<tags::logical_and> const& x) const { return apply(x.left) && apply(x.right); }
+
+    result_type operator()(binary_node<tags::logical_or> const& x) const { return apply(x.left) || apply(x.right); }
+
+    result_type operator()(unary_node<tags::logical_not> const& x) const { return !apply(x.expr); }
+
+    template<typename Tag>
+    result_type operator()(binary_node<Tag> const& x) const
+    {
+        if constexpr (detail::is_comparison_tag<Tag>::value)
+        {
+            return eval_.compare(x);
+        }
+        else
+        {
+            return eval_(x).to_bool();
+        }
+    }
+
+    template<typename T>
+    result_type operator()(T const& node) const
+    {
+        return eval_(node).to_bool();
+    }
+
+    evaluate<T0, T1, T2> eval_;
+};
+
 } // namespace mapnik
 
 #endif // MAPNIK_EXPRESSION_EVALUATOR_HPP


=====================================
include/mapnik/expression_node_types.hpp
=====================================
@@ -120,6 +120,8 @@ struct geometry_type_attribute;
 struct unary_function_call;
 struct binary_function_call;
 
+// Variant visitation tests alternatives in order. Keep common filter nodes first without
+// moving leaf types, whose order affects converting and default construction.
 using expr_node = util::variant<value_null,
                                 value_bool,
                                 value_integer,
@@ -128,21 +130,21 @@ using expr_node = util::variant<value_null,
                                 attribute,
                                 global_attribute,
                                 geometry_type_attribute,
+                                util::recursive_wrapper<binary_node<mapnik::tags::logical_and>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::equal_to>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::not_equal_to>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::greater>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::greater_equal>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::less>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::less_equal>>,
+                                util::recursive_wrapper<binary_node<mapnik::tags::logical_or>>,
+                                util::recursive_wrapper<unary_node<mapnik::tags::logical_not>>,
                                 util::recursive_wrapper<unary_node<mapnik::tags::negate>>,
                                 util::recursive_wrapper<binary_node<mapnik::tags::plus>>,
                                 util::recursive_wrapper<binary_node<mapnik::tags::minus>>,
                                 util::recursive_wrapper<binary_node<mapnik::tags::mult>>,
                                 util::recursive_wrapper<binary_node<mapnik::tags::div>>,
                                 util::recursive_wrapper<binary_node<mapnik::tags::mod>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::less>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::less_equal>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::greater>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::greater_equal>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::equal_to>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::not_equal_to>>,
-                                util::recursive_wrapper<unary_node<mapnik::tags::logical_not>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::logical_and>>,
-                                util::recursive_wrapper<binary_node<mapnik::tags::logical_or>>,
                                 util::recursive_wrapper<regex_match_node>,
                                 util::recursive_wrapper<regex_replace_node>,
                                 util::recursive_wrapper<unary_function_call>,


=====================================
include/mapnik/feature.hpp
=====================================
@@ -38,6 +38,7 @@
 #include <memory>
 #include <vector>
 #include <map>
+#include <unordered_map>
 #include <ostream>   // for basic_ostream, operator<<, etc
 #include <sstream>   // for basic_stringstream
 #include <stdexcept> // for out_of_range
@@ -73,17 +74,41 @@ class context : private util::noncopyable
     {
         size_type index = mapping_.size();
         mapping_.emplace(name, index);
+        lookup_.emplace(name, index);
         return index;
     }
 
-    inline void add(key_type const& name, size_type index) { mapping_.emplace(name, index); }
+    inline void add(key_type const& name, size_type index)
+    {
+        mapping_.emplace(name, index);
+        lookup_.emplace(name, index);
+    }
 
     inline size_type size() const { return mapping_.size(); }
     inline const_iterator begin() const { return mapping_.begin(); }
     inline const_iterator end() const { return mapping_.end(); }
 
+    static constexpr size_type npos = static_cast<size_type>(-1);
+
+    inline size_type find_index(key_type const& name) const
+    {
+        auto itr = lookup_.find(name);
+        return (itr != lookup_.end()) ? itr->second : npos;
+    }
+
   private:
+    inline key_type const* find_name(size_type index) const
+    {
+        for (auto const& entry : mapping_)
+        {
+            if (entry.second == index)
+                return &entry.first;
+        }
+        return nullptr;
+    }
+
     map_type mapping_;
+    std::unordered_map<key_type, size_type> lookup_;
 };
 
 using context_type = context<std::map<std::string, std::size_t>>;
@@ -117,6 +142,12 @@ class MAPNIK_DECL feature_impl : private util::noncopyable
         put(key, value(val));
     }
 
+    template<typename T>
+    inline void put(std::size_t index, T const& val)
+    {
+        put(index, value(val));
+    }
+
     template<typename T>
     inline void put_new(context_type::key_type const& key, T const& val)
     {
@@ -125,10 +156,10 @@ class MAPNIK_DECL feature_impl : private util::noncopyable
 
     inline void put(context_type::key_type const& key, value&& val)
     {
-        context_type::map_type::const_iterator itr = ctx_->mapping_.find(key);
-        if (itr != ctx_->mapping_.end() && itr->second < data_.size())
+        context_type::size_type const index = ctx_->find_index(key);
+        if (index != context_type::npos && index < data_.size())
         {
-            data_[itr->second] = std::move(val);
+            data_[index] = std::move(val);
         }
         else
         {
@@ -136,12 +167,28 @@ class MAPNIK_DECL feature_impl : private util::noncopyable
         }
     }
 
+    inline void put(std::size_t index, value&& val)
+    {
+        if (index < data_.size())
+        {
+            data_[index] = std::move(val);
+        }
+        else
+        {
+            if (context_type::key_type const* key = ctx_->find_name(index))
+            {
+                throw std::out_of_range(std::string("Key does not exist: '") + *key + "'");
+            }
+            throw std::out_of_range(std::string("Attribute index does not exist: '") + std::to_string(index) + "'");
+        }
+    }
+
     inline void put_new(context_type::key_type const& key, value&& val)
     {
-        context_type::map_type::const_iterator itr = ctx_->mapping_.find(key);
-        if (itr != ctx_->mapping_.end() && itr->second < data_.size())
+        context_type::size_type const index = ctx_->find_index(key);
+        if (index != context_type::npos && index < data_.size())
         {
-            data_[itr->second] = std::move(val);
+            data_[index] = std::move(val);
         }
         else
         {
@@ -151,13 +198,13 @@ class MAPNIK_DECL feature_impl : private util::noncopyable
         }
     }
 
-    inline bool has_key(context_type::key_type const& key) const { return (ctx_->mapping_.count(key) == 1); }
+    inline bool has_key(context_type::key_type const& key) const { return ctx_->find_index(key) != context_type::npos; }
 
     inline value_type const& get(context_type::key_type const& key) const
     {
-        context_type::map_type::const_iterator itr = ctx_->mapping_.find(key);
-        if (itr != ctx_->mapping_.end())
-            return get(itr->second);
+        context_type::size_type const index = ctx_->find_index(key);
+        if (index != context_type::npos)
+            return get(index);
         else
             return default_feature_value;
     }


=====================================
include/mapnik/feature_style_processor_impl.hpp
=====================================
@@ -47,6 +47,8 @@
 #include <mapnik/symbolizer_dispatch.hpp>
 
 // stl
+#include <algorithm>
+#include <span>
 #include <vector>
 #include <stdexcept>
 
@@ -605,16 +607,69 @@ void feature_style_processor<Processor>::render_style(Processor& p,
     mapnik::attributes vars = p.variables();
     feature_ptr feature;
     bool was_painted = false;
+
+    rule_cache::rule_ptrs const& if_rules = rc.get_if_rules();
+    std::span<std::size_t const> rules_without_precondition = rc.get_rules_without_precondition();
+    using resolved_group = std::pair<std::size_t, rule_cache::precondition_values const*>;
+    std::vector<resolved_group> precondition_groups;
+    rule_cache::rule_indices candidates;
+    context_type const* cached_context = nullptr;
+
     while ((feature = features->next()))
     {
+        context_type const* ctx = feature->context().get();
+        if (ctx != cached_context)
+        {
+            cached_context = ctx;
+            precondition_groups.clear();
+            precondition_groups.reserve(rc.get_precondition_groups().size());
+            for (rule_cache::precondition_group const& group : rc.get_precondition_groups())
+            {
+                precondition_groups.emplace_back(ctx->find_index(group.name), &group.rules);
+            }
+        }
+
+        std::span<std::size_t const> candidate_rules;
+        if (precondition_groups.empty())
+        {
+            candidate_rules = rules_without_precondition;
+        }
+        else if (precondition_groups.size() == 1 && rules_without_precondition.empty())
+        {
+            resolved_group const& group = precondition_groups.front();
+            value_type const& actual =
+              group.first == context_type::npos ? default_feature_value : feature->get(group.first);
+            auto const match = group.second->find(actual);
+            if (match != group.second->end())
+            {
+                candidate_rules = match->second;
+            }
+        }
+        else
+        {
+            candidates.assign(rules_without_precondition.begin(), rules_without_precondition.end());
+            for (resolved_group const& group : precondition_groups)
+            {
+                value_type const& actual =
+                  group.first == context_type::npos ? default_feature_value : feature->get(group.first);
+                auto const match = group.second->find(actual);
+                if (match != group.second->end())
+                {
+                    candidates.insert(candidates.end(), match->second.begin(), match->second.end());
+                }
+            }
+            // Restore stylesheet order after merging groups.
+            std::sort(candidates.begin(), candidates.end());
+            candidate_rules = candidates;
+        }
+
         bool do_else = true;
         bool do_also = false;
-        for (rule const* r : rc.get_if_rules())
+        for (std::size_t index : candidate_rules)
         {
+            rule const* r = if_rules[index];
             expression_ptr const& expr = r->get_filter();
-            value_type result =
-              util::apply_visitor(evaluate<feature_impl, value_type, attributes>(*feature, vars), *expr);
-            if (result.to_bool())
+            if (util::apply_visitor(evaluate_boolean<feature_impl, value_type, attributes>(*feature, vars), *expr))
             {
                 was_painted = true;
                 do_else = false;


=====================================
include/mapnik/rule_cache.hpp
=====================================
@@ -24,29 +24,79 @@
 #define MAPNIK_RULE_CACHE_HPP
 
 // mapnik
+#include <mapnik/expression_node.hpp>
 #include <mapnik/rule.hpp>
 #include <mapnik/util/noncopyable.hpp>
 
 // stl
+#include <string>
+#include <span>
+#include <unordered_map>
 #include <vector>
-#include <type_traits>
 
 namespace mapnik {
 
 class rule_cache : private util::noncopyable
 {
+  private:
+    struct precondition
+    {
+        std::string name;
+        value expected;
+    };
+
+    static bool extract_precondition(expr_node const& node, precondition& result)
+    {
+        if (node.is<binary_node<tags::equal_to>>())
+        {
+            auto const& equality = node.get_unchecked<binary_node<tags::equal_to>>();
+            if (equality.left.is<attribute>() && equality.right.is<value_unicode_string>())
+            {
+                result.name = equality.left.get_unchecked<attribute>().name();
+                result.expected = value(equality.right.get_unchecked<value_unicode_string>());
+                return true;
+            }
+            if (equality.right.is<attribute>() && equality.left.is<value_unicode_string>())
+            {
+                result.name = equality.right.get_unchecked<attribute>().name();
+                result.expected = value(equality.left.get_unchecked<value_unicode_string>());
+                return true;
+            }
+            return false;
+        }
+        if (node.is<binary_node<tags::logical_and>>())
+        {
+            auto const& conjunction = node.get_unchecked<binary_node<tags::logical_and>>();
+            return extract_precondition(conjunction.left, result) || extract_precondition(conjunction.right, result);
+        }
+        return false;
+    }
+
   public:
     using rule_ptrs = std::vector<rule const*>;
+    using rule_indices = std::vector<std::size_t>;
+    using precondition_values = std::unordered_map<value, rule_indices>;
+
+    struct precondition_group
+    {
+        std::string name;
+        precondition_values rules;
+    };
+
     rule_cache()
         : if_rules_(),
           else_rules_(),
-          also_rules_()
+          also_rules_(),
+          rules_without_precondition_(),
+          precondition_groups_()
     {}
 
     rule_cache(rule_cache&& rhs) // move ctor
         : if_rules_(std::move(rhs.if_rules_)),
           else_rules_(std::move(rhs.else_rules_)),
-          also_rules_(std::move(rhs.also_rules_))
+          also_rules_(std::move(rhs.also_rules_)),
+          rules_without_precondition_(std::move(rhs.rules_without_precondition_)),
+          precondition_groups_(std::move(rhs.precondition_groups_))
     {}
 
     rule_cache& operator=(rule_cache&& rhs) // move assign
@@ -54,6 +104,8 @@ class rule_cache : private util::noncopyable
         std::swap(if_rules_, rhs.if_rules_);
         std::swap(else_rules_, rhs.else_rules_);
         std::swap(also_rules_, rhs.also_rules_);
+        std::swap(rules_without_precondition_, rhs.rules_without_precondition_);
+        std::swap(precondition_groups_, rhs.precondition_groups_);
         return *this;
     }
 
@@ -69,7 +121,26 @@ class rule_cache : private util::noncopyable
         }
         else
         {
+            std::size_t const index = if_rules_.size();
             if_rules_.push_back(&r);
+            precondition condition;
+            expression_ptr const& filter = r.get_filter();
+            if (!filter || !extract_precondition(*filter, condition))
+            {
+                rules_without_precondition_.push_back(index);
+                return;
+            }
+
+            for (precondition_group& group : precondition_groups_)
+            {
+                if (group.name == condition.name)
+                {
+                    group.rules[condition.expected].push_back(index);
+                    return;
+                }
+            }
+            precondition_groups_.push_back({condition.name, {}});
+            precondition_groups_.back().rules[condition.expected].push_back(index);
         }
     }
 
@@ -79,10 +150,16 @@ class rule_cache : private util::noncopyable
 
     rule_ptrs const& get_also_rules() const { return also_rules_; }
 
+    std::span<std::size_t const> get_rules_without_precondition() const { return rules_without_precondition_; }
+
+    std::span<precondition_group const> get_precondition_groups() const { return precondition_groups_; }
+
   private:
     rule_ptrs if_rules_;
     rule_ptrs else_rules_;
     rule_ptrs also_rules_;
+    rule_indices rules_without_precondition_;
+    std::vector<precondition_group> precondition_groups_;
 };
 
 } // namespace mapnik


=====================================
include/mapnik/text/harfbuzz_shaper.hpp
=====================================
@@ -316,7 +316,6 @@ struct harfbuzz_shaper
                 hb_glyph_position_t* positions = hb_buffer_get_glyph_positions(buffer.get(), &num_glyphs);
 
                 unsigned cluster = 0;
-                bool in_cluster = false;
                 std::vector<unsigned> clusters;
                 std::vector<std::vector<glyph_face_info>> current_clusters;
                 current_clusters.resize(text.length());
@@ -332,11 +331,6 @@ struct harfbuzz_shaper
                     {
                         cluster = glyphs[i].cluster;
                         clusters.push_back(cluster);
-                        in_cluster = false;
-                    }
-                    else if (i != 0)
-                    {
-                        in_cluster = true;
                     }
                     if (glyphinfos.size() <= cluster)
                     {
@@ -359,13 +353,12 @@ struct harfbuzz_shaper
                             break;
                         }
                     }
-                    if (valid)
-                    {
-                        glyphinfos[cluster_id] = cluster_glyphs;
-                    }
-                    else if (glyphinfos[cluster_id].empty())
+                    if (glyphinfos[cluster_id].empty())
                     {
-                        glyphinfos[cluster_id] = cluster_glyphs;
+                        if (valid || pos == num_faces)
+                        {
+                            glyphinfos[cluster_id] = cluster_glyphs;
+                        }
                     }
                 }
                 bool all_set = true;


=====================================
include/mapnik/util/sort_by.hpp
=====================================
@@ -38,7 +38,7 @@ inline bool parse_sort_by(std::string const& str, sort_by_type& result)
     auto apply_sort_by = [&](auto const& ctx) {
         result.first = _attr(ctx);
     };
-    auto apply_desc = [&](auto const& ctx) {
+    auto apply_desc = [&](auto const& /*ctx*/) {
         result.second = true;
     };
     if (!x3::phrase_parse(itr,


=====================================
include/mapnik/version.hpp
=====================================
@@ -27,7 +27,7 @@
 
 #define MAPNIK_MAJOR_VERSION 4
 #define MAPNIK_MINOR_VERSION 3
-#define MAPNIK_PATCH_VERSION 0
+#define MAPNIK_PATCH_VERSION 1
 
 #define MAPNIK_VERSION MAPNIK_VERSION_ENCODE(MAPNIK_MAJOR_VERSION, MAPNIK_MINOR_VERSION, MAPNIK_PATCH_VERSION)
 


=====================================
plugins/input/postgis/postgis_featureset.cpp
=====================================
@@ -67,16 +67,16 @@ feature_ptr postgis_featureset::next()
     {
         // new feature
         unsigned pos = 1;
+        std::size_t attr_index = 0;
         feature_ptr feature;
 
         if (key_field_)
         {
-            std::string name = rs_->getFieldName(pos);
-
             // null feature id is not acceptable
             if (rs_->isNull(pos))
             {
-                MAPNIK_LOG_WARN(postgis) << "postgis_featureset: null value encountered for key_field: " << name;
+                MAPNIK_LOG_WARN(postgis) << "postgis_featureset: null value encountered for key_field: "
+                                         << rs_->getFieldName(pos);
                 continue;
             }
             // create feature with user driven id from attribute
@@ -102,7 +102,7 @@ feature_ptr postgis_featureset::next()
             feature = feature_factory::create(ctx_, val);
             if (key_field_as_attribute_)
             {
-                feature->put<mapnik::value_integer>(name, val);
+                feature->put<mapnik::value_integer>(attr_index++, val);
             }
             ++pos;
         }
@@ -135,14 +135,14 @@ feature_ptr postgis_featureset::next()
 
         totalGeomSize_ += size;
         unsigned num_attrs = ctx_->size() + 1;
-        if (!key_field_as_attribute_)
+        if (key_field_ && !key_field_as_attribute_)
         {
             num_attrs++;
         }
+
+        // SELECT columns and context entries are constructed in the same order.
         for (; pos < num_attrs; ++pos)
         {
-            std::string name = rs_->getFieldName(pos);
-
             // NOTE: we intentionally do not store null here
             // since it is equivalent to the attribute not existing
             if (!rs_->isNull(pos))
@@ -153,25 +153,25 @@ feature_ptr postgis_featureset::next()
                 {
                     case 16: // bool
                     {
-                        feature->put(name, (buf[0] != 0));
+                        feature->put(attr_index, (buf[0] != 0));
                         break;
                     }
 
                     case 23: // int4
                     {
-                        feature->put<mapnik::value_integer>(name, int4net(buf));
+                        feature->put<mapnik::value_integer>(attr_index, int4net(buf));
                         break;
                     }
 
                     case 21: // int2
                     {
-                        feature->put<mapnik::value_integer>(name, int2net(buf));
+                        feature->put<mapnik::value_integer>(attr_index, int2net(buf));
                         break;
                     }
 
                     case 20: // int8/BigInt
                     {
-                        feature->put<mapnik::value_integer>(name, int8net(buf));
+                        feature->put<mapnik::value_integer>(attr_index, int8net(buf));
                         break;
                     }
 
@@ -179,7 +179,7 @@ feature_ptr postgis_featureset::next()
                     {
                         float val;
                         float4net(val, buf);
-                        feature->put(name, static_cast<double>(val));
+                        feature->put(attr_index, static_cast<double>(val));
                         break;
                     }
 
@@ -187,7 +187,7 @@ feature_ptr postgis_featureset::next()
                     {
                         double val;
                         float8net(val, buf);
-                        feature->put(name, val);
+                        feature->put(attr_index, val);
                         break;
                     }
 
@@ -195,14 +195,14 @@ feature_ptr postgis_featureset::next()
                     case 1043: // varchar
                     case 705:  // literal
                     {
-                        feature->put(name, tr_->transcode(buf));
+                        feature->put(attr_index, tr_->transcode(buf));
                         break;
                     }
 
                     case 1042: // bpchar
                     {
                         std::string str = mapnik::util::trim_copy(buf);
-                        feature->put(name, tr_->transcode(str.c_str()));
+                        feature->put(attr_index, tr_->transcode(str.c_str()));
                         break;
                     }
 
@@ -212,7 +212,7 @@ feature_ptr postgis_featureset::next()
                         std::string str = numeric2string(buf);
                         if (mapnik::util::string2double(str, val))
                         {
-                            feature->put(name, val);
+                            feature->put(attr_index, val);
                         }
                         break;
                     }
@@ -224,6 +224,7 @@ feature_ptr postgis_featureset::next()
                     }
                 }
             }
+            ++attr_index;
         }
         return feature;
     }


=====================================
test/unit/core/expressions_test.cpp
=====================================
@@ -48,6 +48,14 @@ mapnik::value evaluate_string(mapnik::feature_ptr const& feature, std::string co
     return evaluate(*feature, *expr);
 }
 
+bool evaluate_boolean_string(mapnik::feature_ptr const& feature, std::string const& str)
+{
+    auto expr = mapnik::parse_expression(str);
+    return mapnik::util::apply_visitor(
+      mapnik::evaluate_boolean<mapnik::feature_impl, mapnik::value_type, mapnik::attributes>(*feature, {}),
+      *expr);
+}
+
 std::string parse_and_dump(std::string const& str)
 {
     auto expr = mapnik::parse_expression(str);
@@ -56,6 +64,24 @@ std::string parse_and_dump(std::string const& str)
 
 } // namespace
 
+TEST_CASE("feature attributes can be assigned by index")
+{
+    auto ctx = std::make_shared<mapnik::context_type>();
+    std::size_t const first = ctx->push("first");
+    std::size_t const second = ctx->push("second");
+    mapnik::feature_ptr feature(mapnik::feature_factory::create(ctx, 1));
+
+    feature->put(first, mapnik::value_integer(12));
+    feature->put(second, mapnik::value_unicode_string("value"));
+
+    CHECK(feature->get("first").to_int() == 12);
+    CHECK(feature->get("second").to_string() == "value");
+    CHECK_THROWS_WITH(feature->put(ctx->size(), mapnik::value_integer(0)), "Attribute index does not exist: '2'");
+
+    std::size_t const third = ctx->push("third");
+    CHECK_THROWS_WITH(feature->put(third, mapnik::value_integer(0)), "Key does not exist: 'third'");
+}
+
 TEST_CASE("expressions")
 {
     using namespace std::placeholders;
@@ -76,6 +102,7 @@ TEST_CASE("expressions")
 
     auto feature = make_test_feature(1, "POINT(100 200)", prop);
     auto eval = std::bind(evaluate_string, feature, _1);
+    auto eval_bool = std::bind(evaluate_boolean_string, feature, _1);
     auto approx = Approx::custom().epsilon(1e-6);
 
     // primary expressions
@@ -174,6 +201,8 @@ TEST_CASE("expressions")
     // logical
     TRY_CHECK(eval(" [int] = 123 and [double] = 1.23456 && [bool] = true and [null] = null && [foo] = 'bar' ") == true);
     TRY_CHECK(eval(" [int] = 456 or [foo].match('foo') || length([foo]) = 3 ") == true);
+    TRY_CHECK(eval_bool("[foo] = 'bar' and [int] > 100"));
+    TRY_CHECK(eval_bool("not ([foo] = 'missing' or length([foo]) != 3)"));
     TRY_CHECK(eval(" not true  and not true  ") == false); // (not true) and (not true)
     TRY_CHECK(eval(" not false and not true  ") == false); // (not false) and (not true)
     TRY_CHECK(eval(" not true  or  not false ") == true);  // (not true) or (not false)


=====================================
test/unit/renderer/feature_style_processor.cpp
=====================================
@@ -23,6 +23,8 @@ struct rendering_result
 
     unsigned start_style_processing = 0;
     unsigned end_style_processing = 0;
+    std::size_t style_geometry_start = 0;
+    std::vector<std::size_t> style_geometry_counts;
 
     std::vector<mapnik::box2d<double>> layer_query_extents;
     std::vector<mapnik::geometry::geometry<double>> geometries;
@@ -51,9 +53,17 @@ class test_renderer : public mapnik::feature_style_processor<test_renderer>
 
     void end_layer_processing(mapnik::layer const& lay) { result_.end_layer_processing++; }
 
-    void start_style_processing(mapnik::feature_type_style const& st) { result_.start_style_processing++; }
+    void start_style_processing(mapnik::feature_type_style const& st)
+    {
+        result_.start_style_processing++;
+        result_.style_geometry_start = result_.geometries.size();
+    }
 
-    void end_style_processing(mapnik::feature_type_style const& st) { result_.end_style_processing++; }
+    void end_style_processing(mapnik::feature_type_style const& st)
+    {
+        result_.end_style_processing++;
+        result_.style_geometry_counts.push_back(result_.geometries.size() - result_.style_geometry_start);
+    }
 
     template<typename Symbolizer>
     void process(Symbolizer const& sym, mapnik::feature_impl& feature, mapnik::proj_transform const& prj_trans)
@@ -341,4 +351,76 @@ TEST_CASE("feature_style_processor")
         CHECK(datasource->last_bbox() == clipped_extent);
         CHECK(datasource->last_unbuffered_bbox() == clipped_extent);
     }
+
+    SECTION("rule indexing preserves filter behavior")
+    {
+        mapnik::parameters params;
+        params["type"] = "memory";
+        auto datasource = std::make_shared<mapnik::memory_datasource>(params);
+        auto context = std::make_shared<mapnik::context_type>();
+        auto feature = mapnik::feature_factory::create(context, 1);
+        feature->put_new("kind", mapnik::value_unicode_string("a"));
+        feature->put_new("other", mapnik::value_unicode_string("b"));
+        feature->set_geometry(mapnik::geometry::point<double>(1, 1));
+        datasource->push(feature);
+
+        auto make_rule = [](char const* filter, std::size_t symbol_count) {
+            mapnik::rule result;
+            if (filter)
+            {
+                result.set_filter(mapnik::parse_expression(filter));
+            }
+            for (std::size_t i = 0; i < symbol_count; ++i)
+            {
+                result.append(mapnik::line_symbolizer());
+            }
+            return result;
+        };
+
+        auto make_matching_style = [&](mapnik::filter_mode_e mode) {
+            mapnik::feature_type_style style;
+            style.set_filter_mode(mode);
+            style.add_rule(make_rule("[kind] = 'a'", 1));
+            style.add_rule(make_rule(nullptr, 2));
+            style.add_rule(make_rule("[other] = 'b'", 3));
+            return style;
+        };
+
+        mapnik::Map map(256, 256);
+        map.insert_style("all", make_matching_style(mapnik::filter_mode_enum::FILTER_ALL));
+        map.insert_style("first", make_matching_style(mapnik::filter_mode_enum::FILTER_FIRST));
+
+        mapnik::feature_type_style else_style;
+        else_style.add_rule(make_rule("[kind] = 'missing'", 1));
+        mapnik::rule else_rule = make_rule(nullptr, 4);
+        else_rule.set_else(true);
+        else_style.add_rule(std::move(else_rule));
+        map.insert_style("else", std::move(else_style));
+
+        mapnik::feature_type_style also_style;
+        also_style.add_rule(make_rule("[kind] = 'a'", 1));
+        mapnik::rule also_rule = make_rule(nullptr, 5);
+        also_rule.set_also(true);
+        also_style.add_rule(std::move(also_rule));
+        map.insert_style("also", std::move(also_style));
+
+        mapnik::layer layer("layer");
+        layer.set_datasource(datasource);
+        layer.add_style("all");
+        layer.add_style("first");
+        layer.add_style("else");
+        layer.add_style("also");
+        map.add_layer(layer);
+        map.zoom_to_box(mapnik::box2d<double>(0, 0, 2, 2));
+
+        rendering_result result;
+        test_renderer renderer(map, result);
+        renderer.apply();
+
+        REQUIRE(result.style_geometry_counts.size() == 4);
+        CHECK(result.style_geometry_counts[0] == 6);
+        CHECK(result.style_geometry_counts[1] == 1);
+        CHECK(result.style_geometry_counts[2] == 4);
+        CHECK(result.style_geometry_counts[3] == 6);
+    }
 }


=====================================
test/unit/text/shaping.cpp
=====================================
@@ -31,26 +31,24 @@ void test_shaping(mapnik::font_set const& fontset,
     mapnik::harfbuzz_shaper::shape_text(line, itemizer, width_map, fm, scale_factor, "");
 
     std::size_t index = 0;
+    if (debug)
+        std::cerr << std::endl << str << std::endl;
     for (auto const& g : line)
     {
+        unsigned glyph_index, char_index;
+        std::tie(glyph_index, char_index) = expected[index++];
         if (debug)
         {
-            if (index++ > 0)
-                std::cerr << ",";
-            std::cerr << "{" << g.glyph_index << ", "
-                      << g.char_index
-                      //<< ", " << g.face->family_name() << ":" << g.face->style_name()
-                      << "}";
+            std::cerr << "{" << glyph_index << "==" << g.glyph_index << ", " << char_index << "==" << g.char_index
+                      << ", " << g.face->family_name() << ":" << g.face->style_name() << "}" << std::endl;
         }
         else
         {
-            unsigned glyph_index, char_index;
-            CHECK(index < expected.size());
-            std::tie(glyph_index, char_index) = expected[index++];
             REQUIRE(glyph_index == g.glyph_index);
             REQUIRE(char_index == g.char_index);
         }
     }
+    CHECK(index == expected.size());
 }
 } // namespace
 



View it on GitLab: https://salsa.debian.org/debian-gis-team/mapnik/-/commit/407b4b9e45b9c8df9456d8874f044f5f92517bfe

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/mapnik/-/commit/407b4b9e45b9c8df9456d8874f044f5f92517bfe
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20260828/480d6be6/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list