Skip to main content

oxigdal_server/
server.rs

1//! HTTP server implementation
2//!
3//! Sets up the Axum web server with all routes and middleware for serving tiles.
4
5use 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/// Server errors
31#[derive(Debug, Error)]
32pub enum ServerError {
33    /// Configuration error
34    #[error("Configuration error: {0}")]
35    Config(String),
36
37    /// Registry error
38    #[error("Registry error: {0}")]
39    Registry(#[from] crate::dataset_registry::RegistryError),
40
41    /// HTTP server error
42    #[error("HTTP server error: {0}")]
43    Http(String),
44
45    /// I/O error
46    #[error("I/O error: {0}")]
47    Io(#[from] std::io::Error),
48}
49
50/// Result type for server operations
51pub type ServerResult<T> = Result<T, ServerError>;
52
53/// Tile server instance
54pub struct TileServer {
55    /// Server configuration
56    config: Config,
57
58    /// Dataset registry
59    registry: DatasetRegistry,
60
61    /// Tile cache
62    cache: TileCache,
63}
64
65impl TileServer {
66    /// Create a new tile server
67    pub fn new(config: Config) -> ServerResult<Self> {
68        // Create dataset registry
69        let registry = DatasetRegistry::new();
70
71        // Register all configured layers
72        registry
73            .register_layers(config.layers.clone())
74            .map_err(ServerError::Registry)?;
75
76        info!("Registered {} layers", registry.layer_count());
77
78        // Create tile cache
79        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    /// Build the Axum router
97    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        // Create shared state for WMS
104        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        // Create shared state for WMTS
113        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        // Create shared state for XYZ tiles
122        let tile_state = Arc::new(TileState {
123            registry: self.registry.clone(),
124            cache: self.cache.clone(),
125        });
126
127        // Build CORS layer
128        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        // Build middleware stack
152        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        // Build routes with timeout middleware
158        let timeout_duration = Duration::from_secs(self.config.server.timeout_seconds);
159
160        Router::new()
161            // Home/landing page
162            .route("/", get(home_handler))
163            // Health check
164            .route("/health", get(health_handler))
165            // Cache stats
166            .route("/stats", get(stats_handler))
167            // WMS endpoints
168            .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            // WMTS endpoints
178            .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            // XYZ tile endpoints
188            .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    /// Start the server
203    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        // Build router
215        let app = self.build_router();
216
217        // Create TCP listener
218        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        // Serve with Axum
234        axum::serve(listener, app)
235            .await
236            .map_err(|e| ServerError::Http(e.to_string()))?;
237
238        Ok(())
239    }
240
241    /// Get the service URL
242    fn get_service_url(&self) -> String {
243        format!(
244            "http://{}:{}",
245            self.config.server.host, self.config.server.port
246        )
247    }
248
249    /// Get the dataset registry
250    pub fn registry(&self) -> &DatasetRegistry {
251        &self.registry
252    }
253
254    /// Get the tile cache
255    pub fn cache(&self) -> &TileCache {
256        &self.cache
257    }
258
259    /// Get the configuration
260    pub fn config(&self) -> &Config {
261        &self.config
262    }
263}
264
265/// Timeout middleware that wraps requests with a timeout
266async 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
280/// Home page handler
281async 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
348/// Health check handler
349async 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
358/// Cache statistics handler
359async fn stats_handler() -> Response {
360    // This is a placeholder - in a real implementation, we'd need to access the cache
361    // through shared state
362    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        // Server creation should succeed with default config
385        // (even though it has no layers)
386        assert!(result.is_ok());
387    }
388}