Skip to content

stations_map

djangomain.dash_apps.stations_map ¤

Dash app for selecting stations and rendering them on an interactive map.

This module defines a DjangoDash application with two station checklist sections, plus a third block for spatial layer controls. GeoTIFF layers are loaded from MapLayerImport entries and rendered below station points.

Attributes¤

SCROLL_HEIGHT = '150px' module-attribute ¤

_COLOR_MAP = {'My Stations': '#e74c3c', 'Public': '#3498db'} module-attribute ¤

_DEFAULT_MAP_STYLE = 'carto-positron' module-attribute ¤

_MAP_STYLE_OPTIONS = [{'label': 'Carto Positron', 'value': 'carto-positron'}, {'label': 'Carto Darkmatter', 'value': 'carto-darkmatter'}, {'label': 'OpenStreetMap', 'value': 'open-street-map'}] module-attribute ¤

_STATION_KEYS = ('station_id', 'station_code', 'station_name', 'station_latitude', 'station_longitude') module-attribute ¤

_map_col = dbc.Col(dcc.Graph(id='map_graph', style={'height': '50vh'}, config={'scrollZoom': True}, figure={'data': [], 'layout': {'mapbox': {'style': _DEFAULT_MAP_STYLE, 'zoom': 3.6, 'center': {'lat': -9.182731, 'lon': -60.658738}}, 'margin': {'r': 0, 't': 0, 'l': 0, 'b': 0}, 'uirevision': True, 'legend': {'title': '', 'x': 0.01, 'y': 0.99, 'bgcolor': 'rgba(255,255,255,0.8)'}}}), width=9, style={'padding': '0'}) module-attribute ¤

_sidebar = dbc.Col([_map_style_block(), html.Div(_station_block('owned', 'My Stations'), id='owned-block'), _station_block('public', 'Public Stations'), _spatial_data_block()], width=3, style={'overflowY': 'auto', 'height': '100vh', 'borderRight': '1px solid #dee2e6', 'paddingRight': '12px'}) module-attribute ¤

app = DjangoDash('StationsMap', external_stylesheets=[dbc.themes.BOOTSTRAP]) module-attribute ¤

Classes¤

Station ¤

Bases: PermissionsBase

Main representation of a station, including several metadata.

Attributes:

Name Type Description
visibility str

Visibility level of the object, including an "internal" option.

station_id int

Primary key.

station_code str

Unique code for the station.

station_name str

Brief description of the station.

station_type StationType

Type of the station.

country Country

Country where the station is located.

region Region

Region within the Country where the station is located.

ecosystem Ecosystem

Ecosystem associated with the station.

institution Institution

Institutional partner responsible for the station.

place_basin PlaceBasin

Place-Basin association.

station_state bool

Is the station operational?

timezone str

Timezone of the station.

station_latitude Decimal

Latitude of the station, in degrees [-90 to 90].

station_longitude Decimal

Longitude of the station, in degrees [-180 to 180].

station_altitude int

Altitude of the station.

influence_km Decimal

Area of influence in km2.

station_file ImageField

Photography of the station.

station_external bool

Is the station external?

variables str

Comma-separated list of variables measured by the station.

Attributes¤
variables_list property ¤

Return the list of variables measured by the station.

Only variables with data in the database are returned.

Returns:

Type Description
list[str]

list[str]: List of variables measured by the station.

Functions¤
__str__() ¤

Return the station code.

Source code in station/models.py
458
459
460
def __str__(self) -> str:
    """Return the station code."""
    return str(self.station_code)
get_absolute_url() ¤

Return the absolute url of the station.

Source code in station/models.py
462
463
464
def get_absolute_url(self) -> str:
    """Return the absolute url of the station."""
    return reverse("station:station_detail", kwargs={"pk": self.pk})
set_object_permissions() ¤

Set object-level permissions.

This method is called by the save method of the model to set the object-level permissions based on the visibility level of the object. In addition to the standard permissions for the station, the view_measurements permission is set which controls who can view the measurements associated to the station.

