Skip to content

maplibre module

Map

Bases: AnyWidget

Create a MapLibre map widget.

Source code in mapwidget/maplibre.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 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
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 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
 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
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
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
332
333
334
335
336
337
338
339
340
341
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
372
373
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
403
404
405
406
407
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
456
457
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
588
589
590
591
592
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
634
635
636
637
638
639
640
641
642
643
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
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
class Map(anywidget.AnyWidget):
    """Create a MapLibre map widget."""

    _cwd = os.path.dirname(os.path.abspath(__file__))
    _esm = pathlib.Path(os.path.join(_cwd, "js", "maplibre.js"))
    _css = pathlib.Path(os.path.join(_cwd, "css", "maplibre.css"))
    center = traitlets.List([0, 20]).tag(sync=True, o=True)
    zoom = traitlets.Float(2).tag(sync=True, o=True)
    bearing = traitlets.Float(0).tag(sync=True, o=True)
    pitch = traitlets.Float(0).tag(sync=True, o=True)
    bounds = traitlets.List([0, 0, 0, 0]).tag(sync=True, o=True)
    width = traitlets.Unicode("100%").tag(sync=True, o=True)
    height = traitlets.Unicode("600px").tag(sync=True, o=True)
    clicked_latlng = traitlets.List([None, None]).tag(sync=True, o=True)
    calls = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True, o=True)
    view_state = traitlets.Dict().tag(sync=True)
    root = traitlets.Dict().tag(sync=True)
    sources = traitlets.Dict().tag(sync=True)
    loaded = traitlets.Bool(False).tag(sync=True)
    controls = traitlets.List(traitlets.Dict(), default_value=[]).tag(sync=True, o=True)
    style = traitlets.Any().tag(sync=True)

    # Draw-related traitlets
    draw_features_selected = traitlets.List(traitlets.Dict(), default_value=[]).tag(
        sync=True
    )
    draw_feature_collection_all = traitlets.Dict(
        default_value={"type": "FeatureCollection", "features": []}
    ).tag(sync=True)
    draw_features_created = traitlets.List(traitlets.Dict(), default_value=[]).tag(
        sync=True
    )
    draw_features_updated = traitlets.List(traitlets.Dict(), default_value=[]).tag(
        sync=True
    )
    draw_features_deleted = traitlets.List(traitlets.Dict(), default_value=[]).tag(
        sync=True
    )
    draw_repeat_mode = traitlets.Bool(False).tag(sync=True)

    def __init__(
        self,
        center=[0, 20],
        zoom=2,
        bearing=0,
        pitch=0,
        style="https://tiles.openfreemap.org/styles/liberty",
        controls=None,
        **kwargs,
    ):
        """Initialize the Map widget.

        Args:
            center: Initial center [lng, lat]. Defaults to [0, 20]
            zoom: Initial zoom level. Defaults to 2
            controls: List of controls to add by default. Defaults to ["navigation", "fullscreen", "globe"]
            **kwargs: Additional widget parameters
        """
        self._draw_control_request = None

        super().__init__(
            center=center,
            zoom=zoom,
            bearing=bearing,
            pitch=pitch,
            style=style,
            **kwargs,
        )

        # Store default controls to add after initialization
        self._default_controls = (
            controls if controls is not None else ["navigation", "fullscreen", "globe"]
        )

        # Add default controls after widget is ready
        self.observe(self._add_default_controls, names="loaded")

    def _add_default_controls(self, change):
        """Add default controls when the map is loaded."""
        if change["new"] and self._default_controls:
            for control in self._default_controls:
                self.add_control(control, "top-right")
            self._default_controls = []  # Clear to avoid re-adding

        if self._draw_control_request:
            self.add_call("addDrawControl", self._draw_control_request)
            self._draw_control_request = None

        if hasattr(self, "_pending_draw_mode"):
            for mode in self._pending_draw_mode:
                self.add_call("setDrawMode", [mode])
            del self._pending_draw_mode

        if hasattr(self, "_pending_legend"):
            for targets, options, position in self._pending_legend:
                self.add_call("addLegendControl", [targets, options, position])
            del self._pending_legend

        if hasattr(self, "_pending_opacity"):
            for (
                baseLayers,
                overLayers,
                options,
                position,
                defaultVisibility,
            ) in self._pending_opacity:
                self.add_call(
                    "addOpacityControl",
                    [baseLayers, overLayers, options, position, defaultVisibility],
                )
            del self._pending_opacity

    @property
    def layers(self):
        """Get the current style of the map."""
        return self.root.get("layers", [])

    @property
    def layer_names(self):
        """Get the names of the layers in the map."""
        return [layer["id"] for layer in self.layers]

    def add_call(self, method: str, args: list = None, kwargs: dict = None):
        """Invoke a JS map method with arguments."""
        if args is None:
            args = []
        if kwargs is None:
            kwargs = {}
        self.calls = self.calls + [{"method": method, "args": args, "kwargs": kwargs}]

    def set_center(self, lng: float, lat: float):
        """Set the center of the map."""
        self.add_call("setCenter", [[lng, lat]])

    def set_zoom(self, zoom: float):
        """Set the zoom level."""
        self.add_call("setZoom", [zoom])

    def pan_to(self, lng: float, lat: float):
        """Pan the map to a given location."""
        self.add_call("panTo", [[lng, lat]])

    def fly_to(self, center=None, zoom=None, bearing=None, pitch=None):
        """Fly to a given location with optional zoom, bearing, and pitch."""
        options = {}
        if center:
            options["center"] = center
        if zoom is not None:
            options["zoom"] = zoom
        if bearing is not None:
            options["bearing"] = bearing
        if pitch is not None:
            options["pitch"] = pitch
        self.add_call("flyTo", [options])

    def fit_bounds(self, bounds: list, options: dict = None):
        """Fit the map to given bounds [[west, south], [east, north]]."""
        args = [bounds]
        if options:
            args.append(options)
        self.add_call("fitBounds", args)

    def set_pitch(self, pitch: float):
        """Set the pitch of the map."""
        self.add_call("setPitch", [pitch])

    def set_bearing(self, bearing: float):
        """Set the bearing of the map."""
        self.add_call("setBearing", [bearing])

    def resize(self):
        """Trigger map resize."""
        self.add_call("resize")

    def add_source(self, source_id: str, source: dict):
        """Add a new source to the map."""
        self.add_call("addSource", [source_id, source])

    def remove_source(self, source_id: str):
        """Remove a source from the map."""
        self.add_call("removeSource", [source_id])

    def add_layer(self, layer: dict, before_id: str = None):
        """Add a new layer to the map."""
        args = [layer]
        if before_id:
            args.append(before_id)
        self.add_call("addLayer", args)

    def remove_layer(self, layer_id: str):
        """Remove a layer from the map."""
        self.add_call("removeLayer", [layer_id])

    def set_paint_property(self, layer_id: str, prop: str, value):
        """Set a paint property on a layer."""
        self.add_call("setPaintProperty", [layer_id, prop, value])

    def set_layout_property(self, layer_id: str, prop: str, value):
        """Set a layout property on a layer."""
        self.add_call("setLayoutProperty", [layer_id, prop, value])

    def set_filter(self, layer_id: str, filter_expr):
        """Set a filter expression on a layer."""
        self.add_call("setFilter", [layer_id, filter_expr])

    def set_style(self, style_url: str):
        """Set the map style."""
        self.add_call("setStyle", [style_url])

    def set_layer_visibility(self, layer_id: str, visibility: str):
        """Set visibility of a layer ('visible' or 'none')."""
        self.set_layout_property(layer_id, "visibility", visibility)

    def add_control(
        self, control_type: str, position: str = "top-right", options: dict = None
    ):
        """Add a control to the map.

        Args:
            control_type: Type of control to add. Options include:
                - 'navigation' or 'NavigationControl'
                - 'geolocate' or 'GeolocateControl'
                - 'scale' or 'ScaleControl'
                - 'fullscreen' or 'FullscreenControl'
                - 'attribution' or 'AttributionControl'
                - 'globe' or 'GlobeControl'
                - 'logo' or 'LogoControl'
                - 'terrain' or 'TerrainControl'
            position: Position on the map. Options: 'top-left', 'top-right', 'bottom-left', 'bottom-right'
            options: Optional configuration for the control
        """
        if options is None:
            options = {}
        self.add_call("addControl", [control_type, position, options])
        self.controls.append(
            {"type": control_type, "position": position, "options": options}
        )

    def remove_control(self, control_type: str):
        """Remove a control from the map.

        Args:
            control_type: The type of control to remove (e.g., 'navigation', 'fullscreen')
        """
        self.add_call("removeControl", [control_type])
        self.controls = [
            control for control in self.controls if control["type"] != control_type
        ]

    def add_draw_control(
        self,
        options: Optional[Dict[str, Any]] = None,
        controls: Optional[Dict[str, Any]] = None,
        position: str = "top-right",
        geojson: Optional[Dict[str, Any]] = None,
        **kwargs: Any,
    ) -> None:
        """
        Adds a drawing control to the map.

        This method enables users to add interactive drawing controls to the map,
        allowing for the creation, editing, and deletion of geometric shapes on
        the map. The options, position, and initial GeoJSON can be customized.

        Args:
            options (Optional[Dict[str, Any]]): Configuration options for the
                drawing control. Defaults to None.
            controls (Optional[Dict[str, Any]]): The drawing controls to enable.
                Can be one or more of the following: 'polygon', 'line_string',
                'point', 'trash', 'combine_features', 'uncombine_features'.
                Defaults to None.
            position (str): The position of the control on the map. Defaults
                to "top-right".
            geojson (Optional[Dict[str, Any]]): Initial GeoJSON data to load
                into the drawing control. Defaults to None.
            **kwargs (Any): Additional keyword arguments to be passed to the
                drawing control.

        Returns:
            None
        """
        if options is None:
            options = {}
        if controls is None:
            controls = {}

        # Merge kwargs into options
        options.update(kwargs)

        if self.loaded:
            self.add_call("addDrawControl", [options, controls, position, geojson])
        else:
            self._draw_control_request = [options, controls, position, geojson]

    def remove_draw_control(self) -> None:
        """
        Removes the drawing control from the map.

        This method removes the drawing control and clears all associated
        draw features from the map and model.

        Returns:
            None
        """
        self.add_call("removeDrawControl")

    def draw_features_delete_all(self) -> None:
        """
        Deletes all features from the drawing control.

        This method removes all drawn features from the map and updates
        the model accordingly.

        Returns:
            None
        """
        self.add_call("drawFeaturesDeleteAll")

    def add_legend(
        self,
        targets: Dict[str, str],
        options: Optional[Dict[str, Any]] = None,
        position: str = "top-right",
    ) -> None:
        """
        Adds a legend control to the map using mapbox-gl-legend plugin.

        Args:
            targets (Dict[str, str]): A dictionary mapping layer IDs to display names
            options (Optional[Dict[str, Any]]): Configuration options for the legend control.
                Available options:
                - showDefault (bool): Whether to show default legend. Defaults to False.
                - showCheckbox (bool): Whether to show checkboxes. Defaults to False.
                - onlyRendered (bool): Whether to show only rendered layers. Defaults to True.
                - reverseOrder (bool): Whether to reverse the order. Defaults to True.
            position (str): The position of the control on the map. Defaults to "top-right".

        Returns:
            None
        """
        if options is None:
            options = {
                "showDefault": False,
                "showCheckbox": False,
                "onlyRendered": True,
                "reverseOrder": True,
            }

        if self.loaded:
            self.add_call("addLegendControl", [targets, options, position])
        else:
            if not hasattr(self, "_pending_legend"):
                self._pending_legend = []
            self._pending_legend.append((targets, options, position))

    def set_draw_mode(self, mode: str):
        """Set the drawing mode, even if the map is not yet loaded."""
        if self.loaded:
            self.add_call("setDrawMode", [mode])
        else:
            if not hasattr(self, "_pending_draw_mode"):
                self._pending_draw_mode = []
            self._pending_draw_mode.append(mode)

    def add_opacity_control(
        self,
        base_layers: Optional[Dict[str, str]] = None,
        over_layers: Optional[Dict[str, str]] = None,
        options: Optional[Dict[str, Any]] = None,
        position: str = "top-right",
        default_visibility: Optional[Dict[str, bool]] = None,
        collapsible: bool = False,
    ) -> None:
        """
        Adds an opacity control to the map using maplibre-gl-opacity plugin.

        This control allows users to adjust the opacity of multiple tile layers
        and switch between base layers and overlays.

        Args:
            base_layers (Optional[Dict[str, str]]): A dictionary mapping base layer IDs
                to display names. Only one base layer can be visible at a time.
            over_layers (Optional[Dict[str, str]]): A dictionary mapping overlay layer IDs
                to display names. Multiple overlays can be visible simultaneously.
            options (Optional[Dict[str, Any]]): Configuration options for the opacity control.
                Available options:
                - opacityControl (bool): Whether to show opacity sliders. Defaults to True.
            position (str): The position of the control on the map. Defaults to "top-right".
            default_visibility (Optional[Dict[str, bool]]): A dictionary mapping layer IDs
                to their default visibility state. If not specified, overlay layers will
                be visible by default and base layers will follow the first-wins rule.
            collapsible (bool): Whether to add a toggle button to show/hide the control.
                Defaults to False.

        Returns:
            None

        Example:
            ```python
            # Define base layers (mutually exclusive)
            base_layers = {
                "osm": "OpenStreetMap",
                "satellite": "Satellite"
            }

            # Define overlay layers (can be combined)
            over_layers = {
                "roads": "Roads",
                "labels": "Labels"
            }

            # Control which layers are visible by default
            default_visibility = {
                "osm": True,        # Base layer visible
                "roads": True,      # Overlay visible
                "labels": False     # Overlay hidden
            }

            m.add_opacity_control(
                base_layers=base_layers,
                over_layers=over_layers,
                default_visibility=default_visibility,
                collapsible=True,  # Add toggle button
                position="top-left"
            )
            ```
        """
        if base_layers is None:
            base_layers = {}
        if over_layers is None:
            over_layers = {}
        if options is None:
            options = {"opacityControl": True}
        if default_visibility is None:
            default_visibility = {}

        # Add collapsible option to the options dict
        if collapsible:
            options["collapsible"] = True

        if self.loaded:
            self.add_call(
                "addOpacityControl",
                [base_layers, over_layers, options, position, default_visibility],
            )
        else:
            if not hasattr(self, "_pending_opacity"):
                self._pending_opacity = []
            self._pending_opacity.append(
                (base_layers, over_layers, options, position, default_visibility)
            )

    def add_cog_layer(
        self,
        url: str,
        source_id: Optional[str] = None,
        layer_id: Optional[str] = None,
        source_options: Optional[Dict[str, Any]] = None,
        layer_options: Optional[Dict[str, Any]] = None,
    ) -> None:
        """
        Adds a Cloud Optimized GeoTIFF (COG) layer to the map.

        This method uses the maplibre-cog-protocol to add COG raster data as a layer
        on the map. The COG protocol allows for efficient streaming of large raster
        datasets directly from cloud storage.

        Args:
            url (str): The URL of the COG file to load.
            source_id (Optional[str]): The ID for the COG source. If not provided,
                a unique ID will be generated based on the URL.
            layer_id (Optional[str]): The ID for the COG layer. If not provided,
                a unique ID will be generated based on the source ID.
            source_options (Optional[Dict[str, Any]]): Additional options for the
                COG source. Common options include:
                - tileSize (int): Tile size in pixels. Defaults to 256.
                - maxzoom (int): Maximum zoom level for the source.
                - minzoom (int): Minimum zoom level for the source.
            layer_options (Optional[Dict[str, Any]]): Additional options for the
                COG layer. Common options include:
                - paint (Dict): Paint properties for the layer (e.g., opacity).
                - layout (Dict): Layout properties for the layer.
                - beforeId (str): ID of the layer before which to insert this layer.

        Returns:
            None

        Example:
            ```python
            # Add a simple COG layer
            m.add_cog_layer("https://example.com/data.tif")

            # Add a COG layer with custom IDs and options
            m.add_cog_layer(
                url="https://example.com/elevation.tif",
                source_id="elevation-source",
                layer_id="elevation-layer",
                source_options={"tileSize": 512, "maxzoom": 14},
                layer_options={"paint": {"raster-opacity": 0.8}}
            )
            ```

        Note:
            The COG file must be accessible via HTTP/HTTPS and should be properly
            formatted as a Cloud Optimized GeoTIFF with internal tiling and overviews.
        """
        if source_options is None:
            source_options = {}
        if layer_options is None:
            layer_options = {}

        # Generate unique IDs if not provided
        if source_id is None:
            source_id = f"cog-source-{uuid.uuid4().hex[:8]}"
        if layer_id is None:
            layer_id = f"cog-layer-{uuid.uuid4().hex[:8]}"

        # Add the COG layer
        self.add_call(
            "addCogLayer", [url, source_id, layer_id, source_options, layer_options]
        )

    def add_geojson(
        self,
        data: Union[str, Dict[str, Any]],
        source_id: Optional[str] = None,
        layer_id: Optional[str] = None,
        layer_type: Optional[str] = None,
        paint: Optional[Dict[str, Any]] = None,
        layout: Optional[Dict[str, Any]] = None,
        source_options: Optional[Dict[str, Any]] = None,
        fit_bounds: bool = True,
        before_id: Optional[str] = None,
    ) -> str:
        """Add GeoJSON data to the map as a source and layer.

        This method supports multiple input formats:
        - A GeoJSON dictionary (FeatureCollection, Feature, or Geometry)
        - A file path to a .geojson or .json file
        - A URL pointing to a GeoJSON resource
        - A GeoDataFrame (requires geopandas)

        The method automatically detects geometry types and creates appropriate
        map layers (fill for Polygon, line for LineString, circle for Point).
        For mixed-geometry FeatureCollections, multiple layers are created.

        Args:
            data: GeoJSON data as a dict, file path, URL string, or GeoDataFrame.
            source_id: ID for the GeoJSON source. Auto-generated if not provided.
            layer_id: Base ID for the layer(s). Auto-generated if not provided.
                For mixed-geometry data, suffixes like '-fill', '-line', '-circle'
                are appended.
            layer_type: Explicit layer type override ('fill', 'line', 'circle',
                'fill-extrusion', 'heatmap', 'symbol'). If not provided, the type
                is inferred from the geometry.
            paint: Paint properties for the layer. If not provided, sensible
                defaults are used based on the layer type.
            layout: Layout properties for the layer.
            source_options: Additional options for the GeoJSON source (e.g.,
                'cluster', 'clusterMaxZoom', 'clusterRadius', 'tolerance').
            fit_bounds: Whether to automatically fit the map to the data bounds.
                Defaults to True.
            before_id: ID of an existing layer to insert the new layer(s) before.

        Returns:
            str: The source ID used for the added data.

        Example:
            ```python
            # Add GeoJSON from a dictionary
            geojson = {
                "type": "FeatureCollection",
                "features": [
                    {
                        "type": "Feature",
                        "geometry": {"type": "Point", "coordinates": [-77.03, 38.90]},
                        "properties": {"name": "Washington DC"}
                    }
                ]
            }
            m.add_geojson(geojson)

            # Add GeoJSON from a URL
            m.add_geojson(
                "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson",
                paint={"fill-color": "#088", "fill-opacity": 0.5}
            )

            # Add GeoJSON from a file
            m.add_geojson("data/parcels.geojson")

            # Add from a GeoDataFrame
            import geopandas as gpd
            gdf = gpd.read_file("data/roads.shp")
            m.add_geojson(gdf, paint={"line-color": "red", "line-width": 2})

            # Add with clustering enabled
            m.add_geojson(
                geojson,
                source_options={"cluster": True, "clusterRadius": 50},
            )
            ```
        """
        # Resolve the data to a GeoJSON dict
        geojson = self._resolve_geojson(data)

        if source_options is None:
            source_options = {}
        if paint is None:
            paint = {}
        if layout is None:
            layout = {}

        # Generate unique IDs if not provided
        if source_id is None:
            source_id = f"geojson-{uuid.uuid4().hex[:8]}"
        if layer_id is None:
            layer_id = f"{source_id}-layer"

        # Detect geometry types present in the data
        geom_types = self._get_geometry_types(geojson)

        # Compute bounds for fit_bounds
        bounds = None
        if fit_bounds:
            bounds = self._compute_geojson_bounds(geojson)

        # Delegate to JS side for source + layer creation
        self.add_call(
            "addGeoJSON",
            [
                geojson,
                source_id,
                layer_id,
                layer_type,
                paint,
                layout,
                source_options,
                list(geom_types),
                bounds,
                before_id,
            ],
        )

        return source_id

    def _resolve_geojson(self, data) -> Dict[str, Any]:
        """Resolve various input types to a GeoJSON dictionary.

        Args:
            data: GeoJSON dict, file path string, URL string, or GeoDataFrame.

        Returns:
            dict: A valid GeoJSON dictionary.

        Raises:
            TypeError: If the data type is not supported.
            FileNotFoundError: If a file path doesn't exist.
        """
        # Handle GeoDataFrame
        if hasattr(data, "__geo_interface__"):
            geojson = data.__geo_interface__
            if isinstance(geojson, str):
                geojson = json.loads(geojson)
            return geojson

        # Handle dict (already GeoJSON)
        if isinstance(data, dict):
            return data

        # Handle string: file path or URL
        if isinstance(data, str):
            # URL
            if data.startswith(("http://", "https://")):
                return data  # Pass URL directly to JS for loading

            # File path
            if os.path.isfile(data):
                with open(data, "r") as f:
                    return json.load(f)
            else:
                raise FileNotFoundError(f"File not found: {data}")

        raise TypeError(
            f"Unsupported data type: {type(data).__name__}. "
            "Expected a GeoJSON dict, file path, URL, or GeoDataFrame."
        )

    @staticmethod
    def _get_geometry_types(geojson) -> set:
        """Extract unique geometry types from GeoJSON data.

        Args:
            geojson: A GeoJSON dictionary or URL string.

        Returns:
            set: A set of geometry type strings (e.g., {'Point', 'Polygon'}).
        """
        if isinstance(geojson, str):
            # URL — can't inspect, return empty (JS will handle)
            return set()

        types = set()

        if geojson.get("type") == "FeatureCollection":
            for feature in geojson.get("features", []):
                geom = feature.get("geometry", {})
                if geom:
                    types.add(geom.get("type", ""))
        elif geojson.get("type") == "Feature":
            geom = geojson.get("geometry", {})
            if geom:
                types.add(geom.get("type", ""))
        elif geojson.get("type") in (
            "Point",
            "MultiPoint",
            "LineString",
            "MultiLineString",
            "Polygon",
            "MultiPolygon",
            "GeometryCollection",
        ):
            types.add(geojson["type"])

        return types

    @staticmethod
    def _compute_geojson_bounds(geojson) -> Optional[List[float]]:
        """Compute bounding box [west, south, east, north] from GeoJSON.

        Args:
            geojson: A GeoJSON dictionary.

        Returns:
            list or None: [west, south, east, north] or None if bounds
            cannot be computed (e.g., URL input or empty data).
        """
        if isinstance(geojson, str):
            return None  # URL — JS will handle bounds

        coords = []

        def _extract_coords(obj):
            """Recursively extract all coordinate pairs."""
            if isinstance(obj, dict):
                if "coordinates" in obj:
                    _flatten_coords(obj["coordinates"])
                if "geometry" in obj:
                    _extract_coords(obj["geometry"])
                if "features" in obj:
                    for f in obj["features"]:
                        _extract_coords(f)
                if "geometries" in obj:
                    for g in obj["geometries"]:
                        _extract_coords(g)

        def _flatten_coords(c):
            """Flatten nested coordinate arrays to [lng, lat] pairs."""
            if not c:
                return
            if isinstance(c[0], (int, float)):
                coords.append(c[:2])  # [lng, lat]
            else:
                for item in c:
                    _flatten_coords(item)

        _extract_coords(geojson)

        if not coords:
            return None

        lngs = [c[0] for c in coords]
        lats = [c[1] for c in coords]

        return [min(lngs), min(lats), max(lngs), max(lats)]

    def add_stac_layer(
        self,
        url: str,
        asset_key: str = "visual",
        source_id: Optional[str] = None,
        layer_id: Optional[str] = None,
        source_options: Optional[Dict[str, Any]] = None,
        layer_options: Optional[Dict[str, Any]] = None,
        fit_bounds: bool = True,
    ) -> None:
        """Add a STAC (SpatioTemporal Asset Catalog) item's raster asset to the map.

        This method loads a STAC item from a URL, extracts the specified asset's
        COG href, and adds it as a raster layer using the COG protocol. Optionally
        fits the map to the item's bounding box.

        Args:
            url: URL to a STAC Item JSON (e.g.,
                'https://planetarycomputer.microsoft.com/api/stac/v1/collections/...')
            asset_key: The asset key to load from the STAC item. Common values
                include 'visual', 'B04', 'data', 'rendered_preview'.
                Defaults to 'visual'.
            source_id: ID for the raster source. Auto-generated if not provided.
            layer_id: ID for the raster layer. Auto-generated if not provided.
            source_options: Additional options for the raster source (e.g.,
                tileSize, maxzoom, minzoom).
            layer_options: Additional options for the raster layer (e.g.,
                paint properties like raster-opacity).
            fit_bounds: Whether to fit the map to the STAC item's bounding box.
                Defaults to True.

        Returns:
            None

        Example:
            ```python
            # Add a STAC item's visual asset
            m.add_stac_layer(
                "https://planetarycomputer.microsoft.com/api/stac/v1/collections/"
                "sentinel-2-l2a/items/S2A_MSIL2A_20230101T100401_R022_T33UUP_20230101T121000"
            )

            # Add a specific band with custom options
            m.add_stac_layer(
                url="https://example.com/stac/item.json",
                asset_key="B04",
                layer_options={"paint": {"raster-opacity": 0.7}},
            )
            ```

        Note:
            The STAC item must contain the specified asset key, and the asset
            must have an 'href' pointing to a Cloud Optimized GeoTIFF (COG).
        """
        if source_options is None:
            source_options = {}
        if layer_options is None:
            layer_options = {}

        if source_id is None:
            source_id = f"stac-source-{uuid.uuid4().hex[:8]}"
        if layer_id is None:
            layer_id = f"stac-layer-{uuid.uuid4().hex[:8]}"

        self.add_call(
            "addStacLayer",
            [
                url,
                asset_key,
                source_id,
                layer_id,
                source_options,
                layer_options,
                fit_bounds,
            ],
        )

