{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=notebooks/92_maplibre.ipynb)\n", "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/notebooks/92_maplibre.ipynb)\n", "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", "\n", "**Creating 3D maps with MapLibre**\n", "\n", "The notebook demonstrates how to create 3D maps using the [MapLibre](https://github.com/eodaGmbH/py-maplibregl) Python package. The examples shown in this notebook are based on the [MapLibre documentation](https://eodagmbh.github.io/py-maplibregl/examples/vancouver_blocks/). Credits to the original authors at [eoda GmbH](https://www.eoda.de/en/).\n", "\n", "## Installation\n", "\n", "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." ] }, { "cell_type": "code", "execution_count": null, "id": "1", "metadata": {}, "outputs": [], "source": [ "# %pip install \"leafmap[maplibre]\"" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "## Import libraries" ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": {}, "outputs": [], "source": [ "import leafmap.maplibregl as leafmap" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "## Create maps\n", "\n", "Create an interactive map by specifying map center [lon, lat], zoom level, pitch, and bearing." ] }, { "cell_type": "code", "execution_count": null, "id": "5", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(center=[-100, 40], zoom=3, pitch=0, bearing=0)\n", "m" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "![](https://i.imgur.com/8ITaEZa.png)" ] }, { "cell_type": "markdown", "id": "7", "metadata": {}, "source": [ "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`." ] }, { "cell_type": "code", "execution_count": null, "id": "8", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(style=\"positron\")\n", "m" ] }, { "cell_type": "markdown", "id": "9", "metadata": {}, "source": [ "![](https://i.imgur.com/9fImW21.png)" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "To create a map with a background color, use `style=\"background-\"`, such as `background-lightgray` and `background-green`." ] }, { "cell_type": "code", "execution_count": null, "id": "11", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(style=\"background-lightgray\")\n", "m" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "![](https://i.imgur.com/xFeDTkE.png)" ] }, { "cell_type": "markdown", "id": "13", "metadata": {}, "source": [ "Alternatively, you can provide a URL to a vector style." ] }, { "cell_type": "code", "execution_count": null, "id": "14", "metadata": {}, "outputs": [], "source": [ "style = \"https://demotiles.maplibre.org/style.json\"\n", "m = leafmap.Map(style=style)\n", "m" ] }, { "cell_type": "markdown", "id": "15", "metadata": {}, "source": [ "![](https://i.imgur.com/yaZYrr1.png)" ] }, { "cell_type": "markdown", "id": "16", "metadata": {}, "source": [ "## Add controls\n", "\n", "The control to add to the map. Can be one of the following: `scale`, `fullscreen`, `geolocate`, `navigation`." ] }, { "cell_type": "code", "execution_count": null, "id": "17", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "m.add_control(\"geolocate\", position=\"top-left\")\n", "m" ] }, { "cell_type": "markdown", "id": "18", "metadata": {}, "source": [ "![](https://i.imgur.com/7LS5WAk.png)" ] }, { "cell_type": "markdown", "id": "19", "metadata": {}, "source": [ "## Add basemaps" ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "m" ] }, { "cell_type": "code", "execution_count": null, "id": "21", "metadata": {}, "outputs": [], "source": [ "m.add_basemap()" ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "m.add_basemap(\"OpenTopoMap\")\n", "m" ] }, { "cell_type": "markdown", "id": "23", "metadata": {}, "source": [ "![](https://i.imgur.com/MRRw1MW.png)" ] }, { "cell_type": "code", "execution_count": null, "id": "24", "metadata": {}, "outputs": [], "source": [ "m.add_basemap(\"Esri.WorldImagery\")" ] }, { "cell_type": "markdown", "id": "25", "metadata": {}, "source": [ "## XYZ tile layer" ] }, { "cell_type": "code", "execution_count": null, "id": "26", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "url = \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\"\n", "m.add_tile_layer(\n", " url, name=\"OpenStreetMap\", attribution=\"OpenStreetMap\", opacity=1.0, visible=True\n", ")\n", "m" ] }, { "cell_type": "markdown", "id": "27", "metadata": {}, "source": [ "![](https://i.imgur.com/V9wmsjl.png)" ] }, { "cell_type": "markdown", "id": "28", "metadata": {}, "source": [ "## WMS layer" ] }, { "cell_type": "code", "execution_count": null, "id": "29", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(center=[-100, 40], zoom=3)\n", "m.add_basemap(\"Esri.WorldImagery\")\n", "url = \"https://www.mrlc.gov/geoserver/mrlc_display/NLCD_2021_Land_Cover_L48/wms\"\n", "layers = \"NLCD_2021_Land_Cover_L48\"\n", "m.add_wms_layer(url, layers=layers, name=\"NLCD\", opacity=0.8)\n", "m" ] }, { "cell_type": "markdown", "id": "30", "metadata": {}, "source": [ "![](https://i.imgur.com/xcZ4VKv.png)" ] }, { "cell_type": "markdown", "id": "31", "metadata": {}, "source": [ "## COG layer" ] }, { "cell_type": "code", "execution_count": null, "id": "32", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "url = (\n", " \"https://github.com/opengeos/datasets/releases/download/raster/Libya-2023-07-01.tif\"\n", ")\n", "m.add_cog_layer(url, name=\"COG\", attribution=\"Maxar\", fit_bounds=True)\n", "m" ] }, { "cell_type": "markdown", "id": "33", "metadata": {}, "source": [ "![](https://i.imgur.com/ApGhjDp.png)" ] }, { "cell_type": "markdown", "id": "34", "metadata": {}, "source": [ "## STAC layer" ] }, { "cell_type": "code", "execution_count": null, "id": "35", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "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\"\n", "m.add_stac_layer(url, bands=[\"B4\", \"B3\", \"B2\"], name=\"SPOT\", vmin=0, vmax=150)\n", "m" ] }, { "cell_type": "markdown", "id": "36", "metadata": {}, "source": [ "![](https://i.imgur.com/RJAhsV5.png)" ] }, { "cell_type": "markdown", "id": "37", "metadata": {}, "source": [ "## Local raster" ] }, { "cell_type": "code", "execution_count": null, "id": "38", "metadata": {}, "outputs": [], "source": [ "url = \"https://github.com/opengeos/datasets/releases/download/raster/srtm90.tif\"\n", "filepath = \"srtm90.tif\"\n", "leafmap.download_file(url, filepath)" ] }, { "cell_type": "code", "execution_count": null, "id": "39", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map()\n", "m.add_raster(filepath, colormap=\"terrain\", name=\"DEM\")\n", "m" ] }, { "cell_type": "markdown", "id": "40", "metadata": {}, "source": [ "![](https://i.imgur.com/pMcuQAp.png)" ] }, { "cell_type": "markdown", "id": "41", "metadata": {}, "source": [ "## Vancouver Property Value" ] }, { "cell_type": "code", "execution_count": null, "id": "42", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(\n", " center=[-123.13, 49.254], zoom=11, style=\"dark-matter\", pitch=45, bearing=0\n", ")\n", "url = \"https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/geojson/vancouver-blocks.json\"\n", "paint_line = {\n", " \"line-color\": \"white\",\n", " \"line-width\": 2,\n", "}\n", "paint_fill = {\n", " \"fill-extrusion-color\": {\n", " \"property\": \"valuePerSqm\",\n", " \"stops\": [\n", " [0, \"grey\"],\n", " [1000, \"yellow\"],\n", " [5000, \"orange\"],\n", " [10000, \"darkred\"],\n", " [50000, \"lightblue\"],\n", " ],\n", " },\n", " \"fill-extrusion-height\": [\"*\", 10, [\"sqrt\", [\"get\", \"valuePerSqm\"]]],\n", " \"fill-extrusion-opacity\": 0.9,\n", "}\n", "m.add_geojson(url, layer_type=\"line\", paint=paint_line, name=\"blocks-line\")\n", "m.add_geojson(url, layer_type=\"fill-extrusion\", paint=paint_fill, name=\"blocks-fill\")\n", "m" ] }, { "cell_type": "code", "execution_count": null, "id": "43", "metadata": {}, "outputs": [], "source": [ "m.layer_interact()" ] }, { "cell_type": "markdown", "id": "44", "metadata": {}, "source": [ "![](https://i.imgur.com/IZXfgSz.gif)" ] }, { "cell_type": "markdown", "id": "45", "metadata": {}, "source": [ "## Earthquake Clusters" ] }, { "cell_type": "code", "execution_count": null, "id": "46", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(style=\"positron\")\n", "\n", "data = \"https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson\"\n", "source_args = {\n", " \"cluster\": True,\n", " \"cluster_radius\": 50,\n", " \"cluster_min_points\": 2,\n", " \"cluster_max_zoom\": 14,\n", " \"cluster_properties\": {\n", " \"maxMag\": [\"max\", [\"get\", \"mag\"]],\n", " \"minMag\": [\"min\", [\"get\", \"mag\"]],\n", " },\n", "}\n", "\n", "m.add_geojson(\n", " data,\n", " layer_type=\"circle\",\n", " name=\"earthquake-circles\",\n", " filter=[\"!\", [\"has\", \"point_count\"]],\n", " paint={\"circle-color\": \"darkblue\"},\n", " source_args=source_args,\n", ")\n", "\n", "m.add_geojson(\n", " data,\n", " layer_type=\"circle\",\n", " name=\"earthquake-clusters\",\n", " filter=[\"has\", \"point_count\"],\n", " paint={\n", " \"circle-color\": [\n", " \"step\",\n", " [\"get\", \"point_count\"],\n", " \"#51bbd6\",\n", " 100,\n", " \"#f1f075\",\n", " 750,\n", " \"#f28cb1\",\n", " ],\n", " \"circle-radius\": [\"step\", [\"get\", \"point_count\"], 20, 100, 30, 750, 40],\n", " },\n", " source_args=source_args,\n", ")\n", "\n", "m.add_geojson(\n", " data,\n", " layer_type=\"symbol\",\n", " name=\"earthquake-labels\",\n", " filter=[\"has\", \"point_count\"],\n", " layout={\n", " \"text-field\": [\"get\", \"point_count_abbreviated\"],\n", " \"text-size\": 12,\n", " },\n", " source_args=source_args,\n", ")\n", "m" ] }, { "cell_type": "markdown", "id": "47", "metadata": {}, "source": [ "![](https://i.imgur.com/vge4jF4.png)" ] }, { "cell_type": "markdown", "id": "48", "metadata": {}, "source": [ "## Airport Markers" ] }, { "cell_type": "code", "execution_count": null, "id": "49", "metadata": {}, "outputs": [], "source": [ "from maplibre.controls import Marker, MarkerOptions, Popup, PopupOptions\n", "import pandas as pd" ] }, { "cell_type": "code", "execution_count": null, "id": "50", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(style=\"positron\")\n", "\n", "url = \"https://github.com/visgl/deck.gl-data/raw/master/examples/line/airports.json\"\n", "data = leafmap.pandas_to_geojson(\n", " url, \"coordinates\", properties=[\"type\", \"name\", \"abbrev\"]\n", ")\n", "\n", "m.add_geojson(\n", " data,\n", " name=\"Airports\",\n", " layer_type=\"circle\",\n", " paint={\n", " \"circle-color\": [\n", " \"match\",\n", " [\"get\", \"type\"],\n", " \"mid\",\n", " \"darkred\",\n", " \"major\",\n", " \"darkgreen\",\n", " \"darkblue\",\n", " ],\n", " \"circle_radius\": 10,\n", " \"circle-opacity\": 0.3,\n", " },\n", ")\n", "\n", "\n", "def get_color(airport_type: str) -> str:\n", " color = \"darkblue\"\n", " if airport_type == \"mid\":\n", " color = \"darkred\"\n", " elif airport_type == \"major\":\n", " color = \"darkgreen\"\n", "\n", " return color\n", "\n", "\n", "airports_data = pd.read_json(url)\n", "popup_options = PopupOptions(close_button=False)\n", "\n", "for _, r in airports_data.iterrows():\n", " m.add_marker(\n", " lng_lat=r[\"coordinates\"],\n", " options=MarkerOptions(color=get_color(r[\"type\"])),\n", " popup=Popup(\n", " text=r[\"name\"],\n", " options=popup_options,\n", " ),\n", " )\n", "\n", "m" ] }, { "cell_type": "markdown", "id": "51", "metadata": {}, "source": [ "![](https://i.imgur.com/q7nN1PW.png)" ] }, { "cell_type": "markdown", "id": "52", "metadata": {}, "source": [ "## 3D Indoor Mapping" ] }, { "cell_type": "code", "execution_count": null, "id": "53", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(\n", " center=(-87.61694, 41.86625), zoom=17, pitch=40, bearing=20, style=\"positron\"\n", ")\n", "m.add_basemap(\"OpenStreetMap.Mapnik\")\n", "data = \"https://maplibre.org/maplibre-gl-js/docs/assets/indoor-3d-map.geojson\"\n", "m.add_geojson(\n", " data,\n", " layer_type=\"fill-extrusion\",\n", " name=\"floorplan\",\n", " paint={\n", " \"fill-extrusion-color\": [\"get\", \"color\"],\n", " \"fill-extrusion-height\": [\"get\", \"height\"],\n", " \"fill-extrusion-base\": [\"get\", \"base_height\"],\n", " \"fill-extrusion-opacity\": 0.5,\n", " },\n", ")\n", "m" ] }, { "cell_type": "markdown", "id": "54", "metadata": {}, "source": [ "![](https://i.imgur.com/dteQlQC.png)" ] }, { "cell_type": "markdown", "id": "55", "metadata": {}, "source": [ "## Custom Basemap" ] }, { "cell_type": "code", "execution_count": null, "id": "56", "metadata": {}, "outputs": [], "source": [ "import leafmap.maplibregl as leafmap\n", "from maplibre.basemaps import construct_basemap_style\n", "from maplibre import Layer, LayerType, Map, MapOptions\n", "from maplibre.sources import GeoJSONSource\n", "\n", "\n", "bg_layer = Layer(\n", " type=LayerType.BACKGROUND,\n", " id=\"background\",\n", " source=None,\n", " paint={\"background-color\": \"darkblue\", \"background-opacity\": 0.8},\n", ")\n", "\n", "countries_source = GeoJSONSource(\n", " data=\"https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_0_countries.geojson\"\n", ")\n", "\n", "lines_layer = Layer(\n", " type=LayerType.LINE,\n", " source=\"countries\",\n", " paint={\"line-color\": \"white\", \"line-width\": 1.5},\n", ")\n", "\n", "polygons_layer = Layer(\n", " type=LayerType.FILL,\n", " source=\"countries\",\n", " paint={\"fill-color\": \"darkred\", \"fill-opacity\": 0.8},\n", ")\n", "\n", "custom_basemap = construct_basemap_style(\n", " layers=[bg_layer, polygons_layer, lines_layer],\n", " sources={\"countries\": countries_source},\n", ")\n", "\n", "\n", "m = leafmap.Map(style=custom_basemap)\n", "data = \"https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson\"\n", "m.add_geojson(\n", " data,\n", " layer_type=\"circle\",\n", " name=\"earthquakes\",\n", " paint={\"circle-color\": \"yellow\", \"circle-radius\": 5},\n", ")\n", "m.add_popup(\"earthquakes\", \"mag\")\n", "m" ] }, { "cell_type": "markdown", "id": "57", "metadata": {}, "source": [ "![](https://i.imgur.com/Bn9Kwje.png)" ] }, { "cell_type": "markdown", "id": "58", "metadata": {}, "source": [ "## H3 Grid UK Road Safety" ] }, { "cell_type": "code", "execution_count": null, "id": "59", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import h3" ] }, { "cell_type": "code", "execution_count": null, "id": "60", "metadata": {}, "outputs": [], "source": [ "RESOLUTION = 7\n", "COLORS = (\n", " \"lightblue\",\n", " \"turquoise\",\n", " \"lightgreen\",\n", " \"yellow\",\n", " \"orange\",\n", " \"darkred\",\n", ")\n", "\n", "road_safety = pd.read_csv(\n", " \"https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/3d-heatmap/heatmap-data.csv\"\n", ").dropna()\n", "\n", "\n", "def create_h3_grid(res=RESOLUTION) -> dict:\n", " road_safety[\"h3\"] = road_safety.apply(\n", " lambda x: h3.geo_to_h3(x[\"lat\"], x[\"lng\"], resolution=res), axis=1\n", " )\n", " df = road_safety.groupby(\"h3\").h3.agg(\"count\").to_frame(\"count\").reset_index()\n", " df[\"hexagon\"] = df.apply(\n", " lambda x: [h3.h3_to_geo_boundary(x[\"h3\"], geo_json=True)], axis=1\n", " )\n", " df[\"color\"] = pd.cut(\n", " df[\"count\"],\n", " bins=len(COLORS),\n", " labels=COLORS,\n", " )\n", " return leafmap.pandas_to_geojson(\n", " df, \"hexagon\", geometry_type=\"Polygon\", properties=[\"count\", \"color\"]\n", " )\n", "\n", "\n", "m = leafmap.Map(\n", " center=(-1.415727, 52.232395),\n", " zoom=7,\n", " pitch=40,\n", " bearing=-27,\n", ")\n", "try:\n", " data = create_h3_grid()\n", " m.add_geojson(\n", " data,\n", " layer_type=\"fill-extrusion\",\n", " paint={\n", " \"fill-extrusion-color\": [\"get\", \"color\"],\n", " \"fill-extrusion-opacity\": 0.7,\n", " \"fill-extrusion-height\": [\"*\", 100, [\"get\", \"count\"]],\n", " },\n", " )\n", "except:\n", " pass\n", "m" ] }, { "cell_type": "markdown", "id": "61", "metadata": {}, "source": [ "![](https://i.imgur.com/DYWmj5y.png)" ] }, { "cell_type": "markdown", "id": "62", "metadata": {}, "source": [ "## Deck.GL Layer" ] }, { "cell_type": "code", "execution_count": null, "id": "63", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(\n", " style=\"positron\",\n", " center=(-122.4, 37.74),\n", " zoom=12,\n", " pitch=40,\n", ")\n", "deck_grid_layer = {\n", " \"@@type\": \"GridLayer\",\n", " \"id\": \"GridLayer\",\n", " \"data\": \"https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json\",\n", " \"extruded\": True,\n", " \"getPosition\": \"@@=COORDINATES\",\n", " \"getColorWeight\": \"@@=SPACES\",\n", " \"getElevationWeight\": \"@@=SPACES\",\n", " \"elevationScale\": 4,\n", " \"cellSize\": 200,\n", " \"pickable\": True,\n", "}\n", "\n", "m.add_deck_layers([deck_grid_layer], tooltip=\"Number of points: {{ count }}\")\n", "m" ] }, { "cell_type": "markdown", "id": "64", "metadata": {}, "source": [ "![](https://i.imgur.com/xBVdT2u.png)" ] }, { "cell_type": "markdown", "id": "65", "metadata": {}, "source": [ "## Multiple Deck.GL Layers" ] }, { "cell_type": "code", "execution_count": null, "id": "66", "metadata": {}, "outputs": [], "source": [ "import requests" ] }, { "cell_type": "code", "execution_count": null, "id": "67", "metadata": {}, "outputs": [], "source": [ "data = requests.get(\n", " \"https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_airports.geojson\"\n", ").json()" ] }, { "cell_type": "code", "execution_count": null, "id": "68", "metadata": {}, "outputs": [], "source": [ "m = leafmap.Map(\n", " style=\"positron\",\n", " center=(0.45, 51.47),\n", " zoom=4,\n", " pitch=30,\n", ")\n", "deck_geojson_layer = {\n", " \"@@type\": \"GeoJsonLayer\",\n", " \"id\": \"airports\",\n", " \"data\": data,\n", " \"filled\": True,\n", " \"pointRadiusMinPixels\": 2,\n", " \"pointRadiusScale\": 2000,\n", " \"getPointRadius\": \"@@=11 - properties.scalerank\",\n", " \"getFillColor\": [200, 0, 80, 180],\n", " \"autoHighlight\": True,\n", " \"pickable\": True,\n", "}\n", "\n", "deck_arc_layer = {\n", " \"@@type\": \"ArcLayer\",\n", " \"id\": \"arcs\",\n", " \"data\": [\n", " feature\n", " for feature in data[\"features\"]\n", " if feature[\"properties\"][\"scalerank\"] < 4\n", " ],\n", " \"getSourcePosition\": [-0.4531566, 51.4709959], # London\n", " \"getTargetPosition\": \"@@=geometry.coordinates\",\n", " \"getSourceColor\": [0, 128, 200],\n", " \"getTargetColor\": [200, 0, 80],\n", " \"getWidth\": 2,\n", " \"pickable\": True,\n", "}\n", "\n", "m.add_deck_layers(\n", " [deck_geojson_layer, deck_arc_layer],\n", " tooltip={\n", " \"airports\": \"{{ &properties.name }}\",\n", " \"arcs\": \"gps_code: {{ properties.gps_code }}\",\n", " },\n", ")\n", "m" ] }, { "cell_type": "markdown", "id": "69", "metadata": {}, "source": [ "![](https://i.imgur.com/eFc4IbZ.png)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.2" } }, "nbformat": 4, "nbformat_minor": 5 }