Skip to main content

dioxus_maplibre/components/
declarative.rs

1//! Declarative map object components built on top of `MapHandle`.
2
3use dioxus::prelude::*;
4
5use crate::handle::MapHandle;
6use crate::options::{
7    ControlPosition, GeoJsonSourceOptions, ImageSourceOptions, LayerOptions, MarkerOptions,
8    PopupOptions, RasterDemSourceOptions, RasterSourceOptions, VectorSourceOptions,
9};
10use crate::types::LatLng;
11
12use super::context::try_use_map_handle_signal;
13
14#[derive(Debug, Clone, PartialEq)]
15pub enum MapSourceKind {
16    GeoJson(GeoJsonSourceOptions),
17    Vector(VectorSourceOptions),
18    Raster(RasterSourceOptions),
19    RasterDem(RasterDemSourceOptions),
20    Image(ImageSourceOptions),
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum MapControlKind {
25    Navigation,
26    Geolocate,
27    Scale,
28    Fullscreen,
29    Attribution,
30}
31
32#[derive(Debug, Clone, PartialEq)]
33struct SourceState {
34    id: String,
35    source: MapSourceKind,
36}
37
38#[derive(Debug, Clone, PartialEq)]
39struct LayerState {
40    options: LayerOptions,
41    register_click_events: bool,
42    register_hover_events: bool,
43}
44
45#[derive(Debug, Clone, PartialEq)]
46struct MarkerState {
47    id: String,
48    position: LatLng,
49    options: MarkerOptions,
50}
51
52#[derive(Debug, Clone, PartialEq)]
53struct PopupState {
54    id: String,
55    position: LatLng,
56    html: String,
57    options: PopupOptions,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61struct ControlState {
62    kind: MapControlKind,
63    position: ControlPosition,
64}
65
66fn add_source(map: &MapHandle, source: &SourceState) {
67    match &source.source {
68        MapSourceKind::GeoJson(options) => map.add_geojson_source(&source.id, options.clone()),
69        MapSourceKind::Vector(options) => map.add_vector_source(&source.id, options.clone()),
70        MapSourceKind::Raster(options) => map.add_raster_source(&source.id, options.clone()),
71        MapSourceKind::RasterDem(options) => map.add_raster_dem_source(&source.id, options.clone()),
72        MapSourceKind::Image(options) => map.add_image_source(&source.id, options.clone()),
73    }
74}
75
76fn try_update_geojson_source(map: &MapHandle, previous: &SourceState, next: &SourceState) -> bool {
77    if previous.id != next.id {
78        return false;
79    }
80    let (MapSourceKind::GeoJson(previous_opts), MapSourceKind::GeoJson(next_opts)) =
81        (&previous.source, &next.source)
82    else {
83        return false;
84    };
85
86    if previous_opts == next_opts {
87        return true;
88    }
89
90    let unchanged_non_data_fields = previous_opts.cluster == next_opts.cluster
91        && previous_opts.cluster_radius == next_opts.cluster_radius
92        && previous_opts.cluster_max_zoom == next_opts.cluster_max_zoom
93        && previous_opts.cluster_properties == next_opts.cluster_properties
94        && previous_opts.generate_id == next_opts.generate_id
95        && previous_opts.promote_id == next_opts.promote_id;
96
97    if unchanged_non_data_fields {
98        map.update_geojson_source(&next.id, next_opts.data.clone());
99        return true;
100    }
101
102    false
103}
104
105fn remove_layer_bindings(map: &MapHandle, layer: &LayerState) {
106    if layer.register_click_events {
107        map.off_layer_click(&layer.options.id);
108    }
109    if layer.register_hover_events {
110        map.off_layer_hover(&layer.options.id);
111    }
112    map.remove_layer(&layer.options.id);
113}
114
115fn add_layer_bindings(map: &MapHandle, layer: &LayerState) {
116    map.add_layer(layer.options.clone());
117    if layer.register_click_events {
118        map.on_layer_click(&layer.options.id);
119    }
120    if layer.register_hover_events {
121        map.on_layer_hover(&layer.options.id);
122    }
123}
124
125fn remove_control(map: &MapHandle, control: ControlState) {
126    match control.kind {
127        MapControlKind::Navigation => map.remove_navigation_control(control.position),
128        MapControlKind::Geolocate => map.remove_geolocate_control(control.position),
129        MapControlKind::Scale => map.remove_scale_control(control.position),
130        MapControlKind::Fullscreen => map.remove_fullscreen_control(control.position),
131        MapControlKind::Attribution => map.remove_attribution_control(control.position),
132    }
133}
134
135fn add_control(map: &MapHandle, control: ControlState) {
136    match control.kind {
137        MapControlKind::Navigation => map.add_navigation_control(control.position),
138        MapControlKind::Geolocate => map.add_geolocate_control(control.position),
139        MapControlKind::Scale => map.add_scale_control(control.position),
140        MapControlKind::Fullscreen => map.add_fullscreen_control(control.position),
141        MapControlKind::Attribution => map.add_attribution_control(control.position),
142    }
143}
144
145/// Declaratively mount a source and remove it on unmount.
146#[derive(Props, Clone, PartialEq)]
147pub struct MapSourceProps {
148    pub id: String,
149    pub source: MapSourceKind,
150    #[props(default)]
151    pub children: Element,
152}
153
154#[component]
155pub fn MapSource(props: MapSourceProps) -> Element {
156    let handle_signal = try_use_map_handle_signal();
157    let mut applied_source = use_signal(|| None::<SourceState>);
158
159    let desired_source = SourceState {
160        id: props.id.clone(),
161        source: props.source.clone(),
162    };
163
164    use_effect(move || {
165        let Some(handle_signal) = handle_signal else {
166            return;
167        };
168        let Some(map) = handle_signal() else {
169            return;
170        };
171
172        let previous = applied_source.peek().clone();
173        if previous.as_ref() == Some(&desired_source) {
174            return;
175        }
176
177        if let Some(previous) = &previous {
178            if try_update_geojson_source(&map, previous, &desired_source) {
179                applied_source.set(Some(desired_source.clone()));
180                return;
181            }
182            map.remove_source(&previous.id);
183        }
184
185        add_source(&map, &desired_source);
186        applied_source.set(Some(desired_source.clone()));
187    });
188
189    use_drop(move || {
190        if let Some(handle_signal) = handle_signal
191            && let Some(map) = handle_signal.peek().clone()
192            && let Some(source) = applied_source.peek().as_ref()
193        {
194            map.remove_source(&source.id);
195        }
196    });
197
198    rsx! {{props.children}}
199}
200
201/// Declaratively mount a layer and remove it on unmount.
202#[derive(Props, Clone, PartialEq)]
203pub struct MapLayerProps {
204    pub options: LayerOptions,
205    #[props(default = false)]
206    pub register_click_events: bool,
207    #[props(default = false)]
208    pub register_hover_events: bool,
209}
210
211#[component]
212pub fn MapLayer(props: MapLayerProps) -> Element {
213    let handle_signal = try_use_map_handle_signal();
214    let mut applied_layer = use_signal(|| None::<LayerState>);
215
216    let desired_layer = LayerState {
217        options: props.options.clone(),
218        register_click_events: props.register_click_events,
219        register_hover_events: props.register_hover_events,
220    };
221
222    use_effect(move || {
223        let Some(handle_signal) = handle_signal else {
224            return;
225        };
226        let Some(map) = handle_signal() else {
227            return;
228        };
229
230        let previous = applied_layer.peek().clone();
231        if previous.as_ref() == Some(&desired_layer) {
232            return;
233        }
234
235        if let Some(previous) = &previous {
236            remove_layer_bindings(&map, previous);
237        }
238
239        add_layer_bindings(&map, &desired_layer);
240        applied_layer.set(Some(desired_layer.clone()));
241    });
242
243    use_drop(move || {
244        if let Some(handle_signal) = handle_signal
245            && let Some(map) = handle_signal.peek().clone()
246            && let Some(layer) = applied_layer.peek().as_ref()
247        {
248            remove_layer_bindings(&map, layer);
249        }
250    });
251
252    rsx! {}
253}
254
255/// Declaratively mount a marker and remove it on unmount.
256#[derive(Props, Clone, PartialEq)]
257pub struct MapMarkerProps {
258    pub id: String,
259    pub position: LatLng,
260    #[props(default)]
261    pub options: MarkerOptions,
262}
263
264#[component]
265pub fn MapMarker(props: MapMarkerProps) -> Element {
266    let handle_signal = try_use_map_handle_signal();
267    let mut applied_marker = use_signal(|| None::<MarkerState>);
268
269    let desired_marker = MarkerState {
270        id: props.id,
271        position: props.position,
272        options: props.options,
273    };
274
275    use_effect(move || {
276        let Some(handle_signal) = handle_signal else {
277            return;
278        };
279        let Some(map) = handle_signal() else {
280            return;
281        };
282
283        let previous = applied_marker.peek().clone();
284        if previous.as_ref() == Some(&desired_marker) {
285            return;
286        }
287
288        if let Some(previous) = &previous {
289            if previous.id == desired_marker.id && previous.options == desired_marker.options {
290                map.update_marker_position(&desired_marker.id, desired_marker.position);
291                applied_marker.set(Some(desired_marker.clone()));
292                return;
293            }
294            map.remove_marker(&previous.id);
295        }
296
297        map.add_marker(
298            &desired_marker.id,
299            desired_marker.position,
300            desired_marker.options.clone(),
301        );
302        applied_marker.set(Some(desired_marker.clone()));
303    });
304
305    use_drop(move || {
306        if let Some(handle_signal) = handle_signal
307            && let Some(map) = handle_signal.peek().clone()
308            && let Some(marker) = applied_marker.peek().as_ref()
309        {
310            map.remove_marker(&marker.id);
311        }
312    });
313
314    rsx! {}
315}
316
317/// Declaratively mount a popup and remove it on unmount.
318#[derive(Props, Clone, PartialEq)]
319pub struct MapPopupProps {
320    pub id: String,
321    pub position: LatLng,
322    pub html: String,
323    #[props(default)]
324    pub options: PopupOptions,
325}
326
327#[component]
328pub fn MapPopup(props: MapPopupProps) -> Element {
329    let handle_signal = try_use_map_handle_signal();
330    let mut applied_popup = use_signal(|| None::<PopupState>);
331
332    let desired_popup = PopupState {
333        id: props.id,
334        position: props.position,
335        html: props.html,
336        options: props.options,
337    };
338
339    use_effect(move || {
340        let Some(handle_signal) = handle_signal else {
341            return;
342        };
343        let Some(map) = handle_signal() else {
344            return;
345        };
346
347        let previous = applied_popup.peek().clone();
348        if previous.as_ref() == Some(&desired_popup) {
349            return;
350        }
351
352        if let Some(previous) = &previous {
353            map.remove_popup(&previous.id);
354        }
355
356        map.add_popup(
357            &desired_popup.id,
358            desired_popup.position,
359            &desired_popup.html,
360            desired_popup.options.clone(),
361        );
362        applied_popup.set(Some(desired_popup.clone()));
363    });
364
365    use_drop(move || {
366        if let Some(handle_signal) = handle_signal
367            && let Some(map) = handle_signal.peek().clone()
368            && let Some(popup) = applied_popup.peek().as_ref()
369        {
370            map.remove_popup(&popup.id);
371        }
372    });
373
374    rsx! {}
375}
376
377/// Declaratively add a control to the map.
378#[derive(Props, Clone, PartialEq, Eq)]
379pub struct MapControlProps {
380    pub kind: MapControlKind,
381    #[props(default)]
382    pub position: ControlPosition,
383}
384
385#[component]
386pub fn MapControl(props: MapControlProps) -> Element {
387    let handle_signal = try_use_map_handle_signal();
388    let mut applied_control = use_signal(|| None::<ControlState>);
389
390    let desired_control = ControlState {
391        kind: props.kind,
392        position: props.position,
393    };
394
395    use_effect(move || {
396        let Some(handle_signal) = handle_signal else {
397            return;
398        };
399        let Some(map) = handle_signal() else {
400            return;
401        };
402
403        let previous = *applied_control.peek();
404        if previous == Some(desired_control) {
405            return;
406        }
407
408        if let Some(previous) = previous {
409            remove_control(&map, previous);
410        }
411
412        add_control(&map, desired_control);
413        applied_control.set(Some(desired_control));
414    });
415
416    use_drop(move || {
417        if let Some(handle_signal) = handle_signal
418            && let Some(map) = handle_signal.peek().clone()
419            && let Some(control) = applied_control.peek().as_ref().copied()
420        {
421            remove_control(&map, control);
422        }
423    });
424
425    rsx! {}
426}