diff options
author | Anand Thakker <github@anandthakker.net> | 2017-07-06 09:37:46 -0400 |
---|---|---|
committer | Anand Thakker <github@anandthakker.net> | 2017-08-11 21:54:48 -0400 |
commit | fd2c1055a786c365b973a80b3f8cc36c9ba2fa8d (patch) | |
tree | f26f9fb9f1f1ef13259f7d937f1c3b70188acf76 | |
parent | 25a0b7925c0d8c215ae7b02550cadf4df59022ac (diff) | |
download | qtlocation-mapboxgl-fd2c1055a786c365b973a80b3f8cc36c9ba2fa8d.tar.gz |
Add base Expression model
-rw-r--r-- | cmake/core-files.cmake | 15 | ||||
-rw-r--r-- | cmake/node.cmake | 2 | ||||
-rw-r--r-- | include/mbgl/style/conversion/expression.hpp | 27 | ||||
-rw-r--r-- | include/mbgl/style/expression/definitions.hpp | 322 | ||||
-rw-r--r-- | include/mbgl/style/expression/expression.hpp | 257 | ||||
-rw-r--r-- | include/mbgl/style/expression/parse.hpp | 117 | ||||
-rw-r--r-- | include/mbgl/style/expression/parsing_context.hpp | 40 | ||||
-rw-r--r-- | include/mbgl/style/expression/type.hpp | 116 | ||||
-rw-r--r-- | include/mbgl/style/expression/type_check.hpp | 14 | ||||
-rw-r--r-- | include/mbgl/style/expression/value.hpp | 43 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | platform/node/src/node_expression.cpp | 201 | ||||
-rw-r--r-- | platform/node/src/node_expression.hpp | 40 | ||||
-rw-r--r-- | platform/node/src/node_mapbox_gl_native.cpp | 2 | ||||
-rw-r--r-- | platform/node/test/expression.test.js | 52 | ||||
-rw-r--r-- | src/mbgl/style/expression/definitions.cpp | 355 | ||||
-rw-r--r-- | src/mbgl/style/expression/type.cpp | 120 | ||||
-rw-r--r-- | src/mbgl/style/expression/type_check.cpp | 121 | ||||
-rw-r--r-- | src/mbgl/style/expression/value.cpp | 108 | ||||
-rw-r--r-- | src/mbgl/style/function/expression.cpp | 50 |
20 files changed, 2003 insertions, 1 deletions
diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake index 1a86da6f24..ee15bf5b91 100644 --- a/cmake/core-files.cmake +++ b/cmake/core-files.cmake @@ -371,6 +371,7 @@ set(MBGL_CORE_FILES include/mbgl/style/conversion/constant.hpp include/mbgl/style/conversion/coordinate.hpp include/mbgl/style/conversion/data_driven_property_value.hpp + include/mbgl/style/conversion/expression.hpp include/mbgl/style/conversion/filter.hpp include/mbgl/style/conversion/function.hpp include/mbgl/style/conversion/geojson.hpp @@ -388,6 +389,19 @@ set(MBGL_CORE_FILES src/mbgl/style/conversion/json.hpp src/mbgl/style/conversion/stringify.hpp + # style/expression + include/mbgl/style/expression/definitions.hpp + include/mbgl/style/expression/expression.hpp + include/mbgl/style/expression/parse.hpp + include/mbgl/style/expression/parsing_context.hpp + include/mbgl/style/expression/type.hpp + include/mbgl/style/expression/type_check.hpp + include/mbgl/style/expression/value.hpp + src/mbgl/style/expression/definitions.cpp + src/mbgl/style/expression/type.cpp + src/mbgl/style/expression/type_check.cpp + src/mbgl/style/expression/value.cpp + # style/function include/mbgl/style/function/camera_function.hpp include/mbgl/style/function/categorical_stops.hpp @@ -400,6 +414,7 @@ set(MBGL_CORE_FILES include/mbgl/style/function/interval_stops.hpp include/mbgl/style/function/source_function.hpp src/mbgl/style/function/categorical_stops.cpp + src/mbgl/style/function/expression.cpp src/mbgl/style/function/identity_stops.cpp # style/layers diff --git a/cmake/node.cmake b/cmake/node.cmake index 502edd8293..add66e7fbb 100644 --- a/cmake/node.cmake +++ b/cmake/node.cmake @@ -21,6 +21,8 @@ target_sources(mbgl-node PRIVATE platform/node/src/node_feature.cpp PRIVATE platform/node/src/node_thread_pool.hpp PRIVATE platform/node/src/node_thread_pool.cpp + PRIVATE platform/node/src/node_expression.hpp + PRIVATE platform/node/src/node_expression.cpp PRIVATE platform/node/src/util/async_queue.hpp ) diff --git a/include/mbgl/style/conversion/expression.hpp b/include/mbgl/style/conversion/expression.hpp new file mode 100644 index 0000000000..bce7c91396 --- /dev/null +++ b/include/mbgl/style/conversion/expression.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <memory> +#include <mbgl/style/expression/parse.hpp> +#include <mbgl/style/conversion.hpp> + +namespace mbgl { +namespace style { +namespace conversion { + +using namespace mbgl::style::expression; + +template<> struct Converter<std::unique_ptr<Expression>> { + template <class V> + optional<std::unique_ptr<Expression>> operator()(const V& value, Error& error) const { + auto parsed = parseExpression(value, ParsingContext()); + if (parsed.template is<std::unique_ptr<Expression>>()) { + return std::move(parsed.template get<std::unique_ptr<Expression>>()); + } + error = { parsed.template get<CompileError>().message }; + return {}; + }; +}; + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/definitions.hpp b/include/mbgl/style/expression/definitions.hpp new file mode 100644 index 0000000000..7557e9a682 --- /dev/null +++ b/include/mbgl/style/expression/definitions.hpp @@ -0,0 +1,322 @@ +#include <mbgl/style/expression/expression.hpp> + +namespace mbgl { +namespace style { +namespace expression { + + +// Concrete expression definitions +class MathConstant : public LambdaExpression { +public: + MathConstant(const std::string& key, const std::string& name, float value_) : + LambdaExpression(key, name, {}, type::Number, {{}}), + value(value_) + {} + + EvaluationResult evaluate(const EvaluationParameters&) const override { return value; } + + std::unique_ptr<Expression> applyInferredType(const type::Type&, Args) const override { + return std::make_unique<MathConstant>(getKey(), getName(), value); + } + + // TODO: declaring these constants like `static constexpr double E = 2.718...` caused + // a puzzling link error. + static std::unique_ptr<Expression> ln2(const ParsingContext& ctx) { + return std::make_unique<MathConstant>(ctx.key(), "ln2", 0.693147180559945309417); + } + static std::unique_ptr<Expression> e(const ParsingContext& ctx) { + return std::make_unique<MathConstant>(ctx.key(), "e", 2.71828182845904523536); + } + static std::unique_ptr<Expression> pi(const ParsingContext& ctx) { + return std::make_unique<MathConstant>(ctx.key(), "pi", 3.14159265358979323846); + } +private: + float value; +}; + +class TypeOf : public LambdaBase<TypeOf> { +public: + using LambdaBase::LambdaBase; + static type::StringType type() { return type::String; }; + static std::vector<Params> signatures() { + return {{type::Value}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +// TODO: This doesn't work with CRTP. With: +// `template <typename T> class Assertion : public LambdaBase<Assertion<T>>`, +// there's an error referring to member 'args'. +template <typename T> +class Assertion : public LambdaExpression { +public: + Assertion(const std::string& key, const std::string& name, Args args) : + LambdaExpression(key, name, std::move(args), Assertion::type(), Assertion::signatures()) + {} + Assertion(const std::string& key, const std::string& name, const type::Type& type, Args args) : + LambdaExpression(key, name, std::move(args), type, Assertion::signatures()) + {} + + static type::Type type() { return valueTypeToExpressionType<T>(); } + static std::vector<LambdaExpression::Params> signatures() { return {{type::Value}}; }; + + std::unique_ptr<Expression> applyInferredType(const type::Type& type, Args args) const override { + return std::make_unique<Assertion>(getKey(), getName(), type, std::move(args)); + } + + EvaluationResult evaluate(const EvaluationParameters& params) const override { + const auto& result = args[0]->template evaluate<T>(params); + if (result) return *result; + return result.error(); + }; +}; + +class Array : public LambdaBase<Array> { +public: + Array(const std::string& key, const std::string& name, type::Type type, Args args) : + LambdaBase(key, name, type, std::move(args)) + {} + + static std::vector<Params> signatures() { return {{type::Value}}; } + + template <typename V> + static ParseResult parse(const V& value, const ParsingContext& ctx) { + assert(isArray(value)); + auto length = arrayLength(value); + if (length < 2) return CompileError { "Expected at least one argument to \"array\"", ctx.key() }; + if (length > 4) return CompileError { + "Expected one, two, or three arguments to \"array\", but found " + std::to_string(length - 1) + " instead.", + ctx.key() + }; + + const std::string& name = *toString(arrayMember(value, 0)); + + auto arg = parseExpression(arrayMember(value, 1), ParsingContext(ctx, {1}, {"array"})); + if (arg.template is<CompileError>()) return arg.template get<CompileError>(); + Args args; + args.push_back(std::move(arg.template get<std::unique_ptr<Expression>>())); + + // parse the optional item type and length arguments + optional<type::Type> itemType; + optional<size_t> N; + if (length > 2) { + const auto& itemTypeName = toString(arrayMember(value, 2)); + if (itemTypeName && *itemTypeName == "string") { + itemType = {type::String}; + } else if (itemTypeName && *itemTypeName == "number") { + itemType = {type::String}; + } else if (itemTypeName && *itemTypeName == "boolean") { + itemType = {type::String}; + } else { + return CompileError { + "The item type argument to \"array\" must be one of ${Object.keys(types).join(', ')}", + ctx.key(2) + }; + } + } + if (length > 3) { + const auto& arrayLength = toNumber(arrayMember(value, 3)); + if (!arrayLength) return CompileError { + "The length argument to \"array\" must be a number literal.", + ctx.key(3) + }; + N = static_cast<size_t>(*arrayLength); + } + return std::make_unique<Array>(ctx.key(), name, type::Array(type::Value), std::move(args)); + } + + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class ToString : public LambdaBase<ToString> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::String; }; + static std::vector<Params> signatures() { return {{ type::Value }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class ToNumber : public LambdaBase<ToNumber> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Number; }; + static std::vector<Params> signatures() { return {{ type::Value }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class ToBoolean : public LambdaBase<ToBoolean> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Boolean; }; + static std::vector<Params> signatures() { return {{ type::Value }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class ToRGBA : public LambdaBase<ToRGBA> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Array(type::Number, 4); }; + static std::vector<Params> signatures() { return {{ type::Color }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class ParseColor : public LambdaBase<ParseColor> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Color; }; + static std::vector<Params> signatures() { return {{ type::String }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class RGB : public LambdaBase<RGB> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Color; }; + static std::vector<Params> signatures() { return {{ type::Number, type::Number, type::Number }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class RGBA : public LambdaBase<RGBA> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Color; }; + static std::vector<Params> signatures() { return {{ type::Number, type::Number, type::Number, type::Number }}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Get : public LambdaBase<Get> { +public: + using LambdaBase::LambdaBase; + static type::ValueType type() { return type::Value; }; + static std::vector<Params> signatures() { + return {{type::String, NArgs { {type::Object}, 1 }}}; + }; + bool isFeatureConstant() const override; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Has : public LambdaBase<Has> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Boolean; }; + static std::vector<Params> signatures() { + return {{type::String, NArgs { {type::Object}, 1 }}}; + }; + bool isFeatureConstant() const override; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class At : public LambdaBase<At> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Typename("T"); }; + static std::vector<Params> signatures() { + return {{type::Number, type::Array(type::Typename("T"))}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Length : public LambdaBase<Length> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Number; }; + static std::vector<Params> signatures() { + return { + {type::Array(type::Typename("T"))}, + {type::String} + }; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Properties : public LambdaBase<Properties> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Object; }; + static std::vector<Params> signatures() { return {{}}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; + bool isFeatureConstant() const override; +}; + +class Id : public LambdaBase<Id> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::Value; }; + static std::vector<Params> signatures() { return {{}}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; + bool isFeatureConstant() const override; +}; + +class GeometryType : public LambdaBase<GeometryType> { +public: + using LambdaBase::LambdaBase; + static type::Type type() { return type::String; }; + static std::vector<Params> signatures() { return {{}}; }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; + bool isFeatureConstant() const override; +}; + +class Plus : public LambdaBase<Plus> { +public: + using LambdaBase::LambdaBase; + static type::NumberType type() { return type::Number; }; + static std::vector<Params> signatures() { + return {{NArgs {{type::Number}, {}}}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Times : public LambdaBase<Times> { +public: + using LambdaBase::LambdaBase; + static type::NumberType type() { return type::Number; }; + static std::vector<Params> signatures() { + return {{NArgs {{type::Number}, {}}}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Minus : public LambdaBase<Minus> { +public: + using LambdaBase::LambdaBase; + static type::NumberType type() { return type::Number; }; + static std::vector<Params> signatures() { + return {{type::Number, type::Number}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Divide : public LambdaBase<Divide> { +public: + using LambdaBase::LambdaBase; + static type::NumberType type() { return type::Number; }; + static std::vector<Params> signatures() { + return {{type::Number, type::Number}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Mod : public LambdaBase<Mod> { +public: + using LambdaBase::LambdaBase; + static type::NumberType type() { return type::Number; }; + static std::vector<Params> signatures() { + return {{type::Number, type::Number}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + +class Power : public LambdaBase<Power> { +public: + using LambdaBase::LambdaBase; + static type::NumberType type() { return type::Number; }; + static std::vector<Params> signatures() { + return {{type::Number, type::Number}}; + }; + EvaluationResult evaluate(const EvaluationParameters& params) const override; +}; + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp new file mode 100644 index 0000000000..9c4d55784a --- /dev/null +++ b/include/mbgl/style/expression/expression.hpp @@ -0,0 +1,257 @@ +#pragma once + +#include <array> +#include <vector> +#include <memory> +#include <mbgl/util/optional.hpp> +#include <mbgl/util/variant.hpp> +#include <mbgl/util/color.hpp> +#include <mbgl/style/expression/type.hpp> +#include <mbgl/style/expression/value.hpp> +#include <mbgl/style/expression/parsing_context.hpp> +#include <mbgl/style/conversion.hpp> + + +namespace mbgl { + +class GeometryTileFeature; + +namespace style { +namespace expression { + +struct EvaluationError { + std::string message; +}; + +struct EvaluationParameters { + float zoom; + const GeometryTileFeature& feature; +}; + +template<typename T> +class Result : private variant<EvaluationError, T> { +public: + using variant<EvaluationError, T>::variant; + + explicit operator bool () const { + return this->template is<T>(); + } + + // optional does some type trait magic for this one, so this might + // be problematic as is. + const T* operator->() const { + assert(this->template is<T>()); + return std::addressof(this->template get<T>()); + } + + T* operator->() { + assert(this->template is<T>()); + return std::addressof(this->template get<T>()); + } + + T& operator*() { + assert(this->template is<T>()); + return this->template get<T>(); + } + + const T& operator*() const { + assert(this->template is<T>()); + return this->template get<T>(); + } + + const EvaluationError& error() const { + assert(this->template is<EvaluationError>()); + return this->template get<EvaluationError>(); + } +}; + +using EvaluationResult = Result<Value>; + +struct CompileError { + std::string message; + std::string key; +}; + +class Expression { +public: + Expression(std::string key_, type::Type type_) : key(key_), type(type_) {} + virtual ~Expression() {}; + + virtual EvaluationResult evaluate(const EvaluationParameters& params) const = 0; + + /* + Evaluate this expression to a particular value type T. (See expression/value.hpp for + possible types T.) + */ + template <typename T> + Result<T> evaluate(const EvaluationParameters& params) { + const auto& result = evaluate(params); + if (!result) { return result.error(); } + return result->match( + [&] (const T& v) -> variant<EvaluationError, T> { return v; }, + [&] (const auto& v) -> variant<EvaluationError, T> { + return EvaluationError { + "Expected value to be of type " + toString(valueTypeToExpressionType<T>()) + + ", but found " + toString(typeOf(v)) + " instead." + }; + } + ); + } + + EvaluationResult evaluate(float z, const Feature& feature) const; + + type::Type getType() const { return type; } + std::string getKey() const { return key; } + + virtual bool isFeatureConstant() const { return true; } + virtual bool isZoomConstant() const { return true; } + +protected: + void setType(type::Type newType) { type = newType; } + +private: + std::string key; + type::Type type; +}; + + +using ParseResult = variant<CompileError, std::unique_ptr<Expression>>; +template <class V> +ParseResult parseExpression(const V& value, const ParsingContext& context); + +using TypecheckResult = variant<std::vector<CompileError>, std::unique_ptr<Expression>>; + +using namespace mbgl::style::conversion; + +class LiteralExpression : public Expression { +public: + LiteralExpression(std::string key_, Value value_) : Expression(key_, typeOf(value_)), value(value_) {} + + Value getValue() const { return value; } + + EvaluationResult evaluate(const EvaluationParameters&) const override { + return value; + } + + template <class V> + static ParseResult parse(const V& value, const ParsingContext& ctx) { + const Value& parsedValue = parseValue(value); + return std::make_unique<LiteralExpression>(ctx.key(), parsedValue); + } + +private: + template <class V> + static Value parseValue(const V& value) { + if (isUndefined(value)) return Null; + if (isObject(value)) { + std::unordered_map<std::string, Value> result; + eachMember(value, [&] (const std::string& k, const V& v) -> optional<conversion::Error> { + result.emplace(k, parseValue(v)); + return {}; + }); + return result; + } + if (isArray(value)) { + std::vector<Value> result; + const auto length = arrayLength(value); + for(std::size_t i = 0; i < length; i++) { + result.emplace_back(parseValue(arrayMember(value, i))); + } + return result; + } + + optional<mbgl::Value> v = toValue(value); + assert(v); + return convertValue(*v); + } + + Value value; +}; + +struct NArgs { + std::vector<type::Type> types; + optional<std::size_t> N; +}; + +class LambdaExpression : public Expression { +public: + using Params = std::vector<variant<type::Type, NArgs>>; + using Args = std::vector<std::unique_ptr<Expression>>; + + LambdaExpression(std::string key_, + std::string name_, + Args args_, + type::Type type_, + std::vector<Params> signatures_) : + Expression(key_, type_), + args(std::move(args_)), + signatures(signatures_), + name(name_) + {} + + std::string getName() const { return name; } + + virtual bool isFeatureConstant() const override { + bool isFC = true; + for (const auto& arg : args) { + isFC = isFC && arg->isFeatureConstant(); + } + return isFC; + } + + virtual bool isZoomConstant() const override { + bool isZC = true; + for (const auto& arg : args) { + isZC = isZC && arg->isZoomConstant(); + } + return isZC; + } + + virtual std::unique_ptr<Expression> applyInferredType(const type::Type& type, Args args) const = 0; + + friend TypecheckResult typecheck(const type::Type& expected, const std::unique_ptr<Expression>& e); + + template <class Expr, class V> + static ParseResult parse(const V& value, const ParsingContext& ctx) { + assert(isArray(value)); + auto length = arrayLength(value); + const std::string& name = *toString(arrayMember(value, 0)); + Args args; + for(size_t i = 1; i < length; i++) { + const auto& arg = arrayMember(value, i); + auto parsedArg = parseExpression(arg, ParsingContext(ctx, i, {})); + if (parsedArg.template is<std::unique_ptr<Expression>>()) { + args.push_back(std::move(parsedArg.template get<std::unique_ptr<Expression>>())); + } else { + return parsedArg.template get<CompileError>(); + } + } + return std::make_unique<Expr>(ctx.key(), name, std::move(args)); + } + +protected: + Args args; +private: + std::vector<Params> signatures; + std::string name; +}; + +template<class Expr> +class LambdaBase : public LambdaExpression { +public: + LambdaBase(const std::string& key, const std::string& name, Args args) : + LambdaExpression(key, name, std::move(args), Expr::type(), Expr::signatures()) + {} + LambdaBase(const std::string& key, const std::string& name, const type::Type& type, Args args) : + LambdaExpression(key, name, std::move(args), type, Expr::signatures()) + {} + + std::unique_ptr<Expression> applyInferredType(const type::Type& type, Args args) const override { + return std::make_unique<Expr>(getKey(), getName(), type, std::move(args)); + } +}; + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/parse.hpp b/include/mbgl/style/expression/parse.hpp new file mode 100644 index 0000000000..9f04cef848 --- /dev/null +++ b/include/mbgl/style/expression/parse.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include <memory> +#include <mbgl/style/expression/parsing_context.hpp> +#include <mbgl/style/expression/expression.hpp> +#include <mbgl/style/expression/definitions.hpp> +#include <mbgl/style/conversion.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +using namespace mbgl::style; + +template <class V> +std::string getJSType(const V& value) { + using namespace mbgl::style::conversion; + if (isUndefined(value)) { + return "undefined"; + } + if (isArray(value) || isObject(value)) { + return "object"; + } + optional<mbgl::Value> v = toValue(value); + assert(v); + return v->match( + [&] (std::string) { return "string"; }, + [&] (bool) { return "boolean"; }, + [&] (auto) { return "number"; } + ); +} + +using ParseResult = variant<CompileError, std::unique_ptr<Expression>>; + +template <class V> +ParseResult parseExpression(const V& value, const ParsingContext& context) +{ + using namespace mbgl::style::conversion; + + if (isArray(value)) { + const std::size_t length = arrayLength(value); + if (length == 0) { + CompileError error = { + "Expected an array with at least one element. If you wanted a literal array, use [\"literal\", []].", + context.key() + }; + return error; + } + + const optional<std::string>& op = toString(arrayMember(value, 0)); + if (!op) { + CompileError error = { + "Expression name must be a string, but found " + getJSType(arrayMember(value, 0)) + + " instead. If you wanted a literal array, use [\"literal\", [...]].", + context.key(0) + }; + return error; + } + + if (*op == "literal") { + if (length != 2) return CompileError { + "'literal' expression requires exactly one argument, but found " + std::to_string(length - 1) + " instead.", + context.key() + }; + return LiteralExpression::parse(arrayMember(value, 1), ParsingContext(context, {1}, {"literal"})); + } + + if (*op == "e") return MathConstant::e(context); + if (*op == "pi") return MathConstant::pi(context); + if (*op == "ln2") return MathConstant::ln2(context); + if (*op == "typeof") return LambdaExpression::parse<TypeOf>(value, context); + if (*op == "string") return LambdaExpression::parse<Assertion<std::string>>(value, context); + if (*op == "number") return LambdaExpression::parse<Assertion<float>>(value, context); + if (*op == "boolean") return LambdaExpression::parse<Assertion<bool>>(value, context); + if (*op == "array") return Array::parse(value, context); + if (*op == "to_string") return LambdaExpression::parse<ToString>(value, context); + if (*op == "to_number") return LambdaExpression::parse<ToNumber>(value, context); + if (*op == "to_boolean") return LambdaExpression::parse<ToBoolean>(value, context); + if (*op == "to_rgba") return LambdaExpression::parse<ToRGBA>(value, context); + if (*op == "parse_color") return LambdaExpression::parse<ParseColor>(value, context); + if (*op == "rgba") return LambdaExpression::parse<RGBA>(value, context); + if (*op == "rgb") return LambdaExpression::parse<RGB>(value, context); + if (*op == "get") return LambdaExpression::parse<Get>(value, context); + if (*op == "has") return LambdaExpression::parse<Has>(value, context); + if (*op == "at") return LambdaExpression::parse<At>(value, context); + if (*op == "length") return LambdaExpression::parse<Length>(value, context); + if (*op == "properties") return LambdaExpression::parse<Properties>(value, context); + if (*op == "id") return LambdaExpression::parse<Id>(value, context); + if (*op == "geometry_type") return LambdaExpression::parse<GeometryType>(value, context); + if (*op == "+") return LambdaExpression::parse<Plus>(value, context); + if (*op == "-") return LambdaExpression::parse<Minus>(value, context); + if (*op == "*") return LambdaExpression::parse<Times>(value, context); + if (*op == "/") return LambdaExpression::parse<Divide>(value, context); + if (*op == "^") return LambdaExpression::parse<Power>(value, context); + if (*op == "%") return LambdaExpression::parse<Mod>(value, context); + + + return CompileError { + std::string("Unknown expression \"") + *op + "\". If you wanted a literal array, use [\"literal\", [...]].", + context.key(0) + }; + } + + if (isObject(value)) { + return CompileError { + "Bare objects invalid. Use [\"literal\", {...}] instead.", + context.key() + }; + } + + return LiteralExpression::parse(value, context); +} + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/parsing_context.hpp b/include/mbgl/style/expression/parsing_context.hpp new file mode 100644 index 0000000000..7898da790d --- /dev/null +++ b/include/mbgl/style/expression/parsing_context.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include <string> +#include <vector> +#include <mbgl/util/optional.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +class ParsingContext { +public: + ParsingContext() {} + ParsingContext(ParsingContext previous, + optional<size_t> index, + optional<std::string> name) : + path(previous.path), + ancestors(previous.ancestors) + { + if (index) path.emplace_back(*index); + if (name) ancestors.emplace_back(*name); + } + + std::string key() const { + std::string result; + for(auto const& index : path) { result += "[" + std::to_string(index) + "]"; } + return result; + } + + std::string key(size_t lastIndex) const { + return key() + "[" + std::to_string(lastIndex) + "]"; + } + + std::vector<size_t> path; + std::vector<std::string> ancestors; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/type.hpp b/include/mbgl/style/expression/type.hpp new file mode 100644 index 0000000000..995440dfde --- /dev/null +++ b/include/mbgl/style/expression/type.hpp @@ -0,0 +1,116 @@ +#pragma once + +#include <unordered_map> +#include <vector> +#include <mbgl/util/optional.hpp> +#include <mbgl/util/variant.hpp> + +namespace mbgl { +namespace style { +namespace expression { +namespace type { + +template <class T> +std::string toString(const T& t); + +struct NullType { + constexpr NullType() {} + std::string getName() const { return "Null"; } +}; + +struct NumberType { + constexpr NumberType() {} + std::string getName() const { return "Number"; } +}; + +struct BooleanType { + constexpr BooleanType() {} + std::string getName() const { return "Boolean"; } +}; + +struct StringType { + constexpr StringType() {} + std::string getName() const { return "String"; } +}; + +struct ColorType { + constexpr ColorType() {} + std::string getName() const { return "Color"; } +}; + +struct ObjectType { + constexpr ObjectType() {} + std::string getName() const { return "Object"; } +}; + +struct ValueType { + constexpr ValueType() {} + std::string getName() const { return "Value"; } +}; + +constexpr NullType Null; +constexpr NumberType Number; +constexpr StringType String; +constexpr BooleanType Boolean; +constexpr ColorType Color; +constexpr ValueType Value; +constexpr ObjectType Object; + +class Typename { +public: + Typename(std::string name_) : name(name_) {} + std::string getName() const { return name; } +private: + std::string name; +}; + +struct Array; + +using Type = variant< + NullType, + NumberType, + BooleanType, + StringType, + ColorType, + ObjectType, + ValueType, + Typename, + mapbox::util::recursive_wrapper<Array>>; + +struct Array { + Array(Type itemType_) : itemType(itemType_) {} + Array(Type itemType_, std::size_t N_) : itemType(itemType_), N(N_) {} + Array(Type itemType_, optional<std::size_t> N_) : itemType(itemType_), N(N_) {} + std::string getName() const { + if (N) { + return "Array<" + toString(itemType) + ", " + std::to_string(*N) + ">"; + } else if (toString(itemType) == "Value") { + return "Array"; + } else { + return "Array<" + toString(itemType) + ">"; + } + } + + Type itemType; + optional<std::size_t> N; +}; + +template <class T> +std::string toString(const T& t) { return t.match([&] (const auto& t) { return t.getName(); }); } + +bool isGeneric(const Type& t); +Type resolveTypenamesIfPossible(const Type&, const std::unordered_map<std::string, Type>&); + +optional<std::string> matchType(const Type& expected, const Type& t); + +enum TypenameScope { expected, actual }; +optional<std::string> matchType(const Type& expected, + const Type& t, + std::unordered_map<std::string, Type>& typenames, + TypenameScope scope = TypenameScope::expected); + + +} // namespace type +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/type_check.hpp b/include/mbgl/style/expression/type_check.hpp new file mode 100644 index 0000000000..d30b7be002 --- /dev/null +++ b/include/mbgl/style/expression/type_check.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include <mbgl/util/variant.hpp> +#include <mbgl/style/expression/expression.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +TypecheckResult typecheck(const type::Type& expected, const std::unique_ptr<Expression>& e); + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/value.hpp b/include/mbgl/style/expression/value.hpp new file mode 100644 index 0000000000..ffc31856e0 --- /dev/null +++ b/include/mbgl/style/expression/value.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include <mbgl/util/color.hpp> +#include <mbgl/util/variant.hpp> +#include <mbgl/util/feature.hpp> +#include <mbgl/style/expression/type.hpp> + + +namespace mbgl { +namespace style { +namespace expression { + +struct Value; +using ValueBase = variant< + NullValue, + bool, + // TODO: should we break up the numeric types here like mbgl::Value does? + float, + std::string, + mbgl::Color, + mapbox::util::recursive_wrapper<std::vector<Value>>, + mapbox::util::recursive_wrapper<std::unordered_map<std::string, Value>>>; +struct Value : ValueBase { + using ValueBase::ValueBase; +}; + +constexpr NullValue Null = NullValue(); + +Value convertValue(const mbgl::Value&); +type::Type typeOf(const Value& value); +std::string stringify(const Value& value); + +/* + Returns a Type object representing the expression type that corresponds to + the value type T. (Specialized for primitives and specific array types in + the .cpp.) +*/ +template <typename T> +type::Type valueTypeToExpressionType(); + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/package.json b/package.json index 2f665e680a..7a9e5591b0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "license": "BSD-2-Clause", "dependencies": { - "nan": "^2.4.0", + "nan": "^2.6.2", "node-pre-gyp": "^0.6.36" }, "devDependencies": { diff --git a/platform/node/src/node_expression.cpp b/platform/node/src/node_expression.cpp new file mode 100644 index 0000000000..5392f6c041 --- /dev/null +++ b/platform/node/src/node_expression.cpp @@ -0,0 +1,201 @@ +#include "node_conversion.hpp" +#include "node_expression.hpp" + +#include <mbgl/style/expression/parse.hpp> +#include <mbgl/style/expression/type_check.hpp> +#include <mbgl/style/conversion/geojson.hpp> +#include <mbgl/util/geojson.hpp> +#include <nan.h> + +using namespace mbgl::style; +using namespace mbgl::style::expression; + +namespace node_mbgl { + +Nan::Persistent<v8::Function> NodeExpression::constructor; + +void NodeExpression::Init(v8::Local<v8::Object> target) { + v8::Local<v8::FunctionTemplate> tpl = Nan::New<v8::FunctionTemplate>(New); + tpl->SetClassName(Nan::New("Expression").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); // what is this doing? + + Nan::SetPrototypeMethod(tpl, "evaluate", Evaluate); + Nan::SetPrototypeMethod(tpl, "getType", GetType); + Nan::SetPrototypeMethod(tpl, "isFeatureConstant", IsFeatureConstant); + Nan::SetPrototypeMethod(tpl, "isZoomConstant", IsZoomConstant); + + Nan::SetMethod(tpl, "parse", Parse); + + constructor.Reset(tpl->GetFunction()); // what is this doing? + Nan::Set(target, Nan::New("Expression").ToLocalChecked(), tpl->GetFunction()); +} + +void NodeExpression::Parse(const Nan::FunctionCallbackInfo<v8::Value>& info) { + v8::Local<v8::Function> cons = Nan::New(constructor); + + if (info.Length() < 1 || info[0]->IsUndefined()) { + return Nan::ThrowTypeError("Requires a JSON style expression argument."); + } + + auto expr = info[0]; + + try { + std::vector<CompileError> errors; + auto parsed = parseExpression(expr, ParsingContext()); + if (parsed.template is<std::unique_ptr<Expression>>()) { + const auto& e = parsed.template get<std::unique_ptr<Expression>>(); + auto checked = typecheck(e->getType(), e); + if (checked.template is<std::unique_ptr<Expression>>()) { + auto nodeExpr = new NodeExpression(std::move(checked.template get<std::unique_ptr<Expression>>())); + const int argc = 0; + v8::Local<v8::Value> argv[0] = {}; + auto wrapped = Nan::NewInstance(cons, argc, argv).ToLocalChecked(); + nodeExpr->Wrap(wrapped); + info.GetReturnValue().Set(wrapped); + return; + } else { + const auto& typeErrors = checked.template get<std::vector<CompileError>>(); + errors.insert(errors.end(), typeErrors.begin(), typeErrors.end()); + } + } else { + errors.emplace_back(parsed.template get<CompileError>()); + } + + v8::Local<v8::Array> result = Nan::New<v8::Array>(); + for (std::size_t i = 0; i < errors.size(); i++) { + const auto& error = errors[i]; + v8::Local<v8::Object> err = Nan::New<v8::Object>(); + Nan::Set(err, + Nan::New("key").ToLocalChecked(), + Nan::New(error.key.c_str()).ToLocalChecked()); + Nan::Set(err, + Nan::New("error").ToLocalChecked(), + Nan::New(error.message.c_str()).ToLocalChecked()); + Nan::Set(result, Nan::New((uint32_t)i), err); + } + info.GetReturnValue().Set(result); + } catch(std::exception &ex) { + return Nan::ThrowError(ex.what()); + } +} + +void NodeExpression::New(const Nan::FunctionCallbackInfo<v8::Value>& info) { + if (!info.IsConstructCall()) { + return Nan::ThrowTypeError("Use the new operator to create new Expression objects"); + } + + info.GetReturnValue().Set(info.This()); +} + +struct ToValue { + v8::Local<v8::Value> operator()(mbgl::NullValue) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::Null()); + } + + v8::Local<v8::Value> operator()(bool t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t)); + } + + v8::Local<v8::Value> operator()(float t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t)); + } + + v8::Local<v8::Value> operator()(const std::string& t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t).ToLocalChecked()); + } + + v8::Local<v8::Value> operator()(const std::vector<Value>& array) { + Nan::EscapableHandleScope scope; + v8::Local<v8::Array> result = Nan::New<v8::Array>(); + for (unsigned int i = 0; i < array.size(); i++) { + result->Set(i, toJS(array[i])); + } + return scope.Escape(result); + } + + v8::Local<v8::Value> operator()(const mbgl::Color& color) { + return operator()(std::vector<Value> { color.r, color.g, color.b, color.a }); + } + + v8::Local<v8::Value> operator()(const std::unordered_map<std::string, Value>& map) { + Nan::EscapableHandleScope scope; + v8::Local<v8::Object> result = Nan::New<v8::Object>(); + for (const auto& entry : map) { + Nan::Set(result, Nan::New(entry.first).ToLocalChecked(), toJS(entry.second)); + } + + return scope.Escape(result); + } +}; + +v8::Local<v8::Value> toJS(const Value& value) { + return Value::visit(value, ToValue()); +} + +void NodeExpression::Evaluate(const Nan::FunctionCallbackInfo<v8::Value>& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap<NodeExpression>(info.Holder()); + const auto& expression = nodeExpr->expression; + + if (info.Length() < 2) { + return Nan::ThrowTypeError("Requires arguments zoom and feature arguments."); + } + + float zoom = info[0]->NumberValue(); + + // Pending https://github.com/mapbox/mapbox-gl-native/issues/5623, + // stringify the geojson feature in order to use convert<GeoJSON, string>() + Nan::JSON NanJSON; + Nan::MaybeLocal<v8::String> geojsonString = NanJSON.Stringify(Nan::To<v8::Object>(info[1]).ToLocalChecked()); + if (geojsonString.IsEmpty()) { + return Nan::ThrowTypeError("couldn't stringify JSON"); + } + conversion::Error conversionError; + mbgl::optional<mbgl::GeoJSON> geoJSON = conversion::convert<mbgl::GeoJSON, std::string>(*Nan::Utf8String(geojsonString.ToLocalChecked()), conversionError); + if (!geoJSON) { + Nan::ThrowTypeError(conversionError.message.c_str()); + return; + } + + try { + mapbox::geojson::feature feature = geoJSON->get<mapbox::geojson::feature>(); + auto result = expression->evaluate(zoom, feature); + if (result) { + info.GetReturnValue().Set(toJS(*result)); + } else { + v8::Local<v8::Object> res = Nan::New<v8::Object>(); + Nan::Set(res, + Nan::New("error").ToLocalChecked(), + Nan::New(result.error().message.c_str()).ToLocalChecked()); + info.GetReturnValue().Set(res); + } + } catch(std::exception &ex) { + return Nan::ThrowTypeError(ex.what()); + } +} + +void NodeExpression::GetType(const Nan::FunctionCallbackInfo<v8::Value>& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap<NodeExpression>(info.Holder()); + const auto& expression = nodeExpr->expression; + + const auto& type = expression->getType(); + const auto& name = type.match([&] (const auto& t) { return t.getName(); }); + info.GetReturnValue().Set(Nan::New(name.c_str()).ToLocalChecked()); +} + +void NodeExpression::IsFeatureConstant(const Nan::FunctionCallbackInfo<v8::Value>& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap<NodeExpression>(info.Holder()); + const auto& expression = nodeExpr->expression; + info.GetReturnValue().Set(Nan::New(expression->isFeatureConstant())); +} + +void NodeExpression::IsZoomConstant(const Nan::FunctionCallbackInfo<v8::Value>& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap<NodeExpression>(info.Holder()); + const auto& expression = nodeExpr->expression; + info.GetReturnValue().Set(Nan::New(expression->isZoomConstant())); +} + +} // namespace node_mbgl diff --git a/platform/node/src/node_expression.hpp b/platform/node/src/node_expression.hpp new file mode 100644 index 0000000000..e977b58288 --- /dev/null +++ b/platform/node/src/node_expression.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include <exception> +#include <memory> +#include <mbgl/style/conversion.hpp> +#include <mbgl/style/expression/expression.hpp> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wshadow" +#include <nan.h> +#pragma GCC diagnostic pop + +using namespace mbgl::style::expression; + +namespace node_mbgl { + +v8::Local<v8::Value> toJS(const Value&); + +class NodeExpression : public Nan::ObjectWrap { +public: + static void Init(v8::Local<v8::Object>); + +private: + NodeExpression(std::unique_ptr<Expression> expression_) : + expression(std::move(expression_)) + {}; + + static void New(const Nan::FunctionCallbackInfo<v8::Value>&); + static void Parse(const Nan::FunctionCallbackInfo<v8::Value>&); + static void Evaluate(const Nan::FunctionCallbackInfo<v8::Value>&); + static void GetType(const Nan::FunctionCallbackInfo<v8::Value>&); + static void IsFeatureConstant(const Nan::FunctionCallbackInfo<v8::Value>&); + static void IsZoomConstant(const Nan::FunctionCallbackInfo<v8::Value>&); + static Nan::Persistent<v8::Function> constructor; + + std::unique_ptr<Expression> expression; +}; + +} // namespace node_mbgl diff --git a/platform/node/src/node_mapbox_gl_native.cpp b/platform/node/src/node_mapbox_gl_native.cpp index cdcc982220..96e96e4298 100644 --- a/platform/node/src/node_mapbox_gl_native.cpp +++ b/platform/node/src/node_mapbox_gl_native.cpp @@ -10,6 +10,7 @@ #include "node_map.hpp" #include "node_logging.hpp" #include "node_request.hpp" +#include "node_expression.hpp" void RegisterModule(v8::Local<v8::Object> target, v8::Local<v8::Object> module) { // This has the effect of: @@ -20,6 +21,7 @@ void RegisterModule(v8::Local<v8::Object> target, v8::Local<v8::Object> module) node_mbgl::NodeMap::Init(target); node_mbgl::NodeRequest::Init(); + node_mbgl::NodeExpression::Init(target); // Exports Resource constants. v8::Local<v8::Object> resource = Nan::New<v8::Object>(); diff --git a/platform/node/test/expression.test.js b/platform/node/test/expression.test.js new file mode 100644 index 0000000000..842faa7350 --- /dev/null +++ b/platform/node/test/expression.test.js @@ -0,0 +1,52 @@ +'use strict'; + +var suite = require('../../../mapbox-gl-js/test/integration').expression; +var mbgl = require('../index'); + +var tests; + +if (process.argv[1] === __filename && process.argv.length > 2) { + tests = process.argv.slice(2); +} + +suite.run('native', {tests: tests}, (fixture) => { + const compileResult = {}; + const testResult = { + compileResult + }; + + const expression = mbgl.Expression.parse(fixture.expression); + + if (expression instanceof mbgl.Expression) { + compileResult.result = 'success'; + compileResult.isFeatureConstant = expression.isFeatureConstant(); + compileResult.isZoomConstant = expression.isZoomConstant(); + compileResult.type = expression.getType(); + if (fixture.evaluate) { + const evaluateResults = []; + for (const input of fixture.evaluate) { + const zoom = typeof input[0].zoom === 'number' ? + input[0].zoom : -1; + + const feature = Object.assign({ + type: 'Feature', + properties: {}, + geometry: { type: 'Point', coordinates: [0, 0] } + }, input[1]) + + const output = expression.evaluate(zoom, feature); + evaluateResults.push(output); + } + + if (evaluateResults.length) { + testResult.evaluateResults = evaluateResults; + } + } + } else { + compileResult.result = 'error'; + compileResult.errors = expression; + } + + return testResult; +}); + diff --git a/src/mbgl/style/expression/definitions.cpp b/src/mbgl/style/expression/definitions.cpp new file mode 100644 index 0000000000..9fb82767e7 --- /dev/null +++ b/src/mbgl/style/expression/definitions.cpp @@ -0,0 +1,355 @@ +#include <mbgl/style/expression/definitions.hpp> +#include <mbgl/tile/geometry_tile_data.hpp> + +namespace mbgl { +namespace style { +namespace expression { + + +Result<std::vector<Value>> evaluateArgs(const EvaluationParameters& params, + const LambdaExpression::Args& args) +{ + std::vector<Value> argValues; + for(const auto& arg : args) { + auto argValue = arg->evaluate(params); + if (!argValue) return argValue.error(); + argValues.emplace_back(*argValue); + } + return argValues; +} + +template<typename T, typename EvalFunc> +EvaluationResult evaluateFromArgs(const EvaluationParameters& params, + const std::unique_ptr<Expression>& a0, + EvalFunc evaluate) +{ + const auto& a0value = a0->evaluate<T>(params); + if (!a0value) return a0value.error(); + return evaluate(*a0value); +} + +// TODO: get this working to replace all the overloads below + +//template <typename ...Ts> +//struct restargs { using expression = std::unique_ptr<Expression>; }; +// +//template <typename T, typename ...Ts, typename Eval> +//EvaluationResult evaluateFromArgs(const EvaluationParameters& params, +// const std::unique_ptr<Expression>& a0, +// const typename restargs<Ts...>::expression& args, +// Eval evaluate) +//{ +// const auto& a0value = a0->evaluate<T>(params); +// if (a0value.template is<EvaluationError>()) { +// return a0value.template get<EvaluationError>(); +// } +// return evaluateFromArgs<Ts...>( +// params, +// std::forward<const typename restargs<Ts...>::expression&>(args), +// [&] (Ts... restValues) { +// return evaluate(a0value.template get<T>(), std::forward<Ts...>(restValues)...); +// } +// ); +//} + +template<typename T, typename U, typename EvalFunc> +EvaluationResult evaluateFromArgs(const EvaluationParameters& params, + const std::unique_ptr<Expression>& a0, + const std::unique_ptr<Expression>& a1, + EvalFunc evaluate) +{ + const auto& a0value = a0->evaluate<T>(params); + if (!a0value) return a0value.error(); + const auto& a1value = a1->evaluate<U>(params); + if (!a1value) return a1value.error(); + return evaluate(*a0value, *a1value); +} + +template<typename T0, typename T1, typename T2, typename EvalFunc> +EvaluationResult evaluateFromArgs(const EvaluationParameters& params, + const std::unique_ptr<Expression>& a0, + const std::unique_ptr<Expression>& a1, + const std::unique_ptr<Expression>& a2, + EvalFunc evaluate) +{ + const auto& a0value = a0->evaluate<T0>(params); + if (!a0value) return a0value.error(); + const auto& a1value = a1->evaluate<T1>(params); + if (!a1value) return a1value.error(); + const auto& a2value = a2->evaluate<T2>(params); + if (!a2value) return a2value.error(); + return evaluate(*a0value, *a1value, *a2value); +} + +template<typename T0, typename T1, typename T2, typename T3, typename EvalFunc> +EvaluationResult evaluateFromArgs(const EvaluationParameters& params, + const std::unique_ptr<Expression>& a0, + const std::unique_ptr<Expression>& a1, + const std::unique_ptr<Expression>& a2, + const std::unique_ptr<Expression>& a3, + EvalFunc evaluate) +{ + const auto& a0value = a0->evaluate<T0>(params); + if (!a0value) return a0value.error(); + const auto& a1value = a1->evaluate<T1>(params); + if (!a1value) return a1value.error(); + const auto& a2value = a2->evaluate<T2>(params); + if (!a2value) return a2value.error(); + const auto& a3value = a3->evaluate<T3>(params); + if (!a3value) return a3value.error(); + return evaluate(*a0value, *a1value, *a2value, *a3value); +} + + +EvaluationResult TypeOf::evaluate(const EvaluationParameters& params) const { + const auto& result = args[0]->evaluate(params); + if (!result) return result.error(); + return toString(typeOf(*result)); +} + +EvaluationResult Array::evaluate(const EvaluationParameters& params) const { + const auto& result = args[0]->evaluate(params); + if (!result) { return result.error(); } + const Value& v = *result; + const auto& expected = getType().get<type::Array>(); + const auto& actual = typeOf(v); + if (actual.is<type::Array>()) { + const auto& arrayType = actual.get<type::Array>(); + bool match = (!expected.N || expected.N == arrayType.N); + if (expected.itemType.is<type::ValueType>()) { + match = match && (arrayType.itemType.is<type::StringType>() || + arrayType.itemType.is<type::NumberType>() || + arrayType.itemType.is<type::BooleanType>()); + } else { + match = match && (toString(expected.itemType) == toString(arrayType.itemType)); + } + + if (match) return EvaluationResult(v); + } + + return EvaluationError { + "Expected value to be of type " + toString(getType()) + + ", but found " + toString(actual) + " instead." + }; +} + +EvaluationResult ToString::evaluate(const EvaluationParameters& params) const { + const auto& result = args[0]->evaluate(params); + if (!result) return result.error(); + return result->match( + [&] (const std::string& s) -> EvaluationResult { return s; }, + [&] (float v) -> EvaluationResult { return stringify(v); }, + [&] (bool v) -> EvaluationResult { return stringify(v); }, + [&] (const NullValue& v) -> EvaluationResult { return stringify(v); }, + [&] (const auto& v) -> EvaluationResult { + return EvaluationError { + "Expected a primitive value in [\"string\", ...], but found " + toString(typeOf(v)) + " instead." + }; + } + ); +} + +EvaluationResult ToNumber::evaluate(const EvaluationParameters& params) const { + const auto& result = args[0]->evaluate(params); + if (!result) return result.error(); + if (result->is<float>()) { return result->get<float>(); } + if (result->is<std::string>()) { + const std::string& s = result->get<std::string>(); + try { + return std::stof(s); + } catch(std::exception) { + } + } + return EvaluationError { + "Could not convert " + stringify(*result) + " to number." + }; +} + +EvaluationResult ToBoolean::evaluate(const EvaluationParameters& params) const { + const auto& result = args[0]->evaluate(params); + if (!result) return result.error(); + return result->match( + [&] (float f) { return (bool)f; }, + [&] (const std::string& s) { return s.length() > 0; }, + [&] (bool b) { return b; }, + [&] (const NullValue&) { return false; }, + [&] (const auto&) { return true; } + ); +} + +EvaluationResult ToRGBA::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<mbgl::Color>(params, args[0], [&] (const mbgl::Color& color) { + return std::vector<Value> { color.r, color.g, color.b, color.a }; + }); +} + +EvaluationResult ParseColor::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<std::string>(params, args[0], [&] (const std::string& colorString) -> EvaluationResult { + const auto& result = mbgl::Color::parse(colorString); + if (result) return EvaluationResult(*result); + return EvaluationError { + "Could not parse color from value '" + colorString + "'" + }; + }); +} + +EvaluationResult RGB::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, float, float>(params, args[0], args[1], args[2], + [&] (float r, float g, float b) -> EvaluationResult { + return mbgl::Color(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f); + } + ); +} + +EvaluationResult RGBA::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, float, float, float>(params, args[0], args[1], args[2], args[3], + [&] (float r, float g, float b, float a) -> EvaluationResult { + return mbgl::Color(r / 255.0f, g / 255.0f, b / 255.0f, a); + } + ); +} + +bool Get::isFeatureConstant() const { + return args.size() == 1 ? false : LambdaExpression::isFeatureConstant(); +} +EvaluationResult Get::evaluate(const EvaluationParameters& params) const { + if (args.size() == 1) { + return evaluateFromArgs<std::string>(params, args[0], [&] (const std::string& key) -> EvaluationResult { + const auto& value = params.feature.getValue(key); + if (!value) return EvaluationError { "Property '" + key + "' not found in feature.properties" }; + return convertValue(*value); + }); + } else { + return evaluateFromArgs<std::string, std::unordered_map<std::string, Value>>( + params, + args[0], + args[1], + [&] (const std::string& key, const std::unordered_map<std::string, Value>& object) -> EvaluationResult { + if (object.find(key) == object.end()) return EvaluationError { "Property '" + key + "' not found in object" }; + return object.at(key); + } + ); + } +} + +bool Has::isFeatureConstant() const { + return args.size() == 1 ? false : LambdaExpression::isFeatureConstant(); +} +EvaluationResult Has::evaluate(const EvaluationParameters& params) const { + if (args.size() == 1) { + return evaluateFromArgs<std::string>(params, args[0], [&] (const std::string& key) -> EvaluationResult { + const auto& value = params.feature.getValue(key); + return value ? true : false; + }); + } else { + return evaluateFromArgs<std::string, std::unordered_map<std::string, Value>>( + params, + args[0], + args[1], + [&] (const std::string& key, const std::unordered_map<std::string, Value>& object) -> EvaluationResult { + return object.find(key) != object.end(); + } + ); + } +} + +EvaluationResult At::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, std::vector<Value>>( + params, + args[0], + args[1], + [&] (float index, const std::vector<Value>& arr) -> EvaluationResult { + const size_t i = index; + if (index > arr.size()) { + return EvaluationError { + "Array index out of bounds: " + std::to_string(i) + " >= " + std::to_string(arr.size()) + }; + } + return arr[i]; + } + ); +} + +EvaluationResult Length::evaluate(const EvaluationParameters& params) const { + const auto& result = args[0]->evaluate(params); + if (!result) return result.error(); + return (float) result->match( + [&] (const std::string& s) { return s.size(); }, + [&] (const std::vector<Value>& v) { return v.size(); }, + [&] (const auto&) { assert(false); return -1; } + ); +} + +bool Properties::isFeatureConstant() const { return false; } +EvaluationResult Properties::evaluate(const EvaluationParameters& params) const { + return convertValue(params.feature.getProperties()); +} + +bool Id::isFeatureConstant() const { return false; } +EvaluationResult Id::evaluate(const EvaluationParameters& params) const { + const auto& id = params.feature.getID(); + if (!id) return EvaluationError { "Property 'id' not found in feature" }; + return id->match( + [&](const std::string& s) -> EvaluationResult { return s; }, + [&](const auto& n) -> EvaluationResult { return *numericValue<float>(n); } + ); +} + +bool GeometryType::isFeatureConstant() const { return false; } +EvaluationResult GeometryType::evaluate(const EvaluationParameters& params) const { + switch(params.feature.getType()) { + case FeatureType::Unknown: return std::string("Unknown"); + case FeatureType::LineString: return std::string("LineString"); + case FeatureType::Point: return std::string("Point"); + case FeatureType::Polygon: return std::string("Polygon"); + } +} + +EvaluationResult Plus::evaluate(const EvaluationParameters& params) const { + const auto& evaluated = evaluateArgs(params, args); + if (!evaluated) return evaluated.error(); + float sum = 0.0f; + for (const Value& v : *evaluated) { + sum += v.get<float>(); + } + return sum; +} + +EvaluationResult Times::evaluate(const EvaluationParameters& params) const { + const auto& evaluated = evaluateArgs(params, args); + if (!evaluated) return evaluated.error(); + float product = 1.0f; + for (const Value& v : *evaluated) { + product *= v.get<float>(); + } + return product; +} + +EvaluationResult Minus::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, float>(params, args[0], args[1], [&] (float a, float b) { + return a - b; + }); +} + +EvaluationResult Divide::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, float>(params, args[0], args[1], [&] (float a, float b) { + return a / b; + }); +} + +EvaluationResult Mod::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, float>(params, args[0], args[1], [&] (float a, float b) { + return (float) fmod(a, b); + }); +} + +EvaluationResult Power::evaluate(const EvaluationParameters& params) const { + return evaluateFromArgs<float, float>(params, args[0], args[1], [&] (float a, float b) { + return (float) pow(a, b); + }); +} + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/type.cpp b/src/mbgl/style/expression/type.cpp new file mode 100644 index 0000000000..2e4a6eb3f5 --- /dev/null +++ b/src/mbgl/style/expression/type.cpp @@ -0,0 +1,120 @@ +#include <mbgl/style/expression/type.hpp> + +namespace mbgl { +namespace style { +namespace expression { +namespace type { + +bool isGeneric(const Type& t) { + return t.match( + [&](const Array& arr) { return isGeneric(arr.itemType); }, + [&](const Typename&) { return true; }, + [&](const auto&) { return false; } + ); +} + +Type resolveTypenamesIfPossible(const Type& t, const std::unordered_map<std::string, Type>& map) { + if (!isGeneric(t)) return t; + return t.match( + [&](const Typename& t) -> Type { + if (map.find(t.getName()) == map.end()) { + return t; + } else { + return map.at(t.getName()); + } + }, + [&](const Array& arr) { + return Array(resolveTypenamesIfPossible(arr.itemType, map), arr.N); + }, + [&](const auto&) { return t; } + ); +} + +inline std::string errorMessage(const Type& expected, const Type& t) { + return {"Expected " + toString(expected) + " but found " + toString(t) + " instead."}; +} + +optional<std::string> matchType(const Type& expected, const Type& t) { + // TODO this is wasteful; just make typenames param optional. + std::unordered_map<std::string, Type> typenames; + return matchType(expected, t, typenames); +} + +optional<std::string> matchType(const Type& expected, + const Type& t, + std::unordered_map<std::string, Type>& typenames, + TypenameScope scope) { + if (expected.is<Typename>()) { + const auto& name = expected.get<Typename>().getName(); + if ( + scope == TypenameScope::expected && + !isGeneric(t) && + !t.is<NullType>() && + typenames.find(name) == typenames.end() + ) { + typenames.emplace(name, t); + } + return {}; + } + + if (t.is<Typename>()) { + const auto& name = t.get<Typename>().getName(); + if ( + scope == TypenameScope::actual && + !isGeneric(expected) && + !expected.is<NullType>() && + typenames.find(name) == typenames.end() + ) { + typenames.emplace(name, expected); + } + return {}; + } + + if (t.is<NullType>()) return {}; + + return expected.match( + [&] (const Array& expectedArray) -> optional<std::string> { + if (!t.is<Array>()) { return {errorMessage(expected, t)}; } + const auto& tArr = t.get<Array>(); + const auto err = matchType(expectedArray.itemType, tArr.itemType, typenames); + if (err) return { errorMessage(expected, t) + " (" + (*err) + ")" }; + if (expectedArray.N && expectedArray.N != tArr.N) return { errorMessage(expected, t) }; + return {}; + }, + [&] (const ValueType&) -> optional<std::string> { + if (t.is<ValueType>()) return {}; + + const Type members[] = { + Null, + Boolean, + Number, + String, + Object, + Array(Value) + }; + + for (const auto& member : members) { + std::unordered_map<std::string, Type> memberTypenames; + const auto err = matchType(member, t, memberTypenames, scope); + if (!err) { + typenames.insert(memberTypenames.begin(), memberTypenames.end()); + return {}; + } + } + return { errorMessage(expected, t) }; + }, + [&] (const auto&) -> optional<std::string> { + // TODO silly to stringify for this; implement == / != operators instead. + if (toString(expected) != toString(t)) { + return { errorMessage(expected, t) }; + } + return {}; + } + ); +} + + +} // namespace type +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/type_check.cpp b/src/mbgl/style/expression/type_check.cpp new file mode 100644 index 0000000000..37bb3db823 --- /dev/null +++ b/src/mbgl/style/expression/type_check.cpp @@ -0,0 +1,121 @@ +#include <mbgl/style/expression/type_check.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +using namespace mbgl::style::expression::type; + +TypecheckResult typecheck(const Type& expected, const std::unique_ptr<Expression>& e) { + if (LiteralExpression* literal = dynamic_cast<LiteralExpression*>(e.get())) { + const auto& error = matchType(expected, literal->getType()); + if (error) return std::vector<CompileError> {{ *error, literal->getKey() }}; + return std::make_unique<LiteralExpression>(literal->getKey(), literal->getValue()); + } else if (LambdaExpression* lambda = dynamic_cast<LambdaExpression*>(e.get())) { + // Check if the expected type matches the expression's output type; + // If expression's output type is generic and the expected type is concrete, + // pick up a typename binding + std::unordered_map<std::string, Type> initialTypenames; + const auto& resultMatchError = matchType(expected, lambda->getType(), initialTypenames, TypenameScope::actual); + if (resultMatchError) return std::vector<CompileError> {{ *resultMatchError, lambda->getKey() }}; + + std::vector<CompileError> errors; + for (const auto& params : lambda->signatures) { + std::unordered_map<std::string, Type> typenames(initialTypenames); + errors.clear(); + // "Unroll" NArgs if present in the parameter list: + // argCount = nargType.type.length * n + nonNargParameterCount + // where n is the number of times the NArgs sequence must be + // repeated. + std::vector<Type> expandedParams; + for (const auto& param : params) { + param.match( + [&](const NArgs& nargs) { + size_t count = ceil( + float(lambda->args.size() - (params.size() - 1)) / + nargs.types.size() + ); + if (nargs.N && *(nargs.N) < count) count = *(nargs.N); + while(count-- > 0) { + for(const auto& type : nargs.types) { + expandedParams.emplace_back(type); + } + } + }, + [&](const Type& t) { + expandedParams.emplace_back(t); + } + ); + } + + if (expandedParams.size() != lambda->args.size()) { + errors.emplace_back(CompileError { + "Expected " + std::to_string(expandedParams.size()) + + " arguments, but found " + std::to_string(lambda->args.size()) + " instead.", + lambda->getKey() + }); + continue; + } + + // Iterate through arguments to: + // - match parameter type vs argument type, checking argument's result type only (don't recursively typecheck subexpressions at this stage) + // - collect typename mappings when ^ succeeds or type errors when it fails + for (size_t i = 0; i < lambda->args.size(); i++) { + const auto& param = expandedParams.at(i); + const auto& arg = lambda->args.at(i); +// if (arg instanceof Reference) { +// arg = scope.get(arg.name); +// } + + const auto& error = matchType( + resolveTypenamesIfPossible(param, typenames), + arg->getType(), + typenames + ); + if (error) { + errors.emplace_back(CompileError{ *error, arg->getKey() }); + } + } + + const auto& resultType = resolveTypenamesIfPossible(expected, typenames); + + if (isGeneric(resultType)) { + errors.emplace_back(CompileError { + "Could not resolve " + toString(lambda->getType()) + ". This expression must be wrapped in a type conversion, e.g. [\"string\", ${stringifyExpression(e)}].", + lambda->getKey() + }); + }; + + // If we already have errors, return early so we don't get duplicates when + // we typecheck against the resolved argument types + if (errors.size()) continue; + + std::vector<Type> resolvedParams; + std::vector<std::unique_ptr<Expression>> checkedArgs; + for (std::size_t i = 0; i < expandedParams.size(); i++) { + const auto& t = expandedParams.at(i); + const auto& expectedArgType = resolveTypenamesIfPossible(t, typenames); + auto checked = typecheck(expectedArgType, lambda->args.at(i)); + if (checked.is<std::vector<CompileError>>()) { + const auto& errs = checked.get<std::vector<CompileError>>(); + errors.insert(errors.end(), errs.begin(), errs.end()); + } else if (checked.is<std::unique_ptr<Expression>>()) { + resolvedParams.emplace_back(expectedArgType); + checkedArgs.emplace_back(std::move(checked.get<std::unique_ptr<Expression>>())); + } + } + + if (errors.size() == 0) { + return lambda->applyInferredType(resultType, std::move(checkedArgs)); + } + } + + return errors; + } + + assert(false); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/value.cpp b/src/mbgl/style/expression/value.cpp new file mode 100644 index 0000000000..4e670ec5dd --- /dev/null +++ b/src/mbgl/style/expression/value.cpp @@ -0,0 +1,108 @@ +#include <mbgl/style/expression/value.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +struct ConvertValue { +// null_value_t, bool, uint64_t, int64_t, double, std::string, +// mapbox::util::recursive_wrapper<std::vector<value>>, +// mapbox::util::recursive_wrapper<std::unordered_map<std::string, value>> + Value operator()(const std::vector<mbgl::Value>& v) { + std::vector<Value> result; + for(const auto& item : v) { + result.emplace_back(convertValue(item)); + } + return result; + } + + Value operator()(const std::unordered_map<std::string, mbgl::Value>& v) { + std::unordered_map<std::string, Value> result; + for(const auto& entry : v) { + result.emplace(entry.first, convertValue(entry.second)); + } + return result; + } + + Value operator()(const std::string& s) { return s; } + Value operator()(const bool& b) { return b; } + Value operator()(const mbgl::NullValue) { return Null; } + + template <typename T> + Value operator()(const T& v) { return *numericValue<float>(v); } +}; + +Value convertValue(const mbgl::Value& value) { + return mbgl::Value::visit(value, ConvertValue()); +} + +type::Type typeOf(const Value& value) { + return value.match( + [&](bool) -> type::Type { return type::Boolean; }, + [&](float) -> type::Type { return type::Number; }, + [&](const std::string&) -> type::Type { return type::String; }, + [&](const mbgl::Color&) -> type::Type { return type::Color; }, + [&](const NullValue&) -> type::Type { return type::Null; }, + [&](const std::unordered_map<std::string, Value>&) -> type::Type { return type::Object; }, + [&](const std::vector<Value>& arr) -> type::Type { + optional<type::Type> itemType; + for (const auto& item : arr) { + const auto& t = typeOf(item); + const auto& tname = type::toString(t); + if (!itemType) { + itemType = {t}; + } else if (type::toString(*itemType) == tname) { + continue; + } else { + itemType = {type::Value}; + break; + } + } + + if (!itemType) { itemType = {type::Value}; } + + return type::Array(*itemType, arr.size()); + } + ); +} + +std::string stringify(const Value& value) { + return value.match( + [&] (const NullValue&) { return std::string("null"); }, + [&] (bool b) { return std::string(b ? "true" : "false"); }, + [&] (float f) { return ceilf(f) == f ? std::to_string((int)f) : std::to_string(f); }, + [&] (const std::string& s) { return "\"" + s + "\""; }, + [&] (const mbgl::Color& c) { return c.stringify(); }, + [&] (const std::vector<Value>& arr) { + std::string result = "["; + for(const auto& item : arr) { + if (result.size() > 1) result += ","; + result += stringify(item); + } + return result + "]"; + }, + [&] (const std::unordered_map<std::string, Value>& obj) { + std::string result = "{"; + for(const auto& entry : obj) { + if (result.size() > 1) result += ","; + result += stringify(entry.first) + ":" + stringify(entry.second); + } + return result + "}"; + } + ); +} + +template <> type::Type valueTypeToExpressionType<Value>() { return type::Value; } +template <> type::Type valueTypeToExpressionType<NullValue>() { return type::Null; } +template <> type::Type valueTypeToExpressionType<bool>() { return type::Boolean; } +template <> type::Type valueTypeToExpressionType<float>() { return type::Number; } +template <> type::Type valueTypeToExpressionType<std::string>() { return type::String; } +template <> type::Type valueTypeToExpressionType<mbgl::Color>() { return type::Color; } +template <> type::Type valueTypeToExpressionType<std::unordered_map<std::string, Value>>() { return type::Object; } +template <> type::Type valueTypeToExpressionType<std::array<float, 2>>() { return type::Array(type::Number, 2); } +template <> type::Type valueTypeToExpressionType<std::array<float, 4>>() { return type::Array(type::Number, 4); } +template <> type::Type valueTypeToExpressionType<std::vector<Value>>() { return type::Array(type::Value); } + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/function/expression.cpp b/src/mbgl/style/function/expression.cpp new file mode 100644 index 0000000000..e6760eb345 --- /dev/null +++ b/src/mbgl/style/function/expression.cpp @@ -0,0 +1,50 @@ +#include <mbgl/style/expression/expression.hpp> +#include <mbgl/tile/geometry_tile_data.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +class GeoJSONFeature : public GeometryTileFeature { +public: + const Feature& feature; + + GeoJSONFeature(const Feature& feature_) + : feature(feature_) { + } + + FeatureType getType() const override { + return apply_visitor(ToFeatureType(), feature.geometry); + } + + PropertyMap getProperties() const override { + return feature.properties; + } + + optional<FeatureIdentifier> getID() const override { + return feature.id; + } + + GeometryCollection getGeometries() const override { + return {}; + } + + optional<mbgl::Value> getValue(const std::string& key) const override { + auto it = feature.properties.find(key); + if (it != feature.properties.end()) { + return optional<mbgl::Value>(it->second); + } + return optional<mbgl::Value>(); + } +}; + + + +EvaluationResult Expression::evaluate(float z, const Feature& feature) const { + std::unique_ptr<const GeometryTileFeature> f = std::make_unique<const GeoJSONFeature>(feature); + return this->evaluate(EvaluationParameters {z, *f}); +} + +} // namespace expression +} // namespace style +} // namespace mbgl |