1use crate::Result;
6use serde::{Deserialize, Serialize};
7use std::path::Path;
8
9pub struct DataGenerator {
11 seed: u64,
13}
14
15impl DataGenerator {
16 pub fn new() -> Self {
18 Self { seed: 12345 }
19 }
20
21 pub fn with_seed(seed: u64) -> Self {
23 Self { seed }
24 }
25
26 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 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 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 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#[derive(Debug, Clone)]
140pub enum RasterPattern {
141 Flat(f64),
143 Gradient {
145 from: f64,
147 to: f64,
149 direction: GradientDirection,
151 },
152 Checkerboard {
154 size: usize,
156 color1: f64,
158 color2: f64,
160 },
161 Noise {
163 min: f64,
165 max: f64,
167 },
168 Sine {
170 amplitude: f64,
172 frequency: f64,
174 },
175}
176
177#[derive(Debug, Clone, Copy)]
179pub enum GradientDirection {
180 Horizontal,
182 Vertical,
184 Diagonal,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct Point {
191 pub x: f64,
193 pub y: f64,
195}
196
197#[derive(Debug, Clone, Copy)]
199pub struct Bounds {
200 pub min_x: f64,
202 pub min_y: f64,
204 pub max_x: f64,
206 pub max_y: f64,
208}
209
210impl Bounds {
211 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
222pub struct FileGenerator;
224
225impl FileGenerator {
226 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 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 let raw: Vec<u8> = float_data.iter().flat_map(|v| v.to_le_bytes()).collect();
259
260 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, pixel_width, 0.0, 1.0, 0.0, pixel_height, );
273
274 let config = WriterConfig::new(
275 width as u64,
276 height as u64,
277 1, RasterDataType::Float32,
279 )
280 .with_compression(Compression::Lzw)
281 .with_geo_transform(geo_transform)
282 .with_epsg_code(4326); let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())?;
285 writer.write(&raw)?;
286
287 Ok(())
288 }
289
290 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); assert!((data[9] - 100.0).abs() < 0.01); }
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 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); 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}