Skip to content

Commit a6e4a23

Browse files
meliteleNatalia KowalczykHarelM
authored
Fix for global-state doesn't work with icon-size/text-size layout property of symbol layer (#6321)
* render tests to reproduce issue of `icon-size` and `text-size` properties ignoring `global-state` #6308 * style layer `update` has been expecting an object as `parameters` for 7 years * add reference to `global-state` to style layer and use it when recalculating layers * add `global-state` reference to all layout properties at the point of evaluation * changelog --------- Co-authored-by: Natalia Kowalczyk <melitele@code42day.com> Co-authored-by: Harel M <harel.mazor@gmail.com>
1 parent 51edebf commit a6e4a23

13 files changed

Lines changed: 230 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- Fix evaluating `global-state` in paint `...-pattern` properties ([6301](https://github.com/maplibre/maplibre-gl-js/pull/6301))
99
- Fix evaluation of `text-color` when using `format` within `step` ([#5833](https://github.com/maplibre/maplibre-gl-js/issues/5432))
1010
- Fix regression in `mergeSourceDiffs`: handle add/remove/removeAll ([6342](https://github.com/maplibre/maplibre-gl-js/pull/6342))
11+
- Fix evaluating `global-state` in layout properties `icon-size` and `text-size` ([#6308](https://github.com/maplibre/maplibre-gl-js/issues/6308))
1112
- _...Add new stuff here..._
1213

1314
## 5.7.0

src/source/worker_tile.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import {StyleLayerIndex} from '../style/style_layer_index';
66
import {type WorkerTileParameters} from './worker_source';
77
import {type VectorTile} from '@mapbox/vector-tile';
88
import {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings';
9+
import {type EvaluationParameters} from '../style/evaluation_parameters';
10+
import {type PossiblyEvaluated} from '../style/properties';
11+
import {type SymbolLayoutProps, type SymbolLayoutPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties.g';
912

1013
function createWorkerTile(params?: {globalState?: Record<string, any>}): WorkerTile {
1114
return new WorkerTile({
@@ -286,4 +289,26 @@ describe('worker tile', () => {
286289
expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({data: expect.objectContaining({'icons': ['hello'], 'type': 'patterns'})}), expect.any(Object));
287290
expect(sendAsync).toHaveBeenCalledWith(expect.objectContaining({data: expect.objectContaining({'source': 'source', 'type': 'glyphs', 'stacks': {'StandardFont-Bold': [101, 115, 116]}})}), expect.any(Object));
288291
});
292+
293+
test('WorkerTile.parse passes global-state to layers', async () => {
294+
const layerIndex = new StyleLayerIndex([
295+
{
296+
id: 'layer-id',
297+
type: 'symbol',
298+
source: 'source',
299+
layout: {
300+
'text-size': ['global-state', 'size']
301+
}
302+
}
303+
]);
304+
305+
const globalState = {} as any;
306+
const tile = createWorkerTile({globalState});
307+
globalState.size = 12;
308+
await tile.parse(createLineWrapper(), layerIndex, [], {} as any, SubdivisionGranularitySetting.noSubdivision);
309+
const layer = layerIndex._layers['layer-id'];
310+
layer.recalculate({} as EvaluationParameters, []);
311+
const layout = layer.layout as PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>;
312+
expect(layout.get('text-size').evaluate({} as any, {})).toBe(12);
313+
});
289314
});

src/source/worker_tile.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,9 @@ export class WorkerTile {
206206

207207
function recalculateLayers(layers: ReadonlyArray<StyleLayer>, zoom: number, availableImages: Array<string>, globalState: Record<string, any>) {
208208
// Layers are shared and may have been used by a WorkerTile with a different zoom.
209-
const parameters = new EvaluationParameters(zoom, {globalState});
209+
const parameters = new EvaluationParameters(zoom);
210210
for (const layer of layers) {
211+
layer.setGlobalState(globalState);
211212
layer.recalculate(parameters, availableImages);
212213
}
213214
}

src/style/properties.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {describe, test, expect} from 'vitest';
2+
import {Layout, PropertyValue} from './properties';
3+
import symbolProperties from './style_layer/symbol_style_layer_properties.g';
4+
import {type EvaluationParameters} from './evaluation_parameters';
5+
6+
describe('PropertyValue', () => {
7+
test('set global state', () => {
8+
const propertyValue = new PropertyValue(symbolProperties.layout.properties['text-size'], ['global-state', 'size']);
9+
propertyValue.setGlobalState({size: 17});
10+
expect(propertyValue.expression.evaluate({} as EvaluationParameters)).toBe(17);
11+
});
12+
});
13+
14+
describe('Layout', () => {
15+
test('set global state', () => {
16+
const layout = new Layout(symbolProperties.layout);
17+
layout.setValue('text-size', ['global-state', 'textSize']);
18+
layout.setGlobalState({textSize: 15, textTransform: 'uppercase'});
19+
layout.setValue('text-transform', ['global-state', 'textTransform']);
20+
const _layout = layout.possiblyEvaluate({} as EvaluationParameters);
21+
expect(_layout.get('text-size').evaluate()).toBe(15);
22+
expect(_layout.get('text-transform').evaluate()).toBe('uppercase');
23+
});
24+
});

src/style/properties.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {clone, extend, easeCubicInOut} from '../util/util';
22
import {interpolates, type Color, type StylePropertySpecification, normalizePropertyExpression,
33
type Feature,
44
type FeatureState,
5+
type GlobalProperties,
56
type StylePropertyExpression,
67
type SourceExpression,
78
type CompositeExpression, type TransitionSpecification,
@@ -71,11 +72,13 @@ export class PropertyValue<T, R> {
7172
property: Property<T, R>;
7273
value: PropertyValueSpecification<T> | void;
7374
expression: StylePropertyExpression;
75+
private _evaluate: (parameters: EvaluationParameters) => R; // original evaluate function
7476

7577
constructor(property: Property<T, R>, value: PropertyValueSpecification<T> | void) {
7678
this.property = property;
7779
this.value = value;
7880
this.expression = normalizePropertyExpression(value === undefined ? property.specification.default : value, property.specification);
81+
this._evaluate = this.expression.evaluate;
7982
}
8083

8184
isDataDriven(): boolean {
@@ -93,6 +96,16 @@ export class PropertyValue<T, R> {
9396
): R {
9497
return this.property.possiblyEvaluate(this, parameters, canonical, availableImages);
9598
}
99+
100+
setGlobalState(globalState: Record<string, any>) {
101+
//@ts-ignore
102+
this.expression.evaluate = (globals: GlobalProperties, feature?: Feature, featureState?: FeatureState, canonical?: ICanonicalTileID, availableImages?: Array<string>, formattedSection?: FormattedSection) => {
103+
// force the use of global state object when evaluating any expression
104+
//@ts-ignore
105+
globals.globalState = globalState;
106+
return this._evaluate.call(this.expression, globals, feature, featureState, canonical, availableImages, formattedSection);
107+
};
108+
}
96109
}
97110

98111
export type TransitionParameters = {
@@ -315,10 +328,12 @@ export class Transitioning<Props> {
315328
export class Layout<Props> {
316329
_properties: Properties<Props>;
317330
_values: {[K in keyof Props]: PropertyValue<any, PossiblyEvaluatedPropertyValue<any>>};
331+
private _globalState: Record<string, any>; // reference to global state
318332

319333
constructor(properties: Properties<Props>) {
320334
this._properties = properties;
321335
this._values = (Object.create(properties.defaultPropertyValues) as any);
336+
this._globalState = {};
322337
}
323338

324339
hasValue<S extends keyof Props>(name: S) {
@@ -331,6 +346,7 @@ export class Layout<Props> {
331346

332347
setValue<S extends keyof Props>(name: S, value: any) {
333348
this._values[name] = new PropertyValue(this._values[name].property, value === null ? undefined : clone(value)) as any;
349+
this._values[name].setGlobalState(this._globalState);
334350
}
335351

336352
serialize() {
@@ -355,6 +371,13 @@ export class Layout<Props> {
355371
}
356372
return result;
357373
}
374+
375+
setGlobalState(globalState: Record<string, any>) {
376+
this._globalState = globalState;
377+
for (const value of Object.values(this._values)) {
378+
(value as PropertyValue<any, PossiblyEvaluatedPropertyValue<any>>).setGlobalState(globalState);
379+
}
380+
}
358381
}
359382

360383
// ------- PossiblyEvaluated -------

src/style/style.test.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ import {OverscaledTileID} from '../source/tile_id';
1111
import {fakeServer, type FakeServer} from 'nise';
1212

1313
import {type EvaluationParameters} from './evaluation_parameters';
14-
import {type LayerSpecification, type GeoJSONSourceSpecification, type FilterSpecification, type SourceSpecification, type StyleSpecification, type SymbolLayerSpecification, type SkySpecification} from '@maplibre/maplibre-gl-style-spec';
14+
import {type Feature, type LayerSpecification, type GeoJSONSourceSpecification, type FilterSpecification, type SourceSpecification, type StyleSpecification, type SymbolLayerSpecification, type SkySpecification} from '@maplibre/maplibre-gl-style-spec';
1515
import {type GeoJSONSource} from '../source/geojson_source';
1616
import {StubMap, sleep, waitForEvent} from '../util/test/util';
1717
import {RTLPluginLoadedEventName} from '../source/rtl_text_plugin_status';
1818
import {MessageType} from '../util/actor_messages';
1919
import {MercatorTransform} from '../geo/projection/mercator_transform';
2020
import {type Tile} from '../source/tile';
2121
import type Point from '@mapbox/point-geometry';
22+
import {type PossiblyEvaluated} from './properties';
23+
import {type SymbolLayoutProps, type SymbolLayoutPropsPossiblyEvaluated} from './style_layer/symbol_style_layer_properties.g';
2224

2325
function createStyleJSON(properties?): StyleSpecification {
2426
return extend({
@@ -469,6 +471,37 @@ describe('Style.loadJSON', () => {
469471
expect(style.stylesheet.layers[0].id).toBe(previousStyle.layers[0].id);
470472
expect(style.stylesheet.layers).toHaveLength(1);
471473
});
474+
475+
test('propagates global state object to layers', async () => {
476+
const style = new Style(getStubMap());
477+
style.loadJSON(
478+
createStyleJSON({
479+
sources: {
480+
'source-id': createGeoJSONSource()
481+
},
482+
layers: [
483+
{
484+
id: 'layer-id',
485+
type: 'symbol',
486+
source: 'source-id',
487+
layout: {
488+
'text-size': ['global-state', 'size']
489+
}
490+
}
491+
]
492+
})
493+
);
494+
await style.once('style.load');
495+
// tests that reference to globalState is propagated to layers
496+
// by changing globalState property and checking if the changed value
497+
// was used when evaluating the layer
498+
const globalState = style.getGlobalState();
499+
globalState.size = 12;
500+
const layer = style.getLayer('layer-id');
501+
layer.recalculate({} as EvaluationParameters, []);
502+
const layout = layer.layout as PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>;
503+
expect(layout.get('text-size').evaluate({} as Feature, {})).toBe(12);
504+
});
472505
});
473506

474507
describe('Style._load', () => {
@@ -1090,7 +1123,7 @@ describe('Style.removeSource', () => {
10901123
}]
10911124
}));
10921125
await style.once('style.load');
1093-
style.update(1 as any as EvaluationParameters);
1126+
style.update({zoom: 1} as EvaluationParameters);
10941127
return style;
10951128
}
10961129

@@ -2344,7 +2377,7 @@ describe('Style.setPaintProperty', () => {
23442377
tr.resize(512, 512);
23452378

23462379
await style.once('style.load');
2347-
style.update(tr.zoom as any as EvaluationParameters);
2380+
style.update({zoom: tr.zoom} as EvaluationParameters);
23482381
const sourceCache = style.sourceCaches['geojson'];
23492382
const source = style.getSource('geojson') as GeoJSONSource;
23502383

@@ -2739,7 +2772,7 @@ describe('Style.setLayerZoomRange', () => {
27392772
vi.spyOn(style, '_reloadSource');
27402773

27412774
style.setLayerZoomRange('raster', 5, 12);
2742-
style.update(0 as any as EvaluationParameters);
2775+
style.update({zoom: 0} as EvaluationParameters);
27432776
expect(style._reloadSource).not.toHaveBeenCalled();
27442777
});
27452778
});
@@ -2888,7 +2921,7 @@ describe('Style.queryRenderedFeatures', () => {
28882921
style.sourceCaches.mapLibre.transform = transform;
28892922
style.sourceCaches.other.transform = transform;
28902923

2891-
style.update(0 as any as EvaluationParameters);
2924+
style.update({zoom: 0} as EvaluationParameters);
28922925
style._updateSources(transform);
28932926
callback();
28942927
});

src/style/style.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,6 @@ export class Style extends Evented {
447447

448448
this.map.setTerrain(this.stylesheet.terrain ?? null);
449449

450-
this.setGlobalState(this.stylesheet.state ?? null);
451-
452450
this.fire(new Event('data', {dataType: 'style'}));
453451
this.fire(new Event('style.load'));
454452
}
@@ -463,11 +461,14 @@ export class Style extends Evented {
463461
this._order = dereferencedLayers.map((layer) => layer.id);
464462
this._layers = {};
465463

464+
this.setGlobalState(this.stylesheet.state ?? null);
465+
466466
// reset serialization field, to be populated only when needed
467467
this._serializedLayers = null;
468468
for (const layer of dereferencedLayers) {
469469
const styledLayer = createStyleLayer(layer);
470470
styledLayer.setEventedParent(this, {layer: {id: layer.id}});
471+
styledLayer.setGlobalState(this._globalState);
471472
this._layers[layer.id] = styledLayer;
472473
}
473474
}

src/style/style_layer.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,23 @@ describe('StyleLayer.serialize', () => {
452452
expect(layer.paint).toBeTruthy();
453453
});
454454
});
455+
456+
describe('StyleLayer.globalState', () => {
457+
test('uses layer global state when recalculating layout properties', () => {
458+
const layer = createStyleLayer({
459+
id: 'symbol',
460+
type: 'symbol',
461+
layout: {
462+
'text-field': '{text}',
463+
'text-size': ['global-state', 'textSize'],
464+
'text-transform': ['global-state', 'textTransform']
465+
}
466+
} as LayerSpecification) as SymbolStyleLayer;
467+
layer.setGlobalState({textSize: 15, textTransform: 'uppercase'});
468+
469+
layer.recalculate({zoom: 0, zoomHistory: {}, globalState: {textSize: 13, textTransform: 'lowercase'} as Record<string, any>} as EvaluationParameters, undefined);
470+
471+
expect(layer.layout.get('text-size').evaluate(undefined, {})).toBe(15);
472+
expect(layer.layout.get('text-transform').evaluate(undefined, {})).toBe('uppercase');
473+
});
474+
});

src/style/style_layer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export abstract class StyleLayer extends Evented {
106106
queryIntersectsFeature?(params: QueryIntersectsFeatureParams): boolean | number;
107107
createBucket?(parameters: BucketParameters<any>): Bucket;
108108

109+
private _globalState: Record<string, any>; // reference to global state
110+
109111
constructor(layer: LayerSpecification | CustomLayerInterface, properties: Readonly<{
110112
layout?: Properties<any>;
111113
paint?: Properties<any>;
@@ -124,6 +126,8 @@ export abstract class StyleLayer extends Evented {
124126
this.minzoom = layer.minzoom;
125127
this.maxzoom = layer.maxzoom;
126128

129+
this._globalState = {};
130+
127131
if (layer.type !== 'background') {
128132
this.source = layer.source;
129133
this.sourceLayer = layer['source-layer'];
@@ -295,6 +299,7 @@ export abstract class StyleLayer extends Evented {
295299
}
296300

297301
recalculate(parameters: EvaluationParameters, availableImages: Array<string>) {
302+
parameters.globalState = this._globalState;
298303
if (parameters.getCrossfadeParameters) {
299304
this._crossfadeParameters = parameters.getCrossfadeParameters();
300305
}
@@ -306,6 +311,13 @@ export abstract class StyleLayer extends Evented {
306311
(this as any).paint = this._transitioningPaint.possiblyEvaluate(parameters, undefined, availableImages);
307312
}
308313

314+
setGlobalState(globalState: Record<string, any>) {
315+
this._globalState = globalState;
316+
if (this._unevaluatedLayout) {
317+
this._unevaluatedLayout.setGlobalState(globalState);
318+
}
319+
}
320+
309321
serialize(): LayerSpecification {
310322
const output: LayerSpecification = {
311323
'id': this.id,
3.22 KB
Loading

0 commit comments

Comments
 (0)