layer_names property

Get the names of the layers in the map.

layers property

Get the current style of the map.

__init__(center=[0, 20], zoom=2, bearing=0, pitch=0, style='https://tiles.openfreemap.org/styles/liberty', controls=None, **kwargs)

Initialize the Map widget.

Parameters:

Name Type Description Default
center

Initial center [lng, lat]. Defaults to [0, 20]

[0, 20]
zoom

Initial zoom level. Defaults to 2

2
controls

List of controls to add by default. Defaults to ["navigation", "fullscreen", "globe"]

None
**kwargs

Additional widget parameters

{}
Source code in mapwidget/maplibre.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def __init__(
    self,
    center=[0, 20],
    zoom=2,
    bearing=0,
    pitch=0,
    style="https://tiles.openfreemap.org/styles/liberty",
    controls=None,
    **kwargs,
):
    """Initialize the Map widget.

    Args:
        center: Initial center [lng, lat]. Defaults to [0, 20]
        zoom: Initial zoom level. Defaults to 2
        controls: List of controls to add by default. Defaults to ["navigation", "fullscreen", "globe"]
        **kwargs: Additional widget parameters
    """
    self._draw_control_request = None

    super().__init__(
        center=center,
        zoom=zoom,
        bearing=bearing,
        pitch=pitch,
        style=style,
        **kwargs,
    )

    # Store default controls to add after initialization
    self._default_controls = (
        controls if controls is not None else ["navigation", "fullscreen", "globe"]
    )

    # Add default controls after widget is ready
    self.observe(self._add_default_controls, names="loaded")

