diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a78fd51b..78fef9f7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,6 +241,7 @@ list(APPEND INCLUDE_FILES ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/match.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/number_format.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/parsing_context.hpp + ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/slice.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/step.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/type.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/value.hpp @@ -641,6 +642,7 @@ list(APPEND SRC_FILES ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/match.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/number_format.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/parsing_context.cpp + ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/slice.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/step.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/util.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/util.hpp diff --git a/bazel/core.bzl b/bazel/core.bzl index b9023b0b3..3232fdace 100644 --- a/bazel/core.bzl +++ b/bazel/core.bzl @@ -398,6 +398,7 @@ MLN_CORE_SOURCE = [ "src/mbgl/style/expression/match.cpp", "src/mbgl/style/expression/number_format.cpp", "src/mbgl/style/expression/parsing_context.cpp", + "src/mbgl/style/expression/slice.cpp", "src/mbgl/style/expression/step.cpp", "src/mbgl/style/expression/util.cpp", "src/mbgl/style/expression/util.hpp", @@ -719,6 +720,7 @@ MLN_CORE_HEADERS = [ "include/mbgl/style/expression/match.hpp", "include/mbgl/style/expression/number_format.hpp", "include/mbgl/style/expression/parsing_context.hpp", + "include/mbgl/style/expression/slice.hpp", "include/mbgl/style/expression/step.hpp", "include/mbgl/style/expression/type.hpp", "include/mbgl/style/expression/value.hpp", diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp index e907e6bed..99da94e1e 100644 --- a/include/mbgl/style/expression/expression.hpp +++ b/include/mbgl/style/expression/expression.hpp @@ -179,7 +179,8 @@ enum class Kind : int32_t { In, Within, Distance, - IndexOf + IndexOf, + Slice }; class Expression { diff --git a/include/mbgl/style/expression/slice.hpp b/include/mbgl/style/expression/slice.hpp new file mode 100644 index 000000000..60bfbd401 --- /dev/null +++ b/include/mbgl/style/expression/slice.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +class Slice : public Expression { +public: + Slice(std::unique_ptr input_, + std::unique_ptr fromIndex_, + std::unique_ptr toIndex_); + + 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; + + std::vector> possibleOutputs() const override; + std::string getOperator() const override; + +private: + EvaluationResult evaluateForStringInput(const std::string& input, int fromIndexValue, int toIndexValue) const; + EvaluationResult evaluateForArrayInput(const std::vector& input, int fromIndexValue, int toIndexValue) const; + +private: + std::unique_ptr input; + std::unique_ptr fromIndex; + std::unique_ptr toIndex; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/metrics/integration/expression-tests/slice/array-one-index/test.json b/metrics/integration/expression-tests/slice/array-one-index/test.json new file mode 100644 index 000000000..bb71d7130 --- /dev/null +++ b/metrics/integration/expression-tests/slice/array-one-index/test.json @@ -0,0 +1,19 @@ +{ + "expression": ["slice", ["array", ["get", "val"]], ["get", "index"]], + "inputs": [ + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "index": 2}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "index": 0}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "index": 99}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "index": -2}}] + ], + "expected": { + "serialized": ["slice", ["array", ["get", "val"]], ["get", "index"]], + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "array" + }, + "outputs": [[3, 4, 5], [1, 2, 3, 4, 5], [], [4, 5]] + } + } \ No newline at end of file diff --git a/metrics/integration/expression-tests/slice/array-two-indexes/test.json b/metrics/integration/expression-tests/slice/array-two-indexes/test.json new file mode 100644 index 000000000..cbf65d324 --- /dev/null +++ b/metrics/integration/expression-tests/slice/array-two-indexes/test.json @@ -0,0 +1,31 @@ +{ + "expression": [ + "slice", + ["array", ["get", "val"]], + ["get", "i1"], + ["get", "i2"] + ], + "inputs": [ + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "i1": 2, "i2": 4}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "i1": 1, "i2": 5}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "i1": 1, "i2": 99}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "i1": -4, "i2": -2}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "i1": 0, "i2": -1}}], + [{}, {"properties": {"val": [1, 2, 3, 4, 5], "i1": 0, "i2": 0}}] + ], + "expected": { + "serialized": [ + "slice", + ["array", ["get", "val"]], + ["get", "i1"], + ["get", "i2"] + ], + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "array" + }, + "outputs": [[3, 4], [2, 3, 4, 5], [2, 3, 4, 5], [2, 3], [1, 2, 3, 4], []] + } + } \ No newline at end of file diff --git a/metrics/integration/expression-tests/slice/invalid-inputs/test.json b/metrics/integration/expression-tests/slice/invalid-inputs/test.json new file mode 100644 index 000000000..020c76571 --- /dev/null +++ b/metrics/integration/expression-tests/slice/invalid-inputs/test.json @@ -0,0 +1,40 @@ +{ + "expression": ["slice", ["get", "input"], ["get", "i1"]], + "inputs": [ + [{}, {"properties": {"input": false, "i1": 1}}], + [{}, {"properties": {"input": null, "i1": 1}}], + [{}, {"properties": {"input": 12, "i1": 1}}], + [{}, {"properties": {"input": {}, "i1": 1}}], + [{}, {"properties": {"other": 1, "i1": 1}}], + [{}, {"properties": {"input": "correct", "i1": "one"}}] + ], + "expected": { + "serialized": ["slice", ["get", "input"], ["get", "i1"]], + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "value" + }, + "outputs": [ + { + "error": "Expected first argument to be of type array or string, but found boolean instead." + }, + { + "error": "Expected first argument to be of type array or string, but found null instead." + }, + { + "error": "Expected first argument to be of type array or string, but found number instead." + }, + { + "error": "Expected first argument to be of type array or string, but found object instead." + }, + { + "error": "Expected first argument to be of type array or string, but found null instead." + }, + { + "error": "Expected value to be of type number, but found string instead." + } + ] + } + } \ No newline at end of file diff --git a/metrics/integration/expression-tests/slice/string-one-index/test.json b/metrics/integration/expression-tests/slice/string-one-index/test.json new file mode 100644 index 000000000..cb14b972c --- /dev/null +++ b/metrics/integration/expression-tests/slice/string-one-index/test.json @@ -0,0 +1,19 @@ +{ + "expression": ["slice", ["get", "val"], ["get", "index"]], + "inputs": [ + [{}, {"properties": {"val": "0123456789", "index": 0}}], + [{}, {"properties": {"val": "0123456789", "index": 4}}], + [{}, {"properties": {"val": "0123456789", "index": 99}}], + [{}, {"properties": {"val": "0123456789", "index": -2}}] + ], + "expected": { + "serialized": ["slice", ["get", "val"], ["get", "index"]], + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "value" + }, + "outputs": ["0123456789", "456789", "", "89"] + } + } \ No newline at end of file diff --git a/metrics/integration/expression-tests/slice/string-two-indexes/test.json b/metrics/integration/expression-tests/slice/string-two-indexes/test.json new file mode 100644 index 000000000..0af5bd258 --- /dev/null +++ b/metrics/integration/expression-tests/slice/string-two-indexes/test.json @@ -0,0 +1,20 @@ +{ + "expression": ["slice", ["get", "val"], ["get", "i1"], ["get", "i2"]], + "inputs": [ + [{}, {"properties": {"val": "0123456789", "i1": 1, "i2": 8}}], + [{}, {"properties": {"val": "0123456789", "i1": 4, "i2": -2}}], + [{}, {"properties": {"val": "0123456789", "i1": -3, "i2": -1}}], + [{}, {"properties": {"val": "0123456789", "i1": 0, "i2": -1}}], + [{}, {"properties": {"val": "0123456789", "i1": 0, "i2": 99}}] + ], + "expected": { + "serialized": ["slice", ["get", "val"], ["get", "i1"], ["get", "i2"]], + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "value" + }, + "outputs": ["1234567", "4567", "78", "012345678", "0123456789"] + } + } \ No newline at end of file diff --git a/platform/android/CHANGELOG.md b/platform/android/CHANGELOG.md index c7d43e00a..de075c24b 100644 --- a/platform/android/CHANGELOG.md +++ b/platform/android/CHANGELOG.md @@ -5,6 +5,7 @@ MapLibre welcomes participation and contributions from everyone. Please read [`C ## main ### ✨ Features and improvements +* Add support for the [`slice` expression](https://maplibre.org/maplibre-style-spec/expressions/#slice) ([#1113](https://github.com/maplibre/maplibre-native/pull/1133)) - 💥 Breaking: Change package of all classes from `com.mapbox.mapboxsdk` to `org.maplibre.android` ([#1201](https://github.com/maplibre/maplibre-native/pull/1201)). This means you will need to fix your imports. diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/style/expressions/Expression.java b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/style/expressions/Expression.java index ee57d8021..5c6153c55 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/style/expressions/Expression.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/org/maplibre/android/style/expressions/Expression.java @@ -1565,7 +1565,7 @@ public class Expression { } /** - * Returns the first position at which a `needle` can be found in a `haystack`. + * Returns the first position at which a `needle` can be found in a `haystack`. * * @param needle the item expression * @param haystack the array or string expression @@ -1577,6 +1577,33 @@ public class Expression { return new Expression("index-of", keyword, input, fromIndex); } + /** + * Returns items from an array or a substring from a string from a specified start index. + * The return value is inclusive of the start index. + * + * @param input the array or string expression + * @param fromIndex the index to start slice from + * @return array or string + * @see Style specification + */ + public static Expression slice(@NonNull Expression input, @NonNull Expression fromIndex) { + return new Expression("slice", input, fromIndex); + } + + /** + * Returns items from an array or a substring from a string between a start index and an end index if set. + * The return value is inclusive of the start index, but not of the end index. + * + * @param input the array or string expression + * @param fromIndex the index to start slice from + * @param toIndex the index to end slice at + * @return array or string + * @see Style specification + */ + public static Expression slice(@NonNull Expression input, @NonNull Expression fromIndex, @NonNull Expression toIndex) { + return new Expression("slice", input, fromIndex, toIndex); + } + /** * Retrieves whether an item exists in an array or a substring exists in a string. * diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt index edf0d51cf..481ad3bb7 100644 --- a/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/org/maplibre/android/style/expressions/ExpressionTest.kt @@ -477,6 +477,26 @@ class ExpressionTest { Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual)) } + @Test + @Throws(Exception::class) + fun testSlice() { + val expected = arrayOf("slice", arrayOf("literal", arrayOf(1f, 2f, 3f)), 1f) + val actual = + Expression.slice(Expression.literal(arrayOf(1f, 2f, 3f)), Expression.literal(1f)) + .toArray() + Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual)) + } + + @Test + @Throws(Exception::class) + fun testSliceWithToIndex() { + val expected = arrayOf("slice", arrayOf("literal", arrayOf(1f, 2f, 3f)), 1f, 3f) + val actual = + Expression.slice(Expression.literal(arrayOf(1f, 2f, 3f)), Expression.literal(1f), Expression.literal(3f)) + .toArray() + Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual)) + } + @Test @Throws(Exception::class) fun testInNumber() { diff --git a/src/mbgl/style/expression/index_of.cpp b/src/mbgl/style/expression/index_of.cpp index ecb7e514d..432e40b7c 100644 --- a/src/mbgl/style/expression/index_of.cpp +++ b/src/mbgl/style/expression/index_of.cpp @@ -2,7 +2,7 @@ #include #include #include -#include + namespace mbgl { namespace style { namespace expression { diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp index 1d6fcb251..bf24d115a 100644 --- a/src/mbgl/style/expression/parsing_context.cpp +++ b/src/mbgl/style/expression/parsing_context.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -105,43 +106,42 @@ ParseResult ParsingContext::parse(const Convertible& value, using ParseFunction = ParseResult (*)(const conversion::Convertible&, ParsingContext&); MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry = - mapbox::eternal::hash_map({ - {"==", parseComparison}, - {"!=", parseComparison}, - {">", parseComparison}, - {"<", parseComparison}, - {">=", parseComparison}, - {"<=", parseComparison}, - {"all", All::parse}, - {"any", Any::parse}, - {"array", Assertion::parse}, - {"at", At::parse}, - {"in", In::parse}, - {"boolean", Assertion::parse}, - {"case", Case::parse}, - {"coalesce", Coalesce::parse}, - {"collator", CollatorExpression::parse}, - {"distance", Distance::parse}, - {"format", FormatExpression::parse}, - {"image", ImageExpression::parse}, - {"interpolate", parseInterpolate}, - {"length", Length::parse}, - {"let", Let::parse}, - {"literal", Literal::parse}, - {"match", parseMatch}, - {"number", Assertion::parse}, - {"number-format", NumberFormat::parse}, - {"object", Assertion::parse}, - {"step", Step::parse}, - {"string", Assertion::parse}, - {"to-boolean", Coercion::parse}, - {"to-color", Coercion::parse}, - {"to-number", Coercion::parse}, - {"to-string", Coercion::parse}, - {"var", Var::parse}, - {"within", Within::parse}, - {"index-of", IndexOf::parse}, - }); + mapbox::eternal::hash_map({{"==", parseComparison}, + {"!=", parseComparison}, + {">", parseComparison}, + {"<", parseComparison}, + {">=", parseComparison}, + {"<=", parseComparison}, + {"all", All::parse}, + {"any", Any::parse}, + {"array", Assertion::parse}, + {"at", At::parse}, + {"in", In::parse}, + {"boolean", Assertion::parse}, + {"case", Case::parse}, + {"coalesce", Coalesce::parse}, + {"collator", CollatorExpression::parse}, + {"distance", Distance::parse}, + {"format", FormatExpression::parse}, + {"image", ImageExpression::parse}, + {"interpolate", parseInterpolate}, + {"length", Length::parse}, + {"let", Let::parse}, + {"literal", Literal::parse}, + {"match", parseMatch}, + {"number", Assertion::parse}, + {"number-format", NumberFormat::parse}, + {"object", Assertion::parse}, + {"step", Step::parse}, + {"string", Assertion::parse}, + {"to-boolean", Coercion::parse}, + {"to-color", Coercion::parse}, + {"to-number", Coercion::parse}, + {"to-string", Coercion::parse}, + {"var", Var::parse}, + {"within", Within::parse}, + {"index-of", IndexOf::parse}, + {"slice", Slice::parse}}); bool isExpression(const std::string& name) { return expressionRegistry.contains(name.c_str()); diff --git a/src/mbgl/style/expression/slice.cpp b/src/mbgl/style/expression/slice.cpp new file mode 100644 index 000000000..5c3a723ca --- /dev/null +++ b/src/mbgl/style/expression/slice.cpp @@ -0,0 +1,156 @@ +#include "mbgl/style/expression/expression.hpp" +#include +#include +#include +#include + +namespace mbgl { +namespace style { +namespace expression { + +namespace { + +int normalizeIndex(int index, int length) { + if (index < 0) { + index += length; + } + return index; +} + +} // namespace + +Slice::Slice(std::unique_ptr input_, + std::unique_ptr fromIndex_, + std::unique_ptr toIndex_) + : Expression(Kind::Slice, input_->getType()), + input(std::move(input_)), + fromIndex(std::move(fromIndex_)), + toIndex(std::move(toIndex_)) {} + +EvaluationResult Slice::evaluate(const EvaluationContext ¶ms) const { + const EvaluationResult evaluatedInput = input->evaluate(params); + if (!evaluatedInput) { + return evaluatedInput.error(); + } + const EvaluationResult evaluatedFromIndex = fromIndex->evaluate(params); + if (!fromIndex) { + return evaluatedFromIndex.error(); + } + if (!evaluatedFromIndex->is()) { + return EvaluationError{"Expected value to be of type number, but found " + + toString(typeOf(*evaluatedFromIndex)) + " instead."}; + } + + int fromIndexValue = static_cast(evaluatedFromIndex->get()); + int toIndexValue = std::numeric_limits::max(); + + if (toIndex) { + const EvaluationResult evaluatedToIndex = toIndex->evaluate(params); + if (!evaluatedToIndex) { + return evaluatedToIndex.error(); + } + if (!evaluatedToIndex->is()) { + return EvaluationError{"Expected value to be of type number, but found " + + toString(typeOf(*evaluatedFromIndex)) + " instead."}; + } + toIndexValue = static_cast(evaluatedToIndex->get()); + } + + return evaluatedInput->match( + [&](const std::string &s) { return evaluateForStringInput(s, fromIndexValue, toIndexValue); }, + [&](const std::vector &v) { return evaluateForArrayInput(v, fromIndexValue, toIndexValue); }, + [&](const auto &) -> EvaluationResult { + return EvaluationError{"Expected first argument to be of type array or string, but found " + + toString(typeOf(*evaluatedInput)) + " instead."}; + }); +} + +EvaluationResult Slice::evaluateForArrayInput(const std::vector &array, + int fromIndexValue, + int toIndexValue) const { + int length = static_cast(array.size()); + if (toIndexValue == std::numeric_limits::max()) { + toIndexValue = length; + } + fromIndexValue = normalizeIndex(fromIndexValue, length); + toIndexValue = normalizeIndex(toIndexValue, length); + + if (fromIndexValue >= toIndexValue) { + return std::vector{}; + } + toIndexValue = std::min(toIndexValue, length); + + std::vector result; + result.reserve(toIndexValue - fromIndexValue); + for (int index = fromIndexValue; index < toIndexValue; ++index) { + result.push_back(array[index]); + } + return result; +} + +EvaluationResult Slice::evaluateForStringInput(const std::string &string, int fromIndexValue, int toIndexValue) const { + int length = static_cast(string.size()); + if (toIndexValue == std::numeric_limits::max()) { + toIndexValue = length; + } + fromIndexValue = normalizeIndex(fromIndexValue, length); + toIndexValue = normalizeIndex(toIndexValue, length); + if (fromIndexValue >= length) { + return std::string{}; + } + + return string.substr(fromIndexValue, toIndexValue - fromIndexValue); +} + +void Slice::eachChild(const std::function &visit) const { + visit(*input); + visit(*fromIndex); + if (toIndex) { + visit(*toIndex); + } +} + +using namespace mbgl::style::conversion; +ParseResult Slice::parse(const Convertible &value, ParsingContext &ctx) { + assert(isArray(value)); + + std::size_t length = arrayLength(value); + if (length != 3 && length != 4) { + ctx.error("Expected 2 or 3 arguments, but found " + util::toString(length - 1) + " instead."); + return ParseResult(); + } + + ParseResult input = ctx.parse(arrayMember(value, 1), 1); + ParseResult fromIndex = ctx.parse(arrayMember(value, 2), 2); + + ParseResult toIndex = length == 4 ? ctx.parse(arrayMember(value, 3), 3) : ParseResult(); + + if (!input || !fromIndex) { + return ParseResult(); + } + + return ParseResult( + std::make_unique(std::move(*input), std::move(*fromIndex), toIndex ? std::move(*toIndex) : nullptr)); +} + +bool Slice::operator==(const Expression &e) const { + if (e.getKind() == Kind::Slice) { + auto rhs = static_cast(&e); + const auto toIndexEqual = (toIndex && rhs->toIndex && *toIndex == *(rhs->toIndex)) || + (!toIndex && !rhs->toIndex); + return *input == *(rhs->input) && *fromIndex == *(rhs->fromIndex) && toIndexEqual; + } + return false; +} + +std::vector> Slice::possibleOutputs() const { + return {std::nullopt}; +} + +std::string Slice::getOperator() const { + return "slice"; +} + +} // namespace expression +} // namespace style +} // namespace mbgl