Creating 3D maps with MapLibre
The notebook demonstrates how to create 3D maps using the MapLibre Python package. The examples shown in this notebook are based on the MapLibre documentation. Credits to the original authors at eoda GmbH.
Installation¶
Uncomment the following line to install leafmap if needed.
# %pip install "leafmap[maplibre]"
Import libraries¶
import leafmap.maplibregl as leafmap
Create maps¶
Create an interactive map by specifying map center [lon, lat], zoom level, pitch, and bearing.
m = leafmap.Map(center=[-100, 40], zoom=3, pitch=0, bearing=0)
m
To customize the basemap, you can specify the style
parameter. It can be an URL or a string, such as dark-matter
, positron
, voyager
, demotiles
.
m = leafmap.Map(style="positron")
m
To create a map with a background color, use style="background-<COLOR>"
, such as background-lightgray
and background-green
.
m = leafmap.Map(style="background-lightgray")
m
Alternatively, you can provide a URL to a vector style.
style = "https://demotiles.maplibre.org/style.json"
m = leafmap.Map(style=style)
m
Add controls¶
The control to add to the map. Can be one of the following: scale
, fullscreen
, geolocate
, navigation
.
m = leafmap.Map()
m.add_control("geolocate", position="top-left")
m
Add basemaps¶
m = leafmap.Map()
m
m.add_basemap()
m = leafmap.Map()
m.add_basemap("OpenTopoMap")
m
m.add_basemap("Esri.WorldImagery")
XYZ tile layer¶
m = leafmap.Map()
url = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
m.add_tile_layer(
url, name="OpenStreetMap", attribution="OpenStreetMap", opacity=1.0, visible=True
)
m
WMS layer¶
m = leafmap.Map(center=[-100, 40], zoom=3)
m.add_basemap("Esri.WorldImagery")
url = "https://www.mrlc.gov/geoserver/mrlc_display/NLCD_2021_Land_Cover_L48/wms?bbox={bbox-epsg-3857}&format=image/png&service=WMS&version=1.1.1&request=GetMap&srs=EPSG:3857&transparent=true&width=256&height=256&layers=NLCD_2021_Land_Cover_L48"
m.add_wms_layer(url, name="NLCD", opacity=0.8)
m
COG layer¶
m = leafmap.Map()
url = (
"https://github.com/opengeos/datasets/releases/download/raster/Libya-2023-07-01.tif"
)
m.add_cog_layer(url, name="COG", attribution="Maxar", fit_bounds=True)
m
STAC layer¶
m = leafmap.Map()
url = "https://canada-spot-ortho.s3.amazonaws.com/canada_spot_orthoimages/canada_spot5_orthoimages/S5_2007/S5_11055_6057_20070622/S5_11055_6057_20070622.json"
m.add_stac_layer(url, bands=["B4", "B3", "B2"], name="SPOT", vmin=0, vmax=150)
m
Local raster¶
url = "https://github.com/opengeos/datasets/releases/download/raster/srtm90.tif"
filepath = "srtm90.tif"
leafmap.download_file(url, filepath)
srtm90.tif already exists. Skip downloading. Set overwrite=True to overwrite.
'/home/runner/work/leafmap/leafmap/docs/notebooks/srtm90.tif'
m = leafmap.Map()
m.add_raster(filepath, colormap="terrain", name="DEM")
m
Vancouver Property Value¶
m = leafmap.Map(
center=[-123.13, 49.254], zoom=11, style="dark-matter", pitch=45, bearing=0
)
url = "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/geojson/vancouver-blocks.json"
paint_line = {
"line-color": "white",
"line-width": 2,
}
paint_fill = {
"fill-extrusion-color": {
"property": "valuePerSqm",
"stops": [
[0, "grey"],
[1000, "yellow"],
[5000, "orange"],
[10000, "darkred"],
[50000, "lightblue"],
],
},
"fill-extrusion-height": ["*", 10, ["sqrt", ["get", "valuePerSqm"]]],
"fill-extrusion-opacity": 0.9,
}
m.add_geojson(url, layer_type="line", paint=paint_line, name="blocks-line")
m.add_geojson(url, layer_type="fill-extrusion", paint=paint_fill, name="blocks-fill")
m
m.layer_interact()
Earthquake Clusters¶
m = leafmap.Map(style="positron")
data = "https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
source_args = {
"cluster": True,
"cluster_radius": 50,
"cluster_min_points": 2,
"cluster_max_zoom": 14,
"cluster_properties": {
"maxMag": ["max", ["get", "mag"]],
"minMag": ["min", ["get", "mag"]],
},
}
m.add_geojson(
data,
layer_type="circle",
name="earthquake-circles",
filter=["!", ["has", "point_count"]],
paint={"circle-color": "darkblue"},
source_args=source_args,
)
m.add_geojson(
data,
layer_type="circle",
name="earthquake-clusters",
filter=["has", "point_count"],
paint={
"circle-color": [
"step",
["get", "point_count"],
"#51bbd6",
100,
"#f1f075",
750,
"#f28cb1",
],
"circle-radius": ["step", ["get", "point_count"], 20, 100, 30, 750, 40],
},
source_args=source_args,
)
m.add_geojson(
data,
layer_type="symbol",
name="earthquake-labels",
filter=["has", "point_count"],
layout={
"text-field": ["get", "point_count_abbreviated"],
"text-size": 12,
},
source_args=source_args,
)
m
Airport Markers¶
from maplibre.controls import Marker, MarkerOptions, Popup, PopupOptions
import pandas as pd
m = leafmap.Map(style="positron")
url = "https://github.com/visgl/deck.gl-data/raw/master/examples/line/airports.json"
data = leafmap.pandas_to_geojson(
url, "coordinates", properties=["type", "name", "abbrev"]
)
m.add_geojson(
data,
name="Airports",
layer_type="circle",
paint={
"circle-color": [
"match",
["get", "type"],
"mid",
"darkred",
"major",
"darkgreen",
"darkblue",
],
"circle_radius": 10,
"circle-opacity": 0.3,
},
)
def get_color(airport_type: str) -> str:
color = "darkblue"
if airport_type == "mid":
color = "darkred"
elif airport_type == "major":
color = "darkgreen"
return color
airports_data = pd.read_json(url)
popup_options = PopupOptions(close_button=False)
for _, r in airports_data.iterrows():
m.add_marker(
lng_lat=r["coordinates"],
options=MarkerOptions(color=get_color(r["type"])),
popup=Popup(
text=r["name"],
options=popup_options,
),
)
m
3D Indoor Mapping¶
m = leafmap.Map(
center=(-87.61694, 41.86625), zoom=17, pitch=40, bearing=20, style="positron"
)
m.add_basemap("OpenStreetMap.Mapnik")
data = "https://maplibre.org/maplibre-gl-js/docs/assets/indoor-3d-map.geojson"
m.add_geojson(
data,
layer_type="fill-extrusion",
name="floorplan",
paint={
"fill-extrusion-color": ["get", "color"],
"fill-extrusion-height": ["get", "height"],
"fill-extrusion-base": ["get", "base_height"],
"fill-extrusion-opacity": 0.5,
},
)
m
Custom Basemap¶
import leafmap.maplibregl as leafmap
from maplibre.basemaps import construct_basemap_style
from maplibre import Layer, LayerType, Map, MapOptions
from maplibre.sources import GeoJSONSource
bg_layer = Layer(
type=LayerType.BACKGROUND,
id="background",
source=None,
paint={"background-color": "darkblue", "background-opacity": 0.8},
)
countries_source = GeoJSONSource(
data="https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson"
)
lines_layer = Layer(
type=LayerType.LINE,
source="countries",
paint={"line-color": "white", "line-width": 1.5},
)
polygons_layer = Layer(
type=LayerType.FILL,
source="countries",
paint={"fill-color": "darkred", "fill-opacity": 0.8},
)
custom_basemap = construct_basemap_style(
layers=[bg_layer, polygons_layer, lines_layer],
sources={"countries": countries_source},
)
m = leafmap.Map(style=custom_basemap)
data = "https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"
m.add_geojson(
data,
layer_type="circle",
name="earthquakes",
paint={"circle-color": "yellow", "circle-radius": 5},
)
m.add_popup("earthquakes", "mag")
m
H3 Grid UK Road Safety¶
import pandas as pd
import h3
RESOLUTION = 7
COLORS = (
"lightblue",
"turquoise",
"lightgreen",
"yellow",
"orange",
"darkred",
)
road_safety = pd.read_csv(
"https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv"
).dropna()
def create_h3_grid(res=RESOLUTION) -> dict:
road_safety["h3"] = road_safety.apply(
lambda x: h3.geo_to_h3(x["lat"], x["lng"], resolution=res), axis=1
)
df = road_safety.groupby("h3").h3.agg("count").to_frame("count").reset_index()
df["hexagon"] = df.apply(
lambda x: [h3.h3_to_geo_boundary(x["h3"], geo_json=True)], axis=1
)
df["color"] = pd.cut(
df["count"],
bins=len(COLORS),
labels=COLORS,
)
return leafmap.pandas_to_geojson(
df, "hexagon", geometry_type="Polygon", properties=["count", "color"]
)
m = leafmap.Map(
center=(-1.415727, 52.232395),
zoom=7,
pitch=40,
bearing=-27,
)
data = create_h3_grid()
m.add_geojson(
data,
layer_type="fill-extrusion",
paint={
"fill-extrusion-color": ["get", "color"],
"fill-extrusion-opacity": 0.7,
"fill-extrusion-height": ["*", 100, ["get", "count"]],
},
)
m
Deck.GL Layer¶
m = leafmap.Map(
style="positron",
center=(-122.4, 37.74),
zoom=12,
pitch=40,
)
deck_grid_layer = {
"@@type": "GridLayer",
"id": "GridLayer",
"data": "https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json",
"extruded": True,
"getPosition": "@@=COORDINATES",
"getColorWeight": "@@=SPACES",
"getElevationWeight": "@@=SPACES",
"elevationScale": 4,
"cellSize": 200,
"pickable": True,
}
m.add_deck_layers([deck_grid_layer], tooltip="Number of points: {{ count }}")
m
Multiple Deck.GL Layers¶
import requests
data = requests.get(
"https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson"
).json()
m = leafmap.Map(
style="positron",
center=(0.45, 51.47),
zoom=4,
pitch=30,
)
deck_geojson_layer = {
"@@type": "GeoJsonLayer",
"id": "airports",
"data": data,
"filled": True,
"pointRadiusMinPixels": 2,
"pointRadiusScale": 2000,
"getPointRadius": "@@=11 - properties.scalerank",
"getFillColor": [200, 0, 80, 180],
"autoHighlight": True,
"pickable": True,
}
deck_arc_layer = {
"@@type": "ArcLayer",
"id": "arcs",
"data": [
feature
for feature in data["features"]
if feature["properties"]["scalerank"] < 4
],
"getSourcePosition": [-0.4531566, 51.4709959], # London
"getTargetPosition": "@@=geometry.coordinates",
"getSourceColor": [0, 128, 200],
"getTargetColor": [200, 0, 80],
"getWidth": 2,
"pickable": True,
}
m.add_deck_layers(
[deck_geojson_layer, deck_arc_layer],
tooltip={
"airports": "{{ &properties.name }}",
"arcs": "gps_code: {{ properties.gps_code }}",
},
)
m