Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Leaflet element for interactive maps #1217

Merged
merged 44 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
60d02ee
start to implement leaflet element
falkoschindler Jul 20, 2023
bf650b9
introduce LeafletLayer
falkoschindler Jul 21, 2023
9b0e527
add support for multiple layers
falkoschindler Jul 21, 2023
dd7b741
add draw controls
falkoschindler Jul 23, 2023
2cd346e
refactoring
falkoschindler Jul 23, 2023
306631f
Merge branch 'main' into leaflet
falkoschindler Oct 13, 2023
c3f1854
mypy and pylint
falkoschindler Oct 13, 2023
5373df4
Merge branch 'main' into leaflet
falkoschindler Dec 7, 2023
e26e392
Merge branch 'main' into leaflet
falkoschindler Dec 7, 2023
a3f4c9c
updates for new NiceGUI version
falkoschindler Dec 7, 2023
a7c5116
introduce loadResource()
falkoschindler Dec 8, 2023
9e7b7df
load resources in parallel
falkoschindler Dec 8, 2023
b465af3
serve CSS and JS resources locally
falkoschindler Dec 8, 2023
a347134
add local resources for leaflet-draw
falkoschindler Dec 8, 2023
38007e1
await leaflet before loading leaflet-draw
falkoschindler Dec 8, 2023
6e7f40a
fix pytests
falkoschindler Dec 8, 2023
2d9dd01
set default size
falkoschindler Dec 8, 2023
bcdcd7b
add simple pytest
falkoschindler Dec 8, 2023
ca6af29
tiny fix
falkoschindler Dec 8, 2023
7d23b0a
add some missing type annotations
falkoschindler Dec 8, 2023
f8382b3
add some documentation
falkoschindler Dec 8, 2023
68cda60
cleanup
falkoschindler Dec 8, 2023
9e099e9
use https for tiles
falkoschindler Dec 8, 2023
7c636dc
add generic layer
falkoschindler Dec 14, 2023
47f3fa2
fix type annotation for Python 3.8
falkoschindler Dec 14, 2023
9d3177f
leaflet docs: remove default tile_layer
rodja Dec 15, 2023
ab942d0
leaflet click event demo (does not work yet)
rodja Dec 15, 2023
4da5322
leaflet demo showing generic_layer usage
rodja Dec 15, 2023
1ecbd5f
cleanup
rodja Dec 15, 2023
1085c97
fixed function name
rodja Dec 15, 2023
810492d
code review; introduce map mouse events
falkoschindler Dec 15, 2023
7ec31e7
rename show_draw_toolbar
falkoschindler Dec 15, 2023
58ccf3e
remove the old map example
falkoschindler Dec 15, 2023
eaf5047
use more comprehensive list of map events
falkoschindler Dec 15, 2023
2ae3031
avoid duplicate events
falkoschindler Dec 15, 2023
6fce331
provide draw events
falkoschindler Dec 15, 2023
bf0f7ca
fix draw_control and add demo
falkoschindler Dec 15, 2023
0485665
Merge branch 'main' into leaflet
falkoschindler Dec 15, 2023
26df7cc
rename "location" to "center" and "latlng"
falkoschindler Dec 15, 2023
dd988fb
add `options` parameter; update center and zoom independently
falkoschindler Dec 15, 2023
331a257
add leaflet options demo
rodja Dec 16, 2023
da7be6f
code review
falkoschindler Dec 16, 2023
c94b3a9
fix zoom and other problems
falkoschindler Dec 16, 2023
aed3ae9
add more options to anti-zoom demo
falkoschindler Dec 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- es-module-shims: 1.7.3
- aggrid: 30.0.3
- highcharts: 11.1.0
- leaflet: 1.9.4
- mermaid: 10.2.4
- nipplejs: 0.10.1
- plotly: 2.24.3
Expand Down
44 changes: 44 additions & 0 deletions nicegui/elements/leaflet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export default {
template: "<div></div>",
props: {
map_options: Object,
},
async mounted() {
await this.load_dependencies();
this.map = L.map(this.$el, this.map_options);
this.map.on("moveend", (e) => this.$emit("moveend", e.target.getCenter()));
this.map.on("zoomend", (e) => this.$emit("zoomend", e.target.getZoom()));
const connectInterval = setInterval(async () => {
if (window.socket.id === undefined) return;
this.$emit("init", { socket_id: window.socket.id });
clearInterval(connectInterval);
}, 100);
},
updated() {
this.map.setView(L.latLng(this.map_options.center.lat, this.map_options.center.lng), this.map_options.zoom);
},
methods: {
load_dependencies() {
if (!document.querySelector(`style[data-leaflet-css]`)) {
const link = document.createElement("link");
link.setAttribute("href", "https://unpkg.com/[email protected]/dist/leaflet.css");
link.setAttribute("rel", "stylesheet");
link.setAttribute("data-leaflet-css", "");
document.head.appendChild(link);
}
if (!document.querySelector(`script[data-leaflet-js]`)) {
const script = document.createElement("script");
script.setAttribute("src", "https://unpkg.com/[email protected]/dist/leaflet.js");
falkoschindler marked this conversation as resolved.
Show resolved Hide resolved
script.setAttribute("data-leaflet-js", "");
document.head.appendChild(script);
return new Promise((resolve, reject) => {
script.onload = resolve;
script.onerror = reject;
});
}
},
add_layer(layer) {
L[layer.type](...layer.args).addTo(this.map);
},
},
};
69 changes: 69 additions & 0 deletions nicegui/elements/leaflet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from typing import Any, List, Tuple, cast

