Skip to content

maplibre module

MapLibre GL JS map widget implementation.

MapLibreMap (MapWidget)

Interactive map widget using MapLibre GL JS.

This class provides a Python interface to MapLibre GL JS maps with full bidirectional communication through anywidget.

Examples:

>>> from anymap_ts import Map
>>> m = Map(center=[-122.4, 37.8], zoom=10)
>>> m.add_basemap("OpenStreetMap")
>>> m
Source code in anymap_ts/maplibre.py
class MapLibreMap(MapWidget):
    """Interactive map widget using MapLibre GL JS.

    This class provides a Python interface to MapLibre GL JS maps with
    full bidirectional communication through anywidget.

    Example:
        >>> from anymap_ts import Map
        >>> m = Map(center=[-122.4, 37.8], zoom=10)
        >>> m.add_basemap("OpenStreetMap")
        >>> m
    """

    # ESM module for frontend
    _esm = STATIC_DIR / "maplibre.js"
    _css = STATIC_DIR / "maplibre.css"

    # MapLibre-specific traits
    bearing = traitlets.Float(0.0).tag(sync=True)
    pitch = traitlets.Float(0.0).tag(sync=True)
    antialias = traitlets.Bool(True).tag(sync=True)
    double_click_zoom = traitlets.Bool(True).tag(sync=True)

    # Layer tracking
    _layer_dict = traitlets.Dict({}).tag(sync=True)

    def __init__(
        self,
        center: Tuple[float, float] = (0.0, 0.0),
        zoom: float = 2.0,
        width: str = "100%",
        height: str = "700px",
        style: Union[
            str, Dict
        ] = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
        bearing: float = 0.0,
        pitch: float = 0.0,
        max_pitch: float = 85.0,
        controls: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):
        """Initialize a MapLibre map.

        Args:
            center: Map center as (longitude, latitude).
            zoom: Initial zoom level.
            width: Map width as CSS string.
            height: Map height as CSS string. Default is "700px".
            style: MapLibre style URL or style object. Default is "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json".
            bearing: Map bearing in degrees.
            pitch: Map pitch in degrees.
            max_pitch: Maximum pitch angle in degrees (default: 85).
            controls: Dict of controls to add. If None, defaults to
                {"layer-control": True, "control-grid": True}.
                Use {"layer-control": {"collapsed": True}} for custom options.
            **kwargs: Additional widget arguments.
        """
        # Handle style shortcuts
        if isinstance(style, str) and not style.startswith("http"):
            try:
                style = get_maplibre_style(style)
            except ValueError:
                pass  # Use as-is

        super().__init__(
            center=list(center),
            zoom=zoom,
            width=width,
            height=height,
            style=style,
            bearing=bearing,
            pitch=pitch,
            max_pitch=max_pitch,
            **kwargs,
        )

        # Initialize layer dictionary
        self._layer_dict = {"Background": []}

        # Add default controls
        if controls is None:
            controls = {
                "layer-control": True,
                "control-grid": True,
            }

        for control_name, config in controls.items():
            if config:
                if control_name == "layer-control":
                    self.add_layer_control(
                        **(config if isinstance(config, dict) else {})
                    )
                elif control_name == "control-grid":
                    self.add_control_grid(
                        **(config if isinstance(config, dict) else {})
                    )
                else:
                    self.add_control(
                        control_name, **(config if isinstance(config, dict) else {})
                    )

    # -------------------------------------------------------------------------
    # Basemap Methods
    # -------------------------------------------------------------------------

    def add_basemap(
        self,
        basemap: str = "OpenStreetMap",
        attribution: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add a basemap layer.

        Args:
            basemap: Name of basemap provider (e.g., "OpenStreetMap", "CartoDB.Positron")
            attribution: Custom attribution text
            **kwargs: Additional options
        """
        url, default_attribution = get_basemap_url(basemap)
        self.call_js_method(
            "addBasemap",
            url,
            attribution=attribution or default_attribution,
            name=basemap,
            **kwargs,
        )

        # Track in layer dict
        basemaps = self._layer_dict.get("Basemaps", [])
        if basemap not in basemaps:
            self._layer_dict = {
                **self._layer_dict,
                "Basemaps": basemaps + [basemap],
            }

    # -------------------------------------------------------------------------
    # Vector Data Methods
    # -------------------------------------------------------------------------

    def add_vector(
        self,
        data: Any,
        layer_type: Optional[str] = None,
        paint: Optional[Dict] = None,
        name: Optional[str] = None,
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add vector data to the map.

        Supports GeoJSON, GeoDataFrame, or file paths to vector formats.

        Args:
            data: GeoJSON dict, GeoDataFrame, or path to vector file
            layer_type: MapLibre layer type ('circle', 'line', 'fill', 'symbol')
            paint: MapLibre paint properties
            name: Layer name
            fit_bounds: Whether to fit map to data bounds
            **kwargs: Additional layer options
        """
        geojson = to_geojson(data)

        # Handle URL data
        if geojson.get("type") == "url":
            self.add_geojson(
                geojson["url"],
                layer_type=layer_type,
                paint=paint,
                name=name,
                fit_bounds=fit_bounds,
                **kwargs,
            )
            return

        layer_id = name or f"vector-{len(self._layers)}"

        # Infer layer type if not specified
        if layer_type is None:
            layer_type = infer_layer_type(geojson)

        # Get default paint if not provided
        if paint is None:
            paint = get_default_paint(layer_type)

        # Get bounds
        bounds = get_bounds(data) if fit_bounds else None

        # Call JavaScript
        self.call_js_method(
            "addGeoJSON",
            data=geojson,
            name=layer_id,
            layerType=layer_type,
            paint=paint,
            fitBounds=fit_bounds,
            bounds=bounds,
            **kwargs,
        )

        # Track layer
        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": layer_type,
                "source": f"{layer_id}-source",
                "paint": paint,
            },
        }

    def add_geojson(
        self,
        data: Union[str, Dict],
        layer_type: Optional[str] = None,
        paint: Optional[Dict] = None,
        name: Optional[str] = None,
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add GeoJSON data to the map.

        Args:
            data: GeoJSON dict or URL to GeoJSON file
            layer_type: MapLibre layer type
            paint: MapLibre paint properties
            name: Layer name
            fit_bounds: Whether to fit map to data bounds
            **kwargs: Additional layer options
        """
        self.add_vector(
            data,
            layer_type=layer_type,
            paint=paint,
            name=name,
            fit_bounds=fit_bounds,
            **kwargs,
        )

    # -------------------------------------------------------------------------
    # Raster Data Methods
    # -------------------------------------------------------------------------

    def add_raster(
        self,
        source: str,
        name: Optional[str] = None,
        attribution: str = "",
        indexes: Optional[List[int]] = None,
        colormap: Optional[str] = None,
        vmin: Optional[float] = None,
        vmax: Optional[float] = None,
        nodata: Optional[float] = None,
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add a raster layer from a local file using localtileserver.

        Args:
            source: Path to local raster file
            name: Layer name
            attribution: Attribution text
            indexes: Band indexes to use
            colormap: Colormap name
            vmin: Minimum value for colormap
            vmax: Maximum value for colormap
            nodata: NoData value
            fit_bounds: Whether to fit map to raster bounds
            **kwargs: Additional options
        """
        try:
            from localtileserver import TileClient
        except ImportError:
            raise ImportError(
                "localtileserver is required for local raster support. "
                "Install with: pip install anymap-ts[raster]"
            )

        client = TileClient(source)

        # Build tile URL with parameters
        tile_url = client.get_tile_url()
        if indexes:
            tile_url = client.get_tile_url(indexes=indexes)
        if colormap:
            tile_url = client.get_tile_url(colormap=colormap)
        if vmin is not None or vmax is not None:
            tile_url = client.get_tile_url(
                vmin=vmin or client.min, vmax=vmax or client.max
            )
        if nodata is not None:
            tile_url = client.get_tile_url(nodata=nodata)

        layer_name = name or Path(source).stem

        self.add_tile_layer(
            tile_url,
            name=layer_name,
            attribution=attribution,
            **kwargs,
        )

        # Fit bounds if requested
        if fit_bounds:
            bounds = client.bounds()
            if bounds:
                self.fit_bounds([bounds[0], bounds[1], bounds[2], bounds[3]])

    def add_tile_layer(
        self,
        url: str,
        name: Optional[str] = None,
        attribution: str = "",
        min_zoom: int = 0,
        max_zoom: int = 22,
        **kwargs,
    ) -> None:
        """Add an XYZ tile layer.

        Args:
            url: Tile URL template with {x}, {y}, {z} placeholders
            name: Layer name
            attribution: Attribution text
            min_zoom: Minimum zoom level
            max_zoom: Maximum zoom level
            **kwargs: Additional options
        """
        layer_id = name or f"tiles-{len(self._layers)}"

        self.call_js_method(
            "addTileLayer",
            url,
            name=layer_id,
            attribution=attribution,
            minZoom=min_zoom,
            maxZoom=max_zoom,
            **kwargs,
        )

        # Track layer
        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "raster",
                "source": f"{layer_id}-source",
            },
        }

    # -------------------------------------------------------------------------
    # COG Layer (deck.gl)
    # -------------------------------------------------------------------------

    def add_cog_layer(
        self,
        url: str,
        name: Optional[str] = None,
        opacity: float = 1.0,
        visible: bool = True,
        debug: bool = False,
        debug_opacity: float = 0.25,
        max_error: float = 0.125,
        fit_bounds: bool = True,
        before_id: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add a Cloud Optimized GeoTIFF (COG) layer using @developmentseed/deck.gl-geotiff.

        This method renders COG files directly in the browser using GPU-accelerated
        deck.gl-geotiff rendering with automatic reprojection support.

        Args:
            url: URL to the Cloud Optimized GeoTIFF file.
            name: Layer ID. If None, auto-generated.
            opacity: Layer opacity (0-1).
            visible: Whether layer is visible.
            debug: Show reprojection mesh for debugging.
            debug_opacity: Opacity of debug mesh (0-1).
            max_error: Maximum reprojection error in pixels. Lower values
                create denser mesh for better accuracy.
            fit_bounds: Whether to fit map to COG bounds after loading.
            before_id: ID of layer to insert before.
            **kwargs: Additional COGLayer props.

        Example:
            >>> from anymap_ts import Map
            >>> m = Map()
            >>> m.add_cog_layer(
            ...     "https://example.com/landcover.tif",
            ...     name="landcover",
            ...     opacity=0.8
            ... )
        """
        layer_id = name or f"cog-{len(self._layers)}"

        self.call_js_method(
            "addCOGLayer",
            id=layer_id,
            geotiff=url,
            opacity=opacity,
            visible=visible,
            debug=debug,
            debugOpacity=debug_opacity,
            maxError=max_error,
            fitBounds=fit_bounds,
            beforeId=before_id,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "cog",
                "url": url,
            },
        }

    def remove_cog_layer(self, layer_id: str) -> None:
        """Remove a COG layer.

        Args:
            layer_id: Layer identifier to remove.
        """
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self.call_js_method("removeCOGLayer", layer_id)

    # -------------------------------------------------------------------------
    # Zarr Layer (@carbonplan/zarr-layer)
    # -------------------------------------------------------------------------

    def add_zarr_layer(
        self,
        url: str,
        variable: str,
        name: Optional[str] = None,
        colormap: Optional[List[str]] = None,
        clim: Optional[Tuple[float, float]] = None,
        opacity: float = 1.0,
        selector: Optional[Dict[str, Any]] = None,
        minzoom: int = 0,
        maxzoom: int = 22,
        fill_value: Optional[float] = None,
        spatial_dimensions: Optional[Dict[str, str]] = None,
        zarr_version: Optional[int] = None,
        bounds: Optional[List[float]] = None,
        **kwargs,
    ) -> None:
        """Add a Zarr dataset layer for visualizing multidimensional array data.

        This method renders Zarr pyramid datasets directly in the browser using
        GPU-accelerated WebGL rendering via @carbonplan/zarr-layer.

        Args:
            url: URL to the Zarr store (pyramid format recommended).
            variable: Variable name in the Zarr dataset to visualize.
            name: Layer ID. If None, auto-generated.
            colormap: List of hex color strings for visualization.
                Example: ['#0000ff', '#ffff00', '#ff0000'] (blue-yellow-red).
                Default: ['#000000', '#ffffff'] (black to white).
            clim: Color range as (min, max) tuple.
                Default: (0, 100).
            opacity: Layer opacity (0-1).
            selector: Dimension selector for multi-dimensional data.
                Example: {"month": 4} to select 4th month.
            minzoom: Minimum zoom level for rendering.
            maxzoom: Maximum zoom level for rendering.
            fill_value: No-data value (auto-detected from metadata if not set).
            spatial_dimensions: Custom spatial dimension names.
                Example: {"lat": "y", "lon": "x"} for non-standard names.
            zarr_version: Zarr format version (2 or 3). Auto-detected if not set.
            bounds: Explicit spatial bounds [xMin, yMin, xMax, yMax].
                Units depend on CRS: degrees for EPSG:4326, meters for EPSG:3857.
            **kwargs: Additional ZarrLayer props.

        Example:
            >>> from anymap_ts import Map
            >>> m = Map()
            >>> m.add_zarr_layer(
            ...     "https://example.com/climate.zarr",
            ...     variable="temperature",
            ...     clim=(270, 310),
            ...     colormap=['#0000ff', '#ffff00', '#ff0000'],
            ...     selector={"month": 7}
            ... )
        """
        layer_id = name or f"zarr-{len(self._layers)}"

        self.call_js_method(
            "addZarrLayer",
            id=layer_id,
            source=url,
            variable=variable,
            colormap=colormap or ["#000000", "#ffffff"],
            clim=list(clim) if clim else [0, 100],
            opacity=opacity,
            selector=selector or {},
            minzoom=minzoom,
            maxzoom=maxzoom,
            fillValue=fill_value,
            spatialDimensions=spatial_dimensions,
            zarrVersion=zarr_version,
            bounds=bounds,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "zarr",
                "url": url,
                "variable": variable,
            },
        }

    def remove_zarr_layer(self, layer_id: str) -> None:
        """Remove a Zarr layer.

        Args:
            layer_id: Layer identifier to remove.
        """
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self.call_js_method("removeZarrLayer", layer_id)

    def update_zarr_layer(
        self,
        layer_id: str,
        selector: Optional[Dict[str, Any]] = None,
        clim: Optional[Tuple[float, float]] = None,
        colormap: Optional[List[str]] = None,
        opacity: Optional[float] = None,
    ) -> None:
        """Update a Zarr layer's properties dynamically.

        Args:
            layer_id: Layer identifier.
            selector: New dimension selector.
            clim: New color range.
            colormap: New colormap.
            opacity: New opacity value (0-1).
        """
        update_kwargs: Dict[str, Any] = {"id": layer_id}
        if selector is not None:
            update_kwargs["selector"] = selector
        if clim is not None:
            update_kwargs["clim"] = list(clim)
        if colormap is not None:
            update_kwargs["colormap"] = colormap
        if opacity is not None:
            update_kwargs["opacity"] = opacity
        self.call_js_method("updateZarrLayer", **update_kwargs)

    # -------------------------------------------------------------------------
    # Arc Layer (deck.gl)
    # -------------------------------------------------------------------------

    def add_arc_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_source_position: Union[str, Any] = "source",
        get_target_position: Union[str, Any] = "target",
        get_source_color: Optional[List[int]] = None,
        get_target_color: Optional[List[int]] = None,
        get_width: Union[float, str] = 1,
        get_height: float = 1,
        great_circle: bool = False,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add an arc layer for origin-destination visualization using deck.gl.

        Arc layers are ideal for visualizing connections between locations,
        such as flight routes, migration patterns, or network flows.

        Args:
            data: Array of data objects with source/target coordinates.
                Each object should have source and target positions.
            name: Layer ID. If None, auto-generated.
            get_source_position: Accessor for source position [lng, lat].
                Can be a string (property name) or a value.
            get_target_position: Accessor for target position [lng, lat].
                Can be a string (property name) or a value.
            get_source_color: Source end color as [r, g, b, a].
                Default: [51, 136, 255, 255] (blue).
            get_target_color: Target end color as [r, g, b, a].
                Default: [255, 136, 51, 255] (orange).
            get_width: Arc width in pixels. Can be a number or accessor.
            get_height: Arc height multiplier. Higher values create more curved arcs.
            great_circle: Whether to draw arcs along great circles.
            pickable: Whether layer responds to hover/click events.
            opacity: Layer opacity (0-1).
            **kwargs: Additional ArcLayer props.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap()
            >>> arcs = [
            ...     {"source": [-122.4, 37.8], "target": [-73.9, 40.7]},
            ...     {"source": [-122.4, 37.8], "target": [-0.1, 51.5]},
            ... ]
            >>> m.add_arc_layer(arcs, name="flights")
        """
        layer_id = name or f"arc-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addArcLayer",
            id=layer_id,
            data=processed_data,
            getSourcePosition=get_source_position,
            getTargetPosition=get_target_position,
            getSourceColor=get_source_color or [51, 136, 255, 255],
            getTargetColor=get_target_color or [255, 136, 51, 255],
            getWidth=get_width,
            getHeight=get_height,
            greatCircle=great_circle,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "arc",
            },
        }

    def remove_arc_layer(self, layer_id: str) -> None:
        """Remove an arc layer.

        Args:
            layer_id: Layer identifier to remove.
        """
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self.call_js_method("removeArcLayer", layer_id)

    # -------------------------------------------------------------------------
    # PointCloud Layer (deck.gl)
    # -------------------------------------------------------------------------

    def add_point_cloud_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "position",
        get_color: Optional[Union[List[int], str]] = None,
        get_normal: Optional[Union[str, Any]] = None,
        point_size: float = 2,
        size_units: str = "pixels",
        pickable: bool = True,
        opacity: float = 1.0,
        material: bool = True,
        coordinate_system: Optional[int] = None,
        coordinate_origin: Optional[List[float]] = None,
        **kwargs,
    ) -> None:
        """Add a point cloud layer for 3D point visualization using deck.gl.

        Point cloud layers render large collections of 3D points, ideal for
        LiDAR data, photogrammetry outputs, or any 3D point dataset.

        Args:
            data: Array of point data with positions. Each point should have
                x, y, z coordinates (or position array).
            name: Layer ID. If None, auto-generated.
            get_position: Accessor for point position [x, y, z].
                Can be a string (property name) or a value.
            get_color: Accessor or value for point color [r, g, b, a].
                Default: [255, 255, 255, 255] (white).
            get_normal: Accessor for point normal [nx, ny, nz] for lighting.
                Default: [0, 0, 1] (pointing up).
            point_size: Point size in pixels or meters (depends on size_units).
            size_units: Size units: 'pixels', 'meters', or 'common'.
            pickable: Whether layer responds to hover/click events.
            opacity: Layer opacity (0-1).
            material: Whether to enable lighting effects.
            coordinate_system: Coordinate system for positions.
            coordinate_origin: Origin for coordinate system [x, y, z].
            **kwargs: Additional PointCloudLayer props.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> import numpy as np
            >>> m = MapLibreMap(pitch=45)
            >>> points = [
            ...     {"position": [-122.4, 37.8, 100], "color": [255, 0, 0, 255]},
            ...     {"position": [-122.3, 37.7, 200], "color": [0, 255, 0, 255]},
            ... ]
            >>> m.add_point_cloud_layer(points, point_size=5)
        """
        layer_id = name or f"pointcloud-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addPointCloudLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getColor=get_color or [255, 255, 255, 255],
            getNormal=get_normal,
            pointSize=point_size,
            sizeUnits=size_units,
            pickable=pickable,
            opacity=opacity,
            material=material,
            coordinateSystem=coordinate_system,
            coordinateOrigin=coordinate_origin,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "pointcloud",
            },
        }

    def remove_point_cloud_layer(self, layer_id: str) -> None:
        """Remove a point cloud layer.

        Args:
            layer_id: Layer identifier to remove.
        """
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self.call_js_method("removePointCloudLayer", layer_id)

    # -------------------------------------------------------------------------
    # LiDAR Layers (maplibre-gl-lidar)
    # -------------------------------------------------------------------------

    def add_lidar_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        title: str = "LiDAR Viewer",
        point_size: float = 2,
        opacity: float = 1.0,
        color_scheme: str = "elevation",
        use_percentile: bool = True,
        point_budget: int = 1000000,
        pickable: bool = False,
        auto_zoom: bool = True,
        copc_loading_mode: Optional[str] = None,
        streaming_point_budget: int = 5000000,
        panel_max_height: int = 600,
        **kwargs,
    ) -> None:
        """Add an interactive LiDAR control panel.

        The LiDAR control provides a UI panel for loading, visualizing, and
        styling LiDAR point cloud files (LAS, LAZ, COPC formats).

        Args:
            position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
            collapsed: Whether the panel starts collapsed.
            title: Title displayed on the panel.
            point_size: Point size in pixels.
            opacity: Layer opacity (0-1).
            color_scheme: Color scheme ('elevation', 'intensity', 'classification', 'rgb').
            use_percentile: Use 2-98% percentile for color scaling.
            point_budget: Maximum number of points to display.
            pickable: Enable hover/click interactions.
            auto_zoom: Auto-zoom to point cloud after loading.
            copc_loading_mode: COPC loading mode ('full' or 'dynamic').
            streaming_point_budget: Point budget for streaming mode.
            panel_max_height: Maximum height of the panel in pixels.
            **kwargs: Additional control options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap(pitch=60)
            >>> m.add_lidar_control(color_scheme="classification", pickable=True)
        """
        self.call_js_method(
            "addLidarControl",
            position=position,
            collapsed=collapsed,
            title=title,
            pointSize=point_size,
            opacity=opacity,
            colorScheme=color_scheme,
            usePercentile=use_percentile,
            pointBudget=point_budget,
            pickable=pickable,
            autoZoom=auto_zoom,
            copcLoadingMode=copc_loading_mode,
            streamingPointBudget=streaming_point_budget,
            panelMaxHeight=panel_max_height,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "lidar-control": {"position": position, "collapsed": collapsed},
        }

    def add_lidar_layer(
        self,
        source: Union[str, Path],
        name: Optional[str] = None,
        color_scheme: str = "elevation",
        point_size: float = 2,
        opacity: float = 1.0,
        pickable: bool = True,
        auto_zoom: bool = True,
        streaming_mode: bool = True,
        point_budget: int = 1000000,
        **kwargs,
    ) -> None:
        """Load and display a LiDAR file from URL or local path.

        Supports LAS, LAZ, and COPC (Cloud-Optimized Point Cloud) formats.
        For local files, the file is read and sent as base64 to JavaScript.
        For URLs, the data is loaded directly via streaming when possible.

        Args:
            source: URL or local file path to the LiDAR file.
            name: Layer identifier. If None, auto-generated.
            color_scheme: Color scheme ('elevation', 'intensity', 'classification', 'rgb').
            point_size: Point size in pixels.
            opacity: Layer opacity (0-1).
            pickable: Enable hover/click interactions.
            auto_zoom: Auto-zoom to point cloud after loading.
            streaming_mode: Use streaming mode for large COPC files.
            point_budget: Maximum number of points to display.
            **kwargs: Additional layer options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap(center=[-123.07, 44.05], zoom=14, pitch=60)
            >>> m.add_lidar_layer(
            ...     source="https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz",
            ...     name="autzen",
            ...     color_scheme="classification",
            ... )
        """
        layer_id = name or f"lidar-{len(self._layers)}"

        # Check if source is a local file
        source_path = Path(source) if isinstance(source, (str, Path)) else None
        is_local = source_path is not None and source_path.exists()

        if is_local:
            # Read local file and encode as base64
            import base64

            with open(source_path, "rb") as f:
                file_data = f.read()
            source_b64 = base64.b64encode(file_data).decode("utf-8")

            self.call_js_method(
                "addLidarLayer",
                source=source_b64,
                name=layer_id,
                isBase64=True,
                filename=source_path.name,
                colorScheme=color_scheme,
                pointSize=point_size,
                opacity=opacity,
                pickable=pickable,
                autoZoom=auto_zoom,
                streamingMode=streaming_mode,
                pointBudget=point_budget,
                **kwargs,
            )
        else:
            # Load from URL
            self.call_js_method(
                "addLidarLayer",
                source=str(source),
                name=layer_id,
                isBase64=False,
                colorScheme=color_scheme,
                pointSize=point_size,
                opacity=opacity,
                pickable=pickable,
                autoZoom=auto_zoom,
                streamingMode=streaming_mode,
                pointBudget=point_budget,
                **kwargs,
            )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "lidar",
                "source": str(source),
            },
        }

    def remove_lidar_layer(self, layer_id: Optional[str] = None) -> None:
        """Remove a LiDAR layer.

        Args:
            layer_id: Layer identifier to remove. If None, removes all LiDAR layers.
        """
        if layer_id:
            if layer_id in self._layers:
                layers = dict(self._layers)
                del layers[layer_id]
                self._layers = layers
            self.call_js_method("removeLidarLayer", id=layer_id)
        else:
            # Remove all lidar layers
            layers = dict(self._layers)
            self._layers = {k: v for k, v in layers.items() if v.get("type") != "lidar"}
            self.call_js_method("removeLidarLayer")

    def set_lidar_color_scheme(self, color_scheme: str) -> None:
        """Set the LiDAR color scheme.

        Args:
            color_scheme: Color scheme ('elevation', 'intensity', 'classification', 'rgb').
        """
        self.call_js_method("setLidarColorScheme", colorScheme=color_scheme)

    def set_lidar_point_size(self, point_size: float) -> None:
        """Set the LiDAR point size.

        Args:
            point_size: Point size in pixels.
        """
        self.call_js_method("setLidarPointSize", pointSize=point_size)

    def set_lidar_opacity(self, opacity: float) -> None:
        """Set the LiDAR layer opacity.

        Args:
            opacity: Opacity value between 0 and 1.
        """
        self.call_js_method("setLidarOpacity", opacity=opacity)

    # -------------------------------------------------------------------------
    # maplibre-gl-components UI Controls
    # -------------------------------------------------------------------------

    def add_pmtiles_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        default_url: Optional[str] = None,
        load_default_url: bool = False,
        default_opacity: float = 1.0,
        default_fill_color: str = "steelblue",
        default_line_color: str = "#333",
        default_pickable: bool = True,
        **kwargs,
    ) -> None:
        """Add a PMTiles layer control for loading PMTiles files via UI.

        This provides an interactive panel for users to enter PMTiles URLs
        and visualize vector or raster tile data.

        Args:
            position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
            collapsed: Whether the panel starts collapsed.
            default_url: Default PMTiles URL to pre-fill.
            load_default_url: Whether to auto-load the default URL.
            default_opacity: Default layer opacity (0-1).
            default_fill_color: Default fill color for vector polygons.
            default_line_color: Default line color for vector lines.
            default_pickable: Whether features are clickable by default.
            **kwargs: Additional control options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap()
            >>> m.add_pmtiles_control(
            ...     default_url="https://pmtiles.io/protomaps(vector)ODbL_firenze.pmtiles",
            ...     load_default_url=True
            ... )
        """
        self.call_js_method(
            "addPMTilesControl",
            position=position,
            collapsed=collapsed,
            defaultUrl=default_url or "",
            loadDefaultUrl=load_default_url,
            defaultOpacity=default_opacity,
            defaultFillColor=default_fill_color,
            defaultLineColor=default_line_color,
            defaultPickable=default_pickable,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "pmtiles-control": {"position": position, "collapsed": collapsed},
        }

    def add_cog_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        default_url: Optional[str] = None,
        load_default_url: bool = False,
        default_opacity: float = 1.0,
        default_colormap: str = "viridis",
        default_bands: str = "1",
        default_rescale_min: float = 0,
        default_rescale_max: float = 255,
        **kwargs,
    ) -> None:
        """Add a COG layer control for loading Cloud Optimized GeoTIFFs via UI.

        This provides an interactive panel for users to enter COG URLs
        and configure visualization parameters like colormap and rescaling.

        Args:
            position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
            collapsed: Whether the panel starts collapsed.
            default_url: Default COG URL to pre-fill.
            load_default_url: Whether to auto-load the default URL.
            default_opacity: Default layer opacity (0-1).
            default_colormap: Default colormap name.
            default_bands: Default bands (e.g., '1' or '1,2,3').
            default_rescale_min: Default minimum value for rescaling.
            default_rescale_max: Default maximum value for rescaling.
            **kwargs: Additional control options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap()
            >>> m.add_cog_control(
            ...     default_url="https://example.com/cog.tif",
            ...     default_colormap="terrain"
            ... )
        """
        self.call_js_method(
            "addCogControl",
            position=position,
            collapsed=collapsed,
            defaultUrl=default_url or "",
            loadDefaultUrl=load_default_url,
            defaultOpacity=default_opacity,
            defaultColormap=default_colormap,
            defaultBands=default_bands,
            defaultRescaleMin=default_rescale_min,
            defaultRescaleMax=default_rescale_max,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "cog-control": {"position": position, "collapsed": collapsed},
        }

    def add_zarr_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        default_url: Optional[str] = None,
        load_default_url: bool = False,
        default_opacity: float = 1.0,
        default_variable: str = "",
        default_clim: Optional[Tuple[float, float]] = None,
        **kwargs,
    ) -> None:
        """Add a Zarr layer control for loading Zarr datasets via UI.

        This provides an interactive panel for users to enter Zarr URLs
        and configure visualization parameters.

        Args:
            position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
            collapsed: Whether the panel starts collapsed.
            default_url: Default Zarr URL to pre-fill.
            load_default_url: Whether to auto-load the default URL.
            default_opacity: Default layer opacity (0-1).
            default_variable: Default variable name.
            default_clim: Default color limits (min, max).
            **kwargs: Additional control options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap()
            >>> m.add_zarr_control(
            ...     default_url="https://example.com/data.zarr",
            ...     default_variable="temperature"
            ... )
        """
        self.call_js_method(
            "addZarrControl",
            position=position,
            collapsed=collapsed,
            defaultUrl=default_url or "",
            loadDefaultUrl=load_default_url,
            defaultOpacity=default_opacity,
            defaultVariable=default_variable,
            defaultClim=list(default_clim) if default_clim else [0, 1],
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "zarr-control": {"position": position, "collapsed": collapsed},
        }

    def add_vector_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        default_url: Optional[str] = None,
        load_default_url: bool = False,
        default_opacity: float = 1.0,
        default_fill_color: str = "#3388ff",
        default_stroke_color: str = "#3388ff",
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add a vector layer control for loading vector datasets from URLs.

        This provides an interactive panel for users to enter URLs to
        GeoJSON, GeoParquet, or FlatGeobuf datasets.

        Args:
            position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
            collapsed: Whether the panel starts collapsed.
            default_url: Default vector URL to pre-fill.
            load_default_url: Whether to auto-load the default URL.
            default_opacity: Default layer opacity (0-1).
            default_fill_color: Default fill color for polygons.
            default_stroke_color: Default stroke color for lines/outlines.
            fit_bounds: Whether to fit map to loaded data bounds.
            **kwargs: Additional control options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap()
            >>> m.add_vector_control(
            ...     default_url="https://example.com/data.geojson",
            ...     default_fill_color="#ff0000"
            ... )
        """
        self.call_js_method(
            "addVectorControl",
            position=position,
            collapsed=collapsed,
            defaultUrl=default_url or "",
            loadDefaultUrl=load_default_url,
            defaultOpacity=default_opacity,
            defaultFillColor=default_fill_color,
            defaultStrokeColor=default_stroke_color,
            fitBounds=fit_bounds,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "vector-control": {"position": position, "collapsed": collapsed},
        }

    def add_control_grid(
        self,
        position: str = "top-right",
        default_controls: Optional[List[str]] = None,
        exclude: Optional[List[str]] = None,
        rows: Optional[int] = None,
        columns: Optional[int] = None,
        collapsed: bool = True,
        collapsible: bool = True,
        title: str = "",
        show_row_column_controls: bool = True,
        gap: int = 2,
        basemap_style_url: Optional[str] = None,
        exclude_layers: Optional[List[str]] = None,
        **kwargs,
    ) -> None:
        """Add a ControlGrid with all default tools or a custom subset.

        The ControlGrid provides a collapsible toolbar with up to 26 built-in
        controls (search, basemap, terrain, measure, draw, etc.) in a
        configurable grid layout.

        Args:
            position: Control position ('top-left', 'top-right', 'bottom-left',
                'bottom-right').
            default_controls: Explicit list of control names to include. If None,
                all 26 default controls are used (minus any in ``exclude``).
                Valid names: 'globe', 'fullscreen', 'north', 'terrain', 'search',
                'viewState', 'inspect', 'vectorDataset', 'basemap', 'measure',
                'geoEditor', 'bookmark', 'print', 'minimap', 'swipe',
                'streetView', 'addVector', 'cogLayer', 'zarrLayer',
                'pmtilesLayer', 'stacLayer', 'stacSearch', 'planetaryComputer',
                'gaussianSplat', 'lidar', 'usgsLidar'.
            exclude: Controls to remove from the default set. Ignored when
                ``default_controls`` is provided.
            rows: Number of grid rows (auto-calculated if None).
            columns: Number of grid columns (auto-calculated if None).
            collapsed: Whether the grid starts collapsed. Default True.
            collapsible: Whether the grid can be collapsed. Default True.
            title: Optional header title for the grid.
            show_row_column_controls: Show row/column input fields. Default True.
            gap: Gap between grid cells in pixels. Default 2.
            basemap_style_url: Basemap style URL for SwipeControl layer grouping.
                If None, the current map style is used automatically.
            exclude_layers: Layer ID patterns to exclude from SwipeControl
                (e.g., 'measure-*', 'gl-draw-*'). If None, sensible defaults
                are applied.
            **kwargs: Additional ControlGrid options.

        Example:
            >>> from anymap_ts import MapLibreMap
            >>> m = MapLibreMap()
            >>> m.add_control_grid()  # All 26 controls
            >>> # Or with customization:
            >>> m.add_control_grid(
            ...     exclude=["minimap", "streetView"],
            ...     collapsed=True,
            ... )
        """
        js_kwargs: Dict[str, Any] = {
            "position": position,
            "collapsed": collapsed,
            "collapsible": collapsible,
            "showRowColumnControls": show_row_column_controls,
            "gap": gap,
            **kwargs,
        }
        if default_controls is not None:
            js_kwargs["defaultControls"] = default_controls
        if exclude is not None:
            js_kwargs["exclude"] = exclude
        if rows is not None:
            js_kwargs["rows"] = rows
        if columns is not None:
            js_kwargs["columns"] = columns
        if title:
            js_kwargs["title"] = title
        if basemap_style_url is not None:
            js_kwargs["basemapStyleUrl"] = basemap_style_url
        if exclude_layers is not None:
            js_kwargs["excludeLayers"] = exclude_layers

        self.call_js_method("addControlGrid", **js_kwargs)
        self._controls = {
            **self._controls,
            "control-grid": {"position": position, "collapsed": collapsed},
        }

    def _process_deck_data(self, data: Any) -> Any:
        """Process data for deck.gl layers.

        Handles GeoDataFrame, file paths, GeoJSON, and list of dicts.

        Args:
            data: Input data in various formats.

        Returns:
            Processed data suitable for deck.gl layers.
        """
        # Handle GeoDataFrame
        if hasattr(data, "__geo_interface__"):
            return data.__geo_interface__

        # Handle file paths
        if isinstance(data, (str, Path)):
            path = Path(data)
            if path.exists():
                try:
                    import geopandas as gpd

                    gdf = gpd.read_file(path)
                    return gdf.__geo_interface__
                except ImportError:
                    pass

        # Return as-is for lists, dicts, etc.
        return data

    # -------------------------------------------------------------------------
    # Layer Management
    # -------------------------------------------------------------------------

    def add_layer(
        self,
        layer_id: str,
        layer_type: str,
        source: Union[str, Dict],
        paint: Optional[Dict] = None,
        layout: Optional[Dict] = None,
        before_id: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add a generic layer to the map.

        Args:
            layer_id: Unique layer identifier
            layer_type: MapLibre layer type
            source: Source ID or source configuration dict
            paint: Paint properties
            layout: Layout properties
            before_id: ID of layer to insert before
            **kwargs: Additional layer options
        """
        layer_config = {
            "id": layer_id,
            "type": layer_type,
            "paint": paint or {},
            "layout": layout or {},
            **kwargs,
        }

        if isinstance(source, str):
            layer_config["source"] = source
        else:
            source_id = f"{layer_id}-source"
            self._sources = {**self._sources, source_id: source}
            self.call_js_method("addSource", source_id, **source)
            layer_config["source"] = source_id

        self._layers = {**self._layers, layer_id: layer_config}
        self.call_js_method("addLayer", beforeId=before_id, **layer_config)

    def remove_layer(self, layer_id: str) -> None:
        """Remove a layer from the map.

        Args:
            layer_id: Layer identifier to remove
        """
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self.call_js_method("removeLayer", layer_id)

    def set_visibility(self, layer_id: str, visible: bool) -> None:
        """Set layer visibility.

        Args:
            layer_id: Layer identifier
            visible: Whether layer should be visible
        """
        self.call_js_method("setVisibility", layer_id, visible)

    def set_opacity(self, layer_id: str, opacity: float) -> None:
        """Set layer opacity.

        Args:
            layer_id: Layer identifier
            opacity: Opacity value between 0 and 1
        """
        self.call_js_method("setOpacity", layer_id, opacity)

    # -------------------------------------------------------------------------
    # Controls
    # -------------------------------------------------------------------------

    def add_control(
        self,
        control_type: str,
        position: str = "top-right",
        **kwargs,
    ) -> None:
        """Add a map control.

        Args:
            control_type: Type of control ('navigation', 'scale', 'fullscreen', etc.)
            position: Control position
            **kwargs: Control-specific options
        """
        self.call_js_method("addControl", control_type, position=position, **kwargs)
        self._controls = {
            **self._controls,
            control_type: {"type": control_type, "position": position, **kwargs},
        }

    def remove_control(self, control_type: str) -> None:
        """Remove a map control.

        Args:
            control_type: Type of control to remove
        """
        self.call_js_method("removeControl", control_type)
        if control_type in self._controls:
            controls = dict(self._controls)
            del controls[control_type]
            self._controls = controls

    def add_layer_control(
        self,
        layers: Optional[List[str]] = None,
        position: str = "top-right",
        collapsed: bool = True,
    ) -> None:
        """Add a layer visibility control.

        Uses maplibre-gl-layer-control for layer toggling and opacity.

        Args:
            layers: List of layer IDs to include (None = all layers)
            position: Control position
            collapsed: Whether control starts collapsed
        """
        if layers is None:
            layers = list(self._layers.keys())

        self.call_js_method(
            "addLayerControl",
            layers=layers,
            position=position,
            collapsed=collapsed,
        )
        self._controls = {
            **self._controls,
            "layer-control": {"layers": layers, "position": position},
        }

    # -------------------------------------------------------------------------
    # Drawing
    # -------------------------------------------------------------------------

    def add_draw_control(
        self,
        position: str = "top-right",
        draw_modes: Optional[List[str]] = None,
        edit_modes: Optional[List[str]] = None,
        collapsed: bool = False,
        **kwargs,
    ) -> None:
        """Add a drawing control using maplibre-gl-geo-editor.

        Args:
            position: Control position
            draw_modes: Drawing modes to enable (e.g., ['polygon', 'line', 'marker'])
            edit_modes: Edit modes to enable (e.g., ['select', 'drag', 'delete'])
            collapsed: Whether control starts collapsed
            **kwargs: Additional geo-editor options
        """
        if draw_modes is None:
            draw_modes = ["polygon", "line", "rectangle", "circle", "marker"]
        if edit_modes is None:
            edit_modes = ["select", "drag", "change", "rotate", "delete"]

        self.call_js_method(
            "addDrawControl",
            position=position,
            drawModes=draw_modes,
            editModes=edit_modes,
            collapsed=collapsed,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "draw-control": {
                "position": position,
                "drawModes": draw_modes,
                "editModes": edit_modes,
            },
        }

    def get_draw_data(self) -> Dict:
        """Get the current drawn features as GeoJSON.

        Returns:
            GeoJSON FeatureCollection of drawn features
        """
        self.call_js_method("getDrawData")
        # Small delay to allow JS to update the trait
        import time

        time.sleep(0.1)
        return self._draw_data or {"type": "FeatureCollection", "features": []}

    @property
    def draw_data(self) -> Dict:
        """Property to access current draw data."""
        return self._draw_data or {"type": "FeatureCollection", "features": []}

    def load_draw_data(self, geojson: Dict) -> None:
        """Load GeoJSON features into the drawing layer.

        Args:
            geojson: GeoJSON FeatureCollection to load
        """
        self._draw_data = geojson
        self.call_js_method("loadDrawData", geojson)

    def clear_draw_data(self) -> None:
        """Clear all drawn features."""
        self._draw_data = {"type": "FeatureCollection", "features": []}
        self.call_js_method("clearDrawData")

    def save_draw_data(
        self,
        filepath: Union[str, Path],
        driver: Optional[str] = None,
    ) -> None:
        """Save drawn features to a file.

        Args:
            filepath: Path to save file
            driver: Output driver (auto-detected from extension if not provided)

        Raises:
            ImportError: If geopandas is not installed
        """
        try:
            import geopandas as gpd
        except ImportError:
            raise ImportError(
                "geopandas is required to save draw data. "
                "Install with: pip install anymap-ts[vector]"
            )

        data = self.get_draw_data()
        if not data.get("features"):
            print("No features to save")
            return

        gdf = gpd.GeoDataFrame.from_features(data["features"])
        filepath = Path(filepath)

        # Infer driver from extension
        if driver is None:
            ext = filepath.suffix.lower()
            driver_map = {
                ".geojson": "GeoJSON",
                ".json": "GeoJSON",
                ".shp": "ESRI Shapefile",
                ".gpkg": "GPKG",
            }
            driver = driver_map.get(ext, "GeoJSON")

        gdf.to_file(filepath, driver=driver)

    # -------------------------------------------------------------------------
    # HTML Export
    # -------------------------------------------------------------------------

    def _generate_html_template(self) -> str:
        """Generate standalone HTML for the map."""
        template_path = Path(__file__).parent / "templates" / "maplibre.html"

        if template_path.exists():
            template = template_path.read_text(encoding="utf-8")
        else:
            template = self._get_default_template()

        # Serialize state
        state = {
            "center": self.center,
            "zoom": self.zoom,
            "style": self.style,
            "bearing": self.bearing,
            "pitch": self.pitch,
            "width": self.width,
            "height": self.height,
            "layers": self._layers,
            "sources": self._sources,
            "controls": self._controls,
            "js_calls": self._js_calls,
        }

        template = template.replace("{{state}}", json.dumps(state, indent=2))
        return template

    def _get_default_template(self) -> str:
        """Get default HTML template."""
        return """<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>{{title}}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.js"></script>
    <link href="https://unpkg.com/maplibre-gl@5/dist/maplibre-gl.css" rel="stylesheet" />
    <style>
        body { margin: 0; padding: 0; }
        #map { position: absolute; top: 0; bottom: 0; width: 100%; }
    </style>
</head>
<body>
    <div id="map"></div>
    <script>
        const state = {{state}};

        const map = new maplibregl.Map({
            container: 'map',
            style: state.style,
            center: state.center,
            zoom: state.zoom,
            bearing: state.bearing || 0,
            pitch: state.pitch || 0
        });

        map.on('load', function() {
            // Replay JS calls
            for (const call of state.js_calls || []) {
                try {
                    executeMethod(call.method, call.args, call.kwargs);
                } catch (e) {
                    console.error('Error executing', call.method, e);
                }
            }
        });

        function executeMethod(method, args, kwargs) {
            switch (method) {
                case 'addBasemap':
                    const url = args[0];
                    const name = kwargs.name || 'basemap';
                    const sourceId = 'basemap-' + name;
                    if (!map.getSource(sourceId)) {
                        map.addSource(sourceId, {
                            type: 'raster',
                            tiles: [url],
                            tileSize: 256,
                            attribution: kwargs.attribution || ''
                        });
                    }
                    if (!map.getLayer(sourceId)) {
                        map.addLayer({
                            id: sourceId,
                            type: 'raster',
                            source: sourceId
                        });
                    }
                    break;

                case 'addGeoJSON':
                    const layerName = kwargs.name;
                    const sourceIdGeo = layerName + '-source';
                    if (!map.getSource(sourceIdGeo)) {
                        map.addSource(sourceIdGeo, {
                            type: 'geojson',
                            data: kwargs.data
                        });
                    }
                    if (!map.getLayer(layerName)) {
                        map.addLayer({
                            id: layerName,
                            type: kwargs.layerType || 'circle',
                            source: sourceIdGeo,
                            paint: kwargs.paint || {}
                        });
                    }
                    if (kwargs.fitBounds && kwargs.bounds) {
                        map.fitBounds([
                            [kwargs.bounds[0], kwargs.bounds[1]],
                            [kwargs.bounds[2], kwargs.bounds[3]]
                        ], { padding: 50 });
                    }
                    break;

                case 'addTileLayer':
                    const tileUrl = args[0];
                    const tileName = kwargs.name;
                    const tileSourceId = tileName + '-source';
                    if (!map.getSource(tileSourceId)) {
                        map.addSource(tileSourceId, {
                            type: 'raster',
                            tiles: [tileUrl],
                            tileSize: 256,
                            attribution: kwargs.attribution || ''
                        });
                    }
                    if (!map.getLayer(tileName)) {
                        map.addLayer({
                            id: tileName,
                            type: 'raster',
                            source: tileSourceId
                        });
                    }
                    break;

                case 'addControl':
                    const controlType = args[0];
                    const position = kwargs.position || 'top-right';
                    let control;
                    switch (controlType) {
                        case 'navigation':
                            control = new maplibregl.NavigationControl();
                            break;
                        case 'scale':
                            control = new maplibregl.ScaleControl();
                            break;
                        case 'fullscreen':
                            control = new maplibregl.FullscreenControl();
                            break;
                    }
                    if (control) {
                        map.addControl(control, position);
                    }
                    break;

                case 'flyTo':
                    map.flyTo({
                        center: [args[0], args[1]],
                        zoom: kwargs.zoom,
                        duration: kwargs.duration || 2000
                    });
                    break;

                case 'fitBounds':
                    const bounds = args[0];
                    map.fitBounds([
                        [bounds[0], bounds[1]],
                        [bounds[2], bounds[3]]
                    ], {
                        padding: kwargs.padding || 50,
                        duration: kwargs.duration || 1000
                    });
                    break;

                default:
                    console.log('Unknown method:', method);
            }
        }
    </script>
</body>
</html>"""

