pub mod tiles; pub mod osm; mod render; use std::io::Cursor; use std::num::{NonZeroU16, NonZeroU64}; use std::sync::{Arc, Mutex}; use egui::{Event, MouseWheelUnit, Rect, Sense, Ui}; use image::{DynamicImage, ImageReader}; use poll_promise::Promise; use tracing::{debug, warn}; use crate::map::osm::OSMBaselayer; use crate::map::render::{ExtraRenderOptions, MapRender}; use crate::map::tiles::{LayerManager, LayerSource, Tile}; pub struct Map { pub layer_manager: LayerManager, pub lat: f64, pub long: f64, pub zoom: f64, pub render: Arc>, pub options: ExtraRenderOptions } impl Map { pub fn new<'a>(layer_manager: LayerManager, cc: &'a eframe::CreationContext<'a>) -> Option { let gl = cc.gl.as_ref().unwrap(); Some(Self { layer_manager: layer_manager, lat: 35.227085, long: -80.843124, zoom: 12.0, render: Arc::new(Mutex::new(MapRender::new(gl)?)), options: ExtraRenderOptions { } }) } pub fn process_input(&mut self, ctx: &egui::Context, ui: &mut Ui, rect: Rect, response: egui::Response) { let xy_delta = response.drag_motion(); let z_delta = if ui.rect_contains_pointer(rect) { ctx.input(|i| { let mut yd_sum = 0.0; for event in &i.events { yd_sum += match event { Event::MouseWheel { unit, delta, .. } => { match unit { // TODO: scaling of this MouseWheelUnit::Point => { delta.y }, MouseWheelUnit::Line => { delta.y }, MouseWheelUnit::Page => { delta.y }, } }, _ => 0.0 } } yd_sum }) } else { 0.0 }; self.lat += xy_delta.y as f64 * 0.001; self.long += xy_delta.x as f64 * 0.001; self.zoom += z_delta as f64 * 0.01; if self.zoom < 3.0 { self.zoom = 3.0 }; if self.zoom > 19.0 { self.zoom = 19.0 }; if self.long < -180.0 { self.long = -180.0 }; if self.long > 180.0 { self.lat = 180.0 }; if self.lat < -85.051129 { self.lat = -85.051129 }; if self.lat > 85.051129 { self.lat = 85.051129 }; } fn load_tiles(&mut self, ctx: &egui::Context, viewport_width: usize, viewport_height: usize) -> Tileset { // lat, long is top left // tiles are rendered at 256x256. how many can we fit? let tiles_x = viewport_width / 256; let tiles_y = viewport_height / 256; let mut tiles = vec![]; // determine start/end tiles let x = self.long.to_radians(); let y = self.lat.to_radians().tan().asinh(); let x = (1.0 + x/std::f64::consts::PI) / 2.0; let y= (1.0 - y/std::f64::consts::PI) / 2.0; let z_clamped = self.zoom.floor(); let tiles_across = 2usize.pow(z_clamped as u32); let x_coordinate = x * tiles_across as f64; let y_coordinate = y * tiles_across as f64; let x_tile_frac = x_coordinate.fract() * 256.0; let y_tile_frac = y_coordinate.fract() * 256.0; let tilex = x_coordinate.floor() as usize; let tiley = y_coordinate.floor() as usize; let xrange = tilex..(tilex + tiles_x); let yrange = tiley..(tiley + tiles_y); // for each tile, determine pixel coordinates, then offset by the tile fractionals for x in 0..tiles_x { let tile_x = tilex + x; for y in 0..tiles_y { let tile_y = tiley + y; let start_pixel_x = x as isize * 256 - x_tile_frac as isize; let start_pixel_y = y as isize * 256 - y_tile_frac as isize; let end_pixel_x = start_pixel_x + 256 - x_tile_frac as isize; let end_pixel_y = start_pixel_y + 256 -y_tile_frac as isize; let startx_zerotoone = start_pixel_x as f64 / viewport_width as f64; let starty_zerotoone = start_pixel_y as f64 / viewport_height as f64; let endx_zerotoone = end_pixel_x as f64 / viewport_width as f64; let endy_zerotoone = end_pixel_y as f64 / viewport_height as f64; let startx_opengl = startx_zerotoone * 2.0 - 1.0; let starty_opengl = starty_zerotoone * 2.0 - 1.0; let endx_opengl = endx_zerotoone * 2.0 - 1.0; let endy_opengl = endy_zerotoone * 2.0 - 1.0; // download the tile let tile = self.layer_manager.tiles.entry((z_clamped as usize, tile_x, tile_y, OSMBaselayer::SOURCE_ID)) .or_insert_with(|| { debug!("fetching {}/{}/{}", z_clamped, tile_x, tile_y); let (sender, promise) = Promise::new(); let request = ehttp::Request::get(format!("https://tile.openstreetmap.org/{}/{tile_x}/{tile_y}.png", z_clamped as usize)); let ctx = ctx.clone(); ehttp::fetch(request, move | response | { sender.send(match response { Ok(r) => match ImageReader::new(Cursor::new(r.bytes)).with_guessed_format() { Ok(img) => match img.decode() { Ok(img) => { ctx.request_repaint(); // wake up the ui Ok(img) }, Err(e) => Err(e.to_string()), }, Err(e) => Err(e.to_string()), }, Err(e) => Err(e.to_string()), }); }); Tile { promise } }); tiles.push(TilesetTile { sp_x: start_pixel_x as f32, sp_xclip: startx_opengl as f32, sp_y: start_pixel_y as f32, sp_yclip: starty_opengl as f32, ep_x: end_pixel_x as f32, ep_xclip: endy_opengl as f32, ep_y: end_pixel_y as f32, ep_yclip: endy_opengl as f32, tile: match tile.promise.ready() { None => None, Some(r) => match r { Ok(i) => Some(i.clone()), Err(_) => None } }, }); } } Tileset { tiles: Arc::new(Mutex::new(tiles)), } } pub fn custom_painting(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) { let (rect, response) = ui.allocate_exact_size( ui.available_size(), Sense::all() ); self.process_input(ctx, ui, rect, response); let tileset = self.load_tiles(ctx, rect.width() as usize, rect.height() as usize); let render = self.render.clone(); let options = self.options; let cb = egui_glow::CallbackFn::new(move |_info, painter| { let tileset = tileset.clone(); render.lock().unwrap().paint(painter.gl(), tileset, options); }); ui.painter().add(egui::PaintCallback { rect, callback: Arc::new(cb) }); } } #[derive(Clone)] struct Tileset { tiles: Arc>> } #[derive(Clone)] struct TilesetTile { sp_x: f32, sp_xclip: f32, sp_y: f32, sp_yclip: f32, ep_x: f32, ep_xclip: f32, ep_y: f32, ep_yclip: f32, tile: Option }