Skip to main content

oxigdal_dev_tools/
generator.rs

1//! Test data generation utilities
2//!
3//! This module provides tools for generating test data for OxiGDAL operations.
4
5use crate::Result;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9/// Test data generator
10pub struct DataGenerator {
11    /// Random seed
12    seed: u64,
13}
14
15impl DataGenerator {
16    /// Create a new data generator
17    pub fn new() -> Self {
18        Self { seed: 12345 }
19    }
20
21    /// Create a generator with a specific seed
22    pub fn with_seed(seed: u64) -> Self {
23        Self { seed }
24    }
25
26    /// Generate raster data
27    pub fn generate_raster(&self, width: usize, height: usize, pattern: RasterPattern) -> Vec<f64> {
28        let mut data = vec![0.0; width * height];
29
30        match pattern {
31            RasterPattern::Flat(value) => {
32                data.fill(value);
33            }
34            RasterPattern::Gradient {
35                from,
36                to,
37                direction,
38            } => {
39                for y in 0..height {
40                    for x in 0..width {
41                        let t = match direction {
42                            GradientDirection::Horizontal => x as f64 / (width - 1) as f64,
43                            GradientDirection::Vertical => y as f64 / (height - 1) as f64,
44                            GradientDirection::Diagonal => {
45                                ((x + y) as f64) / ((width + height - 2) as f64)
46                            }
47                        };
48                        data[y * width + x] = from + (to - from) * t;
49                    }
50                }
51            }
52            RasterPattern::Checkerboard {
53                size,
54                color1,
55                color2,
56            } => {
57                for y in 0..height {
58                    for x in 0..width {
59                        let is_odd = ((x / size) + (y / size)) % 2 == 1;
60                        data[y * width + x] = if is_odd { color1 } else { color2 };
61                    }
62                }
63            }
64            RasterPattern::Noise { min, max } => {
65                for (i, item) in data.iter_mut().enumerate() {
66                    *item = min + (max - min) * self.pseudo_random(i);
67                }
68            }
69            RasterPattern::Sine {
70                amplitude,
71                frequency,
72            } => {
73                use std::f64::consts::PI;
74                for y in 0..height {
75                    for x in 0..width {
76                        let phase = 2.0 * PI * frequency * (x as f64 / width as f64);
77                        data[y * width + x] = amplitude * phase.sin();
78                    }
79                }
80            }
81        }
82
83        data
84    }
85
86    /// Simple pseudo-random number generator (LCG)
87    fn pseudo_random(&self, index: usize) -> f64 {
88        let a = 1103515245u64;
89        let c = 12345u64;
90        let m = 2u64.pow(31);
91
92        let x = ((a
93            .wrapping_mul(self.seed.wrapping_add(index as u64))
94            .wrapping_add(c))
95            % m) as f64;
96        x / m as f64
97    }
98
99    /// Generate vector features (points)
100    pub fn generate_points(&self, count: usize, bounds: Bounds) -> Vec<Point> {
101        let mut points = Vec::with_capacity(count);
102
103        for i in 0..count {
104            let x = bounds.min_x + (bounds.max_x - bounds.min_x) * self.pseudo_random(i * 2);
105            let y = bounds.min_y + (bounds.max_y - bounds.min_y) * self.pseudo_random(i * 2 + 1);
106
107            points.push(Point { x, y });
108        }
109
110        points
111    }
112
113    /// Generate regular grid of points
114    pub fn generate_grid(&self, rows: usize, cols: usize, bounds: Bounds) -> Vec<Point> {
115        let mut points = Vec::with_capacity(rows * cols);
116
117        let dx = (bounds.max_x - bounds.min_x) / (cols - 1) as f64;
118        let dy = (bounds.max_y - bounds.min_y) / (rows - 1) as f64;
119
120        for row in 0..rows {
121            for col in 0..cols {
122                let x = bounds.min_x + col as f64 * dx;
123                let y = bounds.min_y + row as f64 * dy;
124                points.push(Point { x, y });
125            }
126        }
127
128        points
129    }
130}
131
132impl Default for DataGenerator {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138/// Raster pattern
139#[derive(Debug, Clone)]
140pub enum RasterPattern {
141    /// Flat value
142    Flat(f64),
143    /// Gradient
144    Gradient {
145        /// Start value
146        from: f64,
147        /// End value
148        to: f64,
149        /// Direction
150        direction: GradientDirection,
151    },
152    /// Checkerboard pattern
153    Checkerboard {
154        /// Cell size
155        size: usize,
156        /// Color 1
157        color1: f64,
158        /// Color 2
159        color2: f64,
160    },
161    /// Random noise
162    Noise {
163        /// Minimum value
164        min: f64,
165        /// Maximum value
166        max: f64,
167    },
168    /// Sine wave
169    Sine {
170        /// Amplitude
171        amplitude: f64,
172        /// Frequency
173        frequency: f64,
174    },
175}
176
177/// Gradient direction
178#[derive(Debug, Clone, Copy)]
179pub enum GradientDirection {
180    /// Horizontal (left to right)
181    Horizontal,
182    /// Vertical (top to bottom)
183    Vertical,
184    /// Diagonal (top-left to bottom-right)
185    Diagonal,
186}
187
188/// 2D point
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct Point {
191    /// X coordinate
192    pub x: f64,
193    /// Y coordinate
194    pub y: f64,
195}
196
197/// Bounding box
198#[derive(Debug, Clone, Copy)]
199pub struct Bounds {
200    /// Minimum X
201    pub min_x: f64,
202    /// Minimum Y
203    pub min_y: f64,
204    /// Maximum X
205    pub max_x: f64,
206    /// Maximum Y
207    pub max_y: f64,
208}
209
210impl Bounds {
211    /// Create new bounds
212    pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
213        Self {
214            min_x,
215            min_y,
216            max_x,
217            max_y,
218        }
219    }
220}
221
222/// File generator for creating test files
223pub struct FileGenerator;
224
225impl FileGenerator {
226    /// Generate a minimal valid single-band Float32 GeoTIFF with WGS84 georeferencing.
227    ///
228    /// The file is written using the `oxigdal-geotiff` driver so it is a fully
229    /// conformant TIFF/GeoTIFF that can be re-opened by any compliant reader.
230    ///
231    /// # Arguments
232    /// * `path`   – Destination file path.
233    /// * `width`  – Image width in pixels (must be ≥ 1).
234    /// * `height` – Image height in pixels (must be ≥ 1).
235    ///
236    /// The georeferencing covers a small WGS84 bounding box centred on the
237    /// prime meridian / equator (lon 0..1°, lat 1..0° — north-up).
238    pub fn generate_geotiff(path: &Path, width: usize, height: usize) -> Result<()> {
239        use oxigdal_core::types::{GeoTransform, RasterDataType};
240        use oxigdal_geotiff::tiff::Compression;
241        use oxigdal_geotiff::writer::{GeoTiffWriter, GeoTiffWriterOptions, WriterConfig};
242
243        if width == 0 || height == 0 {
244            return Err(crate::DevToolsError::Generator(
245                "width and height must be >= 1".to_string(),
246            ));
247        }
248
249        // Build a simple gradient raster (Float32, 1 band).
250        // Values increase linearly from 0.0 at top-left to 1.0 at bottom-right.
251        let pixel_count = width * height;
252        let max_idx = (pixel_count - 1) as f32;
253        let float_data: Vec<f32> = (0..pixel_count)
254            .map(|i| i as f32 / max_idx.max(1.0))
255            .collect();
256
257        // Re-interpret as raw bytes for the writer (little-endian f32).
258        let raw: Vec<u8> = float_data.iter().flat_map(|v| v.to_le_bytes()).collect();
259
260        // WGS84 bounding box: upper-left (0°E, 1°N), lower-right (1°E, 0°N).
261        // pixel_width  =  1.0 / width  degrees per pixel  (west→east)
262        // pixel_height = -1.0 / height degrees per pixel  (north→south, negative)
263        let pixel_width = 1.0_f64 / width as f64;
264        let pixel_height = -1.0_f64 / height as f64;
265        let geo_transform = GeoTransform::new(
266            0.0,          // origin_x  (upper-left longitude)
267            pixel_width,  // pixel_width
268            0.0,          // row_rotation (north-up → 0)
269            1.0,          // origin_y  (upper-left latitude)
270            0.0,          // col_rotation (north-up → 0)
271            pixel_height, // pixel_height (negative = north-up)
272        );
273
274        let config = WriterConfig::new(
275            width as u64,
276            height as u64,
277            1, // single band
278            RasterDataType::Float32,
279        )
280        .with_compression(Compression::Lzw)
281        .with_geo_transform(geo_transform)
282        .with_epsg_code(4326); // WGS84 geographic CRS
283
284        let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())?;
285        writer.write(&raw)?;
286
287        Ok(())
288    }
289
290    /// Generate a simple GeoJSON file
291    pub fn generate_geojson(path: &Path, points: &[Point]) -> Result<()> {
292        use std::io::Write;
293
294        let mut geojson =
295            String::from("{\n  \"type\": \"FeatureCollection\",\n  \"features\": [\n");
296
297        for (i, point) in points.iter().enumerate() {
298            geojson.push_str(&format!(
299                "    {{\n      \"type\": \"Feature\",\n      \"geometry\": {{\n        \"type\": \"Point\",\n        \"coordinates\": [{}, {}]\n      }},\n      \"properties\": {{\n        \"id\": {}\n      }}\n    }}",
300                point.x, point.y, i
301            ));
302
303            if i < points.len() - 1 {
304                geojson.push_str(",\n");
305            } else {
306                geojson.push('\n');
307            }
308        }
309
310        geojson.push_str("  ]\n}");
311
312        let mut file = std::fs::File::create(path)?;
313        file.write_all(geojson.as_bytes())?;
314
315        Ok(())
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn test_generator_creation() {
325        let generator = DataGenerator::new();
326        assert_eq!(generator.seed, 12345);
327    }
328
329    #[test]
330    fn test_generate_flat_raster() {
331        let generator = DataGenerator::new();
332        let data = generator.generate_raster(10, 10, RasterPattern::Flat(42.0));
333        assert_eq!(data.len(), 100);
334        assert!(data.iter().all(|&v| v == 42.0));
335    }
336
337    #[test]
338    fn test_generate_gradient_raster() {
339        let generator = DataGenerator::new();
340        let data = generator.generate_raster(
341            10,
342            10,
343            RasterPattern::Gradient {
344                from: 0.0,
345                to: 100.0,
346                direction: GradientDirection::Horizontal,
347            },
348        );
349        assert_eq!(data.len(), 100);
350        assert_eq!(data[0], 0.0); // First column
351        assert!((data[9] - 100.0).abs() < 0.01); // Last column
352    }
353
354    #[test]
355    fn test_generate_checkerboard() {
356        let generator = DataGenerator::new();
357        let data = generator.generate_raster(
358            10,
359            10,
360            RasterPattern::Checkerboard {
361                size: 5,
362                color1: 0.0,
363                color2: 100.0,
364            },
365        );
366        assert_eq!(data.len(), 100);
367    }
368
369    #[test]
370    fn test_generate_noise() {
371        let generator = DataGenerator::new();
372        let data = generator.generate_raster(
373            10,
374            10,
375            RasterPattern::Noise {
376                min: 0.0,
377                max: 100.0,
378            },
379        );
380        assert_eq!(data.len(), 100);
381        assert!(data.iter().all(|&v| (0.0..=100.0).contains(&v)));
382    }
383
384    #[test]
385    fn test_generate_points() {
386        let generator = DataGenerator::new();
387        let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
388        let points = generator.generate_points(10, bounds);
389        assert_eq!(points.len(), 10);
390        assert!(points.iter().all(|p| p.x >= 0.0 && p.x <= 100.0));
391        assert!(points.iter().all(|p| p.y >= 0.0 && p.y <= 100.0));
392    }
393
394    #[test]
395    fn test_generate_grid() {
396        let generator = DataGenerator::new();
397        let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
398        let points = generator.generate_grid(5, 5, bounds);
399        assert_eq!(points.len(), 25);
400    }
401
402    #[test]
403    fn test_generate_geojson() -> Result<()> {
404        use tempfile::NamedTempFile;
405
406        let temp_file = NamedTempFile::new()?;
407        let points = vec![Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 }];
408
409        FileGenerator::generate_geojson(temp_file.path(), &points)?;
410
411        let content = std::fs::read_to_string(temp_file.path())?;
412        assert!(content.contains("FeatureCollection"));
413        assert!(content.contains("Point"));
414
415        Ok(())
416    }
417
418    #[test]
419    fn test_generate_geotiff_creates_nonempty_file() -> Result<()> {
420        let mut path = std::env::temp_dir();
421        path.push(format!(
422            "oxigdal_devtools_test_{}.tif",
423            std::time::SystemTime::now()
424                .duration_since(std::time::UNIX_EPOCH)
425                .map(|d| d.as_nanos())
426                .unwrap_or(0)
427        ));
428
429        FileGenerator::generate_geotiff(&path, 32, 32)?;
430
431        let metadata = std::fs::metadata(&path)?;
432        assert!(metadata.is_file(), "output must be a regular file");
433        assert!(metadata.len() > 0, "generated GeoTIFF must be non-empty");
434
435        // Verify TIFF magic bytes (little-endian: II + 42 or II + 43 for BigTIFF).
436        let bytes = std::fs::read(&path)?;
437        assert!(bytes.len() >= 4, "file too short to contain a TIFF header");
438        let is_tiff_le = bytes[0] == b'I'
439            && bytes[1] == b'I'
440            && bytes[3] == 0
441            && (bytes[2] == 42 || bytes[2] == 43); // 42 = classic TIFF, 43 = BigTIFF
442        let is_tiff_be = bytes[0] == b'M' && bytes[1] == b'M' && (bytes[3] == 42 || bytes[3] == 43);
443        assert!(
444            is_tiff_le || is_tiff_be,
445            "file does not start with a valid TIFF magic sequence"
446        );
447
448        let _ = std::fs::remove_file(&path);
449        Ok(())
450    }
451
452    #[test]
453    fn test_generate_geotiff_rejects_zero_dimensions() {
454        let path = std::env::temp_dir().join("oxigdal_devtools_zero.tif");
455        assert!(
456            FileGenerator::generate_geotiff(&path, 0, 16).is_err(),
457            "zero width should return an error"
458        );
459        assert!(
460            FileGenerator::generate_geotiff(&path, 16, 0).is_err(),
461            "zero height should return an error"
462        );
463    }
464}