draw_data: Dict property readonly

Property to access current draw data.

__init__(self, center=(0.0, 0.0), zoom=2.0, width='100%', height='700px', style='https://basemaps.cartocdn.com/gl/positron-gl-style/style.json', bearing=0.0, pitch=0.0, max_pitch=85.0, controls=None, **kwargs) special

Initialize a MapLibre map.

Parameters:

Name Type Description Default
center Tuple[float, float]

Map center as (longitude, latitude).

(0.0, 0.0)
zoom float

Initial zoom level.

2.0
width str

Map width as CSS string.

'100%'
height str

Map height as CSS string. Default is "700px".

'700px'
style Union[str, Dict]

MapLibre style URL or style object. Default is "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json".

'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
bearing float

Map bearing in degrees.

0.0
pitch float

Map pitch in degrees.

0.0
max_pitch float

Maximum pitch angle in degrees (default: 85).

85.0
controls Optional[Dict[str, Any]]

Dict of controls to add. If None, defaults to {"layer-control": True, "control-grid": True}. Use {"layer-control": {"collapsed": True}} for custom options.

None
**kwargs

Additional widget arguments.

{}
Source code in anymap_ts/maplibre.py
def __init__(
    self,
    center: Tuple[float, float] = (0.0, 0.0),
    zoom: float = 2.0,
    width: str = "100%",
    height: str = "700px",
    style: Union[
        str, Dict
    ] = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
    bearing: float = 0.0,
    pitch: float = 0.0,
    max_pitch: float = 85.0,
    controls: Optional[Dict[str, Any]] = None,
    **kwargs,
):
    """Initialize a MapLibre map.

    Args:
        center: Map center as (longitude, latitude).
        zoom: Initial zoom level.
        width: Map width as CSS string.
        height: Map height as CSS string. Default is "700px".
        style: MapLibre style URL or style object. Default is "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json".
        bearing: Map bearing in degrees.
        pitch: Map pitch in degrees.
        max_pitch: Maximum pitch angle in degrees (default: 85).
        controls: Dict of controls to add. If None, defaults to
            {"layer-control": True, "control-grid": True}.
            Use {"layer-control": {"collapsed": True}} for custom options.
        **kwargs: Additional widget arguments.
    """
    # Handle style shortcuts
    if isinstance(style, str) and not style.startswith("http"):
        try:
            style = get_maplibre_style(style)
        except ValueError:
            pass  # Use as-is

    super().__init__(
        center=list(center),
        zoom=zoom,
        width=width,
        height=height,
        style=style,
        bearing=bearing,
        pitch=pitch,
        max_pitch=max_pitch,
        **kwargs,
    )

    # Initialize layer dictionary
    self._layer_dict = {"Background": []}

    # Add default controls
    if controls is None:
        controls = {
            "layer-control": True,
            "control-grid": True,
        }

    for control_name, config in controls.items():
        if config:
            if control_name == "layer-control":
                self.add_layer_control(
                    **(config if isinstance(config, dict) else {})
                )
            elif control_name == "control-grid":
                self.add_control_grid(
                    **(config if isinstance(config, dict) else {})
                )
            else:
                self.add_control(
                    control_name, **(config if isinstance(config, dict) else {})
                )

add_arc_layer(self, data, name=None, get_source_position='source', get_target_position='target', get_source_color=None, get_target_color=None, get_width=1, get_height=1, great_circle=False, pickable=True, opacity=0.8, **kwargs)

Add an arc layer for origin-destination visualization using deck.gl.

Arc layers are ideal for visualizing connections between locations, such as flight routes, migration patterns, or network flows.

Parameters:

Name Type Description Default
data Any

Array of data objects with source/target coordinates. Each object should have source and target positions.

required
name Optional[str]

Layer ID. If None, auto-generated.

None
get_source_position Union[str, Any]

Accessor for source position [lng, lat]. Can be a string (property name) or a value.

'source'
get_target_position Union[str, Any]

Accessor for target position [lng, lat]. Can be a string (property name) or a value.

'target'
get_source_color Optional[List[int]]

Source end color as [r, g, b, a]. Default: [51, 136, 255, 255] (blue).

None
get_target_color Optional[List[int]]

Target end color as [r, g, b, a]. Default: [255, 136, 51, 255] (orange).

None
get_width Union[float, str]

Arc width in pixels. Can be a number or accessor.

1
get_height float

Arc height multiplier. Higher values create more curved arcs.

1
great_circle bool

Whether to draw arcs along great circles.

False
pickable bool