from .. import binding, globals
from ..element import Element
from ..events import GenericEventArguments
from .leaflet_layer import Layer


class Leaflet(Element, component='leaflet.js'):
from .leaflet_layers import Marker as marker
from .leaflet_layers import TileLayer as tile_layer

location = binding.BindableProperty(lambda sender, _: cast(Leaflet, sender).update())
zoom = binding.BindableProperty(lambda sender, _: cast(Leaflet, sender).update())

def __init__(self, location: Tuple[float, float] = (0, 0), zoom: int = 13) -> None:
super().__init__()
self.layers: List[Layer] = []
self.set_location(location)
self.set_zoom(zoom)
self.is_initialized = False
self.on('init', self.handle_init)
self.on('moveend', lambda e: self.set_location((e.args['lat'], e.args['lng'])))
self.on('zoomend', lambda e: self.set_zoom(e.args))
self.tile_layer(
url_template='http://{s}.tile.osm.org/{z}/{x}/{y}.png',
options={'attribution': '&copy; <a href="https://openstreetmap.org">OpenStreetMap</a> contributors'},
)

def __enter__(self) -> 'Leaflet':
Layer.current_leaflet = self
return super().__enter__()

def __getattribute__(self, name: str) -> Any:
attribute = super().__getattribute__(name)
if isinstance(attribute, type) and issubclass(attribute, Layer):
Layer.current_leaflet = self
return attribute

def handle_init(self, e: GenericEventArguments) -> None:
self.is_initialized = True
with globals.socket_id(e.args['socket_id']):
for layer in self.layers:
self.run_method('add_layer', layer.to_dict())

def run_method(self, name: str, *args: Any) -> None:
if not self.is_initialized:
return
super().run_method(name, *args)

def set_location(self, location: Tuple[float, float]) -> None:
self.location = location

def set_zoom(self, zoom: int) -> None:
self.zoom = zoom

def update(self) -> None:
self._props['map_options'] = {
'center': {
'lat': self.location[0],
'lng': self.location[1],
},
'zoom': self.zoom,
}
super().update()

def delete(self) -> None:
binding.remove(self.layers, Layer)
super().delete()
23 changes: 23 additions & 0 deletions nicegui/elements/leaflet_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from abc import abstractmethod
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, ClassVar, Optional

from ..helpers import KWONLY_SLOTS

if TYPE_CHECKING:
from .leaflet import Leaflet


@dataclass(**KWONLY_SLOTS)
class Layer:
current_leaflet: ClassVar[Optional['Leaflet']] = None
leaflet: 'Leaflet' = field(init=False)

def __post_init__(self) -> None:
self.leaflet = self.current_leaflet
self.leaflet.layers.append(self)
self.leaflet.run_method('add_layer', self.to_dict())

@abstractmethod
def to_dict(self) -> dict:
pass
35 changes: 35 additions & 0 deletions nicegui/elements/leaflet_layers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from dataclasses import dataclass, field
from typing import Dict, Tuple

from typing_extensions import Self

from ..helpers import KWONLY_SLOTS
from .leaflet_layer import Layer


@dataclass(**KWONLY_SLOTS)
class TileLayer(Layer):
url_template: str
options: Dict = field(default_factory=dict)

def to_dict(self) -> Dict:
return {
'type': 'tileLayer',
'args': [self.url_template, self.options],
}


@dataclass(**KWONLY_SLOTS)
class Marker(Layer):
location: Tuple[float, float]
options: Dict = field(default_factory=dict)

def to_dict(self) -> Dict:
return {
'type': 'marker',
'args': [{'lat': self.location[0], 'lng': self.location[1]}, self.options],
}

def draggable(self, value: bool = True) -> Self:
self.options['draggable'] = value
return self
Binary file added nicegui/elements/lib/leaflet/images/layers-2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added nicegui/elements/lib/leaflet/images/layers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading