@mapbox/mapbox-gl-style-spec 14.11.0-beta.1 → 14.11.0-beta.2

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.
@@ -1,46 +1,62 @@
1
- import {ColorType, ResolvedImageType, StringType} from '../types';
2
1
  import ResolvedImage from '../types/resolved_image';
2
+ import {ImageId} from '../types/image_id';
3
+ import {ColorType, ResolvedImageType, StringType} from '../types';
3
4
 
4
5
  import type Color from '../../util/color';
5
- import type {Expression, SerializedExpression} from '../expression';
6
- import type EvaluationContext from '../evaluation_context';
7
6
  import type ParsingContext from '../parsing_context';
7
+ import type EvaluationContext from '../evaluation_context';
8
8
  import type {Type} from '../types';
9
+ import type {Expression, SerializedExpression} from '../expression';
9
10
 
10
11
  export type ImageParams = Record<string, Expression>;
12
+ export type IconsetParams = {id: string};
11
13
 
12
14
  export type ImageOptions = {
13
- params: ImageParams;
15
+ params?: ImageParams;
16
+ iconset?: IconsetParams;
14
17
  }
15
18
 
16
- function isImageOptions(value: unknown) {
17
- if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
18
- return true;
19
- }
19
+ type SerializedImageOptions = {
20
+ params?: Record<string, SerializedExpression>;
21
+ iconset?: IconsetParams;
22
+ };
20
23
 
21
- return false;
24
+ function isImageOptions(value: unknown): value is ImageOptions {
25
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
22
26
  }
23
27
 
