Skip to content

mapbox module

Mapbox GL JS map widget implementation.

MapboxMap (MapWidget)

Interactive map widget using Mapbox GL JS.

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

Note

Requires a Mapbox access token. Set via MAPBOX_TOKEN environment variable or pass directly to the constructor.

Examples:

>>> from anymap_ts import MapboxMap
>>> m = MapboxMap(center=[-122.4, 37.8], zoom=10)
>>> m.add_basemap("mapbox://styles/mapbox/streets-v12")
>>> m
Source code in anymap_ts/mapbox.py
class MapboxMap(MapWidget):
    """Interactive map widget using Mapbox GL JS.

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

    Note:
        Requires a Mapbox access token. Set via MAPBOX_TOKEN environment
        variable or pass directly to the constructor.

    Example:
        >>> from anymap_ts import MapboxMap
        >>> m = MapboxMap(center=[-122.4, 37.8], zoom=10)
        >>> m.add_basemap("mapbox://styles/mapbox/streets-v12")
        >>> m
    """

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

    # Mapbox-specific traits
    access_token = traitlets.Unicode("").tag(sync=True)
    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 = "600px",
        style: str = "mapbox://styles/mapbox/streets-v12",
        bearing: float = 0.0,
        pitch: float = 0.0,
        max_pitch: float = 85.0,
        access_token: Optional[str] = None,
        controls: Optional[Dict[str, Any]] = None,
        **kwargs,
    ):
        """Initialize a Mapbox map.

        Args:
            center: Map center as (longitude, latitude).
            zoom: Initial zoom level.
            width: Map width as CSS string.
            height: Map height as CSS string.
            style: Mapbox style URL (e.g., "mapbox://styles/mapbox/streets-v12").
            bearing: Map bearing in degrees.
            pitch: Map pitch in degrees.
            max_pitch: Maximum pitch angle in degrees (default: 85).
            access_token: Mapbox access token. If None, reads from MAPBOX_TOKEN env var.
            controls: Dict of controls to add (e.g., {"navigation": True}).
            **kwargs: Additional widget arguments.
        """
        # Get access token
        token = access_token or get_mapbox_token()
        if not token:
            print(
                "Warning: No Mapbox access token provided. "
                "Set MAPBOX_TOKEN environment variable or pass access_token parameter."
            )

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

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

        # Storage for auto-discovered PMTiles layer styles
        self._pmtiles_styles: Dict[str, List[Dict[str, Any]]] = {}

        # Add default controls
        if controls is None:
            controls = {"navigation": True, "fullscreen": True}

        for control_name, config in controls.items():
            if config:
                self.add_control(
                    control_name, **(config if isinstance(config, dict) else {})
                )

    def set_access_token(self, token: str) -> None:
        """Set the Mapbox access token.

        Args:
            token: Mapbox access token.
        """
        self.access_token = token

    # -------------------------------------------------------------------------
    # Layer Dict Helpers
    # -------------------------------------------------------------------------

    def _add_to_layer_dict(self, layer_id: str, category: str = "Overlays") -> None:
        """Add a layer to the layer dictionary for UI tracking."""
        layers = self._layer_dict.get(category, [])
        if layer_id not in layers:
            self._layer_dict = {
                **self._layer_dict,
                category: layers + [layer_id],
            }

    def _remove_from_layer_dict(self, layer_id: str) -> None:
        """Remove a layer from the layer dictionary."""
        new_dict = {}
        for category, layers in self._layer_dict.items():
            if layer_id in layers:
                new_layers = [lid for lid in layers if lid != layer_id]
                if new_layers:
                    new_dict[category] = new_layers
            else:
                new_dict[category] = layers
        self._layer_dict = new_dict

    def _validate_opacity(self, opacity: float, param_name: str = "opacity") -> float:
        """Validate opacity value is between 0 and 1."""
        if not 0 <= opacity <= 1:
            raise ValueError(f"{param_name} must be between 0 and 1, got {opacity}")
        return opacity

    def _validate_position(self, position: str) -> str:
        """Validate control position is valid."""
        valid_positions = ["top-left", "top-right", "bottom-left", "bottom-right"]
        if position not in valid_positions:
            raise ValueError(
                f"Position must be one of: {', '.join(valid_positions)}, got '{position}'"
            )
        return position

    def _remove_layer_internal(self, layer_id: str, js_method: str) -> None:
        """Internal helper to remove a layer."""
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self._remove_from_layer_dict(layer_id)
        self.call_js_method(js_method, layer_id)

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

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

        For Mapbox styles, use the style URL format:
        - "mapbox://styles/mapbox/streets-v12"
        - "mapbox://styles/mapbox/satellite-v9"
        - "mapbox://styles/mapbox/satellite-streets-v12"
        - "mapbox://styles/mapbox/light-v11"
        - "mapbox://styles/mapbox/dark-v11"
        - "mapbox://styles/mapbox/outdoors-v12"

        Or use XYZ tile URLs for custom basemaps.

        Args:
            basemap: Mapbox style URL or XYZ tile URL.
            attribution: Custom attribution text.
            **kwargs: Additional options.
        """
        # If it's a Mapbox style URL, set it as the map style
        if basemap.startswith("mapbox://"):
            self.style = basemap
            return

        # Otherwise, treat as XYZ tile URL
        try:
            url, default_attribution = get_basemap_url(basemap)
        except (ValueError, KeyError):
            url = basemap
            default_attribution = ""

        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: Mapbox layer type ('circle', 'line', 'fill', 'symbol').
            paint: Mapbox paint properties.
            name: Layer name.
            fit_bounds: Whether to fit map to data bounds.
            **kwargs: Additional layer options.
        """
        geojson = to_geojson(data)

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

        # Handle URL data - fetch GeoJSON to get bounds and infer layer type
        if geojson.get("type") == "url":
            url = geojson["url"]
            geojson = fetch_geojson(url)

        # 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 (use geojson dict, not original data which may be a URL)
        bounds = get_bounds(geojson) 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,
            },
        }
        self._add_to_layer_dict(layer_id, "Vector")

    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: Mapbox layer type.
            paint: Mapbox 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_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",
            },
        }
        self._add_to_layer_dict(layer_id, "Raster")

    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."""
        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)

        tile_params = {}
        if indexes:
            tile_params["indexes"] = indexes
        if colormap:
            tile_params["colormap"] = colormap
        if vmin is not None or vmax is not None:
            tile_params["vmin"] = vmin if vmin is not None else client.min
            tile_params["vmax"] = vmax if vmax is not None else client.max
        if nodata is not None:
            tile_params["nodata"] = nodata

        tile_url = client.get_tile_url(**tile_params)

        layer_name = name or Path(source).stem

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

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

    def add_stac_layer(
        self,
        url: Optional[str] = None,
        item: Optional[Any] = None,
        assets: Optional[List[str]] = None,
        colormap: Optional[str] = None,
        rescale: Optional[List[float]] = None,
        opacity: float = 1.0,
        layer_id: Optional[str] = None,
        titiler_endpoint: str = "https://titiler.xyz",
        attribution: str = "STAC",
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add a STAC (SpatioTemporal Asset Catalog) layer to the map."""
        if url is None and item is None:
            raise ValueError("Either 'url' or 'item' must be provided")

        if url is not None and item is not None:
            raise ValueError("Provide either 'url' or 'item', not both")

        if item is not None:
            try:
                if hasattr(item, "to_dict") and hasattr(item, "self_href"):
                    stac_url = item.self_href
                    if not stac_url and hasattr(item, "links"):
                        for link in item.links:
                            if link.rel == "self":
                                stac_url = link.href
                                break
                    if not stac_url:
                        raise ValueError("STAC item must have a self_href or self link")
                else:
                    raise ValueError(
                        "Item must be a pystac Item object with to_dict() and self_href"
                    )
            except Exception as e:
                raise ValueError(f"Invalid STAC item: {e}")
        else:
            stac_url = url

        tile_params = {"url": stac_url}
        if assets:
            tile_params["assets"] = ",".join(assets)
        if colormap:
            tile_params["colormap_name"] = colormap
        if rescale:
            if len(rescale) == 2:
                tile_params["rescale"] = f"{rescale[0]},{rescale[1]}"
            else:
                raise ValueError("rescale must be a list of two values [min, max]")

        query_string = urlencode(tile_params)
        tile_url = f"{titiler_endpoint.rstrip('/')}/stac/tiles/{{z}}/{{x}}/{{y}}?{query_string}"

        layer_name = layer_id or f"stac-{len(self._layers)}"

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

        if fit_bounds and item is not None:
            try:
                bbox = item.bbox
                if bbox and len(bbox) == 4:
                    self.fit_bounds([[bbox[0], bbox[1]], [bbox[2], bbox[3]]])
            except Exception:
                pass

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

    def add_heatmap(
        self,
        data: Any,
        weight_property: Optional[str] = None,
        radius: int = 20,
        intensity: float = 1.0,
        colormap: Optional[List] = None,
        opacity: float = 0.8,
        name: Optional[str] = None,
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add a heatmap layer to the map."""
        self._validate_opacity(opacity)
        layer_id = name or f"heatmap-{len(self._layers)}"

        geojson = to_geojson(data)

        if geojson.get("type") == "url":
            url = geojson["url"]
            geojson = fetch_geojson(url)

        if colormap is None:
            colormap = [
                [0, "rgba(33,102,172,0)"],
                [0.2, "rgb(103,169,207)"],
                [0.4, "rgb(209,229,240)"],
                [0.6, "rgb(253,219,199)"],
                [0.8, "rgb(239,138,98)"],
                [1, "rgb(178,24,43)"],
            ]

        paint = {
            "heatmap-radius": radius,
            "heatmap-intensity": intensity,
            "heatmap-opacity": opacity,
            "heatmap-color": [
                "interpolate",
                ["linear"],
                ["heatmap-density"],
            ],
        }

        for stop, color in colormap:
            paint["heatmap-color"].extend([stop, color])

        if weight_property:
            paint["heatmap-weight"] = ["get", weight_property]

        bounds = get_bounds(geojson) if fit_bounds else None

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

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "heatmap",
                "source": f"{layer_id}-source",
                "paint": paint,
            },
        }
        self._add_to_layer_dict(layer_id, "Heatmap")

    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 deck.gl-raster.

        This method renders COG files directly in the browser using GPU-accelerated
        deck.gl 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 MapboxMap
            >>> m = MapboxMap()
            >>> 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,
            },
        }
        self._add_to_layer_dict(layer_id, "Raster")

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

    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."""
        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,
            },
        }
        self._add_to_layer_dict(layer_id, "Raster")

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

    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."""
        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)

    def add_pmtiles_layer(
        self,
        url: str,
        layer_id: Optional[str] = None,
        style: Optional[Dict[str, Any]] = None,
        opacity: float = 1.0,
        visible: bool = True,
        fit_bounds: bool = False,
        source_type: str = "vector",
        prefix: str = "",
        popup: Optional[Union[bool, List[str], str]] = None,
        **kwargs,
    ) -> None:
        """Add a PMTiles layer for efficient vector or raster tile serving.

        When no style is provided for vector PMTiles, the method automatically
        discovers all source layers from the PMTiles metadata and renders each
        one with a distinct color. Geometry types from the metadata determine
        the layer type: Polygon becomes fill, LineString becomes line, and
        Point becomes circle. For text labels and icons, use the symbol layer
        type with layout properties like text-field and icon-image.

        Args:
            url: URL to the PMTiles file.
            layer_id: Layer identifier. If None, auto-generated.
            style: Layer style configuration. If None and source_type is
                "vector", auto-discovers all source layers with distinct colors.
            opacity: Layer opacity (0-1).
            visible: Whether layer is initially visible.
            fit_bounds: Whether to fit map to layer bounds after loading.
            source_type: Source type - "vector" or "raster".
            prefix: Prefix for auto-discovered layer names in the layer
                control. Defaults to empty string (no prefix).
            popup: Configure popups on click. Accepts "all" or True (all
                properties), a list of property names, or an HTML template
                string with {property_name} placeholders. Defaults to None
                (no popup).
            **kwargs: Additional layer options.
        """
        layer_id = layer_id or f"pmtiles-{len(self._layers)}"

        popup_config: Optional[Dict[str, Any]] = None
        if popup is True or popup == "all":
            popup_config = {"enabled": True}
        elif isinstance(popup, list):
            popup_config = {"enabled": True, "properties": popup}
        elif isinstance(popup, str):
            popup_config = {"enabled": True, "template": popup}

        self.call_js_method(
            "addPMTilesLayer",
            url=url,
            id=layer_id,
            style=style or {},
            opacity=opacity,
            visible=visible,
            fitBounds=fit_bounds,
            sourceType=source_type,
            prefix=prefix,
            popup=popup_config,
            name=layer_id,
            **kwargs,
        )

        # Listen for auto-discovered styles from JS
        if style is None and source_type == "vector":

            def _on_discovered(data: Dict[str, Any]) -> None:
                discovered_id = data.get("layerId", layer_id)
                self._pmtiles_styles[discovered_id] = data.get("subLayers", [])

            self.on_map_event("pmtiles_layers_discovered", _on_discovered)

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "pmtiles",
                "url": url,
                "source_type": source_type,
            },
        }
        category = "Vector" if source_type == "vector" else "Raster"
        self._add_to_layer_dict(layer_id, category)

    @property
    def pmtiles_styles(self) -> Dict[str, List[Dict[str, Any]]]:
        """Get auto-discovered PMTiles layer styles.

        Returns a dict keyed by layer_id, where each value is a list of
        sub-layer dicts containing: id, sourceLayer, geometryType, color,
        type, and paint.

        Returns:
            Dict mapping layer IDs to lists of sub-layer style dicts.
        """
        return dict(self._pmtiles_styles)

    def remove_pmtiles_layer(self, layer_id: str) -> None:
        """Remove a PMTiles layer."""
        self._remove_layer_internal(layer_id, "removePMTilesLayer")
        self._pmtiles_styles.pop(layer_id, None)

    # -------------------------------------------------------------------------
    # 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 MapboxMap
            >>> m = MapboxMap()
            >>> 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",
            },
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

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

    # -------------------------------------------------------------------------
    # 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 MapboxMap
            >>> m = MapboxMap(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",
            },
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

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

    def add_scatterplot_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_radius: Union[float, str] = 5,
        get_fill_color: Optional[Union[List[int], str]] = None,
        get_line_color: Optional[Union[List[int], str]] = None,
        radius_scale: float = 1,
        radius_min_pixels: float = 1,
        radius_max_pixels: float = 100,
        line_width_min_pixels: float = 1,
        stroked: bool = True,
        filled: bool = True,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a scatterplot layer using deck.gl."""
        layer_id = name or f"scatterplot-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addScatterplotLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getRadius=get_radius,
            getFillColor=get_fill_color or [51, 136, 255, 200],
            getLineColor=get_line_color or [255, 255, 255, 255],
            radiusScale=radius_scale,
            radiusMinPixels=radius_min_pixels,
            radiusMaxPixels=radius_max_pixels,
            lineWidthMinPixels=line_width_min_pixels,
            stroked=stroked,
            filled=filled,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {"id": layer_id, "type": "scatterplot"},
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_path_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_path: Union[str, Any] = "path",
        get_color: Optional[Union[List[int], str]] = None,
        get_width: Union[float, str] = 1,
        width_scale: float = 1,
        width_min_pixels: float = 1,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a path layer using deck.gl."""
        layer_id = name or f"path-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addPathLayer",
            id=layer_id,
            data=processed_data,
            getPath=get_path,
            getColor=get_color or [51, 136, 255, 200],
            getWidth=get_width,
            widthScale=width_scale,
            widthMinPixels=width_min_pixels,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "path"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_polygon_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_polygon: Union[str, Any] = "polygon",
        get_fill_color: Optional[Union[List[int], str]] = None,
        get_line_color: Optional[Union[List[int], str]] = None,
        get_line_width: Union[float, str] = 1,
        get_elevation: Union[float, str] = 0,
        extruded: bool = False,
        wireframe: bool = False,
        filled: bool = True,
        stroked: bool = True,
        line_width_min_pixels: float = 1,
        pickable: bool = True,
        opacity: float = 0.5,
        **kwargs,
    ) -> None:
        """Add a polygon layer using deck.gl."""
        layer_id = name or f"polygon-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addPolygonLayer",
            id=layer_id,
            data=processed_data,
            getPolygon=get_polygon,
            getFillColor=get_fill_color or [51, 136, 255, 128],
            getLineColor=get_line_color or [0, 0, 255, 255],
            getLineWidth=get_line_width,
            getElevation=get_elevation,
            extruded=extruded,
            wireframe=wireframe,
            filled=filled,
            stroked=stroked,
            lineWidthMinPixels=line_width_min_pixels,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "polygon"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_hexagon_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        radius: float = 1000,
        elevation_scale: float = 4,
        extruded: bool = True,
        color_range: Optional[List[List[int]]] = None,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a hexagon layer using deck.gl."""
        layer_id = name or f"hexagon-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        default_color_range = [
            [1, 152, 189],
            [73, 227, 206],
            [216, 254, 181],
            [254, 237, 177],
            [254, 173, 84],
            [209, 55, 78],
        ]

        self.call_js_method(
            "addHexagonLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            radius=radius,
            elevationScale=elevation_scale,
            extruded=extruded,
            colorRange=color_range or default_color_range,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "hexagon"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_deck_heatmap_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_weight: Union[float, str] = 1,
        radius_pixels: float = 30,
        intensity: float = 1,
        threshold: float = 0.05,
        color_range: Optional[List[List[int]]] = None,
        opacity: float = 1,
        **kwargs,
    ) -> None:
        """Add a GPU-accelerated heatmap layer using deck.gl."""
        layer_id = name or f"deck-heatmap-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        default_color_range = [
            [255, 255, 178, 25],
            [254, 217, 118, 85],
            [254, 178, 76, 127],
            [253, 141, 60, 170],
            [240, 59, 32, 212],
            [189, 0, 38, 255],
        ]

        self.call_js_method(
            "addHeatmapLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getWeight=get_weight,
            radiusPixels=radius_pixels,
            intensity=intensity,
            threshold=threshold,
            colorRange=color_range or default_color_range,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {"id": layer_id, "type": "deck-heatmap"},
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_grid_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        cell_size: float = 200,
        elevation_scale: float = 4,
        extruded: bool = True,
        color_range: Optional[List[List[int]]] = None,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a grid layer using deck.gl."""
        layer_id = name or f"grid-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        default_color_range = [
            [1, 152, 189],
            [73, 227, 206],
            [216, 254, 181],
            [254, 237, 177],
            [254, 173, 84],
            [209, 55, 78],
        ]

        self.call_js_method(
            "addGridLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            cellSize=cell_size,
            elevationScale=elevation_scale,
            extruded=extruded,
            colorRange=color_range or default_color_range,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "grid"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_icon_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_icon: Union[str, Any] = "icon",
        get_size: Union[float, str] = 20,
        get_color: Optional[Union[List[int], str]] = None,
        icon_atlas: Optional[str] = None,
        icon_mapping: Optional[Dict] = None,
        pickable: bool = True,
        opacity: float = 1,
        **kwargs,
    ) -> None:
        """Add an icon layer using deck.gl."""
        layer_id = name or f"icon-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addIconLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getIcon=get_icon,
            getSize=get_size,
            getColor=get_color or [255, 255, 255, 255],
            iconAtlas=icon_atlas,
            iconMapping=icon_mapping,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "icon"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_text_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_text: Union[str, Any] = "text",
        get_size: Union[float, str] = 12,
        get_color: Optional[Union[List[int], str]] = None,
        get_angle: Union[float, str] = 0,
        text_anchor: str = "middle",
        alignment_baseline: str = "center",
        pickable: bool = True,
        opacity: float = 1,
        **kwargs,
    ) -> None:
        """Add a text layer using deck.gl."""
        layer_id = name or f"text-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addTextLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getText=get_text,
            getSize=get_size,
            getColor=get_color or [0, 0, 0, 255],
            getAngle=get_angle,
            getTextAnchor=text_anchor,
            getAlignmentBaseline=alignment_baseline,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "text"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_geojson_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_fill_color: Optional[Union[List[int], str]] = None,
        get_line_color: Optional[Union[List[int], str]] = None,
        get_line_width: Union[float, str] = 1,
        get_point_radius: Union[float, str] = 5,
        get_elevation: Union[float, str] = 0,
        extruded: bool = False,
        wireframe: bool = False,
        filled: bool = True,
        stroked: bool = True,
        line_width_min_pixels: float = 1,
        point_radius_min_pixels: float = 2,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a GeoJSON layer with auto-styling using deck.gl."""
        layer_id = name or f"geojson-deck-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addGeoJsonLayer",
            id=layer_id,
            data=processed_data,
            getFillColor=get_fill_color or [51, 136, 255, 128],
            getLineColor=get_line_color or [0, 0, 0, 255],
            getLineWidth=get_line_width,
            getPointRadius=get_point_radius,
            getElevation=get_elevation,
            extruded=extruded,
            wireframe=wireframe,
            filled=filled,
            stroked=stroked,
            lineWidthMinPixels=line_width_min_pixels,
            pointRadiusMinPixels=point_radius_min_pixels,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {"id": layer_id, "type": "geojson-deck"},
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_contour_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_weight: Union[float, str] = 1,
        cell_size: float = 200,
        contours: Optional[List[Dict]] = None,
        pickable: bool = True,
        opacity: float = 1,
        **kwargs,
    ) -> None:
        """Add a contour layer using deck.gl."""
        layer_id = name or f"contour-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        default_contours = [
            {"threshold": 1, "color": [255, 255, 255], "strokeWidth": 1},
            {"threshold": 5, "color": [51, 136, 255], "strokeWidth": 2},
            {"threshold": 10, "color": [0, 0, 255], "strokeWidth": 3},
        ]

        self.call_js_method(
            "addContourLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getWeight=get_weight,
            cellSize=cell_size,
            contours=contours or default_contours,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "contour"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_screen_grid_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_weight: Union[float, str] = 1,
        cell_size_pixels: float = 50,
        color_range: Optional[List[List[int]]] = None,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a screen grid layer using deck.gl."""
        layer_id = name or f"screengrid-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        default_color_range = [
            [255, 255, 178, 25],
            [254, 217, 118, 85],
            [254, 178, 76, 127],
            [253, 141, 60, 170],
            [240, 59, 32, 212],
            [189, 0, 38, 255],
        ]

        self.call_js_method(
            "addScreenGridLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getWeight=get_weight,
            cellSizePixels=cell_size_pixels,
            colorRange=color_range or default_color_range,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {"id": layer_id, "type": "screengrid"},
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_trips_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_path: Union[str, Any] = "waypoints",
        get_timestamps: Union[str, Any] = "timestamps",
        get_color: Optional[Union[List[int], str]] = None,
        width_min_pixels: float = 2,
        trail_length: float = 180,
        current_time: float = 0,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a trips layer using deck.gl."""
        layer_id = name or f"trips-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addTripsLayer",
            id=layer_id,
            data=processed_data,
            getPath=get_path,
            getTimestamps=get_timestamps,
            getColor=get_color or [253, 128, 93],
            widthMinPixels=width_min_pixels,
            trailLength=trail_length,
            currentTime=current_time,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "trips"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_line_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_source_position: Union[str, Any] = "sourcePosition",
        get_target_position: Union[str, Any] = "targetPosition",
        get_color: Optional[Union[List[int], str]] = None,
        get_width: Union[float, str] = 1,
        width_min_pixels: float = 1,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a line layer using deck.gl."""
        layer_id = name or f"line-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addLineLayer",
            id=layer_id,
            data=processed_data,
            getSourcePosition=get_source_position,
            getTargetPosition=get_target_position,
            getColor=get_color or [51, 136, 255, 200],
            getWidth=get_width,
            widthMinPixels=width_min_pixels,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "line"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_deckgl_layer(
        self,
        layer_type: str,
        data: Any,
        name: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add a generic deck.gl layer to the map."""
        layer_type_clean = layer_type.replace("Layer", "")
        prefix = layer_type_clean.lower()
        layer_id = name or f"{prefix}-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addDeckGLLayer",
            layerType=layer_type,
            id=layer_id,
            data=processed_data,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": layer_type}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def remove_deck_layer(self, layer_id: str) -> None:
        """Remove a deck.gl layer from the map."""
        self._remove_layer_internal(layer_id, "removeDeckLayer")

    def add_column_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_fill_color: Optional[Union[List[int], str]] = None,
        get_line_color: Optional[Union[List[int], str]] = None,
        get_elevation: Union[float, str] = 1000,
        radius: float = 1000,
        disk_resolution: int = 20,
        elevation_scale: float = 1,
        coverage: float = 1,
        extruded: bool = True,
        filled: bool = True,
        stroked: bool = False,
        wireframe: bool = False,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a column layer using deck.gl."""
        layer_id = name or f"column-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addColumnLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getFillColor=get_fill_color or [255, 140, 0, 200],
            getLineColor=get_line_color or [0, 0, 0, 255],
            getElevation=get_elevation,
            radius=radius,
            diskResolution=disk_resolution,
            elevationScale=elevation_scale,
            coverage=coverage,
            extruded=extruded,
            filled=filled,
            stroked=stroked,
            wireframe=wireframe,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "column"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_bitmap_layer(
        self,
        image: str,
        bounds: List[float],
        name: Optional[str] = None,
        opacity: float = 1.0,
        visible: bool = True,
        pickable: bool = False,
        desaturate: float = 0,
        transparent_color: Optional[List[int]] = None,
        tint_color: Optional[List[int]] = None,
        **kwargs,
    ) -> None:
        """Add a bitmap layer using deck.gl."""
        layer_id = name or f"bitmap-{len(self._layers)}"

        self.call_js_method(
            "addBitmapLayer",
            id=layer_id,
            image=image,
            bounds=bounds,
            opacity=opacity,
            visible=visible,
            pickable=pickable,
            desaturate=desaturate,
            transparentColor=transparent_color or [0, 0, 0, 0],
            tintColor=tint_color or [255, 255, 255],
            **kwargs,
        )

        self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "bitmap"}}
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_solid_polygon_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_polygon: Union[str, Any] = "polygon",
        get_fill_color: Optional[Union[List[int], str]] = None,
        get_line_color: Optional[Union[List[int], str]] = None,
        get_elevation: Union[float, str] = 0,
        filled: bool = True,
        extruded: bool = False,
        wireframe: bool = False,
        elevation_scale: float = 1,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a solid polygon layer using deck.gl."""
        layer_id = name or f"solidpolygon-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addSolidPolygonLayer",
            id=layer_id,
            data=processed_data,
            getPolygon=get_polygon,
            getFillColor=get_fill_color or [51, 136, 255, 128],
            getLineColor=get_line_color or [0, 0, 0, 255],
            getElevation=get_elevation,
            filled=filled,
            extruded=extruded,
            wireframe=wireframe,
            elevationScale=elevation_scale,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {"id": layer_id, "type": "solidpolygon"},
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

    def add_grid_cell_layer(
        self,
        data: Any,
        name: Optional[str] = None,
        get_position: Union[str, Any] = "coordinates",
        get_color: Optional[Union[List[int], str]] = None,
        get_elevation: Union[float, str] = 1000,
        cell_size: float = 200,
        coverage: float = 1,
        elevation_scale: float = 1,
        extruded: bool = True,
        pickable: bool = True,
        opacity: float = 0.8,
        **kwargs,
    ) -> None:
        """Add a grid cell layer using deck.gl."""
        layer_id = name or f"gridcell-{len(self._layers)}"
        processed_data = self._process_deck_data(data)

        self.call_js_method(
            "addGridCellLayer",
            id=layer_id,
            data=processed_data,
            getPosition=get_position,
            getColor=get_color or [255, 140, 0, 200],
            getElevation=get_elevation,
            cellSize=cell_size,
            coverage=coverage,
            elevationScale=elevation_scale,
            extruded=extruded,
            pickable=pickable,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {"id": layer_id, "type": "gridcell"},
        }
        self._add_to_layer_dict(layer_id, "Deck.gl")

    # -------------------------------------------------------------------------
    # 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,
        **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.
            **kwargs: Additional control options.

        Example:
            >>> from anymap_ts import MapboxMap
            >>> m = MapboxMap(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,
            **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 MapboxMap
            >>> m = MapboxMap(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",
            ... )
        """
        import base64

        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
            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)

    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

    # -------------------------------------------------------------------------
    # Terrain Methods (Mapbox-specific)
    # -------------------------------------------------------------------------

    def add_terrain(
        self, exaggeration: float = 1.0, source: str = "mapbox-dem"
    ) -> None:
        """Add 3D terrain to the map.

        Args:
            exaggeration: Terrain exaggeration factor.
            source: Terrain source ID.
        """
        self.call_js_method("addTerrain", source=source, exaggeration=exaggeration)

    def remove_terrain(self) -> None:
        """Remove 3D terrain from the map."""
        self.call_js_method("removeTerrain")

    def add_3d_terrain(
        self, exaggeration: float = 1.0, source: str = "mapbox-dem", **kwargs
    ) -> None:
        """Alias for add_terrain for MapLibre compatibility."""
        self.add_terrain(exaggeration=exaggeration, source=source)

    # -------------------------------------------------------------------------
    # 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: Mapbox 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)
        lt = layer_config.get("type", "")
        self._add_to_layer_dict(layer_id, "Raster" if lt == "raster" else "Vector")

    def remove_layer(self, layer_id: str) -> None:
        """Remove a layer from the map."""
        if layer_id in self._layers:
            layers = dict(self._layers)
            del layers[layer_id]
            self._layers = layers
        self._remove_from_layer_dict(layer_id)
        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."""
        self._validate_opacity(opacity)
        self.call_js_method("setOpacity", layer_id, opacity)

    def set_paint_property(self, layer_id: str, property_name: str, value: Any) -> None:
        """Set a paint property for a layer."""
        self.call_js_method("setPaintProperty", layer_id, property_name, value)

    def set_layout_property(
        self, layer_id: str, property_name: str, value: Any
    ) -> None:
        """Set a layout property for a layer."""
        self.call_js_method("setLayoutProperty", layer_id, property_name, value)

    def move_layer(self, layer_id: str, before_id: Optional[str] = None) -> None:
        """Move a layer in the layer stack."""
        self.call_js_method("moveLayer", layer_id, before_id)

    def get_layer(self, layer_id: str) -> Optional[Dict]:
        """Get layer configuration by ID."""
        return self._layers.get(layer_id)

    def get_layer_ids(self) -> List[str]:
        """Get list of all layer IDs."""
        return list(self._layers.keys())

    def add_popup(
        self,
        layer_id: str,
        properties: Optional[List[str]] = None,
        template: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add popup on click for a layer."""
        self.call_js_method(
            "addPopup",
            layerId=layer_id,
            properties=properties,
            template=template,
            **kwargs,
        )

    # -------------------------------------------------------------------------
    # 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."""
        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."""
        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,
                "collapsed": collapsed,
            },
        }

    def add_colorbar(
        self,
        colormap: str = "viridis",
        vmin: float = 0,
        vmax: float = 1,
        label: str = "",
        units: str = "",
        orientation: str = "horizontal",
        position: str = "bottom-right",
        bar_thickness: Optional[int] = None,
        bar_length: Optional[int] = None,
        ticks: Optional[Dict] = None,
        opacity: Optional[float] = None,
        colorbar_id: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add a continuous gradient colorbar to the map."""
        self._validate_position(position)

        cbar_id = (
            colorbar_id
            or f"colorbar-{len([k for k in self._controls.keys() if k.startswith('colorbar')])}"
        )

        js_kwargs: Dict[str, Any] = {
            "colormap": colormap,
            "vmin": vmin,
            "vmax": vmax,
            "label": label,
            "units": units,
            "orientation": orientation,
            "position": position,
            "colorbarId": cbar_id,
            **kwargs,
        }
        if bar_thickness is not None:
            js_kwargs["barThickness"] = bar_thickness
        if bar_length is not None:
            js_kwargs["barLength"] = bar_length
        if ticks is not None:
            js_kwargs["ticks"] = ticks
        if opacity is not None:
            js_kwargs["opacity"] = opacity

        self.call_js_method("addColorbar", **js_kwargs)

        self._controls = {
            **self._controls,
            cbar_id: {
                "type": "colorbar",
                "colormap": colormap,
                "vmin": vmin,
                "vmax": vmax,
                "label": label,
                "units": units,
                "orientation": orientation,
                "position": position,
            },
        }

    def remove_colorbar(self, colorbar_id: Optional[str] = None) -> None:
        """Remove a colorbar from the map."""
        if colorbar_id is None:
            cbar_keys = [k for k in self._controls.keys() if k.startswith("colorbar")]
            for key in cbar_keys:
                self.call_js_method("removeColorbar", colorbarId=key)
            self._controls = {
                k: v for k, v in self._controls.items() if not k.startswith("colorbar")
            }
        else:
            self.call_js_method("removeColorbar", colorbarId=colorbar_id)
            if colorbar_id in self._controls:
                controls = dict(self._controls)
                del controls[colorbar_id]
                self._controls = controls

    def update_colorbar(self, colorbar_id: Optional[str] = None, **kwargs) -> None:
        """Update an existing colorbar's properties."""
        if colorbar_id is None:
            cbar_keys = [k for k in self._controls.keys() if k.startswith("colorbar")]
            if not cbar_keys:
                raise ValueError("No colorbar found to update")
            colorbar_id = cbar_keys[0]

        if colorbar_id not in self._controls:
            raise ValueError(f"Colorbar '{colorbar_id}' not found")

        js_kwargs: Dict[str, Any] = {"colorbarId": colorbar_id}
        key_map = {"bar_thickness": "barThickness", "bar_length": "barLength"}
        for key, value in kwargs.items():
            js_key = key_map.get(key, key)
            js_kwargs[js_key] = value

        self.call_js_method("updateColorbar", **js_kwargs)

    def add_search_control(
        self,
        position: str = "top-left",
        placeholder: str = "Search places...",
        collapsed: bool = True,
        fly_to_zoom: int = 14,
        show_marker: bool = True,
        marker_color: str = "#4264fb",
        **kwargs,
    ) -> None:
        """Add a search/geocoder control."""
        self._validate_position(position)
        self.call_js_method(
            "addSearchControl",
            position=position,
            placeholder=placeholder,
            collapsed=collapsed,
            flyToZoom=fly_to_zoom,
            showMarker=show_marker,
            markerColor=marker_color,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "search-control": {
                "type": "search-control",
                "position": position,
                "collapsed": collapsed,
            },
        }

    def remove_search_control(self) -> None:
        """Remove the search/geocoder control."""
        self.call_js_method("removeSearchControl")
        if "search-control" in self._controls:
            controls = dict(self._controls)
            del controls["search-control"]
            self._controls = controls

    def add_measure_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        default_mode: str = "distance",
        distance_unit: str = "kilometers",
        area_unit: str = "square-kilometers",
        line_color: str = "#3b82f6",
        fill_color: str = "rgba(59, 130, 246, 0.2)",
        **kwargs,
    ) -> None:
        """Add a measurement control."""
        self._validate_position(position)
        self.call_js_method(
            "addMeasureControl",
            position=position,
            collapsed=collapsed,
            defaultMode=default_mode,
            distanceUnit=distance_unit,
            areaUnit=area_unit,
            lineColor=line_color,
            fillColor=fill_color,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "measure-control": {
                "type": "measure-control",
                "position": position,
                "collapsed": collapsed,
            },
        }

    def remove_measure_control(self) -> None:
        """Remove the measurement control."""
        self.call_js_method("removeMeasureControl")
        if "measure-control" in self._controls:
            controls = dict(self._controls)
            del controls["measure-control"]
            self._controls = controls

    def add_print_control(
        self,
        position: str = "top-right",
        collapsed: bool = True,
        format: str = "png",
        filename: str = "map-export",
        include_north_arrow: bool = False,
        include_scale_bar: bool = False,
        **kwargs,
    ) -> None:
        """Add a print/export control."""
        self._validate_position(position)
        self.call_js_method(
            "addPrintControl",
            position=position,
            collapsed=collapsed,
            format=format,
            filename=filename,
            includeNorthArrow=include_north_arrow,
            includeScaleBar=include_scale_bar,
            **kwargs,
        )
        self._controls = {
            **self._controls,
            "print-control": {
                "type": "print-control",
                "position": position,
                "collapsed": collapsed,
            },
        }

    def remove_print_control(self) -> None:
        """Remove the print/export control."""
        self.call_js_method("removePrintControl")
        if "print-control" in self._controls:
            controls = dict(self._controls)
            del controls["print-control"]
            self._controls = controls

    def add_coordinates_control(
        self,
        position: str = "bottom-left",
        precision: int = 4,
    ) -> None:
        """Add a coordinates display control."""
        self.call_js_method(
            "addCoordinatesControl",
            position=position,
            precision=precision,
        )

    def remove_coordinates_control(self) -> None:
        """Remove the coordinates display control."""
        self.call_js_method("removeCoordinatesControl")

    def add_time_slider(
        self,
        layer_id: str,
        property: str,
        min_value: float = 0,
        max_value: float = 100,
        step: float = 1,
        position: str = "bottom-left",
        label: str = "Time",
        auto_play: bool = False,
        interval: int = 500,
    ) -> None:
        """Add a time slider to filter data by a temporal property."""
        self.call_js_method(
            "addTimeSlider",
            layerId=layer_id,
            property=property,
            min=min_value,
            max=max_value,
            step=step,
            position=position,
            label=label,
            autoPlay=auto_play,
            interval=interval,
        )

    def remove_time_slider(self) -> None:
        """Remove the time slider control."""
        self.call_js_method("removeTimeSlider")

    def add_opacity_slider(
        self,
        layer_id: str,
        position: str = "top-right",
        label: Optional[str] = None,
    ) -> None:
        """Add a UI slider to control layer opacity."""
        self.call_js_method(
            "addOpacitySlider",
            layerId=layer_id,
            position=position,
            label=label or layer_id,
        )

    def remove_opacity_slider(self, layer_id: str) -> None:
        """Remove the opacity slider for a layer."""
        self.call_js_method("removeOpacitySlider", layerId=layer_id)

    def add_style_switcher(
        self,
        styles: Dict[str, str],
        position: str = "top-right",
    ) -> None:
        """Add a dropdown to switch between map styles."""
        self.call_js_method(
            "addStyleSwitcher",
            styles=styles,
            position=position,
        )

    def remove_style_switcher(self) -> None:
        """Remove the style switcher control."""
        self.call_js_method("removeStyleSwitcher")

    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."""
        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."""
        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."""
        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."""
        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."""
        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,
                "collapsible": collapsible,
            },
        }

    def add_legend(
        self,
        title: str,
        labels: List[str],
        colors: List[str],
        position: str = "bottom-right",
        opacity: float = 1.0,
        legend_id: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add a floating legend control to the map."""
        if len(labels) != len(colors):
            raise ValueError("Number of labels must match number of colors")

        self._validate_position(position)

        for i, color in enumerate(colors):
            if not isinstance(color, str) or not color.startswith("#"):
                raise ValueError(
                    f"Color at index {i} must be a hex color string (e.g., '#ff0000')"
                )

        legend_id = (
            legend_id
            or f"legend-{len([k for k in self._controls.keys() if k.startswith('legend')])}"
        )

        legend_items = [
            {"label": label, "color": color} for label, color in zip(labels, colors)
        ]

        self.call_js_method(
            "addLegend",
            id=legend_id,
            title=title,
            items=legend_items,
            position=position,
            opacity=opacity,
            **kwargs,
        )

        self._controls = {
            **self._controls,
            legend_id: {
                "type": "legend",
                "title": title,
                "labels": labels,
                "colors": colors,
                "position": position,
                "opacity": opacity,
            },
        }

    def remove_legend(self, legend_id: Optional[str] = None) -> None:
        """Remove a legend control from the map."""
        if legend_id is None:
            legend_keys = [k for k in self._controls.keys() if k.startswith("legend")]
            for key in legend_keys:
                self.call_js_method("removeLegend", key)
            self._controls = {
                k: v for k, v in self._controls.items() if not k.startswith("legend")
            }
        else:
            self.call_js_method("removeLegend", legend_id)
            if legend_id in self._controls:
                controls = dict(self._controls)
                del controls[legend_id]
                self._controls = controls

    def update_legend(
        self,
        legend_id: str,
        title: Optional[str] = None,
        labels: Optional[List[str]] = None,
        colors: Optional[List[str]] = None,
        opacity: Optional[float] = None,
        **kwargs,
    ) -> None:
        """Update an existing legend's properties."""
        if legend_id not in self._controls:
            raise ValueError(f"Legend '{legend_id}' not found")

        update_params = {"id": legend_id}

        if title is not None:
            update_params["title"] = title
            self._controls[legend_id]["title"] = title

        if labels is not None and colors is not None:
            if len(labels) != len(colors):
                raise ValueError("Number of labels must match number of colors")

            legend_items = [
                {"label": label, "color": color} for label, color in zip(labels, colors)
            ]
            update_params["items"] = legend_items
            self._controls[legend_id]["labels"] = labels
            self._controls[legend_id]["colors"] = colors

        elif labels is not None or colors is not None:
            raise ValueError("Both labels and colors must be provided together")

        if opacity is not None:
            update_params["opacity"] = opacity
            self._controls[legend_id]["opacity"] = opacity

        update_params.update(kwargs)
        self.call_js_method("updateLegend", **update_params)

    def add_tooltip(
        self,
        layer_id: str,
        template: Optional[str] = None,
        properties: Optional[List[str]] = None,
    ) -> None:
        """Add a tooltip that shows on feature hover."""
        self.call_js_method(
            "addTooltip",
            layerId=layer_id,
            template=template or "",
            properties=properties,
        )

    def remove_tooltip(self, layer_id: str) -> None:
        """Remove tooltip from a layer."""
        self.call_js_method("removeTooltip", layerId=layer_id)

    def add_flatgeobuf(
        self,
        url: str,
        name: Optional[str] = None,
        layer_type: Optional[str] = None,
        paint: Optional[Dict] = None,
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add a FlatGeobuf layer from a URL."""
        layer_id = name or f"flatgeobuf-{len(self._layers)}"

        self.call_js_method(
            "addFlatGeobuf",
            url=url,
            name=layer_id,
            layerType=layer_type,
            paint=paint,
            fitBounds=fit_bounds,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "flatgeobuf",
                "url": url,
            },
        }
        self._add_to_layer_dict(layer_id, "Vector")

    def remove_flatgeobuf(self, name: str) -> None:
        """Remove a FlatGeobuf layer from the map."""
        if name in self._layers:
            layers = dict(self._layers)
            del layers[name]
            self._layers = layers
        self._remove_from_layer_dict(name)
        self.call_js_method("removeFlatGeobuf", name=name)

    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."""
        if draw_modes is None:
            draw_modes = ["polygon", "line", "rectangle", "circle", "marker"]
        if edit_modes is None:
            edit_modes = [
                "select",
                "drag",
                "change",
                "rotate",
                "cut",
                "delete",
                "scale",
                "copy",
                "split",
                "union",
                "difference",
                "simplify",
                "lasso",
            ]

        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."""
        self.call_js_method("getDrawData")
        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."""
        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."""
        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)

        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)

    def add_cluster_layer(
        self,
        data: Any,
        cluster_radius: int = 50,
        cluster_max_zoom: int = 14,
        cluster_colors: Optional[List[str]] = None,
        cluster_steps: Optional[List[int]] = None,
        cluster_min_radius: int = 15,
        cluster_max_radius: int = 30,
        unclustered_color: str = "#11b4da",
        unclustered_radius: int = 8,
        show_cluster_count: bool = True,
        name: Optional[str] = None,
        zoom_on_click: bool = True,
        fit_bounds: bool = True,
        **kwargs,
    ) -> str:
        """Add a clustered point layer."""
        layer_id = name or f"cluster-{len(self._layers)}"

        if cluster_colors is None:
            cluster_colors = ["#51bbd6", "#f1f075", "#f28cb1"]
        if cluster_steps is None:
            cluster_steps = [100, 750]

        if len(cluster_steps) != len(cluster_colors) - 1:
            raise ValueError(
                f"cluster_steps must have {len(cluster_colors) - 1} values "
                f"(one less than cluster_colors), got {len(cluster_steps)}"
            )

        geojson = to_geojson(data)

        if geojson.get("type") == "url":
            url = geojson["url"]
            geojson = fetch_geojson(url)

        bounds = get_bounds(geojson) if fit_bounds else None

        self.call_js_method(
            "addClusterLayer",
            data=geojson,
            name=layer_id,
            clusterRadius=cluster_radius,
            clusterMaxZoom=cluster_max_zoom,
            clusterColors=cluster_colors,
            clusterSteps=cluster_steps,
            clusterMinRadius=cluster_min_radius,
            clusterMaxRadius=cluster_max_radius,
            unclusteredColor=unclustered_color,
            unclusteredRadius=unclustered_radius,
            showClusterCount=show_cluster_count,
            zoomOnClick=zoom_on_click,
            fitBounds=fit_bounds,
            bounds=bounds,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "cluster",
                "source": f"{layer_id}-source",
            },
        }
        self._add_to_layer_dict(layer_id, "Vector")
        return layer_id

    def remove_cluster_layer(self, layer_id: str) -> None:
        """Remove a cluster layer."""
        self._remove_layer_internal(layer_id, "removeClusterLayer")

    def add_choropleth(
        self,
        data: Any,
        column: str,
        cmap: str = "viridis",
        classification: str = "quantile",
        k: int = 5,
        breaks: Optional[List[float]] = None,
        fill_opacity: float = 0.7,
        line_color: str = "#000000",
        line_width: float = 1,
        legend: bool = True,
        legend_title: Optional[str] = None,
        hover: bool = True,
        layer_id: Optional[str] = None,
        fit_bounds: bool = True,
        **kwargs,
    ) -> None:
        """Add a choropleth (thematic) map layer."""
        from .utils import (
            get_choropleth_colors,
            compute_breaks,
            build_step_expression,
        )

        layer_name = layer_id or f"choropleth-{len(self._layers)}"

        geojson = to_geojson(data)

        if geojson.get("type") == "url":
            url = geojson["url"]
            geojson = fetch_geojson(url)

        features = geojson.get("features", [])
        values = []
        for feature in features:
            props = feature.get("properties", {})
            val = props.get(column)
            if val is not None:
                try:
                    values.append(float(val))
                except (TypeError, ValueError):
                    pass

        if not values:
            raise ValueError(f"No valid numeric values found for column '{column}'")

        computed_breaks = compute_breaks(values, classification, k, breaks)

        colors = get_choropleth_colors(cmap, k)

        step_expr = build_step_expression(column, computed_breaks, colors)

        bounds = get_bounds(geojson) if fit_bounds else None

        self.call_js_method(
            "addChoropleth",
            data=geojson,
            name=layer_name,
            column=column,
            stepExpression=step_expr,
            fillOpacity=fill_opacity,
            lineColor=line_color,
            lineWidth=line_width,
            hover=hover,
            fitBounds=fit_bounds,
            bounds=bounds,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_name: {
                "id": layer_name,
                "type": "choropleth",
                "source": f"{layer_name}-source",
                "column": column,
            },
        }
        self._add_to_layer_dict(layer_name, "Vector")

        if legend:
            title = legend_title or column
            labels = []
            for i in range(len(computed_breaks) - 1):
                low = computed_breaks[i]
                high = computed_breaks[i + 1]
                labels.append(f"{low:.1f} - {high:.1f}")

            self.add_legend(
                title=title,
                labels=labels,
                colors=colors,
                position="bottom-right",
            )

    def add_3d_buildings(
        self,
        source: str = "openmaptiles",
        min_zoom: float = 14,
        fill_extrusion_color: str = "#aaa",
        fill_extrusion_opacity: float = 0.6,
        height_property: str = "render_height",
        base_property: str = "render_min_height",
        layer_id: Optional[str] = None,
        **kwargs,
    ) -> None:
        """Add 3D building extrusions from vector tiles."""
        layer_name = layer_id or "3d-buildings"

        self.call_js_method(
            "add3DBuildings",
            source=source,
            minZoom=min_zoom,
            fillExtrusionColor=fill_extrusion_color,
            fillExtrusionOpacity=fill_extrusion_opacity,
            heightProperty=height_property,
            baseProperty=base_property,
            layerId=layer_name,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_name: {
                "id": layer_name,
                "type": "fill-extrusion",
            },
        }
        self._add_to_layer_dict(layer_name, "Vector")

    def animate_along_route(
        self,
        route: Any,
        duration: int = 10000,
        loop: bool = True,
        marker_color: str = "#3388ff",
        marker_size: float = 1.0,
        show_trail: bool = False,
        trail_color: str = "#3388ff",
        trail_width: float = 3,
        animation_id: Optional[str] = None,
        **kwargs,
    ) -> str:
        """Animate a marker along a route."""
        anim_id = animation_id or f"animation-{len(self._layers)}"

        if isinstance(route, list) and len(route) > 0:
            if isinstance(route[0], (list, tuple)):
                coordinates = route
            else:
                raise ValueError("Route list must contain coordinate pairs")
        elif isinstance(route, dict):
            if route.get("type") == "LineString":
                coordinates = route.get("coordinates", [])
            elif route.get("type") == "Feature":
                geometry = route.get("geometry", {})
                if geometry.get("type") == "LineString":
                    coordinates = geometry.get("coordinates", [])
                else:
                    raise ValueError("Feature geometry must be LineString")
            elif route.get("type") == "FeatureCollection":
                features = route.get("features", [])
                if (
                    features
                    and features[0].get("geometry", {}).get("type") == "LineString"
                ):
                    coordinates = features[0]["geometry"]["coordinates"]
                else:
                    raise ValueError(
                        "FeatureCollection must contain LineString features"
                    )
            else:
                raise ValueError(
                    "GeoJSON must be LineString, Feature, or FeatureCollection"
                )
        else:
            geojson = to_geojson(route)
            if geojson.get("type") == "url":
                geojson = fetch_geojson(geojson["url"])
            if geojson.get("type") == "FeatureCollection":
                features = geojson.get("features", [])
                if features:
                    coordinates = features[0].get("geometry", {}).get("coordinates", [])
                else:
                    raise ValueError("No features found in data")
            elif geojson.get("type") == "Feature":
                coordinates = geojson.get("geometry", {}).get("coordinates", [])
            else:
                coordinates = geojson.get("coordinates", [])

        if len(coordinates) < 2:
            raise ValueError("Route must have at least 2 points")

        self.call_js_method(
            "animateAlongRoute",
            id=anim_id,
            coordinates=coordinates,
            duration=duration,
            loop=loop,
            markerColor=marker_color,
            markerSize=marker_size,
            showTrail=show_trail,
            trailColor=trail_color,
            trailWidth=trail_width,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            anim_id: {
                "id": anim_id,
                "type": "animation",
            },
        }
        return anim_id

    def stop_animation(self, animation_id: str) -> None:
        """Stop a running animation."""
        self.call_js_method("stopAnimation", animation_id)
        if animation_id in self._layers:
            layers = dict(self._layers)
            del layers[animation_id]
            self._layers = layers

    def pause_animation(self, animation_id: str) -> None:
        """Pause a running animation."""
        self.call_js_method("pauseAnimation", animation_id)

    def resume_animation(self, animation_id: str) -> None:
        """Resume a paused animation."""
        self.call_js_method("resumeAnimation", animation_id)

    def set_animation_speed(self, animation_id: str, speed: float) -> None:
        """Set animation speed multiplier."""
        self.call_js_method("setAnimationSpeed", animation_id, speed)

    def add_hover_effect(
        self,
        layer_id: str,
        highlight_color: Optional[str] = None,
        highlight_opacity: Optional[float] = None,
        highlight_outline_width: float = 2,
        **kwargs,
    ) -> None:
        """Add hover highlight effect to an existing layer."""
        self.call_js_method(
            "addHoverEffect",
            layerId=layer_id,
            highlightColor=highlight_color,
            highlightOpacity=highlight_opacity,
            highlightOutlineWidth=highlight_outline_width,
            **kwargs,
        )

    def set_fog(
        self,
        color: Optional[str] = None,
        high_color: Optional[str] = None,
        low_color: Optional[str] = None,
        horizon_blend: Optional[float] = None,
        range: Optional[List[float]] = None,
        **kwargs,
    ) -> None:
        """Set fog atmospheric effect (Mapbox uses map.setFog() API)."""
        self.call_js_method(
            "setFog",
            color=color,
            highColor=high_color,
            lowColor=low_color,
            horizonBlend=horizon_blend,
            range=range,
            **kwargs,
        )

    def remove_fog(self) -> None:
        """Remove fog atmospheric effects from the map."""
        self.call_js_method("removeFog")

    def add_image_layer(
        self,
        url: str,
        coordinates: List[List[float]],
        name: Optional[str] = None,
        opacity: float = 1.0,
        **kwargs,
    ) -> None:
        """Add a georeferenced image overlay."""
        self._validate_opacity(opacity)
        layer_id = name or f"image-{len(self._layers)}"

        if len(coordinates) != 4:
            raise ValueError(
                "coordinates must have exactly 4 corner points "
                "[top-left, top-right, bottom-right, bottom-left]"
            )

        self.call_js_method(
            "addImageLayer",
            id=layer_id,
            url=url,
            coordinates=coordinates,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "image",
                "url": url,
                "coordinates": coordinates,
            },
        }
        self._add_to_layer_dict(layer_id, "Raster")

    def add_video_layer(
        self,
        urls: List[str],
        coordinates: List[List[float]],
        name: Optional[str] = None,
        opacity: float = 1.0,
        **kwargs,
    ) -> None:
        """Add a georeferenced video overlay on the map."""
        self._validate_opacity(opacity)
        layer_id = name or f"video-{len(self._layers)}"

        if len(coordinates) != 4:
            raise ValueError(
                "coordinates must have exactly 4 corner points "
                "[top-left, top-right, bottom-right, bottom-left]"
            )

        self.call_js_method(
            "addVideoLayer",
            id=layer_id,
            urls=urls,
            coordinates=coordinates,
            opacity=opacity,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "video",
                "source": f"{layer_id}-source",
            },
        }
        self._add_to_layer_dict(layer_id, "Raster")

    def remove_video_layer(self, name: str) -> None:
        """Remove a video layer from the map."""
        if name in self._layers:
            layers = dict(self._layers)
            del layers[name]
            self._layers = layers
        self._remove_from_layer_dict(name)
        self.call_js_method("removeVideoLayer", id=name)

    def play_video(self, name: str) -> None:
        """Start playing a video layer."""
        self.call_js_method("playVideo", id=name)

    def pause_video(self, name: str) -> None:
        """Pause a video layer."""
        self.call_js_method("pauseVideo", id=name)

    def seek_video(self, name: str, time: float) -> None:
        """Seek to a specific time in a video layer."""
        self.call_js_method("seekVideo", id=name, time=time)

    def add_split_map(
        self,
        left_layer: str,
        right_layer: str,
        position: int = 50,
    ) -> None:
        """Add a split map comparison view with a draggable divider."""
        if not 0 <= position <= 100:
            raise ValueError(f"position must be between 0 and 100, got {position}")

        self.call_js_method(
            "addSplitMap",
            leftLayer=left_layer,
            rightLayer=right_layer,
            position=position,
        )

    def remove_split_map(self) -> None:
        """Remove the split map comparison view."""
        self.call_js_method("removeSplitMap")

    def set_projection(self, projection: str = "mercator") -> None:
        """Set the map projection (Mapbox supports 'globe' and 'mercator')."""
        self.call_js_method("setProjection", projection=projection)

    def update_geojson_source(self, source_id: str, data: Any) -> None:
        """Update the data of an existing GeoJSON source in place."""
        processed_data = self._process_deck_data(data)
        self.call_js_method(
            "updateGeoJSONSource",
            sourceId=source_id,
            data=processed_data,
        )

    def add_image(self, name: str, url: str) -> None:
        """Load a custom icon image for use in symbol layers."""
        self.call_js_method("addMapImage", name=name, url=url)

    def add_swipe_map(self, left_layer: str, right_layer: str) -> None:
        """Add a drag-to-compare swipe control for two layers."""
        self.call_js_method(
            "addSwipeMap",
            leftLayer=left_layer,
            rightLayer=right_layer,
        )

    def remove_swipe_map(self) -> None:
        """Remove the swipe map comparison control."""
        self.call_js_method("removeSwipeMap")

    def set_filter(
        self,
        layer_id: str,
        filter_expression: Optional[List] = None,
    ) -> None:
        """Set or clear a filter on a map layer."""
        self.call_js_method(
            "setFilter",
            layerId=layer_id,
            filter=filter_expression,
        )

    def query_rendered_features(
        self,
        geometry: Optional[Any] = None,
        layers: Optional[List[str]] = None,
        filter_expression: Optional[List] = None,
    ) -> Dict:
        """Query features currently rendered on the map."""
        kwargs: Dict[str, Any] = {}
        if geometry is not None:
            kwargs["geometry"] = geometry
        if layers is not None:
            kwargs["layers"] = layers
        if filter_expression is not None:
            kwargs["filter"] = filter_expression

        self.call_js_method("queryRenderedFeatures", **kwargs)
        return self._queried_features

    def query_source_features(
        self,
        source_id: str,
        source_layer: Optional[str] = None,
        filter_expression: Optional[List] = None,
    ) -> Dict:
        """Query features from a source."""
        kwargs: Dict[str, Any] = {"sourceId": source_id}
        if source_layer is not None:
            kwargs["sourceLayer"] = source_layer
        if filter_expression is not None:
            kwargs["filter"] = filter_expression

        self.call_js_method("querySourceFeatures", **kwargs)
        return self._queried_features

    @property
    def queried_features(self) -> Dict:
        """Get the most recent query results."""
        return self._queried_features

    def get_visible_features(
        self,
        layers: Optional[List[str]] = None,
    ) -> Optional[Dict]:
        """Get all features currently visible in the viewport."""
        if layers is not None:
            self.call_js_method("getVisibleFeatures", layers=layers)
        features = self._queried_features
        if features and "data" in features:
            return features["data"]
        return None

    def to_geojson(self, layer_id: Optional[str] = None) -> Optional[Dict]:
        """Get layer data as GeoJSON."""
        if layer_id:
            self.call_js_method("getLayerData", sourceId=layer_id)
        features = self._queried_features
        if features and "data" in features:
            return features["data"]
        return None

    def to_geopandas(self, layer_id: Optional[str] = None) -> Any:
        """Get layer data as a GeoDataFrame."""
        geojson = self.to_geojson(layer_id)
        if geojson is None:
            return None
        try:
            import geopandas as gpd

            return gpd.GeoDataFrame.from_features(geojson.get("features", []))
        except ImportError:
            raise ImportError("geopandas is required for to_geopandas()")

    # -------------------------------------------------------------------------
    # Markers
    # -------------------------------------------------------------------------

    def add_marker(
        self,
        lng: float,
        lat: float,
        popup: Optional[str] = None,
        tooltip: Optional[str] = None,
        color: str = "#3388ff",
        draggable: bool = False,
        scale: float = 1.0,
        popup_max_width: str = "240px",
        tooltip_max_width: str = "240px",
        name: Optional[str] = None,
        **kwargs,
    ) -> str:
        """Add a single marker to the map."""
        marker_id = name or f"marker-{len(self._layers)}"

        self.call_js_method(
            "addMarker",
            lng,
            lat,
            id=marker_id,
            popup=popup,
            tooltip=tooltip,
            color=color,
            draggable=draggable,
            scale=scale,
            popupMaxWidth=popup_max_width,
            tooltipMaxWidth=tooltip_max_width,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            marker_id: {
                "id": marker_id,
                "type": "marker",
                "lngLat": [lng, lat],
            },
        }
        self._add_to_layer_dict(marker_id, "Markers")
        return marker_id

    def add_markers(
        self,
        data: Any,
        lng_column: Optional[str] = None,
        lat_column: Optional[str] = None,
        popup_column: Optional[str] = None,
        tooltip_column: Optional[str] = None,
        color: str = "#3388ff",
        scale: float = 1.0,
        popup_max_width: str = "240px",
        tooltip_max_width: str = "240px",
        draggable: bool = False,
        name: Optional[str] = None,
        **kwargs,
    ) -> str:
        """Add multiple markers from data."""
        layer_id = name or f"markers-{len(self._layers)}"
        markers = []

        if hasattr(data, "geometry"):
            for _, row in data.iterrows():
                geom = row.geometry
                if geom.geom_type == "Point":
                    marker = {"lngLat": [geom.x, geom.y]}
                    if popup_column and popup_column in row:
                        marker["popup"] = str(row[popup_column])
                    if tooltip_column and tooltip_column in row:
                        marker["tooltip"] = str(row[tooltip_column])
                    markers.append(marker)
        elif isinstance(data, dict) and data.get("type") == "FeatureCollection":
            for feature in data.get("features", []):
                geom = feature.get("geometry", {})
                if geom.get("type") == "Point":
                    coords = geom.get("coordinates", [])
                    marker = {"lngLat": coords[:2]}
                    props = feature.get("properties", {})
                    if popup_column and popup_column in props:
                        marker["popup"] = str(props[popup_column])
                    if tooltip_column and tooltip_column in props:
                        marker["tooltip"] = str(props[tooltip_column])
                    markers.append(marker)
        elif isinstance(data, list):
            lng_keys = ["lng", "lon", "longitude", "x"]
            lat_keys = ["lat", "latitude", "y"]

            for item in data:
                if not isinstance(item, dict):
                    continue

                lng_val = None
                lat_val = None

                if lng_column and lng_column in item:
                    lng_val = item[lng_column]
                else:
                    for key in lng_keys:
                        if key in item:
                            lng_val = item[key]
                            break

                if lat_column and lat_column in item:
                    lat_val = item[lat_column]
                else:
                    for key in lat_keys:
                        if key in item:
                            lat_val = item[key]
                            break

                if lng_val is not None and lat_val is not None:
                    marker = {"lngLat": [float(lng_val), float(lat_val)]}
                    if popup_column and popup_column in item:
                        marker["popup"] = str(item[popup_column])
                    if tooltip_column and tooltip_column in item:
                        marker["tooltip"] = str(item[tooltip_column])
                    markers.append(marker)

        if not markers:
            raise ValueError("No valid point data found in input")

        self.call_js_method(
            "addMarkers",
            id=layer_id,
            markers=markers,
            color=color,
            scale=scale,
            popupMaxWidth=popup_max_width,
            tooltipMaxWidth=tooltip_max_width,
            draggable=draggable,
            **kwargs,
        )

        self._layers = {
            **self._layers,
            layer_id: {
                "id": layer_id,
                "type": "markers",
                "count": len(markers),
            },
        }
        self._add_to_layer_dict(layer_id, "Markers")
        return layer_id

    def remove_marker(self, marker_id: str) -> None:
        """Remove a marker from the map."""
        self._remove_layer_internal(marker_id, "removeMarker")

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

    def _generate_html_template(self) -> str:
        """Generate standalone HTML for the map."""
        template_path = Path(__file__).parent / "templates" / "mapbox.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,
            "access_token": self.access_token,
        }

        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://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-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}};

        mapboxgl.accessToken = state.access_token;

        const map = new mapboxgl.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 mapboxgl.NavigationControl();
                            break;
                        case 'scale':
                            control = new mapboxgl.ScaleControl();
                            break;
                        case 'fullscreen':
                            control = new mapboxgl.FullscreenControl();
                            break;
                    }
                    if (control) {
                        map.addControl(control, position);
                    }
                    break;

                case 'addTerrain':
                    const terrainSource = kwargs.source || 'mapbox-dem';
                    if (!map.getSource(terrainSource)) {
                        map.addSource(terrainSource, {
                            type: 'raster-dem',
                            url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
                            tileSize: 512,
                            maxzoom: 14
                        });
                    }
                    map.setTerrain({ source: terrainSource, exaggeration: kwargs.exaggeration || 1 });
                    break;

                case 'removeTerrain':
                    map.setTerrain(null);
                    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;

                case 'addMarker':
                    new mapboxgl.Marker({ color: kwargs.color || '#3388ff' })
                        .setLngLat([args[0], args[1]])
                        .addTo(map);
                    break;

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

draw_data: Dict property readonly

Property to access current draw data.

pmtiles_styles: Dict[str, List[Dict[str, Any]]] property readonly

Get auto-discovered PMTiles layer styles.

Returns a dict keyed by layer_id, where each value is a list of sub-layer dicts containing: id, sourceLayer, geometryType, color, type, and paint.

Returns:

Type Description
Dict[str, List[Dict[str, Any]]]

Dict mapping layer IDs to lists of sub-layer style dicts.

queried_features: Dict property readonly

Get the most recent query results.

__init__(self, center=(0.0, 0.0), zoom=2.0, width='100%', height='600px', style='mapbox://styles/mapbox/streets-v12', bearing=0.0, pitch=0.0, max_pitch=85.0, access_token=None, controls=None, **kwargs) special

Initialize a Mapbox 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.

'600px'
style str

Mapbox style URL (e.g., "mapbox://styles/mapbox/streets-v12").

'mapbox://styles/mapbox/streets-v12'
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
access_token Optional[str]

Mapbox access token. If None, reads from MAPBOX_TOKEN env var.

None
controls Optional[Dict[str, Any]]

Dict of controls to add (e.g., {"navigation": True}).

None
**kwargs

Additional widget arguments.

{}
Source code in anymap_ts/mapbox.py
def __init__(
    self,
    center: Tuple[float, float] = (0.0, 0.0),
    zoom: float = 2.0,
    width: str = "100%",
    height: str = "600px",
    style: str = "mapbox://styles/mapbox/streets-v12",
    bearing: float = 0.0,
    pitch: float = 0.0,
    max_pitch: float = 85.0,
    access_token: Optional[str] = None,
    controls: Optional[Dict[str, Any]] = None,
    **kwargs,
):
    """Initialize a Mapbox map.

    Args:
        center: Map center as (longitude, latitude).
        zoom: Initial zoom level.
        width: Map width as CSS string.
        height: Map height as CSS string.
        style: Mapbox style URL (e.g., "mapbox://styles/mapbox/streets-v12").
        bearing: Map bearing in degrees.
        pitch: Map pitch in degrees.
        max_pitch: Maximum pitch angle in degrees (default: 85).
        access_token: Mapbox access token. If None, reads from MAPBOX_TOKEN env var.
        controls: Dict of controls to add (e.g., {"navigation": True}).
        **kwargs: Additional widget arguments.
    """
    # Get access token
    token = access_token or get_mapbox_token()
    if not token:
        print(
            "Warning: No Mapbox access token provided. "
            "Set MAPBOX_TOKEN environment variable or pass access_token parameter."
        )

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

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

    # Storage for auto-discovered PMTiles layer styles
    self._pmtiles_styles: Dict[str, List[Dict[str, Any]]] = {}

    # Add default controls
    if controls is None:
        controls = {"navigation": True, "fullscreen": True}

    for control_name, config in controls.items():
        if config:
            self.add_control(
                control_name, **(config if isinstance(config, dict) else {})
            )

add_3d_buildings(self, source='openmaptiles', min_zoom=14, fill_extrusion_color='#aaa', fill_extrusion_opacity=0.6, height_property='render_height', base_property='render_min_height', layer_id=None, **kwargs)

Add 3D building extrusions from vector tiles.

Source code in anymap_ts/mapbox.py
def add_3d_buildings(
    self,
    source: str = "openmaptiles",
    min_zoom: float = 14,
    fill_extrusion_color: str = "#aaa",
    fill_extrusion_opacity: float = 0.6,
    height_property: str = "render_height",
    base_property: str = "render_min_height",
    layer_id: Optional[str] = None,
    **kwargs,
) -> None:
    """Add 3D building extrusions from vector tiles."""
    layer_name = layer_id or "3d-buildings"

    self.call_js_method(
        "add3DBuildings",
        source=source,
        minZoom=min_zoom,
        fillExtrusionColor=fill_extrusion_color,
        fillExtrusionOpacity=fill_extrusion_opacity,
        heightProperty=height_property,
        baseProperty=base_property,
        layerId=layer_name,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_name: {
            "id": layer_name,
            "type": "fill-extrusion",
        },
    }
    self._add_to_layer_dict(layer_name, "Vector")

add_3d_terrain(self, exaggeration=1.0, source='mapbox-dem', **kwargs)

Alias for add_terrain for MapLibre compatibility.

Source code in anymap_ts/mapbox.py
def add_3d_terrain(
    self, exaggeration: float = 1.0, source: str = "mapbox-dem", **kwargs
) -> None:
    """Alias for add_terrain for MapLibre compatibility."""
    self.add_terrain(exaggeration=exaggeration, source=source)

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 MapboxMap
>>> m = MapboxMap()
>>> 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/mapbox.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 MapboxMap
        >>> m = MapboxMap()
        >>> 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",
        },
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_basemap(self, basemap='mapbox://styles/mapbox/streets-v12', attribution=None, **kwargs)

Add a basemap layer.

For Mapbox styles, use the style URL format: - "mapbox://styles/mapbox/streets-v12" - "mapbox://styles/mapbox/satellite-v9" - "mapbox://styles/mapbox/satellite-streets-v12" - "mapbox://styles/mapbox/light-v11" - "mapbox://styles/mapbox/dark-v11" - "mapbox://styles/mapbox/outdoors-v12"

Or use XYZ tile URLs for custom basemaps.

Parameters:

Name Type Description Default
basemap str

Mapbox style URL or XYZ tile URL.

'mapbox://styles/mapbox/streets-v12'
attribution Optional[str]

Custom attribution text.

None
**kwargs

Additional options.

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

    For Mapbox styles, use the style URL format:
    - "mapbox://styles/mapbox/streets-v12"
    - "mapbox://styles/mapbox/satellite-v9"
    - "mapbox://styles/mapbox/satellite-streets-v12"
    - "mapbox://styles/mapbox/light-v11"
    - "mapbox://styles/mapbox/dark-v11"
    - "mapbox://styles/mapbox/outdoors-v12"

    Or use XYZ tile URLs for custom basemaps.

    Args:
        basemap: Mapbox style URL or XYZ tile URL.
        attribution: Custom attribution text.
        **kwargs: Additional options.
    """
    # If it's a Mapbox style URL, set it as the map style
    if basemap.startswith("mapbox://"):
        self.style = basemap
        return

    # Otherwise, treat as XYZ tile URL
    try:
        url, default_attribution = get_basemap_url(basemap)
    except (ValueError, KeyError):
        url = basemap
        default_attribution = ""

    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_bitmap_layer(self, image, bounds, name=None, opacity=1.0, visible=True, pickable=False, desaturate=0, transparent_color=None, tint_color=None, **kwargs)

Add a bitmap layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_bitmap_layer(
    self,
    image: str,
    bounds: List[float],
    name: Optional[str] = None,
    opacity: float = 1.0,
    visible: bool = True,
    pickable: bool = False,
    desaturate: float = 0,
    transparent_color: Optional[List[int]] = None,
    tint_color: Optional[List[int]] = None,
    **kwargs,
) -> None:
    """Add a bitmap layer using deck.gl."""
    layer_id = name or f"bitmap-{len(self._layers)}"

    self.call_js_method(
        "addBitmapLayer",
        id=layer_id,
        image=image,
        bounds=bounds,
        opacity=opacity,
        visible=visible,
        pickable=pickable,
        desaturate=desaturate,
        transparentColor=transparent_color or [0, 0, 0, 0],
        tintColor=tint_color or [255, 255, 255],
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "bitmap"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_choropleth(self, data, column, cmap='viridis', classification='quantile', k=5, breaks=None, fill_opacity=0.7, line_color='#000000', line_width=1, legend=True, legend_title=None, hover=True, layer_id=None, fit_bounds=True, **kwargs)

Add a choropleth (thematic) map layer.

Source code in anymap_ts/mapbox.py
def add_choropleth(
    self,
    data: Any,
    column: str,
    cmap: str = "viridis",
    classification: str = "quantile",
    k: int = 5,
    breaks: Optional[List[float]] = None,
    fill_opacity: float = 0.7,
    line_color: str = "#000000",
    line_width: float = 1,
    legend: bool = True,
    legend_title: Optional[str] = None,
    hover: bool = True,
    layer_id: Optional[str] = None,
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add a choropleth (thematic) map layer."""
    from .utils import (
        get_choropleth_colors,
        compute_breaks,
        build_step_expression,
    )

    layer_name = layer_id or f"choropleth-{len(self._layers)}"

    geojson = to_geojson(data)

    if geojson.get("type") == "url":
        url = geojson["url"]
        geojson = fetch_geojson(url)

    features = geojson.get("features", [])
    values = []
    for feature in features:
        props = feature.get("properties", {})
        val = props.get(column)
        if val is not None:
            try:
                values.append(float(val))
            except (TypeError, ValueError):
                pass

    if not values:
        raise ValueError(f"No valid numeric values found for column '{column}'")

    computed_breaks = compute_breaks(values, classification, k, breaks)

    colors = get_choropleth_colors(cmap, k)

    step_expr = build_step_expression(column, computed_breaks, colors)

    bounds = get_bounds(geojson) if fit_bounds else None

    self.call_js_method(
        "addChoropleth",
        data=geojson,
        name=layer_name,
        column=column,
        stepExpression=step_expr,
        fillOpacity=fill_opacity,
        lineColor=line_color,
        lineWidth=line_width,
        hover=hover,
        fitBounds=fit_bounds,
        bounds=bounds,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_name: {
            "id": layer_name,
            "type": "choropleth",
            "source": f"{layer_name}-source",
            "column": column,
        },
    }
    self._add_to_layer_dict(layer_name, "Vector")

    if legend:
        title = legend_title or column
        labels = []
        for i in range(len(computed_breaks) - 1):
            low = computed_breaks[i]
            high = computed_breaks[i + 1]
            labels.append(f"{low:.1f} - {high:.1f}")

        self.add_legend(
            title=title,
            labels=labels,
            colors=colors,
            position="bottom-right",
        )

add_cluster_layer(self, data, cluster_radius=50, cluster_max_zoom=14, cluster_colors=None, cluster_steps=None, cluster_min_radius=15, cluster_max_radius=30, unclustered_color='#11b4da', unclustered_radius=8, show_cluster_count=True, name=None, zoom_on_click=True, fit_bounds=True, **kwargs)

Add a clustered point layer.

Source code in anymap_ts/mapbox.py
def add_cluster_layer(
    self,
    data: Any,
    cluster_radius: int = 50,
    cluster_max_zoom: int = 14,
    cluster_colors: Optional[List[str]] = None,
    cluster_steps: Optional[List[int]] = None,
    cluster_min_radius: int = 15,
    cluster_max_radius: int = 30,
    unclustered_color: str = "#11b4da",
    unclustered_radius: int = 8,
    show_cluster_count: bool = True,
    name: Optional[str] = None,
    zoom_on_click: bool = True,
    fit_bounds: bool = True,
    **kwargs,
) -> str:
    """Add a clustered point layer."""
    layer_id = name or f"cluster-{len(self._layers)}"

    if cluster_colors is None:
        cluster_colors = ["#51bbd6", "#f1f075", "#f28cb1"]
    if cluster_steps is None:
        cluster_steps = [100, 750]

    if len(cluster_steps) != len(cluster_colors) - 1:
        raise ValueError(
            f"cluster_steps must have {len(cluster_colors) - 1} values "
            f"(one less than cluster_colors), got {len(cluster_steps)}"
        )

    geojson = to_geojson(data)

    if geojson.get("type") == "url":
        url = geojson["url"]
        geojson = fetch_geojson(url)

    bounds = get_bounds(geojson) if fit_bounds else None

    self.call_js_method(
        "addClusterLayer",
        data=geojson,
        name=layer_id,
        clusterRadius=cluster_radius,
        clusterMaxZoom=cluster_max_zoom,
        clusterColors=cluster_colors,
        clusterSteps=cluster_steps,
        clusterMinRadius=cluster_min_radius,
        clusterMaxRadius=cluster_max_radius,
        unclusteredColor=unclustered_color,
        unclusteredRadius=unclustered_radius,
        showClusterCount=show_cluster_count,
        zoomOnClick=zoom_on_click,
        fitBounds=fit_bounds,
        bounds=bounds,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "cluster",
            "source": f"{layer_id}-source",
        },
    }
    self._add_to_layer_dict(layer_id, "Vector")
    return layer_id

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.

Source code in anymap_ts/mapbox.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."""
    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 deck.gl-raster.

This method renders COG files directly in the browser using GPU-accelerated deck.gl 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 MapboxMap
>>> m = MapboxMap()
>>> m.add_cog_layer(
...     "https://example.com/landcover.tif",
...     name="landcover",
...     opacity=0.8
... )
Source code in anymap_ts/mapbox.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 deck.gl-raster.

    This method renders COG files directly in the browser using GPU-accelerated
    deck.gl 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 MapboxMap
        >>> m = MapboxMap()
        >>> 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,
        },
    }
    self._add_to_layer_dict(layer_id, "Raster")

add_colorbar(self, colormap='viridis', vmin=0, vmax=1, label='', units='', orientation='horizontal', position='bottom-right', bar_thickness=None, bar_length=None, ticks=None, opacity=None, colorbar_id=None, **kwargs)

Add a continuous gradient colorbar to the map.

Source code in anymap_ts/mapbox.py
def add_colorbar(
    self,
    colormap: str = "viridis",
    vmin: float = 0,
    vmax: float = 1,
    label: str = "",
    units: str = "",
    orientation: str = "horizontal",
    position: str = "bottom-right",
    bar_thickness: Optional[int] = None,
    bar_length: Optional[int] = None,
    ticks: Optional[Dict] = None,
    opacity: Optional[float] = None,
    colorbar_id: Optional[str] = None,
    **kwargs,
) -> None:
    """Add a continuous gradient colorbar to the map."""
    self._validate_position(position)

    cbar_id = (
        colorbar_id
        or f"colorbar-{len([k for k in self._controls.keys() if k.startswith('colorbar')])}"
    )

    js_kwargs: Dict[str, Any] = {
        "colormap": colormap,
        "vmin": vmin,
        "vmax": vmax,
        "label": label,
        "units": units,
        "orientation": orientation,
        "position": position,
        "colorbarId": cbar_id,
        **kwargs,
    }
    if bar_thickness is not None:
        js_kwargs["barThickness"] = bar_thickness
    if bar_length is not None:
        js_kwargs["barLength"] = bar_length
    if ticks is not None:
        js_kwargs["ticks"] = ticks
    if opacity is not None:
        js_kwargs["opacity"] = opacity

    self.call_js_method("addColorbar", **js_kwargs)

    self._controls = {
        **self._controls,
        cbar_id: {
            "type": "colorbar",
            "colormap": colormap,
            "vmin": vmin,
            "vmax": vmax,
            "label": label,
            "units": units,
            "orientation": orientation,
            "position": position,
        },
    }

add_column_layer(self, data, name=None, get_position='coordinates', get_fill_color=None, get_line_color=None, get_elevation=1000, radius=1000, disk_resolution=20, elevation_scale=1, coverage=1, extruded=True, filled=True, stroked=False, wireframe=False, pickable=True, opacity=0.8, **kwargs)

Add a column layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_column_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_fill_color: Optional[Union[List[int], str]] = None,
    get_line_color: Optional[Union[List[int], str]] = None,
    get_elevation: Union[float, str] = 1000,
    radius: float = 1000,
    disk_resolution: int = 20,
    elevation_scale: float = 1,
    coverage: float = 1,
    extruded: bool = True,
    filled: bool = True,
    stroked: bool = False,
    wireframe: bool = False,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a column layer using deck.gl."""
    layer_id = name or f"column-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addColumnLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getFillColor=get_fill_color or [255, 140, 0, 200],
        getLineColor=get_line_color or [0, 0, 0, 255],
        getElevation=get_elevation,
        radius=radius,
        diskResolution=disk_resolution,
        elevationScale=elevation_scale,
        coverage=coverage,
        extruded=extruded,
        filled=filled,
        stroked=stroked,
        wireframe=wireframe,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "column"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_contour_layer(self, data, name=None, get_position='coordinates', get_weight=1, cell_size=200, contours=None, pickable=True, opacity=1, **kwargs)

Add a contour layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_contour_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_weight: Union[float, str] = 1,
    cell_size: float = 200,
    contours: Optional[List[Dict]] = None,
    pickable: bool = True,
    opacity: float = 1,
    **kwargs,
) -> None:
    """Add a contour layer using deck.gl."""
    layer_id = name or f"contour-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    default_contours = [
        {"threshold": 1, "color": [255, 255, 255], "strokeWidth": 1},
        {"threshold": 5, "color": [51, 136, 255], "strokeWidth": 2},
        {"threshold": 10, "color": [0, 0, 255], "strokeWidth": 3},
    ]

    self.call_js_method(
        "addContourLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getWeight=get_weight,
        cellSize=cell_size,
        contours=contours or default_contours,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "contour"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

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

Source code in anymap_ts/mapbox.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."""
    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,
            "collapsible": collapsible,
        },
    }

add_coordinates_control(self, position='bottom-left', precision=4)

Add a coordinates display control.

Source code in anymap_ts/mapbox.py
def add_coordinates_control(
    self,
    position: str = "bottom-left",
    precision: int = 4,
) -> None:
    """Add a coordinates display control."""
    self.call_js_method(
        "addCoordinatesControl",
        position=position,
        precision=precision,
    )

add_deck_heatmap_layer(self, data, name=None, get_position='coordinates', get_weight=1, radius_pixels=30, intensity=1, threshold=0.05, color_range=None, opacity=1, **kwargs)

Add a GPU-accelerated heatmap layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_deck_heatmap_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_weight: Union[float, str] = 1,
    radius_pixels: float = 30,
    intensity: float = 1,
    threshold: float = 0.05,
    color_range: Optional[List[List[int]]] = None,
    opacity: float = 1,
    **kwargs,
) -> None:
    """Add a GPU-accelerated heatmap layer using deck.gl."""
    layer_id = name or f"deck-heatmap-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    default_color_range = [
        [255, 255, 178, 25],
        [254, 217, 118, 85],
        [254, 178, 76, 127],
        [253, 141, 60, 170],
        [240, 59, 32, 212],
        [189, 0, 38, 255],
    ]

    self.call_js_method(
        "addHeatmapLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getWeight=get_weight,
        radiusPixels=radius_pixels,
        intensity=intensity,
        threshold=threshold,
        colorRange=color_range or default_color_range,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {"id": layer_id, "type": "deck-heatmap"},
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_deckgl_layer(self, layer_type, data, name=None, **kwargs)

Add a generic deck.gl layer to the map.

Source code in anymap_ts/mapbox.py
def add_deckgl_layer(
    self,
    layer_type: str,
    data: Any,
    name: Optional[str] = None,
    **kwargs,
) -> None:
    """Add a generic deck.gl layer to the map."""
    layer_type_clean = layer_type.replace("Layer", "")
    prefix = layer_type_clean.lower()
    layer_id = name or f"{prefix}-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addDeckGLLayer",
        layerType=layer_type,
        id=layer_id,
        data=processed_data,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": layer_type}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

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

Add a drawing control.

Source code in anymap_ts/mapbox.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."""
    if draw_modes is None:
        draw_modes = ["polygon", "line", "rectangle", "circle", "marker"]
    if edit_modes is None:
        edit_modes = [
            "select",
            "drag",
            "change",
            "rotate",
            "cut",
            "delete",
            "scale",
            "copy",
            "split",
            "union",
            "difference",
            "simplify",
            "lasso",
        ]

    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_flatgeobuf(self, url, name=None, layer_type=None, paint=None, fit_bounds=True, **kwargs)

Add a FlatGeobuf layer from a URL.

Source code in anymap_ts/mapbox.py
def add_flatgeobuf(
    self,
    url: str,
    name: Optional[str] = None,
    layer_type: Optional[str] = None,
    paint: Optional[Dict] = None,
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add a FlatGeobuf layer from a URL."""
    layer_id = name or f"flatgeobuf-{len(self._layers)}"

    self.call_js_method(
        "addFlatGeobuf",
        url=url,
        name=layer_id,
        layerType=layer_type,
        paint=paint,
        fitBounds=fit_bounds,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "flatgeobuf",
            "url": url,
        },
    }
    self._add_to_layer_dict(layer_id, "Vector")

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]

