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:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -42,9 +42,11 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
|
||||
name = "analysis"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"colors-transform",
|
||||
"csdemo",
|
||||
"image",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"tracing",
|
||||
"tracing-test",
|
||||
]
|
||||
@@ -277,6 +279,7 @@ dependencies = [
|
||||
"analysis",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"common",
|
||||
"csdemo",
|
||||
@@ -284,6 +287,7 @@ dependencies = [
|
||||
"diesel-async",
|
||||
"diesel_async_migrations",
|
||||
"futures-util",
|
||||
"image",
|
||||
"memmap2",
|
||||
"reqwest 0.12.7",
|
||||
"serde",
|
||||
@@ -517,6 +521,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
|
||||
|
||||
[[package]]
|
||||
name = "colors-transform"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178"
|
||||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -6,7 +6,11 @@ edition = "2021"
|
||||
[dependencies]
|
||||
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
|
||||
tracing = { version = "0.1.4" }
|
||||
|
||||
image = { version = "0.25" }
|
||||
colors-transform = { version = "0.2" }
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { version = "1.4" }
|
||||
|
||||
@@ -2,6 +2,7 @@ pub struct Config {
|
||||
pub cell_size: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct HeatMap {
|
||||
max_x: 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 output = csdemo::parser::parse(
|
||||
csdemo::FrameIterator::parse(tmp.inner),
|
||||
@@ -64,6 +65,8 @@ pub fn parse(config: &Config, buf: &[u8]) -> Result<std::collections::HashMap<cs
|
||||
})
|
||||
.collect();
|
||||
|
||||
tracing::debug!("Pawn-IDs: {:?}", pawn_ids);
|
||||
|
||||
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_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(
|
||||
@@ -103,33 +121,20 @@ fn process_tick(
|
||||
.iter()
|
||||
.filter(|s| s.class == "CCSPlayerPawn")
|
||||
{
|
||||
let user_id_entry = entity_id_to_user.entry(entity_state.id);
|
||||
let user_id = match user_id_entry {
|
||||
std::collections::hash_map::Entry::Occupied(v) => v.into_mut(),
|
||||
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 user_id = match get_entityid(&entity_state.props) {
|
||||
Some(pawn_id) => {
|
||||
let user_id = pawn_ids.get(&pawn_id).cloned().unwrap();
|
||||
|
||||
let pawn_id: i32 = match &prop.value {
|
||||
csdemo::parser::Variant::U32(v) => *v as i32,
|
||||
other => panic!("Unexpected Variant: {:?}", other),
|
||||
};
|
||||
|
||||
Some(pawn_id)
|
||||
});
|
||||
|
||||
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),
|
||||
entity_id_to_user.insert(entity_state.id, user_id.clone());
|
||||
user_id.clone()
|
||||
}
|
||||
None => {
|
||||
match entity_id_to_user.get(&entity_state.id).cloned() {
|
||||
Some(user) => user,
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let _inner_guard =
|
||||
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),
|
||||
};
|
||||
|
||||
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() {
|
||||
Some(c) => c,
|
||||
@@ -162,7 +167,7 @@ fn process_tick(
|
||||
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!(y_coord >= 0.0);
|
||||
@@ -198,7 +203,7 @@ fn process_tick(
|
||||
|
||||
let lifestate = match n_lifestate {
|
||||
Some(state) => {
|
||||
player_lifestate.insert(*user_id, state);
|
||||
player_lifestate.insert(user_id, state);
|
||||
state
|
||||
}
|
||||
None => player_lifestate.get(&user_id).copied().unwrap_or(1),
|
||||
@@ -209,7 +214,7 @@ fn process_tick(
|
||||
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());
|
||||
heatmap.increment(x_cell, y_cell);
|
||||
@@ -233,13 +238,16 @@ impl core::fmt::Display for HeatMap {
|
||||
|
||||
impl HeatMap {
|
||||
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);
|
||||
|
||||
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() {
|
||||
for (x, cell) in row.iter().enumerate() {
|
||||
buffer.put_pixel(x as u32, y as u32, image::Rgb([*cell as u8, 0, 0]))
|
||||
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]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_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);
|
||||
for row in self.rows.iter_mut() {
|
||||
let _ = row.drain(0..min_x);
|
||||
|
||||
@@ -8,8 +8,8 @@ fn heatmap_nuke() {
|
||||
dbg!(path);
|
||||
let input_bytes = std::fs::read(path).unwrap();
|
||||
|
||||
let config = heatmap::Config { cell_size: 25.0 };
|
||||
let result = heatmap::parse(&config, &input_bytes).unwrap();
|
||||
let config = heatmap::Config { cell_size: 5.0 };
|
||||
let (result, players) = heatmap::parse(&config, &input_bytes).unwrap();
|
||||
|
||||
for (user, mut heatmap) in result {
|
||||
heatmap.shrink();
|
||||
|
||||
@@ -26,6 +26,9 @@ reqwest = { version = "0.12", features = ["json"] }
|
||||
common = { path = "../common/" }
|
||||
analysis = { path = "../analysis/" }
|
||||
|
||||
image = { version = "0.25" }
|
||||
base64 = { version = "0.22" }
|
||||
|
||||
csdemo = { package = "csdemo", git = "https://github.com/Lol3rrr/csdemo.git", ref = "main" }
|
||||
memmap2 = { version = "0.9" }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
@@ -63,7 +63,7 @@ pub async fn poll_next_task(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AnalysisInput {
|
||||
pub steamid: String,
|
||||
pub demoid: i64,
|
||||
@@ -120,3 +120,29 @@ pub fn analyse_base(input: AnalysisInput) -> BaseInfo {
|
||||
}).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()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub mod steam {
|
||||
) -> Result<axum::response::Redirect, axum::http::StatusCode> {
|
||||
let url = state.openid.get_redirect_url();
|
||||
|
||||
tracing::info!("Redirecting to {:?}", url);
|
||||
|
||||
Ok(axum::response::Redirect::to(url))
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ where
|
||||
.route("/:id/info", axum::routing::get(info))
|
||||
.route("/:id/analysis/scoreboard", axum::routing::get(scoreboard))
|
||||
.route("/:id/reanalyse", axum::routing::get(analyise))
|
||||
.route("/:id/analysis/heatmap", axum::routing::get(heatmap))
|
||||
.with_state(Arc::new(DemoState {
|
||||
upload_folder: upload_folder.into(),
|
||||
}))
|
||||
@@ -239,3 +240,43 @@ async fn scoreboard(
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ pub async fn run_api(
|
||||
"/api/",
|
||||
crate::api::router(crate::api::RouterConfig {
|
||||
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_path: "/api/steam/callback".into(),
|
||||
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 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
|
||||
.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 (player_info, player_stats): (Vec<_>, Vec<_>) = result
|
||||
let (player_info, player_stats): (Vec<_>, Vec<_>) = base_result
|
||||
.players
|
||||
.into_iter()
|
||||
.map(|(info, stats)| {
|
||||
@@ -133,9 +136,19 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
|
||||
})
|
||||
.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 {
|
||||
demo_id,
|
||||
map: result.map,
|
||||
map: base_result.map,
|
||||
};
|
||||
|
||||
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,
|
||||
)),
|
||||
));
|
||||
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 =
|
||||
diesel::dsl::update(crate::schema::processing_status::dsl::processing_status)
|
||||
.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_players_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?;
|
||||
Ok(())
|
||||
})
|
||||
@@ -191,7 +210,6 @@ pub async fn run_analysis(upload_folder: impl Into<std::path::PathBuf>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO
|
||||
// Remove task from queue
|
||||
tracing::info!("Stored analysis results");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ async fn main() {
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(tracing_subscriber::filter::filter_fn(|meta| {
|
||||
meta.target().contains("backend")
|
||||
|| meta.target().contains("analysis")
|
||||
}));
|
||||
tracing::subscriber::set_global_default(registry).unwrap();
|
||||
|
||||
|
||||
@@ -79,3 +79,12 @@ pub struct AnalysisTask {
|
||||
pub demo_id: i64,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
demo_heatmaps (demo_id, steam_id) {
|
||||
demo_id -> Int8,
|
||||
steam_id -> Text,
|
||||
data -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
demo_info (demo_id) {
|
||||
demo_id -> Int8,
|
||||
@@ -66,6 +74,7 @@ diesel::table! {
|
||||
}
|
||||
|
||||
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_player_stats -> 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!(
|
||||
analysis_queue,
|
||||
demo_heatmaps,
|
||||
demo_info,
|
||||
demo_player_stats,
|
||||
demo_players,
|
||||
|
||||
@@ -31,4 +31,10 @@ pub mod demo_analysis {
|
||||
pub damage: usize,
|
||||
pub assists: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PlayerHeatmap {
|
||||
pub name: String,
|
||||
pub png_data: String,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/main.css">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use leptos::*;
|
||||
use leptos_router::{Outlet, A};
|
||||
|
||||
pub mod heatmap;
|
||||
|
||||
#[leptos::component]
|
||||
pub fn demo() -> impl leptos::IntoView {
|
||||
let params = leptos_router::use_params_map();
|
||||
@@ -39,7 +41,7 @@ pub fn demo() -> impl leptos::IntoView {
|
||||
"Demo",
|
||||
.analysis_bar {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-columns: auto auto auto;
|
||||
column-gap: 20px;
|
||||
|
||||
background-color: #2d2d2d;
|
||||
@@ -68,6 +70,7 @@ pub fn demo() -> impl leptos::IntoView {
|
||||
<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() == "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>
|
||||
<Outlet/>
|
||||
|
||||
83
frontend/src/demo/heatmap.rs
Normal file
83
frontend/src/demo/heatmap.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ fn main() {
|
||||
<Route path="/demo/:id" view=Demo>
|
||||
<Route path="perround" view=frontend::demo::PerRound />
|
||||
<Route path="scoreboard" view=frontend::demo::Scoreboard />
|
||||
<Route path="heatmaps" view=frontend::demo::heatmap::Heatmaps />
|
||||
<Route path="" view=frontend::demo::Scoreboard />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
2
migrations/2024-09-28-132839_heatmap/down.sql
Normal file
2
migrations/2024-09-28-132839_heatmap/down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE demo_heatmaps;
|
||||
7
migrations/2024-09-28-132839_heatmap/up.sql
Normal file
7
migrations/2024-09-28-132839_heatmap/up.sql
Normal 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)
|
||||
);
|
||||
Reference in New Issue
Block a user