Skip to content

Style Tree


Overview

Instead of defining each layer's style individually, Elzar uses a hierarchical tree structure where styles inherit from parent nodes. This reduces duplication and makes styles easier to maintain.


How it works

Selector paths

Styles are defined using dot-separated selector paths:

road                    # Base road style
road.highway            # Highway roads
road.highway.motorway   # Motorway specifically
road.primary            # Primary roads
road.secondary          # Secondary roads

Style resolution

When rendering a layer with ID road.highway.motorway, the Style Tree:

  1. Finds the road node and collects its styles
  2. Traverses to road.highway and collects its styles
  3. Traverses to road.highway.motorway and collects its styles
  4. Merges all collected styles (later styles override earlier ones)
graph LR
    A[road] -->|inherit| B[road.highway]
    B -->|inherit| C[road.highway.motorway]

    style A fill:#e1f5fe
    style B fill:#b3e5fc
    style C fill:#81d4fa

Implementation

StyleNode

Each node in the tree:

"""StyleNode fields in the style tree hierarchy."""

from map_style.generator.styler import StyleNode

# StyleNode contains:
# - name: str (node identifier)
# - elements: list[StyleElement] (styles at this node)
# - children: dict[str, StyleNode] (child nodes)

node = StyleNode("road")
assert node.name == "road"
assert node.elements == []
assert node.children == {}

StyleTree

The tree manages node creation and resolution:

"""StyleTree initialization and resolution."""

from map_style.generator.mapbox_style import LinePaint
from map_style.generator.styler import StyleElement, StyleTree

styles: dict[str, StyleElement] = {
    "road": StyleElement(paint=LinePaint(line_color="#ffffff")),
    "road.highway": StyleElement(paint=LinePaint(line_color="#ffcc00", line_width=4)),
}

# Create tree from style dict
tree = StyleTree(styles)

# Resolve collects all styles from root to leaf
elements = tree.resolve("road.highway")

Example

Style definition

"""Style definition with hierarchical selectors."""

from map_style.generator.mapbox_style import LineLayout, LinePaint
from map_style.generator.styler import StyleElement

styles: dict[str, StyleElement] = {
    "road": StyleElement(
        paint=LinePaint(line_color="white"),
        layout=LineLayout(line_cap="round"),
    ),
    "road.highway": StyleElement(
        paint=LinePaint(line_color="yellow", line_width=4),
    ),
    "road.highway.motorway": StyleElement(
        paint=LinePaint(line_width=6),
    ),
}

Tree structure

ROOT
└── road
    ├── elements: [LinePaint(color=white), LineLayout(cap=round)]
    └── highway
        ├── elements: [LinePaint(color=yellow, width=4)]
        └── motorway
            └── elements: [LinePaint(width=6)]

Resolution

Resolving road.highway.motorway:

Step Node Collected Paint
1 road {color: white}
2 road.highway {color: yellow, width: 4}
3 road.highway.motorway {color: yellow, width: 6}

Final merged style:

"""Merged style result after tree resolution."""

from map_style.generator.mapbox_style import LineLayout, LinePaint

resolved_paint = LinePaint(
    line_color="yellow",  # from road.highway
    line_width=6,  # from road.highway.motorway
)
resolved_layout = LineLayout(
    line_cap="round",  # from road
)


Benefits

Reduced duplication

Without Style Tree:

# Must repeat common properties
motorway: dict[str, Any] = {"line-color": "yellow", "line-cap": "round", "line-width": 6}
trunk: dict[str, Any] = {"line-color": "yellow", "line-cap": "round", "line-width": 5}
primary: dict[str, Any] = {"line-color": "yellow", "line-cap": "round", "line-width": 4}

With Style Tree:

# Common properties defined once
styles: dict[str, StyleElement] = {
    "road.highway": StyleElement(paint=LinePaint(line_color="yellow")),
    "road.highway.motorway": StyleElement(paint=LinePaint(line_width=6)),
    "road.highway.trunk": StyleElement(paint=LinePaint(line_width=5)),
}

Easier maintenance

Change the highway color once at road.highway, and all child styles inherit the change.

Clear hierarchy

The selector path structure makes relationships between styles explicit and discoverable.

Caveats

  • Selector paths must match layer IDs exactly
  • Deep hierarchies can make debugging harder
  • Order of style application matters (later overrides earlier)