Mapbox layer type.

None
paint Optional[Dict]

Mapbox 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/mapbox.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: Mapbox layer type.
        paint: Mapbox 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_geojson_layer(self, data, name=None, get_fill_color=None, get_line_color=None, get_line_width=1, get_point_radius=5, get_elevation=0, extruded=False, wireframe=False, filled=True, stroked=True, line_width_min_pixels=1, point_radius_min_pixels=2, pickable=True, opacity=0.8, **kwargs)

Add a GeoJSON layer with auto-styling using deck.gl.

Source code in anymap_ts/mapbox.py
def add_geojson_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_fill_color: Optional[Union[List[int], str]] = None,
    get_line_color: Optional[Union[List[int], str]] = None,
    get_line_width: Union[float, str] = 1,
    get_point_radius: Union[float, str] = 5,
    get_elevation: Union[float, str] = 0,
    extruded: bool = False,
    wireframe: bool = False,
    filled: bool = True,
    stroked: bool = True,
    line_width_min_pixels: float = 1,
    point_radius_min_pixels: float = 2,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a GeoJSON layer with auto-styling using deck.gl."""
    layer_id = name or f"geojson-deck-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addGeoJsonLayer",
        id=layer_id,
        data=processed_data,
        getFillColor=get_fill_color or [51, 136, 255, 128],
        getLineColor=get_line_color or [0, 0, 0, 255],
        getLineWidth=get_line_width,
        getPointRadius=get_point_radius,
        getElevation=get_elevation,
        extruded=extruded,
        wireframe=wireframe,
        filled=filled,
        stroked=stroked,
        lineWidthMinPixels=line_width_min_pixels,
        pointRadiusMinPixels=point_radius_min_pixels,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {"id": layer_id, "type": "geojson-deck"},
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_grid_cell_layer(self, data, name=None, get_position='coordinates', get_color=None, get_elevation=1000, cell_size=200, coverage=1, elevation_scale=1, extruded=True, pickable=True, opacity=0.8, **kwargs)

Add a grid cell layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_grid_cell_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_color: Optional[Union[List[int], str]] = None,
    get_elevation: Union[float, str] = 1000,
    cell_size: float = 200,
    coverage: float = 1,
    elevation_scale: float = 1,
    extruded: bool = True,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a grid cell layer using deck.gl."""
    layer_id = name or f"gridcell-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addGridCellLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getColor=get_color or [255, 140, 0, 200],
        getElevation=get_elevation,
        cellSize=cell_size,
        coverage=coverage,
        elevationScale=elevation_scale,
        extruded=extruded,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {"id": layer_id, "type": "gridcell"},
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_grid_layer(self, data, name=None, get_position='coordinates', cell_size=200, elevation_scale=4, extruded=True, color_range=None, pickable=True, opacity=0.8, **kwargs)

