first commit
This commit is contained in:
83
backend/src/db.rs
Normal file
83
backend/src/db.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use scylla::{Session, SessionBuilder};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn initialize_schema(session: &Session) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create keyspace
|
||||
session
|
||||
.query(
|
||||
"CREATE KEYSPACE IF NOT EXISTS map_data WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 }",
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create table for OSM nodes (points)
|
||||
// Partition by tile coordinates (zoom, x, y) for efficient retrieval
|
||||
// This is a simplified schema. Real OSM data is more complex.
|
||||
session
|
||||
.query(
|
||||
"CREATE TABLE IF NOT EXISTS map_data.nodes (
|
||||
zoom int,
|
||||
tile_x int,
|
||||
tile_y int,
|
||||
id bigint,
|
||||
lat double,
|
||||
lon double,
|
||||
tags map<text, text>,
|
||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||
)",
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
session
|
||||
.query(
|
||||
"CREATE TABLE IF NOT EXISTS map_data.ways (
|
||||
zoom int,
|
||||
tile_x int,
|
||||
tile_y int,
|
||||
id bigint,
|
||||
tags map<text, text>,
|
||||
points blob,
|
||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||
)",
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
session
|
||||
.query(
|
||||
"CREATE TABLE IF NOT EXISTS map_data.buildings (
|
||||
zoom int,
|
||||
tile_x int,
|
||||
tile_y int,
|
||||
id bigint,
|
||||
tags map<text, text>,
|
||||
points blob,
|
||||
PRIMARY KEY ((zoom, tile_x, tile_y), id)
|
||||
)",
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Schema initialized.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn seed_data(session: &Session) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Insert some dummy data for Munich (approx lat/lon)
|
||||
// Munich is roughly at lat 48.1351, lon 11.5820
|
||||
// At zoom 10, this falls into a specific tile.
|
||||
// For simplicity, we'll just use a fixed tile coordinate for testing: 10/500/500 (not accurate, just for ID)
|
||||
|
||||
let insert_stmt = "INSERT INTO map_data.nodes (zoom, tile_x, tile_y, id, lat, lon, tags) VALUES (?, ?, ?, ?, ?, ?, ?)";
|
||||
let prepared = session.prepare(insert_stmt).await?;
|
||||
|
||||
// Point 1: Marienplatz
|
||||
session.execute(&prepared, (10, 500, 500, 1_i64, 48.137, 11.575, std::collections::HashMap::from([("name".to_string(), "Marienplatz".to_string())]))).await?;
|
||||
|
||||
// Point 2: English Garden
|
||||
session.execute(&prepared, (10, 500, 500, 2_i64, 48.150, 11.590, std::collections::HashMap::from([("name".to_string(), "English Garden".to_string())]))).await?;
|
||||
|
||||
println!("Test data seeded.");
|
||||
Ok(())
|
||||
}
|
||||
146
backend/src/main.rs
Normal file
146
backend/src/main.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
mod db;
|
||||
|
||||
use axum::{
|
||||
routing::get,
|
||||
Router,
|
||||
extract::{State, Path},
|
||||
Json,
|
||||
};
|
||||
use scylla::{Session, SessionBuilder};
|
||||
use std::sync::Arc;
|
||||
use tower_http::services::ServeDir;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use serde::Serialize;
|
||||
|
||||
struct AppState {
|
||||
scylla_session: Arc<Session>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize tracing
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
println!("Connecting to ScyllaDB...");
|
||||
let uri = std::env::var("SCYLLA_URI").unwrap_or_else(|_| "127.0.0.1:9042".to_string());
|
||||
|
||||
let session = SessionBuilder::new()
|
||||
.known_node(uri)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Initialize schema and seed data
|
||||
db::initialize_schema(&session).await?;
|
||||
db::seed_data(&session).await?;
|
||||
|
||||
let session = Arc::new(session);
|
||||
println!("Connected to ScyllaDB!");
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
scylla_session: session,
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_check))
|
||||
.route("/api/tiles/:z/:x/:y", get(get_tile))
|
||||
.route("/api/tiles/:z/:x/:y/ways", get(get_tile_ways))
|
||||
.route("/api/tiles/:z/:x/:y/buildings", get(get_tile_buildings))
|
||||
.nest_service("/", ServeDir::new("static"))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
|
||||
println!("Server listening on {}", listener.local_addr()?);
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MapNode {
|
||||
id: i64,
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
tags: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
async fn get_tile(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<Vec<MapNode>> {
|
||||
let query = "SELECT id, lat, lon, tags FROM map_data.nodes WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
|
||||
let rows = state.scylla_session.query(query, (z, x, y)).await.unwrap().rows.unwrap_or_default();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
for row in rows {
|
||||
let (id, lat, lon, tags) = row.into_typed::<(i64, f64, f64, std::collections::HashMap<String, String>)>().unwrap();
|
||||
nodes.push(MapNode { id, lat, lon, tags });
|
||||
}
|
||||
|
||||
Json(nodes)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct MapWay {
|
||||
id: i64,
|
||||
tags: std::collections::HashMap<String, String>,
|
||||
points: Vec<Vec<f64>>, // List of [lat, lon]
|
||||
}
|
||||
|
||||
async fn get_tile_ways(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<Vec<MapWay>> {
|
||||
let query = "SELECT id, tags, points FROM map_data.ways WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
|
||||
let rows = state.scylla_session.query(query, (z, x, y)).await.unwrap().rows.unwrap_or_default();
|
||||
|
||||
let mut ways = Vec::new();
|
||||
for row in rows {
|
||||
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>().unwrap();
|
||||
|
||||
// Deserialize points blob
|
||||
let mut points = Vec::new();
|
||||
for chunk in points_blob.chunks(16) {
|
||||
if chunk.len() == 16 {
|
||||
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
|
||||
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
|
||||
points.push(vec![lat, lon]);
|
||||
}
|
||||
}
|
||||
|
||||
ways.push(MapWay { id, tags, points });
|
||||
}
|
||||
|
||||
Json(ways)
|
||||
}
|
||||
|
||||
async fn get_tile_buildings(
|
||||
Path((z, x, y)): Path<(i32, i32, i32)>,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<Vec<MapWay>> {
|
||||
let query = "SELECT id, tags, points FROM map_data.buildings WHERE zoom = ? AND tile_x = ? AND tile_y = ?";
|
||||
let rows = state.scylla_session.query(query, (z, x, y)).await.unwrap().rows.unwrap_or_default();
|
||||
|
||||
let mut buildings = Vec::new();
|
||||
for row in rows {
|
||||
let (id, tags, points_blob) = row.into_typed::<(i64, std::collections::HashMap<String, String>, Vec<u8>)>().unwrap();
|
||||
|
||||
// Deserialize points blob
|
||||
let mut points = Vec::new();
|
||||
for chunk in points_blob.chunks(16) {
|
||||
if chunk.len() == 16 {
|
||||
let lat = f64::from_be_bytes(chunk[0..8].try_into().unwrap());
|
||||
let lon = f64::from_be_bytes(chunk[8..16].try_into().unwrap());
|
||||
points.push(vec![lat, lon]);
|
||||
}
|
||||
}
|
||||
|
||||
buildings.push(MapWay { id, tags, points });
|
||||
}
|
||||
|
||||
Json(buildings)
|
||||
}
|
||||
Reference in New Issue
Block a user