Whether layer responds to hover/click events.

True
opacity float

Layer opacity (0-1).

0.8
**kwargs

Additional ArcLayer props.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap()
>>> arcs = [
...     {"source": [-122.4, 37.8], "target": [-73.9, 40.7]},
...     {"source": [-122.4, 37.8], "target": [-0.1, 51.5]},
... ]
>>> m.add_arc_layer(arcs, name="flights")
Source code in anymap_ts/maplibre.py
def add_arc_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_source_position: Union[str, Any] = "source",
    get_target_position: Union[str, Any] = "target",
    get_source_color: Optional[List[int]] = None,
    get_target_color: Optional[List[int]] = None,
    get_width: Union[float, str] = 1,
    get_height: float = 1,
    great_circle: bool = False,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add an arc layer for origin-destination visualization using deck.gl.

    Arc layers are ideal for visualizing connections between locations,
    such as flight routes, migration patterns, or network flows.

    Args:
        data: Array of data objects with source/target coordinates.
            Each object should have source and target positions.
        name: Layer ID. If None, auto-generated.
        get_source_position: Accessor for source position [lng, lat].
            Can be a string (property name) or a value.
        get_target_position: Accessor for target position [lng, lat].
            Can be a string (property name) or a value.
        get_source_color: Source end color as [r, g, b, a].
            Default: [51, 136, 255, 255] (blue).
        get_target_color: Target end color as [r, g, b, a].
            Default: [255, 136, 51, 255] (orange).
        get_width: Arc width in pixels. Can be a number or accessor.
        get_height: Arc height multiplier. Higher values create more curved arcs.
        great_circle: Whether to draw arcs along great circles.
        pickable: Whether layer responds to hover/click events.
        opacity: Layer opacity (0-1).
        **kwargs: Additional ArcLayer props.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap()
        >>> arcs = [
        ...     {"source": [-122.4, 37.8], "target": [-73.9, 40.7]},
        ...     {"source": [-122.4, 37.8], "target": [-0.1, 51.5]},
        ... ]
        >>> m.add_arc_layer(arcs, name="flights")
    """
    layer_id = name or f"arc-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addArcLayer",
        id=layer_id,
        data=processed_data,
        getSourcePosition=get_source_position,
        getTargetPosition=get_target_position,
        getSourceColor=get_source_color or [51, 136, 255, 255],
        getTargetColor=get_target_color or [255, 136, 51, 255],
        getWidth=get_width,
        getHeight=get_height,
        greatCircle=great_circle,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "arc",
        },
    }

add_basemap(self, basemap='OpenStreetMap', attribution=None, **kwargs)

Add a basemap layer.

Parameters:

Name Type Description Default
basemap str

Name of basemap provider (e.g., "OpenStreetMap", "CartoDB.Positron")

'OpenStreetMap'
attribution Optional[str]

Custom attribution text

None
**kwargs

Additional options

{}
Source code in anymap_ts/maplibre.py
def add_basemap(
    self,
    basemap: str = "OpenStreetMap",
    attribution: Optional[str] = None,
    **kwargs,
) -> None:
    """Add a basemap layer.

    Args:
        basemap: Name of basemap provider (e.g., "OpenStreetMap", "CartoDB.Positron")
        attribution: Custom attribution text
        **kwargs: Additional options
    """
    url, default_attribution = get_basemap_url(basemap)
    self.call_js_method(
        "addBasemap",
        url,
        attribution=attribution or default_attribution,
        name=basemap,
        **kwargs,
    )

    # Track in layer dict
    basemaps = self._layer_dict.get("Basemaps", [])
    if basemap not in basemaps:
        self._layer_dict = {
            **self._layer_dict,
            "Basemaps": basemaps + [basemap],
        }

add_cog_control(self, position='top-right', collapsed=True, default_url=None, load_default_url=False, default_opacity=1.0, default_colormap='viridis', default_bands='1', default_rescale_min=0, default_rescale_max=255, **kwargs)

Add a COG layer control for loading Cloud Optimized GeoTIFFs via UI.

This provides an interactive panel for users to enter COG URLs and configure visualization parameters like colormap and rescaling.

Parameters:

Name Type Description Default
position str

Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').

'top-right'
collapsed bool

Whether the panel starts collapsed.

True
default_url Optional[str]

Default COG URL to pre-fill.

None
load_default_url bool

Whether to auto-load the default URL.

False
default_opacity float

Default layer opacity (0-1).

1.0
default_colormap str

Default colormap name.

'viridis'
default_bands str

Default bands (e.g., '1' or '1,2,3').

'1'
default_rescale_min float

Default minimum value for rescaling.

0
default_rescale_max float

Default maximum value for rescaling.

255
**kwargs

Additional control options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap()
>>> m.add_cog_control(
...     default_url="https://example.com/cog.tif",
...     default_colormap="terrain"
... )
Source code in anymap_ts/maplibre.py
def add_cog_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    default_url: Optional[str] = None,
    load_default_url: bool = False,
    default_opacity: float = 1.0,
    default_colormap: str = "viridis",
    default_bands: str = "1",
    default_rescale_min: float = 0,
    default_rescale_max: float = 255,
    **kwargs,
) -> None:
    """Add a COG layer control for loading Cloud Optimized GeoTIFFs via UI.

    This provides an interactive panel for users to enter COG URLs
    and configure visualization parameters like colormap and rescaling.

    Args:
        position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
        collapsed: Whether the panel starts collapsed.
        default_url: Default COG URL to pre-fill.
        load_default_url: Whether to auto-load the default URL.
        default_opacity: Default layer opacity (0-1).
        default_colormap: Default colormap name.
        default_bands: Default bands (e.g., '1' or '1,2,3').
        default_rescale_min: Default minimum value for rescaling.
        default_rescale_max: Default maximum value for rescaling.
        **kwargs: Additional control options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap()
        >>> m.add_cog_control(
        ...     default_url="https://example.com/cog.tif",
        ...     default_colormap="terrain"
        ... )
    """
    self.call_js_method(
        "addCogControl",
        position=position,
        collapsed=collapsed,
        defaultUrl=default_url or "",
        loadDefaultUrl=load_default_url,
        defaultOpacity=default_opacity,
        defaultColormap=default_colormap,
        defaultBands=default_bands,
        defaultRescaleMin=default_rescale_min,
        defaultRescaleMax=default_rescale_max,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "cog-control": {"position": position, "collapsed": collapsed},
    }

add_cog_layer(self, url, name=None, opacity=1.0, visible=True, debug=False, debug_opacity=0.25, max_error=0.125, fit_bounds=True, before_id=None, **kwargs)

Add a Cloud Optimized GeoTIFF (COG) layer using @developmentseed/deck.gl-geotiff.

This method renders COG files directly in the browser using GPU-accelerated deck.gl-geotiff rendering with automatic reprojection support.

Parameters:

Name Type Description Default
url str

URL to the Cloud Optimized GeoTIFF file.

required
name Optional[str]

Layer ID. If None, auto-generated.

None
opacity float

Layer opacity (0-1).

1.0
visible bool

Whether layer is visible.

True
debug bool

Show reprojection mesh for debugging.

False
debug_opacity float

Opacity of debug mesh (0-1).

0.25
max_error float

Maximum reprojection error in pixels. Lower values create denser mesh for better accuracy.

0.125
fit_bounds bool

Whether to fit map to COG bounds after loading.

True
before_id Optional[str]

ID of layer to insert before.

None
**kwargs

Additional COGLayer props.

{}

Examples:

>>> from anymap_ts import Map
>>> m = Map()
>>> m.add_cog_layer(
...     "https://example.com/landcover.tif",
...     name="landcover",
...     opacity=0.8
... )
Source code in anymap_ts/maplibre.py
def add_cog_layer(
    self,
    url: str,
    name: Optional[str] = None,
    opacity: float = 1.0,
    visible: bool = True,
    debug: bool = False,
    debug_opacity: float = 0.25,
    max_error: float = 0.125,
    fit_bounds: bool = True,
    before_id: Optional[str] = None,
    **kwargs,
) -> None:
    """Add a Cloud Optimized GeoTIFF (COG) layer using @developmentseed/deck.gl-geotiff.

    This method renders COG files directly in the browser using GPU-accelerated
    deck.gl-geotiff rendering with automatic reprojection support.

    Args:
        url: URL to the Cloud Optimized GeoTIFF file.
        name: Layer ID. If None, auto-generated.
        opacity: Layer opacity (0-1).
        visible: Whether layer is visible.
        debug: Show reprojection mesh for debugging.
        debug_opacity: Opacity of debug mesh (0-1).
        max_error: Maximum reprojection error in pixels. Lower values
            create denser mesh for better accuracy.
        fit_bounds: Whether to fit map to COG bounds after loading.
        before_id: ID of layer to insert before.
        **kwargs: Additional COGLayer props.

    Example:
        >>> from anymap_ts import Map
        >>> m = Map()
        >>> m.add_cog_layer(
        ...     "https://example.com/landcover.tif",
        ...     name="landcover",
        ...     opacity=0.8
        ... )
    """
    layer_id = name or f"cog-{len(self._layers)}"

    self.call_js_method(
        "addCOGLayer",
        id=layer_id,
        geotiff=url,
        opacity=opacity,
        visible=visible,
        debug=debug,
        debugOpacity=debug_opacity,
        maxError=max_error,
        fitBounds=fit_bounds,
        beforeId=before_id,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "cog",
            "url": url,
        },
    }

add_control(self, control_type, position='top-right', **kwargs)

Add a map control.

Parameters:

Name Type Description Default
control_type str

Type of control ('navigation', 'scale', 'fullscreen', etc.)

required
position str

Control position

'top-right'
**kwargs

Control-specific options

{}
Source code in anymap_ts/maplibre.py
def add_control(
    self,
    control_type: str,
    position: str = "top-right",
    **kwargs,
) -> None:
    """Add a map control.

    Args:
        control_type: Type of control ('navigation', 'scale', 'fullscreen', etc.)
        position: Control position
        **kwargs: Control-specific options
    """
    self.call_js_method("addControl", control_type, position=position, **kwargs)
    self._controls = {
        **self._controls,
        control_type: {"type": control_type, "position": position, **kwargs},
    }

add_control_grid(self, position='top-right', default_controls=None, exclude=None, rows=None, columns=None, collapsed=True, collapsible=True, title='', show_row_column_controls=True, gap=2, basemap_style_url=None, exclude_layers=None, **kwargs)

Add a ControlGrid with all default tools or a custom subset.

The ControlGrid provides a collapsible toolbar with up to 26 built-in controls (search, basemap, terrain, measure, draw, etc.) in a configurable grid layout.

Parameters:

Name Type Description Default
position str

Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').

'top-right'
default_controls Optional[List[str]]

Explicit list of control names to include. If None, all 26 default controls are used (minus any in exclude). Valid names: 'globe', 'fullscreen', 'north', 'terrain', 'search', 'viewState', 'inspect', 'vectorDataset', 'basemap', 'measure', 'geoEditor', 'bookmark', 'print', 'minimap', 'swipe', 'streetView', 'addVector', 'cogLayer', 'zarrLayer', 'pmtilesLayer', 'stacLayer', 'stacSearch', 'planetaryComputer', 'gaussianSplat', 'lidar', 'usgsLidar'.

None
exclude Optional[List[str]]

Controls to remove from the default set. Ignored when default_controls is provided.

None
rows Optional[int]

Number of grid rows (auto-calculated if None).

None
columns Optional[int]

Number of grid columns (auto-calculated if None).

None
collapsed bool

Whether the grid starts collapsed. Default True.

True
collapsible bool

Whether the grid can be collapsed. Default True.

True
title str

Optional header title for the grid.

''
show_row_column_controls bool

Show row/column input fields. Default True.

True
gap int

Gap between grid cells in pixels. Default 2.

2
basemap_style_url Optional[str]

Basemap style URL for SwipeControl layer grouping. If None, the current map style is used automatically.

None
exclude_layers Optional[List[str]]

Layer ID patterns to exclude from SwipeControl (e.g., 'measure-', 'gl-draw-'). If None, sensible defaults are applied.

None
**kwargs

Additional ControlGrid options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap()
>>> m.add_control_grid()  # All 26 controls
>>> # Or with customization:
>>> m.add_control_grid(
...     exclude=["minimap", "streetView"],
...     collapsed=True,
... )
Source code in anymap_ts/maplibre.py
def add_control_grid(
    self,
    position: str = "top-right",
    default_controls: Optional[List[str]] = None,
    exclude: Optional[List[str]] = None,
    rows: Optional[int] = None,
    columns: Optional[int] = None,
    collapsed: bool = True,
    collapsible: bool = True,
    title: str = "",
    show_row_column_controls: bool = True,
    gap: int = 2,
    basemap_style_url: Optional[str] = None,
    exclude_layers: Optional[List[str]] = None,
    **kwargs,
) -> None:
    """Add a ControlGrid with all default tools or a custom subset.

    The ControlGrid provides a collapsible toolbar with up to 26 built-in
    controls (search, basemap, terrain, measure, draw, etc.) in a
    configurable grid layout.

    Args:
        position: Control position ('top-left', 'top-right', 'bottom-left',
            'bottom-right').
        default_controls: Explicit list of control names to include. If None,
            all 26 default controls are used (minus any in ``exclude``).
            Valid names: 'globe', 'fullscreen', 'north', 'terrain', 'search',
            'viewState', 'inspect', 'vectorDataset', 'basemap', 'measure',
            'geoEditor', 'bookmark', 'print', 'minimap', 'swipe',
            'streetView', 'addVector', 'cogLayer', 'zarrLayer',
            'pmtilesLayer', 'stacLayer', 'stacSearch', 'planetaryComputer',
            'gaussianSplat', 'lidar', 'usgsLidar'.
        exclude: Controls to remove from the default set. Ignored when
            ``default_controls`` is provided.
        rows: Number of grid rows (auto-calculated if None).
        columns: Number of grid columns (auto-calculated if None).
        collapsed: Whether the grid starts collapsed. Default True.
        collapsible: Whether the grid can be collapsed. Default True.
        title: Optional header title for the grid.
        show_row_column_controls: Show row/column input fields. Default True.
        gap: Gap between grid cells in pixels. Default 2.
        basemap_style_url: Basemap style URL for SwipeControl layer grouping.
            If None, the current map style is used automatically.
        exclude_layers: Layer ID patterns to exclude from SwipeControl
            (e.g., 'measure-*', 'gl-draw-*'). If None, sensible defaults
            are applied.
        **kwargs: Additional ControlGrid options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap()
        >>> m.add_control_grid()  # All 26 controls
        >>> # Or with customization:
        >>> m.add_control_grid(
        ...     exclude=["minimap", "streetView"],
        ...     collapsed=True,
        ... )
    """
    js_kwargs: Dict[str, Any] = {
        "position": position,
        "collapsed": collapsed,
        "collapsible": collapsible,
        "showRowColumnControls": show_row_column_controls,
        "gap": gap,
        **kwargs,
    }
    if default_controls is not None:
        js_kwargs["defaultControls"] = default_controls
    if exclude is not None:
        js_kwargs["exclude"] = exclude
    if rows is not None:
        js_kwargs["rows"] = rows
    if columns is not None:
        js_kwargs["columns"] = columns
    if title:
        js_kwargs["title"] = title
    if basemap_style_url is not None:
        js_kwargs["basemapStyleUrl"] = basemap_style_url
    if exclude_layers is not None:
        js_kwargs["excludeLayers"] = exclude_layers

    self.call_js_method("addControlGrid", **js_kwargs)
    self._controls = {
        **self._controls,
        "control-grid": {"position": position, "collapsed": collapsed},
    }

add_draw_control(self, position='top-right', draw_modes=None, edit_modes=None, collapsed=False, **kwargs)

Add a drawing control using maplibre-gl-geo-editor.

Parameters:

Name Type Description Default
position str

Control position

'top-right'
draw_modes Optional[List[str]]

Drawing modes to enable (e.g., ['polygon', 'line', 'marker'])

None
edit_modes Optional[List[str]]

Edit modes to enable (e.g., ['select', 'drag', 'delete'])

None
collapsed bool

Whether control starts collapsed

False
**kwargs

Additional geo-editor options

{}
Source code in anymap_ts/maplibre.py
def add_draw_control(
    self,
    position: str = "top-right",
    draw_modes: Optional[List[str]] = None,
    edit_modes: Optional[List[str]] = None,
    collapsed: bool = False,
    **kwargs,
) -> None:
    """Add a drawing control using maplibre-gl-geo-editor.

    Args:
        position: Control position
        draw_modes: Drawing modes to enable (e.g., ['polygon', 'line', 'marker'])
        edit_modes: Edit modes to enable (e.g., ['select', 'drag', 'delete'])
        collapsed: Whether control starts collapsed
        **kwargs: Additional geo-editor options
    """
    if draw_modes is None:
        draw_modes = ["polygon", "line", "rectangle", "circle", "marker"]
    if edit_modes is None:
        edit_modes = ["select", "drag", "change", "rotate", "delete"]

    self.call_js_method(
        "addDrawControl",
        position=position,
        drawModes=draw_modes,
        editModes=edit_modes,
        collapsed=collapsed,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "draw-control": {
            "position": position,
            "drawModes": draw_modes,
            "editModes": edit_modes,
        },
    }

add_geojson(self, data, layer_type=None, paint=None, name=None, fit_bounds=True, **kwargs)

Add GeoJSON data to the map.

Parameters:

Name Type Description Default
data Union[str, Dict]

GeoJSON dict or URL to GeoJSON file

required
layer_type Optional[str]

MapLibre layer type

None
paint Optional[Dict]

MapLibre paint properties

None
name Optional[str]

Layer name

None
fit_bounds bool

Whether to fit map to data bounds

True
**kwargs

Additional layer options

{}
Source code in anymap_ts/maplibre.py
def add_geojson(
    self,
    data: Union[str, Dict],
    layer_type: Optional[str] = None,
    paint: Optional[Dict] = None,
    name: Optional[str] = None,
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add GeoJSON data to the map.

    Args:
        data: GeoJSON dict or URL to GeoJSON file
        layer_type: MapLibre layer type
        paint: MapLibre paint properties
        name: Layer name
        fit_bounds: Whether to fit map to data bounds
        **kwargs: Additional layer options
    """
    self.add_vector(
        data,
        layer_type=layer_type,
        paint=paint,
        name=name,
        fit_bounds=fit_bounds,
        **kwargs,
    )

