Implement slice expression (#1133)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
Siarhei Fedartsou
2023-08-26 18:32:45 +02:00
committed by GitHub
parent ecd59d4cc4
commit 09f17c0fc4
15 changed files with 417 additions and 40 deletions

View File

@@ -241,6 +241,7 @@ list(APPEND INCLUDE_FILES
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/match.hpp ${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/number_format.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/parsing_context.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/step.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/type.hpp ${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/type.hpp
${PROJECT_SOURCE_DIR}/include/mbgl/style/expression/value.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/match.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/number_format.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/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/step.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/util.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/util.cpp
${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/util.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/style/expression/util.hpp

View File

@@ -398,6 +398,7 @@ MLN_CORE_SOURCE = [
"src/mbgl/style/expression/match.cpp", "src/mbgl/style/expression/match.cpp",
"src/mbgl/style/expression/number_format.cpp", "src/mbgl/style/expression/number_format.cpp",
"src/mbgl/style/expression/parsing_context.cpp", "src/mbgl/style/expression/parsing_context.cpp",
"src/mbgl/style/expression/slice.cpp",
"src/mbgl/style/expression/step.cpp", "src/mbgl/style/expression/step.cpp",
"src/mbgl/style/expression/util.cpp", "src/mbgl/style/expression/util.cpp",
"src/mbgl/style/expression/util.hpp", "src/mbgl/style/expression/util.hpp",
@@ -719,6 +720,7 @@ MLN_CORE_HEADERS = [
"include/mbgl/style/expression/match.hpp", "include/mbgl/style/expression/match.hpp",
"include/mbgl/style/expression/number_format.hpp", "include/mbgl/style/expression/number_format.hpp",
"include/mbgl/style/expression/parsing_context.hpp", "include/mbgl/style/expression/parsing_context.hpp",
"include/mbgl/style/expression/slice.hpp",
"include/mbgl/style/expression/step.hpp", "include/mbgl/style/expression/step.hpp",
"include/mbgl/style/expression/type.hpp", "include/mbgl/style/expression/type.hpp",
"include/mbgl/style/expression/value.hpp", "include/mbgl/style/expression/value.hpp",

View File

@@ -179,7 +179,8 @@ enum class Kind : int32_t {
In, In,
Within, Within,
Distance, Distance,
IndexOf IndexOf,
Slice
}; };
class Expression { class Expression {

View File

@@ -0,0 +1,39 @@
#pragma once
#include <mbgl/style/expression/expression.hpp>
#include <mbgl/style/conversion.hpp>
#include <memory>
namespace mbgl {
namespace style {
namespace expression {
class Slice : public Expression {
public:
Slice(std::unique_ptr<Expression> input_,
std::unique_ptr<Expression> fromIndex_,
std::unique_ptr<Expression> toIndex_);
static ParseResult parse(const mbgl::style::conversion::Convertible& value, ParsingContext& ctx);
EvaluationResult evaluate(const EvaluationContext& params) const override;
void eachChild(const std::function<void(const Expression&)>&) const override;
bool operator==(const Expression& e) const override;
std::vector<std::optional<Value>> 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<Value>& input, int fromIndexValue, int toIndexValue) const;
private:
std::unique_ptr<Expression> input;
std::unique_ptr<Expression> fromIndex;
std::unique_ptr<Expression> toIndex;
};
} // namespace expression
} // namespace style
} // namespace mbgl

View File

@@ -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]]
}
}

View File

@@ -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], []]
}
}

View File

@@ -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."
}
]
}
}

View File

@@ -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"]
}
}

View File

@@ -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"]
}
}

View File