Add a grid layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_grid_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    cell_size: float = 200,
    elevation_scale: float = 4,
    extruded: bool = True,
    color_range: Optional[List[List[int]]] = None,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a grid layer using deck.gl."""
    layer_id = name or f"grid-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    default_color_range = [
        [1, 152, 189],
        [73, 227, 206],
        [216, 254, 181],
        [254, 237, 177],
        [254, 173, 84],
        [209, 55, 78],
    ]

    self.call_js_method(
        "addGridLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        cellSize=cell_size,
        elevationScale=elevation_scale,
        extruded=extruded,
        colorRange=color_range or default_color_range,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "grid"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_heatmap(self, data, weight_property=None, radius=20, intensity=1.0, colormap=None, opacity=0.8, name=None, fit_bounds=True, **kwargs)

Add a heatmap layer to the map.

Source code in anymap_ts/mapbox.py
def add_heatmap(
    self,
    data: Any,
    weight_property: Optional[str] = None,
    radius: int = 20,
    intensity: float = 1.0,
    colormap: Optional[List] = None,
    opacity: float = 0.8,
    name: Optional[str] = None,
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add a heatmap layer to the map."""
    self._validate_opacity(opacity)
    layer_id = name or f"heatmap-{len(self._layers)}"

    geojson = to_geojson(data)

    if geojson.get("type") == "url":
        url = geojson["url"]
        geojson = fetch_geojson(url)

    if colormap is None:
        colormap = [
            [0, "rgba(33,102,172,0)"],
            [0.2, "rgb(103,169,207)"],
            [0.4, "rgb(209,229,240)"],
            [0.6, "rgb(253,219,199)"],
            [0.8, "rgb(239,138,98)"],
            [1, "rgb(178,24,43)"],
        ]

    paint = {
        "heatmap-radius": radius,
        "heatmap-intensity": intensity,
        "heatmap-opacity": opacity,
        "heatmap-color": [
            "interpolate",
            ["linear"],
            ["heatmap-density"],
        ],
    }

    for stop, color in colormap:
        paint["heatmap-color"].extend([stop, color])

    if weight_property:
        paint["heatmap-weight"] = ["get", weight_property]

    bounds = get_bounds(geojson) if fit_bounds else None

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

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "heatmap",
            "source": f"{layer_id}-source",
            "paint": paint,
        },
    }
    self._add_to_layer_dict(layer_id, "Heatmap")

