Tile Selector
pick map tiles by clicking or shift-dragging, and round-trip them to Python.
Overview
dl2.TileSelector is a map control that turns the map into a tile picker. While it is armed the cursor becomes a crosshair, a dashed outline tracks the tile under the pointer at the current zoom, and:
- click a tile to add or remove it from the selection
- shift-drag a box to capture every tile inside it
Place it as a child of dl2.Map, anywhere among the other layers.
The data boundary
Each selected tile round-trips to Python as a dict:
{
"z": 11, "x": 470, "y": 843,
"url": "https://tile.openstreetmap.org/11/470/843.png",
"bounds": [south, west, north, east],
}
tileUrl decides which template those url values are built from — it does not have to match the dl2.TileLayer you are displaying. The demo below renders CARTO light tiles but hands back OpenStreetMap PNG URLs, which is the usual shape when the map is a picker for some other tileset.
Selections are keyed by z/x/y, so panning and zooming never lose them.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
selectedTiles | list of dicts | [] | The selection. [MUTABLE] — an output (user clicks) and an input (write [] to clear). |
tileUrl | string | OSM {z}/{x}/{y} | Template the returned url values are built from. |
position | string | 'topleft' | topleft / topright / bottomleft / bottomright. |
hoverColor | string | '#fa5252' | Colour of the dashed outline under the cursor. |
selectedColor | string | '#228be6' | Stroke and fill of selected-tile rectangles and the box-drag preview. |
Because selectedTiles is [MUTABLE], a plain callback both reads the user's picks and pushes new state back — the Clear button in the demo is one line.
Live demo
Source
# File: docs/tile-selector/example.py
"""
TileSelector — pick tiles off the map by clicking or shift-dragging.
`dl2.TileSelector` is a map control. While it is armed the cursor becomes a
crosshair, a dashed outline tracks the tile under the pointer at the current
zoom, and:
• **click** a tile to add / remove it from the selection
• **shift-drag** a box to capture every tile inside it
Each selected tile round-trips to Python as `{z, x, y, url, bounds}`, where
`bounds` is `[south, west, north, east]`. Selections are keyed by `z/x/y`, so
they survive panning and zooming.
`selectedTiles` is `[MUTABLE]` — it is both an output (the user's clicks) and
an input (the Clear button below writes `[]` straight back into it).
"""
import dash_leaflet2 as dl2
import dash_mantine_components as dmc
from dash import Input, Output, callback, html, no_update
from dash_iconify import DashIconify
from dl2_tiles import USGS_TOPO, register_theme_swap
from dl2_locations import AUSTIN
from dl2_shared import code_panel, info_panel
# Basemap pair for this page — dl2_tiles owns the light/dark wiring.
TILES = USGS_TOPO
# The template the PICKED tile URLs are built from. Deliberately not the
# basemap: this page's point is that the two are independent.
OSM = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
CODE = """import dash_leaflet2 as dl2
dl2.Map(
center=[30.2672, -97.7431],
zoom=11,
children=[
dl2.TileLayer(url=OSM),
dl2.TileSelector(
id="tile-picker",
position="topright",
tileUrl=OSM, # which tileset the picked URLs point at
hoverColor="#fa5252", # dashed outline under the cursor
selectedColor="#228be6",
),
],
)
@callback(Output("out", "children"), Input("tile-picker", "selectedTiles"))
def show(tiles):
return f"{len(tiles or [])} tiles selected"
"""
def _tile_row(tile):
"""One selected tile as a table row: z/x/y, bounds, and its URL."""
s, w, n, e = tile.get("bounds") or [0, 0, 0, 0]
return dmc.TableTr(
[
dmc.TableTd(
dmc.Code(f"{tile['z']}/{tile['x']}/{tile['y']}"),
),
dmc.TableTd(
dmc.Text(f"{s:.4f}, {w:.4f} → {n:.4f}, {e:.4f}",
size="xs", c="dimmed", ff="monospace"),
),
dmc.TableTd(
dmc.Anchor(
dmc.Group(
[DashIconify(icon="tabler:photo", width=14), dmc.Text("PNG", size="xs")],
gap=4,
),
href=tile.get("url", "#"),
target="_blank",
),
style={"textAlign": "center"},
),
]
)
# `component` is the name the `.. exec::` directive looks for.
component = dmc.Stack(
[
dmc.Alert(
dmc.Stack(
[
dmc.Text(
"Click the crosshair button in the map's top-right corner to arm "
"the selector, then click tiles — or hold Shift and drag a box.",
size="sm",
),
dmc.Text(
"Zoom changes the tile grid; already-picked tiles stay picked "
"because each one is keyed by z/x/y.",
size="sm",
c="dimmed",
),
],
gap=4,
),
title="How to use it",
color="blue",
variant="light",
icon=DashIconify(icon="tabler:hand-click"),
),
dmc.Paper(
dl2.Map(
id="ts-map",
center=AUSTIN.center,
zoom=11,
style={"height": "58vh", "width": "100%"},
children=[
dl2.TileLayer(id="ts-tile", **TILES.kwargs("light")),
dl2.TileSelector(
id="ts-picker",
position="topright",
# The URLs handed back in `selectedTiles` point at THIS
# template — it does not have to be the tileset you are
# displaying. Here we show USGS Topo but hand back OSM PNGs.
tileUrl=OSM,
hoverColor="#fa5252",
selectedColor="#228be6",
),
],
),
shadow="sm",
radius="md",
withBorder=True,
style={"overflow": "hidden"},
),
dmc.Group(
[
dmc.Button(
"Clear selection",
id="ts-clear",
variant="light",
color="red",
leftSection=DashIconify(icon="tabler:trash", width=16),
),
html.Div(id="ts-count"),
],
justify="space-between",
align="center",
),
info_panel("Selected tiles", html.Div(id="ts-table")),
code_panel("Usage", CODE),
],
gap="md",
)
@callback(
Output("ts-count", "children"),
Output("ts-table", "children"),
Input("ts-picker", "selectedTiles"),
)
def show_selection(tiles):
tiles = tiles or []
badge = dmc.Badge(
f"{len(tiles)} tile{'' if len(tiles) == 1 else 's'} selected",
color="blue" if tiles else "gray",
variant="light",
size="lg",
)
if not tiles:
return badge, dmc.Text(
"Nothing selected yet — arm the control and click a tile.",
c="dimmed", size="sm",
)
table = dmc.Table(
[
dmc.TableThead(
dmc.TableTr(
[
dmc.TableTh("z/x/y"),
dmc.TableTh("bounds (S, W → N, E)"),
dmc.TableTh("tile", style={"textAlign": "center"}),
]
)
),
# Newest first, and capped — a shift-drag at low zoom can select a
# lot of tiles and this is a doc page, not a data grid.
dmc.TableTbody([_tile_row(t) for t in list(reversed(tiles))[:25]]),
],
striped=True,
highlightOnHover=True,
)
footer = (
dmc.Text(f"Showing the 25 most recent of {len(tiles)}.", size="xs", c="dimmed", mt="xs")
if len(tiles) > 25
else None
)
return badge, dmc.Stack([table, footer], gap=0)
@callback(
Output("ts-picker", "selectedTiles"),
Input("ts-clear", "n_clicks"),
prevent_initial_call=True,
)
def clear_selection(n_clicks):
"""Python → map: writing the [MUTABLE] prop clears the component's state."""
return [] if n_clicks else no_update
# Light/dark basemap, driven off the color-scheme store.
register_theme_swap("ts-tile", TILES)
:defaultExpanded: false :withExpandedButton: true
Source: /tile-selector