Skip to main content

everymap_core/domains/
imaging.rs

1use crate::error::EveryMapResult;
2use crate::types::Coordinate;
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6/// Options for static map image generation.
7#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct ImageOptions {
9    /// Image format (e.g., "png", "jpg")
10    pub format: Option<String>,
11    /// Preferred response language (BCP 47 language tag)
12    pub language: Option<String>,
13    /// Image width in pixels (overrides the `size` argument when set)
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub width: Option<u32>,
16    /// Image height in pixels (overrides the `size` argument when set)
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub height: Option<u32>,
19    /// Provider-specific options (HERE: style, poi, overlay; Google: maptype, markers, path)
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub provider_extra: Option<serde_json::Value>,
22}
23
24/// Simplified image response from the core trait.
25#[derive(Debug, Clone)]
26pub struct ImageResponse {
27    pub data: Vec<u8>,
28    pub content_type: Option<String>,
29}
30
31impl ImageResponse {
32    /// Create a new image response from raw bytes.
33    pub fn new(data: Vec<u8>, content_type: Option<String>) -> Self {
34        Self { data, content_type }
35    }
36}
37
38#[async_trait]
39pub trait MapImageProvider: Send + Sync {
40    async fn get_image(
41        &self,
42        center: &Coordinate,
43        zoom: u32,
44        size: (u32, u32),
45        options: &ImageOptions,
46    ) -> EveryMapResult<ImageResponse>;
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn test_image_options_default() {
55        let options = ImageOptions::default();
56        assert!(options.format.is_none());
57        assert!(options.language.is_none());
58        assert!(options.width.is_none());
59        assert!(options.height.is_none());
60        assert!(options.provider_extra.is_none());
61    }
62
63    #[test]
64    fn test_image_options_with_fields() {
65        let options = ImageOptions {
66            format: Some("png".to_string()),
67            language: Some("en".to_string()),
68            width: Some(1024),
69            height: Some(768),
70            provider_extra: Some(serde_json::json!({"style": "default"})),
71        };
72        assert_eq!(options.format, Some("png".to_string()));
73        assert_eq!(options.width, Some(1024));
74        assert_eq!(options.height, Some(768));
75    }
76
77    #[test]
78    fn test_image_options_serde_missing_fields() {
79        let back: ImageOptions = serde_json::from_str("{}").unwrap();
80        assert!(back.format.is_none());
81        assert!(back.language.is_none());
82        assert!(back.width.is_none());
83        assert!(back.height.is_none());
84        assert!(back.provider_extra.is_none());
85    }
86
87    #[test]
88    fn test_image_response_construction() {
89        let response =
90            ImageResponse::new(vec![0x89, 0x50, 0x4E, 0x47], Some("image/png".to_string()));
91        assert_eq!(response.data.len(), 4);
92        assert_eq!(response.content_type, Some("image/png".to_string()));
93    }
94
95    // --- ImageOptions serde roundtrip ---
96
97    #[test]
98    fn test_image_options_serde_roundtrip() {
99        let options = ImageOptions {
100            format: Some("jpg".to_string()),
101            language: Some("ja".to_string()),
102            width: Some(640),
103            height: Some(480),
104            provider_extra: Some(
105                serde_json::json!({"maptype": "satellite", "markers": [{"lat": 35.6762, "lng": 139.6503}]}),
106            ),
107        };
108        let json = serde_json::to_string(&options).unwrap();
109        let back: ImageOptions = serde_json::from_str(&json).unwrap();
110        assert_eq!(back.format, Some("jpg".to_string()));
111        assert_eq!(back.width, Some(640));
112        assert_eq!(back.height, Some(480));
113        assert!(back.provider_extra.is_some());
114    }
115
116    // --- ImageResponse edge cases ---
117
118    #[test]
119    fn test_image_response_empty_data() {
120        let response = ImageResponse::new(vec![], None);
121        assert!(response.data.is_empty());
122        assert!(response.content_type.is_none());
123    }
124
125    #[test]
126    fn test_image_response_no_content_type() {
127        let response = ImageResponse::new(vec![0xFF, 0xD8, 0xFF], None);
128        assert_eq!(response.data.len(), 3);
129        assert!(response.content_type.is_none());
130    }
131
132    #[test]
133    fn test_image_response_jpeg_content_type() {
134        let response =
135            ImageResponse::new(vec![0xFF, 0xD8, 0xFF, 0xE0], Some("image/jpeg".to_string()));
136        assert_eq!(response.content_type, Some("image/jpeg".to_string()));
137    }
138
139    #[test]
140    fn test_image_response_large_data() {
141        let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
142        let response = ImageResponse::new(data, Some("image/png".to_string()));
143        assert_eq!(response.data.len(), 1024);
144    }
145
146    // --- ImageResponse Clone ---
147
148    #[test]
149    fn test_image_response_clone() {
150        let response = ImageResponse::new(vec![1, 2, 3], Some("image/png".to_string()));
151        let cloned = response.clone();
152        assert_eq!(cloned.data, response.data);
153        assert_eq!(cloned.content_type, response.content_type);
154    }
155}