load scores every 5 minutes and display a colored map
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,6 +1,3 @@
|
||||
/target
|
||||
*~
|
||||
player_scores.csv
|
||||
team_scores.csv
|
||||
territory_breakdown.csv
|
||||
players.csv
|
||||
scores.json
|
||||
8
map.html
8
map.html
@ -12,13 +12,7 @@
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
function style(feature) {
|
||||
// Customize color based on a feature property; e.g., 'type' or 'category'
|
||||
switch(feature.properties.type) {
|
||||
case 'park': return { color: '#228B22' }; // green
|
||||
case 'water': return { color: '#1E90FF' }; // blue
|
||||
case 'residential': return { color: '#FFD700' }; // gold
|
||||
default: return { color: '#FF0000' }; // red fallback
|
||||
}
|
||||
return { color: feature.properties.color};
|
||||
}
|
||||
var map = L.map('map').setView([47.6062, -122.3321], 12);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
|
||||
322
src/csv_io.rs
322
src/csv_io.rs
@ -1,322 +0,0 @@
|
||||
use std::{collections::{HashMap, HashSet}, error::Error};
|
||||
use csv::{Reader, Writer};
|
||||
use crate::data::{Player, PlayerScore, TeamScore};
|
||||
|
||||
/// Load players from a CSV file
|
||||
/// Expected format: username,team
|
||||
pub fn load_players(file_path: &str) -> Result<Vec<Player>, Box<dyn Error>> {
|
||||
let mut reader = Reader::from_path(file_path)?;
|
||||
let mut players = Vec::new();
|
||||
|
||||
for result in reader.deserialize() {
|
||||
let player: Player = result?;
|
||||
players.push(player);
|
||||
}
|
||||
|
||||
Ok(players)
|
||||
}
|
||||
|
||||
/// Write player scores to a CSV file
|
||||
pub fn write_player_scores(
|
||||
file_path: &str,
|
||||
player_scores: &[PlayerScore],
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut writer = Writer::from_path(file_path)?;
|
||||
|
||||
// Write header
|
||||
writer.write_record(&["username", "team", "total_score"])?;
|
||||
|
||||
// Write data
|
||||
for score in player_scores {
|
||||
writer.write_record(&[
|
||||
&score.player.username,
|
||||
&score.player.team,
|
||||
&score.total_score.to_string(),
|
||||
])?;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write team scores to a CSV file
|
||||
pub fn write_team_scores(
|
||||
file_path: &str,
|
||||
team_scores: &[TeamScore],
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut writer = Writer::from_path(file_path)?;
|
||||
let mut territories: HashSet<String> = HashSet::new();
|
||||
for score in team_scores.iter() {
|
||||
for (territory, _) in score.territory_scores.clone() {
|
||||
territories.insert(territory);
|
||||
}
|
||||
}
|
||||
let territories: Vec<String> = territories.into_iter().collect();
|
||||
|
||||
// Write header
|
||||
let mut header = vec![String::from("team")];
|
||||
header.append(&mut territories.clone());
|
||||
writer.write_record(&header)?;
|
||||
|
||||
// Write data
|
||||
for score in team_scores {
|
||||
let mut scores = vec![score.team.clone()];
|
||||
let default = 0;
|
||||
for territory in territories.clone() {
|
||||
scores.push(score.territory_scores.get(&territory).unwrap_or(&default).to_string());
|
||||
}
|
||||
writer.write_record(scores)?;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write detailed territory scores to a CSV file
|
||||
pub fn write_territory_scores(
|
||||
file_path: &str,
|
||||
player_scores: &[PlayerScore],
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut writer = Writer::from_path(file_path)?;
|
||||
|
||||
// Collect all unique territories
|
||||
let mut territories: Vec<String> = player_scores
|
||||
.iter()
|
||||
.flat_map(|ps| ps.territory_scores.keys())
|
||||
.cloned()
|
||||
.collect::<std::collections::HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
territories.sort();
|
||||
|
||||
// Write header
|
||||
let mut header = vec!["username".to_string(), "team".to_string()];
|
||||
header.extend(territories.iter().cloned());
|
||||
header.push("total".to_string());
|
||||
writer.write_record(&header)?;
|
||||
|
||||
// Write data
|
||||
for score in player_scores {
|
||||
let mut row = vec![score.player.username.clone(), score.player.team.clone()];
|
||||
|
||||
for territory in &territories {
|
||||
let territory_score = score.territory_scores.get(territory).unwrap_or(&0);
|
||||
row.push(territory_score.to_string());
|
||||
}
|
||||
|
||||
row.push(score.total_score.to_string());
|
||||
writer.write_record(&row)?;
|
||||
}
|
||||
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calculate team scores from player scores
|
||||
pub fn calculate_team_scores(player_scores: &[PlayerScore]) -> Vec<TeamScore> {
|
||||
let mut team_map: HashMap<String, TeamScore> = HashMap::new();
|
||||
|
||||
for player_score in player_scores {
|
||||
let team_score = team_map
|
||||
.entry(player_score.player.team.clone())
|
||||
.or_insert_with(|| TeamScore::new(player_score.player.team.clone()));
|
||||
|
||||
for (territory, score) in &player_score.territory_scores {
|
||||
team_score.add_territory_score(territory.clone(), *score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut teams: Vec<TeamScore> = team_map.into_values().collect();
|
||||
teams.sort_by(|a, b| b.total_score.cmp(&a.total_score));
|
||||
teams
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_load_players() {
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
writeln!(temp_file, "username,team").unwrap();
|
||||
writeln!(temp_file, "player1,Red Team").unwrap();
|
||||
writeln!(temp_file, "player2,Blue Team").unwrap();
|
||||
writeln!(temp_file, "player3,Red Team").unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let players = load_players(temp_file.path().to_str().unwrap()).unwrap();
|
||||
|
||||
assert_eq!(players.len(), 3);
|
||||
assert_eq!(players[0].username, "player1");
|
||||
assert_eq!(players[0].team, "Red Team");
|
||||
assert_eq!(players[1].username, "player2");
|
||||
assert_eq!(players[1].team, "Blue Team");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_player_scores() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
|
||||
let player1 = Player {
|
||||
username: "player1".to_string(),
|
||||
team: "Red Team".to_string(),
|
||||
};
|
||||
let mut score1 = PlayerScore::new(player1);
|
||||
score1.add_territory_score("Capitol Hill".to_string(), 10);
|
||||
|
||||
let player2 = Player {
|
||||
username: "player2".to_string(),
|
||||
team: "Blue Team".to_string(),
|
||||
};
|
||||
let mut score2 = PlayerScore::new(player2);
|
||||
score2.add_territory_score("Downtown".to_string(), 5);
|
||||
|
||||
let player_scores = vec![score1, score2];
|
||||
|
||||
write_player_scores(file_path, &player_scores).unwrap();
|
||||
|
||||
let content = fs::read_to_string(file_path).unwrap();
|
||||
assert!(content.contains("username,team,total_score"));
|
||||
assert!(content.contains("player1,Red Team,10"));
|
||||
assert!(content.contains("player2,Blue Team,5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_team_scores() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
|
||||
let mut team1 = TeamScore::new("Red Team".to_string());
|
||||
team1.add_territory_score("Capitol Hill".to_string(), 20);
|
||||
|
||||
let mut team2 = TeamScore::new("Blue Team".to_string());
|
||||
team2.add_territory_score("Downtown".to_string(), 15);
|
||||
|
||||
let team_scores = vec![team1, team2];
|
||||
|
||||
write_team_scores(file_path, &team_scores).unwrap();
|
||||
|
||||
let content = fs::read_to_string(file_path).unwrap();
|
||||
assert!(content.contains("team,Capitol Hill,Downtown"));
|
||||
assert!(content.contains("Red Team,20,0"));
|
||||
assert!(content.contains("Blue Team,0,15"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_team_scores() {
|
||||
let player1 = Player {
|
||||
username: "player1".to_string(),
|
||||
team: "Red Team".to_string(),
|
||||
};
|
||||
let mut score1 = PlayerScore::new(player1);
|
||||
score1.add_territory_score("Capitol Hill".to_string(), 10);
|
||||
score1.add_territory_score("Downtown".to_string(), 5);
|
||||
|
||||
let player2 = Player {
|
||||
username: "player2".to_string(),
|
||||
team: "Red Team".to_string(),
|
||||
};
|
||||
let mut score2 = PlayerScore::new(player2);
|
||||
score2.add_territory_score("Capitol Hill".to_string(), 8);
|
||||
|
||||
let player3 = Player {
|
||||
username: "player3".to_string(),
|
||||
team: "Blue Team".to_string(),
|
||||
};
|
||||
let mut score3 = PlayerScore::new(player3);
|
||||
score3.add_territory_score("Fremont".to_string(), 12);
|
||||
|
||||
let player_scores = vec![score1, score2, score3];
|
||||
let team_scores = calculate_team_scores(&player_scores);
|
||||
|
||||
assert_eq!(team_scores.len(), 2);
|
||||
|
||||
// Red Team should be first (higher score)
|
||||
assert_eq!(team_scores[0].team, "Red Team");
|
||||
assert_eq!(team_scores[0].total_score, 23); // 10 + 5 + 8
|
||||
assert_eq!(team_scores[0].territory_scores.get("Capitol Hill"), Some(&18));
|
||||
assert_eq!(team_scores[0].territory_scores.get("Downtown"), Some(&5));
|
||||
|
||||
// Blue Team should be second
|
||||
assert_eq!(team_scores[1].team, "Blue Team");
|
||||
assert_eq!(team_scores[1].total_score, 12);
|
||||
assert_eq!(team_scores[1].territory_scores.get("Fremont"), Some(&12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_territory_scores() {
|
||||
let temp_file = NamedTempFile::new().unwrap();
|
||||
let file_path = temp_file.path().to_str().unwrap();
|
||||
|
||||
let player1 = Player {
|
||||
username: "player1".to_string(),
|
||||
team: "Red Team".to_string(),
|
||||
};
|
||||
let mut score1 = PlayerScore::new(player1);
|
||||
score1.add_territory_score("Capitol Hill".to_string(), 10);
|
||||
score1.add_territory_score("Downtown".to_string(), 5);
|
||||
|
||||
let player2 = Player {
|
||||
username: "player2".to_string(),
|
||||
team: "Blue Team".to_string(),
|
||||
};
|
||||
let mut score2 = PlayerScore::new(player2);
|
||||
score2.add_territory_score("Capitol Hill".to_string(), 8);
|
||||
|
||||
let player_scores = vec![score1, score2];
|
||||
|
||||
write_territory_scores(file_path, &player_scores).unwrap();
|
||||
|
||||
let content = fs::read_to_string(file_path).unwrap();
|
||||
|
||||
// Check header contains territory names
|
||||
assert!(content.contains("username"));
|
||||
assert!(content.contains("team"));
|
||||
assert!(content.contains("Capitol Hill"));
|
||||
assert!(content.contains("Downtown"));
|
||||
assert!(content.contains("total"));
|
||||
|
||||
// Check player data
|
||||
assert!(content.contains("player1"));
|
||||
assert!(content.contains("player2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_team_scores_sorting() {
|
||||
let player1 = Player {
|
||||
username: "player1".to_string(),
|
||||
team: "Team A".to_string(),
|
||||
};
|
||||
let mut score1 = PlayerScore::new(player1);
|
||||
score1.add_territory_score("Area1".to_string(), 5);
|
||||
|
||||
let player2 = Player {
|
||||
username: "player2".to_string(),
|
||||
team: "Team B".to_string(),
|
||||
};
|
||||
let mut score2 = PlayerScore::new(player2);
|
||||
score2.add_territory_score("Area2".to_string(), 20);
|
||||
|
||||
let player3 = Player {
|
||||
username: "player3".to_string(),
|
||||
team: "Team C".to_string(),
|
||||
};
|
||||
let mut score3 = PlayerScore::new(player3);
|
||||
score3.add_territory_score("Area3".to_string(), 15);
|
||||
|
||||
let player_scores = vec![score1, score2, score3];
|
||||
let team_scores = calculate_team_scores(&player_scores);
|
||||
|
||||
// Should be sorted by total_score descending
|
||||
assert_eq!(team_scores[0].team, "Team B");
|
||||
assert_eq!(team_scores[0].total_score, 20);
|
||||
assert_eq!(team_scores[1].team, "Team C");
|
||||
assert_eq!(team_scores[1].total_score, 15);
|
||||
assert_eq!(team_scores[2].team, "Team A");
|
||||
assert_eq!(team_scores[2].total_score, 5);
|
||||
}
|
||||
}
|
||||
58
src/data.rs
58
src/data.rs
@ -4,57 +4,25 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Player {
|
||||
pub username: String,
|
||||
pub team: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PlayerScore {
|
||||
pub player: Player,
|
||||
pub total_score: i32,
|
||||
pub territory_scores: HashMap<String, i32>,
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Team {
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
pub players: Vec<Player>,
|
||||
pub scores: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl PlayerScore {
|
||||
pub fn new(player: Player) -> Self {
|
||||
PlayerScore {
|
||||
player: player,
|
||||
total_score: 0,
|
||||
territory_scores: HashMap::new(),
|
||||
impl Team {
|
||||
pub fn add_scores(&mut self, scores: &HashMap<String, i32>) {
|
||||
for (territory, score) in scores {
|
||||
self.scores
|
||||
.entry(territory.to_string())
|
||||
.and_modify(|s| *s += score)
|
||||
.or_insert(*score);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_territory_score(&mut self, territory: String, score: i32) {
|
||||
self.total_score += score;
|
||||
self.territory_scores
|
||||
.entry(territory)
|
||||
.and_modify(|s| *s += score)
|
||||
.or_insert(score);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TeamScore {
|
||||
pub team: String,
|
||||
pub total_score: i32,
|
||||
pub territory_scores: HashMap<String, i32>,
|
||||
}
|
||||
|
||||
impl TeamScore {
|
||||
pub fn new(team: String) -> Self {
|
||||
TeamScore {
|
||||
team,
|
||||
total_score: 0,
|
||||
territory_scores: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_territory_score(&mut self, territory: String, score: i32) {
|
||||
self.total_score += score;
|
||||
self.territory_scores
|
||||
.entry(territory)
|
||||
.and_modify(|s| *s += score)
|
||||
.or_insert(score);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
104
src/geo_utils.rs
104
src/geo_utils.rs
@ -4,15 +4,18 @@ use std::convert::TryFrom;
|
||||
use std::fs;
|
||||
use geo::algorithm::contains::Contains;
|
||||
use geojson::{FeatureCollection, GeoJson, Geometry, Value};
|
||||
use crate::Team;
|
||||
|
||||
pub struct GeoUtils {
|
||||
feature_collection: FeatureCollection
|
||||
}
|
||||
|
||||
impl GeoUtils {
|
||||
|
||||
pub fn new() -> Self {
|
||||
let fc = Self::load_feature_collection();
|
||||
let mut fc = Self::load_feature_collection();
|
||||
for feature in &mut fc.features {
|
||||
feature.set_property("color", "#888888ff");
|
||||
}
|
||||
Self {feature_collection: fc}
|
||||
}
|
||||
|
||||
@ -26,47 +29,70 @@ impl GeoUtils {
|
||||
pub fn get_feature_collection_copy(self) -> FeatureCollection {
|
||||
self.feature_collection.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_territory_for(point: geo_types::Point) -> String {
|
||||
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();
|
||||
let feature_collection: FeatureCollection = FeatureCollection::try_from(geojson).unwrap();
|
||||
for feature in feature_collection {
|
||||
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
|
||||
let exterior = coords[0]
|
||||
.iter()
|
||||
.map(|coord| (coord[0], coord[1]))
|
||||
.collect::<Vec<(f64, f64)>>();
|
||||
let polygon = geo_types::Polygon::new(exterior.into(), vec![]);
|
||||
if polygon.contains(&point) {
|
||||
return String::from(name);
|
||||
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();
|
||||
let name = name.as_str().unwrap_or("unknown");
|
||||
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 {
|
||||
max_team = (Some(team.color.clone()), *score);
|
||||
} else if *score > 0 && *score == max_team.1 {
|
||||
// Tie doesn't count
|
||||
max_team = (None, 0);
|
||||
}
|
||||
}
|
||||
Value::MultiPolygon(coords) => {
|
||||
let multipoly = coords.iter().map(|poly_coords| {
|
||||
let exterior = poly_coords[0]
|
||||
match max_team.0 {
|
||||
None => feature.set_property("color", "#888888ff"),
|
||||
Some(c) => {
|
||||
feature.set_property("color", c);
|
||||
}
|
||||
}
|
||||
}
|
||||
copy
|
||||
}
|
||||
|
||||
pub fn get_territory_for(self, point: geo_types::Point) -> String {
|
||||
let feature_collection: FeatureCollection = self.get_feature_collection_copy();
|
||||
for feature in feature_collection {
|
||||
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
|
||||
let exterior = coords[0]
|
||||
.iter()
|
||||
.map(|coord| (coord[0], coord[1]))
|
||||
.collect::<Vec<(f64, f64)>>()
|
||||
.try_into()
|
||||
.expect("Invalid exterior ring");
|
||||
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);
|
||||
.collect::<Vec<(f64, f64)>>();
|
||||
let polygon = geo_types::Polygon::new(exterior.into(), vec![]);
|
||||
if polygon.contains(&point) {
|
||||
return String::from(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => panic!("Geometry is not supported"),
|
||||
};
|
||||
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)>>()
|
||||
.try_into()
|
||||
.expect("Invalid exterior ring");
|
||||
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);
|
||||
}
|
||||
}
|
||||
_ => panic!("Geometry is not supported"),
|
||||
};
|
||||
}
|
||||
String::from("Outside")
|
||||
}
|
||||
String::from("Outside")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
88
src/main.rs
88
src/main.rs
@ -1,6 +1,5 @@
|
||||
pub mod osm;
|
||||
mod data;
|
||||
mod csv_io;
|
||||
mod geo_utils;
|
||||
|
||||
use crate::osm::scores;
|
||||
@ -9,14 +8,23 @@ use std::fs;
|
||||
use chrono::NaiveDateTime;
|
||||
use reqwest::Client;
|
||||
use xml::reader::EventReader;
|
||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
|
||||
use actix_web::{get, App, HttpResponse, HttpServer, Responder};
|
||||
use crate::data::{Team, Player};
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use std::io::Write;
|
||||
use tokio::time;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
const DATE_FORMAT: &str = "%Y-%m-%dT%H:%M:%SZ";
|
||||
|
||||
#[get("/geojson")]
|
||||
async fn geojson_endpoint() -> impl Responder {
|
||||
let utils = geo_utils::GeoUtils::new();
|
||||
let feature_collection = utils.get_feature_collection_copy();
|
||||
let teams = read_scores();
|
||||
let feature_collection = utils.get_colored_collection_copy(teams);
|
||||
HttpResponse::Ok()
|
||||
.content_type("application/json")
|
||||
.body(serde_json::to_string(&feature_collection).unwrap())
|
||||
@ -32,6 +40,13 @@ async fn index() -> impl Responder {
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let mut interval = time::interval(Duration::from_secs(60 * 5));
|
||||
let _maintainance = tokio::task::spawn(async move {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let _ = update_scores().await;
|
||||
}
|
||||
});
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(index)
|
||||
@ -42,53 +57,50 @@ async fn main() -> std::io::Result<()> {
|
||||
.await
|
||||
}
|
||||
|
||||
//#[tokio::main]
|
||||
async fn main2()-> Result<(), Box<dyn Error>> {
|
||||
async fn update_scores()-> Result<(), Box<dyn Error>> {
|
||||
let client = Client::builder()
|
||||
.user_agent("MapBattle/0.1")
|
||||
.build()?;
|
||||
|
||||
// Load players from CSV
|
||||
let players = csv_io::load_players("players.csv")?;
|
||||
println!("Loaded {} players", players.len());
|
||||
// Load players from a database?
|
||||
let mut teams: Vec<Team> = vec![
|
||||
Team{
|
||||
name: String::from("potato"),
|
||||
color: String::from("#df1aeaff"),
|
||||
scores: HashMap::new(),
|
||||
players: vec![
|
||||
Player {
|
||||
username: String::from("ammaratef45")
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Configure time range
|
||||
let start_date = NaiveDateTime::parse_from_str("2025-11-13T11:55:07Z", DATE_FORMAT)?;
|
||||
let end_date = NaiveDateTime::parse_from_str("2025-11-13T15:55:07Z", DATE_FORMAT)?;
|
||||
|
||||
// Calculate scores for each player
|
||||
let mut player_scores = Vec::new();
|
||||
|
||||
for player in players {
|
||||
println!("Processing player: {} ({})", player.username, player.team);
|
||||
|
||||
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);
|
||||
let territory_scores = scores::changesets_to_points(changesets, &client).await;
|
||||
|
||||
let mut player_score = data::PlayerScore::new(player);
|
||||
for (territory, score) in territory_scores {
|
||||
player_score.add_territory_score(territory, score);
|
||||
for team in &mut teams {
|
||||
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 reader = EventReader::from_str(&body);
|
||||
let changesets = scores::extract_changesets_from_reader(reader);
|
||||
let changesets = scores::time_bound_changesets(changesets, start_date, end_date);
|
||||
let territory_scores = scores::changesets_to_points(changesets).await;
|
||||
team.add_scores(&territory_scores);
|
||||
}
|
||||
|
||||
println!(" Total score: {}", player_score.total_score);
|
||||
player_scores.push(player_score);
|
||||
}
|
||||
|
||||
// Calculate team scores
|
||||
let team_scores = csv_io::calculate_team_scores(&player_scores);
|
||||
|
||||
// Write results to CSV files
|
||||
csv_io::write_player_scores("player_scores.csv", &player_scores)?;
|
||||
csv_io::write_team_scores("team_scores.csv", &team_scores)?;
|
||||
csv_io::write_territory_scores("territory_breakdown.csv", &player_scores)?;
|
||||
|
||||
println!("\nResults written to:");
|
||||
println!(" - player_scores.csv");
|
||||
println!(" - team_scores.csv");
|
||||
println!(" - territory_breakdown.csv");
|
||||
let file = File::create("scores.json")?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
serde_json::to_writer_pretty(&mut writer, &teams)?;
|
||||
writer.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_scores() -> Vec<Team> {
|
||||
let text: String = fs::read_to_string("scores.json").expect("file should be present");
|
||||
serde_json::from_str(&text).unwrap()
|
||||
}
|
||||
|
||||
39
src/osm.rs
39
src/osm.rs
@ -12,34 +12,6 @@ mod network {
|
||||
}
|
||||
}
|
||||
|
||||
mod xml_utils {
|
||||
use xml::reader::{EventReader, XmlEvent};
|
||||
pub fn extract_tag_value_from_reader(reader: EventReader<&[u8]>, tag: String) -> Option<String> {
|
||||
let mut inside_tag = false;
|
||||
for e in reader {
|
||||
match e {
|
||||
Ok(XmlEvent::StartElement { name, .. }) => {
|
||||
if name.local_name == tag {
|
||||
inside_tag = true;
|
||||
}
|
||||
}
|
||||
Ok(XmlEvent::Characters(data)) => {
|
||||
if inside_tag {
|
||||
return Some(data);
|
||||
}
|
||||
}
|
||||
Ok(XmlEvent::EndElement { name }) => {
|
||||
if name.local_name == tag {
|
||||
inside_tag = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub mod scores {
|
||||
use crate::osm::network;
|
||||
use chrono::NaiveDateTime;
|
||||
@ -67,7 +39,7 @@ pub mod scores {
|
||||
let reader = EventReader::from_str(&raw_changesets);
|
||||
let changesets = extract_changesets_from_reader(reader);
|
||||
let changesets = time_bound(changesets, input.start_time, input.end_time);
|
||||
let points = to_points(changesets, &client).await;
|
||||
let points = to_points(changesets).await;
|
||||
println!("Points for {player}: {points:?}");
|
||||
}
|
||||
}
|
||||
@ -116,7 +88,7 @@ pub mod scores {
|
||||
}
|
||||
|
||||
// Public function for converting changesets to points
|
||||
pub async fn changesets_to_points(changesets: Vec<Vec<OwnedAttribute>>, client: &Client) -> 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")).unwrap().value;
|
||||
@ -127,8 +99,7 @@ pub mod scores {
|
||||
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::get_territory_for(point);
|
||||
println!("{territory}");
|
||||
let territory = geo_utils::GeoUtils::new().get_territory_for(point);
|
||||
result.entry(territory).and_modify(|val| *val += score).or_insert(score);
|
||||
}
|
||||
result
|
||||
@ -138,8 +109,8 @@ pub mod scores {
|
||||
time_bound_changesets(input, start_date, end_date)
|
||||
}
|
||||
|
||||
async fn to_points(changesets: Vec<Vec<OwnedAttribute>>, client: &Client) -> HashMap<String, i32> {
|
||||
changesets_to_points(changesets, client).await
|
||||
async fn to_points(changesets: Vec<Vec<OwnedAttribute>>) -> HashMap<String, i32> {
|
||||
changesets_to_points(changesets).await
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user