From 0fa5944f39854f42c86ce478da8869a8d4e3f1fe Mon Sep 17 00:00:00 2001 From: Tobrun Date: Mon, 2 Sep 2019 14:48:11 +0200 Subject: Add number format expression (#15424) * [android] add number format expression --- platform/android/CHANGELOG.md | 1 + .../mapboxsdk/style/expressions/Expression.java | 166 +++++++++++++++++++- .../mapboxsdk/testapp/style/ExpressionTest.java | 171 +++++++++++++++++++++ .../activity/style/SymbolLayerActivity.java | 26 +++- 4 files changed, 351 insertions(+), 13 deletions(-) diff --git a/platform/android/CHANGELOG.md b/platform/android/CHANGELOG.md index 5ad7f0635d..5d8266c931 100644 --- a/platform/android/CHANGELOG.md +++ b/platform/android/CHANGELOG.md @@ -6,6 +6,7 @@ Mapbox welcomes participation and contributions from everyone. If you'd like to ### Features - Introduce `clusterProperties` option for aggregated cluster properties. [#15425](https://github.com/mapbox/mapbox-gl-native/pull/15425) - Expose the `CameraPosition#padding` field and associated utility camera position builders. This gives a choice to set a persisting map padding immediately during a transition instead of setting it lazily `MapboxMap#setPadding`, which required scheduling additional transition to be applied. This also deprecates `MapboxMap#setPadding` as there should be no need for a lazy padding setter. [#15444](https://github.com/mapbox/mapbox-gl-native/pull/15444) + - Add number-format expression that allows to format a number to a string, with configurations as minimal/maximal fraction and locale/currency. [#15424](https://github.com/mapbox/mapbox-gl-native/pull/15424) ### Performance improvements - Mark used offline region resources in batches. [#15521](https://github.com/mapbox/mapbox-gl-native/pull/15521) 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 d6cddea066..34b3308809 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 @@ -5,7 +5,6 @@ import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.Size; - import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -3144,6 +3143,40 @@ public class Expression { return new Expression("number", input); } + /** + * Converts the input number into a string representation using the providing formatting rules. + * If set, the locale argument specifies the locale to use, as a BCP 47 language tag. + * If set, the currency argument specifies an ISO 4217 code to use for currency-style formatting. + * If set, the min-fraction-digits and max-fraction-digits arguments specify the minimum and maximum number + * of fractional digits to include. + * + * @param number number expression + * @param options number formatting options + * @return expression + */ + public static Expression numberFormat(@NonNull Expression number, @NonNull NumberFormatOption... options) { + final Map map = new HashMap<>(); + for (NumberFormatOption option : options) { + map.put(option.type, option.value); + } + return new Expression("number-format", number, new ExpressionMap(map)); + } + + /** + * Converts the input number into a string representation using the providing formatting rules. + * If set, the locale argument specifies the locale to use, as a BCP 47 language tag. + * If set, the currency argument specifies an ISO 4217 code to use for currency-style formatting. + * If set, the min-fraction-digits and max-fraction-digits arguments specify the minimum and maximum number + * of fractional digits to include. + * + * @param number number expression + * @param options number formatting options + * @return expression + */ + public static Expression numberFormat(@NonNull Number number, @NonNull NumberFormatOption... options) { + return numberFormat(literal(number), options); + } + /** * Asserts that the input value is a boolean. * If multiple values are provided, each one is evaluated in order until a boolean value is obtained. @@ -4385,21 +4418,138 @@ public class Expression { } } + /** + * Base class for an option entry that is encapsulated as a json object member for an expression. + */ + private static class Option { + @NonNull + String type; + @NonNull + Expression value; + + /** + * Create an option option entry that is encapsulated as a json object member for an expression. + * + * @param type json object member name + * @param value json object member value + */ + Option(@NonNull String type, @NonNull Expression value) { + this.type = type; + this.value = value; + } + } + + /** + * Holds format options used in a {@link #numberFormat(Number, NumberFormatOption...)} expression. + */ + public static class NumberFormatOption extends Option { + + /** + * {@inheritDoc} + */ + NumberFormatOption(@NonNull String type, @NonNull Expression value) { + super(type, value); + } + + /** + * Number formatting option for specifying the locale to use, as a BCP 47 language tag. + * + * @param string the locale to use while performing number formatting + * @return number format option + */ + @NonNull + public static NumberFormatOption locale(@NonNull Expression string) { + return new NumberFormatOption("locale", string); + } + + /** + * Number formatting option for specifying the locale to use, as a BCP 47 language tag. + * + * @param string the locale to use while performing number formatting + * @return number format option + */ + @NonNull + public static NumberFormatOption locale(@NonNull String string) { + return new NumberFormatOption("locale", literal(string)); + } + + /** + * Number formatting option for specifying the currency to use, an ISO 4217 code. + * + * @param string the currency to use while performing number formatting + * @return number format option + */ + @NonNull + public static NumberFormatOption currency(@NonNull Expression string) { + return new NumberFormatOption("currency", string); + } + + /** + * Number formatting options for specifying the currency to use, an ISO 4217 code. + * + * @param string the currency to use while performing number formatting + * @return number format option + */ + @NonNull + public static NumberFormatOption currency(@NonNull String string) { + return new NumberFormatOption("currency", literal(string)); + } + + /** + * Number formatting options for specifying the minimum fraction digits to include. + * + * @param number the amount of minimum fraction digits to include + * @return number format option + */ + @NonNull + public static NumberFormatOption minFractionDigits(@NonNull Expression number) { + return new NumberFormatOption("min-fraction-digits", number); + } + + /** + * Number formatting options for specifying the minimum fraction digits to include. + * + * @param number the amount of minimum fraction digits to include + * @return number format option + */ + @NonNull + public static NumberFormatOption minFractionDigits(int number) { + return new NumberFormatOption("min-fraction-digits", literal(number)); + } + + /** + * Number formatting options for specifying the maximum fraction digits to include. + * + * @param number the amount of minimum fraction digits to include + * @return number format option + */ + @NonNull + public static NumberFormatOption maxFractionDigits(@NonNull Expression number) { + return new NumberFormatOption("max-fraction-digits", number); + } + + /** + * Number formatting options for specifying the maximum fraction digits to include. + * + * @param number the amount of minimum fraction digits to include + * @return number format option + */ + @NonNull + public static NumberFormatOption maxFractionDigits(@NonNull int number) { + return new NumberFormatOption("max-fraction-digits", literal(number)); + } + } + /** * 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; + public static class FormatOption extends Option { FormatOption(@NonNull String type, @NonNull Expression value) { - this.type = type; - this.value = value; + super(type, value); } /** diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/ExpressionTest.java b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/ExpressionTest.java index fbb866ddff..3cbdf016b4 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/ExpressionTest.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/style/ExpressionTest.java @@ -25,12 +25,17 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.IOException; +import java.util.HashMap; import timber.log.Timber; import static com.mapbox.mapboxsdk.style.expressions.Expression.FormatOption.formatFontScale; import static com.mapbox.mapboxsdk.style.expressions.Expression.FormatOption.formatTextColor; import static com.mapbox.mapboxsdk.style.expressions.Expression.FormatOption.formatTextFont; +import static com.mapbox.mapboxsdk.style.expressions.Expression.NumberFormatOption.currency; +import static com.mapbox.mapboxsdk.style.expressions.Expression.NumberFormatOption.locale; +import static com.mapbox.mapboxsdk.style.expressions.Expression.NumberFormatOption.maxFractionDigits; +import static com.mapbox.mapboxsdk.style.expressions.Expression.NumberFormatOption.minFractionDigits; import static com.mapbox.mapboxsdk.style.expressions.Expression.collator; import static com.mapbox.mapboxsdk.style.expressions.Expression.eq; import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential; @@ -41,6 +46,7 @@ import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate; import static com.mapbox.mapboxsdk.style.expressions.Expression.literal; import static com.mapbox.mapboxsdk.style.expressions.Expression.match; import static com.mapbox.mapboxsdk.style.expressions.Expression.number; +import static com.mapbox.mapboxsdk.style.expressions.Expression.numberFormat; import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb; import static com.mapbox.mapboxsdk.style.expressions.Expression.rgba; import static com.mapbox.mapboxsdk.style.expressions.Expression.step; @@ -56,8 +62,10 @@ import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOutlineColor import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textColor; import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField; import static com.mapbox.mapboxsdk.testapp.action.MapboxMapAction.invoke; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @RunWith(AndroidJUnit4.class) @@ -561,6 +569,169 @@ public class ExpressionTest extends EspressoTest { }); } + @Test + public void testNumberFormatCurrencyExpression() { + validateTestSetup(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + LatLng latLng = new LatLng(51, 17); + mapboxMap.getStyle() + .addSource(new GeoJsonSource("source", Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude()))); + SymbolLayer layer = new SymbolLayer("layer", "source"); + mapboxMap.getStyle().addLayer(layer); + + layer.setProperties( + textField( + numberFormat(12.345, locale("en-US"), currency("USD")) + ) + ); + TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView); + + assertFalse(mapboxMap.queryRenderedFeatures( + mapboxMap.getProjection().toScreenLocation(latLng), "layer").isEmpty() + ); + assertNull(layer.getTextField().getExpression()); + assertEquals("$12.35", layer.getTextField().getValue().getFormattedSections()[0].getText()); + }); + } + + @Test + public void testNumberFormatMaxExpression() { + validateTestSetup(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + LatLng latLng = new LatLng(51, 17); + mapboxMap.getStyle() + .addSource(new GeoJsonSource("source", Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude()))); + SymbolLayer layer = new SymbolLayer("layer", "source"); + mapboxMap.getStyle().addLayer(layer); + + layer.setProperties( + textField( + numberFormat(12.34567890, maxFractionDigits(5), minFractionDigits(0)) + ) + ); + TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView); + + assertFalse(mapboxMap.queryRenderedFeatures( + mapboxMap.getProjection().toScreenLocation(latLng), "layer").isEmpty() + ); + assertNull(layer.getTextField().getExpression()); + assertEquals("12.34568", layer.getTextField().getValue().getFormattedSections()[0].getText()); + }); + } + + @Test + public void testNumberFormatMinExpression() { + validateTestSetup(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + LatLng latLng = new LatLng(51, 17); + mapboxMap.getStyle() + .addSource(new GeoJsonSource("source", Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude()))); + SymbolLayer layer = new SymbolLayer("layer", "source"); + mapboxMap.getStyle().addLayer(layer); + + layer.setProperties( + textField( + numberFormat(12.0000001, maxFractionDigits(5), minFractionDigits(0)) + ) + ); + TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView); + + assertFalse(mapboxMap.queryRenderedFeatures( + mapboxMap.getProjection().toScreenLocation(latLng), "layer").isEmpty() + ); + assertNull(layer.getTextField().getExpression()); + assertEquals("12", layer.getTextField().getValue().getFormattedSections()[0].getText()); + }); + } + + @Test + public void testNumberFormatLocaleExpression() { + validateTestSetup(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + LatLng latLng = new LatLng(51, 17); + mapboxMap.getStyle() + .addSource(new GeoJsonSource("source", Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude()))); + SymbolLayer layer = new SymbolLayer("layer", "source"); + mapboxMap.getStyle().addLayer(layer); + + layer.setProperties( + textField( + numberFormat(12.0000001, locale("nl-BE"), maxFractionDigits(5), minFractionDigits(1)) + ) + ); + TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView); + + assertFalse(mapboxMap.queryRenderedFeatures( + mapboxMap.getProjection().toScreenLocation(latLng), "layer").isEmpty() + ); + assertNull(layer.getTextField().getExpression()); + assertEquals("12,0", layer.getTextField().getValue().getFormattedSections()[0].getText()); + }); + } + + @Test + public void testNumberFormatNonConstantExpression() { + validateTestSetup(); + invoke(mapboxMap, (uiController, mapboxMap) -> { + LatLng latLng = new LatLng(51, 17); + Feature feature = Feature.fromGeometry(Point.fromLngLat(latLng.getLongitude(), latLng.getLatitude())); + feature.addNumberProperty("number_value", 12.345678); + feature.addStringProperty("locale_value", "nl-BE"); + feature.addNumberProperty("max_value", 5); + feature.addNumberProperty("min_value", 1); + + + mapboxMap.getStyle().addSource(new GeoJsonSource("source", feature)); + SymbolLayer layer = new SymbolLayer("layer", "source"); + mapboxMap.getStyle().addLayer(layer); + + Expression numberFormatExpression = numberFormat( + number(number(get("number_value"))), + locale(string(get("locale_value"))), + maxFractionDigits(number(get("max_value"))), + minFractionDigits(number(get("min_value"))) + ); + + layer.setProperties(textField(numberFormatExpression)); + TestingAsyncUtils.INSTANCE.waitForLayer(uiController, mapView); + + assertFalse(mapboxMap.queryRenderedFeatures( + mapboxMap.getProjection().toScreenLocation(latLng), "layer").isEmpty() + ); + + assertNotNull(layer.getTextField().getExpression()); + + // Expressions evaluated to string are wrapped by a format expression, take array index 1 to get original + Object[] returnExpression = (Object[]) layer.getTextField().getExpression().toArray()[1]; + Object[] setExpression = numberFormatExpression.toArray(); + assertEquals("Number format should match",returnExpression[0], setExpression[0]); + assertArrayEquals("Get value expression should match", + (Object[]) returnExpression[1], + (Object[]) setExpression[1] + ); + + // number format objects + HashMap returnMap = (HashMap) returnExpression[2]; + HashMap setMap = (HashMap) returnExpression[2]; + + assertArrayEquals("Number format min fraction digits should match ", + (Object[]) returnMap.get("min-fraction-digits"), + (Object[]) setMap.get("min-fraction-digits") + ); + + assertArrayEquals("Number format max fraction digits should match ", + (Object[]) returnMap.get("max-fraction-digits"), + (Object[]) setMap.get("max-fraction-digits") + ); + + assertArrayEquals("Number format min fraction digits should match ", + (Object[]) returnMap.get("locale"), + (Object[]) setMap.get("locale") + ); + }); + + } + private void setupStyle() { invoke(mapboxMap, (uiController, mapboxMap) -> { // Add a source diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java index 3d65a92f72..ee562ad6e8 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/style/SymbolLayerActivity.java @@ -38,12 +38,15 @@ import java.util.Random; import static com.mapbox.mapboxsdk.style.expressions.Expression.FormatOption.formatFontScale; import static com.mapbox.mapboxsdk.style.expressions.Expression.FormatOption.formatTextColor; import static com.mapbox.mapboxsdk.style.expressions.Expression.FormatOption.formatTextFont; +import static com.mapbox.mapboxsdk.style.expressions.Expression.NumberFormatOption.currency; +import static com.mapbox.mapboxsdk.style.expressions.Expression.NumberFormatOption.locale; import static com.mapbox.mapboxsdk.style.expressions.Expression.concat; import static com.mapbox.mapboxsdk.style.expressions.Expression.format; import static com.mapbox.mapboxsdk.style.expressions.Expression.formatEntry; import static com.mapbox.mapboxsdk.style.expressions.Expression.get; import static com.mapbox.mapboxsdk.style.expressions.Expression.literal; import static com.mapbox.mapboxsdk.style.expressions.Expression.match; +import static com.mapbox.mapboxsdk.style.expressions.Expression.numberFormat; import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb; import static com.mapbox.mapboxsdk.style.expressions.Expression.stop; import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase; @@ -72,8 +75,6 @@ import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize; */ public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap.OnMapClickListener, OnMapReadyCallback { - private static final String MARKER_SOURCE = "marker-source"; - private static final String MARKER_LAYER = "marker-layer"; private static final String ID_FEATURE_PROPERTY = "id"; private static final String SELECTED_FEATURE_PROPERTY = "selected"; private static final String TITLE_FEATURE_PROPERTY = "title"; @@ -81,8 +82,13 @@ public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap. private static final String[] NORMAL_FONT_STACK = new String[] {"DIN Offc Pro Regular", "Arial Unicode MS Regular"}; private static final String[] BOLD_FONT_STACK = new String[] {"DIN Offc Pro Bold", "Arial Unicode MS Regular"}; + // layer & source constants + private static final String MARKER_SOURCE = "marker-source"; + private static final String MARKER_LAYER = "marker-layer"; private static final String MAPBOX_SIGN_SOURCE = "mapbox-sign-source"; private static final String MAPBOX_SIGN_LAYER = "mapbox-sign-layer"; + private static final String NUMBER_FORMAT_SOURCE = "mapbox-number-source"; + private static final String NUMBER_FORMAT_LAYER = "mapbox-number-layer"; private static final Expression TEXT_FIELD_EXPRESSION = switchCase(toBool(get(SELECTED_FEATURE_PROPERTY)), @@ -108,6 +114,7 @@ public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap. private FeatureCollection markerCollection; private SymbolLayer markerSymbolLayer; private SymbolLayer mapboxSignSymbolLayer; + private SymbolLayer numberFormatSymbolLayer; private MapboxMap mapboxMap; private MapView mapView; @@ -177,11 +184,20 @@ public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap. mapboxSignSymbolLayer = new SymbolLayer(MAPBOX_SIGN_LAYER, MAPBOX_SIGN_SOURCE); shuffleMapboxSign(); + // number format layer + Source numberFormatSource = new GeoJsonSource(NUMBER_FORMAT_SOURCE, Point.fromLngLat(4.92756, 52.3516)); + numberFormatSymbolLayer = new SymbolLayer(NUMBER_FORMAT_LAYER, NUMBER_FORMAT_SOURCE); + numberFormatSymbolLayer.setProperties( + textField( + numberFormat(123.456789, locale("nl-NL"), currency("EUR")) + ) + ); + mapboxMap.setStyle(new Style.Builder() .fromUri("asset://streets.json") .withImage("Car", Objects.requireNonNull(carBitmap), false) - .withSources(markerSource, mapboxSignSource) - .withLayers(markerSymbolLayer, mapboxSignSymbolLayer) + .withSources(markerSource, mapboxSignSource, numberFormatSource) + .withLayers(markerSymbolLayer, mapboxSignSymbolLayer, numberFormatSymbolLayer) ); // Set a click-listener so we can manipulate the map @@ -205,7 +221,7 @@ public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap. // validate symbol flicker regression for #13407 markerSymbolLayer.setProperties(iconOpacity(match( get(ID_FEATURE_PROPERTY), literal(1.0f), - stop(feature.getStringProperty("id"), selected ? 0.3f : 1.0f) + stop(feature.getStringProperty("id"), selected ? 0.3f : 1.0f) ))); } } -- cgit v1.2.1