Skip to content

drawFill reads layer.layout unguarded: layers added from a styledata listener are painted before recalculate #13717

Description

@guillem-aistech

Summary

Style.update() fires styledata synchronously at its own tail, from inside Map._render(), after the per-layer recalculate() loop and before painter.render(). A layer added by a styledata listener therefore enters the render order for the current frame with layout === undefined, and is painted before it is ever evaluated.

drawFill reads layer.layout unguarded, so it throws. Two sibling functions in the same file already guard the identical access.

Version

Reproduced on v3.20.0. The unguarded read is still present on v3.28.1.

Error

Uncaught TypeError: Cannot read properties of undefined (reading 'get')
    at drawFill
    at Painter.renderLayer
    at Painter.render
    at Map._render
    at <requestAnimationFrame>

Mechanism

Within a single frame:

  1. Map._render()this.style.update(parameters)src/ui/map.ts:4537
  2. Style.update() recalculates layers — src/style/style.ts:1797
    if (layer.visibility !== 'none' || layer.hasTransition()) layer.recalculate(parameters, this._availableImages);
  3. Style.update() then fires styledata at its tail — src/style/style.ts:1870
    if (changed) {
        this.fire(new Event('data', {dataType: 'style'}));
    }
    A listener calling map.addLayer() here inserts a layer after step 2 has run.
  4. Map._render()this.painter.render(this.style, …)src/ui/map.ts:4570
  5. Painter.renderLayer guards only on isHidden() and coords.lengthsrc/render/painter.ts:1497-1499. It never checks layout.
  6. drawFillsrc/render/draw_fill.ts:80
    if (layer.layout.get('fill-elevation-reference') !== 'none') {   // throws

layout is assigned only in StyleLayer.recalculate() (src/style/style_layer.ts:315-322), so the freshly added layer has none.

isHidden() does not save it either: when the addLayer spec has no layout block, visibility stays undefined, and isHidden() only returns true for the literal 'none' (src/style/style_layer.ts:301-305).

Minimal reproduction

map.on('load', () => {
    map.addSource('demo', {type: 'geojson', data: polygonsInViewport});
});

// Re-create the layer on each styledata. Note: no `layout` block in the spec.
map.on('styledata', () => {
    if (!map.isStyleLoaded()) return;
    if (map.getLayer('demo-fill')) map.removeLayer('demo-fill');
    map.addLayer({
        id: 'demo-fill',
        type: 'fill',
        source: 'demo',
        paint: {'fill-color': '#ff0000'}
    });
});

Then trigger any styledata while the polygons are inside the viewport.

The source must have renderable tiles on screen — otherwise renderLayer returns early on coords.length === 0 and never reaches drawFill. That is what makes this look intermittent.

Suggested fix

drawFill is the only unguarded consumer on this path. The same guard already exists twice in that file (v3.20.0 lines 151 and 235):

if (layer.layout && layer.layout.get('fill-elevation-reference') !== 'none') {
    elevationType = 'road';
}

Alternatively, skip layers with no layout in Painter.renderLayer, which would cover any other consumer reachable the same way.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions