Skip to content

Generator Reference

The generator package provides the core rendering engine for Elzar.

Modules

render

Style rendering functions.

map_style.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 map_style/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 map_style/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
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:
        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 map_style/generator/render.py
200
201
202
203
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
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.

map_style.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 map_style/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 map_style/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 map_style/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 map_style/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.

map_style.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,
)

GeometryType

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 map_style/generator/data.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
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 map_style/generator/data.py
312
313
314
315
316
317
318
319
320
321
322
323
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 map_style/generator/data.py
272
273
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 map_style/generator/data.py
285
286
287
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 map_style/generator/data.py
22
23
24
def render(self, stack: Stack[InjectionsNames]) -> None:
    """Render this node to the layer stack. Override in subclasses."""
    pass

filters

Filter expression handling.

map_style.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 map_style/generator/filters.py
15
16
def __init__(self, q: Q | None = None) -> None:
    self.expr = q or Q()