Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6bb780f
add global-state expression
zbigniewmatysek-tomtom Mar 11, 2025
b088f0e
self review
zbigniewmatysek-tomtom Mar 11, 2025
85616b8
Merge branch 'main' of github.com:tomtom-forks/maplibre-style-spec in…
zbigniewmatysek-tomtom Mar 11, 2025
c9724a6
code hygiene
zbigniewmatysek-tomtom Mar 11, 2025
c6034a0
add integration tests
zbigniewmatysek-tomtom Mar 12, 2025
bbb988c
add unit tests
zbigniewmatysek-tomtom Mar 12, 2025
7987b5c
update sdk-support
zbigniewmatysek-tomtom Mar 12, 2025
d2cd15f
use getOwn
zbigniewmatysek-tomtom Mar 12, 2025
2031d0e
add state root property
zbigniewmatysek-tomtom Mar 13, 2025
6af796a
state property validation
zbigniewmatysek-tomtom Mar 13, 2025
1dd679e
validate state keys existance when global-state is used
zbigniewmatysek-tomtom Mar 14, 2025
a776b4c
improve expression parsing
zbigniewmatysek-tomtom Mar 14, 2025
377cc6f
refactor
zbigniewmatysek-tomtom Mar 17, 2025
2c64ac7
temporarily build for testing purposes
zbigniewmatysek-tomtom Mar 17, 2025
dba4c4f
improve docs
zbigniewmatysek-tomtom Mar 18, 2025
cd861b9
schema support
zbigniewmatysek-tomtom Mar 27, 2025
7e70626
cleanout
zbigniewmatysek-tomtom Mar 27, 2025
b98c7bd
export schema validator
zbigniewmatysek-tomtom Mar 28, 2025
ed13aa9
minor fix
zbigniewmatysek-tomtom Mar 28, 2025
c0e90a3
cleanout
zbigniewmatysek-tomtom Mar 28, 2025
e476181
update types
zbigniewmatysek-tomtom Mar 28, 2025
cd68ddb
docs
zbigniewmatysek-tomtom Mar 31, 2025
3bc66a0
minor fixes
zbigniewmatysek-tomtom Mar 31, 2025
830552b
add schema ts types
zbigniewmatysek-tomtom Apr 1, 2025
915a264
cleanout
zbigniewmatysek-tomtom Apr 1, 2025
bdab151
fixes + increase test coverage
zbigniewmatysek-tomtom Apr 1, 2025
fe0fe42
review fixes
zbigniewmatysek-tomtom Apr 1, 2025
9dd0595
make default optional in the ts types
zbigniewmatysek-tomtom Apr 1, 2025
09f9767
docs update
zbigniewmatysek-tomtom Apr 1, 2025
5281217
docs fixes
zbigniewmatysek-tomtom Apr 1, 2025
6f3ffc8
review fixes
zbigniewmatysek-tomtom Apr 2, 2025
1dfc05f
minor fix
zbigniewmatysek-tomtom Apr 2, 2025
2ff83bc
Merge branch 'main' into NAV-171460-global-state-expression
zbigniewmatysek-tomtom Apr 2, 2025
3d356fb
review fixes
zbigniewmatysek-tomtom Apr 2, 2025
6300620
Merge branch 'NAV-171460-global-state-expression' of github.com:tomto…
zbigniewmatysek-tomtom Apr 2, 2025
7a26fe1
remove annotations section
zbigniewmatysek-tomtom Apr 2, 2025
81d498d
remove validation
zbigniewmatysek-tomtom Apr 16, 2025
9c042f5
Merge branch 'main' into NAV-171460-global-state-expression
zbigniewmatysek-tomtom Apr 16, 2025
c15c12e
cleanout
zbigniewmatysek-tomtom Apr 16, 2025
ba2a103
Merge branch 'NAV-171460-global-state-expression' of github.com:tomto…
zbigniewmatysek-tomtom Apr 16, 2025
4e591d2
add changelog
zbigniewmatysek-tomtom Apr 16, 2025
ce515fb
Review fix: update src/reference/v8.json
stanislawpuda-tomtom Apr 22, 2025
205d379
Review fix: update src/reference/v8.json
stanislawpuda-tomtom Apr 22, 2025
82b028d
Merge branch 'main' of https://github.com/tomtom-forks/maplibre-style…
stanislawpuda-tomtom Apr 22, 2025
2a27194
fix links
stanislawpuda-tomtom Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### ✨ Features and improvements
- `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))
- `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))
- Add `global-state` expression and `state` root property ([#1044](https://github.com/maplibre/maplibre-style-spec/pull/1044)).

### 🐞 Bug fixes
- Fix RuntimeError class, make it inherited from Error ([#983](https://github.com/maplibre/maplibre-style-spec/issues/983))
Expand Down
5 changes: 2 additions & 3 deletions build/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type JsonSdkSupport = {
type JsonObject = {
required?: boolean;
units?: string;
default?: string | number | boolean;
default?: string | number | boolean | {};
type: string;
doc: string;
requires?: any[];
Expand Down Expand Up @@ -58,7 +58,6 @@ function topicElement(key: string, value: JsonObject): boolean {
key !== 'sprite' &&
key !== 'layers' &&
key !== 'sources';

}

/**
Expand Down Expand Up @@ -233,7 +232,7 @@ function convertPropertyToMarkdown(key: string, value: JsonObject, keyPrefix = '
markdown += `Units in ${value.units}. `;
}
if (value.default !== undefined) {
markdown += `Defaults to \`${value.default}\`. `;
markdown += `Defaults to \`${JSON.stringify(value.default)}\`. `;
}
if (value.requires) {
markdown += requiresToMarkdown(value.requires);
Expand Down
9 changes: 9 additions & 0 deletions build/generate-style-spec.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ function propertyType(property) {
return '{[_: string]: SourceSpecification}';
case 'projection:':
return 'ProjectionSpecification';
case 'state':
return 'StateSpecification';
case 'numberArray':
return 'NumberArraySpecification';
case 'colorArray':
Expand Down Expand Up @@ -321,6 +323,13 @@ export type DataDrivenPropertyValueSpecification<T> =
| CompositeFunctionSpecification<T>
| ExpressionSpecification;

export type SchemaSpecification = {
default?: unknown
};

// State
export type StateSpecification = Record<string, SchemaSpecification>;

${objectDeclaration('StyleSpecification', spec.$root)}

${objectDeclaration('LightSpecification', spec.light)}
Expand Down
10 changes: 10 additions & 0 deletions src/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,16 @@ describe('diff', () => {
} as StyleSpecification)).toEqual([]);
});

test('set state', () => {
expect(diff({
state: {foo: 1}
} as any as StyleSpecification, {
state: {foo: 2}
} as any as StyleSpecification)).toEqual([
{command: 'setGlobalState', args: [{foo: 2}]}
]);
});

test('set center', () => {
expect(diff({
center: [0, 0]
Expand Down
6 changes: 5 additions & 1 deletion src/diff.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

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

/**
Expand Down Expand Up @@ -31,6 +31,7 @@ export type DiffOperationsMap = {
'setTerrain': [TerrainSpecification];
'setSky': [SkySpecification];
'setProjection': [ProjectionSpecification];
'setGlobalState': [StateSpecification];
}

export type DiffOperations = keyof DiffOperationsMap;
Expand Down Expand Up @@ -280,6 +281,9 @@ export function diff(before: StyleSpecification, after: StyleSpecification): Dif
if (!deepEqual(before.center, after.center)) {
commands.push({command: 'setCenter', args: [after.center]});
}
if (!deepEqual(before.state, after.state)) {
commands.push({command: 'setGlobalState', args: [after.state]});
}
if (!deepEqual(before.centerAltitude, after.centerAltitude)) {
commands.push({command: 'setCenterAltitude', args: [after.centerAltitude]});
}
Expand Down
3 changes: 3 additions & 0 deletions src/expression/compound_expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {Assertion} from './definitions/assertion';
import {Coercion} from './definitions/coercion';
import {Var} from './definitions/var';
import {Distance} from './definitions/distance';
import {GlobalState} from './definitions/global_state';

import type {Expression, ExpressionRegistry} from './expression';
import type {Value} from './values';
Expand Down Expand Up @@ -666,6 +667,8 @@ function isExpressionConstant(expression: Expression) {
return false;
} else if (expression instanceof Distance) {
return false;
} else if (expression instanceof GlobalState) {
return false;
}

const isTypeAnnotation = expression instanceof Coercion ||
Expand Down
47 changes: 47 additions & 0 deletions src/expression/definitions/global_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Type, ValueType} from '../types';
import type {Expression} from '../expression';
import {ParsingContext} from '../parsing_context';
import {EvaluationContext} from '../evaluation_context';
import {getOwn} from '../../util/get_own';

export class GlobalState implements Expression {
type: Type;
key: string;

constructor(key: string) {
this.type = ValueType;
this.key = key;
}

static parse(args: ReadonlyArray<unknown>, context: ParsingContext): Expression {
if (args.length !== 2) {
return context.error(`Expected 1 argument, but found ${args.length - 1} instead.`) as null;
}

const key = args[1];

if (key === undefined || key === null) {
return context.error('Global state property must be defined.') as null;
}

if (typeof key !== 'string') {
return context.error(`Global state property must be string, but found ${typeof args[1]} instead.`) as null;
}

return new GlobalState(key);
}

evaluate(ctx: EvaluationContext) {
const globalState = ctx.globals?.globalState;

if (!globalState || Object.keys(globalState).length === 0) return null;

return getOwn(globalState, this.key);
}

eachChild() {}

outputDefined() {
return false;
}
}
4 changes: 3 additions & 1 deletion src/expression/definitions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {ImageExpression} from './image';
import {Length} from './length';
import {Within} from './within';
import {Distance} from './distance';
import {GlobalState} from './global_state';

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

Expand Down Expand Up @@ -68,5 +69,6 @@ export const expressions: ExpressionRegistry = {
'to-string': Coercion,
'var': Var,
'within': Within,
'distance': Distance
'distance': Distance,
'global-state': GlobalState
};
37 changes: 32 additions & 5 deletions src/expression/expression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,6 @@ describe('slice expression', () => {
});

describe('projection expression', () => {

test('step', () => {
const response = createExpression(['step', ['zoom'], 'vertical-perspective', 10, 'mercator']);

Expand All @@ -643,7 +642,7 @@ describe('projection expression', () => {
} else {
throw new Error('Failed to parse Step expression');
}
})
});

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

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

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

describe('global-state expression', () => {
test('requires a property argument', () => {
const response = createExpression(['global-state']);
expect(response.result).toBe('error');
expect(response.value[0]).toBeInstanceOf(ExpressionParsingError);
expect((response.value[0] as ExpressionParsingError).message).toBe('Expected 1 argument, but found 0 instead.');
});
test('requires a string as the property argument', () => {
const response = createExpression(['global-state', true]);
expect(response.result).toBe('error');
expect(response.value[0]).toBeInstanceOf(ExpressionParsingError);
expect((response.value[0] as ExpressionParsingError).message).toBe('Global state property must be string, but found boolean instead.');
});
test('rejects a second argument', () => {
const response = createExpression(['global-state', 'foo', 'bar']);
expect(response.result).toBe('error');
expect(response.value[0]).toBeInstanceOf(ExpressionParsingError);
expect((response.value[0] as ExpressionParsingError).message).toBe('Expected 1 argument, but found 2 instead.');
});
test('evaluates a global state property', () => {
const response = createExpression(['global-state', 'foo']);
if (response.result === 'success') {
expect(response.value.evaluate({globalState: {foo: 'bar'}, zoom: 0}, {} as Feature)).toBe('bar');
} else {
throw new Error('Failed to parse GlobalState expression');
}
});
});
1 change: 1 addition & 0 deletions src/expression/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export type GlobalProperties = Readonly<{
lineProgress?: number;
isSupportedScript?: (_: string) => boolean;
accumulated?: Value;
globalState?: Record<string, any>;
}>;

export class StyleExpression {
Expand Down
3 changes: 2 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ function validSchema(k, v, obj, ref, version, kind) {
'colorArray',
'variableAnchorOffsetCollection',
'sprite',
'projectionDefinition'
'projectionDefinition',
'state'
]);
const keys = [
'default',
Expand Down
38 changes: 38 additions & 0 deletions src/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@
}
}
},
"state": {
"type": "state",
"default": {},
"doc": "An object used to define default values when using the [`global-state`](https://maplibre.org/maplibre-style-spec/expressions/#global-state) expression.",
"example": {
"chargerType": {
"default": ["CCS", "CHAdeMO", "Type2"]
},
"minPreferredChargingSpeed": {
"default": 50
}
},
"sdk-support": {
"basic functionality": {
"js": "https://github.com/maplibre/maplibre-gl-js/issues/4964",
"android": "https://github.com/maplibre/maplibre-native/issues/3302",
"ios": "https://github.com/maplibre/maplibre-native/issues/3302"
}
}
},
"light": {
"type": "light",
"doc": "The global light source.",
Expand Down Expand Up @@ -3308,6 +3328,24 @@
}
}
},
"global-state": {
"doc": "Retrieves a property value from global state that can be set with platform-specific APIs. Defaults can be provided using the [`state`](https://maplibre.org/maplibre-style-spec/root/#state) root property. Returns `null` if no value nor default value is set for the retrieved property.",
"group": "Lookup",
"example": {
"syntax": {
"method": ["string"],
"result": "value"
},
"value": ["global-state", "someProperty"]
},
"sdk-support": {
"basic functionality": {
"js": "https://github.com/maplibre/maplibre-gl-js/issues/4964",
"android": "https://github.com/maplibre/maplibre-native/issues/3302",
"ios": "https://github.com/maplibre/maplibre-native/issues/3302"
}
}
},
"number-format": {
"doc": "Converts the input number into a string representation using the providing formatting rules. If set, the `locale` argument specifies the locale to use, as a BCP 47 language tag. If set, the `currency` argument specifies an ISO 4217 code to use for currency-style formatting. If set, the `min-fraction-digits` and `max-fraction-digits` arguments specify the minimum and maximum number of fractional digits to include.\n\n - [Display HTML clusters with custom properties](https://maplibre.org/maplibre-gl-js/docs/examples/cluster-html/)",
"example": {
Expand Down
5 changes: 5 additions & 0 deletions src/util/is_object_literal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function isObjectLiteral(
anything: unknown
): anything is Record<string, unknown> {
return Boolean(anything) && anything.constructor === Object;
}
2 changes: 2 additions & 0 deletions src/validate/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {validateSprite} from './validate_sprite';
import {ValidationError} from '../error/validation_error';
import {validateProjection} from './validate_projection';
import {validateProjectionDefinition} from './validate_projectiondefinition';
import {validateState} from './validate_state';

const VALIDATORS = {
'*'() {
Expand Down Expand Up @@ -59,6 +60,7 @@ const VALIDATORS = {
'colorArray': validateColorArray,
'variableAnchorOffsetCollection': validateVariableAnchorOffsetCollection,
'sprite': validateSprite,
'state': validateState
};

/**
Expand Down
9 changes: 9 additions & 0 deletions src/validate/validate_state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {validateState} from './validate_state';

describe('Validate state', () => {
test('Should return error if type is not an object', () => {
const errors = validateState({key: 'state', value: 3});
expect(errors).toHaveLength(1);
expect(errors[0].message).toBe('state: object expected, number found');
});
});
20 changes: 20 additions & 0 deletions src/validate/validate_state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {ValidationError} from '../error/validation_error';
import {getType} from '../util/get_type';
import {isObjectLiteral} from '../util/is_object_literal';

interface ValidateStateOptions {
key: 'state';
value: unknown;
}

export function validateState(options: ValidateStateOptions) {
if (!isObjectLiteral(options.value)) {
return [
new ValidationError(
options.key,
options.value,
`object expected, ${getType(options.value)} found`
),
];
}
}
2 changes: 2 additions & 0 deletions src/validate_style.min.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {validateSource} from './validate/validate_source';
import {validateLight} from './validate/validate_light';
import {validateSky} from './validate/validate_sky';
import {validateTerrain} from './validate/validate_terrain';
import {validateState} from './validate/validate_state';
import {validateLayer} from './validate/validate_layer';
import {validateFilter} from './validate/validate_filter';
import {validatePaintProperty} from './validate/validate_paint_property';
Expand Down Expand Up @@ -66,6 +67,7 @@ validateStyleMin.glyphs = wrapCleanErrors(injectValidateSpec(validateGlyphsUrl))
validateStyleMin.light = wrapCleanErrors(injectValidateSpec(validateLight));
validateStyleMin.sky = wrapCleanErrors(injectValidateSpec(validateSky));
validateStyleMin.terrain = wrapCleanErrors(injectValidateSpec(validateTerrain));
validateStyleMin.state = wrapCleanErrors(injectValidateSpec(validateState));
validateStyleMin.layer = wrapCleanErrors(injectValidateSpec(validateLayer));
validateStyleMin.filter = wrapCleanErrors(injectValidateSpec(validateFilter));
validateStyleMin.paintProperty = wrapCleanErrors(injectValidateSpec(validatePaintProperty));
Expand Down
Loading