Skip to content

Commit d8432c2

Browse files
meliteleNatalia Kowalczyk
andauthored
global-state expression support in layout properties (#6209)
* add unit tests checking that sources are reloaded only when necessary on changes of global state rferenced by layout properties * `global-state` expression support in layout properties complements `global-state` expression support in filter and paint properties #5613 * changelog --------- Co-authored-by: Natalia Kowalczyk <[email protected]>
1 parent 8b56c22 commit d8432c2

File tree

6 files changed

+255
-11
lines changed

6 files changed

+255
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### ✨ Features and improvements
44
- Pass document's `lang` to Tiny-SDF to render Simplified and Traditional Chinese characters ([#6223](https://github.com/maplibre/maplibre-gl-js/issues/6223))
5+
- Enable `global-state` expressions in layout properties([#6209](https://github.com/maplibre/maplibre-gl-js/pull/6209))
56

67
- _...Add new stuff here..._
78

src/source/worker_tile.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import {type WorkerTileParameters} from './worker_source';
77
import {type VectorTile} from '@mapbox/vector-tile';
88
import {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings';
99

10-
function createWorkerTile() {
10+
function createWorkerTile(params?: {globalState?: Record<string, any>}): WorkerTile {
1111
return new WorkerTile({
1212
uid: '',
1313
zoom: 0,
1414
maxZoom: 20,
1515
tileSize: 512,
1616
source: 'source',
1717
tileID: new OverscaledTileID(1, 0, 1, 1, 1),
18-
overscaling: 1
18+
overscaling: 1,
19+
globalState: params?.globalState
1920
} as any as WorkerTileParameters);
2021
}
2122

@@ -27,6 +28,14 @@ function createWrapper() {
2728
} as any as Feature]);
2829
}
2930

31+
function createLineWrapper() {
32+
return new GeoJSONWrapper([{
33+
type: 2,
34+
geometry: [[0, 0], [1, 1]],
35+
tags: {}
36+
} as any as Feature]);
37+
}
38+
3039
describe('worker tile', () => {
3140
test('WorkerTile.parse', async () => {
3241
const layerIndex = new StyleLayerIndex([{
@@ -40,6 +49,40 @@ describe('worker tile', () => {
4049
expect(result.buckets[0]).toBeTruthy();
4150
});
4251

52+
test('WorkerTile.parse layer with layout property', async () => {
53+
const layerIndex = new StyleLayerIndex([{
54+
id: 'test',
55+
source: 'source',
56+
type: 'line',
57+
layout: {
58+
'line-join': 'bevel'
59+
}
60+
}]);
61+
62+
const tile = createWorkerTile();
63+
const result = await tile.parse(createLineWrapper(), layerIndex, [], {} as any, SubdivisionGranularitySetting.noSubdivision);
64+
expect(result.buckets[0]).toBeTruthy();
65+
expect(result.buckets[0].layers[0].layout._values['line-join'].value.value).toBe('bevel');
66+
});
67+
68+
test('WorkerTile.parse layer with layout property using global-state', async () => {
69+
const layerIndex = new StyleLayerIndex([{
70+
id: 'test',
71+
source: 'source',
72+
type: 'line',
73+
layout: {
74+
'line-join': ['global-state', 'test']
75+
}
76+
}]);
77+
78+
const tile = createWorkerTile({
79+
globalState: {test: 'bevel'}
80+
});
81+
const result = await tile.parse(createLineWrapper(), layerIndex, [], {} as any, SubdivisionGranularitySetting.noSubdivision);
82+
expect(result.buckets[0]).toBeTruthy();
83+
expect(result.buckets[0].layers[0].layout._values['line-join'].value.value).toBe('bevel');
84+
});
85+
4386
test('WorkerTile.parse skips hidden layers', async () => {
4487
const layerIndex = new StyleLayerIndex([{
4588
id: 'test-hidden',

src/source/worker_tile.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class WorkerTile {
113113
if (layer.maxzoom && this.zoom >= layer.maxzoom) continue;
114114
if (layer.visibility === 'none') continue;
115115

116-
recalculateLayers(family, this.zoom, availableImages);
116+
recalculateLayers(family, this.zoom, availableImages, this.globalState);
117117

118118
const bucket = buckets[layer.id] = layer.createBucket({
119119
index: featureIndex.bucketLayerIDs.length,
@@ -169,7 +169,7 @@ export class WorkerTile {
169169
for (const key in buckets) {
170170
const bucket = buckets[key];
171171
if (bucket instanceof SymbolBucket) {
172-
recalculateLayers(bucket.layers, this.zoom, availableImages);
172+
recalculateLayers(bucket.layers, this.zoom, availableImages, this.globalState);
173173
performSymbolLayout({
174174
bucket,
175175
glyphMap,
@@ -184,7 +184,7 @@ export class WorkerTile {
184184
(bucket instanceof LineBucket ||
185185
bucket instanceof FillBucket ||
186186
bucket instanceof FillExtrusionBucket)) {
187-
recalculateLayers(bucket.layers, this.zoom, availableImages);
187+
recalculateLayers(bucket.layers, this.zoom, availableImages, this.globalState);
188188
bucket.addFeatures(options, this.tileID.canonical, imageAtlas.patternPositions);
189189
}
190190
}
@@ -204,9 +204,9 @@ export class WorkerTile {
204204
}
205205
}
206206

207-
function recalculateLayers(layers: ReadonlyArray<StyleLayer>, zoom: number, availableImages: Array<string>) {
207+
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);
209+
const parameters = new EvaluationParameters(zoom, {globalState});
210210
for (const layer of layers) {
211211
layer.recalculate(parameters, availableImages);
212212
}

src/style/style.test.ts

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,33 @@ describe('Style.setGlobalState', () => {
13931393
expect(style.sourceCaches['fill-source-id'].reload).toHaveBeenCalled();
13941394
});
13951395

1396+
test('reloads sources when state property is used in layout property', async () => {
1397+
const style = new Style(getStubMap());
1398+
style.loadJSON(createStyleJSON({
1399+
sources: {
1400+
'line-source-id': createGeoJSONSource()
1401+
},
1402+
layers: [{
1403+
id: 'first-layer-id',
1404+
type: 'line',
1405+
source: 'line-source-id',
1406+
layout: {
1407+
'line-join': ['global-state', 'lineJoin']
1408+
}
1409+
}]
1410+
}));
1411+
1412+
await style.once('style.load');
1413+
1414+
style.sourceCaches['line-source-id'].resume = vi.fn();
1415+
style.sourceCaches['line-source-id'].reload = vi.fn();
1416+
1417+
style.setGlobalState({lineJoin: {default: 'bevel'}});
1418+
1419+
expect(style.sourceCaches['line-source-id'].resume).toHaveBeenCalled();
1420+
expect(style.sourceCaches['line-source-id'].reload).toHaveBeenCalled();
1421+
});
1422+
13961423
test('does not reload sources when state property is set to the same value as current one', async () => {
13971424
const style = new Style(getStubMap());
13981425
style.loadJSON(createStyleJSON({
@@ -1451,6 +1478,36 @@ describe('Style.setGlobalState', () => {
14511478
expect(style.sourceCaches['circle-source-id'].resume).not.toHaveBeenCalled();
14521479
expect(style.sourceCaches['circle-source-id'].reload).not.toHaveBeenCalled();
14531480
});
1481+
1482+
test('does not reload sources when new state property is used in paint property while state property used in layout is unchanged', async () => {
1483+
const style = new Style(getStubMap());
1484+
style.loadJSON(createStyleJSON({
1485+
sources: {
1486+
'line-source-id': createGeoJSONSource()
1487+
},
1488+
layers: [{
1489+
id: 'first-layer-id',
1490+
type: 'line',
1491+
source: 'line-source-id',
1492+
layout: {
1493+
'line-join': ['global-state', 'lineJoin']
1494+
},
1495+
paint: {
1496+
'line-color': ['global-state', 'lineColor']
1497+
}
1498+
}]
1499+
}));
1500+
1501+
await style.once('style.load');
1502+
1503+
style.sourceCaches['line-source-id'].resume = vi.fn();
1504+
style.sourceCaches['line-source-id'].reload = vi.fn();
1505+
1506+
style.setGlobalState({lineColor: {default: 'red'}});
1507+
1508+
expect(style.sourceCaches['line-source-id'].resume).not.toHaveBeenCalled();
1509+
expect(style.sourceCaches['line-source-id'].reload).not.toHaveBeenCalled();
1510+
});
14541511
});
14551512

14561513
describe('Style.setGlobalStateProperty', () => {
@@ -1541,6 +1598,63 @@ describe('Style.setGlobalStateProperty', () => {
15411598
expect(style.sourceCaches['fill-source-id'].reload).not.toHaveBeenCalled();
15421599
});
15431600

1601+
test('reloads sources when state property is used in layout property', async () => {
1602+
const style = new Style(getStubMap());
1603+
style.loadJSON(createStyleJSON({
1604+
sources: {
1605+
'line-1-source-id': createGeoJSONSource(),
1606+
'line-2-source-id': createGeoJSONSource(),
1607+
'line-3-source-id': createGeoJSONSource()
1608+
},
1609+
layers: [{
1610+
id: 'first-layer-id',
1611+
type: 'line',
1612+
source: 'line-1-source-id',
1613+
layout: {
1614+
'line-join': ['global-state', 'lineJoin']
1615+
}
1616+
}, {
1617+
id: 'second-layer-id',
1618+
type: 'line',
1619+
source: 'line-3-source-id'
1620+
}, {
1621+
id: 'third-layer-id',
1622+
type: 'line',
1623+
source: 'line-2-source-id',
1624+
layout: {
1625+
'line-join': ['global-state', 'lineJoin']
1626+
}
1627+
}, {
1628+
id: 'fourth-layer-id',
1629+
type: 'line',
1630+
source: 'line-3-source-id',
1631+
layout: {
1632+
'line-cap': ['global-state', 'lineCap']
1633+
}
1634+
}]
1635+
}));
1636+
1637+
await style.once('style.load');
1638+
1639+
style.sourceCaches['line-1-source-id'].resume = vi.fn();
1640+
style.sourceCaches['line-1-source-id'].reload = vi.fn();
1641+
style.sourceCaches['line-2-source-id'].resume = vi.fn();
1642+
style.sourceCaches['line-2-source-id'].reload = vi.fn();
1643+
style.sourceCaches['line-3-source-id'].resume = vi.fn();
1644+
style.sourceCaches['line-3-source-id'].reload = vi.fn();
1645+
1646+
style.setGlobalStateProperty('lineJoin', 'bevel');
1647+
1648+
// sources line-1 and line-2 should be reloaded
1649+
expect(style.sourceCaches['line-1-source-id'].resume).toHaveBeenCalled();
1650+
expect(style.sourceCaches['line-1-source-id'].reload).toHaveBeenCalled();
1651+
expect(style.sourceCaches['line-2-source-id'].resume).toHaveBeenCalled();
1652+
expect(style.sourceCaches['line-2-source-id'].reload).toHaveBeenCalled();
1653+
// source line-3 should not be reloaded
1654+
expect(style.sourceCaches['line-3-source-id'].resume).not.toHaveBeenCalled();
1655+
expect(style.sourceCaches['line-3-source-id'].reload).not.toHaveBeenCalled();
1656+
});
1657+
15441658
test('does not reload sources when state property is set to the same value as current one', async () => {
15451659
const style = new Style(getStubMap());
15461660
style.loadJSON(createStyleJSON({
@@ -1599,6 +1713,36 @@ describe('Style.setGlobalStateProperty', () => {
15991713
expect(style.sourceCaches['circle-source-id'].resume).not.toHaveBeenCalled();
16001714
expect(style.sourceCaches['circle-source-id'].reload).not.toHaveBeenCalled();
16011715
});
1716+
1717+
test('does not reload sources when state property is used in paint property while a different state property used in layout is unchanged', async () => {
1718+
const style = new Style(getStubMap());
1719+
style.loadJSON(createStyleJSON({
1720+
sources: {
1721+
'line-source-id': createGeoJSONSource()
1722+
},
1723+
layers: [{
1724+
id: 'first-layer-id',
1725+
type: 'line',
1726+
source: 'line-source-id',
1727+
layout: {
1728+
'line-join': ['global-state', 'lineJoin']
1729+
},
1730+
paint: {
1731+
'line-color': ['global-state', 'lineColor']
1732+
}
1733+
}]
1734+
}));
1735+
1736+
await style.once('style.load');
1737+
1738+
style.sourceCaches['line-source-id'].resume = vi.fn();
1739+
style.sourceCaches['line-source-id'].reload = vi.fn();
1740+
1741+
style.setGlobalStateProperty('lineColor', 'red');
1742+
1743+
expect(style.sourceCaches['line-source-id'].resume).not.toHaveBeenCalled();
1744+
expect(style.sourceCaches['line-source-id'].reload).not.toHaveBeenCalled();
1745+
});
16021746
});
16031747

16041748
describe('Style.addLayer', () => {
@@ -1660,7 +1804,7 @@ describe('Style.addLayer', () => {
16601804
style.loadJSON(createStyleJSON());
16611805
await style.once('style.load');
16621806
const errorPromise = style.once('error');
1663-
1807+
16641808
style.addLayer({
16651809
id: 'background',
16661810
type: 'background',
@@ -1887,9 +2031,9 @@ describe('Style.addLayer', () => {
18872031
}as LayerSpecification;
18882032

18892033
await style.once('style.load');
1890-
const errorPromise = style.once('error');
2034+
const errorPromise = style.once('error');
18912035
style.addLayer(layer);
1892-
2036+
18932037
const {error} = await errorPromise;
18942038
expect(error.message).toMatch(/does not exist on source/);
18952039
});
@@ -2078,7 +2222,7 @@ describe('Style.setPaintProperty', () => {
20782222

20792223
await source.once('data');
20802224
vi.spyOn(sourceCache, 'reload');
2081-
2225+
20822226
source.setData({'type': 'FeatureCollection', 'features': []});
20832227
style.setPaintProperty('circle', 'circle-color', {type: 'identity', property: 'foo'});
20842228
await waitForEvent(source, 'data', (e) => e.sourceDataType === 'content');
3.79 KB
Loading
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"version": 8,
3+
"metadata": {
4+
"test": {
5+
"height": 256,
6+
"width": 256
7+
}
8+
},
9+
"center": [ 0, 0 ],
10+
"zoom": 0,
11+
"state": {
12+
"prefix": { "default": "pre_"},
13+
"suffix": { "default": "_suf" }
14+
},
15+
"sources": {
16+
"point": {
17+
"type": "geojson",
18+
"data": {
19+
"type": "FeatureCollection",
20+
"features": [
21+
{
22+
"type": "Feature",
23+
"properties": {
24+
"name": "Napoli"
25+
},
26+
"geometry": {
27+
"type": "Point",
28+
"coordinates": [ 0, 0 ]
29+
}
30+
}
31+
]
32+
}
33+
}
34+
},
35+
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
36+
"layers": [
37+
{
38+
"id": "text",
39+
"type": "symbol",
40+
"source": "point",
41+
"layout": {
42+
"text-field": [
43+
"concat",
44+
["global-state", "prefix"],
45+
["get", "name"],
46+
["global-state", "suffix"]
47+
],
48+
"text-font": [
49+
"Open Sans Semibold",
50+
"Arial Unicode MS Bold"
51+
],
52+
"text-size": 32
53+
}
54+
}
55+
]
56+
}

0 commit comments

Comments
 (0)