diff --git a/.examples/earthquakes_cluster/app.py b/.examples/earthquakes_cluster/app.py index cfead397..ac7ac004 100644 --- a/.examples/earthquakes_cluster/app.py +++ b/.examples/earthquakes_cluster/app.py @@ -1,8 +1,7 @@ -from shiny import App, reactive, ui - from maplibre import Layer, Map, MapOptions, output_maplibregl, render_maplibregl from maplibre.basemaps import Carto from maplibre.sources import GeoJSONSource +from shiny import App, reactive, ui SOURCE_ID = "earthquakes" LAYER_ID = "earthquakes" diff --git a/.examples/experimental/app2.py b/.examples/experimental/app2.py index 7bd84718..ae00f5ea 100644 --- a/.examples/experimental/app2.py +++ b/.examples/experimental/app2.py @@ -1,16 +1,9 @@ import requests as req -from shiny import App, reactive, render, ui - -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.experimental import LineLayer +from shiny import App, reactive, render, ui LAYER_ID = "counties" LAYER_ID_LINE = "us-states-line" @@ -103,3 +96,4 @@ async def color(): if __name__ == "__main__": app.run() +un() diff --git a/.examples/experimental/flights.py b/.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/h3_hexagons/app.py b/.examples/h3_hexagons/app.py index d621dd23..1ef8a02c 100644 --- a/.examples/h3_hexagons/app.py +++ b/.examples/h3_hexagons/app.py @@ -1,18 +1,11 @@ import h3 import pandas as pd -from shiny import App, reactive, ui - # 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 LAYER_ID = "motor_vehicle_collisions" diff --git a/.examples/motor_vehicle_collisions/app.py b/.examples/motor_vehicle_collisions/app.py index 50df99ea..a197d634 100644 --- a/.examples/motor_vehicle_collisions/app.py +++ b/.examples/motor_vehicle_collisions/app.py @@ -1,6 +1,5 @@ import h3 import pandas as pd -from shiny import App, reactive, ui # import shapely from maplibre import ( @@ -13,6 +12,7 @@ ) from maplibre.basemaps import Carto from maplibre.utils import GeometryType, df_to_geojson, get_bounds +from shiny import App, reactive, ui LAYER_ID = "motor_vehicle_collisions" diff --git a/.examples/text_layer/app.py b/.examples/text_layer/app.py index cf4fab75..044bd919 100644 --- a/.examples/text_layer/app.py +++ b/.examples/text_layer/app.py @@ -1,5 +1,4 @@ -from maplibre import (Layer, LayerType, Map, output_maplibregl, - render_maplibregl) +from maplibre import Layer, LayerType, Map, output_maplibregl, render_maplibregl from maplibre.basemaps import Carto from shiny import App, reactive, render, ui diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ed3ca0..ca4e0022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog for MapLibre for Python +## maplibre v0.1.3 + +* Display all properties in popup and tooltip if `prop = None` (#26) +* Support [mustache](https://github.com/janl/mustache.js) templates for popups and tooltips (#27) + ## maplibre v0.1.2 * Add `Map.set_data` diff --git a/README.md b/README.md index 63ab8ab1..907a0ced 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ poetry install poetry run pytest -poetry run pytest --doctest-modules maplibre +poetry run pytest --ignore=maplibre/ipywidget.py --doctest-modules maplibre ``` ### JavaScript diff --git a/build-docs.sh b/build-docs.sh new file mode 100755 index 00000000..ae81fe1a --- /dev/null +++ b/build-docs.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e + +cp CHANGELOG.md docs/changelog.md + +poetry run mkdocs build diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..ca4e0022 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,19 @@ +# Changelog for MapLibre for Python + +## maplibre v0.1.3 + +* Display all properties in popup and tooltip if `prop = None` (#26) +* Support [mustache](https://github.com/janl/mustache.js) templates for popups and tooltips (#27) + +## 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/docs/examples/3d_indoor_mapping/index.md b/docs/examples/3d_indoor_mapping/index.md index 9f231f34..ce42e75f 100644 --- a/docs/examples/3d_indoor_mapping/index.md +++ b/docs/examples/3d_indoor_mapping/index.md @@ -1,4 +1,6 @@ -See example in action + + + ```python -8<-- "3d_indoor_mapping/app.py" diff --git a/docs/examples/airports/app.html b/docs/examples/airports/app.html new file mode 100644 index 00000000..2c498ef9 --- /dev/null +++ b/docs/examples/airports/app.html @@ -0,0 +1,167 @@ + + + +Pymaplibregl + + + + +
+ + + \ No newline at end of file diff --git a/docs/examples/airports/app.py b/docs/examples/airports/app.py index 39e2d94d..d754a3ba 100644 --- a/docs/examples/airports/app.py +++ b/docs/examples/airports/app.py @@ -1,3 +1,5 @@ +import sys + import pandas as pd from maplibre import ( Layer, @@ -64,6 +66,23 @@ def get_color(airport_type: str) -> str: popup_options = PopupOptions(close_button=False) + +def create_map() -> Map: + m = Map(map_options) + m.add_layer(airport_circles) + for _, r in airports_data.iterrows(): + marker = Marker( + lng_lat=r["coordinates"], + options=MarkerOptions(color=get_color(r["type"])), + popup=Popup( + text=r["name"], + options=popup_options, + ), + ) + m.add_marker(marker) + return m + + app_ui = ui.page_fluid( ui.panel_title("Airports"), output_maplibregl("maplibre", height=600), @@ -73,22 +92,14 @@ def get_color(airport_type: str) -> str: def server(input, output, session): @render_maplibregl def maplibre(): - m = Map(map_options) - for _, r in airports_data.iterrows(): - marker = Marker( - lng_lat=r["coordinates"], - options=MarkerOptions(color=get_color(r["type"])), - popup=Popup( - text=r["name"], - options=popup_options, - ), - ) - m.add_marker(marker) - m.add_layer(airport_circles) - return m + return create_map() app = App(app_ui, server) if __name__ == "__main__": - app.run() + if len(sys.argv) == 2: + with open(sys.argv[1], "w") as f: + f.write(create_map().to_html()) + else: + app.run() diff --git a/docs/examples/airports/index.md b/docs/examples/airports/index.md index 031c511d..cd813974 100644 --- a/docs/examples/airports/index.md +++ b/docs/examples/airports/index.md @@ -1,3 +1,5 @@ + + ```python -8<-- "airports/app.py" ``` diff --git a/docs/examples/custom_basemap/index.md b/docs/examples/custom_basemap/index.md index 608ef7df..f23ec65c 100644 --- a/docs/examples/custom_basemap/index.md +++ b/docs/examples/custom_basemap/index.md @@ -1,9 +1,11 @@ + + + + ```python -8<-- "custom_basemap/app.py" ``` -See example in action - Run example: ``` bash diff --git a/docs/examples/earthquake_clusters/app.html b/docs/examples/earthquake_clusters/app.html new file mode 100644 index 00000000..d550851d --- /dev/null +++ b/docs/examples/earthquake_clusters/app.html @@ -0,0 +1,167 @@ + + + +Pymaplibregl + + + + +
+ + + \ No newline at end of file diff --git a/docs/examples/earthquake_clusters/app.py b/docs/examples/earthquake_clusters/app.py index bba6fe29..1342b8ee 100644 --- a/docs/examples/earthquake_clusters/app.py +++ b/docs/examples/earthquake_clusters/app.py @@ -1,3 +1,5 @@ +import sys + from maplibre import ( Layer, LayerType, @@ -69,6 +71,17 @@ map_options = MapOptions(style=Carto.POSITRON, center=CENTER, zoom=3, hash=True) + +def create_map() -> Map: + m = Map(map_options) + m.add_source(EARTHQUAKE_SOURCE, earthquakes_source) + m.add_layer(earthquake_clusters) + m.add_layer(earthquake_circles) + m.add_tooltip(EARTHQUAKE_CLUSTERS, "maxMag") + m.add_layer(earthquake_labels) + return m + + app_ui = ui.page_fluid( ui.panel_title("Earthquakes Cluster"), output_maplibregl("maplibre", height=500), @@ -78,13 +91,7 @@ def server(input, output, session): @render_maplibregl def maplibre(): - m = Map(map_options) - m.add_source(EARTHQUAKE_SOURCE, earthquakes_source) - m.add_layer(earthquake_clusters) - m.add_layer(earthquake_circles) - m.add_tooltip(EARTHQUAKE_CLUSTERS, "maxMag") - m.add_layer(earthquake_labels) - return m + return create_map() @reactive.Effect @reactive.event(input.maplibre) @@ -95,4 +102,9 @@ async def result(): app = App(app_ui, server) if __name__ == "__main__": - app.run() + if len(sys.argv) == 2: + file_name = sys.argv[1] + with open(file_name, "w") as f: + f.write(create_map().to_html()) + else: + app.run() diff --git a/docs/examples/earthquake_clusters/index.md b/docs/examples/earthquake_clusters/index.md index 2c56003d..77f57d29 100644 --- a/docs/examples/earthquake_clusters/index.md +++ b/docs/examples/earthquake_clusters/index.md @@ -1,3 +1,5 @@ + + ```python -8<-- "earthquake_clusters/app.py" ``` diff --git a/docs/examples/every_person_in_manhattan/app.html b/docs/examples/every_person_in_manhattan/app.html new file mode 100644 index 00000000..734fdd37 --- /dev/null +++ b/docs/examples/every_person_in_manhattan/app.html @@ -0,0 +1,167 @@ + + + +Pymaplibregl + + + + +
+ + + \ No newline at end of file diff --git a/docs/examples/every_person_in_manhattan/app.py b/docs/examples/every_person_in_manhattan/app.py index 164ef77f..89ea6278 100644 --- a/docs/examples/every_person_in_manhattan/app.py +++ b/docs/examples/every_person_in_manhattan/app.py @@ -1,4 +1,5 @@ import json +import sys import pandas as pd import shapely @@ -52,6 +53,14 @@ fit_bounds_options={"padding": 20}, ) + +def create_map() -> Map: + m = Map(map_options) + m.add_control(ScaleControl(), position="bottom-left") + m.add_layer(every_person_in_manhattan_circles) + return m + + app_ui = ui.page_fluid( ui.panel_title("Every Person in Manhattan"), output_maplibregl("maplibre", height=600), @@ -62,10 +71,7 @@ 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 + return create_map() @reactive.Effect @reactive.event(input.radius, ignore_init=True) @@ -77,4 +83,9 @@ async def radius(): app = App(app_ui, server) if __name__ == "__main__": - app.run() + if len(sys.argv) == 2: + file_name = sys.argv[1] + with open(file_name, "w") as f: + f.write(create_map().to_html()) + else: + app.run() diff --git a/docs/examples/every_person_in_manhattan/index.md b/docs/examples/every_person_in_manhattan/index.md index b3e6fcd6..411b88a9 100644 --- a/docs/examples/every_person_in_manhattan/index.md +++ b/docs/examples/every_person_in_manhattan/index.md @@ -1,3 +1,5 @@ + + ```python -8<-- "every_person_in_manhattan/app.py" ``` diff --git a/docs/examples/geopandas/index.md b/docs/examples/geopandas/index.md index fffccb96..516f4995 100644 --- a/docs/examples/geopandas/index.md +++ b/docs/examples/geopandas/index.md @@ -1,4 +1,6 @@ -See example in action + + + ```python -8<-- "geopandas/app.py" diff --git a/docs/examples/jupyter/getting_started.ipynb b/docs/examples/jupyter/getting_started.ipynb index 388953c0..1d293c23 100644 --- a/docs/examples/jupyter/getting_started.ipynb +++ b/docs/examples/jupyter/getting_started.ipynb @@ -70,14 +70,14 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 27, "id": "e6e2284c-1862-4697-ad94-f535b3682197", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ece4babe53924aa7886553d1b5d53bec", + "model_id": "8424f04853ea4a41ac6a3697349bbd3e", "version_major": 2, "version_minor": 0 }, @@ -85,7 +85,7 @@ "MapWidget(calls=[['addControl', ('ScaleControl', {'unit': 'metric'}, 'bottom-left')], ['addLayer', ({'id': 'ea…" ] }, - "execution_count": 4, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -94,7 +94,7 @@ "m = Map()\n", "m.add_control(ScaleControl(), position=\"bottom-left\")\n", "m.add_layer(earthquake_circles)\n", - "m.add_tooltip(layer_id, \"mag\")\n", + "m.add_tooltip(layer_id)\n", "m.add_marker(Marker(lng_lat=(100.507, 13.745)))\n", "m" ] @@ -109,39 +109,29 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 28, "id": "356960fa-b866-42c8-a58e-0c9a417c28eb", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "60ef57a918734016b9766e41615c1935", + "model_id": "7998e7feb14045d6a8dfc637d6775c98", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(IntSlider(value=5, description='radius', max=15, min=-5), Output()), _dom_classes=('widg…" + "interactive(children=(IntSlider(value=4, description='radius', max=8, min=1), Output()), _dom_classes=('widget…" ] }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "(radius)>" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "widgets.interact(\n", + "change_radius = widgets.interact(\n", " lambda radius: m.set_paint_property(layer_id, \"circle-radius\", radius),\n", - " radius=5\n", + " radius=(1, 8, 1)\n", ")" ] }, @@ -155,14 +145,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 21, "id": "8ecd93a6-f471-4350-a052-7a9171fa1606", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9b52cdb12c3b4af4ac8bbbfc885aea30", + "model_id": "ca7ad45e679d479795b3c05014c8e6b4", "version_major": 2, "version_minor": 0 }, @@ -172,20 +162,10 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "(color)>" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "widgets.interact(\n", + "change_color = widgets.interact(\n", " lambda color: m.set_paint_property(layer_id, \"circle-color\", color),\n", " color=[\"green\", \"yellow\", \"orange\", \"red\"]\n", ")" @@ -201,39 +181,29 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 31, "id": "0b73f056-f35a-46bb-a092-d899c64cd67e", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5a4891d6fb0e418bbfe22c4661363022", + "model_id": "405b4aba579b42248573eaf356f629d4", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(IntSlider(value=3, description='mag_min', max=9, min=-3), Output()), _dom_classes=('widg…" + "interactive(children=(IntSlider(value=4, description='mag_min', max=7, min=1), Output()), _dom_classes=('widge…" ] }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "(mag_min)>" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "widgets.interact(\n", + "filter_mag = widgets.interact(\n", " lambda mag_min: m.set_filter(layer_id, [\">=\", [\"get\", \"mag\"], mag_min]),\n", - " mag_min=3\n", + " mag_min=(1,7,1)\n", ")" ] }, @@ -247,14 +217,14 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 32, "id": "a9c5ddf7-074e-45b0-8cfe-15750fd0b4d5", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "119b58c9640f4b058fc218ce17203edf", + "model_id": "3957fc738b2643cfa207f9b62edbfdda", "version_major": 2, "version_minor": 0 }, @@ -262,7 +232,7 @@ "Output()" ] }, - "execution_count": 8, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -284,136 +254,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [] }, { diff --git a/docs/examples/road_safety/app.py b/docs/examples/road_safety/app.py index 2e8b9c8d..a2a73176 100644 --- a/docs/examples/road_safety/app.py +++ b/docs/examples/road_safety/app.py @@ -51,21 +51,28 @@ def create_h3_grid(res=RESOLUTION) -> dict: source = GeoJSONSource(data=create_h3_grid()) +map_options = MapOptions( + center=(-1.415727, 52.232395), + zoom=7, + pitch=40, + bearing=-27, +) + +h3_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"]], + }, +) + 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 = Map(map_options) + m.add_layer(h3_layer) m.add_tooltip("road-safety", "count") return m diff --git a/docs/examples/road_safety/index.md b/docs/examples/road_safety/index.md index 4173e20c..5ba821d2 100644 --- a/docs/examples/road_safety/index.md +++ b/docs/examples/road_safety/index.md @@ -1,4 +1,6 @@ -See example in action (without reactive effects) + + + ```python -8<-- "road_safety/app.py" diff --git a/docs/examples/vancouver_blocks/index.md b/docs/examples/vancouver_blocks/index.md index 5af20d82..3e25f50c 100644 --- a/docs/examples/vancouver_blocks/index.md +++ b/docs/examples/vancouver_blocks/index.md @@ -1,4 +1,6 @@ -See example in action (without reactive effects) + + + ```python -8<-- "vancouver_blocks/app.py" diff --git a/docs/jupyter.md b/docs/jupyter.md index 0ca35638..600601f1 100644 --- a/docs/jupyter.md +++ b/docs/jupyter.md @@ -32,21 +32,21 @@ m.add_marker(Marker(lng_lat=(100.507, 13.745))) m # Change radius -widgets.interact( +_ = widgets.interact( lambda radius: m.set_paint_property(layer_id, "circle-radius", radius), - radius=5 + radius=(1, 8, 1) ) # Change color -widgets.interact( +_ = widgets.interact( lambda color: m.set_paint_property(layer_id, "circle-color", color), color=["green", "yellow", "orange", "red"] ) # Set filter on magnitude -widgets.interact( +_ = widgets.interact( lambda mag_min: m.set_filter(layer_id, [">=", ["get", "mag"], mag_min]), - mag_min=3 + mag_min=(1, 8, 1) ) # Observe map-on-click event diff --git a/maplibre/_constants.py b/maplibre/_constants.py index 2e82e7f8..2b588e1a 100644 --- a/maplibre/_constants.py +++ b/maplibre/_constants.py @@ -1,4 +1,4 @@ -__version__ = "0.1.0" +__version__ = "0.1.3" _shiny_output_class = "shiny-maplibregl-output" # tmp diff --git a/maplibre/map.py b/maplibre/map.py index ede455fb..8f5938f1 100644 --- a/maplibre/map.py +++ b/maplibre/map.py @@ -158,23 +158,32 @@ def add_marker(self, marker: Marker) -> None: """ self.add_call("addMarker", marker.to_dict()) - def add_popup(self, layer_id: str, prop: str) -> None: + def add_popup(self, layer_id: str, prop: str = None, template: str = None) -> None: """Add a popup to the map Args: layer_id (str): The layer to which the popup is added. - prop (str): The property of the source to be displayed. + prop (str): The property of the source to be displayed. If `None`, all properties are displayed. + template (str): A mustache template. If supplied, `prop` is ignored. """ - self.add_call("addPopup", layer_id, prop) + self.add_call("addPopup", layer_id, prop, template) - def add_tooltip(self, layer_id: str, prop: str) -> None: + def add_tooltip( + self, layer_id: str, prop: str = None, template: str = None + ) -> None: """Add a tooltip to the map Args: layer_id (str): The layer to which the tooltip is added. - prop (str): The property of the source to be displayed. + prop (str): The property of the source to be displayed. If `None`, all properties are displayed. + template (str): A mustache template. If supplied, `prop` is ignored. + + Examples: + >>> map = Map() + >>> # ... + >>> map.add_tooltip("test-layer", template="Name: {{ name }}") """ - self.add_call("addTooltip", layer_id, prop) + self.add_call("addTooltip", layer_id, prop, template) def set_filter(self, layer_id: str, filter_: list): """Update the filter of a layer diff --git a/maplibre/server.py b/maplibre/server.py index 165932ae..72a7b24a 100644 --- a/maplibre/server.py +++ b/maplibre/server.py @@ -6,17 +6,10 @@ output_transformer, resolve_value_fn, ) -from shiny.session import get_current_session from .map import Map -async def maplibre_render(id_: str = None, color: str = "black") -> None: - session = get_current_session() - opts = {"id": id_, "color": color} - await session.send_custom_message("maplibre", opts) - - @output_transformer async def render_maplibregl( _meta: TransformerMetadata, diff --git a/maplibre/srcjs/index.js b/maplibre/srcjs/index.js index c1121f06..ecbff443 100644 --- a/maplibre/srcjs/index.js +++ b/maplibre/srcjs/index.js @@ -1,148 +1,12 @@ -(() => { - // srcjs/pymaplibregl.js - var PyMapLibreGL = class { - constructor(mapOptions) { - this._id = mapOptions.container; - this._map = new maplibregl.Map(mapOptions); - this._map.on("mouseover", () => { - this._map.getCanvas().style.cursor = "pointer"; - }); - this._map.on("mouseout", () => { - this._map.getCanvas().style.cursor = ""; - }); - this._map.addControl(new maplibregl.NavigationControl()); - } - getMap() { - return this._map; - } - applyMapMethod(name, params) { - this._map[name](...params); - } - addControl(type, options, position) { - this._map.addControl(new maplibregl[type](options), position); - } - addMarker({ lngLat, popup, options }) { - const marker = new maplibregl.Marker(options).setLngLat(lngLat); - if (popup) { - const popup_ = new maplibregl.Popup(popup.options).setHTML(popup.text); - marker.setPopup(popup_); - } - marker.addTo(this._map); - } - addLayer(layer) { - this._map.addLayer(layer); - if (typeof Shiny !== "undefined") { - this._map.on("click", layer.id, (e) => { - console.log(e, e.features[0]); - const layerId_ = layer.id.replaceAll("-", "_"); - const inputName = `${this._id}_layer_${layerId_}`; - const feature = { - props: e.features[0].properties, - layer_id: layer.id - }; - console.log(inputName, feature); - Shiny.onInputChange(inputName, feature); - }); - } - } - addPopup(layerId, property) { - const popupOptions = { - closeButton: false - }; - const popup = new maplibregl.Popup(popupOptions); - this._map.on("click", layerId, (e) => { - const feature = e.features[0]; - const text = feature.properties[property]; - popup.setLngLat(e.lngLat).setHTML(text).addTo(this._map); - }); - } - addTooltip(layerId, property) { - const popupOptions = { - closeButton: false, - closeOnClick: false - }; - const popup = new maplibregl.Popup(popupOptions); - this._map.on("mousemove", layerId, (e) => { - const feature = e.features[0]; - const text = feature.properties[property]; - popup.setLngLat(e.lngLat).setHTML(text).addTo(this._map); - }); - this._map.on("mouseleave", layerId, () => { - popup.remove(); - }); - } - setSourceData(sourceId, data) { - this._map.getSource(sourceId).setData(data); - } - render(calls) { - calls.forEach(([name, params]) => { - if ([ - "addLayer", - "addPopup", - "addTooltip", - "addMarker", - "addPopup", - "addControl", - "setSourceData" - ].includes(name)) { - console.log("Custom method", name, params); - this[name](...params); - return; - } - console.log("Map method", name); - this.applyMapMethod(name, params); - }); - } - }; +(()=>{var B=Object.prototype.toString,_=Array.isArray||function(e){return B.call(e)==="[object Array]"};function R(r){return typeof r=="function"}function D(r){return _(r)?"array":typeof r}function P(r){return r.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function j(r,e){return r!=null&&typeof r=="object"&&e in r}function F(r,e){return r!=null&&typeof r!="object"&&r.hasOwnProperty&&r.hasOwnProperty(e)}var H=RegExp.prototype.test;function N(r,e){return H.call(r,e)}var W=/\S/;function q(r){return!N(W,r)}var G={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function K(r){return String(r).replace(/[&<>"'`=\/]/g,function(t){return G[t]})}var V=/\s*/,z=/\s+/,I=/\s*=/,J=/\s*\}/,Q=/#|\^|\/|>|\{|&|=|!/;function X(r,e){if(!r)return[];var t=!1,n=[],a=[],s=[],o=!1,i=!1,p="",u=0;function h(){if(o&&!i)for(;s.length;)delete a[s.pop()];else s=[];o=!1,i=!1}var v,m,x;function U(y){if(typeof y=="string"&&(y=y.split(z,2)),!_(y)||y.length!==2)throw new Error("Invalid tags: "+y);v=new RegExp(P(y[0])+"\\s*"),m=new RegExp("\\s*"+P(y[1])),x=new RegExp("\\s*"+P("}"+y[1]))}U(e||g.tags);for(var l=new E(r),w,f,d,k,L,b;!l.eos();){if(w=l.pos,d=l.scanUntil(v),d)for(var M=0,A=d.length;M"?L=[f,d,w,l.pos,p,u,t]:L=[f,d,w,l.pos],u++,a.push(L),f==="#"||f==="^")n.push(L);else if(f==="/"){if(b=n.pop(),!b)throw new Error('Unopened section "'+d+'" at '+w);if(b[1]!==d)throw new Error('Unclosed section "'+b[1]+'" at '+w)}else f==="name"||f==="{"||f==="&"?i=!0:f==="="&&U(d)}if(h(),b=n.pop(),b)throw new Error('Unclosed section "'+b[1]+'" at '+l.pos);return Z(Y(a))}function Y(r){for(var e=[],t,n,a=0,s=r.length;a0?n[n.length-1][4]:e;break;default:t.push(a)}return e}function E(r){this.string=r,this.tail=r,this.pos=0}E.prototype.eos=function(){return this.tail===""};E.prototype.scan=function(e){var t=this.tail.match(e);if(!t||t.index!==0)return"";var n=t[0];return this.tail=this.tail.substring(n.length),this.pos+=n.length,n};E.prototype.scanUntil=function(e){var t=this.tail.search(e),n;switch(t){case-1:n=this.tail,this.tail="";break;case 0:n="";break;default:n=this.tail.substring(0,t),this.tail=this.tail.substring(t)}return this.pos+=n.length,n};function C(r,e){this.view=r,this.cache={".":this.view},this.parent=e}C.prototype.push=function(e){return new C(e,this)};C.prototype.lookup=function(e){var t=this.cache,n;if(t.hasOwnProperty(e))n=t[e];else{for(var a=this,s,o,i,p=!1;a;){if(e.indexOf(".")>0)for(s=a.view,o=e.split("."),i=0;s!=null&&i"?u=this.renderPartial(i,t,n,s):p==="&"?u=this.unescapedValue(i,t):p==="name"?u=this.escapedValue(i,t,s):p==="text"&&(u=this.rawValue(i)),u!==void 0&&(o+=u);return o};c.prototype.renderSection=function(e,t,n,a,s){var o=this,i="",p=t.lookup(e[1]);function u(m){return o.render(m,t,n,s)}if(p){if(_(p))for(var h=0,v=p.length;h0||!n)&&(s[o]=a+s[o]);return s.join(` +`)};c.prototype.renderPartial=function(e,t,n,a){if(n){var s=this.getConfigTags(a),o=R(n)?n(e[1]):n[e[1]];if(o!=null){var i=e[6],p=e[5],u=e[4],h=o;p==0&&u&&(h=this.indentPartial(o,u,i));var v=this.parse(h,s);return this.renderTokens(v,t,n,h,a)}}};c.prototype.unescapedValue=function(e,t){var n=t.lookup(e[1]);if(n!=null)return n};c.prototype.escapedValue=function(e,t,n){var a=this.getConfigEscape(n)||g.escape,s=t.lookup(e[1]);if(s!=null)return typeof s=="number"&&a===g.escape?String(s):a(s)};c.prototype.rawValue=function(e){return e[1]};c.prototype.getConfigTags=function(e){return _(e)?e:e&&typeof e=="object"?e.tags:void 0};c.prototype.getConfigEscape=function(e){if(e&&typeof e=="object"&&!_(e))return e.escape};var g={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:void 0,escape:void 0,parse:void 0,render:void 0,Scanner:void 0,Context:void 0,Writer:void 0,set templateCache(r){T.templateCache=r},get templateCache(){return T.templateCache}},T=new c;g.clearCache=function(){return T.clearCache()};g.parse=function(e,t){return T.parse(e,t)};g.render=function(e,t,n,a){if(typeof e!="string")throw new TypeError('Invalid template! Template should be a "string" but "'+D(e)+'" was given as the first argument for mustache#render(template, view, partials)');return T.render(e,t,n,a)};g.escape=K;g.Scanner=E;g.Context=C;g.Writer=c;var $=g;function O(r,e,t){return t!==null?$.render(t,r.properties):e===null?Object.keys(r.properties).map(a=>`${a}: ${r.properties[a]}`).join("
"):r.properties[e]}var S=class{constructor(e){this._id=e.container,this._map=new maplibregl.Map(e),this._map.on("mouseover",()=>{this._map.getCanvas().style.cursor="pointer"}),this._map.on("mouseout",()=>{this._map.getCanvas().style.cursor=""}),this._map.addControl(new maplibregl.NavigationControl)}getMap(){return this._map}applyMapMethod(e,t){this._map[e](...t)}addControl(e,t,n){this._map.addControl(new maplibregl[e](t),n)}addMarker({lngLat:e,popup:t,options:n}){let a=new maplibregl.Marker(n).setLngLat(e);if(t){let s=new maplibregl.Popup(t.options).setHTML(t.text);a.setPopup(s)}a.addTo(this._map)}addLayer(e){this._map.addLayer(e),typeof Shiny<"u"&&this._map.on("click",e.id,t=>{console.log(t,t.features[0]);let n=e.id.replaceAll("-","_"),a=`${this._id}_layer_${n}`,s={props:t.features[0].properties,layer_id:e.id};console.log(a,s),Shiny.onInputChange(a,s)})}addPopup(e,t=null,n=null){let a={closeButton:!1},s=new maplibregl.Popup(a);this._map.on("click",e,o=>{let i=o.features[0],p=O(i,t,n);s.setLngLat(o.lngLat).setHTML(p).addTo(this._map)})}addTooltip(e,t=null,n=null){let a={closeButton:!1,closeOnClick:!1},s=new maplibregl.Popup(a);this._map.on("mousemove",e,o=>{let i=o.features[0],p=O(i,t,n);s.setLngLat(o.lngLat).setHTML(p).addTo(this._map)}),this._map.on("mouseleave",e,()=>{s.remove()})}setSourceData(e,t){this._map.getSource(e).setData(t)}render(e){e.forEach(([t,n])=>{if(["addLayer","addPopup","addTooltip","addMarker","addPopup","addControl","setSourceData"].includes(t)){console.log("Custom method",t,n),this[t](...n);return}console.log("Map method",t),this.applyMapMethod(t,n)})}};var ee="0.1.0";console.log("pymaplibregl",ee);typeof Shiny>"u"&&(window.pymaplibregl=function({mapOptions:r,calls:e}){let t="pymaplibregl",n=document.getElementById(t),a=new S(Object.assign({container:n.id},r));a.getMap().on("load",()=>{a.render(e)})});if(typeof Shiny<"u"){class r extends Shiny.OutputBinding{find(t){return t.find(".shiny-maplibregl-output")}renderValue(t,n){console.log("id:",t.id,"payload:",n);let a=new S(Object.assign({container:t.id},n.mapData.mapOptions)),s=a.getMap();s.on("load",()=>{a.render(n.mapData.calls)}),s.on("click",i=>{console.log(i);let p=`${t.id}`,u={coords:i.lngLat,point:i.point};console.log(p,u),Shiny.onInputChange(p,u)});let o=`pymaplibregl-${t.id}`;console.log(o),Shiny.addCustomMessageHandler(o,({id:i,calls:p})=>{console.log(i,p),a.render(p)})}}Shiny.outputBindings.register(new r,"shiny-maplibregl-output")}})(); +/*! Bundled license information: - // srcjs/index.js - var version = "0.1.0"; - console.log("pymaplibregl", version); - if (typeof Shiny === "undefined") { - window.pymaplibregl = function({ mapOptions, calls }) { - const id = "pymaplibregl"; - const container = document.getElementById(id); - const pyMapLibreGL = new PyMapLibreGL( - Object.assign({ container: container.id }, mapOptions) - ); - const map = pyMapLibreGL.getMap(); - map.on("load", () => { - pyMapLibreGL.render(calls); - }); - }; - } - if (typeof Shiny !== "undefined") { - class MapLibreGLOutputBinding extends Shiny.OutputBinding { - find(scope) { - return scope.find(".shiny-maplibregl-output"); - } - renderValue(el, payload) { - console.log("id:", el.id, "payload:", payload); - const pyMapLibreGL = new PyMapLibreGL( - Object.assign({ container: el.id }, payload.mapData.mapOptions) - ); - const map = pyMapLibreGL.getMap(); - map.on("load", () => { - pyMapLibreGL.render(payload.mapData.calls); - }); - map.on("click", (e) => { - console.log(e); - const inputName = `${el.id}`; - const data = { coords: e.lngLat, point: e.point }; - console.log(inputName, data); - Shiny.onInputChange(inputName, data); - }); - const messageHandlerName = `pymaplibregl-${el.id}`; - console.log(messageHandlerName); - Shiny.addCustomMessageHandler(messageHandlerName, ({ id, calls }) => { - console.log(id, calls); - pyMapLibreGL.render(calls); - }); - } - } - Shiny.outputBindings.register( - new MapLibreGLOutputBinding(), - "shiny-maplibregl-output" - ); - } -})(); +mustache/mustache.mjs: + (*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + *) +*/ diff --git a/maplibre/srcjs/ipywidget.js b/maplibre/srcjs/ipywidget.js index 3a488ce7..f7dbaabf 100644 --- a/maplibre/srcjs/ipywidget.js +++ b/maplibre/srcjs/ipywidget.js @@ -1,122 +1,12 @@ -// srcjs/ipywidget.js -import maplibregl from "https://esm.sh/maplibre-gl@3.6.2"; +import R from"https://esm.sh/maplibre-gl@3.6.2";var H=Object.prototype.toString,T=Array.isArray||function(e){return H.call(e)==="[object Array]"};function U(n){return typeof n=="function"}function N(n){return T(n)?"array":typeof n}function L(n){return n.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function _(n,e){return n!=null&&typeof n=="object"&&e in n}function $(n,e){return n!=null&&typeof n!="object"&&n.hasOwnProperty&&n.hasOwnProperty(e)}var B=RegExp.prototype.test;function D(n,e){return B.call(n,e)}var z=/\S/;function K(n){return!D(z,n)}var G={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function J(n){return String(n).replace(/[&<>"'`=\/]/g,function(t){return G[t]})}var Q=/\s*/,V=/\s+/,A=/\s*=/,X=/\s*\}/,Y=/#|\^|\/|>|\{|&|=|!/;function Z(n,e){if(!n)return[];var t=!1,r=[],s=[],a=[],o=!1,i=!1,c="",u=0;function p(){if(o&&!i)for(;a.length;)delete s[a.pop()];else a=[];o=!1,i=!1}var g,C,m;function O(y){if(typeof y=="string"&&(y=y.split(V,2)),!T(y)||y.length!==2)throw new Error("Invalid tags: "+y);g=new RegExp(L(y[0])+"\\s*"),C=new RegExp("\\s*"+L(y[1])),m=new RegExp("\\s*"+L("}"+y[1]))}O(e||v.tags);for(var l=new S(n),w,h,d,E,P,b;!l.eos();){if(w=l.pos,d=l.scanUntil(g),d)for(var x=0,q=d.length;x"?P=[h,d,w,l.pos,c,u,t]:P=[h,d,w,l.pos],u++,s.push(P),h==="#"||h==="^")r.push(P);else if(h==="/"){if(b=r.pop(),!b)throw new Error('Unopened section "'+d+'" at '+w);if(b[1]!==d)throw new Error('Unclosed section "'+b[1]+'" at '+w)}else h==="name"||h==="{"||h==="&"?i=!0:h==="="&&O(d)}if(p(),b=r.pop(),b)throw new Error('Unclosed section "'+b[1]+'" at '+l.pos);return te(ee(s))}function ee(n){for(var e=[],t,r,s=0,a=n.length;s0?r[r.length-1][4]:e;break;default:t.push(s)}return e}function S(n){this.string=n,this.tail=n,this.pos=0}S.prototype.eos=function(){return this.tail===""};S.prototype.scan=function(e){var t=this.tail.match(e);if(!t||t.index!==0)return"";var r=t[0];return this.tail=this.tail.substring(r.length),this.pos+=r.length,r};S.prototype.scanUntil=function(e){var t=this.tail.search(e),r;switch(t){case-1:r=this.tail,this.tail="";break;case 0:r="";break;default:r=this.tail.substring(0,t),this.tail=this.tail.substring(t)}return this.pos+=r.length,r};function k(n,e){this.view=n,this.cache={".":this.view},this.parent=e}k.prototype.push=function(e){return new k(e,this)};k.prototype.lookup=function(e){var t=this.cache,r;if(t.hasOwnProperty(e))r=t[e];else{for(var s=this,a,o,i,c=!1;s;){if(e.indexOf(".")>0)for(a=s.view,o=e.split("."),i=0;a!=null&&i"?u=this.renderPartial(i,t,r,a):c==="&"?u=this.unescapedValue(i,t):c==="name"?u=this.escapedValue(i,t,a):c==="text"&&(u=this.rawValue(i)),u!==void 0&&(o+=u);return o};f.prototype.renderSection=function(e,t,r,s,a){var o=this,i="",c=t.lookup(e[1]);function u(C){return o.render(C,t,r,a)}if(c){if(T(c))for(var p=0,g=c.length;p0||!r)&&(a[o]=s+a[o]);return a.join(` +`)};f.prototype.renderPartial=function(e,t,r,s){if(r){var a=this.getConfigTags(s),o=U(r)?r(e[1]):r[e[1]];if(o!=null){var i=e[6],c=e[5],u=e[4],p=o;c==0&&u&&(p=this.indentPartial(o,u,i));var g=this.parse(p,a);return this.renderTokens(g,t,r,p,s)}}};f.prototype.unescapedValue=function(e,t){var r=t.lookup(e[1]);if(r!=null)return r};f.prototype.escapedValue=function(e,t,r){var s=this.getConfigEscape(r)||v.escape,a=t.lookup(e[1]);if(a!=null)return typeof a=="number"&&s===v.escape?String(a):s(a)};f.prototype.rawValue=function(e){return e[1]};f.prototype.getConfigTags=function(e){return T(e)?e:e&&typeof e=="object"?e.tags:void 0};f.prototype.getConfigEscape=function(e){if(e&&typeof e=="object"&&!T(e))return e.escape};var v={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:void 0,escape:void 0,parse:void 0,render:void 0,Scanner:void 0,Context:void 0,Writer:void 0,set templateCache(n){M.templateCache=n},get templateCache(){return M.templateCache}},M=new f;v.clearCache=function(){return M.clearCache()};v.parse=function(e,t){return M.parse(e,t)};v.render=function(e,t,r,s){if(typeof e!="string")throw new TypeError('Invalid template! Template should be a "string" but "'+N(e)+'" was given as the first argument for mustache#render(template, view, partials)');return M.render(e,t,r,s)};v.escape=J;v.Scanner=S;v.Context=k;v.Writer=f;var F=v;function j(n,e,t){return t!==null?F.render(t,n.properties):e===null?Object.keys(n.properties).map(s=>`${s}: ${n.properties[s]}`).join("
"):n.properties[e]}function I(n,e){let[t,r]=e;console.log(t,r),n[t](...r)}function W(n,e){return{addTooltip:function(t,r=null,s=null){let a={closeButton:!1,closeOnClick:!1},o=new n.Popup(a);e.on("mousemove",t,i=>{let c=i.features[0],u=j(c,r,s);o.setLngLat(i.lngLat).setHTML(u).addTo(e)}),e.on("mouseleave",t,()=>{o.remove()})},addControl:function(t,r,s){e.addControl(new n[t](r),s)},addPopup:function(t,r=null,s=null){let a={closeButton:!1},o=new n.Popup(a);e.on("click",t,i=>{let c=i.features[0],u=j(c,r,s);o.setLngLat(i.lngLat).setHTML(u).addTo(e)})},addMarker:function({lngLat:t,popup:r,options:s}){let a=new n.Marker(s).setLngLat(t);if(r){let o=new n.Popup(r.options).setHTML(r.text);a.setPopup(o)}a.addTo(e)},setSourceData:function(t,r){e.getSource(t).setData(r)}}}function ne(n){let e="pymaplibregl",t=document.createElement("div");return t.id=e,t.style.height=n.get("height"),t}function re(n,e){let t=new R.Map(n);return n.navigationControl===void 0&&(n.navigationControl=!0),n.navigationControl&&t.addControl(new R.NavigationControl),t.on("mouseover",()=>{t.getCanvas().style.cursor="pointer"}),t.on("mouseout",()=>{t.getCanvas().style.cursor=""}),t.on("click",r=>{e.set("lng_lat",r.lngLat),e.save_changes()}),t.once("load",()=>{t.resize()}),t}function pe({model:n,el:e}){console.log("maplibregl",R.version);let t=ne(n),r=Object.assign({container:t},n.get("map_options"));console.log(r);let s=re(r,n),a=W(R,s),o=c=>{c.forEach(u=>{if(Object.keys(a).includes(u[0])){console.log("internal call",u);let[p,g]=u;a[p](...g);return}I(s,u)})},i=n.get("calls");s.on("load",()=>{console.log("init calls",i),o(i),n.set("_rendered",!0),n.save_changes()}),n.on("msg:custom",c=>{console.log("custom msg",c),o(c.calls)}),e.appendChild(t)}export{pe as render}; +/*! Bundled license information: -// srcjs/mapmethods.js -function applyMapMethod(map, call) { - const [methodName, params] = call; - console.log(methodName, params); - map[methodName](...params); -} -function getCustomMapMethods(maplibregl2, map) { - return { - addTooltip: function(layerId, property) { - const popupOptions = { - closeButton: false, - closeOnClick: false - }; - const popup = new maplibregl2.Popup(popupOptions); - map.on("mousemove", layerId, (e) => { - const feature = e.features[0]; - const text = feature.properties[property]; - popup.setLngLat(e.lngLat).setHTML(text).addTo(map); - }); - map.on("mouseleave", layerId, () => { - popup.remove(); - }); - }, - addControl: function(type, options, position) { - map.addControl(new maplibregl2[type](options), position); - }, - addPopup: function(layerId, property) { - const popupOptions = { - closeButton: false - }; - const popup = new maplibregl2.Popup(popupOptions); - map.on("click", layerId, (e) => { - const feature = e.features[0]; - const text = feature.properties[property]; - popup.setLngLat(e.lngLat).setHTML(text).addTo(map); - }); - }, - addMarker: function({ lngLat, popup, options }) { - const marker = new maplibregl2.Marker(options).setLngLat(lngLat); - if (popup) { - const popup_ = new maplibregl2.Popup(popup.options).setHTML(popup.text); - marker.setPopup(popup_); - } - marker.addTo(map); - }, - setSourceData: function(sourceId, data) { - map.getSource(sourceId).setData(data); - } - }; -} - -// srcjs/ipywidget.js -function createContainer(model) { - const id = "pymaplibregl"; - const container = document.createElement("div"); - container.id = id; - container.style.height = model.get("height"); - return container; -} -function createMap(mapOptions, model) { - const map = new maplibregl.Map(mapOptions); - if (mapOptions.navigationControl === void 0) { - mapOptions.navigationControl = true; - } - if (mapOptions.navigationControl) { - map.addControl(new maplibregl.NavigationControl()); - } - map.on("mouseover", () => { - map.getCanvas().style.cursor = "pointer"; - }); - map.on("mouseout", () => { - map.getCanvas().style.cursor = ""; - }); - map.on("click", (e) => { - model.set("lng_lat", e.lngLat); - model.save_changes(); - }); - map.once("load", () => { - map.resize(); - }); - return map; -} -function render({ model, el }) { - console.log("maplibregl", maplibregl.version); - const container = createContainer(model); - const mapOptions = Object.assign( - { container }, - model.get("map_options") - ); - console.log(mapOptions); - const map = createMap(mapOptions, model); - const customMapMethods = getCustomMapMethods(maplibregl, map); - const apply = (calls2) => { - calls2.forEach((call) => { - if (Object.keys(customMapMethods).includes(call[0])) { - console.log("internal call", call); - const [name, params] = call; - customMapMethods[name](...params); - return; - } - applyMapMethod(map, call); - }); - }; - const calls = model.get("calls"); - map.on("load", () => { - console.log("init calls", calls); - apply(calls); - model.set("_rendered", true); - model.save_changes(); - }); - model.on("msg:custom", (msg) => { - console.log("custom msg", msg); - apply(msg.calls); - }); - el.appendChild(container); -} -export { - render -}; +mustache/mustache.mjs: + (*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + *) +*/ diff --git a/mkdocs.yml b/mkdocs.yml index 95e5a0d3..24767f1b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ nav: - Layers: layers.md - Shiny: shiny.md - Jupyter: jupyter.md + - Changelog: changelog.md - API Documentation: - Map: api/map.md - Layer: api/layer.md @@ -29,7 +30,7 @@ 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 + - H3 Grid UK Road Safety: examples/road_safety/index.md - Where is the ISS: examples/where_is_the_iss/index.md plugins: diff --git a/package-lock.json b/package-lock.json index da69116b..92d81e05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,16 @@ { "name": "pymaplibregl", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pymaplibregl", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", + "dependencies": { + "mustache": "^4.2.0" + }, "devDependencies": { "esbuild": "0.19.10", "prettier": "3.1.1" @@ -419,6 +422,14 @@ "@esbuild/win32-x64": "0.19.10" } }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/prettier": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", diff --git a/package.json b/package.json index c4f6b08a..afbf809e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pymaplibregl", - "version": "0.1.0", + "version": "0.1.1", "description": "...", "main": "index.js", "directories": { @@ -8,8 +8,10 @@ "test": "tests" }, "scripts": { - "build": "esbuild srcjs/index.js --bundle --outfile=maplibre/srcjs/index.js", - "build-ipywidget": "esbuild srcjs/ipywidget.js --bundle --format=esm --outfile=maplibre/srcjs/ipywidget.js", + "build": "esbuild srcjs/index.js --bundle --minify --outfile=maplibre/srcjs/index.js", + "build-dev": "esbuild srcjs/index.js --bundle --outfile=maplibre/srcjs/index.js", + "build-ipywidget": "esbuild srcjs/ipywidget.js --bundle --minify --format=esm --outfile=maplibre/srcjs/ipywidget.js", + "build-ipywidget-dev": "esbuild srcjs/ipywidget.js --bundle --format=esm --outfile=maplibre/srcjs/ipywidget.js", "build-rwidget": "esbuild srcjs/rwidget.js --bundle --minify --outfile=../r-maplibregl/inst/htmlwidgets/maplibre.js", "prettier": "prettier srcjs --write", "test": "echo \"Error: no test specified\" && exit 1" @@ -19,5 +21,8 @@ "devDependencies": { "esbuild": "0.19.10", "prettier": "3.1.1" + }, + "dependencies": { + "mustache": "^4.2.0" } } diff --git a/pyproject.toml b/pyproject.toml index 43e3cd7d..af4a8927 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maplibre" -version = "0.1.2" +version = "0.1.3" description = "Python bindings for MapLibre GL JS" authors = ["Stefan Kuethe "] readme = "README.md" diff --git a/srcjs/mapmethods.js b/srcjs/mapmethods.js index 17d094fd..573a2b0a 100644 --- a/srcjs/mapmethods.js +++ b/srcjs/mapmethods.js @@ -1,3 +1,5 @@ +import { getTextFromFeature } from "./utils"; + function applyMapMethod(map, call) { const [methodName, params] = call; console.log(methodName, params); @@ -8,7 +10,7 @@ function applyMapMethod(map, call) { // Custom map methods function getCustomMapMethods(maplibregl, map) { return { - addTooltip: function (layerId, property) { + addTooltip: function (layerId, property = null, template = null) { const popupOptions = { closeButton: false, closeOnClick: false, @@ -17,7 +19,9 @@ function getCustomMapMethods(maplibregl, map) { map.on("mousemove", layerId, (e) => { const feature = e.features[0]; - const text = feature.properties[property]; + + // const text = feature.properties[property]; + const text = getTextFromFeature(feature, property, template); popup.setLngLat(e.lngLat).setHTML(text).addTo(map); }); @@ -30,14 +34,16 @@ function getCustomMapMethods(maplibregl, map) { map.addControl(new maplibregl[type](options), position); }, - addPopup: function (layerId, property) { + addPopup: function (layerId, property = null, template = null) { const popupOptions = { closeButton: false, }; const popup = new maplibregl.Popup(popupOptions); map.on("click", layerId, (e) => { const feature = e.features[0]; - const text = feature.properties[property]; + + // const text = feature.properties[property]; + const text = getTextFromFeature(feature, property, template); popup.setLngLat(e.lngLat).setHTML(text).addTo(map); }); }, diff --git a/srcjs/pymaplibregl.js b/srcjs/pymaplibregl.js index 52e0e4c6..2538c850 100644 --- a/srcjs/pymaplibregl.js +++ b/srcjs/pymaplibregl.js @@ -1,4 +1,5 @@ // import { getCustomMapMethods } from "./mapmethods"; +import { getTextFromFeature } from "./utils"; export default class PyMapLibreGL { constructor(mapOptions) { @@ -59,19 +60,20 @@ export default class PyMapLibreGL { } } - addPopup(layerId, property) { + addPopup(layerId, property = null, template = null) { const popupOptions = { closeButton: false, }; const popup = new maplibregl.Popup(popupOptions); this._map.on("click", layerId, (e) => { const feature = e.features[0]; - const text = feature.properties[property]; + // const text = feature.properties[property]; + const text = getTextFromFeature(feature, property, template); popup.setLngLat(e.lngLat).setHTML(text).addTo(this._map); }); } - addTooltip(layerId, property) { + addTooltip(layerId, property = null, template = null) { const popupOptions = { closeButton: false, closeOnClick: false, @@ -79,7 +81,7 @@ export default class PyMapLibreGL { const popup = new maplibregl.Popup(popupOptions); this._map.on("mousemove", layerId, (e) => { const feature = e.features[0]; - const text = feature.properties[property]; + const text = getTextFromFeature(feature, property, template); popup.setLngLat(e.lngLat).setHTML(text).addTo(this._map); }); diff --git a/srcjs/utils.js b/srcjs/utils.js new file mode 100644 index 00000000..cedd917b --- /dev/null +++ b/srcjs/utils.js @@ -0,0 +1,18 @@ +import mustache from "mustache"; + +function getTextFromFeature(feature, property, template) { + if (template !== null) { + return mustache.render(template, feature.properties); + } + + if (property === null) { + const text = Object.keys(feature.properties) + .map((key) => `${key}: ${feature.properties[key]}`) + .join("
"); + return text; + } + + return feature.properties[property]; +} + +export { getTextFromFeature };