add_call(method, args=None, kwargs=None)

Invoke a JS map method with arguments.

Source code in mapwidget/maplibre.py
132
133
134
135
136
137
138
def add_call(self, method: str, args: list = None, kwargs: dict = None):
    """Invoke a JS map method with arguments."""
    if args is None:
        args = []
    if kwargs is None:
        kwargs = {}
    self.calls = self.calls + [{"method": method, "args": args, "kwargs": kwargs}]

add_cog_layer(url, source_id=None, layer_id=None, source_options=None, layer_options=None)

Adds a Cloud Optimized GeoTIFF (COG) layer to the map.

This method uses the maplibre-cog-protocol to add COG raster data as a layer on the map. The COG protocol allows for efficient streaming of large raster datasets directly from cloud storage.

Parameters:

Name Type Description Default
url str

The URL of the COG file to load.

required
source_id Optional[str]

The ID for the COG source. If not provided, a unique ID will be generated based on the URL.

None
layer_id Optional[str]

The ID for the COG layer. If not provided, a unique ID will be generated based on the source ID.

None
source_options Optional[Dict[str, Any]]

Additional options for the COG source. Common options include: - tileSize (int): Tile size in pixels. Defaults to 256. - maxzoom (int): Maximum zoom level for the source. - minzoom (int): Minimum zoom level for the source.