add_layer(self, layer_id, layer_type, source, paint=None, layout=None, before_id=None, **kwargs)

Add a generic layer to the map.

Parameters:

Name Type Description Default
layer_id str

Unique layer identifier

required
layer_type str

MapLibre layer type

required
source Union[str, Dict]

Source ID or source configuration dict

required
paint Optional[Dict]

Paint properties

None
layout Optional[Dict]

Layout properties

None
before_id Optional[str]

ID of layer to insert before

None
**kwargs

Additional layer options

{}
Source code in anymap_ts/maplibre.py
def add_layer(
    self,
    layer_id: str,
    layer_type: str,
    source: Union[str, Dict],
    paint: Optional[Dict] = None,
    layout: Optional[Dict] = None,
    before_id: Optional[str] = None,
    **kwargs,
) -> None:
    """Add a generic layer to the map.

    Args:
        layer_id: Unique layer identifier
        layer_type: MapLibre layer type
        source: Source ID or source configuration dict
        paint: Paint properties
        layout: Layout properties
        before_id: ID of layer to insert before
        **kwargs: Additional layer options
    """
    layer_config = {
        "id": layer_id,
        "type": layer_type,
        "paint": paint or {},
        "layout": layout or {},
        **kwargs,
    }

    if isinstance(source, str):
        layer_config["source"] = source
    else:
        source_id = f"{layer_id}-source"
        self._sources = {**self._sources, source_id: source}
        self.call_js_method("addSource", source_id, **source)
        layer_config["source"] = source_id

    self._layers = {**self._layers, layer_id: layer_config}
    self.call_js_method("addLayer", beforeId=before_id, **layer_config)

