Skip to content

core module

A generic Map interface and lightweight implementation.

AbstractDrawControl

Abstract class for the draw control.

Source code in geemap/core.py
class AbstractDrawControl(object):
    """Abstract class for the draw control."""

    host_map = None
    layer = None
    geometries = []
    properties = []
    last_geometry = None
    last_draw_action = None
    _geometry_create_dispatcher = ipywidgets.CallbackDispatcher()
    _geometry_edit_dispatcher = ipywidgets.CallbackDispatcher()
    _geometry_delete_dispatcher = ipywidgets.CallbackDispatcher()

    def __init__(self, host_map):
        """Initialize the draw control.

        Args:
            host_map (geemap.Map): The geemap.Map instance to be linked with
                the draw control.
        """

        self.host_map = host_map
        self.layer = None
        self.geometries = []
        self.properties = []
        self.last_geometry = None
        self.last_draw_action = None
        self._geometry_create_dispatcher = ipywidgets.CallbackDispatcher()
        self._geometry_edit_dispatcher = ipywidgets.CallbackDispatcher()
        self._geometry_delete_dispatcher = ipywidgets.CallbackDispatcher()
        self._bind_to_draw_control()

    @property
    def features(self):
        if self.count:
            features = []
            for i, geometry in enumerate(self.geometries):
                if i < len(self.properties):
                    property = self.properties[i]
                else:
                    property = None
                features.append(ee.Feature(geometry, property))
            return features
        else:
            return []

    @property
    def collection(self):
        return ee.FeatureCollection(self.features if self.count else [])

    @property
    def last_feature(self):
        property = self.get_geometry_properties(self.last_geometry)
        return ee.Feature(self.last_geometry, property) if self.last_geometry else None

    @property
    def count(self):
        return len(self.geometries)

    def reset(self, clear_draw_control=True):
        """Resets the draw controls."""
        if self.layer is not None:
            self.host_map.remove_layer(self.layer)
        self.geometries = []
        self.properties = []
        self.last_geometry = None
        self.layer = None
        if clear_draw_control:
            self._clear_draw_control()

    def remove_geometry(self, geometry):
        """Removes a geometry from the draw control."""
        if not geometry:
            return
        try:
            index = self.geometries.index(geometry)
        except ValueError:
            return
        if index >= 0:
            del self.geometries[index]
            del self.properties[index]
            self._remove_geometry_at_index_on_draw_control(index)
            if index == self.count and geometry == self.last_geometry:
                # Treat this like an "undo" of the last drawn geometry.
                if len(self.geometries):
                    self.last_geometry = self.geometries[-1]
                else:
                    self.last_geometry = geometry
                self.last_draw_action = DrawActions.REMOVED_LAST
            if self.layer is not None:
                self._redraw_layer()

    def get_geometry_properties(self, geometry):
        """Gets the properties of a geometry."""
        if not geometry:
            return None
        try:
            index = self.geometries.index(geometry)
        except ValueError:
            return None
        if index >= 0:
            return self.properties[index]
        else:
            return None

    def set_geometry_properties(self, geometry, property):
        """Sets the properties of a geometry."""
        if not geometry:
            return
        try:
            index = self.geometries.index(geometry)
        except ValueError:
            return
        if index >= 0:
            self.properties[index] = property

    def on_geometry_create(self, callback, remove=False):
        self._geometry_create_dispatcher.register_callback(callback, remove=remove)

    def on_geometry_edit(self, callback, remove=False):
        self._geometry_edit_dispatcher.register_callback(callback, remove=remove)

    def on_geometry_delete(self, callback, remove=False):
        self._geometry_delete_dispatcher.register_callback(callback, remove=remove)

    def _bind_to_draw_control(self):
        """Set up draw control event handling like create, edit, and delete."""
        raise NotImplementedError()

    def _remove_geometry_at_index_on_draw_control(self):
        """Remove the geometry at the given index on the draw control."""
        raise NotImplementedError()

    def _clear_draw_control(self):
        """Clears the geometries from the draw control."""
        raise NotImplementedError()

    def _get_synced_geojson_from_draw_control(self):
        """Returns an up-to-date list of GeoJSON from the draw control."""
        raise NotImplementedError()

    def _sync_geometries(self):
        """Sync the local geometries with those from the draw control."""
        if not self.count:
            return
        # The current geometries from the draw_control.
        test_geojsons = self._get_synced_geojson_from_draw_control()
        self.geometries = [
            common.geojson_to_ee(geo_json, geodesic=False) for geo_json in test_geojsons
        ]

    def _redraw_layer(self):
        if not self.host_map:
            return
        # If the layer already exists, substitute it. This can avoid flickering.
        if _DRAWN_FEATURES_LAYER in self.host_map.ee_layers:
            old_layer = self.host_map.ee_layers.get(_DRAWN_FEATURES_LAYER, {})[
                "ee_layer"
            ]
            new_layer = ee_tile_layers.EELeafletTileLayer(
                self.collection,
                {"color": "blue"},
                _DRAWN_FEATURES_LAYER,
                old_layer.visible,
                0.5,
            )
            self.host_map.substitute(old_layer, new_layer)
            self.layer = self.host_map.ee_layers.get(_DRAWN_FEATURES_LAYER, {}).get(
                "ee_layer", None
            )
            self.host_map.ee_layers.get(_DRAWN_FEATURES_LAYER, {})[
                "ee_layer"
            ] = new_layer
        else:  # Otherwise, add the layer.
            self.host_map.add_layer(
                self.collection,
                {"color": "blue"},
                _DRAWN_FEATURES_LAYER,
                False,
                0.5,
            )
            self.layer = self.host_map.ee_layers.get(_DRAWN_FEATURES_LAYER, {}).get(
                "ee_layer", None
            )

    def _handle_geometry_created(self, geo_json):
        geometry = common.geojson_to_ee(geo_json, geodesic=False)
        self.last_geometry = geometry
        self.last_draw_action = DrawActions.CREATED
        self.geometries.append(geometry)
        self.properties.append(None)
        self._redraw_layer()
        self._geometry_create_dispatcher(self, geometry=geometry)

    def _handle_geometry_edited(self, geo_json):
        geometry = common.geojson_to_ee(geo_json, geodesic=False)
        self.last_geometry = geometry
        self.last_draw_action = DrawActions.EDITED
        self._sync_geometries()
        self._geometry_edit_dispatcher(self, geometry=geometry)

    def _handle_geometry_deleted(self, geo_json):
        geometry = common.geojson_to_ee(geo_json, geodesic=False)
        self.last_geometry = geometry
        self.last_draw_action = DrawActions.DELETED
        try:
            index = self.geometries.index(geometry)
        except ValueError:
            return
        if index >= 0:
            del self.geometries[index]
            del self.properties[index]
            if self.count:
                self._redraw_layer()
            elif _DRAWN_FEATURES_LAYER in self.host_map.ee_layers:
                # Remove drawn features layer if there are no geometries.
                self.host_map.remove_layer(_DRAWN_FEATURES_LAYER)
            self._geometry_delete_dispatcher(self, geometry=geometry)

