diff options
60 files changed, 907 insertions, 176 deletions
diff --git a/cmake/core-files.txt b/cmake/core-files.txt index 2db2d5bf87..1d7d2ef41f 100644 --- a/cmake/core-files.txt +++ b/cmake/core-files.txt @@ -465,6 +465,8 @@ include/mbgl/style/expression/dsl.hpp include/mbgl/style/expression/error.hpp include/mbgl/style/expression/expression.hpp include/mbgl/style/expression/find_zoom_curve.hpp +include/mbgl/style/expression/format_expression.hpp +include/mbgl/style/expression/formatted.hpp include/mbgl/style/expression/get_covering_stops.hpp include/mbgl/style/expression/interpolate.hpp include/mbgl/style/expression/interpolator.hpp @@ -491,6 +493,8 @@ src/mbgl/style/expression/compound_expression.cpp src/mbgl/style/expression/dsl.cpp src/mbgl/style/expression/expression.cpp src/mbgl/style/expression/find_zoom_curve.cpp +src/mbgl/style/expression/format_expression.cpp +src/mbgl/style/expression/formatted.cpp src/mbgl/style/expression/get_covering_stops.cpp src/mbgl/style/expression/interpolate.cpp src/mbgl/style/expression/is_constant.cpp @@ -620,6 +624,8 @@ src/mbgl/text/quads.cpp src/mbgl/text/quads.hpp src/mbgl/text/shaping.cpp src/mbgl/text/shaping.hpp +src/mbgl/text/tagged_string.cpp +src/mbgl/text/tagged_string.hpp # tile include/mbgl/tile/tile_id.hpp diff --git a/include/mbgl/style/conversion/function.hpp b/include/mbgl/style/conversion/function.hpp index ba9acd7a3b..47ce6843b5 100644 --- a/include/mbgl/style/conversion/function.hpp +++ b/include/mbgl/style/conversion/function.hpp @@ -11,6 +11,7 @@ namespace style { namespace conversion { bool hasTokens(const std::string&); +std::unique_ptr<expression::Expression> convertTokenStringToFormatExpression(const std::string&); std::unique_ptr<expression::Expression> convertTokenStringToExpression(const std::string&); optional<std::unique_ptr<expression::Expression>> convertFunctionToExpression(expression::type::Type, const Convertible&, Error&, bool convertTokens); diff --git a/include/mbgl/style/conversion/property_value.hpp b/include/mbgl/style/conversion/property_value.hpp index f6f36db983..9d619f7a02 100644 --- a/include/mbgl/style/conversion/property_value.hpp +++ b/include/mbgl/style/conversion/property_value.hpp @@ -28,6 +28,17 @@ struct Converter<PropertyValue<T>> { ? PropertyValue<T>(PropertyExpression<T>(convertTokenStringToExpression(t))) : PropertyValue<T>(t); } + + PropertyValue<T> maybeConvertTokens(const expression::Formatted& t) const { + // This only works with a single-section `Formatted` created automatically + // by parsing a plain-text `text-field` property. + // Token conversion happens later than the initial string->Formatted conversion + // General purpose `format` expressions with embedded tokens are not supported + const std::string& firstUnformattedSection = t.sections[0].text; + return hasTokens(firstUnformattedSection) + ? PropertyValue<T>(PropertyExpression<T>(convertTokenStringToFormatExpression(firstUnformattedSection))) + : PropertyValue<T>(t); + } }; } // namespace conversion diff --git a/include/mbgl/style/expression/coercion.hpp b/include/mbgl/style/expression/coercion.hpp index d83bd6dfa7..28c2c0c679 100644 --- a/include/mbgl/style/expression/coercion.hpp +++ b/include/mbgl/style/expression/coercion.hpp @@ -10,11 +10,6 @@ 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<std::unique_ptr<Expression>> inputs_); @@ -23,6 +18,8 @@ public: EvaluationResult evaluate(const EvaluationContext& params) const override; void eachChild(const std::function<void(const Expression&)>& visit) const override; + + mbgl::Value serialize() const override; bool operator==(const Expression& e) const override; diff --git a/include/mbgl/style/expression/expression.hpp b/include/mbgl/style/expression/expression.hpp index ce02c4114b..97b143b3d9 100644 --- a/include/mbgl/style/expression/expression.hpp +++ b/include/mbgl/style/expression/expression.hpp @@ -133,6 +133,7 @@ enum class Kind : int32_t { Any, All, Comparison, + FormatExpression, }; class Expression { diff --git a/include/mbgl/style/expression/format_expression.hpp b/include/mbgl/style/expression/format_expression.hpp new file mode 100644 index 0000000000..b00674a88e --- /dev/null +++ b/include/mbgl/style/expression/format_expression.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include <mbgl/style/expression/expression.hpp> +#include <mbgl/style/expression/formatted.hpp> +#include <mbgl/style/expression/parsing_context.hpp> +#include <mbgl/style/conversion.hpp> + +#include <memory> + +namespace mbgl { +namespace style { +namespace expression { + +struct FormatExpressionSection { + FormatExpressionSection(std::unique_ptr<Expression> text_, + optional<std::unique_ptr<Expression>> fontScale_, + optional<std::unique_ptr<Expression>> textFont_); + + std::shared_ptr<Expression> text; + optional<std::shared_ptr<Expression>> fontScale; + optional<std::shared_ptr<Expression>> textFont; +}; + +class FormatExpression : public Expression { +public: + FormatExpression(std::vector<FormatExpressionSection> sections); + + EvaluationResult evaluate(const EvaluationContext&) const override; + static ParseResult parse(const mbgl::style::conversion::Convertible&, ParsingContext&); + + void eachChild(const std::function<void(const Expression&)>&) const override; + + bool operator==(const Expression& e) const override; + + std::vector<optional<Value>> possibleOutputs() const override { + // Technically the combinatoric set of all children + // Usually, this.text will be undefined anyway + return { nullopt }; + } + + mbgl::Value serialize() const override; + std::string getOperator() const override { return "format"; } +private: + std::vector<FormatExpressionSection> sections; + std::unique_ptr<Expression> text; + optional<std::unique_ptr<Expression>> fontScale; + optional<std::unique_ptr<Expression>> textFont; +}; + +} // namespace expression +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/formatted.hpp b/include/mbgl/style/expression/formatted.hpp new file mode 100644 index 0000000000..9e7e7308cb --- /dev/null +++ b/include/mbgl/style/expression/formatted.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include <mbgl/style/conversion.hpp> +#include <mbgl/util/font_stack.hpp> +#include <mbgl/util/optional.hpp> +#include <mbgl/util/variant.hpp> + +#include <vector> +#include <string> + +namespace mbgl { +namespace style { +namespace expression { + +struct FormattedSection { + FormattedSection(std::string text_, optional<double> fontScale_, optional<FontStack> fontStack_) + : text(std::move(text_)) + , fontScale(std::move(fontScale_)) + , fontStack(std::move(fontStack_)) + {} + std::string text; + optional<double> fontScale; + optional<FontStack> fontStack; +}; + +class Formatted { +public: + Formatted() = default; + + Formatted(const char* plainU8String) { + sections.emplace_back(std::string(plainU8String), nullopt, nullopt); + } + + Formatted(std::vector<FormattedSection> sections_) + : sections(std::move(sections_)) + {} + + bool operator==(const Formatted& ) const; + + std::string toString() const; + + bool empty() const { + return sections.empty() || sections.at(0).text.empty(); + } + + std::vector<FormattedSection> sections; +}; + +} // namespace expression + +namespace conversion { + +template <> +struct Converter<mbgl::style::expression::Formatted> { +public: + optional<mbgl::style::expression::Formatted> operator()(const Convertible& value, Error& error) const; +}; + +} // namespace conversion + +} // namespace style +} // namespace mbgl diff --git a/include/mbgl/style/expression/type.hpp b/include/mbgl/style/expression/type.hpp index 316496839b..a5a1e76164 100644 --- a/include/mbgl/style/expression/type.hpp +++ b/include/mbgl/style/expression/type.hpp @@ -66,6 +66,13 @@ struct CollatorType { std::string getName() const { return "collator"; } bool operator==(const CollatorType&) const { return true; } }; + +struct FormattedType { + constexpr FormattedType() {}; // NOLINT + std::string getName() const { return "formatted"; } + bool operator==(const FormattedType&) const { return true; } +}; + constexpr NullType Null; constexpr NumberType Number; @@ -75,6 +82,7 @@ constexpr ColorType Color; constexpr ValueType Value; constexpr ObjectType Object; constexpr CollatorType Collator; +constexpr FormattedType Formatted; constexpr ErrorType Error; struct Array; @@ -89,6 +97,7 @@ using Type = variant< ValueType, mapbox::util::recursive_wrapper<Array>, CollatorType, + FormattedType, ErrorType>; struct Array { diff --git a/include/mbgl/style/expression/value.hpp b/include/mbgl/style/expression/value.hpp index 2036ab8abe..91239d083f 100644 --- a/include/mbgl/style/expression/value.hpp +++ b/include/mbgl/style/expression/value.hpp @@ -1,6 +1,7 @@ #pragma once #include <mbgl/style/expression/collator.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/style/expression/type.hpp> #include <mbgl/style/position.hpp> #include <mbgl/style/types.hpp> @@ -25,6 +26,7 @@ using ValueBase = variant< std::string, Color, Collator, + Formatted, mapbox::util::recursive_wrapper<std::vector<Value>>, mapbox::util::recursive_wrapper<std::unordered_map<std::string, Value>>>; struct Value : ValueBase { diff --git a/include/mbgl/style/layers/background_layer.hpp b/include/mbgl/style/layers/background_layer.hpp index 76230df12c..ef01ea41be 100644 --- a/include/mbgl/style/layers/background_layer.hpp +++ b/include/mbgl/style/layers/background_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/circle_layer.hpp b/include/mbgl/style/layers/circle_layer.hpp index cde691c893..0f8d1c0c13 100644 --- a/include/mbgl/style/layers/circle_layer.hpp +++ b/include/mbgl/style/layers/circle_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/fill_extrusion_layer.hpp b/include/mbgl/style/layers/fill_extrusion_layer.hpp index e72fcade61..d30ffa26da 100644 --- a/include/mbgl/style/layers/fill_extrusion_layer.hpp +++ b/include/mbgl/style/layers/fill_extrusion_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/fill_layer.hpp b/include/mbgl/style/layers/fill_layer.hpp index 430d7a011f..25c46d312f 100644 --- a/include/mbgl/style/layers/fill_layer.hpp +++ b/include/mbgl/style/layers/fill_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/heatmap_layer.hpp b/include/mbgl/style/layers/heatmap_layer.hpp index fd0051f44c..347bb8a4aa 100644 --- a/include/mbgl/style/layers/heatmap_layer.hpp +++ b/include/mbgl/style/layers/heatmap_layer.hpp @@ -6,6 +6,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/hillshade_layer.hpp b/include/mbgl/style/layers/hillshade_layer.hpp index 89d0ae686f..697d4c71ad 100644 --- a/include/mbgl/style/layers/hillshade_layer.hpp +++ b/include/mbgl/style/layers/hillshade_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/layer.hpp.ejs b/include/mbgl/style/layers/layer.hpp.ejs index cf31a4d672..9d595d2035 100644 --- a/include/mbgl/style/layers/layer.hpp.ejs +++ b/include/mbgl/style/layers/layer.hpp.ejs @@ -13,6 +13,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/line_layer.hpp b/include/mbgl/style/layers/line_layer.hpp index 9350b3d102..1e55561bbd 100644 --- a/include/mbgl/style/layers/line_layer.hpp +++ b/include/mbgl/style/layers/line_layer.hpp @@ -6,6 +6,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/raster_layer.hpp b/include/mbgl/style/layers/raster_layer.hpp index fcc35412a0..b1c716c17f 100644 --- a/include/mbgl/style/layers/raster_layer.hpp +++ b/include/mbgl/style/layers/raster_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> diff --git a/include/mbgl/style/layers/symbol_layer.hpp b/include/mbgl/style/layers/symbol_layer.hpp index aabda0d794..6d82e5df85 100644 --- a/include/mbgl/style/layers/symbol_layer.hpp +++ b/include/mbgl/style/layers/symbol_layer.hpp @@ -5,6 +5,7 @@ #include <mbgl/style/layer.hpp> #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/color.hpp> @@ -121,9 +122,9 @@ public: PropertyValue<AlignmentType> getTextRotationAlignment() const; void setTextRotationAlignment(PropertyValue<AlignmentType>); - static PropertyValue<std::string> getDefaultTextField(); - PropertyValue<std::string> getTextField() const; - void setTextField(PropertyValue<std::string>); + static PropertyValue<expression::Formatted> getDefaultTextField(); + PropertyValue<expression::Formatted> getTextField() const; + void setTextField(PropertyValue<expression::Formatted>); static PropertyValue<std::vector<std::string>> getDefaultTextFont(); PropertyValue<std::vector<std::string>> getTextFont() const; diff --git a/include/mbgl/util/font_stack.hpp b/include/mbgl/util/font_stack.hpp index ace60a4ba6..ccc1a06068 100644 --- a/include/mbgl/util/font_stack.hpp +++ b/include/mbgl/util/font_stack.hpp @@ -11,11 +11,12 @@ namespace mbgl { // An array of font names using FontStack = std::vector<std::string>; +using FontStackHash = std::size_t; std::string fontStackToString(const FontStack&); -struct FontStackHash { - std::size_t operator()(const FontStack&) const; +struct FontStackHasher { + FontStackHash operator()(const FontStack&) const; }; // Statically evaluate layer properties to determine what font stacks are used. diff --git a/platform/node/src/node_expression.cpp b/platform/node/src/node_expression.cpp index c0b512a899..4f1e392505 100644 --- a/platform/node/src/node_expression.cpp +++ b/platform/node/src/node_expression.cpp @@ -37,10 +37,11 @@ type::Type parseType(v8::Local<v8::Object> type) { static std::unordered_map<std::string, type::Type> types = { {"string", type::String}, {"number", type::Number}, - {"noolean", type::Boolean}, + {"boolean", type::Boolean}, {"object", type::Object}, {"color", type::Color}, - {"value", type::Value} + {"value", type::Value}, + {"formatted", type::Formatted} }; v8::Local<v8::Value> v8kind = Nan::Get(type, Nan::New("kind").ToLocalChecked()).ToLocalChecked(); @@ -151,6 +152,33 @@ struct ToValue { Nan::EscapableHandleScope scope; return scope.Escape(Nan::Null()); } + + v8::Local<v8::Value> operator()(const Formatted& formatted) { + // This mimics the internal structure of the Formatted class in formatted.js + // A better approach might be to use the explicit serialized form + // both here and on the JS side? e.g. toJS(fromExpressionValue<mbgl::Value>(formatted)) + std::unordered_map<std::string, mbgl::Value> serialized; + std::vector<mbgl::Value> sections; + for (const auto& section : formatted.sections) { + std::unordered_map<std::string, mbgl::Value> serializedSection; + serializedSection.emplace("text", section.text); + if (section.fontScale) { + serializedSection.emplace("scale", *section.fontScale); + } else { + serializedSection.emplace("scale", mbgl::NullValue()); + } + if (section.fontStack) { + std::string fontStackString; + serializedSection.emplace("fontStack", mbgl::fontStackToString(*section.fontStack)); + } else { + serializedSection.emplace("fontStack", mbgl::NullValue()); + } + sections.push_back(serializedSection); + } + serialized.emplace("sections", sections); + + return toJS(serialized); + } v8::Local<v8::Value> operator()(const mbgl::Color& color) { return operator()(std::vector<Value> { diff --git a/platform/node/test/ignores.json b/platform/node/test/ignores.json index e6ef57bd1e..9632677d46 100644 --- a/platform/node/test/ignores.json +++ b/platform/node/test/ignores.json @@ -1,17 +1,5 @@ { "expression-tests/collator/accent-equals-de": "Locale-specific behavior changes based on platform.", - "expression-tests/format/basic": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/coercion": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/implicit": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/implicit-assert": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/implicit-coerce": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/implicit-omit": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/to-string": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/to-string": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/to-string": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/to-string": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/basic": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "expression-tests/format/to-string": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", "expression-tests/interpolate-hcl/linear": "https://github.com/mapbox/mapbox-gl-native/issues/8720", "expression-tests/interpolate-lab/linear": "https://github.com/mapbox/mapbox-gl-native/issues/8720", "expression-tests/is-supported-script/default": "This tests RTL text plugin behavior specific to GL JS", @@ -82,6 +70,7 @@ "query-tests/feature-state/default": "skip - port https://github.com/mapbox/mapbox-gl-js/pull/6263 - needs issue", "query-tests/regressions/mapbox-gl-js#6555": "skip - no querySourceFeatures in mbgl-node; needs issue", "render-tests/background-color/transition": "https://github.com/mapbox/mapbox-gl-native/issues/10619", + "render-tests/canvas/default": "skip - js specific", "render-tests/collator/resolved-locale": "Some test platforms don't resolve 'en' locale", "render-tests/collator/default": "Some test platforms don't resolve 'en' locale", "render-tests/custom-layer-js/null-island": "skip - js specific", @@ -121,6 +110,7 @@ "render-tests/regressions/mapbox-gl-js#6706": "https://github.com/mapbox/mapbox-gl-native/issues/10619", "render-tests/regressions/mapbox-gl-js#6806": "pending https://github.com/mapbox/mapbox-gl-js/pull/6812", "render-tests/regressions/mapbox-gl-js#7271": "https://github.com/mapbox/mapbox-gl-native/issues/12888", + "render-tests/regressions/mapbox-gl-js#7302": "skip - js specific", "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", @@ -128,9 +118,6 @@ "render-tests/symbol-cross-fade/chinese": "https://github.com/mapbox/mapbox-gl-native/issues/10619", "render-tests/symbol-placement/line-overscaled": "https://github.com/mapbox/mapbox-gl-js/issues/5654", "render-tests/symbol-visibility/visible": "https://github.com/mapbox/mapbox-gl-native/issues/10409", - "render-tests/text-field/formatted-arabic": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "render-tests/text-field/formatted-line": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", - "render-tests/text-field/formatted": "skip - https://github.com/mapbox/mapbox-gl-native/pull/12624", "render-tests/text-pitch-alignment/auto-text-rotation-alignment-map": "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", "render-tests/text-pitch-alignment/viewport-text-rotation-alignment-map": "https://github.com/mapbox/mapbox-gl-native/issues/9732", diff --git a/scripts/generate-style-code.js b/scripts/generate-style-code.js index 8b9b6ba5a2..121d2d774a 100755 --- a/scripts/generate-style-code.js +++ b/scripts/generate-style-code.js @@ -38,6 +38,7 @@ global.evaluatedType = function (property) { case 'number': return 'float'; case 'formatted': + return 'expression::Formatted'; case 'string': return 'std::string'; case 'enum': diff --git a/src/mbgl/layout/merge_lines.cpp b/src/mbgl/layout/merge_lines.cpp index 807ecb868f..2a3afa42b2 100644 --- a/src/mbgl/layout/merge_lines.cpp +++ b/src/mbgl/layout/merge_lines.cpp @@ -57,12 +57,14 @@ void mergeLines(std::vector<SymbolFeature>& features) { SymbolFeature& feature = features[k]; GeometryCollection& geometry = feature.geometry; - if (!feature.text || geometry.empty() || geometry[0].empty()) { + if (!feature.formattedText || geometry.empty() || geometry[0].empty()) { continue; } + + // TODO: Key should include formatting options (see https://github.com/mapbox/mapbox-gl-js/issues/3645) - const size_t leftKey = getKey(*feature.text, geometry[0].front()); - const size_t rightKey = getKey(*feature.text, geometry[0].back()); + const size_t leftKey = getKey(feature.formattedText->rawText(), geometry[0].front()); + const size_t rightKey = getKey(feature.formattedText->rawText(), geometry[0].back()); const auto left = rightIndex.find(leftKey); const auto right = leftIndex.find(rightKey); @@ -75,7 +77,7 @@ void mergeLines(std::vector<SymbolFeature>& features) { leftIndex.erase(leftKey); rightIndex.erase(rightKey); - rightIndex[getKey(*feature.text, features[i].geometry[0].back())] = i; + rightIndex[getKey(feature.formattedText->rawText(), features[i].geometry[0].back())] = i; } else if (left != rightIndex.end()) { // found mergeable line adjacent to the start of the current line, merge diff --git a/src/mbgl/layout/symbol_feature.hpp b/src/mbgl/layout/symbol_feature.hpp index f4dc1680bc..ff498f3c2a 100644 --- a/src/mbgl/layout/symbol_feature.hpp +++ b/src/mbgl/layout/symbol_feature.hpp @@ -2,6 +2,7 @@ #include <mbgl/tile/geometry_tile_data.hpp> #include <mbgl/util/optional.hpp> +#include <mbgl/text/tagged_string.hpp> #include <array> #include <string> @@ -23,7 +24,7 @@ public: std::unique_ptr<GeometryTileFeature> feature; GeometryCollection geometry; - optional<std::u16string> text; + optional<TaggedString> formattedText; optional<std::string> icon; std::size_t index; }; diff --git a/src/mbgl/layout/symbol_instance.cpp b/src/mbgl/layout/symbol_instance.cpp index a9b4b929ec..2d60020dd4 100644 --- a/src/mbgl/layout/symbol_instance.cpp +++ b/src/mbgl/layout/symbol_instance.cpp @@ -18,7 +18,7 @@ SymbolInstance::SymbolInstance(Anchor& anchor_, const float iconBoxScale, const float iconPadding, const std::array<float, 2> iconOffset_, - const GlyphPositionMap& positions, + const GlyphPositions& positions, const IndexedSubfeature& indexedFeature, const std::size_t layoutFeatureIndex_, const std::size_t dataFeatureIndex_, diff --git a/src/mbgl/layout/symbol_instance.hpp b/src/mbgl/layout/symbol_instance.hpp index ae79311790..6be19a0595 100644 --- a/src/mbgl/layout/symbol_instance.hpp +++ b/src/mbgl/layout/symbol_instance.hpp @@ -26,7 +26,7 @@ public: const float iconBoxScale, const float iconPadding, const std::array<float, 2> iconOffset, - const GlyphPositionMap&, + const GlyphPositions&, const IndexedSubfeature&, const std::size_t layoutFeatureIndex, const std::size_t dataFeatureIndex, diff --git a/src/mbgl/layout/symbol_layout.cpp b/src/mbgl/layout/symbol_layout.cpp index 74abd74f01..f4a55a1f91 100644 --- a/src/mbgl/layout/symbol_layout.cpp +++ b/src/mbgl/layout/symbol_layout.cpp @@ -108,28 +108,40 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, ft.index = i; if (hasText) { - std::string u8string = layout.evaluate<TextField>(zoom, ft); - + auto formatted = layout.evaluate<TextField>(zoom, ft); auto textTransform = layout.evaluate<TextTransform>(zoom, ft); - if (textTransform == TextTransformType::Uppercase) { - u8string = platform::uppercase(u8string); - } else if (textTransform == TextTransformType::Lowercase) { - u8string = platform::lowercase(u8string); + FontStack baseFontStack = layout.evaluate<TextFont>(zoom, ft); + FontStackHash baseFontStackHash = FontStackHasher()(baseFontStack); + + ft.formattedText = TaggedString(); + for (std::size_t j = 0; j < formatted.sections.size(); j++) { + const auto& section = formatted.sections[j]; + std::string u8string = section.text; + if (textTransform == TextTransformType::Uppercase) { + u8string = platform::uppercase(u8string); + } else if (textTransform == TextTransformType::Lowercase) { + u8string = platform::lowercase(u8string); + } + + ft.formattedText->addSection(applyArabicShaping(util::utf8_to_utf16::convert(u8string)), + section.fontScale ? *section.fontScale : 1.0, + section.fontStack ? FontStackHasher()(*section.fontStack) : baseFontStackHash); + } - ft.text = applyArabicShaping(util::utf8_to_utf16::convert(u8string)); + const bool canVerticalizeText = layout.get<TextRotationAlignment>() == AlignmentType::Map && layout.get<SymbolPlacement>() != SymbolPlacementType::Point - && util::i18n::allowsVerticalWritingMode(*ft.text); - - FontStack fontStack = layout.evaluate<TextFont>(zoom, ft); - GlyphIDs& dependencies = glyphDependencies[fontStack]; - + && util::i18n::allowsVerticalWritingMode(ft.formattedText->rawText()); + // Loop through all characters of this text and collect unique codepoints. - for (char16_t chr : *ft.text) { - dependencies.insert(chr); + for (std::size_t j = 0; j < ft.formattedText->length(); j++) { + const auto& sectionFontStack = formatted.sections[ft.formattedText->getSectionIndex(j)].fontStack; + GlyphIDs& dependencies = glyphDependencies[sectionFontStack ? *sectionFontStack : baseFontStack]; + char16_t codePoint = ft.formattedText->getCharCodeAt(j); + dependencies.insert(codePoint); if (canVerticalizeText) { - if (char16_t verticalChr = util::i18n::verticalizePunctuation(chr)) { + if (char16_t verticalChr = util::i18n::verticalizePunctuation(codePoint)) { dependencies.insert(verticalChr); } } @@ -141,7 +153,7 @@ SymbolLayout::SymbolLayout(const BucketParameters& parameters, imageDependencies.emplace(*ft.icon, ImageType::Icon); } - if (ft.text || ft.icon) { + if (ft.formattedText || ft.icon) { features.push_back(std::move(ft)); } } @@ -168,44 +180,35 @@ void SymbolLayout::prepareSymbols(const GlyphMap& glyphMap, const GlyphPositions auto& feature = *it; if (feature.geometry.empty()) continue; - FontStack fontStack = layout.evaluate<TextFont>(zoom, feature); - - auto glyphMapIt = glyphMap.find(fontStack); - const Glyphs& glyphs = glyphMapIt != glyphMap.end() - ? glyphMapIt->second : Glyphs(); - - auto glyphPositionsIt = glyphPositions.find(fontStack); - const GlyphPositionMap& glyphPositionMap = glyphPositionsIt != glyphPositions.end() - ? glyphPositionsIt->second : GlyphPositionMap(); - std::pair<Shaping, Shaping> shapedTextOrientations; optional<PositionedIcon> shapedIcon; // if feature has text, shape the text - if (feature.text) { - auto applyShaping = [&] (const std::u16string& text, WritingModeType writingMode) { + if (feature.formattedText) { + auto applyShaping = [&] (const TaggedString& formattedText, WritingModeType writingMode) { const float oneEm = 24.0f; const Shaping result = getShaping( - /* string */ text, + /* string */ formattedText, /* maxWidth: ems */ layout.get<SymbolPlacement>() == SymbolPlacementType::Point ? layout.evaluate<TextMaxWidth>(zoom, feature) * oneEm : 0, /* lineHeight: ems */ layout.get<TextLineHeight>() * oneEm, /* anchor */ layout.evaluate<TextAnchor>(zoom, feature), /* justify */ layout.evaluate<TextJustify>(zoom, feature), - /* spacing: ems */ util::i18n::allowsLetterSpacing(*feature.text) ? layout.evaluate<TextLetterSpacing>(zoom, feature) * oneEm : 0.0f, + /* spacing: ems */ util::i18n::allowsLetterSpacing(feature.formattedText->rawText()) ? layout.evaluate<TextLetterSpacing>(zoom, feature) * oneEm : 0.0f, /* translate */ Point<float>(layout.evaluate<TextOffset>(zoom, feature)[0] * oneEm, layout.evaluate<TextOffset>(zoom, feature)[1] * oneEm), /* verticalHeight */ oneEm, /* writingMode */ writingMode, /* bidirectional algorithm object */ bidi, - /* glyphs */ glyphs); + /* glyphs */ glyphMap); return result; }; - shapedTextOrientations.first = applyShaping(*feature.text, WritingModeType::Horizontal); + shapedTextOrientations.first = applyShaping(*feature.formattedText, WritingModeType::Horizontal); - if (util::i18n::allowsVerticalWritingMode(*feature.text) && textAlongLine) { - shapedTextOrientations.second = applyShaping(util::i18n::verticalizePunctuation(*feature.text), WritingModeType::Vertical); + if (util::i18n::allowsVerticalWritingMode(feature.formattedText->rawText()) && textAlongLine) { + feature.formattedText->verticalizePunctuation(); + shapedTextOrientations.second = applyShaping(*feature.formattedText, WritingModeType::Vertical); } } @@ -231,7 +234,7 @@ void SymbolLayout::prepareSymbols(const GlyphMap& glyphMap, const GlyphPositions // if either shapedText or icon position is present, add the feature if (shapedTextOrientations.first || shapedIcon) { - addFeature(std::distance(features.begin(), it), feature, shapedTextOrientations, shapedIcon, glyphPositionMap); + addFeature(std::distance(features.begin(), it), feature, shapedTextOrientations, shapedIcon, glyphPositions); } feature.geometry.clear(); @@ -244,7 +247,7 @@ void SymbolLayout::addFeature(const std::size_t layoutFeatureIndex, const SymbolFeature& feature, const std::pair<Shaping, Shaping>& shapedTextOrientations, optional<PositionedIcon> shapedIcon, - const GlyphPositionMap& glyphPositionMap) { + const GlyphPositions& glyphPositions) { const float minScale = 0.5f; const float glyphSize = 24.0f; @@ -287,7 +290,8 @@ void SymbolLayout::addFeature(const std::size_t layoutFeatureIndex, layout.evaluate(zoom, feature), layoutTextSize, textBoxScale, textPadding, textPlacement, textOffset, iconBoxScale, iconPadding, iconOffset, - glyphPositionMap, indexedFeature, layoutFeatureIndex, feature.index, feature.text.value_or(std::u16string()), overscaling); + glyphPositions, indexedFeature, layoutFeatureIndex, feature.index, + feature.formattedText ? feature.formattedText->rawText() : std::u16string(), overscaling); } }; @@ -308,7 +312,7 @@ void SymbolLayout::addFeature(const std::size_t layoutFeatureIndex, overscaling); for (auto& anchor : anchors) { - if (!feature.text || !anchorIsTooClose(*feature.text, textRepeatDistance, anchor)) { + if (!feature.formattedText || !anchorIsTooClose(feature.formattedText->rawText(), textRepeatDistance, anchor)) { addSymbolInstance(line, anchor); } } diff --git a/src/mbgl/layout/symbol_layout.hpp b/src/mbgl/layout/symbol_layout.hpp index b866703e6c..7045eebc22 100644 --- a/src/mbgl/layout/symbol_layout.hpp +++ b/src/mbgl/layout/symbol_layout.hpp @@ -55,7 +55,7 @@ private: const SymbolFeature&, const std::pair<Shaping, Shaping>& shapedTextOrientations, optional<PositionedIcon> shapedIcon, - const GlyphPositionMap&); + const GlyphPositions&); bool anchorIsTooClose(const std::u16string& text, const float repeatDistance, const Anchor&); std::map<std::u16string, std::vector<Anchor>> compareText; diff --git a/src/mbgl/style/conversion/function.cpp b/src/mbgl/style/conversion/function.cpp index 69fb0725d8..34ac52ec1b 100644 --- a/src/mbgl/style/conversion/function.cpp +++ b/src/mbgl/style/conversion/function.cpp @@ -6,6 +6,7 @@ #include <mbgl/style/expression/interpolate.hpp> #include <mbgl/style/expression/match.hpp> #include <mbgl/style/expression/case.hpp> +#include <mbgl/style/expression/format_expression.hpp> #include <mbgl/util/string.hpp> #include <cassert> @@ -36,6 +37,13 @@ bool hasTokens(const std::string& source) { return false; } + +std::unique_ptr<Expression> convertTokenStringToFormatExpression(const std::string& source) { + auto textExpression = convertTokenStringToExpression(source); + std::vector<FormatExpressionSection> sections; + sections.emplace_back(std::move(textExpression), nullopt, nullopt); + return std::make_unique<FormatExpression>(sections); +} std::unique_ptr<Expression> convertTokenStringToExpression(const std::string& source) { std::vector<std::unique_ptr<Expression>> inputs; @@ -138,6 +146,9 @@ template optional<PropertyExpression<TextTransformType>> convertFunctionToExpression<TextTransformType>(const Convertible&, Error&, bool); template optional<PropertyExpression<TranslateAnchorType>> convertFunctionToExpression<TranslateAnchorType>(const Convertible&, Error&, bool); + +template optional<PropertyExpression<Formatted>> + convertFunctionToExpression<Formatted>(const Convertible&, Error&, bool); // Ad-hoc Converters for double and int64_t. We should replace float with double wholesale, // and promote the int64_t Converter to general use (and it should check that the input is @@ -280,6 +291,15 @@ static optional<std::unique_ptr<Expression>> convertLiteral(type::Type type, con [&] (const type::CollatorType&) -> optional<std::unique_ptr<Expression>> { assert(false); // No properties use this type. return nullopt; + }, + [&] (const type::FormattedType&) -> optional<std::unique_ptr<Expression>> { + auto result = convert<std::string>(value, error); + if (!result) { + return nullopt; + } + return convertTokens ? + convertTokenStringToFormatExpression(*result) : + literal(Formatted(result->c_str())); } ); } diff --git a/src/mbgl/style/conversion/property_value.cpp b/src/mbgl/style/conversion/property_value.cpp index 3b79ecc0db..ff038908b6 100644 --- a/src/mbgl/style/conversion/property_value.cpp +++ b/src/mbgl/style/conversion/property_value.cpp @@ -78,6 +78,7 @@ template optional<PropertyValue<SymbolZOrderType>> Converter<PropertyValue<Symbo template optional<PropertyValue<TextJustifyType>> Converter<PropertyValue<TextJustifyType>>::operator()(conversion::Convertible const&, conversion::Error&, bool, bool) const; template optional<PropertyValue<TextTransformType>> Converter<PropertyValue<TextTransformType>>::operator()(conversion::Convertible const&, conversion::Error&, bool, bool) const; template optional<PropertyValue<TranslateAnchorType>> Converter<PropertyValue<TranslateAnchorType>>::operator()(conversion::Convertible const&, conversion::Error&, bool, bool) const; +template optional<PropertyValue<mbgl::style::expression::Formatted>> Converter<PropertyValue<mbgl::style::expression::Formatted>>::operator()(conversion::Convertible const&, conversion::Error&, bool, bool) const; } // namespace conversion } // namespace style diff --git a/src/mbgl/style/conversion/stringify.hpp b/src/mbgl/style/conversion/stringify.hpp index 3d188b6390..c5cd52093f 100644 --- a/src/mbgl/style/conversion/stringify.hpp +++ b/src/mbgl/style/conversion/stringify.hpp @@ -2,6 +2,8 @@ #include <mbgl/style/filter.hpp> #include <mbgl/style/property_value.hpp> +#include <mbgl/style/expression/value.hpp> +#include <mbgl/style/expression/formatted.hpp> #include <mbgl/util/enum.hpp> #include <mbgl/util/color.hpp> #include <mbgl/util/feature.hpp> @@ -129,6 +131,15 @@ void stringify(Writer& writer, const Filter& filter) { if (!filter.expression) writer.Null(); else stringify(writer, (*filter.expression)->serialize()); } + + +template <class Writer> +void stringify(Writer& writer, const expression::Formatted& v) { + // Convert to mbgl::Value and then use the existing stringify + // Serialization strategy for Formatted objects is to return the constant + // expression that would generate them. + stringify(writer, expression::ValueConverter<mbgl::Value>::fromExpressionValue(v)); +} template <class Writer> void stringify(Writer& writer, const Undefined&) { diff --git a/src/mbgl/style/expression/check_subtype.cpp b/src/mbgl/style/expression/check_subtype.cpp index 73f7d18a34..73e6e3bff7 100644 --- a/src/mbgl/style/expression/check_subtype.cpp +++ b/src/mbgl/style/expression/check_subtype.cpp @@ -40,6 +40,7 @@ optional<std::string> checkSubtype(const Type& expected, const Type& t) { String, Object, Color, + Formatted, Array(Value) }; diff --git a/src/mbgl/style/expression/coercion.cpp b/src/mbgl/style/expression/coercion.cpp index 4a17895991..75a6056081 100644 --- a/src/mbgl/style/expression/coercion.cpp +++ b/src/mbgl/style/expression/coercion.cpp @@ -82,6 +82,10 @@ EvaluationResult toColor(const Value& colorValue) { ); } +EvaluationResult toFormatted(const Value& formattedValue) { + return Formatted(toString(formattedValue).c_str()); +} + Coercion::Coercion(type::Type type_, std::vector<std::unique_ptr<Expression>> inputs_) : Expression(Kind::Coercion, std::move(type_)), inputs(std::move(inputs_)) @@ -96,11 +100,26 @@ Coercion::Coercion(type::Type type_, std::vector<std::unique_ptr<Expression>> in coerceSingleValue = toNumber; } else if (t.is<type::StringType>()) { coerceSingleValue = [] (const Value& v) -> EvaluationResult { return toString(v); }; + } else if (t.is<type::FormattedType>()) { + coerceSingleValue = toFormatted; } else { assert(false); } } +mbgl::Value Coercion::serialize() const { + if (getType().is<type::FormattedType>()) { + // Since there's no explicit "to-formatted" coercion, the only coercions should be created + // by string expressions that get implicitly coerced to "formatted". + std::vector<mbgl::Value> serialized{{ std::string("format") }}; + serialized.push_back(inputs[0]->serialize()); + serialized.push_back(std::unordered_map<std::string, mbgl::Value>()); + return serialized; + } else { + return Expression::serialize(); + } +}; + std::string Coercion::getOperator() const { return getType().match( [](const type::BooleanType&) { return "to-boolean"; }, @@ -129,10 +148,16 @@ ParseResult Coercion::parse(const Convertible& value, ParsingContext& ctx) { auto it = types.find(*toString(arrayMember(value, 0))); assert(it != types.end()); - if ((it->second == type::Boolean || it->second == type::String) && length != 2) { + if ((it->second == type::Boolean || it->second == type::String || it->second == type::Formatted) && length != 2) { ctx.error("Expected one argument."); return ParseResult(); } + + /** + * 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. + */ std::vector<std::unique_ptr<Expression>> parsed; parsed.reserve(length - 1); @@ -186,6 +211,3 @@ std::vector<optional<Value>> Coercion::possibleOutputs() const { } // namespace expression } // namespace style } // namespace mbgl - - - diff --git a/src/mbgl/style/expression/dsl.cpp b/src/mbgl/style/expression/dsl.cpp index c6318fb637..f5ff83a9e7 100644 --- a/src/mbgl/style/expression/dsl.cpp +++ b/src/mbgl/style/expression/dsl.cpp @@ -7,6 +7,7 @@ #include <mbgl/style/expression/step.hpp> #include <mbgl/style/expression/interpolate.hpp> #include <mbgl/style/expression/compound_expression.hpp> +#include <mbgl/style/expression/format_expression.hpp> namespace mbgl { namespace style { @@ -77,6 +78,10 @@ std::unique_ptr<Expression> toString(std::unique_ptr<Expression> value) { return std::make_unique<Coercion>(type::String, vec(std::move(value))); } +std::unique_ptr<Expression> toFormatted(std::unique_ptr<Expression> value) { + return std::make_unique<Coercion>(type::Formatted, vec(std::move(value))); +} + std::unique_ptr<Expression> get(const char* value) { return get(literal(value)); } @@ -181,6 +186,16 @@ std::unique_ptr<Expression> concat(std::vector<std::unique_ptr<Expression>> inpu return compound("concat", std::move(inputs)); } +std::unique_ptr<Expression> format(const char* value) { + return std::make_unique<Literal>(Formatted(value)); +} + +std::unique_ptr<Expression> format(std::unique_ptr<Expression> input) { + std::vector<FormatExpressionSection> sections; + sections.emplace_back(std::move(input), nullopt, nullopt); + return std::make_unique<FormatExpression>(sections); +} + } // namespace dsl } // namespace expression } // namespace style diff --git a/src/mbgl/style/expression/format_expression.cpp b/src/mbgl/style/expression/format_expression.cpp new file mode 100644 index 0000000000..144df4b160 --- /dev/null +++ b/src/mbgl/style/expression/format_expression.cpp @@ -0,0 +1,175 @@ +#include <mbgl/style/conversion_impl.hpp> +#include <mbgl/style/expression/format_expression.hpp> +#include <mbgl/style/expression/literal.hpp> +#include <mbgl/util/font_stack.hpp> +#include <mbgl/util/string.hpp> + +namespace mbgl { +namespace style { +namespace expression { + +FormatExpressionSection::FormatExpressionSection(std::unique_ptr<Expression> text_, + optional<std::unique_ptr<Expression>> fontScale_, + optional<std::unique_ptr<Expression>> textFont_) + : text(std::move(text_)) +{ + if (fontScale_) { + fontScale = std::shared_ptr<Expression>(std::move(*fontScale_)); + } + if (textFont_) { + textFont = std::shared_ptr<Expression>(std::move(*textFont_)); + } +} + +FormatExpression::FormatExpression(std::vector<FormatExpressionSection> sections_) + : Expression(Kind::FormatExpression, type::Formatted) + , sections(std::move(sections_)) +{} + +using namespace mbgl::style::conversion; + +ParseResult FormatExpression::parse(const Convertible& value, ParsingContext& ctx) { + std::size_t argsLength = arrayLength(value); + if (argsLength < 3) { + ctx.error("Expected at least two arguments."); + return ParseResult(); + } + + if ((argsLength - 1) % 2 != 0) { + ctx.error("Expected an even number of arguments."); + return ParseResult(); + } + + std::vector<FormatExpressionSection> sections; + for (std::size_t i = 1; i < argsLength - 1; i += 2) { + auto textArg = arrayMember(value, i); + ParseResult text = ctx.parse(textArg, 1, {type::Value}); + if (!text) { + return ParseResult(); + } + auto options = arrayMember(value, i + 1); + if (!isObject(options)) { + ctx.error("Format options argument must be an object."); + return ParseResult(); + } + + const optional<Convertible> fontScaleOption = objectMember(options, "font-scale"); + ParseResult fontScale; + if (fontScaleOption) { + fontScale = ctx.parse(*fontScaleOption, 1, {type::Number}); + if (!fontScale) { + return ParseResult(); + } + } + + const optional<Convertible> textFontOption = objectMember(options, "text-font"); + ParseResult textFont; + if (textFontOption) { + textFont = ctx.parse(*textFontOption, 1, {type::Array(type::String)}); + if (!textFont) { + return ParseResult(); + } + } + sections.emplace_back(std::move(*text), std::move(fontScale), std::move(textFont)); + } + + return ParseResult(std::make_unique<FormatExpression>(std::move(sections))); +} + +void FormatExpression::eachChild(const std::function<void(const Expression&)>& fn) const { + for (auto& section : sections) { + fn(*section.text); + if (section.fontScale) { + fn(**section.fontScale); + } + if (section.textFont) { + fn(**section.textFont); + } + } +} + +bool FormatExpression::operator==(const Expression& e) const { + if (e.getKind() == Kind::FormatExpression) { + auto rhs = static_cast<const FormatExpression*>(&e); + if (sections.size() != rhs->sections.size()) { + return false; + } + for (std::size_t i = 0; i < sections.size(); i++) { + const auto& lhsSection = sections.at(i); + const auto& rhsSection = rhs->sections.at(i); + if (*lhsSection.text != *rhsSection.text) { + return false; + } + if ((lhsSection.fontScale && (!rhsSection.fontScale || **lhsSection.fontScale != **rhsSection.fontScale)) || + (!lhsSection.fontScale && rhsSection.fontScale)) { + return false; + } + if ((lhsSection.textFont && (!rhsSection.textFont || **lhsSection.textFont != **rhsSection.textFont)) || + (!lhsSection.textFont && rhsSection.textFont)) { + return false; + } + } + return true; + } + return false; +} + +mbgl::Value FormatExpression::serialize() const { + std::vector<mbgl::Value> serialized{{ std::string("format") }}; + for (const auto& section : sections) { + serialized.push_back(section.text->serialize()); + std::unordered_map<std::string, mbgl::Value> options; + if (section.fontScale) { + options.emplace("font-scale", (*section.fontScale)->serialize()); + } + if (section.textFont) { + options.emplace("text-font", (*section.textFont)->serialize()); + } + serialized.push_back(options); + } + return serialized; +} + +EvaluationResult FormatExpression::evaluate(const EvaluationContext& params) const { + std::vector<FormattedSection> evaluatedSections; + for (const auto& section : sections) { + auto textResult = section.text->evaluate(params); + if (!textResult) { + return textResult.error(); + } + + optional<std::string> evaluatedText = toString(*textResult); + if (!evaluatedText) { + return EvaluationError({ "Could not coerce format expression text input to string." }); + } + + optional<double> evaluatedFontScale; + if (section.fontScale) { + auto fontScaleResult = (*section.fontScale)->evaluate(params); + if (!fontScaleResult) { + return fontScaleResult.error(); + } + evaluatedFontScale = fontScaleResult->get<double>(); + } + + optional<FontStack> evaluatedTextFont; + if (section.textFont) { + auto textFontResult = (*section.textFont)->evaluate(params); + if (!textFontResult) { + return textFontResult.error(); + } + auto textFontValue = ValueConverter<std::vector<std::string>>::fromExpressionValue(*textFontResult); + if (!textFontValue) { + return EvaluationError { "Format text-font option must evaluate to an array of strings" }; + } + evaluatedTextFont = *textFontValue; + } + evaluatedSections.emplace_back(*evaluatedText, evaluatedFontScale, evaluatedTextFont); + } + return Formatted(evaluatedSections); +} + +} // namespace expression +} // namespace style +} // namespace mbgl + diff --git a/src/mbgl/style/expression/formatted.cpp b/src/mbgl/style/expression/formatted.cpp new file mode 100644 index 0000000000..6eb106dfec --- /dev/null +++ b/src/mbgl/style/expression/formatted.cpp @@ -0,0 +1,61 @@ +#include <mbgl/style/expression/formatted.hpp> +#include <mbgl/style/conversion_impl.hpp> +#include <mbgl/style/expression/is_constant.hpp> +#include <mbgl/style/expression/is_expression.hpp> +#include <mbgl/style/expression/literal.hpp> +#include <mbgl/style/expression/expression.hpp> +#include <mbgl/style/expression/type.hpp> +#include <mbgl/style/expression/compound_expression.hpp> +#include <mbgl/style/expression/boolean_operator.hpp> + +namespace mbgl { +namespace style { + +namespace expression { + +bool Formatted::operator==(const Formatted& other) const { + if (other.sections.size() != sections.size()) { + return false; + } + for (std::size_t i = 0; i < sections.size(); i++) { + const auto& thisSection = sections.at(i); + const auto& otherSection = other.sections.at(i); + if (thisSection.text != otherSection.text || + thisSection.fontScale != otherSection.fontScale || + thisSection.fontStack != otherSection.fontStack) { + return false; + } + } + return true; +} + + +std::string Formatted::toString() const { + std::string result; + for (const auto& section : sections) { + result += section.text; + } + return result; +} + +} // namespace expression + +namespace conversion { + +using namespace mbgl::style::expression; + +optional<Formatted> Converter<Formatted>::operator()(const Convertible& value, Error&) const { + using namespace mbgl::style::expression; + + auto result = toString(value); + if (result) { + return Formatted(result->c_str()); + } else { + return nullopt; + } +} + +} // namespace conversion +} // namespace style +} // namespace mbgl + diff --git a/src/mbgl/style/expression/parsing_context.cpp b/src/mbgl/style/expression/parsing_context.cpp index 0373b9721f..34fbc5c380 100644 --- a/src/mbgl/style/expression/parsing_context.cpp +++ b/src/mbgl/style/expression/parsing_context.cpp @@ -13,6 +13,7 @@ #include <mbgl/style/expression/coercion.hpp> #include <mbgl/style/expression/compound_expression.hpp> #include <mbgl/style/expression/comparison.hpp> +#include <mbgl/style/expression/format_expression.hpp> #include <mbgl/style/expression/interpolate.hpp> #include <mbgl/style/expression/length.hpp> #include <mbgl/style/expression/let.hpp> @@ -110,6 +111,7 @@ const ExpressionRegistry& getExpressionRegistry() { {"case", Case::parse}, {"coalesce", Coalesce::parse}, {"collator", CollatorExpression::parse}, + {"format", FormatExpression::parse}, {"interpolate", parseInterpolate}, {"length", Length::parse}, {"let", Let::parse}, @@ -183,7 +185,7 @@ ParseResult ParsingContext::parse(const Convertible& value, optional<TypeAnnotat const type::Type actual = (*parsed)->getType(); if ((*expected == type::String || *expected == type::Number || *expected == type::Boolean || *expected == type::Object || expected->is<type::Array>()) && actual == type::Value) { parsed = { annotate(std::move(*parsed), *expected, typeAnnotationOption.value_or(TypeAnnotationOption::assert)) }; - } else if (*expected == type::Color && (actual == type::Value || actual == type::String)) { + } else if ((*expected == type::Color || *expected == type::Formatted) && (actual == type::Value || actual == type::String)) { parsed = { annotate(std::move(*parsed), *expected, typeAnnotationOption.value_or(TypeAnnotationOption::coerce)) }; } else { checkType((*parsed)->getType()); diff --git a/src/mbgl/style/expression/value.cpp b/src/mbgl/style/expression/value.cpp index 4bac8116c2..436ed83ecd 100644 --- a/src/mbgl/style/expression/value.cpp +++ b/src/mbgl/style/expression/value.cpp @@ -1,6 +1,7 @@ #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> #include <mbgl/style/expression/value.hpp> +#include <mbgl/style/conversion/stringify.hpp> namespace mbgl { namespace style { @@ -13,6 +14,7 @@ type::Type typeOf(const Value& value) { [&](const std::string&) -> type::Type { return type::String; }, [&](const Color&) -> type::Type { return type::Color; }, [&](const Collator&) -> type::Type { return type::Collator; }, + [&](const Formatted&) -> type::Type { return type::Formatted; }, [&](const NullValue&) -> type::Type { return type::Null; }, [&](const std::unordered_map<std::string, Value>&) -> type::Type { return type::Object; }, [&](const std::vector<Value>& arr) -> type::Type { @@ -38,6 +40,7 @@ std::string toString(const Value& value) { return value.match( [](const NullValue&) { return std::string(); }, [](const Color& c) { return c.stringify(); }, // avoid quoting + [](const Formatted& f) { return f.toString(); }, [](const std::string& s) { return s; }, // avoid quoting [](const auto& v_) { return stringify(v_); } ); @@ -58,6 +61,12 @@ void writeJSON(rapidjson::Writer<rapidjson::StringBuffer>& writer, const Value& // for them so there shouldn't be any way to serialize this value. assert(false); }, + [&] (const Formatted& f) { + // `stringify` in turns calls ValueConverter::fromExpressionValue below + // Serialization strategy for Formatted objects is to return the constant + // expression that would generate them. + mbgl::style::conversion::stringify(writer, f); + }, [&] (const std::vector<Value>& arr) { writer.StartArray(); for(const auto& item : arr) { @@ -136,6 +145,31 @@ mbgl::Value ValueConverter<mbgl::Value>::fromExpressionValue(const Value& value) assert(false); return mbgl::Value(); }, + [&](const Formatted& formatted)->mbgl::Value { + // Serialization strategy for Formatted objects is to return the constant + // expression that would generate them. + std::vector<mbgl::Value> serialized; + static std::string formatOperator("format"); + serialized.emplace_back(formatOperator); + for (const auto& section : formatted.sections) { + serialized.emplace_back(section.text); + std::unordered_map<std::string, mbgl::Value> options; + + if (section.fontScale) { + options.emplace("font-scale", *section.fontScale); + } + + if (section.fontStack) { + std::vector<mbgl::Value> fontStack; + for (const auto& font : *section.fontStack) { + fontStack.emplace_back(font); + } + options.emplace("text-font", std::vector<mbgl::Value>{ std::string("literal"), fontStack }); + } + serialized.push_back(options); + } + return serialized; + }, [&](const std::vector<Value>& values)->mbgl::Value { std::vector<mbgl::Value> converted; converted.reserve(values.size()); @@ -262,6 +296,7 @@ template <> type::Type valueTypeToExpressionType<double>() { return type::Number template <> type::Type valueTypeToExpressionType<std::string>() { return type::String; } template <> type::Type valueTypeToExpressionType<Color>() { return type::Color; } template <> type::Type valueTypeToExpressionType<Collator>() { return type::Collator; } +template <> type::Type valueTypeToExpressionType<Formatted>() { return type::Formatted; } template <> type::Type valueTypeToExpressionType<std::unordered_map<std::string, Value>>() { return type::Object; } template <> type::Type valueTypeToExpressionType<std::vector<Value>>() { return type::Array(type::Value); } diff --git a/src/mbgl/style/layers/symbol_layer.cpp b/src/mbgl/style/layers/symbol_layer.cpp index c116d5b7e9..848678b5f1 100644 --- a/src/mbgl/style/layers/symbol_layer.cpp +++ b/src/mbgl/style/layers/symbol_layer.cpp @@ -421,15 +421,15 @@ void SymbolLayer::setTextRotationAlignment(PropertyValue<AlignmentType> value) { baseImpl = std::move(impl_); observer->onLayerChanged(*this); } -PropertyValue<std::string> SymbolLayer::getDefaultTextField() { +PropertyValue<expression::Formatted> SymbolLayer::getDefaultTextField() { return TextField::defaultValue(); } -PropertyValue<std::string> SymbolLayer::getTextField() const { +PropertyValue<expression::Formatted> SymbolLayer::getTextField() const { return impl().layout.get<TextField>(); } -void SymbolLayer::setTextField(PropertyValue<std::string> value) { +void SymbolLayer::setTextField(PropertyValue<expression::Formatted> value) { if (value == getTextField()) return; auto impl_ = mutableImpl(); @@ -1928,22 +1928,15 @@ optional<Error> SymbolLayer::setLayoutProperty(const std::string& name, const Co } - if (property == Property::IconImage || property == Property::TextField) { + if (property == Property::IconImage) { Error error; optional<PropertyValue<std::string>> typedValue = convert<PropertyValue<std::string>>(value, error, true, true); if (!typedValue) { return error; } - if (property == Property::IconImage) { - setIconImage(*typedValue); - return nullopt; - } - - if (property == Property::TextField) { - setTextField(*typedValue); - return nullopt; - } + setIconImage(*typedValue); + return nullopt; } @@ -1985,6 +1978,18 @@ optional<Error> SymbolLayer::setLayoutProperty(const std::string& name, const Co } + if (property == Property::TextField) { + Error error; + optional<PropertyValue<expression::Formatted>> typedValue = convert<PropertyValue<expression::Formatted>>(value, error, true, true); + if (!typedValue) { + return error; + } + + setTextField(*typedValue); + return nullopt; + + } + if (property == Property::TextFont) { Error error; optional<PropertyValue<std::vector<std::string>>> typedValue = convert<PropertyValue<std::vector<std::string>>>(value, error, true, false); diff --git a/src/mbgl/style/layers/symbol_layer_properties.hpp b/src/mbgl/style/layers/symbol_layer_properties.hpp index 10d059e787..6c147f440d 100644 --- a/src/mbgl/style/layers/symbol_layer_properties.hpp +++ b/src/mbgl/style/layers/symbol_layer_properties.hpp @@ -112,9 +112,9 @@ struct TextRotationAlignment : LayoutProperty<AlignmentType> { static AlignmentType defaultValue() { return AlignmentType::Auto; } }; -struct TextField : DataDrivenLayoutProperty<std::string> { +struct TextField : DataDrivenLayoutProperty<expression::Formatted> { static constexpr const char * key = "text-field"; - static std::string defaultValue() { return ""; } + static expression::Formatted defaultValue() { return ""; } }; struct TextFont : DataDrivenLayoutProperty<std::vector<std::string>> { diff --git a/src/mbgl/text/bidi.hpp b/src/mbgl/text/bidi.hpp index d90f3e5d1b..5ce2887db8 100644 --- a/src/mbgl/text/bidi.hpp +++ b/src/mbgl/text/bidi.hpp @@ -14,6 +14,10 @@ class BiDiImpl; std::u16string applyArabicShaping(const std::u16string&); +// StyledText pairs each code point in a string with an integer indicating +// the styling options to use for rendering that code point +// The data structure is intended to accomodate the reordering/interleaving +// of formatting that can happen when BiDi rearranges inputs using StyledText = std::pair<std::u16string, std::vector<uint8_t>>; class BiDi : private util::noncopyable { @@ -21,7 +25,10 @@ public: BiDi(); ~BiDi(); + // Given text in logical ordering and a set of line break points, + // return a set of lines in visual order with bidi and line breaking applied std::vector<std::u16string> processText(const std::u16string&, std::set<std::size_t>); + // Same as processText but preserves per-code-point formatting information std::vector<StyledText> processStyledText(const StyledText&, std::set<std::size_t>); private: diff --git a/src/mbgl/text/glyph.hpp b/src/mbgl/text/glyph.hpp index 2c03da308a..55cd50fd9b 100644 --- a/src/mbgl/text/glyph.hpp +++ b/src/mbgl/text/glyph.hpp @@ -55,17 +55,21 @@ public: }; using Glyphs = std::map<GlyphID, optional<Immutable<Glyph>>>; -using GlyphMap = std::map<FontStack, Glyphs>; +using GlyphMap = std::map<FontStackHash, Glyphs>; class PositionedGlyph { public: - explicit PositionedGlyph(GlyphID glyph_, float x_, float y_, bool vertical_) - : glyph(glyph_), x(x_), y(y_), vertical(vertical_) {} + explicit PositionedGlyph(GlyphID glyph_, float x_, float y_, bool vertical_, FontStackHash font_, float scale_) + : glyph(glyph_), x(x_), y(y_), vertical(vertical_), font(font_), scale(scale_) + {} GlyphID glyph = 0; float x = 0; float y = 0; bool vertical = false; + + FontStackHash font = 0; + float scale = 0.0; }; enum class WritingModeType : uint8_t; diff --git a/src/mbgl/text/glyph_atlas.cpp b/src/mbgl/text/glyph_atlas.cpp index 1b98ea36bf..da65aea8a9 100644 --- a/src/mbgl/text/glyph_atlas.cpp +++ b/src/mbgl/text/glyph_atlas.cpp @@ -14,7 +14,7 @@ GlyphAtlas makeGlyphAtlas(const GlyphMap& glyphs) { mapbox::ShelfPack pack(0, 0, options); for (const auto& glyphMapEntry : glyphs) { - const FontStack& fontStack = glyphMapEntry.first; + FontStackHash fontStack = glyphMapEntry.first; GlyphPositionMap& positions = result.positions[fontStack]; for (const auto& entry : glyphMapEntry.second) { diff --git a/src/mbgl/text/glyph_atlas.hpp b/src/mbgl/text/glyph_atlas.hpp index bb9115e4b4..9dd063ef69 100644 --- a/src/mbgl/text/glyph_atlas.hpp +++ b/src/mbgl/text/glyph_atlas.hpp @@ -12,7 +12,7 @@ struct GlyphPosition { }; using GlyphPositionMap = std::map<GlyphID, GlyphPosition>; -using GlyphPositions = std::map<FontStack, GlyphPositionMap>; +using GlyphPositions = std::map<FontStackHash, GlyphPositionMap>; class GlyphAtlas { public: diff --git a/src/mbgl/text/glyph_manager.cpp b/src/mbgl/text/glyph_manager.cpp index 8e7cfe5ba7..c4a7a2de66 100644 --- a/src/mbgl/text/glyph_manager.cpp +++ b/src/mbgl/text/glyph_manager.cpp @@ -130,7 +130,7 @@ void GlyphManager::notify(GlyphRequestor& requestor, const GlyphDependencies& gl const FontStack& fontStack = dependency.first; const GlyphIDs& glyphIDs = dependency.second; - Glyphs& glyphs = response[fontStack]; + Glyphs& glyphs = response[FontStackHasher()(fontStack)]; Entry& entry = entries[fontStack]; for (const auto& glyphID : glyphIDs) { diff --git a/src/mbgl/text/glyph_manager.hpp b/src/mbgl/text/glyph_manager.hpp index 642471bbf2..831d84719c 100644 --- a/src/mbgl/text/glyph_manager.hpp +++ b/src/mbgl/text/glyph_manager.hpp @@ -62,7 +62,7 @@ private: std::map<GlyphID, Immutable<Glyph>> glyphs; }; - std::unordered_map<FontStack, Entry, FontStackHash> entries; + std::unordered_map<FontStack, Entry, FontStackHasher> entries; void requestRange(GlyphRequest&, const FontStack&, const GlyphRange&); void processResponse(const Response&, const FontStack&, const GlyphRange&); diff --git a/src/mbgl/text/quads.cpp b/src/mbgl/text/quads.cpp index ec4461ac6d..9d582f14d6 100644 --- a/src/mbgl/text/quads.cpp +++ b/src/mbgl/text/quads.cpp @@ -94,7 +94,7 @@ SymbolQuad getIconQuad(const PositionedIcon& shapedIcon, SymbolQuads getGlyphQuads(const Shaping& shapedText, const SymbolLayoutProperties::Evaluated& layout, const style::SymbolPlacementType placement, - const GlyphPositionMap& positions) { + const GlyphPositions& positions) { const float textRotate = layout.get<TextRotate>() * util::DEG2RAD; const float oneEm = 24.0; @@ -105,8 +105,12 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, SymbolQuads quads; for (const PositionedGlyph &positionedGlyph: shapedText.positionedGlyphs) { - auto positionsIt = positions.find(positionedGlyph.glyph); - if (positionsIt == positions.end()) + auto fontPositions = positions.find(positionedGlyph.font); + if (fontPositions == positions.end()) + continue; + + auto positionsIt = fontPositions->second.find(positionedGlyph.glyph); + if (positionsIt == fontPositions->second.end()) continue; const GlyphPosition& glyph = positionsIt->second; @@ -116,7 +120,7 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, const float glyphPadding = 1.0f; const float rectBuffer = 3.0f + glyphPadding; - const float halfAdvance = glyph.metrics.advance / 2.0; + const float halfAdvance = glyph.metrics.advance * positionedGlyph.scale / 2.0; const bool alongLine = layout.get<TextRotationAlignment>() == AlignmentType::Map && placement != SymbolPlacementType::Point; const Point<float> glyphOffset = alongLine ? @@ -128,10 +132,10 @@ SymbolQuads getGlyphQuads(const Shaping& shapedText, Point<float>{ positionedGlyph.x + halfAdvance + textOffset[0], positionedGlyph.y + textOffset[1] }; - const float x1 = glyph.metrics.left - rectBuffer - halfAdvance + builtInOffset.x; - const float y1 = -glyph.metrics.top - rectBuffer + builtInOffset.y; - const float x2 = x1 + rect.w; - const float y2 = y1 + rect.h; + const float x1 = (glyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset.x; + const float y1 = (-glyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset.y; + const float x2 = x1 + rect.w * positionedGlyph.scale; + const float y2 = y1 + rect.h * positionedGlyph.scale; Point<float> tl{x1, y1}; Point<float> tr{x2, y1}; diff --git a/src/mbgl/text/quads.hpp b/src/mbgl/text/quads.hpp index 33d003c935..44a35a5014 100644 --- a/src/mbgl/text/quads.hpp +++ b/src/mbgl/text/quads.hpp @@ -48,6 +48,6 @@ SymbolQuad getIconQuad(const PositionedIcon& shapedIcon, SymbolQuads getGlyphQuads(const Shaping& shapedText, const style::SymbolLayoutProperties::Evaluated&, style::SymbolPlacementType placement, - const GlyphPositionMap& positions); + const GlyphPositions& positions); } // namespace mbgl diff --git a/src/mbgl/text/shaping.cpp b/src/mbgl/text/shaping.cpp index a8232836b6..d6cbb2a4f8 100644 --- a/src/mbgl/text/shaping.cpp +++ b/src/mbgl/text/shaping.cpp @@ -91,7 +91,7 @@ void align(Shaping& shaping, // justify left = 0, right = 1, center = .5 void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs, - const Glyphs& glyphs, + const GlyphMap& glyphMap, std::size_t start, std::size_t end, float justify) { @@ -100,9 +100,13 @@ void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs, } PositionedGlyph& glyph = positionedGlyphs[end]; - auto it = glyphs.find(glyph.glyph); - if (it != glyphs.end() && it->second) { - const uint32_t lastAdvance = (*it->second)->metrics.advance; + auto glyphs = glyphMap.find(glyph.font); + if (glyphs == glyphMap.end()) { + return; + } + auto it = glyphs->second.find(glyph.glyph); + if (it != glyphs->second.end() && it->second) { + const float lastAdvance = (*it->second)->metrics.advance * glyph.scale; const float lineIndent = float(glyph.x + lastAdvance) * justify; for (std::size_t j = start; j <= end; j++) { @@ -111,17 +115,25 @@ void justifyLine(std::vector<PositionedGlyph>& positionedGlyphs, } } -float determineAverageLineWidth(const std::u16string& logicalInput, +float determineAverageLineWidth(const TaggedString& logicalInput, const float spacing, float maxWidth, - const Glyphs& glyphs) { + const GlyphMap& glyphMap) { float totalWidth = 0; - for (char16_t chr : logicalInput) { - auto it = glyphs.find(chr); - if (it != glyphs.end() && it->second) { - totalWidth += (*it->second)->metrics.advance + spacing; + for (std::size_t i = 0; i < logicalInput.length(); i++) { + const SectionOptions& section = logicalInput.getSection(i); + char16_t codePoint = logicalInput.getCharCodeAt(i); + auto glyphs = glyphMap.find(section.fontStackHash); + if (glyphs == glyphMap.end()) { + continue; + } + auto it = glyphs->second.find(codePoint); + if (it == glyphs->second.end() || !it->second) { + continue; } + + totalWidth += (*it->second)->metrics.advance * section.scale + spacing; } int32_t targetLineCount = ::fmax(1, std::ceil(totalWidth / maxWidth)); @@ -209,11 +221,11 @@ std::set<std::size_t> leastBadBreaks(const PotentialBreak& lastLineBreak) { // We determine line breaks based on shaped text in logical order. Working in visual order would be // more intuitive, but we can't do that because the visual order may be changed by line breaks! -std::set<std::size_t> determineLineBreaks(const std::u16string& logicalInput, +std::set<std::size_t> determineLineBreaks(const TaggedString& logicalInput, const float spacing, float maxWidth, const WritingModeType writingMode, - const Glyphs& glyphs) { + const GlyphMap& glyphMap) { if (!maxWidth || writingMode != WritingModeType::Horizontal) { return {}; } @@ -222,40 +234,45 @@ std::set<std::size_t> determineLineBreaks(const std::u16string& logicalInput, return {}; } - const float targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphs); + const float targetWidth = determineAverageLineWidth(logicalInput, spacing, maxWidth, glyphMap); std::list<PotentialBreak> potentialBreaks; float currentX = 0; - for (std::size_t i = 0; i < logicalInput.size(); i++) { - const char16_t codePoint = logicalInput[i]; - auto it = glyphs.find(codePoint); - if (it != glyphs.end() && it->second && !boost::algorithm::is_any_of(u" \t\n\v\f\r")(codePoint)) { - currentX += (*it->second)->metrics.advance + spacing; + for (std::size_t i = 0; i < logicalInput.length(); i++) { + const SectionOptions& section = logicalInput.getSection(i); + char16_t codePoint = logicalInput.getCharCodeAt(i); + auto glyphs = glyphMap.find(section.fontStackHash); + if (glyphs == glyphMap.end()) { + continue; + } + auto it = glyphs->second.find(codePoint); + if (it != glyphs->second.end() && it->second && !boost::algorithm::is_any_of(u" \t\n\v\f\r")(codePoint)) { + currentX += (*it->second)->metrics.advance * section.scale + spacing; } // Ideographic characters, spaces, and word-breaking punctuation that often appear without // surrounding spaces. - if ((i < logicalInput.size() - 1) && + if ((i < logicalInput.length() - 1) && (util::i18n::allowsWordBreaking(codePoint) || util::i18n::allowsIdeographicBreaking(codePoint))) { potentialBreaks.push_back(evaluateBreak(i+1, currentX, targetWidth, potentialBreaks, - calculatePenalty(codePoint, logicalInput[i+1]), + calculatePenalty(codePoint, logicalInput.getCharCodeAt(i+1)), false)); } } - return leastBadBreaks(evaluateBreak(logicalInput.size(), currentX, targetWidth, potentialBreaks, 0, true)); + return leastBadBreaks(evaluateBreak(logicalInput.length(), currentX, targetWidth, potentialBreaks, 0, true)); } void shapeLines(Shaping& shaping, - const std::vector<std::u16string>& lines, - const float spacing, - const float lineHeight, - const style::SymbolAnchorType textAnchor, - const style::TextJustifyType textJustify, - const float verticalHeight, - const WritingModeType writingMode, - const Glyphs& glyphs) { + std::vector<TaggedString>& lines, + const float spacing, + const float lineHeight, + const style::SymbolAnchorType textAnchor, + const style::TextJustifyType textJustify, + const float verticalHeight, + const WritingModeType writingMode, + const GlyphMap& glyphMap) { // the y offset *should* be part of the font metadata const int32_t yOffset = -17; @@ -265,13 +282,16 @@ void shapeLines(Shaping& shaping, float maxLineLength = 0; + const float justify = textJustify == style::TextJustifyType::Right ? 1 : textJustify == style::TextJustifyType::Left ? 0 : 0.5; - for (std::u16string line : lines) { + for (TaggedString& line : lines) { // Collapse whitespace so it doesn't throw off justification - boost::algorithm::trim_if(line, boost::algorithm::is_any_of(u" \t\n\v\f\r")); + line.trim(); + + const double lineMaxScale = line.getMaxScale(); if (line.empty()) { y += lineHeight; // Still need a line feed after empty line @@ -279,20 +299,31 @@ void shapeLines(Shaping& shaping, } std::size_t lineStartIndex = shaping.positionedGlyphs.size(); - for (char16_t chr : line) { - auto it = glyphs.find(chr); - if (it == glyphs.end() || !it->second) { + for (std::size_t i = 0; i < line.length(); i++) { + const SectionOptions& section = line.getSection(i); + char16_t codePoint = line.getCharCodeAt(i); + auto glyphs = glyphMap.find(section.fontStackHash); + if (glyphs == glyphMap.end()) { + continue; + } + auto it = glyphs->second.find(codePoint); + if (it == glyphs->second.end() || !it->second) { continue; } + // We don't know the baseline, but since we're laying out + // at 24 points, we can calculate how much it will move when + // we scale up or down. + const double baselineOffset = (lineMaxScale - section.scale) * 24; + const Glyph& glyph = **it->second; - if (writingMode == WritingModeType::Horizontal || !util::i18n::hasUprightVerticalOrientation(chr)) { - shaping.positionedGlyphs.emplace_back(chr, x, y, false); - x += glyph.metrics.advance + spacing; + if (writingMode == WritingModeType::Horizontal || !util::i18n::hasUprightVerticalOrientation(codePoint)) { + shaping.positionedGlyphs.emplace_back(codePoint, x, y + baselineOffset, false, section.fontStackHash, section.scale); + x += glyph.metrics.advance * section.scale + spacing; } else { - shaping.positionedGlyphs.emplace_back(chr, x, 0, true); - x += verticalHeight + spacing; + shaping.positionedGlyphs.emplace_back(codePoint, x, baselineOffset, true, section.fontStackHash, section.scale); + x += verticalHeight * section.scale + spacing; } } @@ -301,19 +332,19 @@ void shapeLines(Shaping& shaping, float lineLength = x - spacing; // Don't count trailing spacing maxLineLength = util::max(lineLength, maxLineLength); - justifyLine(shaping.positionedGlyphs, glyphs, lineStartIndex, + justifyLine(shaping.positionedGlyphs, glyphMap, lineStartIndex, shaping.positionedGlyphs.size() - 1, justify); } x = 0; - y += lineHeight; + y += lineHeight * lineMaxScale; } auto anchorAlign = getAnchorAlignment(textAnchor); align(shaping, justify, anchorAlign.horizontalAlign, anchorAlign.verticalAlign, maxLineLength, lineHeight, lines.size()); - const float height = lines.size() * lineHeight; + const float height = y - yOffset; // Calculate the bounding box shaping.top += -anchorAlign.verticalAlign * height; @@ -322,7 +353,7 @@ void shapeLines(Shaping& shaping, shaping.right = shaping.left + maxLineLength; } -const Shaping getShaping(const std::u16string& logicalInput, +const Shaping getShaping(const TaggedString& formattedString, const float maxWidth, const float lineHeight, const style::SymbolAnchorType textAnchor, @@ -332,12 +363,23 @@ const Shaping getShaping(const std::u16string& logicalInput, const float verticalHeight, const WritingModeType writingMode, BiDi& bidi, - const Glyphs& glyphs) { + const GlyphMap& glyphs) { Shaping shaping(translate.x, translate.y, writingMode); - std::vector<std::u16string> reorderedLines = - bidi.processText(logicalInput, - determineLineBreaks(logicalInput, spacing, maxWidth, writingMode, glyphs)); + std::vector<TaggedString> reorderedLines; + if (formattedString.sectionCount() == 1) { + auto untaggedLines = bidi.processText(formattedString.rawText(), + determineLineBreaks(formattedString, spacing, maxWidth, writingMode, glyphs)); + for (const auto& line : untaggedLines) { + reorderedLines.emplace_back(line, formattedString.sectionAt(0)); + } + } else { + auto processedLines = bidi.processStyledText(formattedString.getStyledText(), + determineLineBreaks(formattedString, spacing, maxWidth, writingMode, glyphs)); + for (const auto& line : processedLines) { + reorderedLines.emplace_back(line, formattedString.getSections()); + } + } shapeLines(shaping, reorderedLines, spacing, lineHeight, textAnchor, textJustify, verticalHeight, writingMode, glyphs); diff --git a/src/mbgl/text/shaping.hpp b/src/mbgl/text/shaping.hpp index 0a961849e5..50ac893098 100644 --- a/src/mbgl/text/shaping.hpp +++ b/src/mbgl/text/shaping.hpp @@ -1,6 +1,7 @@ #pragma once #include <mbgl/text/glyph.hpp> +#include <mbgl/text/tagged_string.hpp> #include <mbgl/renderer/image_atlas.hpp> #include <mbgl/style/types.hpp> @@ -45,7 +46,7 @@ public: float angle() const { return _angle; } }; -const Shaping getShaping(const std::u16string& string, +const Shaping getShaping(const TaggedString& string, float maxWidth, float lineHeight, style::SymbolAnchorType textAnchor, @@ -55,6 +56,6 @@ const Shaping getShaping(const std::u16string& string, float verticalHeight, const WritingModeType, BiDi& bidi, - const Glyphs& glyphs); + const GlyphMap& glyphs); } // namespace mbgl diff --git a/src/mbgl/text/tagged_string.cpp b/src/mbgl/text/tagged_string.cpp new file mode 100644 index 0000000000..e199a7c962 --- /dev/null +++ b/src/mbgl/text/tagged_string.cpp @@ -0,0 +1,42 @@ +#include <mbgl/text/tagged_string.hpp> +#include <mbgl/util/i18n.hpp> + +#include <boost/algorithm/string.hpp> + +namespace mbgl { + +void TaggedString::addSection(const std::u16string& sectionText, double scale, FontStackHash fontStack) { + styledText.first += sectionText; + sections.emplace_back(scale, fontStack); + styledText.second.resize(styledText.first.size(), sections.size() - 1); +} + +void TaggedString::trim() { + auto whiteSpace = boost::algorithm::is_any_of(u" \t\n\v\f\r"); + std::size_t beginningWhitespace = styledText.first.find_first_not_of(u" \t\n\v\f\r"); + if (beginningWhitespace == std::u16string::npos) { + // Entirely whitespace + styledText.first.clear(); + styledText.second.clear(); + } else { + std::size_t trailingWhitespace = styledText.first.find_last_not_of(u" \t\n\v\f\r") + 1; + + styledText.first = styledText.first.substr(beginningWhitespace, trailingWhitespace - beginningWhitespace); + styledText.second = std::vector<uint8_t>(styledText.second.begin() + beginningWhitespace, styledText.second.begin() + trailingWhitespace); + } +} + +double TaggedString::getMaxScale() const { + double maxScale = 0.0; + for (std::size_t i = 0; i < styledText.first.length(); i++) { + maxScale = std::max(maxScale, getSection(i).scale); + } + return maxScale; +} + +void TaggedString::verticalizePunctuation() { + // Relies on verticalization changing characters in place so that style indices don't need updating + styledText.first = util::i18n::verticalizePunctuation(styledText.first); +} + +} // namespace mbgl diff --git a/src/mbgl/text/tagged_string.hpp b/src/mbgl/text/tagged_string.hpp new file mode 100644 index 0000000000..476c2225f0 --- /dev/null +++ b/src/mbgl/text/tagged_string.hpp @@ -0,0 +1,97 @@ +#pragma once + +#include <mbgl/text/glyph.hpp> +#include <mbgl/text/bidi.hpp> + +namespace mbgl { + +struct SectionOptions { + SectionOptions(double scale_, FontStackHash fontStackHash_) + : scale(scale_), fontStackHash(fontStackHash_) + {} + + double scale; + FontStackHash fontStackHash; +}; + +/** + * A TaggedString is the shaping-code counterpart of the Formatted type + * Whereas Formatted matches the logical structure of a 'format' expression, + * a TaggedString represents the same data at a per-character level so that + * character-rearranging operations (e.g. BiDi) preserve formatting. + * Text is represented as: + * - A string of characters + * - A matching array of indices, pointing to: + * - An array of SectionsOptions, representing the evaluated formatting + * options of the original sections. + * + * Once the guts of a TaggedString have been re-arranged by BiDi, you can + * iterate over the contents in order, using getCharCodeAt and getSection + * to get the formatting options for each character in turn. + */ +struct TaggedString { + TaggedString() = default; + + TaggedString(std::u16string text_, SectionOptions options) + : styledText(std::move(text_), + std::vector<uint8_t>(text_.size(), 0)) { + sections.push_back(std::move(options)); + } + + TaggedString(StyledText styledText_, std::vector<SectionOptions> sections_) + : styledText(std::move(styledText_)) + , sections(std::move(sections_)) { + } + + std::size_t length() const { + return styledText.first.length(); + } + + std::size_t sectionCount() const { + return sections.size(); + } + + bool empty() const { + return styledText.first.empty(); + } + + const SectionOptions& getSection(std::size_t index) const { + return sections.at(styledText.second.at(index)); + } + + char16_t getCharCodeAt(std::size_t index) const { + return styledText.first[index]; + } + + const std::u16string& rawText() const { + return styledText.first; + } + + const StyledText& getStyledText() const { + return styledText; + } + + void addSection(const std::u16string& text, double scale, FontStackHash fontStack); + const SectionOptions& sectionAt(std::size_t index) const { + return sections.at(index); + } + + const std::vector<SectionOptions>& getSections() const { + return sections; + } + + uint8_t getSectionIndex(std::size_t characterIndex) const { + return styledText.second.at(characterIndex); + } + + double getMaxScale() const; + void trim(); + + void verticalizePunctuation(); + +private: + StyledText styledText; + std::vector<SectionOptions> sections; +}; + +} // namespace mbgl diff --git a/src/mbgl/tile/geometry_tile_worker.cpp b/src/mbgl/tile/geometry_tile_worker.cpp index c9032894a4..e16b805f6b 100644 --- a/src/mbgl/tile/geometry_tile_worker.cpp +++ b/src/mbgl/tile/geometry_tile_worker.cpp @@ -252,18 +252,24 @@ void GeometryTileWorker::coalesce() { void GeometryTileWorker::onGlyphsAvailable(GlyphMap newGlyphMap) { for (auto& newFontGlyphs : newGlyphMap) { - const FontStack& fontStack = newFontGlyphs.first; + FontStackHash fontStack = newFontGlyphs.first; Glyphs& newGlyphs = newFontGlyphs.second; Glyphs& glyphs = glyphMap[fontStack]; - GlyphIDs& pendingGlyphIDs = pendingGlyphDependencies[fontStack]; - - for (auto& newGlyph : newGlyphs) { - const GlyphID& glyphID = newGlyph.first; - optional<Immutable<Glyph>>& glyph = newGlyph.second; - - if (pendingGlyphIDs.erase(glyphID)) { - glyphs.emplace(glyphID, std::move(glyph)); + for (auto& pendingGlyphDependency : pendingGlyphDependencies) { + // Linear lookup here to handle reverse of FontStackHash -> FontStack, + // since dependencies need the full font stack name to make a request + // There should not be many fontstacks to look through + if (FontStackHasher()(pendingGlyphDependency.first) == fontStack) { + GlyphIDs& pendingGlyphIDs = pendingGlyphDependency.second; + for (auto& newGlyph : newGlyphs) { + const GlyphID& glyphID = newGlyph.first; + optional<Immutable<Glyph>>& glyph = newGlyph.second; + + if (pendingGlyphIDs.erase(glyphID)) { + glyphs.emplace(glyphID, std::move(glyph)); + } + } } } } @@ -282,7 +288,7 @@ void GeometryTileWorker::onImagesAvailable(ImageMap newIconMap, ImageMap newPatt void GeometryTileWorker::requestNewGlyphs(const GlyphDependencies& glyphDependencies) { for (auto& fontDependencies : glyphDependencies) { - auto fontGlyphs = glyphMap.find(fontDependencies.first); + auto fontGlyphs = glyphMap.find(FontStackHasher()(fontDependencies.first)); for (auto glyphID : fontDependencies.second) { if (fontGlyphs == glyphMap.end() || fontGlyphs->second.find(glyphID) == fontGlyphs->second.end()) { pendingGlyphDependencies[fontDependencies.first].insert(glyphID); diff --git a/src/mbgl/util/font_stack.cpp b/src/mbgl/util/font_stack.cpp index 177d5e6f31..fb1b716fb6 100644 --- a/src/mbgl/util/font_stack.cpp +++ b/src/mbgl/util/font_stack.cpp @@ -13,7 +13,7 @@ std::string fontStackToString(const FontStack& fontStack) { return boost::algorithm::join(fontStack, ","); } -std::size_t FontStackHash::operator()(const FontStack& fontStack) const { +FontStackHash FontStackHasher::operator()(const FontStack& fontStack) const { return boost::hash_range(fontStack.begin(), fontStack.end()); } diff --git a/src/mbgl/util/i18n.cpp b/src/mbgl/util/i18n.cpp index 5530796915..0fa6122673 100644 --- a/src/mbgl/util/i18n.cpp +++ b/src/mbgl/util/i18n.cpp @@ -556,6 +556,9 @@ bool hasRotatedVerticalOrientation(char16_t chr) { return !(hasUprightVerticalOrientation(chr) || hasNeutralVerticalOrientation(chr)); } +// Replaces "horizontal" with "vertical" punctuation in place +// Does not re-order or change length of string +// (TaggedString::verticalizePunctuation depends on this behavior) std::u16string verticalizePunctuation(const std::u16string& input) { std::u16string output; diff --git a/test/text/cross_tile_symbol_index.test.cpp b/test/text/cross_tile_symbol_index.test.cpp index 4060d1665c..866e79729c 100644 --- a/test/text/cross_tile_symbol_index.test.cpp +++ b/test/text/cross_tile_symbol_index.test.cpp @@ -6,12 +6,12 @@ using namespace mbgl; SymbolInstance makeSymbolInstance(float x, float y, std::u16string key) { GeometryCoordinates line; - GlyphPositionMap gpm; + GlyphPositions positions; const std::pair<Shaping, Shaping> shaping(Shaping{}, Shaping{}); style::SymbolLayoutProperties::Evaluated layout_; IndexedSubfeature subfeature(0, "", "", 0); Anchor anchor(x, y, 0, 0); - return {anchor, line, shaping, {}, layout_, 0, 0, 0, style::SymbolPlacementType::Point, {{0, 0}}, 0, 0, {{0, 0}}, gpm, subfeature, 0, 0, key, 0 }; + return {anchor, line, shaping, {}, layout_, 0, 0, 0, style::SymbolPlacementType::Point, {{0, 0}}, 0, 0, {{0, 0}}, positions, subfeature, 0, 0, key, 0 }; } diff --git a/test/text/glyph_manager.test.cpp b/test/text/glyph_manager.test.cpp index a96e1b970c..3d7a220dc5 100644 --- a/test/text/glyph_manager.test.cpp +++ b/test/text/glyph_manager.test.cpp @@ -107,7 +107,7 @@ TEST(GlyphManager, LoadingSuccess) { }; test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at({{"Test Stack"}}); + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); ASSERT_EQ(testPositions.size(), 3u); ASSERT_EQ(testPositions.count(u'a'), 1u); @@ -224,7 +224,7 @@ TEST(GlyphManager, LoadLocalCJKGlyph) { test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { EXPECT_EQ(glyphResponses, 0); // Local generation should prevent requesting any glyphs - const auto& testPositions = glyphs.at({{"Test Stack"}}); + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); ASSERT_EQ(testPositions.size(), 1u); ASSERT_EQ(testPositions.count(u'中'), 1u); @@ -275,7 +275,7 @@ TEST(GlyphManager, LoadingInvalid) { }; test.requestor.glyphsAvailable = [&] (GlyphMap glyphs) { - const auto& testPositions = glyphs.at({{"Test Stack"}}); + const auto& testPositions = glyphs.at(FontStackHasher()({{"Test Stack"}})); ASSERT_EQ(testPositions.size(), 2u); ASSERT_FALSE(bool(testPositions.at(u'A'))); diff --git a/test/text/quads.test.cpp b/test/text/quads.test.cpp index 8eedd9bd2e..c032d58b88 100644 --- a/test/text/quads.test.cpp +++ b/test/text/quads.test.cpp @@ -50,7 +50,7 @@ TEST(getIconQuads, style) { shapedText.bottom = 30.0f; shapedText.left = -60.0f; shapedText.right = 20.0f; - shapedText.positionedGlyphs.emplace_back(PositionedGlyph(32, 0.0f, 0.0f, false)); + shapedText.positionedGlyphs.emplace_back(PositionedGlyph(32, 0.0f, 0.0f, false, 0, 1.0)); // none { diff --git a/test/util/merge_lines.test.cpp b/test/util/merge_lines.test.cpp index d3a2ebae03..032a09ba21 100644 --- a/test/util/merge_lines.test.cpp +++ b/test/util/merge_lines.test.cpp @@ -24,7 +24,9 @@ public: optional<std::string> icon_, std::size_t index_) : SymbolFeature(std::make_unique<StubGeometryTileFeature>(std::move(id_), type_, std::move(geometry_), std::move(properties_))) { - text = std::move(text_); + if (text_) { + formattedText = TaggedString(*text_, SectionOptions(1.0, 0)); + } icon = std::move(icon_); index = index_; } |