add_hexagon_layer(self, data, name=None, get_position='coordinates', radius=1000, elevation_scale=4, extruded=True, color_range=None, pickable=True, opacity=0.8, **kwargs)

Add a hexagon layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_hexagon_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    radius: float = 1000,
    elevation_scale: float = 4,
    extruded: bool = True,
    color_range: Optional[List[List[int]]] = None,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a hexagon layer using deck.gl."""
    layer_id = name or f"hexagon-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    default_color_range = [
        [1, 152, 189],
        [73, 227, 206],
        [216, 254, 181],
        [254, 237, 177],
        [254, 173, 84],
        [209, 55, 78],
    ]

    self.call_js_method(
        "addHexagonLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        radius=radius,
        elevationScale=elevation_scale,
        extruded=extruded,
        colorRange=color_range or default_color_range,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "hexagon"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_hover_effect(self, layer_id, highlight_color=None, highlight_opacity=None, highlight_outline_width=2, **kwargs)

Add hover highlight effect to an existing layer.

Source code in anymap_ts/mapbox.py
def add_hover_effect(
    self,
    layer_id: str,
    highlight_color: Optional[str] = None,
    highlight_opacity: Optional[float] = None,
    highlight_outline_width: float = 2,
    **kwargs,
) -> None:
    """Add hover highlight effect to an existing layer."""
    self.call_js_method(
        "addHoverEffect",
        layerId=layer_id,
        highlightColor=highlight_color,
        highlightOpacity=highlight_opacity,
        highlightOutlineWidth=highlight_outline_width,
        **kwargs,
    )

add_icon_layer(self, data, name=None, get_position='coordinates', get_icon='icon', get_size=20, get_color=None, icon_atlas=None, icon_mapping=None, pickable=True, opacity=1, **kwargs)

Add an icon layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_icon_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_icon: Union[str, Any] = "icon",
    get_size: Union[float, str] = 20,
    get_color: Optional[Union[List[int], str]] = None,
    icon_atlas: Optional[str] = None,
    icon_mapping: Optional[Dict] = None,
    pickable: bool = True,
    opacity: float = 1,
    **kwargs,
) -> None:
    """Add an icon layer using deck.gl."""
    layer_id = name or f"icon-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addIconLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getIcon=get_icon,
        getSize=get_size,
        getColor=get_color or [255, 255, 255, 255],
        iconAtlas=icon_atlas,
        iconMapping=icon_mapping,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "icon"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_image(self, name, url)

Load a custom icon image for use in symbol layers.

Source code in anymap_ts/mapbox.py
def add_image(self, name: str, url: str) -> None:
    """Load a custom icon image for use in symbol layers."""
    self.call_js_method("addMapImage", name=name, url=url)

add_image_layer(self, url, coordinates, name=None, opacity=1.0, **kwargs)

Add a georeferenced image overlay.

Source code in anymap_ts/mapbox.py
def add_image_layer(
    self,
    url: str,
    coordinates: List[List[float]],
    name: Optional[str] = None,
    opacity: float = 1.0,
    **kwargs,
) -> None:
    """Add a georeferenced image overlay."""
    self._validate_opacity(opacity)
    layer_id = name or f"image-{len(self._layers)}"

    if len(coordinates) != 4:
        raise ValueError(
            "coordinates must have exactly 4 corner points "
            "[top-left, top-right, bottom-right, bottom-left]"
        )

    self.call_js_method(
        "addImageLayer",
        id=layer_id,
        url=url,
        coordinates=coordinates,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "image",
            "url": url,
            "coordinates": coordinates,
        },
    }
    self._add_to_layer_dict(layer_id, "Raster")

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

Mapbox 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/mapbox.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: Mapbox 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)
    lt = layer_config.get("type", "")
    self._add_to_layer_dict(layer_id, "Raster" if lt == "raster" else "Vector")

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

Add a layer visibility control.

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

add_legend(self, title, labels, colors, position='bottom-right', opacity=1.0, legend_id=None, **kwargs)

Add a floating legend control to the map.

Source code in anymap_ts/mapbox.py
def add_legend(
    self,
    title: str,
    labels: List[str],
    colors: List[str],
    position: str = "bottom-right",
    opacity: float = 1.0,
    legend_id: Optional[str] = None,
    **kwargs,
) -> None:
    """Add a floating legend control to the map."""
    if len(labels) != len(colors):
        raise ValueError("Number of labels must match number of colors")

    self._validate_position(position)

    for i, color in enumerate(colors):
        if not isinstance(color, str) or not color.startswith("#"):
            raise ValueError(
                f"Color at index {i} must be a hex color string (e.g., '#ff0000')"
            )

    legend_id = (
        legend_id
        or f"legend-{len([k for k in self._controls.keys() if k.startswith('legend')])}"
    )

    legend_items = [
        {"label": label, "color": color} for label, color in zip(labels, colors)
    ]

    self.call_js_method(
        "addLegend",
        id=legend_id,
        title=title,
        items=legend_items,
        position=position,
        opacity=opacity,
        **kwargs,
    )

    self._controls = {
        **self._controls,
        legend_id: {
            "type": "legend",
            "title": title,
            "labels": labels,
            "colors": colors,
            "position": position,
            "opacity": opacity,
        },
    }

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, **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
**kwargs

Additional control options.

{}

Examples:

>>> from anymap_ts import MapboxMap
>>> m = MapboxMap(pitch=60)
>>> m.add_lidar_control(color_scheme="classification", pickable=True)
Source code in anymap_ts/mapbox.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,
    **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.
        **kwargs: Additional control options.

    Example:
        >>> from anymap_ts import MapboxMap
        >>> m = MapboxMap(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,
        **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 MapboxMap
>>> m = MapboxMap(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/mapbox.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 MapboxMap
        >>> m = MapboxMap(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",
        ... )
    """
    import base64

    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
        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_line_layer(self, data, name=None, get_source_position='sourcePosition', get_target_position='targetPosition', get_color=None, get_width=1, width_min_pixels=1, pickable=True, opacity=0.8, **kwargs)