None
layer_options Optional[Dict[str, Any]]

Additional options for the COG layer. Common options include: - paint (Dict): Paint properties for the layer (e.g., opacity). - layout (Dict): Layout properties for the layer. - beforeId (str): ID of the layer before which to insert this layer.

None

Returns:

Type Description
None

None

Example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Add a simple COG layer
m.add_cog_layer("https://example.com/data.tif")

# Add a COG layer with custom IDs and options
m.add_cog_layer(
    url="https://example.com/elevation.tif",
    source_id="elevation-source",
    layer_id="elevation-layer",
    source_options={"tileSize": 512, "maxzoom": 14},
    layer_options={"paint": {"raster-opacity": 0.8}}
)
Note

The COG file must be accessible via HTTP/HTTPS and should be properly formatted as a Cloud Optimized GeoTIFF with internal tiling and overviews.

Source code in mapwidget/maplibre.py
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
def add_cog_layer(
    self,
    url: str,
    source_id: Optional[str] = None,
    layer_id: Optional[str] = None,
    source_options: Optional[Dict[str, Any]] = None,
    layer_options: Optional[Dict[str, Any]] = None,
) -> None:
    """
    Adds a Cloud Optimized GeoTIFF (COG) layer to the map.

    This method uses the maplibre-cog-protocol to add COG raster data as a layer
    on the map. The COG protocol allows for efficient streaming of large raster
    datasets directly from cloud storage.

    Args:
        url (str): The URL of the COG file to load.
        source_id (Optional[str]): The ID for the COG source. If not provided,
            a unique ID will be generated based on the URL.
        layer_id (Optional[str]): The ID for the COG layer. If not provided,
            a unique ID will be generated based on the source ID.
        source_options (Optional[Dict[str, Any]]): Additional options for the
            COG source. Common options include:
            - tileSize (int): Tile size in pixels. Defaults to 256.
            - maxzoom (int): Maximum zoom level for the source.
            - minzoom (int): Minimum zoom level for the source.
        layer_options (Optional[Dict[str, Any]]): Additional options for the
            COG layer. Common options include:
            - paint (Dict): Paint properties for the layer (e.g., opacity).
            - layout (Dict): Layout properties for the layer.
            - beforeId (str): ID of the layer before which to insert this layer.

    Returns:
        None

    Example:
        ```python
        # Add a simple COG layer
        m.add_cog_layer("https://example.com/data.tif")

        # Add a COG layer with custom IDs and options
        m.add_cog_layer(
            url="https://example.com/elevation.tif",
            source_id="elevation-source",
            layer_id="elevation-layer",
            source_options={"tileSize": 512, "maxzoom": 14},
            layer_options={"paint": {"raster-opacity": 0.8}}
        )
        ```

    Note:
        The COG file must be accessible via HTTP/HTTPS and should be properly
        formatted as a Cloud Optimized GeoTIFF with internal tiling and overviews.
    """
    if source_options is None:
        source_options = {}
    if layer_options is None:
        layer_options = {}

    # Generate unique IDs if not provided
    if source_id is None:
        source_id = f"cog-source-{uuid.uuid4().hex[:8]}"
    if layer_id is None:
        layer_id = f"cog-layer-{uuid.uuid4().hex[:8]}"

    # Add the COG layer
    self.add_call(
        "addCogLayer", [url, source_id, layer_id, source_options, layer_options]
    )