__init__(self, host_map) special

Initialize the draw control.

Parameters:

Name Type Description Default
host_map geemap.Map

The geemap.Map instance to be linked with the draw control.

required
Source code in geemap/core.py
def __init__(self, host_map):
    """Initialize the draw control.

    Args:
        host_map (geemap.Map): The geemap.Map instance to be linked with
            the draw control.
    """

    self.host_map = host_map
    self.layer = None
    self.geometries = []
    self.properties = []
    self.last_geometry = None
    self.last_draw_action = None
    self._geometry_create_dispatcher = ipywidgets.CallbackDispatcher()
    self._geometry_edit_dispatcher = ipywidgets.CallbackDispatcher()
    self._geometry_delete_dispatcher = ipywidgets.CallbackDispatcher()
    self._bind_to_draw_control()

get_geometry_properties(self, geometry)

Gets the properties of a geometry.

Source code in geemap/core.py
def get_geometry_properties(self, geometry):
    """Gets the properties of a geometry."""
    if not geometry:
        return None
    try:
        index = self.geometries.index(geometry)
    except ValueError:
        return None
    if index >= 0:
        return self.properties[index]
    else:
        return None

remove_geometry(self, geometry)

Removes a geometry from the draw control.

Source code in geemap/core.py
def remove_geometry(self, geometry):
    """Removes a geometry from the draw control."""
    if not geometry:
        return
    try:
        index = self.geometries.index(geometry)
    except ValueError:
        return
    if index >= 0:
        del self.geometries[index]
        del self.properties[index]
        self._remove_geometry_at_index_on_draw_control(index)
        if index == self.count and geometry == self.last_geometry:
            # Treat this like an "undo" of the last drawn geometry.
            if len(self.geometries):
                self.last_geometry = self.geometries[-1]
            else:
                self.last_geometry = geometry
            self.last_draw_action = DrawActions.REMOVED_LAST
        if self.layer is not None:
            self._redraw_layer()

reset(self, clear_draw_control=True)

Resets the draw controls.

Source code in geemap/core.py
def reset(self, clear_draw_control=True):
    """Resets the draw controls."""
    if self.layer is not None:
        self.host_map.remove_layer(self.layer)
    self.geometries = []
    self.properties = []
    self.last_geometry = None
    self.layer = None
    if clear_draw_control:
        self._clear_draw_control()

set_geometry_properties(self, geometry, property)

Sets the properties of a geometry.

Source code in geemap/core.py
def set_geometry_properties(self, geometry, property):
    """Sets the properties of a geometry."""
    if not geometry:
        return
    try:
        index = self.geometries.index(geometry)
    except ValueError:
        return
    if index >= 0:
        self.properties[index] = property

DrawActions (Enum)

Action types for the draw control.

Parameters:

Name Type Description Default
enum str

Action type.

required
Source code in geemap/core.py
class DrawActions(enum.Enum):
    """Action types for the draw control.

    Args:
        enum (str): Action type.
    """

    CREATED = "created"
    EDITED = "edited"
    DELETED = "deleted"
    REMOVED_LAST = "removed-last"

Map (Map, MapInterface)

The Map class inherits the ipyleaflet Map class.

Parameters:

Name Type Description Default
center list

Center of the map (lat, lon). Defaults to [0, 0].

required
zoom int

Zoom level of the map. Defaults to 2.

required
height str

Height of the map. Defaults to "600px".

required
width str

Width of the map. Defaults to "100%".

required

Returns:

Type Description
ipyleaflet

ipyleaflet map object.