Add a line layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_line_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_source_position: Union[str, Any] = "sourcePosition",
    get_target_position: Union[str, Any] = "targetPosition",
    get_color: Optional[Union[List[int], str]] = None,
    get_width: Union[float, str] = 1,
    width_min_pixels: float = 1,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a line layer using deck.gl."""
    layer_id = name or f"line-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addLineLayer",
        id=layer_id,
        data=processed_data,
        getSourcePosition=get_source_position,
        getTargetPosition=get_target_position,
        getColor=get_color or [51, 136, 255, 200],
        getWidth=get_width,
        widthMinPixels=width_min_pixels,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "line"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_marker(self, lng, lat, popup=None, tooltip=None, color='#3388ff', draggable=False, scale=1.0, popup_max_width='240px', tooltip_max_width='240px', name=None, **kwargs)

Add a single marker to the map.

Source code in anymap_ts/mapbox.py
def add_marker(
    self,
    lng: float,
    lat: float,
    popup: Optional[str] = None,
    tooltip: Optional[str] = None,
    color: str = "#3388ff",
    draggable: bool = False,
    scale: float = 1.0,
    popup_max_width: str = "240px",
    tooltip_max_width: str = "240px",
    name: Optional[str] = None,
    **kwargs,
) -> str:
    """Add a single marker to the map."""
    marker_id = name or f"marker-{len(self._layers)}"

    self.call_js_method(
        "addMarker",
        lng,
        lat,
        id=marker_id,
        popup=popup,
        tooltip=tooltip,
        color=color,
        draggable=draggable,
        scale=scale,
        popupMaxWidth=popup_max_width,
        tooltipMaxWidth=tooltip_max_width,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        marker_id: {
            "id": marker_id,
            "type": "marker",
            "lngLat": [lng, lat],
        },
    }
    self._add_to_layer_dict(marker_id, "Markers")
    return marker_id

add_markers(self, data, lng_column=None, lat_column=None, popup_column=None, tooltip_column=None, color='#3388ff', scale=1.0, popup_max_width='240px', tooltip_max_width='240px', draggable=False, name=None, **kwargs)

Add multiple markers from data.

Source code in anymap_ts/mapbox.py
def add_markers(
    self,
    data: Any,
    lng_column: Optional[str] = None,
    lat_column: Optional[str] = None,
    popup_column: Optional[str] = None,
    tooltip_column: Optional[str] = None,
    color: str = "#3388ff",
    scale: float = 1.0,
    popup_max_width: str = "240px",
    tooltip_max_width: str = "240px",
    draggable: bool = False,
    name: Optional[str] = None,
    **kwargs,
) -> str:
    """Add multiple markers from data."""
    layer_id = name or f"markers-{len(self._layers)}"
    markers = []

    if hasattr(data, "geometry"):
        for _, row in data.iterrows():
            geom = row.geometry
            if geom.geom_type == "Point":
                marker = {"lngLat": [geom.x, geom.y]}
                if popup_column and popup_column in row:
                    marker["popup"] = str(row[popup_column])
                if tooltip_column and tooltip_column in row:
                    marker["tooltip"] = str(row[tooltip_column])
                markers.append(marker)
    elif isinstance(data, dict) and data.get("type") == "FeatureCollection":
        for feature in data.get("features", []):
            geom = feature.get("geometry", {})
            if geom.get("type") == "Point":
                coords = geom.get("coordinates", [])
                marker = {"lngLat": coords[:2]}
                props = feature.get("properties", {})
                if popup_column and popup_column in props:
                    marker["popup"] = str(props[popup_column])
                if tooltip_column and tooltip_column in props:
                    marker["tooltip"] = str(props[tooltip_column])
                markers.append(marker)
    elif isinstance(data, list):
        lng_keys = ["lng", "lon", "longitude", "x"]
        lat_keys = ["lat", "latitude", "y"]

        for item in data:
            if not isinstance(item, dict):
                continue

            lng_val = None
            lat_val = None

            if lng_column and lng_column in item:
                lng_val = item[lng_column]
            else:
                for key in lng_keys:
                    if key in item:
                        lng_val = item[key]
                        break

            if lat_column and lat_column in item:
                lat_val = item[lat_column]
            else:
                for key in lat_keys:
                    if key in item:
                        lat_val = item[key]
                        break

            if lng_val is not None and lat_val is not None:
                marker = {"lngLat": [float(lng_val), float(lat_val)]}
                if popup_column and popup_column in item:
                    marker["popup"] = str(item[popup_column])
                if tooltip_column and tooltip_column in item:
                    marker["tooltip"] = str(item[tooltip_column])
                markers.append(marker)

    if not markers:
        raise ValueError("No valid point data found in input")

    self.call_js_method(
        "addMarkers",
        id=layer_id,
        markers=markers,
        color=color,
        scale=scale,
        popupMaxWidth=popup_max_width,
        tooltipMaxWidth=tooltip_max_width,
        draggable=draggable,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "markers",
            "count": len(markers),
        },
    }
    self._add_to_layer_dict(layer_id, "Markers")
    return layer_id

add_measure_control(self, position='top-right', collapsed=True, default_mode='distance', distance_unit='kilometers', area_unit='square-kilometers', line_color='#3b82f6', fill_color='rgba(59, 130, 246, 0.2)', **kwargs)

Add a measurement control.

Source code in anymap_ts/mapbox.py
def add_measure_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    default_mode: str = "distance",
    distance_unit: str = "kilometers",
    area_unit: str = "square-kilometers",
    line_color: str = "#3b82f6",
    fill_color: str = "rgba(59, 130, 246, 0.2)",
    **kwargs,
) -> None:
    """Add a measurement control."""
    self._validate_position(position)
    self.call_js_method(
        "addMeasureControl",
        position=position,
        collapsed=collapsed,
        defaultMode=default_mode,
        distanceUnit=distance_unit,
        areaUnit=area_unit,
        lineColor=line_color,
        fillColor=fill_color,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "measure-control": {
            "type": "measure-control",
            "position": position,
            "collapsed": collapsed,
        },
    }

add_opacity_slider(self, layer_id, position='top-right', label=None)

Add a UI slider to control layer opacity.

Source code in anymap_ts/mapbox.py
def add_opacity_slider(
    self,
    layer_id: str,
    position: str = "top-right",
    label: Optional[str] = None,
) -> None:
    """Add a UI slider to control layer opacity."""
    self.call_js_method(
        "addOpacitySlider",
        layerId=layer_id,
        position=position,
        label=label or layer_id,
    )

add_path_layer(self, data, name=None, get_path='path', get_color=None, get_width=1, width_scale=1, width_min_pixels=1, pickable=True, opacity=0.8, **kwargs)

Add a path layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_path_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_path: Union[str, Any] = "path",
    get_color: Optional[Union[List[int], str]] = None,
    get_width: Union[float, str] = 1,
    width_scale: float = 1,
    width_min_pixels: float = 1,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a path layer using deck.gl."""
    layer_id = name or f"path-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addPathLayer",
        id=layer_id,
        data=processed_data,
        getPath=get_path,
        getColor=get_color or [51, 136, 255, 200],
        getWidth=get_width,
        widthScale=width_scale,
        widthMinPixels=width_min_pixels,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "path"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

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.