add_control(control_type, position='top-right', options=None)

Add a control to the map.

Parameters:

Name Type Description Default
control_type str

Type of control to add. Options include: - 'navigation' or 'NavigationControl' - 'geolocate' or 'GeolocateControl' - 'scale' or 'ScaleControl' - 'fullscreen' or 'FullscreenControl' - 'attribution' or 'AttributionControl' - 'globe' or 'GlobeControl' - 'logo' or 'LogoControl' - 'terrain' or 'TerrainControl'

required
position str

Position on the map. Options: 'top-left', 'top-right', 'bottom-left', 'bottom-right'

'top-right'
options dict

Optional configuration for the control

None
Source code in mapwidget/maplibre.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def add_control(
    self, control_type: str, position: str = "top-right", options: dict = None
):
    """Add a control to the map.

    Args:
        control_type: Type of control to add. Options include:
            - 'navigation' or 'NavigationControl'
            - 'geolocate' or 'GeolocateControl'
            - 'scale' or 'ScaleControl'
            - 'fullscreen' or 'FullscreenControl'
            - 'attribution' or 'AttributionControl'
            - 'globe' or 'GlobeControl'
            - 'logo' or 'LogoControl'
            - 'terrain' or 'TerrainControl'
        position: Position on the map. Options: 'top-left', 'top-right', 'bottom-left', 'bottom-right'
        options: Optional configuration for the control
    """
    if options is None:
        options = {}
    self.add_call("addControl", [control_type, position, options])
    self.controls.append(
        {"type": control_type, "position": position, "options": options}
    )

add_draw_control(options=None, controls=None, position='top-right', geojson=None, **kwargs)

Adds a drawing control to the map.

This method enables users to add interactive drawing controls to the map, allowing for the creation, editing, and deletion of geometric shapes on the map. The options, position, and initial GeoJSON can be customized.

Parameters:

Name Type Description Default
options Optional[Dict[str, Any]]

Configuration options for the drawing control. Defaults to None.

None
controls Optional[Dict[str, Any]]

The drawing controls to enable. Can be one or more of the following: 'polygon', 'line_string', 'point', 'trash', 'combine_features', 'uncombine_features'. Defaults to None.

None
position str

The position of the control on the map. Defaults to "top-right".

'top-right'
geojson Optional[Dict[str, Any]]

Initial GeoJSON data to load into the drawing control. Defaults to None.

None
**kwargs Any

Additional keyword arguments to be passed to the drawing control.

{}

Returns:

Type Description
None

None

Source code in mapwidget/maplibre.py
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
290
291
292
293
294
295
296
297
298
299
300
301
302
def add_draw_control(
    self,
    options: Optional[Dict[str, Any]] = None,
    controls: Optional[Dict[str, Any]] = None,
    position: str = "top-right",
    geojson: Optional[Dict[str, Any]] = None,
    **kwargs: Any,
) -> None:
    """
    Adds a drawing control to the map.

    This method enables users to add interactive drawing controls to the map,
    allowing for the creation, editing, and deletion of geometric shapes on
    the map. The options, position, and initial GeoJSON can be customized.

    Args:
        options (Optional[Dict[str, Any]]): Configuration options for the
            drawing control. Defaults to None.
        controls (Optional[Dict[str, Any]]): The drawing controls to enable.
            Can be one or more of the following: 'polygon', 'line_string',
            'point', 'trash', 'combine_features', 'uncombine_features'.
            Defaults to None.
        position (str): The position of the control on the map. Defaults
            to "top-right".
        geojson (Optional[Dict[str, Any]]): Initial GeoJSON data to load
            into the drawing control. Defaults to None.
        **kwargs (Any): Additional keyword arguments to be passed to the
            drawing control.

    Returns:
        None
    """
    if options is None:
        options = {}
    if controls is None:
        controls = {}

    # Merge kwargs into options
    options.update(kwargs)

    if self.loaded:
        self.add_call("addDrawControl", [options, controls, position, geojson])
    else:
        self._draw_control_request = [options, controls, position, geojson]

add_geojson(data, source_id=None, layer_id=None, layer_type=None, paint=None, layout=None, source_options=None, fit_bounds=True, before_id=None)

Add GeoJSON data to the map as a source and layer.

This method supports multiple input formats: - A GeoJSON dictionary (FeatureCollection, Feature, or Geometry) - A file path to a .geojson or .json file - A URL pointing to a GeoJSON resource - A GeoDataFrame (requires geopandas)

The method automatically detects geometry types and creates appropriate map layers (fill for Polygon, line for LineString, circle for Point). For mixed-geometry FeatureCollections, multiple layers are created.

Parameters:

Name Type Description Default
data Union[str, Dict[str, Any]]

GeoJSON data as a dict, file path, URL string, or GeoDataFrame.

required
source_id Optional[str]

ID for the GeoJSON source. Auto-generated if not provided.

None
layer_id Optional[str]

Base ID for the layer(s). Auto-generated if not provided. For mixed-geometry data, suffixes like '-fill', '-line', '-circle' are appended.

None
layer_type Optional[str]

