From 12b496945498358b7afb4af1efe4ae0c52b9c7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Paczos?= Date: Fri, 28 Sep 2018 09:05:41 +0200 Subject: [android] "format" expression support --- .../mapboxsdk/style/expressions/Expression.java | 319 ++++++++++++++++++--- .../mapboxsdk/style/layers/PropertyValue.java | 4 +- .../mapbox/mapboxsdk/style/layers/SymbolLayer.java | 28 +- .../mapbox/mapboxsdk/style/layers/layer.java.ejs | 34 +++ .../mapbox/mapboxsdk/style/types/Formatted.java | 54 ++++ .../mapboxsdk/style/types/FormattedSection.java | 100 +++++++ 6 files changed, 491 insertions(+), 48 deletions(-) create mode 100644 platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/Formatted.java create mode 100644 platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/FormattedSection.java (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java') diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/expressions/Expression.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/expressions/Expression.java index f586ecfdb2..1184baba65 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/expressions/Expression.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/expressions/Expression.java @@ -183,7 +183,7 @@ public class Expression { */ public static Expression literal(@NonNull Object object) { if (object.getClass().isArray()) { - return literal(ExpressionArray.toObjectArray(object)); + return literal(toObjectArray(object)); } else if (object instanceof Expression) { throw new RuntimeException("Can't convert an expression to a literal"); } @@ -197,7 +197,7 @@ public class Expression { * @return the expression */ public static Expression literal(@NonNull Object[] array) { - return new ExpressionArray(array); + return new Expression("literal", new ExpressionLiteralArray(array)); } /** @@ -2872,7 +2872,8 @@ public class Expression { * * @param expression the expression to evaluate * @return expression - * @see Style specification + * @see Style + * specification */ public static Expression isSupportedScript(Expression expression) { return new Expression("is-supported-script", expression); @@ -2902,7 +2903,8 @@ public class Expression { * * @param string the string to evaluate * @return expression - * @see Style specification + * @see Style + * specification */ public static Expression isSupportedScript(String string) { return new Expression("is-supported-script", literal(string)); @@ -3224,9 +3226,121 @@ public class Expression { return new Expression("collator", new ExpressionMap(map)); } - public static Expression format(Expression input) { - Map map = new HashMap<>(); - return new Expression("format", input, new ExpressionMap(map)); + /** + * Returns formatted text containing annotations for use in mixed-format text-field entries. + *

+ * To build the expression, use {@link #formatEntry(Expression, FormatOption...)}. + *

+ * "format" expression can be used, for example, with the {@link PropertyFactory#textField(Expression)} + * and accepts unlimited numbers of formatted sections. + *

+ * Each section consist of the input, the displayed text, and options, like font-scale and text-font. + *

+ * Example usage: + *

+ *
+   * {@code
+   * SymbolLayer symbolLayer = new SymbolLayer("layer-id", "source-id");
+   * symbolLayer.setProperties(
+   *   textField(
+   *     format(
+   *       formatEntry(
+   *         get("header_property"),
+   *         fontScale(2.0),
+   *         textFont(new String[] {"DIN Offc Pro Regular", "Arial Unicode MS Regular"})
+   *       ),
+   *       formatEntry(concat(literal("\n"), get("description_property")), fontScale(1.5)),
+   * }
+   * 
+ * + * @param formatEntries format entries + * @return expression + * @see Style specification + */ + public static Expression format(@NonNull FormatEntry... formatEntries) { + // for each entry we are going to build an input and parameters + Expression[] mappedExpressions = new Expression[formatEntries.length * 2]; + + int mappedIndex = 0; + for (FormatEntry formatEntry : formatEntries) { + // input + mappedExpressions[mappedIndex++] = formatEntry.text; + + // parameters + Map map = new HashMap<>(); + + if (formatEntry.options != null) { + for (FormatOption option : formatEntry.options) { + map.put(option.type, option.value); + } + } + + mappedExpressions[mappedIndex++] = new ExpressionMap(map); + } + + return new Expression("format", mappedExpressions); + } + + /** + * Returns a format entry that can be used in {@link #format(FormatEntry...)} to create formatted text fields. + *

+ * Text is required to be of a resulting type string. + *

+ * Text is required to be passed; {@link FormatOption}s are optional and will default to the base values defined + * for the symbol. + * + * @param text displayed text + * @param formatOptions format options + * @return format entry + */ + public static FormatEntry formatEntry(@NonNull Expression text, @Nullable FormatOption... formatOptions) { + return new FormatEntry(text, formatOptions); + } + + /** + * Returns a format entry that can be used in {@link #format(FormatEntry...)} to create formatted text fields. + *

+ * Text is required to be of a resulting type string. + *

+ * Text is required to be passed; {@link FormatOption}s are optional and will default to the base values defined + * for the symbol. + * + * @param text displayed text + * @return format entry + */ + public static FormatEntry formatEntry(@NonNull Expression text) { + return new FormatEntry(text, null); + } + + /** + * Returns a format entry that can be used in {@link #format(FormatEntry...)} to create formatted text fields. + *

+ * Text is required to be of a resulting type string. + *

+ * Text is required to be passed; {@link FormatOption}s are optional and will default to the base values defined + * for the symbol. + * + * @param text displayed text + * @param formatOptions format options + * @return format entry + */ + public static FormatEntry formatEntry(@NonNull String text, @Nullable FormatOption... formatOptions) { + return new FormatEntry(literal(text), formatOptions); + } + + /** + * Returns a format entry that can be used in {@link #format(FormatEntry...)} to create formatted text fields. + *

+ * Text is required to be of a resulting type string. + *

+ * Text is required to be passed; {@link FormatOption}s are optional and will default to the base values defined + * for the symbol. + * + * @param text displayed text + * @return format entry + */ + public static FormatEntry formatEntry(@NonNull String text) { + return new FormatEntry(literal(text), null); } /** @@ -3981,18 +4095,7 @@ public class Expression { if (arguments != null) { for (Object argument : arguments) { builder.append(", "); - if (argument instanceof ExpressionLiteral) { - Object literalValue = ((ExpressionLiteral) argument).toValue(); - - // special case for handling unusual input like 'rgba(r, g, b, a)' - if (literalValue instanceof String) { - builder.append("\"").append(literalValue).append("\""); - } else { - builder.append(literalValue); - } - } else { - builder.append(argument.toString()); - } + builder.append(argument.toString()); } } builder.append("]"); @@ -4230,6 +4333,99 @@ public class Expression { } } + /** + * Holds format entries used in a {@link #format(FormatEntry...)} expression. + */ + public static class FormatEntry { + @NonNull + private Expression text; + @Nullable + private FormatOption[] options; + + FormatEntry(@NonNull Expression text, @Nullable FormatOption[] options) { + this.text = text; + this.options = options; + } + } + + /** + * Holds format options used in a {@link #formatEntry(Expression, FormatOption...)} that builds + * a {@link #format(FormatEntry...)} expression. + *

+ * If an option is not set, it defaults to the base value defined for the symbol. + */ + public static class FormatOption { + @NonNull + private String type; + @NonNull + private Expression value; + + FormatOption(@NonNull String type, @NonNull Expression value) { + this.type = type; + this.value = value; + } + + /** + * If set, the font-scale argument specifies a scaling factor relative to the text-size + * specified in the root layout properties. + *

+ * "font-scale" is required to be of a resulting type number. + * + * @param expression expression + * @return format option + */ + @NonNull + public static FormatOption fontScale(@NonNull Expression expression) { + return new FormatOption("font-scale", expression); + } + + /** + * If set, the font-scale argument specifies a scaling factor relative to the text-size + * specified in the root layout properties. + *

+ * "font-scale" is required to be of a resulting type number. + * + * @param scale value + * @return format option + */ + @NonNull + public static FormatOption fontScale(double scale) { + return new FormatOption("font-scale", literal(scale)); + } + + /** + * If set, the text-font argument overrides the font specified by the root layout properties. + *

+ * "text-font" is required to a literal array. + *

+ * The requested font stack has to be a part of the used style. + * For more information see the documentation. + * + * @param expression expression + * @return format option + */ + @NonNull + public static FormatOption textFont(@NonNull Expression expression) { + return new FormatOption("text-font", expression); + } + + /** + * If set, the text-font argument overrides the font specified by the root layout properties. + *

+ * "text-font" is required to a literal array. + *

+ * The requested font stack has to be a part of the used style. + * For more information see the documentation. + * + * @param fontStack value + * @return format option + */ + @NonNull + public static FormatOption textFont(@NonNull String[] fontStack) { + return new FormatOption("text-font", literal(fontStack)); + } + } + /** * Converts a JsonArray or a raw expression to a Java expression. */ @@ -4251,10 +4447,24 @@ public class Expression { final String operator = jsonArray.get(0).getAsString(); final List arguments = new ArrayList<>(); - JsonElement jsonElement; for (int i = 1; i < jsonArray.size(); i++) { - jsonElement = jsonArray.get(i); - arguments.add(convert(jsonElement)); + JsonElement jsonElement = jsonArray.get(i); + if (operator.equals("literal") && jsonElement instanceof JsonArray) { + JsonArray nestedArray = (JsonArray) jsonElement; + Object[] array = new Object[nestedArray.size()]; + for (int j = 0; j < nestedArray.size(); j++) { + JsonElement element = nestedArray.get(j); + if (element instanceof JsonPrimitive) { + array[j] = convertToValue((JsonPrimitive) element); + } else { + throw new IllegalArgumentException("Nested literal arrays are not supported."); + } + } + + arguments.add(new ExpressionLiteralArray(array)); + } else { + arguments.add(convert(jsonElement)); + } } return new Expression(operator, arguments.toArray(new Expression[arguments.size()])); } @@ -4290,12 +4500,22 @@ public class Expression { * @return the expression literal */ private static Expression convert(@NonNull JsonPrimitive jsonPrimitive) { + return new ExpressionLiteral(convertToValue(jsonPrimitive)); + } + + /** + * Converts a JsonPrimitive to value + * + * @param jsonPrimitive the json primitive to convert + * @return the value + */ + private static Object convertToValue(@NonNull JsonPrimitive jsonPrimitive) { if (jsonPrimitive.isBoolean()) { - return new Expression.ExpressionLiteral(jsonPrimitive.getAsBoolean()); + return jsonPrimitive.getAsBoolean(); } else if (jsonPrimitive.isNumber()) { - return new Expression.ExpressionLiteral(jsonPrimitive.getAsFloat()); + return jsonPrimitive.getAsFloat(); } else if (jsonPrimitive.isString()) { - return new Expression.ExpressionLiteral(jsonPrimitive.getAsString()); + return jsonPrimitive.getAsString(); } else { throw new RuntimeException("Unsupported literal expression conversion for " + jsonPrimitive.getClass()); } @@ -4316,20 +4536,15 @@ public class Expression { /** * Expression to wrap Object[] as a literal */ - private static class ExpressionArray extends Expression { - - private Object[] array; - - ExpressionArray(Object[] array) { - this.array = array; - } + private static class ExpressionLiteralArray extends ExpressionLiteral { - @NonNull - @Override - public Object[] toArray() { - return new Object[] { - "literal", array - }; + /** + * Create an expression literal. + * + * @param object the object to be treated as literal + */ + ExpressionLiteralArray(@NonNull Object[] object) { + super(object); } /** @@ -4339,10 +4554,10 @@ public class Expression { */ @Override public String toString() { - StringBuilder builder = new StringBuilder("[\"literal\"], ["); - Object argument; + Object[] array = (Object[]) literal; + StringBuilder builder = new StringBuilder("["); for (int i = 0; i < array.length; i++) { - argument = array[i]; + Object argument = array[i]; if (argument instanceof String) { builder.append("\"").append(argument).append("\""); } else { @@ -4353,9 +4568,23 @@ public class Expression { builder.append(", "); } } - builder.append("]]"); + builder.append("]"); return builder.toString(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ExpressionLiteralArray that = (ExpressionLiteralArray) o; + + return Arrays.equals((Object[]) this.literal, (Object[]) that.literal); + } } /** @@ -4373,8 +4602,8 @@ public class Expression { Map unwrappedMap = new HashMap<>(); for (String key : map.keySet()) { Expression expression = map.get(key); - if (expression instanceof Expression.ExpressionLiteral) { - unwrappedMap.put(key, ((ExpressionLiteral) expression).toValue()); + if (expression instanceof ValueExpression) { + unwrappedMap.put(key, ((ValueExpression) expression).toValue()); } else { unwrappedMap.put(key, expression.toArray()); } @@ -4437,7 +4666,7 @@ public class Expression { * @param object the object to convert to an object array * @return the converted object array */ - static Object[] toObjectArray(Object object) { + private static Object[] toObjectArray(Object object) { // object is a primitive array int len = java.lang.reflect.Array.getLength(object); Object[] objects = new Object[len]; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyValue.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyValue.java index 848165f00f..6936c302b2 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyValue.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/PropertyValue.java @@ -62,7 +62,7 @@ public class PropertyValue { ? Expression.Converter.convert((JsonArray) value) : (Expression) value; } else { - Logger.w(TAG, "not a expression, try value"); + Logger.w(TAG, "not an expression, try PropertyValue#getValue()"); return null; } } @@ -87,7 +87,7 @@ public class PropertyValue { // noinspection unchecked return value; } else { - Logger.w(TAG, "not a value, try function"); + Logger.w(TAG, "not a value, try PropertyValue#getExpression()"); return null; } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/SymbolLayer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/SymbolLayer.java index 1d45f34bd3..7b9128343c 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/SymbolLayer.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/SymbolLayer.java @@ -13,6 +13,8 @@ import static com.mapbox.mapboxsdk.utils.ColorUtils.rgbaToColor; import com.google.gson.JsonArray; import com.mapbox.mapboxsdk.style.expressions.Expression; import com.mapbox.mapboxsdk.style.layers.TransitionOptions; +import com.mapbox.mapboxsdk.style.types.Formatted; +import com.mapbox.mapboxsdk.style.types.FormattedSection; /** * An icon or a text label. @@ -365,7 +367,31 @@ public class SymbolLayer extends Layer { @SuppressWarnings("unchecked") public PropertyValue getTextField() { checkThread(); - return (PropertyValue) new PropertyValue("text-field", nativeGetTextField()); + + PropertyValue propertyValue = new PropertyValue<>("text-field", nativeGetTextField()); + if (propertyValue.isExpression()) { + return (PropertyValue) propertyValue; + } else { + Formatted formatted = (Formatted) nativeGetTextField(); + StringBuilder builder = new StringBuilder(); + for (FormattedSection section : formatted.getFormattedSections()) { + builder.append(section.getText()); + } + + return (PropertyValue) new PropertyValue("text-field", builder.toString()); + } + } + + /** + * Get the TextField property as {@link Formatted} object + * + * @return property wrapper value around String + * @see Expression#format(Expression...) + */ + @SuppressWarnings("unchecked") + public PropertyValue getFormattedTextField() { + checkThread(); + return (PropertyValue) new PropertyValue("text-field", nativeGetTextField()); } /** diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/layer.java.ejs b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/layer.java.ejs index 958cb7383d..961991c7a1 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/layer.java.ejs +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/layers/layer.java.ejs @@ -18,6 +18,10 @@ import static com.mapbox.mapboxsdk.utils.ColorUtils.rgbaToColor; import com.google.gson.JsonArray; import com.mapbox.mapboxsdk.style.expressions.Expression; import com.mapbox.mapboxsdk.style.layers.TransitionOptions; +<% if (type === 'symbol') { -%> +import com.mapbox.mapboxsdk.style.types.Formatted; +import com.mapbox.mapboxsdk.style.types.FormattedSection; +<% } -%> /** * <%- doc %> @@ -171,8 +175,38 @@ public class <%- camelize(type) %>Layer extends Layer { @SuppressWarnings("unchecked") public PropertyValue<<%- propertyType(property) %>> get<%- camelize(property.name) %>() { checkThread(); +<% if (property.name === 'text-field' && property.type === 'formatted') { -%> + + PropertyValue propertyValue = new PropertyValue<>("text-field", nativeGetTextField()); + if (propertyValue.isExpression()) { + return (PropertyValue) propertyValue; + } else { + Formatted formatted = (Formatted) nativeGetTextField(); + StringBuilder builder = new StringBuilder(); + for (FormattedSection section : formatted.getFormattedSections()) { + builder.append(section.getText()); + } + + return (PropertyValue) new PropertyValue("text-field", builder.toString()); + } +<% } else { -%> return (PropertyValue<<%- propertyType(property) %>>) new PropertyValue("<%- property.name %>", nativeGet<%- camelize(property.name) %>()); +<% } -%> + } +<% if (property.name === 'text-field' && property.type === 'formatted') { -%> + + /** + * Get the <%- camelize(property.name) %> property as {@link Formatted} object + * + * @return property wrapper value around <%- propertyType(property) %> + * @see Expression#format(Expression...) + */ + @SuppressWarnings("unchecked") + public PropertyValue getFormatted<%- camelize(property.name) %>() { + checkThread(); + return (PropertyValue) new PropertyValue("<%- property.name %>", nativeGet<%- camelize(property.name) %>()); } +<% } -%> <% if (property.type == 'color') { -%> /** diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/Formatted.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/Formatted.java new file mode 100644 index 0000000000..b11a1b5bc7 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/Formatted.java @@ -0,0 +1,54 @@ +package com.mapbox.mapboxsdk.style.types; + +import android.support.annotation.Keep; +import android.support.annotation.VisibleForTesting; + +import java.util.Arrays; + +/** + * Represents a string broken into sections annotated with separate formatting options. + * + * @see Style specification + */ +@Keep +public class Formatted { + private final FormattedSection[] formattedSections; + + /** + * Create a new formatted text. + * + * @param formattedSections sections with formatting options + */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public Formatted(FormattedSection[] formattedSections) { + this.formattedSections = formattedSections; + } + + /** + * Returns sections with separate formatting options that are a part of this formatted text. + * + * @return formatted sections + */ + public FormattedSection[] getFormattedSections() { + return formattedSections; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Formatted formatted = (Formatted) o; + + return Arrays.equals(formattedSections, formatted.formattedSections); + } + + @Override + public int hashCode() { + return Arrays.hashCode(formattedSections); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/FormattedSection.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/FormattedSection.java new file mode 100644 index 0000000000..b3c36414cc --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/style/types/FormattedSection.java @@ -0,0 +1,100 @@ +package com.mapbox.mapboxsdk.style.types; + +import android.support.annotation.Keep; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import java.util.Arrays; + +/** + * A component of the {@link Formatted}. + */ +@Keep +public class FormattedSection { + private String text; + private double fontScale; + private String[] fontStack; + + /** + * Creates a formatted section. + * + * @param text displayed string + * @param fontScale scale of the font, 1.0 is default + * @param fontStack main and fallback fonts that are a part of the style + */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public FormattedSection(@NonNull String text, double fontScale, @Nullable String[] fontStack) { + this.text = text; + this.fontScale = fontScale; + this.fontStack = fontStack; + } + + /** + * Creates a formatted section. + * + * @param text displayed string + * @param fontScale scale of the font, 1.0 is default + */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public FormattedSection(@NonNull String text, double fontScale) { + this.text = text; + this.fontScale = fontScale; + } + + /** + * Returns the displayed text. + * + * @return text + */ + @NonNull + public String getText() { + return text; + } + + /** + * Returns displayed text's font scale. + * + * @return font scale, defaults to 1.0 + */ + public double getFontScale() { + return fontScale; + } + + /** + * Returns the font stack with main and fallback fonts. + * + * @return font stack + */ + @Nullable + public String[] getFontStack() { + return fontStack; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + FormattedSection section = (FormattedSection) o; + + return Double.compare(section.fontScale, fontScale) == 0 + && (text != null ? text.equals(section.text) : section.text == null) + && Arrays.equals(fontStack, section.fontStack); + } + + @Override + public int hashCode() { + int result; + long temp; + result = text != null ? text.hashCode() : 0; + temp = Double.doubleToLongBits(fontScale); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + result = 31 * result + Arrays.hashCode(fontStack); + return result; + } +} -- cgit v1.2.1