@@ -5,6 +5,7 @@ MapLibre welcomes participation and contributions from everyone. Please read [`C
## main ## main
### ✨ Features and improvements ### ✨ 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. - 💥 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.

View File

@@ -1577,6 +1577,33 @@ public class Expression {
return new Expression("index-of", keyword, input, fromIndex); 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 <a href="https://maplibre.org/maplibre-style-spec/expressions/#slice">Style specification</a>
*/
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 <a href="https://maplibre.org/maplibre-style-spec/expressions/#slice">Style specification</a>
*/
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. * Retrieves whether an item exists in an array or a substring exists in a string.
* *

View File

@@ -477,6 +477,26 @@ class ExpressionTest {
Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual)) Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual))
} }
@Test
@Throws(Exception::class)
fun testSlice() {
val expected = arrayOf<Any>("slice", arrayOf<Any>("literal", arrayOf<Any>(1f, 2f, 3f)), 1f)
val actual =
Expression.slice(Expression.literal(arrayOf<Any>(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<Any>("slice", arrayOf<Any>("literal", arrayOf<Any>(1f, 2f, 3f)), 1f, 3f)
val actual =
Expression.slice(Expression.literal(arrayOf<Any>(1f, 2f, 3f)), Expression.literal(1f), Expression.literal(3f))
.toArray()
Assert.assertTrue("expression should match", Arrays.deepEquals(expected, actual))
}
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testInNumber() { fun testInNumber() {

View File

@@ -2,7 +2,7 @@
#include <mbgl/style/conversion_impl.hpp> #include <mbgl/style/conversion_impl.hpp>
#include <mbgl/style/expression/index_of.hpp> #include <mbgl/style/expression/index_of.hpp>
#include <mbgl/util/string.hpp> #include <mbgl/util/string.hpp>
#include <iostream>
namespace mbgl { namespace mbgl {
namespace style { namespace style {
namespace expression { namespace expression {

View File

@@ -26,6 +26,7 @@
#include <mbgl/style/expression/step.hpp> #include <mbgl/style/expression/step.hpp>
#include <mbgl/style/expression/within.hpp> #include <mbgl/style/expression/within.hpp>
#include <mbgl/style/expression/index_of.hpp> #include <mbgl/style/expression/index_of.hpp>
#include <mbgl/style/expression/slice.hpp>
#include <mbgl/style/expression/find_zoom_curve.hpp> #include <mbgl/style/expression/find_zoom_curve.hpp>
#include <mbgl/style/expression/dsl.hpp> #include <mbgl/style/expression/dsl.hpp>
@@ -105,8 +106,7 @@ ParseResult ParsingContext::parse(const Convertible& value,
using ParseFunction = ParseResult (*)(const conversion::Convertible&, ParsingContext&); using ParseFunction = ParseResult (*)(const conversion::Convertible&, ParsingContext&);
MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry = MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry =
mapbox::eternal::hash_map<mapbox::eternal::string, ParseFunction>({ mapbox::eternal::hash_map<mapbox::eternal::string, ParseFunction>({{"==", parseComparison},
{"==", parseComparison},
{"!=", parseComparison}, {"!=", parseComparison},
{">", parseComparison}, {">", parseComparison},
{"<", parseComparison}, {"<", parseComparison},
@@ -141,7 +141,7 @@ MAPBOX_ETERNAL_CONSTEXPR const auto expressionRegistry =
{"var", Var::parse}, {"var", Var::parse},
{"within", Within::parse}, {"within", Within::parse},
{"index-of", IndexOf::parse}, {"index-of", IndexOf::parse},
}); {"slice", Slice::parse}});
bool isExpression(const std::string& name) { bool isExpression(const std::string& name) {
return expressionRegistry.contains(name.c_str()); return expressionRegistry.contains(name.c_str());

View File

@@ -0,0 +1,156 @@
#include "mbgl/style/expression/expression.hpp"
#include <limits>
#include <mbgl/style/conversion_impl.hpp>
#include <mbgl/style/expression/slice.hpp>
#include <mbgl/util/string.hpp>
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<Expression> input_,
std::unique_ptr<Expression> fromIndex_,
std::unique_ptr<Expression> toIndex_)
: Expression(Kind::Slice, input_->getType()),
input(std::move(input_)),
fromIndex(std::move(fromIndex_)),
toIndex(std::move(toIndex_)) {}
EvaluationResult Slice::evaluate(const EvaluationContext &params) 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<double>()) {
return EvaluationError{"Expected value to be of type number, but found " +
toString(typeOf(*evaluatedFromIndex)) + " instead."};
}
int fromIndexValue = static_cast<int>(evaluatedFromIndex->get<double>());
int toIndexValue = std::numeric_limits<int>::max();
if (toIndex) {
const EvaluationResult evaluatedToIndex = toIndex->evaluate(params);
if (!evaluatedToIndex) {
return evaluatedToIndex.error();
}
if (!evaluatedToIndex->is<double>()) {
return EvaluationError{"Expected value to be of type number, but found " +
toString(typeOf(*evaluatedFromIndex)) + " instead."};
}
toIndexValue = static_cast<int>(evaluatedToIndex->get<double>());
}
return evaluatedInput->match(
[&](const std::string &s) { return evaluateForStringInput(s, fromIndexValue, toIndexValue); },
[&](const std::vector<Value> &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<Value> &array,
int fromIndexValue,
int toIndexValue) const {
int length = static_cast<int>(array.size());
if (toIndexValue == std::numeric_limits<int>::max()) {
toIndexValue = length;
}
fromIndexValue = normalizeIndex(fromIndexValue, length);
toIndexValue = normalizeIndex(toIndexValue, length);
if (fromIndexValue >= toIndexValue) {
return std::vector<Value>{};
}
toIndexValue = std::min(toIndexValue, length);
std::vector<Value> 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<int>(string.size());
if (toIndexValue == std::numeric_limits<int>::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<void(const Expression &)> &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<Slice>(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<const Slice *>(&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<std::optional<Value>> Slice::possibleOutputs() const {
return {std::nullopt};
}
std::string Slice::getOperator() const {
return "slice";
}
} // namespace expression
} // namespace style
} // namespace mbgl