## maps.py
"""
Tools for generating interactive maps with folium and ipyleaflet.
License: The code in this notebook is licensed under the Apache License,
Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0). Digital Earth
Australia data is licensed under the Creative Commons by Attribution 4.0
license (https://creativecommons.org/licenses/by/4.0/).
Contact: If you need assistance, please post a question on the Open Data
Cube Discord chat (https://discord.com/invite/4hhBQVas5U) or on the GIS Stack
Exchange (https://gis.stackexchange.com/questions/ask?tags=open-data-cube)
using the `open-data-cube` tag (you can view previously asked questions
here: https://gis.stackexchange.com/questions/tagged/open-data-cube).
If you would like to report an issue with this script, you can file one on
GitHub (https://github.com/GeoscienceAustralia/dea-notebooks/issues/new).
Last modified: July 2025
"""
import folium
import folium.plugins
import numpy as np
# Attempt to import datacube and raise an error if not available
try:
from datacube_ows.styles.api import apply_ows_style_cfg, xarray_image_as_png
from odc.ui import mk_data_uri, zoom_from_bbox
from odc.ui._images import xr_bounds
except ImportError as e:
raise ImportError(
"`datacube_ows` and `odc.ui` are required to use this module. "
"Please install DEA Tools with the `[datacube]` extra, e.g.: "
"`pip install dea-tools[datacube]`"
) from e
# ipyleaflet is listed as an optional dependency for the dea-tools package
# so instead of trying to import it here, we do that as needed
# default fallback OWS style configuration
RGB_CFG = {
"components": {
"red": {"red": 1.0},
"green": {"green": 1.0},
"blue": {"blue": 1.0},
},
"scale_range": (50, 3000),
}
def center_of_bbox(bbox):
return (bbox.bottom + bbox.top) * 0.5, (bbox.right + bbox.left) * 0.5
[docs]
def folium_map_default(bbox, zoom_start=None, location=None, **kwargs):
"""
Sensible defaults for a folium map based on the bounding box of the image to be shown.
"""
if zoom_start is None:
zoom_start = zoom_from_bbox(bbox)
if location is None:
location = center_of_bbox(bbox)
kwargs["zoom_start"] = zoom_start
kwargs["location"] = location
return folium.Map(**kwargs)
[docs]
def folium_dualmap_default(bbox, zoom_start=None, location=None, **kwargs):
"""
Sensible defaults for a folium dual map based on the bounding box of the image to be shown.
"""
if zoom_start is None:
zoom_start = zoom_from_bbox(bbox)
if location is None:
location = center_of_bbox(bbox)
kwargs["zoom_start"] = zoom_start
kwargs["location"] = location
return folium.plugins.DualMap(**kwargs)
[docs]
def ipyleaflet_map_default(bbox, zoom=None, center=None, **kwargs):
"""
Sensible defaults for a ipyleaflet map based on the bounding box of the image to be shown.
"""
import ipyleaflet
if zoom is None:
zoom = zoom_from_bbox(bbox)
if center is None:
center = center_of_bbox(bbox)
kwargs["zoom"] = zoom
kwargs["center"] = center
return ipyleaflet.Map(**kwargs)
[docs]
def valid_data_mask(data):
"""
Calculate valid data mask array for xarray dataset.
"""
def mask_array(data_array):
# adopted from odc.algo._rgba.to_rgba_np
nodata = data_array.attrs.get("nodata")
if data_array.dtype.kind == "f":
valid = ~np.isnan(data_array)
if nodata is not None:
valid = valid & (data_array != nodata)
elif nodata is not None:
valid = data_array != nodata
else:
valid = np.ones(data_array.shape, dtype=bool)
return valid
var_names = list(data.data_vars)
if var_names == []:
raise ValueError("no data given")
first, *rest = var_names
mask = mask_array(data.data_vars[first])
for other in rest:
mask = mask & mask_array(data.data_vars[other])
return mask
[docs]
def apply_ows_style(data, ows_style_config=None):
"""
Convert xarray dataset to a PNG image by applying the OWS style.
"""
# inspired by odc.ui.mk_image_overlay
# and https://datacube-ows.readthedocs.io/en/latest/styling_howto.html
# Get rid of the time dimension
if "time" in data.dims:
assert data.time.shape[0] == 1, "multiple observations not supported yet"
data = data.isel(time=0)
if ows_style_config is None:
ows_style_config = RGB_CFG
mask = valid_data_mask(data)
xr_image = apply_ows_style_cfg(ows_style_config, data, valid_data_mask=mask)
return xarray_image_as_png(xr_image)
def folium_image_overlay(data, ows_style_config=None, name=None):
png_str = mk_data_uri(apply_ows_style(data, ows_style_config=ows_style_config), "image/png")
return folium.raster_layers.ImageOverlay(png_str, bounds=xr_bounds(data), name=name)
def ipyleaflet_image_overlay(data, ows_style_config=None, layer_name="Image"):
import ipyleaflet
png_str = mk_data_uri(apply_ows_style(data, ows_style_config=ows_style_config), "image/png")
return ipyleaflet.ImageOverlay(url=png_str, bounds=xr_bounds(data), layer_name=layer_name)
def folium_add_controls(fm, enable_fullscreen=True, enable_layers_control=False):
if enable_fullscreen:
folium.plugins.Fullscreen(position="topright", title="Fullscreen", title_cancel="Exit fullscreen").add_to(fm)
if enable_layers_control:
folium.LayerControl().add_to(fm)
def ipyleaflet_add_controls(im, enable_fullscreen=True, enable_layers_control=False):
import ipyleaflet
if enable_fullscreen:
im.add_control(ipyleaflet.FullScreenControl())
if enable_layers_control:
im.add_control(ipyleaflet.LayersControl())
def bounding_box(data):
return data.extent.to_crs("EPSG:4326").boundingbox
[docs]
def folium_map(
data,
ows_style_config=None,
enable_fullscreen=True,
enable_layers_control=False,
zoom_start=None,
location=None,
**folium_map_kwargs,
):
"""
Puts an xarray Dataset with a single observation in time
on to a `folium` map (see: https://python-visualization.github.io/folium/).
Parameters
----------
data : xarray Dataset
A dataset with a single observation in time (or without a time dimension)
ows_style_config : dict
Datacube OWS style configuration (see https://datacube-ows.readthedocs.io/en/latest/styling_howto.html)
enable_fullscreen : bool
Enable a Full Screen control on the map
enable_layers_control : bool
Enable a Layers control (that lists the layers to show/hide them)
zoom_start : int
The zoom level (default: a zoom layer that shows the whole dataset)
location : (float, float)
The location the starting view is centered on (default: center of the dataset bounds)
Returns
-------
the newly created `folium` map
"""
fm = folium_map_default(bounding_box(data), zoom_start=zoom_start, location=location, **folium_map_kwargs)
folium_image_overlay(data, ows_style_config=ows_style_config).add_to(fm)
folium_add_controls(fm, enable_fullscreen=enable_fullscreen, enable_layers_control=enable_layers_control)
return fm
[docs]
def folium_dual_map(
left_data,
right_data,
left_ows_style=None,
right_ows_style=None,
enable_fullscreen=False,
enable_layers_control=False,
zoom_start=None,
location=None,
**folium_map_kwargs,
):
"""
Puts two xarray datasets side-by-side for comparison
on to a `folium` map (see: https://python-visualization.github.io/folium/).
Parameters
----------
data : xarray Dataset
A dataset with a single observation in time (or without a time dimension)
ows_style_config : dict
Datacube OWS style configuration (see https://datacube-ows.readthedocs.io/en/latest/styling_howto.html)
enable_fullscreen : bool
Enable a Full Screen control on the map
enable_layers_control : bool
Enable a Layers control (that lists the layers to show/hide them)
zoom_start : int
The zoom level (default: a zoom layer that shows the whole dataset)
location : (float, float)
The location the starting view is centered on (default: center of the dataset bounds)
Returns
-------
the newly created `folium` map
"""
fm = folium_dualmap_default(bounding_box(left_data), zoom_start=zoom_start, location=location, **folium_map_kwargs)
left_layer = folium_image_overlay(left_data, ows_style_config=left_ows_style, name="left")
right_layer = folium_image_overlay(right_data, ows_style_config=right_ows_style, name="right")
left_layer.add_to(fm.m1)
right_layer.add_to(fm.m2)
folium_add_controls(fm, enable_fullscreen=enable_fullscreen, enable_layers_control=enable_layers_control)
return fm
[docs]
def ipyleaflet_map(
data,
ows_style_config=None,
enable_fullscreen=True,
enable_layers_control=False,
zoom=None,
center=None,
**ipyleaflet_map_kwargs,
):
"""
Puts two xarray datasets side-by-side for comparison
on to a `ipyleaflet` map.
Parameters
----------
data : xarray Dataset
A dataset with a single observation in time (or without a time dimension)
ows_style_config : dict
Datacube OWS style configuration (see https://datacube-ows.readthedocs.io/en/latest/styling_howto.html)
enable_fullscreen : bool
Enable a Full Screen control on the map
enable_layers_control : bool
Enable a Layers control (that lists the layers to show/hide them)
zoom_start : int
The zoom level (default: a zoom layer that shows the whole dataset)
location : (float, float)
The location the starting view is centered on (default: center of the dataset bounds)
Returns
-------
the newly created `ipyleaflet` map
"""
import ipyleaflet # noqa
im = ipyleaflet_map_default(bounding_box(data), zoom=zoom, center=center, **ipyleaflet_map_kwargs)
layer = ipyleaflet_image_overlay(data, ows_style_config=ows_style_config)
im.add_layer(layer)
ipyleaflet_add_controls(im, enable_fullscreen=enable_fullscreen, enable_layers_control=enable_layers_control)
return im