add territories struct #6

Merged
linnealovespie merged 8 commits from territories into main 2026-05-24 05:17:54 +00:00
7 changed files with 154 additions and 67 deletions

1
Cargo.lock generated
View File

@ -1487,6 +1487,7 @@ version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]

View File

@ -16,7 +16,7 @@ geojson = "0.24.2"
r2d2 = "0.8.10"
r2d2_sqlite = "0.31.0"
reqwest = "0.12.24"
rusqlite = "0.37.0"
rusqlite = {version="0.37.0", features=["bundled"]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.145"
tera = "1.20.1"

View File

@ -1,5 +1,7 @@
use crate::geo_utils::GeoUtils;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Player {
pub username: String,
@ -10,7 +12,7 @@ pub struct Team {
pub name: String,
pub color: String,
pub players: Vec<Player>,
pub scores: HashMap<String, i32>,
pub scores: HashMap<String, i32>, // neighborhood to number of valid changes
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@ -19,6 +21,60 @@ pub struct Game {
pub teams: Vec<Team>,
pub start_time: String,
pub end_time: String,
pub territories: HashMap<String, Territory>, // territory name/id to struct
}
impl Game {
fn build_territories(&mut self, geoutils: &GeoUtils) {
let territory_json = geoutils.get_feature_collection_copy();
for feature in &territory_json.features {
let name = feature.property("S_HOOD").unwrap_or_default();
let name = name.as_str().unwrap_or("unknown");
let t: Territory = Territory {
territory_name: String::from(name),
claiming_team: None,
claiming_score: 0,
};
self.territories.insert(name.to_string(), t);
}
}
pub fn new(code: String, teams: Vec<Team>, start_time: String, end_time: String) -> Self {
let geoutils = GeoUtils::new();
let mut game = Game {
code: code,
teams: teams,
start_time: start_time,
end_time: end_time,
territories: HashMap::new(),
};
game.build_territories(&geoutils);
game
}
pub fn update_territories(&mut self) {
for (_, territory) in &mut self.territories {
let mut max_team: (Option<Team>, i32) = (None, 0);
for team in &self.teams {
let score = team.scores.get(&territory.territory_name).unwrap_or(&0);
if *score > max_team.1 {
max_team = (Some(team.clone()), *score);
} else if *score > 0 && *score == max_team.1 {
// Tie doesn't count
max_team = (None, max_team.1);
}
}
territory.claiming_team = max_team.0;
territory.claiming_score = max_team.1;
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Territory {
pub territory_name: String,
pub claiming_team: Option<Team>,
pub claiming_score: i32,
}
impl Team {

View File

@ -66,12 +66,7 @@ fn get_all_games(conn: Connection, all_teams: HashMap<String, Team>) -> GamesRes
.map(|part| all_teams.get(&format!("{game_name}_{part}")).unwrap())
.map(|part| part.to_owned())
.collect();
Game {
code: game_name,
teams,
start_time: games_row.get(2)?,
end_time: games_row.get(3)?,
}
Game::new(game_name, teams, games_row.get(2)?, games_row.get(3)?)
})
})
.and_then(Iterator::collect)

View File

@ -1,4 +1,4 @@
use crate::Team;
use crate::data::{Game, Territory};
use geo as geo_types;
use geo::algorithm::contains::Contains;
use geojson::{FeatureCollection, GeoJson, Geometry, Value};
@ -29,31 +29,30 @@ impl GeoUtils {
FeatureCollection::try_from(geojson).unwrap()
}
pub fn get_feature_collection_copy(self) -> FeatureCollection {
pub fn get_feature_collection_copy(&self) -> FeatureCollection {
self.feature_collection.clone()
}
pub fn get_colored_collection_copy(self, teams: Vec<Team>) -> FeatureCollection {
pub fn get_colored_collection_copy(self, game: &Game) -> 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);
}
}
match max_team.0 {
None => feature.set_property("color", "#888888ff"),
Some(c) => {
feature.set_property("color", c);
let this_territory: Territory = game
.territories
.get(name)
.expect("Whaaa no territory found!")
.clone();
match this_territory.claiming_team.clone() {
Some(team) => {
feature.set_property("color", team.color.clone());
feature.set_property("claiming_team", team.name.clone());
}
None => {
feature.set_property("color", "#888888ff");
}
}
feature.set_property("claiming_score", this_territory.claiming_score);
}
copy
}

View File

@ -131,6 +131,23 @@ async fn legend_data(
.body("{}")
}
#[get("/territories/{game_code}")]
async fn get_territories(
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)
{
return HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::to_string(&game.territories).unwrap());
}
HttpResponse::InternalServerError()
.content_type("application/json")
.body("{}")
}
#[get("/geojson/{game_code}")]
async fn geojson_endpoint(
game_code: web::Path<String>,
@ -139,9 +156,8 @@ async fn geojson_endpoint(
if let Ok(Responses::GamesResult(games)) = games_actor.send(Messages::GetGames).await
&& 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());
let feature_collection = utils.get_colored_collection_copy(&game);
return HttpResponse::Ok()
.content_type("application/json")
.body(serde_json::to_string(&feature_collection).unwrap());
@ -198,7 +214,7 @@ async fn main() -> std::io::Result<()> {
let games = db::games(&pool)
.await
.expect("retirieving games from db should work fine");
let state = Arc::new(Mutex::new(games));
let state = Arc::new(Mutex::new(games.clone()));
let actor_addr = GamesActor {
games: state.clone(),
}
@ -215,6 +231,7 @@ async fn main() -> std::io::Result<()> {
.service(admin)
.service(admin_query)
.service(list_of_games)
.service(get_territories)
})
.bind(("0.0.0.0", 8080))?
.run()
@ -266,6 +283,7 @@ async fn update_scores(games: Vec<Game>) -> Result<Vec<Game>, Box<dyn Error>> {
team.add_scores(&territory_scores);
}
}
neogame.update_territories();
result.push(neogame);
}
Ok(result)

