Skip to main content

yew_hooks/hooks/
use_geolocation.rs

1use gloo::utils::window;
2use wasm_bindgen::{prelude::*, JsCast};
3use yew::prelude::*;
4
5use super::use_effect_once;
6
7pub use web_sys::PositionOptions as UseGeolocationOptions;
8
9#[cfg(web_sys_unstable_apis)]
10type PositionErrorType = web_sys::GeolocationPositionError;
11#[cfg(not(web_sys_unstable_apis))]
12type PositionErrorType = web_sys::PositionError;
13
14#[derive(PartialEq, Default, Clone)]
15pub struct UseGeolocationState {
16    pub loading: bool,
17    pub accuracy: f64,
18    pub altitude: Option<f64>,
19    pub altitude_accuracy: Option<f64>,
20    pub heading: Option<f64>,
21    pub latitude: f64,
22    pub longitude: f64,
23    pub speed: Option<f64>,
24    pub timestamp: f64,
25    pub error: Option<PositionErrorType>,
26}
27
28/// A sensor hook that tracks user's geographic location.
29///
30/// # Example
31///
32/// ```rust
33/// # use yew::prelude::*;
34/// #
35/// use yew_hooks::prelude::*;
36///
37/// #[function_component(UseGeolocation)]
38/// fn geolocation() -> Html {
39///     let state = use_geolocation();
40///
41///     html! {
42///         <>
43///             <b>{ " loading: " }</b>
44///             { state.loading }
45///             <b>{ " latitude: " }</b>
46///             { state.latitude }
47///             <b>{ " longitude: " }</b>
48///             { state.longitude }
49///             <b>{ " altitude: " }</b>
50///             { state.altitude.unwrap_or_default() }
51///             <b>{ " altitude_accuracy: " }</b>
52///             { state.altitude_accuracy.unwrap_or_default() }
53///             <b>{ " heading: " }</b>
54///             { state.heading.unwrap_or_default() }
55///             <b>{ " speed: " }</b>
56///             { state.speed.unwrap_or_default() }
57///             <b>{ " timestamp: " }</b>
58///             { state.timestamp }
59///         </>
60///     }
61/// }
62/// ```
63#[hook]
64pub fn use_geolocation() -> UseGeolocationState {
65    use_geolocation_with_options(UseGeolocationOptions::default())
66}
67
68/// A sensor hook that tracks user's geographic location.
69/// See [`use_geolocation`]
70#[hook]
71pub fn use_geolocation_with_options(options: UseGeolocationOptions) -> UseGeolocationState {
72    let state = use_state(|| UseGeolocationState {
73        loading: true,
74        ..Default::default()
75    });
76
77    {
78        let state = state.clone();
79        use_effect_once(move || {
80            #[cfg(web_sys_unstable_apis)]
81            let (closure, error_closure, watch_id) = {
82                use web_sys::GeolocationPosition;
83
84                let closure = {
85                    let state = state.clone();
86                    Closure::wrap(Box::new(move |position: GeolocationPosition| {
87                        state.set(UseGeolocationState {
88                            loading: false,
89                            accuracy: position.coords().accuracy(),
90                            altitude: position.coords().altitude(),
91                            altitude_accuracy: position.coords().altitude_accuracy(),
92                            heading: position.coords().heading(),
93                            latitude: position.coords().latitude(),
94                            longitude: position.coords().longitude(),
95                            speed: position.coords().speed(),
96                            timestamp: position.timestamp(),
97                            error: None,
98                        });
99                    }) as Box<dyn Fn(GeolocationPosition)>)
100                };
101
102                let error_closure = {
103                    let state = state.clone();
104                    Closure::wrap(Box::new(move |error: PositionErrorType| {
105                        state.set(UseGeolocationState {
106                            loading: false,
107                            error: Some(error),
108                            ..*state
109                        });
110                    }) as Box<dyn Fn(PositionErrorType)>)
111                };
112
113                window()
114                    .navigator()
115                    .geolocation()
116                    .unwrap_throw()
117                    .get_current_position_with_error_callback_and_options(
118                        closure.as_ref().unchecked_ref(),
119                        Some(error_closure.as_ref().unchecked_ref()),
120                        &options,
121                    );
122
123                let watch_id = window()
124                    .navigator()
125                    .geolocation()
126                    .unwrap_throw()
127                    .watch_position_with_error_callback_and_options(
128                        closure.as_ref().unchecked_ref(),
129                        Some(error_closure.as_ref().unchecked_ref()),
130                        &options,
131                    );
132
133                (closure, error_closure, watch_id)
134            };
135
136            #[cfg(not(web_sys_unstable_apis))]
137            let (closure, error_closure, watch_id) = {
138                use web_sys::Position;
139
140                let closure = {
141                    let state = state.clone();
142                    Closure::wrap(Box::new(move |position: Position| {
143                        state.set(UseGeolocationState {
144                            loading: false,
145                            accuracy: position.coords().accuracy(),
146                            altitude: position.coords().altitude(),
147                            altitude_accuracy: position.coords().altitude_accuracy(),
148                            heading: position.coords().heading(),
149                            latitude: position.coords().latitude(),
150                            longitude: position.coords().longitude(),
151                            speed: position.coords().speed(),
152                            timestamp: position.timestamp(),
153                            error: None,
154                        });
155                    }) as Box<dyn Fn(Position)>)
156                };
157
158                let error_closure = {
159                    let state = state.clone();
160                    Closure::wrap(Box::new(move |error: PositionErrorType| {
161                        state.set(UseGeolocationState {
162                            loading: false,
163                            error: Some(error),
164                            ..*state
165                        });
166                    }) as Box<dyn Fn(PositionErrorType)>)
167                };
168
169                window()
170                    .navigator()
171                    .geolocation()
172                    .unwrap_throw()
173                    .get_current_position_with_error_callback_and_options(
174                        closure.as_ref().unchecked_ref(),
175                        Some(error_closure.as_ref().unchecked_ref()),
176                        &options,
177                    )
178                    .unwrap_throw();
179
180                let watch_id = window()
181                    .navigator()
182                    .geolocation()
183                    .unwrap_throw()
184                    .watch_position_with_error_callback_and_options(
185                        closure.as_ref().unchecked_ref(),
186                        Some(error_closure.as_ref().unchecked_ref()),
187                        &options,
188                    )
189                    .unwrap_throw();
190
191                (closure, error_closure, watch_id)
192            };
193
194            // Forget the closures to keep them alive
195            closure.forget();
196            error_closure.forget();
197
198            move || {
199                window()
200                    .navigator()
201                    .geolocation()
202                    .unwrap_throw()
203                    .clear_watch(watch_id);
204            }
205        });
206    }
207
208    (*state).clone()
209}