Source code in geemap/core.py
class Map(ipyleaflet.Map, MapInterface):
    """The Map class inherits the ipyleaflet Map class.

    Args:
        center (list, optional): Center of the map (lat, lon). Defaults to [0, 0].
        zoom (int, optional): Zoom level of the map. Defaults to 2.
        height (str, optional): Height of the map. Defaults to "600px".
        width (str, optional): Width of the map. Defaults to "100%".

    Returns:
        ipyleaflet: ipyleaflet map object.
    """

    _KWARG_DEFAULTS: Dict[str, Any] = {
        "center": [0, 0],
        "zoom": 2,
        "zoom_control": False,
        "attribution_control": False,
        "ee_initialize": True,
        "scroll_wheel_zoom": True,
    }

    _BASEMAP_ALIASES: Dict[str, List[str]] = {
        "DEFAULT": ["Google.Roadmap", "OpenStreetMap.Mapnik"],
        "ROADMAP": ["Google.Roadmap", "Esri.WorldStreetMap"],
        "SATELLITE": ["Google.Satellite", "Esri.WorldImagery"],
        "TERRAIN": ["Google.Terrain", "Esri.WorldTopoMap"],
        "HYBRID": ["Google.Hybrid", "Esri.WorldImagery"],
    }

    _USER_AGENT_PREFIX = "geemap-core"

    @property
    def width(self) -> str:
        return self.layout.width

    @width.setter
    def width(self, value: str) -> None:
        self.layout.width = value

    @property
    def height(self) -> str:
        return self.layout.height

    @height.setter
    def height(self, value: str) -> None:
        self.layout.height = value

    @property
    def _toolbar(self) -> Optional[toolbar.Toolbar]:
        return self._find_widget_of_type(toolbar.Toolbar)

    @property
    def _inspector(self) -> Optional[map_widgets.Inspector]:
        return self._find_widget_of_type(map_widgets.Inspector)

    @property
    def _draw_control(self) -> MapDrawControl:
        return self._find_widget_of_type(MapDrawControl)

    @property
    def _layer_manager(self) -> Optional[map_widgets.LayerManager]:
        if toolbar_widget := self._toolbar:
            if isinstance(toolbar_widget.accessory_widget, map_widgets.LayerManager):
                return toolbar_widget.accessory_widget
        return self._find_widget_of_type(map_widgets.LayerManager)

    @property
    def _layer_editor(self) -> Optional[map_widgets.LayerEditor]:
        return self._find_widget_of_type(map_widgets.LayerEditor)

    @property
    def _basemap_selector(self) -> Optional[map_widgets.Basemap]:
        return self._find_widget_of_type(map_widgets.Basemap)

    def __init__(self, **kwargs):
        self._available_basemaps = self._get_available_basemaps()

        # Use the first basemap in the list of available basemaps.
        if "basemap" not in kwargs:
            kwargs["basemap"] = next(iter(self._available_basemaps.values()))
        elif "basemap" in kwargs and isinstance(kwargs["basemap"], str):
            if kwargs["basemap"] in self._available_basemaps:
                kwargs["basemap"] = self._available_basemaps.get(kwargs["basemap"])

        if "width" in kwargs:
            self.width: str = kwargs.pop("width", "100%")
        self.height: str = kwargs.pop("height", "600px")

        self.ee_layers: Dict[str, Dict[str, Any]] = {}
        self.geojson_layers: List[Any] = []

        kwargs = self._apply_kwarg_defaults(kwargs)
        super().__init__(**kwargs)

        for position, widgets in self._control_config().items():
            for widget in widgets:
                self.add(widget, position=position)

        # Authenticate and initialize EE.
        if kwargs.get("ee_initialize", True):
            common.ee_initialize(user_agent_prefix=self._USER_AGENT_PREFIX)

        # Listen for layers being added/removed so we can update the layer manager.
        self.observe(self._on_layers_change, "layers")

    def get_zoom(self) -> int:
        return self.zoom

    def set_zoom(self, value: int) -> None:
        self.zoom = value

    def get_center(self) -> Sequence:
        return self.center

    def get_bounds(self, as_geojson: bool = False) -> Sequence:
        """Returns the bounds of the current map view.

        Args:
            as_geojson (bool, optional): If true, returns map bounds as
                GeoJSON. Defaults to False.

        Returns:
            list|dict: A list in the format [west, south, east, north] in
                degrees or a GeoJSON dictionary.
        """
        bounds = self.bounds
        if not bounds:
            raise RuntimeError(
                "Map bounds are undefined. Please display the " "map then try again."
            )
        # ipyleaflet returns bounds in the format [[south, west], [north, east]]
        # https://ipyleaflet.readthedocs.io/en/latest/map_and_basemaps/map.html#ipyleaflet.Map.fit_bounds
        coords = [bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]]

        if as_geojson:
            return ee.Geometry.BBox(*coords).getInfo()
        return coords

    def get_scale(self) -> float:
        # Reference:
        # - https://blogs.bing.com/maps/2006/02/25/map-control-zoom-levels-gt-resolution
        # - https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
        center_lat = self.center[0]
        center_lat_cos = math.cos(math.radians(center_lat))
        return 156543.04 * center_lat_cos / math.pow(2, self.zoom)

    def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
        self.center = (lat, lon)
        if zoom is not None:
            self.zoom = zoom

    def _get_geometry(
        self, ee_object: ee.ComputedObject, max_error: float
    ) -> ee.Geometry:
        """Returns the geometry for an arbitrary EE object."""
        if isinstance(ee_object, ee.Geometry):
            return ee_object
        try:
            return ee_object.geometry(maxError=max_error)
        except Exception as exc:
            raise Exception(
                "ee_object must be one of ee.Geometry, ee.FeatureCollection, ee.Image, or ee.ImageCollection."
            ) from exc

    def center_object(
        self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
    ) -> None:
        max_error = 0.001
        geometry = self._get_geometry(ee_object, max_error).transform(
            maxError=max_error
        )
        if zoom is None:
            coordinates = geometry.bounds(max_error).getInfo()["coordinates"][0]
            x_vals = [c[0] for c in coordinates]
            y_vals = [c[1] for c in coordinates]
            self.fit_bounds([[min(y_vals), min(x_vals)], [max(y_vals), max(x_vals)]])
        else:
            if not isinstance(zoom, int):
                raise ValueError("Zoom must be an integer.")
            centroid = geometry.centroid(maxError=max_error).getInfo()["coordinates"]
            self.set_center(centroid[0], centroid[1], zoom)

    def _find_widget_of_type(
        self, widget_type: Type, return_control: bool = False
    ) -> Optional[Any]:
        """Finds a widget in the controls with the passed in type."""
        for widget in self.controls:
            if isinstance(widget, ipyleaflet.WidgetControl):
                if isinstance(widget.widget, widget_type):
                    return widget if return_control else widget.widget
            elif isinstance(widget, widget_type):
                return widget
        return None

    def add(self, obj: Any, position: str = "", **kwargs) -> None:
        if not position:
            for default_position, widgets in self._control_config().items():
                if obj in widgets:
                    position = default_position
            if not position:
                position = "topright"

        # Basic controls:
        #   - can only be added to the map once,
        #   - have a constructor that takes a position arg, and
        #   - don't need to be stored as instance vars.
        basic_controls: Dict[str, Tuple[ipyleaflet.Control, Dict[str, Any]]] = {
            "zoom_control": (ipyleaflet.ZoomControl, {}),
            "fullscreen_control": (ipyleaflet.FullScreenControl, {}),
            "scale_control": (ipyleaflet.ScaleControl, {"metric": True}),
            "attribution_control": (ipyleaflet.AttributionControl, {}),
        }
        if obj in basic_controls:
            basic_control = basic_controls[obj]
            # Check if widget is already on the map.
            if self._find_widget_of_type(basic_control[0]):
                return
            new_kwargs = {**basic_control[1], **kwargs}
            super().add(basic_control[0](position=position, **new_kwargs))
        elif obj == "toolbar":
            self._add_toolbar(position, **kwargs)
        elif obj == "inspector":
            self._add_inspector(position, **kwargs)
        elif obj == "layer_manager":
            self._add_layer_manager(position, **kwargs)
        elif obj == "layer_editor":
            self._add_layer_editor(position, **kwargs)
        elif obj == "draw_control":
            self._add_draw_control(position, **kwargs)
        elif obj == "basemap_selector":
            self._add_basemap_selector(position, **kwargs)
        else:
            super().add(obj)

    def _on_toggle_toolbar_layers(self, is_open: bool) -> None:
        if is_open:
            if self._layer_manager:
                return

            def _on_open_vis(layer_name: str) -> None:
                layer = self.ee_layers.get(layer_name, None)
                self._add_layer_editor(position="bottomright", layer_dict=layer)

            layer_manager = map_widgets.LayerManager(self)
            layer_manager.header_hidden = True
            layer_manager.close_button_hidden = True
            layer_manager.on_open_vis = _on_open_vis
            self._toolbar.accessory_widget = layer_manager
        else:
            self._toolbar.accessory_widget = None
            self.remove("layer_manager")

    def _add_layer_manager(self, position: str, **kwargs) -> None:
        if self._layer_manager:
            return

        def _on_open_vis(layer_name: str) -> None:
            layer = self.ee_layers.get(layer_name, None)
            self._add_layer_editor(position="bottomright", layer_dict=layer)

        layer_manager = map_widgets.LayerManager(self, **kwargs)
        layer_manager.on_close = lambda: self.remove("layer_manager")
        layer_manager.on_open_vis = _on_open_vis
        layer_manager_control = ipyleaflet.WidgetControl(
            widget=layer_manager, position=position
        )
        super().add(layer_manager_control)

    def _add_toolbar(self, position: str, **kwargs) -> None:
        if self._toolbar:
            return

        toolbar_val = toolbar.Toolbar(
            self, self._toolbar_main_tools(), self._toolbar_extra_tools(), **kwargs
        )
        toolbar_val.on_layers_toggled = self._on_toggle_toolbar_layers
        toolbar_control = ipyleaflet.WidgetControl(
            widget=toolbar_val, position=position
        )
        super().add(toolbar_control)
        # Enable the layer manager by default.
        toolbar_val.toggle_layers(True)

    def _add_inspector(self, position: str, **kwargs) -> None:
        if self._inspector:
            return

        inspector = map_widgets.Inspector(self, **kwargs)
        inspector.on_close = lambda: self.remove("inspector")
        inspector_control = ipyleaflet.WidgetControl(
            widget=inspector, position=position
        )
        super().add(inspector_control)

    def _add_layer_editor(self, position: str, **kwargs) -> None:
        if self._layer_editor:
            return

        widget = map_widgets.LayerEditor(self, **kwargs)
        widget.on_close = lambda: self.remove("layer_editor")
        control = ipyleaflet.WidgetControl(widget=widget, position=position)
        super().add(control)

    def _add_draw_control(self, position="topleft", **kwargs) -> None:
        """Add a draw control to the map

        Args:
            position (str, optional): The position of the draw control. Defaults to "topleft".
        """
        if self._draw_control:
            return
        default_args = dict(
            marker={"shapeOptions": {"color": "#3388ff"}},
            rectangle={"shapeOptions": {"color": "#3388ff"}},
            circlemarker={},
            edit=True,
            remove=True,
        )
        control = MapDrawControl(
            host_map=self,
            position=position,
            **{**default_args, **kwargs},
        )
        super().add(control)

    def get_draw_control(self) -> Optional[MapDrawControl]:
        return self._draw_control

    def _add_basemap_selector(self, position: str, **kwargs) -> None:
        if self._basemap_selector:
            return

        basemap_names = kwargs.pop("basemaps", list(self._available_basemaps.keys()))
        value = kwargs.pop(
            "value", self._get_preferred_basemap_name(self.layers[0].name)
        )
        basemap = map_widgets.Basemap(basemap_names, value, **kwargs)
        basemap.on_close = lambda: self.remove("basemap_selector")
        basemap.on_basemap_changed = self._replace_basemap
        basemap_control = ipyleaflet.WidgetControl(widget=basemap, position=position)
        super().add(basemap_control)

    def remove(self, widget: Any) -> None:
        """Removes a widget to the map."""

        basic_controls: Dict[str, ipyleaflet.Control] = {
            "zoom_control": ipyleaflet.ZoomControl,
            "fullscreen_control": ipyleaflet.FullScreenControl,
            "scale_control": ipyleaflet.ScaleControl,
            "attribution_control": ipyleaflet.AttributionControl,
            "toolbar": toolbar.Toolbar,
            "inspector": map_widgets.Inspector,
            "layer_manager": map_widgets.LayerManager,
            "layer_editor": map_widgets.LayerEditor,
            "draw_control": MapDrawControl,
            "basemap_selector": map_widgets.Basemap,
        }
        if widget_type := basic_controls.get(widget, None):
            if control := self._find_widget_of_type(widget_type, return_control=True):
                self.remove(control)
                control.close()
            return

        if hasattr(widget, "name") and widget.name in self.ee_layers:
            self.ee_layers.pop(widget.name)

        if ee_layer := self.ee_layers.pop(widget, None):
            tile_layer = ee_layer.get("ee_layer", None)
            if tile_layer is not None:
                self.remove_layer(tile_layer)
            if legend := ee_layer.get("legend", None):
                self.remove(legend)
            if colorbar := ee_layer.get("colorbar", None):
                self.remove(colorbar)
            return

        super().remove(widget)
        if isinstance(widget, ipywidgets.Widget):
            widget.close()

    def add_layer(
        self,
        ee_object: ee.ComputedObject,
        vis_params: Dict[str, Any] = None,
        name: Optional[str] = None,
        shown: bool = True,
        opacity: float = 1.0,
    ) -> None:
        """Adds a layer to the map."""

        # Call super if not an EE object.
        if not isinstance(ee_object, ee_tile_layers.EELeafletTileLayer.EE_TYPES):
            super().add_layer(ee_object)
            return

        if vis_params is None:
            vis_params = {}
        if name is None:
            name = f"Layer {len(self.ee_layers) + 1}"

        if isinstance(ee_object, ee.ImageCollection):
            ee_object = ee_object.mosaic()
        tile_layer = ee_tile_layers.EELeafletTileLayer(
            ee_object, vis_params, name, shown, opacity
        )

        # Remove the layer if it already exists.
        self.remove(name)

        self.ee_layers[name] = {
            "ee_object": ee_object,
            "ee_layer": tile_layer,
            "vis_params": vis_params,
        }
        super().add(tile_layer)

    def _open_help_page(
        self, host_map: MapInterface, selected: bool, item: toolbar.Toolbar.Item
    ) -> None:
        del host_map, item  # Unused.
        if selected:
            common.open_url("https://geemap.org")

    def _toolbar_main_tools(self) -> List[toolbar.Toolbar.Item]:
        @toolbar._cleanup_toolbar_item
        def inspector_tool_callback(
            map: Map, selected: bool, item: toolbar.Toolbar.Item
        ):
            del selected, item  # Unused.
            map.add("inspector")
            return map._inspector

        @toolbar._cleanup_toolbar_item
        def basemap_tool_callback(map: Map, selected: bool, item: toolbar.Toolbar.Item):
            del selected, item  # Unused.
            map.add("basemap_selector")
            return map._basemap_selector

        return [
            toolbar.Toolbar.Item(
                icon="map",
                tooltip="Basemap selector",
                callback=basemap_tool_callback,
                reset=False,
            ),
            toolbar.Toolbar.Item(
                icon="info",
                tooltip="Inspector",
                callback=inspector_tool_callback,
                reset=False,
            ),
            toolbar.Toolbar.Item(
                icon="question", tooltip="Get help", callback=self._open_help_page
            ),
        ]

    def _toolbar_extra_tools(self) -> Optional[List[toolbar.Toolbar.Item]]:
        return None

    def _control_config(self) -> Dict[str, List[str]]:
        return {
            "topleft": ["zoom_control", "fullscreen_control", "draw_control"],
            "bottomleft": ["scale_control", "measure_control"],
            "topright": ["toolbar"],
            "bottomright": ["attribution_control"],
        }

    def _apply_kwarg_defaults(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
        ret_kwargs = {}
        for kwarg, default in self._KWARG_DEFAULTS.items():
            ret_kwargs[kwarg] = kwargs.pop(kwarg, default)
        ret_kwargs.update(kwargs)
        return ret_kwargs

    def _replace_basemap(self, basemap_name: str) -> None:
        basemap = self._available_basemaps.get(basemap_name, None)
        if basemap is None:
            logging.warning("Invalid basemap selected: %s", basemap_name)
            return
        new_layer = ipyleaflet.TileLayer(
            url=basemap["url"],
            name=basemap["name"],
            max_zoom=basemap.get("max_zoom", 24),
            attribution=basemap.get("attribution", None),
        )
        # substitute_layer is broken when the map has a single layer.
        if len(self.layers) == 1:
            self.clear_layers()
            self.add_layer(new_layer)
        else:
            self.substitute_layer(self.layers[0], new_layer)

    def _get_available_basemaps(self) -> Dict[str, Any]:
        """Convert xyz tile services to a dictionary of basemaps."""
        tile_providers = list(basemaps.get_xyz_dict().values())
        if common.get_google_maps_api_key():
            tile_providers = tile_providers + list(
                basemaps.get_google_map_tile_providers().values()
            )

        ret_dict = {}
        for tile_info in tile_providers:
            tile_info["url"] = tile_info.build_url()
            ret_dict[tile_info["name"]] = tile_info

        # Each alias needs to point to a single map. For each alias, pick the
        # first aliased map in `self._BASEMAP_ALIASES`.
        aliased_maps = {}
        for alias, maps in self._BASEMAP_ALIASES.items():
            for map_name in maps:
                if provider := ret_dict.get(map_name):
                    aliased_maps[alias] = provider
                    break
        return {**aliased_maps, **ret_dict}

    def _get_preferred_basemap_name(self, basemap_name: str) -> str:
        """Returns the aliased basemap name."""
        reverse_aliases = {}
        for alias, maps in self._BASEMAP_ALIASES.items():
            for map_name in maps:
                if map_name not in reverse_aliases:
                    reverse_aliases[map_name] = alias
        return reverse_aliases.get(basemap_name, basemap_name)

    def _on_layers_change(self, change) -> None:
        del change  # Unused.
        if self._layer_manager:
            self._layer_manager.refresh_layers()

    # Keep the following three camelCase methods for backwards compatibility.
    addLayer = add_layer
    centerObject = center_object
    setCenter = set_center
    getBounds = get_bounds

height: str property writable

Returns the current height of the map.

width: str property writable

Returns the current width of the map.

add(self, obj, position='', **kwargs)

Add an item on the map: either a layer or a control.

Parameters

Layer or Control instance

The layer or control to add.

int

The index to insert a Layer. If not specified, the layer is added to the end (on top).

Source code in geemap/core.py
def add(self, obj: Any, position: str = "", **kwargs) -> None:
    if not position:
        for default_position, widgets in self._control_config().items():
            if obj in widgets:
                position = default_position
        if not position:
            position = "topright"

    # Basic controls:
    #   - can only be added to the map once,
    #   - have a constructor that takes a position arg, and
    #   - don't need to be stored as instance vars.
    basic_controls: Dict[str, Tuple[ipyleaflet.Control, Dict[str, Any]]] = {
        "zoom_control": (ipyleaflet.ZoomControl, {}),
        "fullscreen_control": (ipyleaflet.FullScreenControl, {}),
        "scale_control": (ipyleaflet.ScaleControl, {"metric": True}),
        "attribution_control": (ipyleaflet.AttributionControl, {}),
    }
    if obj in basic_controls:
        basic_control = basic_controls[obj]
        # Check if widget is already on the map.
        if self._find_widget_of_type(basic_control[0]):
            return
        new_kwargs = {**basic_control[1], **kwargs}
        super().add(basic_control[0](position=position, **new_kwargs))
    elif obj == "toolbar":
        self._add_toolbar(position, **kwargs)
    elif obj == "inspector":
        self._add_inspector(position, **kwargs)
    elif obj == "layer_manager":
        self._add_layer_manager(position, **kwargs)
    elif obj == "layer_editor":
        self._add_layer_editor(position, **kwargs)
    elif obj == "draw_control":
        self._add_draw_control(position, **kwargs)
    elif obj == "basemap_selector":
        self._add_basemap_selector(position, **kwargs)
    else:
        super().add(obj)

addLayer(self, ee_object, vis_params=None, name=None, shown=True, opacity=1.0)

Adds a layer to the map.

Source code in geemap/core.py
def add_layer(
    self,
    ee_object: ee.ComputedObject,
    vis_params: Dict[str, Any] = None,
    name: Optional[str] = None,
    shown: bool = True,
    opacity: float = 1.0,
) -> None:
    """Adds a layer to the map."""

    # Call super if not an EE object.
    if not isinstance(ee_object, ee_tile_layers.EELeafletTileLayer.EE_TYPES):
        super().add_layer(ee_object)
        return

    if vis_params is None:
        vis_params = {}
    if name is None:
        name = f"Layer {len(self.ee_layers) + 1}"

    if isinstance(ee_object, ee.ImageCollection):
        ee_object = ee_object.mosaic()
    tile_layer = ee_tile_layers.EELeafletTileLayer(
        ee_object, vis_params, name, shown, opacity
    )

    # Remove the layer if it already exists.
    self.remove(name)

    self.ee_layers[name] = {
        "ee_object": ee_object,
        "ee_layer": tile_layer,
        "vis_params": vis_params,
    }
    super().add(tile_layer)

add_layer(self, ee_object, vis_params=None, name=None, shown=True, opacity=1.0)

Adds a layer to the map.

Source code in geemap/core.py
def add_layer(
    self,
    ee_object: ee.ComputedObject,
    vis_params: Dict[str, Any] = None,
    name: Optional[str] = None,
    shown: bool = True,
    opacity: float = 1.0,
) -> None:
    """Adds a layer to the map."""

    # Call super if not an EE object.
    if not isinstance(ee_object, ee_tile_layers.EELeafletTileLayer.EE_TYPES):
        super().add_layer(ee_object)
        return

    if vis_params is None:
        vis_params = {}
    if name is None:
        name = f"Layer {len(self.ee_layers) + 1}"

    if isinstance(ee_object, ee.ImageCollection):
        ee_object = ee_object.mosaic()
    tile_layer = ee_tile_layers.EELeafletTileLayer(
        ee_object, vis_params, name, shown, opacity
    )

    # Remove the layer if it already exists.
    self.remove(name)

    self.ee_layers[name] = {
        "ee_object": ee_object,
        "ee_layer": tile_layer,
        "vis_params": vis_params,
    }
    super().add(tile_layer)

centerObject(self, ee_object, zoom=None)

Centers the map view on a given object.

Source code in geemap/core.py
def center_object(
    self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
) -> None:
    max_error = 0.001
    geometry = self._get_geometry(ee_object, max_error).transform(
        maxError=max_error
    )
    if zoom is None:
        coordinates = geometry.bounds(max_error).getInfo()["coordinates"][0]
        x_vals = [c[0] for c in coordinates]
        y_vals = [c[1] for c in coordinates]
        self.fit_bounds([[min(y_vals), min(x_vals)], [max(y_vals), max(x_vals)]])
    else:
        if not isinstance(zoom, int):
            raise ValueError("Zoom must be an integer.")
        centroid = geometry.centroid(maxError=max_error).getInfo()["coordinates"]
        self.set_center(centroid[0], centroid[1], zoom)

center_object(self, ee_object, zoom=None)

Centers the map view on a given object.

Source code in geemap/core.py
def center_object(
    self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
) -> None:
    max_error = 0.001
    geometry = self._get_geometry(ee_object, max_error).transform(
        maxError=max_error
    )
    if zoom is None:
        coordinates = geometry.bounds(max_error).getInfo()["coordinates"][0]
        x_vals = [c[0] for c in coordinates]
        y_vals = [c[1] for c in coordinates]
        self.fit_bounds([[min(y_vals), min(x_vals)], [max(y_vals), max(x_vals)]])
    else:
        if not isinstance(zoom, int):
            raise ValueError("Zoom must be an integer.")
        centroid = geometry.centroid(maxError=max_error).getInfo()["coordinates"]
        self.set_center(centroid[0], centroid[1], zoom)

getBounds(self, as_geojson=False)

Returns the bounds of the current map view.

Parameters:

Name Type Description Default
as_geojson bool

If true, returns map bounds as GeoJSON. Defaults to False.

False

Returns:

Type Description
list|dict

A list in the format [west, south, east, north] in degrees or a GeoJSON dictionary.

Source code in geemap/core.py
def get_bounds(self, as_geojson: bool = False) -> Sequence:
    """Returns the bounds of the current map view.

    Args:
        as_geojson (bool, optional): If true, returns map bounds as
            GeoJSON. Defaults to False.

    Returns:
        list|dict: A list in the format [west, south, east, north] in
            degrees or a GeoJSON dictionary.
    """
    bounds = self.bounds
    if not bounds:
        raise RuntimeError(
            "Map bounds are undefined. Please display the " "map then try again."
        )
    # ipyleaflet returns bounds in the format [[south, west], [north, east]]
    # https://ipyleaflet.readthedocs.io/en/latest/map_and_basemaps/map.html#ipyleaflet.Map.fit_bounds
    coords = [bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]]

    if as_geojson:
        return ee.Geometry.BBox(*coords).getInfo()
    return coords

