Add Heatmaps to UI

Add Heatmap analysis to website as well as a basic UI for viewing the Heatmaps.
There are still issues, like some players not getting a heatmap assigned and heatmaps including data
from warmup etc.
This commit is contained in:
Lol3rrr
2024-09-29 00:32:20 +02:00
parent 7f23f4882d
commit 83b4a24b15
19 changed files with 280 additions and 46 deletions

10
Cargo.lock generated
View File

@@ -42,9 +42,11 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
name = "analysis" name = "analysis"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"colors-transform",
"csdemo", "csdemo",
"image", "image",
"pretty_assertions", "pretty_assertions",
"serde",
"tracing", "tracing",
"tracing-test", "tracing-test",
] ]
@@ -277,6 +279,7 @@ dependencies = [
"analysis", "analysis",
"async-trait", "async-trait",
"axum", "axum",
"base64 0.22.1",
"clap", "clap",
"common", "common",
"csdemo", "csdemo",
@@ -284,6 +287,7 @@ dependencies = [
"diesel-async", "diesel-async",
"diesel_async_migrations", "diesel_async_migrations",
"futures-util", "futures-util",
"image",
"memmap2", "memmap2",
"reqwest 0.12.7", "reqwest 0.12.7",
"serde", "serde",
@@ -517,6 +521,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "colors-transform"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178"
[[package]] [[package]]
name = "common" name = "common"
version = "0.1.0" version = "0.1.0"

View File

@@ -6,7 +6,11 @@ edition = "2021"
[dependencies] [dependencies]
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" } csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
tracing = { version = "0.1.4" } tracing = { version = "0.1.4" }
image = { version = "0.25" } image = { version = "0.25" }
colors-transform = { version = "0.2" }
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
pretty_assertions = { version = "1.4" } pretty_assertions = { version = "1.4" }

View File

@@ -2,6 +2,7 @@ pub struct Config {
pub cell_size: f32, pub cell_size: f32,
} }
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct HeatMap { pub struct HeatMap {
max_x: usize, max_x: usize,
max_y: usize, max_y: usize,
@@ -41,7 +42,7 @@ impl HeatMap {
} }
} }
pub fn parse(config: &Config, buf: &[u8]) -> Result<std::collections::HashMap<csdemo::UserId, HeatMap>, ()> { pub fn parse(config: &Config, buf: &[u8]) -> Result<(std::collections::HashMap<csdemo::UserId, HeatMap>, std::collections::HashMap<csdemo::UserId, csdemo::parser::Player>), ()> {
let tmp = csdemo::Container::parse(buf).map_err(|e| ())?; let tmp = csdemo::Container::parse(buf).map_err(|e| ())?;
let output = csdemo::parser::parse( let output = csdemo::parser::parse(
csdemo::FrameIterator::parse(tmp.inner), csdemo::FrameIterator::parse(tmp.inner),
@@ -64,6 +65,8 @@ pub fn parse(config: &Config, buf: &[u8]) -> Result<std::collections::HashMap<cs
}) })
.collect(); .collect();
tracing::debug!("Pawn-IDs: {:?}", pawn_ids);
let mut entity_id_to_user = std::collections::HashMap::<i32, csdemo::UserId>::new(); let mut entity_id_to_user = std::collections::HashMap::<i32, csdemo::UserId>::new();
let mut player_lifestate = std::collections::HashMap::<csdemo::UserId, u32>::new(); let mut player_lifestate = std::collections::HashMap::<csdemo::UserId, u32>::new();
let mut player_position = std::collections::HashMap::<csdemo::UserId, (f32, f32, f32)>::new(); let mut player_position = std::collections::HashMap::<csdemo::UserId, (f32, f32, f32)>::new();
@@ -85,7 +88,22 @@ pub fn parse(config: &Config, buf: &[u8]) -> Result<std::collections::HashMap<cs
); );
} }
Ok(heatmaps) Ok((heatmaps, output.player_info))
}
fn get_entityid(props: &[csdemo::parser::entities::EntityProp]) -> Option<i32> {
props.iter().find_map(|prop| {
if prop.prop_info.prop_name.as_ref() != "CCSPlayerPawn.m_nEntityId" {
return None;
}
let pawn_id: i32 = match &prop.value {
csdemo::parser::Variant::U32(v) => *v as i32,
other => panic!("Unexpected Variant: {:?}", other),
};
Some(pawn_id)
})
} }
fn process_tick( fn process_tick(
@@ -103,33 +121,20 @@ fn process_tick(
.iter() .iter()
.filter(|s| s.class == "CCSPlayerPawn") .filter(|s| s.class == "CCSPlayerPawn")
{ {
let user_id_entry = entity_id_to_user.entry(entity_state.id); let user_id = match get_entityid(&entity_state.props) {
let user_id = match user_id_entry { Some(pawn_id) => {
std::collections::hash_map::Entry::Occupied(v) => v.into_mut(), let user_id = pawn_ids.get(&pawn_id).cloned().unwrap();
std::collections::hash_map::Entry::Vacant(v) => {
let pawn_id_prop: Option<i32> = entity_state.props.iter().find_map(|prop| {
if prop.prop_info.prop_name.as_ref() != "CCSPlayerPawn.m_nEntityId" {
return None;
}
let pawn_id: i32 = match &prop.value { entity_id_to_user.insert(entity_state.id, user_id.clone());
csdemo::parser::Variant::U32(v) => *v as i32, user_id.clone()
other => panic!("Unexpected Variant: {:?}", other), }
}; None => {
match entity_id_to_user.get(&entity_state.id).cloned() {
Some(pawn_id) Some(user) => user,
});
let user_id: Option<csdemo::UserId> = pawn_id_prop
.map(|pawn_id| pawn_ids.get(&pawn_id).cloned())
.flatten();
match user_id {
Some(user_id) => v.insert(user_id),
None => continue, None => continue,
} }
} }
}; };
let _inner_guard = let _inner_guard =
tracing::trace_span!("Entity", ?user_id, entity_id=?entity_state.id).entered(); tracing::trace_span!("Entity", ?user_id, entity_id=?entity_state.id).entered();
@@ -147,7 +152,7 @@ fn process_tick(
None => player_cells.get(&user_id).map(|(_, _, z)| *z).unwrap_or(0), None => player_cells.get(&user_id).map(|(_, _, z)| *z).unwrap_or(0),
}; };
player_cells.insert(*user_id, (x_cell, y_cell, z_cell)); player_cells.insert(user_id, (x_cell, y_cell, z_cell));
let x_coord = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_vecX").map(|prop| prop.value.as_f32()).flatten() { let x_coord = match entity_state.get_prop("CCSPlayerPawn.CBodyComponentBaseAnimGraph.m_vecX").map(|prop| prop.value.as_f32()).flatten() {
Some(c) => c, Some(c) => c,
@@ -162,7 +167,7 @@ fn process_tick(
None => player_position.get(&user_id).map(|(_, _, z)| *z).unwrap_or(0.0), None => player_position.get(&user_id).map(|(_, _, z)| *z).unwrap_or(0.0),
}; };
player_position.insert(*user_id, (x_coord, y_coord, z_coord)); player_position.insert(user_id, (x_coord, y_coord, z_coord));
assert!(x_coord >= 0.0); assert!(x_coord >= 0.0);
assert!(y_coord >= 0.0); assert!(y_coord >= 0.0);
@@ -198,7 +203,7 @@ fn process_tick(
let lifestate = match n_lifestate { let lifestate = match n_lifestate {
Some(state) => { Some(state) => {
player_lifestate.insert(*user_id, state); player_lifestate.insert(user_id, state);
state state
} }
None => player_lifestate.get(&user_id).copied().unwrap_or(1), None => player_lifestate.get(&user_id).copied().unwrap_or(1),
@@ -209,7 +214,7 @@ fn process_tick(
continue; continue;
} }
tracing::trace!("Coord (X, Y, Z): {:?} -> {:?}", (x_coord, y_coord, z_coord), (x_cell, y_cell)); // tracing::trace!("Coord (X, Y, Z): {:?} -> {:?}", (x_coord, y_coord, z_coord), (x_cell, y_cell));
let heatmap = heatmaps.entry(user_id.clone()).or_insert(HeatMap::new()); let heatmap = heatmaps.entry(user_id.clone()).or_insert(HeatMap::new());
heatmap.increment(x_cell, y_cell); heatmap.increment(x_cell, y_cell);
@@ -233,13 +238,16 @@ impl core::fmt::Display for HeatMap {
impl HeatMap { impl HeatMap {
pub fn as_image(&self) -> image::RgbImage { pub fn as_image(&self) -> image::RgbImage {
use colors_transform::Color;
let mut buffer = image::RgbImage::new(self.max_x as u32 + 1, self.max_y as u32 + 1); let mut buffer = image::RgbImage::new(self.max_x as u32 + 1, self.max_y as u32 + 1);
tracing::trace!("Creating Image with Dimensions: {}x{}", buffer.width(), buffer.height()); for (y, row) in self.rows.iter().rev().enumerate() {
for (x, cell) in row.iter().copied().chain(core::iter::repeat(0)).enumerate().take(self.max_x) {
let scaled = (1.0/(1.0 + (cell as f32))) * 240.0;
let raw_rgb = colors_transform::Hsl::from(scaled, 100.0, 50.0).to_rgb();
for (y, row) in self.rows.iter().enumerate() { buffer.put_pixel(x as u32, y as u32, image::Rgb([raw_rgb.get_red() as u8, raw_rgb.get_green() as u8, raw_rgb.get_blue() as u8]))
for (x, cell) in row.iter().enumerate() {
buffer.put_pixel(x as u32, y as u32, image::Rgb([*cell as u8, 0, 0]))
} }
} }
@@ -250,8 +258,6 @@ impl HeatMap {
let min_x = self.rows.iter().filter_map(|row| row.iter().enumerate().filter(|(_, v)| **v != 0).map(|(i, _)| i).next()).min().unwrap_or(0); let min_x = self.rows.iter().filter_map(|row| row.iter().enumerate().filter(|(_, v)| **v != 0).map(|(i, _)| i).next()).min().unwrap_or(0);
let min_y = self.rows.iter().enumerate().filter(|(y, row)| row.iter().any(|v| *v != 0)).map(|(i, _)| i).min().unwrap_or(0); let min_y = self.rows.iter().enumerate().filter(|(y, row)| row.iter().any(|v| *v != 0)).map(|(i, _)| i).min().unwrap_or(0);
tracing::trace!("Truncate to Min-X: {} - Min-Y: {}", min_x, min_y);
let _ = self.rows.drain(0..min_y); let _ = self.rows.drain(0..min_y);
for row in self.rows.iter_mut() { for row in self.rows.iter_mut() {
let _ = row.drain(0..min_x); let _ = row.drain(0..min_x);

View File

@@ -8,8 +8,8 @@ fn heatmap_nuke() {
dbg!(path); dbg!(path);
let input_bytes = std::fs::read(path).unwrap(); let input_bytes = std::fs::read(path).unwrap();
let config = heatmap::Config { cell_size: 25.0 }; let config = heatmap::Config { cell_size: 5.0 };
let result = heatmap::parse(&config, &input_bytes).unwrap(); let (result, players) = heatmap::parse(&config, &input_bytes).unwrap();
for (user, mut heatmap) in result { for (user, mut heatmap) in result {
heatmap.shrink(); heatmap.shrink();

View File

@@ -26,6 +26,9 @@ reqwest = { version = "0.12", features = ["json"] }
common = { path = "../common/" } common = { path = "../common/" }
analysis = { path = "../analysis/" } analysis = { path = "../analysis/" }
image = { version = "0.25" }
base64 = { version = "0.22" }
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" } csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
memmap2 = { version = "0.9" } memmap2 = { version = "0.9" }
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }

View File

@@ -63,7 +63,7 @@ pub async fn poll_next_task(
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct AnalysisInput { pub struct AnalysisInput {
pub steamid: String, pub steamid: String,
pub demoid: i64, pub demoid: i64,
@@ -120,3 +120,29 @@ pub fn analyse_base(input: AnalysisInput) -> BaseInfo {
}).collect() }).collect()
} }
} }
#[tracing::instrument(skip(input))]
pub fn analyse_heatmap(input: AnalysisInput) -> std::collections::HashMap<String, analysis::heatmap::HeatMap> {
tracing::info!("Generating HEATMAPs");
let file = std::fs::File::open(&input.path).unwrap();
let mmap = unsafe { memmap2::MmapOptions::new().map(&file).unwrap() };
let config = analysis::heatmap::Config {
cell_size: 5.0,
};
let (heatmaps, players) = analysis::heatmap::parse(&config, &mmap).unwrap();
tracing::info!("Got {} Heatmaps", heatmaps.len());
heatmaps.into_iter().filter_map(|(userid, heatmap)| {
let player = match players.get(&userid) {
Some(p) => p,
None => {
tracing::warn!("Could not find player: {:?}", userid);
return None;
}
};
Some((player.xuid.to_string(), heatmap))
}).collect()
}

View File

@@ -40,6 +40,8 @@ pub mod steam {
) -> Result<axum::response::Redirect, axum::http::StatusCode> { ) -> Result<axum::response::Redirect, axum::http::StatusCode> {
let url = state.openid.get_redirect_url(); let url = state.openid.get_redirect_url();
tracing::info!("Redirecting to {:?}", url);
Ok(axum::response::Redirect::to(url)) Ok(axum::response::Redirect::to(url))
} }

View File

@@ -22,6 +22,7 @@ where
.route("/:id/info", axum::routing::get(info)) .route("/:id/info", axum::routing::get(info))
.route("/:id/analysis/scoreboard", axum::routing::get(scoreboard)) .route("/:id/analysis/scoreboard", axum::routing::get(scoreboard))
.route("/:id/reanalyse", axum::routing::get(analyise)) .route("/:id/reanalyse", axum::routing::get(analyise))
.route("/:id/analysis/heatmap", axum::routing::get(heatmap))
.with_state(Arc::new(DemoState { .with_state(Arc::new(DemoState {
upload_folder: upload_folder.into(), upload_folder: upload_folder.into(),
})) }))
@@ -239,3 +240,43 @@ async fn scoreboard(
team2, team2,
})) }))
} }
#[tracing::instrument(skip(session))]
async fn heatmap(
session: UserSession,
Path(demo_id): Path<i64>,
) -> Result<axum::response::Json<Vec<common::demo_analysis::PlayerHeatmap>>, axum::http::StatusCode> {
use base64::prelude::Engine;
let query = crate::schema::demo_players::dsl::demo_players
.inner_join(crate::schema::demo_heatmaps::dsl::demo_heatmaps.on(
crate::schema::demo_players::dsl::steam_id.eq(crate::schema::demo_heatmaps::dsl::steam_id)
.and(crate::schema::demo_players::dsl::demo_id.eq(crate::schema::demo_heatmaps::dsl::demo_id))
)).filter(crate::schema::demo_players::dsl::demo_id.eq(demo_id));
let mut db_con = crate::db_connection().await;
let result: Vec<(crate::models::DemoPlayer, crate::models::DemoPlayerHeatmap)> = match query.load(&mut db_con).await {
Ok(d) => d,
Err(e) => {
tracing::error!("Querying DB: {:?}", e);
return Err(axum::http::StatusCode::INTERNAL_SERVER_ERROR);;
}
};
let data: Vec<common::demo_analysis::PlayerHeatmap> = result.into_iter().map(|(player, heatmap)| {
let mut heatmap: analysis::heatmap::HeatMap = serde_json::from_str(&heatmap.data).unwrap();
heatmap.shrink();
let h_image = heatmap.as_image();
let mut buffer = std::io::Cursor::new(Vec::new());
h_image.write_to(&mut buffer, image::ImageFormat::Png).unwrap();
common::demo_analysis::PlayerHeatmap {
name: player.name,
png_data: base64::prelude::BASE64_STANDARD.encode(buffer.into_inner()),
}
}).collect();
Ok(axum::Json(data))
}

View File

@@ -66,7 +66,7 @@ pub async fn run_api(
"/api/", "/api/",
crate::api::router(crate::api::RouterConfig { crate::api::router(crate::api::RouterConfig {
steam_api_key: steam_api_key.into(), steam_api_key: steam_api_key.into(),
steam_callback_base_url: "http://192.168.0.156:3000".into(), steam_callback_base_url: "http://localhost:3000".into(),
// steam_callback_base_url: "http://localhost:3000".into(), // steam_callback_base_url: "http://localhost:3000".into(),
steam_callback_path: "/api/steam/callback".into(), steam_callback_path: "/api/steam/callback".into(),
upload_dir: upload_folder.clone(), upload_dir: upload_folder.clone(),
@@ -101,15 +101,18 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
let demo_id = input.demoid; let demo_id = input.demoid;
let result = tokio::task::spawn_blocking(move || crate::analysis::analyse_base(input)) let base_input = input.clone();
let base_result = tokio::task::spawn_blocking(move || crate::analysis::analyse_base(base_input))
.await .await
.unwrap(); .unwrap();
tracing::debug!("Analysis-Result: {:?}", result); let heatmap_result = tokio::task::spawn_blocking(move || crate::analysis::analyse_heatmap(input))
.await
.unwrap();
let mut db_con = crate::db_connection().await; let mut db_con = crate::db_connection().await;
let (player_info, player_stats): (Vec<_>, Vec<_>) = result let (player_info, player_stats): (Vec<_>, Vec<_>) = base_result
.players .players
.into_iter() .into_iter()
.map(|(info, stats)| { .map(|(info, stats)| {
@@ -133,9 +136,19 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
}) })
.unzip(); .unzip();
let player_heatmaps: Vec<_> = heatmap_result.into_iter().map(|(player, heatmap)| {
tracing::trace!("HeatMap for Player: {:?}", player);
crate::models::DemoPlayerHeatmap {
demo_id,
steam_id: player,
data: serde_json::to_string(&heatmap).unwrap(),
}
}).collect();
let demo_info = crate::models::DemoInfo { let demo_info = crate::models::DemoInfo {
demo_id, demo_id,
map: result.map, map: base_result.map,
}; };
let store_demo_info_query = let store_demo_info_query =
@@ -173,6 +186,11 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
crate::schema::demo_player_stats::dsl::damage, crate::schema::demo_player_stats::dsl::damage,
)), )),
)); ));
let store_demo_player_heatmaps_query = diesel::dsl::insert_into(crate::schema::demo_heatmaps::dsl::demo_heatmaps)
.values(player_heatmaps)
.on_conflict((crate::schema::demo_heatmaps::dsl::demo_id, crate::schema::demo_heatmaps::dsl::steam_id))
.do_update()
.set((crate::schema::demo_heatmaps::dsl::data.eq(diesel::upsert::excluded(crate::schema::demo_heatmaps::dsl::data))));
let update_process_info = let update_process_info =
diesel::dsl::update(crate::schema::processing_status::dsl::processing_status) diesel::dsl::update(crate::schema::processing_status::dsl::processing_status)
.set(crate::schema::processing_status::dsl::info.eq(1)) .set(crate::schema::processing_status::dsl::info.eq(1))
@@ -184,6 +202,7 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
store_demo_info_query.execute(conn).await?; store_demo_info_query.execute(conn).await?;
store_demo_players_query.execute(conn).await?; store_demo_players_query.execute(conn).await?;
store_demo_player_stats_query.execute(conn).await?; store_demo_player_stats_query.execute(conn).await?;
store_demo_player_heatmaps_query.execute(conn).await?;
update_process_info.execute(conn).await?; update_process_info.execute(conn).await?;
Ok(()) Ok(())
}) })
@@ -191,7 +210,6 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
.await .await
.unwrap(); .unwrap();
// TODO tracing::info!("Stored analysis results");
// Remove task from queue
} }
} }

