From 70f8e86189e4acda1d80c156fb2eb988401444d1 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Thu, 20 Jun 2024 08:51:40 -0400 Subject: [PATCH] Add more MapLibre examples (#775) --- docs/maplibre/add_image.ipynb | 129 +++++++++++++++ docs/maplibre/add_image_generated.ipynb | 146 +++++++++++++++++ docs/maplibre/deckgl_layer.ipynb | 209 ++++++++++++++++++++++++ docs/maplibre/overview.md | 18 ++ leafmap/maplibregl.py | 85 ++++++++++ mkdocs.yml | 3 + 6 files changed, 590 insertions(+) create mode 100644 docs/maplibre/add_image.ipynb create mode 100644 docs/maplibre/add_image_generated.ipynb create mode 100644 docs/maplibre/deckgl_layer.ipynb diff --git a/docs/maplibre/add_image.ipynb b/docs/maplibre/add_image.ipynb new file mode 100644 index 000000000..cd858db87 --- /dev/null +++ b/docs/maplibre/add_image.ipynb @@ -0,0 +1,129 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/add_image.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/add_image.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Add an icon to the map**\n", + "\n", + "This source code of this example is adapted from the MapLibre GL JS example - [Add an icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-image).\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"leafmap[maplibre]\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import leafmap.maplibregl as leafmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run this notebook, you will need an [API key](https://docs.maptiler.com/cloud/api/authentication-key/) from [MapTiler](https://www.maptiler.com/cloud/). Once you have the API key, you can set it as an environment variable in your notebook or script as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"MAPTILER_KEY\"] = \"YOUR_API_KEY\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "MAPTILER_KEY = os.environ.get(\"MAPTILER_KEY\")\n", + "style = f\"https://api.maptiler.com/maps/streets/style.json?key={MAPTILER_KEY}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(center=[-1.80921, 0.349419], zoom=3, style=style)\n", + "image = \"https://upload.wikimedia.org/wikipedia/commons/7/7c/201408_cat.png\"\n", + "source = {\n", + " \"type\": \"geojson\",\n", + " \"data\": {\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [0, 0]}}\n", + " ],\n", + " },\n", + "}\n", + "\n", + "layer = {\n", + " \"id\": \"points\",\n", + " \"type\": \"symbol\",\n", + " \"source\": \"point\",\n", + " \"layout\": {\n", + " \"icon-image\": \"cat\",\n", + " \"icon-size\": 0.25,\n", + " \"text-field\": \"I love kitty!\",\n", + " \"text-font\": [\"Open Sans Regular\"],\n", + " \"text-offset\": [0, 3],\n", + " \"text-anchor\": \"top\",\n", + " },\n", + "}\n", + "m.add_image(\"cat\", image)\n", + "m.add_source(\"point\", source)\n", + "m.add_layer(layer)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/Nq1uV9d.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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/maplibre/add_image_generated.ipynb b/docs/maplibre/add_image_generated.ipynb new file mode 100644 index 000000000..bda21e801 --- /dev/null +++ b/docs/maplibre/add_image_generated.ipynb @@ -0,0 +1,146 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/add_image_generated.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/add_image_generated.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Add a generated icon to the map**\n", + "\n", + "This source code of this example is adapted from the MapLibre GL JS example - [Add a generated icon to the map](https://maplibre.org/maplibre-gl-js/docs/examples/add-image-generated).\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"leafmap[maplibre]\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import numpy as np\n", + "import leafmap.maplibregl as leafmap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To run this notebook, you will need an [API key](https://docs.maptiler.com/cloud/api/authentication-key/) from [MapTiler](https://www.maptiler.com/cloud/). Once you have the API key, you can set it as an environment variable in your notebook or script as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# os.environ[\"MAPTILER_KEY\"] = \"YOUR_API_KEY\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "MAPTILER_KEY = os.environ.get(\"MAPTILER_KEY\")\n", + "style = f\"https://api.maptiler.com/maps/streets/style.json?key={MAPTILER_KEY}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate the icon data\n", + "width = 64 # The image will be 64 pixels square\n", + "height = 64\n", + "bytes_per_pixel = 4 # Each pixel is represented by 4 bytes: red, green, blue, and alpha\n", + "data = np.zeros((width, width, bytes_per_pixel), dtype=np.uint8)\n", + "\n", + "for x in range(width):\n", + " for y in range(width):\n", + " data[y, x, 0] = int((y / width) * 255) # red\n", + " data[y, x, 1] = int((x / width) * 255) # green\n", + " data[y, x, 2] = 128 # blue\n", + " data[y, x, 3] = 255 # alpha\n", + "\n", + "# Flatten the data array\n", + "flat_data = data.flatten()\n", + "\n", + "# Create the image dictionary\n", + "image_dict = {\n", + " \"width\": width,\n", + " \"height\": height,\n", + " \"data\": flat_data.tolist(),\n", + "}\n", + "\n", + "m = leafmap.Map(center=[0, 0], zoom=1, style=style)\n", + "m.add_image(\"gradient\", image_dict)\n", + "source = {\n", + " \"type\": \"geojson\",\n", + " \"data\": {\n", + " \"type\": \"FeatureCollection\",\n", + " \"features\": [\n", + " {\"type\": \"Feature\", \"geometry\": {\"type\": \"Point\", \"coordinates\": [0, 0]}}\n", + " ],\n", + " },\n", + "}\n", + "\n", + "layer = {\n", + " \"id\": \"points\",\n", + " \"type\": \"symbol\",\n", + " \"source\": \"point\",\n", + " \"layout\": {\"icon-image\": \"gradient\"},\n", + "}\n", + "\n", + "m.add_source(\"point\", source)\n", + "m.add_layer(layer)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/qWWlnAm.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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/maplibre/deckgl_layer.ipynb b/docs/maplibre/deckgl_layer.ipynb new file mode 100644 index 000000000..82563e37d --- /dev/null +++ b/docs/maplibre/deckgl_layer.ipynb @@ -0,0 +1,209 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/deckgl_layer.ipynb)\n", + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/deckgl_layer.ipynb)\n", + "[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n", + "\n", + "**Add deck.gl layers**\n", + "\n", + "This source code of this example is adapted from the MapLibre GL JS example - [Create deck.gl layer using REST API](https://maplibre.org/maplibre-gl-js/docs/examples/add-deckgl-layer-using-rest-api).\n", + "\n", + "Uncomment the following line to install [leafmap](https://leafmap.org) if needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"leafmap[maplibre]\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import leafmap.maplibregl as leafmap" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(\n", + " style=\"positron\",\n", + " center=(37.74, -122.4),\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", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/rQR4687.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(\n", + " style=\"positron\",\n", + " center=(49.254, -123.13),\n", + " zoom=11,\n", + " pitch=45,\n", + ")\n", + "deck_grid_layer = {\n", + " \"@@type\": \"GeoJsonLayer\",\n", + " \"id\": \"GeoJsonLayer\",\n", + " \"data\": \"https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/geojson/vancouver-blocks.json\",\n", + " \"opacity\": 0.8,\n", + " \"stroked\": False,\n", + " \"filled\": True,\n", + " \"extruded\": True,\n", + " \"wireframe\": True,\n", + " \"getElevation\": \"@@=properties.valuePerSqm / 20\",\n", + " \"getFillColor\": [255, 255, \"@@=properties.growth * 255\"],\n", + " \"getLineColor\": [255, 255, 255],\n", + "}\n", + "m.add_deck_layers([deck_grid_layer])\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/rcO5RAD.png)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "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, + "metadata": {}, + "outputs": [], + "source": [ + "m = leafmap.Map(\n", + " style=\"positron\",\n", + " center=(51.47, 0.45),\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", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/mO1Z1Kz.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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/maplibre/overview.md b/docs/maplibre/overview.md index 097bbda96..dc6435066 100644 --- a/docs/maplibre/overview.md +++ b/docs/maplibre/overview.md @@ -26,6 +26,24 @@ Add a default marker to the map. [![](https://i.imgur.com/ufmqTzx.png)](https://leafmap.org/maplibre/add_marker) +## Add deck.gl layers + +Add deck.gl layers to the map. + +[![](https://i.imgur.com/rQR4687.png)](https://leafmap.org/maplibre/deckgl_layers) + +## Add an icon to the map + +Add an icon to the map from an external URL and use it in a symbol layer. + +[![](https://i.imgur.com/Nq1uV9d.png)](https://leafmap.org/maplibre/add_image) + +## Add a generated icon to the map + +Add an icon to the map that was generated at runtime. + +[![](https://i.imgur.com/qWWlnAm.png)](https://leafmap.org/maplibre/add_image_generated) + ## Add a video Display a video on top of a satellite raster baselayer. diff --git a/leafmap/maplibregl.py b/leafmap/maplibregl.py index cb2959e92..1da654544 100644 --- a/leafmap/maplibregl.py +++ b/leafmap/maplibregl.py @@ -1322,3 +1322,88 @@ def fly_to( kwargs["essential"] = essential super().add_call("flyTo", kwargs) + + def _read_image(self, image: str) -> Dict[str, Union[int, List[int]]]: + """ + Reads an image from a URL or a local file path and returns a dictionary + with the image data. + + Args: + image (str): The URL or local file path to the image. + + Returns: + Dict[str, Union[int, List[int]]]: A dictionary with the image width, + height, and flattened data. + + Raises: + ValueError: If the image argument is not a string representing a URL + or a local file path. + """ + + import os + from PIL import Image + import requests + from io import BytesIO + import numpy as np + + if isinstance(image, str): + try: + if os.path.isfile(image): + img = Image.open(image) + else: + response = requests.get(image) + img = Image.open(BytesIO(response.content)) + + width, height = img.size + + # Convert image to numpy array and then flatten it + img_data = np.array(img, dtype="uint8") + flat_img_data = img_data.flatten() + + # Create the image dictionary with the flattened data + image_dict = { + "width": width, + "height": height, + "data": flat_img_data.tolist(), # Convert to list if necessary + } + + return image_dict + except Exception as e: + print(e) + return None + else: + raise ValueError("The image must be a URL or a local file path.") + + def add_image( + self, id: str, image: Union[str, Dict], width: int = None, height: int = None + ) -> None: + """Add an image to the map. + + Args: + id (str): The layer ID of the image. + image (Union[str, Dict, np.ndarray]): The URL or local file path to + the image, or a dictionary containing image data, or a numpy + array representing the image. + width (int, optional): The width of the image. Defaults to None. + height (int, optional): The height of the image. Defaults to None. + + Returns: + None + """ + import numpy as np + + if isinstance(image, str): + image_dict = self._read_image(image) + elif isinstance(image, dict): + image_dict = image + elif isinstance(image, np.ndarray): + image_dict = { + "width": width, + "height": height, + "data": image.tolist(), + } + else: + raise ValueError( + "The image must be a URL, a local file path, or a numpy array." + ) + super().add_call("addImage", id, image_dict) diff --git a/mkdocs.yml b/mkdocs.yml index b2b25c24f..86a90832f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -142,6 +142,9 @@ nav: - maplibre/3d_indoor_mapping.ipynb - maplibre/3d_terrain.ipynb - maplibre/add_marker.ipynb + - maplibre/deckgl_layer.ipynb + - maplibre/add_image.ipynb + - maplibre/add_image_generated.ipynb - maplibre/fit_bounds.ipynb - maplibre/fly_to.ipynb - maplibre/fly_to_options.ipynb