get_bounds(self, as_geojson=False)

Returns the bounds of the current map view.

Parameters:

Name Type Description Default
as_geojson bool

If true, returns map bounds as GeoJSON. Defaults to False.

False

Returns:

Type Description
list|dict

A list in the format [west, south, east, north] in degrees or a GeoJSON dictionary.

Source code in geemap/core.py
def get_bounds(self, as_geojson: bool = False) -> Sequence:
    """Returns the bounds of the current map view.

    Args:
        as_geojson (bool, optional): If true, returns map bounds as
            GeoJSON. Defaults to False.

    Returns:
        list|dict: A list in the format [west, south, east, north] in
            degrees or a GeoJSON dictionary.
    """
    bounds = self.bounds
    if not bounds:
        raise RuntimeError(
            "Map bounds are undefined. Please display the " "map then try again."
        )
    # ipyleaflet returns bounds in the format [[south, west], [north, east]]
    # https://ipyleaflet.readthedocs.io/en/latest/map_and_basemaps/map.html#ipyleaflet.Map.fit_bounds
    coords = [bounds[0][1], bounds[0][0], bounds[1][1], bounds[1][0]]

    if as_geojson:
        return ee.Geometry.BBox(*coords).getInfo()
    return coords

get_center(self)

Returns the current center of the map (lat, lon).

