@mapwhit/tilerenderer 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/build/min/package.json +1 -1
  2. package/build/min/src/shaders/fill_extrusion.fragment.glsl.txt +1 -9
  3. package/build/min/src/shaders/fill_extrusion.vertex.glsl.txt +4 -4
  4. package/package.json +3 -2
  5. package/src/data/array_types.js +169 -0
  6. package/src/data/bucket/symbol_attributes.js +18 -0
  7. package/src/data/bucket/symbol_bucket.js +108 -78
  8. package/src/geo/transform.js +13 -5
  9. package/src/render/glyph_atlas.js +3 -6
  10. package/src/render/image_atlas.js +3 -6
  11. package/src/render/image_manager.js +41 -41
  12. package/src/shaders/fill_extrusion.fragment.glsl +0 -7
  13. package/src/shaders/fill_extrusion.vertex.glsl +4 -4
  14. package/src/style/style_layer/symbol_style_layer_properties.js +6 -0
  15. package/src/style-spec/expression/definitions/assertion.js +52 -5
  16. package/src/style-spec/expression/definitions/coalesce.js +1 -3
  17. package/src/style-spec/expression/definitions/coercion.js +32 -21
  18. package/src/style-spec/expression/definitions/collator.js +1 -23
  19. package/src/style-spec/expression/definitions/{formatted.js → format.js} +3 -29
  20. package/src/style-spec/expression/definitions/index.js +8 -26
  21. package/src/style-spec/expression/definitions/let.js +1 -1
  22. package/src/style-spec/expression/definitions/literal.js +1 -1
  23. package/src/style-spec/expression/evaluation_context.js +3 -0
  24. package/src/style-spec/expression/index.js +18 -17
  25. package/src/style-spec/expression/parsing_context.js +28 -24
  26. package/src/style-spec/expression/types/collator.js +24 -0
  27. package/src/style-spec/expression/types/formatted.js +39 -0
  28. package/src/style-spec/expression/types.js +1 -1
  29. package/src/style-spec/expression/values.js +24 -1
  30. package/src/style-spec/feature_filter/convert.js +197 -0
  31. package/src/style-spec/feature_filter/index.js +4 -1
  32. package/src/style-spec/function/convert.js +86 -102
  33. package/src/style-spec/function/index.js +4 -0
  34. package/src/style-spec/reference/v8.json +43 -6
  35. package/src/symbol/collision_index.js +0 -1
  36. package/src/symbol/cross_tile_symbol_index.js +12 -7
  37. package/src/symbol/mergelines.js +2 -2
  38. package/src/symbol/placement.js +71 -54
  39. package/src/symbol/shaping.js +9 -18
  40. package/src/symbol/symbol_layout.js +33 -33
  41. package/src/symbol/transform_text.js +5 -8
  42. package/src/style-spec/expression/definitions/array.js +0 -82
@@ -1,7 +1,6 @@
1
- const ShelfPack = require('@mapbox/shelf-pack');
2
-
3
1
  const { RGBAImage } = require('../util/image');
4
2
  const { register } = require('../util/transfer_registry');
3
+ const potpack = require('potpack');
5
4
 
6
5
  const padding = 1;
7
6
 
