[android] "format" expression support

This commit is contained in:
Łukasz Paczos
2018-09-28 09:05:41 +02:00
committed by Łukasz Paczos
parent 78a1c30e80
commit 12b4969454
23 changed files with 1062 additions and 109 deletions

View File

@@ -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 <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-is-supported-script">Style specification</a>
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-is-supported-script">Style
* specification</a>
*/
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 <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-is-supported-script">Style specification</a>
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-is-supported-script">Style
* specification</a>
*/
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<String, Expression> map = new HashMap<>();
return new Expression("format", input, new ExpressionMap(map));
/**
* Returns formatted text containing annotations for use in mixed-format text-field entries.
* <p>
* To build the expression, use {@link #formatEntry(Expression, FormatOption...)}.
* <p>
* "format" expression can be used, for example, with the {@link PropertyFactory#textField(Expression)}
* and accepts unlimited numbers of formatted sections.
* <p>
* Each section consist of the input, the displayed text, and options, like font-scale and text-font.
* <p>
* Example usage:
* </p>
* <pre>
* {@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)),
* }
* </pre>
*
* @param formatEntries format entries
* @return expression
* @see <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-types-format">Style specification</a>
*/
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<String, Expression> 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.
* <p>
* Text is required to be of a resulting type string.
* <p>
* 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.
* <p>
* Text is required to be of a resulting type string.
* <p>
* 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.
* <p>
* Text is required to be of a resulting type string.
* <p>
* 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.
* <p>
* Text is required to be of a resulting type string.
* <p>
* 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.
* <p>
* 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.
* <p>
* "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.
* <p>
* "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.
* <p>
* "text-font" is required to a literal array.
* <p>
* The requested font stack has to be a part of the used style.
* For more information see <a href="https://www.mapbox.com/help/define-font-stack/">the documentation</a>.
*
* @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.
* <p>
* "text-font" is required to a literal array.
* <p>
* The requested font stack has to be a part of the used style.
* For more information see <a href="https://www.mapbox.com/help/define-font-stack/">the documentation</a>.
*
* @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<Expression> 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 static class ExpressionLiteralArray extends ExpressionLiteral {
private Object[] array;
ExpressionArray(Object[] array) {
this.array = array;
}
@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<String, Object> 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];

View File

@@ -62,7 +62,7 @@ public class PropertyValue<T> {
? 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<T> {
// noinspection unchecked
return value;
} else {
Logger.w(TAG, "not a value, try function");
Logger.w(TAG, "not a value, try PropertyValue#getExpression()");
return null;
}
}

View File

@@ -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<String> getTextField() {
checkThread();
return (PropertyValue<String>) new PropertyValue("text-field", nativeGetTextField());
PropertyValue propertyValue = new PropertyValue<>("text-field", nativeGetTextField());
if (propertyValue.isExpression()) {
return (PropertyValue<String>) propertyValue;
} else {
Formatted formatted = (Formatted) nativeGetTextField();
StringBuilder builder = new StringBuilder();
for (FormattedSection section : formatted.getFormattedSections()) {
builder.append(section.getText());
}
return (PropertyValue<String>) 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<Formatted> getFormattedTextField() {
checkThread();
return (PropertyValue<Formatted>) new PropertyValue("text-field", nativeGetTextField());
}
/**

View File

@@ -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<String>) propertyValue;
} else {
Formatted formatted = (Formatted) nativeGetTextField();
StringBuilder builder = new StringBuilder();
for (FormattedSection section : formatted.getFormattedSections()) {
builder.append(section.getText());
}
return (PropertyValue<String>) 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<Formatted> getFormatted<%- camelize(property.name) %>() {
checkThread();
return (PropertyValue<Formatted>) new PropertyValue("<%- property.name %>", nativeGet<%- camelize(property.name) %>());
}
<% } -%>
<% if (property.type == 'color') { -%>
/**

View File

@@ -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 <a href="https://www.mapbox.com/mapbox-gl-js/style-spec/#types-formatted">Style specification</a>
*/
@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);
}
}

View File

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