Source code in anymap_ts/mapbox.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."""
    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_pmtiles_layer(self, url, layer_id=None, style=None, opacity=1.0, visible=True, fit_bounds=False, source_type='vector', prefix='', popup=None, **kwargs)

Add a PMTiles layer for efficient vector or raster tile serving.

When no style is provided for vector PMTiles, the method automatically discovers all source layers from the PMTiles metadata and renders each one with a distinct color. Geometry types from the metadata determine the layer type: Polygon becomes fill, LineString becomes line, and Point becomes circle. For text labels and icons, use the symbol layer type with layout properties like text-field and icon-image.

Parameters:

Name Type Description Default
url str

URL to the PMTiles file.

required
layer_id Optional[str]

Layer identifier. If None, auto-generated.

None
style Optional[Dict[str, Any]]

Layer style configuration. If None and source_type is "vector", auto-discovers all source layers with distinct colors.

None
opacity float

Layer opacity (0-1).

1.0
visible bool

Whether layer is initially visible.

True
fit_bounds bool

Whether to fit map to layer bounds after loading.

False
source_type str

Source type - "vector" or "raster".

'vector'
prefix str

Prefix for auto-discovered layer names in the layer control. Defaults to empty string (no prefix).

''
popup Optional[Union[bool, List[str], str]]

Configure popups on click. Accepts "all" or True (all properties), a list of property names, or an HTML template string with {property_name} placeholders. Defaults to None (no popup).

None
**kwargs

Additional layer options.

{}
Source code in anymap_ts/mapbox.py
def add_pmtiles_layer(
    self,
    url: str,
    layer_id: Optional[str] = None,
    style: Optional[Dict[str, Any]] = None,
    opacity: float = 1.0,
    visible: bool = True,
    fit_bounds: bool = False,
    source_type: str = "vector",
    prefix: str = "",
    popup: Optional[Union[bool, List[str], str]] = None,
    **kwargs,
) -> None:
    """Add a PMTiles layer for efficient vector or raster tile serving.

    When no style is provided for vector PMTiles, the method automatically
    discovers all source layers from the PMTiles metadata and renders each
    one with a distinct color. Geometry types from the metadata determine
    the layer type: Polygon becomes fill, LineString becomes line, and
    Point becomes circle. For text labels and icons, use the symbol layer
    type with layout properties like text-field and icon-image.

    Args:
        url: URL to the PMTiles file.
        layer_id: Layer identifier. If None, auto-generated.
        style: Layer style configuration. If None and source_type is
            "vector", auto-discovers all source layers with distinct colors.
        opacity: Layer opacity (0-1).
        visible: Whether layer is initially visible.
        fit_bounds: Whether to fit map to layer bounds after loading.
        source_type: Source type - "vector" or "raster".
        prefix: Prefix for auto-discovered layer names in the layer
            control. Defaults to empty string (no prefix).
        popup: Configure popups on click. Accepts "all" or True (all
            properties), a list of property names, or an HTML template
            string with {property_name} placeholders. Defaults to None
            (no popup).
        **kwargs: Additional layer options.
    """
    layer_id = layer_id or f"pmtiles-{len(self._layers)}"

    popup_config: Optional[Dict[str, Any]] = None
    if popup is True or popup == "all":
        popup_config = {"enabled": True}
    elif isinstance(popup, list):
        popup_config = {"enabled": True, "properties": popup}
    elif isinstance(popup, str):
        popup_config = {"enabled": True, "template": popup}

    self.call_js_method(
        "addPMTilesLayer",
        url=url,
        id=layer_id,
        style=style or {},
        opacity=opacity,
        visible=visible,
        fitBounds=fit_bounds,
        sourceType=source_type,
        prefix=prefix,
        popup=popup_config,
        name=layer_id,
        **kwargs,
    )

    # Listen for auto-discovered styles from JS
    if style is None and source_type == "vector":

        def _on_discovered(data: Dict[str, Any]) -> None:
            discovered_id = data.get("layerId", layer_id)
            self._pmtiles_styles[discovered_id] = data.get("subLayers", [])

        self.on_map_event("pmtiles_layers_discovered", _on_discovered)

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "pmtiles",
            "url": url,
            "source_type": source_type,
        },
    }
    category = "Vector" if source_type == "vector" else "Raster"
    self._add_to_layer_dict(layer_id, category)

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 MapboxMap
>>> m = MapboxMap(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/mapbox.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 MapboxMap
        >>> m = MapboxMap(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",
        },
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_polygon_layer(self, data, name=None, get_polygon='polygon', get_fill_color=None, get_line_color=None, get_line_width=1, get_elevation=0, extruded=False, wireframe=False, filled=True, stroked=True, line_width_min_pixels=1, pickable=True, opacity=0.5, **kwargs)

Add a polygon layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_polygon_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_polygon: Union[str, Any] = "polygon",
    get_fill_color: Optional[Union[List[int], str]] = None,
    get_line_color: Optional[Union[List[int], str]] = None,
    get_line_width: Union[float, str] = 1,
    get_elevation: Union[float, str] = 0,
    extruded: bool = False,
    wireframe: bool = False,
    filled: bool = True,
    stroked: bool = True,
    line_width_min_pixels: float = 1,
    pickable: bool = True,
    opacity: float = 0.5,
    **kwargs,
) -> None:
    """Add a polygon layer using deck.gl."""
    layer_id = name or f"polygon-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addPolygonLayer",
        id=layer_id,
        data=processed_data,
        getPolygon=get_polygon,
        getFillColor=get_fill_color or [51, 136, 255, 128],
        getLineColor=get_line_color or [0, 0, 255, 255],
        getLineWidth=get_line_width,
        getElevation=get_elevation,
        extruded=extruded,
        wireframe=wireframe,
        filled=filled,
        stroked=stroked,
        lineWidthMinPixels=line_width_min_pixels,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "polygon"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_popup(self, layer_id, properties=None, template=None, **kwargs)

Add popup on click for a layer.

Source code in anymap_ts/mapbox.py
def add_popup(
    self,
    layer_id: str,
    properties: Optional[List[str]] = None,
    template: Optional[str] = None,
    **kwargs,
) -> None:
    """Add popup on click for a layer."""
    self.call_js_method(
        "addPopup",
        layerId=layer_id,
        properties=properties,
        template=template,
        **kwargs,
    )

add_print_control(self, position='top-right', collapsed=True, format='png', filename='map-export', include_north_arrow=False, include_scale_bar=False, **kwargs)

Add a print/export control.

Source code in anymap_ts/mapbox.py
def add_print_control(
    self,
    position: str = "top-right",
    collapsed: bool = True,
    format: str = "png",
    filename: str = "map-export",
    include_north_arrow: bool = False,
    include_scale_bar: bool = False,
    **kwargs,
) -> None:
    """Add a print/export control."""
    self._validate_position(position)
    self.call_js_method(
        "addPrintControl",
        position=position,
        collapsed=collapsed,
        format=format,
        filename=filename,
        includeNorthArrow=include_north_arrow,
        includeScaleBar=include_scale_bar,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "print-control": {
            "type": "print-control",
            "position": position,
            "collapsed": collapsed,
        },
    }

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.

Source code in anymap_ts/mapbox.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."""
    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)

    tile_params = {}
    if indexes:
        tile_params["indexes"] = indexes
    if colormap:
        tile_params["colormap"] = colormap
    if vmin is not None or vmax is not None:
        tile_params["vmin"] = vmin if vmin is not None else client.min
        tile_params["vmax"] = vmax if vmax is not None else client.max
    if nodata is not None:
        tile_params["nodata"] = nodata

    tile_url = client.get_tile_url(**tile_params)

    layer_name = name or Path(source).stem

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

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