View File

@ -28,52 +28,70 @@
<div id="map"></div>
<ul id="legend" class="legend"></ul>
<script>
// <map>
function style(feature) {
return { color: feature.properties.color};
// <map>
function style(feature) {
return { color: feature.properties.color};
};
function onEachFeature(feature, layer) {
// bind a popup to each geojson element
// does this feature have a property named popupContent?
if (feature.properties) {
var popup = document.createElement("div");
popup.innerHTML += feature.properties.S_HOOD;
popup.innerHTML += "<br>Team: " + feature.properties.claiming_team;
popup.innerHTML += "<br>Score: " + feature.properties.claiming_score;
layer.bindPopup(popup);
}
var map = L.map('map').setView([47.6062, -122.3321], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
}).addTo(map);
}
fetch('/geojson/{{game_code}}')
.then(res => res.json())
.then(data => L.geoJSON(data, {style: style}).addTo(map));
// </map>
// <legend>
const endpoint = "/legend-data/{{game_code}}";
fetch(endpoint)
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(data => {
const legend = document.getElementById("legend");
// Clear existing content if any
legend.innerHTML = "";
var map = L.map('map').setView([47.6062, -122.3321], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
}).addTo(map);
data.forEach(item => {
const li = document.createElement("li");
li.className = "legend-item";
fetch('/geojson/{{game_code}}')
.then(res => res.json())
.then(data => {
L.geoJSON(data, {style: style, onEachFeature : onEachFeature}).addTo(map)
});
// </map>
// <legend>
const endpoint = "/legend-data/{{game_code}}";
fetch(endpoint)
.then(response => {
if (!response.ok) {
throw new Error("Network response was not ok");
}
return response.json();
})
.then(data => {
const legend = document.getElementById("legend");
// Clear existing content if any
legend.innerHTML = "";
const colorBox = document.createElement("span");
colorBox.className = "color-box";
colorBox.style.backgroundColor = item.color;
data.forEach(item => {
const li = document.createElement("li");
li.className = "legend-item";
const label = document.createElement("span");
label.textContent = item.name;
const colorBox = document.createElement("span");
colorBox.className = "color-box";
colorBox.style.backgroundColor = item.color;
li.appendChild(colorBox);
li.appendChild(label);
legend.appendChild(li);
});
})
.catch(error => {
console.error("Fetching legend data failed:", error);
const label = document.createElement("span");
label.textContent = item.name;
li.appendChild(colorBox);
li.appendChild(label);
legend.appendChild(li);
});
})
.catch(error => {
console.error("Fetching legend data failed:", error);
});
const territories_endpoint = "/territories/{game_code}"
</script>
</body>
</html>