add_layer_control(self, layers=None, position='top-right', collapsed=True)

Add a layer visibility control.

Uses maplibre-gl-layer-control for layer toggling and opacity.

Parameters:

Name Type Description Default
layers Optional[List[str]]

List of layer IDs to include (None = all layers)

None
position str

Control position

'top-right'
collapsed bool

Whether control starts collapsed

True
Source code in anymap_ts/maplibre.py
def add_layer_control(
    self,
    layers: Optional[List[str]] = None,
    position: str = "top-right",
    collapsed: bool = True,
) -> None:
    """Add a layer visibility control.

    Uses maplibre-gl-layer-control for layer toggling and opacity.

    Args:
        layers: List of layer IDs to include (None = all layers)
        position: Control position
        collapsed: Whether control starts collapsed
    """
    if layers is None:
        layers = list(self._layers.keys())

    self.call_js_method(
        "addLayerControl",
        layers=layers,
        position=position,
        collapsed=collapsed,
    )
    self._controls = {
        **self._controls,
        "layer-control": {"layers": layers, "position": position},
    }

add_lidar_control(self, position='top-right', collapsed=True, title='LiDAR Viewer', point_size=2, opacity=1.0, color_scheme='elevation', use_percentile=True, point_budget=1000000, pickable=False, auto_zoom=True, copc_loading_mode=None, streaming_point_budget=5000000, panel_max_height=600, **kwargs)

Add an interactive LiDAR control panel.

The LiDAR control provides a UI panel for loading, visualizing, and styling LiDAR point cloud files (LAS, LAZ, COPC formats).

Parameters:

Name Type Description Default
position str

Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').

'top-right'
collapsed bool

Whether the panel starts collapsed.

True
title str

Title displayed on the panel.

'LiDAR Viewer'
point_size float

Point size in pixels.

2
opacity float

Layer opacity (0-1).

1.0
color_scheme str

Color scheme ('elevation', 'intensity', 'classification', 'rgb').

'elevation'
use_percentile bool

Use 2-98% percentile for color scaling.

True
point_budget int

Maximum number of points to display.

1000000
pickable bool

Enable hover/click interactions.

False
auto_zoom bool

Auto-zoom to point cloud after loading.

True
copc_loading_mode Optional[str]

COPC loading mode ('full' or 'dynamic').

None
streaming_point_budget int

Point budget for streaming mode.

5000000
panel_max_height int

Maximum height of the panel in pixels.

600
**kwargs

Additional control options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap(pitch=60)
>>> m.add_lidar_control(color_scheme="classification", pickable=True)
Source code in anymap_ts/maplibre.py
def add_lidar_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    title: str = "LiDAR Viewer",
    point_size: float = 2,
    opacity: float = 1.0,
    color_scheme: str = "elevation",
    use_percentile: bool = True,
    point_budget: int = 1000000,
    pickable: bool = False,
    auto_zoom: bool = True,
    copc_loading_mode: Optional[str] = None,
    streaming_point_budget: int = 5000000,
    panel_max_height: int = 600,
    **kwargs,
) -> None:
    """Add an interactive LiDAR control panel.

    The LiDAR control provides a UI panel for loading, visualizing, and
    styling LiDAR point cloud files (LAS, LAZ, COPC formats).

    Args:
        position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
        collapsed: Whether the panel starts collapsed.
        title: Title displayed on the panel.
        point_size: Point size in pixels.
        opacity: Layer opacity (0-1).
        color_scheme: Color scheme ('elevation', 'intensity', 'classification', 'rgb').
        use_percentile: Use 2-98% percentile for color scaling.
        point_budget: Maximum number of points to display.
        pickable: Enable hover/click interactions.
        auto_zoom: Auto-zoom to point cloud after loading.
        copc_loading_mode: COPC loading mode ('full' or 'dynamic').
        streaming_point_budget: Point budget for streaming mode.
        panel_max_height: Maximum height of the panel in pixels.
        **kwargs: Additional control options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap(pitch=60)
        >>> m.add_lidar_control(color_scheme="classification", pickable=True)
    """
    self.call_js_method(
        "addLidarControl",
        position=position,
        collapsed=collapsed,
        title=title,
        pointSize=point_size,
        opacity=opacity,
        colorScheme=color_scheme,
        usePercentile=use_percentile,
        pointBudget=point_budget,
        pickable=pickable,
        autoZoom=auto_zoom,
        copcLoadingMode=copc_loading_mode,
        streamingPointBudget=streaming_point_budget,
        panelMaxHeight=panel_max_height,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "lidar-control": {"position": position, "collapsed": collapsed},
    }

add_lidar_layer(self, source, name=None, color_scheme='elevation', point_size=2, opacity=1.0, pickable=True, auto_zoom=True, streaming_mode=True, point_budget=1000000, **kwargs)

Load and display a LiDAR file from URL or local path.

Supports LAS, LAZ, and COPC (Cloud-Optimized Point Cloud) formats. For local files, the file is read and sent as base64 to JavaScript. For URLs, the data is loaded directly via streaming when possible.

Parameters:

Name Type Description Default
source Union[str, Path]

URL or local file path to the LiDAR file.

required
name Optional[str]

Layer identifier. If None, auto-generated.

None
color_scheme str

Color scheme ('elevation', 'intensity', 'classification', 'rgb').

'elevation'
point_size float

Point size in pixels.

2
opacity float

Layer opacity (0-1).

1.0
pickable bool

Enable hover/click interactions.

True
auto_zoom bool

Auto-zoom to point cloud after loading.

True
streaming_mode bool

Use streaming mode for large COPC files.

True
point_budget int

Maximum number of points to display.

1000000
**kwargs

Additional layer options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap(center=[-123.07, 44.05], zoom=14, pitch=60)
>>> m.add_lidar_layer(
...     source="https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz",
...     name="autzen",
...     color_scheme="classification",
... )
Source code in anymap_ts/maplibre.py
def add_lidar_layer(
    self,
    source: Union[str, Path],
    name: Optional[str] = None,
    color_scheme: str = "elevation",
    point_size: float = 2,
    opacity: float = 1.0,
    pickable: bool = True,
    auto_zoom: bool = True,
    streaming_mode: bool = True,
    point_budget: int = 1000000,
    **kwargs,
) -> None:
    """Load and display a LiDAR file from URL or local path.

    Supports LAS, LAZ, and COPC (Cloud-Optimized Point Cloud) formats.
    For local files, the file is read and sent as base64 to JavaScript.
    For URLs, the data is loaded directly via streaming when possible.

    Args:
        source: URL or local file path to the LiDAR file.
        name: Layer identifier. If None, auto-generated.
        color_scheme: Color scheme ('elevation', 'intensity', 'classification', 'rgb').
        point_size: Point size in pixels.
        opacity: Layer opacity (0-1).
        pickable: Enable hover/click interactions.
        auto_zoom: Auto-zoom to point cloud after loading.
        streaming_mode: Use streaming mode for large COPC files.
        point_budget: Maximum number of points to display.
        **kwargs: Additional layer options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap(center=[-123.07, 44.05], zoom=14, pitch=60)
        >>> m.add_lidar_layer(
        ...     source="https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz",
        ...     name="autzen",
        ...     color_scheme="classification",
        ... )
    """
    layer_id = name or f"lidar-{len(self._layers)}"

    # Check if source is a local file
    source_path = Path(source) if isinstance(source, (str, Path)) else None
    is_local = source_path is not None and source_path.exists()

    if is_local:
        # Read local file and encode as base64
        import base64

        with open(source_path, "rb") as f:
            file_data = f.read()
        source_b64 = base64.b64encode(file_data).decode("utf-8")

        self.call_js_method(
            "addLidarLayer",
            source=source_b64,
            name=layer_id,
            isBase64=True,
            filename=source_path.name,
            colorScheme=color_scheme,
            pointSize=point_size,
            opacity=opacity,
            pickable=pickable,
            autoZoom=auto_zoom,
            streamingMode=streaming_mode,
            pointBudget=point_budget,
            **kwargs,
        )
    else:
        # Load from URL
        self.call_js_method(
            "addLidarLayer",
            source=str(source),
            name=layer_id,
            isBase64=False,
            colorScheme=color_scheme,
            pointSize=point_size,
            opacity=opacity,
            pickable=pickable,
            autoZoom=auto_zoom,
            streamingMode=streaming_mode,
            pointBudget=point_budget,
            **kwargs,
        )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "lidar",
            "source": str(source),
        },
    }

add_pmtiles_control(self, position='top-right', collapsed=True, default_url=None, load_default_url=False, default_opacity=1.0, default_fill_color='steelblue', default_line_color='#333', default_pickable=True, **kwargs)

Add a PMTiles layer control for loading PMTiles files via UI.

This provides an interactive panel for users to enter PMTiles URLs and visualize vector or raster tile data.

Parameters:

Name Type Description Default
position str

Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').

'top-right'
collapsed bool

Whether the panel starts collapsed.

True
default_url Optional[str]

Default PMTiles URL to pre-fill.

None
load_default_url bool

Whether to auto-load the default URL.

False
default_opacity float

Default layer opacity (0-1).

1.0
default_fill_color str

Default fill color for vector polygons.

'steelblue'
default_line_color str

Default line color for vector lines.

'#333'
default_pickable bool

Whether features are clickable by default.

True
**kwargs

Additional control options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap()
>>> m.add_pmtiles_control(
...     default_url="https://pmtiles.io/protomaps(vector)ODbL_firenze.pmtiles",
...     load_default_url=True
... )
Source code in anymap_ts/maplibre.py
def add_pmtiles_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    default_url: Optional[str] = None,
    load_default_url: bool = False,
    default_opacity: float = 1.0,
    default_fill_color: str = "steelblue",
    default_line_color: str = "#333",
    default_pickable: bool = True,
    **kwargs,
) -> None:
    """Add a PMTiles layer control for loading PMTiles files via UI.

    This provides an interactive panel for users to enter PMTiles URLs
    and visualize vector or raster tile data.

    Args:
        position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
        collapsed: Whether the panel starts collapsed.
        default_url: Default PMTiles URL to pre-fill.
        load_default_url: Whether to auto-load the default URL.
        default_opacity: Default layer opacity (0-1).
        default_fill_color: Default fill color for vector polygons.
        default_line_color: Default line color for vector lines.
        default_pickable: Whether features are clickable by default.
        **kwargs: Additional control options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap()
        >>> m.add_pmtiles_control(
        ...     default_url="https://pmtiles.io/protomaps(vector)ODbL_firenze.pmtiles",
        ...     load_default_url=True
        ... )
    """
    self.call_js_method(
        "addPMTilesControl",
        position=position,
        collapsed=collapsed,
        defaultUrl=default_url or "",
        loadDefaultUrl=load_default_url,
        defaultOpacity=default_opacity,
        defaultFillColor=default_fill_color,
        defaultLineColor=default_line_color,
        defaultPickable=default_pickable,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "pmtiles-control": {"position": position, "collapsed": collapsed},
    }

add_point_cloud_layer(self, data, name=None, get_position='position', get_color=None, get_normal=None, point_size=2, size_units='pixels', pickable=True, opacity=1.0, material=True, coordinate_system=None, coordinate_origin=None, **kwargs)

Add a point cloud layer for 3D point visualization using deck.gl.

Point cloud layers render large collections of 3D points, ideal for LiDAR data, photogrammetry outputs, or any 3D point dataset.

Parameters:

Name Type Description Default
data Any

Array of point data with positions. Each point should have x, y, z coordinates (or position array).

required
name Optional[str]

Layer ID. If None, auto-generated.

None
get_position Union[str, Any]

Accessor for point position [x, y, z]. Can be a string (property name) or a value.

'position'
get_color Optional[Union[List[int], str]]

Accessor or value for point color [r, g, b, a]. Default: [255, 255, 255, 255] (white).

None
get_normal Optional[Union[str, Any]]

Accessor for point normal [nx, ny, nz] for lighting. Default: [0, 0, 1] (pointing up).

None
point_size float

Point size in pixels or meters (depends on size_units).

2
size_units str

Size units: 'pixels', 'meters', or 'common'.

'pixels'
pickable bool

Whether layer responds to hover/click events.

True
opacity float

Layer opacity (0-1).

1.0
material bool

Whether to enable lighting effects.

True
coordinate_system Optional[int]

Coordinate system for positions.

None
coordinate_origin Optional[List[float]]

Origin for coordinate system [x, y, z].

None
**kwargs

