From f648cfeef6544755fdb10c3cf8847e878d70e0ff Mon Sep 17 00:00:00 2001 From: Anand Thakker Date: Wed, 8 Nov 2017 12:34:02 -0500 Subject: [PATCH] Implement Expressions (#9439) Ports https://github.com/mapbox/mapbox-gl-js/pull/4777 (and its several follow-ups) --- cmake/core-files.cmake | 51 ++ cmake/node.cmake | 13 + cmake/render.cmake | 1 + cmake/test-files.cmake | 4 + .../conversion/data_driven_property_value.hpp | 25 + include/mbgl/style/conversion/expression.hpp | 39 ++ .../mbgl/style/conversion/get_json_type.hpp | 14 + .../mbgl/style/conversion/property_value.hpp | 16 + .../mbgl/style/expression/array_assertion.hpp | 39 ++ include/mbgl/style/expression/assertion.hpp | 33 + include/mbgl/style/expression/at.hpp | 38 ++ .../style/expression/boolean_operator.hpp | 49 ++ include/mbgl/style/expression/case.hpp | 36 ++ .../mbgl/style/expression/check_subtype.hpp | 17 + include/mbgl/style/expression/coalesce.hpp | 45 ++ include/mbgl/style/expression/coercion.hpp | 34 ++ .../style/expression/compound_expression.hpp | 138 +++++ include/mbgl/style/expression/expression.hpp | 169 ++++++ .../mbgl/style/expression/find_zoom_curve.hpp | 20 + .../style/expression/get_covering_stops.hpp | 18 + include/mbgl/style/expression/interpolate.hpp | 177 ++++++ include/mbgl/style/expression/is_constant.hpp | 35 ++ .../mbgl/style/expression/is_expression.hpp | 13 + include/mbgl/style/expression/let.hpp | 72 +++ include/mbgl/style/expression/literal.hpp | 38 ++ include/mbgl/style/expression/match.hpp | 45 ++ .../mbgl/style/expression/parsing_context.hpp | 147 +++++ include/mbgl/style/expression/step.hpp | 45 ++ include/mbgl/style/expression/type.hpp | 111 ++++ include/mbgl/style/expression/value.hpp | 153 +++++ .../mbgl/style/function/camera_function.hpp | 58 +- .../style/function/composite_function.hpp | 134 ++-- include/mbgl/style/function/convert.hpp | 351 +++++++++++ .../mbgl/style/function/source_function.hpp | 39 +- include/mbgl/util/enum.hpp | 1 + include/mbgl/util/interpolate.hpp | 33 + include/mbgl/util/unitbezier.hpp | 6 + mapbox-gl-js | 2 +- package.json | 6 +- platform/node/src/node_expression.cpp | 230 +++++++ platform/node/src/node_expression.hpp | 40 ++ platform/node/src/node_mapbox_gl_native.cpp | 2 + platform/node/test/expression.test.js | 71 +++ platform/node/test/ignores.json | 72 ++- src/mbgl/programs/symbol_program.hpp | 51 +- src/mbgl/renderer/paint_property_binder.hpp | 11 +- src/mbgl/style/conversion/get_json_type.cpp | 34 ++ src/mbgl/style/expression/array_assertion.cpp | 85 +++ src/mbgl/style/expression/assertion.cpp | 73 +++ src/mbgl/style/expression/at.cpp | 63 ++ .../style/expression/boolean_operator.cpp | 87 +++ src/mbgl/style/expression/case.cpp | 90 +++ src/mbgl/style/expression/check_subtype.cpp | 60 ++ src/mbgl/style/expression/coalesce.cpp | 62 ++ src/mbgl/style/expression/coercion.cpp | 143 +++++ .../style/expression/compound_expression.cpp | 571 ++++++++++++++++++ src/mbgl/style/expression/find_zoom_curve.cpp | 76 +++ .../style/expression/get_covering_stops.cpp | 26 + src/mbgl/style/expression/interpolate.cpp | 211 +++++++ src/mbgl/style/expression/is_constant.cpp | 40 ++ src/mbgl/style/expression/is_expression.cpp | 29 + src/mbgl/style/expression/let.cpp | 91 +++ src/mbgl/style/expression/literal.cpp | 108 ++++ src/mbgl/style/expression/match.cpp | 262 ++++++++ src/mbgl/style/expression/parsing_context.cpp | 206 +++++++ src/mbgl/style/expression/step.cpp | 151 +++++ src/mbgl/style/expression/util.cpp | 39 ++ src/mbgl/style/expression/util.hpp | 14 + src/mbgl/style/expression/value.cpp | 322 ++++++++++ src/mbgl/style/function/expression.cpp | 38 ++ test/fixtures/expression_equality/acos.a.json | 4 + test/fixtures/expression_equality/acos.b.json | 4 + test/fixtures/expression_equality/all.a.json | 17 + test/fixtures/expression_equality/all.b.json | 17 + test/fixtures/expression_equality/any.a.json | 17 + test/fixtures/expression_equality/any.b.json | 17 + .../fixtures/expression_equality/array.a.json | 11 + .../fixtures/expression_equality/array.b.json | 11 + test/fixtures/expression_equality/asin.a.json | 4 + test/fixtures/expression_equality/asin.b.json | 4 + test/fixtures/expression_equality/at.a.json | 20 + test/fixtures/expression_equality/at.b.json | 20 + test/fixtures/expression_equality/atan.a.json | 4 + test/fixtures/expression_equality/atan.b.json | 4 + .../expression_equality/boolean.a.json | 7 + .../expression_equality/boolean.b.json | 7 + test/fixtures/expression_equality/case.a.json | 14 + test/fixtures/expression_equality/case.b.json | 14 + .../expression_equality/coalesce.a.json | 16 + .../expression_equality/coalesce.b.json | 16 + .../expression_equality/concat.a.json | 6 + .../expression_equality/concat.b.json | 6 + test/fixtures/expression_equality/cos.a.json | 4 + test/fixtures/expression_equality/cos.b.json | 4 + .../expression_equality/divide.a.json | 5 + .../expression_equality/divide.b.json | 5 + .../expression_equality/downcase.a.json | 4 + .../expression_equality/downcase.b.json | 4 + test/fixtures/expression_equality/get.a.json | 7 + test/fixtures/expression_equality/get.b.json | 7 + test/fixtures/expression_equality/has.a.json | 4 + test/fixtures/expression_equality/has.b.json | 4 + .../heatmap-density.a.json | 23 + .../heatmap-density.b.json | 23 + test/fixtures/expression_equality/let.a.json | 25 + test/fixtures/expression_equality/let.b.json | 25 + test/fixtures/expression_equality/ln.a.json | 4 + test/fixtures/expression_equality/ln.b.json | 6 + .../fixtures/expression_equality/log10.a.json | 4 + .../fixtures/expression_equality/log10.b.json | 4 + test/fixtures/expression_equality/log2.a.json | 4 + test/fixtures/expression_equality/log2.b.json | 4 + .../fixtures/expression_equality/match.a.json | 12 + .../fixtures/expression_equality/match.b.json | 12 + test/fixtures/expression_equality/max.a.json | 6 + test/fixtures/expression_equality/max.b.json | 6 + test/fixtures/expression_equality/min.a.json | 5 + test/fixtures/expression_equality/min.b.json | 5 + .../fixtures/expression_equality/minus.a.json | 5 + .../fixtures/expression_equality/minus.b.json | 5 + test/fixtures/expression_equality/mod.a.json | 5 + test/fixtures/expression_equality/mod.b.json | 5 + test/fixtures/expression_equality/not.a.json | 10 + test/fixtures/expression_equality/not.b.json | 10 + .../expression_equality/number.a.json | 7 + .../expression_equality/number.b.json | 7 + .../expression_equality/object.a.json | 7 + .../expression_equality/object.b.json | 7 + test/fixtures/expression_equality/plus.a.json | 7 + test/fixtures/expression_equality/plus.b.json | 7 + test/fixtures/expression_equality/pow.a.json | 11 + test/fixtures/expression_equality/pow.b.json | 11 + test/fixtures/expression_equality/rgb.a.json | 6 + test/fixtures/expression_equality/rgb.b.json | 6 + test/fixtures/expression_equality/rgba.a.json | 7 + test/fixtures/expression_equality/rgba.b.json | 7 + test/fixtures/expression_equality/sin.a.json | 4 + test/fixtures/expression_equality/sin.b.json | 4 + test/fixtures/expression_equality/sqrt.a.json | 7 + test/fixtures/expression_equality/sqrt.b.json | 7 + test/fixtures/expression_equality/step.a.json | 18 + test/fixtures/expression_equality/step.b.json | 18 + .../expression_equality/string.a.json | 7 + .../expression_equality/string.b.json | 7 + test/fixtures/expression_equality/tan.a.json | 4 + test/fixtures/expression_equality/tan.b.json | 4 + .../fixtures/expression_equality/times.a.json | 7 + .../fixtures/expression_equality/times.b.json | 7 + .../expression_equality/to-boolean.a.json | 7 + .../expression_equality/to-boolean.b.json | 7 + .../expression_equality/to-color.a.json | 7 + .../expression_equality/to-color.b.json | 7 + .../expression_equality/to-number.a.json | 7 + .../expression_equality/to-number.b.json | 7 + .../expression_equality/to-string.a.json | 7 + .../expression_equality/to-string.b.json | 7 + .../expression_equality/typeof.a.json | 7 + .../expression_equality/typeof.b.json | 7 + .../expression_equality/upcase.a.json | 4 + .../expression_equality/upcase.b.json | 4 + test/fixtures/expression_equality/zoom.a.json | 13 + test/fixtures/expression_equality/zoom.b.json | 13 + .../style_parser/expressions.info.json | 12 + .../style_parser/expressions.style.json | 74 +++ test/style/conversion/function.test.cpp | 28 + test/style/expression/expression.test.cpp | 91 +++ test/style/expression/util.test.cpp | 23 + 167 files changed, 6460 insertions(+), 175 deletions(-) create mode 100644 include/mbgl/style/conversion/expression.hpp create mode 100644 include/mbgl/style/conversion/get_json_type.hpp create mode 100644 include/mbgl/style/expression/array_assertion.hpp create mode 100644 include/mbgl/style/expression/assertion.hpp create mode 100644 include/mbgl/style/expression/at.hpp create mode 100644 include/mbgl/style/expression/boolean_operator.hpp create mode 100644 include/mbgl/style/expression/case.hpp create mode 100644 include/mbgl/style/expression/check_subtype.hpp create mode 100644 include/mbgl/style/expression/coalesce.hpp create mode 100644 include/mbgl/style/expression/coercion.hpp create mode 100644 include/mbgl/style/expression/compound_expression.hpp create mode 100644 include/mbgl/style/expression/expression.hpp create mode 100644 include/mbgl/style/expression/find_zoom_curve.hpp create mode 100644 include/mbgl/style/expression/get_covering_stops.hpp create mode 100644 include/mbgl/style/expression/interpolate.hpp create mode 100644 include/mbgl/style/expression/is_constant.hpp create mode 100644 include/mbgl/style/expression/is_expression.hpp create mode 100644 include/mbgl/style/expression/let.hpp create mode 100644 include/mbgl/style/expression/literal.hpp create mode 100644 include/mbgl/style/expression/match.hpp create mode 100644 include/mbgl/style/expression/parsing_context.hpp create mode 100644 include/mbgl/style/expression/step.hpp create mode 100644 include/mbgl/style/expression/type.hpp create mode 100644 include/mbgl/style/expression/value.hpp create mode 100644 include/mbgl/style/function/convert.hpp create mode 100644 platform/node/src/node_expression.cpp create mode 100644 platform/node/src/node_expression.hpp create mode 100644 platform/node/test/expression.test.js create mode 100644 src/mbgl/style/conversion/get_json_type.cpp create mode 100644 src/mbgl/style/expression/array_assertion.cpp create mode 100644 src/mbgl/style/expression/assertion.cpp create mode 100644 src/mbgl/style/expression/at.cpp create mode 100644 src/mbgl/style/expression/boolean_operator.cpp create mode 100644 src/mbgl/style/expression/case.cpp create mode 100644 src/mbgl/style/expression/check_subtype.cpp create mode 100644 src/mbgl/style/expression/coalesce.cpp create mode 100644 src/mbgl/style/expression/coercion.cpp create mode 100644 src/mbgl/style/expression/compound_expression.cpp create mode 100644 src/mbgl/style/expression/find_zoom_curve.cpp create mode 100644 src/mbgl/style/expression/get_covering_stops.cpp create mode 100644 src/mbgl/style/expression/interpolate.cpp create mode 100644 src/mbgl/style/expression/is_constant.cpp create mode 100644 src/mbgl/style/expression/is_expression.cpp create mode 100644 src/mbgl/style/expression/let.cpp create mode 100644 src/mbgl/style/expression/literal.cpp create mode 100644 src/mbgl/style/expression/match.cpp create mode 100644 src/mbgl/style/expression/parsing_context.cpp create mode 100644 src/mbgl/style/expression/step.cpp create mode 100644 src/mbgl/style/expression/util.cpp create mode 100644 src/mbgl/style/expression/util.hpp create mode 100644 src/mbgl/style/expression/value.cpp create mode 100644 src/mbgl/style/function/expression.cpp create mode 100644 test/fixtures/expression_equality/acos.a.json create mode 100644 test/fixtures/expression_equality/acos.b.json create mode 100644 test/fixtures/expression_equality/all.a.json create mode 100644 test/fixtures/expression_equality/all.b.json create mode 100644 test/fixtures/expression_equality/any.a.json create mode 100644 test/fixtures/expression_equality/any.b.json create mode 100644 test/fixtures/expression_equality/array.a.json create mode 100644 test/fixtures/expression_equality/array.b.json create mode 100644 test/fixtures/expression_equality/asin.a.json create mode 100644 test/fixtures/expression_equality/asin.b.json create mode 100644 test/fixtures/expression_equality/at.a.json create mode 100644 test/fixtures/expression_equality/at.b.json create mode 100644 test/fixtures/expression_equality/atan.a.json create mode 100644 test/fixtures/expression_equality/atan.b.json create mode 100644 test/fixtures/expression_equality/boolean.a.json create mode 100644 test/fixtures/expression_equality/boolean.b.json create mode 100644 test/fixtures/expression_equality/case.a.json create mode 100644 test/fixtures/expression_equality/case.b.json create mode 100644 test/fixtures/expression_equality/coalesce.a.json create mode 100644 test/fixtures/expression_equality/coalesce.b.json create mode 100644 test/fixtures/expression_equality/concat.a.json create mode 100644 test/fixtures/expression_equality/concat.b.json create mode 100644 test/fixtures/expression_equality/cos.a.json create mode 100644 test/fixtures/expression_equality/cos.b.json create mode 100644 test/fixtures/expression_equality/divide.a.json create mode 100644 test/fixtures/expression_equality/divide.b.json create mode 100644 test/fixtures/expression_equality/downcase.a.json create mode 100644 test/fixtures/expression_equality/downcase.b.json create mode 100644 test/fixtures/expression_equality/get.a.json create mode 100644 test/fixtures/expression_equality/get.b.json create mode 100644 test/fixtures/expression_equality/has.a.json create mode 100644 test/fixtures/expression_equality/has.b.json create mode 100644 test/fixtures/expression_equality/heatmap-density.a.json create mode 100644 test/fixtures/expression_equality/heatmap-density.b.json create mode 100644 test/fixtures/expression_equality/let.a.json create mode 100644 test/fixtures/expression_equality/let.b.json create mode 100644 test/fixtures/expression_equality/ln.a.json create mode 100644 test/fixtures/expression_equality/ln.b.json create mode 100644 test/fixtures/expression_equality/log10.a.json create mode 100644 test/fixtures/expression_equality/log10.b.json create mode 100644 test/fixtures/expression_equality/log2.a.json create mode 100644 test/fixtures/expression_equality/log2.b.json create mode 100644 test/fixtures/expression_equality/match.a.json create mode 100644 test/fixtures/expression_equality/match.b.json create mode 100644 test/fixtures/expression_equality/max.a.json create mode 100644 test/fixtures/expression_equality/max.b.json create mode 100644 test/fixtures/expression_equality/min.a.json create mode 100644 test/fixtures/expression_equality/min.b.json create mode 100644 test/fixtures/expression_equality/minus.a.json create mode 100644 test/fixtures/expression_equality/minus.b.json create mode 100644 test/fixtures/expression_equality/mod.a.json create mode 100644 test/fixtures/expression_equality/mod.b.json create mode 100644 test/fixtures/expression_equality/not.a.json create mode 100644 test/fixtures/expression_equality/not.b.json create mode 100644 test/fixtures/expression_equality/number.a.json create mode 100644 test/fixtures/expression_equality/number.b.json create mode 100644 test/fixtures/expression_equality/object.a.json create mode 100644 test/fixtures/expression_equality/object.b.json create mode 100644 test/fixtures/expression_equality/plus.a.json create mode 100644 test/fixtures/expression_equality/plus.b.json create mode 100644 test/fixtures/expression_equality/pow.a.json create mode 100644 test/fixtures/expression_equality/pow.b.json create mode 100644 test/fixtures/expression_equality/rgb.a.json create mode 100644 test/fixtures/expression_equality/rgb.b.json create mode 100644 test/fixtures/expression_equality/rgba.a.json create mode 100644 test/fixtures/expression_equality/rgba.b.json create mode 100644 test/fixtures/expression_equality/sin.a.json create mode 100644 test/fixtures/expression_equality/sin.b.json create mode 100644 test/fixtures/expression_equality/sqrt.a.json create mode 100644 test/fixtures/expression_equality/sqrt.b.json create mode 100644 test/fixtures/expression_equality/step.a.json create mode 100644 test/fixtures/expression_equality/step.b.json create mode 100644 test/fixtures/expression_equality/string.a.json create mode 100644 test/fixtures/expression_equality/string.b.json create mode 100644 test/fixtures/expression_equality/tan.a.json create mode 100644 test/fixtures/expression_equality/tan.b.json create mode 100644 test/fixtures/expression_equality/times.a.json create mode 100644 test/fixtures/expression_equality/times.b.json create mode 100644 test/fixtures/expression_equality/to-boolean.a.json create mode 100644 test/fixtures/expression_equality/to-boolean.b.json create mode 100644 test/fixtures/expression_equality/to-color.a.json create mode 100644 test/fixtures/expression_equality/to-color.b.json create mode 100644 test/fixtures/expression_equality/to-number.a.json create mode 100644 test/fixtures/expression_equality/to-number.b.json create mode 100644 test/fixtures/expression_equality/to-string.a.json create mode 100644 test/fixtures/expression_equality/to-string.b.json create mode 100644 test/fixtures/expression_equality/typeof.a.json create mode 100644 test/fixtures/expression_equality/typeof.b.json create mode 100644 test/fixtures/expression_equality/upcase.a.json create mode 100644 test/fixtures/expression_equality/upcase.b.json create mode 100644 test/fixtures/expression_equality/zoom.a.json create mode 100644 test/fixtures/expression_equality/zoom.b.json create mode 100644 test/fixtures/style_parser/expressions.info.json create mode 100644 test/fixtures/style_parser/expressions.style.json create mode 100644 test/style/expression/expression.test.cpp create mode 100644 test/style/expression/util.test.cpp diff --git a/cmake/core-files.cmake b/cmake/core-files.cmake index 5db1dd9ea04..f70bbb943f7 100644 --- a/cmake/core-files.cmake +++ b/cmake/core-files.cmake @@ -365,10 +365,12 @@ 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 include/mbgl/style/conversion/geojson_options.hpp + include/mbgl/style/conversion/get_json_type.hpp include/mbgl/style/conversion/layer.hpp include/mbgl/style/conversion/light.hpp include/mbgl/style/conversion/position.hpp @@ -381,6 +383,7 @@ set(MBGL_CORE_FILES src/mbgl/style/conversion/filter.cpp src/mbgl/style/conversion/geojson.cpp src/mbgl/style/conversion/geojson_options.cpp + src/mbgl/style/conversion/get_json_type.cpp src/mbgl/style/conversion/json.hpp src/mbgl/style/conversion/layer.cpp src/mbgl/style/conversion/light.cpp @@ -392,6 +395,52 @@ set(MBGL_CORE_FILES src/mbgl/style/conversion/tileset.cpp src/mbgl/style/conversion/transition_options.cpp + # style/expression + include/mbgl/style/expression/array_assertion.hpp + include/mbgl/style/expression/assertion.hpp + include/mbgl/style/expression/at.hpp + include/mbgl/style/expression/boolean_operator.hpp + include/mbgl/style/expression/case.hpp + include/mbgl/style/expression/check_subtype.hpp + include/mbgl/style/expression/coalesce.hpp + include/mbgl/style/expression/coercion.hpp + include/mbgl/style/expression/compound_expression.hpp + include/mbgl/style/expression/expression.hpp + include/mbgl/style/expression/find_zoom_curve.hpp + include/mbgl/style/expression/get_covering_stops.hpp + include/mbgl/style/expression/interpolate.hpp + include/mbgl/style/expression/is_constant.hpp + include/mbgl/style/expression/is_expression.hpp + include/mbgl/style/expression/let.hpp + include/mbgl/style/expression/literal.hpp + include/mbgl/style/expression/match.hpp + include/mbgl/style/expression/parsing_context.hpp + include/mbgl/style/expression/step.hpp + include/mbgl/style/expression/type.hpp + include/mbgl/style/expression/value.hpp + src/mbgl/style/expression/array_assertion.cpp + src/mbgl/style/expression/assertion.cpp + src/mbgl/style/expression/at.cpp + src/mbgl/style/expression/boolean_operator.cpp + src/mbgl/style/expression/case.cpp + src/mbgl/style/expression/check_subtype.cpp + src/mbgl/style/expression/coalesce.cpp + src/mbgl/style/expression/coercion.cpp + src/mbgl/style/expression/compound_expression.cpp + src/mbgl/style/expression/find_zoom_curve.cpp + src/mbgl/style/expression/get_covering_stops.cpp + src/mbgl/style/expression/interpolate.cpp + src/mbgl/style/expression/is_constant.cpp + src/mbgl/style/expression/is_expression.cpp + src/mbgl/style/expression/let.cpp + src/mbgl/style/expression/literal.cpp + src/mbgl/style/expression/match.cpp + src/mbgl/style/expression/parsing_context.cpp + src/mbgl/style/expression/step.cpp + src/mbgl/style/expression/util.cpp + src/mbgl/style/expression/util.hpp + src/mbgl/style/expression/value.cpp + # style/function include/mbgl/style/function/camera_function.hpp include/mbgl/style/function/categorical_stops.hpp @@ -399,11 +448,13 @@ set(MBGL_CORE_FILES include/mbgl/style/function/composite_exponential_stops.hpp include/mbgl/style/function/composite_function.hpp include/mbgl/style/function/composite_interval_stops.hpp + include/mbgl/style/function/convert.hpp include/mbgl/style/function/exponential_stops.hpp include/mbgl/style/function/identity_stops.hpp 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 388a98b68f3..3f7bcdb784f 100644 --- a/cmake/node.cmake +++ b/cmake/node.cmake @@ -22,6 +22,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 ) @@ -91,6 +93,17 @@ xcode_create_scheme( "test" ) +xcode_create_scheme( + TARGET mbgl-node + TYPE node + NAME "node expression tests" + ARGS + "platform/node/test/expression.test.js" + OPTIONAL_ARGS + "group" + "test" +) + xcode_create_scheme( TARGET mbgl-node TYPE node diff --git a/cmake/render.cmake b/cmake/render.cmake index f69aed16c0a..aff9397f42f 100644 --- a/cmake/render.cmake +++ b/cmake/render.cmake @@ -16,6 +16,7 @@ target_link_libraries(mbgl-render target_add_mason_package(mbgl-render PRIVATE boost) target_add_mason_package(mbgl-render PRIVATE boost_libprogram_options) +target_add_mason_package(mbgl-render PRIVATE geojson) mbgl_platform_render() diff --git a/cmake/test-files.cmake b/cmake/test-files.cmake index 3f11e75e07a..319790f05a8 100644 --- a/cmake/test-files.cmake +++ b/cmake/test-files.cmake @@ -88,6 +88,10 @@ set(MBGL_TEST_FILES test/style/conversion/light.test.cpp test/style/conversion/stringify.test.cpp + # style/expression + test/style/expression/expression.test.cpp + test/style/expression/util.test.cpp + # style test/style/filter.test.cpp diff --git a/include/mbgl/style/conversion/data_driven_property_value.hpp b/include/mbgl/style/conversion/data_driven_property_value.hpp index 1e54c15a493..8880d28fb16 100644 --- a/include/mbgl/style/conversion/data_driven_property_value.hpp +++ b/include/mbgl/style/conversion/data_driven_property_value.hpp @@ -4,6 +4,13 @@ #include #include #include +#include +#include +#include +#include + +#include + namespace mbgl { namespace style { @@ -11,9 +18,27 @@ namespace conversion { template struct Converter> { + optional> operator()(const Convertible& value, Error& error) const { if (isUndefined(value)) { return DataDrivenPropertyValue(); + } else if (expression::isExpression(value)) { + optional> expression = convert>( + value, + error, + valueTypeToExpressionType()); + + if (!expression) { + return {}; + } + + if (isFeatureConstant(**expression)) { + return DataDrivenPropertyValue(CameraFunction(std::move(*expression))); + } else if (isZoomConstant(**expression)) { + return DataDrivenPropertyValue(SourceFunction(std::move(*expression))); + } else { + return DataDrivenPropertyValue(CompositeFunction(std::move(*expression))); + } } else if (!isObject(value)) { optional constant = convert(value, error); if (!constant) { diff --git a/include/mbgl/style/conversion/expression.hpp b/include/mbgl/style/conversion/expression.hpp new file mode 100644 index 00000000000..c5fcf906a7b --- /dev/null +++ b/include/mbgl/style/conversion/expression.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace style { +namespace conversion { + +using namespace mbgl::style::expression; + +template<> struct Converter> { + optional> operator()(const Convertible& value, Error& error, type::Type expected) const { + ParsingContext ctx(optional {expected}); + ParseResult parsed = ctx.parse(value); + if (parsed) { + return std::move(*parsed); + } + std::string combinedError; + for (const ParsingError& parsingError : ctx.getErrors()) { + if (combinedError.size() > 0) { + combinedError += "\n"; + } + if (parsingError.key.size() > 0) { + combinedError += parsingError.key + ": "; + } + combinedError += parsingError.message; + } + error = { combinedError }; + return {}; + }; +}; + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/conversion/get_json_type.hpp b/include/mbgl/style/conversion/get_json_type.hpp new file mode 100644 index 00000000000..f7efebccce0 --- /dev/null +++ b/include/mbgl/style/conversion/get_json_type.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace style { +namespace conversion { + +std::string getJSONType(const Convertible& value); + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/conversion/property_value.hpp b/include/mbgl/style/conversion/property_value.hpp index c7f971ec91f..97117de2ece 100644 --- a/include/mbgl/style/conversion/property_value.hpp +++ b/include/mbgl/style/conversion/property_value.hpp @@ -4,6 +4,11 @@ #include #include #include +#include +#include +#include +#include +#include namespace mbgl { namespace style { @@ -14,6 +19,17 @@ struct Converter> { optional> operator()(const Convertible& value, Error& error) const { if (isUndefined(value)) { return PropertyValue(); + } else if (isExpression(value)) { + optional> expression = convert>(value, error, valueTypeToExpressionType()); + if (!expression) { + return {}; + } + if (isFeatureConstant(**expression)) { + return { CameraFunction(std::move(*expression)) }; + } else { + error = { "property expressions not supported" }; + return {}; + } } else if (isObject(value)) { optional> function = convert>(value, error); if (!function) { diff --git a/include/mbgl/style/expression/array_assertion.hpp b/include/mbgl/style/expression/array_assertion.hpp new file mode 100644 index 00000000000..2516eea024c --- /dev/null +++ b/include/mbgl/style/expression/array_assertion.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace mbgl { +namespace style { +namespace expression { + +class ArrayAssertion : public Expression { +public: + ArrayAssertion(type::Array type_, std::unique_ptr input_) : + Expression(type_), + input(std::move(input_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + return getType() == rhs->getType() && *input == *(rhs->input); + } + return false; + } + +private: + std::unique_ptr input; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/assertion.hpp b/include/mbgl/style/expression/assertion.hpp new file mode 100644 index 00000000000..504d49f4e56 --- /dev/null +++ b/include/mbgl/style/expression/assertion.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Assertion : public Expression { +public: + Assertion(type::Type type_, std::vector> inputs_) : + Expression(type_), + inputs(std::move(inputs_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override; + +private: + std::vector> inputs; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/include/mbgl/style/expression/at.hpp b/include/mbgl/style/expression/at.hpp new file mode 100644 index 00000000000..e3eefa4fe82 --- /dev/null +++ b/include/mbgl/style/expression/at.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class At : public Expression { +public: + At(std::unique_ptr index_, std::unique_ptr input_) : + Expression(input_->getType().get().itemType), + index(std::move(index_)), + input(std::move(input_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function&) const override; + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + return *index == *(rhs->index) && *input == *(rhs->input); + } + return false; + } + +private: + std::unique_ptr index; + std::unique_ptr input; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/boolean_operator.hpp b/include/mbgl/style/expression/boolean_operator.hpp new file mode 100644 index 00000000000..01231d706b9 --- /dev/null +++ b/include/mbgl/style/expression/boolean_operator.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Any : public Expression { +public: + Any(std::vector> inputs_) : + Expression(type::Boolean), + inputs(std::move(inputs_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + bool operator==(const Expression& e) const override; + +private: + std::vector> inputs; +}; + +class All : public Expression { +public: + All(std::vector> inputs_) : + Expression(type::Boolean), + inputs(std::move(inputs_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override; + +private: + std::vector> inputs; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/include/mbgl/style/expression/case.hpp b/include/mbgl/style/expression/case.hpp new file mode 100644 index 00000000000..ece2fe03298 --- /dev/null +++ b/include/mbgl/style/expression/case.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Case : public Expression { +public: + using Branch = std::pair, std::unique_ptr>; + + Case(type::Type type_, std::vector branches_, std::unique_ptr otherwise_) + : Expression(type_), branches(std::move(branches_)), otherwise(std::move(otherwise_)) { + } + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override; + +private: + std::vector branches; + std::unique_ptr otherwise; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/check_subtype.hpp b/include/mbgl/style/expression/check_subtype.hpp new file mode 100644 index 00000000000..90e5169de7c --- /dev/null +++ b/include/mbgl/style/expression/check_subtype.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { +namespace type { + +optional checkSubtype(const Type& expected, const Type& t); + +} // namespace type +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/coalesce.hpp b/include/mbgl/style/expression/coalesce.hpp new file mode 100644 index 00000000000..4e6a9b37931 --- /dev/null +++ b/include/mbgl/style/expression/coalesce.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Coalesce : public Expression { +public: + using Args = std::vector>; + Coalesce(const type::Type& type_, Args args_) : + Expression(type_), + args(std::move(args_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + + EvaluationResult evaluate(const EvaluationContext& params) const override; + + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override; + + std::size_t getLength() const { + return args.size(); + } + + Expression* getChild(std::size_t i) const { + return args.at(i).get(); + } + +private: + Args args; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/coercion.hpp b/include/mbgl/style/expression/coercion.hpp new file mode 100644 index 00000000000..665bb7ce7cc --- /dev/null +++ b/include/mbgl/style/expression/coercion.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +/** + * Special form for error-coalescing coercion expressions "to-number", + * "to-color". Since these coercions can fail at runtime, they accept multiple + * arguments, only evaluating one at a time until one succeeds. + */ +class Coercion : public Expression { +public: + Coercion(type::Type type_, std::vector> inputs_); + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override; +private: + EvaluationResult (*coerceSingleValue) (const Value& v); + std::vector> inputs; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/include/mbgl/style/expression/compound_expression.hpp b/include/mbgl/style/expression/compound_expression.hpp new file mode 100644 index 00000000000..fc3edbfd4a6 --- /dev/null +++ b/include/mbgl/style/expression/compound_expression.hpp @@ -0,0 +1,138 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +/* + CompoundExpression provides a mechanism for implementing an expression + simply by providing a list of pure functions of the form + (const T0& arg0, const T1& arg1, ...) -> Result where T0, T1, ..., U are + member types of mbgl::style::expression::Value. + + The majority of expressions specified in the style-spec are implemented in + this fashion (see compound_expression.cpp). +*/ + + +/* + Represents the parameter list for an expression that takes an arbitrary + number of arguments (of a specific type). +*/ +struct VarargsType { type::Type type; }; +template +struct Varargs : std::vector { using std::vector::vector; }; + +namespace detail { +// Base class for the Signature structs that are used to determine the +// each CompoundExpression definition's type::Type data from the type of its +// "evaluate" function. +struct SignatureBase { + SignatureBase(type::Type result_, variant, VarargsType> params_) : + result(std::move(result_)), + params(std::move(params_)) + {} + virtual ~SignatureBase() = default; + virtual std::unique_ptr makeExpression(const std::string& name, std::vector>) const = 0; + type::Type result; + variant, VarargsType> params; +}; +} // namespace detail + + +/* + Common base class for CompoundExpression instances. Used to + allow downcasting (and access to things like name & parameter list) during + an Expression tree traversal. +*/ +class CompoundExpressionBase : public Expression { +public: + CompoundExpressionBase(std::string name_, const detail::SignatureBase& signature) : + Expression(signature.result), + name(std::move(name_)), + params(signature.params) + {} + + std::string getName() const { return name; } + optional getParameterCount() const { + return params.match( + [&](const VarargsType&) { return optional(); }, + [&](const std::vector& p) -> optional { return p.size(); } + ); + } + +private: + std::string name; + variant, VarargsType> params; +}; + +template +class CompoundExpression : public CompoundExpressionBase { +public: + using Args = typename Signature::Args; + + CompoundExpression(const std::string& name_, + Signature signature_, + typename Signature::Args args_) : + CompoundExpressionBase(name_, signature_), + signature(signature_), + args(std::move(args_)) + {} + + EvaluationResult evaluate(const EvaluationContext& evaluationParams) const override { + return signature.apply(evaluationParams, args); + } + + void eachChild(const std::function& visit) const override { + for (const std::unique_ptr& e : args) { + visit(*e); + } + } + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + return getName() == rhs->getName() && Expression::childrenEqual(args, rhs->args); + } + return false; + } + +private: + Signature signature; + typename Signature::Args args; +}; + +/* + Holds the map of expression name => implementation (which is just one or + more evaluation functions, each wrapped in a Signature struct). +*/ +struct CompoundExpressionRegistry { + using Definition = std::vector>; + static std::unordered_map definitions; +}; + +ParseResult parseCompoundExpression(const std::string name, const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + +ParseResult createCompoundExpression(const std::string& name, + const CompoundExpressionRegistry::Definition& definition, + std::vector> args, + ParsingContext& ctx); + +ParseResult createCompoundExpression(const std::string& name, + std::vector> args, + ParsingContext& ctx); + +} // 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 00000000000..1954d8b090f --- /dev/null +++ b/include/mbgl/style/expression/expression.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +class GeometryTileFeature; + +namespace style { +namespace expression { + +class EvaluationError { +public: + std::string message; +}; + +class EvaluationContext { +public: + EvaluationContext(float zoom_) : zoom(zoom_), feature(nullptr) {} + EvaluationContext(GeometryTileFeature const * feature_) : zoom(optional()), feature(feature_) {} + EvaluationContext(float zoom_, GeometryTileFeature const * feature_) : + zoom(zoom_), feature(feature_) + {} + EvaluationContext(optional zoom_, GeometryTileFeature const * feature_, optional heatmapDensity_) : + zoom(std::move(zoom_)), feature(feature_), heatmapDensity(std::move(heatmapDensity_)) + {} + + optional zoom; + GeometryTileFeature const * feature; + optional heatmapDensity; +}; + +template +class Result : private variant { +public: + using variant::variant; + using Value = T; + + explicit operator bool () const { + return this->template is(); + } + + // optional does some type trait magic for this one, so this might + // be problematic as is. + const T* operator->() const { + assert(this->template is()); + return std::addressof(this->template get()); + } + + T* operator->() { + assert(this->template is()); + return std::addressof(this->template get()); + } + + T& operator*() { + assert(this->template is()); + return this->template get(); + } + + const T& operator*() const { + assert(this->template is()); + return this->template get(); + } + + const EvaluationError& error() const { + assert(this->template is()); + return this->template get(); + } +}; + +class EvaluationResult : public Result { +public: + using Result::Result; // NOLINT + + EvaluationResult(const std::array& arr) : + Result(toExpressionValue(arr)) + {} + + // used only for the special (private) "error" expression + EvaluationResult(const type::ErrorType&) { + assert(false); + } +}; + +/* + Expression is an abstract class that serves as an interface and base class + for particular expression implementations. + + CompoundExpression implements the majority of expressions in the spec by + inferring the argument and output from a simple function (const T0& arg0, + const T1& arg1, ...) -> Result where T0, T1, ..., U are member types of + mbgl::style::expression::Value. + + The other Expression subclasses (Let, Curve, Match, etc.) exist in order to + implement expressions that need specialized parsing, type checking, or + evaluation logic that can't be handled by CompoundExpression's inference + mechanism. + + Each Expression subclass also provides a static + ParseResult ExpressionClass::parse(const V&, ParsingContext), + which handles parsing a style-spec JSON representation of the expression. +*/ +class Expression { +public: + Expression(type::Type type_) : type(std::move(type_)) {} + virtual ~Expression() = default; + + virtual EvaluationResult evaluate(const EvaluationContext& params) const = 0; + virtual void eachChild(const std::function&) const = 0; + virtual bool operator==(const Expression&) const = 0; + bool operator!=(const Expression& rhs) const { + return !operator==(rhs); + } + + type::Type getType() const { return type; }; + + EvaluationResult evaluate(optional zoom, const Feature& feature, optional heatmapDensity) const; + +protected: + template + static bool childrenEqual(const T& lhs, const T& rhs) { + if (lhs.size() != rhs.size()) return false; + for (auto leftChild = lhs.begin(), rightChild = rhs.begin(); + leftChild != lhs.end(); + leftChild++, rightChild++) + { + if (!Expression::childEqual(*leftChild, *rightChild)) return false; + } + return true; + } + + static bool childEqual(const std::unique_ptr& lhs, const std::unique_ptr& rhs) { + return *lhs == *rhs; + } + + template + static bool childEqual(const std::pair>& lhs, + const std::pair>& rhs) { + return lhs.first == rhs.first && *(lhs.second) == *(rhs.second); + } + + template + static bool childEqual(const std::pair>& lhs, + const std::pair>& rhs) { + return lhs.first == rhs.first && *(lhs.second) == *(rhs.second); + } + + static bool childEqual(const std::pair, std::unique_ptr>& lhs, + const std::pair, std::unique_ptr>& rhs) { + return *(lhs.first) == *(rhs.first) && *(lhs.second) == *(rhs.second); + } + + + +private: + type::Type type; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/find_zoom_curve.hpp b/include/mbgl/style/expression/find_zoom_curve.hpp new file mode 100644 index 00000000000..63019380334 --- /dev/null +++ b/include/mbgl/style/expression/find_zoom_curve.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +optional> findZoomCurve(const expression::Expression* e); + +variant findZoomCurveChecked(const expression::Expression* e); + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/get_covering_stops.hpp b/include/mbgl/style/expression/get_covering_stops.hpp new file mode 100644 index 00000000000..157aefe7bc7 --- /dev/null +++ b/include/mbgl/style/expression/get_covering_stops.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +// Return the smallest range of stops that covers the interval [lower, upper] +Range getCoveringStops(const std::map>& stops, + const double lower, const double upper); + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/interpolate.hpp b/include/mbgl/style/expression/interpolate.hpp new file mode 100644 index 00000000000..2dcb5a32a4d --- /dev/null +++ b/include/mbgl/style/expression/interpolate.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + + +namespace mbgl { +namespace style { +namespace expression { + +class ExponentialInterpolator { +public: + ExponentialInterpolator(double base_) : base(base_) {} + + double base; + + double interpolationFactor(const Range& inputLevels, const double input) const { + return util::interpolationFactor(base, + Range { + static_cast(inputLevels.min), + static_cast(inputLevels.max) + }, + input); + } + + bool operator==(const ExponentialInterpolator& rhs) const { + return base == rhs.base; + } +}; + +class CubicBezierInterpolator { +public: + CubicBezierInterpolator(double x1_, double y1_, double x2_, double y2_) : ub(x1_, y1_, x2_, y2_) {} + + double interpolationFactor(const Range& inputLevels, const double input) const { + return ub.solve(input / (inputLevels.max - inputLevels.min), 1e-6); + } + + bool operator==(const CubicBezierInterpolator& rhs) const { + return ub == rhs.ub; + } + + util::UnitBezier ub; +}; + + +ParseResult parseInterpolate(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + +class InterpolateBase : public Expression { +public: + using Interpolator = variant; + + InterpolateBase(const type::Type& type_, + Interpolator interpolator_, + std::unique_ptr input_, + std::map> stops_ + ) : Expression(type_), + interpolator(std::move(interpolator_)), + input(std::move(input_)), + stops(std::move(stops_)) + {} + + const std::unique_ptr& getInput() const { return input; } + + void eachChild(const std::function& visit) const override { + visit(*input); + for (const std::pair&>& stop : stops) { + visit(*stop.second); + } + } + + // Return the smallest range of stops that covers the interval [lower, upper] + Range getCoveringStops(const double lower, const double upper) const { + return ::mbgl::style::expression::getCoveringStops(stops, lower, upper); + } + + double interpolationFactor(const Range& inputLevels, const double inputValue) const { + return interpolator.match( + [&](const auto& interp) { return interp.interpolationFactor(inputLevels, inputValue); } + ); + } + +protected: + const Interpolator interpolator; + const std::unique_ptr input; + const std::map> stops; +}; + +template +class Interpolate : public InterpolateBase { +public: + Interpolate(type::Type type_, + Interpolator interpolator_, + std::unique_ptr input_, + std::map> stops_ + ) : InterpolateBase(std::move(type_), std::move(interpolator_), std::move(input_), std::move(stops_)) + { + static_assert(util::Interpolatable::value, "Interpolate expression requires an interpolatable value type."); + } + + EvaluationResult evaluate(const EvaluationContext& params) const override { + const EvaluationResult evaluatedInput = input->evaluate(params); + if (!evaluatedInput) { return evaluatedInput.error(); } + float x = *fromExpressionValue(*evaluatedInput); + + if (stops.empty()) { + return EvaluationError { "No stops in exponential curve." }; + } + + auto it = stops.upper_bound(x); + if (it == stops.end()) { + return stops.rbegin()->second->evaluate(params); + } else if (it == stops.begin()) { + return stops.begin()->second->evaluate(params); + } else { + float t = interpolationFactor({ std::prev(it)->first, it->first }, x); + + if (t == 0.0f) { + return std::prev(it)->second->evaluate(params); + } + if (t == 1.0f) { + return it->second->evaluate(params); + } + + EvaluationResult lower = std::prev(it)->second->evaluate(params); + if (!lower) { + return lower.error(); + } + EvaluationResult upper = it->second->evaluate(params); + if (!upper) { + return upper.error(); + } + + if (!lower->is()) { + return EvaluationError { + "Expected value to be of type " + toString(valueTypeToExpressionType()) + + ", but found " + toString(typeOf(*lower)) + " instead." + }; + } + + if (!upper->is()) { + return EvaluationError { + "Expected value to be of type " + toString(valueTypeToExpressionType()) + + ", but found " + toString(typeOf(*upper)) + " instead." + }; + } + return util::interpolate(lower->get(), upper->get(), t); + } + } + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + if (interpolator != rhs->interpolator || + *input != *(rhs->input) || + stops.size() != rhs->stops.size()) + { + return false; + } + + return Expression::childrenEqual(stops, rhs->stops); + } + return false; + } +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/is_constant.hpp b/include/mbgl/style/expression/is_constant.hpp new file mode 100644 index 00000000000..29e03ccbc01 --- /dev/null +++ b/include/mbgl/style/expression/is_constant.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +template +bool isGlobalPropertyConstant(const Expression& expression, const T& properties) { + if (auto e = dynamic_cast(&expression)) { + for (const std::string& property : properties) { + if (e->getName() == property) { + return false; + } + } + } + + bool isConstant = true; + expression.eachChild([&](const Expression& e) { + if (isConstant && !isGlobalPropertyConstant(e, properties)) { + isConstant = false; + } + }); + return isConstant; +}; + +bool isFeatureConstant(const Expression& expression); +bool isZoomConstant(const Expression& e); + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/is_expression.hpp b/include/mbgl/style/expression/is_expression.hpp new file mode 100644 index 00000000000..77c489619c7 --- /dev/null +++ b/include/mbgl/style/expression/is_expression.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace mbgl { +namespace style { +namespace expression { + +bool isExpression(const conversion::Convertible& value); + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/let.hpp b/include/mbgl/style/expression/let.hpp new file mode 100644 index 00000000000..aaa16ca0c24 --- /dev/null +++ b/include/mbgl/style/expression/let.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Let : public Expression { +public: + using Bindings = std::map>; + + Let(Bindings bindings_, std::unique_ptr result_) : + Expression(result_->getType()), + bindings(std::move(bindings_)), + result(std::move(result_)) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible&, ParsingContext&); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function&) const override; + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + return *result == *(rhs->result); + } + return false; + } + + Expression* getResult() const { + return result.get(); + } + +private: + Bindings bindings; + std::unique_ptr result; +}; + +class Var : public Expression { +public: + Var(std::string name_, std::shared_ptr value_) : + Expression(value_->getType()), + name(std::move(name_)), + value(value_) + {} + + static ParseResult parse(const mbgl::style::conversion::Convertible&, ParsingContext&); + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function&) const override; + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + return *value == *(rhs->value); + } + return false; + } + +private: + std::string name; + std::shared_ptr value; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/literal.hpp b/include/mbgl/style/expression/literal.hpp new file mode 100644 index 00000000000..a0819c7e730 --- /dev/null +++ b/include/mbgl/style/expression/literal.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Literal : public Expression { +public: + Literal(Value value_) : Expression(typeOf(value_)), value(value_) {} + Literal(type::Array type_, std::vector value_) : Expression(type_), value(value_) {} + EvaluationResult evaluate(const EvaluationContext&) const override { + return value; + } + + static ParseResult parse(const mbgl::style::conversion::Convertible&, ParsingContext&); + + void eachChild(const std::function&) const override {} + + bool operator==(const Expression& e) const override { + if (auto rhs = dynamic_cast(&e)) { + return value == rhs->value; + } + return false; + } + +private: + Value value; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/match.hpp b/include/mbgl/style/expression/match.hpp new file mode 100644 index 00000000000..e17fe96bfe4 --- /dev/null +++ b/include/mbgl/style/expression/match.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include + +namespace mbgl { +namespace style { +namespace expression { + +template +class Match : public Expression { +public: + using Branches = std::unordered_map>; + + Match(type::Type type_, + std::unique_ptr input_, + Branches branches_, + std::unique_ptr otherwise_ + ) : Expression(type_), + input(std::move(input_)), + branches(std::move(branches_)), + otherwise(std::move(otherwise_)) + {} + + void eachChild(const std::function& visit) const override; + + bool operator==(const Expression& e) const override; + + EvaluationResult evaluate(const EvaluationContext& params) const override; + +private: + + std::unique_ptr input; + Branches branches; + std::unique_ptr otherwise; +}; + +ParseResult parseMatch(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + +} // 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 00000000000..65c5ebe1888 --- /dev/null +++ b/include/mbgl/style/expression/parsing_context.hpp @@ -0,0 +1,147 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Expression; + +struct ParsingError { + std::string message; + std::string key; + bool operator==(const ParsingError& rhs) const { return message == rhs.message && key == rhs.key; } +}; + +using ParseResult = optional>; + +namespace detail { + +class Scope { +public: + Scope(const std::map>& bindings_, std::shared_ptr parent_ = nullptr) : + bindings(bindings_), + parent(std::move(parent_)) + {} + + const std::map>& bindings; + std::shared_ptr parent; + + optional> get(const std::string& name) { + auto it = bindings.find(name); + if (it != bindings.end()) { + return {it->second}; + } else if (parent) { + return parent->get(name); + } else { + return optional>(); + } + } +}; + +} // namespace detail + +class ParsingContext { +public: + ParsingContext() : errors(std::make_shared>()) {} + ParsingContext(std::string key_) : key(std::move(key_)), errors(std::make_shared>()) {} + explicit ParsingContext(optional expected_) + : expected(std::move(expected_)), + errors(std::make_shared>()) + {} + ParsingContext(ParsingContext&&) = default; + + ParsingContext(const ParsingContext&) = delete; + ParsingContext& operator=(const ParsingContext&) = delete; + + std::string getKey() const { return key; } + optional getExpected() const { return expected; } + const std::vector& getErrors() const { return *errors; } + + /* + Parse the given style-spec JSON value into an Expression object. + Specifically, this function is responsible for determining the expression + type (either Literal, or the one named in value[0]) and dispatching to the + appropriate ParseXxxx::parse(const V&, ParsingContext) method. + */ + ParseResult parse(const mbgl::style::conversion::Convertible& value); + + /* + Parse a child expression. + */ + ParseResult parse(const mbgl::style::conversion::Convertible&, + std::size_t, + optional = {}); + + /* + Parse a child expression. + */ + ParseResult parse(const mbgl::style::conversion::Convertible&, + std::size_t index, + optional, + const std::map>&); + + /* + Check whether `t` is a subtype of `expected`, collecting an error if not. + */ + optional checkType(const type::Type& t); + + optional> getBinding(const std::string name) { + if (!scope) return optional>(); + return scope->get(name); + } + + void error(std::string message) { + errors->push_back({message, key}); + } + + void error(std::string message, std::size_t child) { + errors->push_back({message, key + "[" + std::to_string(child) + "]"}); + } + + void error(std::string message, std::size_t child, std::size_t grandchild) { + errors->push_back({message, key + "[" + std::to_string(child) + "][" + std::to_string(grandchild) + "]"}); + } + + void appendErrors(ParsingContext&& ctx) { + errors->reserve(errors->size() + ctx.errors->size()); + std::move(ctx.errors->begin(), ctx.errors->end(), std::inserter(*errors, errors->end())); + ctx.errors->clear(); + } + + void clearErrors() { + errors->clear(); + } + +private: + ParsingContext(std::string key_, + std::shared_ptr> errors_, + optional expected_, + std::shared_ptr scope_) + : key(std::move(key_)), + expected(std::move(expected_)), + scope(std::move(scope_)), + errors(std::move(errors_)) + {} + + std::string key; + optional expected; + std::shared_ptr scope; + std::shared_ptr> errors; +}; + +using ParseFunction = ParseResult (*)(const conversion::Convertible&, ParsingContext&); +using ExpressionRegistry = std::unordered_map; +const ExpressionRegistry& getExpressionRegistry(); + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/step.hpp b/include/mbgl/style/expression/step.hpp new file mode 100644 index 00000000000..e3c49bc6098 --- /dev/null +++ b/include/mbgl/style/expression/step.hpp @@ -0,0 +1,45 @@ + +#pragma once + +#include +#include +#include + +#include + +#include +#include + + +namespace mbgl { +namespace style { +namespace expression { + +class Step : public Expression { +public: + Step(const type::Type& type_, + std::unique_ptr input_, + std::map> stops_ + ) : Expression(type_), + input(std::move(input_)), + stops(std::move(stops_)) + {} + + EvaluationResult evaluate(const EvaluationContext& params) const override; + void eachChild(const std::function& visit) const override; + + const std::unique_ptr& getInput() const { return input; } + Range getCoveringStops(const double lower, const double upper) const; + + bool operator==(const Expression& e) const override; + + static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx); + +private: + const std::unique_ptr input; + const std::map> stops; +}; + +} // 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 00000000000..d801cd3ac9c --- /dev/null +++ b/include/mbgl/style/expression/type.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { +namespace type { + +template +std::string toString(const T& t); + +struct NullType { + constexpr NullType() = default; + std::string getName() const { return "null"; } + bool operator==(const NullType&) const { return true; } +}; + +struct NumberType { + constexpr NumberType() = default; + std::string getName() const { return "number"; } + bool operator==(const NumberType&) const { return true; } +}; + +struct BooleanType { + constexpr BooleanType() = default; + std::string getName() const { return "boolean"; } + bool operator==(const BooleanType&) const { return true; } +}; + +struct StringType { + constexpr StringType() = default; + std::string getName() const { return "string"; } + bool operator==(const StringType&) const { return true; } +}; + +struct ColorType { + constexpr ColorType() = default; + std::string getName() const { return "color"; } + bool operator==(const ColorType&) const { return true; } +}; + +struct ObjectType { + constexpr ObjectType() = default; + std::string getName() const { return "object"; } + bool operator==(const ObjectType&) const { return true; } +}; + +struct ErrorType { + constexpr ErrorType() = default; + std::string getName() const { return "error"; } + bool operator==(const ErrorType&) const { return true; } +}; + +struct ValueType { + constexpr ValueType() = default; + std::string getName() const { return "value"; } + bool operator==(const ValueType&) const { return true; } +}; + +constexpr NullType Null; +constexpr NumberType Number; +constexpr StringType String; +constexpr BooleanType Boolean; +constexpr ColorType Color; +constexpr ValueType Value; +constexpr ObjectType Object; +constexpr ErrorType Error; + +struct Array; + +using Type = variant< + NullType, + NumberType, + BooleanType, + StringType, + ColorType, + ObjectType, + ValueType, + mapbox::util::recursive_wrapper, + ErrorType>; + +struct Array { + explicit Array(Type itemType_) : itemType(std::move(itemType_)) {} + Array(Type itemType_, std::size_t N_) : itemType(std::move(itemType_)), N(N_) {} + Array(Type itemType_, optional N_) : itemType(std::move(itemType_)), N(std::move(N_)) {} + std::string getName() const { + if (N) { + return "array<" + toString(itemType) + ", " + std::to_string(*N) + ">"; + } else if (itemType == Value) { + return "array"; + } else { + return "array<" + toString(itemType) + ">"; + } + } + + bool operator==(const Array& rhs) const { return itemType == rhs.itemType && N == rhs.N; } + + Type itemType; + optional N; +}; + +template +std::string toString(const T& type) { return type.match([&] (const auto& t) { return t.getName(); }); } + +} // namespace type +} // 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 00000000000..8baa9d2dba1 --- /dev/null +++ b/include/mbgl/style/expression/value.hpp @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +struct Value; + +using ValueBase = variant< + NullValue, + bool, + double, + std::string, + Color, + mapbox::util::recursive_wrapper>, + mapbox::util::recursive_wrapper>>; +struct Value : ValueBase { + using ValueBase::ValueBase; + + // Javascript's Number.MAX_SAFE_INTEGER + static uint64_t maxSafeInteger() { return 9007199254740991ULL; } + + static bool isSafeInteger(uint64_t x) { return x <= maxSafeInteger(); }; + static bool isSafeInteger(int64_t x) { + return static_cast(x > 0 ? x : -x) <= maxSafeInteger(); + } + static bool isSafeInteger(double x) { + return static_cast(x > 0 ? x : -x) <= maxSafeInteger(); + } + +}; + +constexpr NullValue Null = NullValue(); + +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 +type::Type valueTypeToExpressionType(); + +/* + Conversions between style value types and expression::Value +*/ + +// no-op overloads +Value toExpressionValue(const Value&); + +// T = Value (just wrap in optional) +template +std::enable_if_t::value, +optional> fromExpressionValue(const Value& v) +{ + return optional(v); +} + +// T = member type of Value +template +std::enable_if_t< std::is_convertible::value && !std::is_same::value, +optional> fromExpressionValue(const Value& v) +{ + return v.template is() ? v.template get() : optional(); +} + +// real conversions +template ::value >> +Value toExpressionValue(const T& value); + +template +std::enable_if_t< !std::is_convertible::value, +optional> fromExpressionValue(const Value& v); + + + +template +struct ValueConverter { + using ExpressionType = T; + + static Value toExpressionValue(const T& value) { + return Value(value); + } + static optional fromExpressionValue(const Value& value) { + return value.template is() ? value.template get() : optional(); + } +}; + +template <> +struct ValueConverter { + using ExpressionType = double; + static type::Type expressionType() { return type::Number; } + static Value toExpressionValue(const float value); + static optional fromExpressionValue(const Value& value); +}; + +template<> +struct ValueConverter { + static Value toExpressionValue(const mbgl::Value& value); +}; + +template +struct ValueConverter> { + using ExpressionType = std::vector; + static type::Type expressionType() { + return type::Array(valueTypeToExpressionType(), N); + } + static Value toExpressionValue(const std::array& value); + static optional> fromExpressionValue(const Value& value); +}; + +template +struct ValueConverter> { + using ExpressionType = std::vector; + static type::Type expressionType() { + return type::Array(valueTypeToExpressionType()); + } + static Value toExpressionValue(const std::vector& value); + static optional> fromExpressionValue(const Value& value); +}; + +template <> +struct ValueConverter { + using ExpressionType = std::vector; + static type::Type expressionType() { return type::Array(type::Number, 3); } + static Value toExpressionValue(const mbgl::style::Position& value); + static optional fromExpressionValue(const Value& v); +}; + +template +struct ValueConverter::value >> { + using ExpressionType = std::string; + static type::Type expressionType() { return type::String; } + static Value toExpressionValue(const T& value); + static optional fromExpressionValue(const Value& value); +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/function/camera_function.hpp b/include/mbgl/style/function/camera_function.hpp index 7fde365b3d8..25b38e36168 100644 --- a/include/mbgl/style/function/camera_function.hpp +++ b/include/mbgl/style/function/camera_function.hpp @@ -1,10 +1,18 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include #include #include #include + namespace mbgl { namespace style { @@ -18,24 +26,60 @@ class CameraFunction { IntervalStops>, variant< IntervalStops>>; + + CameraFunction(std::unique_ptr expression_) + : expression(std::move(expression_)), + zoomCurve(expression::findZoomCurveChecked(expression.get())) + { + assert(!expression::isZoomConstant(*expression)); + assert(expression::isFeatureConstant(*expression)); + } CameraFunction(Stops stops_) - : stops(std::move(stops_)) { - } + : stops(std::move(stops_)), + expression(stops.match([&] (const auto& s) { + return expression::Convert::toExpression(s); + })), + zoomCurve(expression::findZoomCurveChecked(expression.get())) + {} T evaluate(float zoom) const { - return stops.match([&] (const auto& s) { - return s.evaluate(zoom).value_or(T()); - }); + const expression::EvaluationResult result = expression->evaluate(expression::EvaluationContext(zoom, nullptr)); + if (result) { + const optional typed = expression::fromExpressionValue(*result); + return typed ? *typed : T(); + } + return T(); + } + + float interpolationFactor(const Range& inputLevels, const float inputValue) const { + return zoomCurve.match( + [&](const expression::InterpolateBase* z) { + return z->interpolationFactor(Range { inputLevels.min, inputLevels.max }, inputValue); + }, + [&](const expression::Step*) { return 0.0f; } + ); + } + + Range getCoveringStops(const float lower, const float upper) const { + return zoomCurve.match( + [&](auto z) { return z->getCoveringStops(lower, upper); } + ); } friend bool operator==(const CameraFunction& lhs, const CameraFunction& rhs) { - return lhs.stops == rhs.stops; + return *lhs.expression == *rhs.expression; } - Stops stops; bool useIntegerZoom = false; + + // retained for compatibility with pre-expression function API + Stops stops; + +private: + std::shared_ptr expression; + const variant zoomCurve; }; } // namespace style diff --git a/include/mbgl/style/function/composite_function.hpp b/include/mbgl/style/function/composite_function.hpp index 7b524b60215..b44bf8e6fe9 100644 --- a/include/mbgl/style/function/composite_function.hpp +++ b/include/mbgl/style/function/composite_function.hpp @@ -1,5 +1,12 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -43,110 +50,71 @@ class CompositeFunction { CompositeIntervalStops, CompositeCategoricalStops>>; - CompositeFunction(std::string property_, Stops stops_, optional defaultValue_ = {}) - : property(std::move(property_)), - stops(std::move(stops_)), - defaultValue(std::move(defaultValue_)) { - } - - struct CoveringRanges { - float zoom; - Range coveringZoomRange; - Range coveringStopsRange; - }; - - // Return the relevant stop zoom values and inner stops that bracket a given zoom level. This - // is the first step toward evaluating the function, and is used for in the course of both partial - // evaluation of data-driven paint properties, and full evaluation of data-driven layout properties. - CoveringRanges coveringRanges(float zoom) const { - return stops.match( - [&] (const auto& s) { - assert(!s.stops.empty()); - auto minIt = s.stops.lower_bound(zoom); - auto maxIt = s.stops.upper_bound(zoom); - - // lower_bound yields first element >= zoom, but we want the *last* - // element <= zoom, so if we found a stop > zoom, back up by one. - if (minIt != s.stops.begin() && minIt != s.stops.end() && minIt->first > zoom) { - minIt--; - } - - return CoveringRanges { - zoom, - Range { - minIt == s.stops.end() ? s.stops.rbegin()->first : minIt->first, - maxIt == s.stops.end() ? s.stops.rbegin()->first : maxIt->first - }, - Range { - s.innerStops(minIt == s.stops.end() ? s.stops.rbegin()->second : minIt->second), - s.innerStops(maxIt == s.stops.end() ? s.stops.rbegin()->second : maxIt->second) - } - }; - } - ); + CompositeFunction(std::unique_ptr expression_) + : expression(std::move(expression_)), + zoomCurve(expression::findZoomCurveChecked(expression.get())) + { + assert(!expression::isZoomConstant(*expression)); + assert(!expression::isFeatureConstant(*expression)); } - // Given a range of zoom values (typically two adjacent integer zoom levels, e.g. 5.0 and 6.0), - // return the covering ranges for both. This is used in the course of partial evaluation for - // data-driven paint properties. - Range rangeOfCoveringRanges(Range zoomRange) { - return Range { - coveringRanges(zoomRange.min), - coveringRanges(zoomRange.max) - }; - } - - // Given the covering ranges for range of zoom values (typically two adjacent integer zoom levels, - // e.g. 5.0 and 6.0), and a feature, return the results of fully evaluating the function for that - // feature at each of the two zoom levels. These two results are what go into the paint vertex buffers - // for vertices associated with this feature. The shader will interpolate between them at render time. + CompositeFunction(std::string property_, Stops stops_, optional defaultValue_ = {}) + : property(std::move(property_)), + stops(std::move(stops_)), + defaultValue(std::move(defaultValue_)), + expression(stops.match([&] (const auto& s) { + return expression::Convert::toExpression(property, s); + })), + zoomCurve(expression::findZoomCurveChecked(expression.get())) + {} + + // Return the range obtained by evaluating the function at each of the zoom levels in zoomRange template - Range evaluate(const Range& ranges, const Feature& feature, T finalDefaultValue) { - optional value = feature.getValue(property); - if (!value) { - return Range { - defaultValue.value_or(finalDefaultValue), - defaultValue.value_or(finalDefaultValue) - }; - } + Range evaluate(const Range& zoomRange, const Feature& feature, T finalDefaultValue) { return Range { - evaluateFinal(ranges.min, *value, finalDefaultValue), - evaluateFinal(ranges.max, *value, finalDefaultValue) + evaluate(zoomRange.min, feature, finalDefaultValue), + evaluate(zoomRange.max, feature, finalDefaultValue) }; } - // Fully evaluate the function for a zoom value and feature. This is used when evaluating data-driven - // layout properties. template T evaluate(float zoom, const Feature& feature, T finalDefaultValue) const { - optional value = feature.getValue(property); - if (!value) { - return defaultValue.value_or(finalDefaultValue); + const expression::EvaluationResult result = expression->evaluate(expression::EvaluationContext({zoom}, &feature)); + if (result) { + const optional typed = expression::fromExpressionValue(*result); + return typed ? *typed : defaultValue ? *defaultValue : finalDefaultValue; } - return evaluateFinal(coveringRanges(zoom), *value, finalDefaultValue); + return defaultValue ? *defaultValue : finalDefaultValue; + } + + float interpolationFactor(const Range& inputLevels, const float inputValue) const { + return zoomCurve.match( + [&](const expression::InterpolateBase* z) { + return z->interpolationFactor(Range { inputLevels.min, inputLevels.max }, inputValue); + }, + [&](const expression::Step*) { return 0.0f; } + ); + } + + Range getCoveringStops(const float lower, const float upper) const { + return zoomCurve.match( + [&](auto z) { return z->getCoveringStops(lower, upper); } + ); } friend bool operator==(const CompositeFunction& lhs, const CompositeFunction& rhs) { - return std::tie(lhs.property, lhs.stops, lhs.defaultValue) - == std::tie(rhs.property, rhs.stops, rhs.defaultValue); + return *lhs.expression == *rhs.expression; } std::string property; Stops stops; optional defaultValue; bool useIntegerZoom = false; - + private: - T evaluateFinal(const CoveringRanges& ranges, const Value& value, T finalDefaultValue) const { - auto eval = [&] (const auto& s) { - return s.evaluate(value).value_or(defaultValue.value_or(finalDefaultValue)); - }; - return util::interpolate( - ranges.coveringStopsRange.min.match(eval), - ranges.coveringStopsRange.max.match(eval), - util::interpolationFactor(1.0f, ranges.coveringZoomRange, ranges.zoom)); - } + std::shared_ptr expression; + const variant zoomCurve; }; } // namespace style diff --git a/include/mbgl/style/function/convert.hpp b/include/mbgl/style/function/convert.hpp new file mode 100644 index 00000000000..ed35b4bf146 --- /dev/null +++ b/include/mbgl/style/function/convert.hpp @@ -0,0 +1,351 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + + +namespace mbgl { +namespace style { +namespace expression { + +namespace detail { + +class ErrorExpression : public Expression { +public: + ErrorExpression(std::string message_) : Expression(type::Error), message(std::move(message_)) {} + void eachChild(const std::function&) const override {} + + bool operator==(const Expression& e) const override { + return dynamic_cast(&e); + } + + EvaluationResult evaluate(const EvaluationContext&) const override { + return EvaluationError{message}; + } + +private: + std::string message; +}; + +} // namespace detail + + +// Create expressions representing 'classic' (i.e. stop-based) style functions + +struct Convert { + template + static std::unique_ptr makeLiteral(const T& value) { + return std::make_unique(Value(toExpressionValue(value))); + } + + static std::unique_ptr makeGet(type::Type type, const std::string& property) { + ParsingContext ctx; + std::vector> getArgs; + getArgs.push_back(makeLiteral(property)); + ParseResult get = createCompoundExpression("get", std::move(getArgs), ctx); + assert(get); + assert(ctx.getErrors().size() == 0); + + std::vector> assertionArgs; + assertionArgs.push_back(std::move(*get)); + + return std::make_unique(type, std::move(assertionArgs)); + } + + static std::unique_ptr makeZoom() { + ParsingContext ctx; + ParseResult zoom = createCompoundExpression("zoom", std::vector>(), ctx); + assert(zoom); + assert(ctx.getErrors().size() == 0); + return std::move(*zoom); + } + + static std::unique_ptr makeError(std::string message) { + return std::make_unique(message); + } + + template + static ParseResult makeInterpolate(type::Type type, + std::unique_ptr input, + std::map> convertedStops, + typename Interpolate::Interpolator interpolator) + { + ParseResult curve = ParseResult(std::make_unique>( + std::move(type), + std::move(interpolator), + std::move(input), + std::move(convertedStops) + )); + assert(curve); + return std::move(*curve); + } + + template + static ParseResult makeMatch(type::Type type, + std::unique_ptr input, + std::map> stops) { + // match expression + typename Match::Branches branches; + for(auto it = stops.begin(); it != stops.end(); it++) { + assert(it->first.template is()); + Key key = it->first.template get(); + branches.emplace( + std::move(key), + std::move(it->second) + ); + } + + return ParseResult(std::make_unique>(std::move(type), + std::move(input), + std::move(branches), + makeError("No matching label"))); + } + + static ParseResult makeCase(type::Type type, + std::unique_ptr input, + std::map> stops) { + // case expression + std::vector branches; + + auto it = stops.find(true); + std::unique_ptr true_case = it == stops.end() ? + makeError("No matching label") : + std::move(it->second); + + it = stops.find(false); + std::unique_ptr false_case = it == stops.end() ? + makeError("No matching label") : + std::move(it->second); + + branches.push_back(std::make_pair(std::move(input), std::move(true_case))); + return ParseResult(std::make_unique(std::move(type), std::move(branches), std::move(false_case))); + } + + template + static ParseResult fromCategoricalStops(std::map stops, const std::string& property) { + assert(stops.size() > 0); + + std::map> convertedStops; + for(const std::pair& stop : stops) { + convertedStops.emplace( + stop.first, + makeLiteral(stop.second) + ); + } + + type::Type type = valueTypeToExpressionType(); + + const CategoricalValue& firstKey = stops.begin()->first; + return firstKey.match( + [&](bool) { + return makeCase(type, makeGet(type::Boolean, property), std::move(convertedStops)); + }, + [&](const std::string&) { + return makeMatch(type, makeGet(type::String, property), std::move(convertedStops)); + }, + [&](int64_t) { + return makeMatch(type, makeGet(type::Number, property), std::move(convertedStops)); + } + ); + } + + template + static std::map> convertStops(const std::map& stops) { + std::map> convertedStops; + for(const std::pair& stop : stops) { + convertedStops.emplace( + stop.first, + makeLiteral(stop.second) + ); + } + return convertedStops; + } + + template + static std::unique_ptr toExpression(const ExponentialStops& stops) + { + ParseResult e = makeInterpolate::ExpressionType>( + valueTypeToExpressionType(), + makeZoom(), + convertStops(stops.stops), + ExponentialInterpolator(stops.base)); + assert(e); + return std::move(*e); + } + + template + static std::unique_ptr toExpression(const IntervalStops& stops) + { + ParseResult e(std::make_unique(valueTypeToExpressionType(), + makeZoom(), + convertStops(stops.stops))); + assert(e); + return std::move(*e); + } + + template + static std::unique_ptr toExpression(const std::string& property, + const ExponentialStops& stops) + { + ParseResult e = makeInterpolate::ExpressionType>(valueTypeToExpressionType(), + makeGet(type::Number, property), + convertStops(stops.stops), + ExponentialInterpolator(stops.base)); + assert(e); + return std::move(*e); + } + + template + static std::unique_ptr toExpression(const std::string& property, + const IntervalStops& stops) + { + std::unique_ptr get = makeGet(type::Number, property); + ParseResult e(std::make_unique(valueTypeToExpressionType(), + std::move(get), + convertStops(stops.stops))); + assert(e); + return std::move(*e); + } + + template + static std::unique_ptr toExpression(const std::string& property, + const CategoricalStops& stops) + { + ParseResult expr = fromCategoricalStops(stops.stops, property); + assert(expr); + return std::move(*expr); + } + + // interpolatable zoom curve + template + static typename std::enable_if_t::value, + ParseResult> makeZoomCurve(std::map> stops) { + return makeInterpolate::ExpressionType>(valueTypeToExpressionType(), + makeZoom(), + std::move(stops), + ExponentialInterpolator(1.0)); + } + + // non-interpolatable zoom curve + template + static typename std::enable_if_t::value, + ParseResult> makeZoomCurve(std::map> stops) { + return ParseResult(std::make_unique(valueTypeToExpressionType(), makeZoom(), std::move(stops))); + } + + template + static std::unique_ptr toExpression(const std::string& property, + const CompositeExponentialStops& stops) + { + std::map> outerStops; + for (const std::pair>& stop : stops.stops) { + std::unique_ptr get = makeGet(type::Number, property); + ParseResult innerInterpolate = makeInterpolate::ExpressionType>(valueTypeToExpressionType(), + std::move(get), + convertStops(stop.second), + ExponentialInterpolator(stops.base)); + assert(innerInterpolate); + outerStops.emplace(stop.first, std::move(*innerInterpolate)); + } + + ParseResult zoomCurve = makeZoomCurve(std::move(outerStops)); + assert(zoomCurve); + return std::move(*zoomCurve); + } + + template + static std::unique_ptr toExpression(const std::string& property, + const CompositeIntervalStops& stops) + { + std::map> outerStops; + for (const std::pair>& stop : stops.stops) { + std::unique_ptr get = makeGet(type::Number, property); + ParseResult innerInterpolate(std::make_unique(valueTypeToExpressionType(), + std::move(get), + convertStops(stop.second))); + assert(innerInterpolate); + outerStops.emplace(stop.first, std::move(*innerInterpolate)); + } + + ParseResult zoomCurve = makeZoomCurve(std::move(outerStops)); + assert(zoomCurve); + return std::move(*zoomCurve); + } + + template + static std::unique_ptr toExpression(const std::string& property, + const CompositeCategoricalStops& stops) + { + std::map> outerStops; + for (const std::pair>& stop : stops.stops) { + ParseResult innerInterpolate = fromCategoricalStops(stop.second, property); + assert(innerInterpolate); + outerStops.emplace(stop.first, std::move(*innerInterpolate)); + } + + ParseResult zoomCurve = makeZoomCurve(std::move(outerStops)); + assert(zoomCurve); + return std::move(*zoomCurve); + } + + + static std::unique_ptr fromIdentityFunction(type::Type type, const std::string& property) + { + std::unique_ptr input = type.match( + [&] (const type::StringType&) { + return makeGet(type::String, property); + }, + [&] (const type::NumberType&) { + return makeGet(type::Number, property); + }, + [&] (const type::BooleanType&) { + return makeGet(type::Boolean, property); + }, + [&] (const type::ColorType&) { + std::vector> args; + args.push_back(makeGet(type::String, property)); + return std::make_unique(type::Color, std::move(args)); + }, + [&] (const type::Array& arr) { + std::vector> getArgs; + getArgs.push_back(makeLiteral(property)); + ParsingContext ctx; + ParseResult get = createCompoundExpression("get", std::move(getArgs), ctx); + assert(get); + assert(ctx.getErrors().size() == 0); + return std::make_unique(arr, std::move(*get)); + }, + [&] (const auto&) -> std::unique_ptr { + return makeLiteral(Null); + } + ); + + return input; + } +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/function/source_function.hpp b/include/mbgl/style/function/source_function.hpp index 9c2ad101ec0..02e4b604e23 100644 --- a/include/mbgl/style/function/source_function.hpp +++ b/include/mbgl/style/function/source_function.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -27,33 +29,48 @@ class SourceFunction { CategoricalStops, IdentityStops>>; + SourceFunction(std::unique_ptr expression_) + : expression(std::move(expression_)) + { + assert(expression::isZoomConstant(*expression)); + assert(!expression::isFeatureConstant(*expression)); + } + SourceFunction(std::string property_, Stops stops_, optional defaultValue_ = {}) : property(std::move(property_)), stops(std::move(stops_)), - defaultValue(std::move(defaultValue_)) { - } + defaultValue(std::move(defaultValue_)), + expression(stops.match([&] (const IdentityStops&) { + return expression::Convert::fromIdentityFunction(expression::valueTypeToExpressionType(), property); + }, [&] (const auto& s) { + return expression::Convert::toExpression(property, s); + })) + {} template T evaluate(const Feature& feature, T finalDefaultValue) const { - optional v = feature.getValue(property); - if (!v) { - return defaultValue.value_or(finalDefaultValue); + const expression::EvaluationResult result = expression->evaluate(expression::EvaluationContext(&feature)); + if (result) { + const optional typed = expression::fromExpressionValue(*result); + return typed ? *typed : defaultValue ? *defaultValue : finalDefaultValue; } - return stops.match([&] (const auto& s) -> T { - return s.evaluate(*v).value_or(defaultValue.value_or(finalDefaultValue)); - }); + return defaultValue ? *defaultValue : finalDefaultValue; } friend bool operator==(const SourceFunction& lhs, const SourceFunction& rhs) { - return std::tie(lhs.property, lhs.stops, lhs.defaultValue) - == std::tie(rhs.property, rhs.stops, rhs.defaultValue); + return *lhs.expression == *rhs.expression; } + bool useIntegerZoom = false; + + // retained for compatibility with pre-expression function API std::string property; Stops stops; optional defaultValue; - bool useIntegerZoom = false; + +private: + std::shared_ptr expression; }; } // namespace style diff --git a/include/mbgl/util/enum.hpp b/include/mbgl/util/enum.hpp index 369ca86bfdb..608befd3c4e 100644 --- a/include/mbgl/util/enum.hpp +++ b/include/mbgl/util/enum.hpp @@ -11,6 +11,7 @@ namespace mbgl { template class Enum { public: + using Type = T; static const char * toString(T); static optional toEnum(const std::string&); }; diff --git a/include/mbgl/util/interpolate.hpp b/include/mbgl/util/interpolate.hpp index 6738987598d..aff730a0a24 100644 --- a/include/mbgl/util/interpolate.hpp +++ b/include/mbgl/util/interpolate.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,36 @@ struct Interpolator> { } }; + +// In order to accept Array as an output value for Curve +// expressions, we need to have an interpolatable std::vector type. +// However, style properties like line-dasharray are represented using +// std::vector, and should NOT be considered interpolatable. +// So, we use std::vector to represent expression array values, +// asserting that (a) the vectors are the same size, and (b) they contain +// only numeric values. (These invariants should be relatively safe, +// being enforced by the expression type system.) +template<> +struct Interpolator> { + std::vector operator()(const std::vector& a, + const std::vector& b, + const double t) const { + assert(a.size() == b.size()); + if (a.size() == 0) return {}; + std::vector result; + for (std::size_t i = 0; i < a.size(); i++) { + assert(a[i].template is()); + assert(b[i].template is()); + style::expression::Value item = interpolate( + a[i].template get(), + b[i].template get(), + t); + result.push_back(item); + } + return result; + } +}; + template <> struct Interpolator { public: @@ -101,5 +132,7 @@ struct Interpolatable std::true_type, std::false_type> {}; + + } // namespace util } // namespace mbgl diff --git a/include/mbgl/util/unitbezier.hpp b/include/mbgl/util/unitbezier.hpp index 6e644e2d1f6..92f23d67185 100644 --- a/include/mbgl/util/unitbezier.hpp +++ b/include/mbgl/util/unitbezier.hpp @@ -26,6 +26,7 @@ #pragma once #include +#include namespace mbgl { namespace util { @@ -102,6 +103,11 @@ struct UnitBezier { double solve(double x, double epsilon) const { return sampleCurveY(solveCurveX(x, epsilon)); } + + bool operator==(const UnitBezier& rhs) const { + return std::tie(cx, bx, ax, cy, by, ay) == + std::tie(rhs.cx, rhs.bx, rhs.ax, rhs.cy, rhs.by, rhs.ay); + } private: const double cx; diff --git a/mapbox-gl-js b/mapbox-gl-js index cecd21c9dcf..ff47116f248 160000 --- a/mapbox-gl-js +++ b/mapbox-gl-js @@ -1 +1 @@ -Subproject commit cecd21c9dcf87e4b1a5282b3a071f409c164398a +Subproject commit ff47116f248c323f6fa6f1b517c1c840a1b5ab63 diff --git a/package.json b/package.json index edb41f1f685..4ff4b32735b 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.37", "npm-run-all": "^4.0.2" }, @@ -23,6 +23,7 @@ "ejs": "^2.4.1", "express": "^4.11.1", "flow-remove-types": "^1.2.1", + "json-stringify-pretty-compact": "^1.0.4", "lodash": "^4.16.4", "mapbox-gl-styles": "2.0.2", "pixelmatch": "^4.0.2", @@ -38,7 +39,8 @@ "install": "node-pre-gyp install --fallback-to-build=false || make node", "test": "tape platform/node/test/js/**/*.test.js", "test-memory": "node --expose-gc platform/node/test/memory.test.js", - "test-suite": "run-s test-render test-query", + "test-suite": "run-s test-render test-query test-expressions", + "test-expressions": "node platform/node/test/expression.test.js", "test-render": "node platform/node/test/render.test.js", "test-query": "node platform/node/test/query.test.js" }, diff --git a/platform/node/src/node_expression.cpp b/platform/node/src/node_expression.cpp new file mode 100644 index 00000000000..8958d5c6c7a --- /dev/null +++ b/platform/node/src/node_expression.cpp @@ -0,0 +1,230 @@ +#include "node_conversion.hpp" +#include "node_expression.hpp" + +#include +#include +#include +#include +#include + +using namespace mbgl::style; +using namespace mbgl::style::expression; + +namespace node_mbgl { + +Nan::Persistent NodeExpression::constructor; + +void NodeExpression::Init(v8::Local target) { + v8::Local tpl = Nan::New(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()); +} + +type::Type parseType(v8::Local type) { + static std::unordered_map types = { + {"string", type::String}, + {"number", type::Number}, + {"noolean", type::Boolean}, + {"object", type::Object}, + {"color", type::Color}, + {"value", type::Value} + }; + + v8::Local v8kind = Nan::Get(type, Nan::New("kind").ToLocalChecked()).ToLocalChecked(); + std::string kind(*v8::String::Utf8Value(v8kind)); + + if (kind == "array") { + type::Type itemType = parseType(Nan::Get(type, Nan::New("itemType").ToLocalChecked()).ToLocalChecked()->ToObject()); + mbgl::optional N; + + v8::Local Nkey = Nan::New("N").ToLocalChecked(); + if (Nan::Has(type, Nkey).FromMaybe(false)) { + N = Nan::Get(type, Nkey).ToLocalChecked()->ToInt32()->Value(); + } + return type::Array(itemType, N); + } + + return types[kind]; +} + +void NodeExpression::Parse(const Nan::FunctionCallbackInfo& info) { + v8::Local cons = Nan::New(constructor); + + if (info.Length() < 1 || info[0]->IsUndefined()) { + return Nan::ThrowTypeError("Requires a JSON style expression argument."); + } + + mbgl::optional expected; + if (info.Length() > 1 && info[1]->IsObject()) { + expected = parseType(info[1]->ToObject()); + } + + auto expr = info[0]; + + try { + ParsingContext ctx(expected); + ParseResult parsed = ctx.parse(mbgl::style::conversion::Convertible(expr)); + if (parsed) { + assert(ctx.getErrors().size() == 0); + auto nodeExpr = new NodeExpression(std::move(*parsed)); + const int argc = 0; + v8::Local argv[0] = {}; + auto wrapped = Nan::NewInstance(cons, argc, argv).ToLocalChecked(); + nodeExpr->Wrap(wrapped); + info.GetReturnValue().Set(wrapped); + return; + } + + v8::Local result = Nan::New(); + for (std::size_t i = 0; i < ctx.getErrors().size(); i++) { + const auto& error = ctx.getErrors()[i]; + v8::Local err = Nan::New(); + 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& 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 operator()(mbgl::NullValue) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::Null()); + } + + v8::Local operator()(bool t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t)); + } + + v8::Local operator()(double t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t)); + } + + v8::Local operator()(const std::string& t) { + Nan::EscapableHandleScope scope; + return scope.Escape(Nan::New(t).ToLocalChecked()); + } + + v8::Local operator()(const std::vector& array) { + Nan::EscapableHandleScope scope; + v8::Local result = Nan::New(); + for (unsigned int i = 0; i < array.size(); i++) { + result->Set(i, toJS(array[i])); + } + return scope.Escape(result); + } + + v8::Local operator()(const mbgl::Color& color) { + return operator()(std::vector { + static_cast(color.r), + static_cast(color.g), + static_cast(color.b), + static_cast(color.a) + }); + } + + v8::Local operator()(const std::unordered_map& map) { + Nan::EscapableHandleScope scope; + v8::Local result = Nan::New(); + for (const auto& entry : map) { + Nan::Set(result, Nan::New(entry.first).ToLocalChecked(), toJS(entry.second)); + } + + return scope.Escape(result); + } +}; + +v8::Local toJS(const Value& value) { + return Value::visit(value, ToValue()); +} + +void NodeExpression::Evaluate(const Nan::FunctionCallbackInfo& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap(info.Holder()); + const std::unique_ptr& expression = nodeExpr->expression; + + if (info.Length() < 2 || !info[0]->IsObject()) { + return Nan::ThrowTypeError("Requires globals and feature arguments."); + } + + mbgl::optional zoom; + v8::Local v8zoom = Nan::Get(info[0]->ToObject(), Nan::New("zoom").ToLocalChecked()).ToLocalChecked(); + if (v8zoom->IsNumber()) zoom = v8zoom->NumberValue(); + + mbgl::optional heatmapDensity; + v8::Local v8heatmapDensity = Nan::Get(info[0]->ToObject(), Nan::New("heatmapDensity").ToLocalChecked()).ToLocalChecked(); + if (v8heatmapDensity->IsNumber()) heatmapDensity = v8heatmapDensity->NumberValue(); + + Nan::JSON NanJSON; + conversion::Error conversionError; + mbgl::optional geoJSON = conversion::convert(info[1], conversionError); + if (!geoJSON) { + Nan::ThrowTypeError(conversionError.message.c_str()); + return; + } + + try { + mapbox::geojson::feature feature = geoJSON->get(); + auto result = expression->evaluate(zoom, feature, heatmapDensity); + if (result) { + info.GetReturnValue().Set(toJS(*result)); + } else { + v8::Local res = Nan::New(); + 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& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap(info.Holder()); + const std::unique_ptr& expression = nodeExpr->expression; + + const type::Type type = expression->getType(); + const std::string name = type.match([&] (const auto& t) { return t.getName(); }); + info.GetReturnValue().Set(Nan::New(name.c_str()).ToLocalChecked()); +} + +void NodeExpression::IsFeatureConstant(const Nan::FunctionCallbackInfo& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap(info.Holder()); + const std::unique_ptr& expression = nodeExpr->expression; + info.GetReturnValue().Set(Nan::New(isFeatureConstant(*expression))); +} + +void NodeExpression::IsZoomConstant(const Nan::FunctionCallbackInfo& info) { + NodeExpression* nodeExpr = ObjectWrap::Unwrap(info.Holder()); + const std::unique_ptr& expression = nodeExpr->expression; + info.GetReturnValue().Set(Nan::New(isZoomConstant(*expression))); +} + +} // namespace node_mbgl diff --git a/platform/node/src/node_expression.hpp b/platform/node/src/node_expression.hpp new file mode 100644 index 00000000000..7af5b7ab51b --- /dev/null +++ b/platform/node/src/node_expression.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wshadow" +#include +#pragma GCC diagnostic pop + +using namespace mbgl::style::expression; + +namespace node_mbgl { + +v8::Local toJS(const Value&); + +class NodeExpression : public Nan::ObjectWrap { +public: + static void Init(v8::Local); + +private: + NodeExpression(std::unique_ptr expression_) : + expression(std::move(expression_)) + {}; + + static void New(const Nan::FunctionCallbackInfo&); + static void Parse(const Nan::FunctionCallbackInfo&); + static void Evaluate(const Nan::FunctionCallbackInfo&); + static void GetType(const Nan::FunctionCallbackInfo&); + static void IsFeatureConstant(const Nan::FunctionCallbackInfo&); + static void IsZoomConstant(const Nan::FunctionCallbackInfo&); + static Nan::Persistent constructor; + + std::unique_ptr 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 cdcc9822206..96e96e42986 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 target, v8::Local module) { // This has the effect of: @@ -20,6 +21,7 @@ void RegisterModule(v8::Local target, v8::Local module) node_mbgl::NodeMap::Init(target); node_mbgl::NodeRequest::Init(); + node_mbgl::NodeExpression::Init(target); // Exports Resource constants. v8::Local resource = Nan::New(); diff --git a/platform/node/test/expression.test.js b/platform/node/test/expression.test.js new file mode 100644 index 00000000000..aac039ce18e --- /dev/null +++ b/platform/node/test/expression.test.js @@ -0,0 +1,71 @@ +'use strict'; + +var suite = require('../../../mapbox-gl-js/test/integration').expression; +var mbgl = require('../index'); +var ignores = require('./ignores.json'); + +var tests; + +if (process.argv[1] === __filename && process.argv.length > 2) { + tests = process.argv.slice(2); +} + +function getExpectedType(spec) { + if (spec.type === 'array') { + const itemType = getExpectedType({ type: spec.value }); + const array = { + kind: 'array', + itemType: itemType || { kind: 'value' }, + }; + if (typeof spec.length === 'number') { + array.N = spec.length; + } + return array; + } + + if (spec.type === 'enum') { + return { kind: 'string' }; + } + + return typeof spec.type === 'string' ? {kind: spec.type} : null; +} + +suite.run('native', {ignores: ignores, tests: tests}, (fixture) => { + const compiled = {}; + const result = { + compiled + }; + + const spec = fixture.propertySpec || {}; + const expression = mbgl.Expression.parse(fixture.expression, getExpectedType(spec)); + + if (expression instanceof mbgl.Expression) { + compiled.result = 'success'; + compiled.isFeatureConstant = expression.isFeatureConstant(); + compiled.isZoomConstant = expression.isZoomConstant(); + compiled.type = expression.getType(); + + const evaluate = fixture.inputs || []; + const evaluateResults = []; + for (const input of evaluate) { + const feature = Object.assign({ + type: 'Feature', + properties: {}, + geometry: { type: 'Point', coordinates: [0, 0] } + }, input[1]) + + const output = expression.evaluate(input[0], feature); + evaluateResults.push(output); + } + + if (fixture.inputs) { + result.outputs = evaluateResults; + } + } else { + compiled.result = 'error'; + compiled.errors = expression; + } + + return result; +}); + diff --git a/platform/node/test/ignores.json b/platform/node/test/ignores.json index 81dff0bee4e..dd9679cde44 100644 --- a/platform/node/test/ignores.json +++ b/platform/node/test/ignores.json @@ -1,51 +1,88 @@ { + "expression-tests/curve/step": "https://github.com/mapbox/mapbox-gl-js/issues/5580", + "expression-tests/curve/interpolate": "https://github.com/mapbox/mapbox-gl-js/issues/5580", + "query-tests/circle-stroke-width/inside": "https://github.com/mapbox/mapbox-gl-native/issues/10307", "query-tests/geometry/multilinestring": "needs investigation", "query-tests/geometry/multipolygon": "needs investigation", "query-tests/geometry/polygon": "needs investigation", "query-tests/regressions/mapbox-gl-js#3534": "https://github.com/mapbox/mapbox-gl-native/issues/8193", "query-tests/regressions/mapbox-gl-js#4417": "https://github.com/mapbox/mapbox-gl-native/issues/8007", + "query-tests/regressions/mapbox-gl-js#5554": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "query-tests/symbol/panned-after-insert": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "query-tests/symbol/rotated-after-insert": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "query-tests/symbol/rotated-inside": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "query-tests/symbol-features-in/pitched-screen": "https://github.com/mapbox/mapbox-gl-native/issues/6817", "query-tests/symbol-features-in/tilted-inside": "https://github.com/mapbox/mapbox-gl-native/issues/5056", "query-tests/symbol-features-in/tilted-outside": "https://github.com/mapbox/mapbox-gl-native/issues/9435", "query-tests/world-wrapping/box": "skip - needs issue", "query-tests/world-wrapping/point": "skip - needs issue", + "render-tests/debug/collision": "https://github.com/mapbox/mapbox-gl-native/issues/3841", + "render-tests/debug/collision-lines": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/debug/collision-lines-pitched": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/debug/collision-overscaled": "https://github.com/mapbox/mapbox-gl-native/issues/3841", - "render-tests/debug/collision-pitched-wrapped": "https://github.com/mapbox/mapbox-gl-native/issues/3841", "render-tests/debug/collision-pitched": "https://github.com/mapbox/mapbox-gl-native/issues/3841", - "render-tests/debug/collision": "https://github.com/mapbox/mapbox-gl-native/issues/3841", - "render-tests/debug/tile-overscaled": "https://github.com/mapbox/mapbox-gl-native/issues/3841", + "render-tests/debug/collision-pitched-wrapped": "https://github.com/mapbox/mapbox-gl-native/issues/3841", "render-tests/debug/tile": "https://github.com/mapbox/mapbox-gl-native/issues/3841", + "render-tests/debug/tile-overscaled": "https://github.com/mapbox/mapbox-gl-native/issues/3841", "render-tests/extent/1024-circle": "needs investigation", "render-tests/extent/1024-symbol": "needs investigation", "render-tests/fill-extrusion-pattern/@2x": "https://github.com/mapbox/mapbox-gl-js/issues/3327", - "render-tests/fill-extrusion-pattern/function-2": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/function": "https://github.com/mapbox/mapbox-gl-js/issues/3327", + "render-tests/fill-extrusion-pattern/function-2": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/literal": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/missing": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/fill-extrusion-pattern/opacity": "https://github.com/mapbox/mapbox-gl-js/issues/3327", "render-tests/geojson/inline-linestring-fill": "current behavior is arbitrary", "render-tests/geojson/inline-polygon-symbol": "behavior needs reconciliation with gl-js", + "render-tests/heatmap-color/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-color/expression": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-color/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-intensity/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-intensity/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-intensity/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-opacity/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-opacity/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-opacity/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-radius/antimeridian": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-radius/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-radius/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-radius/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-radius/pitch30": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-weight/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-weight/identity-property-function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", + "render-tests/heatmap-weight/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", "render-tests/icon-size/composite-function-high-base-plain": "https://github.com/mapbox/mapbox-gl-native/issues/8654", "render-tests/icon-size/composite-function-high-base-sdf": "https://github.com/mapbox/mapbox-gl-native/issues/8654", - "render-tests/icon-text-fit/both-padding": "https://github.com/mapbox/mapbox-gl-native/issues/5602", "render-tests/icon-text-fit/both": "https://github.com/mapbox/mapbox-gl-native/issues/5602", + "render-tests/icon-text-fit/both-padding": "https://github.com/mapbox/mapbox-gl-native/issues/5602", "render-tests/icon-text-fit/height": "https://github.com/mapbox/mapbox-gl-native/issues/5602", - "render-tests/icon-text-fit/width-padding": "https://github.com/mapbox/mapbox-gl-native/issues/5602", + "render-tests/icon-text-fit/placement-line": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/icon-text-fit/width": "https://github.com/mapbox/mapbox-gl-native/issues/5602", - "render-tests/line-width/property-function": "https://github.com/mapbox/mapbox-gl-js/issues/3682#issuecomment-264348200", + "render-tests/icon-text-fit/width-padding": "https://github.com/mapbox/mapbox-gl-native/issues/5602", "render-tests/line-join/property-function": "https://github.com/mapbox/mapbox-gl-js/pull/5020", "render-tests/line-join/property-function-dasharray": "https://github.com/mapbox/mapbox-gl-js/pull/5020", "render-tests/line-opacity/step-curve": "https://github.com/mapbox/mapbox-gl-native/pull/9439", + "render-tests/line-width/property-function": "https://github.com/mapbox/mapbox-gl-js/issues/3682#issuecomment-264348200", + "render-tests/mixed-zoom/z10-z11": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/raster-masking/overlapping-zoom": "https://github.com/mapbox/mapbox-gl-native/issues/10195", "render-tests/regressions/mapbox-gl-js#2305": "https://github.com/mapbox/mapbox-gl-native/issues/6927", "render-tests/regressions/mapbox-gl-js#3682": "https://github.com/mapbox/mapbox-gl-js/issues/3682", + "render-tests/regressions/mapbox-gl-js#4647": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/regressions/mapbox-gl-js#5370": "skip - https://github.com/mapbox/mapbox-gl-native/pull/9439", + "render-tests/regressions/mapbox-gl-js#5599": "https://github.com/mapbox/mapbox-gl-native/issues/10399", "render-tests/regressions/mapbox-gl-native#7357": "https://github.com/mapbox/mapbox-gl-native/issues/7357", "render-tests/runtime-styling/image-add-sdf": "https://github.com/mapbox/mapbox-gl-native/issues/9847", "render-tests/runtime-styling/paint-property-fill-flat-to-extrude": "https://github.com/mapbox/mapbox-gl-native/issues/6745", + "render-tests/runtime-styling/set-style-glyphs": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/runtime-styling/set-style-paint-property-fill-flat-to-extrude": "https://github.com/mapbox/mapbox-gl-native/issues/6745", "render-tests/symbol-placement/line": "needs issue", + "render-tests/symbol-placement/line-overscaled": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/symbol-placement/point": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/symbol-spacing/line-close": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/symbol-spacing/line-far": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/symbol-visibility/visible": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/text-font/camera-function": "https://github.com/mapbox/mapbox-gl-native/pull/9439", + "render-tests/text-font/chinese": "https://github.com/mapbox/mapbox-gl-native/pull/10103", "render-tests/text-pitch-alignment/auto-text-rotation-alignment-map": "https://github.com/mapbox/mapbox-gl-native/issues/9732", "render-tests/text-pitch-alignment/auto-text-rotation-alignment-viewport": "https://github.com/mapbox/mapbox-gl-native/issues/9732", "render-tests/text-pitch-alignment/map-text-rotation-alignment-map": "https://github.com/mapbox/mapbox-gl-native/issues/9732", @@ -56,22 +93,7 @@ "render-tests/text-pitch-alignment/viewport-text-rotation-alignment-viewport": "https://github.com/mapbox/mapbox-gl-native/issues/9732", "render-tests/text-pitch-scaling/line-half": "https://github.com/mapbox/mapbox-gl-native/issues/9732", "render-tests/text-size/composite-expression": "https://github.com/mapbox/mapbox-gl-native/pull/9439", - "render-tests/video/default": "skip - https://github.com/mapbox/mapbox-gl-native/issues/601", - "render-tests/heatmap-color/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-color/expression": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-color/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-intensity/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-intensity/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-intensity/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-opacity/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-opacity/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-opacity/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-radius/antimeridian": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-radius/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-radius/function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-radius/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-radius/pitch30": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-weight/default": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-weight/identity-property-function": "https://github.com/mapbox/mapbox-gl-native/issues/10146", - "render-tests/heatmap-weight/literal": "https://github.com/mapbox/mapbox-gl-native/issues/10146" + "render-tests/text-tile-edge-clipping/default": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/text-visibility/visible": "https://github.com/mapbox/mapbox-gl-native/pull/10103", + "render-tests/video/default": "skip - https://github.com/mapbox/mapbox-gl-native/issues/601" } diff --git a/src/mbgl/programs/symbol_program.hpp b/src/mbgl/programs/symbol_program.hpp index a7abf94f560..5065b364f73 100644 --- a/src/mbgl/programs/symbol_program.hpp +++ b/src/mbgl/programs/symbol_program.hpp @@ -128,23 +128,6 @@ class SymbolSizeBinder { } }; -// Return the smallest range of stops that covers the interval [lowerZoom, upperZoom] -template -Range getCoveringStops(Stops s, float lowerZoom, float upperZoom) { - assert(!s.stops.empty()); - auto minIt = s.stops.lower_bound(lowerZoom); - auto maxIt = s.stops.lower_bound(upperZoom); - - // lower_bound yields first element >= lowerZoom, but we want the *last* - // element <= lowerZoom, so if we found a stop > lowerZoom, back up by one. - if (minIt != s.stops.begin() && minIt != s.stops.end() && minIt->first > lowerZoom) { - minIt--; - } - return Range { - minIt == s.stops.end() ? s.stops.rbegin()->first : minIt->first, - maxIt == s.stops.end() ? s.stops.rbegin()->first : maxIt->first - }; -} class ConstantSymbolSizeBinder final : public SymbolSizeBinder { public: @@ -155,19 +138,12 @@ class ConstantSymbolSizeBinder final : public SymbolSizeBinder { : layoutSize(defaultValue) {} ConstantSymbolSizeBinder(const float tileZoom, const style::CameraFunction& function_, const float /*defaultValue*/) - : layoutSize(function_.evaluate(tileZoom + 1)) { - function_.stops.match( - [&] (const style::ExponentialStops& stops) { - const auto& zoomLevels = getCoveringStops(stops, tileZoom, tileZoom + 1); - coveringRanges = std::make_tuple( - zoomLevels, - Range { function_.evaluate(zoomLevels.min), function_.evaluate(zoomLevels.max) } - ); - functionInterpolationBase = stops.base; - }, - [&] (const style::IntervalStops&) { - function = function_; - } + : layoutSize(function_.evaluate(tileZoom + 1)), + function(function_) { + const Range zoomLevels = function_.getCoveringStops(tileZoom, tileZoom + 1); + coveringRanges = std::make_tuple( + zoomLevels, + Range { function_.evaluate(zoomLevels.min), function_.evaluate(zoomLevels.max) } ); } @@ -185,7 +161,7 @@ class ConstantSymbolSizeBinder final : public SymbolSizeBinder { const Range& zoomLevels = std::get<0>(*coveringRanges); const Range& sizeLevels = std::get<1>(*coveringRanges); float t = util::clamp( - util::interpolationFactor(*functionInterpolationBase, zoomLevels, currentZoom), + function->interpolationFactor(zoomLevels, currentZoom), 0.0f, 1.0f ); size = sizeLevels.min + t * (sizeLevels.max - sizeLevels.min); @@ -198,10 +174,7 @@ class ConstantSymbolSizeBinder final : public SymbolSizeBinder { } float layoutSize; - // used for exponential functions optional, Range>> coveringRanges; - optional functionInterpolationBase; - // used for interval functions optional> function; }; @@ -226,7 +199,7 @@ class SourceFunctionSymbolSizeBinder final : public SymbolSizeBinder { return { true, false, unused, unused, unused }; } - const style::SourceFunction& function; + style::SourceFunction function; const float defaultValue; }; @@ -237,9 +210,7 @@ class CompositeFunctionSymbolSizeBinder final : public SymbolSizeBinder { : function(function_), defaultValue(defaultValue_), layoutZoom(tileZoom + 1), - coveringZoomStops(function.stops.match( - [&] (const auto& stops) { - return getCoveringStops(stops, tileZoom, tileZoom + 1); })) + coveringZoomStops(function.getCoveringStops(tileZoom, tileZoom + 1)) {} Range getVertexSizeData(const GeometryTileFeature& feature) override { @@ -251,7 +222,7 @@ class CompositeFunctionSymbolSizeBinder final : public SymbolSizeBinder { ZoomEvaluatedSize evaluateForZoom(float currentZoom) const override { float sizeInterpolationT = util::clamp( - util::interpolationFactor(1.0f, coveringZoomStops, currentZoom), + function.interpolationFactor(coveringZoomStops, currentZoom), 0.0f, 1.0f ); @@ -259,7 +230,7 @@ class CompositeFunctionSymbolSizeBinder final : public SymbolSizeBinder { return { false, false, sizeInterpolationT, unused, unused }; } - const style::CompositeFunction& function; + style::CompositeFunction function; const float defaultValue; float layoutZoom; Range coveringZoomStops; diff --git a/src/mbgl/renderer/paint_property_binder.hpp b/src/mbgl/renderer/paint_property_binder.hpp index 652948c8df8..3a49882f12f 100644 --- a/src/mbgl/renderer/paint_property_binder.hpp +++ b/src/mbgl/renderer/paint_property_binder.hpp @@ -190,11 +190,11 @@ class CompositeFunctionPaintPropertyBinder : public PaintPropertyBinder { CompositeFunctionPaintPropertyBinder(style::CompositeFunction function_, float zoom, T defaultValue_) : function(std::move(function_)), defaultValue(std::move(defaultValue_)), - rangeOfCoveringRanges(function.rangeOfCoveringRanges({zoom, zoom + 1})) { + zoomRange({zoom, zoom + 1}) { } void populateVertexVector(const GeometryTileFeature& feature, std::size_t length) override { - Range range = function.evaluate(rangeOfCoveringRanges, feature, defaultValue); + Range range = function.evaluate(zoomRange, feature, defaultValue); this->statistics.add(range.min); this->statistics.add(range.max); AttributeValue value = zoomInterpolatedAttributeValue( @@ -219,9 +219,9 @@ class CompositeFunctionPaintPropertyBinder : public PaintPropertyBinder { float interpolationFactor(float currentZoom) const override { if (function.useIntegerZoom) { - return util::interpolationFactor(1.0f, { rangeOfCoveringRanges.min.zoom, rangeOfCoveringRanges.max.zoom }, std::floor(currentZoom)); + return function.interpolationFactor(zoomRange, std::floor(currentZoom)); } else { - return util::interpolationFactor(1.0f, { rangeOfCoveringRanges.min.zoom, rangeOfCoveringRanges.max.zoom }, currentZoom); + return function.interpolationFactor(zoomRange, currentZoom); } } @@ -237,8 +237,7 @@ class CompositeFunctionPaintPropertyBinder : public PaintPropertyBinder { private: style::CompositeFunction function; T defaultValue; - using CoveringRanges = typename style::CompositeFunction::CoveringRanges; - Range rangeOfCoveringRanges; + Range zoomRange; gl::VertexVector vertexVector; optional> vertexBuffer; }; diff --git a/src/mbgl/style/conversion/get_json_type.cpp b/src/mbgl/style/conversion/get_json_type.cpp new file mode 100644 index 00000000000..cd3b4608b1a --- /dev/null +++ b/src/mbgl/style/conversion/get_json_type.cpp @@ -0,0 +1,34 @@ +#include +#include + +namespace mbgl { +namespace style { +namespace conversion { + +std::string getJSONType(const Convertible& value) { + if (isUndefined(value)) { + return "null"; + } + if (isArray(value)) { + return "array"; + } + if (isObject(value)) { + return "object"; + } + optional v = toValue(value); + + // Since we've already checked the non-atomic types above, value must then + // be a string, number, or boolean -- thus, assume that the toValue() + // conversion succeeds. + assert(v); + + return v->match( + [&] (const std::string&) { return "string"; }, + [&] (bool) { return "boolean"; }, + [&] (auto) { return "number"; } + ); +} + +} // namespace conversion +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/array_assertion.cpp b/src/mbgl/style/expression/array_assertion.cpp new file mode 100644 index 00000000000..a62f67fbb59 --- /dev/null +++ b/src/mbgl/style/expression/array_assertion.cpp @@ -0,0 +1,85 @@ +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult ArrayAssertion::evaluate(const EvaluationContext& params) const { + auto result = input->evaluate(params); + if (!result) { + return result.error(); + } + type::Type expected = getType(); + type::Type actual = typeOf(*result); + if (checkSubtype(expected, actual)) { + return EvaluationError { + "Expected value to be of type " + toString(expected) + + ", but found " + toString(actual) + " instead." + }; + } + return *result; +} + +void ArrayAssertion::eachChild(const std::function& visit) const { + visit(*input); +} + +using namespace mbgl::style::conversion; +ParseResult ArrayAssertion::parse(const Convertible& value, ParsingContext& ctx) { + + static std::unordered_map itemTypes { + {"string", type::String}, + {"number", type::Number}, + {"boolean", type::Boolean} + }; + + auto length = arrayLength(value); + if (length < 2 || length > 4) { + ctx.error("Expected 1, 2, or 3 arguments, but found " + std::to_string(length - 1) + " instead."); + return ParseResult(); + } + + optional itemType; + optional N; + if (length > 2) { + optional itemTypeName = toString(arrayMember(value, 1)); + auto it = itemTypeName ? itemTypes.find(*itemTypeName) : itemTypes.end(); + if (it == itemTypes.end()) { + ctx.error( + R"(The item type argument of "array" must be one of string, number, boolean)", + 1 + ); + return ParseResult(); + } + itemType = it->second; + } else { + itemType = {type::Value}; + } + + if (length > 3) { + auto n = toNumber(arrayMember(value, 2)); + if (!n || *n != std::floor(*n)) { + ctx.error( + R"(The length argument to "array" must be a positive integer literal.)", + 2 + ); + return ParseResult(); + } + N = optional(*n); + } + + auto input = ctx.parse(arrayMember(value, length - 1), length - 1, {type::Value}); + if (!input) { + return input; + } + + return ParseResult(std::make_unique( + type::Array(*itemType, N), + std::move(*input) + )); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/assertion.cpp b/src/mbgl/style/expression/assertion.cpp new file mode 100644 index 00000000000..a17c53cf549 --- /dev/null +++ b/src/mbgl/style/expression/assertion.cpp @@ -0,0 +1,73 @@ +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +using namespace mbgl::style::conversion; +ParseResult Assertion::parse(const Convertible& value, ParsingContext& ctx) { + static std::unordered_map types { + {"string", type::String}, + {"number", type::Number}, + {"boolean", type::Boolean}, + {"object", type::Object} + }; + + std::size_t length = arrayLength(value); + + if (length < 2) { + ctx.error("Expected at least one argument."); + return ParseResult(); + } + + auto it = types.find(*toString(arrayMember(value, 0))); + assert(it != types.end()); + + std::vector> parsed; + parsed.reserve(length - 1); + for (std::size_t i = 1; i < length; i++) { + ParseResult input = ctx.parse(arrayMember(value, i), i, {type::Value}); + if (!input) return ParseResult(); + parsed.push_back(std::move(*input)); + } + + return ParseResult(std::make_unique(it->second, std::move(parsed))); +} + +EvaluationResult Assertion::evaluate(const EvaluationContext& params) const { + for (std::size_t i = 0; i < inputs.size(); i++) { + EvaluationResult value = inputs[i]->evaluate(params); + if (!value) return value; + if (!type::checkSubtype(getType(), typeOf(*value))) { + return value; + } else if (i == inputs.size() - 1) { + return EvaluationError { + "Expected value to be of type " + toString(getType()) + + ", but found " + toString(typeOf(*value)) + " instead." + }; + } + } + + assert(false); + return EvaluationError { "Unreachable" }; +}; + +void Assertion::eachChild(const std::function& visit) const { + for(const std::unique_ptr& input : inputs) { + visit(*input); + } +}; + +bool Assertion::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return getType() == rhs->getType() && Expression::childrenEqual(inputs, rhs->inputs); + } + return false; +} + +} // namespace expression +} // namespace style +} // namespace mbgl + + diff --git a/src/mbgl/style/expression/at.cpp b/src/mbgl/style/expression/at.cpp new file mode 100644 index 00000000000..d9beb63b529 --- /dev/null +++ b/src/mbgl/style/expression/at.cpp @@ -0,0 +1,63 @@ +#include + + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult At::evaluate(const EvaluationContext& params) const { + const EvaluationResult evaluatedIndex = index->evaluate(params); + const EvaluationResult evaluatedInput = input->evaluate(params); + if (!evaluatedIndex) { + return evaluatedIndex.error(); + } + if (!evaluatedInput) { + return evaluatedInput.error(); + } + + const auto i = evaluatedIndex->get(); + const auto inputArray = evaluatedInput->get>(); + + if (i < 0 || i >= inputArray.size()) { + return EvaluationError { + "Array index out of bounds: " + stringify(i) + + " > " + std::to_string(inputArray.size()) + "." + }; + } + if (i != std::floor(i)) { + return EvaluationError { + "Array index must be an integer, but found " + stringify(i) + " instead." + }; + } + return inputArray[static_cast(i)]; +} + +void At::eachChild(const std::function& visit) const { + visit(*index); + visit(*input); +} + +using namespace mbgl::style::conversion; +ParseResult At::parse(const Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + + std::size_t length = arrayLength(value); + if (length != 3) { + ctx.error("Expected 2 arguments, but found " + std::to_string(length - 1) + " instead."); + return ParseResult(); + } + + ParseResult index = ctx.parse(arrayMember(value, 1), 1, {type::Number}); + + type::Type inputType = type::Array(ctx.getExpected() ? *ctx.getExpected() : type::Value); + ParseResult input = ctx.parse(arrayMember(value, 2), 2, {inputType}); + + if (!index || !input) return ParseResult(); + + return ParseResult(std::make_unique(std::move(*index), std::move(*input))); + +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/boolean_operator.cpp b/src/mbgl/style/expression/boolean_operator.cpp new file mode 100644 index 00000000000..88797f965a0 --- /dev/null +++ b/src/mbgl/style/expression/boolean_operator.cpp @@ -0,0 +1,87 @@ +#include + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult Any::evaluate(const EvaluationContext& params) const { + for (auto it = inputs.begin(); it != inputs.end(); it++) { + const EvaluationResult result = (*it)->evaluate(params); + if (!result) return result; + if (result->get()) return EvaluationResult(true); + } + return EvaluationResult(false); +} + +void Any::eachChild(const std::function& visit) const { + for (const std::unique_ptr& input : inputs) { + visit(*input); + } +} + +bool Any::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return Expression::childrenEqual(inputs, rhs->inputs); + } + return false; +} + + +EvaluationResult All::evaluate(const EvaluationContext& params) const { + for (auto it = inputs.begin(); it != inputs.end(); it++) { + const EvaluationResult result = (*it)->evaluate(params); + if (!result) return result; + if (!result->get()) return EvaluationResult(false); + } + return EvaluationResult(true); +} + +void All::eachChild(const std::function& visit) const { + for (const std::unique_ptr& input : inputs) { + visit(*input); + } +} + +bool All::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return Expression::childrenEqual(inputs, rhs->inputs); + } + return false; +} + +using namespace mbgl::style::conversion; + +template +ParseResult parseBooleanOp(const Convertible& value, ParsingContext& ctx) { + + assert(isArray(value)); + auto length = arrayLength(value); + + std::vector> parsedInputs; + + parsedInputs.reserve(length - 1); + for (std::size_t i = 1; i < length; i++) { + auto parsed = ctx.parse(arrayMember(value, i), i, {type::Boolean}); + if (!parsed) { + return parsed; + } + + parsedInputs.push_back(std::move(*parsed)); + } + + return ParseResult(std::make_unique(std::move(parsedInputs))); +} + +ParseResult Any::parse(const Convertible& value, ParsingContext& ctx) { + return parseBooleanOp(value, ctx); +} + +ParseResult All::parse(const Convertible& value, ParsingContext& ctx) { + return parseBooleanOp(value, ctx); +} + + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/src/mbgl/style/expression/case.cpp b/src/mbgl/style/expression/case.cpp new file mode 100644 index 00000000000..a435b71fc56 --- /dev/null +++ b/src/mbgl/style/expression/case.cpp @@ -0,0 +1,90 @@ +#include + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult Case::evaluate(const EvaluationContext& params) const { + for (const auto& branch : branches) { + const EvaluationResult evaluatedTest = branch.first->evaluate(params); + if (!evaluatedTest) { + return evaluatedTest.error(); + } + if (evaluatedTest->get()) { + return branch.second->evaluate(params); + } + } + + return otherwise->evaluate(params); +} + +void Case::eachChild(const std::function& visit) const { + for (const Branch& branch : branches) { + visit(*branch.first); + visit(*branch.second); + } + visit(*otherwise); +} + +bool Case::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return *otherwise == *(rhs->otherwise) && Expression::childrenEqual(branches, rhs->branches); + } + return false; +} + +using namespace mbgl::style::conversion; +ParseResult Case::parse(const Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + auto length = arrayLength(value); + if (length < 4) { + ctx.error("Expected at least 3 arguments, but found only " + std::to_string(length - 1) + "."); + return ParseResult(); + } + + // Expect even-length array: ["case", 2 * (n pairs)..., otherwise] + if (length % 2 != 0) { + ctx.error("Expected an odd number of arguments"); + return ParseResult(); + } + + optional outputType; + if (ctx.getExpected() && *ctx.getExpected() != type::Value) { + outputType = ctx.getExpected(); + } + + std::vector branches; + branches.reserve((length - 2) / 2); + for (size_t i = 1; i + 1 < length; i += 2) { + auto test = ctx.parse(arrayMember(value, i), i, {type::Boolean}); + if (!test) { + return test; + } + + auto output = ctx.parse(arrayMember(value, i + 1), i + 1, outputType); + if (!output) { + return output; + } + + if (!outputType) { + outputType = (*output)->getType(); + } + + branches.push_back(std::make_pair(std::move(*test), std::move(*output))); + } + + assert(outputType); + + auto otherwise = ctx.parse(arrayMember(value, length - 1), length - 1, outputType); + if (!otherwise) { + return otherwise; + } + + return ParseResult(std::make_unique(*outputType, + std::move(branches), + std::move(*otherwise))); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/check_subtype.cpp b/src/mbgl/style/expression/check_subtype.cpp new file mode 100644 index 00000000000..04a1643f0ca --- /dev/null +++ b/src/mbgl/style/expression/check_subtype.cpp @@ -0,0 +1,60 @@ +#include +#include + +namespace mbgl { +namespace style { +namespace expression { +namespace type { + +std::string errorMessage(const Type& expected, const Type& t) { + return {"Expected " + toString(expected) + " but found " + toString(t) + " instead."}; +} + +optional checkSubtype(const Type& expected, const Type& t) { + if (t.is()) return {}; + + optional result = expected.match( + [&] (const Array& expectedArray) -> optional { + if (!t.is()) { return {errorMessage(expected, t)}; } + const auto& actualArray = t.get(); + const auto err = checkSubtype(expectedArray.itemType, actualArray.itemType); + if (err) return { errorMessage(expected, t) }; + if (expectedArray.N && expectedArray.N != actualArray.N) return { errorMessage(expected, t) }; + return {}; + }, + [&] (const ValueType&) -> optional { + if (t.is()) return {}; + + const Type members[] = { + Null, + Boolean, + Number, + String, + Object, + Color, + Array(Value) + }; + + for (const auto& member : members) { + const auto err = checkSubtype(member, t); + if (!err) { + return {}; + } + } + return { errorMessage(expected, t) }; + }, + [&] (const auto&) -> optional { + if (expected != t) { + return { errorMessage(expected, t) }; + } + return {}; + } + ); + + return result; +} + +} // namespace type +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/coalesce.cpp b/src/mbgl/style/expression/coalesce.cpp new file mode 100644 index 00000000000..bfde3c75814 --- /dev/null +++ b/src/mbgl/style/expression/coalesce.cpp @@ -0,0 +1,62 @@ +#include + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult Coalesce::evaluate(const EvaluationContext& params) const { + EvaluationResult result = Null; + for (const auto& arg : args) { + result = arg->evaluate(params); + if (!result || *result != Null) break; + } + return result; +} + +void Coalesce::eachChild(const std::function& visit) const { + for (const std::unique_ptr& arg : args) { + visit(*arg); + } +} + +bool Coalesce::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return Expression::childrenEqual(args, rhs->args); + } + return false; +} + +using namespace mbgl::style::conversion; +ParseResult Coalesce::parse(const Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + auto length = arrayLength(value); + if (length < 2) { + ctx.error("Expected at least one argument."); + return ParseResult(); + } + + optional outputType; + if (ctx.getExpected() && *ctx.getExpected() != type::Value) { + outputType = ctx.getExpected(); + } + + Coalesce::Args args; + args.reserve(length - 1); + for (std::size_t i = 1; i < length; i++) { + auto parsed = ctx.parse(arrayMember(value, i), i, outputType); + if (!parsed) { + return parsed; + } + if (!outputType) { + outputType = (*parsed)->getType(); + } + args.push_back(std::move(*parsed)); + } + + assert(outputType); + return ParseResult(std::make_unique(*outputType, std::move(args))); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/coercion.cpp b/src/mbgl/style/expression/coercion.cpp new file mode 100644 index 00000000000..f2042ffd8f4 --- /dev/null +++ b/src/mbgl/style/expression/coercion.cpp @@ -0,0 +1,143 @@ +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult toNumber(const Value& v) { + optional result = v.match( + [](const double f) -> optional { return f; }, + [](const std::string& s) -> optional { + try { + return std::stof(s); + } catch(std::exception) { + return optional(); + } + }, + [](const auto&) { return optional(); } + ); + if (!result) { + return EvaluationError { + "Could not convert " + stringify(v) + " to number." + }; + } + return *result; +} + +EvaluationResult toColor(const Value& colorValue) { + return colorValue.match( + [&](const std::string& colorString) -> EvaluationResult { + const optional result = Color::parse(colorString); + if (result) { + return *result; + } else { + return EvaluationError{ + "Could not parse color from value '" + colorString + "'" + }; + } + }, + [&](const std::vector& components) -> EvaluationResult { + std::size_t len = components.size(); + bool isNumeric = std::all_of(components.begin(), components.end(), [](const Value& item) { + return item.template is(); + }); + if ((len == 3 || len == 4) && isNumeric) { + Result c = {rgba( + components[0].template get(), + components[1].template get(), + components[2].template get(), + len == 4 ? components[3].template get() : 1.0 + )}; + if (!c) return c.error(); + return *c; + } else { + return EvaluationError{ + "Invalid rbga value " + stringify(colorValue) + ": expected an array containing either three or four numeric values." + }; + } + }, + [&](const auto&) -> EvaluationResult { + return EvaluationError{ + "Could not parse color from value '" + stringify(colorValue) + "'" + }; + } + ); +} + +Coercion::Coercion(type::Type type_, std::vector> inputs_) : + Expression(std::move(type_)), + inputs(std::move(inputs_)) +{ + type::Type t = getType(); + if (t.is()) { + coerceSingleValue = toNumber; + } else if (t.is()) { + coerceSingleValue = toColor; + } else { + assert(false); + } +} + +using namespace mbgl::style::conversion; +ParseResult Coercion::parse(const Convertible& value, ParsingContext& ctx) { + static std::unordered_map types { + {"to-number", type::Number}, + {"to-color", type::Color} + }; + + std::size_t length = arrayLength(value); + + if (length < 2) { + ctx.error("Expected at least one argument."); + return ParseResult(); + } + + auto it = types.find(*toString(arrayMember(value, 0))); + assert(it != types.end()); + + std::vector> parsed; + parsed.reserve(length - 1); + for (std::size_t i = 1; i < length; i++) { + ParseResult input = ctx.parse(arrayMember(value, i), i, {type::Value}); + if (!input) return ParseResult(); + parsed.push_back(std::move(*input)); + } + + return ParseResult(std::make_unique(it->second, std::move(parsed))); +} + +EvaluationResult Coercion::evaluate(const EvaluationContext& params) const { + for (std::size_t i = 0; i < inputs.size(); i++) { + EvaluationResult value = inputs[i]->evaluate(params); + if (!value) return value; + EvaluationResult coerced = coerceSingleValue(*value); + if (coerced || i == inputs.size() - 1) { + return coerced; + } + } + + assert(false); + return EvaluationError { "Unreachable" }; +}; + +void Coercion::eachChild(const std::function& visit) const { + for(const std::unique_ptr& input : inputs) { + visit(*input); + } +}; + +bool Coercion::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return getType() == rhs->getType() && Expression::childrenEqual(inputs, rhs->inputs); + } + return false; +} + +} // namespace expression +} // namespace style +} // namespace mbgl + + + diff --git a/src/mbgl/style/expression/compound_expression.cpp b/src/mbgl/style/expression/compound_expression.cpp new file mode 100644 index 00000000000..c1e06395620 --- /dev/null +++ b/src/mbgl/style/expression/compound_expression.cpp @@ -0,0 +1,571 @@ +#include +#include +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +namespace detail { + +/* + The Signature structs are wrappers around an "evaluate()" function whose + purpose is to extract the necessary Type data from the evaluate function's + type. There are three key (partial) specializations: + + Signature: + Wraps a simple evaluate function (const T0&, const T1&, ...) -> Result + + Signature&)>: + Wraps an evaluate function that takes an arbitrary number of arguments (via + a Varargs, which is just an alias for std::vector). + + Signature: + Wraps an evaluate function that needs to access the expression evaluation + parameters in addition to its subexpressions, i.e., + (const EvaluationParams& const T0&, const T1&, ...) -> Result. Needed + for expressions like ["zoom"], ["get", key], etc. + + In each of the above evaluate signatures, T0, T1, etc. are the types of + the successfully evaluated subexpressions. +*/ +template +struct Signature; + +// Simple evaluate function (const T0&, const T1&, ...) -> Result +template +struct Signature : SignatureBase { + using Args = std::array, sizeof...(Params)>; + + Signature(R (*evaluate_)(Params...)) : + SignatureBase( + valueTypeToExpressionType>(), + std::vector {valueTypeToExpressionType>()...} + ), + evaluate(evaluate_) + {} + + EvaluationResult apply(const EvaluationContext& evaluationParameters, const Args& args) const { + return applyImpl(evaluationParameters, args, std::index_sequence_for{}); + } + + std::unique_ptr makeExpression(const std::string& name, + std::vector> args) const override { + typename Signature::Args argsArray; + std::copy_n(std::make_move_iterator(args.begin()), sizeof...(Params), argsArray.begin()); + return std::make_unique>(name, *this, std::move(argsArray)); + } + + R (*evaluate)(Params...); +private: + template + EvaluationResult applyImpl(const EvaluationContext& evaluationParameters, const Args& args, std::index_sequence) const { + const std::array evaluated = {{std::get(args)->evaluate(evaluationParameters)...}}; + for (const auto& arg : evaluated) { + if(!arg) return arg.error(); + } + const R value = evaluate(*fromExpressionValue>(*(evaluated[I]))...); + if (!value) return value.error(); + return *value; + } +}; + +// Varargs evaluate function (const Varargs&) -> Result +template +struct Signature&)> : SignatureBase { + using Args = std::vector>; + + Signature(R (*evaluate_)(const Varargs&)) : + SignatureBase( + valueTypeToExpressionType>(), + VarargsType { valueTypeToExpressionType() } + ), + evaluate(evaluate_) + {} + + std::unique_ptr makeExpression(const std::string& name, + std::vector> args) const override { + return std::make_unique>(name, *this, std::move(args)); + }; + + EvaluationResult apply(const EvaluationContext& evaluationParameters, const Args& args) const { + Varargs evaluated; + evaluated.reserve(args.size()); + for (const auto& arg : args) { + const EvaluationResult evaluatedArg = arg->evaluate(evaluationParameters); + if(!evaluatedArg) return evaluatedArg.error(); + evaluated.push_back(*fromExpressionValue>(*evaluatedArg)); + } + const R value = evaluate(evaluated); + if (!value) return value.error(); + return *value; + } + + R (*evaluate)(const Varargs&); +}; + +// Evaluate function needing parameter access, +// (const EvaluationParams&, const T0&, const T1&, ...) -> Result +template +struct Signature : SignatureBase { + using Args = std::array, sizeof...(Params)>; + + Signature(R (*evaluate_)(const EvaluationContext&, Params...)) : + SignatureBase( + valueTypeToExpressionType>(), + std::vector {valueTypeToExpressionType>()...} + ), + evaluate(evaluate_) + {} + + std::unique_ptr makeExpression(const std::string& name, + std::vector> args) const override { + typename Signature::Args argsArray; + std::copy_n(std::make_move_iterator(args.begin()), sizeof...(Params), argsArray.begin()); + return std::make_unique>(name, *this, std::move(argsArray)); + } + + EvaluationResult apply(const EvaluationContext& evaluationParameters, const Args& args) const { + return applyImpl(evaluationParameters, args, std::index_sequence_for{}); + } + +private: + template + EvaluationResult applyImpl(const EvaluationContext& evaluationParameters, const Args& args, std::index_sequence) const { + const std::array evaluated = {{std::get(args)->evaluate(evaluationParameters)...}}; + for (const auto& arg : evaluated) { + if(!arg) return arg.error(); + } + // TODO: assert correct runtime type of each arg value + const R value = evaluate(evaluationParameters, *fromExpressionValue>(*(evaluated[I]))...); + if (!value) return value.error(); + return *value; + } + + R (*evaluate)(const EvaluationContext&, Params...); +}; + +// Machinery to pull out function types from class methods, lambdas, etc. +template +struct Signature + : Signature +{ using Signature::Signature; }; + +template +struct Signature + : Signature +{ using Signature::Signature; }; + +template +struct Signature + : Signature +{ using Signature::Signature; }; + +template +struct Signature::value>> + : Signature +{ using Signature::Signature; }; + +} // namespace detail + +using Definition = CompoundExpressionRegistry::Definition; + +template +Result equal(const T& lhs, const T& rhs) { return lhs == rhs; } + +template +Result notEqual(const T& lhs, const T& rhs) { return lhs != rhs; } + +template +static std::unique_ptr makeSignature(Fn evaluateFunction) { + return std::make_unique>(evaluateFunction); +} + +std::unordered_map initializeDefinitions() { + std::unordered_map definitions; + auto define = [&](std::string name, auto fn) { + definitions[name].push_back(makeSignature(fn)); + }; + + define("e", []() -> Result { return 2.718281828459045; }); + define("pi", []() -> Result { return 3.141592653589793; }); + define("ln2", []() -> Result { return 0.6931471805599453; }); + + define("typeof", [](const Value& v) -> Result { return toString(typeOf(v)); }); + + define("to-string", [](const Value& value) -> Result { + return value.match( + [](const Color& c) -> Result { return c.stringify(); }, // avoid quoting + [](const std::string& s) -> Result { return s; }, // avoid quoting + [](const auto& v) -> Result { return stringify(v); } + ); + }); + + define("to-boolean", [](const Value& v) -> Result { + return v.match( + [&] (double f) { return (bool)f; }, + [&] (const std::string& s) { return s.length() > 0; }, + [&] (bool b) { return b; }, + [&] (const NullValue&) { return false; }, + [&] (const auto&) { return true; } + ); + }); + define("to-rgba", [](const Color& color) -> Result> { + return std::array {{ color.r, color.g, color.b, color.a }}; + }); + + define("rgba", rgba); + define("rgb", [](double r, double g, double b) { return rgba(r, g, b, 1.0f); }); + + define("zoom", [](const EvaluationContext& params) -> Result { + if (!params.zoom) { + return EvaluationError { + "The 'zoom' expression is unavailable in the current evaluation context." + }; + } + return *(params.zoom); + }); + + define("heatmap-density", [](const EvaluationContext& params) -> Result { + if (!params.heatmapDensity) { + return EvaluationError { + "The 'heatmap-density' expression is unavailable in the current evaluation context." + }; + } + return *(params.heatmapDensity); + }); + + define("has", [](const EvaluationContext& params, const std::string& key) -> Result { + if (!params.feature) { + return EvaluationError { + "Feature data is unavailable in the current evaluation context." + }; + } + + return params.feature->getValue(key) ? true : false; + }); + define("has", [](const std::string& key, const std::unordered_map& object) -> Result { + return object.find(key) != object.end(); + }); + + define("get", [](const EvaluationContext& params, const std::string& key) -> Result { + if (!params.feature) { + return EvaluationError { + "Feature data is unavailable in the current evaluation context." + }; + } + + auto propertyValue = params.feature->getValue(key); + if (!propertyValue) { + return Null; + } + return Value(toExpressionValue(*propertyValue)); + }); + define("get", [](const std::string& key, const std::unordered_map& object) -> Result { + if (object.find(key) == object.end()) { + return Null; + } + return object.at(key); + }); + + define("length", [](const std::vector& arr) -> Result { + return arr.size(); + }); + define("length", [] (const std::string s) -> Result { + return s.size(); + }); + + define("properties", [](const EvaluationContext& params) -> Result> { + if (!params.feature) { + return EvaluationError { + "Feature data is unavailable in the current evaluation context." + }; + } + std::unordered_map result; + const PropertyMap properties = params.feature->getProperties(); + for (const auto& entry : properties) { + result[entry.first] = toExpressionValue(entry.second); + } + return result; + }); + + define("geometry-type", [](const EvaluationContext& params) -> Result { + if (!params.feature) { + return EvaluationError { + "Feature data is unavailable in the current evaluation context." + }; + } + + auto type = params.feature->getType(); + if (type == FeatureType::Point) { + return "Point"; + } else if (type == FeatureType::LineString) { + return "LineString"; + } else if (type == FeatureType::Polygon) { + return "Polygon"; + } else { + return "Unknown"; + } + }); + + define("id", [](const EvaluationContext& params) -> Result { + if (!params.feature) { + return EvaluationError { + "Feature data is unavailable in the current evaluation context." + }; + } + + auto id = params.feature->getID(); + if (!id) { + return Null; + } + return id->match( + [](const auto& idValue) { + return toExpressionValue(mbgl::Value(idValue)); + } + ); + }); + + define("+", [](const Varargs& args) -> Result { + double sum = 0.0f; + for (auto arg : args) { + sum += arg; + } + return sum; + }); + define("-", [](double a, double b) -> Result { return a - b; }); + define("-", [](double a) -> Result { return -a; }); + define("*", [](const Varargs& args) -> Result { + double prod = 1.0f; + for (auto arg : args) { + prod *= arg; + } + return prod; + }); + define("/", [](double a, double b) -> Result { return a / b; }); + define("%", [](double a, double b) -> Result { return fmod(a, b); }); + define("^", [](double a, double b) -> Result { return pow(a, b); }); + define("sqrt", [](double x) -> Result { return sqrt(x); }); + define("log10", [](double x) -> Result { return log10(x); }); + define("ln", [](double x) -> Result { return log(x); }); + define("log2", [](double x) -> Result { return log2(x); }); + define("sin", [](double x) -> Result { return sin(x); }); + define("cos", [](double x) -> Result { return cos(x); }); + define("tan", [](double x) -> Result { return tan(x); }); + define("asin", [](double x) -> Result { return asin(x); }); + define("acos", [](double x) -> Result { return acos(x); }); + define("atan", [](double x) -> Result { return atan(x); }); + + define("min", [](const Varargs& args) -> Result { + double result = std::numeric_limits::infinity(); + for (double arg : args) { + result = fmin(arg, result); + } + return result; + }); + define("max", [](const Varargs& args) -> Result { + double result = -std::numeric_limits::infinity(); + for (double arg : args) { + result = fmax(arg, result); + } + return result; + }); + + define("==", equal); + define("==", equal); + define("==", equal); + define("==", equal); + + define("!=", notEqual); + define("!=", notEqual); + define("!=", notEqual); + define("!=", notEqual); + + define(">", [](double lhs, double rhs) -> Result { return lhs > rhs; }); + define(">", [](const std::string& lhs, const std::string& rhs) -> Result { return lhs > rhs; }); + define(">=", [](double lhs, double rhs) -> Result { return lhs >= rhs; }); + define(">=",[](const std::string& lhs, const std::string& rhs) -> Result { return lhs >= rhs; }); + define("<", [](double lhs, double rhs) -> Result { return lhs < rhs; }); + define("<", [](const std::string& lhs, const std::string& rhs) -> Result { return lhs < rhs; }); + define("<=", [](double lhs, double rhs) -> Result { return lhs <= rhs; }); + define("<=", [](const std::string& lhs, const std::string& rhs) -> Result { return lhs <= rhs; }); + + define("!", [](bool e) -> Result { return !e; }); + + define("upcase", [](const std::string& input) -> Result { + std::string s = input; + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c){ return std::toupper(c); }); + return s; + }); + define("downcase", [](const std::string& input) -> Result { + std::string s = input; + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c){ return std::tolower(c); }); + return s; + }); + define("concat", [](const Varargs& args) -> Result { + std::string s; + for (const std::string& arg : args) { + s += arg; + } + return s; + }); + define("error", [](const std::string& input) -> Result { + return EvaluationError { input }; + }); + + return definitions; +} + +std::unordered_map CompoundExpressionRegistry::definitions = initializeDefinitions(); + +using namespace mbgl::style::conversion; +ParseResult parseCompoundExpression(const std::string name, const Convertible& value, ParsingContext& ctx) { + assert(isArray(value) && arrayLength(value) > 0); + + auto it = CompoundExpressionRegistry::definitions.find(name); + if (it == CompoundExpressionRegistry::definitions.end()) { + ctx.error( + R"(Unknown expression ")" + name + R"(". If you wanted a literal array, use ["literal", [...]].)", + 0 + ); + return ParseResult(); + } + const CompoundExpressionRegistry::Definition& definition = it->second; + + auto length = arrayLength(value); + + // Check if we have a single signature with the correct number of + // parameters. If so, then use that signature's parameter types for parsing + // (and inferring the types of) the arguments. + optional singleMatchingSignature; + for (std::size_t j = 0; j < definition.size(); j++) { + const std::unique_ptr& signature = definition[j]; + if ( + signature->params.is() || + signature->params.get>().size() == length - 1 + ) { + if (singleMatchingSignature) { + singleMatchingSignature = {}; + } else { + singleMatchingSignature = j; + } + } + } + + // parse subexpressions first + std::vector> args; + args.reserve(length - 1); + for (std::size_t i = 1; i < length; i++) { + optional expected; + + if (singleMatchingSignature) { + expected = definition[*singleMatchingSignature]->params.match( + [](const VarargsType& varargs) { return varargs.type; }, + [&](const std::vector& params_) { return params_[i - 1]; } + ); + } + + auto parsed = ctx.parse(arrayMember(value, i), i, expected); + if (!parsed) { + return parsed; + } + args.push_back(std::move(*parsed)); + } + return createCompoundExpression(name, definition, std::move(args), ctx); +} + + +ParseResult createCompoundExpression(const std::string& name, + std::vector> args, + ParsingContext& ctx) +{ + return createCompoundExpression(name, CompoundExpressionRegistry::definitions.at(name), std::move(args), ctx); +} + + +ParseResult createCompoundExpression(const std::string& name, + const Definition& definition, + std::vector> args, + ParsingContext& ctx) +{ + ParsingContext signatureContext(ctx.getKey()); + + for (const std::unique_ptr& signature : definition) { + signatureContext.clearErrors(); + + if (signature->params.is>()) { + const std::vector& params = signature->params.get>(); + if (params.size() != args.size()) { + signatureContext.error( + "Expected " + std::to_string(params.size()) + + " arguments, but found " + std::to_string(args.size()) + " instead." + ); + continue; + } + + for (std::size_t i = 0; i < args.size(); i++) { + const std::unique_ptr& arg = args[i]; + optional err = type::checkSubtype(params.at(i), arg->getType()); + if (err) { + signatureContext.error(*err, i + 1); + } + } + } else if (signature->params.is()) { + const type::Type& paramType = signature->params.get().type; + for (std::size_t i = 0; i < args.size(); i++) { + const std::unique_ptr& arg = args[i]; + optional err = type::checkSubtype(paramType, arg->getType()); + if (err) { + signatureContext.error(*err, i + 1); + } + } + } + + if (signatureContext.getErrors().size() == 0) { + return ParseResult(signature->makeExpression(name, std::move(args))); + } + } + + if (definition.size() == 1) { + ctx.appendErrors(std::move(signatureContext)); + } else { + std::string signatures; + for (const auto& signature : definition) { + signatures += (signatures.size() > 0 ? " | " : ""); + signature->params.match( + [&](const VarargsType& varargs) { + signatures += "(" + toString(varargs.type) + ")"; + }, + [&](const std::vector& params) { + signatures += "("; + bool first = true; + for (const type::Type& param : params) { + if (!first) signatures += ", "; + signatures += toString(param); + first = false; + } + signatures += ")"; + } + ); + + } + std::string actualTypes; + for (const auto& arg : args) { + if (actualTypes.size() > 0) { + actualTypes += ", "; + } + actualTypes += toString(arg->getType()); + } + ctx.error("Expected arguments of type " + signatures + ", but found (" + actualTypes + ") instead."); + } + + return ParseResult(); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/find_zoom_curve.cpp b/src/mbgl/style/expression/find_zoom_curve.cpp new file mode 100644 index 00000000000..5d39e0791ed --- /dev/null +++ b/src/mbgl/style/expression/find_zoom_curve.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +optional> findZoomCurve(const expression::Expression* e) { + optional> result; + + if (auto let = dynamic_cast(e)) { + result = findZoomCurve(let->getResult()); + } else if (auto coalesce = dynamic_cast(e)) { + std::size_t length = coalesce->getLength(); + for (std::size_t i = 0; i < length; i++) { + result = findZoomCurve(coalesce->getChild(i)); + if (result) { + break; + } + } + } else if (auto curve = dynamic_cast(e)) { + auto z = dynamic_cast(curve->getInput().get()); + if (z && z->getName() == "zoom") { + result = {curve}; + } + } else if (auto step = dynamic_cast(e)) { + auto z = dynamic_cast(step->getInput().get()); + if (z && z->getName() == "zoom") { + result = {step}; + } + } + + if (result && result->is()) { + return result; + } + + e->eachChild([&](const Expression& child) { + optional> childResult(findZoomCurve(&child)); + if (childResult) { + if (childResult->is()) { + result = childResult; + } else if (!result && childResult) { + result = {ParsingError { + R"("zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.)", "" + }}; + } else if (result && childResult && result != childResult) { + result = {ParsingError { + R"(Only one zoom-based "step" or "interpolate" subexpression may be used in an expression.)", "" + }}; + } + } + }); + + return result; +} + +variant findZoomCurveChecked(const expression::Expression* e) { + return findZoomCurve(e)->match( + [](const ParsingError&) -> variant { + assert(false); + return {}; + }, + [](auto zoomCurve) -> variant { + return {std::move(zoomCurve)}; + } + ); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/get_covering_stops.cpp b/src/mbgl/style/expression/get_covering_stops.cpp new file mode 100644 index 00000000000..c9f87d93ac5 --- /dev/null +++ b/src/mbgl/style/expression/get_covering_stops.cpp @@ -0,0 +1,26 @@ +#include + +namespace mbgl { +namespace style { +namespace expression { + +Range getCoveringStops(const std::map>& stops, + const double lower, const double upper) { + assert(!stops.empty()); + auto minIt = stops.lower_bound(lower); + auto maxIt = stops.lower_bound(upper); + + // lower_bound yields first element >= lowerZoom, but we want the *last* + // element <= lowerZoom, so if we found a stop > lowerZoom, back up by one. + if (minIt != stops.begin() && minIt != stops.end() && minIt->first > lower) { + minIt--; + } + return Range { + static_cast(minIt == stops.end() ? stops.rbegin()->first : minIt->first), + static_cast(maxIt == stops.end() ? stops.rbegin()->first : maxIt->first) + }; +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/interpolate.cpp b/src/mbgl/style/expression/interpolate.cpp new file mode 100644 index 00000000000..020aba9dcee --- /dev/null +++ b/src/mbgl/style/expression/interpolate.cpp @@ -0,0 +1,211 @@ + +#include + +namespace mbgl { +namespace style { +namespace expression { + +using Interpolator = variant; + +using namespace mbgl::style::conversion; + +ParseResult parseInterpolate(const Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + + auto length = arrayLength(value); + + if (length < 2) { + ctx.error("Expected an interpolation type expression."); + return ParseResult(); + } + + const Convertible& interp = arrayMember(value, 1); + if (!isArray(interp) || arrayLength(interp) == 0) { + ctx.error("Expected an interpolation type expression."); + return ParseResult(); + } + + optional interpolator; + + const optional interpName = toString(arrayMember(interp, 0)); + if (interpName && *interpName == "linear") { + interpolator = {ExponentialInterpolator(1.0)}; + } else if (interpName && *interpName == "exponential") { + optional base; + if (arrayLength(interp) == 2) { + base = toDouble(arrayMember(interp, 1)); + } + if (!base) { + ctx.error("Exponential interpolation requires a numeric base.", 1, 1); + return ParseResult(); + } + interpolator = {ExponentialInterpolator(*base)}; + } else if (interpName && *interpName == "cubic-bezier") { + optional x1; + optional y1; + optional x2; + optional y2; + if (arrayLength(interp) == 5) { + x1 = toDouble(arrayMember(interp, 1)); + y1 = toDouble(arrayMember(interp, 2)); + x2 = toDouble(arrayMember(interp, 3)); + y2 = toDouble(arrayMember(interp, 4)); + } + if ( + !x1 || !y1 || !x2 || !y2 || + *x1 < 0 || *x1 > 1 || + *y1 < 0 || *y1 > 1 || + *x2 < 0 || *x2 > 1 || + *y2 < 0 || *y2 > 1 + ) { + ctx.error("Cubic bezier interpolation requires four numeric arguments with values between 0 and 1.", 1); + return ParseResult(); + + } + interpolator = {CubicBezierInterpolator(*x1, *y1, *x2, *y2)}; + } + + if (!interpolator) { + ctx.error("Unknown interpolation type " + (interpName ? *interpName : ""), 1, 0); + return ParseResult(); + } + + std::size_t minArgs = 4; + if (length - 1 < minArgs) { + ctx.error("Expected at least 4 arguments, but found only " + std::to_string(length - 1) + "."); + return ParseResult(); + } + + // [interpolation, interp_type, input, 2 * (n pairs)...] + if ((length - 1) % 2 != 0) { + ctx.error("Expected an even number of arguments."); + return ParseResult(); + } + + ParseResult input = ctx.parse(arrayMember(value, 2), 2, {type::Number}); + if (!input) { + return input; + } + + std::map> stops; + optional outputType; + if (ctx.getExpected() && *ctx.getExpected() != type::Value) { + outputType = ctx.getExpected(); + } + + double previous = - std::numeric_limits::infinity(); + + for (std::size_t i = 3; i + 1 < length; i += 2) { + const optional labelValue = toValue(arrayMember(value, i)); + optional label; + optional labelError; + if (labelValue) { + labelValue->match( + [&](uint64_t n) { + if (n > std::numeric_limits::max()) { + label = {std::numeric_limits::infinity()}; + } else { + label = {static_cast(n)}; + } + }, + [&](int64_t n) { + if (n > std::numeric_limits::max()) { + label = {std::numeric_limits::infinity()}; + } else { + label = {static_cast(n)}; + } + }, + [&](double n) { + if (n > std::numeric_limits::max()) { + label = {std::numeric_limits::infinity()}; + } else { + label = {static_cast(n)}; + } + }, + [&](const auto&) {} + ); + } + if (!label) { + ctx.error(labelError ? *labelError : + R"(Input/output pairs for "interpolate" expressions must be defined using literal numeric values (not computed expressions) for the input values.)", + i); + return ParseResult(); + } + + if (*label <= previous) { + ctx.error( + R"(Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.)", + i + ); + return ParseResult(); + } + previous = *label; + + auto output = ctx.parse(arrayMember(value, i + 1), i + 1, outputType); + if (!output) { + return ParseResult(); + } + if (!outputType) { + outputType = (*output)->getType(); + } + + stops.emplace(*label, std::move(*output)); + } + + assert(outputType); + + if ( + *outputType != type::Number && + *outputType != type::Color && + !( + outputType->is() && + outputType->get().itemType == type::Number && + outputType->get().N + ) + ) + { + ctx.error("Type " + toString(*outputType) + " is not interpolatable."); + return ParseResult(); + } + + return outputType->match( + [&](const type::NumberType&) -> ParseResult { + return interpolator->match([&](const auto& interpolator_) { + return ParseResult(std::make_unique>( + *outputType, interpolator_, std::move(*input), std::move(stops) + )); + }); + }, + [&](const type::ColorType&) -> ParseResult { + return interpolator->match([&](const auto& interpolator_) { + return ParseResult(std::make_unique>( + *outputType, interpolator_, std::move(*input), std::move(stops) + )); + }); + }, + [&](const type::Array& arrayType) -> ParseResult { + return interpolator->match( + [&](const auto& continuousInterpolator) { + if (arrayType.itemType != type::Number || !arrayType.N) { + assert(false); // interpolability already checked above. + return ParseResult(); + } + return ParseResult(std::make_unique>>( + *outputType, continuousInterpolator, std::move(*input), std::move(stops) + )); + } + ); + }, + [&](const auto&) { + // unreachable: Null, Boolean, String, Object, Value output types + // are not interpolatable, and interpolability was already checked above + assert(false); + return ParseResult(); + } + ); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/is_constant.cpp b/src/mbgl/style/expression/is_constant.cpp new file mode 100644 index 00000000000..0ebb37faa99 --- /dev/null +++ b/src/mbgl/style/expression/is_constant.cpp @@ -0,0 +1,40 @@ +#include + +namespace mbgl { +namespace style { +namespace expression { + +bool isFeatureConstant(const Expression& expression) { + if (auto e = dynamic_cast(&expression)) { + const std::string name = e->getName(); + optional parameterCount = e->getParameterCount(); + if (name == "get" && parameterCount && *parameterCount == 1) { + return false; + } else if (name == "has" && parameterCount && *parameterCount == 1) { + return false; + } else if ( + name == "properties" || + name == "geometry-type" || + name == "id" + ) { + return false; + } + } + + bool featureConstant = true; + expression.eachChild([&](const Expression& e) { + if (featureConstant && !isFeatureConstant(e)) { + featureConstant = false; + } + }); + return featureConstant; +} + +bool isZoomConstant(const Expression& e) { + return isGlobalPropertyConstant(e, std::array{{"zoom"}}); +} + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/is_expression.cpp b/src/mbgl/style/expression/is_expression.cpp new file mode 100644 index 00000000000..77212f6a1e9 --- /dev/null +++ b/src/mbgl/style/expression/is_expression.cpp @@ -0,0 +1,29 @@ +#include +#include +#include + +#include + +#include + +namespace mbgl { +namespace style { +namespace expression { + +using namespace mbgl::style::conversion; + +bool isExpression(const Convertible& value) { + const ExpressionRegistry& registry = getExpressionRegistry(); + + if (!isArray(value) || arrayLength(value) == 0) return false; + optional name = toString(arrayMember(value, 0)); + if (!name) return false; + + return (registry.find(*name) != registry.end()) || + (CompoundExpressionRegistry::definitions.find(*name) != CompoundExpressionRegistry::definitions.end()); +} + + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/let.cpp b/src/mbgl/style/expression/let.cpp new file mode 100644 index 00000000000..8e206d35826 --- /dev/null +++ b/src/mbgl/style/expression/let.cpp @@ -0,0 +1,91 @@ +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult Let::evaluate(const EvaluationContext& params) const { + return result->evaluate(params); +} + +void Let::eachChild(const std::function& visit) const { + for (auto it = bindings.begin(); it != bindings.end(); it++) { + visit(*it->second); + } + visit(*result); +} + +using namespace mbgl::style::conversion; + +ParseResult Let::parse(const Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + + std::size_t length = arrayLength(value); + + if (length < 4) { + ctx.error("Expected at least 3 arguments, but found " + std::to_string(length - 1) + " instead."); + return ParseResult(); + } + + std::map> bindings_; + for(std::size_t i = 1; i < length - 1; i += 2) { + optional name = toString(arrayMember(value, i)); + if (!name) { + ctx.error("Expected string, but found " + getJSONType(arrayMember(value, i)) + " instead.", i); + return ParseResult(); + } + + bool isValidName = std::all_of(name->begin(), name->end(), [](unsigned char c) { + return std::isalnum(c) || c == '_'; + }); + if (!isValidName) { + ctx.error("Variable names must contain only alphanumeric characters or '_'.", 1); + return ParseResult(); + } + + ParseResult bindingValue = ctx.parse(arrayMember(value, i + 1), i + 1); + if (!bindingValue) { + return ParseResult(); + } + + bindings_.emplace(*name, std::move(*bindingValue)); + } + + ParseResult result_ = ctx.parse(arrayMember(value, length - 1), length - 1, ctx.getExpected(), bindings_); + if (!result_) { + return ParseResult(); + } + + return ParseResult(std::make_unique(std::move(bindings_), std::move(*result_))); +} + +EvaluationResult Var::evaluate(const EvaluationContext& params) const { + return value->evaluate(params); +} + +void Var::eachChild(const std::function&) const {} + +ParseResult Var::parse(const Convertible& value_, ParsingContext& ctx) { + assert(isArray(value_)); + + if (arrayLength(value_) != 2 || !toString(arrayMember(value_, 1))) { + ctx.error("'var' expression requires exactly one string literal argument."); + return ParseResult(); + } + + std::string name_ = *toString(arrayMember(value_, 1)); + + optional> bindingValue = ctx.getBinding(name_); + if (!bindingValue) { + ctx.error(R"(Unknown variable ")" + name_ + R"(". Make sure ")" + + name_ + R"(" has been bound in an enclosing "let" expression before using it.)", 1); + return ParseResult(); + } + + return ParseResult(std::make_unique(name_, std::move(*bindingValue))); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/literal.cpp b/src/mbgl/style/expression/literal.cpp new file mode 100644 index 00000000000..fc11878bea9 --- /dev/null +++ b/src/mbgl/style/expression/literal.cpp @@ -0,0 +1,108 @@ + +#include + +namespace mbgl { +namespace style { +namespace expression { + +template +optional checkNumber(T n) { + if (n > std::numeric_limits::max()) { + return {std::numeric_limits::infinity()}; + } else { + return {static_cast(n)}; + } +} + +using namespace mbgl::style::conversion; +optional parseValue(const Convertible& value, ParsingContext& ctx) { + if (isUndefined(value)) return {Null}; + if (isObject(value)) { + std::unordered_map result; + bool error = false; + eachMember(value, [&] (const std::string& k, const mbgl::style::conversion::Convertible& v) -> optional { + if (!error) { + optional memberValue = parseValue(v, ctx); + if (memberValue) { + result.emplace(k, *memberValue); + } else { + error = true; + } + } + return {}; + }); + return error ? optional() : optional(result); + } + + if (isArray(value)) { + std::vector result; + const auto length = arrayLength(value); + for(std::size_t i = 0; i < length; i++) { + optional item = parseValue(arrayMember(value, i), ctx); + if (item) { + result.emplace_back(*item); + } else { + return optional(); + } + } + return optional(result); + } + + optional v = toValue(value); + // since value represents a JSON value, if it's not undefined, object, or + // array, it must be convertible to mbgl::Value + assert(v); + + return v->match( + [&](uint64_t n) { return checkNumber(n); }, + [&](int64_t n) { return checkNumber(n); }, + [&](double n) { return checkNumber(n); }, + [&](const auto&) { + return optional(toExpressionValue(*v)); + } + ); +} + +ParseResult Literal::parse(const Convertible& value, ParsingContext& ctx) { + if (isObject(value)) { + ctx.error(R"(Bare objects invalid. Use ["literal", {...}] instead.)"); + return ParseResult(); + } else if (isArray(value)) { + // object or array value, quoted with ["literal", value] + if (arrayLength(value) != 2) { + ctx.error("'literal' expression requires exactly one argument, but found " + std::to_string(arrayLength(value) - 1) + " instead."); + return ParseResult(); + } + const optional parsedValue = parseValue(arrayMember(value, 1), ctx); + if (!parsedValue) { + return ParseResult(); + } + + // special case: infer the item type if possible for zero-length arrays + if ( + ctx.getExpected() && + ctx.getExpected()->template is() && + parsedValue->template is>() + ) { + auto type = typeOf(*parsedValue).template get(); + auto expected = ctx.getExpected()->template get(); + if ( + type.N && (*type.N == 0) && + (!expected.N || (*expected.N == 0)) + ) { + return ParseResult(std::make_unique(expected, parsedValue->template get>())); + } + } + + return ParseResult(std::make_unique(*parsedValue)); + } else { + // bare primitive value (string, number, boolean, null) + const optional parsedValue = parseValue(value, ctx); + return ParseResult(std::make_unique(*parsedValue)); + } +} + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/src/mbgl/style/expression/match.cpp b/src/mbgl/style/expression/match.cpp new file mode 100644 index 00000000000..6336eba1e6c --- /dev/null +++ b/src/mbgl/style/expression/match.cpp @@ -0,0 +1,262 @@ +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +template +void Match::eachChild(const std::function& visit) const { + visit(*input); + for (const std::pair>& branch : branches) { + visit(*branch.second); + } + visit(*otherwise); +} + +template +bool Match::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return (*input == *(rhs->input) && + *otherwise == *(rhs->otherwise) && + Expression::childrenEqual(branches, rhs->branches)); + } + return false; +} + + +template<> EvaluationResult Match::evaluate(const EvaluationContext& params) const { + const EvaluationResult inputValue = input->evaluate(params); + if (!inputValue) { + return inputValue.error(); + } + + auto it = branches.find(inputValue->get()); + if (it != branches.end()) { + return (*it).second->evaluate(params); + } + + return otherwise->evaluate(params); +} + +template<> EvaluationResult Match::evaluate(const EvaluationContext& params) const { + const EvaluationResult inputValue = input->evaluate(params); + if (!inputValue) { + return inputValue.error(); + } + + const auto numeric = inputValue->get(); + int64_t rounded = std::floor(numeric); + if (numeric == rounded) { + auto it = branches.find(rounded); + if (it != branches.end()) { + return (*it).second->evaluate(params); + } + } + + return otherwise->evaluate(params); +} + +template class Match; +template class Match; + +using InputType = variant; + +using namespace mbgl::style::conversion; +optional parseInputValue(const Convertible& input, ParsingContext& parentContext, std::size_t index, optional& inputType) { + using namespace mbgl::style::conversion; + optional result; + optional type; + + auto value = toValue(input); + + if (value) { + value->match( + [&] (uint64_t n) { + if (!Value::isSafeInteger(n)) { + parentContext.error("Branch labels must be integers no larger than " + std::to_string(Value::maxSafeInteger()) + ".", index); + } else { + type = {type::Number}; + result = {static_cast(n)}; + } + }, + [&] (int64_t n) { + if (!Value::isSafeInteger(n)) { + parentContext.error("Branch labels must be integers no larger than " + std::to_string(Value::maxSafeInteger()) + ".", index); + } else { + type = {type::Number}; + result = {n}; + } + }, + [&] (double n) { + if (!Value::isSafeInteger(n)) { + parentContext.error("Branch labels must be integers no larger than " + std::to_string(Value::maxSafeInteger()) + ".", index); + } else if (n != std::floor(n)) { + parentContext.error("Numeric branch labels must be integer values.", index); + } else { + type = {type::Number}; + result = {static_cast(n)}; + } + }, + [&] (const std::string& s) { + type = {type::String}; + result = {s}; + }, + [&] (const auto&) { + parentContext.error("Branch labels must be numbers or strings.", index); + } + ); + } else { + parentContext.error("Branch labels must be numbers or strings.", index); + } + + if (!type) { + return result; + } + + if (!inputType) { + inputType = type; + } else { + optional err = type::checkSubtype(*inputType, *type); + if (err) { + parentContext.error(*err, index); + return optional(); + } + } + + return result; +} + +template +static ParseResult create(type::Type outputType, + std::unique_ptrinput, + std::vector, + std::unique_ptr>> branches, + std::unique_ptr otherwise, + ParsingContext& ctx) { + typename Match::Branches typedBranches; + + std::size_t index = 2; + + typedBranches.reserve(branches.size()); + for (std::pair, + std::unique_ptr>& pair : branches) { + std::shared_ptr result = std::move(pair.second); + for (const InputType& label : pair.first) { + const auto& typedLabel = label.template get(); + if (typedBranches.find(typedLabel) != typedBranches.end()) { + ctx.error("Branch labels must be unique.", index); + return ParseResult(); + } + typedBranches.emplace(typedLabel, result); + } + + index += 2; + } + return ParseResult(std::make_unique>( + outputType, + std::move(input), + std::move(typedBranches), + std::move(otherwise) + )); +} + +ParseResult parseMatch(const Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + auto length = arrayLength(value); + if (length < 5) { + ctx.error( + "Expected at least 4 arguments, but found only " + std::to_string(length - 1) + "." + ); + return ParseResult(); + } + + // Expect odd-length array: ["match", input, 2 * (n pairs)..., otherwise] + if (length % 2 != 1) { + ctx.error("Expected an even number of arguments."); + return ParseResult(); + } + + optional inputType; + optional outputType; + if (ctx.getExpected() && *ctx.getExpected() != type::Value) { + outputType = ctx.getExpected(); + } + + std::vector, + std::unique_ptr>> branches; + + branches.reserve((length - 3) / 2); + for (size_t i = 2; i + 1 < length; i += 2) { + const auto& label = arrayMember(value, i); + + std::vector labels; + // Match pair inputs are provided as either a literal value or a + // raw JSON array of string / number / boolean values. + if (isArray(label)) { + auto groupLength = arrayLength(label); + if (groupLength == 0) { + ctx.error("Expected at least one branch label.", i); + return ParseResult(); + } + + labels.reserve(groupLength); + for (size_t j = 0; j < groupLength; j++) { + const optional inputValue = parseInputValue(arrayMember(label, j), ctx, i, inputType); + if (!inputValue) { + return ParseResult(); + } + labels.push_back(*inputValue); + } + } else { + const optional inputValue = parseInputValue(label, ctx, i, inputType); + if (!inputValue) { + return ParseResult(); + } + labels.push_back(*inputValue); + } + + ParseResult output = ctx.parse(arrayMember(value, i + 1), i + 1, outputType); + if (!output) { + return ParseResult(); + } + + if (!outputType) { + outputType = (*output)->getType(); + } + + branches.push_back(std::make_pair(std::move(labels), std::move(*output))); + } + + auto input = ctx.parse(arrayMember(value, 1), 1, inputType); + if (!input) { + return ParseResult(); + } + + auto otherwise = ctx.parse(arrayMember(value, length - 1), length - 1, outputType); + if (!otherwise) { + return ParseResult(); + } + + assert(inputType && outputType); + + return inputType->match( + [&](const type::NumberType&) { + return create(*outputType, std::move(*input), std::move(branches), std::move(*otherwise), ctx); + }, + [&](const type::StringType&) { + return create(*outputType, std::move(*input), std::move(branches), std::move(*otherwise), ctx); + }, + [&](const auto&) { + // unreachable: inputType is set by parseInputValue(), which only + // accepts string and (integer) numeric values. + assert(false); + return ParseResult(); + } + ); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp new file mode 100644 index 00000000000..81cbdede593 --- /dev/null +++ b/src/mbgl/style/expression/parsing_context.cpp @@ -0,0 +1,206 @@ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace mbgl { +namespace style { +namespace expression { + +bool isConstant(const Expression& expression) { + if (dynamic_cast(&expression)) { + return false; + } + + if (auto compound = dynamic_cast(&expression)) { + if (compound->getName() == "error") { + return false; + } + } + + bool literalArgs = true; + expression.eachChild([&](const Expression& child) { + if (!dynamic_cast(&child)) { + literalArgs = false; + } + }); + if (!literalArgs) { + return false; + } + + return isFeatureConstant(expression) && + isGlobalPropertyConstant(expression, std::array{{"zoom", "heatmap-density"}}); +} + +using namespace mbgl::style::conversion; + +ParseResult ParsingContext::parse(const Convertible& value, std::size_t index_, optional expected_) { + ParsingContext child(key + "[" + std::to_string(index_) + "]", + errors, + std::move(expected_), + scope); + return child.parse(value); +} + +ParseResult ParsingContext::parse(const Convertible& value, std::size_t index_, optional expected_, + const std::map>& bindings) { + ParsingContext child(key + "[" + std::to_string(index_) + "]", + errors, + std::move(expected_), + std::make_shared(bindings, scope)); + return child.parse(value); +} + +const ExpressionRegistry& getExpressionRegistry() { + static ExpressionRegistry registry {{ + {"all", All::parse}, + {"any", Any::parse}, + {"array", ArrayAssertion::parse}, + {"at", At::parse}, + {"boolean", Assertion::parse}, + {"case", Case::parse}, + {"coalesce", Coalesce::parse}, + {"interpolate", parseInterpolate}, + {"let", Let::parse}, + {"literal", Literal::parse}, + {"match", parseMatch}, + {"number", Assertion::parse}, + {"object", Assertion::parse}, + {"step", Step::parse}, + {"string", Assertion::parse}, + {"to-color", Coercion::parse}, + {"to-number", Coercion::parse}, + {"var", Var::parse} + }}; + return registry; +} + +ParseResult ParsingContext::parse(const Convertible& value) +{ + ParseResult parsed; + + if (isArray(value)) { + const std::size_t length = arrayLength(value); + if (length == 0) { + error(R"(Expected an array with at least one element. If you wanted a literal array, use ["literal", []].)"); + return ParseResult(); + } + + const optional op = toString(arrayMember(value, 0)); + if (!op) { + error( + "Expression name must be a string, but found " + getJSONType(arrayMember(value, 0)) + + R"( instead. If you wanted a literal array, use ["literal", [...]].)", + 0 + ); + return ParseResult(); + } + + const ExpressionRegistry& registry = getExpressionRegistry(); + auto parseFunction = registry.find(*op); + if (parseFunction != registry.end()) { + parsed = parseFunction->second(value, *this); + } else { + parsed = parseCompoundExpression(*op, value, *this); + } + } else { + parsed = Literal::parse(value, *this); + } + + if (!parsed) { + assert(errors->size() > 0); + } else if (expected) { + auto wrapForType = [&](const type::Type& target, std::unique_ptr expression) -> std::unique_ptr { + std::vector> args; + args.push_back(std::move(expression)); + if (target == type::Color) { + return std::make_unique(target, std::move(args)); + } else { + return std::make_unique(target, std::move(args)); + } + }; + + const type::Type actual = (*parsed)->getType(); + if (*expected == type::Color && (actual == type::String || actual == type::Value)) { + parsed = wrapForType(type::Color, std::move(*parsed)); + } else if ((*expected == type::String || *expected == type::Number || *expected == type::Boolean) && actual == type::Value) { + parsed = wrapForType(*expected, std::move(*parsed)); + } + + checkType((*parsed)->getType()); + if (errors->size() > 0) { + return ParseResult(); + } + } + + // If an expression's arguments are all literals, we can evaluate + // it immediately and replace it with a literal value in the + // parsed result. + if (parsed && !dynamic_cast(parsed->get()) && isConstant(**parsed)) { + EvaluationContext params(nullptr); + EvaluationResult evaluated((*parsed)->evaluate(params)); + if (!evaluated) { + error(evaluated.error().message); + return ParseResult(); + } + + const type::Type type = (*parsed)->getType(); + if (type.is()) { + // keep the original expression's array type, even if the evaluated + // type is more specific. + return ParseResult(std::make_unique( + type.get(), + evaluated->get>()) + ); + } else { + return ParseResult(std::make_unique(*evaluated)); + } + } + + // if this is the root expression, enforce constraints on the use ["zoom"]. + if (key.size() == 0 && parsed && !isZoomConstant(**parsed)) { + optional> zoomCurve = findZoomCurve(parsed->get()); + if (!zoomCurve) { + error(R"("zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.)"); + return ParseResult(); + } else if (zoomCurve->is()) { + error(zoomCurve->get().message); + return ParseResult(); + } + } + + return parsed; +} + +optional ParsingContext::checkType(const type::Type& t) { + assert(expected); + optional err = type::checkSubtype(*expected, t); + if (err) { + error(*err); + } + return err; +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/src/mbgl/style/expression/step.cpp b/src/mbgl/style/expression/step.cpp new file mode 100644 index 00000000000..2720e9257aa --- /dev/null +++ b/src/mbgl/style/expression/step.cpp @@ -0,0 +1,151 @@ +#include +#include + + +namespace mbgl { +namespace style { +namespace expression { + +EvaluationResult Step::evaluate(const EvaluationContext& params) const { + const EvaluationResult evaluatedInput = input->evaluate(params); + if (!evaluatedInput) { return evaluatedInput.error(); } + float x = *fromExpressionValue(*evaluatedInput); + + if (stops.empty()) { + return EvaluationError { "No stops in step curve." }; + } + + auto it = stops.upper_bound(x); + if (it == stops.end()) { + return stops.rbegin()->second->evaluate(params); + } else if (it == stops.begin()) { + return stops.begin()->second->evaluate(params); + } else { + return std::prev(it)->second->evaluate(params); + } +} + +void Step::eachChild(const std::function& visit) const { + visit(*input); + for (auto it = stops.begin(); it != stops.end(); it++) { + visit(*it->second); + } +} + +bool Step::operator==(const Expression& e) const { + if (auto rhs = dynamic_cast(&e)) { + return *input == *(rhs->input) && Expression::childrenEqual(stops, rhs->stops); + } + return false; +} + +Range Step::getCoveringStops(const double lower, const double upper) const { + return ::mbgl::style::expression::getCoveringStops(stops, lower, upper); +} + + +ParseResult Step::parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx) { + assert(isArray(value)); + + auto length = arrayLength(value); + + if (length - 1 < 4) { + ctx.error("Expected at least 4 arguments, but found only " + std::to_string(length - 1) + "."); + return ParseResult(); + } + + // [step, input, firstOutput_value, 2 * (n pairs)...] + if ((length - 1) % 2 != 0) { + ctx.error("Expected an even number of arguments."); + return ParseResult(); + } + + ParseResult input = ctx.parse(arrayMember(value, 1), 1, {type::Number}); + if (!input) { + return input; + } + + std::map> stops; + optional outputType; + if (ctx.getExpected() && *ctx.getExpected() != type::Value) { + outputType = ctx.getExpected(); + } + + double previous = - std::numeric_limits::infinity(); + + // consume the first output value, which doesn't have a corresponding input value, + // before proceeding into the "stops" loop below. + auto firstOutput = ctx.parse(arrayMember(value, 2), 2, outputType); + if (!firstOutput) { + return ParseResult(); + } + if (!outputType) { + outputType = (*firstOutput)->getType(); + } + stops.emplace(-std::numeric_limits::infinity(), std::move(*firstOutput)); + + + for (std::size_t i = 3; i + 1 < length; i += 2) { + const optional labelValue = toValue(arrayMember(value, i)); + optional label; + if (labelValue) { + labelValue->match( + [&](uint64_t n) { + if (n > std::numeric_limits::max()) { + label = {std::numeric_limits::infinity()}; + } else { + label = {static_cast(n)}; + } + }, + [&](int64_t n) { + if (n > std::numeric_limits::max()) { + label = {std::numeric_limits::infinity()}; + } else { + label = {static_cast(n)}; + } + }, + [&](double n) { + if (n > std::numeric_limits::max()) { + label = {std::numeric_limits::infinity()}; + } else { + label = {static_cast(n)}; + } + }, + [&](const auto&) {} + ); + } + if (!label) { + ctx.error(R"(Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values.)", i); + return ParseResult(); + } + + if (*label <= previous) { + ctx.error( + R"(Input/output pairs for "step" expressions must be arranged with input values in strictly ascending order.)", + i + ); + return ParseResult(); + } + previous = *label; + + auto output = ctx.parse(arrayMember(value, i + 1), i + 1, outputType); + if (!output) { + return ParseResult(); + } + if (!outputType) { + outputType = (*output)->getType(); + } + + stops.emplace(*label, std::move(*output)); + } + + assert(outputType); + + return ParseResult(std::make_unique(*outputType, std::move(*input), std::move(stops))); +} + + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/src/mbgl/style/expression/util.cpp b/src/mbgl/style/expression/util.cpp new file mode 100644 index 00000000000..f198fb3e1b7 --- /dev/null +++ b/src/mbgl/style/expression/util.cpp @@ -0,0 +1,39 @@ + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +std::string stringifyColor(double r, double g, double b, double a) { + return stringify(r) + ", " + + stringify(g) + ", " + + stringify(b) + ", " + + stringify(a); +} + +Result rgba(double r, double g, double b, double a) { + if ( + r < 0 || r > 255 || + g < 0 || g > 255 || + b < 0 || b > 255 + ) { + return EvaluationError { + "Invalid rgba value [" + stringifyColor(r, g, b, a) + + "]: 'r', 'g', and 'b' must be between 0 and 255." + }; + } + if (a < 0 || a > 1) { + return EvaluationError { + "Invalid rgba value [" + stringifyColor(r, g, b, a) + + "]: 'a' must be between 0 and 1." + }; + } + return Color(r / 255, g / 255, b / 255, a); +} + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/src/mbgl/style/expression/util.hpp b/src/mbgl/style/expression/util.hpp new file mode 100644 index 00000000000..b6fc408ed99 --- /dev/null +++ b/src/mbgl/style/expression/util.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +Result rgba(double r, double g, double b, double a); + +} // 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 00000000000..b75f471ce3d --- /dev/null +++ b/src/mbgl/style/expression/value.cpp @@ -0,0 +1,322 @@ +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +type::Type typeOf(const Value& value) { + return value.match( + [&](bool) -> type::Type { return type::Boolean; }, + [&](double) -> type::Type { return type::Number; }, + [&](const std::string&) -> type::Type { return type::String; }, + [&](const Color&) -> type::Type { return type::Color; }, + [&](const NullValue&) -> type::Type { return type::Null; }, + [&](const std::unordered_map&) -> type::Type { return type::Object; }, + [&](const std::vector& arr) -> type::Type { + optional itemType; + for (const auto& item : arr) { + const type::Type t = typeOf(item); + if (!itemType) { + itemType = {t}; + } else if (*itemType == t) { + continue; + } else { + itemType = {type::Value}; + break; + } + } + + return type::Array(itemType.value_or(type::Value), arr.size()); + } + ); +} + +void writeJSON(rapidjson::Writer& writer, const Value& value) { + value.match( + [&] (const NullValue&) { writer.Null(); }, + [&] (bool b) { writer.Bool(b); }, + [&] (double f) { + // make sure integer values are stringified without trailing ".0". + f == std::floor(f) ? writer.Int(f) : writer.Double(f); + }, + [&] (const std::string& s) { writer.String(s); }, + [&] (const Color& c) { writer.String(c.stringify()); }, + [&] (const std::vector& arr) { + writer.StartArray(); + for(const auto& item : arr) { + writeJSON(writer, item); + } + writer.EndArray(); + }, + [&] (const std::unordered_map& obj) { + writer.StartObject(); + for(const auto& entry : obj) { + writer.Key(entry.first.c_str()); + writeJSON(writer, entry.second); + } + writer.EndObject(); + } + ); +} + +std::string stringify(const Value& value) { + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + writeJSON(writer, value); + return buffer.GetString(); +} + +struct FromMBGLValue { + Value operator()(const std::vector& v) { + std::vector result; + result.reserve(v.size()); + for(const auto& item : v) { + result.emplace_back(toExpressionValue(item)); + } + return result; + } + + Value operator()(const std::unordered_map& v) { + std::unordered_map result; + result.reserve(v.size()); + for(const auto& entry : v) { + result.emplace(entry.first, toExpressionValue(entry.second)); + } + return result; + } + + Value operator()(const std::string& s) { return s; } + Value operator()(const bool b) { return b; } + Value operator()(const NullValue) { return Null; } + Value operator()(const double v) { return v; } + Value operator()(const uint64_t& v) { + return static_cast(v); + } + Value operator()(const int64_t& v) { + return static_cast(v); + } +}; + +Value ValueConverter::toExpressionValue(const mbgl::Value& value) { + return mbgl::Value::visit(value, FromMBGLValue()); +} + +Value ValueConverter::toExpressionValue(const float value) { + return static_cast(value); +} + +optional ValueConverter::fromExpressionValue(const Value& value) { + if (value.template is()) { + double v = value.template get(); + if (v <= std::numeric_limits::max()) { + return static_cast(v); + } + } + return optional(); +} + + +template +std::vector toArrayValue(const Container& value) { + std::vector result; + result.reserve(value.size()); + for (const T& item : value) { + result.push_back(ValueConverter::toExpressionValue(item)); + } + return result; +} + +template +Value ValueConverter>::toExpressionValue(const std::array& value) { + return toArrayValue(value); +} + +template +optional> ValueConverter>::fromExpressionValue(const Value& value) { + return value.match( + [&] (const std::vector& v) -> optional> { + if (v.size() != N) return optional>(); + std::array result; + auto it = result.begin(); + for(const Value& item : v) { + optional convertedItem = ValueConverter::fromExpressionValue(item); + if (!convertedItem) { + return optional>(); + } + *it = *convertedItem; + it = std::next(it); + } + return result; + }, + [&] (const auto&) { return optional>(); } + ); +} + + +template +Value ValueConverter>::toExpressionValue(const std::vector& value) { + return toArrayValue(value); +} + +template +optional> ValueConverter>::fromExpressionValue(const Value& value) { + return value.match( + [&] (const std::vector& v) -> optional> { + std::vector result; + result.reserve(v.size()); + for(const Value& item : v) { + optional convertedItem = ValueConverter::fromExpressionValue(item); + if (!convertedItem) { + return optional>(); + } + result.push_back(*convertedItem); + } + return result; + }, + [&] (const auto&) { return optional>(); } + ); +} + +Value ValueConverter::toExpressionValue(const mbgl::style::Position& value) { + return ValueConverter>::toExpressionValue(value.getSpherical()); +} + +optional ValueConverter::fromExpressionValue(const Value& v) { + auto pos = ValueConverter>::fromExpressionValue(v); + return pos ? optional(Position(*pos)) : optional(); +} + +template +Value ValueConverter::value >>::toExpressionValue(const T& value) { + return std::string(Enum::toString(value)); +} + +template +optional ValueConverter::value >>::fromExpressionValue(const Value& value) { + return value.match( + [&] (const std::string& v) { return Enum::toEnum(v); }, + [&] (const auto&) { return optional(); } + ); +} + + +Value toExpressionValue(const Value& v) { + return v; +} + +template +Value toExpressionValue(const T& value) { + return ValueConverter::toExpressionValue(value); +} + +optional fromExpressionValue(const Value& v) { + return optional(v); +} + +template +std::enable_if_t< !std::is_convertible::value, +optional> fromExpressionValue(const Value& v) +{ + return ValueConverter::fromExpressionValue(v); +} + +template +type::Type valueTypeToExpressionType() { + return ValueConverter::expressionType(); +} + +template <> type::Type valueTypeToExpressionType() { return type::Value; } +template <> type::Type valueTypeToExpressionType() { return type::Null; } +template <> type::Type valueTypeToExpressionType() { return type::Boolean; } +template <> type::Type valueTypeToExpressionType() { return type::Number; } +template <> type::Type valueTypeToExpressionType() { return type::String; } +template <> type::Type valueTypeToExpressionType() { return type::Color; } +template <> type::Type valueTypeToExpressionType>() { return type::Object; } +template <> type::Type valueTypeToExpressionType>() { return type::Array(type::Value); } + +// used only for the special (and private) "error" expression +template <> type::Type valueTypeToExpressionType() { return type::Error; } + + +template Value toExpressionValue(const mbgl::Value&); + + +// for to_rgba expression +template type::Type valueTypeToExpressionType>(); +template optional> fromExpressionValue>(const Value&); +template Value toExpressionValue(const std::array&); + +// layout/paint property types +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const float&); + +template type::Type valueTypeToExpressionType>(); +template optional> fromExpressionValue>(const Value&); +template Value toExpressionValue(const std::array&); + +template type::Type valueTypeToExpressionType>(); +template optional> fromExpressionValue>(const Value&); +template Value toExpressionValue(const std::array&); + +template type::Type valueTypeToExpressionType>(); +template optional> fromExpressionValue>(const Value&); +template Value toExpressionValue(const std::vector&); + +template type::Type valueTypeToExpressionType>(); +template optional> fromExpressionValue>(const Value&); +template Value toExpressionValue(const std::vector&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const AlignmentType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const CirclePitchScaleType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const IconTextFitType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const LineCapType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const LineJoinType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const SymbolPlacementType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const SymbolAnchorType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const TextJustifyType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const TextTransformType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const TranslateAnchorType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const LightAnchorType&); + +template type::Type valueTypeToExpressionType(); +template optional fromExpressionValue(const Value&); +template Value toExpressionValue(const Position&); + +} // 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 00000000000..d9dbbfa1d3f --- /dev/null +++ b/src/mbgl/style/function/expression.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +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 getID() const override { return feature.id; } + GeometryCollection getGeometries() const override { return {}; } + optional getValue(const std::string& key) const override { + auto it = feature.properties.find(key); + if (it != feature.properties.end()) { + return optional(it->second); + } + return optional(); + } +}; + + +EvaluationResult Expression::evaluate(optional zoom, const Feature& feature, optional heatmapDensity) const { + GeoJSONFeature f(feature); + return this->evaluate(EvaluationContext(zoom, &f, heatmapDensity)); +} + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/test/fixtures/expression_equality/acos.a.json b/test/fixtures/expression_equality/acos.a.json new file mode 100644 index 00000000000..1e9bb752cad --- /dev/null +++ b/test/fixtures/expression_equality/acos.a.json @@ -0,0 +1,4 @@ +[ + "acos", + 0.5 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/acos.b.json b/test/fixtures/expression_equality/acos.b.json new file mode 100644 index 00000000000..54e035cb7ed --- /dev/null +++ b/test/fixtures/expression_equality/acos.b.json @@ -0,0 +1,4 @@ +[ + "acos", + 1.5 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/all.a.json b/test/fixtures/expression_equality/all.a.json new file mode 100644 index 00000000000..ec7154b7b98 --- /dev/null +++ b/test/fixtures/expression_equality/all.a.json @@ -0,0 +1,17 @@ +[ + "all", + [ + "boolean", + [ + "get", + "x" + ] + ], + [ + "boolean", + [ + "get", + "y" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/all.b.json b/test/fixtures/expression_equality/all.b.json new file mode 100644 index 00000000000..8eab839bb03 --- /dev/null +++ b/test/fixtures/expression_equality/all.b.json @@ -0,0 +1,17 @@ +[ + "all", + [ + "boolean", + [ + "get", + "x" + ] + ], + [ + "boolean", + [ + "get", + "y_other" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/any.a.json b/test/fixtures/expression_equality/any.a.json new file mode 100644 index 00000000000..3f044c1f79c --- /dev/null +++ b/test/fixtures/expression_equality/any.a.json @@ -0,0 +1,17 @@ +[ + "any", + [ + "boolean", + [ + "get", + "x" + ] + ], + [ + "boolean", + [ + "get", + "y" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/any.b.json b/test/fixtures/expression_equality/any.b.json new file mode 100644 index 00000000000..720662751fa --- /dev/null +++ b/test/fixtures/expression_equality/any.b.json @@ -0,0 +1,17 @@ +[ + "any", + [ + "boolean", + [ + "get", + "x" + ] + ], + [ + "boolean", + [ + "get", + "y_other" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/array.a.json b/test/fixtures/expression_equality/array.a.json new file mode 100644 index 00000000000..3c31303ca3d --- /dev/null +++ b/test/fixtures/expression_equality/array.a.json @@ -0,0 +1,11 @@ +[ + "array", + [ + "literal", + [ + 1, + 2, + 3 + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/array.b.json b/test/fixtures/expression_equality/array.b.json new file mode 100644 index 00000000000..7606794d561 --- /dev/null +++ b/test/fixtures/expression_equality/array.b.json @@ -0,0 +1,11 @@ +[ + "array", + [ + "literal", + [ + 1, + 2, + 4 + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/asin.a.json b/test/fixtures/expression_equality/asin.a.json new file mode 100644 index 00000000000..3cd730ccbfe --- /dev/null +++ b/test/fixtures/expression_equality/asin.a.json @@ -0,0 +1,4 @@ +[ + "asin", + 0.5 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/asin.b.json b/test/fixtures/expression_equality/asin.b.json new file mode 100644 index 00000000000..2c862c8cbe3 --- /dev/null +++ b/test/fixtures/expression_equality/asin.b.json @@ -0,0 +1,4 @@ +[ + "asin", + 1.5 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/at.a.json b/test/fixtures/expression_equality/at.a.json new file mode 100644 index 00000000000..c69b0d933bf --- /dev/null +++ b/test/fixtures/expression_equality/at.a.json @@ -0,0 +1,20 @@ +[ + "number", + [ + "at", + [ + "number", + [ + "get", + "i" + ] + ], + [ + "array", + [ + "get", + "arr" + ] + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/at.b.json b/test/fixtures/expression_equality/at.b.json new file mode 100644 index 00000000000..6e19c28606b --- /dev/null +++ b/test/fixtures/expression_equality/at.b.json @@ -0,0 +1,20 @@ +[ + "number", + [ + "at", + [ + "number", + [ + "get", + "i" + ] + ], + [ + "array", + [ + "get", + "arr_other" + ] + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/atan.a.json b/test/fixtures/expression_equality/atan.a.json new file mode 100644 index 00000000000..b76406bc445 --- /dev/null +++ b/test/fixtures/expression_equality/atan.a.json @@ -0,0 +1,4 @@ +[ + "atan", + 1 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/atan.b.json b/test/fixtures/expression_equality/atan.b.json new file mode 100644 index 00000000000..aafbbb0594c --- /dev/null +++ b/test/fixtures/expression_equality/atan.b.json @@ -0,0 +1,4 @@ +[ + "atan", + 2 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/boolean.a.json b/test/fixtures/expression_equality/boolean.a.json new file mode 100644 index 00000000000..1230a2a926f --- /dev/null +++ b/test/fixtures/expression_equality/boolean.a.json @@ -0,0 +1,7 @@ +[ + "boolean", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/boolean.b.json b/test/fixtures/expression_equality/boolean.b.json new file mode 100644 index 00000000000..1ae91ef60c1 --- /dev/null +++ b/test/fixtures/expression_equality/boolean.b.json @@ -0,0 +1,7 @@ +[ + "boolean", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/case.a.json b/test/fixtures/expression_equality/case.a.json new file mode 100644 index 00000000000..84049294f53 --- /dev/null +++ b/test/fixtures/expression_equality/case.a.json @@ -0,0 +1,14 @@ +[ + "case", + [ + "get", + "x" + ], + "x", + [ + "get", + "y" + ], + "y", + "otherwise" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/case.b.json b/test/fixtures/expression_equality/case.b.json new file mode 100644 index 00000000000..038806043f7 --- /dev/null +++ b/test/fixtures/expression_equality/case.b.json @@ -0,0 +1,14 @@ +[ + "case", + [ + "get", + "x" + ], + "x", + [ + "get", + "y" + ], + "y", + "otherwise_other" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/coalesce.a.json b/test/fixtures/expression_equality/coalesce.a.json new file mode 100644 index 00000000000..8fae579e7c7 --- /dev/null +++ b/test/fixtures/expression_equality/coalesce.a.json @@ -0,0 +1,16 @@ +[ + "coalesce", + [ + "get", + "x" + ], + [ + "get", + "y" + ], + [ + "get", + "z" + ], + 0 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/coalesce.b.json b/test/fixtures/expression_equality/coalesce.b.json new file mode 100644 index 00000000000..4e0af8baa0d --- /dev/null +++ b/test/fixtures/expression_equality/coalesce.b.json @@ -0,0 +1,16 @@ +[ + "coalesce", + [ + "get", + "x" + ], + [ + "get", + "y" + ], + [ + "get", + "z" + ], + 1 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/concat.a.json b/test/fixtures/expression_equality/concat.a.json new file mode 100644 index 00000000000..08c95d7f497 --- /dev/null +++ b/test/fixtures/expression_equality/concat.a.json @@ -0,0 +1,6 @@ +[ + "concat", + "a", + "b", + "c" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/concat.b.json b/test/fixtures/expression_equality/concat.b.json new file mode 100644 index 00000000000..e3396d4fc0e --- /dev/null +++ b/test/fixtures/expression_equality/concat.b.json @@ -0,0 +1,6 @@ +[ + "concat", + "a", + "b", + "c_other" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/cos.a.json b/test/fixtures/expression_equality/cos.a.json new file mode 100644 index 00000000000..e41430de536 --- /dev/null +++ b/test/fixtures/expression_equality/cos.a.json @@ -0,0 +1,4 @@ +[ + "cos", + 0 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/cos.b.json b/test/fixtures/expression_equality/cos.b.json new file mode 100644 index 00000000000..5ba4424daed --- /dev/null +++ b/test/fixtures/expression_equality/cos.b.json @@ -0,0 +1,4 @@ +[ + "cos", + 1 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/divide.a.json b/test/fixtures/expression_equality/divide.a.json new file mode 100644 index 00000000000..40a67a871cc --- /dev/null +++ b/test/fixtures/expression_equality/divide.a.json @@ -0,0 +1,5 @@ +[ + "/", + 10, + 5 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/divide.b.json b/test/fixtures/expression_equality/divide.b.json new file mode 100644 index 00000000000..e3f7b155b23 --- /dev/null +++ b/test/fixtures/expression_equality/divide.b.json @@ -0,0 +1,5 @@ +[ + "/", + 10, + 6 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/downcase.a.json b/test/fixtures/expression_equality/downcase.a.json new file mode 100644 index 00000000000..ca367218c43 --- /dev/null +++ b/test/fixtures/expression_equality/downcase.a.json @@ -0,0 +1,4 @@ +[ + "downcase", + "StRiNg" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/downcase.b.json b/test/fixtures/expression_equality/downcase.b.json new file mode 100644 index 00000000000..fd9ea9881d0 --- /dev/null +++ b/test/fixtures/expression_equality/downcase.b.json @@ -0,0 +1,4 @@ +[ + "downcase", + "StRiNg_other" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/get.a.json b/test/fixtures/expression_equality/get.a.json new file mode 100644 index 00000000000..57c3df48e71 --- /dev/null +++ b/test/fixtures/expression_equality/get.a.json @@ -0,0 +1,7 @@ +[ + "number", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/get.b.json b/test/fixtures/expression_equality/get.b.json new file mode 100644 index 00000000000..d1843362d33 --- /dev/null +++ b/test/fixtures/expression_equality/get.b.json @@ -0,0 +1,7 @@ +[ + "number", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/has.a.json b/test/fixtures/expression_equality/has.a.json new file mode 100644 index 00000000000..8326754107e --- /dev/null +++ b/test/fixtures/expression_equality/has.a.json @@ -0,0 +1,4 @@ +[ + "has", + "x" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/has.b.json b/test/fixtures/expression_equality/has.b.json new file mode 100644 index 00000000000..20b6072303f --- /dev/null +++ b/test/fixtures/expression_equality/has.b.json @@ -0,0 +1,4 @@ +[ + "has", + "x_other" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/heatmap-density.a.json b/test/fixtures/expression_equality/heatmap-density.a.json new file mode 100644 index 00000000000..90bd396f54c --- /dev/null +++ b/test/fixtures/expression_equality/heatmap-density.a.json @@ -0,0 +1,23 @@ +[ + "interpolate", + [ + "linear" + ], + [ + "heatmap-density" + ], + 0, + [ + "rgb", + 0, + 0, + 255 + ], + 1, + [ + "rgb", + 255, + 0, + 0 + ] +] diff --git a/test/fixtures/expression_equality/heatmap-density.b.json b/test/fixtures/expression_equality/heatmap-density.b.json new file mode 100644 index 00000000000..bce8ab03a18 --- /dev/null +++ b/test/fixtures/expression_equality/heatmap-density.b.json @@ -0,0 +1,23 @@ +[ + "interpolate", + [ + "linear" + ], + [ + "heatmap-density" + ], + 0, + [ + "rgb", + 0, + 0, + 255 + ], + 1, + [ + "rgb", + 255, + 255, + 255 + ] +] diff --git a/test/fixtures/expression_equality/let.a.json b/test/fixtures/expression_equality/let.a.json new file mode 100644 index 00000000000..fb24e50cfb0 --- /dev/null +++ b/test/fixtures/expression_equality/let.a.json @@ -0,0 +1,25 @@ +[ + "let", + "a", + 1, + "b", + 2, + [ + "+", + [ + "+", + [ + "var", + "a" + ], + [ + "var", + "b" + ] + ], + [ + "var", + "a" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/let.b.json b/test/fixtures/expression_equality/let.b.json new file mode 100644 index 00000000000..26813cb6ffd --- /dev/null +++ b/test/fixtures/expression_equality/let.b.json @@ -0,0 +1,25 @@ +[ + "let", + "a", + 1, + "b", + 3, + [ + "+", + [ + "+", + [ + "var", + "a" + ], + [ + "var", + "b" + ] + ], + [ + "var", + "b" + ] + ] +] diff --git a/test/fixtures/expression_equality/ln.a.json b/test/fixtures/expression_equality/ln.a.json new file mode 100644 index 00000000000..30d80f36ae0 --- /dev/null +++ b/test/fixtures/expression_equality/ln.a.json @@ -0,0 +1,4 @@ +[ + "ln", + 2 +] diff --git a/test/fixtures/expression_equality/ln.b.json b/test/fixtures/expression_equality/ln.b.json new file mode 100644 index 00000000000..9bc04ad5867 --- /dev/null +++ b/test/fixtures/expression_equality/ln.b.json @@ -0,0 +1,6 @@ +[ + "ln", + [ + "e" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/log10.a.json b/test/fixtures/expression_equality/log10.a.json new file mode 100644 index 00000000000..32e4c188072 --- /dev/null +++ b/test/fixtures/expression_equality/log10.a.json @@ -0,0 +1,4 @@ +[ + "log10", + 100 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/log10.b.json b/test/fixtures/expression_equality/log10.b.json new file mode 100644 index 00000000000..8f32c204f99 --- /dev/null +++ b/test/fixtures/expression_equality/log10.b.json @@ -0,0 +1,4 @@ +[ + "log10", + 101 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/log2.a.json b/test/fixtures/expression_equality/log2.a.json new file mode 100644 index 00000000000..95cdc15373b --- /dev/null +++ b/test/fixtures/expression_equality/log2.a.json @@ -0,0 +1,4 @@ +[ + "log2", + 1024 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/log2.b.json b/test/fixtures/expression_equality/log2.b.json new file mode 100644 index 00000000000..2fffaeb32ab --- /dev/null +++ b/test/fixtures/expression_equality/log2.b.json @@ -0,0 +1,4 @@ +[ + "log2", + 1025 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/match.a.json b/test/fixtures/expression_equality/match.a.json new file mode 100644 index 00000000000..ba8afc41260 --- /dev/null +++ b/test/fixtures/expression_equality/match.a.json @@ -0,0 +1,12 @@ +[ + "match", + [ + "get", + "x" + ], + "a", + "Apple", + "b", + "Banana", + "Kumquat" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/match.b.json b/test/fixtures/expression_equality/match.b.json new file mode 100644 index 00000000000..2404b8e2e77 --- /dev/null +++ b/test/fixtures/expression_equality/match.b.json @@ -0,0 +1,12 @@ +[ + "match", + [ + "get", + "x" + ], + "a", + "Apple", + "b", + "Banana", + "Kumquat_other" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/max.a.json b/test/fixtures/expression_equality/max.a.json new file mode 100644 index 00000000000..09a8f82bd77 --- /dev/null +++ b/test/fixtures/expression_equality/max.a.json @@ -0,0 +1,6 @@ +[ + "max", + 0, + -1, + 100 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/max.b.json b/test/fixtures/expression_equality/max.b.json new file mode 100644 index 00000000000..1b0beb20d6e --- /dev/null +++ b/test/fixtures/expression_equality/max.b.json @@ -0,0 +1,6 @@ +[ + "max", + 0, + -1, + 101 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/min.a.json b/test/fixtures/expression_equality/min.a.json new file mode 100644 index 00000000000..38cc90f1cd7 --- /dev/null +++ b/test/fixtures/expression_equality/min.a.json @@ -0,0 +1,5 @@ +[ + "min", + ["get", "x"], + 0 +] diff --git a/test/fixtures/expression_equality/min.b.json b/test/fixtures/expression_equality/min.b.json new file mode 100644 index 00000000000..84a5f66842a --- /dev/null +++ b/test/fixtures/expression_equality/min.b.json @@ -0,0 +1,5 @@ +[ + "min", + ["get", "x"], + 1 +] diff --git a/test/fixtures/expression_equality/minus.a.json b/test/fixtures/expression_equality/minus.a.json new file mode 100644 index 00000000000..9eb4f954e71 --- /dev/null +++ b/test/fixtures/expression_equality/minus.a.json @@ -0,0 +1,5 @@ +[ + "-", + 5, + 7 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/minus.b.json b/test/fixtures/expression_equality/minus.b.json new file mode 100644 index 00000000000..87042b98efe --- /dev/null +++ b/test/fixtures/expression_equality/minus.b.json @@ -0,0 +1,5 @@ +[ + "-", + 5, + 8 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/mod.a.json b/test/fixtures/expression_equality/mod.a.json new file mode 100644 index 00000000000..8439bafcd1d --- /dev/null +++ b/test/fixtures/expression_equality/mod.a.json @@ -0,0 +1,5 @@ +[ + "%", + 18, + 12 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/mod.b.json b/test/fixtures/expression_equality/mod.b.json new file mode 100644 index 00000000000..362e1721c13 --- /dev/null +++ b/test/fixtures/expression_equality/mod.b.json @@ -0,0 +1,5 @@ +[ + "%", + 18, + 13 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/not.a.json b/test/fixtures/expression_equality/not.a.json new file mode 100644 index 00000000000..b5f03e0ac0d --- /dev/null +++ b/test/fixtures/expression_equality/not.a.json @@ -0,0 +1,10 @@ +[ + "!", + [ + "boolean", + [ + "get", + "x" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/not.b.json b/test/fixtures/expression_equality/not.b.json new file mode 100644 index 00000000000..a4d77adf2e8 --- /dev/null +++ b/test/fixtures/expression_equality/not.b.json @@ -0,0 +1,10 @@ +[ + "!", + [ + "boolean", + [ + "get", + "x_other" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/number.a.json b/test/fixtures/expression_equality/number.a.json new file mode 100644 index 00000000000..57c3df48e71 --- /dev/null +++ b/test/fixtures/expression_equality/number.a.json @@ -0,0 +1,7 @@ +[ + "number", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/number.b.json b/test/fixtures/expression_equality/number.b.json new file mode 100644 index 00000000000..d1843362d33 --- /dev/null +++ b/test/fixtures/expression_equality/number.b.json @@ -0,0 +1,7 @@ +[ + "number", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/object.a.json b/test/fixtures/expression_equality/object.a.json new file mode 100644 index 00000000000..7551cfdbb23 --- /dev/null +++ b/test/fixtures/expression_equality/object.a.json @@ -0,0 +1,7 @@ +[ + "object", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/object.b.json b/test/fixtures/expression_equality/object.b.json new file mode 100644 index 00000000000..8444d40c0e6 --- /dev/null +++ b/test/fixtures/expression_equality/object.b.json @@ -0,0 +1,7 @@ +[ + "object", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/plus.a.json b/test/fixtures/expression_equality/plus.a.json new file mode 100644 index 00000000000..a00c4409fa7 --- /dev/null +++ b/test/fixtures/expression_equality/plus.a.json @@ -0,0 +1,7 @@ +[ + "+", + 1, + 2, + 3, + 4 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/plus.b.json b/test/fixtures/expression_equality/plus.b.json new file mode 100644 index 00000000000..87c071123f4 --- /dev/null +++ b/test/fixtures/expression_equality/plus.b.json @@ -0,0 +1,7 @@ +[ + "+", + 1, + 2, + 3, + 5 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/pow.a.json b/test/fixtures/expression_equality/pow.a.json new file mode 100644 index 00000000000..c1a1e67f866 --- /dev/null +++ b/test/fixtures/expression_equality/pow.a.json @@ -0,0 +1,11 @@ +[ + "^", + 4, + [ + "number", + [ + "get", + "x" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/pow.b.json b/test/fixtures/expression_equality/pow.b.json new file mode 100644 index 00000000000..ca5331b92ad --- /dev/null +++ b/test/fixtures/expression_equality/pow.b.json @@ -0,0 +1,11 @@ +[ + "^", + 4, + [ + "number", + [ + "get", + "x_other" + ] + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/rgb.a.json b/test/fixtures/expression_equality/rgb.a.json new file mode 100644 index 00000000000..ce6c5e5dd02 --- /dev/null +++ b/test/fixtures/expression_equality/rgb.a.json @@ -0,0 +1,6 @@ +[ + "rgb", + 0, + 0, + 255 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/rgb.b.json b/test/fixtures/expression_equality/rgb.b.json new file mode 100644 index 00000000000..577c19748ba --- /dev/null +++ b/test/fixtures/expression_equality/rgb.b.json @@ -0,0 +1,6 @@ +[ + "rgb", + 0, + 0, + 0 +] diff --git a/test/fixtures/expression_equality/rgba.a.json b/test/fixtures/expression_equality/rgba.a.json new file mode 100644 index 00000000000..e8ad7326c10 --- /dev/null +++ b/test/fixtures/expression_equality/rgba.a.json @@ -0,0 +1,7 @@ +[ + "rgba", + 0, + 0, + 255, + 1 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/rgba.b.json b/test/fixtures/expression_equality/rgba.b.json new file mode 100644 index 00000000000..81d442eaae1 --- /dev/null +++ b/test/fixtures/expression_equality/rgba.b.json @@ -0,0 +1,7 @@ +[ + "rgba", + 0, + 0, + 255, + 0.5 +] diff --git a/test/fixtures/expression_equality/sin.a.json b/test/fixtures/expression_equality/sin.a.json new file mode 100644 index 00000000000..0f7ae2966fa --- /dev/null +++ b/test/fixtures/expression_equality/sin.a.json @@ -0,0 +1,4 @@ +[ + "sin", + 0 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/sin.b.json b/test/fixtures/expression_equality/sin.b.json new file mode 100644 index 00000000000..90f309b80f8 --- /dev/null +++ b/test/fixtures/expression_equality/sin.b.json @@ -0,0 +1,4 @@ +[ + "sin", + 1 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/sqrt.a.json b/test/fixtures/expression_equality/sqrt.a.json new file mode 100644 index 00000000000..56dd85bc1a5 --- /dev/null +++ b/test/fixtures/expression_equality/sqrt.a.json @@ -0,0 +1,7 @@ +[ + "sqrt", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/sqrt.b.json b/test/fixtures/expression_equality/sqrt.b.json new file mode 100644 index 00000000000..ab05d5084c6 --- /dev/null +++ b/test/fixtures/expression_equality/sqrt.b.json @@ -0,0 +1,7 @@ +[ + "sqrt", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/step.a.json b/test/fixtures/expression_equality/step.a.json new file mode 100644 index 00000000000..4fee85cd036 --- /dev/null +++ b/test/fixtures/expression_equality/step.a.json @@ -0,0 +1,18 @@ +[ + "number", + [ + "step", + [ + "number", + [ + "get", + "x" + ] + ], + 11, + 0, + 111, + 1, + 1111 + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/step.b.json b/test/fixtures/expression_equality/step.b.json new file mode 100644 index 00000000000..0a591a84df1 --- /dev/null +++ b/test/fixtures/expression_equality/step.b.json @@ -0,0 +1,18 @@ +[ + "number", + [ + "step", + [ + "number", + [ + "get", + "x" + ] + ], + 11, + 0, + 111, + 1, + 1112 + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/string.a.json b/test/fixtures/expression_equality/string.a.json new file mode 100644 index 00000000000..a79344f3389 --- /dev/null +++ b/test/fixtures/expression_equality/string.a.json @@ -0,0 +1,7 @@ +[ + "string", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/string.b.json b/test/fixtures/expression_equality/string.b.json new file mode 100644 index 00000000000..6f77f3c3cfd --- /dev/null +++ b/test/fixtures/expression_equality/string.b.json @@ -0,0 +1,7 @@ +[ + "string", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/tan.a.json b/test/fixtures/expression_equality/tan.a.json new file mode 100644 index 00000000000..c78e47e4924 --- /dev/null +++ b/test/fixtures/expression_equality/tan.a.json @@ -0,0 +1,4 @@ +[ + "tan", + 0.7853981633974483 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/tan.b.json b/test/fixtures/expression_equality/tan.b.json new file mode 100644 index 00000000000..c22e64cf8a0 --- /dev/null +++ b/test/fixtures/expression_equality/tan.b.json @@ -0,0 +1,4 @@ +[ + "tan", + 1.7853981633974483 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/times.a.json b/test/fixtures/expression_equality/times.a.json new file mode 100644 index 00000000000..ce6d9b46e08 --- /dev/null +++ b/test/fixtures/expression_equality/times.a.json @@ -0,0 +1,7 @@ +[ + "*", + 3, + 2, + 0.5, + 2 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/times.b.json b/test/fixtures/expression_equality/times.b.json new file mode 100644 index 00000000000..147e011172d --- /dev/null +++ b/test/fixtures/expression_equality/times.b.json @@ -0,0 +1,7 @@ +[ + "*", + 3, + 2, + 0.5, + 3 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-boolean.a.json b/test/fixtures/expression_equality/to-boolean.a.json new file mode 100644 index 00000000000..ccf48149ecb --- /dev/null +++ b/test/fixtures/expression_equality/to-boolean.a.json @@ -0,0 +1,7 @@ +[ + "to-boolean", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-boolean.b.json b/test/fixtures/expression_equality/to-boolean.b.json new file mode 100644 index 00000000000..78962612411 --- /dev/null +++ b/test/fixtures/expression_equality/to-boolean.b.json @@ -0,0 +1,7 @@ +[ + "to-boolean", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-color.a.json b/test/fixtures/expression_equality/to-color.a.json new file mode 100644 index 00000000000..de9ab59eec3 --- /dev/null +++ b/test/fixtures/expression_equality/to-color.a.json @@ -0,0 +1,7 @@ +[ + "to-color", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-color.b.json b/test/fixtures/expression_equality/to-color.b.json new file mode 100644 index 00000000000..c0566ef6a72 --- /dev/null +++ b/test/fixtures/expression_equality/to-color.b.json @@ -0,0 +1,7 @@ +[ + "to-color", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-number.a.json b/test/fixtures/expression_equality/to-number.a.json new file mode 100644 index 00000000000..65b2df5014a --- /dev/null +++ b/test/fixtures/expression_equality/to-number.a.json @@ -0,0 +1,7 @@ +[ + "to-number", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-number.b.json b/test/fixtures/expression_equality/to-number.b.json new file mode 100644 index 00000000000..b38dc5a4559 --- /dev/null +++ b/test/fixtures/expression_equality/to-number.b.json @@ -0,0 +1,7 @@ +[ + "to-number", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-string.a.json b/test/fixtures/expression_equality/to-string.a.json new file mode 100644 index 00000000000..66f9a9caa13 --- /dev/null +++ b/test/fixtures/expression_equality/to-string.a.json @@ -0,0 +1,7 @@ +[ + "to-string", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/to-string.b.json b/test/fixtures/expression_equality/to-string.b.json new file mode 100644 index 00000000000..977a9d7769c --- /dev/null +++ b/test/fixtures/expression_equality/to-string.b.json @@ -0,0 +1,7 @@ +[ + "to-string", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/typeof.a.json b/test/fixtures/expression_equality/typeof.a.json new file mode 100644 index 00000000000..7843ff8c7fa --- /dev/null +++ b/test/fixtures/expression_equality/typeof.a.json @@ -0,0 +1,7 @@ +[ + "typeof", + [ + "get", + "x" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/typeof.b.json b/test/fixtures/expression_equality/typeof.b.json new file mode 100644 index 00000000000..412482347ab --- /dev/null +++ b/test/fixtures/expression_equality/typeof.b.json @@ -0,0 +1,7 @@ +[ + "typeof", + [ + "get", + "x_other" + ] +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/upcase.a.json b/test/fixtures/expression_equality/upcase.a.json new file mode 100644 index 00000000000..d12ca7b08da --- /dev/null +++ b/test/fixtures/expression_equality/upcase.a.json @@ -0,0 +1,4 @@ +[ + "upcase", + "string" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/upcase.b.json b/test/fixtures/expression_equality/upcase.b.json new file mode 100644 index 00000000000..ddeeb0300cf --- /dev/null +++ b/test/fixtures/expression_equality/upcase.b.json @@ -0,0 +1,4 @@ +[ + "upcase", + "string_other" +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/zoom.a.json b/test/fixtures/expression_equality/zoom.a.json new file mode 100644 index 00000000000..fc675721ab9 --- /dev/null +++ b/test/fixtures/expression_equality/zoom.a.json @@ -0,0 +1,13 @@ +[ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0, + 30, + 30 +] \ No newline at end of file diff --git a/test/fixtures/expression_equality/zoom.b.json b/test/fixtures/expression_equality/zoom.b.json new file mode 100644 index 00000000000..6314858a5e2 --- /dev/null +++ b/test/fixtures/expression_equality/zoom.b.json @@ -0,0 +1,13 @@ +[ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0, + 30, + 31 +] \ No newline at end of file diff --git a/test/fixtures/style_parser/expressions.info.json b/test/fixtures/style_parser/expressions.info.json new file mode 100644 index 00000000000..9e1765ecd0e --- /dev/null +++ b/test/fixtures/style_parser/expressions.info.json @@ -0,0 +1,12 @@ +{ + "default": { + "log": [ + [1, "WARNING", "ParseStyle", "Expected color but found number instead."], + [1, "WARNING", "ParseStyle", "[2]: Expected number but found string instead."], + [1, "WARNING", "ParseStyle", "\"zoom\" expression may only be used as input to a top-level \"step\" or \"interpolate\" expression."], + [1, "WARNING", "ParseStyle", "value must be a string"], + [1, "WARNING", "ParseStyle", "property expressions not supported"], + [1, "WARNING", "ParseStyle", "Type array is not interpolatable."] + ] + } +} diff --git a/test/fixtures/style_parser/expressions.style.json b/test/fixtures/style_parser/expressions.style.json new file mode 100644 index 00000000000..b9b4aeac7f2 --- /dev/null +++ b/test/fixtures/style_parser/expressions.style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "sources": { + "source": { + "type": "vector", + "url": "mapbox://mapbox.mapbox-streets-v5" + } + }, + "layers": [ + { + "id": "valid expression", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-color": ["rgba", 10, ["number", ["get", "x"]], 30, 1] + } + }, + { + "id": "invalid expression type - color", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-color": ["pi"] + } + }, + { + "id": "invalid expression - fails type checking", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-color": ["rgba", 1, "should be a number", 0, 1] + } + }, + { + "id": "invalid expression - nested zoom expression", + "type": "fill", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-opacity": ["+", 0.5, ["interpolate", ["linear"], ["zoom"], 0, 0, 1, 1]] + } + }, + { + "id": "invalid expression - not allowed in visibility", + "type": "fill", + "source": "source", + "source-layer": "layer", + "layout": { + "visibility": ["literal", true] + } + }, + { + "id": "invalid expression - not a DDS property", + "type": "fill-extrusion", + "source": "source", + "source-layer": "layer", + "paint": { + "fill-extrusion-opacity": ["get", "opacity"] + } + }, + { + "id": "invalid expression - line-dasharray must use step interpolation", + "type": "line", + "source": "source", + "source-layer": "layer", + "paint": { + "line-dasharray": ["interpolate", ["linear"], ["zoom"], 0, ["literal", [1, 2]], 1, ["literal", [3, 4]]] + } + } + ] +} diff --git a/test/style/conversion/function.test.cpp b/test/style/conversion/function.test.cpp index 9e8a6b3a7f1..a48be2c0750 100644 --- a/test/style/conversion/function.test.cpp +++ b/test/style/conversion/function.test.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include using namespace mbgl; using namespace mbgl::style; @@ -50,3 +52,29 @@ TEST(StyleConversion, Function) { ASSERT_FALSE(fn9); ASSERT_EQ("function base must be a number", error.message); } + +TEST(StyleConversion, CompositeFunctionExpression) { + Error error; + + auto parseFunction = [&](const std::string& src) { + JSDocument doc; + doc.Parse<0>(src); + return convert>(doc, error); + }; + + auto fn1 = parseFunction(R"(["interpolate", ["linear"], ["zoom"], 0, ["number", ["get", "x"]], 10, 10])"); + ASSERT_TRUE(fn1); + + auto fn2 = parseFunction(R"(["coalesce", ["interpolate", ["linear"], ["zoom"], 0, ["number", ["get", "x"]], 10, 10], 0])"); + ASSERT_TRUE(fn2); + + auto fn3 = parseFunction(R"(["let", "a", 0, ["interpolate", ["linear"], ["zoom"], 0, ["number", ["get", "x"]], 10, 10] ])"); + ASSERT_TRUE(fn3); + + auto fn4 = parseFunction(R"(["coalesce", ["let", "a", 0, ["interpolate", ["linear"], ["zoom"], 0, ["number", ["get", "x"]], 10, 10], 0 ])"); + ASSERT_TRUE(fn4); + + auto fn5 = parseFunction(R"(["coalesce", ["interpolate", ["linear"], ["number", ["get", "x"]], 0, ["zoom"], 10, 10], 0])"); + ASSERT_FALSE(fn5); + ASSERT_EQ(R"("zoom" expression may only be used as input to a top-level "step" or "interpolate" expression.)", error.message); +} diff --git a/test/style/expression/expression.test.cpp b/test/style/expression/expression.test.cpp new file mode 100644 index 00000000000..694569695c7 --- /dev/null +++ b/test/style/expression/expression.test.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + + +using namespace mbgl; +using namespace mbgl::style; + +TEST(Expression, IsExpression) { + rapidjson::GenericDocument, rapidjson::CrtAllocator> spec; + spec.Parse<0>(util::read_file("mapbox-gl-js/src/style-spec/reference/v8.json").c_str()); + ASSERT_FALSE(spec.HasParseError()); + ASSERT_TRUE(spec.IsObject() && + spec.HasMember("expression_name") && + spec["expression_name"].IsObject() && + spec["expression_name"].HasMember("values") && + spec["expression_name"]["values"].IsObject()); + + const auto& allExpressions = spec["expression_name"]["values"]; + + for(auto& entry : allExpressions.GetObject()) { + const std::string name { entry.name.GetString(), entry.name.GetStringLength() }; + JSDocument document; + document.Parse<0>(R"([")" + name + R"("])"); + + const JSValue* expression = &document; + EXPECT_TRUE(expression::isExpression(conversion::Convertible(expression))) << name; + } +} + +class ExpressionEqualityTest : public ::testing::TestWithParam {}; + +TEST_P(ExpressionEqualityTest, ExpressionEquality) { + const std::string base = std::string("test/fixtures/expression_equality/") + GetParam(); + + std::string error; + auto parse = [&](std::string filename, std::string& error_) -> std::unique_ptr { + rapidjson::GenericDocument, rapidjson::CrtAllocator> document; + document.Parse<0>(util::read_file(filename).c_str()); + assert(!document.HasParseError()); + const JSValue* expression = &document; + expression::ParsingContext ctx; + expression::ParseResult parsed = ctx.parse(conversion::Convertible(expression)); + if (!parsed) { + error_ = ctx.getErrors().size() > 0 ? ctx.getErrors()[0].message : "failed to parse"; + }; + return std::move(*parsed); + }; + + std::unique_ptr expression_a1 = parse(base + ".a.json", error); + ASSERT_TRUE(expression_a1) << GetParam() << ": " << error; + + std::unique_ptr expression_a2 = parse(base + ".a.json", error); + ASSERT_TRUE(expression_a2) << GetParam() << ": " << error; + + std::unique_ptr expression_b = parse(base + ".b.json", error); + ASSERT_TRUE(expression_b) << GetParam() << ": " << error; + + + EXPECT_TRUE(*expression_a1 == *expression_a2); + EXPECT_TRUE(*expression_a1 != *expression_b); +} + +INSTANTIATE_TEST_CASE_P(Expression, ExpressionEqualityTest, ::testing::ValuesIn([] { + std::vector names; + const std::string ending = ".a.json"; + + const std::string style_directory = "test/fixtures/expression_equality"; + DIR *dir = opendir(style_directory.c_str()); + if (dir != nullptr) { + for (dirent *dp = nullptr; (dp = readdir(dir)) != nullptr;) { + const std::string name = dp->d_name; + if (name.length() >= ending.length() && name.compare(name.length() - ending.length(), ending.length(), ending) == 0) { + names.push_back(name.substr(0, name.length() - ending.length())); + } + } + closedir(dir); + } + + EXPECT_GT(names.size(), 0u); + return names; +}())); diff --git a/test/style/expression/util.test.cpp b/test/style/expression/util.test.cpp new file mode 100644 index 00000000000..0337cd871f5 --- /dev/null +++ b/test/style/expression/util.test.cpp @@ -0,0 +1,23 @@ +#include +#include + +using namespace mbgl; +using namespace mbgl::style::expression; + +TEST(Expression, Util_rgba) { + Result valid = rgba(0, 0, 0, 0); + ASSERT_TRUE(valid); + ASSERT_EQ(valid->r, 0); + ASSERT_EQ(valid->g, 0); + ASSERT_EQ(valid->b, 0); + ASSERT_EQ(valid->a, 0); + + ASSERT_FALSE(rgba(0, 0, 0, -0.1)); + ASSERT_FALSE(rgba(0, 0, 0, 1.1)); + ASSERT_FALSE(rgba(0, 0, -1, 1)); + ASSERT_FALSE(rgba(0, 0, 256, 1)); + ASSERT_FALSE(rgba(0, -1, 0, 1)); + ASSERT_FALSE(rgba(0, 256, 0, 1)); + ASSERT_FALSE(rgba(-1, 1, 0, 1)); + ASSERT_FALSE(rgba(-256, 1, 0, 1)); +}