Skip to content

Fall back to the configured style when a vector tile feature lacks the property - #360

Draft
slesaad wants to merge 1 commit into
developmentfrom
fix/341-vectortile-binary-property-presence
Draft

Fall back to the configured style when a vector tile feature lacks the property#360
slesaad wants to merge 1 commit into
developmentfrom
fix/341-vectortile-binary-property-presence

Conversation

@slesaad

@slesaad slesaad commented Aug 28, 2026

Copy link
Copy Markdown
Member

Related: #341

Problem

deck.gl's MVTLayer defaults to binary: true. In that format a numeric feature property is not stored per feature — it is hoisted into a tile-wide typed array covering every feature in the tile, allocated zero-filled and written only for the features that actually carry the key. deck then merges that array into the object it hands every style accessor, unconditionally.

The result is that a feature which never carried the property is indistinguishable from one carrying 0. From @deck.gl/layers/dist/geojson-layer/geojson-binary.js:

const feature = { properties: { ...data.properties[propertiesIndex] } };
for (const prop in data.numericProps) {
    feature.properties[prop] = data.numericProps[prop].value[numericPropsIndex];
}

So the six per-feature style fields #341 added to vector tile layers silently misbehave for any feature missing the named property:

  • weightProp resolves to line width 0 — the feature draws invisibly
  • radiusProp resolves to radius 0 — the point disappears
  • the opacity and colour props resolve against 0 rather than falling back to the configured value

This only bites when at least one feature in the tile carries the property; if none do, the typed array is never created and the fallback works correctly. That is why it went unnoticed — a tileset where every feature carries the field behaves perfectly.

Change

resolveStyleAccessors now reports whether anything it built actually reads a feature property, and the vector tile layer decodes tiles as GeoJSON when so:

binary: !readsFeatureProperties

In GeoJSON form an absent key is genuinely absent, so the existing undefined fallbacks work as written. Layers with no *Prop configured keep the binary fast path untouched. It sits ahead of the nativeOptions spread, so a mission can still override it.

binary: false is a first-class path in this deck.gl version, not a corner: getPickingInfo's binary branch exists only to rebuild via binaryToGeojson what GeoJSON already has, getHighlightedObjectIndex carries both branches, and deck itself forces binary: false under GlobeView.

Cost

Tiles for layers that use per-feature styling are parsed to GeoJSON rather than binary, which is slower and heavier per tile. It is scoped to the layers that need it, but a dense tileset with per-feature styling will be measurably heavier than one without. The alternative is drawing the wrong thing.

Verified

Unit tests cover each of the six props individually, that a layer with none of them keeps binary decoding, and that nativeOptions can still override. Reverting the one-line predicate fails them.

🤖 Generated with Claude Code

deck.gl's MVTLayer decodes tiles into a binary form by default. That
format hoists every numeric property into one tile-wide typed array
covering every feature in the tile, zero filled for the features that
never carried it, and merges the whole set into the object handed to
style accessors. A landcover polygon with no elevation therefore arrives
as `{class: 'grass', ele: 0}` - a real number, indistinguishable from a
measured zero.

The `*Prop` style fields added in #341 read straight into that zero fill.
A feature lacking the property named by `weightProp` took line width 0
and drew invisibly; one lacking `radiusProp` took radius 0 and did not
draw at all; one lacking `fillOpacityProp` or `opacityProp` took alpha 0
and vanished - each of them instead of falling back to the layer's
configured `weight`, `radius` or opacity, which is the whole point of
having a flat field beside the prop. The colour props name string
properties, which the binary form does omit when absent, but they are
resolved by the same accessors, so they are covered by the same switch.

Nothing in the binary payload records presence. The string properties it
keeps alongside omit the key, but the numeric arrays are dense and
untagged, and there is no mask - confirmed against @loaders.gl/gis,
where the arrays are allocated with `new T(n)` and written only where
`propName in properties`. So presence cannot be recovered downstream;
the format has to change.

It changes only where it buys something. `binary: false` costs parse
time and memory on every tile, so it is switched on exactly when some
accessor reads a feature property, and every other vector tile layer
keeps the binary fast path. Picking, autoHighlight and uniqueIdProperty
all carry a branch for each mode, and GlobeView already forces this same
setting, so nothing else depends on it. A mission can still override it
through nativeOptions.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant