1use crate::cache::{TileCache, TileCacheConfig};
6use crate::config::Config;
7use crate::dataset_registry::DatasetRegistry;
8use crate::handlers::{
9 TileState, WmsState, WmtsState, get_feature_info, get_map, get_tile, get_tile_kvp,
10 get_tile_rest, get_tilejson, wms_get_capabilities, wmts_get_capabilities,
11};
12use crate::metrics::{AppMetrics, metrics_handler, track_http_metrics};
13use axum::{
14 Router,
15 extract::{DefaultBodyLimit, Request},
16 http::{Method, StatusCode, header},
17 middleware::{self, Next},
18 response::{Html, IntoResponse, Response},
19 routing::get,
20};
21use std::sync::Arc;
22use std::time::Duration;
23use thiserror::Error;
24use tower::ServiceBuilder;
25use tower_http::{
26 cors::{Any, CorsLayer},
27 trace::TraceLayer,
28};
29use tracing::{error, info};
30
31#[derive(Debug, Error)]
33pub enum ServerError {
34 #[error("Configuration error: {0}")]
36 Config(String),
37
38 #[error("Registry error: {0}")]
40 Registry(#[from] crate::dataset_registry::RegistryError),
41
42 #[error("HTTP server error: {0}")]
44 Http(String),
45
46 #[error("I/O error: {0}")]
48 Io(#[from] std::io::Error),
49}
50
51pub type ServerResult<T> = Result<T, ServerError>;
53
54pub struct TileServer {
56 config: Config,
58
59 registry: DatasetRegistry,
61
62 cache: TileCache,
64
65 metrics: AppMetrics,
67}
68
69impl TileServer {
70 pub fn new(config: Config) -> ServerResult<Self> {
72 let registry = DatasetRegistry::new();
74
75 registry
77 .register_layers(config.layers.clone())
78 .map_err(ServerError::Registry)?;
79
80 info!("Registered {} layers", registry.layer_count());
81
82 let cache_config = TileCacheConfig {
84 max_memory_bytes: config.cache.memory_size_mb * 1024 * 1024,
85 disk_cache_dir: config.cache.disk_cache.clone(),
86 ttl: Duration::from_secs(config.cache.ttl_seconds),
87 enable_stats: config.cache.enable_stats,
88 compression: config.cache.compression,
89 };
90
91 let cache = TileCache::new(cache_config);
92
93 Ok(Self {
94 config,
95 registry,
96 cache,
97 metrics: AppMetrics::new(),
98 })
99 }
100
101 pub fn build_router(&self) -> Router {
103 let service_url = format!(
104 "http://{}:{}",
105 self.config.server.host, self.config.server.port
106 );
107
108 let wms_state = Arc::new(WmsState {
110 registry: self.registry.clone(),
111 cache: self.cache.clone(),
112 service_url: service_url.clone(),
113 service_title: self.config.metadata.title.clone(),
114 service_abstract: self.config.metadata.abstract_.clone(),
115 });
116
117 let wmts_state = Arc::new(WmtsState {
119 registry: self.registry.clone(),
120 cache: self.cache.clone(),
121 service_url: service_url.clone(),
122 service_title: self.config.metadata.title.clone(),
123 service_abstract: self.config.metadata.abstract_.clone(),
124 });
125
126 let tile_state = Arc::new(TileState {
128 registry: self.registry.clone(),
129 cache: self.cache.clone(),
130 });
131
132 let cors = if self.config.server.enable_cors {
134 let mut cors = CorsLayer::new()
135 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
136 .allow_headers([header::CONTENT_TYPE, header::ACCEPT]);
137
138 cors = if self.config.server.cors_origins.is_empty() {
139 cors.allow_origin(Any)
140 } else {
141 let origins: Vec<_> = self
142 .config
143 .server
144 .cors_origins
145 .iter()
146 .filter_map(|o| o.parse().ok())
147 .collect();
148 cors.allow_origin(origins)
149 };
150
151 cors
152 } else {
153 CorsLayer::permissive()
154 };
155
156 let middleware = ServiceBuilder::new()
158 .layer(TraceLayer::new_for_http())
159 .layer(cors)
160 .layer(DefaultBodyLimit::max(self.config.server.max_request_size));
161
162 let timeout_duration = Duration::from_secs(self.config.server.timeout_seconds);
164
165 Router::new()
166 .route("/", get(home_handler))
168 .route("/health", get(health_handler))
170 .route("/stats", get(stats_handler))
172 .route("/wms", get(get_map).with_state(wms_state.clone()))
174 .route(
175 "/wms/capabilities",
176 get(wms_get_capabilities).with_state(wms_state.clone()),
177 )
178 .route(
179 "/wms/feature_info",
180 get(get_feature_info).with_state(wms_state),
181 )
182 .route("/wmts", get(get_tile_kvp).with_state(wmts_state.clone()))
184 .route(
185 "/wmts/capabilities",
186 get(wmts_get_capabilities).with_state(wmts_state.clone()),
187 )
188 .route(
189 "/wmts/1.0.0/{layer}/{tile_matrix_set}/{tile_matrix}/{tile_row}/{tile_col}",
193 get(get_tile_rest).with_state(wmts_state),
194 )
195 .route(
197 "/tiles/{layer}/{z}/{x}/{y}",
198 get(get_tile).with_state(tile_state.clone()),
199 )
200 .route(
201 "/tiles/{layer}/tilejson",
202 get(get_tilejson).with_state(tile_state),
203 )
204 .route_layer(middleware::from_fn_with_state(
207 self.metrics.clone(),
208 track_http_metrics,
209 ))
210 .layer(middleware)
211 .layer(middleware::from_fn(move |req, next| {
212 timeout_middleware(req, next, timeout_duration)
213 }))
214 }
215
216 fn build_metrics_router(&self) -> Router {
221 Router::new()
222 .route("/metrics", get(metrics_handler))
223 .with_state((self.metrics.clone(), self.cache.clone()))
224 }
225
226 pub async fn serve(self) -> ServerResult<()> {
234 let bind_addr = self.config.bind_address();
235 info!("Starting OxiGDAL tile server on {}", bind_addr);
236 info!("Service URL: {}", self.get_service_url());
237 info!("Workers: {}", self.config.server.workers);
238 info!("Cache: {} MB memory", self.config.cache.memory_size_mb);
239
240 if let Some(ref disk_cache) = self.config.cache.disk_cache {
241 info!("Disk cache: {}", disk_cache.display());
242 }
243
244 let app = self.build_router();
246
247 let listener = tokio::net::TcpListener::bind(&bind_addr)
249 .await
250 .map_err(|e| ServerError::Http(format!("Failed to bind to {}: {}", bind_addr, e)))?;
251
252 info!("Server listening on {}", bind_addr);
253 info!("Available endpoints:");
254 info!(" - WMS: http://{}/wms", bind_addr);
255 info!(" - WMTS: http://{}/wmts", bind_addr);
256 info!(
257 " - XYZ: http://{}/tiles/{{layer}}/{{z}}/{{x}}/{{y}}.png",
258 bind_addr
259 );
260 info!(" - Health: http://{}/health", bind_addr);
261 info!(" - Stats: http://{}/stats", bind_addr);
262
263 let app_future = async {
264 axum::serve(listener, app)
265 .await
266 .map_err(|e| ServerError::Http(e.to_string()))
267 };
268
269 if self.config.server.metrics_enabled {
270 let metrics_bind_addr = format!(
271 "{}:{}",
272 self.config.server.host, self.config.server.metrics_port
273 );
274 let metrics_app = self.build_metrics_router();
275
276 let metrics_listener = tokio::net::TcpListener::bind(&metrics_bind_addr)
277 .await
278 .map_err(|e| {
279 ServerError::Http(format!(
280 "Failed to bind metrics listener to {}: {}",
281 metrics_bind_addr, e
282 ))
283 })?;
284
285 info!("Metrics listening on http://{}/metrics", metrics_bind_addr);
286
287 let metrics_future = async {
288 axum::serve(metrics_listener, metrics_app)
289 .await
290 .map_err(|e| ServerError::Http(e.to_string()))
291 };
292
293 let (app_result, metrics_result) = tokio::join!(app_future, metrics_future);
294 app_result?;
295 metrics_result?;
296 } else {
297 info!("Metrics endpoint disabled (server.metrics_enabled = false)");
298 app_future.await?;
299 }
300
301 Ok(())
302 }
303
304 fn get_service_url(&self) -> String {
306 format!(
307 "http://{}:{}",
308 self.config.server.host, self.config.server.port
309 )
310 }
311
312 pub fn registry(&self) -> &DatasetRegistry {
314 &self.registry
315 }
316
317 pub fn cache(&self) -> &TileCache {
319 &self.cache
320 }
321
322 pub fn config(&self) -> &Config {
324 &self.config
325 }
326}
327
328async fn timeout_middleware(
330 req: Request,
331 next: Next,
332 duration: Duration,
333) -> Result<Response, StatusCode> {
334 match tokio::time::timeout(duration, next.run(req)).await {
335 Ok(response) => Ok(response),
336 Err(_) => {
337 error!("Request timeout after {:?}", duration);
338 Err(StatusCode::GATEWAY_TIMEOUT)
339 }
340 }
341}
342
343async fn home_handler() -> Html<&'static str> {
345 Html(
346 r#"<!DOCTYPE html>
347<html>
348<head>
349 <title>OxiGDAL Tile Server</title>
350 <style>
351 body {
352 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
353 max-width: 800px;
354 margin: 50px auto;
355 padding: 20px;
356 line-height: 1.6;
357 }
358 h1 { color: #2c3e50; }
359 h2 { color: #34495e; margin-top: 30px; }
360 code {
361 background: #f4f4f4;
362 padding: 2px 6px;
363 border-radius: 3px;
364 font-family: 'Courier New', monospace;
365 }
366 .endpoint {
367 background: #ecf0f1;
368 padding: 10px;
369 margin: 10px 0;
370 border-left: 4px solid #3498db;
371 }
372 a { color: #3498db; text-decoration: none; }
373 a:hover { text-decoration: underline; }
374 </style>
375</head>
376<body>
377 <h1>OxiGDAL Tile Server</h1>
378 <p>WMS/WMTS tile server powered by OxiGDAL - Pure Rust geospatial data access library.</p>
379
380 <h2>Available Endpoints</h2>
381
382 <div class="endpoint">
383 <h3>WMS (Web Map Service)</h3>
384 <p><a href="/wms?SERVICE=WMS&REQUEST=GetCapabilities">GetCapabilities</a></p>
385 <p>GetMap: <code>/wms?SERVICE=WMS&REQUEST=GetMap&LAYERS=layer&BBOX=...</code></p>
386 </div>
387
388 <div class="endpoint">
389 <h3>WMTS (Web Map Tile Service)</h3>
390 <p><a href="/wmts?SERVICE=WMTS&REQUEST=GetCapabilities">GetCapabilities</a></p>
391 <p>GetTile: <code>/wmts/1.0.0/{layer}/{tileMatrixSet}/{z}/{x}/{y}.png</code></p>
392 </div>
393
394 <div class="endpoint">
395 <h3>XYZ Tiles</h3>
396 <p>Tiles: <code>/tiles/{layer}/{z}/{x}/{y}.png</code></p>
397 <p>TileJSON: <code>/tiles/{layer}/tilejson</code></p>
398 </div>
399
400 <h2>Server Status</h2>
401 <p><a href="/health">Health Check</a> | <a href="/stats">Cache Statistics</a></p>
402
403 <h2>Documentation</h2>
404 <p>For more information, visit the <a href="https://github.com/cool-japan/oxigdal">OxiGDAL repository</a>.</p>
405</body>
406</html>
407"#,
408 )
409}
410
411async fn health_handler() -> Response {
413 (
414 StatusCode::OK,
415 [(header::CONTENT_TYPE, "application/json")],
416 r#"{"status":"healthy","service":"oxigdal-tile-server"}"#,
417 )
418 .into_response()
419}
420
421async fn stats_handler() -> Response {
423 let stats = serde_json::json!({
426 "status": "ok",
427 "message": "Cache statistics endpoint - requires state injection"
428 });
429
430 (
431 StatusCode::OK,
432 [(header::CONTENT_TYPE, "application/json")],
433 serde_json::to_string_pretty(&stats).unwrap_or_default(),
434 )
435 .into_response()
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_server_creation() {
444 let config = Config::default_config();
445 let result = TileServer::new(config);
446
447 assert!(result.is_ok());
450 }
451}