Skip to content

Commit 0d641c0

Browse files
underootgithub-actions[bot]
authored andcommitted
Fix update placement after changes (internal-8903)
GitOrigin-RevId: d0af91f6de57bd859b23ecf2953868d8e56e5af8
1 parent 6841af7 commit 0d641c0

File tree

3 files changed

+142
-3
lines changed

3 files changed

+142
-3
lines changed

src/style/pauseable_placement.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ class PauseablePlacement {
102102
return this._forceFullPlacement;
103103
}
104104

105+
setStale(): void {
106+
if (this.placement) {
107+
this.placement.stale = true;
108+
}
109+
}
110+
111+
isStale(): boolean {
112+
if (!this.placement) return false;
113+
return this.placement.stale;
114+
}
115+
105116
isDone(): boolean {
106117
return this._done;
107118
}

src/style/style.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4180,13 +4180,15 @@ class Style extends Evented<MapEvents> {
41804180
const transformChanged = Boolean(this.placement && !transform.equals(this.placement.transform));
41814181
const replacementSourceChanged = Boolean(this.placement && ((this.placement.lastReplacementSourceUpdateTime !== 0 && !replacementSource) || this.placement.lastReplacementSourceUpdateTime !== replacementSource.updateTime));
41824182

4183+
const isDonePlacementConsideredStale = transformChanged || replacementSourceChanged || symbolBucketsChanged;
41834184
// Force full placement when fadeDuration === 0 to ensure that newly loaded
41844185
// tiles will fully display symbols in their first frame.
41854186
// Make sure to not do this for the static camera, otherwise
41864187
// we will do expensive full placements on every frame.
4187-
const fullFrameUpdateRequired = (transformChanged || replacementSourceChanged || symbolBucketsChanged || (this.placement && this.placement.isStale())) && fadeDuration === 0;
4188+
const newImmediatePlacementRequired = (isDonePlacementConsideredStale || this.pauseablePlacement.isStale()) && fadeDuration === 0;
4189+
const newNormalPlacementRequired = this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom) && fadeDuration !== 0;
41884190