Source code in station/models.py
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def set_object_permissions(self) -> None:
    """Set object-level permissions.

    This method is called by the save method of the model to set the object-level
    permissions based on the visibility level of the object. In addition to the
    standard permissions for the station, the view_measurements permission is set
    which controls who can view the measurements associated to the station.
    """
    super().set_object_permissions()

    standard_group = Group.objects.get(name="Standard")
    anonymous_user = get_anonymous_user()

    # Assign view_measurements permission based on permissions level
    if self.visibility == "public":
        assign_perm("view_measurements", standard_group, self)
        assign_perm("view_measurements", anonymous_user, self)
        if self.owner:
            remove_perm("view_measurements", self.owner, self)
    elif self.visibility == "internal":
        assign_perm("view_measurements", standard_group, self)
        remove_perm("view_measurements", anonymous_user, self)
        if self.owner:
            remove_perm("view_measurements", self.owner, self)
    elif self.visibility == "private":
        remove_perm("view_measurements", standard_group, self)
        remove_perm("view_measurements", anonymous_user, self)
        if self.owner:
            assign_perm("view_measurements", self.owner, self)

Functions¤

_all_none_buttons(block_id) ¤

Build the bulk-selection button group for a station checklist.

Parameters:

Name Type Description Default
block_id str

Prefix used to build the button component ids.

required

Returns:

Type Description
ButtonGroup

Button group containing All and None buttons.

Source code in djangomain/dash_apps/stations_map.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def _all_none_buttons(block_id: str) -> dbc.ButtonGroup:
    """Build the bulk-selection button group for a station checklist.

    Args:
        block_id: Prefix used to build the button component ids.

    Returns:
        Button group containing *All* and *None* buttons.
    """
    return dbc.ButtonGroup(
        [
            dbc.Button(
                "All",
                id={"type": "select-all", "index": block_id},
                size="sm",
                color="secondary",
                outline=True,
            ),
            dbc.Button(
                "None",
                id={"type": "select-none", "index": block_id},
                size="sm",
                color="secondary",
                outline=True,
            ),
        ],
        className="mb-2",
    )

_build_options(codes) ¤

Build Dash checklist option dicts from an iterable of station codes.

Each option label is "<code> - <name>" when the station has a name, otherwise just the code. Codes are sorted alphabetically.

Parameters:

Name Type Description Default
codes Any

Station codes to include.

required

Returns:

Type Description
list[dict[str, str]]

Option dicts with "label" and "value" keys, ready for use in dcc.Checklist.

Source code in djangomain/dash_apps/stations_map.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def _build_options(codes: Any) -> list[dict[str, str]]:
    """Build Dash checklist option dicts from an iterable of station codes.

    Each option label is ``"<code> - <name>"`` when the station has a name,
    otherwise just the code. Codes are sorted alphabetically.

    Args:
        codes: Station codes to include.

    Returns:
        Option dicts with ``"label"`` and ``"value"``
            keys, ready for use in ``dcc.Checklist``.
    """
    sorted_codes = sorted(_ensure_list(codes))
    station_names = {
        station.station_code: station.station_name
        for station in Station.objects.filter(station_code__in=sorted_codes).only(
            "station_code", "station_name"
        )
    }

    options = []
    for code in sorted_codes:
        if code not in station_names:
            continue
        name = station_names[code]
        options.append(
            {
                "label": f"{code} - {name}" if name else code,
                "value": code,
            }
        )
    return options

_build_spatial_layer_row(layer_id, layer_name, visible) ¤

Build one selected-layer row with a visibility toggle and remove button.

Parameters:

Name Type Description Default
layer_id str

Map layer identifier used by callbacks.

required
layer_name str

Label shown to the user.

required
visible bool

Whether the layer is currently visible on the map.

required

Returns:

Type Description
Div

Row containing checkbox, name, and a remove button.

Source code in djangomain/dash_apps/stations_map.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
def _build_spatial_layer_row(layer_id: str, layer_name: str, visible: bool) -> html.Div:
    """Build one selected-layer row with a visibility toggle and remove button.

    Args:
        layer_id: Map layer identifier used by callbacks.
        layer_name: Label shown to the user.
        visible: Whether the layer is currently visible on the map.

    Returns:
        Row containing checkbox, name, and a remove button.
    """
    return html.Div(
        [
            dbc.Checkbox(
                id={"type": "spatial-layer-visible", "index": layer_id},
                value=visible,
                className="me-2",
            ),
            html.Span(layer_name, className="flex-grow-1"),
            html.Button(
                "x",
                id={"type": "spatial-layer-remove", "index": layer_id},
                n_clicks=0,
                type="button",
                className="btn btn-link text-danger p-0 ms-2",
                title=f"Remove {layer_name}",
            ),
        ],
        className="d-flex align-items-center py-1 border-bottom",
    )

