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::{LayerId, LayerManager, LayerSource, Tile, XCoord, YCoord, ZoomLevel}; 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.0005; self.long -= xy_delta.x as f64 * 0.0005; 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+2 { let tile_x = tilex + x; for y in 0..=tiles_y+2 { let tile_y = tiley + y; let pixel_x = 512_f32 * x as f32 - (2.0*x_tile_frac as f32); let pixel_y = (-512_f32 * y as f32) + (2.0*y_tile_frac as f32); // 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 { tileid: (z_clamped as usize, tile_x, tile_y, OSMBaselayer::SOURCE_ID), x: pixel_x, y: pixel_y, 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 width = rect.width(); let height = rect.height(); let cb = egui_glow::CallbackFn::new(move |_info, painter| { let tileset = tileset.clone(); render.lock().unwrap().paint(painter.gl(), width, height, tileset, options); }); ui.painter().add(egui::PaintCallback { rect, callback: Arc::new(cb) }); } } #[derive(Clone)] struct Tileset { tiles: Arc>> } #[derive(Clone)] struct TilesetTile { tileid: (ZoomLevel, XCoord, YCoord, LayerId), x: f32, y: f32, tile: Option }