Source code in geemap/core.py
def get_center(self) -> Sequence:
    return self.center

get_scale(self)

Returns the approximate pixel scale of the current map view, in meters.

Source code in geemap/core.py
def get_scale(self) -> float:
    # Reference:
    # - https://blogs.bing.com/maps/2006/02/25/map-control-zoom-levels-gt-resolution
    # - https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Resolution_and_Scale
    center_lat = self.center[0]
    center_lat_cos = math.cos(math.radians(center_lat))
    return 156543.04 * center_lat_cos / math.pow(2, self.zoom)

get_zoom(self)

Returns the current zoom level of the map.

Source code in geemap/core.py
def get_zoom(self) -> int:
    return self.zoom

remove(self, widget)

Removes a widget to the map.

Source code in geemap/core.py
def remove(self, widget: Any) -> None:
    """Removes a widget to the map."""

    basic_controls: Dict[str, ipyleaflet.Control] = {
        "zoom_control": ipyleaflet.ZoomControl,
        "fullscreen_control": ipyleaflet.FullScreenControl,
        "scale_control": ipyleaflet.ScaleControl,
        "attribution_control": ipyleaflet.AttributionControl,
        "toolbar": toolbar.Toolbar,
        "inspector": map_widgets.Inspector,
        "layer_manager": map_widgets.LayerManager,
        "layer_editor": map_widgets.LayerEditor,
        "draw_control": MapDrawControl,
        "basemap_selector": map_widgets.Basemap,
    }
    if widget_type := basic_controls.get(widget, None):
        if control := self._find_widget_of_type(widget_type, return_control=True):
            self.remove(control)
            control.close()
        return

    if hasattr(widget, "name") and widget.name in self.ee_layers:
        self.ee_layers.pop(widget.name)

    if ee_layer := self.ee_layers.pop(widget, None):
        tile_layer = ee_layer.get("ee_layer", None)
        if tile_layer is not None:
            self.remove_layer(tile_layer)
        if legend := ee_layer.get("legend", None):
            self.remove(legend)
        if colorbar := ee_layer.get("colorbar", None):
            self.remove(colorbar)
        return

    super().remove(widget)
    if isinstance(widget, ipywidgets.Widget):
        widget.close()

