diff --git a/src/style-spec/expression/definitions/formatted.js b/src/style-spec/expression/definitions/formatted.js index 881aa35309a..fc24419ba48 100644 --- a/src/style-spec/expression/definitions/formatted.js +++ b/src/style-spec/expression/definitions/formatted.js @@ -18,6 +18,13 @@ export class FormattedSection { this.scale = scale; this.fontStack = fontStack; } + + serialize() { + const fontStack = this.fontStack ? + ["literal", this.fontStack.split(',')] : + null; + return ["formatted", this.text, { "text-font": fontStack, "font-scale": this.scale }]; + } } export class Formatted { @@ -30,6 +37,14 @@ export class Formatted { toString(): string { return this.sections.map(section => section.text).join(); } + + serialize() { + if (this.sections.length === 1) { + return this.sections[0].serialize(); + } else { + return ["concat"].concat(this.sections.map(section => section.serialize())); + } + } } export class FormattedExpression implements Expression { diff --git a/src/style-spec/expression/definitions/literal.js b/src/style-spec/expression/definitions/literal.js index d1800400afc..09146fec3d5 100644 --- a/src/style-spec/expression/definitions/literal.js +++ b/src/style-spec/expression/definitions/literal.js @@ -2,6 +2,7 @@ import assert from 'assert'; import { isValue, typeOf, Color } from '../values'; +import { Formatted } from './formatted'; import type { Type } from '../types'; import type { Value } from '../values'; @@ -60,11 +61,17 @@ class Literal implements Expression { // couldn't actually generate with a "literal" expression, // so we have to implement an equivalent serialization here return ["rgba"].concat(this.value.toArray()); + } else if (this.value instanceof Formatted) { + // Constant-folding can generate Literal expressions that you + // couldn't actually generate with a "literal" expression, + // so we have to implement an equivalent serialization here + return this.value.serialize(); } else { assert(this.value === null || typeof this.value === 'string' || typeof this.value === 'number' || - typeof this.value === 'boolean'); + typeof this.value === 'boolean' || + typeof this.value === 'formatted'); return (this.value: any); } } diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index cd3d7d1ec10..9698f1c624d 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -1505,7 +1505,7 @@ "property-type": "data-constant" }, "text-field": { - "type": "string | formatted", + "type": "formatted", "default": "", "tokens": true, "doc": "Value to use for a text label.", diff --git a/src/style-spec/validate/validate.js b/src/style-spec/validate/validate.js index 231d9ff8686..5caa8d75800 100644 --- a/src/style-spec/validate/validate.js +++ b/src/style-spec/validate/validate.js @@ -18,6 +18,7 @@ import validateLayer from './validate_layer'; import validateSource from './validate_source'; import validateLight from './validate_light'; import validateString from './validate_string'; +import validateFormatted from './validate_formatted'; const VALIDATORS = { '*': function() { @@ -35,7 +36,8 @@ const VALIDATORS = { 'object': validateObject, 'source': validateSource, 'light': validateLight, - 'string': validateString + 'string': validateString, + 'formatted': validateFormatted }; @@ -64,8 +66,12 @@ export default function validate(options) { return VALIDATORS[valueSpec.type](options); } else { - return validateObject(extend({}, options, { + const valid = validateObject(extend({}, options, { valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec })); + if (!valid) { + console.log("not valid"); + } + return valid; } } diff --git a/src/style-spec/validate/validate_formatted.js b/src/style-spec/validate/validate_formatted.js new file mode 100644 index 00000000000..4cac73e2450 --- /dev/null +++ b/src/style-spec/validate/validate_formatted.js @@ -0,0 +1,13 @@ +// @flow + +import ValidationError from '../error/validation_error'; +import validateExpression from './validate_expression'; +import validateString from './validate_string'; + +export default function validateFormatted(options: any) { + if (validateString(options).length === 0) { + return []; + } + + return validateExpression(options); +} diff --git a/test/integration/expression-tests/concat/formatted/test.json b/test/integration/expression-tests/concat/formatted/test.json new file mode 100644 index 00000000000..dffabf25f70 --- /dev/null +++ b/test/integration/expression-tests/concat/formatted/test.json @@ -0,0 +1,88 @@ +{ + "expression": [ + "concat", + "a", + [ + "formatted", + "b", + { + "font-scale": 2 + } + ], + [ + "formatted", + "c", + { + "text-font": [ + "literal", + [ + "a", + "b" + ] + ] + } + ] + ], + "inputs": [ + [ + {}, + {} + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": true, + "isZoomConstant": true, + "type": "string" + }, + "outputs": [ + { + "sections": [ + { + "text": "a", + "scale": null, + "fontStack": null + }, + { + "text": "b", + "scale": 2, + "fontStack": null + }, + { + "text": "c", + "scale": null, + "fontStack": "a,b" + } + ] + } + ], + "serialized": [ + "concat", + [ + "formatted", + "a", + { + "font-scale": null, + "text-font": null + } + ], + [ + "formatted", + "b", + { + "font-scale": 2, + "text-font": null + } + ], + [ + "formatted", + "c", + { + "font-scale": null, + "text-font": ["literal", ["a", "b"]] + } + ] + ] + } +} diff --git a/test/integration/render-tests/text-field/formatted-line/expected.png b/test/integration/render-tests/text-field/formatted-line/expected.png new file mode 100644 index 00000000000..2e8583d0719 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted-line/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted-line/style.json b/test/integration/render-tests/text-field/formatted-line/style.json new file mode 100644 index 00000000000..5f2766c9119 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted-line/style.json @@ -0,0 +1,56 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256 + } + }, + "center": [ + 13.418056, + 52.499167 + ], + "zoom": 14, + "sources": { + "mapbox": { + "type": "vector", + "maxzoom": 14, + "tiles": [ + "local://tiles/{z}-{x}-{y}.mvt" + ] + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "literal", + "type": "symbol", + "source": "mapbox", + "source-layer": "road_label", + "layout": { + "symbol-placement": "line", + "text-allow-overlap": true, + "text-ignore-placement": true, + "text-field": ["concat", + ["formatted", ["get", "name"], { "font-scale": 1.2 }], + " - ", + ["formatted", ["get", "class"], { "font-scale": 0.8, "text-font": ["literal", [ "NotoCJK" ]] }] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 10 + }, + "paint": { + "text-opacity": 1 + } + } + ] +} diff --git a/test/integration/render-tests/text-field/formatted/expected.png b/test/integration/render-tests/text-field/formatted/expected.png new file mode 100644 index 00000000000..08f856cb509 Binary files /dev/null and b/test/integration/render-tests/text-field/formatted/expected.png differ diff --git a/test/integration/render-tests/text-field/formatted/style.json b/test/integration/render-tests/text-field/formatted/style.json new file mode 100644 index 00000000000..042c1d26191 --- /dev/null +++ b/test/integration/render-tests/text-field/formatted/style.json @@ -0,0 +1,53 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "width": 256 + } + }, + "center": [ 0, 0 ], + "zoom": 0, + "sources": { + "point": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "Napoli", + "name_en": "Naples" + }, + "geometry": { + "type": "Point", + "coordinates": [ 0, 0 ] + } + } + ] + } + } + }, + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "text", + "type": "symbol", + "source": "point", + "layout": { + "text-field": ["concat", + ["formatted", ["get", "name_en"], { "font-scale": 1.5 }], + ["formatted", "Italy", { "font-scale": 0.5} ], + "\n", + ["formatted", ["get", "name"], { "font-scale": 0.5, "text-font": ["literal", [ "NotoCJK" ]] }] + ], + "text-font": [ + "Open Sans Semibold", + "Arial Unicode MS Bold" + ], + "text-size": 32 + } + } + ] +}