4189-
if (this.pauseablePlacement.isFullPlacementRequested() || !this.pauseablePlacement.placement || fullFrameUpdateRequired || (fadeDuration !== 0 && this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) {
4191+
if (this.pauseablePlacement.isFullPlacementRequested() || !this.pauseablePlacement.placement || newImmediatePlacementRequired || newNormalPlacementRequired) {
41904192
const fogState = this.fog && transform.projection.supportsFog ? this.fog.state : null;
41914193
this.pauseablePlacement = this.pauseablePlacement.startNewPlacement(transform, this._mergedOrder, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement, fogState, this._buildingIndex);
41924194
}
@@ -4203,8 +4205,10 @@ class Style extends Evented<MapEvents> {
42034205
// since the placement gets split over multiple frames it is possible
42044206
// these buckets were processed before they were changed and so the
42054207
// placement is already stale while it is in progress
4206-
this.pauseablePlacement.placement.setStale();
4208+
this.pauseablePlacement.setStale();
42074209
}
4210+
} else if (isDonePlacementConsideredStale && fadeDuration !== 0) {
4211+
this.pauseablePlacement.setStale();
42084212
}
42094213

42104214
if (placementCommitted || symbolBucketsChanged) {

test/unit/style/style.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2856,4 +2856,128 @@ describe('Style#_updatePlacement', () => {
28562856
expect(startNewPlacementSpy).not.toHaveBeenCalled();
28572857
expect(continuePlacementSpy).not.toHaveBeenCalled();
28582858
});
2859+
2860+
test('returns true when symbol layer is added after load due to symbolBucketsChanged', async () => {
2861+
const map = new StubMap();
2862+
// @ts-expect-error - painter is not part of StubMap but required for _updatePlacement
2863+
map.painter = {scaleFactor: 1};
2864+
const replacementSource = {updateTime: 0};
2865+
2866+
const style = new Style(map);
2867+
style.loadJSON({
2868+
"version": 8,
2869+
"sources": {
2870+
"geojson": {
2871+
"type": "geojson",
2872+
"data": {"type": "FeatureCollection", "features": []}
2873+
}
2874+
},
2875+
"layers": []
2876+
});
2877+
2878+
await waitFor(style, 'style.load');
2879+
2880+
const tr = map.transform;
2881+
tr.resize(512, 512);
2882+
2883+
style.update({zoom: tr.zoom, fadeDuration: 0});
2884+
style._updatePlacement(tr, false, 0, false, replacementSource);
2885+
2886+
expect(style.pauseablePlacement.isDone()).toBeTruthy();
2887+
2888+
style.addLayer({
2889+
"id": "symbol",
2890+
"type": "symbol",
2891+
"source": "geojson"
2892+
});
2893+
2894+
style.update({zoom: tr.zoom, fadeDuration: 0});
2895+
2896+
// Spy on crossTileSymbolIndex.addLayer to return true (simulating new symbol buckets)
2897+
// This happens in real scenarios when tiles have symbol features
2898+
vi.spyOn(style.crossTileSymbolIndex, 'addLayer').mockReturnValue(true);
2899+
2900+
const result = style._updatePlacement(tr, false, 0, false, replacementSource);
2901+
2902+
expect(result).toBeTruthy();
2903+
});
2904+
2905+
test('returns true when transformChanged', async () => {
2906+
const map = new StubMap();
2907+
// @ts-expect-error - painter is not part of StubMap but required for _updatePlacement
2908+
map.painter = {scaleFactor: 1};
2909+
const replacementSource = {updateTime: 0};
2910+
2911+
const style = new Style(map);
2912+
style.loadJSON({
2913+
"version": 8,
2914+
"sources": {
2915+
"geojson": {
2916+
"type": "geojson",
2917+
"data": {"type": "FeatureCollection", "features": []}
2918+
}
2919+
},
2920+
"layers": [{
2921+
"id": "symbol",
2922+
"type": "symbol",
2923+
"source": "geojson"
2924+
}]
2925+
});
2926+
2927+
await waitFor(style, 'style.load');
2928+
2929+
const tr = map.transform;
2930+
tr.resize(512, 512);
2931+
2932+
style.update({zoom: tr.zoom, fadeDuration: 0});
2933+
style._updatePlacement(tr, false, 0, false, replacementSource);
2934+
2935+
expect(style.pauseablePlacement.isDone()).toBeTruthy();
2936+
2937+
// Change transform to trigger transformChanged
2938+
tr.zoom = 5;
2939+
2940+
// Use fadeDuration > 0 so that setStale() is called when placement is
2941+
// considered stale due to transform change (line 4210-4211 in style.ts)
2942+
const result = style._updatePlacement(tr, false, 300, false, replacementSource);
2943+
2944+
expect(result).toBeTruthy();
2945+
});
2946+
2947+
test('returns true when replacementSourceChanged', async () => {
2948+
const map = new StubMap();
2949+
// @ts-expect-error - painter is not part of StubMap but required for _updatePlacement
2950+
map.painter = {scaleFactor: 1};
2951+
2952+
const style = new Style(map);
2953+
style.loadJSON({
2954+
"version": 8,
2955+
"sources": {
2956+
"geojson": {
2957+
"type": "geojson",
2958+
"data": {"type": "FeatureCollection", "features": []}
2959+
}
2960+
},
2961+
"layers": [{
2962+
"id": "symbol",
2963+
"type": "symbol",
2964+
"source": "geojson"
2965+
}]
2966+
});
2967+
2968+
await waitFor(style, 'style.load');
2969+
2970+
const tr = map.transform;
2971+
tr.resize(512, 512);
2972+
2973+
style.update({zoom: tr.zoom, fadeDuration: 0});
2974+
style._updatePlacement(tr, false, 0, false, {updateTime: 0});
2975+
2976+
expect(style.pauseablePlacement.isDone()).toBeTruthy();
2977+
2978+
// Change replacementSource updateTime to trigger replacementSourceChanged
2979+
const result = style._updatePlacement(tr, false, 300, false, {updateTime: 1});
2980+
2981+
expect(result).toBeTruthy();
2982+
});
28592983
});

0 commit comments

Comments
 (0)