Explicit layer type override ('fill', 'line', 'circle', 'fill-extrusion', 'heatmap', 'symbol'). If not provided, the type is inferred from the geometry.

None
paint Optional[Dict[str, Any]]

Paint properties for the layer. If not provided, sensible defaults are used based on the layer type.

None
layout Optional[Dict[str, Any]]

Layout properties for the layer.

None
source_options Optional[Dict[str, Any]]

Additional options for the GeoJSON source (e.g., 'cluster', 'clusterMaxZoom', 'clusterRadius', 'tolerance').

None
fit_bounds bool

Whether to automatically fit the map to the data bounds. Defaults to True.

True
before_id Optional[str]

ID of an existing layer to insert the new layer(s) before.

None

Returns:

Name Type Description
str str

The source ID used for the added data.

Example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Add GeoJSON from a dictionary
geojson = {
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "geometry": {"type": "Point", "coordinates": [-77.03, 38.90]},
            "properties": {"name": "Washington DC"}
        }
    ]
}
m.add_geojson(geojson)

# Add GeoJSON from a URL
m.add_geojson(
    "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson",
    paint={"fill-color": "#088", "fill-opacity": 0.5}
)

# Add GeoJSON from a file
m.add_geojson("data/parcels.geojson")

# Add from a GeoDataFrame
import geopandas as gpd
gdf = gpd.read_file("data/roads.shp")
m.add_geojson(gdf, paint={"line-color": "red", "line-width": 2})

# Add with clustering enabled
m.add_geojson(
    geojson,
    source_options={"cluster": True, "clusterRadius": 50},
)
Source code in mapwidget/maplibre.py
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
588
589
590
591
592
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
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
def add_geojson(
    self,
    data: Union[str, Dict[str, Any]],
    source_id: Optional[str] = None,
    layer_id: Optional[str] = None,
    layer_type: Optional[str] = None,
    paint: Optional[Dict[str, Any]] = None,
    layout: Optional[Dict[str, Any]] = None,
    source_options: Optional[Dict[str, Any]] = None,
    fit_bounds: bool = True,
    before_id: Optional[str] = None,
) -> str:
    """Add GeoJSON data to the map as a source and layer.

    This method supports multiple input formats:
    - A GeoJSON dictionary (FeatureCollection, Feature, or Geometry)
    - A file path to a .geojson or .json file
    - A URL pointing to a GeoJSON resource
    - A GeoDataFrame (requires geopandas)

    The method automatically detects geometry types and creates appropriate
    map layers (fill for Polygon, line for LineString, circle for Point).
    For mixed-geometry FeatureCollections, multiple layers are created.

    Args:
        data: GeoJSON data as a dict, file path, URL string, or GeoDataFrame.
        source_id: ID for the GeoJSON source. Auto-generated if not provided.
        layer_id: Base ID for the layer(s). Auto-generated if not provided.
            For mixed-geometry data, suffixes like '-fill', '-line', '-circle'
            are appended.
        layer_type: Explicit layer type override ('fill', 'line', 'circle',
            'fill-extrusion', 'heatmap', 'symbol'). If not provided, the type
            is inferred from the geometry.
        paint: Paint properties for the layer. If not provided, sensible
            defaults are used based on the layer type.
        layout: Layout properties for the layer.
        source_options: Additional options for the GeoJSON source (e.g.,
            'cluster', 'clusterMaxZoom', 'clusterRadius', 'tolerance').
        fit_bounds: Whether to automatically fit the map to the data bounds.
            Defaults to True.
        before_id: ID of an existing layer to insert the new layer(s) before.

    Returns:
        str: The source ID used for the added data.

    Example:
        ```python
        # Add GeoJSON from a dictionary
        geojson = {
            "type": "FeatureCollection",
            "features": [
                {
                    "type": "Feature",
                    "geometry": {"type": "Point", "coordinates": [-77.03, 38.90]},
                    "properties": {"name": "Washington DC"}
                }
            ]
        }
        m.add_geojson(geojson)

        # Add GeoJSON from a URL
        m.add_geojson(
            "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson",
            paint={"fill-color": "#088", "fill-opacity": 0.5}
        )

        # Add GeoJSON from a file
        m.add_geojson("data/parcels.geojson")

        # Add from a GeoDataFrame
        import geopandas as gpd
        gdf = gpd.read_file("data/roads.shp")
        m.add_geojson(gdf, paint={"line-color": "red", "line-width": 2})

        # Add with clustering enabled
        m.add_geojson(
            geojson,
            source_options={"cluster": True, "clusterRadius": 50},
        )
        ```
    """
    # Resolve the data to a GeoJSON dict
    geojson = self._resolve_geojson(data)

    if source_options is None:
        source_options = {}
    if paint is None:
        paint = {}
    if layout is None:
        layout = {}

    # Generate unique IDs if not provided
    if source_id is None:
        source_id = f"geojson-{uuid.uuid4().hex[:8]}"
    if layer_id is None:
        layer_id = f"{source_id}-layer"

    # Detect geometry types present in the data
    geom_types = self._get_geometry_types(geojson)

    # Compute bounds for fit_bounds
    bounds = None
    if fit_bounds:
        bounds = self._compute_geojson_bounds(geojson)

    # Delegate to JS side for source + layer creation
    self.add_call(
        "addGeoJSON",
        [
            geojson,
            source_id,
            layer_id,
            layer_type,
            paint,
            layout,
            source_options,
            list(geom_types),
            bounds,
            before_id,
        ],
    )

    return source_id

add_layer(layer, before_id=None)

Add a new layer to the map.

Source code in mapwidget/maplibre.py
192
193
194
195
196
197
def add_layer(self, layer: dict, before_id: str = None):
    """Add a new layer to the map."""
    args = [layer]
    if before_id:
        args.append(before_id)
    self.add_call("addLayer", args)

add_legend(targets, options=None, position='top-right')

Adds a legend control to the map using mapbox-gl-legend plugin.

Parameters:

Name Type Description Default
targets Dict[str, str]

A dictionary mapping layer IDs to display names

required
options Optional[Dict[str, Any]]

Configuration options for the legend control. Available options: - showDefault (bool): Whether to show default legend. Defaults to False. - showCheckbox (bool): Whether to show checkboxes. Defaults to False. - onlyRendered (bool): Whether to show only rendered layers. Defaults to True. - reverseOrder (bool): Whether to reverse the order. Defaults to True.

None
position str

The position of the control on the map. Defaults to "top-right".

'top-right'

Returns:

Type Description
None

None

Source code in mapwidget/maplibre.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def add_legend(
    self,
    targets: Dict[str, str],
    options: Optional[Dict[str, Any]] = None,
    position: str = "top-right",
) -> None:
    """
    Adds a legend control to the map using mapbox-gl-legend plugin.

    Args:
        targets (Dict[str, str]): A dictionary mapping layer IDs to display names
        options (Optional[Dict[str, Any]]): Configuration options for the legend control.
            Available options:
            - showDefault (bool): Whether to show default legend. Defaults to False.
            - showCheckbox (bool): Whether to show checkboxes. Defaults to False.
            - onlyRendered (bool): Whether to show only rendered layers. Defaults to True.
            - reverseOrder (bool): Whether to reverse the order. Defaults to True.
        position (str): The position of the control on the map. Defaults to "top-right".

    Returns:
        None
    """
    if options is None:
        options = {
            "showDefault": False,
            "showCheckbox": False,
            "onlyRendered": True,
            "reverseOrder": True,
        }

    if self.loaded:
        self.add_call("addLegendControl", [targets, options, position])
    else:
        if not hasattr(self, "_pending_legend"):
            self._pending_legend = []
        self._pending_legend.append((targets, options, position))