@@ -32,7 +31,6 @@ class ImageAtlas {
32
31
  constructor(icons, patterns) {
33
32
  const iconPositions = {};
34
33
  const patternPositions = {};
35
- const pack = new ShelfPack(0, 0, { autoResize: true });
36
34
  const bins = [];
37
35
  for (const id in icons) {
38
36
  const src = icons[id];
@@ -58,9 +56,8 @@ class ImageAtlas {
58
56
  patternPositions[id] = new ImagePosition(bin, src);
59
57
  }
60
58
 
61
- pack.pack(bins, { inPlace: true });
62
-
63
- const image = new RGBAImage({ width: pack.w, height: pack.h });
59
+ const { w, h } = potpack(bins);
60
+ const image = new RGBAImage({ width: w || 1, height: h || 1 });
64
61
 
65
62
  for (const id in icons) {
66
63
  const src = icons[id];
@@ -1,4 +1,4 @@
1
- const ShelfPack = require('@mapbox/shelf-pack');
1
+ const potpack = require('potpack');
2
2
 
3
3
  const { RGBAImage } = require('../util/image');
4
4
  const { ImagePosition } = require('./image_atlas');
@@ -26,9 +26,8 @@ class ImageManager {
26
26
  constructor() {
27
27
  this.images = {};
28
28
 
29
- this.shelfPack = new ShelfPack(64, 64, { autoResize: true });
30
29
  this.patterns = {};
31
- this.atlasImage = new RGBAImage({ width: 64, height: 64 });
30
+ this.atlasImage = new RGBAImage({ width: 1, height: 1 });
32
31
  this.dirty = true;
33
32
  }
34
33
 
@@ -56,12 +55,7 @@ class ImageManager {
56
55
  removeImage(id) {
57
56
  assert(this.images[id]);
58
57
  delete this.images[id];
59
-
60
- const pattern = this.patterns[id];
61
- if (pattern) {
62
- this.shelfPack.unref(pattern.bin);
63
- delete this.patterns[id];
64
- }
58
+ delete this.patterns[id];
65
59
  }
66
60
 
67
61
  listImages() {
@@ -89,10 +83,8 @@ class ImageManager {
89
83
  // Pattern stuff
90
84
 
91
85
  getPixelSize() {
92
- return {
93
- width: this.shelfPack.w,
94
- height: this.shelfPack.h
95
- };
86
+ const { width, height } = this.atlasImage;
87
+ return { width, height };
96
88
  }
97
89
 
98
90
  getPattern(id) {
@@ -106,36 +98,13 @@ class ImageManager {
106
98
  return null;
107
99
  }
108
100
 
109
- const width = image.data.width + padding * 2;
110
- const height = image.data.height + padding * 2;
111
-
112
- const bin = this.shelfPack.packOne(width, height);
113
- if (!bin) {
114
- return null;
115
- }
116
-
117
- this.atlasImage.resize(this.getPixelSize());
118
-
119
- const src = image.data;
120
- const dst = this.atlasImage;
121
-
122
- const x = bin.x + padding;
123
- const y = bin.y + padding;
124
- const w = src.width;
125
- const h = src.height;
126
-
127
- RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h });
128
-
129
- // Add 1 pixel wrapped padding on each side of the image.
130
- RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T
131
- RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B
132
- RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L
133
- RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R
134
-
135
- this.dirty = true;
136
-
101
+ const w = image.data.width + padding * 2;
102
+ const h = image.data.height + padding * 2;
103
+ const bin = { w, h, x: 0, y: 0 };
137
104
  const position = new ImagePosition(bin, image);
138
105
  this.patterns[id] = { bin, position };
106
+ this._updatePatternAtlas();
107
+
139
108
  return position;
140
109
  }
141
110
 
@@ -150,6 +119,37 @@ class ImageManager {
150
119
 
151
120
  this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
152
121
  }
122
+
123
+ _updatePatternAtlas() {
124
+ const bins = [];
125
+ for (const id in this.patterns) {
126
+ bins.push(this.patterns[id].bin);
127
+ }
128
+
129
+ const { w, h } = potpack(bins);
130
+
131
+ const dst = this.atlasImage;
132
+ dst.resize({ width: w ?? 1, height: h ?? 1 });
133
+
134
+ for (const id in this.patterns) {
135
+ const { bin } = this.patterns[id];
136
+ const x = bin.x + padding;
137
+ const y = bin.y + padding;
138
+ const src = this.images[id].data;
139
+ const w = src.width;
140
+ const h = src.height;
141
+
142
+ RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x, y }, { width: w, height: h });
143
+
144
+ // Add 1 pixel wrapped padding on each side of the image.
145
+ RGBAImage.copy(src, dst, { x: 0, y: h - 1 }, { x: x, y: y - 1 }, { width: w, height: 1 }); // T
146
+ RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x, y: y + h }, { width: w, height: 1 }); // B
147
+ RGBAImage.copy(src, dst, { x: w - 1, y: 0 }, { x: x - 1, y: y }, { width: 1, height: h }); // L
148
+ RGBAImage.copy(src, dst, { x: 0, y: 0 }, { x: x + w, y: y }, { width: 1, height: h }); // R
149
+ }
150
+
151
+ this.dirty = true;
152
+ }
153
153
  }
154
154
 
155
155
  module.exports = ImageManager;
@@ -1,13 +1,6 @@
1
1
  varying vec4 v_color;
2
- #pragma mapbox: define lowp float base
3
- #pragma mapbox: define lowp float height
4
- #pragma mapbox: define highp vec4 color
5
2
 
6
3
  void main() {
7
- #pragma mapbox: initialize lowp float base
8
- #pragma mapbox: initialize lowp float height
9
- #pragma mapbox: initialize highp vec4 color
10
-
11
4
  gl_FragColor = v_color;
12
5
 
13
6
  #ifdef OVERDRAW_INSPECTOR
@@ -10,14 +10,14 @@ attribute vec4 a_normal_ed;
10
10
 
11
11
  varying vec4 v_color;
12
12
 
13
- #pragma mapbox: define lowp float base
14
- #pragma mapbox: define lowp float height
13
+ #pragma mapbox: define highp float base
14
+ #pragma mapbox: define highp float height
15
15
 
16
16
  #pragma mapbox: define highp vec4 color
17
17
 
18
18
  void main() {
19
- #pragma mapbox: initialize lowp float base
20
- #pragma mapbox: initialize lowp float height
19
+ #pragma mapbox: initialize highp float base
20
+ #pragma mapbox: initialize highp float height
21
21
  #pragma mapbox: initialize highp vec4 color
22
22
 
23
23
  vec3 normal = a_normal_ed.xyz;
@@ -19,6 +19,12 @@ const layout = new Properties({
19
19
  default: false,
20
20
  expression: { parameters: ['zoom'] }
21
21
  }),
22
+ 'symbol-z-order': new DataConstantProperty({
23
+ type: 'enum',
24
+ values: ['viewport-y', 'source'],
25
+ default: 'viewport-y',
26
+ expression: { parameters: ['zoom'] }
27
+ }),
22
28
  'icon-allow-overlap': new DataConstantProperty({
23
29
  type: 'boolean',
24
30
  default: false,
@@ -1,6 +1,15 @@
1
1
  const assert = require('assert');
2
2
 
3
- const { ObjectType, ValueType, StringType, NumberType, BooleanType, checkSubtype, toString } = require('../types');
3
+ const {
4
+ ObjectType,
5
+ ValueType,
6
+ StringType,
7
+ NumberType,
8
+ BooleanType,
9
+ checkSubtype,
10
+ toString,
11
+ array
12
+ } = require('../types');
4
13
  const RuntimeError = require('../runtime_error');
5
14
  const { typeOf } = require('../values');
6
15
 
@@ -20,13 +29,39 @@ class Assertion {
20
29
  static parse(args, context) {
21
30
  if (args.length < 2) return context.error('Expected at least one argument.');
22
31
 
32
+ let i = 1;
33
+ let type;
34
+
23
35
  const name = args[0];
24
- assert(types[name], name);
36
+ if (name === 'array') {
37
+ let itemType;
38
+ if (args.length > 2) {
39
+ const type = args[1];
40
+ if (typeof type !== 'string' || !(type in types) || type === 'object')
41
+ return context.error('The item type argument of "array" must be one of string, number, boolean', 1);
42
+ itemType = types[type];
43
+ i++;
44
+ } else {
45
+ itemType = ValueType;
46
+ }
25
47
 
26
- const type = types[name];
48
+ let N;
49
+ if (args.length > 3) {
50
+ if (args[2] !== null && (typeof args[2] !== 'number' || args[2] < 0 || args[2] !== Math.floor(args[2]))) {
51
+ return context.error('The length argument to "array" must be a positive integer literal', 2);
52
+ }
53
+ N = args[2];
54
+ i++;
55
+ }
56
+
57
+ type = array(itemType, N);
58
+ } else {
59
+ assert(types[name], name);
60
+ type = types[name];
61
+ }
27
62
 
28
63
  const parsed = [];
29
- for (let i = 1; i < args.length; i++) {
64
+ for (; i < args.length; i++) {
30
65
  const input = context.parse(args[i], i, ValueType);
31
66
  if (!input) return null;
32
67
  parsed.push(input);
@@ -62,7 +97,19 @@ class Assertion {
62
97
  }
63
98
 
64
99
  serialize() {
65
- return [this.type.kind].concat(this.args.map(arg => arg.serialize()));
100
+ const type = this.type;
101
+ const serialized = [type.kind];
102
+ if (type.kind === 'array') {
103
+ const itemType = type.itemType;
104
+ if (itemType.kind === 'string' || itemType.kind === 'number' || itemType.kind === 'boolean') {
105
+ serialized.push(itemType.kind);
106
+ const N = type.N;
107
+ if (typeof N === 'number' || this.args.length > 1) {
108
+ serialized.push(N);
109
+ }
110
+ }
111
+ }
112
+ return serialized.concat(this.args.map(arg => arg.serialize()));
66
113
  }
67
114
  }
68
115
 
@@ -20,9 +20,7 @@ class Coalesce {
20
20
  const parsedArgs = [];
21
21
 
22
22
  for (const arg of args.slice(1)) {
23
- const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, {
24
- omitTypeAnnotations: true
25
- });
23
+ const parsed = context.parse(arg, 1 + parsedArgs.length, outputType, undefined, { typeAnnotation: 'omit' });
26
24
  if (!parsed) return null;
27
25
  outputType = outputType || parsed.type;
28
26
  parsedArgs.push(parsed);
@@ -1,13 +1,16 @@
1
1
  const assert = require('assert');
2
2
 
3
- const { ColorType, ValueType, NumberType } = require('../types');
4
- const { Color, validateRGBA } = require('../values');
3
+ const { BooleanType, ColorType, NumberType, StringType, ValueType } = require('../types');
4
+ const { Color, toString: valueToString, validateRGBA } = require('../values');
5
5
  const RuntimeError = require('../runtime_error');
6
- const { Formatted, FormattedSection } = require('./formatted');
6
+ const { FormatExpression } = require('../definitions/format');
7
+ const { Formatted } = require('../types/formatted');
7
8
 
8
9
  const types = {
10
+ 'to-boolean': BooleanType,
11
+ 'to-color': ColorType,
9
12
  'to-number': NumberType,
10
- 'to-color': ColorType
13
+ 'to-string': StringType
11
14
  };
12
15
 
13
16
  /**
@@ -29,6 +32,9 @@ class Coercion {
29
32
  const name = args[0];
30
33
  assert(types[name], name);
31
34
 
35
+ if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2)
36
+ return context.error('Expected one argument.');
37
+
32
38
  const type = types[name];
33
39
 
34
40
  const parsed = [];
@@ -42,12 +48,18 @@ class Coercion {
42
48
  }
43
49
 
44
50
  evaluate(ctx) {
51
+ if (this.type.kind === 'boolean') {
52
+ return Boolean(this.args[0].evaluate(ctx));
53
+ }
45
54
  if (this.type.kind === 'color') {
46
55
  let input;
47
56
  let error;
48
57
  for (const arg of this.args) {
49
58
  input = arg.evaluate(ctx);
50
59
  error = null;
60
+ if (input instanceof Color) {
61
+ return input;
62
+ }
51
63
  if (typeof input === 'string') {
52
64
  const c = ctx.parseColor(input);
53
65
  if (c) return c;
@@ -66,27 +78,23 @@ class Coercion {
66
78
  error || `Could not parse color from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
67
79
  );
68
80
  }
69
- if (this.type.kind === 'formatted') {
70
- let input;
81
+ if (this.type.kind === 'number') {
82
+ let value = null;
71
83
  for (const arg of this.args) {
72
- input = arg.evaluate(ctx);
73
- if (typeof input === 'string') {
74
- return new Formatted([new FormattedSection(input, null, null)]);
75
- }
84
+ value = arg.evaluate(ctx);
85
+ if (value === null) return 0;
86
+ const num = Number(value);
87
+ if (isNaN(num)) continue;
88
+ return num;
76
89
  }
77
- throw new RuntimeError(
78
- `Could not parse formatted text from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`
79
- );
90
+ throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`);
80
91
  }
81
- let value = null;
82
- for (const arg of this.args) {
83
- value = arg.evaluate(ctx);
84
- if (value === null) continue;
85
- const num = Number(value);
86
- if (isNaN(num)) continue;
87
- return num;
92
+ if (this.type.kind === 'formatted') {
93
+ // There is no explicit 'to-formatted' but this coercion can be implicitly
94
+ // created by properties that expect the 'formatted' type.
95
+ return Formatted.fromString(valueToString(this.args[0].evaluate(ctx)));
88
96
  }
89
- throw new RuntimeError(`Could not convert ${JSON.stringify(value)} to number.`);
97
+ return valueToString(this.args[0].evaluate(ctx));
90
98
  }
91
99
 
92
100
  eachChild(fn) {
@@ -98,6 +106,9 @@ class Coercion {
98
106
  }
99
107
 
100
108
  serialize() {
109
+ if (this.type.kind === 'formatted') {
110
+ return new FormatExpression([{ text: this.args[0], scale: null, font: null }]).serialize();
111
+ }
101
112
  const serialized = [`to-${this.type.kind}`];
102
113
  this.eachChild(child => {
103
114
  serialized.push(child.serialize());
@@ -1,27 +1,5 @@
1
1
  const { StringType, BooleanType, CollatorType } = require('../types');
2
-
3
- class Collator {
4
- constructor(caseSensitive, diacriticSensitive, locale) {
5
- if (caseSensitive) this.sensitivity = diacriticSensitive ? 'variant' : 'case';
6
- else this.sensitivity = diacriticSensitive ? 'accent' : 'base';
7
-
8
- this.locale = locale;
9
- this.collator = new Intl.Collator(this.locale ? this.locale : [], {
10
- sensitivity: this.sensitivity,
11
- usage: 'search'
12
- });
13
- }
14
-
15
- compare(lhs, rhs) {
16
- return this.collator.compare(lhs, rhs);
17
- }
18
-
19
- resolvedLocale() {
20
- // We create a Collator without "usage: search" because we don't want
21
- // the search options encoded in our result (e.g. "en-u-co-search")
22
- return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
23
- }
24
- }
2
+ const { Collator } = require('../types/collator');
25
3
 
26
4
  class CollatorExpression {
27
5
  constructor(caseSensitive, diacriticSensitive, locale) {
@@ -1,32 +1,6 @@
1
1
  const { NumberType, ValueType, FormattedType, array, StringType } = require('../types');
2
-
3
- class FormattedSection {
4
- constructor(text, scale, fontStack) {
5
- this.text = text;
6
- this.scale = scale;
7
- this.fontStack = fontStack;
8
- }
9
- }
10
-
11
- class Formatted {
12
- constructor(sections) {
13
- this.sections = sections;
14
- }
15
-
16
- toString() {
17
- return this.sections.map(section => section.text).join('');
18
- }
19
-
20
- serialize() {
21
- const serialized = ['format'];
22
- for (const section of this.sections) {
23
- serialized.push(section.text);
24
- const fontStack = section.fontStack ? ['literal', section.fontStack.split(',')] : null;
25
- serialized.push({ 'text-font': fontStack, 'font-scale': section.scale });
26
- }
27
- return serialized;
28
- }
29
- }
2
+ const { Formatted, FormattedSection } = require('../types/formatted');
3
+ const { toString } = require('../values');
30
4
 
31
5
  class FormatExpression {
32
6
  constructor(sections) {
@@ -77,7 +51,7 @@ class FormatExpression {
77
51
  this.sections.map(
78
52
  section =>
79
53
  new FormattedSection(
80
- section.text.evaluate(ctx) || '',
54
+ toString(section.text.evaluate(ctx)),
81
55
  section.scale ? section.scale.evaluate(ctx) : null,
82
56
  section.font ? section.font.evaluate(ctx).join(',') : null
83
57
  )
@@ -8,17 +8,16 @@ const {
8
8
  ErrorType,
9
9
  CollatorType,
10
10
  array,
11
- toString
11
+ toString: typeToString
12
12
  } = require('../types');
13
13
 
14
- const { typeOf, Color, validateRGBA } = require('../values');
14
+ const { typeOf, Color, validateRGBA, toString: valueToString } = require('../values');
15
15
  const CompoundExpression = require('../compound_expression');
16
16
  const RuntimeError = require('../runtime_error');
17
17
  const Let = require('./let');
18
18
  const Var = require('./var');
19
19
  const Literal = require('./literal');
20
20
  const Assertion = require('./assertion');
21
- const ArrayAssertion = require('./array');
22
21
  const Coercion = require('./coercion');
23
22
  const At = require('./at');
24
23
  const Match = require('./match');
@@ -28,7 +27,7 @@ const Interpolate = require('./interpolate');
28
27
  const Coalesce = require('./coalesce');
29
28
  const { Equals, NotEquals, LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual } = require('./comparison');
30
29
  const { CollatorExpression } = require('./collator');
31
- const { Formatted, FormatExpression } = require('./formatted');
30
+ const { FormatExpression } = require('./format');
32
31
  const Length = require('./length');
33
32
 
34
33
  const expressions = {
@@ -39,7 +38,7 @@ const expressions = {
39
38
  '<': LessThan,
40
39
  '>=': GreaterThanOrEqual,
41
40
  '<=': LessThanOrEqual,
42
- array: ArrayAssertion,
41
+ array: Assertion,
43
42
  at: At,
44
43
  boolean: Assertion,
45
44
  case: Case,
@@ -57,8 +56,10 @@ const expressions = {
57
56
  object: Assertion,
58
57
  step: Step,
59
58
  string: Assertion,
59
+ 'to-boolean': Coercion,
60
60
  'to-color': Coercion,
61
61
  'to-number': Coercion,
62
+ 'to-string': Coercion,
62
63
  var: Var
63
64
  };
64
65
 
@@ -103,26 +104,7 @@ CompoundExpression.register(expressions, {
103
104
  throw new RuntimeError(v.evaluate(ctx));
104
105
  }
105
106
  ],
106
- typeof: [StringType, [ValueType], (ctx, [v]) => toString(typeOf(v.evaluate(ctx)))],
107
- 'to-string': [
108
- StringType,
109
- [ValueType],
110
- (ctx, [v]) => {
111
- v = v.evaluate(ctx);
112
- const type = typeof v;
113
- if (v === null) {
114
- return '';
115
- }
116
- if (type === 'string' || type === 'number' || type === 'boolean') {
117
- return String(v);
118
- }
119
- if (v instanceof Color || v instanceof Formatted) {
120
- return v.toString();
121
- }
122
- return JSON.stringify(v);
123
- }
124
- ],
125
- 'to-boolean': [BooleanType, [ValueType], (ctx, [v]) => Boolean(v.evaluate(ctx))],
107
+ typeof: [StringType, [ValueType], (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))],
126
108
  'to-rgba': [
127
109
  array(NumberType, 4),
128
110
  [ColorType],
@@ -350,7 +332,7 @@ CompoundExpression.register(expressions, {
350
332
  ],
351
333
  upcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toUpperCase()],
352
334
  downcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase()],
353
- concat: [StringType, varargs(StringType), (ctx, args) => args.map(arg => arg.evaluate(ctx)).join('')],
335
+ concat: [StringType, varargs(ValueType), (ctx, args) => args.map(arg => valueToString(arg.evaluate(ctx))).join('')],
354
336
  'resolved-locale': [StringType, [CollatorType], (ctx, [collator]) => collator.evaluate(ctx).resolvedLocale()]
355
337
  });
356
338
 
@@ -37,7 +37,7 @@ class Let {
37
37
  bindings.push([name, value]);
38
38
  }
39
39
 
40
- const result = context.parse(args[args.length - 1], args.length - 1, undefined, bindings);
40
+ const result = context.parse(args[args.length - 1], args.length - 1, context.expectedType, bindings);
41
41
  if (!result) return null;
42
42
 
43
43
  return new Let(bindings, result);
@@ -1,6 +1,6 @@
1
1
  const assert = require('assert');
2
2
  const { isValue, typeOf, Color } = require('../values');
3
- const { Formatted } = require('./formatted');
3
+ const { Formatted } = require('../types/formatted');
4
4
 
5
5
  class Literal {
6
6
  constructor(type, value) {
@@ -4,6 +4,9 @@ const geometryTypes = ['Unknown', 'Point', 'LineString', 'Polygon'];
4
4
 
5
5
  class EvaluationContext {
6
6
  constructor() {
7
+ this.globals = null;
8
+ this.feature = null;
9
+ this.featureState = null;
7
10
  this._parseColorCache = {};
8
11
  }
9
12
 
@@ -18,17 +18,12 @@ class StyleExpression {
18
18
  constructor(expression, propertySpec) {
19
19
  this.expression = expression;
20
20
  this._warningHistory = {};
21
+ this._evaluator = new EvaluationContext();
21
22
  this._defaultValue = getDefaultValue(propertySpec);
22
- if (propertySpec.type === 'enum') {
23
- this._enumValues = propertySpec.values;
24
- }
23
+ this._enumValues = propertySpec.type === 'enum' ? propertySpec.values : null;
25
24
  }
26
25
 
27
26
  evaluateWithoutErrorHandling(globals, feature, featureState) {
28
- if (!this._evaluator) {
29
- this._evaluator = new EvaluationContext();
30
- }
31
-
32
27
  this._evaluator.globals = globals;
33
28
  this._evaluator.feature = feature;
34
29
  this._evaluator.featureState = featureState;
@@ -37,13 +32,9 @@ class StyleExpression {
37
32
  }
38
33
 
39
34
  evaluate(globals, feature, featureState) {
40
- if (!this._evaluator) {
41
- this._evaluator = new EvaluationContext();
42
- }
43
-
44
35
  this._evaluator.globals = globals;
45
- this._evaluator.feature = feature;
46
- this._evaluator.featureState = featureState;
36
+ this._evaluator.feature = feature || null;
37
+ this._evaluator.featureState = featureState || null;
47
38
 
48
39
  try {
49
40
  const val = this.expression.evaluate(this._evaluator);
@@ -90,7 +81,16 @@ function isExpression(expression) {
90
81
  */
91
82
  function createExpression(expression, propertySpec) {
92
83
  const parser = new ParsingContext(definitions, [], getExpectedType(propertySpec));
93
- const parsed = parser.parse(expression);
84
+
85
+ // For string-valued properties, coerce to string at the top level rather than asserting.
86
+ const parsed = parser.parse(
87
+ expression,
88
+ undefined,
89
+ undefined,
90
+ undefined,
91
+ propertySpec.type === 'string' ? { typeAnnotation: 'coerce' } : undefined
92
+ );
93
+
94
94
  if (!parsed) {
95
95
  assert(parser.errors.length > 0);
96
96
  return error(parser.errors);
@@ -283,7 +283,7 @@ function findZoomCurve(expression) {
283
283
  return result;
284
284
  }
285
285
 
286
- const { ColorType, StringType, NumberType, BooleanType, ValueType, array } = require('./types');
286
+ const { ColorType, StringType, NumberType, BooleanType, ValueType, FormattedType, array } = require('./types');
287
287
 
288
288
  function getExpectedType(spec) {
289
289
  const types = {
@@ -291,14 +291,15 @@ function getExpectedType(spec) {
291
291
  string: StringType,
292
292
  number: NumberType,
293
293
  enum: StringType,
294
- boolean: BooleanType
294
+ boolean: BooleanType,
295
+ formatted: FormattedType
295
296
  };
296
297
 
297
298
  if (spec.type === 'array') {
298
299
  return array(types[spec.value] || ValueType, spec.length);
299
300
  }
300
301
 
301
- return types[spec.type] || null;
302
+ return types[spec.type];
302
303
  }
303
304
 
304
305
  function getDefaultValue(spec) {