diff --git a/doc/stages/filters.expression.rst b/doc/stages/filters.expression.rst new file mode 100644 index 0000000000..087dc625b2 --- /dev/null +++ b/doc/stages/filters.expression.rst @@ -0,0 +1,138 @@ +.. _filters.expression: + +filters.expression +================== + +.. contents:: + +The expression filter applies query logic to the input point cloud based on a +MongoDB-style query expression using the point cloud attributes. + +.. embed:: + +.. streamable:: + +Pipeline Example +---------------- + +This example passes through only the points whose Classification is non-zero. + +.. code-block:: json + + [ + "input.las", + { + "type": "filters.expression", + "expression": { + "Classification": { "$ne": 0 } + } + }, + "filtered.las" + ] + +Options +------- + +expression + A JSON query :ref:`expression` containing a combination of query comparisons + and logical operators. + + +.. _expression: + +Expression +-------------------------------------------------------------------------------- + +A query expression is a combination of comparison and logical operators that +define a query which can be used to select matching points by their attribute +values. + +Comparison operators +................................................................................ + +There are 8 valid query comparison operators: + - ``$eq``: Matches values equal to a specified value. + - ``$gt``: Matches values greater than a specified value. + - ``$gte``: Matches values greater than or equal to a specified value. + - ``$lt``: Matches values less than a specified value. + - ``$lte``: Matches values less than or equal to a specified value. + - ``$ne``: Matches values not equal to a specified value. + - ``$in``: Matches any of the values specified in the array. + - ``$nin``: Matches none of the values specified in the array. + +Comparison operators compare a point cloud attribute with a value or an array +of values. For all comparison operators except for ``$in`` and ``$nin``, the +value must be a number. For ``$in`` and ``$nin``, the value must be an array +of numbers. + +Comparison operators are applied directly to attribute values, and thus must be +contained within an attribute selection by which an attribute is selected by its +name. For example: + +.. code-block:: json + + { "Classification": { "$eq": 2 } } + +.. code-block:: json + + { "Intensity": { "$gt": 0 } } + +.. code-block:: json + + { "Classification": { "$in": [2, 6, 9] } } + +The ``$eq`` comparison operator may be implicitly invoked by setting an +attribute name directly to a value. + +.. code-block:: json + + { "Classification": 2 } + +Logical operators +................................................................................ + +There are 4 valid logical operators: + - ``$and``: Applies a logical **and** on the expressions of the array and + returns a match only if all expressions match. + - ``$not``: Inverts the value of the single sub-expression. + - ``$nor``: Applies a logical **nor** on the expressions of the array and + returns a match only if all expressions fail to match. + - ``$nor``: Applies a logical **or** on the expressions of the array and + returns a match if any of the expressions match. + +Logical operators are used to logically combine sub-expressions. All logical +operators except for ``$not`` are applied to arrays of expressions. +``$not`` is applied to a single expression and negates its result. + +Logical operators may be applied directly to comparison expressions or may +contain further nested logical operators. For example: + +.. code-block:: json + + { "$or": [ + { "Classification": 2 }, + { "Intensity": { "$gt": 0 } } + ] } + +.. code-block:: json + + { "$or": [ + { "Classification": 2 }, + { "$and": [ + { "ReturnNumber": { "$gt": 0 } }, + { "Z": { "$lte": 42 } } + ] } + ] } + +.. code-block:: json + + { "$not": { + "$or": [ + { "Classification": 2 }, + { "$and": [ + { "ReturnNumber": { "$gt": 0 } }, + { "Z": { "$lte": 42 } } + ] } + ] } + } + diff --git a/filters/ExpressionFilter.cpp b/filters/ExpressionFilter.cpp new file mode 100644 index 0000000000..0e054d7914 --- /dev/null +++ b/filters/ExpressionFilter.cpp @@ -0,0 +1,102 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#include "ExpressionFilter.hpp" + +#include "private/expression/Expression.hpp" + +namespace pdal +{ + +static const StaticPluginInfo s_info +{ + "filters.expression", + "Pass only points that pass a logic filter.", + "http://pdal.io/stages/filters.logic.html" +}; + +CREATE_STATIC_STAGE(ExpressionFilter, s_info); + +std::string ExpressionFilter::getName() const +{ + return s_info.name; +} + +ExpressionFilter::ExpressionFilter() +{} + +ExpressionFilter::~ExpressionFilter() +{} + +void ExpressionFilter::addArgs(ProgramArgs& args) +{ + args.add("expression", "Logical query expression", m_json).setPositional(); +} + +void ExpressionFilter::prepared(PointTableRef table) +{ + log()->get(LogLevel::Debug) << "Building expression from: " << m_json << + std::endl; + + m_expression = makeUnique(*table.layout(), m_json); + + log()->get(LogLevel::Debug) << "Built expression: " << *m_expression << + std::endl; +} + +PointViewSet ExpressionFilter::run(PointViewPtr inView) +{ + PointViewSet views; + PointViewPtr view(inView->makeNew()); + + for (PointId i(0); i < inView->size(); ++i) + { + PointRef pr(inView->point(i)); + if (processOne(pr)) + { + view->appendPoint(*inView, i); + } + } + + views.insert(view); + return views; +} + +bool ExpressionFilter::processOne(PointRef& pr) +{ + return m_expression->check(pr); +} + +} // namespace pdal + diff --git a/filters/ExpressionFilter.hpp b/filters/ExpressionFilter.hpp new file mode 100644 index 0000000000..e090eb0cde --- /dev/null +++ b/filters/ExpressionFilter.hpp @@ -0,0 +1,66 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#pragma once + +#include + +#include +#include + +namespace pdal +{ + +class Expression; + +class PDAL_DLL ExpressionFilter : public Filter, public Streamable +{ +public: + ExpressionFilter(); + ~ExpressionFilter(); + + std::string getName() const override; + virtual bool processOne(PointRef& point) override; + +private: + virtual void addArgs(ProgramArgs& args) override; + virtual void prepared(PointTableRef table) override; + virtual PointViewSet run(PointViewPtr view) override; + + Json::Value m_json; + std::unique_ptr m_expression; +}; + +} // namespace pdal + diff --git a/filters/private/expression/Comparison.cpp b/filters/private/expression/Comparison.cpp new file mode 100644 index 0000000000..0e8a8cb02d --- /dev/null +++ b/filters/private/expression/Comparison.cpp @@ -0,0 +1,136 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#include "Comparison.hpp" + +namespace pdal +{ + +namespace +{ + +template +std::unique_ptr> createSingle( + ComparisonType type, + O op, + double d) +{ + return makeUnique>(type, op, d); +} + +} // unnamed namespace + +std::unique_ptr ComparisonOperator::create( + const Json::Value& json) +{ + if (!json.isObject()) + { + // If it's a value specified without the $eq operator, convert it. + Json::Value converted; + converted["$eq"] = json; + return create(converted); + } + + if (json.size() != 1) + { + throw pdal_error("Invalid comparison object: " + json.toStyledString()); + } + + const auto key(json.getMemberNames().at(0)); + const ComparisonType co(toComparisonType(key)); + const auto& val(json[key]); + + if (isSingle(co)) + { + if (!val.isConvertibleTo(Json::ValueType::realValue)) + { + throw pdal_error("Invalid comparison operand: " + + val.toStyledString()); + } + + const double d(val.asDouble()); + + switch (co) + { + case ComparisonType::eq: + return createSingle(co, std::equal_to(), d); + case ComparisonType::gt: + return createSingle(co, std::greater(), d); + case ComparisonType::gte: + return createSingle(co, std::greater_equal(), d); + case ComparisonType::lt: + return createSingle(co, std::less(), d); + case ComparisonType::lte: + return createSingle(co, std::less_equal(), d); + case ComparisonType::ne: + return createSingle(co, std::not_equal_to(), d); + default: + throw pdal_error("Invalid single comparison operator"); + } + } + else + { + if (!val.isArray()) + { + throw pdal_error("Invalid comparison list: " + + val.toStyledString()); + } + + std::vector vals; + + for (const Json::Value& single : val) + { + if (!single.isConvertibleTo(Json::ValueType::realValue)) + { + throw pdal_error("Invalid multi comparison operand: " + + val.toStyledString()); + } + + vals.push_back(single.asDouble()); + } + + switch (co) + { + case ComparisonType::in: + return makeUnique(vals); + case ComparisonType::nin: + return makeUnique(vals); + default: + throw pdal_error("Invalid multi comparison operator"); + } + } +} + +} // namespace pdal + diff --git a/filters/private/expression/Comparison.hpp b/filters/private/expression/Comparison.hpp new file mode 100644 index 0000000000..bcbc7ae802 --- /dev/null +++ b/filters/private/expression/Comparison.hpp @@ -0,0 +1,236 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#pragma once + +#include "Support.hpp" + +namespace pdal +{ + +enum class ComparisonType +{ + eq, + gt, + gte, + lt, + lte, + ne, + in, + nin +}; + +inline bool isComparisonType(const std::string& s) +{ + return s == "$eq" || s == "$gt" || s == "$gte" || s == "$lt" || + s == "$lte" || s == "$ne" || s == "$in" || s == "$nin"; +} + +inline ComparisonType toComparisonType(const std::string& s) +{ + if (s == "$eq") return ComparisonType::eq; + else if (s == "$gt") return ComparisonType::gt; + else if (s == "$gte") return ComparisonType::gte; + else if (s == "$lt") return ComparisonType::lt; + else if (s == "$lte") return ComparisonType::lte; + else if (s == "$ne") return ComparisonType::ne; + else if (s == "$in") return ComparisonType::in; + else if (s == "$nin") return ComparisonType::nin; + else throw pdal_error("Invalid comparison type: " + s); +} + +inline std::string typeToString(ComparisonType c) +{ + switch (c) + { + case ComparisonType::eq: return "$eq"; + case ComparisonType::gt: return "$gt"; + case ComparisonType::gte: return "$gte"; + case ComparisonType::lt: return "$lt"; + case ComparisonType::lte: return "$lte"; + case ComparisonType::ne: return "$ne"; + case ComparisonType::in: return "$in"; + case ComparisonType::nin: return "$nin"; + default: throw pdal_error("Invalid comparison type enum"); + } +} + +inline bool isSingle(ComparisonType co) +{ + return co != ComparisonType::in && co != ComparisonType::nin; +} + +inline bool isMultiple(ComparisonType co) +{ + return !isSingle(co); +} + +class ComparisonOperator : public Comparable +{ +public: + ComparisonOperator(ComparisonType type) : m_type(type) { } + + virtual ~ComparisonOperator() { } + + // Accepts a JSON value of the form: + // { "$": } // E.g. { "$eq": 42 } + // + // or the special case for the "$eq" operator: + // // Equivalent to the above. + // + // + // Returns a pointer to a functor that performs the requested comparison. + static std::unique_ptr create(const Json::Value& json); + + ComparisonType type() const { return m_type; } + +protected: + ComparisonType m_type; +}; + +template +class ComparisonSingle : public ComparisonOperator +{ +public: + ComparisonSingle(ComparisonType type, Op op, double val) + : ComparisonOperator(type) + , m_op(op) + , m_val(val) + { } + + virtual bool operator()(double in) const override + { + return m_op(in, m_val); + } + + virtual std::string toString(std::string pre) const override + { + std::ostringstream ss; + ss << pre << typeToString(m_type) << " " << m_val << std::endl; + return ss.str(); + } + +protected: + Op m_op; + double m_val; +}; + +class ComparisonMulti : public ComparisonOperator +{ +public: + ComparisonMulti(ComparisonType type, const std::vector& vals) + : ComparisonOperator(type) + , m_vals(vals) + { } + + virtual std::string toString(std::string pre) const override + { + std::ostringstream ss; + ss << pre << typeToString(m_type) << " "; + for (const double d : m_vals) ss << d << " "; + ss << std::endl; + return ss.str(); + } + +protected: + std::vector m_vals; +}; + +class ComparisonAny : public ComparisonMulti +{ +public: + ComparisonAny(const std::vector& vals) + : ComparisonMulti(ComparisonType::in, vals) + { } + + virtual bool operator()(double in) const override + { + return std::any_of(m_vals.begin(), m_vals.end(), [in](double val) + { + return in == val; + }); + } +}; + +class ComparisonNone : public ComparisonMulti +{ +public: + ComparisonNone(const std::vector& vals) + : ComparisonMulti(ComparisonType::nin, vals) + { } + + virtual bool operator()(double in) const override + { + return std::none_of(m_vals.begin(), m_vals.end(), [in](double val) + { + return in == val; + }); + } +}; + +class Comparison : public Filterable +{ +public: + Comparison(const PointLayout& layout, std::string dimName, Json::Value json) + : m_dimName(dimName) + , m_dimId(layout.findDim(dimName)) + , m_op(ComparisonOperator::create(json)) + { + if (m_dimId == pdal::Dimension::Id::Unknown) + { + throw pdal_error("Unknown dimension: " + dimName); + } + } + + bool operator()(const pdal::PointRef& pointRef) const override + { + return (*m_op)(pointRef.getFieldAs(m_dimId)); + } + + virtual std::string toString(std::string pre) const override + { + std::ostringstream ss; + ss << pre << m_dimName << " "; + ss << m_op->toString(""); + return ss.str(); + } + +protected: + std::string m_dimName; + pdal::Dimension::Id m_dimId; + std::unique_ptr m_op; +}; + +} // namespace pdal + diff --git a/filters/private/expression/Expression.cpp b/filters/private/expression/Expression.cpp new file mode 100644 index 0000000000..73649e7548 --- /dev/null +++ b/filters/private/expression/Expression.cpp @@ -0,0 +1,107 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#include "Expression.hpp" + +namespace pdal +{ + +void Expression::build(LogicGate& gate, const Json::Value& json) +{ + if (json.isArray()) + { + for (const Json::Value& val : json) build(gate, val); + return; + } + + if (!json.isObject()) + { + throw pdal_error("Unexpected expression: " + json.toStyledString()); + } + + LogicGate* active(&gate); + + std::unique_ptr outer; + + if (json.size() > 1) + { + outer = LogicGate::create(LogicalOperator::lAnd); + active = outer.get(); + } + + for (const std::string key : json.getMemberNames()) + { + const Json::Value& val(json[key]); + + if (isLogicalOperator(key)) + { + auto inner(LogicGate::create(key)); + if (inner->type() != LogicalOperator::lNot && !val.isArray()) + { + throw pdal_error("Logical operator expressions must be arrays"); + } + + build(*inner, val); + active->push(std::move(inner)); + } + else if (!val.isObject() || val.size() == 1) + { + // A comparison object. + active->push(makeUnique(m_layout, key, val)); + } + else + { + // key is the name of a dimension, val is an object of + // multiple comparison key/val pairs, for example: + // + // key: "Red" + // val: { "$gt": 100, "$lt": 200 } + // + // There cannot be any further nested logical operators + // within val, since we've already selected a dimension. + for (const std::string& innerKey : val.getMemberNames()) + { + Json::Value next; + next[innerKey] = val[innerKey]; + active->push( + makeUnique(m_layout, key, next)); + } + } + } + + if (outer) gate.push(std::move(outer)); +} + +} // namespace pdal + diff --git a/filters/private/expression/Expression.hpp b/filters/private/expression/Expression.hpp new file mode 100644 index 0000000000..0884fa54b7 --- /dev/null +++ b/filters/private/expression/Expression.hpp @@ -0,0 +1,76 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#pragma once + +#include "Comparison.hpp" +#include "LogicGate.hpp" + +namespace pdal +{ + +class Expression +{ +public: + Expression(const PointLayout& layout, const Json::Value& json) + : m_layout(layout) + { + build(m_root, json); + } + + bool check(const pdal::PointRef& pr) const + { + return m_root(pr); + } + + std::string toString() const + { + return m_root.toString(""); + } + +private: + void build(LogicGate& gate, const Json::Value& json); + + const PointLayout& m_layout; + LogicalAnd m_root; +}; + +inline std::ostream& operator<<(std::ostream& os, const Expression& expression) +{ + os << expression.toString() << std::endl; + return os; +} + +} // namespace pdal + diff --git a/filters/private/expression/LogicGate.cpp b/filters/private/expression/LogicGate.cpp new file mode 100644 index 0000000000..83acf119b6 --- /dev/null +++ b/filters/private/expression/LogicGate.cpp @@ -0,0 +1,50 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#include "LogicGate.hpp" + +namespace pdal +{ + +std::unique_ptr LogicGate::create(const LogicalOperator type) +{ + if (type == LogicalOperator::lAnd) return makeUnique(); + if (type == LogicalOperator::lNot) return makeUnique(); + if (type == LogicalOperator::lOr) return makeUnique(); + if (type == LogicalOperator::lNor) return makeUnique(); + throw pdal_error("Invalid logic gate type"); +} + +} // namespace pdal + diff --git a/filters/private/expression/LogicGate.hpp b/filters/private/expression/LogicGate.hpp new file mode 100644 index 0000000000..62868b60c7 --- /dev/null +++ b/filters/private/expression/LogicGate.hpp @@ -0,0 +1,188 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#pragma once + +#include "Support.hpp" + +namespace pdal +{ + +enum class LogicalOperator +{ + lAnd, + lNot, + lOr, + lNor +}; + +inline bool isLogicalOperator(std::string s) +{ + return s == "$and" || s == "$not" || s == "$or" || s == "$nor"; +} + +inline LogicalOperator toLogicalOperator(std::string s) +{ + if (s == "$and") return LogicalOperator::lAnd; + if (s == "$not") return LogicalOperator::lNot; + if (s == "$or") return LogicalOperator::lOr; + if (s == "$nor") return LogicalOperator::lNor; + throw pdal_error("Invalid logical operator: " + s); +} + +inline std::string opToString(LogicalOperator o) +{ + switch (o) + { + case LogicalOperator::lAnd: return "$and"; + case LogicalOperator::lNot: return "$not"; + case LogicalOperator::lOr: return "$or"; + case LogicalOperator::lNor: return "$nor"; + default: throw pdal_error("Invalid logical operator"); + } +} + +class LogicGate : public Filterable +{ +public: + virtual ~LogicGate() { } + + static std::unique_ptr create(std::string s) + { + return create(toLogicalOperator(s)); + } + + static std::unique_ptr create(LogicalOperator type); + + virtual void push(std::unique_ptr f) + { + m_filters.push_back(std::move(f)); + } + + virtual std::string toString(std::string pre) const override + { + std::ostringstream ss; + if (m_filters.size()) ss << pre << opToString(type()) << std::endl; + for (const auto& c : m_filters) ss << c->toString(pre + " "); + return ss.str(); + } + + virtual LogicalOperator type() const = 0; + +protected: + std::vector> m_filters; +}; + +class LogicalAnd : public LogicGate +{ +public: + virtual bool operator()(const pdal::PointRef& pr) const override + { + for (const auto& f : m_filters) + { + if (!(*f)(pr)) return false; + } + + return true; + } + +protected: + virtual LogicalOperator type() const override + { + return LogicalOperator::lAnd; + } +}; + +class LogicalNot : public LogicGate +{ +public: + using LogicGate::push; + virtual void push(std::unique_ptr f) override + { + if (!m_filters.empty()) + throw pdal_error("Cannot push onto a logical NOT"); + + LogicGate::push(std::move(f)); + } + + virtual bool operator()(const pdal::PointRef& pr) const override + { + return !(*m_filters.at(0))(pr); + } + +private: + virtual LogicalOperator type() const override + { + return LogicalOperator::lNot; + } +}; + +class LogicalOr : public LogicGate +{ +public: + virtual bool operator()(const pdal::PointRef& pr) const override + { + for (const auto& f : m_filters) + { + if ((*f)(pr)) return true; + } + + return false; + } + +protected: + virtual LogicalOperator type() const override + { + return LogicalOperator::lOr; + } +}; + +class LogicalNor : public LogicalOr +{ +public: + using LogicalOr::operator(); + virtual bool operator()(const pdal::PointRef& pr) const override + { + return !LogicalOr::operator()(pr); + } + +protected: + virtual LogicalOperator type() const override + { + return LogicalOperator::lNor; + } +}; + +} // namespace pdal + diff --git a/filters/private/expression/Support.hpp b/filters/private/expression/Support.hpp new file mode 100644 index 0000000000..1127e8a51f --- /dev/null +++ b/filters/private/expression/Support.hpp @@ -0,0 +1,74 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#pragma once + +#include + +#include + +#include +#include + +namespace pdal +{ + +class Loggable +{ +public: + virtual ~Loggable() { } + + virtual std::string toString(std::string prefix) const = 0; +}; + +class Filterable : public Loggable +{ +public: + virtual bool operator()(const PointRef& pr) const = 0; +}; + +class Comparable : public Loggable +{ +public: + virtual bool operator()(double v) const = 0; +}; + +template +std::unique_ptr makeUnique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} + +} // namespace pdal + diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 47637c97d9..255f872c64 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -168,6 +168,12 @@ PDAL_ADD_TEST(pdal_filters_crop_test PDAL_ADD_TEST(pdal_filters_decimation_test FILES filters/DecimationFilterTest.cpp) PDAL_ADD_TEST(pdal_filters_divider_test FILES filters/DividerFilterTest.cpp) +PDAL_ADD_TEST(pdal_filters_expression_test + FILES + filters/ExpressionFilterTest.cpp + LINK_WITH + ${PDAL_JSONCPP_LIB_NAME} +) PDAL_ADD_TEST(pdal_filters_ferry_test FILES filters/FerryFilterTest.cpp) PDAL_ADD_TEST(pdal_filters_groupby_test FILES filters/GroupByFilterTest.cpp) PDAL_ADD_TEST(pdal_filters_info_test FILES filters/InfoFilterTest.cpp) diff --git a/test/unit/filters/ExpressionFilterTest.cpp b/test/unit/filters/ExpressionFilterTest.cpp new file mode 100644 index 0000000000..1037bb9e51 --- /dev/null +++ b/test/unit/filters/ExpressionFilterTest.cpp @@ -0,0 +1,518 @@ +/****************************************************************************** + * Copyright (c) 2018, Connor Manning (connor@hobu.co) + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following + * conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided + * with the distribution. + * * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + ****************************************************************************/ + +#include + +#include + +#include "Support.hpp" + +#include "json/json.h" + +#include +#include +#include + +using namespace pdal; + +namespace +{ + +using D = Dimension::Id; +const Dimension::IdList dims { { D::X, D::Y, D::Z } }; + +std::unique_ptr makeTable() +{ + std::unique_ptr table(new FixedPointTable(1)); + table->layout()->registerDims(dims); + table->finalize(); + return table; +} + +std::unique_ptr makeFilter(BasePointTable& table, + Json::Value expression) +{ + Options o; + o.add("expression", expression.toStyledString()); + std::unique_ptr filter(new ExpressionFilter()); + filter->setOptions(o); + filter->prepare(table); + return filter; +} + +} // unnamed namespace + +TEST(ExpressionFilterTest, createStage) +{ + StageFactory f; + Stage* filter(f.createStage("filters.expression")); + EXPECT_TRUE(filter); +} + +TEST(ExpressionFilterTest, noExpression) +{ + ExpressionFilter filter; + PointTable table; + EXPECT_THROW(filter.prepare(table), pdal_error); +} + +TEST(ExpressionFilterTest, missingDimension) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + Json::Value e; + e["Red"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); +} + +TEST(ExpressionFilterTest, invalidSingleComparisons) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + { + // Comparison operators must take values, not arrays. + Json::Value e; + e["X"]["$eq"].append(1); + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } + + { + // Comparison operators must take values, not objects. + Json::Value e; + e["X"]["$eq"]["asdf"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } +} + +TEST(ExpressionFilterTest, singleComparisons) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + // Implicit $eq. + { + Json::Value e; + e["X"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_FALSE(f->processOne(pr)); + } + + // Explicit $eq. + { + Json::Value e; + e["X"]["$eq"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_FALSE(f->processOne(pr)); + } + + // $ne + { + Json::Value e; + e["X"]["$ne"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_TRUE(f->processOne(pr)); + } + + // $gt + { + Json::Value e; + e["X"]["$gt"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_TRUE(f->processOne(pr)); + } + + // $gte + { + Json::Value e; + e["X"]["$gte"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_TRUE(f->processOne(pr)); + } + + // $lt + { + Json::Value e; + e["X"]["$lt"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_FALSE(f->processOne(pr)); + } + + // $gte + { + Json::Value e; + e["X"]["$lte"] = 0; + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_FALSE(f->processOne(pr)); + } +} + +TEST(ExpressionFilterTest, inValidMultiComparisons) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + // Invalid multi-comparisons. + { + // Comparison operators must take arrays, not values. + Json::Value e; + e["X"]["$in"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } + { + // Comparison operators must take arrays, not objects. + Json::Value e; + e["X"]["$in"]["asdf"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } +} + +TEST(ExpressionFilterTest, multiComparisons) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + // $in. + { + Json::Value e; + e["X"]["$in"].append(0); + e["X"]["$in"].append(1); + e["X"]["$in"].append(2); + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 2); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 4); + EXPECT_FALSE(f->processOne(pr)); + } + + // $nin. + { + Json::Value e; + e["X"]["$nin"].append(0); + e["X"]["$nin"].append(1); + e["X"]["$nin"].append(2); + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 2); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 4); + EXPECT_TRUE(f->processOne(pr)); + } +} + +TEST(ExpressionFilterTest, invalidLogicalOperators) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + // Logical operators cannot point to values. + { + Json::Value e; + e["$and"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } + + // Logical operators cannot point to objects. + { + Json::Value e; + e["$and"]["X"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } + + // Logical NOT is an oddball compared to the other logical operators, which + // take arrays. Logical NOT accepts a single expression which it negates. + + // Logical NOT cannot point to values. + { + Json::Value e; + e["$not"] = 42; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } + + // Logical NOT must accept only a single expression. + { + Json::Value e; + Json::Value arr; + { + Json::Value curr; + curr["X"] = 0; + arr.append(curr); + } + { + Json::Value curr; + curr["Y"] = 1; + arr.append(curr); + } + + e["$not"] = arr; + EXPECT_THROW(makeFilter(*table, e), pdal_error); + } +} + +TEST(ExpressionFilterTest, logicalOperators) +{ + auto table(makeTable()); + PointRef pr(*table, 0); + + std::array vals { { 0, 1, 2, } }; + + // $and. + { + Json::Value e; + Json::Value arr; + { + Json::Value curr; + curr["X"] = vals[0]; + arr.append(curr); + } + { + Json::Value curr; + curr["Y"] = vals[1]; + arr.append(curr); + } + { + Json::Value curr; + curr["Z"] = vals[2]; + arr.append(curr); + } + + e["$and"] = arr; + + auto f(makeFilter(*table, e)); + + for (PointId x(0); x < 3; ++x) + { + for (PointId y(0); y < 3; ++y) + { + for (PointId z(0); z < 3; ++z) + { + pr.setField(Dimension::Id::X, x); + pr.setField(Dimension::Id::Y, y); + pr.setField(Dimension::Id::Z, z); + + bool check(x == vals[0] && y == vals[1] && z == vals[2]); + EXPECT_EQ(f->processOne(pr), check) << x << ", " << y << + ", " << z << " != " << check << std::endl; + } + } + } + } + + // $or. + { + Json::Value e; + Json::Value arr; + { + Json::Value curr; + curr["X"] = vals[0]; + arr.append(curr); + } + { + Json::Value curr; + curr["Y"] = vals[1]; + arr.append(curr); + } + { + Json::Value curr; + curr["Z"] = vals[2]; + arr.append(curr); + } + + e["$or"] = arr; + + auto f(makeFilter(*table, e)); + + for (PointId x(0); x < 3; ++x) + { + for (PointId y(0); y < 3; ++y) + { + for (PointId z(0); z < 3; ++z) + { + pr.setField(Dimension::Id::X, x); + pr.setField(Dimension::Id::Y, y); + pr.setField(Dimension::Id::Z, z); + + bool check(x == vals[0] || y == vals[1] || z == vals[2]); + EXPECT_EQ(f->processOne(pr), check) << x << ", " << y << + ", " << z << " != " << check << std::endl; + } + } + } + } + + // $nor. + { + Json::Value e; + Json::Value arr; + { + Json::Value curr; + curr["X"] = vals[0]; + arr.append(curr); + } + { + Json::Value curr; + curr["Y"] = vals[1]; + arr.append(curr); + } + { + Json::Value curr; + curr["Z"] = vals[2]; + arr.append(curr); + } + + e["$nor"] = arr; + + auto f(makeFilter(*table, e)); + + for (PointId x(0); x < 3; ++x) + { + for (PointId y(0); y < 3; ++y) + { + for (PointId z(0); z < 3; ++z) + { + pr.setField(Dimension::Id::X, x); + pr.setField(Dimension::Id::Y, y); + pr.setField(Dimension::Id::Z, z); + + bool check(!(x == vals[0] || y == vals[1] || z == vals[2])); + EXPECT_EQ(f->processOne(pr), check) << x << ", " << y << + ", " << z << " != " << check << std::endl; + } + } + } + } + + // $not + { + Json::Value e; + e["$not"]["X"]["$gt"] = 0; + + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, -1); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_TRUE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 1); + EXPECT_FALSE(f->processOne(pr)); + } + + // $not with inner multi-comparison + { + Json::Value e; + e["$not"]["X"]["$in"].append(0); + e["$not"]["X"]["$in"].append(1); + e["$not"]["X"]["$in"].append(2); + + auto f(makeFilter(*table, e)); + + pr.setField(Dimension::Id::X, 0); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 2); + EXPECT_FALSE(f->processOne(pr)); + + pr.setField(Dimension::Id::X, 4); + EXPECT_TRUE(f->processOne(pr)); + } +} +