add_opacity_control(base_layers=None, over_layers=None, options=None, position='top-right', default_visibility=None, collapsible=False)

Adds an opacity control to the map using maplibre-gl-opacity plugin.

This control allows users to adjust the opacity of multiple tile layers and switch between base layers and overlays.

Parameters:

Name Type Description Default
base_layers Optional[Dict[str, str]]

A dictionary mapping base layer IDs to display names. Only one base layer can be visible at a time.

None
over_layers Optional[Dict[str, str]]

A dictionary mapping overlay layer IDs to display names. Multiple overlays can be visible simultaneously.

None
options Optional[Dict[str, Any]]

Configuration options for the opacity control. Available options: - opacityControl (bool): Whether to show opacity sliders. Defaults to True.

None
position str

The position of the control on the map. Defaults to "top-right".

'top-right'
default_visibility Optional[Dict[str, bool]]

A dictionary mapping layer IDs to their default visibility state. If not specified, overlay layers will be visible by default and base layers will follow the first-wins rule.

None
collapsible bool

Whether to add a toggle button to show/hide the control. Defaults to False.

False

Returns:

Type Description
None

None

Example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Define base layers (mutually exclusive)
base_layers = {
    "osm": "OpenStreetMap",
    "satellite": "Satellite"
}

# Define overlay layers (can be combined)
over_layers = {
    "roads": "Roads",
    "labels": "Labels"
}

# Control which layers are visible by default
default_visibility = {
    "osm": True,        # Base layer visible
    "roads": True,      # Overlay visible
    "labels": False     # Overlay hidden
}

m.add_opacity_control(
    base_layers=base_layers,
    over_layers=over_layers,
    default_visibility=default_visibility,
    collapsible=True,  # Add toggle button
    position="top-left"
)
Source code in mapwidget/maplibre.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
403
404
405
406
407
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
456
457
458
459
460
def add_opacity_control(
    self,
    base_layers: Optional[Dict[str, str]] = None,
    over_layers: Optional[Dict[str, str]] = None,
    options: Optional[Dict[str, Any]] = None,
    position: str = "top-right",
    default_visibility: Optional[Dict[str, bool]] = None,
    collapsible: bool = False,
) -> None:
    """
    Adds an opacity control to the map using maplibre-gl-opacity plugin.

    This control allows users to adjust the opacity of multiple tile layers
    and switch between base layers and overlays.

    Args:
        base_layers (Optional[Dict[str, str]]): A dictionary mapping base layer IDs
            to display names. Only one base layer can be visible at a time.
        over_layers (Optional[Dict[str, str]]): A dictionary mapping overlay layer IDs
            to display names. Multiple overlays can be visible simultaneously.
        options (Optional[Dict[str, Any]]): Configuration options for the opacity control.
            Available options:
            - opacityControl (bool): Whether to show opacity sliders. Defaults to True.
        position (str): The position of the control on the map. Defaults to "top-right".
        default_visibility (Optional[Dict[str, bool]]): A dictionary mapping layer IDs
            to their default visibility state. If not specified, overlay layers will
            be visible by default and base layers will follow the first-wins rule.
        collapsible (bool): Whether to add a toggle button to show/hide the control.
            Defaults to False.

    Returns:
        None

    Example:
        ```python
        # Define base layers (mutually exclusive)
        base_layers = {
            "osm": "OpenStreetMap",
            "satellite": "Satellite"
        }

        # Define overlay layers (can be combined)
        over_layers = {
            "roads": "Roads",
            "labels": "Labels"
        }

        # Control which layers are visible by default
        default_visibility = {
            "osm": True,        # Base layer visible
            "roads": True,      # Overlay visible
            "labels": False     # Overlay hidden
        }

        m.add_opacity_control(
            base_layers=base_layers,
            over_layers=over_layers,
            default_visibility=default_visibility,
            collapsible=True,  # Add toggle button
            position="top-left"
        )
        ```
    """
    if base_layers is None:
        base_layers = {}
    if over_layers is None:
        over_layers = {}
    if options is None:
        options = {"opacityControl": True}
    if default_visibility is None:
        default_visibility = {}

    # Add collapsible option to the options dict
    if collapsible:
        options["collapsible"] = True

    if self.loaded:
        self.add_call(
            "addOpacityControl",
            [base_layers, over_layers, options, position, default_visibility],
        )
    else:
        if not hasattr(self, "_pending_opacity"):
            self._pending_opacity = []
        self._pending_opacity.append(
            (base_layers, over_layers, options, position, default_visibility)
        )

add_source(source_id, source)

Add a new source to the map.

Source code in mapwidget/maplibre.py
184
185
186
def add_source(self, source_id: str, source: dict):
    """Add a new source to the map."""
    self.add_call("addSource", [source_id, source])

add_stac_layer(url, asset_key='visual', source_id=None, layer_id=None, source_options=None, layer_options=None, fit_bounds=True)

Add a STAC (SpatioTemporal Asset Catalog) item's raster asset to the map.

This method loads a STAC item from a URL, extracts the specified asset's COG href, and adds it as a raster layer using the COG protocol. Optionally fits the map to the item's bounding box.

Parameters:

Name Type Description Default
url str

URL to a STAC Item JSON (e.g., 'https://planetarycomputer.microsoft.com/api/stac/v1/collections/...')

required
asset_key str

The asset key to load from the STAC item. Common values include 'visual', 'B04', 'data', 'rendered_preview'. Defaults to 'visual'.

'visual'
source_id Optional[str]

ID for the raster source. Auto-generated if not provided.

None
layer_id Optional[str]

ID for the raster layer. Auto-generated if not provided.

None
source_options Optional[Dict[str, Any]]

Additional options for the raster source (e.g., tileSize, maxzoom, minzoom).

None
layer_options Optional[Dict[str, Any]]

Additional options for the raster layer (e.g., paint properties like raster-opacity).

None
fit_bounds bool

Whether to fit the map to the STAC item's bounding box. Defaults to True.

True

Returns:

Type Description
None

None

Example
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Add a STAC item's visual asset
m.add_stac_layer(
    "https://planetarycomputer.microsoft.com/api/stac/v1/collections/"
    "sentinel-2-l2a/items/S2A_MSIL2A_20230101T100401_R022_T33UUP_20230101T121000"
)

# Add a specific band with custom options
m.add_stac_layer(
    url="https://example.com/stac/item.json",
    asset_key="B04",
    layer_options={"paint": {"raster-opacity": 0.7}},
)
Note

The STAC item must contain the specified asset key, and the asset must have an 'href' pointing to a Cloud Optimized GeoTIFF (COG).

