leptos_leaflet/components/
marker.rs

1use crate::components::context::extend_context_with_overlay;
2use crate::components::position::Position;
3use leptos::prelude::*;
4use wasm_bindgen::JsCast;
5
6use super::{
7    DragEvents, LayerEvents, LeafletMapContext, MouseEvents, MoveEvents, PopupEvents, TooltipEvents,
8};
9use crate::core::{JsSignal, JsStoredValue};
10use crate::{setup_layer_leaflet_option, setup_layer_leaflet_string};
11
12/// A marker component.
13#[component(transparent)]
14pub fn Marker(
15    /// Position for the Marker
16    #[prop(into)]
17    position: JsSignal<Position>,
18    #[prop(into, optional)] draggable: Signal<bool>,
19    #[prop(into, optional)] keyboard: Signal<Option<bool>>,
20    #[prop(into, optional)] title: Signal<String>,
21    #[prop(into, optional)] alt: Signal<String>,
22    #[prop(into, optional)] interactive: Signal<Option<bool>>,
23    #[prop(into, optional)] z_index_offset: Signal<Option<f64>>,
24    #[prop(into, optional)] opacity: Signal<Option<f64>>,
25    #[prop(into, optional)] rise_on_hover: Signal<Option<bool>>,
26    #[prop(into, optional)] rise_offset: Signal<Option<f64>>,
27    #[prop(into, optional)] pane: Signal<String>,
28    #[prop(into, optional)] shadow_pane: Signal<String>,
29    #[prop(into, optional)] bubbling_mouse_events: Signal<Option<bool>>,
30    #[prop(into, optional)] auto_pan: Signal<Option<bool>>,
31    #[prop(into, optional)] auto_pan_padding: Signal<Option<(f64, f64)>>,
32    #[prop(into, optional)] auto_pan_speed: Signal<Option<f64>>,
33    #[prop(into, optional)] icon_class: Signal<Option<String>>,
34    #[prop(into, optional)] icon_url: Signal<Option<String>>,
35    #[prop(into, optional)] icon_size: Signal<Option<(f64, f64)>>,
36    #[prop(into, optional)] icon_anchor: Signal<Option<(f64, f64)>>,
37    #[prop(into, optional)] attribution: Signal<String>,
38    #[prop(into, optional)] rotation: Signal<Option<f64>>,
39    #[prop(into, optional)] move_events: MoveEvents,
40    #[prop(into, optional)] mouse_events: MouseEvents,
41    #[prop(into, optional)] drag_events: DragEvents,
42    #[prop(into, optional)] layer_events: LayerEvents,
43    #[prop(into, optional)] popup_events: PopupEvents,
44    #[prop(into, optional)] tooltip_events: TooltipEvents,
45    #[prop(optional)] children: Option<Children>,
46) -> impl IntoView {
47    let position_tracking = position;
48    let icon_class_tracking = icon_class;
49    let icon_url_tracking = icon_url;
50    let icon_size_tracking = icon_size;
51    let icon_anchor_tracking = icon_anchor;
52    let map_context = use_context::<LeafletMapContext>().expect("Map context not found");
53
54    let overlay_context = extend_context_with_overlay();
55    let overlay = JsStoredValue::new_local(None::<leaflet::Marker>);
56
57    Effect::new(move |_| {
58        if let Some(map) = map_context.map() {
59            let options = leaflet::MarkerOptions::new();
60            let drag = draggable.get_untracked();
61            if drag {
62                options.set_draggable(drag);
63            }
64            setup_layer_leaflet_option!(keyboard, options);
65            setup_layer_leaflet_string!(title, options);
66            setup_layer_leaflet_string!(alt, options);
67            setup_layer_leaflet_option!(interactive, options);
68            setup_layer_leaflet_option!(z_index_offset, options);
69            setup_layer_leaflet_option!(opacity, options);
70            setup_layer_leaflet_option!(rise_on_hover, options);
71            setup_layer_leaflet_option!(rise_offset, options);
72            setup_layer_leaflet_string!(pane, options);
73            setup_layer_leaflet_string!(shadow_pane, options);
74            setup_layer_leaflet_option!(bubbling_mouse_events, options);
75            setup_layer_leaflet_option!(auto_pan, options);
76            setup_layer_leaflet_option!(auto_pan_speed, options);
77            setup_layer_leaflet_string!(attribution, options);
78
79            if let Some((x, y)) = auto_pan_padding.get_untracked() {
80                options.set_auto_pan_padding(leaflet::Point::new(x, y));
81            }
82            if let Some(icon_url) = icon_url.get_untracked() {
83                let icon_options = leaflet::IconOptions::new();
84                icon_options.set_icon_url(icon_url);
85                if let Some((x, y)) = icon_size.get_untracked() {
86                    icon_options.set_icon_size(leaflet::Point::new(x, y));
87                }
88                if let Some((x, y)) = icon_anchor.get_untracked() {
89                    icon_options.set_icon_anchor(leaflet::Point::new(x, y));
90                }
91                let icon = leaflet::Icon::new(&icon_options);
92                options.set_icon(icon);
93            } else if let Some(icon_class) = icon_class.get_untracked() {
94                let icon_options = leaflet::DivIconOptions::new();
95                icon_options.set_class_name(icon_class);
96                if let Some((x, y)) = icon_size.get_untracked() {
97                    icon_options.set_icon_size(leaflet::Point::new(x, y));
98                }
99                if let Some((x, y)) = icon_anchor.get_untracked() {
100                    icon_options.set_icon_anchor(leaflet::Point::new(x, y));
101                }
102                let icon = leaflet::DivIcon::new(&icon_options);
103                options.set_icon(icon.into());
104            }
105
106            let marker =
107                leaflet::Marker::new_with_options(&position.get_untracked().as_lat_lng(), &options);
108
109            mouse_events.setup(&marker);
110            move_events.setup(&marker);
111            drag_events.setup(&marker);
112            popup_events.setup(&marker);
113            tooltip_events.setup(&marker);
114            layer_events.setup(&marker);
115
116            marker.add_to(&map);
117            overlay_context.set_container(&marker);
118            overlay.set_value(Some(marker));
119        };
120    });
121
122    let position_stop = Effect::watch(
123        move || position_tracking.get(),
124        move |position_tracking, _, _| {
125            if let Some(marker) = overlay.get_value().as_ref() {
126                marker.set_lat_lng(&position_tracking.as_lat_lng());
127            }
128        },
129        false,
130    );
131
132    let icon_stop = Effect::watch(
133        move || {
134            (
135                icon_url_tracking.get(),
136                icon_class_tracking.get(),
137                icon_size_tracking.get(),
138                icon_anchor_tracking.get(),
139            )
140        },
141        move |(maybe_icon_url, maybe_icon_class, maybe_icon_size, maybe_icon_anchor), _, _| {
142            if let Some(marker) = overlay.get_value() {
143                if let Some(icon_url) = maybe_icon_url {
144                    let icon_options = leaflet::IconOptions::new();
145                    icon_options.set_icon_url(icon_url.clone());
146                    if let Some((x, y)) = maybe_icon_size {
147                        icon_options.set_icon_size(leaflet::Point::new(*x, *y));
148                    }
149                    if let Some((x, y)) = maybe_icon_anchor {
150                        icon_options.set_icon_anchor(leaflet::Point::new(*x, *y));
151                    }
152                    let icon = leaflet::Icon::new(&icon_options);
153                    marker.set_icon(&icon);
154                } else if let Some(icon_class) = maybe_icon_class {
155                    let icon_options = leaflet::DivIconOptions::new();
156                    icon_options.set_class_name(icon_class.clone());
157                    if let Some((x, y)) = maybe_icon_size {
158                        icon_options.set_icon_size(leaflet::Point::new(*x, *y));
159                    }
160                    if let Some((x, y)) = maybe_icon_anchor {
161                        icon_options.set_icon_anchor(leaflet::Point::new(*x, *y));
162                    }
163                    let icon = leaflet::DivIcon::new(&icon_options);
164                    marker.set_icon(&icon.into());
165                }
166            }
167        },
168        false,
169    );
170
171    let opacity_stop = Effect::watch(
172        move || opacity.get(),
173        move |opacity, _, _| {
174            overlay.get_value().as_ref().and_then(|marker| {
175                opacity.map(|opacity| {
176                    marker.set_opacity(opacity);
177                })
178            });
179        },
180        false,
181    );
182
183    let drag_stop = Effect::watch(
184        move || draggable.get(),
185        move |&draggable, _, _| {
186            if let Some(marker) = overlay.get_value().as_ref() {
187                match draggable {
188                    true => marker.dragging().enable(),
189                    false => marker.dragging().disable(),
190                };
191            }
192        },
193        false,
194    );
195
196    let rotation_stop = Effect::watch(
197        move || rotation.get(),
198        move |&rotation, prev_rotation, _| {
199            if let (Some(marker), Some(rotation)) = (overlay.get_value().as_ref(), rotation) {
200                if Some(rotation.trunc()) == prev_rotation.copied().flatten().map(|r| r.trunc()) {
201                    return;
202                }
203                if let Ok(internal_icon) = js_sys::Reflect::get(marker, &"_icon".into()) {
204                    let internal_icon = internal_icon.unchecked_ref::<web_sys::HtmlElement>();
205                    _ = internal_icon
206                        .style()
207                        .set_property("--gps_rotation", &format!("{}deg", rotation));
208
209                    let transform = internal_icon
210                        .style()
211                        .get_property_value("transform")
212                        .unwrap_or_default();
213
214                    let transform = match transform.contains("rotate(") {
215                        true => transform
216                            .split_whitespace()
217                            .map(|part| match part.starts_with("rotate(") {
218                                true => format!("rotate({}deg)", rotation),
219                                false => part.to_string(),
220                            })
221                            .collect::<Vec<String>>()
222                            .join(" "),
223                        false => format!("{} rotate({}deg)", transform, rotation),
224                    };
225
226                    let _ = internal_icon.style().set_property("transform", &transform);
227                    let _ = internal_icon
228                        .style()
229                        .set_property("transform-origin", "center");
230                }
231            }
232        },
233        false,
234    );
235
236    on_cleanup(move || {
237        position_stop.stop();
238        icon_stop.stop();
239        opacity_stop.stop();
240        drag_stop.stop();
241        rotation_stop.stop();
242        if let Some(overlay) = overlay.get_value() {
243            overlay.remove();
244        }
245    });
246
247    children.map(|child| child())
248}