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 axum::{
13 Router,
14 extract::{DefaultBodyLimit, Request},
15 http::{Method, StatusCode, header},
16 middleware::{self, Next},
17 response::{Html, IntoResponse, Response},
18 routing::get,
19};
20use std::sync::Arc;
21use std::time::Duration;
22use thiserror::Error;
23use tower::ServiceBuilder;
24use tower_http::{
25 cors::{Any, CorsLayer},
26 trace::TraceLayer,
27};
28use tracing::{error, info};
29
30#[derive(Debug, Error)]
32pub enum ServerError {
33 #[error("Configuration error: {0}")]
35 Config(String),
36
37 #[error("Registry error: {0}")]
39 Registry(#[from] crate::dataset_registry::RegistryError),
40
41 #[error("HTTP server error: {0}")]
43 Http(String),
44
45 #[error("I/O error: {0}")]
47 Io(#[from] std::io::Error),
48}
49
50pub type ServerResult<T> = Result<T, ServerError>;
52
53pub struct TileServer {
55 config: Config,
57
58 registry: DatasetRegistry,
60
61 cache: TileCache,
63}
64
65impl TileServer {
66 pub fn new(config: Config) -> ServerResult<Self> {
68 let registry = DatasetRegistry::new();
70
71 registry
73 .register_layers(config.layers.clone())
74 .map_err(ServerError::Registry)?;
75
76 info!("Registered {} layers", registry.layer_count());
77
78 let cache_config = TileCacheConfig {
80 max_memory_bytes: config.cache.memory_size_mb * 1024 * 1024,
81 disk_cache_dir: config.cache.disk_cache.clone(),
82 ttl: Duration::from_secs(config.cache.ttl_seconds),
83 enable_stats: config.cache.enable_stats,
84 compression: config.cache.compression,
85 };
86
87 let cache = TileCache::new(cache_config);
88
89 Ok(Self {
90 config,
91 registry,
92 cache,
93 })
94 }
95
96 pub fn build_router(&self) -> Router {
98 let service_url = format!(
99 "http://{}:{}",
100 self.config.server.host, self.config.server.port
101 );
102
103 let wms_state = Arc::new(WmsState {
105 registry: self.registry.clone(),
106 cache: self.cache.clone(),
107 service_url: service_url.clone(),
108 service_title: self.config.metadata.title.clone(),
109 service_abstract: self.config.metadata.abstract_.clone(),
110 });
111
112 let wmts_state = Arc::new(WmtsState {
114 registry: self.registry.clone(),
115 cache: self.cache.clone(),
116 service_url: service_url.clone(),
117 service_title: self.config.metadata.title.clone(),
118 service_abstract: self.config.metadata.abstract_.clone(),
119 });
120
121 let tile_state = Arc::new(TileState {
123 registry: self.registry.clone(),
124 cache: self.cache.clone(),
125 });
126
127 let cors = if self.config.server.enable_cors {
129 let mut cors = CorsLayer::new()
130 .allow_methods([Method::GET, Method::POST, Method::OPTIONS])
131 .allow_headers([header::CONTENT_TYPE, header::ACCEPT]);
132
133 cors = if self.config.server.cors_origins.is_empty() {
134 cors.allow_origin(Any)
135 } else {
136 let origins: Vec<_> = self
137 .config
138 .server
139 .cors_origins
140 .iter()
141 .filter_map(|o| o.parse().ok())
142 .collect();
143 cors.allow_origin(origins)
144 };
145
146 cors
147 } else {
148 CorsLayer::permissive()
149 };
150
151 let middleware = ServiceBuilder::new()
153 .layer(TraceLayer::new_for_http())
154 .layer(cors)
155 .layer(DefaultBodyLimit::max(self.config.server.max_request_size));
156
157 let timeout_duration = Duration::from_secs(self.config.server.timeout_seconds);
159
160 Router::new()
161 .route("/", get(home_handler))
163 .route("/health", get(health_handler))
165 .route("/stats", get(stats_handler))
167 .route("/wms", get(get_map).with_state(wms_state.clone()))
169 .route(
170 "/wms/capabilities",
171 get(wms_get_capabilities).with_state(wms_state.clone()),
172 )
173 .route(
174 "/wms/feature_info",
175 get(get_feature_info).with_state(wms_state),
176 )
177 .route("/wmts", get(get_tile_kvp).with_state(wmts_state.clone()))
179 .route(
180 "/wmts/capabilities",
181 get(wmts_get_capabilities).with_state(wmts_state.clone()),
182 )
183 .route(
184 "/wmts/1.0.0/:layer/:tile_matrix_set/:tile_matrix/:tile_row/:tile_col.png",
185 get(get_tile_rest).with_state(wmts_state),
186 )
187 .route(
189 "/tiles/:layer/:z/:x/:y",
190 get(get_tile).with_state(tile_state.clone()),
191 )
192 .route(
193 "/tiles/:layer/tilejson",
194 get(get_tilejson).with_state(tile_state),
195 )
196 .layer(middleware)
197 .layer(middleware::from_fn(move |req, next| {
198 timeout_middleware(req, next, timeout_duration)
199 }))
200 }
201
202 pub async fn serve(self) -> ServerResult<()> {
204 let bind_addr = self.config.bind_address();
205 info!("Starting OxiGDAL tile server on {}", bind_addr);
206 info!("Service URL: {}", self.get_service_url());
207 info!("Workers: {}", self.config.server.workers);
208 info!("Cache: {} MB memory", self.config.cache.memory_size_mb);
209
210 if let Some(ref disk_cache) = self.config.cache.disk_cache {
211 info!("Disk cache: {}", disk_cache.display());
212 }
213
214 let app = self.build_router();
216
217 let listener = tokio::net::TcpListener::bind(&bind_addr)
219 .await
220 .map_err(|e| ServerError::Http(format!("Failed to bind to {}: {}", bind_addr, e)))?;
221
222 info!("Server listening on {}", bind_addr);
223 info!("Available endpoints:");
224 info!(" - WMS: http://{}/wms", bind_addr);
225 info!(" - WMTS: http://{}/wmts", bind_addr);
226 info!(
227 " - XYZ: http://{}/tiles/{{layer}}/{{z}}/{{x}}/{{y}}.png",
228 bind_addr
229 );
230 info!(" - Health: http://{}/health", bind_addr);
231 info!(" - Stats: http://{}/stats", bind_addr);
232
233 axum::serve(listener, app)
235 .await
236 .map_err(|e| ServerError::Http(e.to_string()))?;
237
238 Ok(())
239 }
240
241 fn get_service_url(&self) -> String {
243 format!(
244 "http://{}:{}",
245 self.config.server.host, self.config.server.port
246 )
247 }
248
249 pub fn registry(&self) -> &DatasetRegistry {
251 &self.registry
252 }
253
254 pub fn cache(&self) -> &TileCache {
256 &self.cache
257 }
258
259 pub fn config(&self) -> &Config {
261 &self.config
262 }
263}
264
265async fn timeout_middleware(
267 req: Request,
268 next: Next,
269 duration: Duration,
270) -> Result<Response, StatusCode> {
271 match tokio::time::timeout(duration, next.run(req)).await {
272 Ok(response) => Ok(response),
273 Err(_) => {
274 error!("Request timeout after {:?}", duration);
275 Err(StatusCode::GATEWAY_TIMEOUT)
276 }
277 }
278}
279
280async fn home_handler() -> Html<&'static str> {
282 Html(
283 r#"<!DOCTYPE html>
284<html>
285<head>
286 <title>OxiGDAL Tile Server</title>
287 <style>
288 body {
289 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
290 max-width: 800px;
291 margin: 50px auto;
292 padding: 20px;
293 line-height: 1.6;
294 }
295 h1 { color: #2c3e50; }
296 h2 { color: #34495e; margin-top: 30px; }
297 code {
298 background: #f4f4f4;
299 padding: 2px 6px;
300 border-radius: 3px;
301 font-family: 'Courier New', monospace;
302 }
303 .endpoint {
304 background: #ecf0f1;
305 padding: 10px;
306 margin: 10px 0;
307 border-left: 4px solid #3498db;
308 }
309 a { color: #3498db; text-decoration: none; }
310 a:hover { text-decoration: underline; }
311 </style>
312</head>
313<body>
314 <h1>OxiGDAL Tile Server</h1>
315 <p>WMS/WMTS tile server powered by OxiGDAL - Pure Rust geospatial data access library.</p>
316
317 <h2>Available Endpoints</h2>
318
319 <div class="endpoint">
320 <h3>WMS (Web Map Service)</h3>
321 <p><a href="/wms?SERVICE=WMS&REQUEST=GetCapabilities">GetCapabilities</a></p>
322 <p>GetMap: <code>/wms?SERVICE=WMS&REQUEST=GetMap&LAYERS=layer&BBOX=...</code></p>
323 </div>
324
325 <div class="endpoint">
326 <h3>WMTS (Web Map Tile Service)</h3>
327 <p><a href="/wmts?SERVICE=WMTS&REQUEST=GetCapabilities">GetCapabilities</a></p>
328 <p>GetTile: <code>/wmts/1.0.0/{layer}/{tileMatrixSet}/{z}/{x}/{y}.png</code></p>
329 </div>
330
331 <div class="endpoint">
332 <h3>XYZ Tiles</h3>
333 <p>Tiles: <code>/tiles/{layer}/{z}/{x}/{y}.png</code></p>
334 <p>TileJSON: <code>/tiles/{layer}/tilejson</code></p>
335 </div>
336
337 <h2>Server Status</h2>
338 <p><a href="/health">Health Check</a> | <a href="/stats">Cache Statistics</a></p>
339
340 <h2>Documentation</h2>
341 <p>For more information, visit the <a href="https://github.com/cool-japan/oxigdal">OxiGDAL repository</a>.</p>
342</body>
343</html>
344"#,
345 )
346}
347
348async fn health_handler() -> Response {
350 (
351 StatusCode::OK,
352 [(header::CONTENT_TYPE, "application/json")],
353 r#"{"status":"healthy","service":"oxigdal-tile-server"}"#,
354 )
355 .into_response()
356}
357
358async fn stats_handler() -> Response {
360 let stats = serde_json::json!({
363 "status": "ok",
364 "message": "Cache statistics endpoint - requires state injection"
365 });
366
367 (
368 StatusCode::OK,
369 [(header::CONTENT_TYPE, "application/json")],
370 serde_json::to_string_pretty(&stats).unwrap_or_default(),
371 )
372 .into_response()
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn test_server_creation() {
381 let config = Config::default_config();
382 let result = TileServer::new(config);
383
384 assert!(result.is_ok());
387 }
388}