Skip to content

Creating Styles


Style architecture

Elzar uses a hierarchical approach where styles are defined as Python dictionaries mapping selector paths to StyleElement objects.

"""StyleTree with hierarchical selector paths."""

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

# Define styles with selector paths
styles: dict[str, StyleElement] = {
    "road": StyleElement(
        paint=LinePaint(line_color="#ffffff"),
        min_zoom=5,
    ),
    "road.highway": StyleElement(
        paint=LinePaint(line_color="#ffcc00", line_width=4),
    ),
    "road.highway.motorway": StyleElement(
        paint=LinePaint(line_width=6),
    ),
}

# Create style tree
tree = StyleTree(styles)

Style inheritance

Styles inherit from parent selectors. When resolving road.highway.motorway:

  1. road styles apply first (base road styling)
  2. road.highway styles override (highway-specific)
  3. road.highway.motorway styles override (motorway-specific)
graph TD
    A[road] --> B[road.highway]
    A --> C[road.primary]
    A --> D[road.secondary]
    B --> E[road.highway.motorway]
    B --> F[road.highway.trunk]

StyleElement

StyleElement defines the visual properties for a selector:

"""StyleElement fields: paint, layout, zoom, and metadata."""

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

element = StyleElement(
    paint=LinePaint(),  # Paint properties (colors, widths)
    layout=LineLayout(),  # Layout properties (caps, joins)
    min_zoom=10,  # Minimum zoom level
    max_zoom=18,  # Maximum zoom level
    metadata={"category": "roads"},  # Custom metadata
)

Paint classes

Different geometry types use different paint classes:

Geometry Paint Class Layout Class
Line LinePaint LineLayout
Polygon FillPaint FillLayout
Symbol SymbolPaint SymbolLayout
Circle CirclePaint CircleLayout
Background BackgroundPaint BaseLayout
Fill Extrusion FillExtrusionPaint BaseLayout

Example: road style

"""Road paint and layout styles with zoom-dependent widths."""

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

road_styles: dict[str, StyleElement] = {
    # Base road style
    "road": StyleElement(
        paint=LinePaint(
            line_color="#ffffff",
            line_opacity=1.0,
        ),
        layout=LineLayout(
            line_cap="round",
            line_join="round",
        ),
    ),
    # Highway styling
    "road.highway": StyleElement(
        paint=LinePaint(
            line_color="#ffcc00",
            line_width={"stops": [[10, 2], [14, 6], [18, 12]]},
        ),
    ),
    # Highway casing (outline)
    "road.highway_casing": StyleElement(
        paint=LinePaint(
            line_color="#e6a800",
            line_width={"stops": [[10, 4], [14, 8], [18, 14]]},
            line_gap_width=0,
        ),
    ),
}

Example: label style

"""Symbol label styles for place names."""

from map_style.generator.mapbox_style import SymbolLayout, SymbolPaint
from map_style.generator.styler import StyleElement

label_styles: dict[str, StyleElement] = {
    "label.place": StyleElement(
        paint=SymbolPaint(
            text_color="#333333",
            text_halo_color="#ffffff",
            text_halo_width=1.5,
        ),
        layout=SymbolLayout(
            text_field="${wgs_localization}",
            text_font=["Noto Sans Regular"],
            text_size=14,
            text_anchor="center",
        ),
    ),
    "label.place.city": StyleElement(
        layout=SymbolLayout(
            text_size=18,
            text_font=["Noto Sans Bold"],
        ),
        min_zoom=4,
        max_zoom=14,
    ),
}

Creating a complete style

"""Complete style class with render method."""

from map_style.generator import SourceLayerRef
from map_style.generator.data import Layer
from map_style.generator.mapbox_style import Style
from map_style.generator.render import render_mapbox_style
from map_style.generator.styler import StyleElement, StyleTree


class MyStyle:
    def __init__(self) -> None:
        self.styles: dict[str, StyleElement] = {
            # Define all your styles here
        }
        self.layers: list[Layer] = [
            # Define layer data
        ]
        self.sources: dict[str, SourceLayerRef] = {
            # Define tile sources
        }

    def render(self) -> tuple[Style, set[str]]:
        style_tree = StyleTree(self.styles)
        return render_mapbox_style(
            self.layers,
            style_tree,
            self.sources,
        )

Best practices

  1. Use meaningful selector paths: road.highway.motorway is clearer than motorway

  2. Define base styles first: Start with general styles, then specialize

  3. Use zoom-dependent values: Adjust line widths and sizes for different zoom levels

    line_width = NumberInterpolate(stops=[(10, 1.0), (14, 3.0), (18, 6.0)])
    

  4. Group related styles: Keep road styles, label styles, and POI styles in separate modules

  5. Document color choices: Use a central colors module

    # colors.py
    HIGHWAY: str = "#ffcc00"
    HIGHWAY_CASING: str = "#e6a800"