1use crate::cache::{CacheKey, TileCache};
7use crate::config::ImageFormat;
8use crate::dataset_registry::DatasetRegistry;
9use axum::{
10 extract::{Path, State},
11 http::{StatusCode, header},
12 response::{IntoResponse, Response},
13};
14use bytes::Bytes;
15use std::sync::Arc;
16use thiserror::Error;
17use tracing::{debug, trace};
18
19#[derive(Debug, Error)]
21pub enum TileError {
22 #[error("Layer not found: {0}")]
24 LayerNotFound(String),
25
26 #[error("Invalid tile coordinates")]
28 InvalidCoordinates,
29
30 #[error("Tile coordinates out of bounds")]
32 TileOutOfBounds,
33
34 #[error("Rendering error: {0}")]
36 Rendering(String),
37
38 #[error("Registry error: {0}")]
40 Registry(#[from] crate::dataset_registry::RegistryError),
41
42 #[error("Unsupported format: {0}")]
44 UnsupportedFormat(String),
45}
46
47impl IntoResponse for TileError {
48 fn into_response(self) -> Response {
49 let (status, message) = match self {
50 TileError::LayerNotFound(_) | TileError::TileOutOfBounds => {
51 (StatusCode::NOT_FOUND, self.to_string())
52 }
53 TileError::InvalidCoordinates => (StatusCode::BAD_REQUEST, self.to_string()),
54 _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
55 };
56
57 (status, [(header::CONTENT_TYPE, "text/plain")], message).into_response()
58 }
59}
60
61#[derive(Clone)]
63pub struct TileState {
64 pub registry: DatasetRegistry,
66
67 pub cache: TileCache,
69}
70
71#[derive(Debug)]
73pub struct TilePath {
74 pub layer: String,
76
77 pub z: u8,
79
80 pub x: u32,
82
83 pub y: u32,
85
86 pub format: String,
88}
89
90pub struct WebMercatorBounds {
92 pub z: u8,
94
95 pub x: u32,
97
98 pub y: u32,
100}
101
102impl WebMercatorBounds {
103 pub fn new(z: u8, x: u32, y: u32) -> Self {
105 Self { z, x, y }
106 }
107
108 pub fn num_tiles(&self) -> u32 {
110 1 << self.z
111 }
112
113 pub fn bbox(&self) -> (f64, f64, f64, f64) {
115 let n = self.num_tiles() as f64;
116 let size = 20037508.34278925 * 2.0;
117
118 let min_x = -20037508.34278925 + (self.x as f64 / n) * size;
119 let max_x = -20037508.34278925 + ((self.x + 1) as f64 / n) * size;
120 let min_y = 20037508.34278925 - ((self.y + 1) as f64 / n) * size;
121 let max_y = 20037508.34278925 - (self.y as f64 / n) * size;
122
123 (min_x, min_y, max_x, max_y)
124 }
125
126 pub fn bbox_wgs84(&self) -> (f64, f64, f64, f64) {
128 let (min_x, min_y, max_x, max_y) = self.bbox();
129
130 let min_lon = (min_x / 20037508.34278925) * 180.0;
132 let max_lon = (max_x / 20037508.34278925) * 180.0;
133
134 let min_lat = (min_y / 20037508.34278925) * 180.0;
135 let min_lat =
136 (2.0 * min_lat.to_radians().exp().atan() - std::f64::consts::PI / 2.0).to_degrees();
137
138 let max_lat = (max_y / 20037508.34278925) * 180.0;
139 let max_lat =
140 (2.0 * max_lat.to_radians().exp().atan() - std::f64::consts::PI / 2.0).to_degrees();
141
142 (min_lon, min_lat, max_lon, max_lat)
143 }
144
145 pub fn is_valid(&self) -> bool {
147 let max_tile = self.num_tiles();
148 self.x < max_tile && self.y < max_tile && self.z <= 30
149 }
150}
151
152pub async fn get_tile(
154 State(state): State<Arc<TileState>>,
155 Path((layer, z, x, y_with_ext)): Path<(String, u8, u32, String)>,
156) -> Result<Response, TileError> {
157 let (y, format) = parse_y_and_format(&y_with_ext)?;
159
160 debug!("XYZ tile request: {}/{}/{}/{}.{}", layer, z, x, y, format);
161
162 let bounds = WebMercatorBounds::new(z, x, y);
164 if !bounds.is_valid() {
165 return Err(TileError::InvalidCoordinates);
166 }
167
168 let cache_key = CacheKey::new(layer.clone(), z, x, y, format.clone());
170
171 if let Some(cached_tile) = state.cache.get(&cache_key) {
172 trace!("Cache hit for tile: {}", cache_key.to_string());
173 let image_format = parse_format(&format)?;
174 return Ok((
175 StatusCode::OK,
176 [(header::CONTENT_TYPE, image_format.mime_type())],
177 cached_tile,
178 )
179 .into_response());
180 }
181
182 let layer_info = state.registry.get_layer(&layer)?;
184
185 if z < layer_info.config.min_zoom || z > layer_info.config.max_zoom {
187 return Err(TileError::TileOutOfBounds);
188 }
189
190 let image_format = parse_format(&format)?;
192
193 if !layer_info.config.formats.contains(&image_format) {
195 return Err(TileError::UnsupportedFormat(format.clone()));
196 }
197
198 let dataset = state.registry.get_dataset(&layer)?;
200
201 let tile_data = render_tile(&dataset, &bounds, layer_info.config.tile_size, image_format)?;
203
204 let _ = state.cache.put(cache_key, tile_data.clone());
206
207 Ok((
208 StatusCode::OK,
209 [(header::CONTENT_TYPE, image_format.mime_type())],
210 tile_data,
211 )
212 .into_response())
213}
214
215fn parse_y_and_format(y_with_ext: &str) -> Result<(u32, String), TileError> {
217 let parts: Vec<&str> = y_with_ext.rsplitn(2, '.').collect();
218
219 if parts.len() != 2 {
220 return Err(TileError::InvalidCoordinates);
221 }
222
223 let format = parts[0].to_string();
224 let y = parts[1]
225 .parse::<u32>()
226 .map_err(|_| TileError::InvalidCoordinates)?;
227
228 Ok((y, format))
229}
230
231fn parse_format(ext: &str) -> Result<ImageFormat, TileError> {
233 ext.parse::<ImageFormat>()
234 .map_err(|_| TileError::UnsupportedFormat(ext.to_string()))
235}
236
237fn render_tile(
239 dataset: &Arc<crate::dataset_registry::Dataset>,
240 bounds: &WebMercatorBounds,
241 tile_size: u32,
242 format: ImageFormat,
243) -> Result<Bytes, TileError> {
244 debug!(
245 "Rendering tile: z={}, x={}, y={}, size={}x{}, format={:?}",
246 bounds.z, bounds.x, bounds.y, tile_size, tile_size, format
247 );
248
249 let (raster_width, raster_height) = dataset.raster_size();
251 let _band_count = dataset.raster_count();
252
253 let tile_bounds = calculate_tile_geographic_bounds(bounds);
255
256 let geotransform = dataset
258 .geotransform()
259 .map_err(|e| TileError::Rendering(e.to_string()))?;
260
261 let (src_x, src_y, src_width, src_height) =
263 calculate_pixel_window(&tile_bounds, &geotransform, raster_width, raster_height);
264
265 debug!(
266 "Pixel window: x={}, y={}, w={}, h={}",
267 src_x, src_y, src_width, src_height
268 );
269
270 let mut img_buffer = vec![0u8; (tile_size * tile_size * 4) as usize];
272
273 let checker_size = tile_size / 8;
275 for y in 0..tile_size {
276 for x in 0..tile_size {
277 let idx = ((y * tile_size + x) * 4) as usize;
278
279 let checker_x = (x / checker_size) % 2;
280 let checker_y = (y / checker_size) % 2;
281 let is_dark = (checker_x + checker_y) % 2 == 0;
282
283 let base_color: u8 = if is_dark { 100 } else { 200 };
284
285 let r = base_color.saturating_add((bounds.x % 50) as u8);
287 let g = base_color.saturating_add((bounds.y % 50) as u8);
288 let b = base_color.saturating_add(bounds.z.saturating_mul(10));
289
290 img_buffer[idx] = r;
291 img_buffer[idx + 1] = g;
292 img_buffer[idx + 2] = b;
293 img_buffer[idx + 3] = 255;
294 }
295 }
296
297 let encoded = match format {
299 ImageFormat::Png => encode_png(&img_buffer, tile_size, tile_size)?,
300 ImageFormat::Jpeg => encode_jpeg(&img_buffer, tile_size, tile_size)?,
301 ImageFormat::Webp => {
302 return Err(TileError::UnsupportedFormat(
303 "WebP not yet supported".to_string(),
304 ));
305 }
306 ImageFormat::Geotiff => {
307 return Err(TileError::UnsupportedFormat(
308 "GeoTIFF not supported for tiles".to_string(),
309 ));
310 }
311 };
312
313 Ok(Bytes::from(encoded))
314}
315
316fn encode_png(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, TileError> {
318 let mut output = Vec::new();
319 {
320 let mut encoder = png::Encoder::new(&mut output, width, height);
321 encoder.set_color(png::ColorType::Rgba);
322 encoder.set_depth(png::BitDepth::Eight);
323
324 let mut writer = encoder
325 .write_header()
326 .map_err(|e| TileError::Rendering(e.to_string()))?;
327
328 writer
329 .write_image_data(data)
330 .map_err(|e| TileError::Rendering(e.to_string()))?;
331 }
332
333 Ok(output)
334}
335
336fn encode_jpeg(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, TileError> {
338 let rgb_data: Vec<u8> = data
340 .chunks(4)
341 .flat_map(|rgba| &rgba[0..3])
342 .copied()
343 .collect();
344
345 let mut jpeg_buffer = Vec::new();
346 let mut encoder = jpeg_encoder::Encoder::new(&mut jpeg_buffer, 90);
347 encoder.set_progressive(true);
348 encoder
349 .encode(
350 &rgb_data,
351 width as u16,
352 height as u16,
353 jpeg_encoder::ColorType::Rgb,
354 )
355 .map_err(|e| TileError::Rendering(e.to_string()))?;
356
357 Ok(jpeg_buffer)
358}
359
360pub async fn get_tilejson(
362 State(state): State<Arc<TileState>>,
363 Path(layer): Path<String>,
364) -> Result<Response, TileError> {
365 debug!("TileJSON request for layer: {}", layer);
366
367 let layer_info = state.registry.get_layer(&layer)?;
369
370 let tilejson = serde_json::json!({
372 "tilejson": "2.2.0",
373 "name": layer_info.title,
374 "description": layer_info.abstract_,
375 "version": "1.0.0",
376 "scheme": "xyz",
377 "tiles": [
378 format!("/tiles/{}/{{z}}/{{x}}/{{y}}.png", layer)
379 ],
380 "minzoom": layer_info.config.min_zoom,
381 "maxzoom": layer_info.config.max_zoom,
382 "bounds": layer_info.metadata.bbox.map(|(min_x, min_y, max_x, max_y)| {
383 vec![min_x, min_y, max_x, max_y]
384 }).unwrap_or_else(|| vec![-180.0, -85.0511, 180.0, 85.0511]),
385 "center": layer_info.metadata.bbox.map(|(min_x, min_y, max_x, max_y)| {
386 let center_lon = (min_x + max_x) / 2.0;
387 let center_lat = (min_y + max_y) / 2.0;
388 let zoom = layer_info.config.min_zoom +
389 ((layer_info.config.max_zoom - layer_info.config.min_zoom) / 2);
390 vec![center_lon, center_lat, zoom as f64]
391 }),
392 });
393
394 Ok((
395 StatusCode::OK,
396 [(header::CONTENT_TYPE, "application/json")],
397 serde_json::to_string_pretty(&tilejson)
398 .map_err(|e: serde_json::Error| TileError::Rendering(e.to_string()))?,
399 )
400 .into_response())
401}
402
403fn calculate_tile_geographic_bounds(bounds: &WebMercatorBounds) -> GeographicBounds {
405 const EARTH_RADIUS: f64 = 6378137.0; let tile_size_meters = 2.0 * std::f64::consts::PI * EARTH_RADIUS / (1 << bounds.z) as f64;
409 let min_x_meters = bounds.x as f64 * tile_size_meters - std::f64::consts::PI * EARTH_RADIUS;
410 let max_y_meters = std::f64::consts::PI * EARTH_RADIUS - bounds.y as f64 * tile_size_meters;
411
412 let max_x_meters = min_x_meters + tile_size_meters;
413 let min_y_meters = max_y_meters - tile_size_meters;
414
415 let min_lon = (min_x_meters / EARTH_RADIUS).to_degrees();
417 let max_lon = (max_x_meters / EARTH_RADIUS).to_degrees();
418
419 let min_lat = ((std::f64::consts::PI / 2.0)
420 - 2.0 * ((-min_y_meters / EARTH_RADIUS).exp()).atan())
421 .to_degrees();
422 let max_lat = ((std::f64::consts::PI / 2.0)
423 - 2.0 * ((-max_y_meters / EARTH_RADIUS).exp()).atan())
424 .to_degrees();
425
426 GeographicBounds {
427 min_lon,
428 max_lon,
429 min_lat,
430 max_lat,
431 }
432}
433
434fn calculate_pixel_window(
436 bounds: &GeographicBounds,
437 geotransform: &[f64; 6],
438 raster_width: usize,
439 raster_height: usize,
440) -> (i32, i32, u32, u32) {
441 let x_origin = geotransform[0];
445 let pixel_width = geotransform[1];
446 let y_origin = geotransform[3];
447 let pixel_height = geotransform[5]; let x_min = ((bounds.min_lon - x_origin) / pixel_width).floor() as i32;
451 let x_max = ((bounds.max_lon - x_origin) / pixel_width).ceil() as i32;
452 let y_min = ((bounds.max_lat - y_origin) / pixel_height).floor() as i32;
453 let y_max = ((bounds.min_lat - y_origin) / pixel_height).ceil() as i32;
454
455 let x_min = x_min.max(0).min(raster_width as i32);
457 let x_max = x_max.max(0).min(raster_width as i32);
458 let y_min = y_min.max(0).min(raster_height as i32);
459 let y_max = y_max.max(0).min(raster_height as i32);
460
461 let width = (x_max - x_min).max(0) as u32;
462 let height = (y_max - y_min).max(0) as u32;
463
464 (x_min, y_min, width, height)
465}
466
467struct GeographicBounds {
469 min_lon: f64,
470 max_lon: f64,
471 min_lat: f64,
472 max_lat: f64,
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_web_mercator_bounds() {
481 let bounds = WebMercatorBounds::new(0, 0, 0);
483 assert_eq!(bounds.num_tiles(), 1);
484 assert!(bounds.is_valid());
485
486 let (min_x, min_y, max_x, max_y) = bounds.bbox();
487 assert!(min_x < max_x);
488 assert!(min_y < max_y);
489
490 let bounds = WebMercatorBounds::new(1, 0, 0);
492 assert_eq!(bounds.num_tiles(), 2);
493 assert!(bounds.is_valid());
494
495 let bounds = WebMercatorBounds::new(1, 2, 0);
497 assert!(!bounds.is_valid());
498
499 let bounds = WebMercatorBounds::new(1, 0, 2);
500 assert!(!bounds.is_valid());
501 }
502
503 #[test]
504 fn test_parse_y_and_format() {
505 assert_eq!(
506 parse_y_and_format("123.png").ok(),
507 Some((123, "png".to_string()))
508 );
509 assert_eq!(
510 parse_y_and_format("0.jpg").ok(),
511 Some((0, "jpg".to_string()))
512 );
513 assert_eq!(
514 parse_y_and_format("999.webp").ok(),
515 Some((999, "webp".to_string()))
516 );
517
518 assert!(parse_y_and_format("invalid").is_err());
519 assert!(parse_y_and_format("abc.png").is_err());
520 }
521
522 #[test]
523 fn test_parse_format() {
524 assert_eq!(parse_format("png").ok(), Some(ImageFormat::Png));
525 assert_eq!(parse_format("jpg").ok(), Some(ImageFormat::Jpeg));
526 assert_eq!(parse_format("jpeg").ok(), Some(ImageFormat::Jpeg));
527
528 assert!(parse_format("invalid").is_err());
529 }
530}