leptos_leaflet/components/
map_container.rs

1use leaflet::Map;
2use leptos::{html::Div, prelude::*};
3use wasm_bindgen::prelude::*;
4use web_sys::HtmlDivElement;
5
6use leaflet::LocateOptions;
7
8use crate::core::JsWriteSignal;
9
10use super::{provide_leaflet_context, MapEvents, PopupEvents, Position, TooltipEvents};
11
12/// A container for the Leaflet map.
13///
14/// This is the main container for the Leaflet map. It provides a way to add child nodes to the map.
15/// It also provides a signal to access the map instance, allowing to interact with the map from other components.
16#[component]
17pub fn MapContainer(
18    #[prop(into, optional)] class: Signal<String>,
19    #[prop(into, optional)] style: Signal<String>,
20    /// Centers the map on the given location
21    #[prop(into, optional)]
22    center: Option<Position>,
23    /// Zoom level of the map. Defaults to 10.0
24    #[prop(optional, default = 10.0)]
25    zoom: f64,
26    /// Wether zoom controls should be added to the map.
27    #[prop(optional, default = true)]
28    zoom_control: bool,
29    /// Wether mouse wheel zoom controls is enabled or disabled.
30    #[prop(optional, default = true)]
31    scroll_wheel_zoom: bool,
32    /// Zoom snap of the map. Defaults to 1.0
33    #[prop(optional, default = 1.0)]
34    zoom_snap: f64,
35    /// Zoom delta of the map. Defaults to 1.0
36    #[prop(optional, default = 1.0)]
37    zoom_delta: f64,
38    /// Allow zoom on double_click
39    #[prop(optional, default = true)]
40    double_click_zoom: bool,
41    /// Sets the minimum zoom level
42    #[prop(optional, default = 0.0)]
43    min_zoom: f64,
44    /// Use geolocation from the browser to track the user
45    #[prop(optional)]
46    locate: bool,
47    /// Tracks position of the user on the map
48    #[prop(optional)]
49    watch: bool,
50    /// Enables high-accuracy tracking
51    #[prop(optional)]
52    enable_high_accuracy: bool,
53    /// Sets the view of the map if geolocation is available
54    #[prop(optional)]
55    set_view: bool,
56    #[prop(optional)] map: Option<JsWriteSignal<Option<Map>>>,
57    #[prop(optional)] events: MapEvents,
58    #[prop(optional)] popup_events: PopupEvents,
59    #[prop(optional)] tooltip_events: TooltipEvents,
60    /// An optional node ref for the map `div` container element.
61    #[prop(optional)]
62    node_ref: Option<NodeRef<Div>>,
63    /// Wether the map should prefer canvas renderer.
64    #[prop(optional)]
65    prefer_canvas: bool,
66    /// Inner map child nodes
67    #[prop(optional)]
68    children: Option<Children>,
69) -> impl IntoView {
70    let map_ref = node_ref.unwrap_or_default();
71    let map_context = provide_leaflet_context();
72
73    let map_load = map_ref;
74    Effect::new(move |_| {
75        if let Some(map_div) = map_load.get() {
76            let html_node = map_div.unchecked_ref::<HtmlDivElement>();
77            // Randomize the id of the map
78            if html_node.id().is_empty() {
79                let id = format!("map-{}", (js_sys::Math::random() * 1000.0) as u32);
80                map_div.set_id(&id);
81            }
82            let events = events.clone();
83            let popup_events = popup_events.clone();
84            let tooltip_events = tooltip_events.clone();
85
86            let options = leaflet::MapOptions::new();
87            options.set_prefer_canvas(prefer_canvas);
88            options.set_zoom_control(zoom_control);
89            options.set_scroll_wheel_zoom(scroll_wheel_zoom);
90            options.set_zoom(zoom);
91            options.set_zoom_snap(zoom_snap);
92            options.set_zoom_delta(zoom_delta);
93            options.set_double_click_zoom(JsValue::from_bool(double_click_zoom));
94            options.set_min_zoom(min_zoom);
95            if let Some(center) = center {
96                options.set_center(center.as_lat_lng());
97            }
98            let Ok(leaflet_map) = Map::new(&html_node.id(), &options) else {
99                tracing::error!("Failed to create map");
100                return;
101            };
102
103            // Setup events
104            events.setup(&leaflet_map);
105            popup_events.setup(&leaflet_map);
106            tooltip_events.setup(&leaflet_map);
107
108            if locate {
109                let mut locate_options = LocateOptions::new();
110                locate_options.enable_high_accuracy(enable_high_accuracy);
111                locate_options.set_view(set_view);
112                locate_options.watch(watch);
113                leaflet_map.locate_with_options(&locate_options);
114            }
115
116            map_context.set_map(&leaflet_map);
117            if let Some(map) = map {
118                map.set(Some(leaflet_map));
119            }
120        };
121    });
122
123    on_cleanup(move || {
124        if let Some(map) = map_context.map_untracked().as_ref() {
125            map.remove();
126        };
127    });
128
129    view! { <div class=move || class.get() node_ref=map_ref style=move || style.get()>{children.map(|child|child())}</div>}
130}
131
132#[derive(Debug, Default, Clone)]
133pub struct LeafletMap {
134    #[cfg(not(feature = "ssr"))]
135    pub map: Option<leaflet::Map>,
136}
137
138impl LeafletMap {
139    pub fn new() -> Self {
140        Self {
141            #[cfg(not(feature = "ssr"))]
142            map: None,
143        }
144    }
145}