waterkit-location 0.1.4

Cross-platform location and GPS services
Documentation
use crate::{Location, LocationError};
use js_sys::{Date, Reflect};
use wasm_bindgen::{JsCast, JsValue, closure::Closure};

pub async fn get_location() -> Result<Location, LocationError> {
    let geolocation = web_sys::window()
        .ok_or_else(|| LocationError::Platform(String::from("browser window is unavailable")))?
        .navigator()
        .geolocation()
        .map_err(platform_error)?;
    let (sender, receiver) = async_channel::bounded(1);
    let success_sender = sender.clone();
    let success = Closure::<dyn FnMut(JsValue)>::once(move |position| {
        let _ = success_sender.try_send(location_from_position(&position));
    });
    let failure = Closure::<dyn FnMut(JsValue)>::once(move |error| {
        let _ = sender.try_send(Err(geolocation_error(&error)));
    });
    geolocation
        .get_current_position_with_error_callback(
            success.as_ref().unchecked_ref(),
            Some(failure.as_ref().unchecked_ref()),
        )
        .map_err(platform_error)?;
    receiver
        .recv()
        .await
        .map_err(|_| LocationError::Platform(String::from("geolocation callback closed")))?
}

fn location_from_position(position: &JsValue) -> Result<Location, LocationError> {
    let coords = Reflect::get(position, &JsValue::from_str("coords")).map_err(platform_error)?;
    let latitude = number_property(&coords, "latitude")?;
    let longitude = number_property(&coords, "longitude")?;
    let timestamp_millis = Reflect::get(position, &JsValue::from_str("timestamp"))
        .map_err(platform_error)?
        .as_f64()
        .unwrap_or_else(Date::now) as i64;
    let timestamp = jiff::Timestamp::from_millisecond(timestamp_millis)
        .map_err(|error| LocationError::Platform(error.to_string()))?;
    let mut location = Location::from_degrees(latitude, longitude, timestamp)?;
    if let Some(altitude) = optional_number_property(&coords, "altitude")? {
        location = location.with_altitude(altitude);
    }
    if let Some(accuracy) = optional_number_property(&coords, "accuracy")? {
        location = location.with_horizontal_accuracy(accuracy);
    }
    if let Some(accuracy) = optional_number_property(&coords, "altitudeAccuracy")? {
        location = location.with_vertical_accuracy(accuracy);
    }
    Ok(location)
}

fn number_property(value: &JsValue, name: &str) -> Result<f64, LocationError> {
    Reflect::get(value, &JsValue::from_str(name))
        .map_err(platform_error)?
        .as_f64()
        .ok_or_else(|| LocationError::Platform(format!("geolocation {name} is not a number")))
}

fn optional_number_property(value: &JsValue, name: &str) -> Result<Option<f64>, LocationError> {
    let value = Reflect::get(value, &JsValue::from_str(name)).map_err(platform_error)?;
    if value.is_null() || value.is_undefined() {
        Ok(None)
    } else {
        value
            .as_f64()
            .map(Some)
            .ok_or_else(|| LocationError::Platform(format!("geolocation {name} is not a number")))
    }
}

fn geolocation_error(error: &JsValue) -> LocationError {
    match Reflect::get(error, &JsValue::from_str("code"))
        .ok()
        .and_then(|value| value.as_f64())
        .map(|code| code as u16)
    {
        Some(1) => LocationError::PermissionDenied,
        Some(2) => LocationError::NotAvailable,
        Some(3) => LocationError::Timeout,
        _ => platform_error(error.clone()),
    }
}

fn platform_error(error: JsValue) -> LocationError {
    LocationError::Platform(
        error
            .as_string()
            .unwrap_or_else(|| format!("browser geolocation error: {error:?}")),
    )
}