add_scatterplot_layer(self, data, name=None, get_position='coordinates', get_radius=5, get_fill_color=None, get_line_color=None, radius_scale=1, radius_min_pixels=1, radius_max_pixels=100, line_width_min_pixels=1, stroked=True, filled=True, pickable=True, opacity=0.8, **kwargs)

Add a scatterplot layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_scatterplot_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_radius: Union[float, str] = 5,
    get_fill_color: Optional[Union[List[int], str]] = None,
    get_line_color: Optional[Union[List[int], str]] = None,
    radius_scale: float = 1,
    radius_min_pixels: float = 1,
    radius_max_pixels: float = 100,
    line_width_min_pixels: float = 1,
    stroked: bool = True,
    filled: bool = True,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a scatterplot layer using deck.gl."""
    layer_id = name or f"scatterplot-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addScatterplotLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getRadius=get_radius,
        getFillColor=get_fill_color or [51, 136, 255, 200],
        getLineColor=get_line_color or [255, 255, 255, 255],
        radiusScale=radius_scale,
        radiusMinPixels=radius_min_pixels,
        radiusMaxPixels=radius_max_pixels,
        lineWidthMinPixels=line_width_min_pixels,
        stroked=stroked,
        filled=filled,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {"id": layer_id, "type": "scatterplot"},
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_screen_grid_layer(self, data, name=None, get_position='coordinates', get_weight=1, cell_size_pixels=50, color_range=None, pickable=True, opacity=0.8, **kwargs)

Add a screen grid layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_screen_grid_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_weight: Union[float, str] = 1,
    cell_size_pixels: float = 50,
    color_range: Optional[List[List[int]]] = None,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a screen grid layer using deck.gl."""
    layer_id = name or f"screengrid-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    default_color_range = [
        [255, 255, 178, 25],
        [254, 217, 118, 85],
        [254, 178, 76, 127],
        [253, 141, 60, 170],
        [240, 59, 32, 212],
        [189, 0, 38, 255],
    ]

    self.call_js_method(
        "addScreenGridLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getWeight=get_weight,
        cellSizePixels=cell_size_pixels,
        colorRange=color_range or default_color_range,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {"id": layer_id, "type": "screengrid"},
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_search_control(self, position='top-left', placeholder='Search places...', collapsed=True, fly_to_zoom=14, show_marker=True, marker_color='#4264fb', **kwargs)

Add a search/geocoder control.

Source code in anymap_ts/mapbox.py
def add_search_control(
    self,
    position: str = "top-left",
    placeholder: str = "Search places...",
    collapsed: bool = True,
    fly_to_zoom: int = 14,
    show_marker: bool = True,
    marker_color: str = "#4264fb",
    **kwargs,
) -> None:
    """Add a search/geocoder control."""
    self._validate_position(position)
    self.call_js_method(
        "addSearchControl",
        position=position,
        placeholder=placeholder,
        collapsed=collapsed,
        flyToZoom=fly_to_zoom,
        showMarker=show_marker,
        markerColor=marker_color,
        **kwargs,
    )
    self._controls = {
        **self._controls,
        "search-control": {
            "type": "search-control",
            "position": position,
            "collapsed": collapsed,
        },
    }

add_solid_polygon_layer(self, data, name=None, get_polygon='polygon', get_fill_color=None, get_line_color=None, get_elevation=0, filled=True, extruded=False, wireframe=False, elevation_scale=1, pickable=True, opacity=0.8, **kwargs)

Add a solid polygon layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_solid_polygon_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_polygon: Union[str, Any] = "polygon",
    get_fill_color: Optional[Union[List[int], str]] = None,
    get_line_color: Optional[Union[List[int], str]] = None,
    get_elevation: Union[float, str] = 0,
    filled: bool = True,
    extruded: bool = False,
    wireframe: bool = False,
    elevation_scale: float = 1,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a solid polygon layer using deck.gl."""
    layer_id = name or f"solidpolygon-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addSolidPolygonLayer",
        id=layer_id,
        data=processed_data,
        getPolygon=get_polygon,
        getFillColor=get_fill_color or [51, 136, 255, 128],
        getLineColor=get_line_color or [0, 0, 0, 255],
        getElevation=get_elevation,
        filled=filled,
        extruded=extruded,
        wireframe=wireframe,
        elevationScale=elevation_scale,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {"id": layer_id, "type": "solidpolygon"},
    }
    self._add_to_layer_dict(layer_id, "Deck.gl")

add_split_map(self, left_layer, right_layer, position=50)

Add a split map comparison view with a draggable divider.

Source code in anymap_ts/mapbox.py
def add_split_map(
    self,
    left_layer: str,
    right_layer: str,
    position: int = 50,
) -> None:
    """Add a split map comparison view with a draggable divider."""
    if not 0 <= position <= 100:
        raise ValueError(f"position must be between 0 and 100, got {position}")

    self.call_js_method(
        "addSplitMap",
        leftLayer=left_layer,
        rightLayer=right_layer,
        position=position,
    )

add_stac_layer(self, url=None, item=None, assets=None, colormap=None, rescale=None, opacity=1.0, layer_id=None, titiler_endpoint='https://titiler.xyz', attribution='STAC', fit_bounds=True, **kwargs)

Add a STAC (SpatioTemporal Asset Catalog) layer to the map.

Source code in anymap_ts/mapbox.py
def add_stac_layer(
    self,
    url: Optional[str] = None,
    item: Optional[Any] = None,
    assets: Optional[List[str]] = None,
    colormap: Optional[str] = None,
    rescale: Optional[List[float]] = None,
    opacity: float = 1.0,
    layer_id: Optional[str] = None,
    titiler_endpoint: str = "https://titiler.xyz",
    attribution: str = "STAC",
    fit_bounds: bool = True,
    **kwargs,
) -> None:
    """Add a STAC (SpatioTemporal Asset Catalog) layer to the map."""
    if url is None and item is None:
        raise ValueError("Either 'url' or 'item' must be provided")

    if url is not None and item is not None:
        raise ValueError("Provide either 'url' or 'item', not both")

    if item is not None:
        try:
            if hasattr(item, "to_dict") and hasattr(item, "self_href"):
                stac_url = item.self_href
                if not stac_url and hasattr(item, "links"):
                    for link in item.links:
                        if link.rel == "self":
                            stac_url = link.href
                            break
                if not stac_url:
                    raise ValueError("STAC item must have a self_href or self link")
            else:
                raise ValueError(
                    "Item must be a pystac Item object with to_dict() and self_href"
                )
        except Exception as e:
            raise ValueError(f"Invalid STAC item: {e}")
    else:
        stac_url = url

    tile_params = {"url": stac_url}
    if assets:
        tile_params["assets"] = ",".join(assets)
    if colormap:
        tile_params["colormap_name"] = colormap
    if rescale:
        if len(rescale) == 2:
            tile_params["rescale"] = f"{rescale[0]},{rescale[1]}"
        else:
            raise ValueError("rescale must be a list of two values [min, max]")

    query_string = urlencode(tile_params)
    tile_url = f"{titiler_endpoint.rstrip('/')}/stac/tiles/{{z}}/{{x}}/{{y}}?{query_string}"

    layer_name = layer_id or f"stac-{len(self._layers)}"

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

    if fit_bounds and item is not None:
        try:
            bbox = item.bbox
            if bbox and len(bbox) == 4:
                self.fit_bounds([[bbox[0], bbox[1]], [bbox[2], bbox[3]]])
        except Exception:
            pass

add_style_switcher(self, styles, position='top-right')

Add a dropdown to switch between map styles.

Source code in anymap_ts/mapbox.py
def add_style_switcher(
    self,
    styles: Dict[str, str],
    position: str = "top-right",
) -> None:
    """Add a dropdown to switch between map styles."""
    self.call_js_method(
        "addStyleSwitcher",
        styles=styles,
        position=position,
    )

add_swipe_map(self, left_layer, right_layer)

Add a drag-to-compare swipe control for two layers.

Source code in anymap_ts/mapbox.py
def add_swipe_map(self, left_layer: str, right_layer: str) -> None:
    """Add a drag-to-compare swipe control for two layers."""
    self.call_js_method(
        "addSwipeMap",
        leftLayer=left_layer,
        rightLayer=right_layer,
    )

add_terrain(self, exaggeration=1.0, source='mapbox-dem')

Add 3D terrain to the map.

Parameters:

Name Type Description Default
exaggeration float

Terrain exaggeration factor.

1.0
source str

Terrain source ID.

'mapbox-dem'
Source code in anymap_ts/mapbox.py
def add_terrain(
    self, exaggeration: float = 1.0, source: str = "mapbox-dem"
) -> None:
    """Add 3D terrain to the map.

    Args:
        exaggeration: Terrain exaggeration factor.
        source: Terrain source ID.
    """
    self.call_js_method("addTerrain", source=source, exaggeration=exaggeration)

add_text_layer(self, data, name=None, get_position='coordinates', get_text='text', get_size=12, get_color=None, get_angle=0, text_anchor='middle', alignment_baseline='center', pickable=True, opacity=1, **kwargs)

Add a text layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_text_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_position: Union[str, Any] = "coordinates",
    get_text: Union[str, Any] = "text",
    get_size: Union[float, str] = 12,
    get_color: Optional[Union[List[int], str]] = None,
    get_angle: Union[float, str] = 0,
    text_anchor: str = "middle",
    alignment_baseline: str = "center",
    pickable: bool = True,
    opacity: float = 1,
    **kwargs,
) -> None:
    """Add a text layer using deck.gl."""
    layer_id = name or f"text-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addTextLayer",
        id=layer_id,
        data=processed_data,
        getPosition=get_position,
        getText=get_text,
        getSize=get_size,
        getColor=get_color or [0, 0, 0, 255],
        getAngle=get_angle,
        getTextAnchor=text_anchor,
        getAlignmentBaseline=alignment_baseline,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "text"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

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/mapbox.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",
        },
    }
    self._add_to_layer_dict(layer_id, "Raster")

add_time_slider(self, layer_id, property, min_value=0, max_value=100, step=1, position='bottom-left', label='Time', auto_play=False, interval=500)

Add a time slider to filter data by a temporal property.

Source code in anymap_ts/mapbox.py
def add_time_slider(
    self,
    layer_id: str,
    property: str,
    min_value: float = 0,
    max_value: float = 100,
    step: float = 1,
    position: str = "bottom-left",
    label: str = "Time",
    auto_play: bool = False,
    interval: int = 500,
) -> None:
    """Add a time slider to filter data by a temporal property."""
    self.call_js_method(
        "addTimeSlider",
        layerId=layer_id,
        property=property,
        min=min_value,
        max=max_value,
        step=step,
        position=position,
        label=label,
        autoPlay=auto_play,
        interval=interval,
    )

add_tooltip(self, layer_id, template=None, properties=None)

Add a tooltip that shows on feature hover.

Source code in anymap_ts/mapbox.py
def add_tooltip(
    self,
    layer_id: str,
    template: Optional[str] = None,
    properties: Optional[List[str]] = None,
) -> None:
    """Add a tooltip that shows on feature hover."""
    self.call_js_method(
        "addTooltip",
        layerId=layer_id,
        template=template or "",
        properties=properties,
    )

add_trips_layer(self, data, name=None, get_path='waypoints', get_timestamps='timestamps', get_color=None, width_min_pixels=2, trail_length=180, current_time=0, pickable=True, opacity=0.8, **kwargs)

Add a trips layer using deck.gl.

Source code in anymap_ts/mapbox.py
def add_trips_layer(
    self,
    data: Any,
    name: Optional[str] = None,
    get_path: Union[str, Any] = "waypoints",
    get_timestamps: Union[str, Any] = "timestamps",
    get_color: Optional[Union[List[int], str]] = None,
    width_min_pixels: float = 2,
    trail_length: float = 180,
    current_time: float = 0,
    pickable: bool = True,
    opacity: float = 0.8,
    **kwargs,
) -> None:
    """Add a trips layer using deck.gl."""
    layer_id = name or f"trips-{len(self._layers)}"
    processed_data = self._process_deck_data(data)

    self.call_js_method(
        "addTripsLayer",
        id=layer_id,
        data=processed_data,
        getPath=get_path,
        getTimestamps=get_timestamps,
        getColor=get_color or [253, 128, 93],
        widthMinPixels=width_min_pixels,
        trailLength=trail_length,
        currentTime=current_time,
        pickable=pickable,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {**self._layers, layer_id: {"id": layer_id, "type": "trips"}}
    self._add_to_layer_dict(layer_id, "Deck.gl")

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]

Mapbox layer type ('circle', 'line', 'fill', 'symbol').

None
paint Optional[Dict]

Mapbox 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/mapbox.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: Mapbox layer type ('circle', 'line', 'fill', 'symbol').
        paint: Mapbox paint properties.
        name: Layer name.
        fit_bounds: Whether to fit map to data bounds.
        **kwargs: Additional layer options.
    """
    geojson = to_geojson(data)

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

    # Handle URL data - fetch GeoJSON to get bounds and infer layer type
    if geojson.get("type") == "url":
        url = geojson["url"]
        geojson = fetch_geojson(url)

    # 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 (use geojson dict, not original data which may be a URL)
    bounds = get_bounds(geojson) 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,
        },
    }
    self._add_to_layer_dict(layer_id, "Vector")

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.

Source code in anymap_ts/mapbox.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."""
    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_video_layer(self, urls, coordinates, name=None, opacity=1.0, **kwargs)

Add a georeferenced video overlay on the map.

Source code in anymap_ts/mapbox.py
def add_video_layer(
    self,
    urls: List[str],
    coordinates: List[List[float]],
    name: Optional[str] = None,
    opacity: float = 1.0,
    **kwargs,
) -> None:
    """Add a georeferenced video overlay on the map."""
    self._validate_opacity(opacity)
    layer_id = name or f"video-{len(self._layers)}"

    if len(coordinates) != 4:
        raise ValueError(
            "coordinates must have exactly 4 corner points "
            "[top-left, top-right, bottom-right, bottom-left]"
        )

    self.call_js_method(
        "addVideoLayer",
        id=layer_id,
        urls=urls,
        coordinates=coordinates,
        opacity=opacity,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        layer_id: {
            "id": layer_id,
            "type": "video",
            "source": f"{layer_id}-source",
        },
    }
    self._add_to_layer_dict(layer_id, "Raster")

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.

Source code in anymap_ts/mapbox.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."""
    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.