setCenter(self, lon, lat, zoom=None)

Centers the map view at a given coordinates with the given zoom level.

Source code in geemap/core.py
def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
    self.center = (lat, lon)
    if zoom is not None:
        self.zoom = zoom

set_center(self, lon, lat, zoom=None)

Centers the map view at a given coordinates with the given zoom level.

Source code in geemap/core.py
def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
    self.center = (lat, lon)
    if zoom is not None:
        self.zoom = zoom

set_zoom(self, value)

Sets the current zoom level of the map.

Source code in geemap/core.py
def set_zoom(self, value: int) -> None:
    self.zoom = value

MapDrawControl (DrawControl, AbstractDrawControl)

Implements the AbstractDrawControl for ipleaflet Map.

Source code in geemap/core.py
class MapDrawControl(ipyleaflet.DrawControl, AbstractDrawControl):
    """Implements the AbstractDrawControl for ipleaflet Map."""

    def __init__(self, host_map, **kwargs):
        """Initialize the map draw control.

        Args:
            host_map (geemap.Map): The geemap.Map object that the control will be added to.
        """
        super(MapDrawControl, self).__init__(host_map=host_map, **kwargs)

    def _get_synced_geojson_from_draw_control(self):
        return [data.copy() for data in self.data]

    def _bind_to_draw_control(self):
        # Handles draw events
        def handle_draw(_, action, geo_json):
            try:
                if action == "created":
                    self._handle_geometry_created(geo_json)
                elif action == "edited":
                    self._handle_geometry_edited(geo_json)
                elif action == "deleted":
                    self._handle_geometry_deleted(geo_json)
            except Exception as e:
                self.reset(clear_draw_control=False)
                print("There was an error creating Earth Engine Feature.")
                raise Exception(e)

        self.on_draw(handle_draw)

        def handle_data_update(_):
            self._sync_geometries()
            # Need to refresh the layer if the last action was an edit.
            if self.last_draw_action == DrawActions.EDITED:
                self._redraw_layer()

        self.observe(handle_data_update, "data")

    def _remove_geometry_at_index_on_draw_control(self, index):
        del self.data[index]
        self.send_state(key="data")

    def _clear_draw_control(self):
        self.data = []  # Remove all drawn features from the map.
        return self.clear()