_ensure_list(value) ¤

Normalise a callback input value to a plain Python list.

Parameters:

Name Type Description Default
value Any

Input that may be None, a scalar, or a list.

required

Returns:

Type Description
list

Empty list for falsy input; the original list; or a single-item list wrapping a scalar value.

Source code in djangomain/dash_apps/stations_map.py
242
243
244
245
246
247
248
249
250
251
252
253
254
def _ensure_list(value: Any) -> list:
    """Normalise a callback input value to a plain Python list.

    Args:
        value: Input that may be ``None``, a scalar, or a list.

    Returns:
        Empty list for falsy input; the original list; or a single-item
            list wrapping a scalar value.
    """
    if not value:
        return []
    return value if isinstance(value, list) else [value]

_get_request_user(kwargs) ¤

Get callback request user from django_plotly_dash callback kwargs.

Parameters:

Name Type Description Default
kwargs dict

Callback keyword arguments from django_plotly_dash.

required

Returns:

Type Description
Any | None

Authenticated user object when present, else None.

Source code in djangomain/dash_apps/stations_map.py
292
293
294
295
296
297
298
299
300
301
302
def _get_request_user(kwargs: dict) -> Any | None:
    """Get callback request user from django_plotly_dash callback kwargs.

    Args:
        kwargs: Callback keyword arguments from django_plotly_dash.

    Returns:
        Authenticated user object when present, else None.
    """
    request = kwargs.get("request")
    return getattr(request, "user", None)

_map_style_block() ¤

Build controls for selecting the map base style.

Returns:

Type Description
Card

Card containing the base map style selector.

Source code in djangomain/dash_apps/stations_map.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def _map_style_block() -> dbc.Card:
    """Build controls for selecting the map base style.

    Returns:
        Card containing the base map style selector.
    """
    return dbc.Card(
        [
            dbc.CardHeader("Map Style", className="py-2"),
            dbc.CardBody(
                dbc.Select(
                    id="map-style-select",
                    options=_MAP_STYLE_OPTIONS,
                    value=_DEFAULT_MAP_STYLE,
                    size="sm",
                ),
                className="py-2",
            ),
        ],
        className="mb-3 shadow-sm",
    )

_normalise_spatial_layer_store(store_data) ¤

Normalise layer-store payload to an ordered list of unique entries.

Parameters:

Name Type Description Default
store_data Any

List-like payload containing layer ids or entry dictionaries.

required

Returns:

Type Description
list[dict[str, Any]]

Entries with id and visible keys.

Source code in djangomain/dash_apps/stations_map.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def _normalise_spatial_layer_store(store_data: Any) -> list[dict[str, Any]]:
    """Normalise layer-store payload to an ordered list of unique entries.

    Args:
        store_data: List-like payload containing layer ids or entry dictionaries.

    Returns:
        Entries with ``id`` and ``visible`` keys.
    """
    normalised = []
    seen = set()

    for item in _ensure_list(store_data):
        if isinstance(item, dict):
            layer_id = item.get("id")
            visible = bool(item.get("visible", True))
        else:
            layer_id = item
            visible = True

        if layer_id in (None, "") or layer_id in seen:
            continue

        normalised.append({"id": layer_id, "visible": visible})
        seen.add(layer_id)

    return normalised

_scrollable_checklist(block_id) ¤

Build a checklist wrapped in a fixed-height scroll container.

Parameters:

Name Type Description Default
block_id str

Prefix used to build the checklist component id.

required

Returns:

Type Description
Div

Scrollable container holding a dcc.Checklist.

