diff --git a/src/data/bucket/symbol_bucket.js b/src/data/bucket/symbol_bucket.js index ca286f528a9..8bb1e141fda 100644 --- a/src/data/bucket/symbol_bucket.js +++ b/src/data/bucket/symbol_bucket.js @@ -6,7 +6,6 @@ const IndexBuffer = require('../../gl/index_buffer'); const {ProgramConfigurationSet} = require('../program_configuration'); const createVertexArrayType = require('../vertex_array_type'); const {TriangleIndexArray, LineIndexArray} = require('../index_array_type'); -const resolveTokens = require('../../util/token'); const transformText = require('../../symbol/transform_text'); const mergeLines = require('../../symbol/mergelines'); const scriptDetection = require('../../util/script_detection'); @@ -463,19 +462,13 @@ class SymbolBucket implements Bucket { let text; if (hasText) { - text = layer.getLayoutValue('text-field', globalProperties, feature); - if (layer.isLayoutValueFeatureConstant('text-field')) { - text = resolveTokens(feature.properties, text); - } + text = layer.getValueAndResolveTokens('text-field', globalProperties, feature); text = transformText(text, layer, globalProperties, feature); } let icon; if (hasIcon) { - icon = layer.getLayoutValue('icon-image', globalProperties, feature); - if (layer.isLayoutValueFeatureConstant('icon-image')) { - icon = resolveTokens(feature.properties, icon); - } + icon = layer.getValueAndResolveTokens('icon-image', globalProperties, feature); } if (!text && !icon) { diff --git a/src/style-spec/function/convert.js b/src/style-spec/function/convert.js index 2930ff56d30..7ba8d4b711b 100644 --- a/src/style-spec/function/convert.js +++ b/src/style-spec/function/convert.js @@ -23,6 +23,10 @@ function convertFunction(parameters, propertySpec, name) { const zoomDependent = zoomAndFeatureDependent || !featureDependent; const stops = parameters.stops.map((stop) => { + if (!featureDependent && (name === 'icon-image' || name === 'text-field') && typeof stop[1] === 'string') { + return [stop[0], convertTokenString(stop[1])]; + + } return [stop[0], convertValue(stop[1], propertySpec)]; }); @@ -223,7 +227,7 @@ function appendStopPair(curve, input, output, isStep) { curve.push(output); } -function getFunctionType (parameters, propertySpec) { +function getFunctionType(parameters, propertySpec) { if (parameters.type) { return parameters.type; } else if (propertySpec.function) { @@ -232,3 +236,28 @@ function getFunctionType (parameters, propertySpec) { return 'exponential'; } } + +// "String with {name} token" => ["concat", "String with ", ["get", "name"], " token"] +function convertTokenString(s) { + const result = ['concat']; + const re = /{([^{}]+)}/g; + let pos = 0; + let match; + while ((match = re.exec(s)) !== null) { + const literal = s.slice(pos, re.lastIndex - match[0].length); + pos = re.lastIndex; + if (literal.length > 0) result.push(literal); + result.push(['to-string', ['get', match[1]]]); + } + + if (result.length === 1) { + return s; + } + + if (pos < s.length) { + result.push(s.slice(pos)); + } + + return result; +} + diff --git a/src/style/style_layer/symbol_style_layer.js b/src/style/style_layer/symbol_style_layer.js index 3d440b42acb..9d0a92be9ab 100644 --- a/src/style/style_layer/symbol_style_layer.js +++ b/src/style/style_layer/symbol_style_layer.js @@ -2,6 +2,8 @@ const StyleLayer = require('../style_layer'); const SymbolBucket = require('../../data/bucket/symbol_bucket'); +const resolveTokens = require('../../util/token'); +const {isExpression} = require('../../style-spec/expression'); const assert = require('assert'); import type {Feature, GlobalProperties} from '../../style-spec/expression'; @@ -42,6 +44,16 @@ class SymbolStyleLayer extends StyleLayer { return !declaration || declaration.expression.isZoomConstant; } + getValueAndResolveTokens(name: 'text-field' | 'icon-image', globals: GlobalProperties, feature: Feature) { + const value = this.getLayoutValue(name, globals, feature); + const declaration = this._layoutDeclarations[name]; + if (this.isLayoutValueFeatureConstant(name) && !isExpression(declaration.value)) { + return resolveTokens(feature.properties, value); + } + + return value; + } + createBucket(parameters: BucketParameters) { // Eventually we need to make SymbolBucket conform to the Bucket interface. // Hack around it with casts for now. diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5599/expected.png b/test/integration/render-tests/regressions/mapbox-gl-js#5599/expected.png new file mode 100644 index 00000000000..9211bbfea3e Binary files /dev/null and b/test/integration/render-tests/regressions/mapbox-gl-js#5599/expected.png differ diff --git a/test/integration/render-tests/regressions/mapbox-gl-js#5599/style.json b/test/integration/render-tests/regressions/mapbox-gl-js#5599/style.json new file mode 100644 index 00000000000..0cbb9b9536d --- /dev/null +++ b/test/integration/render-tests/regressions/mapbox-gl-js#5599/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 64, + "height": 64, + "operations": [] + } + }, + "center": [0, 0], + "zoom": 0, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "Feature", + "properties": { + "icon": "dot.sdf", + "name": "X" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 0, + 0 + ] + } + } + } + }, + "sprite": "local://sprites/sprite", + "glyphs": "local://glyphs/{fontstack}/{range}.pbf", + "layers": [{ + "id": "icon-expression", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-offset": [-16, -16], + "icon-image": ["step", ["zoom"], "{icon}", 10, ""], + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, { + "id": "text-expression", + "type": "symbol", + "source": "geojson", + "layout": { + "text-offset": [1, -1], + "text-field": ["step", ["zoom"], "{name}", 10, ""], + "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"], + "text-allow-overlap": true, + "text-ignore-placement": true + } + }, { + "id": "icon-function", + "type": "symbol", + "source": "geojson", + "layout": { + "icon-offset": [-16, 16], + + "icon-image": { + "stops": [ + [0, "{icon}"], + [22, ""] + ] + }, + "icon-allow-overlap": true, + "icon-ignore-placement": true + } + }, { + "id": "text-function", + "type": "symbol", + "source": "geojson", + "layout": { + "text-offset": [1, 1], + "text-field": { + "stops": [ + [0, "{name}"], + [22, ""] + ] + }, + "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"], + "text-allow-overlap": true, + "text-ignore-placement": true + } + }] +} diff --git a/test/unit/style-spec/convert_function.test.js b/test/unit/style-spec/convert_function.test.js new file mode 100644 index 00000000000..31176d82e2e --- /dev/null +++ b/test/unit/style-spec/convert_function.test.js @@ -0,0 +1,46 @@ +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const convertFunction = require('../../../src/style-spec/function/convert'); + +test('convertFunction', (t) => { + t.test('feature-constant text-field with token replacement', (t) => { + const functionValue = { + stops: [ + [0, 'my name is {name}.'], + [1, '{a} {b} {c}'], + [2, 'no tokens'] + ] + }; + + const expression = convertFunction(functionValue, { + type: 'string', + function: 'piecewise-constant' + }, 'text-field'); + t.deepEqual(expression, [ + 'step', + ['zoom'], + [ + 'concat', + 'my name is ', + ['to-string', ['get', 'name']], + '.' + ], + 1, + [ + 'concat', + ['to-string', ['get', 'a']], + ' ', + ['to-string', ['get', 'b']], + ' ', + ['to-string', ['get', 'c']] + ], + 2, + 'no tokens' + ]); + + t.end(); + }); + + t.end(); +});