__init__(self, host_map, **kwargs) special

Initialize the map draw control.

Parameters:

Name Type Description Default
host_map geemap.Map

The geemap.Map object that the control will be added to.

required
Source code in geemap/core.py
def __init__(self, host_map, **kwargs):
    """Initialize the map draw control.

    Args:
        host_map (geemap.Map): The geemap.Map object that the control will be added to.
    """
    super(MapDrawControl, self).__init__(host_map=host_map, **kwargs)

MapInterface

Interface for all maps.

Source code in geemap/core.py
class MapInterface:
    """Interface for all maps."""

    # The layers on the map.
    ee_layers: Dict[str, Dict[str, Any]]

    # The GeoJSON layers on the map.
    geojson_layers: List[Any]

    def get_zoom(self) -> int:
        """Returns the current zoom level of the map."""
        raise NotImplementedError()

    def set_zoom(self, value: int) -> None:
        """Sets the current zoom level of the map."""
        del value  # Unused.
        raise NotImplementedError()

    def get_center(self) -> Sequence:
        """Returns the current center of the map (lat, lon)."""
        raise NotImplementedError()

    def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
        """Centers the map view at a given coordinates with the given zoom level."""
        del lon, lat, zoom  # Unused.
        raise NotImplementedError()

    def center_object(
        self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
    ) -> None:
        """Centers the map view on a given object."""
        del ee_object, zoom  # Unused.
        raise NotImplementedError()

    def get_scale(self) -> float:
        """Returns the approximate pixel scale of the current map view, in meters."""
        raise NotImplementedError()

    def get_bounds(self) -> Tuple[float]:
        """Returns the bounds of the current map view.

        Returns:
            list: A list in the format [west, south, east, north] in degrees.
        """
        raise NotImplementedError()

    @property
    def width(self) -> str:
        """Returns the current width of the map."""
        raise NotImplementedError()

    @width.setter
    def width(self, value: str) -> None:
        """Sets the width of the map."""
        del value  # Unused.
        raise NotImplementedError()

    @property
    def height(self) -> str:
        """Returns the current height of the map."""
        raise NotImplementedError()

    @height.setter
    def height(self, value: str) -> None:
        """Sets the height of the map."""
        del value  # Unused.
        raise NotImplementedError()

    def add(self, widget: str, position: str, **kwargs) -> None:
        """Adds a widget to the map."""
        del widget, position, kwargs  # Unused.
        raise NotImplementedError()

    def remove(self, widget: str) -> None:
        """Removes a widget to the map."""
        del widget  # Unused.
        raise NotImplementedError()

    def add_layer(
        self,
        ee_object: ee.ComputedObject,
        vis_params: Optional[Dict[str, Any]] = None,
        name: Optional[str] = None,
        shown: bool = True,
        opacity: float = 1.0,
    ) -> None:
        """Adds a layer to the map."""
        del ee_object, vis_params, name, shown, opacity  # Unused.
        raise NotImplementedError()