Source code in djangomain/dash_apps/stations_map.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def _scrollable_checklist(block_id: str) -> html.Div:
    """Build a checklist wrapped in a fixed-height scroll container.

    Args:
        block_id: Prefix used to build the checklist component id.

    Returns:
        Scrollable container holding a ``dcc.Checklist``.
    """
    return html.Div(
        dcc.Checklist(
            id={"type": "checklist", "index": block_id},
            options=[],
            value=[],
            inputStyle={"marginRight": "6px"},
            labelStyle={"display": "block", "paddingBottom": "3px"},
        ),
        style={
            "maxHeight": SCROLL_HEIGHT,
            "overflowY": "auto",
            "border": "1px solid #dee2e6",
            "borderRadius": "4px",
            "padding": "6px 8px",
        },
    )

_spatial_data_block() ¤

Build controls for selecting and toggling spatial layers.

Returns:

Type Description
Card

Card containing a layer picker and selected-layer list.

Source code in djangomain/dash_apps/stations_map.py
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
def _spatial_data_block() -> dbc.Card:
    """Build controls for selecting and toggling spatial layers.

    Returns:
        Card containing a layer picker and selected-layer list.
    """
    return dbc.Card(
        [
            dbc.CardHeader("Spatial Data", className="py-2"),
            dbc.CardBody(
                [
                    dcc.Dropdown(
                        id="spatial-layer-dropdown",
                        options=[],
                        value=None,
                        placeholder="Add a spatial layer...",
                        clearable=True,
                    ),
                    html.Div(
                        id="spatial-layer-list",
                        className="mt-2",
                        style={
                            "maxHeight": SCROLL_HEIGHT,
                            "overflowY": "auto",
                            "border": "1px solid #dee2e6",
                            "borderRadius": "4px",
                            "padding": "6px 8px",
                        },
                    ),
                    dcc.Store(id="spatial-layer-store", data=[]),
                ],
                className="py-2",
            ),
        ],
        className="mb-3 shadow-sm",
    )

_station_block(block_id, title) ¤

Build a station-selection card containing bulk controls and a checklist.

Parameters:

Name Type Description Default
block_id str

Prefix used for internal control ids.

required
title str

Text rendered in the card header.

required

Returns:

Type Description
Card

Card containing an All/None button group and a scrollable checklist.

Source code in djangomain/dash_apps/stations_map.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def _station_block(block_id: str, title: str) -> dbc.Card:
    """Build a station-selection card containing bulk controls and a checklist.

    Args:
        block_id: Prefix used for internal control ids.
        title: Text rendered in the card header.

    Returns:
        Card containing an All/None button group and a scrollable
            checklist.
    """
    return dbc.Card(
        [
            dbc.CardHeader(title, className="py-2"),
            dbc.CardBody(
                [_all_none_buttons(block_id), _scrollable_checklist(block_id)],
                className="py-2",
            ),
        ],
        className="mb-3 shadow-sm",
    )

_station_rows_for_codes(codes, station_group) ¤

Build map rows for selected station codes in display order.

Parameters:

Name Type Description Default
codes Any

Selected station codes in desired output order.

required
station_group str

Label used to tag rows for trace grouping.

required

Returns:

Type Description
list[dict[str, Any]]

Station row dictionaries ready for map trace construction.

Source code in djangomain/dash_apps/stations_map.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def _station_rows_for_codes(codes: Any, station_group: str) -> list[dict[str, Any]]:
    """Build map rows for selected station codes in display order.

    Args:
        codes: Selected station codes in desired output order.
        station_group: Label used to tag rows for trace grouping.

    Returns:
        Station row dictionaries ready for map
            trace construction.
    """
    selected_codes = _ensure_list(codes)
    if not selected_codes:
        return []

    station_rows = {
        row["station_code"]: row
        for row in Station.objects.filter(station_code__in=selected_codes).values(
            *_STATION_KEYS
        )
    }

    rows = []
    for code in selected_codes:
        row = station_rows.get(code)
        if not row:
            continue
        rows.append({**row, "type": station_group})
    return rows

_triggered_component(callback_context) ¤

Return component id from django-plotly-dash callback context.

