[core] Add expression filter support (#11251)
* WIP * WIP * WIP * Remove Filter::operator()(const Feature&) * WIP * WIP * WIP * WIP * Hook up expression filter evaluator * Replace `shared_ptr` with &reference * Fill in implementation of `void operator()(const ExpressionFilter&)` * Fix failing tests * Switch back to a shared_ptr per chat with @anandthakker * Fix benchmark compilation * Shot in the dark to fix CI * Shot in the dark to fix CI (part 2) * Shot in the dark to fix CI (part 3) * In src/mbgl/style/conversion/filter.cpp, add a port of isExpressionFilter and use it to decide in Converter<Filter>::operator() whether to parse the incoming JSON as an ExpressionFilter or one of the legacy filter types * Remove bool Filter::operator()(const GeometryTileFeature&) const * Ensure the map zoom is passed into filtering operations wherever applicable * Add expression filter tests * Addressed PR feedback * Implement `NSPredicate *operator()(mbgl::style::ExpressionFilter filter)` * Fix formatting& nit
This commit is contained in:
committed by
Tobrun
parent
6179110b6b
commit
8a3deab733
Binary file not shown.
@@ -6,6 +6,7 @@
|
||||
#include <mbgl/style/conversion/json.hpp>
|
||||
#include <mbgl/style/conversion/filter.hpp>
|
||||
#include <mbgl/tile/geometry_tile_data.hpp>
|
||||
#include <mbgl/benchmark/stub_geometry_tile_feature.hpp>
|
||||
|
||||
using namespace mbgl;
|
||||
|
||||
@@ -22,15 +23,11 @@ static void Parse_Filter(benchmark::State& state) {
|
||||
|
||||
static void Parse_EvaluateFilter(benchmark::State& state) {
|
||||
const style::Filter filter = parse(R"FILTER(["==", "foo", "bar"])FILTER");
|
||||
const PropertyMap properties = { { "foo", std::string("bar") } };
|
||||
const StubGeometryTileFeature feature = { {}, FeatureType::Unknown , {}, {{ "foo", std::string("bar") }} };
|
||||
const style::expression::EvaluationContext context = { &feature };
|
||||
|
||||
while (state.KeepRunning()) {
|
||||
filter(FeatureType::Unknown, {}, [&] (const std::string& key) -> optional<Value> {
|
||||
auto it = properties.find(key);
|
||||
if (it == properties.end())
|
||||
return {};
|
||||
return it->second;
|
||||
});
|
||||
filter(context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -376,6 +376,8 @@ set(MBGL_CORE_FILES
|
||||
src/mbgl/style/collection.hpp
|
||||
src/mbgl/style/custom_tile_loader.cpp
|
||||
src/mbgl/style/custom_tile_loader.hpp
|
||||
src/mbgl/style/filter.cpp
|
||||
src/mbgl/style/filter_evaluator.cpp
|
||||
src/mbgl/style/image.cpp
|
||||
src/mbgl/style/image_impl.cpp
|
||||
src/mbgl/style/image_impl.hpp
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <mbgl/util/variant.hpp>
|
||||
#include <mbgl/util/feature.hpp>
|
||||
#include <mbgl/util/geometry.hpp>
|
||||
#include <mbgl/style/expression/expression.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -232,6 +233,15 @@ public:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class ExpressionFilter {
|
||||
public:
|
||||
std::shared_ptr<const expression::Expression> expression;
|
||||
|
||||
friend bool operator==(const ExpressionFilter& lhs, const ExpressionFilter& rhs) {
|
||||
return *(lhs.expression) == *(rhs.expression);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
using FilterBase = variant<
|
||||
@@ -258,19 +268,13 @@ using FilterBase = variant<
|
||||
class IdentifierInFilter,
|
||||
class IdentifierNotInFilter,
|
||||
class HasIdentifierFilter,
|
||||
class NotHasIdentifierFilter>;
|
||||
class NotHasIdentifierFilter,
|
||||
class ExpressionFilter>;
|
||||
|
||||
class Filter : public FilterBase {
|
||||
public:
|
||||
using FilterBase::FilterBase;
|
||||
|
||||
bool operator()(const Feature&) const;
|
||||
|
||||
template <class GeometryTileFeature>
|
||||
bool operator()(const GeometryTileFeature&) const;
|
||||
|
||||
template <class PropertyAccessor>
|
||||
bool operator()(FeatureType type, optional<FeatureIdentifier> id, PropertyAccessor accessor) const;
|
||||
bool operator()(const expression::EvaluationContext& context) const;
|
||||
};
|
||||
|
||||
} // namespace style
|
||||
|
||||
@@ -19,242 +19,37 @@ namespace style {
|
||||
// does not match
|
||||
}
|
||||
*/
|
||||
template <class PropertyAccessor>
|
||||
class FilterEvaluator {
|
||||
public:
|
||||
const FeatureType featureType;
|
||||
const optional<FeatureIdentifier> featureIdentifier;
|
||||
const PropertyAccessor propertyAccessor;
|
||||
const expression::EvaluationContext context;
|
||||
|
||||
bool operator()(const NullFilter&) const {
|
||||
return true;
|
||||
}
|
||||
bool operator()(const NullFilter&) const;
|
||||
bool operator()(const EqualsFilter& filter) const;
|
||||
bool operator()(const NotEqualsFilter& filter) const;
|
||||
bool operator()(const LessThanFilter& filter) const;
|
||||
bool operator()(const LessThanEqualsFilter& filter) const;
|
||||
bool operator()(const GreaterThanFilter& filter) const;
|
||||
bool operator()(const GreaterThanEqualsFilter& filter) const;
|
||||
bool operator()(const InFilter& filter) const;
|
||||
bool operator()(const NotInFilter& filter) const;
|
||||
bool operator()(const AnyFilter& filter) const;
|
||||
bool operator()(const AllFilter& filter) const;
|
||||
bool operator()(const NoneFilter& filter) const;
|
||||
bool operator()(const HasFilter& filter) const;
|
||||
bool operator()(const NotHasFilter& filter) const;
|
||||
bool operator()(const TypeEqualsFilter& filter) const;
|
||||
bool operator()(const TypeNotEqualsFilter& filter) const;
|
||||
bool operator()(const TypeInFilter& filter) const;
|
||||
bool operator()(const TypeNotInFilter& filter) const;
|
||||
bool operator()(const IdentifierEqualsFilter& filter) const;
|
||||
bool operator()(const IdentifierNotEqualsFilter& filter) const;
|
||||
bool operator()(const IdentifierInFilter& filter) const;
|
||||
bool operator()(const IdentifierNotInFilter& filter) const;
|
||||
bool operator()(const HasIdentifierFilter&) const;
|
||||
bool operator()(const NotHasIdentifierFilter&) const;
|
||||
bool operator()(const ExpressionFilter&) const;
|
||||
|
||||
bool operator()(const EqualsFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
return actual && equal(*actual, filter.value);
|
||||
}
|
||||
|
||||
bool operator()(const NotEqualsFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
return !actual || !equal(*actual, filter.value);
|
||||
}
|
||||
|
||||
bool operator()(const LessThanFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ < rhs_; });
|
||||
}
|
||||
|
||||
bool operator()(const LessThanEqualsFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ <= rhs_; });
|
||||
}
|
||||
|
||||
bool operator()(const GreaterThanFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ > rhs_; });
|
||||
}
|
||||
|
||||
bool operator()(const GreaterThanEqualsFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ >= rhs_; });
|
||||
}
|
||||
|
||||
bool operator()(const InFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
if (!actual)
|
||||
return false;
|
||||
for (const auto& v: filter.values) {
|
||||
if (equal(*actual, v)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const NotInFilter& filter) const {
|
||||
optional<Value> actual = propertyAccessor(filter.key);
|
||||
if (!actual)
|
||||
return true;
|
||||
for (const auto& v: filter.values) {
|
||||
if (equal(*actual, v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator()(const AnyFilter& filter) const {
|
||||
for (const auto& f: filter.filters) {
|
||||
if (Filter::visit(f, *this)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const AllFilter& filter) const {
|
||||
for (const auto& f: filter.filters) {
|
||||
if (!Filter::visit(f, *this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator()(const NoneFilter& filter) const {
|
||||
for (const auto& f: filter.filters) {
|
||||
if (Filter::visit(f, *this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator()(const HasFilter& filter) const {
|
||||
return bool(propertyAccessor(filter.key));
|
||||
}
|
||||
|
||||
bool operator()(const NotHasFilter& filter) const {
|
||||
return !propertyAccessor(filter.key);
|
||||
}
|
||||
|
||||
|
||||
bool operator()(const TypeEqualsFilter& filter) const {
|
||||
return featureType == filter.value;
|
||||
}
|
||||
|
||||
bool operator()(const TypeNotEqualsFilter& filter) const {
|
||||
return featureType != filter.value;
|
||||
}
|
||||
|
||||
bool operator()(const TypeInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (featureType == v) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const TypeNotInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (featureType == v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool operator()(const IdentifierEqualsFilter& filter) const {
|
||||
return featureIdentifier == filter.value;
|
||||
}
|
||||
|
||||
bool operator()(const IdentifierNotEqualsFilter& filter) const {
|
||||
return featureIdentifier != filter.value;
|
||||
}
|
||||
|
||||
bool operator()(const IdentifierInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (featureIdentifier == v) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const IdentifierNotInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (featureIdentifier == v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator()(const HasIdentifierFilter&) const {
|
||||
return bool(featureIdentifier);
|
||||
}
|
||||
|
||||
bool operator()(const NotHasIdentifierFilter&) const {
|
||||
return !featureIdentifier;
|
||||
}
|
||||
|
||||
private:
|
||||
template <class Op>
|
||||
struct Comparator {
|
||||
const Op& op;
|
||||
|
||||
template <class T>
|
||||
bool operator()(const T& lhs, const T& rhs) const {
|
||||
return op(lhs, rhs);
|
||||
}
|
||||
|
||||
template <class T0, class T1>
|
||||
auto operator()(const T0& lhs, const T1& rhs) const
|
||||
-> typename std::enable_if_t<std::is_arithmetic<T0>::value && !std::is_same<T0, bool>::value &&
|
||||
std::is_arithmetic<T1>::value && !std::is_same<T1, bool>::value, bool> {
|
||||
return op(double(lhs), double(rhs));
|
||||
}
|
||||
|
||||
template <class T0, class T1>
|
||||
auto operator()(const T0&, const T1&) const
|
||||
-> typename std::enable_if_t<!std::is_arithmetic<T0>::value || std::is_same<T0, bool>::value ||
|
||||
!std::is_arithmetic<T1>::value || std::is_same<T1, bool>::value, bool> {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const NullValue&,
|
||||
const NullValue&) const {
|
||||
// Should be unreachable; null is not currently allowed by the style specification.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const std::vector<Value>&,
|
||||
const std::vector<Value>&) const {
|
||||
// Should be unreachable; nested values are not currently allowed by the style specification.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const PropertyMap&,
|
||||
const PropertyMap&) const {
|
||||
// Should be unreachable; nested values are not currently allowed by the style specification.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Op>
|
||||
bool compare(const Value& lhs, const Value& rhs, const Op& op) const {
|
||||
return Value::binary_visit(lhs, rhs, Comparator<Op> { op });
|
||||
}
|
||||
|
||||
bool equal(const Value& lhs, const Value& rhs) const {
|
||||
return compare(lhs, rhs, [] (const auto& lhs_, const auto& rhs_) { return lhs_ == rhs_; });
|
||||
}
|
||||
};
|
||||
|
||||
inline bool Filter::operator()(const Feature& feature) const {
|
||||
return operator()(apply_visitor(ToFeatureType(), feature.geometry), feature.id, [&] (const std::string& key) -> optional<Value> {
|
||||
auto it = feature.properties.find(key);
|
||||
if (it == feature.properties.end())
|
||||
return {};
|
||||
return it->second;
|
||||
});
|
||||
}
|
||||
|
||||
template <class GeometryTileFeature>
|
||||
bool Filter::operator()(const GeometryTileFeature& feature) const {
|
||||
return operator()(feature.getType(), feature.getID(), [&] (const auto& key) { return feature.getValue(key); });
|
||||
}
|
||||
|
||||
template <class PropertyAccessor>
|
||||
bool Filter::operator()(FeatureType type, optional<FeatureIdentifier> id, PropertyAccessor accessor) const {
|
||||
return FilterBase::visit(*this, FilterEvaluator<PropertyAccessor> { type, id, accessor });
|
||||
}
|
||||
|
||||
} // namespace style
|
||||
} // namespace mbgl
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#import "NSPredicate+MGLAdditions.h"
|
||||
|
||||
#import "MGLValueEvaluator.h"
|
||||
#import "MGLStyleValue_Private.h"
|
||||
|
||||
class FilterEvaluator {
|
||||
public:
|
||||
@@ -194,6 +195,11 @@ public:
|
||||
NSPredicate *operator()(mbgl::style::NotHasIdentifierFilter filter) {
|
||||
return [NSPredicate predicateWithFormat:@"%K == nil", @"$id"];
|
||||
}
|
||||
|
||||
NSPredicate *operator()(mbgl::style::ExpressionFilter filter) {
|
||||
id jsonObject = MGLJSONObjectFromMBGLExpression(*filter.expression);
|
||||
return [NSPredicate mgl_predicateWithJSONObject:jsonObject];
|
||||
}
|
||||
};
|
||||
|
||||
@implementation NSPredicate (MGLAdditions)
|
||||
|
||||
@@ -125,7 +125,7 @@ void FeatureIndex::addFeature(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (options.filter && !(*options.filter)(*geometryTileFeature)) {
|
||||
if (options.filter && !(*options.filter)(style::expression::EvaluationContext { static_cast<float>(tileID.z), geometryTileFeature.get() })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters,
|
||||
const size_t featureCount = sourceLayer->featureCount();
|
||||
for (size_t i = 0; i < featureCount; ++i) {
|
||||
auto feature = sourceLayer->getFeature(i);
|
||||
if (!leader.filter(feature->getType(), feature->getID(), [&] (const auto& key) { return feature->getValue(key); }))
|
||||
if (!leader.filter(expression::EvaluationContext { this->zoom, feature.get() }))
|
||||
continue;
|
||||
|
||||
SymbolFeature ft(std::move(feature));
|
||||
|
||||
@@ -1,11 +1,52 @@
|
||||
#include <mbgl/style/conversion/filter.hpp>
|
||||
#include <mbgl/util/geometry.hpp>
|
||||
#include <mbgl/style/expression/expression.hpp>
|
||||
#include <mbgl/style/expression/type.hpp>
|
||||
#include <mbgl/style/conversion/expression.hpp>
|
||||
|
||||
namespace mbgl {
|
||||
namespace style {
|
||||
namespace conversion {
|
||||
|
||||
using GeometryValue = mapbox::geometry::value;
|
||||
|
||||
static optional<Value> normalizeValue(const optional<Value>& value, Error& error) {
|
||||
// This is a port from https://github.com/mapbox/mapbox-gl-js/blob/master/src/style-spec/feature_filter/index.js
|
||||
static bool isExpressionFilter(const Convertible& filter) {
|
||||
if (!isArray(filter) || arrayLength(filter) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
optional<std::string> op = toString(arrayMember(filter, 0));
|
||||
|
||||
if (!op) {
|
||||
return false;
|
||||
|
||||
} else if (*op == "has") {
|
||||
if (arrayLength(filter) < 2) return false;
|
||||
optional<std::string> operand = toString(arrayMember(filter, 1));
|
||||
return operand && *operand != "$id" && *operand != "$type";
|
||||
|
||||
} else if (*op == "in" || *op == "!in" || *op == "!has" || *op == "none") {
|
||||
return false;
|
||||
|
||||
} else if (*op == "==" || *op == "!=" || *op == ">" || *op == ">=" || *op == "<" || *op == "<=") {
|
||||
return arrayLength(filter) == 3 && (isArray(arrayMember(filter, 1)) || isArray(arrayMember(filter, 2)));
|
||||
|
||||
} else if (*op == "any" || *op == "all") {
|
||||
for (std::size_t i = 1; i < arrayLength(filter); i++) {
|
||||
Convertible f = arrayMember(filter, i);
|
||||
if (!isExpressionFilter(f) && !toBool(f)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static optional<GeometryValue> normalizeValue(const optional<GeometryValue>& value, Error& error) {
|
||||
if (!value) {
|
||||
error = { "filter expression value must be a boolean, number, or string" };
|
||||
return {};
|
||||
@@ -32,7 +73,7 @@ static optional<FeatureType> toFeatureType(const Convertible& value, Error& erro
|
||||
}
|
||||
|
||||
static optional<FeatureIdentifier> toFeatureIdentifier(const Convertible& value, Error& error) {
|
||||
optional<Value> identifier = toValue(value);
|
||||
optional<GeometryValue> identifier = toValue(value);
|
||||
if (!identifier) {
|
||||
error = { "filter expression value must be a boolean, number, or string" };
|
||||
return {};
|
||||
@@ -99,7 +140,7 @@ optional<Filter> convertEqualityFilter(const Convertible& value, Error& error) {
|
||||
return { IdentifierFilterType { *filterValue } };
|
||||
|
||||
} else {
|
||||
optional<Value> filterValue = normalizeValue(toValue(arrayMember(value, 2)), error);
|
||||
optional<GeometryValue> filterValue = normalizeValue(toValue(arrayMember(value, 2)), error);
|
||||
if (!filterValue) {
|
||||
return {};
|
||||
}
|
||||
@@ -121,7 +162,7 @@ optional<Filter> convertBinaryFilter(const Convertible& value, Error& error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
optional<Value> filterValue = normalizeValue(toValue(arrayMember(value, 2)), error);
|
||||
optional<GeometryValue> filterValue = normalizeValue(toValue(arrayMember(value, 2)), error);
|
||||
if (!filterValue) {
|
||||
return {};
|
||||
}
|
||||
@@ -167,9 +208,9 @@ optional<Filter> convertSetFilter(const Convertible& value, Error& error) {
|
||||
return { IdentifierFilterType { std::move(values) } };
|
||||
|
||||
} else {
|
||||
std::vector<Value> values;
|
||||
std::vector<GeometryValue> values;
|
||||
for (std::size_t i = 2; i < arrayLength(value); ++i) {
|
||||
optional<Value> filterValue = normalizeValue(toValue(arrayMember(value, i)), error);
|
||||
optional<GeometryValue> filterValue = normalizeValue(toValue(arrayMember(value, i)), error);
|
||||
if (!filterValue) {
|
||||
return {};
|
||||
}
|
||||
@@ -193,8 +234,20 @@ optional<Filter> convertCompoundFilter(const Convertible& value, Error& error) {
|
||||
|
||||
return { FilterType { std::move(filters) } };
|
||||
}
|
||||
|
||||
optional<Filter> convertExpressionFilter(const Convertible& value, Error& error) {
|
||||
optional<std::unique_ptr<Expression>> expression = convert<std::unique_ptr<Expression>>(value, error, expression::type::Boolean);
|
||||
if (!expression) {
|
||||
return {};
|
||||
}
|
||||
return { ExpressionFilter { std::move(*expression) } };
|
||||
}
|
||||
|
||||
optional<Filter> Converter<Filter>::operator()(const Convertible& value, Error& error) const {
|
||||
if (isExpressionFilter(value)) {
|
||||
return convertExpressionFilter(value, error);
|
||||
}
|
||||
|
||||
if (!isArray(value)) {
|
||||
error = { "filter expression must be an array" };
|
||||
return {};
|
||||
|
||||
@@ -225,6 +225,10 @@ public:
|
||||
void operator()(const NotHasIdentifierFilter&) {
|
||||
stringifyUnaryFilter("!has", "$id");
|
||||
}
|
||||
|
||||
void operator()(const ExpressionFilter& filter) {
|
||||
stringify(writer, filter.expression->serialize());
|
||||
}
|
||||
|
||||
private:
|
||||
template <class F>
|
||||
|
||||
13
src/mbgl/style/filter.cpp
Normal file
13
src/mbgl/style/filter.cpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#include <mbgl/style/filter.hpp>
|
||||
#include <mbgl/style/filter_evaluator.hpp>
|
||||
#include <mbgl/tile/geometry_tile_data.hpp>
|
||||
|
||||
namespace mbgl {
|
||||
namespace style {
|
||||
|
||||
bool Filter::operator()(const expression::EvaluationContext &context) const {
|
||||
return FilterBase::visit(*this, FilterEvaluator { context });
|
||||
}
|
||||
|
||||
} // namespace style
|
||||
} // namespace mbgl
|
||||
225
src/mbgl/style/filter_evaluator.cpp
Normal file
225
src/mbgl/style/filter_evaluator.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include <mbgl/style/filter.hpp>
|
||||
#include <mbgl/style/filter_evaluator.hpp>
|
||||
#include <mbgl/tile/geometry_tile_data.hpp>
|
||||
|
||||
namespace mbgl {
|
||||
namespace style {
|
||||
|
||||
template <class Op>
|
||||
struct Comparator {
|
||||
const Op& op;
|
||||
|
||||
template <class T>
|
||||
bool operator()(const T& lhs, const T& rhs) const {
|
||||
return op(lhs, rhs);
|
||||
}
|
||||
|
||||
template <class T0, class T1>
|
||||
auto operator()(const T0& lhs, const T1& rhs) const
|
||||
-> typename std::enable_if_t<std::is_arithmetic<T0>::value && !std::is_same<T0, bool>::value &&
|
||||
std::is_arithmetic<T1>::value && !std::is_same<T1, bool>::value, bool> {
|
||||
return op(double(lhs), double(rhs));
|
||||
}
|
||||
|
||||
template <class T0, class T1>
|
||||
auto operator()(const T0&, const T1&) const
|
||||
-> typename std::enable_if_t<!std::is_arithmetic<T0>::value || std::is_same<T0, bool>::value ||
|
||||
!std::is_arithmetic<T1>::value || std::is_same<T1, bool>::value, bool> {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const NullValue&,
|
||||
const NullValue&) const {
|
||||
// Should be unreachable; null is not currently allowed by the style specification.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const std::vector<Value>&,
|
||||
const std::vector<Value>&) const {
|
||||
// Should be unreachable; nested values are not currently allowed by the style specification.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool operator()(const PropertyMap&,
|
||||
const PropertyMap&) const {
|
||||
// Should be unreachable; nested values are not currently allowed by the style specification.
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template <class Op>
|
||||
bool compare(const Value& lhs, const Value& rhs, const Op& op) {
|
||||
return Value::binary_visit(lhs, rhs, Comparator<Op> { op });
|
||||
}
|
||||
|
||||
bool equal(const Value& lhs, const Value& rhs) {
|
||||
return compare(lhs, rhs, [] (const auto& lhs_, const auto& rhs_) { return lhs_ == rhs_; });
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const NullFilter&) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const EqualsFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
return actual && equal(*actual, filter.value);
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const NotEqualsFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
return !actual || !equal(*actual, filter.value);
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const LessThanFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ < rhs_; });
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const LessThanEqualsFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ <= rhs_; });
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const GreaterThanFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ > rhs_; });
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const GreaterThanEqualsFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
return actual && compare(*actual, filter.value, [] (const auto& lhs_, const auto& rhs_) { return lhs_ >= rhs_; });
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const InFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
if (!actual)
|
||||
return false;
|
||||
for (const auto& v: filter.values) {
|
||||
if (equal(*actual, v)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const NotInFilter& filter) const {
|
||||
optional<Value> actual = context.feature->getValue(filter.key);
|
||||
if (!actual)
|
||||
return true;
|
||||
for (const auto& v: filter.values) {
|
||||
if (equal(*actual, v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const AnyFilter& filter) const {
|
||||
for (const auto& f: filter.filters) {
|
||||
if (Filter::visit(f, *this)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const AllFilter& filter) const {
|
||||
for (const auto& f: filter.filters) {
|
||||
if (!Filter::visit(f, *this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const NoneFilter& filter) const {
|
||||
for (const auto& f: filter.filters) {
|
||||
if (Filter::visit(f, *this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const HasFilter& filter) const {
|
||||
return bool(context.feature->getValue(filter.key));
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const NotHasFilter& filter) const {
|
||||
return !context.feature->getValue(filter.key);
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const TypeEqualsFilter& filter) const {
|
||||
return context.feature->getType() == filter.value;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const TypeNotEqualsFilter& filter) const {
|
||||
return context.feature->getType() != filter.value;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const TypeInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (context.feature->getType() == v) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const TypeNotInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (context.feature->getType() == v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const IdentifierEqualsFilter& filter) const {
|
||||
return context.feature->getID() == filter.value;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const IdentifierNotEqualsFilter& filter) const {
|
||||
return context.feature->getID() != filter.value;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const IdentifierInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (context.feature->getID() == v) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const IdentifierNotInFilter& filter) const {
|
||||
for (const auto& v: filter.values) {
|
||||
if (context.feature->getID() == v) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const HasIdentifierFilter&) const {
|
||||
return bool(context.feature->getID());
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const NotHasIdentifierFilter&) const {
|
||||
return !context.feature->getID();
|
||||
}
|
||||
|
||||
bool FilterEvaluator::operator()(const ExpressionFilter& filter) const {
|
||||
const expression::EvaluationResult result = filter.expression->evaluate(context);
|
||||
if (result) {
|
||||
const optional<bool> typed = expression::fromExpressionValue<bool>(*result);
|
||||
return typed ? *typed : false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace style
|
||||
} // namespace mbgl
|
||||
@@ -79,7 +79,7 @@ void CustomGeometryTile::querySourceFeatures(
|
||||
auto feature = layer->getFeature(i);
|
||||
|
||||
// Apply filter, if any
|
||||
if (queryOptions.filter && !(*queryOptions.filter)(*feature)) {
|
||||
if (queryOptions.filter && !(*queryOptions.filter)(style::expression::EvaluationContext { static_cast<float>(id.overscaledZ), feature.get() })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ void GeoJSONTile::querySourceFeatures(
|
||||
auto feature = layer->getFeature(i);
|
||||
|
||||
// Apply filter, if any
|
||||
if (options.filter && !(*options.filter)(*feature)) {
|
||||
if (options.filter && !(*options.filter)(style::expression::EvaluationContext { static_cast<float>(this->id.overscaledZ), feature.get() })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ void GeometryTile::querySourceFeatures(
|
||||
auto feature = layer->getFeature(i);
|
||||
|
||||
// Apply filter, if any
|
||||
if (options.filter && !(*options.filter)(*feature)) {
|
||||
if (options.filter && !(*options.filter)(style::expression::EvaluationContext { static_cast<float>(this->id.overscaledZ), feature.get() })) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -339,7 +339,7 @@ void GeometryTileWorker::redoLayout() {
|
||||
for (std::size_t i = 0; !obsolete && i < geometryLayer->featureCount(); i++) {
|
||||
std::unique_ptr<GeometryTileFeature> feature = geometryLayer->getFeature(i);
|
||||
|
||||
if (!filter(feature->getType(), feature->getID(), [&] (const auto& key) { return feature->getValue(key); }))
|
||||
if (!filter(expression::EvaluationContext { static_cast<float>(this->id.overscaledZ), feature.get() }))
|
||||
continue;
|
||||
|
||||
GeometryCollection geometries = feature->getGeometries();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <mbgl/test/util.hpp>
|
||||
#include <mbgl/util/feature.hpp>
|
||||
#include <mbgl/util/geometry.hpp>
|
||||
#include <mbgl/test/stub_geometry_tile_feature.hpp>
|
||||
|
||||
#include <mbgl/style/filter.hpp>
|
||||
#include <mbgl/style/filter_evaluator.hpp>
|
||||
@@ -10,114 +11,101 @@
|
||||
using namespace mbgl;
|
||||
using namespace mbgl::style;
|
||||
|
||||
Filter parse(const char * expression) {
|
||||
bool filter(const char * json, const PropertyMap& featureProperties = {{}}, optional<FeatureIdentifier> featureId = {}, FeatureType featureType = FeatureType::Point, GeometryCollection featureGeometry = {}) {
|
||||
conversion::Error error;
|
||||
optional<Filter> filter = conversion::convertJSON<Filter>(expression, error);
|
||||
optional<Filter> filter = conversion::convertJSON<Filter>(json, error);
|
||||
EXPECT_TRUE(bool(filter));
|
||||
return *filter;
|
||||
}
|
||||
|
||||
Feature feature(const PropertyMap& properties, const Geometry<double>& geometry = Point<double>()) {
|
||||
Feature result { geometry };
|
||||
result.properties = properties;
|
||||
return result;
|
||||
EXPECT_EQ(error.message, "");
|
||||
|
||||
StubGeometryTileFeature feature { featureId, featureType, featureGeometry, featureProperties };
|
||||
expression::EvaluationContext context = { &feature };
|
||||
|
||||
return (*filter)(context);
|
||||
}
|
||||
|
||||
TEST(Filter, EqualsString) {
|
||||
Filter f = parse(R"(["==", "foo", "bar"])");
|
||||
ASSERT_TRUE(f(feature({{ "foo", std::string("bar") }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", std::string("baz") }})));
|
||||
auto f = R"(["==", "foo", "bar"])";
|
||||
ASSERT_TRUE(filter(f, {{ "foo", std::string("bar") }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", std::string("baz") }}));
|
||||
}
|
||||
|
||||
TEST(Filter, EqualsNumber) {
|
||||
Filter f = parse(R"(["==", "foo", 0])");
|
||||
ASSERT_TRUE(f(feature({{ "foo", int64_t(0) }})));
|
||||
ASSERT_TRUE(f(feature({{ "foo", uint64_t(0) }})));
|
||||
ASSERT_TRUE(f(feature({{ "foo", double(0) }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", int64_t(1) }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", uint64_t(1) }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", double(1) }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", std::string("0") }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", false }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", true }})));
|
||||
ASSERT_FALSE(f(feature({{ "foo", mapbox::geometry::null_value }})));
|
||||
ASSERT_FALSE(f(feature({{}})));
|
||||
auto f = R"(["==", "foo", 0])";
|
||||
ASSERT_TRUE(filter(f, {{ "foo", int64_t(0) }}));
|
||||
ASSERT_TRUE(filter(f, {{ "foo", uint64_t(0) }}));
|
||||
ASSERT_TRUE(filter(f, {{ "foo", double(0) }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", int64_t(1) }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", uint64_t(1) }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", double(1) }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", std::string("0") }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", false }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", true }}));
|
||||
ASSERT_FALSE(filter(f, {{ "foo", mapbox::geometry::null_value }}));
|
||||
ASSERT_FALSE(filter(f, {{}}));
|
||||
}
|
||||
|
||||
TEST(Filter, EqualsType) {
|
||||
Filter f = parse(R"(["==", "$type", "LineString"])");
|
||||
ASSERT_FALSE(f(feature({{}}, Point<double>())));
|
||||
ASSERT_TRUE(f(feature({{}}, LineString<double>())));
|
||||
auto f = R"(["==", "$type", "LineString"])";
|
||||
ASSERT_FALSE(filter(f, {{}}, {}, FeatureType::Point, {}));
|
||||
ASSERT_TRUE(filter(f, {{}}, {}, FeatureType::LineString, {}));
|
||||
}
|
||||
|
||||
TEST(Filter, InType) {
|
||||
Filter f = parse(R"(["in", "$type", "LineString", "Polygon"])");
|
||||
ASSERT_FALSE(f(feature({{}}, Point<double>())));
|
||||
ASSERT_TRUE(f(feature({{}}, LineString<double>())));
|
||||
ASSERT_TRUE(f(feature({{}}, Polygon<double>())));
|
||||
auto f = R"(["in", "$type", "LineString", "Polygon"])";
|
||||
ASSERT_FALSE(filter(f, {{}}, {}, FeatureType::Point));
|
||||
ASSERT_TRUE(filter(f, {{}}, {}, FeatureType::LineString));
|
||||
ASSERT_TRUE(filter(f, {{}}, {}, FeatureType::Polygon));
|
||||
}
|
||||
|
||||
TEST(Filter, Any) {
|
||||
ASSERT_FALSE(parse("[\"any\"]")(feature({{}})));
|
||||
ASSERT_TRUE(parse("[\"any\", [\"==\", \"foo\", 1]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_FALSE(parse("[\"any\", [\"==\", \"foo\", 0]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_TRUE(parse("[\"any\", [\"==\", \"foo\", 0], [\"==\", \"foo\", 1]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_FALSE(filter("[\"any\"]"));
|
||||
ASSERT_TRUE(filter("[\"any\", [\"==\", \"foo\", 1]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_FALSE(filter("[\"any\", [\"==\", \"foo\", 0]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_TRUE(filter("[\"any\", [\"==\", \"foo\", 0], [\"==\", \"foo\", 1]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
}
|
||||
|
||||
TEST(Filter, All) {
|
||||
ASSERT_TRUE(parse("[\"all\"]")(feature({{}})));
|
||||
ASSERT_TRUE(parse("[\"all\", [\"==\", \"foo\", 1]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_FALSE(parse("[\"all\", [\"==\", \"foo\", 0]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_FALSE(parse("[\"all\", [\"==\", \"foo\", 0], [\"==\", \"foo\", 1]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_TRUE(filter("[\"all\"]", {{}}));
|
||||
ASSERT_TRUE(filter("[\"all\", [\"==\", \"foo\", 1]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_FALSE(filter("[\"all\", [\"==\", \"foo\", 0]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_FALSE(filter("[\"all\", [\"==\", \"foo\", 0], [\"==\", \"foo\", 1]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
}
|
||||
|
||||
TEST(Filter, None) {
|
||||
ASSERT_TRUE(parse("[\"none\"]")(feature({{}})));
|
||||
ASSERT_FALSE(parse("[\"none\", [\"==\", \"foo\", 1]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_TRUE(parse("[\"none\", [\"==\", \"foo\", 0]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_FALSE(parse("[\"none\", [\"==\", \"foo\", 0], [\"==\", \"foo\", 1]]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_TRUE(filter("[\"none\"]"));
|
||||
ASSERT_FALSE(filter("[\"none\", [\"==\", \"foo\", 1]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_TRUE(filter("[\"none\", [\"==\", \"foo\", 0]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_FALSE(filter("[\"none\", [\"==\", \"foo\", 0], [\"==\", \"foo\", 1]]", {{ std::string("foo"), int64_t(1) }}));
|
||||
}
|
||||
|
||||
TEST(Filter, Has) {
|
||||
ASSERT_TRUE(parse("[\"has\", \"foo\"]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_TRUE(parse("[\"has\", \"foo\"]")(
|
||||
feature({{ std::string("foo"), int64_t(0) }})));
|
||||
ASSERT_TRUE(parse("[\"has\", \"foo\"]")(
|
||||
feature({{ std::string("foo"), false }})));
|
||||
ASSERT_FALSE(parse("[\"has\", \"foo\"]")(
|
||||
feature({{}})));
|
||||
ASSERT_TRUE(filter("[\"has\", \"foo\"]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_TRUE(filter("[\"has\", \"foo\"]", {{ std::string("foo"), int64_t(0) }}));
|
||||
ASSERT_TRUE(filter("[\"has\", \"foo\"]", {{ std::string("foo"), false }}));
|
||||
ASSERT_FALSE(filter("[\"has\", \"foo\"]"));
|
||||
}
|
||||
|
||||
TEST(Filter, NotHas) {
|
||||
ASSERT_FALSE(parse("[\"!has\", \"foo\"]")(
|
||||
feature({{ std::string("foo"), int64_t(1) }})));
|
||||
ASSERT_FALSE(parse("[\"!has\", \"foo\"]")(
|
||||
feature({{ std::string("foo"), int64_t(0) }})));
|
||||
ASSERT_FALSE(parse("[\"!has\", \"foo\"]")(
|
||||
feature({{ std::string("foo"), false }})));
|
||||
ASSERT_TRUE(parse("[\"!has\", \"foo\"]")(
|
||||
feature({{}})));
|
||||
ASSERT_FALSE(filter("[\"!has\", \"foo\"]", {{ std::string("foo"), int64_t(1) }}));
|
||||
ASSERT_FALSE(filter("[\"!has\", \"foo\"]", {{ std::string("foo"), int64_t(0) }}));
|
||||
ASSERT_FALSE(filter("[\"!has\", \"foo\"]", {{ std::string("foo"), false }}));
|
||||
ASSERT_TRUE(filter("[\"!has\", \"foo\"]"));
|
||||
}
|
||||
|
||||
TEST(Filter, ID) {
|
||||
Feature feature1 { Point<double>() };
|
||||
feature1.id = { uint64_t(1234) };
|
||||
|
||||
ASSERT_TRUE(parse("[\"==\", \"$id\", 1234]")(feature1));
|
||||
ASSERT_FALSE(parse("[\"==\", \"$id\", \"1234\"]")(feature1));
|
||||
|
||||
Feature feature2 { Point<double>() };
|
||||
feature2.properties["id"] = { uint64_t(1234) };
|
||||
|
||||
ASSERT_FALSE(parse("[\"==\", \"$id\", 1234]")(feature2));
|
||||
FeatureIdentifier id1 { uint64_t{ 1234 } };
|
||||
ASSERT_TRUE(filter("[\"==\", \"$id\", 1234]", {{}}, id1));
|
||||
ASSERT_FALSE(filter("[\"==\", \"$id\", \"1234\"]", {{}}, id1));
|
||||
|
||||
ASSERT_FALSE(filter("[\"==\", \"$id\", 1234]", {{ "id", uint64_t(1234) }}));
|
||||
}
|
||||
|
||||
TEST(Filter, Expression) {
|
||||
ASSERT_TRUE(filter("[\"==\", [\"+\", 1, 1], 2]"));
|
||||
ASSERT_FALSE(filter("[\"==\", [\"+\", 1, 1], 4]"));
|
||||
}
|
||||
|
||||
TEST(Filter, PropertyExpression) {
|
||||
ASSERT_TRUE(filter("[\"==\", [\"get\", \"two\"], 2]", {{"two", int64_t(2)}}));
|
||||
ASSERT_FALSE(filter("[\"==\", [\"get\", \"two\"], 4]", {{"two", int64_t(2)}}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user