View File

@@ -27,6 +27,7 @@ async fn main() {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::filter::filter_fn(|meta| { .with(tracing_subscriber::filter::filter_fn(|meta| {
meta.target().contains("backend") meta.target().contains("backend")
|| meta.target().contains("analysis")
})); }));
tracing::subscriber::set_global_default(registry).unwrap(); tracing::subscriber::set_global_default(registry).unwrap();

View File

@@ -79,3 +79,12 @@ pub struct AnalysisTask {
pub demo_id: i64, pub demo_id: i64,
pub steam_id: String, pub steam_id: String,
} }
#[derive(Queryable, Selectable, Insertable, Debug)]
#[diesel(table_name = crate::schema::demo_heatmaps)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct DemoPlayerHeatmap {
pub demo_id: i64,
pub steam_id: String,
pub data: String,
}

View File

@@ -8,6 +8,14 @@ diesel::table! {
} }
} }
diesel::table! {
demo_heatmaps (demo_id, steam_id) {
demo_id -> Int8,
steam_id -> Text,
data -> Text,
}
}
diesel::table! { diesel::table! {
demo_info (demo_id) { demo_info (demo_id) {
demo_id -> Int8, demo_id -> Int8,
@@ -66,6 +74,7 @@ diesel::table! {
} }
diesel::joinable!(analysis_queue -> demos (demo_id)); diesel::joinable!(analysis_queue -> demos (demo_id));
diesel::joinable!(demo_heatmaps -> demo_info (demo_id));
diesel::joinable!(demo_info -> demos (demo_id)); diesel::joinable!(demo_info -> demos (demo_id));
diesel::joinable!(demo_player_stats -> demo_info (demo_id)); diesel::joinable!(demo_player_stats -> demo_info (demo_id));
diesel::joinable!(demo_players -> demo_info (demo_id)); diesel::joinable!(demo_players -> demo_info (demo_id));
@@ -73,6 +82,7 @@ diesel::joinable!(processing_status -> demos (demo_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
analysis_queue, analysis_queue,
demo_heatmaps,
demo_info, demo_info,
demo_player_stats, demo_player_stats,
demo_players, demo_players,

View File

@@ -31,4 +31,10 @@ pub mod demo_analysis {
pub damage: usize, pub damage: usize,
pub assists: usize, pub assists: usize,
} }
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PlayerHeatmap {
pub name: String,
pub png_data: String,
}
} }

View File

@@ -2,6 +2,8 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="/main.css"> <link rel="stylesheet" href="/main.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> </head>
<body></body> <body></body>
</html> </html>

View File

@@ -1,6 +1,8 @@
use leptos::*; use leptos::*;
use leptos_router::{Outlet, A}; use leptos_router::{Outlet, A};
pub mod heatmap;
#[leptos::component] #[leptos::component]
pub fn demo() -> impl leptos::IntoView { pub fn demo() -> impl leptos::IntoView {
let params = leptos_router::use_params_map(); let params = leptos_router::use_params_map();
@@ -39,7 +41,7 @@ pub fn demo() -> impl leptos::IntoView {
"Demo", "Demo",
.analysis_bar { .analysis_bar {
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto auto auto;
column-gap: 20px; column-gap: 20px;
background-color: #2d2d2d; background-color: #2d2d2d;
@@ -68,6 +70,7 @@ pub fn demo() -> impl leptos::IntoView {
<div class="analysis_bar"> <div class="analysis_bar">
<div class="analysis_selector" class:current=move || selected_tab() == "scoreboard"><A href="scoreboard"><span>Scoreboard</span></A></div> <div class="analysis_selector" class:current=move || selected_tab() == "scoreboard"><A href="scoreboard"><span>Scoreboard</span></A></div>
<div class="analysis_selector" class:current=move || selected_tab() == "perround"><A href="perround"><span>Per Round</span></A></div> <div class="analysis_selector" class:current=move || selected_tab() == "perround"><A href="perround"><span>Per Round</span></A></div>
<div class="analysis_selector" class:current=move || selected_tab() == "heatmaps"><A href="heatmaps"><span>Heatmaps</span></A></div>
</div> </div>
<div> <div>
<Outlet/> <Outlet/>

View File

@@ -0,0 +1,83 @@
use leptos::*;
#[leptos::component]
pub fn heatmaps() -> impl leptos::IntoView {
let heatmaps_resource =
create_resource(leptos_router::use_params_map(), |params| async move {
let id = params.get("id").unwrap();
let res =
reqwasm::http::Request::get(&format!("/api/demos/{}/analysis/heatmap", id))
.send()
.await
.unwrap();
res.json::<Vec<common::demo_analysis::PlayerHeatmap>>()
.await
.unwrap()
});
view! {
<h3>Heatmaps</h3>
<Suspense fallback=move || view! { <p>Loading Heatmaps</p> }>
<div>
{
move || {
heatmaps_resource.get().map(|h| {
view! { <HeatmapView heatmaps=h /> }
})
}
}
</div>
</Suspense>
}
}
#[leptos::component]
fn heatmap_view(heatmaps: Vec<common::demo_analysis::PlayerHeatmap>) -> impl leptos::IntoView {
let (idx, set_idx) = create_signal(0usize);
let (value, set_value) = create_signal(None::<common::demo_analysis::PlayerHeatmap>);
let h1 = heatmaps.clone();
let style = stylers::style! {
"Heatmap-View",
img {
width: 75vw;
height: 75vw;
display: block;
}
};
view! {
class=style,
<div>
<select on:change=move |ev| {
let new_value = event_target_value(&ev);
let idx: usize = new_value.parse().unwrap();
set_value(heatmaps.get(idx).cloned());
set_idx(idx);
} prop:value=move || idx.get().to_string()>
{ (move |heatmaps: Vec<common::demo_analysis::PlayerHeatmap>| {
heatmaps.iter().enumerate().map(|(idx, heatmap)| {
view! {
<option value={idx}>{heatmap.name.clone()}</option>
}
}).collect::<Vec<_>>()
})(h1.clone())}
</select>
<br />
{
move || {
match value.get() {
Some(heatmap) => view! {
<img class="heatmap_img" src=format!("data:image/jpeg;base64,{}", heatmap.png_data) />
}.into_any(),
None => view! { <p>ERROR</p> }.into_any(),
}
}
}
</div>
}
}

View File

@@ -32,6 +32,7 @@ fn main() {
<Route path="/demo/:id" view=Demo> <Route path="/demo/:id" view=Demo>
<Route path="perround" view=frontend::demo::PerRound /> <Route path="perround" view=frontend::demo::PerRound />
<Route path="scoreboard" view=frontend::demo::Scoreboard /> <Route path="scoreboard" view=frontend::demo::Scoreboard />
<Route path="heatmaps" view=frontend::demo::heatmap::Heatmaps />
<Route path="" view=frontend::demo::Scoreboard /> <Route path="" view=frontend::demo::Scoreboard />
</Route> </Route>
</Routes> </Routes>

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE demo_heatmaps;

View File

@@ -0,0 +1,7 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS demo_heatmaps (
demo_id bigint REFERENCES demo_info(demo_id),
steam_id TEXT NOT NULL,
data TEXT NOT NULL,
PRIMARY KEY (demo_id, steam_id)
);