Source code in djangomain/dash_apps/stations_map.py
334
335
336
337
338
339
def _triggered_component(callback_context: Any) -> str:
    """Return component id from django-plotly-dash callback context."""
    triggered_prop = (
        callback_context.triggered[0]["prop_id"] if callback_context.triggered else ""
    )
    return triggered_prop.rsplit(".", 1)[0] if triggered_prop else ""

available_map_layers_by_id(user) ¤

Return user-viewable GeoTIFF layers keyed by dash dropdown id.

Parameters:

Name Type Description Default
user Any | None

Django user used to resolve object-level permissions.

required

Returns:

Type Description
dict[str, dict[str, str]]

Layer metadata keyed by maplayer id. Returns an empty dict when the user is None or lookup fails.

Source code in djangomain/dash_apps/geotiff_layers.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def available_map_layers_by_id(user: Any | None) -> dict[str, dict[str, str]]:
    """Return user-viewable GeoTIFF layers keyed by dash dropdown id.

    Args:
        user: Django user used to resolve object-level permissions.

    Returns:
        Layer metadata keyed by maplayer id. Returns an empty dict when the
            user is None or lookup fails.
    """
    if user is None:
        return {}

    try:
        queryset = get_objects_for_user(
            user,
            "importing.view_maplayerimport",
            klass=MapLayerImport,
        )
    except Exception as e:
        logger.error("Error occurred while fetching available map layers: %s", e)
        return {}

    layer_index = {}
    for layer in queryset.order_by("name", "pk"):
        layer_id = f"maplayer-{layer.pk}"
        layer_index[layer_id] = {
            "id": layer_id,
            "name": str(layer.name),
            "file_path": str(layer.file.path),
        }

    return layer_index

build_mapbox_layers(layers_raw, user) ¤

Build mapbox layout layers for currently visible spatial layers.

GeoTIFF sources are resolved server-side from currently authorised MapLayerImport objects and never from client store values.

Parameters:

Name Type Description Default
layers_raw list

Trusted spatial layer payload from server-side callback state.

required
user Any

Django user used for per-object authorization.

required

Returns:

Type Description
list[dict[str, Any]]

Mapbox image layer dictionaries for visible and authorized GeoTIFF layers.

Source code in djangomain/dash_apps/geotiff_layers.py
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
202
203
204
def build_mapbox_layers(layers_raw: list, user: Any) -> list[dict[str, Any]]:
    """Build mapbox layout layers for currently visible spatial layers.

    GeoTIFF sources are resolved server-side from currently authorised
    MapLayerImport objects and never from client store values.

    Args:
        layers_raw: Trusted spatial layer payload from server-side callback
            state.
        user: Django user used for per-object authorization.

    Returns:
        Mapbox image layer dictionaries for visible
            and authorized GeoTIFF layers.
    """
    map_layers = []
    available_layers = available_map_layers_by_id(user)

    for layer in layers_raw:
        if not layer["visible"]:
            continue

        resolved_layer = available_layers.get(layer["id"])
        if not resolved_layer:
            continue

        try:
            mtime = os.path.getmtime(resolved_layer["file_path"])
            payload = load_geotiff_payload(resolved_layer["file_path"], mtime)
        except (OSError, ValueError) as exc:
            logger.warning("Skipping map layer %s: %s", layer["id"], exc)
            continue

        map_layers.append(
            {
                "type": "raster",
                "sourcetype": "image",
                "source": payload["image"],
                "coordinates": payload["coordinates"],
                "opacity": 0.75,
                "below": "traces",
            }
        )

    return map_layers

checklist_selection(options, _n_all, _n_none, current_value, callback_context) ¤

Resolve station checklist selection for all/none and refresh events.

Parameters:

Name Type Description Default
options Any

Checklist options for the matched block.

required
_n_all Any

Click count for the "All" button.

required
_n_none Any

Click count for the "None" button.

required
current_value Any

Currently selected option values.

required
callback_context Any

Dash callback context used to inspect trigger source.

required

Returns:

Type Description
list

Updated selected values for the matched checklist.

