Skip to content

Generator Reference

The generator package provides the core rendering engine for Elzar.

Modules

render

Style rendering functions.

elzar.generator.render

Render map style layers to MapLibre GL style format.

This module provides functionality to convert internal layer representations to MapLibre GL JSON style format, including icon resolution from templates.

LayerRenderer

LayerRenderer(children)
Source code in elzar/generator/render.py
47
48
def __init__(self, children: list[Node]) -> None:
    self.children = children

render_mapbox_style

render_mapbox_style(data_layers, style_tree, sources, style_name='{style_name}')
Source code in elzar/generator/render.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def render_mapbox_style(
    data_layers: list[Layer],
    style_tree: StyleTree,
    sources: dict[str, SourceLayerRef],
    style_name: str = "{style_name}",
) -> tuple[Style, set[str]]:
    used_icons: set[str] = set()
    obfuscate_layer_id = False
    mb_layers: list[Any] = []
    for layer in data_layers:
        if layer.style_layer is not None:
            mb_layers.append(layer.style_layer)
            continue

        resolved_styles: list[StyleElement] = style_tree.resolve(layer.id)

        if resolved_styles == []:
            continue

        resolved_classes = type_obj_map.get(layer.geometry_type)

        if resolved_classes is not None:
            resolved_style_data: dict[str, Any] = {}
            resolved_layout_data: dict[str, Any] = {}
            min_zoom = None
            max_zoom = None

            # Start with StyleElement metadata, then override with Layer's values
            resolved_metadata: dict[str, Any] = {}

            for resolved_style in resolved_styles:
                paint = resolved_style.paint
                layout = resolved_style.layout
                if paint:
                    resolved_style_data |= paint.model_dump()
                if layout:
                    resolved_layout_data |= layout.model_dump()

                if resolved_style.min_zoom is not None:
                    min_zoom = resolved_style.min_zoom
                if resolved_style.max_zoom is not None:
                    max_zoom = resolved_style.max_zoom

                if resolved_style.metadata is not None:
                    resolved_metadata |= resolved_style.metadata

            # Layer's featureType and elementTypes take precedence over StyleElement
            resolved_metadata["elementTypes"] = layer.element_types
            resolved_metadata["featureType"] = layer.feature_type

            resolved_paint = resolved_classes.paint_class.model_validate(resolved_style_data)
            resolved_layout = resolved_classes.layout_class.model_validate(resolved_layout_data)

            source = sources.get(layer.id.split(".")[0])

            if isinstance(resolved_layout, SymbolLayout):
                used_icons.update(get_used_icons(layer, resolved_layout))

            resolved_filter = ExpressionSet(layer.filter).resolve() if layer.filter else None
            if obfuscate_layer_id:
                layer_id = sha256(layer.id.encode("utf-8")).hexdigest()
            else:
                layer_id = layer.id

            # Build layer kwargs - some layer types don't support all fields
            layer_kwargs: dict[str, Any] = {
                "id": layer_id,
                "paint": resolved_paint,
                "metadata": resolved_metadata or None,
            }

            # Only add min/max zoom if set
            if min_zoom is not None:
                layer_kwargs["min_zoom"] = min_zoom
            if max_zoom is not None:
                layer_kwargs["max_zoom"] = max_zoom

            # Raster, Background, and HillShade layers don't have source_layer
            if layer.geometry_type not in (GeometryType.BACKGROUND,):
                layer_kwargs["source"] = source.source if source else None

            # Only vector layers have source_layer and filter
            if layer.geometry_type not in (
                GeometryType.BACKGROUND,
                GeometryType.RASTER,
            ):
                layer_kwargs["source_layer"] = source.layer if source else None
                layer_kwargs["filter"] = resolved_filter
                layer_kwargs["layout"] = resolved_layout

            resolved_layer = resolved_classes.layer_class(**layer_kwargs)

            mb_layers.append(resolved_layer)

    mapbox_style = Style(
        id=style_name,
        name=style_name,
        layers=mb_layers,
        metadata=StyleMetadata(featureTypeAliases={"transit.station": "poi.transit.station"}),
        sprite=None,
        glyphs=None,
        sources={},
    )

    return mapbox_style, used_icons

get_used_icons