Source code in mapwidget/maplibre.py
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
def add_stac_layer(
    self,
    url: str,
    asset_key: str = "visual",
    source_id: Optional[str] = None,
    layer_id: Optional[str] = None,
    source_options: Optional[Dict[str, Any]] = None,
    layer_options: Optional[Dict[str, Any]] = None,
    fit_bounds: bool = True,
) -> None:
    """Add a STAC (SpatioTemporal Asset Catalog) item's raster asset to the map.

    This method loads a STAC item from a URL, extracts the specified asset's
    COG href, and adds it as a raster layer using the COG protocol. Optionally
    fits the map to the item's bounding box.

    Args:
        url: URL to a STAC Item JSON (e.g.,
            'https://planetarycomputer.microsoft.com/api/stac/v1/collections/...')
        asset_key: The asset key to load from the STAC item. Common values
            include 'visual', 'B04', 'data', 'rendered_preview'.
            Defaults to 'visual'.
        source_id: ID for the raster source. Auto-generated if not provided.
        layer_id: ID for the raster layer. Auto-generated if not provided.
        source_options: Additional options for the raster source (e.g.,
            tileSize, maxzoom, minzoom).
        layer_options: Additional options for the raster layer (e.g.,
            paint properties like raster-opacity).
        fit_bounds: Whether to fit the map to the STAC item's bounding box.
            Defaults to True.

    Returns:
        None

    Example:
        ```python
        # Add a STAC item's visual asset
        m.add_stac_layer(
            "https://planetarycomputer.microsoft.com/api/stac/v1/collections/"
            "sentinel-2-l2a/items/S2A_MSIL2A_20230101T100401_R022_T33UUP_20230101T121000"
        )

        # Add a specific band with custom options
        m.add_stac_layer(
            url="https://example.com/stac/item.json",
            asset_key="B04",
            layer_options={"paint": {"raster-opacity": 0.7}},
        )
        ```

    Note:
        The STAC item must contain the specified asset key, and the asset
        must have an 'href' pointing to a Cloud Optimized GeoTIFF (COG).
    """
    if source_options is None:
        source_options = {}
    if layer_options is None:
        layer_options = {}

    if source_id is None:
        source_id = f"stac-source-{uuid.uuid4().hex[:8]}"
    if layer_id is None:
        layer_id = f"stac-layer-{uuid.uuid4().hex[:8]}"

    self.add_call(
        "addStacLayer",
        [
            url,
            asset_key,
            source_id,
            layer_id,
            source_options,
            layer_options,
            fit_bounds,
        ],
    )

draw_features_delete_all()

Deletes all features from the drawing control.

This method removes all drawn features from the map and updates the model accordingly.

Returns:

Type Description
None

None

Source code in mapwidget/maplibre.py
316
317
318
319
320
321
322
323
324
325
326
def draw_features_delete_all(self) -> None:
    """
    Deletes all features from the drawing control.

    This method removes all drawn features from the map and updates
    the model accordingly.

    Returns:
        None
    """
    self.add_call("drawFeaturesDeleteAll")

fit_bounds(bounds, options=None)

Fit the map to given bounds [[west, south], [east, north]].

Source code in mapwidget/maplibre.py
165
166
167
168
169
170
def fit_bounds(self, bounds: list, options: dict = None):
    """Fit the map to given bounds [[west, south], [east, north]]."""
    args = [bounds]
    if options:
        args.append(options)
    self.add_call("fitBounds", args)

fly_to(center=None, zoom=None, bearing=None, pitch=None)

Fly to a given location with optional zoom, bearing, and pitch.

Source code in mapwidget/maplibre.py
152
153
154
155
156
157
158
159
160
161
162
163
def fly_to(self, center=None, zoom=None, bearing=None, pitch=None):
    """Fly to a given location with optional zoom, bearing, and pitch."""
    options = {}
    if center:
        options["center"] = center
    if zoom is not None:
        options["zoom"] = zoom
    if bearing is not None:
        options["bearing"] = bearing
    if pitch is not None:
        options["pitch"] = pitch
    self.add_call("flyTo", [options])

pan_to(lng, lat)

Pan the map to a given location.

Source code in mapwidget/maplibre.py
148
149
150
def pan_to(self, lng: float, lat: float):
    """Pan the map to a given location."""
    self.add_call("panTo", [[lng, lat]])

remove_control(control_type)

Remove a control from the map.

Parameters:

Name Type Description Default
control_type str

The type of control to remove (e.g., 'navigation', 'fullscreen')

required
Source code in mapwidget/maplibre.py
248
249
250
251
252
253
254
255
256
257
def remove_control(self, control_type: str):
    """Remove a control from the map.

    Args:
        control_type: The type of control to remove (e.g., 'navigation', 'fullscreen')
    """
    self.add_call("removeControl", [control_type])
    self.controls = [
        control for control in self.controls if control["type"] != control_type
    ]

remove_draw_control()

Removes the drawing control from the map.

This method removes the drawing control and clears all associated draw features from the map and model.

Returns:

Type Description
None

None

Source code in mapwidget/maplibre.py
304
305
306
307
308
309
310
311
312
313
314
def remove_draw_control(self) -> None:
    """
    Removes the drawing control from the map.

    This method removes the drawing control and clears all associated
    draw features from the map and model.

    Returns:
        None
    """
    self.add_call("removeDrawControl")

remove_layer(layer_id)

Remove a layer from the map.

Source code in mapwidget/maplibre.py
199
200
201
def remove_layer(self, layer_id: str):
    """Remove a layer from the map."""
    self.add_call("removeLayer", [layer_id])

remove_source(source_id)

Remove a source from the map.

Source code in mapwidget/maplibre.py
188
189
190
def remove_source(self, source_id: str):
    """Remove a source from the map."""
    self.add_call("removeSource", [source_id])

resize()

Trigger map resize.

Source code in mapwidget/maplibre.py
180
181
182
def resize(self):
    """Trigger map resize."""
    self.add_call("resize")

set_bearing(bearing)

Set the bearing of the map.

Source code in mapwidget/maplibre.py
176
177
178
def set_bearing(self, bearing: float):
    """Set the bearing of the map."""
    self.add_call("setBearing", [bearing])

set_center(lng, lat)

Set the center of the map.

Source code in mapwidget/maplibre.py
140
141
142
def set_center(self, lng: float, lat: float):
    """Set the center of the map."""
    self.add_call("setCenter", [[lng, lat]])

set_draw_mode(mode)

Set the drawing mode, even if the map is not yet loaded.

Source code in mapwidget/maplibre.py
365
366
367
368
369
370
371
372
def set_draw_mode(self, mode: str):
    """Set the drawing mode, even if the map is not yet loaded."""
    if self.loaded:
        self.add_call("setDrawMode", [mode])
    else:
        if not hasattr(self, "_pending_draw_mode"):
            self._pending_draw_mode = []
        self._pending_draw_mode.append(mode)

set_filter(layer_id, filter_expr)

Set a filter expression on a layer.

Source code in mapwidget/maplibre.py
211
212
213
def set_filter(self, layer_id: str, filter_expr):
    """Set a filter expression on a layer."""
    self.add_call("setFilter", [layer_id, filter_expr])

set_layer_visibility(layer_id, visibility)

Set visibility of a layer ('visible' or 'none').

Source code in mapwidget/maplibre.py
219
220
221
def set_layer_visibility(self, layer_id: str, visibility: str):
    """Set visibility of a layer ('visible' or 'none')."""
    self.set_layout_property(layer_id, "visibility", visibility)

set_layout_property(layer_id, prop, value)

Set a layout property on a layer.

Source code in mapwidget/maplibre.py
207
208
209
def set_layout_property(self, layer_id: str, prop: str, value):
    """Set a layout property on a layer."""
    self.add_call("setLayoutProperty", [layer_id, prop, value])

set_paint_property(layer_id, prop, value)

Set a paint property on a layer.

Source code in mapwidget/maplibre.py
203
204
205
def set_paint_property(self, layer_id: str, prop: str, value):
    """Set a paint property on a layer."""
    self.add_call("setPaintProperty", [layer_id, prop, value])

set_pitch(pitch)

Set the pitch of the map.

Source code in mapwidget/maplibre.py
172
173
174
def set_pitch(self, pitch: float):
    """Set the pitch of the map."""
    self.add_call("setPitch", [pitch])

set_style(style_url)

Set the map style.

Source code in mapwidget/maplibre.py
215
216
217
def set_style(self, style_url: str):
    """Set the map style."""
    self.add_call("setStyle", [style_url])

set_zoom(zoom)

Set the zoom level.

Source code in mapwidget/maplibre.py
144
145
146
def set_zoom(self, zoom: float):
    """Set the zoom level."""
    self.add_call("setZoom", [zoom])