leaflet.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import asyncio
  2. from pathlib import Path
  3. from typing import Any, Dict, List, Tuple, Union, cast
  4. from typing_extensions import Self
  5. from .. import binding
  6. from ..awaitable_response import AwaitableResponse, NullResponse
  7. from ..element import Element
  8. from ..events import GenericEventArguments
  9. from .leaflet_layer import Layer
  10. class Leaflet(Element, component='leaflet.js'):
  11. # pylint: disable=import-outside-toplevel
  12. from .leaflet_layers import GenericLayer as generic_layer
  13. from .leaflet_layers import Marker as marker
  14. from .leaflet_layers import TileLayer as tile_layer
  15. center = binding.BindableProperty(lambda sender, value: cast(Leaflet, sender).set_center(value))
  16. zoom = binding.BindableProperty(lambda sender, value: cast(Leaflet, sender).set_zoom(value))
  17. def __init__(self,
  18. center: Tuple[float, float] = (0.0, 0.0),
  19. zoom: int = 13,
  20. *,
  21. options: Dict = {}, # noqa: B006
  22. draw_control: Union[bool, Dict] = False,
  23. ) -> None:
  24. """Leaflet map
  25. This element is a wrapper around the `Leaflet <https://leafletjs.com/>`_ JavaScript library.
  26. :param center: initial center location of the map (latitude/longitude, default: (0.0, 0.0))
  27. :param zoom: initial zoom level of the map (default: 13)
  28. :param draw_control: whether to show the draw toolbar (default: False)
  29. :param options: additional options passed to the Leaflet map (default: {})
  30. """
  31. super().__init__()
  32. self.add_resource(Path(__file__).parent / 'lib' / 'leaflet')
  33. self._classes.append('nicegui-leaflet')
  34. self.layers: List[Layer] = []
  35. self.is_initialized = False
  36. self.center = center
  37. self.zoom = zoom
  38. self._props['center'] = center
  39. self._props['zoom'] = zoom
  40. self._props['options'] = {**options}
  41. self._props['draw_control'] = draw_control
  42. self.on('init', self._handle_init)
  43. self.on('map-moveend', self._handle_moveend)
  44. self.on('map-zoomend', self._handle_zoomend)
  45. self.tile_layer(
  46. url_template=r'https://{s}.tile.osm.org/{z}/{x}/{y}.png',
  47. options={'attribution': '&copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors'},
  48. )
  49. def __enter__(self) -> Self:
  50. Layer.current_leaflet = self
  51. return super().__enter__()
  52. def __getattribute__(self, name: str) -> Any:
  53. attribute = super().__getattribute__(name)
  54. if isinstance(attribute, type) and issubclass(attribute, Layer):
  55. Layer.current_leaflet = self
  56. return attribute
  57. def _handle_init(self, e: GenericEventArguments) -> None:
  58. self.is_initialized = True
  59. with self.client.individual_target(e.args['socket_id']):
  60. for layer in self.layers:
  61. self.run_method('add_layer', layer.to_dict(), layer.id)
  62. async def initialized(self) -> None:
  63. """Wait until the map is initialized."""
  64. event = asyncio.Event()
  65. self.on('init', event.set, [])
  66. await self.client.connected()
  67. await event.wait()
  68. async def _handle_moveend(self, e: GenericEventArguments) -> None:
  69. await asyncio.sleep(0.02) # NOTE: wait for zoom to be updated as well
  70. self.center = e.args['center']
  71. async def _handle_zoomend(self, e: GenericEventArguments) -> None:
  72. await asyncio.sleep(0.02) # NOTE: wait for center to be updated as well
  73. self.zoom = e.args['zoom']
  74. def run_method(self, name: str, *args: Any, timeout: float = 1, check_interval: float = 0.01) -> AwaitableResponse:
  75. if not self.is_initialized:
  76. return NullResponse()
  77. return super().run_method(name, *args, timeout=timeout, check_interval=check_interval)
  78. def set_center(self, center: Tuple[float, float]) -> None:
  79. """Set the center location of the map."""
  80. if self._props['center'] == center:
  81. return
  82. self._props['center'] = center
  83. self.update()
  84. def set_zoom(self, zoom: int) -> None:
  85. """Set the zoom level of the map."""
  86. if self._props['zoom'] == zoom:
  87. return
  88. self._props['zoom'] = zoom
  89. self.update()
  90. def remove_layer(self, layer: Layer) -> None:
  91. """Remove a layer from the map."""
  92. self.layers.remove(layer)
  93. self.run_method('remove_layer', layer.id)
  94. def clear_layers(self) -> None:
  95. """Remove all layers from the map."""
  96. self.layers.clear()
  97. self.run_method('clear_layers')
  98. def run_map_method(self, name: str, *args, timeout: float = 1, check_interval: float = 0.01) -> AwaitableResponse:
  99. """Run a method of the Leaflet map instance.
  100. Refer to the `Leaflet documentation <https://leafletjs.com/reference.html#map-methods-for-modifying-map-state>`_ for a list of methods.
  101. If the function is awaited, the result of the method call is returned.
  102. Otherwise, the method is executed without waiting for a response.
  103. :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
  104. :param args: arguments to pass to the method
  105. :param timeout: timeout in seconds (default: 1 second)
  106. :return: AwaitableResponse that can be awaited to get the result of the method call
  107. """
  108. return self.run_method('run_map_method', name, *args, timeout=timeout, check_interval=check_interval)
  109. def run_layer_method(self, layer_id: str, name: str, *args, timeout: float = 1, check_interval: float = 0.01) -> AwaitableResponse:
  110. """Run a method of a Leaflet layer.
  111. If the function is awaited, the result of the method call is returned.
  112. Otherwise, the method is executed without waiting for a response.
  113. :param layer_id: ID of the layer
  114. :param name: name of the method (a prefix ":" indicates that the arguments are JavaScript expressions)
  115. :param args: arguments to pass to the method
  116. :param timeout: timeout in seconds (default: 1 second)
  117. :return: AwaitableResponse that can be awaited to get the result of the method call
  118. """
  119. return self.run_method('run_layer_method', layer_id, name, *args, timeout=timeout, check_interval=check_interval)
  120. def _handle_delete(self) -> None:
  121. binding.remove(self.layers)
  122. super()._handle_delete()