get_used_icons(layer, resolved_layout)
Source code in elzar/generator/render.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def get_used_icons(layer: Layer, resolved_layout: SymbolLayout) -> set[str]:
    resolved_icon = resolved_layout.icon_image
    icons: set[str] = set()

    if resolved_icon is not None:
        # Skip MapLibre expressions (lists) or non-string types - icons are resolved at runtime
        if not isinstance(resolved_icon, str):
            return icons

        if layer.filter and "{" in resolved_icon:
            filter_context = ExpressionSet(layer.filter).get_filter_map()
            template_context = re.match(r".*{(?P<context_key>.+)}.*", resolved_icon)

            if template_context:
                template_key = template_context.group("context_key")
                value = filter_context.get(template_key)

                if value is None:
                    # Template key not extractable from filter - skip icon resolution
                    return icons

                if isinstance(value, list):
                    for v in value:
                        icons.add(resolved_icon.replace(f"{{{template_key}}}", v))
            else:
                icons.add(resolved_icon)
        else:
            icons.add(resolved_icon)

    return icons

styler

Style tree implementation.

elzar.generator.styler

Style tree for hierarchical style resolution.

This module provides a tree structure for organizing and resolving styles based on dot-separated selector paths (e.g., 'road.highway.motorway').

StyleElement

StyleElement(paint=None, layout=None, min_zoom=None, max_zoom=None, metadata=None)
Source code in elzar/generator/styler.py
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    paint: BaseModel | None = None,
    layout: BaseModel | None = None,
    min_zoom: float | None = None,
    max_zoom: float | None = None,
    metadata: dict[str, Any] | None = None,
) -> None:
    self.paint = paint
    self.layout = layout
    self.min_zoom = min_zoom
    self.max_zoom = max_zoom
    self.metadata = metadata

__or__

__or__(other)

Merge two StyleElements. other fields override self where explicitly set.

Source code in elzar/generator/styler.py
50
51
52
53
54
55
56
57
58
59
60
def __or__(self, other: StyleElement) -> StyleElement:
    """Merge two StyleElements. ``other`` fields override ``self`` where explicitly set."""
    paint = _merge_models(self.paint, other.paint)
    layout = _merge_models(self.layout, other.layout)
    return StyleElement(
        paint=paint,
        layout=layout,
        min_zoom=other.min_zoom if other.min_zoom is not None else self.min_zoom,
        max_zoom=other.max_zoom if other.max_zoom is not None else self.max_zoom,
        metadata={**(self.metadata or {}), **(other.metadata or {})} if self.metadata or other.metadata else None,
    )

StyleNode

StyleNode(name)
Source code in elzar/generator/styler.py
77
78
79
80
def __init__(self, name: str) -> None:
    self.name = name
    self.elements = []
    self.children: dict[str, StyleNode] = {}

StyleTree

StyleTree(style)
Source code in elzar/generator/styler.py
84
85
86
87
88
89
90
def __init__(self, style: dict[str, StyleElement]) -> None:
    self.root = StyleNode("ROOT")

    for selector_path, element in style.items():
        if element.metadata is not None:
            element.metadata["featureType"] = selector_path
        self.add_node(selector_path, element)

data

Layer and data definitions.

elzar.generator.data

Data structures for map style layer definitions.

This module defines the core data types for representing map layers, including geometry types, render groups, and specialized road/rail nodes.

Layer dataclass

Layer(id, geometry_type, filter=None, feature_type=None, element_types=None, style_layer=None)

GeometryType

Bases: Enum

InjectionsNames

Bases: Enum

DataNode

DataNode(id, filter=None, geometry_type=BACKGROUND, render_group=TOP, feature_type=_FEATURE_TYPE_DEFAULT, element_types=None)

Bases: Node

Source code in elzar/generator/data.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def __init__(
    self,
    id: str,
    filter: Q | None = None,
    geometry_type: GeometryType = GeometryType.BACKGROUND,
    render_group: InjectionsNames = InjectionsNames.TOP,
    feature_type: str | None | object = _FEATURE_TYPE_DEFAULT,
    element_types: list[str] | None = None,
) -> None:
    self.id = id
    self.filter = filter
    self.geometry_type = geometry_type
    self.render_group = render_group
    # feature_type=None means explicitly null, _FEATURE_TYPE_DEFAULT means use layer id
    self.feature_type: str | None = id if feature_type is _FEATURE_TYPE_DEFAULT else feature_type  # type: ignore[assignment]
    self.element_types = element_types

