leptos_leaflet/components/
marker.rs1use 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#[component(transparent)]
14pub fn Marker(
15 #[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}