Source code in djangomain/dash_apps/stations_map.py
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
@app.callback(
    Output({"type": "checklist", "index": MATCH}, "value"),
    [
        Input({"type": "checklist", "index": MATCH}, "options"),
        Input({"type": "select-all", "index": MATCH}, "n_clicks"),
        Input({"type": "select-none", "index": MATCH}, "n_clicks"),
    ],
    State({"type": "checklist", "index": MATCH}, "value"),
)
def checklist_selection(
    options: Any, _n_all: Any, _n_none: Any, current_value: Any, callback_context: Any
) -> list:
    """Resolve station checklist selection for all/none and refresh events.

    Args:
        options: Checklist options for the matched block.
        _n_all: Click count for the "All" button.
        _n_none: Click count for the "None" button.
        current_value: Currently selected option values.
        callback_context: Dash callback context used to inspect trigger source.

    Returns:
        Updated selected values for the matched checklist.
    """
    triggered_component = _triggered_component(callback_context)
    option_values = [option["value"] for option in (options or [])]

    if "select-none" in triggered_component:
        return []
    if "select-all" in triggered_component:
        return option_values

    # Keep the current user selection whenever options are refreshed.
    option_values_set = set(option_values)
    selected_values = [
        value for value in _ensure_list(current_value) if value in option_values_set
    ]
    if selected_values:
        return selected_values

    return option_values

populate_options(all_raw, **kwargs) ¤

Populate owned and public checklist options from visible station codes.

For authenticated users the owned section is shown and populated with stations that belong to the current user; the remaining visible stations go into the public section. For anonymous users all visible stations are treated as public and the owned section is hidden.

Parameters:

Name Type Description Default
all_raw Any

Raw children value of the hidden stations_list div, containing the station codes the current user may see.

required
**kwargs Any

Extra keyword arguments injected by django_plotly_dash, expected to include request.

{}

Returns:

Type Description
tuple[list[dict[str, str]], list[dict[str, str]], dict]

Owned checklist options, public checklist options, and a CSS style dict for the owned block.

Source code in djangomain/dash_apps/stations_map.py
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
@app.callback(
    [
        Output({"type": "checklist", "index": "owned"}, "options"),
        Output({"type": "checklist", "index": "public"}, "options"),
        Output("owned-block", "style"),
    ],
    Input("stations_list", "children"),
)
def populate_options(
    all_raw: Any, **kwargs: Any
) -> tuple[list[dict[str, str]], list[dict[str, str]], dict]:
    """Populate owned and public checklist options from visible station codes.

    For authenticated users the owned section is shown and populated with
    stations that belong to the current user; the remaining visible stations
    go into the public section.  For anonymous users all visible stations are
    treated as public and the owned section is hidden.

    Args:
        all_raw: Raw children value of the hidden
            ``stations_list`` div, containing the station codes the current
            user may see.
        **kwargs: Extra keyword arguments injected by ``django_plotly_dash``,
            expected to include ``request``.

    Returns:
        Owned checklist options, public
            checklist options, and a CSS style dict for the owned block.
    """
    all_codes = _ensure_list(all_raw)

    request = kwargs.get("request")
    user = getattr(request, "user", None)

    if user and user.is_authenticated:
        owned_codes = set(
            Station.objects.filter(station_code__in=all_codes, owner=user).values_list(
                "station_code", flat=True
            )
        )
        owned_block_style = {}
    else:
        owned_codes = set()
        owned_block_style = {"display": "none"}

    owned = [c for c in all_codes if c in owned_codes]
    public = [c for c in all_codes if c not in owned_codes]
    return _build_options(owned), _build_options(public), owned_block_style

sync_spatial_layer_controls(_stations_raw, _map_style_value, dropdown_layer_id, _remove_clicks, visibility_values, remove_ids, visibility_ids, store_data, callback_context, **kwargs) ¤

Sync spatial-layer picker, selected-list rows, and map visibility state.

Parameters:

Name Type Description Default
_stations_raw Any

Unused trigger input for station list changes.

required
_map_style_value Any

Current map style value.

required
dropdown_layer_id Any

Layer id selected from the add-layer dropdown.

required
_remove_clicks Any

Click counts from per-layer remove buttons.

required
visibility_values Any

Visibility booleans for selected layer checkboxes.

required
remove_ids Any

Pattern-matching ids for the remove buttons.

required
visibility_ids Any

Pattern-matching ids for the visibility checkboxes.

required
store_data Any

Current selected-layer store payload.

required
callback_context Any

Dash callback context used to inspect trigger source.

required
**kwargs Any

Callback kwargs containing request context.

{}

Returns:

Type Description
tuple[list[dict[str, Any]], list[dict[str, str]], None, list]

Selected layer store data, add-layer dropdown options, reset dropdown value, and selected-layer row components.

Source code in djangomain/dash_apps/stations_map.py
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
@app.callback(
    [
        Output("spatial-layer-store", "data"),
        Output("spatial-layer-dropdown", "options"),
        Output("spatial-layer-dropdown", "value"),
        Output("spatial-layer-list", "children"),
    ],
    [
        Input("stations_list", "children"),
        Input("map-style-select", "value"),
        Input("spatial-layer-dropdown", "value"),
        Input({"type": "spatial-layer-remove", "index": ALL}, "n_clicks"),
        Input({"type": "spatial-layer-visible", "index": ALL}, "value"),
    ],
    [
        State({"type": "spatial-layer-remove", "index": ALL}, "id"),
        State({"type": "spatial-layer-visible", "index": ALL}, "id"),
        State("spatial-layer-store", "data"),
    ],
)
def sync_spatial_layer_controls(
    _stations_raw: Any,
    _map_style_value: Any,
    dropdown_layer_id: Any,
    _remove_clicks: Any,
    visibility_values: Any,
    remove_ids: Any,
    visibility_ids: Any,
    store_data: Any,
    callback_context: Any,
    **kwargs: Any,
) -> tuple[list[dict[str, Any]], list[dict[str, str]], None, list]:
    """Sync spatial-layer picker, selected-list rows, and map visibility state.

    Args:
        _stations_raw: Unused trigger input for station list changes.
        _map_style_value: Current map style value.
        dropdown_layer_id: Layer id selected from the add-layer dropdown.
        _remove_clicks: Click counts from per-layer remove buttons.
        visibility_values: Visibility booleans for selected layer checkboxes.
        remove_ids: Pattern-matching ids for the remove buttons.
        visibility_ids: Pattern-matching ids for the visibility checkboxes.
        store_data: Current selected-layer store payload.
        callback_context: Dash callback context used to inspect trigger source.
        **kwargs: Callback kwargs containing request context.

    Returns:
        Selected layer
            store data, add-layer dropdown options, reset dropdown value, and
            selected-layer row components.
    """
    user = _get_request_user(kwargs)
    layer_index = available_map_layers_by_id(user)
    selected_layers = [
        entry
        for entry in _normalise_spatial_layer_store(store_data)
        if entry["id"] in layer_index
    ]

    triggered_component = _triggered_component(callback_context)

    if triggered_component == "map-style-select":
        selected_layers = []
    elif triggered_component == "spatial-layer-dropdown":
        selected_layer_ids = {entry["id"] for entry in selected_layers}
        if (
            dropdown_layer_id in layer_index
            and dropdown_layer_id not in selected_layer_ids
        ):
            # New layers start unchecked and only show once user enables them.
            selected_layers.append({"id": dropdown_layer_id, "visible": False})
    elif '"type":"spatial-layer-remove"' in triggered_component:
        clicked_remove_ids = [
            (button_id.get("index"), clicks)
            for button_id, clicks in zip(
                _ensure_list(remove_ids),
                _ensure_list(_remove_clicks),
            )
            if isinstance(button_id, dict) and clicks
        ]
        removed_id = (
            max(clicked_remove_ids, key=lambda item: item[1])[0]
            if clicked_remove_ids
            else None
        )

        selected_layers = [
            entry for entry in selected_layers if entry["id"] != removed_id
        ]
    elif '"type":"spatial-layer-visible"' in triggered_component:
        visibility_by_id = {
            visibility_id.get("index"): bool(value)
            for visibility_id, value in zip(
                _ensure_list(visibility_ids),
                _ensure_list(visibility_values),
            )
            if isinstance(visibility_id, dict) and "index" in visibility_id
        }
        selected_layers = [
            {
                "id": entry["id"],
                "visible": visibility_by_id.get(entry["id"], entry["visible"]),
            }
            for entry in selected_layers
        ]

    selected_layer_ids = {entry["id"] for entry in selected_layers}
    dropdown_options = [
        {"label": layer["name"], "value": layer_id}
        for layer_id, layer in layer_index.items()
        if layer_id not in selected_layer_ids
    ]

    layer_rows = [
        _build_spatial_layer_row(
            entry["id"],
            layer_index[entry["id"]]["name"],
            entry["visible"],
        )
        for entry in selected_layers
    ]
    if not layer_rows:
        layer_rows = [
            html.Div(
                "No spatial layers selected.",
                className="text-muted fst-italic small",
            )
        ]

    return selected_layers, dropdown_options, None, layer_rows