Source code in anymap_ts/mapbox.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."""
    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,
        },
    }
    self._add_to_layer_dict(layer_id, "Raster")

animate_along_route(self, route, duration=10000, loop=True, marker_color='#3388ff', marker_size=1.0, show_trail=False, trail_color='#3388ff', trail_width=3, animation_id=None, **kwargs)

Animate a marker along a route.

Source code in anymap_ts/mapbox.py
def animate_along_route(
    self,
    route: Any,
    duration: int = 10000,
    loop: bool = True,
    marker_color: str = "#3388ff",
    marker_size: float = 1.0,
    show_trail: bool = False,
    trail_color: str = "#3388ff",
    trail_width: float = 3,
    animation_id: Optional[str] = None,
    **kwargs,
) -> str:
    """Animate a marker along a route."""
    anim_id = animation_id or f"animation-{len(self._layers)}"

    if isinstance(route, list) and len(route) > 0:
        if isinstance(route[0], (list, tuple)):
            coordinates = route
        else:
            raise ValueError("Route list must contain coordinate pairs")
    elif isinstance(route, dict):
        if route.get("type") == "LineString":
            coordinates = route.get("coordinates", [])
        elif route.get("type") == "Feature":
            geometry = route.get("geometry", {})
            if geometry.get("type") == "LineString":
                coordinates = geometry.get("coordinates", [])
            else:
                raise ValueError("Feature geometry must be LineString")
        elif route.get("type") == "FeatureCollection":
            features = route.get("features", [])
            if (
                features
                and features[0].get("geometry", {}).get("type") == "LineString"
            ):
                coordinates = features[0]["geometry"]["coordinates"]
            else:
                raise ValueError(
                    "FeatureCollection must contain LineString features"
                )
        else:
            raise ValueError(
                "GeoJSON must be LineString, Feature, or FeatureCollection"
            )
    else:
        geojson = to_geojson(route)
        if geojson.get("type") == "url":
            geojson = fetch_geojson(geojson["url"])
        if geojson.get("type") == "FeatureCollection":
            features = geojson.get("features", [])
            if features:
                coordinates = features[0].get("geometry", {}).get("coordinates", [])
            else:
                raise ValueError("No features found in data")
        elif geojson.get("type") == "Feature":
            coordinates = geojson.get("geometry", {}).get("coordinates", [])
        else:
            coordinates = geojson.get("coordinates", [])

    if len(coordinates) < 2:
        raise ValueError("Route must have at least 2 points")

    self.call_js_method(
        "animateAlongRoute",
        id=anim_id,
        coordinates=coordinates,
        duration=duration,
        loop=loop,
        markerColor=marker_color,
        markerSize=marker_size,
        showTrail=show_trail,
        trailColor=trail_color,
        trailWidth=trail_width,
        **kwargs,
    )

    self._layers = {
        **self._layers,
        anim_id: {
            "id": anim_id,
            "type": "animation",
        },
    }
    return anim_id

clear_draw_data(self)

Clear all drawn features.

Source code in anymap_ts/mapbox.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.

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

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

get_layer(self, layer_id)

Get layer configuration by ID.

Source code in anymap_ts/mapbox.py
def get_layer(self, layer_id: str) -> Optional[Dict]:
    """Get layer configuration by ID."""
    return self._layers.get(layer_id)

get_layer_ids(self)

Get list of all layer IDs.

Source code in anymap_ts/mapbox.py
def get_layer_ids(self) -> List[str]:
    """Get list of all layer IDs."""
    return list(self._layers.keys())

get_visible_features(self, layers=None)

Get all features currently visible in the viewport.

Source code in anymap_ts/mapbox.py
def get_visible_features(
    self,
    layers: Optional[List[str]] = None,
) -> Optional[Dict]:
    """Get all features currently visible in the viewport."""
    if layers is not None:
        self.call_js_method("getVisibleFeatures", layers=layers)
    features = self._queried_features
    if features and "data" in features:
        return features["data"]
    return None

load_draw_data(self, geojson)

Load GeoJSON features into the drawing layer.

Source code in anymap_ts/mapbox.py
def load_draw_data(self, geojson: Dict) -> None:
    """Load GeoJSON features into the drawing layer."""
    self._draw_data = geojson
    self.call_js_method("loadDrawData", geojson)

move_layer(self, layer_id, before_id=None)

Move a layer in the layer stack.

Source code in anymap_ts/mapbox.py
def move_layer(self, layer_id: str, before_id: Optional[str] = None) -> None:
    """Move a layer in the layer stack."""
    self.call_js_method("moveLayer", layer_id, before_id)

pause_animation(self, animation_id)

Pause a running animation.

Source code in anymap_ts/mapbox.py
def pause_animation(self, animation_id: str) -> None:
    """Pause a running animation."""
    self.call_js_method("pauseAnimation", animation_id)

pause_video(self, name)

Pause a video layer.

Source code in anymap_ts/mapbox.py
def pause_video(self, name: str) -> None:
    """Pause a video layer."""
    self.call_js_method("pauseVideo", id=name)

play_video(self, name)

Start playing a video layer.

Source code in anymap_ts/mapbox.py
def play_video(self, name: str) -> None:
    """Start playing a video layer."""
    self.call_js_method("playVideo", id=name)

query_rendered_features(self, geometry=None, layers=None, filter_expression=None)

Query features currently rendered on the map.

Source code in anymap_ts/mapbox.py
def query_rendered_features(
    self,
    geometry: Optional[Any] = None,
    layers: Optional[List[str]] = None,
    filter_expression: Optional[List] = None,
) -> Dict:
    """Query features currently rendered on the map."""
    kwargs: Dict[str, Any] = {}
    if geometry is not None:
        kwargs["geometry"] = geometry
    if layers is not None:
        kwargs["layers"] = layers
    if filter_expression is not None:
        kwargs["filter"] = filter_expression

    self.call_js_method("queryRenderedFeatures", **kwargs)
    return self._queried_features

query_source_features(self, source_id, source_layer=None, filter_expression=None)

Query features from a source.

Source code in anymap_ts/mapbox.py
def query_source_features(
    self,
    source_id: str,
    source_layer: Optional[str] = None,
    filter_expression: Optional[List] = None,
) -> Dict:
    """Query features from a source."""
    kwargs: Dict[str, Any] = {"sourceId": source_id}
    if source_layer is not None:
        kwargs["sourceLayer"] = source_layer
    if filter_expression is not None:
        kwargs["filter"] = filter_expression

    self.call_js_method("querySourceFeatures", **kwargs)
    return self._queried_features

remove_arc_layer(self, layer_id)

Remove an arc layer.

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

remove_cluster_layer(self, layer_id)

Remove a cluster layer.

Source code in anymap_ts/mapbox.py
def remove_cluster_layer(self, layer_id: str) -> None:
    """Remove a cluster layer."""
    self._remove_layer_internal(layer_id, "removeClusterLayer")

remove_cog_layer(self, layer_id)

Remove a COG layer.

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

remove_colorbar(self, colorbar_id=None)

Remove a colorbar from the map.

Source code in anymap_ts/mapbox.py
def remove_colorbar(self, colorbar_id: Optional[str] = None) -> None:
    """Remove a colorbar from the map."""
    if colorbar_id is None:
        cbar_keys = [k for k in self._controls.keys() if k.startswith("colorbar")]
        for key in cbar_keys:
            self.call_js_method("removeColorbar", colorbarId=key)
        self._controls = {
            k: v for k, v in self._controls.items() if not k.startswith("colorbar")
        }
    else:
        self.call_js_method("removeColorbar", colorbarId=colorbar_id)
        if colorbar_id in self._controls:
            controls = dict(self._controls)
            del controls[colorbar_id]
            self._controls = controls

remove_control(self, control_type)

Remove a map control.

Source code in anymap_ts/mapbox.py
def remove_control(self, control_type: str) -> None:
    """Remove a map control."""
    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_coordinates_control(self)

Remove the coordinates display control.

Source code in anymap_ts/mapbox.py
def remove_coordinates_control(self) -> None:
    """Remove the coordinates display control."""
    self.call_js_method("removeCoordinatesControl")

remove_deck_layer(self, layer_id)

Remove a deck.gl layer from the map.

Source code in anymap_ts/mapbox.py
def remove_deck_layer(self, layer_id: str) -> None:
    """Remove a deck.gl layer from the map."""
    self._remove_layer_internal(layer_id, "removeDeckLayer")

remove_flatgeobuf(self, name)

Remove a FlatGeobuf layer from the map.

Source code in anymap_ts/mapbox.py
def remove_flatgeobuf(self, name: str) -> None:
    """Remove a FlatGeobuf layer from the map."""
    if name in self._layers:
        layers = dict(self._layers)
        del layers[name]
        self._layers = layers
    self._remove_from_layer_dict(name)
    self.call_js_method("removeFlatGeobuf", name=name)

remove_fog(self)

Remove fog atmospheric effects from the map.

Source code in anymap_ts/mapbox.py
def remove_fog(self) -> None:
    """Remove fog atmospheric effects from the map."""
    self.call_js_method("removeFog")

remove_layer(self, layer_id)

Remove a layer from the map.

Source code in anymap_ts/mapbox.py
def remove_layer(self, layer_id: str) -> None:
    """Remove a layer from the map."""
    if layer_id in self._layers:
        layers = dict(self._layers)
        del layers[layer_id]
        self._layers = layers
    self._remove_from_layer_dict(layer_id)
    self.call_js_method("removeLayer", layer_id)

remove_legend(self, legend_id=None)

Remove a legend control from the map.

Source code in anymap_ts/mapbox.py
def remove_legend(self, legend_id: Optional[str] = None) -> None:
    """Remove a legend control from the map."""
    if legend_id is None:
        legend_keys = [k for k in self._controls.keys() if k.startswith("legend")]
        for key in legend_keys:
            self.call_js_method("removeLegend", key)
        self._controls = {
            k: v for k, v in self._controls.items() if not k.startswith("legend")
        }
    else:
        self.call_js_method("removeLegend", legend_id)
        if legend_id in self._controls:
            controls = dict(self._controls)
            del controls[legend_id]
            self._controls = controls

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/mapbox.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_marker(self, marker_id)

Remove a marker from the map.

Source code in anymap_ts/mapbox.py
def remove_marker(self, marker_id: str) -> None:
    """Remove a marker from the map."""
    self._remove_layer_internal(marker_id, "removeMarker")

remove_measure_control(self)

Remove the measurement control.

Source code in anymap_ts/mapbox.py
def remove_measure_control(self) -> None:
    """Remove the measurement control."""
    self.call_js_method("removeMeasureControl")
    if "measure-control" in self._controls:
        controls = dict(self._controls)
        del controls["measure-control"]
        self._controls = controls

remove_opacity_slider(self, layer_id)

Remove the opacity slider for a layer.

Source code in anymap_ts/mapbox.py
def remove_opacity_slider(self, layer_id: str) -> None:
    """Remove the opacity slider for a layer."""
    self.call_js_method("removeOpacitySlider", layerId=layer_id)

remove_pmtiles_layer(self, layer_id)

Remove a PMTiles layer.

Source code in anymap_ts/mapbox.py
def remove_pmtiles_layer(self, layer_id: str) -> None:
    """Remove a PMTiles layer."""
    self._remove_layer_internal(layer_id, "removePMTilesLayer")
    self._pmtiles_styles.pop(layer_id, None)

remove_point_cloud_layer(self, layer_id)

Remove a point cloud layer.

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

remove_print_control(self)

Remove the print/export control.

Source code in anymap_ts/mapbox.py
def remove_print_control(self) -> None:
    """Remove the print/export control."""
    self.call_js_method("removePrintControl")
    if "print-control" in self._controls:
        controls = dict(self._controls)
        del controls["print-control"]
        self._controls = controls

remove_search_control(self)

Remove the search/geocoder control.

Source code in anymap_ts/mapbox.py
def remove_search_control(self) -> None:
    """Remove the search/geocoder control."""
    self.call_js_method("removeSearchControl")
    if "search-control" in self._controls:
        controls = dict(self._controls)
        del controls["search-control"]
        self._controls = controls

remove_split_map(self)

Remove the split map comparison view.

Source code in anymap_ts/mapbox.py
def remove_split_map(self) -> None:
    """Remove the split map comparison view."""
    self.call_js_method("removeSplitMap")

remove_style_switcher(self)

Remove the style switcher control.

Source code in anymap_ts/mapbox.py
def remove_style_switcher(self) -> None:
    """Remove the style switcher control."""
    self.call_js_method("removeStyleSwitcher")

remove_swipe_map(self)

Remove the swipe map comparison control.

Source code in anymap_ts/mapbox.py
def remove_swipe_map(self) -> None:
    """Remove the swipe map comparison control."""
    self.call_js_method("removeSwipeMap")

remove_terrain(self)

Remove 3D terrain from the map.

Source code in anymap_ts/mapbox.py
def remove_terrain(self) -> None:
    """Remove 3D terrain from the map."""
    self.call_js_method("removeTerrain")

remove_time_slider(self)

Remove the time slider control.

Source code in anymap_ts/mapbox.py
def remove_time_slider(self) -> None:
    """Remove the time slider control."""
    self.call_js_method("removeTimeSlider")

remove_tooltip(self, layer_id)

Remove tooltip from a layer.

Source code in anymap_ts/mapbox.py
def remove_tooltip(self, layer_id: str) -> None:
    """Remove tooltip from a layer."""
    self.call_js_method("removeTooltip", layerId=layer_id)

remove_video_layer(self, name)

Remove a video layer from the map.

Source code in anymap_ts/mapbox.py
def remove_video_layer(self, name: str) -> None:
    """Remove a video layer from the map."""
    if name in self._layers:
        layers = dict(self._layers)
        del layers[name]
        self._layers = layers
    self._remove_from_layer_dict(name)
    self.call_js_method("removeVideoLayer", id=name)

remove_zarr_layer(self, layer_id)

Remove a Zarr layer.

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

resume_animation(self, animation_id)

Resume a paused animation.

Source code in anymap_ts/mapbox.py
def resume_animation(self, animation_id: str) -> None:
    """Resume a paused animation."""
    self.call_js_method("resumeAnimation", animation_id)

save_draw_data(self, filepath, driver=None)

Save drawn features to a file.

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

    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)

seek_video(self, name, time)

Seek to a specific time in a video layer.

Source code in anymap_ts/mapbox.py
def seek_video(self, name: str, time: float) -> None:
    """Seek to a specific time in a video layer."""
    self.call_js_method("seekVideo", id=name, time=time)

set_access_token(self, token)

Set the Mapbox access token.

Parameters:

Name Type Description Default
token str

Mapbox access token.

required
Source code in anymap_ts/mapbox.py
def set_access_token(self, token: str) -> None:
    """Set the Mapbox access token.

    Args:
        token: Mapbox access token.
    """
    self.access_token = token

set_animation_speed(self, animation_id, speed)

Set animation speed multiplier.

Source code in anymap_ts/mapbox.py
def set_animation_speed(self, animation_id: str, speed: float) -> None:
    """Set animation speed multiplier."""
    self.call_js_method("setAnimationSpeed", animation_id, speed)

set_filter(self, layer_id, filter_expression=None)

Set or clear a filter on a map layer.

Source code in anymap_ts/mapbox.py
def set_filter(
    self,
    layer_id: str,
    filter_expression: Optional[List] = None,
) -> None:
    """Set or clear a filter on a map layer."""
    self.call_js_method(
        "setFilter",
        layerId=layer_id,
        filter=filter_expression,
    )

set_fog(self, color=None, high_color=None, low_color=None, horizon_blend=None, range=None, **kwargs)

Set fog atmospheric effect (Mapbox uses map.setFog() API).

Source code in anymap_ts/mapbox.py
def set_fog(
    self,
    color: Optional[str] = None,
    high_color: Optional[str] = None,
    low_color: Optional[str] = None,
    horizon_blend: Optional[float] = None,
    range: Optional[List[float]] = None,
    **kwargs,
) -> None:
    """Set fog atmospheric effect (Mapbox uses map.setFog() API)."""
    self.call_js_method(
        "setFog",
        color=color,
        highColor=high_color,
        lowColor=low_color,
        horizonBlend=horizon_blend,
        range=range,
        **kwargs,
    )

set_layout_property(self, layer_id, property_name, value)

Set a layout property for a layer.

Source code in anymap_ts/mapbox.py
def set_layout_property(
    self, layer_id: str, property_name: str, value: Any
) -> None:
    """Set a layout property for a layer."""
    self.call_js_method("setLayoutProperty", layer_id, property_name, value)

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

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

set_paint_property(self, layer_id, property_name, value)

Set a paint property for a layer.

Source code in anymap_ts/mapbox.py
def set_paint_property(self, layer_id: str, property_name: str, value: Any) -> None:
    """Set a paint property for a layer."""
    self.call_js_method("setPaintProperty", layer_id, property_name, value)

set_projection(self, projection='mercator')

Set the map projection (Mapbox supports 'globe' and 'mercator').

Source code in anymap_ts/mapbox.py
def set_projection(self, projection: str = "mercator") -> None:
    """Set the map projection (Mapbox supports 'globe' and 'mercator')."""
    self.call_js_method("setProjection", projection=projection)

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/mapbox.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)

stop_animation(self, animation_id)

Stop a running animation.

Source code in anymap_ts/mapbox.py
def stop_animation(self, animation_id: str) -> None:
    """Stop a running animation."""
    self.call_js_method("stopAnimation", animation_id)
    if animation_id in self._layers:
        layers = dict(self._layers)
        del layers[animation_id]
        self._layers = layers

to_geojson(self, layer_id=None)

Get layer data as GeoJSON.

Source code in anymap_ts/mapbox.py
def to_geojson(self, layer_id: Optional[str] = None) -> Optional[Dict]:
    """Get layer data as GeoJSON."""
    if layer_id:
        self.call_js_method("getLayerData", sourceId=layer_id)
    features = self._queried_features
    if features and "data" in features:
        return features["data"]
    return None

to_geopandas(self, layer_id=None)

Get layer data as a GeoDataFrame.

Source code in anymap_ts/mapbox.py
def to_geopandas(self, layer_id: Optional[str] = None) -> Any:
    """Get layer data as a GeoDataFrame."""
    geojson = self.to_geojson(layer_id)
    if geojson is None:
        return None
    try:
        import geopandas as gpd

        return gpd.GeoDataFrame.from_features(geojson.get("features", []))
    except ImportError:
        raise ImportError("geopandas is required for to_geopandas()")

update_colorbar(self, colorbar_id=None, **kwargs)

Update an existing colorbar's properties.

Source code in anymap_ts/mapbox.py
def update_colorbar(self, colorbar_id: Optional[str] = None, **kwargs) -> None:
    """Update an existing colorbar's properties."""
    if colorbar_id is None:
        cbar_keys = [k for k in self._controls.keys() if k.startswith("colorbar")]
        if not cbar_keys:
            raise ValueError("No colorbar found to update")
        colorbar_id = cbar_keys[0]

    if colorbar_id not in self._controls:
        raise ValueError(f"Colorbar '{colorbar_id}' not found")

    js_kwargs: Dict[str, Any] = {"colorbarId": colorbar_id}
    key_map = {"bar_thickness": "barThickness", "bar_length": "barLength"}
    for key, value in kwargs.items():
        js_key = key_map.get(key, key)
        js_kwargs[js_key] = value

    self.call_js_method("updateColorbar", **js_kwargs)

update_geojson_source(self, source_id, data)

Update the data of an existing GeoJSON source in place.

Source code in anymap_ts/mapbox.py
def update_geojson_source(self, source_id: str, data: Any) -> None:
    """Update the data of an existing GeoJSON source in place."""
    processed_data = self._process_deck_data(data)
    self.call_js_method(
        "updateGeoJSONSource",
        sourceId=source_id,
        data=processed_data,
    )

update_legend(self, legend_id, title=None, labels=None, colors=None, opacity=None, **kwargs)

Update an existing legend's properties.

Source code in anymap_ts/mapbox.py
def update_legend(
    self,
    legend_id: str,
    title: Optional[str] = None,
    labels: Optional[List[str]] = None,
    colors: Optional[List[str]] = None,
    opacity: Optional[float] = None,
    **kwargs,
) -> None:
    """Update an existing legend's properties."""
    if legend_id not in self._controls:
        raise ValueError(f"Legend '{legend_id}' not found")

    update_params = {"id": legend_id}

    if title is not None:
        update_params["title"] = title
        self._controls[legend_id]["title"] = title

    if labels is not None and colors is not None:
        if len(labels) != len(colors):
            raise ValueError("Number of labels must match number of colors")

        legend_items = [
            {"label": label, "color": color} for label, color in zip(labels, colors)
        ]
        update_params["items"] = legend_items
        self._controls[legend_id]["labels"] = labels
        self._controls[legend_id]["colors"] = colors

    elif labels is not None or colors is not None:
        raise ValueError("Both labels and colors must be provided together")

    if opacity is not None:
        update_params["opacity"] = opacity
        self._controls[legend_id]["opacity"] = opacity

    update_params.update(kwargs)
    self.call_js_method("updateLegend", **update_params)

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

Update a Zarr layer's properties dynamically.

Source code in anymap_ts/mapbox.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."""
    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)

get_mapbox_token()

Get Mapbox access token from environment variable.

Returns:

Type Description
str

Mapbox access token string, or empty string if not set.

Source code in anymap_ts/mapbox.py
def get_mapbox_token() -> str:
    """Get Mapbox access token from environment variable.

    Returns:
        Mapbox access token string, or empty string if not set.
    """
    return os.environ.get("MAPBOX_TOKEN", "")