use crate::cache::{TileCache, TileCacheConfig};
use crate::config::Config;
use crate::dataset_registry::DatasetRegistry;
use crate::handlers::{
TileState, WmsState, WmtsState, get_feature_info, get_map, get_tile, get_tile_kvp,
get_tile_rest, get_tilejson, wms_get_capabilities, wmts_get_capabilities,
};
use axum::{
Router,
extract::{DefaultBodyLimit, Request},
http::{Method, StatusCode, header},
middleware::{self, Next},
response::{Html, IntoResponse, Response},
routing::get,
};
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
trace::TraceLayer,
};
use tracing::{error, info};
#[derive(Debug, Error)]
pub enum ServerError {
#[error("Configuration error: {0}")]
Config(String),
#[error("Registry error: {0}")]
Registry(#[from] crate::dataset_registry::RegistryError),
#[error("HTTP server error: {0}")]
Http(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub type ServerResult<T> = Result<T, ServerError>;
pub struct TileServer {
config: Config,
registry: DatasetRegistry,
cache: TileCache,
}
impl TileServer {
pub fn new(config: Config) -> ServerResult<Self> {
let registry = DatasetRegistry::new();
registry
.register_layers(config.layers.clone())
.map_err(ServerError::Registry)?;
info!("Registered {} layers", registry.layer_count());
let cache_config = TileCacheConfig {
max_memory_bytes: config.cache.memory_size_mb * 1024 * 1024,
disk_cache_dir: config.cache.disk_cache.clone(),
ttl: Duration::from_secs(config.cache.ttl_seconds),
enable_stats: config.cache.enable_stats,
compression: config.cache.compression,
};
let cache = TileCache::new(cache_config);
Ok(Self {
config,
registry,
cache,
})
}
pub fn build_router(&self) -> Router {
let service_url = format!(
"http://{}:{}",
self.config.server.host, self.config.server.port
);
let wms_state = Arc::new(WmsState {
registry: self.registry.clone(),
cache: self.cache.clone(),
service_url: service_url.clone(),
service_title: self.config.metadata.title.clone(),
service_abstract: self.config.metadata.abstract_.clone(),
});
let wmts_state = Arc::new(WmtsState {
registry: self.registry.clone(),
cache: self.cache.clone(),
service_url: service_url.clone(),
service_title: self.config.metadata.title.clone(),
service_abstract: self.config.metadata.abstract_.clone(),
});
let tile_state = Arc::new(TileState {
registry: self.registry.clone(),
cache: self.cache.clone(),
});
let cors = if self.config.server.enable_cors {
let mut cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST, Method::OPTIONS])
.allow_headers([header::CONTENT_TYPE, header::ACCEPT]);
cors = if self.config.server.cors_origins.is_empty() {
cors.allow_origin(Any)
} else {
let origins: Vec<_> = self
.config
.server
.cors_origins
.iter()
.filter_map(|o| o.parse().ok())
.collect();
cors.allow_origin(origins)
};
cors
} else {
CorsLayer::permissive()
};
let middleware = ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(cors)
.layer(DefaultBodyLimit::max(self.config.server.max_request_size));
let timeout_duration = Duration::from_secs(self.config.server.timeout_seconds);
Router::new()
.route("/", get(home_handler))
.route("/health", get(health_handler))
.route("/stats", get(stats_handler))
.route("/wms", get(get_map).with_state(wms_state.clone()))
.route(
"/wms/capabilities",
get(wms_get_capabilities).with_state(wms_state.clone()),
)
.route(
"/wms/feature_info",
get(get_feature_info).with_state(wms_state),
)
.route("/wmts", get(get_tile_kvp).with_state(wmts_state.clone()))
.route(
"/wmts/capabilities",
get(wmts_get_capabilities).with_state(wmts_state.clone()),
)
.route(
"/wmts/1.0.0/:layer/:tile_matrix_set/:tile_matrix/:tile_row/:tile_col.png",
get(get_tile_rest).with_state(wmts_state),
)
.route(
"/tiles/:layer/:z/:x/:y",
get(get_tile).with_state(tile_state.clone()),
)
.route(
"/tiles/:layer/tilejson",
get(get_tilejson).with_state(tile_state),
)
.layer(middleware)
.layer(middleware::from_fn(move |req, next| {
timeout_middleware(req, next, timeout_duration)
}))
}
pub async fn serve(self) -> ServerResult<()> {
let bind_addr = self.config.bind_address();
info!("Starting OxiGDAL tile server on {}", bind_addr);
info!("Service URL: {}", self.get_service_url());
info!("Workers: {}", self.config.server.workers);
info!("Cache: {} MB memory", self.config.cache.memory_size_mb);
if let Some(ref disk_cache) = self.config.cache.disk_cache {
info!("Disk cache: {}", disk_cache.display());
}
let app = self.build_router();
let listener = tokio::net::TcpListener::bind(&bind_addr)
.await
.map_err(|e| ServerError::Http(format!("Failed to bind to {}: {}", bind_addr, e)))?;
info!("Server listening on {}", bind_addr);
info!("Available endpoints:");
info!(" - WMS: http://{}/wms", bind_addr);
info!(" - WMTS: http://{}/wmts", bind_addr);
info!(
" - XYZ: http://{}/tiles/{{layer}}/{{z}}/{{x}}/{{y}}.png",
bind_addr
);
info!(" - Health: http://{}/health", bind_addr);
info!(" - Stats: http://{}/stats", bind_addr);
axum::serve(listener, app)
.await
.map_err(|e| ServerError::Http(e.to_string()))?;
Ok(())
}
fn get_service_url(&self) -> String {
format!(
"http://{}:{}",
self.config.server.host, self.config.server.port
)
}
pub fn registry(&self) -> &DatasetRegistry {
&self.registry
}
pub fn cache(&self) -> &TileCache {
&self.cache
}
pub fn config(&self) -> &Config {
&self.config
}
}
async fn timeout_middleware(
req: Request,
next: Next,
duration: Duration,
) -> Result<Response, StatusCode> {
match tokio::time::timeout(duration, next.run(req)).await {
Ok(response) => Ok(response),
Err(_) => {
error!("Request timeout after {:?}", duration);
Err(StatusCode::GATEWAY_TIMEOUT)
}
}
}
async fn home_handler() -> Html<&'static str> {
Html(
r#"<!DOCTYPE html>
<html>
<head>
<title>OxiGDAL Tile Server</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
line-height: 1.6;
}
h1 { color: #2c3e50; }
h2 { color: #34495e; margin-top: 30px; }
code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
.endpoint {
background: #ecf0f1;
padding: 10px;
margin: 10px 0;
border-left: 4px solid #3498db;
}
a { color: #3498db; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>OxiGDAL Tile Server</h1>
<p>WMS/WMTS tile server powered by OxiGDAL - Pure Rust geospatial data access library.</p>
<h2>Available Endpoints</h2>
<div class="endpoint">
<h3>WMS (Web Map Service)</h3>
<p><a href="/wms?SERVICE=WMS&REQUEST=GetCapabilities">GetCapabilities</a></p>
<p>GetMap: <code>/wms?SERVICE=WMS&REQUEST=GetMap&LAYERS=layer&BBOX=...</code></p>
</div>
<div class="endpoint">
<h3>WMTS (Web Map Tile Service)</h3>
<p><a href="/wmts?SERVICE=WMTS&REQUEST=GetCapabilities">GetCapabilities</a></p>
<p>GetTile: <code>/wmts/1.0.0/{layer}/{tileMatrixSet}/{z}/{x}/{y}.png</code></p>
</div>
<div class="endpoint">
<h3>XYZ Tiles</h3>
<p>Tiles: <code>/tiles/{layer}/{z}/{x}/{y}.png</code></p>
<p>TileJSON: <code>/tiles/{layer}/tilejson</code></p>
</div>
<h2>Server Status</h2>
<p><a href="/health">Health Check</a> | <a href="/stats">Cache Statistics</a></p>
<h2>Documentation</h2>
<p>For more information, visit the <a href="https://github.com/cool-japan/oxigdal">OxiGDAL repository</a>.</p>
</body>
</html>
"#,
)
}
async fn health_handler() -> Response {
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
r#"{"status":"healthy","service":"oxigdal-tile-server"}"#,
)
.into_response()
}
async fn stats_handler() -> Response {
let stats = serde_json::json!({
"status": "ok",
"message": "Cache statistics endpoint - requires state injection"
});
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
serde_json::to_string_pretty(&stats).unwrap_or_default(),
)
.into_response()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_server_creation() {
let config = Config::default_config();
let result = TileServer::new(config);
assert!(result.is_ok());
}
}