use crate::error::{Error, Result};
use crate::map::MapPane;
use crate::util::lookup_id;
use squarepeg::{Peg, WebMercatorPos, Wgs84Pos};
use std::cell::RefCell;
use wasm_bindgen::JsCast;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::UnwrapThrowExt;
use web_sys::{Element, Event, PointerEvent, WheelEvent};
struct MapState {
map_pane: MapPane,
pointerdown: Closure<dyn Fn(PointerEvent)>,
pointerup: Closure<dyn Fn(PointerEvent)>,
pointermove: Closure<dyn Fn(PointerEvent)>,
wheel: Closure<dyn Fn(WheelEvent)>,
click: Closure<dyn Fn(Event)>,
click_cb: Box<dyn Fn(Event)>,
zoom_cb: Box<dyn Fn(u32)>,
suppress_click: bool,
origin: (i32, i32),
pan_point: Option<(i32, i32)>,
point: (i32, i32),
nw: Option<Peg>,
}
thread_local! {
static MAP_STATE: RefCell<Option<MapState>> = const { RefCell::new(None) };
}
impl MapState {
fn new(
map_pane: MapPane,
click_cb: impl Fn(Event) + 'static,
zoom_cb: impl Fn(u32) + 'static,
) -> Self {
MapState {
map_pane,
pointerdown: Closure::new(handle_pointerdown),
pointerup: Closure::new(handle_pointerup),
pointermove: Closure::new(handle_pointermove),
wheel: Closure::new(handle_wheel),
click: Closure::new(handle_click),
click_cb: Box::new(click_cb),
zoom_cb: Box::new(zoom_cb),
suppress_click: false,
origin: (0, 0),
pan_point: None,
point: (0, 0),
nw: None,
}
}
fn set_point(&mut self, point: (i32, i32)) {
self.point = point;
}
fn start_panning(&mut self) {
if self.pan_point.is_none() {
self.pan_point = Some(self.point);
}
}
fn stop_panning(&mut self) {
if let Some(pan) = self.pan_point {
self.origin = (
self.origin.0 + self.point.0 - pan.0,
self.origin.1 + self.point.1 - pan.1,
);
self.pan_point = None;
self.set_style("");
}
}
fn translated_point(&self) -> (i32, i32) {
match self.pan_point {
Some(pan) => (
self.origin.0 + self.point.0 - pan.0,
self.origin.1 + self.point.1 - pan.1,
),
None => self.origin,
}
}
fn reset(&mut self, origin: (i32, i32)) {
self.map_pane.next_cycle();
self.origin = origin;
self.pan_point = None;
self.point = (0, 0);
self.set_style("");
}
fn drag_map(&mut self, point: (i32, i32)) {
if self.pan_point.is_some() {
self.set_point(point);
self.set_style("grabbing");
self.map_pane.fetch_edge_tiles();
}
}
fn set_style(&self, cursor: &str) {
let (x, y) = self.translated_point();
let mut css = String::with_capacity(80);
css.push_str("transform: translate(");
css.push_str(&x.to_string());
css.push_str("px, ");
css.push_str(&y.to_string());
css.push_str("px);");
if !cursor.is_empty() {
css.push_str(" cursor: ");
css.push_str(cursor);
css.push(';');
}
self.map_pane.set_style(&css);
}
fn change_zoom(&self, zoom: i32, rx: i32, ry: i32) {
if let Some(nw) = self.nw {
let zoom = zoom as u32;
let point = (rx - self.origin.0, ry - self.origin.1);
let px = nw.x().saturating_add((point.0 / 255) as u32);
let py = nw.y().saturating_add((point.1 / 256) as u32);
if let Some(peg) = Peg::new(nw.z(), px, py) {
let x = (point.0 % 256) as f64 / 256.0;
let y = (point.1 % 256) as f64 / 256.0;
let map_pane = self.map_pane.clone();
let bbox = map_pane.grid().peg_bbox(peg);
let width = bbox.x_span();
let height = bbox.y_span();
let mx = bbox.x_min() + x * width;
let my = bbox.y_max() - y * height;
let pos = WebMercatorPos::new(mx, my);
let loc = Wgs84Pos::from(pos);
let (rx, ry) = map_pane.client_pos(rx, ry);
map_pane.position(zoom, loc.lon_deg(), loc.lat_deg(), rx, ry);
(self.zoom_cb)(zoom);
}
}
}
}
fn handle_pointerdown(pe: PointerEvent) {
if pe.button() == 0 {
let point = (pe.client_x(), pe.client_y());
set_pan_point(true, point);
if let Some(target) = pe.target()
&& let Ok(elem) = target.dyn_into::<Element>()
&& let Err(e) = elem.set_pointer_capture(0)
{
log::warn!("set_pointer_capture: {e:?}");
}
}
}
fn handle_pointerup(pe: PointerEvent) {
if pe.button() == 0 {
let point = (pe.client_x(), pe.client_y());
set_pan_point(false, point);
}
}
fn handle_pointermove(pe: PointerEvent) {
MAP_STATE.with(|rc| {
if let Some(ref mut state) = *rc.borrow_mut() {
let point = (pe.client_x(), pe.client_y());
state.drag_map(point);
state.suppress_click = true;
}
});
}
fn handle_wheel(we: WheelEvent) {
let delta_zoom: i32 = if we.delta_y() < 0.0 {
1
} else if we.delta_y() > 0.0 {
-1
} else {
0
};
if delta_zoom != 0 {
MAP_STATE.with(|rc| {
if let Some(ref mut state) = *rc.borrow_mut() {
let zoom = state
.nw
.map(|peg| peg.z() as i32 + delta_zoom)
.unwrap_or(0);
if zoom > 0 {
state.change_zoom(zoom, we.client_x(), we.client_y());
}
}
});
}
}
fn handle_click(e: Event) {
MAP_STATE.with(|rc| {
if let Some(ref state) = *rc.borrow()
&& !state.suppress_click
{
(state.click_cb)(e);
}
});
}
pub fn init(
id: &str,
groups: &'static [&'static str],
click_cb: impl Fn(Event) + 'static,
zoom_cb: impl Fn(u32) + 'static,
) -> Result<MapPane> {
let mp = lookup_id(id)?;
let map_pane = MapPane::new(id, groups);
MAP_STATE.with(|rc| {
let mut state = rc.borrow_mut();
if state.is_some() {
return Err(Error::Other("init: state exists!"));
}
let ms = MapState::new(map_pane.clone(), click_cb, zoom_cb);
mp.add_event_listener_with_callback(
"pointerdown",
ms.pointerdown.as_ref().unchecked_ref(),
)
.unwrap_throw();
mp.add_event_listener_with_callback(
"pointerup",
ms.pointerup.as_ref().unchecked_ref(),
)
.unwrap_throw();
mp.add_event_listener_with_callback(
"pointercancel",
ms.pointerup.as_ref().unchecked_ref(),
)
.unwrap_throw();
mp.add_event_listener_with_callback(
"pointermove",
ms.pointermove.as_ref().unchecked_ref(),
)
.unwrap_throw();
mp.add_event_listener_with_callback(
"wheel",
ms.wheel.as_ref().unchecked_ref(),
)
.unwrap_throw();
mp.add_event_listener_with_callback(
"click",
ms.click.as_ref().unchecked_ref(),
)
.unwrap_throw();
*state = Some(ms);
Ok(map_pane)
})
}
fn set_pan_point(start: bool, point: (i32, i32)) {
MAP_STATE.with(|rc| {
if let Some(ref mut state) = *rc.borrow_mut() {
if start {
state.suppress_click = false;
state.set_point(point);
state.start_panning();
} else {
state.stop_panning();
}
}
});
}
pub fn reset(origin: (i32, i32), nw: Peg) {
MAP_STATE.with(|rc| {
if let Some(ref mut state) = *rc.borrow_mut() {
state.reset(origin);
state.nw = Some(nw);
}
})
}
pub fn map_pane() -> Option<MapPane> {
MAP_STATE
.with(|rc| (*rc.borrow()).as_ref().map(|state| state.map_pane.clone()))
}
pub fn zoom() -> u32 {
MAP_STATE.with(|rc| {
if let Some(ref state) = *rc.borrow() {
state.nw.map(|peg| peg.z()).unwrap_or(0)
} else {
0
}
})
}