From 9d3d7e5a7b72c4f8cc2231fb7c5b6b76e0185d24 Mon Sep 17 00:00:00 2001 From: Elliott Lewandowski Date: Mon, 5 Aug 2024 10:20:01 -0800 Subject: [PATCH 1/2] Updated compiling locally instructions in README.md --- README.md | 29 +++++++++++++++---------- data-ingest/Dockerfile | 25 --------------------- data-ingest/README.md | 24 +++++++++++++++++++- data-ingest/create_sql.py | 16 +++++++------- heatmap-api/README.md | 3 +++ heatmap-client/README.md | 13 +++++++++-- heatmap-client/src/main.rs | 4 ++-- heatmap-client/src/ui/user_interface.rs | 4 ++-- heatmap-service/README.md | 9 +++----- 9 files changed, 70 insertions(+), 57 deletions(-) delete mode 100644 data-ingest/Dockerfile create mode 100644 heatmap-api/README.md diff --git a/README.md b/README.md index 98beb5a..06e453a 100644 --- a/README.md +++ b/README.md @@ -2,27 +2,34 @@ The goal of this project is to rewrite and consolidate the existing codebases for creating heatmaps of satellite data to create an interactive heatmap ## Compiling Locally -1. Create a file named `.env` in the same directory as ingest.py -2. `.env` should contain login credentials to the PostgreSQL DB, ie. +### Generate sat_data.geojson +1. Navigate to the data-ingest directory, `cd data-ingest` +2. Create a file named `.env` +3. `.env` should contain login credentials to the PostgreSQL DB, ie. ``` export DB_HOST=change_me export DB_USERNAME=change_me export DB_PASSWORD=change_me export DB_NAME=change_me ``` -3. 1) If you have the dependencies installed locally you can now run `python3 ingest.py` and `sat_data.geojson` will be generated +4. 1) If you have the dependencies installed locally you can now run `python3 ingest.py` and `sat_data.geojson` will be generated 2) If you have conda installed then you can create a conda enviornment using `env.yml` inside the `Docker` directory, you can then run `python3 ingest.py` inside this environment to generate `sat_data.geojson` - - 3) If you have docker installed then you can `cd` into `Docker` and then enter `./run.sh` which will generate `sat_data.geojson` +### Setting up rust +1. Install rust, rust-lang.org is the page you're looking for +2. This project uses nightly features of rust, this means you will need a nightly version of rust, run `rustup toolchain install nightly` +3. To swtich to a nightly build of rust run `rustup override set nightly` -## Dependancies -- postgis -- shapely -- pandas -- geopandas -- matplotlib +### Setting up the server +1. Move `sat_data.geojson` to the `heatmap-service` directory, don't change the file name or the server will fail to find the data +2. Navigate into the `heatmap-service` directory, `cd heatmap-service` +3. Run `cargo run` in the terminal and you now have a locally running version of the server, if the terminal you entered this command into closes you will need to repeat this step in a new terminal + +### Setting up the client +1. Navigate to the `heatmap-client` directory, `cd heatmap-client` +2. Install trunk, run `cargo binstall trunk` +3. Run `trunk serve --open`, this should open a page in your default browser, if you would prefer the command not open a page remove `--open` and it will serve the client without opening a new page ## Contributing Elliott Lewandowski diff --git a/data-ingest/Dockerfile b/data-ingest/Dockerfile deleted file mode 100644 index d43334b..0000000 --- a/data-ingest/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ - -FROM ubuntu:latest - -#Silent Install -ARG DEBIAN_FRONTEND=noninteractive - -# Update / Install Stuff: -RUN apt-get update && apt-get upgrade -y - #Non-Python Stuff: -RUN apt-get install -y libpq-dev postgresql postgis - #Python Stuff: -RUN apt-get install -y python3-dev python3-pip python3-matplotlib python3-pyshp - -#Setting up dummy user -RUN adduser heat -RUN su heat && mkdir ~/heatmap_proj - -#Copy over project files -COPY ./heatmap.py home/heat/heatmap_proj/heatmap.py -COPY ./create_sql.py home/heat/heatmap_proj/create_sql.py -COPY ./antimeridian.py home/heat/heatmap_proj/antimeridian.py -COPY ./Resources home/heat/heatmap_proj/Resources -COPY ./cred.env home/heat/heatmap_proj/cred.env - - diff --git a/data-ingest/README.md b/data-ingest/README.md index 82d7985..225d785 100644 --- a/data-ingest/README.md +++ b/data-ingest/README.md @@ -1,3 +1,25 @@ # Data Ingest -This directory handles pulling data from a PostgreSQL database and formatting. The file it generates is served to the heatmap-client from the heatmap-service \ No newline at end of file +This directory handles pulling data from a PostgreSQL database and formatting. The file it generates is served to the heatmap-client from the heatmap-service + +## Compiling Locally + +1. Create a file named `.env` +2. `.env` should contain login credentials to the PostgreSQL DB, ie. + ``` + export DB_HOST=change_me + export DB_USERNAME=change_me + export DB_PASSWORD=change_me + export DB_NAME=change_me + ``` +3. 1) If you have the dependencies installed locally you can now run `python3 ingest.py` and `sat_data.geojson` will be generated + + 2) If you have conda installed then you can create a conda enviornment using `env.yml` inside the `Docker` directory, you can then run `python3 ingest.py` inside this environment to generate `sat_data.geojson` + + +## Dependancies +- postgis +- shapely +- pandas +- geopandas +- dotenv \ No newline at end of file diff --git a/data-ingest/create_sql.py b/data-ingest/create_sql.py index e5f6170..15b9e16 100644 --- a/data-ingest/create_sql.py +++ b/data-ingest/create_sql.py @@ -13,22 +13,22 @@ def generate_command( start=date.datetime(2021, 1, 1), - end=date.datetime(2021, 2, 1), + end=date.datetime(2021, 1, 10), platform_type="'SA', 'SB'", data_type="'GRD'", ): cmd = ( """SELECT ST_AsText(ST_Centroid(shape)), g.granule_name, g.platform_type, g.data_sensor_type, g.start_time, g.shape - - FROM granule g - + + FROM granule g + where g.platform_type in (""" + platform_type + """) and - + g.data_granule_type in ('SENTINEL_1A_FRAME', 'SENTINEL_1B_FRAME' ) and - + substr(granule_name, 8, 3) in (""" + data_type + """) and @@ -39,8 +39,8 @@ def generate_command( + start.strftime("%x") + """' and '""" + end.strftime("%x") - + """' - + + """' + order by shape asc;""" ) diff --git a/heatmap-api/README.md b/heatmap-api/README.md new file mode 100644 index 0000000..12b8423 --- /dev/null +++ b/heatmap-api/README.md @@ -0,0 +1,3 @@ +# Heatmap API + +This directory contains code that is used by both `heatmap-service` and `heatmap-client`. It exists to reduce code duplication. It mainly contains code related to the structure of data requests and responses. \ No newline at end of file diff --git a/heatmap-client/README.md b/heatmap-client/README.md index 8676520..b13aac1 100644 --- a/heatmap-client/README.md +++ b/heatmap-client/README.md @@ -2,8 +2,17 @@ This code is responsible for the client side of the heatmap generation process. -The heavy lifting of generating the actual heatmap occurs in src/canvas +`./src/canvas` does the heavy lifting of generating the actual heatmap The GPU is leveraged to generate these heatmaps, a large portion of the code is getting wgpu to play nicley in wasm -The ingest folder requests data from the server located in the heatmap-service directory \ No newline at end of file +`./src/ingest` requests data from the server located in the heatmap-service directory + +`./src/ui` This contains the user interface that is overlayed onto the heatmap + +`./assets` contains static assets used in the client, ie. colormap textures and resources to export a png + +## Compiling Locally +1. Ensure rust is on a nightly build, you can check with `rustup toolchain list` +2. Install trunk, run `cargo binstall trunk` +3. Run `trunk serve --open`, this should open a page in your default browser, if you would prefer the command not open a page remove `--open` and it will serve the client without opening a new page \ No newline at end of file diff --git a/heatmap-client/src/main.rs b/heatmap-client/src/main.rs index ffc503d..c938d59 100644 --- a/heatmap-client/src/main.rs +++ b/heatmap-client/src/main.rs @@ -26,11 +26,11 @@ fn main() { heatmap_api::PlatformType::Sentinel1A, heatmap_api::PlatformType::Sentinel1B, ], - start_date: NaiveDate::from_ymd_opt(2000, 1, 1) + start_date: NaiveDate::from_ymd_opt(2019, 1, 1) .expect("Failed to create start date when creating filter signal") .format("%Y-%m-%d") .to_string(), - end_date: NaiveDate::from_ymd_opt(2025, 2, 1) + end_date: NaiveDate::from_ymd_opt(2024, 4, 21) .expect("Failed to create end date when creating filter signal") .format("%Y-%m-%d") .to_string(), diff --git a/heatmap-client/src/ui/user_interface.rs b/heatmap-client/src/ui/user_interface.rs index 5fa1676..b61b617 100644 --- a/heatmap-client/src/ui/user_interface.rs +++ b/heatmap-client/src/ui/user_interface.rs @@ -6,12 +6,12 @@ use leptos::*; #[component] pub fn UserInterface(set_filter: WriteSignal) -> impl IntoView { - let min_date = NaiveDate::from_ymd_opt(2000, 1, 2) + let min_date = NaiveDate::from_ymd_opt(2019, 1, 1) .expect("Failed to parse left hand side when finding min_date") .format("%Y-%m-%d") .to_string(); - let max_date = NaiveDate::from_ymd_opt(2025, 2, 2) + let max_date = NaiveDate::from_ymd_opt(2024, 4, 21) .expect("Failed to parse left hand side when finding max_date") .format("%Y-%m-%d") .to_string(); diff --git a/heatmap-service/README.md b/heatmap-service/README.md index 8435065..0068d66 100644 --- a/heatmap-service/README.md +++ b/heatmap-service/README.md @@ -2,9 +2,6 @@ Here is the rust microservice for serving heatmap data. This service requires a sat_data.geojson file which is generated by the ingest script in data-ingest - - - It requires an .env file, or the proper configuration environment variables to run. Ex: @@ -13,8 +10,8 @@ Ex: SERVER_ADDRESS=127.0.0.1:8080 # the bind address for the microservice CACHE_TTL=3600 # how long (in seconds) the cache should last for GEO_JSON_PATH=/path/to/geojson -REDIS.URL=redis://127.0.0.1:6379 -REDIS.POOL.MAX_SIZE=16 ``` -additionally, for integration testing to function a .env_tests file is required configured to use a special testing database \ No newline at end of file +## Compiling Locally +1. Move `sat_data.geojson` to the `heatmap-service` directory, make sure the filename matches the `GEO_JSON_PATH` or the server will fail to find the data +3. Run `cargo run` in the terminal and you now have a locally running version of the server, if the terminal you entered this command into closes you will need to repeat this step in a new terminal \ No newline at end of file From b93623bd466b502d44d79dfbc95a1683839b3b4b Mon Sep 17 00:00:00 2001 From: Elliott Lewandowski Date: Mon, 5 Aug 2024 16:40:18 -0800 Subject: [PATCH 2/2] Improved documentation of heatmap-client and heatmap-api --- heatmap-api/src/lib.rs | 22 ++++--- heatmap-client/src/canvas/app.rs | 26 ++++++-- heatmap-client/src/canvas/camera.rs | 15 ++++- heatmap-client/src/canvas/geometry.rs | 13 +++- heatmap-client/src/canvas/input.rs | 7 +++ heatmap-client/src/canvas/mod.rs | 6 +- heatmap-client/src/canvas/render_context.rs | 32 ++++++++-- .../src/canvas/shaders/colormap.wgsl | 2 - heatmap-client/src/canvas/state.rs | 27 +++++++- heatmap-client/src/canvas/texture.rs | 63 +++++++------------ heatmap-client/src/ingest/load.rs | 19 ++---- heatmap-client/src/ingest/request.rs | 7 +++ heatmap-client/src/ui/user_interface.rs | 7 +-- 13 files changed, 162 insertions(+), 84 deletions(-) diff --git a/heatmap-api/src/lib.rs b/heatmap-api/src/lib.rs index 400416b..3285af2 100644 --- a/heatmap-api/src/lib.rs +++ b/heatmap-api/src/lib.rs @@ -4,6 +4,7 @@ pub trait ToPartialString { fn _to_partial_string(&self) -> String; } +// Enums defining possible filter options #[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq)] pub enum ProductTypes { #[serde(rename = "GRD")] @@ -58,6 +59,7 @@ impl DataSensor { } } +// The filter passed from client to server on a request for data #[derive(Deserialize, Serialize, Clone)] pub struct Filter { pub product_type: Vec, @@ -66,11 +68,19 @@ pub struct Filter { pub end_date: String, } +// Client sends this to server #[derive(Deserialize, Serialize)] pub struct HeatmapQuery { pub filter: Filter, } +// Server sends this back to client after a query, +// contains the granule data +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct HeatmapResponse { + pub data: HeatmapData, +} + #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct HeatmapData { pub data: InteriorData, @@ -82,18 +92,14 @@ pub struct InteriorData { pub weights: Vec, } +// Server sends this back to client after a query, +// contains world outline data #[derive(Deserialize, Serialize, Debug, PartialEq)] -pub struct HeatmapResponse { - pub data: HeatmapData, +pub struct OutlineResponse { + pub data: OutlineData, } - #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct OutlineData { pub length: i32, pub positions: Vec>, } - -#[derive(Deserialize, Serialize, Debug, PartialEq)] -pub struct OutlineResponse { - pub data: OutlineData, -} diff --git a/heatmap-client/src/canvas/app.rs b/heatmap-client/src/canvas/app.rs index c7c25e0..57f1f19 100644 --- a/heatmap-client/src/canvas/app.rs +++ b/heatmap-client/src/canvas/app.rs @@ -27,10 +27,13 @@ pub struct App<'a> { } // The application handler instance doesnt allow for error handling +// The application handler responds to changes in the event loop, we send custom events here using +// an event_loop_proxy impl<'a> ApplicationHandler> for App<'a> { + // This is run on initial startup, creates a window and stores it in the state, also stores + // the windows canvas in external state fn resumed(&mut self, event_loop: &ActiveEventLoop) { - // Create the window and add it to state self.state.window = Some(Rc::new( event_loop .create_window( @@ -40,6 +43,7 @@ impl<'a> ApplicationHandler> for App<'a> { .expect("ERROR: Failed to create window"), )); + // Convert web_sys HtmlCanvasElement into a leptos HtmlElement self.external_state.borrow_mut().canvas = self .state .window @@ -110,7 +114,7 @@ impl<'a> ApplicationHandler> for App<'a> { } WindowEvent::Resized(physical_size) => { - // Initialize setup of state when resized is first called + // Initialize setup of state when resized is first called, otherwise call state.resize if self.state.init_stage == InitStage::Incomplete { web_sys::console::log_1(&"Generating state...".into()); leptos::spawn_local(super::render_context::generate_render_context( @@ -127,6 +131,7 @@ impl<'a> ApplicationHandler> for App<'a> { } } + // Any other event will be handled by the user input handler _ => { self.state.handle_input_event(event); } @@ -161,6 +166,7 @@ impl<'a> ApplicationHandler> for App<'a> { ); } + // There is incoming data from the service, we need to place this new data into buffers to render UserMessage::IncomingData(data, outline_data) => { web_sys::console::log_1(&"Generating Buffers...".into()); let render_context = self @@ -190,9 +196,13 @@ impl<'a> ApplicationHandler> for App<'a> { web_sys::console::log_1(&"Done Generating Buffers".into()); + // Turn off the loading wheel self.external_state.borrow_mut().set_ready.set(true); } + // This is part of getting the max weight of a set of data, to get data from the GPU + // you have to map a buffer to the CPU, this is done asynchronously so we fire off + // a custom event on mapping completion UserMessage::BufferMapped => { let render_context = self .state @@ -200,19 +210,22 @@ impl<'a> ApplicationHandler> for App<'a> { .as_mut() .expect("Failed to get render context in UserMessage::BufferMapped"); + // We read the data contained in the buffer and convert it from &[u8] to Vec let raw_bytes: Vec = (&*render_context .max_weight_context .buffer .slice(..) .get_mapped_range()) .into(); - let mut red_data: Vec = Vec::new(); + // The buffer is formated for f32 but we pulled a Vec, we must reform the Vec from the bytes + let mut red_data: Vec = Vec::new(); let mut raw_iter = raw_bytes.iter(); - while let Ok(raw) = raw_iter.next_chunk::<4>() { + // Read one channel into a f32 red_data.push(f32::from_le_bytes([*raw[0], *raw[1], *raw[2], *raw[3]])); + // The texture we stored in the buffer was rgba32Float but only had red data so we skip the g, b, a channels match raw_iter.advance_by(4 * 3) { Ok(_) => {} Err(_) => { @@ -221,6 +234,7 @@ impl<'a> ApplicationHandler> for App<'a> { } } + // Find the max value in the Vec we just created let mut max = 0.0; for value in red_data.iter() { @@ -231,9 +245,11 @@ impl<'a> ApplicationHandler> for App<'a> { web_sys::console::log_1(&format!("Max: {:?}", max).into()); + // We now update the uniform buffer with our max weight + // so that we can read the max value in the colormap render pass let mut uniform_data: Vec = max.to_le_bytes().into(); - // Uniform Buffer must be 16 byte aligned + // Uniform Buffer must be 16 byte aligned so we pad it with 0's while uniform_data.len() % 16 != 0 { uniform_data.push(0); } diff --git a/heatmap-client/src/canvas/camera.rs b/heatmap-client/src/canvas/camera.rs index 7abd0b4..5c0fe68 100644 --- a/heatmap-client/src/canvas/camera.rs +++ b/heatmap-client/src/canvas/camera.rs @@ -12,6 +12,7 @@ pub enum CameraEvent { EntireView, } +// This is the camera that modifies the viewport that the renderpasses render pub struct CameraContext { pub camera: Camera, pub camera_uniform: CameraUniform, @@ -25,6 +26,7 @@ impl CameraContext { device: &wgpu::Device, config: &wgpu::SurfaceConfiguration, ) -> Self { + // Create a default camera let camera = Camera { aspect: config.width as f64 / config.height as f64, width: config.width as f64, @@ -33,9 +35,13 @@ impl CameraContext { zoom: 1.0, }; + // To access the camera from the inside a render pass we create a camera_uniform which is just a matrix! let mut camera_uniform = CameraUniform::new(); + + // Does some cool Matrix Math to create the view projection matrix! camera_uniform.update_view_proj(&camera); + // We have to store the camera in a uniform buffer so we can access it inside a render pass let camera_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Camera Buffer"), contents: bytemuck::cast_slice(&[camera_uniform]), @@ -76,16 +82,18 @@ impl CameraContext { } pub fn run_camera_logic(&mut self, input_state: &mut InputState) { + // Updates the zoom of the camera self.update_camera(CameraEvent::Zoom( input_state.consume_scroll_delta() * self.camera.zoom * 0.001, (input_state.cursor_position.x, input_state.cursor_position.y).into(), )); - + // Updates the position of the camera let drag_delta = input_state.consume_drag_delta(); self.update_camera(CameraEvent::Translate( self.mouse_coordinate_convert((drag_delta.x, drag_delta.y).into()), )); + // Have to recalculate the view projection matrix for these changes to take effect self.rebuild_view_matrix(); } @@ -112,6 +120,7 @@ impl CameraContext { self.camera.aspect = aspect; } + // Zooms the camera in and out, ensures the camera stays within the bounds of the heatmap CameraEvent::Zoom(mut zoom, mut pos) => { let camera_size = cgmath::Vector2::::new(self.camera.width, self.camera.height) @@ -136,6 +145,7 @@ impl CameraContext { self.update_camera(CameraEvent::Translate(pos - pos * scale_factor)); } + // Moves the camera around, ensures the camera stays within bounds of the heatmap CameraEvent::Translate(mut pos) => { let camera_upper_bounds: cgmath::Vector2 = self.camera.position + pos @@ -169,6 +179,7 @@ impl CameraContext { self.camera.position += pos; } + // Displays the entire heatmap, used to calculate max weight and export to png CameraEvent::EntireView => { self.camera.position = Vector2::new(-180.0, 90.0); @@ -185,6 +196,7 @@ impl CameraContext { self.camera_uniform.update_view_proj(&self.camera); } + // Store the contents of camera_uniform in the buffer pub fn write_camera_buffer(&self, render_context: &RenderContext) { render_context.queue.write_buffer( &self.camera_buffer, @@ -204,6 +216,7 @@ pub struct Camera { } impl Camera { + // This is the cool matrix math that makes this whole thing actually work! pub fn build_view_projection_matrix(&self) -> cgmath::Matrix4 { let view = cgmath::Matrix4::from_scale(self.zoom) * cgmath::Matrix4::from_translation(-self.position.extend(0.0)); diff --git a/heatmap-client/src/canvas/geometry.rs b/heatmap-client/src/canvas/geometry.rs index c9a33aa..b0ff85c 100644 --- a/heatmap-client/src/canvas/geometry.rs +++ b/heatmap-client/src/canvas/geometry.rs @@ -3,7 +3,7 @@ use wgpu::util::DeviceExt; use super::render_context::RenderContext; use crate::ingest::load::BufferStorage; -// To-do: Make this based on the size of the surface +// Used to render the blended texture onto const RECTANGLE_VERTICES: &[Vertex] = &[ Vertex { position: [-180.0, -90.0, 0.0], @@ -21,17 +21,21 @@ const RECTANGLE_VERTICES: &[Vertex] = &[ const RECTANGLE_INDICES: &[u16] = &[0, 2, 3, 0, 2, 1]; +// All the things needed to bind a buffer to a render pass pub struct BufferContext { pub buffer: wgpu::Buffer, pub bind_group_layout: wgpu::BindGroupLayout, pub bind_group: wgpu::BindGroup, } + +// All the things needed to draw a vertex buffer using indices pub struct BufferLayer { pub vertex_buffer: wgpu::Buffer, pub index_buffer: wgpu::Buffer, pub num_indices: u32, } +// All the geometry that is used in the blend render pass to create a colormap texture pub struct Geometry { pub lod_layers: Vec, pub rectangle_layer: BufferLayer, @@ -85,6 +89,7 @@ impl Geometry { } } +// Stores each Level of Detail into its own BufferLayer to be used in the blend render pass fn gen_lod_layers( render_context: &RenderContext, buffer_data: Vec, @@ -123,6 +128,8 @@ fn gen_lod_layers( lod_layers } +// A uniform buffer that a texture can be copied to and can be mapped to the cpu +// used to calculate the max weight pub fn generate_max_weight_buffer( device: &wgpu::Device, size: winit::dpi::PhysicalSize, @@ -138,6 +145,7 @@ pub fn generate_max_weight_buffer( vertex_buffer } +// Used to store the camera_uniform pub fn generate_uniform_buffer(device: &wgpu::Device) -> BufferContext { let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Uniform Buffer"), @@ -176,7 +184,7 @@ pub fn generate_uniform_buffer(device: &wgpu::Device) -> BufferContext { bind_group: uniform_bind_group, } } -/// A vertex passed into the wgsl shader +/// A vertex passed into blend.wgsl #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct BlendVertex { @@ -206,6 +214,7 @@ impl BlendVertex { } } +// A vertex used in colormap.wgsl and max_weight.wgsl #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct Vertex { diff --git a/heatmap-client/src/canvas/input.rs b/heatmap-client/src/canvas/input.rs index 6da5284..4b5682b 100644 --- a/heatmap-client/src/canvas/input.rs +++ b/heatmap-client/src/canvas/input.rs @@ -19,12 +19,14 @@ pub struct InputState { } impl InputState { + // Returns the amount of change in scroll delta since last function call pub fn consume_scroll_delta(&mut self) -> f64 { let delta = self.mouse_scroll_delta; self.mouse_scroll_delta = 0.0; delta } + // Returns the amount of change in cursor position since last function call pub fn consume_drag_delta(&mut self) -> PhysicalPosition { let delta = self.mouse_drag_delta; self.mouse_drag_delta = PhysicalPosition::new(0.0, 0.0); @@ -39,10 +41,12 @@ impl InputState { self.mouse_buttons.contains(&button) } + // Performs the specified action for the given window event pub fn eat_event(&mut self, event: WindowEvent) { use WindowEvent::*; match event { + // Update drag delta based on the change in cursor position CursorMoved { device_id: _, position, @@ -54,6 +58,7 @@ impl InputState { self.cursor_position = position; } + // Add/remove a mouse button press from the list of currently pressed buttons MouseInput { device_id: _, state, @@ -72,6 +77,7 @@ impl InputState { } } + // Track the change in the mouse wheel scroll MouseWheel { device_id: _, delta, @@ -90,6 +96,7 @@ impl InputState { } } + // Add/Remove the keyboard key from the list of pressed keys KeyboardInput { device_id: _, event, diff --git a/heatmap-client/src/canvas/mod.rs b/heatmap-client/src/canvas/mod.rs index 3a866e3..cc7ee03 100644 --- a/heatmap-client/src/canvas/mod.rs +++ b/heatmap-client/src/canvas/mod.rs @@ -24,7 +24,7 @@ use winit::platform::web::EventLoopExtWebSys; use crate::ingest::load::DataLoader; -/// Component to display a wgsl shader +/// Component to display a heatmap generated using wgpu and wgsl shaders #[component] pub fn Canvas() -> impl IntoView { // Signal from the UI containing the filter @@ -36,6 +36,7 @@ pub fn Canvas() -> impl IntoView { .build() .expect("ERROR: Failed to create event loop"); + // Determines if the loading bar is displayed or not, false is displayed, true is hidden let (ready, set_ready) = create_signal(false); // The canvas element will be stored here once it has been created @@ -55,6 +56,7 @@ pub fn Canvas() -> impl IntoView { // Start the event loop event_loop.spawn_app(app); + // Get a reference to the canvas and add a css class to it let canvas = external_state .borrow() .canvas @@ -62,8 +64,10 @@ pub fn Canvas() -> impl IntoView { .expect("ERROR: Failed to get external state") .attr("class", "wgpu_surface"); + // Struct responsible for making requests to the service for new data let data_loader = DataLoader::new(event_loop_proxy, set_ready); + // Anytime the filter signal changes the data loader now calls load data with the new signal create_effect(move |_| data_loader.load_data(filter())); view! { diff --git a/heatmap-client/src/canvas/render_context.rs b/heatmap-client/src/canvas/render_context.rs index f40cb92..bd6e06d 100644 --- a/heatmap-client/src/canvas/render_context.rs +++ b/heatmap-client/src/canvas/render_context.rs @@ -12,6 +12,7 @@ use super::texture::{ generate_blend_texture, generate_colormap_texture, generate_max_weight_texture, TextureContext, }; +// Stores all the things we need to set up wgpu and run render passes, pub struct RenderContext<'a> { pub surface: wgpu::Surface<'a>, pub device: wgpu::Device, @@ -29,11 +30,12 @@ pub struct RenderContext<'a> { pub max_weight_context: MaxWeightContext, } -/// Create a new state +/// Create a new RenderContext pub async fn generate_render_context<'a>( window: Rc, event_loop_proxy: EventLoopProxy>, ) { + // Default starting size, gets changed as soon as WindowEvent::Resize is fired let size = PhysicalSize::new(800, 800); //////////////////// @@ -109,18 +111,25 @@ pub async fn generate_render_context<'a>( view_formats: vec![], }; + //////////////////////////////////////////// + // Set up resources used in Render Passes // + //////////////////////////////////////////// + + // Used to modify the displayed viewport, ie zoom and pan let camera_context = CameraContext::generate_camera_context(&device, &config); + // Used to convert polygons into heatmap let blend_texture_context = generate_blend_texture(&device, size); let colormap_texture_context = generate_colormap_texture(&device, &queue); - let max_weight_texture = generate_max_weight_texture(&device, size); + // Used to calculate the maximum weight for a data set + let max_weight_texture = generate_max_weight_texture(&device, size); let max_weight_buffer = generate_max_weight_buffer(&device, size); let max_weight_uniform_buffer = generate_uniform_buffer(&device); - //////////////////////////// - // Set up render pipeline // - //////////////////////////// + ////////////////////////////////// + // Set up blend render pipeline // + ////////////////////////////////// let blend_shader = device.create_shader_module(wgpu::include_wgsl!("shaders/blend.wgsl")); let blend_render_pipeline_layout = @@ -178,6 +187,9 @@ pub async fn generate_render_context<'a>( multiview: None, }); + ///////////////////////////////////// + // Set up colormap render pipeline // + ///////////////////////////////////// let colormap_shader = device.create_shader_module(wgpu::include_wgsl!("shaders/colormap.wgsl")); let colormap_render_pipeline_layout = @@ -228,6 +240,10 @@ pub async fn generate_render_context<'a>( multiview: None, }); + //////////////////////////////////// + // Set up Outline render pipeline // + //////////////////////////////////// + let outline_shader = device.create_shader_module(wgpu::include_wgsl!("shaders/outline.wgsl")); let outline_render_pipeline_layout = @@ -274,6 +290,9 @@ pub async fn generate_render_context<'a>( multiview: None, }); + /////////////////////////////////////// + // Set up max weight render pipeline // + /////////////////////////////////////// let max_weight_shader = device.create_shader_module(wgpu::include_wgsl!("shaders/max_weight.wgsl")); @@ -348,9 +367,12 @@ pub async fn generate_render_context<'a>( }; web_sys::console::log_1(&"Done Generating State".into()); + // Because this is a wasm application we cannot block on async calls so we instead send a message + // back to the application handler when this function completes let _ = event_loop_proxy.send_event(UserMessage::StateMessage(message)); } +/// Contains resources neccessary to calculate the maximum weight of a set of data pub struct MaxWeightContext { pub texture: wgpu::Texture, pub buffer: wgpu::Buffer, diff --git a/heatmap-client/src/canvas/shaders/colormap.wgsl b/heatmap-client/src/canvas/shaders/colormap.wgsl index f34ad53..1d93b15 100644 --- a/heatmap-client/src/canvas/shaders/colormap.wgsl +++ b/heatmap-client/src/canvas/shaders/colormap.wgsl @@ -19,8 +19,6 @@ fn vs_main( @group(0) @binding(0) var colormap_tex: texture_1d; -@group(0) @binding(1) -var colormap_samp: sampler; @group(1) @binding(0) var blended_tex: texture_2d; diff --git a/heatmap-client/src/canvas/state.rs b/heatmap-client/src/canvas/state.rs index 85b1806..28d2dcd 100644 --- a/heatmap-client/src/canvas/state.rs +++ b/heatmap-client/src/canvas/state.rs @@ -15,7 +15,7 @@ use super::input::InputState; use super::render_context::{MaxWeightState, RenderContext}; use super::texture::generate_blend_texture; -/// Tracks wether state is finished setting up +/// Tracks whether state is finished setting up #[derive(Default, PartialEq, Eq)] pub enum InitStage { #[default] @@ -36,7 +36,7 @@ pub struct State<'a> { } impl<'a> State<'a> { - // handles input events + // Process any user input on the heatmap pub fn handle_input_event(&mut self, event: WindowEvent) { self.input_state.eat_event(event); } @@ -74,6 +74,7 @@ impl<'a> State<'a> { .surface .configure(&render_context.device, &render_context.config); + // Blend texture must be the same size as the window to preserve resolution render_context.blend_texture_context = generate_blend_texture(&render_context.device, new_size); } @@ -86,6 +87,7 @@ impl<'a> State<'a> { .as_mut() .expect("ERROR: Failed to get render context in render"); + // Ensure we have data to render if let Some(geometry) = self.geometry.as_ref() { /////////////////////// // Blend Render Pass // @@ -106,13 +108,17 @@ impl<'a> State<'a> { .camera_context .run_camera_logic(&mut self.input_state); + // If we have not calculated the max weight set the camera to cover the entire screen + // and save the old camera if render_context.max_weight_context.state == MaxWeightState::Empty { self.camera_storage = Some(render_context.camera_context.camera.clone()); render_context .camera_context .update_camera(CameraEvent::EntireView); - } else if self.camera_storage.is_some() { + } + // If we have a camera saved set the current camera to the old camera and remove it from storage + else if self.camera_storage.is_some() { render_context.camera_context.camera = self .camera_storage .as_ref() @@ -125,6 +131,7 @@ impl<'a> State<'a> { .camera_context .write_camera_buffer(render_context); + // Select the Level of Detail to use for the satellite granules based on the zoom let zoom = render_context.camera_context.camera.zoom; let mut active_blend_layer = &geometry.lod_layers[0]; match zoom { @@ -137,6 +144,7 @@ impl<'a> State<'a> { _ => (), } + // Configure render pass and set pipeline, bind groups, vertex buffer, and index buffer let mut blend_render_pass = blend_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Blend Render Pass"), @@ -173,6 +181,7 @@ impl<'a> State<'a> { blend_render_pass.draw_indexed(0..active_blend_layer.num_indices, 0, 0..1); } + // Execute the configured render pass render_context .queue .submit(std::iter::once(blend_encoder.finish())); @@ -181,6 +190,7 @@ impl<'a> State<'a> { // Max Weight Render Pass // //////////////////////////// + // If we have not begun computing a max weight do so now if render_context.max_weight_context.state == MaxWeightState::Empty { let max_weight_output = &render_context.max_weight_context.texture; let max_weight_view = @@ -192,6 +202,8 @@ impl<'a> State<'a> { .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Max Weight Render Encoder"), }); + + // Configure the render pass to render the blend texture to a rgba32Float texture { let mut max_weight_render_pass = max_weight_encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -245,6 +257,7 @@ impl<'a> State<'a> { label: Some("Copy Encoder"), }); + // Copy the rgba32Float texture into a buffer copy_encoder.copy_texture_to_buffer( wgpu::ImageCopyTexture { texture: &render_context.max_weight_context.texture, @@ -279,6 +292,8 @@ impl<'a> State<'a> { .expect("Failed to get event loop proxy when mapping max weight buffer to cpu") .clone(); + // Begin mapping the buffer we just copied the texture into to the CPU, + // send a signal to the event loop upon completion render_context .max_weight_context .buffer @@ -292,7 +307,10 @@ impl<'a> State<'a> { //////////////////////////// // Colormap Render Pass // //////////////////////////// + + // If we have computed a max weight proceed with rendering the heatmap else if render_context.max_weight_context.state == MaxWeightState::Completed { + // We will draw to the surface of the window, this is displayed in the HtmlElement let colormap_output = render_context.surface.get_current_texture()?; let color_view = colormap_output .texture @@ -305,6 +323,7 @@ impl<'a> State<'a> { label: Some("Colormap Render Encoder"), }); + // Select the level of detail for the world outline if let Some(geometry) = self.geometry.as_ref() { let zoom = render_context.camera_context.camera.zoom; let mut active_outline_layer = &geometry.outline_layers[0]; @@ -339,6 +358,7 @@ impl<'a> State<'a> { timestamp_writes: None, }); + // Render the outline of the world color_render_pass.set_pipeline(&render_context.outline_render_pipeline); color_render_pass.set_bind_group( @@ -356,6 +376,7 @@ impl<'a> State<'a> { color_render_pass.draw_indexed(0..active_outline_layer.num_indices, 0, 0..1); + // Render the heatmap over the world outline, uses the blend texture we generated in the first render pass color_render_pass.set_pipeline(&render_context.colormap_render_pipeline); color_render_pass.set_bind_group( 0, diff --git a/heatmap-client/src/canvas/texture.rs b/heatmap-client/src/canvas/texture.rs index 74f985b..88f51e5 100644 --- a/heatmap-client/src/canvas/texture.rs +++ b/heatmap-client/src/canvas/texture.rs @@ -7,6 +7,7 @@ pub struct TextureContext { pub bind_group: wgpu::BindGroup, } +/// Generate a texture that the blend render pass can render to and the colormap render pass can read from pub fn generate_blend_texture( device: &wgpu::Device, size: winit::dpi::PhysicalSize, @@ -17,6 +18,7 @@ pub fn generate_blend_texture( depth_or_array_layers: 1, }; + // Create a 2D R16Float texture with appropriate usages let blend_texture = device.create_texture(&wgpu::TextureDescriptor { size: blend_texture_size, mip_level_count: 1, @@ -28,6 +30,7 @@ pub fn generate_blend_texture( view_formats: &[], }); + // Set up a sampler for the texture we just created let blend_texture_view = blend_texture.create_view(&wgpu::TextureViewDescriptor::default()); let blend_sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, @@ -39,6 +42,7 @@ pub fn generate_blend_texture( ..Default::default() }); + // Set up the bind group for the above texture and sampler let blend_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[ @@ -84,8 +88,9 @@ pub fn generate_blend_texture( } } +/// Generates a 1D texture with the colormap the heatmap will use pub fn generate_colormap_texture(device: &wgpu::Device, queue: &wgpu::Queue) -> TextureContext { - // Def of colormap, should make this fancier in the future, currently a placeholder + // Read in the colormap texture from a .png let colormap_bytes = include_bytes!("../../assets/magma_filtered_alt.png"); let colormap_image = image::load_from_memory(colormap_bytes) .expect("ERROR: Failed to generate image from colormap_bytes"); @@ -109,6 +114,7 @@ pub fn generate_colormap_texture(device: &wgpu::Device, queue: &wgpu::Queue) -> view_formats: &[], }); + // Fill in the texture with the data from the .png we read above queue.write_texture( wgpu::ImageCopyTexture { texture: &texture, @@ -125,51 +131,28 @@ pub fn generate_colormap_texture(device: &wgpu::Device, queue: &wgpu::Queue) -> texture_size, ); - let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); - let sampler = device.create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, - mag_filter: wgpu::FilterMode::Linear, - min_filter: wgpu::FilterMode::Nearest, - mipmap_filter: wgpu::FilterMode::Nearest, - ..Default::default() - }); - + // Set up a bind group for the texture let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D1, - multisampled: false, - }, - count: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D1, + multisampled: false, }, - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), - count: None, - }, - ], + count: None, + }], label: Some("colormap_bind_group_layout"), }); + let texture_view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &bind_group_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&sampler), - }, - ], + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture_view), + }], label: Some("colormap_bind_group"), }); @@ -180,6 +163,8 @@ pub fn generate_colormap_texture(device: &wgpu::Device, queue: &wgpu::Queue) -> } } +/// A texture capable of being copied to a buffer, we render the blend texture onto this texture when +/// calculating the max weight pub fn generate_max_weight_texture( device: &wgpu::Device, size: winit::dpi::PhysicalSize, diff --git a/heatmap-client/src/ingest/load.rs b/heatmap-client/src/ingest/load.rs index 79b3868..24378a5 100644 --- a/heatmap-client/src/ingest/load.rs +++ b/heatmap-client/src/ingest/load.rs @@ -23,6 +23,7 @@ pub struct BufferStorage { pub num_indices: u32, } +// Struct that is responsible for submitting requests to the service for new data pub struct DataLoader { pub event_loop_proxy: EventLoopProxy>, pub active_requests: leptos::ReadSignal, @@ -44,6 +45,8 @@ impl DataLoader { set_ready, } } + + // Updates signals and starts the process of requesting new data based on filter pub fn load_data(&self, filter: heatmap_api::Filter) { self.set_active_requests.update(|n| *n += 1); self.set_ready.set(false); @@ -74,19 +77,6 @@ async fn load_data_async( web_sys::console::log_1(&"Meshing data...".into()); let meshed_data = mesh_data(Data::Heatmap(data)); let meshed_outline_data = mesh_data(Data::Outline(outline_data)); - web_sys::console::log_3( - &"Meshed Data: \n".into(), - &format!( - "Vertices: {:?}", - meshed_data.first().expect("Empty meshed data").vertices - ) - .into(), - &format!( - "Indices: {:?}", - meshed_data.first().expect("no indices").indices - ) - .into(), - ); // Send the triangular mesh to the event loop web_sys::console::log_1(&"Sending Mesh to event loop".into()); @@ -96,6 +86,9 @@ async fn load_data_async( set_active_requests.update(|n| *n -= 1); } +/// Converts the passed data into a triangular mesh using the earcutting algorithm, +/// this is done for a varying level of detail to allow for LODs, polygon simplification +/// is done using the Ramer-Douglas-Peucker algorithm fn mesh_data(data_exterior: Data) -> Vec { let positions: Vec>; let weights: Vec; diff --git a/heatmap-client/src/ingest/request.rs b/heatmap-client/src/ingest/request.rs index 83c348e..0c6d009 100644 --- a/heatmap-client/src/ingest/request.rs +++ b/heatmap-client/src/ingest/request.rs @@ -1,7 +1,10 @@ use heatmap_api::{HeatmapData, OutlineResponse}; +// Send a request to the service for data based on the filter pub async fn request(filter: heatmap_api::Filter) -> (HeatmapData, OutlineResponse) { let client = reqwest::Client::new(); + + // Send a POST request to the service with the filter as a json payload let data = client .post("http://localhost:8000/heatmap") // TODO, some configuration mechanism for this .json(&heatmap_api::HeatmapQuery { filter }) @@ -9,6 +12,7 @@ pub async fn request(filter: heatmap_api::Filter) -> (HeatmapData, OutlineRespon .await .expect("ERROR: Failed to recive data from post request"); + // Deserialize response into json string let str = data .text() .await @@ -16,9 +20,12 @@ pub async fn request(filter: heatmap_api::Filter) -> (HeatmapData, OutlineRespon web_sys::console::log_2(&"Data text: ".into(), &format!("{:?}", str).into()); + // Convert json string into a HeatmapData struct let json_data: heatmap_api::HeatmapData = serde_json::from_str(&str).expect("ERROR: Failed to deserialized json data"); + // Get the outline data from the service + // *** This should be broken out into its own function so we only get and mesh the world outline once *** let outline_data: OutlineResponse = serde_json::from_str( &client .get("http://localhost:8000/outline") diff --git a/heatmap-client/src/ui/user_interface.rs b/heatmap-client/src/ui/user_interface.rs index b61b617..929308d 100644 --- a/heatmap-client/src/ui/user_interface.rs +++ b/heatmap-client/src/ui/user_interface.rs @@ -23,8 +23,7 @@ pub fn UserInterface(set_filter: WriteSignal) -> impl IntoV let doc = document(); - // Might be worth reworking this, we are mixing web_sys and leptos here but weve done the same elsewhere so we could also just roll with it - // This closure is run when the submit button is pressed, it formats a filter string and sets a signal + // Run when an element of the UI changes, updates the filter signal let on_update = move |_: web_sys::Event| { let mut product_type = Vec::new(); @@ -73,7 +72,7 @@ pub fn UserInterface(set_filter: WriteSignal) -> impl IntoV } } - // Convert slider values into Dates + // Gets the selected start and end dates let start_date = start_date_element() .expect("failed to get start date value") .value(); @@ -81,8 +80,6 @@ pub fn UserInterface(set_filter: WriteSignal) -> impl IntoV .expect("failed to get end_date value") .value(); - web_sys::console::log_1(&start_date.clone().into()); - set_filter(heatmap_api::Filter { product_type, platform_type,