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:
- Finds the
roadnode and collects its styles - Traverses to
road.highwayand collects its styles - Traverses to
road.highway.motorwayand collects its styles - 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)