1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use gloo::utils::window;
use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{Position, PositionError};
use yew::prelude::*;

use super::use_effect_once;

pub use web_sys::PositionOptions as UseGeolocationOptions;

#[derive(PartialEq, Default, Clone)]
pub struct UseGeolocationState {
    pub loading: bool,
    pub accuracy: f64,
    pub altitude: Option<f64>,
    pub altitude_accuracy: Option<f64>,
    pub heading: Option<f64>,
    pub latitude: f64,
    pub longitude: f64,
    pub speed: Option<f64>,
    pub timestamp: f64,
    pub error: Option<PositionError>,
}

/// A sensor hook that tracks user's geographic location.
///
/// # Example
///
/// ```rust
/// # use yew::prelude::*;
/// #
/// use yew_hooks::prelude::*;
///
/// #[function_component(UseGeolocation)]
/// fn geolocation() -> Html {
///     let state = use_geolocation();
///     
///     html! {
///         <>
///             <b>{ " loading: " }</b>
///             { state.loading }
///             <b>{ " latitude: " }</b>
///             { state.latitude }
///             <b>{ " longitude: " }</b>
///             { state.longitude }
///             <b>{ " altitude: " }</b>
///             { state.altitude.unwrap_or_default() }
///             <b>{ " altitude_accuracy: " }</b>
///             { state.altitude_accuracy.unwrap_or_default() }
///             <b>{ " heading: " }</b>
///             { state.heading.unwrap_or_default() }
///             <b>{ " speed: " }</b>
///             { state.speed.unwrap_or_default() }
///             <b>{ " timestamp: " }</b>
///             { state.timestamp }
///         </>
///     }
/// }
/// ```
#[hook]
pub fn use_geolocation() -> UseGeolocationState {
    use_geolocation_with_options(UseGeolocationOptions::default())
}

/// A sensor hook that tracks user's geographic location.
/// See [`use_geolocation`]
#[hook]
pub fn use_geolocation_with_options(options: UseGeolocationOptions) -> UseGeolocationState {
    let state = use_state(|| UseGeolocationState {
        loading: true,
        ..Default::default()
    });

    {
        let state = state.clone();
        use_effect_once(move || {
            let closure = {
                let state = state.clone();
                Closure::wrap(Box::new(move |position: Position| {
                    state.set(UseGeolocationState {
                        loading: false,
                        accuracy: position.coords().accuracy(),
                        altitude: position.coords().altitude(),
                        altitude_accuracy: position.coords().altitude_accuracy(),
                        heading: position.coords().heading(),
                        latitude: position.coords().latitude(),
                        longitude: position.coords().longitude(),
                        speed: position.coords().speed(),
                        timestamp: position.timestamp(),
                        error: None,
                    });
                }) as Box<dyn Fn(Position)>)
            };

            let error_closure = {
                let state = state.clone();
                Closure::wrap(Box::new(move |error: PositionError| {
                    state.set(UseGeolocationState {
                        loading: false,
                        error: Some(error),
                        ..*state
                    });
                }) as Box<dyn Fn(PositionError)>)
            };

            window()
                .navigator()
                .geolocation()
                .unwrap_throw()
                .get_current_position_with_error_callback_and_options(
                    closure.as_ref().unchecked_ref(),
                    Some(error_closure.as_ref().unchecked_ref()),
                    &options,
                )
                .unwrap_throw();

            let watch_id = window()
                .navigator()
                .geolocation()
                .unwrap_throw()
                .watch_position_with_error_callback_and_options(
                    closure.as_ref().unchecked_ref(),
                    Some(error_closure.as_ref().unchecked_ref()),
                    &options,
                )
                .unwrap_throw();

            // Forget the closure to keep it alive
            closure.forget();
            error_closure.forget();

            move || {
                window()
                    .navigator()
                    .geolocation()
                    .unwrap_throw()
                    .clear_watch(watch_id);
            }
        });
    }

    (*state).clone()
}