Component Apps: Mapping with WMS
Important
These recipes only apply to Component App development and will not work for Standard Apps.
Last Updated: December 2025
Single Image WMS
@App.page
def single_image_wms(lib):
return lib.tethys.Display(
lib.tethys.Map(
lib.ol.layer.Image(
lib.ol.source.ImageWMS(
options=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(LAYERS='topp:states'),
ratio=1,
serverType="geoserver"
)
)
)
)
)
Tiled WMS
@App.page
def tiled_wms(lib):
return lib.tethys.Display(
lib.tethys.Map(
lib.ol.layer.WebGLTile(
lib.ol.source.TileWMS(
options=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(LAYERS='topp:states', TILED=True),
serverType="geoserver",
# Countries have transparency, so do not fade tiles:
transition=0
)
)
)
)
)
MapServer WMS
@App.page
def tiled_wms(lib):
return lib.tethys.Display(
lib.tethys.Map(projection="EPSG:4326")(
lib.ol.layer.Image(
lib.ol.source.Image(
options=lib.Props(
loader=lib.Props(
url="https://demo.mapserver.org/cgi-bin/wms?",
params=lib.Props(
LAYERS=['bluemarble,country_bounds,cities'],
VERSION="1.3.0",
FORMAT="image/png"
),
projection="EPSG:4326",
# note: serverType only needs to be set when hidpi is True
hidpi=True,
serverType="mapserver"
)
)
)
)
)
)
WMS Custom-Sized Tiles
from pyproj import CRS, Transformer
import math
def get_resolutions():
resolutions = []
crs_3857 = CRS("EPSG:3857")
geographic_bounds = crs_3857.area_of_use.bounds
transformer = Transformer.from_crs(crs_3857.geodetic_crs, crs_3857, always_xy=True)
proj_extent = transformer.transform_bounds(*geographic_bounds)
start_resolution = (proj_extent[2] - proj_extent[0]) / 256
for i in range(22):
resolutions.append(start_resolution / math.pow(2, i))
return resolutions
@App.page
def wms_custom_sized_tiles(lib):
get_resolutions()
return lib.tethys.Display(
lib.tethys.Map(
lib.ol.layer.Tile(
lib.ol.source.TileWMS(
options=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(
LAYERS=['topp:states'],
TILED=True
),
serverType="mapserver",
tileGrid=lib.Props(
extent=[-13884991, 2870341, -7455066, 6338219],
resolutions=get_resolutions(),
tileSize=[512, 256]
)
)
)
)
)
)
WMS Legend
Important
This example uses lib.ol.Map rather than lib.tethys.Map.
This is because lib.tethys.Map does not provide the granular control over the underlying lib.ol.View that is required by this example.
@App.page
def wms_legend(lib):
legend_url, set_legend_url = lib.hooks.use_state("")
wms_source = lib.ol.source.ImageWMS(
options=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(LAYERS='topp:states'),
ratio=1,
serverType="geoserver"
)
)
lib.hooks.use_effect(lambda: set_legend_url(wms_source.get_legend_url()), dependencies=[])
return lib.tethys.Display(style=lib.Style(position="relative"))(
lib.html.div(style=lib.Style(position="absolute", top="5px", right="20px", zIndex=1))(
lib.html.img(src=legend_url) if legend_url else None
),
lib.ol.Map(
lib.ol.View(
onChange=lambda e: set_legend_url(wms_source.get_legend_url(e.target.values_.resolution)),
center=[-10997148, 4569099],
zoom=4
),
lib.ol.layer.Tile(lib.ol.source.OSM()),
lib.ol.layer.Image(
wms_source
)
)
)
WMS Loader with SVG Format
@App.page
def wms_loader_with_svg_format(lib):
return lib.tethys.Display(
lib.tethys.Map(
lib.ol.layer.Image(
lib.ol.source.Image(
options=lib.Props(
loader=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(
LAYERS=['topp:states'],
FORMAT="image/svg+xml"
),
ratio=1,
load=True
)
)
)
)
)
)
WMS Time
import datetime as dt
def three_hours_ago():
now = dt.datetime.now()
return (now - dt.timedelta(hours=3)).replace(
minute=int(now.minute/15)*15,
second=0,
microsecond=0
)
@App.page
def wms_time(lib):
frame_rate = 0.5 # frames per second
wms_time, set_wms_time = lib.hooks.use_state(lambda: three_hours_ago())
timer, set_timer = lib.hooks.use_state(None)
return lib.tethys.Display(style=lib.Style(position="relative"))(
lib.html.div(
style=lib.Style(
zIndex=1,
position="absolute",
top="5px",
right="20px"
)
)(
lib.html.div(style=lib.Style(display="flex", justify_content="center"))(
lib.html.button(
on_click=lambda _: set_timer(
lib.utils.background_execute(
lambda: set_wms_time(lambda old: three_hours_ago() if old + dt.timedelta(minutes=15) > dt.datetime.now() else old + dt.timedelta(minutes=15)),
repeat_seconds=1/frame_rate
)
),
disabled=timer is not None
)(
"Play"
),
lib.html.button(
on_click=lambda _: (
timer.cancel() if timer else None,
set_timer(None)
),
disabled=timer is None
)(
"Stop"
)
),
lib.html.div(
f"Time: {wms_time.isoformat()}"
)
),
lib.tethys.Map(
lib.ol.layer.Tile(
lib.ol.source.TileWMS(
options=lib.Props(
url="https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi"
),
params=lib.Props(
LAYERS=['nexrad-n0r-wmst'],
TIME=wms_time.isoformat()
)
)
)
)
)
WMS without Projection
As long as no coordinate transformations are required, the underlying OpenLayers mapping engine works fine with projections that are only configured with a code and units.
Important
This example uses lib.ol.Map rather than lib.tethys.Map.
This is because lib.tethys.Map provides a default suite of basemaps which are not compatible with projections lacking a full definition, like the one used in this example.
@App.page
def wms_without_projection(lib):
return lib.tethys.Display(
lib.ol.Map(
lib.ol.View(
options=lib.Props(
projection=lib.Props(code="EPSG:21781", units="m"),
),
center=[660000, 190000],
zoom=9
),
lib.ol.layer.Group(options=lib.Props(title="Overlays", fold="open"))(
lib.ol.layer.Tile(options=lib.Props(title="Custom Projection Basemap"))(
lib.ol.source.TileWMS(
options=lib.Props(
attributions=(
'\u00A9 <a href="https://shop.swisstopo.admin.ch/en/products/maps/national/lk1000"' +
'target="_blank">Pixelmap 1:1000000 / geo.admin.ch</a>'
),
crossOrigin="anonymous",
params=lib.Props(
LAYERS="ch.swisstopo.pixelkarte-farbe-pk1000.noscale",
FORMAT="image/jpeg"
),
url="https://wms.geo.admin.ch/",
),
)
),
lib.ol.layer.Image(options=lib.Props(title="Flood Alert"))(
lib.ol.source.ImageWMS(
options=lib.Props(
attributions=(
'\u00A9 <a href="https://www.hydrodaten.admin.ch/en/notes-on-the-flood-alert-maps.html"' +
'target="_blank">Flood Alert / geo.admin.ch</a>'
),
crossOrigin="anonymous",
params=lib.Props(LAYERS="ch.bafu.hydroweb-warnkarte_national"),
serverType="mapserver",
url="https://wms.geo.admin.ch/"
)
)
),
),
lib.ol.control.ScaleLine(),
lib.tethys.LayerPanel(),
)
)
Single Image WMS with Custom Projection
CUSTOM_PRJ = (
'+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 ' +
'+x_0=600000 +y_0=200000 +ellps=bessel ' +
'+towgs84=660.077,13.551,369.344,2.484,1.783,2.939,5.66 +units=m +no_defs'
)
@App.page
def wms_image_with_custom_projection(lib):
return lib.tethys.Display(
lib.tethys.Map(
projection=lib.Props(
code="EPSG:21781",
extent=[485869.5728, 76443.1884, 837076.5648, 299941.7864],
definition=CUSTOM_PRJ
),
zoom=2,
center=lib.utils.transform_coordinate([8.23, 46.86], "EPSG:4326", CUSTOM_PRJ)
)(
lib.ol.layer.Tile(options=lib.Props(title="Custom Projection Basemap"))(
lib.ol.source.TileWMS(
options=lib.Props(
attributions=(
'\u00A9 <a href="https://shop.swisstopo.admin.ch/en/products/maps/national/lk1000"' +
'target="_blank">Pixelmap 1:1000000 / geo.admin.ch</a>'
),
crossOrigin="anonymous",
params=lib.Props(
LAYERS="ch.swisstopo.pixelkarte-farbe-pk1000.noscale",
FORMAT="image/jpeg"
),
url="https://wms.geo.admin.ch/",
),
)
),
lib.ol.layer.Image(options=lib.Props(title="Flood Alert"))(
lib.ol.source.ImageWMS(
options=lib.Props(
attributions=(
'\u00A9 <a href="https://www.hydrodaten.admin.ch/en/notes-on-the-flood-alert-maps.html"' +
'target="_blank">Flood Alert / geo.admin.ch</a>'
),
crossOrigin="anonymous",
params=lib.Props(LAYERS="ch.bafu.hydroweb-warnkarte_national"),
serverType="mapserver",
url="https://wms.geo.admin.ch/"
)
)
)
)
)
WMS GetFeatureInfo (Image Layer)
@App.page
def wms_get_feature_info(lib):
props, set_props = lib.hooks.use_state(None)
wms_source = lib.ol.source.ImageWMS(
options=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(LAYERS="ne:ne"),
serverType="geoserver",
crossOrigin="anonymous"
)
)
return lib.tethys.Display(
lib.html.div(
style=lib.Style(height="85vh")
)(
lib.tethys.Map(
default_basemap=None,
on_click=lambda e: (
set_props(
lib.utils.fetch(
wms_source.get_feature_info_url(
e.coordinate,
e.target.frameState_.viewState.resolution,
e.target.values_.view.projection_.code_,
"EPSG:3857",
{"INFO_FORMAT": "text/html"}
)
)
)
)
)(
lib.ol.layer.Image(wms_source)
)
),
lib.html.div(
style=lib.Style(height="15vh", width="95vw")
)(
lib.html.iframe(width="100%", srcdoc=props) if props else lib.html.h1("Click Map For Feature Info")
)
)
WMS GetFeatureInfo (Tile Layer)
@App.page
def wms_get_feature_info(lib):
props, set_props = lib.hooks.use_state(None)
wms_source = lib.ol.source.TileWMS(
options=lib.Props(
url="https://ahocevar.com/geoserver/wms",
params=lib.Props(LAYERS="ne:ne", TILED=True),
serverType="geoserver",
crossOrigin="anonymous"
),
)
return lib.tethys.Display(
lib.html.div(
style=lib.Style(height="85vh")
)(
lib.tethys.Map(
default_basemap=None,
on_click=lambda e: (
set_props(
lib.utils.fetch(
wms_source.get_feature_info_url(
e.coordinate,
e.target.frameState_.viewState.resolution,
e.target.values_.view.projection_.code_,
"EPSG:3857",
{"INFO_FORMAT": "text/html"}
)
)
)
)
)(
lib.ol.layer.Tile(
wms_source
)
)
),
lib.html.div(
style=lib.Style(height="15vh", width="95vw")
)(
lib.html.iframe(width="100%", srcdoc=props) if props else lib.html.h1("Click Map For Feature Info")
)
)