render

render(stack)

Renders the layer in the stack

Source code in elzar/generator/data.py
315
316
317
318
319
320
321
322
323
324
325
326
def render(self, stack: Stack[InjectionsNames]) -> None:
    """Renders the layer in the stack"""
    stack.add_layer_before(
        self.render_group,
        Layer(
            self.id,
            filter=self.filter,
            feature_type=self.feature_type,
            geometry_type=self.geometry_type,
            element_types=self.element_types,
        ),
    )

Stack

Stack(render_groups)

Bases: Node, Generic[T]

Source code in elzar/generator/data.py
275
276
def __init__(self, render_groups: type[T]) -> None:
    self.layers = [InjectionPointLayer(id=render_group.value) for render_group in render_groups]

get_layers

get_layers()

Returns only renderable layers

Source code in elzar/generator/data.py
288
289
290
def get_layers(self) -> list[Layer]:
    """Returns only renderable layers"""
    return list(filter(lambda layer: not isinstance(layer, InjectionPointLayer), self.layers))

render

render(stack)

Render this node to the layer stack. Override in subclasses.

Source code in elzar/generator/data.py
22
23
24
def render(self, stack: Stack[InjectionsNames]) -> None:
    """Render this node to the layer stack. Override in subclasses."""
    pass

multiclass_fill

Multiclass fill layer builder.

elzar.generator.multiclass_fill

Builder for multiclass fill layers (e.g. landcover).

Produces a single FillLayer where multiple feature classes share one GL layer. The JS runtime reads classMetadata from the layer's metadata to build interpolate+case paint expressions that branch on the feature's class property.

FillClass dataclass

FillClass(fill_color, fill_outline_color=None, visible=True)

Definition of one class within a multiclass fill layer.

MulticlassFillNode

MulticlassFillNode(id, classes, source, source_layer, feature_type, *, fill_opacity=None, max_zoom=None, min_zoom=None, render_group=LANDCOVER)

Bases: Node

A Node that produces a multiclass fill layer through the standard rendering pipeline.

Source code in elzar/generator/multiclass_fill.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def __init__(
    self,
    id: str,
    classes: dict[str, FillClass],
    source: str,
    source_layer: str,
    feature_type: str,
    *,
    fill_opacity: float | dict[str, Any] | list[Any] | None = None,
    max_zoom: float | None = None,
    min_zoom: float | None = None,
    render_group: InjectionsNames = InjectionsNames.LANDCOVER,
) -> None:
    self.id = id
    self.classes = classes
    self.source = source
    self.source_layer = source_layer
    self.feature_type = feature_type
    self.fill_opacity = fill_opacity
    self.max_zoom = max_zoom
    self.min_zoom = min_zoom
    self.render_group = render_group

build_multiclass_fill_layer

build_multiclass_fill_layer(identifier, classes, source, source_layer, feature_type, *, fill_opacity=None, max_zoom=None, min_zoom=None)

Build a single FillLayer with classMetadata for multiple feature classes.

The returned layer has: - Paint with interpolate+case expressions for fill-color (and fill-outline-color if any class defines it) - Metadata with classMetadata and classBaseFilter for JS runtime styling - Filter combining classBaseFilter with all visible class filters

