add adming page and run rustfmt
This commit is contained in:
7
shell.nix
Normal file
7
shell.nix
Normal file
@ -0,0 +1,7 @@
|
||||
let
|
||||
pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/refs/tags/25.11.tar.gz") {};
|
||||
in pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
openssl pkg-config sqlite
|
||||
];
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Player {
|
||||
pub username: String,
|
||||
@ -18,7 +18,7 @@ pub struct Game {
|
||||
pub code: String,
|
||||
pub teams: Vec<Team>,
|
||||
pub start_time: String,
|
||||
pub end_time: String
|
||||
pub end_time: String,
|
||||
}
|
||||
|
||||
impl Team {
|
||||
@ -35,5 +35,5 @@ impl Team {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct TeamLegend {
|
||||
pub name: String,
|
||||
pub color: String
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
86
src/db.rs
86
src/db.rs
@ -1,14 +1,16 @@
|
||||
use actix_web::{web, Error, error};
|
||||
use std::collections::HashMap;
|
||||
use crate::data::{Game, Player, Team};
|
||||
use actix_web::{Error, error, web};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub type Pool = r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>;
|
||||
pub type Connection = r2d2::PooledConnection<r2d2_sqlite::SqliteConnectionManager>;
|
||||
type GamesResult = Result<Vec<Game>, rusqlite::Error>;
|
||||
type TeamsResult = Result<HashMap<String,Team>, rusqlite::Error>;
|
||||
type TeamsResult = Result<HashMap<String, Team>, rusqlite::Error>;
|
||||
|
||||
const GAMES_QUERY: &str = "select code, group_concat(team), max(start_time), max(end_time) from games group by code";
|
||||
const TEAMS_QUERY: &str = "select game, team, min(color), group_concat(player) from teams group by game, team";
|
||||
const GAMES_QUERY: &str =
|
||||
"select code, group_concat(team), max(start_time), max(end_time) from games group by code";
|
||||
const TEAMS_QUERY: &str =
|
||||
"select game, team, min(color), group_concat(player) from teams group by game, team";
|
||||
|
||||
pub async fn games(pool: &Pool) -> Result<Vec<Game>, Error> {
|
||||
let pool = pool.clone();
|
||||
@ -16,30 +18,26 @@ pub async fn games(pool: &Pool) -> Result<Vec<Game>, Error> {
|
||||
let conn = web::block(move || pool.get())
|
||||
.await?
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
web::block(move || {
|
||||
get_all_games(conn, teams_result)
|
||||
})
|
||||
.await?
|
||||
.map_err(error::ErrorInternalServerError)
|
||||
web::block(move || get_all_games(conn, teams_result))
|
||||
.await?
|
||||
.map_err(error::ErrorInternalServerError)
|
||||
}
|
||||
|
||||
pub async fn teams(pool: &Pool) -> Result<HashMap<String,Team>, Error> {
|
||||
pub async fn teams(pool: &Pool) -> Result<HashMap<String, Team>, Error> {
|
||||
let pool = pool.clone();
|
||||
let conn = web::block(move || pool.get())
|
||||
.await?
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
web::block(move || {
|
||||
get_all_teams(conn)
|
||||
})
|
||||
.await?
|
||||
.map_err(error::ErrorInternalServerError)
|
||||
web::block(move || get_all_teams(conn))
|
||||
.await?
|
||||
.map_err(error::ErrorInternalServerError)
|
||||
}
|
||||
|
||||
fn get_all_games(conn: Connection, all_teams: HashMap<String,Team>) -> GamesResult {
|
||||
fn get_all_games(conn: Connection, all_teams: HashMap<String, Team>) -> GamesResult {
|
||||
let mut games_stmt = conn.prepare(GAMES_QUERY)?;
|
||||
games_stmt.query_map([], |games_row| {
|
||||
Ok(
|
||||
{
|
||||
games_stmt
|
||||
.query_map([], |games_row| {
|
||||
Ok({
|
||||
let game_name: String = games_row.get(0)?;
|
||||
let teams: String = games_row.get(1)?;
|
||||
let teams: Vec<Team> = teams
|
||||
@ -47,39 +45,41 @@ fn get_all_games(conn: Connection, all_teams: HashMap<String,Team>) -> GamesResu
|
||||
.map(|part| all_teams.get(&format!("{game_name}_{part}")).unwrap())
|
||||
.map(|part| part.to_owned())
|
||||
.collect();
|
||||
Game{
|
||||
Game {
|
||||
code: game_name,
|
||||
teams,
|
||||
start_time: games_row.get(2)?,
|
||||
end_time: games_row.get(3)?
|
||||
end_time: games_row.get(3)?,
|
||||
}
|
||||
}
|
||||
)
|
||||
}).and_then(Iterator::collect)
|
||||
})
|
||||
})
|
||||
.and_then(Iterator::collect)
|
||||
}
|
||||
|
||||
fn get_all_teams(conn: Connection) -> TeamsResult {
|
||||
let mut teams_stmt = conn.prepare(TEAMS_QUERY)?;
|
||||
teams_stmt.query_map([], |teams_row|{
|
||||
let game_name: String = teams_row.get(0)?;
|
||||
let team_name: String = teams_row.get(1)?;
|
||||
let color: String = teams_row.get(2)?;
|
||||
let key = format!("{game_name}_{team_name}");
|
||||
let players: String = teams_row.get(3)?;
|
||||
let players: Vec<Player> = players
|
||||
.split(',')
|
||||
.map(|part| Player{username: part.to_string()})
|
||||
.collect();
|
||||
Ok(
|
||||
(
|
||||
teams_stmt
|
||||
.query_map([], |teams_row| {
|
||||
let game_name: String = teams_row.get(0)?;
|
||||
let team_name: String = teams_row.get(1)?;
|
||||
let color: String = teams_row.get(2)?;
|
||||
let key = format!("{game_name}_{team_name}");
|
||||
let players: String = teams_row.get(3)?;
|
||||
let players: Vec<Player> = players
|
||||
.split(',')
|
||||
.map(|part| Player {
|
||||
username: part.to_string(),
|
||||
})
|
||||
.collect();
|
||||
Ok((
|
||||
key,
|
||||
Team{
|
||||
Team {
|
||||
name: team_name,
|
||||
color,
|
||||
players,
|
||||
scores: HashMap::new()
|
||||
}
|
||||
)
|
||||
)
|
||||
}).and_then(Iterator::collect)
|
||||
scores: HashMap::new(),
|
||||
},
|
||||
))
|
||||
})
|
||||
.and_then(Iterator::collect)
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
|
||||
use crate::Team;
|
||||
use geo as geo_types;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs;
|
||||
use geo::algorithm::contains::Contains;
|
||||
use geojson::{FeatureCollection, GeoJson, Geometry, Value};
|
||||
use crate::Team;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs;
|
||||
|
||||
pub struct GeoUtils {
|
||||
feature_collection: FeatureCollection
|
||||
feature_collection: FeatureCollection,
|
||||
}
|
||||
|
||||
impl GeoUtils {
|
||||
@ -16,11 +15,14 @@ impl GeoUtils {
|
||||
for feature in &mut fc.features {
|
||||
feature.set_property("color", "#888888ff");
|
||||
}
|
||||
Self {feature_collection: fc}
|
||||
Self {
|
||||
feature_collection: fc,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_feature_collection() -> FeatureCollection {
|
||||
let txt: String = fs::read_to_string("Neighborhood_Map_Atlas_Neighborhoods.geojson").expect("file should be present");
|
||||
let txt: String = fs::read_to_string("Neighborhood_Map_Atlas_Neighborhoods.geojson")
|
||||
.expect("file should be present");
|
||||
let geojson_str = txt;
|
||||
let geojson: GeoJson = geojson_str.parse::<GeoJson>().unwrap();
|
||||
FeatureCollection::try_from(geojson).unwrap()
|
||||
@ -30,7 +32,7 @@ impl GeoUtils {
|
||||
self.feature_collection.clone()
|
||||
}
|
||||
|
||||
pub fn get_colored_collection_copy(self, teams: Vec<Team>) -> FeatureCollection {
|
||||
pub fn get_colored_collection_copy(self, teams: Vec<Team>) -> FeatureCollection {
|
||||
let mut copy = self.feature_collection.clone();
|
||||
for feature in &mut copy.features {
|
||||
let name = feature.property("S_HOOD").unwrap_or_default();
|
||||
@ -38,7 +40,7 @@ impl GeoUtils {
|
||||
let mut max_team: (Option<String>, i32) = (None, 0);
|
||||
for team in &teams {
|
||||
let score = team.scores.get(name).unwrap_or(&0);
|
||||
if *score > max_team.1 {
|
||||
if *score > max_team.1 {
|
||||
max_team = (Some(team.color.clone()), *score);
|
||||
} else if *score > 0 && *score == max_team.1 {
|
||||
// Tie doesn't count
|
||||
@ -61,7 +63,7 @@ impl GeoUtils {
|
||||
let name = feature.property("S_HOOD").unwrap_or_default();
|
||||
let name = name.as_str().unwrap_or("unknown");
|
||||
let geometry: Geometry = feature.clone().geometry.expect("Feature has no geometry");
|
||||
|
||||
|
||||
match geometry.value {
|
||||
Value::Polygon(coords) => {
|
||||
// Outer ring
|
||||
@ -75,14 +77,17 @@ impl GeoUtils {
|
||||
}
|
||||
}
|
||||
Value::MultiPolygon(coords) => {
|
||||
let multipoly = coords.iter().map(|poly_coords| {
|
||||
let exterior = poly_coords[0]
|
||||
.iter()
|
||||
.map(|coord| (coord[0], coord[1]))
|
||||
.collect::<Vec<(f64, f64)>>()
|
||||
.into();
|
||||
geo_types::Polygon::new(exterior, vec![])
|
||||
}).collect::<Vec<geo_types::Polygon<f64>>>();
|
||||
let multipoly = coords
|
||||
.iter()
|
||||
.map(|poly_coords| {
|
||||
let exterior = poly_coords[0]
|
||||
.iter()
|
||||
.map(|coord| (coord[0], coord[1]))
|
||||
.collect::<Vec<(f64, f64)>>()
|
||||
.into();
|
||||
geo_types::Polygon::new(exterior, vec![])
|
||||
})
|
||||
.collect::<Vec<geo_types::Polygon<f64>>>();
|
||||
let multipolygon = geo_types::MultiPolygon::new(multipoly);
|
||||
if multipolygon.contains(&point) {
|
||||
return String::from(name);
|
||||
@ -94,4 +99,3 @@ impl GeoUtils {
|
||||
String::from("Outside")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
124
src/main.rs
124
src/main.rs
@ -1,8 +1,11 @@
|
||||
pub mod osm;
|
||||
mod data;
|
||||
mod geo_utils;
|
||||
mod db;
|
||||
mod geo_utils;
|
||||
pub mod osm;
|
||||
|
||||
use crate::data::Game;
|
||||
use crate::data::{Team, TeamLegend};
|
||||
use crate::osm::scores;
|
||||
use actix::ActorFutureExt;
|
||||
use actix::Addr;
|
||||
use actix::AtomicResponse;
|
||||
@ -11,23 +14,21 @@ use actix::Message;
|
||||
use actix::WrapFuture;
|
||||
use actix::dev::MessageResponse;
|
||||
use actix::dev::OneshotSender;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use actix::prelude::Actor;
|
||||
use actix_web::{App, HttpResponse, HttpServer, Responder, get, web};
|
||||
use chrono::NaiveDateTime;
|
||||
use db::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
use reqwest::Client;
|
||||
use std::error::Error;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tera::Context;
|
||||
use tera::Tera;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::data::Game;
|
||||
use crate::osm::scores;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use chrono::NaiveDateTime;
|
||||
use reqwest::Client;
|
||||
use xml::reader::EventReader;
|
||||
use actix_web::{get, App, web, HttpResponse, HttpServer, Responder};
|
||||
use crate::data::{Team, TeamLegend};
|
||||
use tokio::time;
|
||||
use std::time::Duration;
|
||||
use actix::prelude::Actor;
|
||||
use xml::reader::EventReader;
|
||||
|
||||
const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
|
||||
@ -36,12 +37,12 @@ const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
enum Messages {
|
||||
SynScores,
|
||||
UpdateScores,
|
||||
GetGames
|
||||
GetGames,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Responses {
|
||||
GamesResult(Vec<Game>)
|
||||
GamesResult(Vec<Game>),
|
||||
}
|
||||
|
||||
impl<A, M> MessageResponse<A, M> for Responses
|
||||
@ -58,7 +59,7 @@ where
|
||||
|
||||
#[derive(Default)]
|
||||
struct GamesActor {
|
||||
games: Arc<Mutex<Vec<Game>>>
|
||||
games: Arc<Mutex<Vec<Game>>>,
|
||||
}
|
||||
|
||||
impl Actor for GamesActor {
|
||||
@ -77,34 +78,45 @@ impl Handler<Messages> for GamesActor {
|
||||
// - update should only update games that are within 5 minutes of start or end time?
|
||||
match msg {
|
||||
Messages::SynScores => {
|
||||
let games: Vec<Game> = games.lock().await.iter().map(|v| v.to_owned()).collect();
|
||||
update_scores(games).await.expect("score calculation failed?")
|
||||
},
|
||||
let games: Vec<Game> =
|
||||
games.lock().await.iter().map(|v| v.to_owned()).collect();
|
||||
update_scores(games)
|
||||
.await
|
||||
.expect("score calculation failed?")
|
||||
}
|
||||
Messages::UpdateScores => {
|
||||
let games = games.lock().await.iter().map(|v| v.to_owned()).collect();
|
||||
update_scores(games).await.expect("score calculation failed?")
|
||||
},
|
||||
Messages::GetGames => {
|
||||
games.lock().await.iter().map(|v| v.to_owned()).collect()
|
||||
},
|
||||
update_scores(games)
|
||||
.await
|
||||
.expect("score calculation failed?")
|
||||
}
|
||||
Messages::GetGames => games.lock().await.iter().map(|v| v.to_owned()).collect(),
|
||||
}
|
||||
}
|
||||
.into_actor(self)
|
||||
.map(move |games: Vec<Game>, this, _| {
|
||||
this.games = Arc::new(Mutex::new(games.clone()));
|
||||
Responses::GamesResult(games)
|
||||
})
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/legend-data/{game_code}")]
|
||||
async fn legend_data(game_code: web::Path<String>, games: web::Data<Arc<Mutex<Vec<Game>>>>) -> impl Responder {
|
||||
async fn legend_data(
|
||||
game_code: web::Path<String>,
|
||||
games: web::Data<Arc<Mutex<Vec<Game>>>>,
|
||||
) -> impl Responder {
|
||||
let games = games.lock().await;
|
||||
if let Some(game) = games.iter().find(|g| g.code == *game_code) {
|
||||
let teams = &game.teams;
|
||||
let legends: Vec<TeamLegend> = teams.iter()
|
||||
.map(|t| TeamLegend{name: t.name.clone(), color:t.color.clone()}).collect();
|
||||
let legends: Vec<TeamLegend> = teams
|
||||
.iter()
|
||||
.map(|t| TeamLegend {
|
||||
name: t.name.clone(),
|
||||
color: t.color.clone(),
|
||||
})
|
||||
.collect();
|
||||
return HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(serde_json::to_string(&legends).unwrap());
|
||||
@ -115,9 +127,13 @@ async fn legend_data(game_code: web::Path<String>, games: web::Data<Arc<Mutex<Ve
|
||||
}
|
||||
|
||||
#[get("/geojson/{game_code}")]
|
||||
async fn geojson_endpoint(game_code: web::Path<String>, games_actor: web::Data<Addr<GamesActor>>) -> impl Responder {
|
||||
async fn geojson_endpoint(
|
||||
game_code: web::Path<String>,
|
||||
games_actor: web::Data<Addr<GamesActor>>,
|
||||
) -> impl Responder {
|
||||
if let Ok(Responses::GamesResult(games)) = games_actor.send(Messages::GetGames).await
|
||||
&& let Some(game) = games.iter().find(|g| g.code == *game_code) {
|
||||
&& let Some(game) = games.iter().find(|g| g.code == *game_code)
|
||||
{
|
||||
let teams = &game.teams;
|
||||
let utils = geo_utils::GeoUtils::new();
|
||||
let feature_collection = utils.get_colored_collection_copy(teams.to_vec());
|
||||
@ -130,7 +146,7 @@ async fn geojson_endpoint(game_code: web::Path<String>, games_actor: web::Data<A
|
||||
.body("{}")
|
||||
}
|
||||
|
||||
#[get("/{game_code}")]
|
||||
#[get("/game/{game_code}")]
|
||||
async fn game_page(game_code: web::Path<String>) -> impl Responder {
|
||||
let mut context = Context::new();
|
||||
context.insert("game_code", game_code.as_str());
|
||||
@ -139,6 +155,22 @@ async fn game_page(game_code: web::Path<String>) -> impl Responder {
|
||||
HttpResponse::Ok().body(body)
|
||||
}
|
||||
|
||||
#[get("/admin")]
|
||||
async fn admin() -> impl Responder {
|
||||
let mut context = Context::new();
|
||||
let body = Tera::one_off(include_str!("templates/admin.tera"), &context, false)
|
||||
.expect("Failed to render template");
|
||||
HttpResponse::Ok().body(body)
|
||||
}
|
||||
|
||||
#[get("/admin/games")]
|
||||
async fn list_of_games(games: web::Data<Arc<Mutex<Vec<Game>>>>) -> impl Responder {
|
||||
let games = games.lock().await;
|
||||
let games_json = serde_json::to_string(&*games).unwrap();
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(games_json)
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
@ -149,11 +181,14 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
let manager = SqliteConnectionManager::file("mapbattle.db");
|
||||
let pool = Pool::new(manager).unwrap();
|
||||
let games = db::games(&pool).await.expect("retirieving games from db should work fine");
|
||||
let games = db::games(&pool)
|
||||
.await
|
||||
.expect("retirieving games from db should work fine");
|
||||
let state = Arc::new(Mutex::new(games));
|
||||
let actor_addr = GamesActor{
|
||||
games: state.clone()
|
||||
}.start();
|
||||
let actor_addr = GamesActor {
|
||||
games: state.clone(),
|
||||
}
|
||||
.start();
|
||||
let sync_result = actor_addr.send(Messages::SynScores).await;
|
||||
match sync_result {
|
||||
Ok(_) => println!("Initial score syncing was successful"),
|
||||
@ -179,29 +214,32 @@ async fn main() -> std::io::Result<()> {
|
||||
.service(game_page)
|
||||
.service(geojson_endpoint)
|
||||
.service(legend_data)
|
||||
//.service(admin)
|
||||
//.service(list_of_games)
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_scores(games: Vec<Game>)-> Result<Vec<Game>, Box<dyn Error>> {
|
||||
let client = Client::builder()
|
||||
.user_agent("MapBattle/0.1")
|
||||
.build()?;
|
||||
async fn update_scores(games: Vec<Game>) -> Result<Vec<Game>, Box<dyn Error>> {
|
||||
let client = Client::builder().user_agent("MapBattle/0.1").build()?;
|
||||
|
||||
let mut result: Vec<Game> = vec![];
|
||||
for game in games.iter() {
|
||||
let mut neogame = game.clone();
|
||||
let teams: &mut Vec<Team> = &mut neogame.teams;
|
||||
let start_date = NaiveDateTime::parse_from_str(&game.start_time, DATE_FORMAT).expect("failed to parse start date");
|
||||
let end_date = NaiveDateTime::parse_from_str(&game.end_time, DATE_FORMAT).expect("failed to parse end date");
|
||||
let start_date = NaiveDateTime::parse_from_str(&game.start_time, DATE_FORMAT)
|
||||
.expect("failed to parse start date");
|
||||
let end_date = NaiveDateTime::parse_from_str(&game.end_time, DATE_FORMAT)
|
||||
.expect("failed to parse end date");
|
||||
|
||||
for team in teams {
|
||||
team.scores.clear();
|
||||
for player in team.players.clone() {
|
||||
println!("Processing player: {} ({})", player.username, team.name);
|
||||
let body = scores::get_changesets_for_user(player.username.clone(), &client).await?;
|
||||
let body =
|
||||
scores::get_changesets_for_user(player.username.clone(), &client).await?;
|
||||
let reader = EventReader::from_str(&body);
|
||||
let changesets = scores::extract_changesets_from_reader(reader);
|
||||
let changesets = scores::time_bound_changesets(changesets, start_date, end_date);
|
||||
|
||||
130
src/osm.rs
130
src/osm.rs
@ -7,32 +7,34 @@ mod network {
|
||||
}
|
||||
|
||||
pub async fn get_changesets(user: String, client: &Client) -> Result<String, Box<dyn Error>> {
|
||||
let endpoint = format!("https://api.openstreetmap.org/api/0.6/changesets?display_name={user}");
|
||||
let endpoint =
|
||||
format!("https://api.openstreetmap.org/api/0.6/changesets?display_name={user}");
|
||||
get_data(endpoint, client).await
|
||||
}
|
||||
}
|
||||
|
||||
pub mod scores {
|
||||
use crate::geo_utils;
|
||||
use crate::osm::network;
|
||||
use chrono::NaiveDateTime;
|
||||
use geo as geo_types;
|
||||
use reqwest::Client;
|
||||
use std::{collections::HashMap, error::Error};
|
||||
use xml::{attribute::OwnedAttribute, reader::{EventReader, XmlEvent}};
|
||||
use geo as geo_types;
|
||||
use crate::geo_utils;
|
||||
use xml::{
|
||||
attribute::OwnedAttribute,
|
||||
reader::{EventReader, XmlEvent},
|
||||
};
|
||||
const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
|
||||
pub struct Team {
|
||||
pub players: Vec<String>
|
||||
pub players: Vec<String>,
|
||||
}
|
||||
pub struct Input {
|
||||
pub start_time: NaiveDateTime,
|
||||
pub end_time: NaiveDateTime,
|
||||
pub teams: Vec<Team>
|
||||
pub teams: Vec<Team>,
|
||||
}
|
||||
pub async fn calculate(input: Input)-> Result<(), Box<dyn Error>> {
|
||||
let client = Client::builder()
|
||||
.user_agent("MapBattle/0.1")
|
||||
.build()?;
|
||||
pub async fn calculate(input: Input) -> Result<(), Box<dyn Error>> {
|
||||
let client = Client::builder().user_agent("MapBattle/0.1").build()?;
|
||||
for team in input.teams {
|
||||
for player in team.players {
|
||||
let raw_changesets = network::get_changesets(player.clone(), &client).await?;
|
||||
@ -47,77 +49,115 @@ pub mod scores {
|
||||
}
|
||||
|
||||
// Public function for getting changesets for a user
|
||||
pub async fn get_changesets_for_user(user: String, client: &Client) -> Result<String, Box<dyn Error>> {
|
||||
pub async fn get_changesets_for_user(
|
||||
user: String,
|
||||
client: &Client,
|
||||
) -> Result<String, Box<dyn Error>> {
|
||||
network::get_changesets(user, client).await
|
||||
}
|
||||
|
||||
// Public function for extracting changesets from reader
|
||||
pub fn extract_changesets_from_reader(reader: EventReader<&[u8]>) -> Vec<Vec<OwnedAttribute>> {
|
||||
reader.into_iter().filter_map(|event| match event {
|
||||
Ok(XmlEvent::StartElement { name, attributes, .. }) => {
|
||||
if name.local_name.eq("changeset") {
|
||||
Some(attributes)
|
||||
} else {
|
||||
reader
|
||||
.into_iter()
|
||||
.filter_map(|event| match event {
|
||||
Ok(XmlEvent::StartElement {
|
||||
name, attributes, ..
|
||||
}) => {
|
||||
if name.local_name.eq("changeset") {
|
||||
Some(attributes)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: XML parsing error: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Warning: XML parsing error: {}", e);
|
||||
None
|
||||
},
|
||||
_ => None
|
||||
}).collect()
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Public function for time bounding changesets
|
||||
pub fn time_bound_changesets(input: Vec<Vec<OwnedAttribute>>, start_date: NaiveDateTime, end_date: NaiveDateTime) -> Vec<Vec<OwnedAttribute>> {
|
||||
input.into_iter().filter(|list| {
|
||||
match list.iter().find(|att| att.name.local_name.eq("created_at")) {
|
||||
Some(created_at) => {
|
||||
match NaiveDateTime::parse_from_str(&created_at.value, DATE_FORMAT) {
|
||||
Ok(date) => date > start_date && date < end_date,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse date '{}': {}", created_at.value, e);
|
||||
false
|
||||
pub fn time_bound_changesets(
|
||||
input: Vec<Vec<OwnedAttribute>>,
|
||||
start_date: NaiveDateTime,
|
||||
end_date: NaiveDateTime,
|
||||
) -> Vec<Vec<OwnedAttribute>> {
|
||||
input
|
||||
.into_iter()
|
||||
.filter(
|
||||
|list| match list.iter().find(|att| att.name.local_name.eq("created_at")) {
|
||||
Some(created_at) => {
|
||||
match NaiveDateTime::parse_from_str(&created_at.value, DATE_FORMAT) {
|
||||
Ok(date) => date > start_date && date < end_date,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Failed to parse date '{}': {}",
|
||||
created_at.value, e
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => false
|
||||
}
|
||||
}).collect()
|
||||
None => false,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Public function for converting changesets to points
|
||||
pub async fn changesets_to_points(changesets: Vec<Vec<OwnedAttribute>>) -> HashMap<String, i32> {
|
||||
pub async fn changesets_to_points(
|
||||
changesets: Vec<Vec<OwnedAttribute>>,
|
||||
) -> HashMap<String, i32> {
|
||||
let mut result: HashMap<String, i32> = HashMap::new();
|
||||
for changeset in changesets {
|
||||
let min_lat = changeset.clone().into_iter().find(|att| att.name.local_name.eq("min_lat"));
|
||||
let min_lat = changeset
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|att| att.name.local_name.eq("min_lat"));
|
||||
if min_lat.is_none() {
|
||||
continue;
|
||||
}
|
||||
let min_lat = min_lat.unwrap().value;
|
||||
let min_lon = changeset.clone().into_iter().find(|att| att.name.local_name.eq("min_lon"));
|
||||
let min_lon = changeset
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|att| att.name.local_name.eq("min_lon"));
|
||||
if min_lon.is_none() {
|
||||
continue;
|
||||
}
|
||||
let min_lon = min_lon.unwrap().value;
|
||||
let min_lat: f64 = min_lat.parse().unwrap_or_default();
|
||||
let min_lon: f64 = min_lon.parse().unwrap_or_default();
|
||||
let score = changeset.into_iter().find(|att| att.name.local_name.eq("changes_count")).unwrap().value;
|
||||
let score:i32 = score.parse().expect("changes count returned as not a string");
|
||||
let score = changeset
|
||||
.into_iter()
|
||||
.find(|att| att.name.local_name.eq("changes_count"))
|
||||
.unwrap()
|
||||
.value;
|
||||
let score: i32 = score
|
||||
.parse()
|
||||
.expect("changes count returned as not a string");
|
||||
let point = geo_types::Point::new(min_lon, min_lat);
|
||||
let territory = geo_utils::GeoUtils::new().get_territory_for(point);
|
||||
result.entry(territory).and_modify(|val| *val += score).or_insert(score);
|
||||
result
|
||||
.entry(territory)
|
||||
.and_modify(|val| *val += score)
|
||||
.or_insert(score);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn time_bound(input: Vec<Vec<OwnedAttribute>>, start_date: NaiveDateTime, end_date: NaiveDateTime) -> Vec<Vec<OwnedAttribute>> {
|
||||
fn time_bound(
|
||||
input: Vec<Vec<OwnedAttribute>>,
|
||||
start_date: NaiveDateTime,
|
||||
end_date: NaiveDateTime,
|
||||
) -> Vec<Vec<OwnedAttribute>> {
|
||||
time_bound_changesets(input, start_date, end_date)
|
||||
}
|
||||
|
||||
async fn to_points(changesets: Vec<Vec<OwnedAttribute>>) -> HashMap<String, i32> {
|
||||
changesets_to_points(changesets).await
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
75
src/templates/admin.tera
Normal file
75
src/templates/admin.tera
Normal file
@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Admin Page</title>
|
||||
<style>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: 2px solid rgb(140 140 140);
|
||||
font-family: sans-serif;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
caption {
|
||||
caption-side: bottom;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
thead,
|
||||
tfoot {
|
||||
background-color: rgb(228 240 245);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 1px solid rgb(160 160 160);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>games</h1>
|
||||
<form id="games_form"><button type="submit">Load/Refresh</button></form>
|
||||
<table>
|
||||
<caption> Games table </caption>
|
||||
<thead><tr>
|
||||
<th scope="col">code</th>
|
||||
<th scope="col">team</th>
|
||||
<th scope="col">start_time</th>
|
||||
<th scope="col">end_time</th>
|
||||
</tr></thead>
|
||||
<tbody id="games_table"></tbody>
|
||||
</table>
|
||||
<script>
|
||||
const games_form = document.getElementById("games_form");
|
||||
function refresh_games(event) {
|
||||
event.preventDefault();
|
||||
fetch("/admin/games")
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
const table_body = document.getElementById("games_table");
|
||||
let table_content = "";
|
||||
data.forEach((item,index) => {
|
||||
table_content += '<tr>';
|
||||
table_content += '<th scope="row">' + item['code'] + '</th>';
|
||||
table_content += '<td>teams should go here</td>';
|
||||
table_content += '<td>' + item['start_time'] + '</td>';
|
||||
table_content += '<td>' + item['end_time'] + '</td>';
|
||||
});
|
||||
table_body.innerHTML = table_content;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Fetching legend data failed:", error);
|
||||
});
|
||||
}
|
||||
games_form.addEventListener("submit", refresh_games);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user