diff --git a/examples/airports/marker.py b/.examples/airports/marker.py
similarity index 94%
rename from examples/airports/marker.py
rename to .examples/airports/marker.py
index f3b9f321..9c7565d0 100644
--- a/examples/airports/marker.py
+++ b/.examples/airports/marker.py
@@ -1,15 +1,8 @@
import json
import pandas as pd
-from maplibre import (
- Layer,
- LayerType,
- Map,
- MapContext,
- MapOptions,
- output_maplibregl,
- render_maplibregl,
-)
+from maplibre import (Layer, LayerType, Map, MapContext, MapOptions,
+ output_maplibregl, render_maplibregl)
from maplibre.basemaps import Carto
from maplibre.controls import Marker, MarkerOptions, Popup, PopupOptions
from maplibre.sources import GeoJSONSource
diff --git a/examples/circle_layer/app.py b/.examples/circle_layer/app.py
similarity index 100%
rename from examples/circle_layer/app.py
rename to .examples/circle_layer/app.py
diff --git a/examples/earthquakes_cluster/app.py b/.examples/earthquakes_cluster/app.py
similarity index 100%
rename from examples/earthquakes_cluster/app.py
rename to .examples/earthquakes_cluster/app.py
diff --git a/examples/every_person_in_manhattan/app.py b/.examples/every_person_in_manhattan/app.py
similarity index 93%
rename from examples/every_person_in_manhattan/app.py
rename to .examples/every_person_in_manhattan/app.py
index 36b96e66..b5523dac 100644
--- a/examples/every_person_in_manhattan/app.py
+++ b/.examples/every_person_in_manhattan/app.py
@@ -2,8 +2,15 @@
import pandas as pd
import shapely
-from maplibre import (Layer, LayerType, Map, MapContext, MapOptions,
- output_maplibregl, render_maplibregl)
+from maplibre import (
+ Layer,
+ LayerType,
+ Map,
+ MapContext,
+ MapOptions,
+ output_maplibregl,
+ render_maplibregl,
+)
from maplibre.basemaps import Carto
from maplibre.utils import df_to_geojson
from shiny import App, reactive, ui
diff --git a/.examples/every_person_in_manhattan/app2.py b/.examples/every_person_in_manhattan/app2.py
new file mode 100644
index 00000000..4c57839f
--- /dev/null
+++ b/.examples/every_person_in_manhattan/app2.py
@@ -0,0 +1,77 @@
+import geopandas as gpd
+import pandas as pd
+from maplibre import (
+ Layer,
+ LayerType,
+ Map,
+ MapContext,
+ MapOptions,
+ output_maplibregl,
+ render_maplibregl,
+)
+from maplibre.basemaps import Carto
+from maplibre.controls import ScaleControl
+from maplibre.sources import GeoJSONSource
+from maplibre.utils import geopandas_to_geojson
+from shiny import App, reactive, ui
+
+MALE_COLOR = "rgb(0, 128, 255)"
+FEMALE_COLOR = "rgb(255, 0, 128)"
+LAYER_ID = "every-person-in-manhattan-circles"
+CIRCLE_RADIUS = 2
+
+point_data = pd.read_json(
+ "https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/scatterplot/manhattan.json"
+)
+
+point_data.columns = ["lng", "lat", "sex"]
+
+point_data = gpd.GeoDataFrame(
+ point_data["sex"], geometry=gpd.points_from_xy(point_data.lng, point_data.lat)
+)
+
+every_person_in_manhattan_source = GeoJSONSource(data=geopandas_to_geojson(point_data))
+
+
+every_person_in_manhattan_circles = Layer(
+ type=LayerType.CIRCLE,
+ id=LAYER_ID,
+ source=every_person_in_manhattan_source,
+ paint={
+ "circle-color": ["match", ["get", "sex"], 1, MALE_COLOR, FEMALE_COLOR],
+ "circle-radius": CIRCLE_RADIUS,
+ },
+)
+
+map_options = MapOptions(
+ style=Carto.POSITRON,
+ bounds=point_data.total_bounds,
+ fit_bounds_options={"padding": 20},
+)
+
+app_ui = ui.page_fluid(
+ ui.panel_title("Every Person in Manhattan"),
+ output_maplibregl("maplibre", height=600),
+ ui.input_slider("radius", "Radius", value=CIRCLE_RADIUS, min=1, max=5),
+)
+
+
+def server(input, output, session):
+ @render_maplibregl
+ def maplibre():
+ m = Map(map_options)
+ m.add_control(ScaleControl(), position="bottom-left")
+ m.add_layer(every_person_in_manhattan_circles)
+ return m
+
+ @reactive.Effect
+ @reactive.event(input.radius, ignore_init=True)
+ async def radius():
+ async with MapContext("maplibre") as m:
+ m.set_paint_property(LAYER_ID, "circle-radius", input.radius())
+
+
+app = App(app_ui, server)
+
+if __name__ == "__main__":
+ app.run()
diff --git a/examples/experimental/app.py b/.examples/experimental/app.py
similarity index 100%
rename from examples/experimental/app.py
rename to .examples/experimental/app.py
diff --git a/examples/experimental/app2.py b/.examples/experimental/app2.py
similarity index 100%
rename from examples/experimental/app2.py
rename to .examples/experimental/app2.py
diff --git a/examples/experimental/app3.py b/.examples/experimental/app3.py
similarity index 100%
rename from examples/experimental/app3.py
rename to .examples/experimental/app3.py
diff --git a/examples/experimental/flights.py b/.examples/experimental/flights.py
similarity index 94%
rename from examples/experimental/flights.py
rename to .examples/experimental/flights.py
index 5026d750..24e783b6 100644
--- a/examples/experimental/flights.py
+++ b/.examples/experimental/flights.py
@@ -2,8 +2,14 @@
import pandas as pd
import shapely
-from maplibre import (Layer, LayerType, Map, MapContext, output_maplibregl,
- render_maplibregl)
+from maplibre import (
+ Layer,
+ LayerType,
+ Map,
+ MapContext,
+ output_maplibregl,
+ render_maplibregl,
+)
from maplibre.basemaps import Carto
from maplibre.utils import GeometryType, df_to_geojson
from shiny import App, reactive, ui
diff --git a/examples/experimental/points_to_h3_hexagons.py b/.examples/experimental/points_to_h3_hexagons.py
similarity index 100%
rename from examples/experimental/points_to_h3_hexagons.py
rename to .examples/experimental/points_to_h3_hexagons.py
diff --git a/examples/experimental/readme.txt b/.examples/experimental/readme.txt
similarity index 100%
rename from examples/experimental/readme.txt
rename to .examples/experimental/readme.txt
diff --git a/examples/fill_extrusion_layer/app.py b/.examples/fill_extrusion_layer/app.py
similarity index 100%
rename from examples/fill_extrusion_layer/app.py
rename to .examples/fill_extrusion_layer/app.py
diff --git a/examples/fill_layer/app.py b/.examples/fill_layer/app.py
similarity index 100%
rename from examples/fill_layer/app.py
rename to .examples/fill_layer/app.py
diff --git a/examples/h3_hexagons/app.py b/.examples/h3_hexagons/app.py
similarity index 96%
rename from examples/h3_hexagons/app.py
rename to .examples/h3_hexagons/app.py
index 8031eca5..1ef8a02c 100644
--- a/examples/h3_hexagons/app.py
+++ b/.examples/h3_hexagons/app.py
@@ -1,15 +1,8 @@
import h3
import pandas as pd
-
# import shapely
-from maplibre import (
- Layer,
- LayerType,
- Map,
- MapContext,
- output_maplibregl,
- render_maplibregl,
-)
+from maplibre import (Layer, LayerType, Map, MapContext, output_maplibregl,
+ render_maplibregl)
from maplibre.basemaps import Carto
from maplibre.utils import GeometryType, df_to_geojson, get_bounds
from shiny import App, reactive, ui
diff --git a/examples/heatmap_layer/app.py b/.examples/heatmap_layer/app.py
similarity index 100%
rename from examples/heatmap_layer/app.py
rename to .examples/heatmap_layer/app.py
diff --git a/examples/marker/app.py b/.examples/marker/app.py
similarity index 100%
rename from examples/marker/app.py
rename to .examples/marker/app.py
diff --git a/examples/motor_vehicle_collisions/app.py b/.examples/motor_vehicle_collisions/app.py
similarity index 100%
rename from examples/motor_vehicle_collisions/app.py
rename to .examples/motor_vehicle_collisions/app.py
diff --git a/examples/text_layer/app.py b/.examples/text_layer/app.py
similarity index 100%
rename from examples/text_layer/app.py
rename to .examples/text_layer/app.py
diff --git a/examples/to_html/app.py b/.examples/to_html/app.py
similarity index 100%
rename from examples/to_html/app.py
rename to .examples/to_html/app.py
diff --git a/examples/vancouver_blocks/line.py b/.examples/vancouver_blocks/line.py
similarity index 100%
rename from examples/vancouver_blocks/line.py
rename to .examples/vancouver_blocks/line.py
diff --git a/.github/workflows/docker-image-animation.yml b/.github/workflows/docker-image-animation.yml
new file mode 100644
index 00000000..62e7bcea
--- /dev/null
+++ b/.github/workflows/docker-image-animation.yml
@@ -0,0 +1,33 @@
+name: Docker Image CI
+
+on:
+ push:
+ branches: [ "feature/examples" ]
+ pull_request:
+ branches: [ "main" ]
+
+env:
+ IMAGE_NAME: hike-animation
+ VERSION: latest
+
+jobs:
+
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Build the Docker image
+ run: |
+ cd docs/examples/hike
+ docker build . --file Dockerfile --tag $IMAGE_NAME
+ - name: Log in to registry
+ run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
+ - name: Push image
+ run: |
+ IMAGE_ID=ghcr.io/$GITHUB_REPOSITORY/$IMAGE_NAME
+ # repository name must be lowercase
+ IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
+ docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
+ docker push $IMAGE_ID:$VERSION
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
index 9c166dcd..013080be 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/docker-image.yml
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ "main" ]
+env:
+ IMAGE_NAME: vancouver-blocks
+ VERSION: latest
+
jobs:
build:
@@ -16,5 +20,14 @@ jobs:
- uses: actions/checkout@v3
- name: Build the Docker image
run: |
- cd docs/examples/vancouver-blocks
- docker build . --file Dockerfile --tag my-image-name:$(date +%s)
+ cd docs/examples/vancouver_blocks
+ docker build . --file Dockerfile --tag $IMAGE_NAME
+ - name: Log in to registry
+ run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
+ - name: Push image
+ run: |
+ IMAGE_ID=ghcr.io/$GITHUB_REPOSITORY/$IMAGE_NAME
+ # repository name must be lowercase
+ IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
+ docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
+ docker push $IMAGE_ID:$VERSION
diff --git a/.github/workflows/docker-image2.yml b/.github/workflows/docker-image2.yml
new file mode 100644
index 00000000..e7d9ced8
--- /dev/null
+++ b/.github/workflows/docker-image2.yml
@@ -0,0 +1,33 @@
+name: Docker Image CI
+
+on:
+ push:
+ branches: [ "dev" ]
+ pull_request:
+ branches: [ "main" ]
+
+env:
+ IMAGE_NAME: airports
+ VERSION: latest
+
+jobs:
+
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Build the Docker image
+ run: |
+ cd docs/examples/airports
+ docker build . --file Dockerfile --tag $IMAGE_NAME
+ - name: Log in to registry
+ run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
+ - name: Push image
+ run: |
+ IMAGE_ID=ghcr.io/$GITHUB_REPOSITORY/$IMAGE_NAME
+ # repository name must be lowercase
+ IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
+ docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
+ docker push $IMAGE_ID:$VERSION
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
new file mode 100644
index 00000000..ae7cc3df
--- /dev/null
+++ b/.github/workflows/pytest.yml
@@ -0,0 +1,30 @@
+name: Python package
+
+on: [push]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ # You can test your matrix by printing the current Python version
+ - name: Display Python version
+ run: python -c "import sys; print(sys.version)"
+ - name: Install Poetry and pytest
+ run: pip install poetry pytest
+ - name: Install package
+ run: poetry install
+ - name: Test package
+ run: |
+ poetry run pytest
+ poetry run pytest --doctest-modules maplibre --ignore maplibre/ipywidget.py
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..03ed3ca0
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,14 @@
+# Changelog for MapLibre for Python
+
+## maplibre v0.1.2
+
+* Add `Map.set_data`
+* Add `Map.set_visibility`
+* Do not import `ipywidget.MapWidget` in `__init__` and skip tests for `MapWidget`, because it causes a `core dumped` error, see [anywidget issue](https://github.com/manzt/anywidget/issues/374)
+* Remove `requests` dependency
+* Remove dead code
+* Add more examples
+
+## maplibre v0.1.1
+
+* Initial PyPI release
diff --git a/README.md b/README.md
index 4ceb2ad4..a1388f7c 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,9 @@
# MapLibre for Python
+[![Release](https://img.shields.io/github/v/release/eodaGmbH/py-maplibregl)](https://img.shields.io/github/v/release/eodaGmbH/py-maplibregl)
+[![Build status](https://img.shields.io/github/actions/workflow/status/eodaGmbH/py-maplibregl/pytest.yaml?branch=main)](https://img.shields.io/github/actions/workflow/status/eodaGmbH/py-maplibregl/pytest.yaml?branch=main)
+[![License](https://img.shields.io/github/license/eodaGmbH/py-maplibregl)](https://img.shields.io/github/license/eodaGmbH/py-maplibregl)
+
MapLibre for Python provides Python bindings for [MapLibre GL JS](https://github.com/maplibre/maplibre-gl-js).
It integrates seamlessly into [Shiny for Python](https://github.com/posit-dev/py-shiny) and [Jupyter](https://jupyter.org/).
@@ -20,6 +24,10 @@ pip install "maplibre[all] @ git+https://github.com/eodaGmbH/py-maplibregl@dev"
## Getting started
+```bash
+docker run --rm -p 8050:8050 ghcr.io/eodagmbh/py-maplibregl/vancouver-blocks:latest
+```
+
* [Basic usage](https://eodagmbh.github.io/py-maplibregl/)
* [API Documentation](https://eodagmbh.github.io/py-maplibregl/api/map/)
* [Examples](https://eodagmbh.github.io/py-maplibregl/examples/every_person_in_manhattan/)
diff --git a/docs/api/controls.md b/docs/api/controls.md
index 5a0b6198..90e065f0 100644
--- a/docs/api/controls.md
+++ b/docs/api/controls.md
@@ -11,3 +11,9 @@
docstring_section_style: list
::: maplibre.controls.FullscreenControl
+
+::: maplibre.controls.ScaleControl
+
+::: maplibre.controls.NavigationControl
+
+::: maplibre.controls.GeolocateControl
diff --git a/docs/api/map.md b/docs/api/map.md
index 8e92fd89..a98122cb 100644
--- a/docs/api/map.md
+++ b/docs/api/map.md
@@ -2,7 +2,9 @@
::: maplibre.MapOptions
+::: maplibre.MapContext
+
::: maplibre.ipywidget.MapWidget
options:
- inherited_members: true
+ inherited_members: false
\ No newline at end of file
diff --git a/docs/examples/3d_indoor_mapping/app.html b/docs/examples/3d_indoor_mapping/app.html
new file mode 100644
index 00000000..a8eb1593
--- /dev/null
+++ b/docs/examples/3d_indoor_mapping/app.html
@@ -0,0 +1,163 @@
+
+
+
+
Pymaplibregl
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/3d_indoor_mapping/app.py b/docs/examples/3d_indoor_mapping/app.py
index 337b8638..dcf072fd 100644
--- a/docs/examples/3d_indoor_mapping/app.py
+++ b/docs/examples/3d_indoor_mapping/app.py
@@ -1,10 +1,12 @@
+import sys
import webbrowser
from maplibre import Layer, LayerType, Map, MapOptions
from maplibre.basemaps import background
from maplibre.sources import GeoJSONSource, RasterTileSource
-TEMP_FILE = "/tmp/pymaplibregl_temp.html"
+file_name = "/tmp/pymaplibregl_temp.html"
+
FLOORPLAN_SOURCE_ID = "floorplan"
raster_source = RasterTileSource(
@@ -52,7 +54,10 @@ def create_map():
if __name__ == "__main__":
m = create_map()
- with open(TEMP_FILE, "w") as f:
+ if len(sys.argv) == 2:
+ file_name = sys.argv[1]
+
+ with open(file_name, "w") as f:
f.write(m.to_html())
- webbrowser.open(TEMP_FILE)
+ webbrowser.open(file_name)
diff --git a/docs/examples/3d_indoor_mapping/index.md b/docs/examples/3d_indoor_mapping/index.md
index 7b9d38fd..9f231f34 100644
--- a/docs/examples/3d_indoor_mapping/index.md
+++ b/docs/examples/3d_indoor_mapping/index.md
@@ -1,3 +1,5 @@
+See example in action
+
```python
-8<-- "3d_indoor_mapping/app.py"
```
diff --git a/docs/examples/airports/Dockerfile b/docs/examples/airports/Dockerfile
new file mode 100644
index 00000000..71835dd1
--- /dev/null
+++ b/docs/examples/airports/Dockerfile
@@ -0,0 +1,7 @@
+FROM bitnami/python:3.11.5
+
+RUN pip install maplibre pandas
+
+COPY ./app.py ./app.py
+
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8050"]
diff --git a/docs/examples/custom_basemap/app.html b/docs/examples/custom_basemap/app.html
new file mode 100644
index 00000000..bc0913ba
--- /dev/null
+++ b/docs/examples/custom_basemap/app.html
@@ -0,0 +1,163 @@
+
+
+
+Pymaplibregl
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/custom_basemap/app.py b/docs/examples/custom_basemap/app.py
index df831ecb..a2fb7967 100644
--- a/docs/examples/custom_basemap/app.py
+++ b/docs/examples/custom_basemap/app.py
@@ -1,10 +1,11 @@
+import sys
import webbrowser
from maplibre import Layer, LayerType, Map, MapOptions
from maplibre.basemaps import construct_basemap_style
from maplibre.sources import GeoJSONSource
-TEMP_FILE = "/tmp/pymaplibregl_temp.html"
+file_name = "/tmp/pymaplibregl_temp.html"
bg_layer = Layer(
type=LayerType.BACKGROUND,
@@ -60,7 +61,10 @@ def create_map():
if __name__ == "__main__":
m = create_map()
- with open(TEMP_FILE, "w") as f:
+ if len(sys.argv) == 2:
+ file_name = sys.argv[1]
+
+ with open(file_name, "w") as f:
f.write(m.to_html())
- webbrowser.open(TEMP_FILE)
+ webbrowser.open(file_name)
diff --git a/docs/examples/custom_basemap/index.md b/docs/examples/custom_basemap/index.md
index 6bbd4385..608ef7df 100644
--- a/docs/examples/custom_basemap/index.md
+++ b/docs/examples/custom_basemap/index.md
@@ -2,6 +2,8 @@
-8<-- "custom_basemap/app.py"
```
+See example in action
+
Run example:
``` bash
diff --git a/docs/examples/earthquake_clusters/Dockerfile b/docs/examples/earthquake_clusters/Dockerfile
new file mode 100644
index 00000000..e8bec61c
--- /dev/null
+++ b/docs/examples/earthquake_clusters/Dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.11.5
+
+RUN pip install maplibre
+
+COPY ./app.py ./app.py
+
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8050"]
diff --git a/docs/examples/every_person_in_manhattan/Dockerfile b/docs/examples/every_person_in_manhattan/Dockerfile
new file mode 100644
index 00000000..389b411b
--- /dev/null
+++ b/docs/examples/every_person_in_manhattan/Dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.11.5
+
+RUN pip install maplibre pandas shapely
+
+COPY ./app.py ./app.py
+
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8050"]
diff --git a/docs/examples/geopandas/app.html b/docs/examples/geopandas/app.html
new file mode 100644
index 00000000..e8578429
--- /dev/null
+++ b/docs/examples/geopandas/app.html
@@ -0,0 +1,163 @@
+
+
+
+Pymaplibregl
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/geopandas/app.py b/docs/examples/geopandas/app.py
index fefca9d9..bafebd69 100644
--- a/docs/examples/geopandas/app.py
+++ b/docs/examples/geopandas/app.py
@@ -1,3 +1,4 @@
+import sys
import webbrowser
from maplibre import Layer, LayerType, Map, MapOptions
@@ -6,7 +7,7 @@
import geopandas as gpd
-TEMP_FILE = "/tmp/pymaplibregl_temp.html"
+file_name = "/tmp/pymaplibregl_temp.html"
LAYER_ID = "wilderness"
df_geo = gpd.read_file(
@@ -34,7 +35,10 @@ def create_map():
if __name__ == "__main__":
m = create_map()
- with open(TEMP_FILE, "w") as f:
+ if len(sys.argv) == 2:
+ file_name = sys.argv[1]
+
+ with open(file_name, "w") as f:
f.write(m.to_html())
- webbrowser.open(TEMP_FILE)
+ webbrowser.open(file_name)
diff --git a/docs/examples/geopandas/index.md b/docs/examples/geopandas/index.md
index b856f565..fffccb96 100644
--- a/docs/examples/geopandas/index.md
+++ b/docs/examples/geopandas/index.md
@@ -1,3 +1,5 @@
+See example in action
+
```python
-8<-- "geopandas/app.py"
```
diff --git a/docs/examples/getting_started/Dockerfile b/docs/examples/getting_started/Dockerfile
new file mode 100644
index 00000000..6ee41fe2
--- /dev/null
+++ b/docs/examples/getting_started/Dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.11.5
+
+RUN pip install maplibre
+
+COPY ./basic_usage_shiny.py ./app.py
+
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8050"]
diff --git a/docs/examples/getting_started/basic_usage_shiny.py b/docs/examples/getting_started/basic_usage_shiny.py
index 3cf092b4..f0418037 100644
--- a/docs/examples/getting_started/basic_usage_shiny.py
+++ b/docs/examples/getting_started/basic_usage_shiny.py
@@ -4,6 +4,7 @@
app_ui = ui.page_fluid(
output_maplibregl("maplibre", height=600),
+ ui.div("Click on map to set a marker"),
)
@@ -18,7 +19,10 @@ def maplibre():
async def coords():
async with MapContext("maplibre") as m:
print(input.maplibre())
- m.add_marker(Marker(lng_lat=input.maplibre()["coords"].values()))
+ lng_lat = tuple(input.maplibre()["coords"].values())
+ marker = Marker(lng_lat=lng_lat)
+ m.add_marker(marker)
+ m.add_call("flyTo", {"center": lng_lat})
app = App(app_ui, server)
diff --git a/docs/examples/hike/Dockerfile b/docs/examples/hike/Dockerfile
new file mode 100644
index 00000000..d41f777c
--- /dev/null
+++ b/docs/examples/hike/Dockerfile
@@ -0,0 +1,8 @@
+FROM python:3.11.5
+
+# RUN pip install maplibre geopandas
+RUN pip install "maplibre[all] @ git+https://github.com/eodaGmbH/py-maplibregl@feature/examples"
+
+COPY ./app.py ./app.py
+
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8050"]
diff --git a/docs/examples/hike/app.py b/docs/examples/hike/app.py
new file mode 100644
index 00000000..06ec65ba
--- /dev/null
+++ b/docs/examples/hike/app.py
@@ -0,0 +1,83 @@
+import asyncio
+
+import geopandas as gpd
+from maplibre import (
+ Layer,
+ LayerType,
+ Map,
+ MapContext,
+ MapOptions,
+ output_maplibregl,
+ render_maplibregl,
+)
+from maplibre.controls import Marker
+from maplibre.sources import GeoJSONSource
+from shiny import App, reactive, ui
+
+SOURCE_ID = "hike"
+LAYER_ID = "hike"
+data = gpd.read_file("https://docs.mapbox.com/mapbox-gl-js/assets/hike.geojson")
+# print(data)
+# print(data.get_coordinates().to_numpy()[0])
+
+
+feature_collection = {
+ "type": "FeatureCollection",
+ "features": [
+ {"type": "Feature", "geometry": {"type": "LineString", "coordinates": []}}
+ ],
+}
+print(feature_collection)
+
+hike = GeoJSONSource(data="https://docs.mapbox.com/mapbox-gl-js/assets/hike.geojson")
+layer = Layer(
+ type=LayerType.LINE,
+ id=LAYER_ID,
+ source="hike",
+ paint={"line-color": "orange", "line-width": 3},
+ layout={"visibility": "visible"},
+)
+
+app_ui = ui.page_fluid(
+ output_maplibregl("mapylibre", height=600),
+ ui.input_action_button("run", "Run"),
+)
+
+
+def server(input, output, session):
+ @render_maplibregl
+ def mapylibre():
+ m = Map(
+ # MapOptions(center=data.get_coordinates().to_numpy()[0], zoom=16, pitch=30)
+ MapOptions(bounds=data.total_bounds, fit_bounds_options={"padding": 10})
+ )
+ m.add_source(SOURCE_ID, hike)
+ m.add_layer(layer)
+ m.add_source("hike-animation", GeoJSONSource(data=feature_collection))
+ m.add_layer(
+ Layer(
+ type=LayerType.LINE,
+ id="hike-animation",
+ source="hike-animation",
+ paint={"line-color": "yellow", "line-width": 5},
+ )
+ )
+ return m
+
+ @reactive.Effect
+ @reactive.event(input.run)
+ async def run():
+ for lng_lat in data.get_coordinates().to_numpy():
+ # print(lng_lat)
+ feature_collection["features"][0]["geometry"]["coordinates"].append(
+ tuple(lng_lat)
+ )
+ async with MapContext("mapylibre") as m:
+ m.set_data("hike-animation", feature_collection)
+ m.add_call("panTo", tuple(lng_lat))
+ # await asyncio.sleep(0.005)
+
+ feature_collection["features"][0]["geometry"]["coordinates"] = []
+
+
+app = App(app_ui, server)
diff --git a/docs/examples/jupyter/getting_started.ipynb b/docs/examples/jupyter/getting_started.ipynb
index 1e7a794d..388953c0 100644
--- a/docs/examples/jupyter/getting_started.ipynb
+++ b/docs/examples/jupyter/getting_started.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 1,
"id": "8ae5dd75-d944-4304-88c6-ec2db700dcec",
"metadata": {},
"outputs": [],
@@ -70,22 +70,22 @@
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 4,
"id": "e6e2284c-1862-4697-ad94-f535b3682197",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "32df09ba11cb4c64b7c79c3e389e4662",
+ "model_id": "ece4babe53924aa7886553d1b5d53bec",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "MapWidget(height='400px', map_options={'style': 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.j…"
+ "MapWidget(calls=[['addControl', ('ScaleControl', {'unit': 'metric'}, 'bottom-left')], ['addLayer', ({'id': 'ea…"
]
},
- "execution_count": 9,
+ "execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -109,14 +109,14 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 6,
"id": "356960fa-b866-42c8-a58e-0c9a417c28eb",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "437bd09dbdd24f0896193788a76610b4",
+ "model_id": "60ef57a918734016b9766e41615c1935",
"version_major": 2,
"version_minor": 0
},
@@ -133,7 +133,7 @@
"(radius)>"
]
},
- "execution_count": 5,
+ "execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
@@ -284,9 +284,143 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 5,
"id": "7ad74d91-1137-45b4-8791-83dc3546535e",
"metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Help on _InteractFactory in module ipywidgets.widgets.interaction object:\n",
+ "\n",
+ "class _InteractFactory(builtins.object)\n",
+ " | _InteractFactory(cls, options, kwargs={})\n",
+ " | \n",
+ " | Factory for instances of :class:`interactive`.\n",
+ " | \n",
+ " | This class is needed to support options like::\n",
+ " | \n",
+ " | >>> @interact.options(manual=True)\n",
+ " | ... def greeting(text=\"World\"):\n",
+ " | ... print(\"Hello {}\".format(text))\n",
+ " | \n",
+ " | Parameters\n",
+ " | ----------\n",
+ " | cls : class\n",
+ " | The subclass of :class:`interactive` to construct.\n",
+ " | options : dict\n",
+ " | A dict of options used to construct the interactive\n",
+ " | function. By default, this is returned by\n",
+ " | ``cls.default_options()``.\n",
+ " | kwargs : dict\n",
+ " | A dict of **kwargs to use for widgets.\n",
+ " | \n",
+ " | Methods defined here:\n",
+ " | \n",
+ " | __call__(self, _InteractFactory__interact_f=None, **kwargs)\n",
+ " | Make the given function interactive by adding and displaying\n",
+ " | the corresponding :class:`interactive` widget.\n",
+ " | \n",
+ " | Expects the first argument to be a function. Parameters to this\n",
+ " | function are widget abbreviations passed in as keyword arguments\n",
+ " | (``**kwargs``). Can be used as a decorator (see examples).\n",
+ " | \n",
+ " | Returns\n",
+ " | -------\n",
+ " | f : __interact_f with interactive widget attached to it.\n",
+ " | \n",
+ " | Parameters\n",
+ " | ----------\n",
+ " | __interact_f : function\n",
+ " | The function to which the interactive widgets are tied. The `**kwargs`\n",
+ " | should match the function signature. Passed to :func:`interactive()`\n",
+ " | **kwargs : various, optional\n",
+ " | An interactive widget is created for each keyword argument that is a\n",
+ " | valid widget abbreviation. Passed to :func:`interactive()`\n",
+ " | \n",
+ " | Examples\n",
+ " | --------\n",
+ " | Render an interactive text field that shows the greeting with the passed in\n",
+ " | text::\n",
+ " | \n",
+ " | # 1. Using interact as a function\n",
+ " | def greeting(text=\"World\"):\n",
+ " | print(\"Hello {}\".format(text))\n",
+ " | interact(greeting, text=\"Jupyter Widgets\")\n",
+ " | \n",
+ " | # 2. Using interact as a decorator\n",
+ " | @interact\n",
+ " | def greeting(text=\"World\"):\n",
+ " | print(\"Hello {}\".format(text))\n",
+ " | \n",
+ " | # 3. Using interact as a decorator with named parameters\n",
+ " | @interact(text=\"Jupyter Widgets\")\n",
+ " | def greeting(text=\"World\"):\n",
+ " | print(\"Hello {}\".format(text))\n",
+ " | \n",
+ " | Render an interactive slider widget and prints square of number::\n",
+ " | \n",
+ " | # 1. Using interact as a function\n",
+ " | def square(num=1):\n",
+ " | print(\"{} squared is {}\".format(num, num*num))\n",
+ " | interact(square, num=5)\n",
+ " | \n",
+ " | # 2. Using interact as a decorator\n",
+ " | @interact\n",
+ " | def square(num=2):\n",
+ " | print(\"{} squared is {}\".format(num, num*num))\n",
+ " | \n",
+ " | # 3. Using interact as a decorator with named parameters\n",
+ " | @interact(num=5)\n",
+ " | def square(num=2):\n",
+ " | print(\"{} squared is {}\".format(num, num*num))\n",
+ " | \n",
+ " | __init__(self, cls, options, kwargs={})\n",
+ " | Initialize self. See help(type(self)) for accurate signature.\n",
+ " | \n",
+ " | options(self, **kwds)\n",
+ " | Change options for interactive functions.\n",
+ " | \n",
+ " | Returns\n",
+ " | -------\n",
+ " | A new :class:`_InteractFactory` which will apply the\n",
+ " | options when called.\n",
+ " | \n",
+ " | widget(self, f)\n",
+ " | Return an interactive function widget for the given function.\n",
+ " | \n",
+ " | The widget is only constructed, not displayed nor attached to\n",
+ " | the function.\n",
+ " | \n",
+ " | Returns\n",
+ " | -------\n",
+ " | An instance of ``self.cls`` (typically :class:`interactive`).\n",
+ " | \n",
+ " | Parameters\n",
+ " | ----------\n",
+ " | f : function\n",
+ " | The function to which the interactive widgets are tied.\n",
+ " | \n",
+ " | ----------------------------------------------------------------------\n",
+ " | Data descriptors defined here:\n",
+ " | \n",
+ " | __dict__\n",
+ " | dictionary for instance variables (if defined)\n",
+ " | \n",
+ " | __weakref__\n",
+ " | list of weak references to the object (if defined)\n",
+ "\n"
+ ]
+ }
+ ],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9d34e0d6-7ff9-4f72-9af7-e8a19d51604a",
+ "metadata": {},
"outputs": [],
"source": []
}
diff --git a/docs/examples/road_safety/app.html b/docs/examples/road_safety/app.html
new file mode 100644
index 00000000..1482e9a7
--- /dev/null
+++ b/docs/examples/road_safety/app.html
@@ -0,0 +1,167 @@
+
+
+
+Pymaplibregl
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/road_safety/app.py b/docs/examples/road_safety/app.py
new file mode 100644
index 00000000..2e8b9c8d
--- /dev/null
+++ b/docs/examples/road_safety/app.py
@@ -0,0 +1,103 @@
+import sys
+import webbrowser
+
+import h3
+import pandas as pd
+from maplibre import (
+ Layer,
+ LayerType,
+ Map,
+ MapContext,
+ MapOptions,
+ output_maplibregl,
+ render_maplibregl,
+)
+from maplibre.sources import GeoJSONSource
+from maplibre.utils import df_to_geojson
+from shiny import App, reactive, ui
+
+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 df_to_geojson(
+ df, "hexagon", geometry_type="Polygon", properties=["count", "color"]
+ )
+
+
+source = GeoJSONSource(data=create_h3_grid())
+
+
+def create_map() -> Map:
+ m = Map(MapOptions(center=(-1.415727, 52.232395), zoom=7, pitch=40, bearing=-27))
+ m.add_layer(
+ Layer(
+ id="road-safety",
+ type=LayerType.FILL_EXTRUSION,
+ source=source,
+ paint={
+ "fill-extrusion-color": ["get", "color"],
+ "fill-extrusion-opacity": 0.7,
+ "fill-extrusion-height": ["*", 100, ["get", "count"]],
+ },
+ )
+ )
+ m.add_tooltip("road-safety", "count")
+ return m
+
+
+app_ui = ui.page_fluid(
+ ui.panel_title("Road safety in UK"),
+ output_maplibregl("mapylibre", height=700),
+ ui.input_slider("res", "Resolution", min=4, max=8, step=1, value=RESOLUTION),
+)
+
+
+def server(input, output, session):
+ @render_maplibregl
+ def mapylibre():
+ return create_map()
+
+ @reactive.Effect
+ @reactive.event(input.res, ignore_init=True)
+ async def resolution():
+ async with MapContext("mapylibre") as m:
+ with ui.Progress() as p:
+ p.set(message="H3 calculation in progress")
+ m.set_data("road-safety", create_h3_grid(input.res()))
+ p.set(1, message="Calculation finished")
+
+
+app = App(app_ui, server)
+
+if __name__ == "__main__":
+ filename = sys.argv[1] if len(sys.argv) == 2 else "/tmp/road_safety.html"
+ with open(filename, "w") as f:
+ m = create_map()
+ f.write(m.to_html(style="height: 700px;"))
+
+ webbrowser.open(filename)
diff --git a/docs/examples/road_safety/index.md b/docs/examples/road_safety/index.md
new file mode 100644
index 00000000..4173e20c
--- /dev/null
+++ b/docs/examples/road_safety/index.md
@@ -0,0 +1,11 @@
+See example in action (without reactive effects)
+
+```python
+-8<-- "road_safety/app.py"
+```
+
+Run example:
+
+``` bash
+poetry run uvicorn docs.examples.road_safety.app:app --reload
+```
diff --git a/docs/examples/vancouver_blocks/Dockerfile b/docs/examples/vancouver_blocks/Dockerfile
new file mode 100644
index 00000000..e8bec61c
--- /dev/null
+++ b/docs/examples/vancouver_blocks/Dockerfile
@@ -0,0 +1,7 @@
+FROM python:3.11.5
+
+RUN pip install maplibre
+
+COPY ./app.py ./app.py
+
+CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8050"]
diff --git a/docs/examples/vancouver_blocks/app.html b/docs/examples/vancouver_blocks/app.html
new file mode 100644
index 00000000..d5cebb70
--- /dev/null
+++ b/docs/examples/vancouver_blocks/app.html
@@ -0,0 +1,163 @@
+
+
+
+Pymaplibregl
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/vancouver_blocks/app.py b/docs/examples/vancouver_blocks/app.py
index 648389fe..8b1b7c97 100644
--- a/docs/examples/vancouver_blocks/app.py
+++ b/docs/examples/vancouver_blocks/app.py
@@ -1,3 +1,5 @@
+import sys
+
from maplibre import (
Layer,
LayerType,
@@ -42,8 +44,8 @@
[0, "grey"],
[1000, "yellow"],
[5000, "orange"],
- [10000, "red"],
- [50000, "darkred"],
+ [10000, "darkred"],
+ [50000, "lightblue"],
],
},
"fill-extrusion-height": ["*", 10, ["sqrt", ["get", "valuePerSqm"]]],
@@ -59,31 +61,43 @@
bearing=0,
)
+
+def create_map() -> Map:
+ m = Map(map_options)
+ m.add_control(ScaleControl(), position="bottom-left")
+ m.add_source(SOURCE_ID, vancouver_blocks_source)
+ m.add_layer(vancouver_blocks_lines)
+ m.add_layer(vancouver_blocks_fill)
+ m.add_tooltip(LAYER_ID_FILL, "valuePerSqm")
+ return m
+
+
app_ui = ui.page_fluid(
ui.panel_title("Vancouver Property Value"),
ui.div(
"Height of polygons - average property value per square meter of lot",
style="padding: 10px;",
),
- output_maplibregl("maplibre", height=700),
+ output_maplibregl("maplibre", height=600),
ui.input_select(
"filter",
"max property value per square meter",
choices=[0, 1000, 5000, 10000, 50000, 100000, MAX_FILTER_VALUE],
selected=MAX_FILTER_VALUE,
),
+ ui.input_checkbox_group(
+ "layers",
+ "Layers",
+ choices=[LAYER_ID_FILL, LAYER_ID_LINES],
+ selected=[LAYER_ID_FILL, LAYER_ID_LINES],
+ ),
)
def server(input, output, session):
@render_maplibregl
def maplibre():
- m = Map(map_options)
- m.add_control(ScaleControl(), position="bottom-left")
- m.add_source(SOURCE_ID, vancouver_blocks_source)
- m.add_layer(vancouver_blocks_lines)
- m.add_layer(vancouver_blocks_fill)
- m.add_tooltip(LAYER_ID_FILL, "valuePerSqm")
+ m = create_map()
return m
@reactive.Effect
@@ -93,8 +107,22 @@ async def filter():
filter_ = ["<=", ["get", "valuePerSqm"], int(input.filter())]
m.set_filter(LAYER_ID_FILL, filter_)
+ @reactive.Effect
+ @reactive.event(input.layers)
+ async def layers():
+ visible_layers = input.layers()
+ async with MapContext("maplibre") as m:
+ for layer in [LAYER_ID_FILL, LAYER_ID_LINES]:
+ m.set_visibility(layer, layer in visible_layers)
+
app = App(app_ui, server)
if __name__ == "__main__":
- app.run()
+ if len(sys.argv) == 2:
+ file_name = sys.argv[1]
+ m = create_map()
+ with open(file_name, "w") as f:
+ f.write(m.to_html())
+ else:
+ app.run()
diff --git a/docs/examples/vancouver_blocks/index.md b/docs/examples/vancouver_blocks/index.md
index 3b668664..5af20d82 100644
--- a/docs/examples/vancouver_blocks/index.md
+++ b/docs/examples/vancouver_blocks/index.md
@@ -1,3 +1,5 @@
+See example in action (without reactive effects)
+
```python
-8<-- "vancouver_blocks/app.py"
```
diff --git a/docs/examples/where_is_the_iss/app.py b/docs/examples/where_is_the_iss/app.py
new file mode 100644
index 00000000..d1daf24c
--- /dev/null
+++ b/docs/examples/where_is_the_iss/app.py
@@ -0,0 +1,94 @@
+import requests
+from maplibre import (
+ Layer,
+ LayerType,
+ Map,
+ MapContext,
+ MapOptions,
+ output_maplibregl,
+ render_maplibregl,
+)
+from maplibre.sources import GeoJSONSource
+from shiny import App, reactive, ui
+
+MAX_FEATURES = 30
+SOURCE_ID_ISS_POSITION = "iss_position"
+SOURCE_ID_ISS_LAST_POSITIONS = "iss-last-positions"
+
+
+def where_is_the_iss() -> tuple:
+ r = requests.get("https://api.wheretheiss.at/v1/satellites/25544").json()
+ return (r["longitude"], r["latitude"])
+
+
+def create_feature(lng_lat: tuple) -> dict:
+ return {
+ "type": "Feature",
+ "geometry": {
+ "type": "Point",
+ "coordinates": lng_lat,
+ },
+ "properties": {"coords": ", ".join(map(str, lng_lat))},
+ }
+
+
+lng_lat = where_is_the_iss()
+feature = create_feature(where_is_the_iss())
+feature_collection = {"type": "FeatureCollection", "features": [feature]}
+
+app_ui = ui.page_fluid(
+ ui.panel_title("Where is the ISS"),
+ ui.div("Click on Update to get the current position of the ISS."),
+ ui.div(
+ "The yellow dots show the positions before. Hover the blue dot to display the coordinates."
+ ),
+ output_maplibregl("mapylibre", height=600),
+ ui.input_action_button("update", "Update"),
+)
+
+
+def server(input, output, session):
+ @render_maplibregl
+ def mapylibre():
+ m = Map(MapOptions(center=lng_lat, zoom=3))
+ m.set_paint_property("water", "fill-color", "darkblue")
+ m.add_source(
+ SOURCE_ID_ISS_LAST_POSITIONS, GeoJSONSource(data=feature_collection)
+ )
+ m.add_layer(
+ Layer(
+ type=LayerType.CIRCLE,
+ source=SOURCE_ID_ISS_LAST_POSITIONS,
+ paint={"circle-color": "yellow", "circle-radius": 5},
+ ),
+ )
+ m.add_source(SOURCE_ID_ISS_POSITION, GeoJSONSource(data=feature))
+ m.add_layer(
+ Layer(
+ type=LayerType.CIRCLE,
+ id="iss-position",
+ source=SOURCE_ID_ISS_POSITION,
+ paint={"circle-color": "lightblue", "circle-radius": 7},
+ )
+ )
+ m.add_tooltip("iss-position", "coords")
+ return m
+
+ @reactive.Effect
+ @reactive.event(input.update)
+ async def update():
+ print("Fetching new position")
+ lng_lat = where_is_the_iss()
+ print(lng_lat)
+ if len(feature_collection["features"]) == MAX_FEATURES:
+ feature_collection["features"] = []
+
+ async with MapContext("mapylibre") as m:
+ feature = create_feature(lng_lat)
+ m.set_data(SOURCE_ID_ISS_POSITION, feature)
+ feature_collection["features"].append(feature)
+ m.set_data(SOURCE_ID_ISS_LAST_POSITIONS, feature_collection)
+ m.add_call("flyTo", {"center": lng_lat, "speed": 0.5})
+
+
+app = App(app_ui, server)
diff --git a/docs/examples/where_is_the_iss/index.md b/docs/examples/where_is_the_iss/index.md
new file mode 100644
index 00000000..89bbed77
--- /dev/null
+++ b/docs/examples/where_is_the_iss/index.md
@@ -0,0 +1,9 @@
+```python
+-8<-- "where_is_the_iss/app.py"
+```
+
+Run example:
+
+```bash
+poetry run uvicorn docs.examples.where_is_the_iss.app:app --reload
+```
diff --git a/docs/index.md b/docs/index.md
index 250242a4..d7aae84e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,7 +8,9 @@ It integrates seamlessly into [Shiny for Python](https://github.com/posit-dev/py
```bash
# Stable
-pip install git+https://github.com/eodaGmbH/py-maplibregl
+pip install maplibre
+
+pip install "maplibre[all]"
# Dev
pip install git+https://github.com/eodaGmbH/py-maplibregl@dev
diff --git a/maplibre/__init__.py b/maplibre/__init__.py
index b9b8cea0..d1bd37cc 100644
--- a/maplibre/__init__.py
+++ b/maplibre/__init__.py
@@ -1,5 +1,6 @@
from .controls import ControlPosition, ControlType
-from .ipywidget import MapWidget
+
+# from .ipywidget import MapWidget
from .layer import Layer, LayerType
from .map import Map, MapOptions
from .mapcontext import MapContext
diff --git a/maplibre/controls.py b/maplibre/controls.py
index 62b0cab8..ceba25eb 100644
--- a/maplibre/controls.py
+++ b/maplibre/controls.py
@@ -92,6 +92,8 @@ def type(self):
class AttributionControl(Control):
+ """Attribution control"""
+
# _name: str = ControlType.ATTRIBUTION.value
compact: bool = None
custom_attribution: Union[str, list] = Field(
@@ -115,6 +117,8 @@ class FullscreenControl(Control):
class GeolocateControl(Control):
+ """Geolocate control"""
+
# _name: str = ControlType.GEOLOCATE.value
position_options: dict = Field(None, serialization_alias="positionOptions")
show_accuracy_circle: bool = Field(True, serialization_alias="showAccuracyCircle")
@@ -124,8 +128,10 @@ class GeolocateControl(Control):
class NavigationControl(Control):
+ """Navigation control"""
+
# _name: str = ControlType.NAVIGATION.value
- sho_compass: bool = Field(True, serialization_alias="showCompass")
+ show_compass: bool = Field(True, serialization_alias="showCompass")
show_zoom: bool = Field(True, serialization_alias="showZoom")
visualize_pitch: bool = Field(False, serialization_alias="visualizePitch")
@@ -137,6 +143,8 @@ class ScaleUnit(Enum):
class ScaleControl(Control):
+ """Scale control"""
+
# _name: str = ControlType.SCALE.value
max_width: int = Field(None, serialization_alias="maxWidth")
unit: Literal["imperial", "metric", "nautical"] = "metric"
diff --git a/maplibre/ipywidget.py b/maplibre/ipywidget.py
index 3d9b2175..08a8c5a1 100644
--- a/maplibre/ipywidget.py
+++ b/maplibre/ipywidget.py
@@ -12,59 +12,20 @@
from .sources import Source
-# DEPRECATED: MapWidget now uses map.Map as base class
-class BaseMap(object):
- def __init__(self, map_options=MapOptions(), **kwargs) -> None:
- self.map_options = map_options.to_dict() | kwargs
+class MapWidget(AnyWidget, Map):
+ """MapWidget
- # This method must be overwritten
- def add_call(self, method_name: str, *args) -> None:
- """Add a method call that is executed on the map instance
+ Use this class to display and update maps in Jupyter Notebooks.
+
+ See `maplibre.Map` for available methods.
+
+ Examples:
+ >>> from maplibre import MapOptions
+ >>> from maplibre.ipywidget import MapWidget as Map
+ >>> m = Map(MapOptions(center=(-123.13, 49.254), zoom=11, pitch=45))
+ >>> m # doctest: +SKIP
+ """
- Args:
- method_name (str): The name of the map method to be executed.
- *args (any): The arguments to be passed to the map method.
- """
- # TODO: Pass as dict? {"name": method_name, "args": args}
- call = [method_name, args]
- print(call)
-
- def add_source(self, source_id: str, source: Source) -> None:
- """Add a source to the map"""
- self.add_call("addSource", source_id, source.to_dict())
-
- def add_layer(self, layer: Layer) -> None:
- """Add a layer to the map"""
- self.add_call("addLayer", layer.to_dict())
-
- def add_control(
- self,
- control: Control,
- position: [str | ControlPosition] = ControlPosition.TOP_RIGHT,
- ) -> None:
- """Add a control to the map"""
- self.add_call(
- "addControl",
- control.type,
- control.to_dict(),
- ControlPosition(position).value,
- )
-
- def set_paint_property(self, layer_id: str, prop: str, value: any) -> None:
- """Update a paint property of a layer"""
- self.add_call("setPaintProperty", layer_id, prop, value)
-
- def set_layout_property(self, layer_id: str, prop: str, value: any) -> None:
- """Update a layout property of a layer"""
- self.add_call("setLayoutProperty", layer_id, prop, value)
-
- def add_tooltip(self, layer_id: str, prop: str) -> None:
- """Add a tooltip to the map"""
- self.add_call("addPopup", layer_id, prop)
-
-
-# TODO: Rename to MapWidget or IpyMap
-class MapWidget(AnyWidget, Map):
_esm = join(Path(__file__).parent, "srcjs", "ipywidget.js")
_css = join(Path(__file__).parent, "srcjs", "maplibre-gl.css")
_use_message_queue = False
diff --git a/maplibre/map.py b/maplibre/map.py
index 4f3d7c61..ede455fb 100644
--- a/maplibre/map.py
+++ b/maplibre/map.py
@@ -205,6 +205,25 @@ def set_layout_property(self, layer_id: str, prop: str, value: any) -> None:
"""
self.add_call("setLayoutProperty", layer_id, prop, value)
+ def set_data(self, source_id: str, data: dict) -> None:
+ """Update the data of a GeoJSON source
+
+ Args:
+ source_id (str): The name of the source to be updated.
+ data (dict): The data of the source.
+ """
+ self.add_call("setSourceData", source_id, data)
+
+ def set_visibility(self, layer_id: str, visible: bool = True) -> None:
+ """Update the visibility of a layer
+
+ Args:
+ layer_id (str): The name of the layer to be updated.
+ visible (bool): Whether the layer is visible or not.
+ """
+ value = "visible" if visible else "none"
+ self.add_call("setLayoutProperty", layer_id, "visibility", value)
+
def to_html(self, **kwargs) -> str:
"""Render to html
diff --git a/maplibre/mapcontext.py b/maplibre/mapcontext.py
index 06fdca82..33eebff7 100644
--- a/maplibre/mapcontext.py
+++ b/maplibre/mapcontext.py
@@ -4,6 +4,19 @@
class MapContext(Map):
+ """MapContext
+
+ Use this class to update a `Map` instance in a Shiny app.
+ Must be used inside an async function.
+
+ See `maplibre.Map` for available methods.
+
+ Args:
+ id (string): The id of the map to be updated.
+ session (Session): A Shiny session.
+ If `None`, the active session is used.
+ """
+
def __init__(self, id: str, session: Session = None) -> None:
self.id = id
self._session = require_active_session(session)
diff --git a/maplibre/server.py b/maplibre/server.py
index b4827073..165932ae 100644
--- a/maplibre/server.py
+++ b/maplibre/server.py
@@ -1,7 +1,11 @@
from __future__ import annotations
-from shiny.render.transformer import (TransformerMetadata, ValueFn,
- output_transformer, resolve_value_fn)
+from shiny.render.transformer import (
+ TransformerMetadata,
+ ValueFn,
+ output_transformer,
+ resolve_value_fn,
+)
from shiny.session import get_current_session
from .map import Map
diff --git a/maplibre/srcjs/index.js b/maplibre/srcjs/index.js
index e333e08f..c1121f06 100644
--- a/maplibre/srcjs/index.js
+++ b/maplibre/srcjs/index.js
@@ -71,6 +71,9 @@
popup.remove();
});
}
+ setSourceData(sourceId, data) {
+ this._map.getSource(sourceId).setData(data);
+ }
render(calls) {
calls.forEach(([name, params]) => {
if ([
@@ -79,7 +82,8 @@
"addTooltip",
"addMarker",
"addPopup",
- "addControl"
+ "addControl",
+ "setSourceData"
].includes(name)) {
console.log("Custom method", name, params);
this[name](...params);
diff --git a/maplibre/srcjs/ipywidget.js b/maplibre/srcjs/ipywidget.js
index ec8c48bc..3a488ce7 100644
--- a/maplibre/srcjs/ipywidget.js
+++ b/maplibre/srcjs/ipywidget.js
@@ -45,6 +45,9 @@ function getCustomMapMethods(maplibregl2, map) {
marker.setPopup(popup_);
}
marker.addTo(map);
+ },
+ setSourceData: function(sourceId, data) {
+ map.getSource(sourceId).setData(data);
}
};
}
diff --git a/mkdocs.yml b/mkdocs.yml
index 67d226bb..95e5a0d3 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -29,6 +29,8 @@ nav:
- 3D Indoor mapping: examples/3d_indoor_mapping/index.md
- Custom basemap: examples/custom_basemap/index.md
- GeoPandas: examples/geopandas/index.md
+ - UK Road Safety: examples/road_safety/index.md
+ - Where is the ISS: examples/where_is_the_iss/index.md
plugins:
- search:
diff --git a/poetry.lock b/poetry.lock
index 358abc41..51db4cb3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -203,7 +203,7 @@ files = [
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "main"
+category = "dev"
optional = false
python-versions = ">=3.7.0"
files = [
@@ -1845,7 +1845,7 @@ files = [
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
-category = "main"
+category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@@ -2105,7 +2105,7 @@ test = ["coverage", "pytest", "pytest-cov"]
name = "urllib3"
version = "2.1.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "main"
+category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@@ -2394,4 +2394,4 @@ all = ["geopandas", "pandas"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.9,<4"
-content-hash = "f63acd17c501b755d5a19a20a822b8673678bb1eb09a5dea577d6fefaf1a9b6d"
+content-hash = "47ba10b55a403b30b258b7298c519ac5c9e0a4b5c611c578f0d3a665233d7d88"
diff --git a/pyproject.toml b/pyproject.toml
index 2faec20e..43e3cd7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[tool.poetry]
name = "maplibre"
-version = "0.1.1"
-description = ""
+version = "0.1.2"
+description = "Python bindings for MapLibre GL JS"
authors = ["Stefan Kuethe "]
readme = "README.md"
license = "MIT"
@@ -13,7 +13,6 @@ include = [
python = ">=3.9,<4"
shiny = "^0.6.1"
htmltools = "^0.5.1"
-requests = "^2.31.0"
jinja2 = "^3.1.3"
pydantic = "^2.5.3"
anywidget = "^0.8.1"
diff --git a/srcjs/mapmethods.js b/srcjs/mapmethods.js
index 35820ee1..17d094fd 100644
--- a/srcjs/mapmethods.js
+++ b/srcjs/mapmethods.js
@@ -50,6 +50,10 @@ function getCustomMapMethods(maplibregl, map) {
}
marker.addTo(map);
},
+
+ setSourceData: function (sourceId, data) {
+ map.getSource(sourceId).setData(data);
+ },
};
}
diff --git a/srcjs/pymaplibregl.js b/srcjs/pymaplibregl.js
index 5cebe38a..52e0e4c6 100644
--- a/srcjs/pymaplibregl.js
+++ b/srcjs/pymaplibregl.js
@@ -88,6 +88,10 @@ export default class PyMapLibreGL {
});
}
+ setSourceData(sourceId, data) {
+ this._map.getSource(sourceId).setData(data);
+ }
+
render(calls) {
calls.forEach(([name, params]) => {
// Custom method
@@ -99,6 +103,7 @@ export default class PyMapLibreGL {
"addMarker",
"addPopup",
"addControl",
+ "setSourceData",
].includes(name)
) {
console.log("Custom method", name, params);
diff --git a/tests/test_controls.py b/tests/test_controls.py
index 72abd426..f7414a73 100644
--- a/tests/test_controls.py
+++ b/tests/test_controls.py
@@ -7,6 +7,3 @@ def test_scale_control():
assert control.type == ControlType.SCALE.value
assert control.unit == "metric"
-
-
-# def test_scale_control():
diff --git a/tests/test_experimental.py b/tests/test_experimental.py
deleted file mode 100644
index 4d16ec31..00000000
--- a/tests/test_experimental.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from maplibre.experimental import PydanticSer
-from maplibre.layer import Layer, LayerType
-
-
-def test_pydantic_model():
- m = PydanticSer(a=1, b=2)
-
- print(m)
- print(m.model_dump())
- print(dict(m))
- print(m.to_dict())
-
-
-def test_layer():
- layer = Layer(type=LayerType.LINE)
- print("line", LayerType(LayerType.LINE).value)
-
- print(layer.model_dump())
- # print(layer.to_dict())
diff --git a/tests/test_mapwidget.py b/tests/test_mapwidget.py
index ce8ddc38..ee6198e0 100644
--- a/tests/test_mapwidget.py
+++ b/tests/test_mapwidget.py
@@ -1,11 +1,14 @@
import pytest
-from maplibre.ipywidget import MapWidget
+# anywidget causes "core dumped" error
+# from maplibre.ipywidget import MapWidget
-# @pytest.mark.skip("enable me")
+"""
+@pytest.mark.skip("enable me")
def test_maplibre_widget():
widget = MapWidget(height=200)
print(widget.map_options)
print(widget.height)
assert widget.height == "200px"
+"""