Source code in elzar/generator/multiclass_fill.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def build_multiclass_fill_layer(
    identifier: str,
    classes: dict[str, FillClass],
    source: str,
    source_layer: str,
    feature_type: str,
    *,
    fill_opacity: float | dict[str, Any] | list[Any] | None = None,
    max_zoom: float | None = None,
    min_zoom: float | None = None,
) -> FillLayer:
    """Build a single FillLayer with classMetadata for multiple feature classes.

    The returned layer has:
    - Paint with interpolate+case expressions for fill-color (and fill-outline-color if any class defines it)
    - Metadata with classMetadata and classBaseFilter for JS runtime styling
    - Filter combining classBaseFilter with all visible class filters
    """
    # Build classMetadata dict
    class_metadata: dict[str, ClassMetadata] = {}
    for class_name, cls in classes.items():
        colors: dict[str, list[tuple[int, str]]] = {"fill_color": cls.fill_color}
        if cls.fill_outline_color:
            colors["fill_outline_color"] = cls.fill_outline_color
        class_metadata[class_name] = ClassMetadata(
            filter=["==", ["get", "class"], class_name],
            colors=colors,
            visible=cls.visible,
        )

    # Build paint expressions
    fill_color_expr = _build_interpolate_case(classes, "fill_color")

    has_outline = any(cls.fill_outline_color for cls in classes.values())
    fill_outline: str | list[Any] | None = None
    if has_outline:
        fill_outline = _build_interpolate_case(classes, "fill_outline_color")
    else:
        fill_outline = "transparent"

    paint = FillPaint(
        fill_color=fill_color_expr,
        fill_outline_color=fill_outline,
        fill_opacity=fill_opacity,
    )

    # Build combined filter
    class_base_filter: list[Any] = ["has", "class"]
    visible_filters = [cm.filter for cm in class_metadata.values() if cm.visible]
    layer_filter: list[Any] = ["all", class_base_filter, ["any", *visible_filters]]

    metadata = Metadata(
        feature_type=feature_type,
        class_metadata=class_metadata,
        class_base_filter=class_base_filter,
    )

    return FillLayer(
        id=identifier,
        source=source,
        source_layer=source_layer,
        filter=layer_filter,
        paint=paint,
        metadata=metadata,
        min_zoom=min_zoom,
        max_zoom=max_zoom,
    )

poi

POI layer builder.

elzar.generator.poi

POIClass dataclass

POIClass(filter, icon=None, symbol_color='#fff', color=None, min_size=0.7, min_zoom=14)

POINode

POINode(id, pois, base_filter, *, visible=False, bold=False, all_label_color=None, all_halo_color=None, all_symbol_color=None, default_icon=None, raster=False, category_halo=False, source='openmaptiles', source_layer='poi', render_group=POI_AFTER_LABELS)

Bases: Node

A Node that produces a POI symbol layer through the standard rendering pipeline.

Source code in elzar/generator/poi.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def __init__(
    self,
    id: str,
    pois: dict[str, POIClass],
    base_filter: Q,
    *,
    visible: bool = False,
    bold: bool = False,
    all_label_color: str | None = None,
    all_halo_color: str | None = None,
    all_symbol_color: str | None = None,
    default_icon: str | None = None,
    raster: bool = False,
    category_halo: bool = False,
    source: str = "openmaptiles",
    source_layer: str = "poi",
    render_group: InjectionsNames = InjectionsNames.POI_AFTER_LABELS,
) -> None:
    self.id = id
    self.pois = pois
    self.base_filter = base_filter
    self.visible = visible
    self.bold = bold
    self.all_label_color = all_label_color
    self.all_halo_color = all_halo_color
    self.all_symbol_color = all_symbol_color
    self.default_icon = default_icon
    self.raster = raster
    self.category_halo = category_halo
    self.source = source
    self.source_layer = source_layer
    self.render_group = render_group
    self.used_icons: set[str] = set()

build_poi_layer

build_poi_layer(name, pois, base_filter, visible, bold, all_label_color=None, all_halo_color=None, all_symbol_color=None, default_icon=None, raster=False, category_halo=False, source='openmaptiles', source_layer='poi')

:returns required icons by poi layer and the symbol layer

Source code in elzar/generator/poi.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
def build_poi_layer(
    name: str,
    pois: dict[str, POIClass],
    base_filter: Q,
    visible: bool,
    bold: bool,
    all_label_color: str | None = None,
    all_halo_color: str | None = None,
    all_symbol_color: str | None = None,
    default_icon: str | None = None,
    raster: bool = False,
    category_halo: bool = False,
    source: str = "openmaptiles",
    source_layer: str = "poi",
) -> tuple[list[str | None], SymbolLayer]:
    """:returns required icons by poi layer and the symbol layer"""
    return [v.icon for v in pois.values()], build_poi_singlelayer(
        name,
        pois,
        base_filter,
        visible,
        bold,
        all_label_color,
        all_halo_color,
        all_symbol_color,
        default_icon,
        raster,
        category_halo,
        source,
        source_layer,
    )

filters

Filter expression handling.

elzar.generator.filters

Convert Q filter expressions to MapLibre GL filter format.

This module transforms Django-style Q expressions into MapLibre GL JSON filter expressions for use in map style layer definitions.

ExpressionSet

ExpressionSet(q=None)
Source code in elzar/generator/filters.py
15
16
def __init__(self, q: Q | None = None) -> None:
    self.expr = q or Q()