Additional PointCloudLayer props.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> import numpy as np
>>> m = MapLibreMap(pitch=45)
>>> points = [
...     {"position": [-122.4, 37.8, 100], "color": [255, 0, 0, 255]},
...     {"position": [-122.3, 37.7, 200], "color": [0, 255, 0, 255]},
... ]
>>> m.add_point_cloud_layer(points, point_size=5)
Source code in anymap_ts/maplibre.py
def add_point_cloud_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "position",
    get_color: Optional[Union[List[int], str]] = None,
    get_normal: Optional[Union[str, Any]] = None,
    point_size: float = 2,
    size_units: str = "pixels",
    pickable: bool = True,
    opacity: float = 1.0,
    material: bool = True,
    coordinate_system: Optional[int] = None,
    coordinate_origin: Optional[List[float]] = None,
    **kwargs,
) -> None:
    """Add a point cloud layer for 3D point visualization using deck.gl.

    Point cloud layers render large collections of 3D points, ideal for
    LiDAR data, photogrammetry outputs, or any 3D point dataset.

    Args:
        data: Array of point data with positions. Each point should have
            x, y, z coordinates (or position array).
        name: Layer ID. If None, auto-generated.
        get_position: Accessor for point position [x, y, z].
            Can be a string (property name) or a value.
        get_color: Accessor or value for point color [r, g, b, a].
            Default: [255, 255, 255, 255] (white).
        get_normal: Accessor for point normal [nx, ny, nz] for lighting.
            Default: [0, 0, 1] (pointing up).
        point_size: Point size in pixels or meters (depends on size_units).
        size_units: Size units: 'pixels', 'meters', or 'common'.
        pickable: Whether layer responds to hover/click events.
        opacity: Layer opacity (0-1).
        material: Whether to enable lighting effects.
        coordinate_system: Coordinate system for positions.
        coordinate_origin: Origin for coordinate system [x, y, z].
        **kwargs: Additional PointCloudLayer props.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> import numpy as np
        >>> m = MapLibreMap(pitch=45)
        >>> points = [
        ...     {"position": [-122.4, 37.8, 100], "color": [255, 0, 0, 255]},
        ...     {"position": [-122.3, 37.7, 200], "color": [0, 255, 0, 255]},
        ... ]
        >>> m.add_point_cloud_layer(points, point_size=5)
    """
    layer_id = name or f"pointcloud-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addPointCloudLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getColor=get_color or [255, 255, 255, 255],
        getNormal=get_normal,
        pointSize=point_size,
        sizeUnits=size_units,
        pickable=pickable,
        opacity=opacity,
        material=material,
        coordinateSystem=coordinate_system,
        coordinateOrigin=coordinate_origin,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "pointcloud",
        },
    }

add_raster(self, source, name=None, attribution='', indexes=None, colormap=None, vmin=None, vmax=None, nodata=None, fit_bounds=True, **kwargs)

Add a raster layer from a local file using localtileserver.

Parameters:

Name Type Description Default
source str

Path to local raster file

required
name Optional[str]

Layer name

None
attribution str

Attribution text

''
indexes Optional[List[int]]

Band indexes to use

None
colormap Optional[str]

Colormap name

None
vmin Optional[float]

Minimum value for colormap

None
vmax Optional[float]

Maximum value for colormap

None
nodata Optional[float]

NoData value

None
fit_bounds bool

Whether to fit map to raster bounds

True
**kwargs

Additional options

{}
Source code in anymap_ts/maplibre.py
def add_raster(
    self,
    source: str,
    name: Optional[str] = None,
    attribution: str = "",
    indexes: Optional[List[int]] = None,
    colormap: Optional[str] = None,
    vmin: Optional[float] = None,
    vmax: Optional[float] = None,
    nodata: Optional[float] = None,
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add a raster layer from a local file using localtileserver.

    Args:
        source: Path to local raster file
        name: Layer name
        attribution: Attribution text
        indexes: Band indexes to use
        colormap: Colormap name
        vmin: Minimum value for colormap
        vmax: Maximum value for colormap
        nodata: NoData value
        fit_bounds: Whether to fit map to raster bounds
        **kwargs: Additional options
    """
    try:
        from localtileserver import TileClient
    except ImportError:
        raise ImportError(
            "localtileserver is required for local raster support. "
            "Install with: pip install anymap-ts[raster]"
        )

    client = TileClient(source)

    # Build tile URL with parameters
    tile_url = client.get_tile_url()
    if indexes:
        tile_url = client.get_tile_url(indexes=indexes)
    if colormap:
        tile_url = client.get_tile_url(colormap=colormap)
    if vmin is not None or vmax is not None:
        tile_url = client.get_tile_url(
            vmin=vmin or client.min, vmax=vmax or client.max
        )
    if nodata is not None:
        tile_url = client.get_tile_url(nodata=nodata)

    layer_name = name or Path(source).stem

    self.add_tile_layer(
        tile_url,
        name=layer_name,
        attribution=attribution,
        **kwargs,
    )

    # Fit bounds if requested
    if fit_bounds:
        bounds = client.bounds()
        if bounds:
            self.fit_bounds([bounds[0], bounds[1], bounds[2], bounds[3]])

add_tile_layer(self, url, name=None, attribution='', min_zoom=0, max_zoom=22, **kwargs)

Add an XYZ tile layer.

Parameters:

Name Type Description Default
url str

Tile URL template with {x}, {y}, {z} placeholders

required
name Optional[str]

Layer name

None
attribution str

Attribution text

''
min_zoom int

Minimum zoom level

0
max_zoom int

Maximum zoom level

22
**kwargs

Additional options

{}
Source code in anymap_ts/maplibre.py
def add_tile_layer(
    self,
    url: str,
    name: Optional[str] = None,
    attribution: str = "",
    min_zoom: int = 0,
    max_zoom: int = 22,
    **kwargs,
) -> None:
    """Add an XYZ tile layer.

    Args:
        url: Tile URL template with {x}, {y}, {z} placeholders
        name: Layer name
        attribution: Attribution text
        min_zoom: Minimum zoom level
        max_zoom: Maximum zoom level
        **kwargs: Additional options
    """
    layer_id = name or f"tiles-{len(self._layers)}"

    self.call_js_method(
        "addTileLayer",
        url,
        name=layer_id,
        attribution=attribution,
        minZoom=min_zoom,
        maxZoom=max_zoom,
        **kwargs,
    )

    # Track layer
    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "raster",
            "source": f"{layer_id}-source",
        },
    }

add_vector(self, data, layer_type=None, paint=None, name=None, fit_bounds=True, **kwargs)

Add vector data to the map.

Supports GeoJSON, GeoDataFrame, or file paths to vector formats.

Parameters:

Name Type Description Default
data Any

GeoJSON dict, GeoDataFrame, or path to vector file

required
layer_type Optional[str]

MapLibre layer type ('circle', 'line', 'fill', 'symbol')

None
paint Optional[Dict]

MapLibre paint properties

None
name Optional[str]

Layer name

None
fit_bounds bool

Whether to fit map to data bounds

True
**kwargs

Additional layer options

{}
Source code in anymap_ts/maplibre.py
def add_vector(
    self,
    data: Any,
    layer_type: Optional[str] = None,
    paint: Optional[Dict] = None,
    name: Optional[str] = None,
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add vector data to the map.

    Supports GeoJSON, GeoDataFrame, or file paths to vector formats.

    Args:
        data: GeoJSON dict, GeoDataFrame, or path to vector file
        layer_type: MapLibre layer type ('circle', 'line', 'fill', 'symbol')
        paint: MapLibre paint properties
        name: Layer name
        fit_bounds: Whether to fit map to data bounds
        **kwargs: Additional layer options
    """
    geojson = to_geojson(data)

    # Handle URL data
    if geojson.get("type") == "url":
        self.add_geojson(
            geojson["url"],
            layer_type=layer_type,
            paint=paint,
            name=name,
            fit_bounds=fit_bounds,
            **kwargs,
        )
        return

    layer_id = name or f"vector-{len(self._layers)}"

    # Infer layer type if not specified
    if layer_type is None:
        layer_type = infer_layer_type(geojson)

    # Get default paint if not provided
    if paint is None:
        paint = get_default_paint(layer_type)

    # Get bounds
    bounds = get_bounds(data) if fit_bounds else None

    # Call JavaScript
    self.call_js_method(
        "addGeoJSON",
        data=geojson,
        name=layer_id,
        layerType=layer_type,
        paint=paint,
        fitBounds=fit_bounds,
        bounds=bounds,
        **kwargs,
    )

    # Track layer
    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": layer_type,
            "source": f"{layer_id}-source",
            "paint": paint,
        },
    }

add_vector_control(self, position='top-right', collapsed=True, default_url=None, load_default_url=False, default_opacity=1.0, default_fill_color='#3388ff', default_stroke_color='#3388ff', fit_bounds=True, **kwargs)

Add a vector layer control for loading vector datasets from URLs.

This provides an interactive panel for users to enter URLs to GeoJSON, GeoParquet, or FlatGeobuf datasets.

Parameters:

Name Type Description Default
position str

Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').

'top-right'
collapsed bool

Whether the panel starts collapsed.

True
default_url Optional[str]

Default vector URL to pre-fill.

None
load_default_url bool

Whether to auto-load the default URL.

False
default_opacity float

Default layer opacity (0-1).

1.0
default_fill_color str

Default fill color for polygons.

'#3388ff'
default_stroke_color str

Default stroke color for lines/outlines.

'#3388ff'
fit_bounds bool

Whether to fit map to loaded data bounds.

True
**kwargs