update_map(owned_selected, public_selected, spatial_layer_store, map_style_value, callback_context, **kwargs) ¤

Build a scatter-mapbox figure for currently selected stations and layers.

Parameters:

Name Type Description Default
owned_selected Any

Selected station codes from the owned checklist.

required
public_selected Any

Selected station codes from the public checklist.

required
spatial_layer_store Any

Selected spatial layers and their visibility.

required
map_style_value str

Requested map style value.

required
callback_context Any

Dash callback context used to inspect trigger source.

required
**kwargs Any

Callback kwargs containing request context.

{}

Returns:

Type Description
Patch | object

Patch update for the map figure or no_update.

Source code in djangomain/dash_apps/stations_map.py
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
@app.callback(
    Output("map_graph", "figure"),
    [
        Input({"type": "checklist", "index": "owned"}, "value"),
        Input({"type": "checklist", "index": "public"}, "value"),
        Input("spatial-layer-store", "data"),
        Input("map-style-select", "value"),
    ],
)
def update_map(
    owned_selected: Any,
    public_selected: Any,
    spatial_layer_store: Any,
    map_style_value: str,
    callback_context: Any,
    **kwargs: Any,
) -> Patch | object:
    """Build a scatter-mapbox figure for currently selected stations and layers.

    Args:
        owned_selected: Selected station codes from the owned checklist.
        public_selected: Selected station codes from the public checklist.
        spatial_layer_store: Selected spatial layers and their visibility.
        map_style_value: Requested map style value.
        callback_context: Dash callback context used to inspect trigger source.
        **kwargs: Callback kwargs containing request context.

    Returns:
        Patch update for the map figure or no_update.
    """
    user = _get_request_user(kwargs)

    valid_styles = {option["value"] for option in _MAP_STYLE_OPTIONS}
    map_style = (
        map_style_value if map_style_value in valid_styles else _DEFAULT_MAP_STYLE
    )

    triggered_component = _triggered_component(callback_context)
    is_initial_call = not triggered_component

    patched = Patch()

    if (
        is_initial_call
        or '"type":"checklist"' in triggered_component
        or triggered_component == "spatial-layer-store"
    ):
        rows = _station_rows_for_codes(owned_selected, "My Stations")
        rows.extend(_station_rows_for_codes(public_selected, "Public"))

        df = pd.DataFrame(rows, columns=[*_STATION_KEYS, "type"])
        traces = []
        for group, color in _COLOR_MAP.items():
            sub = df[df["type"] == group]
            traces.append(
                go.Scattermapbox(
                    lat=sub["station_latitude"],
                    lon=sub["station_longitude"],
                    mode="markers",
                    marker={"color": color, "size": 10},
                    name=group,
                    hovertext=sub["station_code"],
                    hoverinfo="text",
                )
            )

        patched["data"] = traces

    if is_initial_call or triggered_component == "spatial-layer-store":
        available_layers = available_map_layers_by_id(user)
        spatial_layers_raw = [
            {"id": entry["id"], "visible": True}
            for entry in _normalise_spatial_layer_store(spatial_layer_store)
            if entry["visible"] and entry["id"] in available_layers
        ]
        patched["layout"]["mapbox"]["layers"] = build_mapbox_layers(
            spatial_layers_raw,
            user,
        )

    if is_initial_call or triggered_component == "map-style-select":
        patched["layout"]["mapbox"]["style"] = map_style
    if triggered_component == "map-style-select":
        patched["layout"]["mapbox"]["layers"] = []

    if patched == {}:
        return no_update

    return patched