Skip to content

Commit 2e2477f

Browse files
zbigniewmatysek-tomtomstanislawpuda-tomtomlouwers
authored
global-state expression (#1044)
* add global-state expression * self review * code hygiene * add integration tests * add unit tests * update sdk-support * use getOwn * add state root property * state property validation * validate state keys existance when global-state is used * improve expression parsing * refactor * temporarily build for testing purposes * improve docs * schema support * cleanout * export schema validator * minor fix * cleanout * update types * docs * minor fixes * add schema ts types * cleanout * fixes + increase test coverage * review fixes * make default optional in the ts types * docs update * docs fixes * review fixes * minor fix * review fixes * remove annotations section * remove validation * cleanout * add changelog * Review fix: update src/reference/v8.json Co-authored-by: Bart Louwers <[email protected]> * Review fix: update src/reference/v8.json Co-authored-by: Bart Louwers <[email protected]> * fix links --------- Co-authored-by: stanislawpuda-tomtom <[email protected]> Co-authored-by: Bart Louwers <[email protected]> Co-authored-by: Stanislaw Puda <[email protected]>
1 parent e48ac7a commit 2e2477f

File tree

20 files changed

+271
-11
lines changed

20 files changed

+271
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### ✨ Features and improvements
44
- `glyphs` is now optional even if a symbol layer specifies `text-field`; if it is unset, `text-font` is interpreted as a fallback font list ([#1068](https://github.com/maplibre/maplibre-style-spec/pull/1068))
55
- `hillshade` layer now supports multiple methods, and the `multidirectional` method supports array values for illumination properties ([#1088](https://github.com/maplibre/maplibre-style-spec/pull/1088))
6+
- Add `global-state` expression and `state` root property ([#1044](https://github.com/maplibre/maplibre-style-spec/pull/1044)).
67

78
### 🐞 Bug fixes
89
- Fix RuntimeError class, make it inherited from Error ([#983](https://github.com/maplibre/maplibre-style-spec/issues/983))

build/generate-docs.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type JsonSdkSupport = {
2020
type JsonObject = {
2121
required?: boolean;
2222
units?: string;
23-
default?: string | number | boolean;
23+
default?: string | number | boolean | {};
2424
type: string;
2525
doc: string;
2626
requires?: any[];
@@ -58,7 +58,6 @@ function topicElement(key: string, value: JsonObject): boolean {
5858
key !== 'sprite' &&
5959
key !== 'layers' &&
6060
key !== 'sources';
61-
6261
}
6362

6463
/**
@@ -233,7 +232,7 @@ function convertPropertyToMarkdown(key: string, value: JsonObject, keyPrefix = '
233232
markdown += `Units in ${value.units}. `;
234233
}
235234
if (value.default !== undefined) {
236-
markdown += `Defaults to \`${value.default}\`. `;
235+
markdown += `Defaults to \`${JSON.stringify(value.default)}\`. `;
237236
}
238237
if (value.requires) {
239238
markdown += requiresToMarkdown(value.requires);

build/generate-style-spec.ts

100644100755
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ function propertyType(property) {
3939
return '{[_: string]: SourceSpecification}';
4040
case 'projection:':
4141
return 'ProjectionSpecification';
42+
case 'state':
43+
return 'StateSpecification';
4244
case 'numberArray':
4345
return 'NumberArraySpecification';
4446
case 'colorArray':
@@ -321,6 +323,13 @@ export type DataDrivenPropertyValueSpecification<T> =
321323
| CompositeFunctionSpecification<T>
322324
| ExpressionSpecification;
323325
326+
export type SchemaSpecification = {
327+
default?: unknown
328+
};
329+
330+
// State
331+
export type StateSpecification = Record<string, SchemaSpecification>;
332+
324333
${objectDeclaration('StyleSpecification', spec.$root)}
325334
326335
${objectDeclaration('LightSpecification', spec.light)}

src/diff.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,16 @@ describe('diff', () => {
262262
} as StyleSpecification)).toEqual([]);
263263
});
264264

265+
test('set state', () => {
266+
expect(diff({
267+
state: {foo: 1}
268+
} as any as StyleSpecification, {
269+
state: {foo: 2}
270+
} as any as StyleSpecification)).toEqual([
271+
{command: 'setGlobalState', args: [{foo: 2}]}
272+
]);
273+
});
274+
265275
test('set center', () => {
266276
expect(diff({
267277
center: [0, 0]

src/diff.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
import {GeoJSONSourceSpecification, LayerSpecification, LightSpecification, ProjectionSpecification, SkySpecification, SourceSpecification, SpriteSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from './types.g';
2+
import {GeoJSONSourceSpecification, LayerSpecification, LightSpecification, ProjectionSpecification, SkySpecification, SourceSpecification, SpriteSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification, StateSpecification} from './types.g';
33
import {deepEqual} from './util/deep_equal';
44

55
/**
@@ -31,6 +31,7 @@ export type DiffOperationsMap = {
3131
'setTerrain': [TerrainSpecification];
3232
'setSky': [SkySpecification];
3333
'setProjection': [ProjectionSpecification];
34+
'setGlobalState': [StateSpecification];
3435
}
3536

3637
export type DiffOperations = keyof DiffOperationsMap;
@@ -280,6 +281,9 @@ export function diff(before: StyleSpecification, after: StyleSpecification): Dif
280281
if (!deepEqual(before.center, after.center)) {
281282
commands.push({command: 'setCenter', args: [after.center]});
282283
}
284+
if (!deepEqual(before.state, after.state)) {
285+
commands.push({command: 'setGlobalState', args: [after.state]});
286+
}
283287
if (!deepEqual(before.centerAltitude, after.centerAltitude)) {
284288
commands.push({command: 'setCenterAltitude', args: [after.centerAltitude]});
285289
}

src/expression/compound_expression.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {Assertion} from './definitions/assertion';
2121
import {Coercion} from './definitions/coercion';
2222
import {Var} from './definitions/var';
2323
import {Distance} from './definitions/distance';
24+
import {GlobalState} from './definitions/global_state';
2425

2526
import type {Expression, ExpressionRegistry} from './expression';
2627
import type {Value} from './values';
@@ -666,6 +667,8 @@ function isExpressionConstant(expression: Expression) {
666667
return false;
667668
} else if (expression instanceof Distance) {
668669
return false;
670+
} else if (expression instanceof GlobalState) {
671+
return false;
669672
}
670673

671674
const isTypeAnnotation = expression instanceof Coercion ||
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {Type, ValueType} from '../types';
2+
import type {Expression} from '../expression';
3+
import {ParsingContext} from '../parsing_context';
4+
import {EvaluationContext} from '../evaluation_context';
5+
import {getOwn} from '../../util/get_own';
6+
7+
export class GlobalState implements Expression {
8+
type: Type;
9+
key: string;
10+
11+
constructor(key: string) {
12+
this.type = ValueType;
13+
this.key = key;
14+
}
15+
16+
static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
17+
if (args.length !== 2) {
18+
return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`) as null;
19+
}
20+
21+
const key = args[1];
22+
23+
if (key === undefined || key === null) {
24+
return context.error('Global state property must be defined.') as null;
25+
}
26+
27+
if (typeof key !== 'string') {
28+
return context.error(`Global state property must be string, but found ${typeof args[1]} instead.`) as null;
29+
}
30+
31+
return new GlobalState(key);
32+
}
33+
34+
evaluate(ctx: EvaluationContext) {
35+
const globalState = ctx.globals?.globalState;
36+
37+
if (!globalState || Object.keys(globalState).length === 0) return null;
38+
39+
return getOwn(globalState, this.key);
40+
}
41+
42+
eachChild() {}
43+
44+
outputDefined() {
45+
return false;
46+
}
47+
}

src/expression/definitions/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {ImageExpression} from './image';
2828
import {Length} from './length';
2929
import {Within} from './within';
3030
import {Distance} from './distance';
31+
import {GlobalState} from './global_state';
3132

3233
import type {ExpressionRegistry} from '../expression';
3334

@@ -68,5 +69,6 @@ export const expressions: ExpressionRegistry = {
6869
'to-string': Coercion,
6970
'var': Var,
7071
'within': Within,
71-
'distance': Distance
72+
'distance': Distance,
73+
'global-state': GlobalState
7274
};

src/expression/expression.test.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,6 @@ describe('slice expression', () => {
632632
});
633633

634634
describe('projection expression', () => {
635-
636635
test('step', () => {
637636
const response = createExpression(['step', ['zoom'], 'vertical-perspective', 10, 'mercator']);
638637

@@ -643,7 +642,7 @@ describe('projection expression', () => {
643642
} else {
644643
throw new Error('Failed to parse Step expression');
645644
}
646-
})
645+
});
647646

648647
test('step array', () => {
649648
const response = createExpression(['step', ['zoom'], ['literal', ['vertical-perspective', 'mercator', 0.5]], 10, 'mercator'], v8.projection.type as StylePropertySpecification);
@@ -655,7 +654,7 @@ describe('projection expression', () => {
655654
} else {
656655
throw new Error('Failed to parse Step expression');
657656
}
658-
})
657+
});
659658

660659
test('interpolate', () => {
661660
const response = createExpression(['interpolate', ['linear'], ['zoom'], 8, 'vertical-perspective', 10, 'mercator'], v8.projection.type as StylePropertySpecification);
@@ -667,7 +666,7 @@ describe('projection expression', () => {
667666
} else {
668667
throw new Error('Failed to parse Interpolate expression');
669668
}
670-
})
669+
});
671670

672671
test('interpolate numberArray', () => {
673672
const response = createExpression(['interpolate', ['linear'], ['zoom'], 8, ['literal', [2,3]], 10, ['literal', [4,5]]], {
@@ -753,6 +752,34 @@ describe('projection expression', () => {
753752
} else {
754753
throw new Error('Failed to parse Interpolate expression');
755754
}
756-
})
755+
});
756+
});
757757

758+
describe('global-state expression', () => {
759+
test('requires a property argument', () => {
760+
const response = createExpression(['global-state']);
761+
expect(response.result).toBe('error');
762+
expect(response.value[0]).toBeInstanceOf(ExpressionParsingError);
763+
expect((response.value[0] as ExpressionParsingError).message).toBe('Expected 1 argument, but found 0 instead.');
764+
});
765+
test('requires a string as the property argument', () => {
766+
const response = createExpression(['global-state', true]);
767+
expect(response.result).toBe('error');
768+
expect(response.value[0]).toBeInstanceOf(ExpressionParsingError);
769+
expect((response.value[0] as ExpressionParsingError).message).toBe('Global state property must be string, but found boolean instead.');
770+
});
771+
test('rejects a second argument', () => {
772+
const response = createExpression(['global-state', 'foo', 'bar']);
773+
expect(response.result).toBe('error');
774+
expect(response.value[0]).toBeInstanceOf(ExpressionParsingError);
775+
expect((response.value[0] as ExpressionParsingError).message).toBe('Expected 1 argument, but found 2 instead.');
776+
});
777+
test('evaluates a global state property', () => {
778+
const response = createExpression(['global-state', 'foo']);
779+
if (response.result === 'success') {
780+
expect(response.value.evaluate({globalState: {foo: 'bar'}, zoom: 0}, {} as Feature)).toBe('bar');
781+
} else {
782+
throw new Error('Failed to parse GlobalState expression');
783+
}
784+
});
758785
});

src/expression/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export type GlobalProperties = Readonly<{
6161
lineProgress?: number;
6262
isSupportedScript?: (_: string) => boolean;
6363
accumulated?: Value;
64+
globalState?: Record<string, any>;
6465
}>;
6566

6667
export class StyleExpression {

0 commit comments

Comments
 (0)