summaryrefslogtreecommitdiff
path: root/rust_backend/src
diff options
context:
space:
mode:
Diffstat (limited to 'rust_backend/src')
-rw-r--r--rust_backend/src/eve_api.rs319
-rw-r--r--rust_backend/src/eve_service.rs42
-rw-r--r--rust_backend/src/main.rs95
-rw-r--r--rust_backend/src/manager.rs90
-rw-r--r--rust_backend/src/market/mod.rs3
-rw-r--r--rust_backend/src/market/order.rs83
-rw-r--r--rust_backend/src/market/trade.rs63
-rw-r--r--rust_backend/src/pathfinding/graph.rs7
-rw-r--r--rust_backend/src/pathfinding/mod.rs15
-rw-r--r--rust_backend/src/pathfinding/models.rs86
-rw-r--r--rust_backend/src/pathfinding/parser.rs46
-rw-r--r--rust_backend/src/route.rs108
12 files changed, 957 insertions, 0 deletions
diff --git a/rust_backend/src/eve_api.rs b/rust_backend/src/eve_api.rs
new file mode 100644
index 0000000..18e1007
--- /dev/null
+++ b/rust_backend/src/eve_api.rs
@@ -0,0 +1,319 @@
+use dotenv::dotenv;
+use std::env;
+use std::error::Error;
+use std::fs::{self, File};
+use std::io::{self, Read, Write};
+use std::path::Path;
+use std::time::{SystemTime, UNIX_EPOCH, Duration};
+use std::sync::Arc;
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use tokio::{task, time};
+use tokio::sync::Semaphore;
+
+
+use crate::pathfinding::Location;
+use crate::market::Order;
+
+const TOKEN_FILE: &str = "token.json";
+
+use std::sync::Mutex;
+
+
+#[derive(Deserialize, Serialize, Clone)]
+struct TokenData {
+ access_token: String,
+ refresh_token: String,
+ expires_at: u64,
+}
+
+#[derive(Deserialize)]
+struct TokenResponse {
+ access_token: String,
+ refresh_token: String,
+ expires_in: u64,
+}
+
+pub struct EveApiClient {
+ client_id: String,
+ client_secret: String,
+ character_id: String,
+ redirect_url: String,
+ access_token: Mutex<Option<String>>,
+ expires_at: Mutex<Option<u64>>,
+}
+
+
+impl EveApiClient {
+ pub fn new() -> Result<Self, Box<dyn Error>> {
+ dotenv().ok();
+
+ let client_id = env::var("CLIENT_ID").map_err(|e| format!("CLIENT_ID error: {}", e))?;
+ let client_secret = env::var("CLIENT_SECRET").map_err(|e| format!("CLIENT_SECRET error: {}", e))?;
+ let character_id = env::var("CHARACTER_ID").map_err(|e| format!("CHARACTER_ID error: {}", e))?;
+ let redirect_url = env::var("REDIRECT_URL").map_err(|e| format!("REDIRECT_URL error: {}", e))?;
+
+ let token_data = match Self::load_token_from_file() {
+ Ok(data) => Some(data),
+ Err(_) => None,
+ };
+
+ Ok(EveApiClient {
+ client_id,
+ client_secret,
+ character_id,
+ redirect_url,
+ access_token: Mutex::new(token_data.clone().map(|data| data.access_token)),
+ expires_at: Mutex::new(token_data.map(|data| data.expires_at)),
+ })
+ }
+
+ pub fn get_authorization_url(&self) -> String {
+ let auth_url = format!(
+ "https://login.eveonline.com/v2/oauth/authorize/?response_type=code&client_id={}&redirect_uri={}&scope={}",
+ self.client_id,
+ self.redirect_url,
+ "esi-assets.read_assets.v1 esi-location.read_location.v1 esi-location.read_online.v1 esi-ui.open_window.v1 esi-ui.write_waypoint.v1".replace(" ", "%20")
+ );
+ auth_url
+ }
+
+ pub async fn exchange_code_for_token(&self, code: &str) -> Result<(), Box<dyn Error>> {
+ let client = reqwest::Client::new();
+ let res = client
+ .post("https://login.eveonline.com/v2/oauth/token")
+ .form(&[
+ ("client_id", self.client_id.as_str()),
+ ("client_secret", self.client_secret.as_str()),
+ ("code", code),
+ ("redirect_uri", self.redirect_url.as_str()),
+ ("grant_type", "authorization_code"),
+ ])
+ .send()
+ .await;
+
+ let res = match res {
+ Ok(response) => response,
+ Err(e) => return Err(Box::new(e)),
+ };
+
+ let token_response: TokenResponse = match res.json().await {
+ Ok(data) => data,
+ Err(e) => return Err(Box::new(e)),
+ };
+
+ let expires_at = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards") // Handle this error as per your need
+ .as_secs() + token_response.expires_in;
+
+ let token_data = TokenData {
+ access_token: token_response.access_token,
+ refresh_token: token_response.refresh_token,
+ expires_at,
+ };
+
+ if let Err(e) = Self::save_token_to_file(&token_data) {
+ return Err(e);
+ }
+
+ let mut token = self.access_token.lock().unwrap();
+ *token = Some(token_data.access_token);
+
+ Ok(())
+ }
+
+ pub async fn refresh_access_token(&self) -> Result<(), Box<dyn Error>> {
+ let token_data = match Self::load_token_from_file() {
+ Ok(data) => data,
+ Err(_) => return Err("Failed to load token data".into()),
+ };
+
+
+ let client = reqwest::Client::new();
+ let res = client
+ .post("https://login.eveonline.com/v2/oauth/token") // Replace with the actual token endpoint
+ .form(&[
+ ("client_id", self.client_id.as_str()),
+ ("client_secret", self.client_secret.as_str()),
+ ("refresh_token", token_data.refresh_token.as_str()),
+ ("grant_type", "refresh_token"),
+ ])
+ .send()
+ .await?;
+
+ let refreshed_token_response: TokenResponse = res.json().await?;
+
+ let current_timestamp = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Time went backwards")
+ .as_secs();
+
+ let new_expires_at = current_timestamp + refreshed_token_response.expires_in;
+
+ let new_token_data = TokenData {
+ access_token: refreshed_token_response.access_token,
+ refresh_token: refreshed_token_response.refresh_token,
+ expires_at: new_expires_at,
+ };
+
+ Self::save_token_to_file(&new_token_data)?;
+
+ let mut access_token = self.access_token.lock().unwrap();
+ *access_token = Some(new_token_data.access_token);
+ let mut expires_at = self.expires_at.lock().unwrap();
+ *expires_at = Some(new_expires_at);
+
+ Ok(())
+ }
+
+
+ pub async fn request_location(&self) -> Result<Location, Box<dyn Error>> {
+ self.ensure_valid_token().await?;
+
+ let url = format!("https://esi.evetech.net/latest/characters/{}/location/", self.character_id);
+
+ println!("{:?}", url);
+
+ let client = reqwest::Client::new();
+
+ let response = client.get(url)
+ .bearer_auth(self.get_access_token().unwrap())
+ .send()
+ .await?;
+
+ println!("{:?}", response);
+
+ if response.status().is_success() {
+ let data = response.json::<Location>().await?;
+ Ok(data)
+ } else {
+ Err(Box::new(response.error_for_status().unwrap_err()))
+ }
+ }
+
+ pub async fn request_order_metadata(&self, region: u64) -> Result<(u64, DateTime<Utc>), Box<dyn Error>> {
+ let url = format!("https://esi.evetech.net/latest/markets/{}/orders/?datasource=tranquility&order_type=all&page=1", region);
+
+ let client = reqwest::Client::new();
+ let response = client.get(&url)
+ .send()
+ .await
+ .map_err(|e| e.to_string())?;
+
+ let x_pages = match response.headers().get("x-pages") {
+ Some(x_pages_value) => {
+ match x_pages_value.to_str() {
+ Ok(value_str) => {
+ match value_str.parse::<u64>() {
+ Ok(value) => value,
+ Err(_) => return Err("Failed to parse x-pages value into u64".into()),
+ }
+ }
+ Err(_) => return Err("Invalid x-pages header value".into()),
+ }
+ }
+ None => return Err("X-Pages header not found".into()),
+ };
+
+ let expiry_unix_time = match response.headers().get("expires") {
+ Some(expiry_value) => {
+ match expiry_value.to_str() {
+ Ok(value_str) => {
+ match DateTime::parse_from_rfc2822(value_str) {
+ Ok(expiry_date) => expiry_date.with_timezone(&Utc),
+ Err(_) => return Err("Failed to parse expiry date".into()),
+ }
+ }
+ Err(_) => return Err("Invalid expiry header value".into()),
+ }
+ }
+ None => return Err("Expires header not found".into()),
+ };
+
+ Ok((x_pages, expiry_unix_time))
+ }
+
+
+
+ pub async fn request_orders(&self, region: u64) -> Result<(Vec<Order>, DateTime<Utc>), Box<dyn Error>> {
+ let (x_pages, expiry) = self.request_order_metadata(region).await?;
+
+ let client = reqwest::Client::builder()
+ .timeout(Duration::from_secs(30))
+ .build()?;
+
+ let semaphore = Arc::new(Semaphore::new(10));
+ let mut tasks = Vec::new();
+
+ for page in 1..=x_pages {
+ let permit = semaphore.clone().acquire_owned().await?;
+ let client = client.clone();
+ let url = format!("https://esi.evetech.net/latest/markets/{}/orders/?datasource=tranquility&order_type=all&page={}", region, page);
+
+ let task = task::spawn(async move {
+ time::sleep(Duration::from_millis(20)).await;
+ let response = client.get(&url).send().await?;
+ println!("{:?}", &url);
+ drop(permit);
+ response.error_for_status()?.json::<Vec<Order>>().await
+ });
+
+ tasks.push(task);
+ }
+
+ let mut orders = Vec::new();
+ for task in tasks {
+ match task.await {
+ Ok(result) => match result {
+ Ok(mut page_orders) => orders.append(&mut page_orders),
+ Err(e) => return Err(Box::new(e)),
+ },
+ Err(e) => return Err(Box::new(e)),
+ }
+ }
+
+ Ok((orders, expiry))
+ }
+
+ pub async fn ensure_valid_token(&self) -> Result<(), Box<dyn Error>> {
+ let current_timestamp = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map_err(|e| format!("System time error: {}", e))?
+ .as_secs();
+
+ let should_refresh = {
+ let expires_at = self.expires_at.lock().unwrap();
+ let expires_at_value = expires_at.unwrap_or(0);
+ current_timestamp > expires_at_value
+ };
+
+ if should_refresh {
+ self.refresh_access_token().await?;
+ }
+
+ Ok(())
+ }
+ pub fn get_access_token(&self) -> Option<String> {
+ let token = self.access_token.lock().unwrap();
+ token.clone()
+ }
+
+ pub fn save_token_to_file(token_data: &TokenData) -> Result<(), Box<dyn Error>> {
+ let token_json = serde_json::to_string(token_data)?;
+ fs::write(Path::new(TOKEN_FILE), token_json)?;
+ Ok(())
+ }
+
+ fn load_token_from_file() -> Result<TokenData, Box<dyn Error>> {
+ let token_json = match fs::read_to_string(Path::new(TOKEN_FILE)) {
+ Ok(content) => content,
+ Err(_) => return Err("Unable to read token file".into()),
+ };
+ let token_data: TokenData = serde_json::from_str(&token_json)?;
+ Ok(token_data)
+ }
+
+
+}
diff --git a/rust_backend/src/eve_service.rs b/rust_backend/src/eve_service.rs
new file mode 100644
index 0000000..c831847
--- /dev/null
+++ b/rust_backend/src/eve_service.rs
@@ -0,0 +1,42 @@
+use crate::eve_api::EveApiClient;
+use crate::pathfinding::Location;
+use crate::market::Orders;
+
+use std::error::Error;
+
+pub struct EveService {
+ client: EveApiClient,
+}
+
+impl EveService {
+ pub fn new() -> Result<Self, Box<dyn Error>> {
+ let eve_api_client = EveApiClient::new()?;
+ Ok(EveService { client: eve_api_client, })
+ }
+
+ pub fn get_authorization_url(&self) -> String {
+ self.client.get_authorization_url()
+ }
+
+ pub async fn exchange_code_for_token(&self, code: &str) -> Result<(), Box<dyn Error>> {
+ self.client.exchange_code_for_token(code).await
+ }
+
+ pub async fn location(&self) -> Result<Location, Box<dyn Error>> {
+ self.client.request_location().await.map_err(|e| e.into())
+ }
+
+ pub async fn updated_orders(&self, current_orders: &Orders) -> Result<Orders, Box<dyn Error>> {
+ let mut region_orders = Orders::new();
+
+ let regions = current_orders.expired_regions();
+
+ for &region in regions.iter() {
+ let (orders, expiry) = self.client.request_orders(region).await?;
+ region_orders.insert(region, orders, expiry);
+ }
+
+ Ok(region_orders)
+
+ }
+}
diff --git a/rust_backend/src/main.rs b/rust_backend/src/main.rs
new file mode 100644
index 0000000..4a2e086
--- /dev/null
+++ b/rust_backend/src/main.rs
@@ -0,0 +1,95 @@
+mod manager;
+mod eve_service;
+mod eve_api;
+mod pathfinding;
+mod market;
+
+use manager::Manager;
+
+use actix_web::{web, App, HttpServer, Responder, HttpResponse, http::header};
+use std::sync::Mutex;
+use tokio::sync::watch;
+use serde::Deserialize;
+
+
+async fn index() -> impl Responder {
+ "Hi"
+}
+
+async fn location(manager: web::Data<Manager>) -> impl Responder {
+ match manager.location() {
+ Some(location) => {
+ let station_id = location.station_id.map_or("None".to_string(), |id| id.to_string());
+ let structure_id = location.structure_id.map_or("None".to_string(), |id| id.to_string());
+ format!(
+ "Current location: Solar System ID: {}, Station ID: {}, Structure ID: {}",
+ location.solar_system_id, station_id, structure_id
+ )
+ },
+ None => "No location available".to_string(),
+ }
+}
+
+async fn orders(manager: web::Data<Manager>) -> impl Responder {
+ let orders = manager.orders();
+ HttpResponse::Ok().json(orders)
+}
+
+async fn start_oauth(manager: web::Data<Manager>) -> HttpResponse {
+ let auth_url = manager.get_authorization_url();
+ HttpResponse::Found()
+ .append_header((header::LOCATION, auth_url.as_str()))
+ .finish()
+}
+
+#[derive(Deserialize)]
+struct AuthCode {
+ code: String,
+}
+
+async fn oauth_callback(
+ query: web::Query<AuthCode>,
+ manager: web::Data<Manager>,
+) -> HttpResponse {
+ match manager.exchange_code_for_token(&query.code).await {
+ Ok(_) => HttpResponse::Ok().body("Token exchange successful"),
+ Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)),
+ }
+}
+
+#[actix_web::main]
+async fn main() -> std::io::Result<()> {
+ let manager = web::Data::new(Manager::new());
+
+
+ let manager_clone = manager.clone();
+ let (shutdown_tx, shutdown_rx) = watch::channel(false);
+
+ let manager_handle = tokio::spawn(async move {
+ manager_clone.run(shutdown_rx).await;
+ });
+
+ let server = HttpServer::new(move || {
+ App::new()
+ .app_data(manager.clone())
+ .route("/", web::get().to(index))
+ .route("/location", web::get().to(location))
+ .route("/orders", web::get().to(orders))
+ .route("/start_oauth", web::get().to(start_oauth))
+ .route("/oauth_callback", web::get().to(oauth_callback))
+ })
+ .bind("127.0.0.1:8080")?
+ .run();
+
+ ctrlc::set_handler(move || {
+ let _ = shutdown_tx.send(true);
+ })
+ .expect("Error setting Ctrl-C handler");
+
+ tokio::select! {
+ _ = server => {},
+ _ = manager_handle => {},
+ }
+
+ Ok(())
+}
diff --git a/rust_backend/src/manager.rs b/rust_backend/src/manager.rs
new file mode 100644
index 0000000..b9cbe08
--- /dev/null
+++ b/rust_backend/src/manager.rs
@@ -0,0 +1,90 @@
+use std::sync::Mutex;
+use tokio::sync::watch;
+
+use std::error::Error;
+
+use crate::eve_service::EveService;
+use crate::pathfinding::{Location, build};
+use crate::market::{Order, Orders};
+
+pub struct Manager {
+ // route_state: Mutex<Route>
+ // route_info_state: Mutex<RouteInfo>
+ orders_state: Mutex<Orders>,
+ location: Mutex<Option<Location>>,
+ // log_state: Mutex<Log>
+ eve_service: EveService,
+
+}
+
+impl Manager {
+ pub fn new() -> Manager {
+
+ build();
+
+ Manager {
+ location: Mutex::new( None ),
+ eve_service: EveService::new().expect("Manager to generate EveService"),
+ orders_state: Mutex::new( Orders::new() ),
+ }
+
+ }
+
+ pub async fn run(&self, shutdown_signal: watch::Receiver<bool>) {
+ loop {
+ tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
+ if *shutdown_signal.borrow() {
+ println!("Manager task is shutting down.");
+ break;
+ }
+
+ match self.eve_service.location().await {
+ Ok(result) => {
+ match self.location.lock() {
+ Ok(mut location) => *location = Some(result),
+ Err(e) => eprintln!("Error locking location mutex: {}", e),
+ }
+ }
+ Err(e) => {
+ eprintln!("Error fetching location {}", e);
+ }
+ }
+
+
+ let orders_clone = match self.orders_state.lock() {
+ Ok(orders_lock) => orders_lock.clone(),
+ Err(e) => {
+ eprintln!("Error locking orders_state mutex: {}", e);
+ continue;
+ }
+ };
+
+ match self.eve_service.updated_orders(&orders_clone).await {
+ Ok(updated_orders) => {
+ match self.orders_state.lock() {
+ Ok(mut orders_state) => orders_state.update(updated_orders),
+ Err(e) => eprintln!("Error locking orders_state mutex: {}", e),
+ }
+ }
+ Err(e) => eprintln!("Error updating orders: {}", e),
+ }
+ }
+ }
+ pub fn get_authorization_url(&self) -> String {
+ self.eve_service.get_authorization_url()
+ }
+
+ pub async fn exchange_code_for_token(&self, code: &str) -> Result<(), Box<dyn Error>> {
+ self.eve_service.exchange_code_for_token(code).await
+ }
+
+ pub fn location(&self) -> Option<Location> {
+ let location = self.location.lock().unwrap();
+ location.clone()
+ }
+
+ pub fn orders(&self) -> Vec<Order> {
+ let orders = self.orders_state.lock().unwrap();
+ orders.orders()
+ }
+} \ No newline at end of file
diff --git a/rust_backend/src/market/mod.rs b/rust_backend/src/market/mod.rs
new file mode 100644
index 0000000..16d1f37
--- /dev/null
+++ b/rust_backend/src/market/mod.rs
@@ -0,0 +1,3 @@
+mod order;
+
+pub use order::{Order, Orders}; \ No newline at end of file
diff --git a/rust_backend/src/market/order.rs b/rust_backend/src/market/order.rs
new file mode 100644
index 0000000..6dbbcf6
--- /dev/null
+++ b/rust_backend/src/market/order.rs
@@ -0,0 +1,83 @@
+use std::error::Error;
+use std::collections::HashMap;
+
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+
+use crate::eve_service::EveService;
+
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Order {
+ duration: u64,
+ is_buy_order: bool,
+ issued: DateTime<Utc>,
+ location_id: u64,
+ min_volume: u64,
+ order_id: u64,
+ price: f64,
+ range: String,
+ system_id: u64,
+ type_id: u64,
+ volume_remain: u64,
+ volume_total: u64
+}
+
+const REGIONS: [u64; 1] = [10000033];
+
+#[derive(Clone, Serialize)]
+pub struct Orders {
+ region_orders: HashMap<u64, Vec<Order>>,
+ expiry_times: HashMap<u64, DateTime<Utc>>, // region_id to expiration time
+}
+
+
+impl Orders {
+ pub fn new() -> Self {
+ Orders {
+ region_orders: HashMap::new(),
+ expiry_times: HashMap::new(),
+ }
+ }
+
+ pub fn insert(&mut self, region: u64, orders: Vec<Order>, expiry: DateTime<Utc>) {
+ self.region_orders.insert(region, orders);
+ self.expiry_times.insert(region, expiry);
+ }
+
+ pub fn expired_regions(&self) -> Vec<u64> {
+ let current_time = Utc::now();
+
+ println!("expiry: {:?}, time now: {:?}", &self.expiry_times, &current_time);
+
+ let mut expired: Vec<u64> = self.expiry_times.iter()
+ .filter(|&(_region_id, &expiry_time)| expiry_time < current_time)
+ .map(|(&region_id, _)| region_id)
+ .collect();
+
+ for &region in REGIONS.iter() {
+ if !self.expiry_times.contains_key(&region) {
+ expired.push(region);
+ }
+ }
+
+
+ expired
+
+ }
+
+ pub fn update(&mut self, updated_orders: Orders) {
+ for (key, value) in updated_orders.region_orders {
+ self.region_orders.insert(key, value);
+
+ if let Some(expiry_time) = updated_orders.expiry_times.get(&key) {
+ self.expiry_times.insert(key, *expiry_time);
+ }
+ }
+ }
+
+
+ pub fn orders(&self) -> Vec<Order> {
+ self.region_orders.values().flat_map(|orders| orders.clone()).collect()
+ }
+} \ No newline at end of file
diff --git a/rust_backend/src/market/trade.rs b/rust_backend/src/market/trade.rs
new file mode 100644
index 0000000..362acbf
--- /dev/null
+++ b/rust_backend/src/market/trade.rs
@@ -0,0 +1,63 @@
+use crate::order::Order;
+
+pub struct Trade<'a> {
+ from: &'a Order,
+ to: &'a Order,
+ quantity: usize,
+ gross_profit: f64,
+}
+
+const TAX_RATE: f64 = 0.08
+
+impl<'a> Trade<'a> {
+ pub fn new(from: &'a Order, to: &'a Order, quantity: usize) -> Self {
+ Self { from, to, quantity }
+ }
+
+ pub fn create_trades(sell_orders: &'a [Order], buy_orders: &'a [Order]) -> Vec<Trade<'a>> {
+ let mut trades: Vec<Trade<'a>> = Vec::new();
+ let mut grouped_orders: HashMap<i32, (Vec<&'a Order>, Vec<&'a Order>)> = HashMap::new();
+
+ // Grouping sell orders
+ for order in sell_orders {
+ grouped_orders.entry(order.type_id)
+ .or_insert((Vec::new(), Vec::new()))
+ .0
+ .push(order);
+ }
+
+ // Grouping buy orders
+ for order in buy_orders {
+ grouped_orders.entry(order.type_id)
+ .or_insert((Vec::new(), Vec::new()))
+ .1
+ .push(order);
+ }
+
+ // Iterating over each type_id's sell and buy orders to create trades
+ for (_type_id, (sells, buys)) in grouped_orders {
+ for &sell_order in sells {
+ for &buy_order in buys {
+ let quantity = std::cmp::min(sell_order.volume_remain, buy_order.volume_remain);
+ let gross_profit = quantity as f64 * (buy_order.price * (1-TAX_RATE) - sell_order.price);
+
+ if gross_profit <= 0.0 {
+ continue;
+ }
+
+ let trade = Trade::new(
+ sell_order,
+ buy_order,
+ quantity,
+ gross_profit,
+ );
+
+ trades.push(trade);
+ }
+ }
+ }
+
+ trades
+ }
+
+}
diff --git a/rust_backend/src/pathfinding/graph.rs b/rust_backend/src/pathfinding/graph.rs
new file mode 100644
index 0000000..0352007
--- /dev/null
+++ b/rust_backend/src/pathfinding/graph.rs
@@ -0,0 +1,7 @@
+use crate::pathfinding::models::{MapNode, Graph};
+
+use std::collections::HashMap;
+
+pub fn generate_graph(stations: &Vec<MapNode>) -> Graph {
+ HashMap::new()
+} \ No newline at end of file
diff --git a/rust_backend/src/pathfinding/mod.rs b/rust_backend/src/pathfinding/mod.rs
new file mode 100644
index 0000000..32c2831
--- /dev/null
+++ b/rust_backend/src/pathfinding/mod.rs
@@ -0,0 +1,15 @@
+mod models;
+mod parser;
+mod graph;
+pub use models::{Location, Graph};
+
+
+use crate::pathfinding::parser::parse_stations;
+use crate::pathfinding::graph::generate_graph;
+
+pub fn build() {
+ let stations = parse_stations().unwrap();
+ let gates_connnections = parse_gate_connections().unwrap()
+ let graph = generate_graph(&stations, &gates_connnections);
+
+}
diff --git a/rust_backend/src/pathfinding/models.rs b/rust_backend/src/pathfinding/models.rs
new file mode 100644
index 0000000..4bbc580
--- /dev/null
+++ b/rust_backend/src/pathfinding/models.rs
@@ -0,0 +1,86 @@
+use serde::{Deserialize, Deserializer, Serialize};
+use std::str::FromStr;
+use std::collections::HashMap;
+
+
+#[derive(Deserialize, Serialize, Clone)]
+pub struct Location {
+ pub solar_system_id: i32,
+ pub station_id: Option<i32>,
+ pub structure_id: Option<i32>,
+}
+
+#[derive(Deserialize, Serialize, Clone, Debug)]
+pub struct RawNode {
+ pub itemID: u64,
+ pub typeID: u64,
+ pub groupID: u64,
+ #[serde(deserialize_with = "deserialize_optional_u64")]
+ pub solarSystemID: Option<u64>,
+ #[serde(deserialize_with = "deserialize_optional_u64")]
+ pub constellationID: Option<u64>,
+ #[serde(deserialize_with = "deserialize_optional_u64")]
+ pub regionID: Option<u64>,
+ #[serde(deserialize_with = "deserialize_optional_u64")]
+ pub orbitID: Option<u64>,
+ pub x: Option<f64>,
+ pub y: Option<f64>,
+ pub z: Option<f64>,
+ #[serde(deserialize_with = "deserialize_optional_f64")]
+ pub radius: Option<f64>,
+ pub itemName: Option<String>,
+ #[serde(deserialize_with = "deserialize_optional_f64")]
+ pub security: Option<f64>,
+}
+
+
+fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ let s: Option<String> = Option::deserialize(deserializer)?;
+ match s.as_deref() {
+ Some("None") | Some("") => Ok(None), // Handle "None" and empty strings
+ Some(str_val) => u64::from_str(str_val)
+ .map(Some)
+ .map_err(serde::de::Error::custom),
+ None => Ok(None),
+ }
+}
+
+fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ let s: Option<String> = Option::deserialize(deserializer)?;
+ match s.as_deref() {
+ Some("None") | Some("") => Ok(None), // Handle "None" and empty strings
+ Some(str_val) => str_val.parse::<f64>()
+ .map(Some)
+ .map_err(serde::de::Error::custom),
+ None => Ok(None),
+ }
+}
+
+#[derive(Debug)]
+pub struct MapNode {
+ pub item_id: u64,
+ pub is_station: bool,
+ pub solar_system_id: u64,
+ pub region_id: u64,
+ pub x: f64,
+ pub y: f64,
+ pub z: f64,
+ pub name: String,
+ pub security: f64,
+}
+
+#[derive(Deserialize, Serialize, Clone, Debug)]
+pub struct Connection {
+ stargate_id: u64,
+ destination_id: u64,
+}
+
+
+
+pub type Graph = HashMap<u64, Vec<u64>>; \ No newline at end of file
diff --git a/rust_backend/src/pathfinding/parser.rs b/rust_backend/src/pathfinding/parser.rs
new file mode 100644
index 0000000..955a448
--- /dev/null
+++ b/rust_backend/src/pathfinding/parser.rs
@@ -0,0 +1,46 @@
+use std::error::Error;
+use std::fs::File;
+use csv;
+
+use crate::pathfinding::models::{RawNode, MapNode};
+
+pub fn parse_stations() -> Result<Vec<MapNode>, Box<dyn Error>> {
+ let file_path = "../mapDenormalize.csv";
+ let file = File::open(file_path)?;
+
+ let mut rdr = csv::Reader::from_reader(file);
+
+ let mut raw_nodes: Vec<RawNode> = Vec::new();
+ for result in rdr.deserialize() {
+ match result {
+ Ok(node) => raw_nodes.push(node),
+ Err(e) => println!("Error deserializing record: {}", e),
+ }
+ }
+
+ println!("raw nodes: {:?}", raw_nodes.len());
+
+ let processed_nodes: Vec<MapNode> = raw_nodes.into_iter()
+ .filter(|node| [15, 10].contains(&node.groupID))
+ .map(|node| MapNode {
+ item_id: node.itemID,
+ is_station: node.groupID == 15,
+ solar_system_id: node.solarSystemID.unwrap_or_default(),
+ region_id: node.regionID.unwrap_or_default(),
+ x: node.x.unwrap_or_default(),
+ y: node.y.unwrap_or_default(),
+ z: node.z.unwrap_or_default(),
+ name: node.itemName.unwrap_or_default(),
+ security: node.security.unwrap_or_default(),
+ })
+ .collect();
+
+ println!("processed nodes: {:?}", processed_nodes.len());
+
+
+ Ok(processed_nodes)
+}
+
+pub fn parse_gate_connections() -> Result<Vec<u64, u64>, Box<dyn Error>> {
+ unimplemented!();
+} \ No newline at end of file
diff --git a/rust_backend/src/route.rs b/rust_backend/src/route.rs
new file mode 100644
index 0000000..a2c477e
--- /dev/null
+++ b/rust_backend/src/route.rs
@@ -0,0 +1,108 @@
+use std::collections::HashMap;
+
+struct Item {
+ profit: f32,
+ cost: f32,
+ cargo: f32,
+ quantity: usize,
+}
+
+#[derive(PartialEq, Eq, Hash, Clone)]
+struct DiscreteState {
+ cost: isize,
+ cargo: isize,
+}
+
+type StateMap = HashMap<DiscreteState, (f32, Vec<(usize, usize)>)>;
+
+fn discretize(value: f32, precision: f32) -> isize {
+ (value / precision).round() as isize
+}
+
+fn knapsack(items: &[Item], max_cost: f32, max_cargo: f32) -> (f32, Vec<(usize, usize)>) {
+ let mut best_state: StateMap = HashMap::new();
+ let precision = 0.01; // Define the precision level for discretization
+ best_state.insert(DiscreteState { cost: 0, cargo: 0 }, (0.0, vec![]));
+
+ for (index, item) in items.iter().enumerate() {
+ let mut new_state = best_state.clone();
+
+ for (&DiscreteState { cost: current_cost, cargo: current_cargo }, &(current_profit, ref indices)) in &best_state {
+ let max_quantity_cost = ((max_cost - current_cost as f32 * precision) / item.cost).floor() as usize;
+ let max_quantity_cargo = ((max_cargo - current_cargo as f32 * precision) / item.cargo).floor() as usize;
+
+ let quantity = *[
+ item.quantity,
+ max_quantity_cost,
+ max_quantity_cargo,
+ ]
+ .iter()
+ .min()
+ .unwrap();
+
+ if quantity == 0 {
+ continue;
+ }
+
+ let total_cost = discretize(current_cost as f32 * precision + item.cost * (quantity as f32), precision);
+ let total_cargo = discretize(current_cargo as f32 * precision + item.cargo * (quantity as f32), precision);
+ let total_profit = current_profit + item.profit * (quantity as f32);
+
+ let mut new_indices = indices.clone();
+ new_indices.push((index, quantity));
+
+ new_state
+ .entry(DiscreteState { cost: total_cost, cargo: total_cargo })
+ .and_modify(|(existing_profit, existing_indices)| {
+ if *existing_profit < total_profit {
+ *existing_profit = total_profit;
+ *existing_indices = new_indices.clone();
+ }
+ })
+ .or_insert((total_profit, new_indices));
+ }
+
+ best_state = new_state;
+ }
+
+ best_state.values().max_by(|&(p1, _), &(p2, _)| p1.partial_cmp(&p2).unwrap()).unwrap_or(&(0.0, vec![])).clone()
+}
+
+fn main() {
+ // Example usage with some items defined
+ let items = vec![
+ Item {
+ profit: 60.0,
+ cost: 0.1,
+ cargo: 0.2,
+ quantity: 100,
+ },
+ Item {
+ profit: 100.0,
+ cost: 0.2,
+ cargo: 0.5,
+ quantity: 50,
+ },
+ Item {
+ profit: 100.0,
+ cost: 0.1,
+ cargo: 0.5,
+ quantity: 50,
+ },
+ Item {
+ profit: 100.0,
+ cost: 0.2,
+ cargo: 0.3,
+ quantity: 50,
+ },
+ ];
+
+ let max_cost = 50.0;
+ let max_cargo = 50.0;
+
+ let (max_profit, item_selections) = knapsack(&items, max_cost, max_cargo);
+ println!("Maximum profit: {}", max_profit);
+ for (index, quantity) in item_selections {
+ println!("Item {} taken with quantity: {}", index, quantity);
+ }
+}