24
28
  export default class ImageExpression implements Expression {
25
29
  type: Type;
26
- inputPrimary: Expression;
27
- inputPrimaryParams: Record<string, Expression> | undefined;
28
- inputSecondary: Expression | null | undefined;
29
- inputSecondaryParams: Record<string, Expression> | undefined;
30
+
31
+ namePrimary: Expression;
32
+ paramsPrimary?: ImageParams;
33
+ iconsetIdPrimary?: string;
34
+
35
+ nameSecondary?: Expression;
36
+ paramsSecondary?: ImageParams;
37
+ iconsetIdSecondary?: string;
30
38
 
31
39
  _imageWarnHistory: Record<string, boolean> = {};
32
40
 
33
41
  constructor(
34
42
  inputPrimary: Expression,
35
43
  inputSecondary?: Expression | null,
36
- inputPrimaryParams?: Record<string, Expression>,
37
- inputSecondaryParams?: Record<string, Expression>
44
+ inputPrimaryOptions?: ImageOptions,
45
+ inputSecondaryOptions?: ImageOptions
38
46
  ) {
39
47
  this.type = ResolvedImageType;
40
- this.inputPrimary = inputPrimary;
41
- this.inputSecondary = inputSecondary;
42
- this.inputPrimaryParams = inputPrimaryParams;
43
- this.inputSecondaryParams = inputSecondaryParams;
48
+ this.namePrimary = inputPrimary;
49
+ this.nameSecondary = inputSecondary;
50
+
51
+ if (inputPrimaryOptions) {
52
+ this.paramsPrimary = inputPrimaryOptions.params;
53
+ this.iconsetIdPrimary = inputPrimaryOptions.iconset ? inputPrimaryOptions.iconset.id : undefined;
54
+ }
55
+
56
+ if (inputSecondaryOptions) {
57
+ this.paramsSecondary = inputSecondaryOptions.params;
58
+ this.iconsetIdSecondary = inputSecondaryOptions.iconset ? inputSecondaryOptions.iconset.id : undefined;
59
+ }
44
60
  }
45
61
 
46
62
  static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression | null | void {
@@ -49,7 +65,7 @@ export default class ImageExpression implements Expression {
49
65
  }
50
66
 
51
67
  let nextArgId = 1;
52
- const imageExpression: Array<{image: Expression, options: Record<string, Expression>}> = [];
68
+ const imageExpression: Array<{image: Expression, options?: ImageOptions}> = [];
53
69
 
54
70
  function tryParseImage() {
55
71
  if (nextArgId < args.length) {
@@ -59,7 +75,7 @@ export default class ImageExpression implements Expression {
59
75
  return false;
60
76
  }
61
77
 
62
- imageExpression.push({image: imageName, options: undefined});
78
+ imageExpression.push({image: imageName, options: {}});
63
79
  return true;
64
80
  }
65
81
 
@@ -68,45 +84,62 @@ export default class ImageExpression implements Expression {
68
84
 
69
85
  function tryParseOptions() {
70
86
  if (nextArgId < args.length) {
71
- if (!isImageOptions(args[nextArgId])) {
87
+ const options = args[nextArgId];
88
+ if (!isImageOptions(options)) {
72
89
  return true;
73
90
  }
74
91
 
75
- const params = (args[nextArgId] as ImageOptions).params;
76
-
92
+ const params = options.params;
93
+ const iconset = options.iconset;
77
94
  const optionsContext = context.concat(nextArgId);
78
95
 
79
- if (!params) {
96
+ if (!params && !iconset) {
80
97
  nextArgId++;
81
98
  return true;
82
99
  }
83
100
 
84
- if (typeof params !== 'object' || params.constructor !== Object) {
85
- optionsContext.error(`Image options \"params\" should be an object`);
86
- return false;
87
- }
101
+ // Parse the image options params as expressions
102
+ if (params) {
103
+ if (typeof params !== 'object' || params.constructor !== Object) {
104
+ optionsContext.error(`Image options \"params\" should be an object`);
105
+ return false;
106
+ }
88
107
 
89
- const parsed = {};
108
+ const parsedParams: ImageParams = {};
109
+ const childContext = optionsContext.concat(undefined, 'params');
110
+ for (const key in params) {
111
+ if (!key) {
112
+ childContext.error(`Image parameter name should be non-empty`);
113
+ return false;
114
+ }
90
115
 
91
- const childContext = optionsContext.concat(undefined, 'params');
116
+ const value = childContext.concat(undefined, key).parse(params[key], undefined, ColorType, undefined, {typeAnnotation: 'coerce'});
117
+ if (!value) {
118
+ return false;
119
+ }
92
120
 
93
- for (const key in params) {
94
- if (!key) {
95
- childContext.error(`Image parameter name should be non-empty`);
121
+ parsedParams[key] = value;
122
+ }
123
+
124
+ imageExpression[imageExpression.length - 1].options.params = parsedParams;
125
+ }
126
+
127
+ // Validate the iconset image options
128
+ if (iconset) {
129
+ if (typeof iconset !== 'object' || iconset.constructor !== Object) {
130
+ optionsContext.error(`Image options \"iconset\" should be an object`);
96
131
  return false;
97
132
  }
98
133
 
99
- const value = childContext.concat(undefined, key).parse(params[key], undefined, ColorType, undefined, {typeAnnotation: 'coerce'});
100
- if (!value) {
134
+ if (!iconset.id) {
135
+ optionsContext.error(`Image options \"iconset\" should have an \"id\" property`);
101
136
  return false;
102
137
  }
103
138
 
104
- parsed[key] = value;
139
+ imageExpression[imageExpression.length - 1].options.iconset = iconset;
105
140
  }
106
141
 
107
- imageExpression[imageExpression.length - 1].options = parsed;
108
142
  nextArgId++;
109
-
110
143
  return true;
111
144
  }
112
145
 
@@ -162,17 +195,30 @@ export default class ImageExpression implements Expression {
162
195
  }
163
196
 
164
197
  evaluate(ctx: EvaluationContext): null | ResolvedImage {
198
+ const primaryId = {
199
+ name: this.namePrimary.evaluate(ctx),
200
+ iconsetId: this.iconsetIdPrimary
201
+ };
202
+
203
+ const secondaryId = this.nameSecondary ? {
204
+ name: this.nameSecondary.evaluate(ctx),
205
+ iconsetId: this.iconsetIdSecondary
206
+ } : undefined;
207
+
165
208
  const value = ResolvedImage.build(
166
- this.inputPrimary.evaluate(ctx),
167
- this.inputSecondary ? this.inputSecondary.evaluate(ctx) : undefined,
168
- this.inputPrimaryParams ? this.evaluateParams(ctx, this.inputPrimaryParams) : undefined,
169
- this.inputSecondaryParams ? this.evaluateParams(ctx, this.inputSecondaryParams) : undefined
209
+ primaryId,
210
+ secondaryId,
211
+ this.paramsPrimary ? this.evaluateParams(ctx, this.paramsPrimary) : undefined,
212
+ this.paramsSecondary ? this.evaluateParams(ctx, this.paramsSecondary) : undefined
170
213
  );
214
+
171
215
  if (value && ctx.availableImages) {
172
- value.available = ctx.availableImages.indexOf(value.namePrimary) > -1;
173
- // If there's a secondary variant, only mark it available if both are present
174
- if (value.nameSecondary && value.available && ctx.availableImages) {
175
- value.available = ctx.availableImages.indexOf(value.nameSecondary) > -1;
216
+ const primaryId = value.getPrimary().id;
217
+ value.available = ctx.availableImages.some((id) => ImageId.isEqual(id, primaryId));
218
+ if (value.available) {
219
+ // If there's a secondary variant, only mark it available if both are present
220
+ const secondaryId = value.getSecondary() ? value.getSecondary().id : null;
221
+ if (secondaryId) value.available = ctx.availableImages.some((id) => ImageId.isEqual(id, secondaryId));
176
222
  }
177
223
  }
178
224
 
@@ -180,20 +226,22 @@ export default class ImageExpression implements Expression {
180
226
  }
181
227
 
182
228
  eachChild(fn: (_: Expression) => void) {
183
- fn(this.inputPrimary);
184
- if (this.inputPrimaryParams) {
185
- for (const key in this.inputPrimaryParams) {
186
- if (this.inputPrimaryParams[key]) {
187
- fn(this.inputPrimaryParams[key]);
229
+ fn(this.namePrimary);
230
+
231
+ if (this.paramsPrimary) {
232
+ for (const key in this.paramsPrimary) {
233
+ if (this.paramsPrimary[key]) {
234
+ fn(this.paramsPrimary[key]);
188
235
  }
189
236
  }
190
237
  }
191
- if (this.inputSecondary) {
192
- fn(this.inputSecondary);
193
- if (this.inputSecondaryParams) {
194
- for (const key in this.inputSecondaryParams) {
195
- if (this.inputSecondaryParams[key]) {
196
- fn(this.inputSecondaryParams[key]);
238
+
239
+ if (this.nameSecondary) {
240
+ fn(this.nameSecondary);
241
+ if (this.paramsSecondary) {
242
+ for (const key in this.paramsSecondary) {
243
+ if (this.paramsSecondary[key]) {
244
+ fn(this.paramsSecondary[key]);
197
245
  }
198
246
  }
199
247
  }
@@ -205,33 +253,39 @@ export default class ImageExpression implements Expression {
205
253
  return false;
206
254
  }
207
255
 
208
- serializeParams(params: Record<string, Expression> | undefined): {params: Record<string, SerializedExpression>} {
209
- const result: Record<string, SerializedExpression> = {};
256
+ serializeOptions(params: ImageParams, iconsetId: string): SerializedImageOptions | undefined {
257
+ const result: SerializedImageOptions = {};
258
+
259
+ if (iconsetId) {
260
+ result.iconset = {id: iconsetId};
261
+ }
262
+
210
263
  if (params) {
264
+ result.params = {};
211
265
  for (const key in params) {
212
266
  if (params[key]) {
213
- result[key] = params[key].serialize();
267
+ result.params[key] = params[key].serialize();
214
268
  }
215
269
  }
216
- } else {
217
- return undefined;
218
270
  }
219
271
 
220
- return {params: result};
272
+ return Object.keys(result).length > 0 ? result : undefined;
221
273
  }
222
274
 
223
275
  serialize(): SerializedExpression {
224
- const serialized: SerializedExpression = ["image", this.inputPrimary.serialize()];
276
+ const serialized: SerializedExpression = ['image', this.namePrimary.serialize()];
225
277
 
226
- if (this.inputPrimaryParams) {
227
- serialized.push(this.serializeParams(this.inputPrimaryParams));
278
+ if (this.paramsPrimary || this.iconsetIdPrimary) {
279
+ const options = this.serializeOptions(this.paramsPrimary, this.iconsetIdPrimary);
280
+ if (options) serialized.push(options);
228
281
  }
229
282
 
230
- if (this.inputSecondary) {
231
- serialized.push(this.inputSecondary.serialize());
283
+ if (this.nameSecondary) {
284
+ serialized.push(this.nameSecondary.serialize());
232
285
 
233
- if (this.inputSecondaryParams) {
234
- serialized.push(this.serializeParams(this.inputSecondaryParams));
286
+ if (this.paramsSecondary || this.iconsetIdSecondary) {
287
+ const options = this.serializeOptions(this.paramsSecondary, this.iconsetIdSecondary);
288
+ if (options) serialized.push(options);
235
289
  }
236
290
  }
237
291
 
@@ -1,6 +1,7 @@
1
1
  import {Color} from './values';
2
2
 
3
3
  import type Point from '@mapbox/point-geometry';
4
+ import type {ImageId} from './types/image_id';
4
5
  import type {FormattedSection} from './types/formatted';
5
6
  import type {GlobalProperties, Feature, FeatureState} from './index';
6
7
  import type {CanonicalTileID} from '../types/tile_id';
@@ -14,7 +15,7 @@ class EvaluationContext {
14
15
  feature: Feature | null | undefined;
15
16
  featureState: FeatureState | null | undefined;
16
17
  formattedSection: FormattedSection | null | undefined;
17
- availableImages: Array<string> | null | undefined;
18
+ availableImages: ImageId[] | null | undefined;
18
19
  canonical: null | CanonicalTileID;
19
20
  featureTileCoord: Point | null | undefined;
20
21
  featureDistanceData: FeatureDistanceData | null | undefined;
@@ -35,6 +35,7 @@ import type Point from '@mapbox/point-geometry';
35
35
  import type {CanonicalTileID} from '../types/tile_id';
36
36
  import type {FeatureDistanceData} from '../feature_filter/index';
37
37
  import type {ConfigOptions} from '../types/config_options';
38
+ import type {ImageId} from './types/image_id';
38
39
 
39
40
  export interface Feature {
40
41
  readonly type: 0 | 1 | 2 | 3 | 'Unknown' | 'Point' | 'LineString' | 'Polygon';
@@ -88,7 +89,7 @@ export class StyleExpression {
88
89
  feature?: Feature,
89
90
  featureState?: FeatureState,
90
91
  canonical?: CanonicalTileID,
91
- availableImages?: Array<string>,
92
+ availableImages?: ImageId[],
92
93
  formattedSection?: FormattedSection,
93
94
  featureTileCoord?: Point,
94
95
  featureDistanceData?: FeatureDistanceData,
@@ -110,7 +111,7 @@ export class StyleExpression {
110
111
  feature?: Feature,
111
112
  featureState?: FeatureState,
112
113
  canonical?: CanonicalTileID,
113
- availableImages?: Array<string>,
114
+ availableImages?: ImageId[],
114
115
  formattedSection?: FormattedSection,
115
116
  featureTileCoord?: Point,
116
117
  featureDistanceData?: FeatureDistanceData,
@@ -202,7 +203,7 @@ export class ZoomConstantExpression<Kind extends EvaluationKind> {
202
203
  feature?: Feature,
203
204
  featureState?: FeatureState,
204
205
  canonical?: CanonicalTileID,
205
- availableImages?: Array<string>,
206
+ availableImages?: ImageId[],
206
207
  formattedSection?: FormattedSection,
207
208
  ): any {
208
209
  return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
@@ -213,7 +214,7 @@ export class ZoomConstantExpression<Kind extends EvaluationKind> {
213
214
  feature?: Feature,
214
215
  featureState?: FeatureState,
215
216
  canonical?: CanonicalTileID,
216
- availableImages?: Array<string>,
217
+ availableImages?: ImageId[],
217
218
  formattedSection?: FormattedSection,
218
219
  ): any {
219
220
  return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
@@ -247,7 +248,7 @@ export class ZoomDependentExpression<Kind extends EvaluationKind> {
247
248
  feature?: Feature,
248
249
  featureState?: FeatureState,
249
250
  canonical?: CanonicalTileID,
250
- availableImages?: Array<string>,
251
+ availableImages?: ImageId[],
251
252
  formattedSection?: FormattedSection,
252
253
  ): any {
253
254
  return this._styleExpression.evaluateWithoutErrorHandling(globals, feature, featureState, canonical, availableImages, formattedSection);
@@ -258,7 +259,7 @@ export class ZoomDependentExpression<Kind extends EvaluationKind> {
258
259
  feature?: Feature,
259
260
  featureState?: FeatureState,
260
261
  canonical?: CanonicalTileID,
261
- availableImages?: Array<string>,
262
+ availableImages?: ImageId[],
262
263
  formattedSection?: FormattedSection,
263
264
  ): any {
264
265
  return this._styleExpression.evaluate(globals, feature, featureState, canonical, availableImages, formattedSection);
@@ -281,7 +282,7 @@ export type ConstantExpression = {
281
282
  feature?: Feature,
282
283
  featureState?: FeatureState,
283
284
  canonical?: CanonicalTileID,
284
- availableImages?: Array<string>,
285
+ availableImages?: ImageId[],
285
286
  ) => any;
286
287
  };
287
288
 
@@ -296,7 +297,7 @@ export type SourceExpression = {
296
297
  feature?: Feature,
297
298
  featureState?: FeatureState,
298
299
  canonical?: CanonicalTileID,
299
- availableImages?: Array<string>,
300
+ availableImages?: ImageId[],
300
301
  formattedSection?: FormattedSection,
301
302
  ) => any;
302
303
  };
@@ -310,7 +311,7 @@ export type CameraExpression = {
310
311
  feature?: Feature,
311
312
  featureState?: FeatureState,
312
313
  canonical?: CanonicalTileID,
313
- availableImages?: Array<string>,
314
+ availableImages?: ImageId[],
314
315
  ) => any;
315
316
  readonly interpolationFactor: (input: number, lower: number, upper: number) => number;
316
317
  zoomStops: Array<number>;
@@ -328,7 +329,7 @@ export interface CompositeExpression {
328
329
  feature?: Feature,
329
330
  featureState?: FeatureState,
330
331
  canonical?: CanonicalTileID,
331
- availableImages?: Array<string>,
332
+ availableImages?: ImageId[],
332
333
  formattedSection?: FormattedSection,
333
334
  ) => any;
334
335
  readonly interpolationFactor: (input: number, lower: number, upper: number) => number;
@@ -31,8 +31,11 @@ export default class Formatted {
31
31
 
32
32
  isEmpty(): boolean {
33
33
  if (this.sections.length === 0) return true;
34
- return !this.sections.some(section => section.text.length !== 0 ||
35
- (section.image && section.image.namePrimary));
34
+ return !this.sections.some(section => {
35
+ if (section.text.length !== 0) return true;
36
+ if (!section.image) return false;
37
+ return section.image.hasPrimary();
38
+ });
36
39
  }
37
40
 
38
41
  static factory(text: Formatted | string): Formatted {
@@ -52,7 +55,8 @@ export default class Formatted {
52
55
  const serialized: Array<unknown> = ["format"];
53
56
  for (const section of this.sections) {
54
57
  if (section.image) {
55
- serialized.push(["image", section.image.namePrimary]);
58
+ const primaryId = section.image.getPrimary().id.toString();
59
+ serialized.push(['image', primaryId]);
56
60
  continue;
57
61
  }
58
62
  serialized.push(section.text);
@@ -0,0 +1,61 @@
1
+ import type {Brand} from '../../types/brand';
2
+
3
+ export type ImageIdSpec = {
4
+ name: string;
5
+ iconsetId?: string;
6
+ };
7
+
8
+ /**
9
+ * `StringifiedImageId` is a stringified version of the `ImageId`.
10
+ *
11
+ * @private
12
+ */
13
+ export type StringifiedImageId = Brand<string, 'ImageId'>;
14
+
15
+ /**
16
+ * `ImageId` is a reference to an {@link ImageVariant} in the sprite or iconset.
17
+ *
18
+ * @private
19
+ */
20
+ export class ImageId {
21
+ name: string;
22
+ iconsetId?: string;
23
+
24
+ constructor(id: string | ImageId | ImageIdSpec) {
25
+ if (typeof id === 'string') {
26
+ this.name = id;
27
+ } else {
28
+ this.name = id.name;
29
+ this.iconsetId = id.iconsetId;
30
+ }
31
+ }
32
+
33
+ static from(id: string | ImageId | ImageIdSpec): ImageId {
34
+ return new ImageId(id);
35
+ }
36
+
37
+ static toString(id: ImageId | ImageIdSpec): StringifiedImageId {
38
+ return JSON.stringify({name: id.name, iconsetId: id.iconsetId}) as StringifiedImageId;
39
+ }
40
+
41
+ static parse(str: StringifiedImageId): ImageId | null {
42
+ try {
43
+ const {name, iconsetId} = JSON.parse(str);
44
+ return new ImageId({name, iconsetId});
45
+ } catch (e) {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ static isEqual(a: ImageId | ImageIdSpec, b: ImageId | ImageIdSpec): boolean {
51
+ return a.name === b.name && a.iconsetId === b.iconsetId;
52
+ }
53
+
54
+ toString(): StringifiedImageId {
55
+ return JSON.stringify({name: this.name, iconsetId: this.iconsetId}) as StringifiedImageId;
56
+ }
57
+
58
+ serialize(): ImageIdSpec {
59
+ return {name: this.name, iconsetId: this.iconsetId};
60
+ }
61
+ }
@@ -0,0 +1,79 @@
1
+ import {ImageId} from './image_id';
2
+
3
+ import type Color from '../../util/color';
4
+ import type {Brand} from '../../types/brand';
5
+ import type {ImageIdSpec} from './image_id';
6
+
7
+ /**
8
+ * `StringifiedImageVariant` is a stringified version of the `ImageVariant`.
9
+ *
10
+ * @private
11
+ */
12
+ export type StringifiedImageVariant = Brand<string, 'ImageVariant'>;
13
+
14
+ /**
15
+ * {@link ImageVariant} rasterization options.
16
+ *
17
+ * @private
18
+ */
19
+ export type RasterizationOptions = {
20
+ params?: Record<string, Color>;
21
+ transform?: DOMMatrix;
22
+ }
23
+
24
+ /**
25
+ * `ImageVariant` is a component of {@link ResolvedImage}
26
+ * that represents either the primary or secondary image
27
+ * along with its rendering configuration.
28
+ *
29
+ * @private
30
+ */
31
+ export class ImageVariant {
32
+ id: ImageId;
33
+ options: RasterizationOptions;
34
+
35
+ constructor(id: string | ImageIdSpec, options: RasterizationOptions = {}) {
36
+ this.id = ImageId.from(id);
37
+ this.options = Object.assign({}, options);
38
+
39
+ if (!options.transform) {
40
+ this.options.transform = new DOMMatrix([1, 0, 0, 1, 0, 0]);
41
+ } else {
42
+ const {a, b, c, d, e, f} = options.transform;
43
+ this.options.transform = new DOMMatrix([a, b, c, d, e, f]);
44
+ }
45
+ }
46
+
47
+ toString(): StringifiedImageVariant {
48
+ const {a, b, c, d, e, f} = this.options.transform;
49
+
50
+ const serialized = {
51
+ name: this.id.name,
52
+ iconsetId: this.id.iconsetId,
53
+ params: this.options.params,
54
+ transform: {a, b, c, d, e, f},
55
+ };
56
+
57
+ return JSON.stringify(serialized) as StringifiedImageVariant;
58
+ }
59
+
60
+ static parse(str: StringifiedImageVariant): ImageVariant | null {
61
+ let name, iconsetId, params, transform;
62
+
63
+ try {
64
+ ({name, iconsetId, params, transform} = JSON.parse(str) || {});
65
+ } catch (e) {
66
+ return null;
67
+ }
68
+
69
+ if (!name) return null;
70
+
71
+ const {a, b, c, d, e, f} = transform || {};
72
+ return new ImageVariant({name, iconsetId}, {params, transform: new DOMMatrix([a, b, c, d, e, f])});
73
+ }
74
+
75
+ scaleSelf(factor: number): this {
76
+ this.options.transform.scaleSelf(factor);
77
+ return this;
78
+ }
79
+ }