height: str property writable

Returns the current height of the map.

width: str property writable

Returns the current width of the map.

add(self, widget, position, **kwargs)

Adds a widget to the map.

Source code in geemap/core.py
def add(self, widget: str, position: str, **kwargs) -> None:
    """Adds a widget to the map."""
    del widget, position, kwargs  # Unused.
    raise NotImplementedError()

add_layer(self, ee_object, vis_params=None, name=None, shown=True, opacity=1.0)

Adds a layer to the map.

Source code in geemap/core.py
def add_layer(
    self,
    ee_object: ee.ComputedObject,
    vis_params: Optional[Dict[str, Any]] = None,
    name: Optional[str] = None,
    shown: bool = True,
    opacity: float = 1.0,
) -> None:
    """Adds a layer to the map."""
    del ee_object, vis_params, name, shown, opacity  # Unused.
    raise NotImplementedError()

center_object(self, ee_object, zoom=None)

Centers the map view on a given object.

Source code in geemap/core.py
def center_object(
    self, ee_object: ee.ComputedObject, zoom: Optional[int] = None
) -> None:
    """Centers the map view on a given object."""
    del ee_object, zoom  # Unused.
    raise NotImplementedError()

get_bounds(self)

Returns the bounds of the current map view.

Returns:

Type Description
list

A list in the format [west, south, east, north] in degrees.

Source code in geemap/core.py
def get_bounds(self) -> Tuple[float]:
    """Returns the bounds of the current map view.

    Returns:
        list: A list in the format [west, south, east, north] in degrees.
    """
    raise NotImplementedError()

get_center(self)

Returns the current center of the map (lat, lon).

Source code in geemap/core.py
def get_center(self) -> Sequence:
    """Returns the current center of the map (lat, lon)."""
    raise NotImplementedError()

get_scale(self)

Returns the approximate pixel scale of the current map view, in meters.

Source code in geemap/core.py
def get_scale(self) -> float:
    """Returns the approximate pixel scale of the current map view, in meters."""
    raise NotImplementedError()

get_zoom(self)

Returns the current zoom level of the map.

Source code in geemap/core.py
def get_zoom(self) -> int:
    """Returns the current zoom level of the map."""
    raise NotImplementedError()

remove(self, widget)

Removes a widget to the map.

Source code in geemap/core.py
def remove(self, widget: str) -> None:
    """Removes a widget to the map."""
    del widget  # Unused.
    raise NotImplementedError()

set_center(self, lon, lat, zoom=None)

Centers the map view at a given coordinates with the given zoom level.

Source code in geemap/core.py
def set_center(self, lon: float, lat: float, zoom: Optional[int] = None) -> None:
    """Centers the map view at a given coordinates with the given zoom level."""
    del lon, lat, zoom  # Unused.
    raise NotImplementedError()

set_zoom(self, value)

Sets the current zoom level of the map.

Source code in geemap/core.py
def set_zoom(self, value: int) -> None:
    """Sets the current zoom level of the map."""
    del value  # Unused.
    raise NotImplementedError()