Scale, FullScreen, ImageOverlay
Three small controls / overlays that close the remaining dl1 gaps: ScaleControl, FullScreenControl, ImageOverlay.
Overview
Three thin wrappers around Leaflet 2 building blocks that dash-leaflet 1.x exposed and dl2 didn't yet:
| Component | Wraps |
|---|---|
ScaleControl | Control.Scale — metric / imperial scale bar in any corner. |
FullScreenControl | A small Control over the browser's requestFullscreen() API. Reports fullscreen and n_clicks back to Dash. |
ImageOverlay | ImageOverlay — drape one image onto a geographic bounding box. Two-way url, bounds, opacity, zIndex. With editable it gains a TextMarker-style transform control system. |
Editable ImageOverlay (resize · rotate · move)
Set editable=True and the overlay becomes a draggable, resizable, rotatable object — the same control language as dl2.TextMarker:
- Click the image to select it (chrome + handles appear).
- Drag the body to move it (translates
bounds). - Drag the corner dot to resize — the bounds scale about the
anchor, which stays pinned.
That dot is also the white anchor marker: it sits at whichever anchor you choose (center defaults to the bottom-right corner).
- Drag the top dot to rotate. Rotation is a CSS-transform visual rotation pivoting at the
anchor — Leaflet's ImageOverlay has no native geographic rotation, so bounds stay axis-aligned and only the rendered pixels turn.
bounds, rotation, and selected round-trip back to Dash (plus an n_transforms counter).
Live demo
A single map with all three pieces: scale bar bottom-left, fullscreen button top-left, and an editable image overlay — select it, then drag to move, resize from the corner/anchor dot, and rotate from the top dot. Change the anchor pivot and watch the white dot follow.
Source
# File: docs/scale-fullscreen-image/example.py
"""
ScaleControl, FullScreenControl, ImageOverlay — limited working example.
A single map with the scale bar bottom-left, the fullscreen button top-left, and
a sample raster ImageOverlay draped over a Honolulu, HI
bounding box. Right column tweaks each piece live.
"""
import dash_leaflet2 as dl2
import dash_mantine_components as dmc
from dash import Input, Output, callback
from dl2_tiles import NATGEO, register_theme_swap
from dl2_locations import HONOLULU
from dl2_shared import code_panel, header, info_panel
# Basemap pair for this page — dl2_tiles owns the light/dark wiring.
TILES = NATGEO
# A ~17 x 20 km box centred on the city. Expressed in kilometres rather than
# degrees so the overlay covers the same ground area at Honolulu's latitude as
# it would anywhere else — a fixed degree box would stretch east-west near the
# equator and squash near the poles.
OVERLAY_BOUNDS = HONOLULU.bounds(8.35, 9.83)
# Public sample image used by Leaflet docs.
SAMPLE_IMAGE = "https://leafletjs.com/examples/crs-simple/uqm_map_full.png"
SAMPLE_IMAGE_2 = "https://maps.lib.utexas.edu/maps/historical/texas_southern_1895.jpg"
ANCHORS = [
"top-left", "top", "top-right",
"left", "center", "right",
"bottom-left", "bottom", "bottom-right",
]
CODE = """dl2.Map(center=[21.3069, -157.8583], zoom=12, children=[
dl2.TileLayer(),
dl2.ScaleControl(position="bottomleft", metric=True, imperial=True),
dl2.FullScreenControl(position="topleft"),
# Editable: click to select, drag to move, corner dot resizes (about `anchor`),
# top dot rotates. bounds / rotation / selected round-trip back to Dash.
dl2.ImageOverlay(
id="img",
url="https://leafletjs.com/examples/crs-simple/uqm_map_full.png",
bounds=[[21.23, -157.95], [21.38, -157.76]],
opacity=0.85,
editable=True, selected=True, anchor="center", rotation=0,
),
])
@callback(Output("out", "children"),
Input("img", "bounds"), Input("img", "rotation"))
def show(bounds, rotation): ..."""
component = dmc.Stack(
[
header(
"Scale, FullScreen, ImageOverlay",
"Scale bar bottom-left (metric + imperial), fullscreen button top-left, and an "
"EDITABLE ImageOverlay: click it to select, then drag to move, drag the corner "
"dot (the anchor marker) to resize, and the top dot to rotate — bounds + rotation "
"round-trip back to Dash. The anchor dot follows the chosen anchor.",
badge="dl2.{Scale,FullScreen}Control · editable dl2.ImageOverlay",
),
dmc.Grid(
[
dmc.GridCol(
dmc.Paper(
dl2.Map(
id="sfi-map",
center=HONOLULU.center,
zoom=11,
style={"height": "60vh"},
children=[
dl2.TileLayer(id="sfi-tile", **TILES.kwargs("light")),
dl2.ScaleControl(
id="sfi-scale",
position="bottomleft",
metric=True,
imperial=True,
),
dl2.FullScreenControl(
id="sfi-fs",
position="topleft",
title="Enter full screen",
titleCancel="Leave full screen",
),
dl2.ImageOverlay(
id="sfi-image",
url=SAMPLE_IMAGE,
bounds=OVERLAY_BOUNDS,
opacity=0.85,
editable=True,
selected=True,
anchor="center",
rotation=0,
),
],
),
shadow="sm",
radius="md",
withBorder=True,
style={"overflow": "hidden", "height": "60vh"},
),
span=8,
),
dmc.GridCol(
dmc.Stack(
[
info_panel(
"Scale bar",
dmc.Stack(
[
dmc.SegmentedControl(
id="sfi-scale-pos",
data=[
{"label": "BL", "value": "bottomleft"},
{"label": "BR", "value": "bottomright"},
{"label": "TL", "value": "topleft"},
{"label": "TR", "value": "topright"},
],
value="bottomleft",
fullWidth=True,
),
dmc.Switch(id="sfi-scale-metric", checked=True, label="metric"),
dmc.Switch(id="sfi-scale-imperial", checked=True, label="imperial"),
],
gap="xs",
),
),
info_panel(
"Fullscreen",
dmc.Stack(
[
dmc.Badge(id="sfi-fs-state", color="gray", variant="light", children="windowed"),
dmc.Text(id="sfi-fs-clicks", size="xs", c="dimmed"),
],
gap="xs",
),
),
info_panel(
"ImageOverlay opacity",
dmc.Slider(
id="sfi-image-opacity",
min=0,
max=1,
step=0.05,
value=0.6,
),
),
info_panel(
"ImageOverlay source",
dmc.SegmentedControl(
id="sfi-image-url",
data=[
{"label": "UQM sample", "value": SAMPLE_IMAGE},
{"label": "1895 TX scan", "value": SAMPLE_IMAGE_2},
],
value=SAMPLE_IMAGE,
fullWidth=True,
),
),
info_panel(
"Transform (anchor pivot)",
dmc.Stack(
[
dmc.SegmentedControl(
id="sfi-anchor",
data=[{"label": a, "value": a} for a in ANCHORS],
value="center",
orientation="vertical",
fullWidth=True,
size="xs",
),
dmc.Text("Rotation", size="xs", c="dimmed"),
dmc.Slider(
id="sfi-rot", min=-180, max=180, value=0,
marks=[{"value": 0, "label": "0°"}],
),
dmc.Code(id="sfi-image-out", block=True, children="…"),
],
gap="xs",
),
),
],
gap="md",
),
span=4,
),
],
gutter="md",
),
code_panel("Three new pieces in one map", CODE),
],
gap="md",
)
@callback(Output("sfi-scale", "position"), Input("sfi-scale-pos", "value"))
def scale_pos(v):
return v or "bottomleft"
@callback(Output("sfi-scale", "metric"), Input("sfi-scale-metric", "checked"))
def scale_metric(checked):
return bool(checked)
@callback(Output("sfi-scale", "imperial"), Input("sfi-scale-imperial", "checked"))
def scale_imperial(checked):
return bool(checked)
@callback(Output("sfi-fs-state", "children"), Output("sfi-fs-state", "color"), Input("sfi-fs", "fullscreen"))
def fs_state(is_full):
if is_full:
return "fullscreen", "blue"
return "windowed", "gray"
@callback(Output("sfi-fs-clicks", "children"), Input("sfi-fs", "n_clicks"))
def fs_clicks(n):
return f"button clicks: {n or 0}"
@callback(Output("sfi-image", "opacity"), Input("sfi-image-opacity", "value"))
def image_opacity(v):
return float(v or 0)
@callback(Output("sfi-image", "url"), Input("sfi-image-url", "value"))
def image_url(v):
return v or SAMPLE_IMAGE
@callback(Output("sfi-image", "anchor"), Input("sfi-anchor", "value"))
def image_anchor(v):
return v or "center"
@callback(Output("sfi-image", "rotation"), Input("sfi-rot", "value"))
def image_rotation(v):
return int(v or 0)
@callback(
Output("sfi-image-out", "children"),
Input("sfi-image", "bounds"),
Input("sfi-image", "rotation"),
Input("sfi-image", "n_transforms"),
)
def image_readback(bounds, rotation, n):
b = bounds or OVERLAY_BOUNDS
return (
f"rotation: {rotation or 0}°\n"
f"bounds : [[{b[0][0]:.4f}, {b[0][1]:.4f}],\n"
f" [{b[1][0]:.4f}, {b[1][1]:.4f}]]\n"
f"n_transforms: {n or 0}"
)
# Light/dark basemap, driven off the color-scheme store.
register_theme_swap("sfi-tile", TILES)
:defaultExpanded: false :withExpandedButton: true
Source: /scale-fullscreen-image