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 all 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
2 changes: 2 additions & 0 deletions DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- es-module-shims: 1.8.0 ([MIT](https://opensource.org/licenses/MIT))
- aggrid: 30.2.0 ([MIT](https://opensource.org/licenses/MIT))
- echarts: 5.4.3 ([Apache-2.0](https://opensource.org/licenses/Apache-2.0))
- leaflet: 1.9.4 ([BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause))
- leaflet-draw: 1.0.4 ([MIT](https://opensource.org/licenses/MIT))
- mermaid: 10.5.1 ([MIT](https://opensource.org/licenses/MIT))
- nipplejs: 0.10.1 ([MIT](https://opensource.org/licenses/MIT))
- plotly: 2.27.0 ([MIT](https://opensource.org/licenses/MIT))
Expand Down
20 changes: 0 additions & 20 deletions examples/map/leaflet.js

This file was deleted.

14 changes: 0 additions & 14 deletions examples/map/leaflet.py

This file was deleted.

22 changes: 0 additions & 22 deletions examples/map/main.py

This file was deleted.

5 changes: 3 additions & 2 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from fastapi import Request
from fastapi.responses import Response
from fastapi.templating import Jinja2Templates
from typing_extensions import Self

from . import background_tasks, binding, core, helpers, json, outbox
from .awaitable_response import AwaitableResponse
Expand Down Expand Up @@ -98,11 +99,11 @@ def body_html(self) -> str:
"""Return the HTML code to be inserted in the <body> of the page template."""
return self.shared_body_html + self._body_html

def __enter__(self):
def __enter__(self) -> Self:
self.content.__enter__()
return self

def __exit__(self, *_):
def __exit__(self, *_) -> None:
self.content.__exit__()

def build_response(self, request: Request, status_code: int = 200) -> Response:
Expand Down
22 changes: 21 additions & 1 deletion nicegui/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ class JsComponent(Component):
pass


@dataclass(**KWONLY_SLOTS)
class Resource:
key: str
path: Path


@dataclass(**KWONLY_SLOTS)
class Library:
key: str
Expand All @@ -49,6 +55,7 @@ class Library:
vue_components: Dict[str, VueComponent] = {}
js_components: Dict[str, JsComponent] = {}
libraries: Dict[str, Library] = {}
resources: Dict[str, Resource] = {}


def register_vue_component(path: Path) -> Component:
Expand Down Expand Up @@ -89,17 +96,30 @@ def register_library(path: Path, *, expose: bool = False) -> Library:
raise ValueError(f'Unsupported library type "{path.suffix}"')


def register_resource(path: Path) -> Resource:
"""Register a resource."""
key = compute_key(path)
if key in resources and resources[key].path == path:
return resources[key]
assert key not in resources, f'Duplicate resource {key}'
resources[key] = Resource(key=key, path=path)
return resources[key]


def compute_key(path: Path) -> str:
"""Compute a key for a given path using a hash function.

If the path is relative to the NiceGUI base directory, the key is computed from the relative path.
"""
nicegui_base = Path(__file__).parent
is_file = path.is_file()
try:
path = path.relative_to(nicegui_base)
except ValueError:
pass
return f'{hash_file_path(path.parent)}/{path.name}'
if is_file:
return f'{hash_file_path(path.parent)}/{path.name}'
return f'{hash_file_path(path)}'


def _get_name(path: Path) -> str:
Expand Down
13 changes: 11 additions & 2 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@

from . import context, core, events, helpers, json, outbox, storage
from .awaitable_response import AwaitableResponse, NullResponse
from .dependencies import Component, Library, register_library, register_vue_component
from .dependencies import Component, Library, register_library, register_resource, register_vue_component
from .elements.mixins.visibility import Visibility
from .event_listener import EventListener
from .slot import Slot
from .tailwind import Tailwind
from .version import __version__

if TYPE_CHECKING:
from .client import Client
Expand Down Expand Up @@ -138,6 +139,14 @@ def glob_absolute_paths(file: Union[str, Path]) -> List[Path]:
cls._default_classes = copy(cls._default_classes)
cls._default_style = copy(cls._default_style)

def add_resource(self, path: Union[str, Path]) -> None:
"""Add a resource to the element.

:param path: path to the resource (e.g. folder with CSS and JavaScript files)
"""
resource = register_resource(Path(path))
self._props['resource_path'] = f'/_nicegui/{__version__}/resources/{resource.key}'

def add_slot(self, name: str, template: Optional[str] = None) -> Slot:
"""Add a slot to the element.

Expand All @@ -152,7 +161,7 @@ def __enter__(self) -> Self:
self.default_slot.__enter__()
return self

def __exit__(self, *_):
def __exit__(self, *_) -> None:
self.default_slot.__exit__(*_)

def __iter__(self) -> Iterator[Element]:
Expand Down
115 changes: 115 additions & 0 deletions nicegui/elements/leaflet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { loadResource } from "../../static/utils/resources.js";

export default {
template: "<div></div>",
props: {
center: Array,
zoom: Number,
options: Object,
draw_control: Object,
resource_path: String,
},
async mounted() {
await this.$nextTick(); // NOTE: wait for window.path_prefix to be set
await Promise.all([
loadResource(window.path_prefix + `${this.resource_path}/leaflet/leaflet.css`),
loadResource(window.path_prefix + `${this.resource_path}/leaflet/leaflet.js`),
]);
if (this.draw_control) {
await Promise.all([
loadResource(window.path_prefix + `${this.resource_path}/leaflet-draw/leaflet.draw.css`),
loadResource(window.path_prefix + `${this.resource_path}/leaflet-draw/leaflet.draw.js`),
]);
}
this.map = L.map(this.$el, {
...this.options,
center: this.center,
zoom: this.zoom,
});
for (const type of [
"baselayerchange",
"overlayadd",
"overlayremove",
"layeradd",
"layerremove",
"zoomlevelschange",
"resize",
"unload",
"viewreset",
"load",
"zoomstart",
"movestart",
"zoom",
"move",
"zoomend",
"moveend",
"popupopen",
"popupclose",
"autopanstart",
"tooltipopen",
"tooltipclose",
"locationerror",
"locationfound",
"click",
"dblclick",
"mousedown",
"mouseup",
"mouseover",
"mouseout",
"mousemove",
"contextmenu",
"keypress",
"keydown",
"keyup",
"preclick",
"zoomanim",
]) {
this.map.on(type, (e) => {
this.$emit(`map-${type}`, {
...e,
originalEvent: undefined,
target: undefined,
sourceTarget: undefined,
center: [e.target.getCenter().lat, e.target.getCenter().lng],
zoom: e.target.getZoom(),
});
});
}
if (this.draw_control) {
for (const key in L.Draw.Event) {
const type = L.Draw.Event[key];
this.map.on(type, (e) => {
this.$emit(type, {
...e,
layer: e.layer ? { ...e.layer, editing: undefined, _events: undefined } : undefined,
target: undefined,
sourceTarget: undefined,
});
});
}
const drawnItems = new L.FeatureGroup();
this.map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
edit: { featureGroup: drawnItems },
...this.draw_control,
});
this.map.addControl(drawControl);
}
const connectInterval = setInterval(async () => {
if (window.socket.id === undefined) return;
this.$emit("init", { socket_id: window.socket.id });
clearInterval(connectInterval);
}, 100);
},
methods: {
setCenter(center) {
this.map.panTo(center);
},
setZoom(zoom) {
this.map.setZoom(zoom);
},
add_layer(layer) {
L[layer.type](...layer.args).addTo(this.map);
},
},
};
Loading
Loading