@mapwhit/tilerenderer 0.49.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 (29) 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 +1 -1
  5. package/src/data/bucket/symbol_bucket.js +15 -14
  6. package/src/shaders/fill_extrusion.fragment.glsl +0 -7
  7. package/src/shaders/fill_extrusion.vertex.glsl +4 -4
  8. package/src/style-spec/expression/definitions/coalesce.js +1 -3
  9. package/src/style-spec/expression/definitions/coercion.js +32 -21
  10. package/src/style-spec/expression/definitions/collator.js +1 -23
  11. package/src/style-spec/expression/definitions/{formatted.js → format.js} +3 -29
  12. package/src/style-spec/expression/definitions/index.js +7 -24
  13. package/src/style-spec/expression/definitions/let.js +1 -1
  14. package/src/style-spec/expression/definitions/literal.js +1 -1
  15. package/src/style-spec/expression/evaluation_context.js +3 -0
  16. package/src/style-spec/expression/index.js +18 -17
  17. package/src/style-spec/expression/parsing_context.js +25 -16
  18. package/src/style-spec/expression/types/collator.js +24 -0
  19. package/src/style-spec/expression/types/formatted.js +39 -0
  20. package/src/style-spec/expression/types.js +1 -1
  21. package/src/style-spec/expression/values.js +24 -1
  22. package/src/style-spec/function/convert.js +15 -9
  23. package/src/style-spec/function/index.js +4 -0
  24. package/src/style-spec/reference/v8.json +1 -1
  25. package/src/symbol/mergelines.js +2 -2
  26. package/src/symbol/placement.js +21 -2
  27. package/src/symbol/shaping.js +9 -18
  28. package/src/symbol/symbol_layout.js +2 -2
  29. package/src/symbol/transform_text.js +5 -8
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.48.0"
2
+ "version": "0.49.0"
3
3
  }
@@ -1,12 +1,4 @@
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
- void main(){
6
- #pragma mapbox: initialize lowp float base
7
- #pragma mapbox: initialize lowp float height
8
- #pragma mapbox: initialize highp vec4 color
9
- gl_FragColor=v_color;
1
+ varying vec4 v_color;void main(){gl_FragColor=v_color;
10
2
  #ifdef OVERDRAW_INSPECTOR
11
3
  gl_FragColor=vec4(1.);
12
4
  #endif
@@ -1,9 +1,9 @@
1
1
  uniform mat4 u_matrix;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp float u_lightintensity;uniform float u_vertical_gradient;uniform lowp float u_opacity;attribute vec2 a_pos;attribute vec4 a_normal_ed;varying vec4 v_color;
2
- #pragma mapbox: define lowp float base
3
- #pragma mapbox: define lowp float height
2
+ #pragma mapbox: define highp float base
3
+ #pragma mapbox: define highp float height
4
4
  #pragma mapbox: define highp vec4 color
5
5
  void main(){
6
- #pragma mapbox: initialize lowp float base
7
- #pragma mapbox: initialize lowp float height
6
+ #pragma mapbox: initialize highp float base
7
+ #pragma mapbox: initialize highp float height
8
8
  #pragma mapbox: initialize highp vec4 color
9
9
  vec3 normal=a_normal_ed.xyz;base=max(0.,base);height=max(0.,height);float t=mod(normal.x,2.);gl_Position=u_matrix*vec4(a_pos,t>0.?height:base,1);float colorvalue=color.r*0.2126+color.g*0.7152+color.b*0.0722;v_color=vec4(0.,0.,0.,1.);vec4 ambientlight=vec4(0.03,0.03,0.03,1.);color+=ambientlight;float directional=clamp(dot(normal/16384.,u_lightpos),0.,1.);directional=mix((1.-u_lightintensity),max((1.-colorvalue+u_lightintensity),1.),directional);if(normal.y!=0.){directional*=((1.-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.,0.5),mix(0.7,0.98,1.-u_lightintensity),1.)));}v_color.r+=clamp(color.r*directional*u_lightcolor.r,mix(0.,0.3,1.-u_lightcolor.r),1.);v_color.g+=clamp(color.g*directional*u_lightcolor.g,mix(0.,0.3,1.-u_lightcolor.g),1.);v_color.b+=clamp(color.b*directional*u_lightcolor.b,mix(0.,0.3,1.-u_lightcolor.b),1.);v_color*=u_opacity;}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mapwhit/tilerenderer",
3
3
  "description": "A WebGL interactive maps library",
4
- "version": "0.49.0",
4
+ "version": "0.50.0",
5
5
  "main": "src/index.js",
6
6
  "license": "BSD-3-Clause",
7
7
  "repository": {
@@ -32,7 +32,7 @@ const { verticalizedCharacterMap } = require('../../util/verticalize_punctuation
32
32
  const { getSizeData } = require('../../symbol/symbol_size');
33
33
  const { register } = require('../../util/transfer_registry');
34
34
  const EvaluationParameters = require('../../style/evaluation_parameters');
35
- const { Formatted } = require('../../style-spec/expression/definitions/formatted');
35
+ const { Formatted } = require('../../style-spec/expression/types/formatted');
36
36
 
37
37
  // Opacity arrays are frequently updated but don't contain a lot of information, so we pack them
38
38
  // tight. Each Uint32 is actually four duplicate Uint8s for the four corners of a glyph
@@ -267,8 +267,15 @@ class SymbolBucket {
267
267
 
268
268
  let text;
269
269
  if (hasText) {
270
- text = layer.getValueAndResolveTokens('text-field', feature);
271
- text = transformText(text, layer, feature);
270
+ // Expression evaluation will automatically coerce to Formatted
271
+ // but plain string token evaluation skips that pathway so do the
272
+ // conversion here.
273
+ const resolvedTokens = layer.getValueAndResolveTokens('text-field', feature);
274
+ text = transformText(
275
+ resolvedTokens instanceof Formatted ? resolvedTokens : Formatted.fromString(resolvedTokens),
276
+ layer,
277
+ feature
278
+ );
272
279
  }
273
280
 
274
281
  let icon;
@@ -300,19 +307,13 @@ class SymbolBucket {
300
307
 
301
308
  if (text) {
302
309
  const fontStack = textFont.evaluate(feature, {}).join(',');
303
- const stack = (stacks[fontStack] = stacks[fontStack] || {});
304
310
  const textAlongLine =
305
311
  layout.get('text-rotation-alignment') === 'map' && layout.get('symbol-placement') !== 'point';
306
- if (text instanceof Formatted) {
307
- for (const section of text.sections) {
308
- const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString());
309
- const sectionFont = section.fontStack || fontStack;
310
- const sectionStack = (stacks[sectionFont] = stacks[sectionFont] || {});
311
- this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode);
312
- }
313
- } else {
314
- const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text);
315
- this.calculateGlyphDependencies(text, stack, textAlongLine, doesAllowVerticalWritingMode);
312
+ for (const section of text.sections) {
313
+ const doesAllowVerticalWritingMode = allowsVerticalWritingMode(text.toString());
314
+ const sectionFont = section.fontStack || fontStack;
315
+ const sectionStack = (stacks[sectionFont] = stacks[sectionFont] || {});
316
+ this.calculateGlyphDependencies(section.text, sectionStack, textAlongLine, doesAllowVerticalWritingMode);
316
317
  }
317
318
  }
318
319
  }
@@ -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;
@@ -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,10 +8,10 @@ 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');
@@ -27,7 +27,7 @@ const Interpolate = require('./interpolate');
27
27
  const Coalesce = require('./coalesce');
28
28
  const { Equals, NotEquals, LessThan, GreaterThan, LessThanOrEqual, GreaterThanOrEqual } = require('./comparison');
29
29
  const { CollatorExpression } = require('./collator');
30
- const { Formatted, FormatExpression } = require('./formatted');
30
+ const { FormatExpression } = require('./format');
31
31
  const Length = require('./length');
32
32
 
33
33
  const expressions = {
@@ -56,8 +56,10 @@ const expressions = {
56
56
  object: Assertion,
57
57
  step: Step,
58
58
  string: Assertion,
59
+ 'to-boolean': Coercion,
59
60
  'to-color': Coercion,
60
61
  'to-number': Coercion,
62
+ 'to-string': Coercion,
61
63
  var: Var
62
64
  };
63
65
 
@@ -102,26 +104,7 @@ CompoundExpression.register(expressions, {
102
104
  throw new RuntimeError(v.evaluate(ctx));
103
105
  }
104
106
  ],
105
- typeof: [StringType, [ValueType], (ctx, [v]) => toString(typeOf(v.evaluate(ctx)))],
106
- 'to-string': [
107
- StringType,
108
- [ValueType],
109
- (ctx, [v]) => {
110
- v = v.evaluate(ctx);
111
- const type = typeof v;
112
- if (v === null) {
113
- return '';
114
- }
115
- if (type === 'string' || type === 'number' || type === 'boolean') {
116
- return String(v);
117
- }
118
- if (v instanceof Color || v instanceof Formatted) {
119
- return v.toString();
120
- }
121
- return JSON.stringify(v);
122
- }
123
- ],
124
- 'to-boolean': [BooleanType, [ValueType], (ctx, [v]) => Boolean(v.evaluate(ctx))],
107
+ typeof: [StringType, [ValueType], (ctx, [v]) => typeToString(typeOf(v.evaluate(ctx)))],
125
108
  'to-rgba': [
126
109
  array(NumberType, 4),
127
110
  [ColorType],
@@ -349,7 +332,7 @@ CompoundExpression.register(expressions, {
349
332
  ],
350
333
  upcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toUpperCase()],
351
334
  downcase: [StringType, [StringType], (ctx, [s]) => s.evaluate(ctx).toLowerCase()],
352
- 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('')],
353
336
  'resolved-locale': [StringType, [CollatorType], (ctx, [collator]) => collator.evaluate(ctx).resolvedLocale()]
354
337
  });
355
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) {
@@ -1,3 +1,4 @@
1
+ const assert = require('assert');
1
2
  const Scope = require('./scope');
2
3
 
3
4
  const { checkSubtype } = require('./types');
@@ -48,6 +49,16 @@ class ParsingContext {
48
49
  expr = ['literal', expr];
49
50
  }
50
51
 
52
+ function annotate(parsed, type, typeAnnotation) {
53
+ if (typeAnnotation === 'assert') {
54
+ return new Assertion(type, [parsed]);
55
+ }
56
+ if (typeAnnotation === 'coerce') {
57
+ return new Coercion(type, [parsed]);
58
+ }
59
+ return parsed;
60
+ }
61
+
51
62
  if (Array.isArray(expr)) {
52
63
  if (expr.length === 0) {
53
64
  return this.error(
@@ -73,11 +84,14 @@ class ParsingContext {
73
84
  const expected = this.expectedType;
74
85
  const actual = parsed.type;
75
86
 
76
- // When we expect a number, string, boolean, or array but
77
- // have a Value, we can wrap it in a refining assertion.
78
- // When we expect a Color but have a String or Value, we
79
- // can wrap it in "to-color" coercion.
87
+ // When we expect a number, string, boolean, or array but have a value, wrap it in an assertion.
88
+ // When we expect a color or formatted string, but have a string or value, wrap it in a coercion.
80
89
  // Otherwise, we do static type-checking.
90
+ //
91
+ // These behaviors are overridable for:
92
+ // * The "coalesce" operator, which needs to omit type annotations.
93
+ // * String-valued properties (e.g. `text-field`), where coercion is more convenient than assertion.
94
+ //
81
95
  if (
82
96
  (expected.kind === 'string' ||
83
97
  expected.kind === 'number' ||
@@ -86,18 +100,13 @@ class ParsingContext {
86
100
  expected.kind === 'array') &&
87
101
  actual.kind === 'value'
88
102
  ) {
89
- if (!options.omitTypeAnnotations) {
90
- parsed = new Assertion(expected, [parsed]);
91
- }
92
- } else if (expected.kind === 'color' && (actual.kind === 'value' || actual.kind === 'string')) {
93
- if (!options.omitTypeAnnotations) {
94
- parsed = new Coercion(expected, [parsed]);
95
- }
96
- } else if (expected.kind === 'formatted' && (actual.kind === 'value' || actual.kind === 'string')) {
97
- if (!options.omitTypeAnnotations) {
98
- parsed = new Coercion(expected, [parsed]);
99
- }
100
- } else if (this.checkSubtype(this.expectedType, parsed.type)) {
103
+ parsed = annotate(parsed, expected, options.typeAnnotation || 'assert');
104
+ } else if (
105
+ (expected.kind === 'color' || expected.kind === 'formatted') &&
106
+ (actual.kind === 'value' || actual.kind === 'string')
107
+ ) {
108
+ parsed = annotate(parsed, expected, options.typeAnnotation || 'coerce');
109
+ } else if (this.checkSubtype(expected, actual)) {
101
110
  return null;
102
111
  }
103
112
  }
@@ -0,0 +1,24 @@
1
+ class Collator {
2
+ constructor(caseSensitive, diacriticSensitive, locale) {
3
+ if (caseSensitive) this.sensitivity = diacriticSensitive ? 'variant' : 'case';
4
+ else this.sensitivity = diacriticSensitive ? 'accent' : 'base';
5
+
6
+ this.locale = locale;
7
+ this.collator = new Intl.Collator(this.locale ? this.locale : [], {
8
+ sensitivity: this.sensitivity,
9
+ usage: 'search'
10
+ });
11
+ }
12
+
13
+ compare(lhs, rhs) {
14
+ return this.collator.compare(lhs, rhs);
15
+ }
16
+
17
+ resolvedLocale() {
18
+ // We create a Collator without "usage: search" because we don't want
19
+ // the search options encoded in our result (e.g. "en-u-co-search")
20
+ return new Intl.Collator(this.locale ? this.locale : []).resolvedOptions().locale;
21
+ }
22
+ }
23
+
24
+ module.exports = { Collator };
@@ -0,0 +1,39 @@
1
+ class FormattedSection {
2
+ constructor(text, scale, fontStack = null) {
3
+ this.text = text;
4
+ this.scale = scale;
5
+ this.fontStack = fontStack;
6
+ }
7
+ }
8
+
9
+ class Formatted {
10
+ constructor(sections) {
11
+ this.sections = sections;
12
+ }
13
+
14
+ static fromString(unformatted) {
15
+ return new Formatted([new FormattedSection(unformatted, null, null)]);
16
+ }
17
+
18
+ toString() {
19
+ return this.sections.map(section => section.text).join('');
20
+ }
21
+
22
+ serialize() {
23
+ const serialized = ['format'];
24
+ for (const section of this.sections) {
25
+ serialized.push(section.text);
26
+ const options = {};
27
+ if (section.fontStack) {
28
+ options['text-font'] = ['literal', section.fontStack.split(',')];
29
+ }
30
+ if (section.scale) {
31
+ options['font-scale'] = section.scale;
32
+ }
33
+ serialized.push(options);
34
+ }
35
+ return serialized;
36
+ }
37
+ }
38
+
39
+ module.exports = { Formatted, FormattedSection };
@@ -53,7 +53,7 @@ function checkSubtype(expected, t) {
53
53
  if (expected.kind === 'array') {
54
54
  if (
55
55
  t.kind === 'array' &&
56
- !checkSubtype(expected.itemType, t.itemType) &&
56
+ ((t.N === 0 && t.itemType.kind === 'value') || !checkSubtype(expected.itemType, t.itemType)) &&
57
57
  (typeof expected.N !== 'number' || expected.N === t.N)
58
58
  ) {
59
59
  return null;
@@ -1,7 +1,8 @@
1
1
  const assert = require('assert');
2
2
 
3
3
  const Color = require('../util/color');
4
- const { Collator } = require('./definitions/collator');
4
+ const { Collator } = require('./types/collator');
5
+ const { Formatted } = require('./types/formatted');
5
6
  const {
6
7
  NullType,
7
8
  NumberType,
@@ -11,6 +12,7 @@ const {
11
12
  ObjectType,
12
13
  ValueType,
13
14
  CollatorType,
15
+ FormattedType,
14
16
  array
15
17
  } = require('./types');
16
18
 
@@ -58,6 +60,9 @@ function isValue(mixed) {
58
60
  if (mixed instanceof Collator) {
59
61
  return true;
60
62
  }
63
+ if (mixed instanceof Formatted) {
64
+ return true;
65
+ }
61
66
  if (Array.isArray(mixed)) {
62
67
  for (const item of mixed) {
63
68
  if (!isValue(item)) {
@@ -96,6 +101,9 @@ function typeOf(value) {
96
101
  if (value instanceof Collator) {
97
102
  return CollatorType;
98
103
  }
104
+ if (value instanceof Formatted) {
105
+ return FormattedType;
106
+ }
99
107
  if (Array.isArray(value)) {
100
108
  const length = value.length;
101
109
  let itemType;
@@ -117,7 +125,22 @@ function typeOf(value) {
117
125
  return ObjectType;
118
126
  }
119
127
 
128
+ function toString(value) {
129
+ const type = typeof value;
130
+ if (value === null) {
131
+ return '';
132
+ }
133
+ if (type === 'string' || type === 'number' || type === 'boolean') {
134
+ return String(value);
135
+ }
136
+ if (value instanceof Color || value instanceof Formatted) {
137
+ return value.toString();
138
+ }
139
+ return JSON.stringify(value);
140
+ }
141
+
120
142
  module.exports = {
143
+ toString,
121
144
  Color,
122
145
  Collator,
123
146
  validateRGBA,
@@ -2,6 +2,10 @@ const assert = require('assert');
2
2
 
3
3
  module.exports = convertFunction;
4
4
 
5
+ function convertLiteral(value) {
6
+ return typeof value === 'object' ? ['literal', value] : value;
7
+ }
8
+
5
9
  function convertFunction(parameters, propertySpec) {
6
10
  let stops = parameters.stops;
7
11
  if (!stops) {
@@ -17,7 +21,7 @@ function convertFunction(parameters, propertySpec) {
17
21
  if (!featureDependent && propertySpec.tokens && typeof stop[1] === 'string') {
18
22
  return [stop[0], convertTokenString(stop[1])];
19
23
  }
20
- return [stop[0], ['literal', stop[1]]];
24
+ return [stop[0], convertLiteral(stop[1])];
21
25
  });
22
26
 
23
27
  if (zoomAndFeatureDependent) {
@@ -33,7 +37,9 @@ function convertIdentityFunction(parameters, propertySpec) {
33
37
  const get = ['get', parameters.property];
34
38
 
35
39
  if (parameters.default === undefined) {
36
- return get;
40
+ // By default, expressions for string-valued properties get coerced. To preserve
41
+ // legacy function semantics, insert an explicit assertion instead.
42
+ return propertySpec.type === 'string' ? ['string', get] : get;
37
43
  }
38
44
  if (propertySpec.type === 'enum') {
39
45
  return ['match', get, Object.keys(propertySpec.values), get, parameters.default];
@@ -41,7 +47,7 @@ function convertIdentityFunction(parameters, propertySpec) {
41
47
  const expression = [
42
48
  propertySpec.type === 'color' ? 'to-color' : propertySpec.type,
43
49
  get,
44
- ['literal', parameters.default]
50
+ convertLiteral(parameters.default)
45
51
  ];
46
52
  if (propertySpec.type === 'array') {
47
53
  expression.splice(1, 0, propertySpec.value, propertySpec.length || null);
@@ -121,7 +127,7 @@ function convertPropertyFunction(parameters, propertySpec, stops) {
121
127
  for (const stop of stops) {
122
128
  expression.push(['==', get, stop[0]], stop[1]);
123
129
  }
124
- expression.push(['literal', coalesce(parameters.default, propertySpec.default)]);
130
+ expression.push(convertLiteral(coalesce(parameters.default, propertySpec.default)));
125
131
  return expression;
126
132
  }
127
133
  if (type === 'categorical') {
@@ -129,7 +135,7 @@ function convertPropertyFunction(parameters, propertySpec, stops) {
129
135
  for (const stop of stops) {
130
136
  appendStopPair(expression, stop[0], stop[1], false);
131
137
  }
132
- expression.push(['literal', coalesce(parameters.default, propertySpec.default)]);
138
+ expression.push(convertLiteral(coalesce(parameters.default, propertySpec.default)));
133
139
  return expression;
134
140
  }
135
141
  if (type === 'interval') {
@@ -140,7 +146,7 @@ function convertPropertyFunction(parameters, propertySpec, stops) {
140
146
  fixupDegenerateStepCurve(expression);
141
147
  return parameters.default === undefined
142
148
  ? expression
143
- : ['case', ['==', ['typeof', get], 'number'], expression, ['literal', parameters.default]];
149
+ : ['case', ['==', ['typeof', get], 'number'], expression, convertLiteral(parameters.default)];
144
150
  }
145
151
  if (type === 'exponential') {
146
152
  const base = parameters.base !== undefined ? parameters.base : 1;
@@ -150,7 +156,7 @@ function convertPropertyFunction(parameters, propertySpec, stops) {
150
156
  }
151
157
  return parameters.default === undefined
152
158
  ? expression
153
- : ['case', ['==', ['typeof', get], 'number'], expression, ['literal', parameters.default]];
159
+ : ['case', ['==', ['typeof', get], 'number'], expression, convertLiteral(parameters.default)];
154
160
  }
155
161
  throw new Error(`Unknown property function type ${type}`);
156
162
  }
@@ -217,7 +223,7 @@ function convertTokenString(s) {
217
223
  const literal = s.slice(pos, re.lastIndex - match[0].length);
218
224
  pos = re.lastIndex;
219
225
  if (literal.length > 0) result.push(literal);
220
- result.push(['to-string', ['get', match[1]]]);
226
+ result.push(['get', match[1]]);
221
227
  }
222
228
 
223
229
  if (result.length === 1) {
@@ -227,7 +233,7 @@ function convertTokenString(s) {
227
233
  if (pos < s.length) {
228
234
  result.push(s.slice(pos));
229
235
  } else if (result.length === 2) {
230
- return result[1];
236
+ return ['to-string', result[1]];
231
237
  }
232
238
 
233
239
  return result;
@@ -3,6 +3,7 @@ const Color = require('../util/color');
3
3
  const getType = require('../util/get_type');
4
4
  const interpolate = require('../util/interpolate');
5
5
  const Interpolate = require('../expression/definitions/interpolate');
6
+ const { Formatted } = require('../expression/types/formatted');
6
7
  const { supportsInterpolation } = require('../util/properties');
7
8
 
8
9
  module.exports = {
@@ -203,6 +204,9 @@ function evaluateIdentityFunction(parameters, propertySpec, input) {
203
204
  case 'color':
204
205
  input = Color.parse(input);
205
206
  break;
207
+ case 'formatted':
208
+ input = Formatted.fromString(input.toString());
209
+ break;
206
210
  case 'enum':
207
211
  if (!propertySpec.values.includes(input)) {
208
212
  input = undefined;
@@ -3148,7 +3148,7 @@
3148
3148
  }
3149
3149
  },
3150
3150
  "concat": {
3151
- "doc": "Returns a `string` consisting of the concatenation of the inputs. If any inputs are `formatted`, returns a `formatted` with default formatting options for all unformatted inputs.",
3151
+ "doc": "Returns a `string` consisting of the concatenation of the inputs. Each input is converted to a string as if by `to-string`.",
3152
3152
  "group": "String",
3153
3153
  "sdk-support": {
3154
3154
  "basic functionality": {
@@ -1,4 +1,4 @@
1
- const { Formatted } = require('../style-spec/expression/definitions/formatted');
1
+ const { Formatted } = require('../style-spec/expression/types/formatted');
2
2
 
3
3
  module.exports = function (features) {
4
4
  const leftIndex = new Map();
@@ -8,7 +8,7 @@ module.exports = function (features) {
8
8
 
9
9
  for (let k = 0; k < features.length; k++) {
10
10
  const { geometry, text: featureText } = features[k];
11
- const text = featureText instanceof Formatted ? featureText.toString() : featureText;
11
+ const text = featureText ? featureText.toString() : null;
12
12
 
13
13
  if (!text) {
14
14
  add(k);
@@ -170,6 +170,25 @@ class Placement {
170
170
  const textOptional = layout.get('text-optional');
171
171
  const iconOptional = layout.get('icon-optional');
172
172
 
173
+ const textAllowOverlap = layout.get('text-allow-overlap');
174
+ const iconAllowOverlap = layout.get('icon-allow-overlap');
175
+ // This logic is similar to the "defaultOpacityState" logic below in updateBucketOpacities
176
+ // If we know a symbol is always supposed to show, force it to be marked visible even if
177
+ // it wasn't placed into the collision index (because some or all of it was outside the range
178
+ // of the collision grid).
179
+ // There is a subtle edge case here we're accepting:
180
+ // Symbol A has text-allow-overlap: true, icon-allow-overlap: true, icon-optional: false
181
+ // A's icon is outside the grid, so doesn't get placed
182
+ // A's text would be inside grid, but doesn't get placed because of icon-optional: false
183
+ // We still show A because of the allow-overlap settings.
184
+ // Symbol B has allow-overlap: false, and gets placed where A's text would be
185
+ // On panning in, there is a short period when Symbol B and Symbol A will overlap
186
+ // This is the reverse of our normal policy of "fade in on pan", but should look like any other
187
+ // collision and hopefully not be too noticeable.
188
+ // See https://github.com/mapbox/mapbox-gl-js/issues/7172
189
+ const alwaysShowText = textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || iconOptional);
190
+ const alwaysShowIcon = iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || textOptional);
191
+
173
192
  const collisionGroup = this.collisionGroups.get(bucket.sourceID);
174
193
 
175
194
  if (!bucket.collisionArrays && collisionBoxArray) {
@@ -304,8 +323,8 @@ class Placement {
304
323
  assert(bucket.bucketInstanceId !== 0);
305
324
 
306
325
  this.placements[symbolInstance.crossTileID] = new JointPlacement(
307
- placeText,
308
- placeIcon,
326
+ placeText || (alwaysShowText && placedGlyphBoxes),
327
+ placeIcon || alwaysShowIcon,
309
328
  offscreen || bucket.justReloaded
310
329
  );
311
330
  seenCrossTileIDs[symbolInstance.crossTileID] = true;
@@ -1,7 +1,6 @@
1
1
  const { charHasUprightVerticalOrientation, charAllowsIdeographicBreaking } = require('../util/script_detection');
2
2
  const verticalizePunctuation = require('../util/verticalize_punctuation');
3
3
  const { plugin: rtlTextPlugin } = require('../source/rtl_text_plugin');
4
- const { Formatted } = require('../style-spec/expression/definitions/formatted');
5
4
 
6
5
  const WritingMode = {
7
6
  horizontal: 1,
@@ -24,23 +23,15 @@ class TaggedString {
24
23
 
25
24
  static fromFeature(text, defaultFontStack) {
26
25
  const result = new TaggedString();
27
- if (text instanceof Formatted) {
28
- for (let i = 0; i < text.sections.length; i++) {
29
- const section = text.sections[i];
30
- result.sections.push({
31
- scale: section.scale || 1,
32
- fontStack: section.fontStack || defaultFontStack
33
- });
34
- result.text += section.text;
35
- for (let j = 0; j < section.text.length; j++) {
36
- result.sectionIndex.push(i);
37
- }
38
- }
39
- } else {
40
- result.text = text;
41
- result.sections.push({ scale: 1, fontStack: defaultFontStack });
42
- for (let i = 0; i < text.length; i++) {
43
- result.sectionIndex.push(0);
26
+ for (let i = 0; i < text.sections.length; i++) {
27
+ const section = text.sections[i];
28
+ result.sections.push({
29
+ scale: section.scale || 1,
30
+ fontStack: section.fontStack || defaultFontStack
31
+ });
32
+ result.text += section.text;
33
+ for (let j = 0; j < section.text.length; j++) {
34
+ result.sectionIndex.push(i);
44
35
  }
45
36
  }
46
37
  return result;
@@ -12,7 +12,7 @@ const classifyRings = require('../util/classify_rings');
12
12
  const EXTENT = require('../data/extent');
13
13
  const SymbolBucket = require('../data/bucket/symbol_bucket');
14
14
  const EvaluationParameters = require('../style/evaluation_parameters');
15
- const { Formatted } = require('../style-spec/expression/definitions/formatted');
15
+ const { Formatted } = require('../style-spec/expression/types/formatted');
16
16
  const murmur3 = require('murmurhash-js');
17
17
 
18
18
  // The symbol layout process needs `text-size` evaluated at up to five different zoom levels, and
@@ -79,7 +79,7 @@ function performSymbolLayout(bucket, glyphMap, glyphPositions, imageMap, imagePo
79
79
  const shapedTextOrientations = {};
80
80
  const text = feature.text;
81
81
  if (text) {
82
- const unformattedText = text instanceof Formatted ? text.toString() : text;
82
+ const unformattedText = text.toString();
83
83
  const textOffset = layout
84
84
  .get('text-offset')
85
85
  .evaluate(feature, {})
@@ -1,5 +1,5 @@
1
1
  const { plugin: rtlTextPlugin } = require('../source/rtl_text_plugin');
2
- const { Formatted } = require('../style-spec/expression/definitions/formatted');
2
+ const { Formatted } = require('../style-spec/expression/types/formatted');
3
3
 
4
4
  function transformText(text, layer, feature) {
5
5
  const transform = layer.layout.get('text-transform').evaluate(feature, {});
@@ -17,11 +17,8 @@ function transformText(text, layer, feature) {
17
17
  }
18
18
 
19
19
  module.exports = function (text, layer, feature) {
20
- if (text instanceof Formatted) {
21
- text.sections.forEach(section => {
22
- section.text = transformText(section.text, layer, feature);
23
- });
24
- return text;
25
- }
26
- return transformText(text, layer, feature);
20
+ text.sections.forEach(section => {
21
+ section.text = transformText(section.text, layer, feature);
22
+ });
23
+ return text;
27
24
  };