Additional control options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap()
>>> m.add_vector_control(
...     default_url="https://example.com/data.geojson",
...     default_fill_color="#ff0000"
... )
Source code in anymap_ts/maplibre.py
def add_vector_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    default_url: Optional[str] = None,
    load_default_url: bool = False,
    default_opacity: float = 1.0,
    default_fill_color: str = "#3388ff",
    default_stroke_color: str = "#3388ff",
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add a vector layer control for loading vector datasets from URLs.

    This provides an interactive panel for users to enter URLs to
    GeoJSON, GeoParquet, or FlatGeobuf datasets.

    Args:
        position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
        collapsed: Whether the panel starts collapsed.
        default_url: Default vector URL to pre-fill.
        load_default_url: Whether to auto-load the default URL.
        default_opacity: Default layer opacity (0-1).
        default_fill_color: Default fill color for polygons.
        default_stroke_color: Default stroke color for lines/outlines.
        fit_bounds: Whether to fit map to loaded data bounds.
        **kwargs: Additional control options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap()
        >>> m.add_vector_control(
        ...     default_url="https://example.com/data.geojson",
        ...     default_fill_color="#ff0000"
        ... )
    """
    self.call_js_method(
        "addVectorControl",
        position=position,
        collapsed=collapsed,
        defaultUrl=default_url or "",
        loadDefaultUrl=load_default_url,
        defaultOpacity=default_opacity,
        defaultFillColor=default_fill_color,
        defaultStrokeColor=default_stroke_color,
        fitBounds=fit_bounds,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "vector-control": {"position": position, "collapsed": collapsed},
    }

add_zarr_control(self, position='top-right', collapsed=True, default_url=None, load_default_url=False, default_opacity=1.0, default_variable='', default_clim=None, **kwargs)

Add a Zarr layer control for loading Zarr datasets via UI.

This provides an interactive panel for users to enter Zarr URLs and configure visualization parameters.

Parameters:

Name Type Description Default
position str

Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').

'top-right'
collapsed bool

Whether the panel starts collapsed.

True
default_url Optional[str]

Default Zarr URL to pre-fill.

None
load_default_url bool

Whether to auto-load the default URL.

False
default_opacity float

Default layer opacity (0-1).

1.0
default_variable str

Default variable name.

''
default_clim Optional[Tuple[float, float]]

Default color limits (min, max).

None
**kwargs

Additional control options.

{}

Examples:

>>> from anymap_ts import MapLibreMap
>>> m = MapLibreMap()
>>> m.add_zarr_control(
...     default_url="https://example.com/data.zarr",
...     default_variable="temperature"
... )
Source code in anymap_ts/maplibre.py
def add_zarr_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    default_url: Optional[str] = None,
    load_default_url: bool = False,
    default_opacity: float = 1.0,
    default_variable: str = "",
    default_clim: Optional[Tuple[float, float]] = None,
    **kwargs,
) -> None:
    """Add a Zarr layer control for loading Zarr datasets via UI.

    This provides an interactive panel for users to enter Zarr URLs
    and configure visualization parameters.

    Args:
        position: Control position ('top-left', 'top-right', 'bottom-left', 'bottom-right').
        collapsed: Whether the panel starts collapsed.
        default_url: Default Zarr URL to pre-fill.
        load_default_url: Whether to auto-load the default URL.
        default_opacity: Default layer opacity (0-1).
        default_variable: Default variable name.
        default_clim: Default color limits (min, max).
        **kwargs: Additional control options.

    Example:
        >>> from anymap_ts import MapLibreMap
        >>> m = MapLibreMap()
        >>> m.add_zarr_control(
        ...     default_url="https://example.com/data.zarr",
        ...     default_variable="temperature"
        ... )
    """
    self.call_js_method(
        "addZarrControl",
        position=position,
        collapsed=collapsed,
        defaultUrl=default_url or "",
        loadDefaultUrl=load_default_url,
        defaultOpacity=default_opacity,
        defaultVariable=default_variable,
        defaultClim=list(default_clim) if default_clim else [0, 1],
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "zarr-control": {"position": position, "collapsed": collapsed},
    }

add_zarr_layer(self, url, variable, name=None, colormap=None, clim=None, opacity=1.0, selector=None, minzoom=0, maxzoom=22, fill_value=None, spatial_dimensions=None, zarr_version=None, bounds=None, **kwargs)

Add a Zarr dataset layer for visualizing multidimensional array data.

This method renders Zarr pyramid datasets directly in the browser using GPU-accelerated WebGL rendering via @carbonplan/zarr-layer.

Parameters:

Name Type Description Default
url str

URL to the Zarr store (pyramid format recommended).

required
variable str

Variable name in the Zarr dataset to visualize.

required
name Optional[str]

Layer ID. If None, auto-generated.

None
colormap Optional[List[str]]

List of hex color strings for visualization. Example: ['#0000ff', '#ffff00', '#ff0000'] (blue-yellow-red). Default: ['#000000', '#ffffff'] (black to white).

None
clim Optional[Tuple[float, float]]

Color range as (min, max) tuple. Default: (0, 100).

None
opacity float

Layer opacity (0-1).

1.0
selector Optional[Dict[str, Any]]

Dimension selector for multi-dimensional data. Example: {"month": 4} to select 4th month.

None
minzoom int

Minimum zoom level for rendering.

0
maxzoom int

Maximum zoom level for rendering.

22
fill_value Optional[float]

No-data value (auto-detected from metadata if not set).

None
spatial_dimensions Optional[Dict[str, str]]

Custom spatial dimension names. Example: {"lat": "y", "lon": "x"} for non-standard names.

None
zarr_version Optional[int]

Zarr format version (2 or 3). Auto-detected if not set.

None
bounds Optional[List[float]]

Explicit spatial bounds [xMin, yMin, xMax, yMax]. Units depend on CRS: degrees for EPSG:4326, meters for EPSG:3857.

None
**kwargs

Additional ZarrLayer props.

{}

Examples:

>>> from anymap_ts import Map
>>> m = Map()
>>> m.add_zarr_layer(
...     "https://example.com/climate.zarr",
...     variable="temperature",
...     clim=(270, 310),
...     colormap=['#0000ff', '#ffff00', '#ff0000'],
...     selector={"month": 7}
... )
Source code in anymap_ts/maplibre.py
def add_zarr_layer(
    self,
    url: str,
    variable: str,
    name: Optional[str] = None,
    colormap: Optional[List[str]] = None,
    clim: Optional[Tuple[float, float]] = None,
    opacity: float = 1.0,
    selector: Optional[Dict[str, Any]] = None,
    minzoom: int = 0,
    maxzoom: int = 22,
    fill_value: Optional[float] = None,
    spatial_dimensions: Optional[Dict[str, str]] = None,
    zarr_version: Optional[int] = None,
    bounds: Optional[List[float]] = None,
    **kwargs,
) -> None:
    """Add a Zarr dataset layer for visualizing multidimensional array data.

    This method renders Zarr pyramid datasets directly in the browser using
    GPU-accelerated WebGL rendering via @carbonplan/zarr-layer.

    Args:
        url: URL to the Zarr store (pyramid format recommended).
        variable: Variable name in the Zarr dataset to visualize.
        name: Layer ID. If None, auto-generated.
        colormap: List of hex color strings for visualization.
            Example: ['#0000ff', '#ffff00', '#ff0000'] (blue-yellow-red).
            Default: ['#000000', '#ffffff'] (black to white).
        clim: Color range as (min, max) tuple.
            Default: (0, 100).
        opacity: Layer opacity (0-1).
        selector: Dimension selector for multi-dimensional data.
            Example: {"month": 4} to select 4th month.
        minzoom: Minimum zoom level for rendering.
        maxzoom: Maximum zoom level for rendering.
        fill_value: No-data value (auto-detected from metadata if not set).
        spatial_dimensions: Custom spatial dimension names.
            Example: {"lat": "y", "lon": "x"} for non-standard names.
        zarr_version: Zarr format version (2 or 3). Auto-detected if not set.
        bounds: Explicit spatial bounds [xMin, yMin, xMax, yMax].
            Units depend on CRS: degrees for EPSG:4326, meters for EPSG:3857.
        **kwargs: Additional ZarrLayer props.

    Example:
        >>> from anymap_ts import Map
        >>> m = Map()
        >>> m.add_zarr_layer(
        ...     "https://example.com/climate.zarr",
        ...     variable="temperature",
        ...     clim=(270, 310),
        ...     colormap=['#0000ff', '#ffff00', '#ff0000'],
        ...     selector={"month": 7}
        ... )
    """
    layer_id = name or f"zarr-{len(self._layers)}"

    self.call_js_method(
        "addZarrLayer",
        id=layer_id,
        source=url,
        variable=variable,
        colormap=colormap or ["#000000", "#ffffff"],
        clim=list(clim) if clim else [0, 100],
        opacity=opacity,
        selector=selector or {},
        minzoom=minzoom,
        maxzoom=maxzoom,
        fillValue=fill_value,
        spatialDimensions=spatial_dimensions,
        zarrVersion=zarr_version,
        bounds=bounds,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "zarr",
            "url": url,
            "variable": variable,
        },
    }

clear_draw_data(self)

Clear all drawn features.

Source code in anymap_ts/maplibre.py
def clear_draw_data(self) -> None:
    """Clear all drawn features."""
    self._draw_data = {"type": "FeatureCollection", "features": []}
    self.call_js_method("clearDrawData")

get_draw_data(self)

Get the current drawn features as GeoJSON.

Returns:

Type Description
Dict

GeoJSON FeatureCollection of drawn features

Source code in anymap_ts/maplibre.py
def get_draw_data(self) -> Dict:
    """Get the current drawn features as GeoJSON.

    Returns:
        GeoJSON FeatureCollection of drawn features
    """
    self.call_js_method("getDrawData")
    # Small delay to allow JS to update the trait
    import time

    time.sleep(0.1)
    return self._draw_data or {"type": "FeatureCollection", "features": []}

load_draw_data(self, geojson)

Load GeoJSON features into the drawing layer.

Parameters:

Name Type Description Default
geojson Dict

GeoJSON FeatureCollection to load

required
Source code in anymap_ts/maplibre.py
def load_draw_data(self, geojson: Dict) -> None:
    """Load GeoJSON features into the drawing layer.

    Args:
        geojson: GeoJSON FeatureCollection to load
    """
    self._draw_data = geojson
    self.call_js_method("loadDrawData", geojson)

remove_arc_layer(self, layer_id)

Remove an arc layer.

Parameters:

Name Type Description Default
layer_id str

Layer identifier to remove.

required
Source code in anymap_ts/maplibre.py
def remove_arc_layer(self, layer_id: str) -> None:
    """Remove an arc layer.

    Args:
        layer_id: Layer identifier to remove.
    """
    if layer_id in self._layers:
        layers = dict(self._layers)
        del layers[layer_id]
        self._layers = layers
    self.call_js_method("removeArcLayer", layer_id)

remove_cog_layer(self, layer_id)

Remove a COG layer.

Parameters:

Name Type Description Default
layer_id str

Layer identifier to remove.

required
Source code in anymap_ts/maplibre.py
def remove_cog_layer(self, layer_id: str) -> None:
    """Remove a COG layer.

    Args:
        layer_id: Layer identifier to remove.
    """
    if layer_id in self._layers:
        layers = dict(self._layers)
        del layers[layer_id]
        self._layers = layers
    self.call_js_method("removeCOGLayer", layer_id)

remove_control(self, control_type)

Remove a map control.

Parameters:

Name Type Description Default
control_type str

Type of control to remove

required
Source code in anymap_ts/maplibre.py
def remove_control(self, control_type: str) -> None:
    """Remove a map control.

    Args:
        control_type: Type of control to remove
    """
    self.call_js_method("removeControl", control_type)
    if control_type in self._controls:
        controls = dict(self._controls)
        del controls[control_type]
        self._controls = controls

remove_layer(self, layer_id)

Remove a layer from the map.

Parameters:

Name Type Description Default
layer_id str

Layer identifier to remove

required
Source code in anymap_ts/maplibre.py
def remove_layer(self, layer_id: str) -> None:
    """Remove a layer from the map.

    Args:
        layer_id: Layer identifier to remove
    """
    if layer_id in self._layers:
        layers = dict(self._layers)
        del layers[layer_id]
        self._layers = layers
    self.call_js_method("removeLayer", layer_id)

remove_lidar_layer(self, layer_id=None)

Remove a LiDAR layer.

Parameters:

Name Type Description Default
layer_id Optional[str]

Layer identifier to remove. If None, removes all LiDAR layers.

None
Source code in anymap_ts/maplibre.py
def remove_lidar_layer(self, layer_id: Optional[str] = None) -> None:
    """Remove a LiDAR layer.

    Args:
        layer_id: Layer identifier to remove. If None, removes all LiDAR layers.
    """
    if layer_id:
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self.call_js_method("removeLidarLayer", id=layer_id)
    else:
        # Remove all lidar layers
        layers = dict(self._layers)
        self._layers = {k: v for k, v in layers.items() if v.get("type") != "lidar"}
        self.call_js_method("removeLidarLayer")

remove_point_cloud_layer(self, layer_id)

Remove a point cloud layer.

Parameters:

Name Type Description Default
layer_id str

Layer identifier to remove.

required
Source code in anymap_ts/maplibre.py
def remove_point_cloud_layer(self, layer_id: str) -> None:
    """Remove a point cloud layer.

    Args:
        layer_id: Layer identifier to remove.
    """
    if layer_id in self._layers:
        layers = dict(self._layers)
        del layers[layer_id]
        self._layers = layers
    self.call_js_method("removePointCloudLayer", layer_id)

remove_zarr_layer(self, layer_id)

Remove a Zarr layer.

Parameters:

Name Type Description Default
layer_id str

Layer identifier to remove.

required
Source code in anymap_ts/maplibre.py
def remove_zarr_layer(self, layer_id: str) -> None:
    """Remove a Zarr layer.

    Args:
        layer_id: Layer identifier to remove.
    """
    if layer_id in self._layers:
        layers = dict(self._layers)
        del layers[layer_id]
        self._layers = layers
    self.call_js_method("removeZarrLayer", layer_id)

save_draw_data(self, filepath, driver=None)

Save drawn features to a file.

Parameters:

Name Type Description Default
filepath Union[str, Path]

Path to save file

required
driver Optional[str]

Output driver (auto-detected from extension if not provided)

None

Exceptions:

Type Description
ImportError

If geopandas is not installed

Source code in anymap_ts/maplibre.py
def save_draw_data(
    self,
    filepath: Union[str, Path],
    driver: Optional[str] = None,
) -> None:
    """Save drawn features to a file.

    Args:
        filepath: Path to save file
        driver: Output driver (auto-detected from extension if not provided)

    Raises:
        ImportError: If geopandas is not installed
    """
    try:
        import geopandas as gpd
    except ImportError:
        raise ImportError(
            "geopandas is required to save draw data. "
            "Install with: pip install anymap-ts[vector]"
        )

    data = self.get_draw_data()
    if not data.get("features"):
        print("No features to save")
        return

    gdf = gpd.GeoDataFrame.from_features(data["features"])
    filepath = Path(filepath)

    # Infer driver from extension
    if driver is None:
        ext = filepath.suffix.lower()
        driver_map = {
            ".geojson": "GeoJSON",
            ".json": "GeoJSON",
            ".shp": "ESRI Shapefile",
            ".gpkg": "GPKG",
        }
        driver = driver_map.get(ext, "GeoJSON")

    gdf.to_file(filepath, driver=driver)

set_lidar_color_scheme(self, color_scheme)

Set the LiDAR color scheme.

Parameters:

Name Type Description Default
color_scheme str

Color scheme ('elevation', 'intensity', 'classification', 'rgb').

required
Source code in anymap_ts/maplibre.py
def set_lidar_color_scheme(self, color_scheme: str) -> None:
    """Set the LiDAR color scheme.

    Args:
        color_scheme: Color scheme ('elevation', 'intensity', 'classification', 'rgb').
    """
    self.call_js_method("setLidarColorScheme", colorScheme=color_scheme)

set_lidar_opacity(self, opacity)

Set the LiDAR layer opacity.

Parameters:

Name Type Description Default
opacity float

Opacity value between 0 and 1.

required
Source code in anymap_ts/maplibre.py
def set_lidar_opacity(self, opacity: float) -> None:
    """Set the LiDAR layer opacity.

    Args:
        opacity: Opacity value between 0 and 1.
    """
    self.call_js_method("setLidarOpacity", opacity=opacity)

set_lidar_point_size(self, point_size)

Set the LiDAR point size.

Parameters:

Name Type Description Default
point_size float

Point size in pixels.

required
Source code in anymap_ts/maplibre.py
def set_lidar_point_size(self, point_size: float) -> None:
    """Set the LiDAR point size.

    Args:
        point_size: Point size in pixels.
    """
    self.call_js_method("setLidarPointSize", pointSize=point_size)

set_opacity(self, layer_id, opacity)

Set layer opacity.

Parameters:

Name Type Description Default
layer_id str

Layer identifier

required
opacity float

Opacity value between 0 and 1

required
Source code in anymap_ts/maplibre.py
def set_opacity(self, layer_id: str, opacity: float) -> None:
    """Set layer opacity.

    Args:
        layer_id: Layer identifier
        opacity: Opacity value between 0 and 1
    """
    self.call_js_method("setOpacity", layer_id, opacity)

set_visibility(self, layer_id, visible)

Set layer visibility.

Parameters:

Name Type Description Default
layer_id str

Layer identifier

required
visible bool

Whether layer should be visible

required
Source code in anymap_ts/maplibre.py
def set_visibility(self, layer_id: str, visible: bool) -> None:
    """Set layer visibility.

    Args:
        layer_id: Layer identifier
        visible: Whether layer should be visible
    """
    self.call_js_method("setVisibility", layer_id, visible)

update_zarr_layer(self, layer_id, selector=None, clim=None, colormap=None, opacity=None)

Update a Zarr layer's properties dynamically.

Parameters:

Name Type Description Default
layer_id str

Layer identifier.

required
selector Optional[Dict[str, Any]]

New dimension selector.

None
clim Optional[Tuple[float, float]]

New color range.

None
colormap Optional[List[str]]

New colormap.

None
opacity Optional[float]

New opacity value (0-1).

None
Source code in anymap_ts/maplibre.py
def update_zarr_layer(
    self,
    layer_id: str,
    selector: Optional[Dict[str, Any]] = None,
    clim: Optional[Tuple[float, float]] = None,
    colormap: Optional[List[str]] = None,
    opacity: Optional[float] = None,
) -> None:
    """Update a Zarr layer's properties dynamically.

    Args:
        layer_id: Layer identifier.
        selector: New dimension selector.
        clim: New color range.
        colormap: New colormap.
        opacity: New opacity value (0-1).
    """
    update_kwargs: Dict[str, Any] = {"id": layer_id}
    if selector is not None:
        update_kwargs["selector"] = selector
    if clim is not None:
        update_kwargs["clim"] = list(clim)
    if colormap is not None:
        update_kwargs["colormap"] = colormap
    if opacity is not None:
        update_kwargs["opacity"] = opacity
    self.call_js_method("updateZarrLayer", **update_kwargs)