1use crate::{DevToolsError, 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) -> Result<Vec<Point>> {
126 if rows == 0 || cols == 0 {
127 return Err(DevToolsError::Generator(format!(
128 "generate_grid requires rows >= 1 and cols >= 1, got rows={rows}, cols={cols}"
129 )));
130 }
131
132 let mut points = Vec::with_capacity(rows * cols);
133
134 let dx = if cols > 1 {
135 (bounds.max_x - bounds.min_x) / (cols - 1) as f64
136 } else {
137 0.0
138 };
139 let dy = if rows > 1 {
140 (bounds.max_y - bounds.min_y) / (rows - 1) as f64
141 } else {
142 0.0
143 };
144
145 for row in 0..rows {
146 for col in 0..cols {
147 let x = bounds.min_x + col as f64 * dx;
148 let y = bounds.min_y + row as f64 * dy;
149 points.push(Point { x, y });
150 }
151 }
152
153 Ok(points)
154 }
155}
156
157impl Default for DataGenerator {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163#[derive(Debug, Clone)]
165pub enum RasterPattern {
166 Flat(f64),
168 Gradient {
170 from: f64,
172 to: f64,
174 direction: GradientDirection,
176 },
177 Checkerboard {
179 size: usize,
181 color1: f64,
183 color2: f64,
185 },
186 Noise {
188 min: f64,
190 max: f64,
192 },
193 Sine {
195 amplitude: f64,
197 frequency: f64,
199 },
200}
201
202#[derive(Debug, Clone, Copy)]
204pub enum GradientDirection {
205 Horizontal,
207 Vertical,
209 Diagonal,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct Point {
216 pub x: f64,
218 pub y: f64,
220}
221
222#[derive(Debug, Clone, Copy)]
224pub struct Bounds {
225 pub min_x: f64,
227 pub min_y: f64,
229 pub max_x: f64,
231 pub max_y: f64,
233}
234
235impl Bounds {
236 pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
238 Self {
239 min_x,
240 min_y,
241 max_x,
242 max_y,
243 }
244 }
245}
246
247pub struct FileGenerator;
249
250impl FileGenerator {
251 pub fn generate_geotiff(path: &Path, width: usize, height: usize) -> Result<()> {
264 use oxigeo_core::types::{GeoTransform, RasterDataType};
265 use oxigeo_geotiff::tiff::Compression;
266 use oxigeo_geotiff::writer::{GeoTiffWriter, GeoTiffWriterOptions, WriterConfig};
267
268 if width == 0 || height == 0 {
269 return Err(crate::DevToolsError::Generator(
270 "width and height must be >= 1".to_string(),
271 ));
272 }
273
274 let pixel_count = width * height;
277 let max_idx = (pixel_count - 1) as f32;
278 let float_data: Vec<f32> = (0..pixel_count)
279 .map(|i| i as f32 / max_idx.max(1.0))
280 .collect();
281
282 let raw: Vec<u8> = float_data.iter().flat_map(|v| v.to_le_bytes()).collect();
284
285 let pixel_width = 1.0_f64 / width as f64;
289 let pixel_height = -1.0_f64 / height as f64;
290 let geo_transform = GeoTransform::new(
291 0.0, pixel_width, 0.0, 1.0, 0.0, pixel_height, );
298
299 let config = WriterConfig::new(
300 width as u64,
301 height as u64,
302 1, RasterDataType::Float32,
304 )
305 .with_compression(Compression::Lzw)
306 .with_geo_transform(geo_transform)
307 .with_epsg_code(4326); let mut writer = GeoTiffWriter::create(path, config, GeoTiffWriterOptions::default())?;
310 writer.write(&raw)?;
311
312 Ok(())
313 }
314
315 pub fn generate_geojson(path: &Path, points: &[Point]) -> Result<()> {
317 use std::io::Write;
318
319 let mut geojson =
320 String::from("{\n \"type\": \"FeatureCollection\",\n \"features\": [\n");
321
322 for (i, point) in points.iter().enumerate() {
323 geojson.push_str(&format!(
324 " {{\n \"type\": \"Feature\",\n \"geometry\": {{\n \"type\": \"Point\",\n \"coordinates\": [{}, {}]\n }},\n \"properties\": {{\n \"id\": {}\n }}\n }}",
325 point.x, point.y, i
326 ));
327
328 if i < points.len() - 1 {
329 geojson.push_str(",\n");
330 } else {
331 geojson.push('\n');
332 }
333 }
334
335 geojson.push_str(" ]\n}");
336
337 let mut file = std::fs::File::create(path)?;
338 file.write_all(geojson.as_bytes())?;
339
340 Ok(())
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use std::sync::atomic::{AtomicU64, Ordering};
348
349 struct TempPath(std::path::PathBuf);
357
358 impl TempPath {
359 fn new(name: &str) -> Self {
360 static COUNTER: AtomicU64 = AtomicU64::new(0);
361 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
362 Self(std::env::temp_dir().join(format!(
363 "oxigeo_devtools_{}_{seq}_{name}",
364 std::process::id()
365 )))
366 }
367 }
368
369 impl std::ops::Deref for TempPath {
370 type Target = std::path::Path;
371
372 fn deref(&self) -> &std::path::Path {
373 &self.0
374 }
375 }
376
377 impl AsRef<std::path::Path> for TempPath {
378 fn as_ref(&self) -> &std::path::Path {
379 &self.0
380 }
381 }
382
383 impl Drop for TempPath {
384 fn drop(&mut self) {
385 let _ = std::fs::remove_file(&self.0);
386 }
387 }
388
389 #[test]
390 fn test_generator_creation() {
391 let generator = DataGenerator::new();
392 assert_eq!(generator.seed, 12345);
393 }
394
395 #[test]
396 fn test_generate_flat_raster() {
397 let generator = DataGenerator::new();
398 let data = generator.generate_raster(10, 10, RasterPattern::Flat(42.0));
399 assert_eq!(data.len(), 100);
400 assert!(data.iter().all(|&v| v == 42.0));
401 }
402
403 #[test]
404 fn test_generate_gradient_raster() {
405 let generator = DataGenerator::new();
406 let data = generator.generate_raster(
407 10,
408 10,
409 RasterPattern::Gradient {
410 from: 0.0,
411 to: 100.0,
412 direction: GradientDirection::Horizontal,
413 },
414 );
415 assert_eq!(data.len(), 100);
416 assert_eq!(data[0], 0.0); assert!((data[9] - 100.0).abs() < 0.01); }
419
420 #[test]
421 fn test_generate_checkerboard() {
422 let generator = DataGenerator::new();
423 let data = generator.generate_raster(
424 10,
425 10,
426 RasterPattern::Checkerboard {
427 size: 5,
428 color1: 0.0,
429 color2: 100.0,
430 },
431 );
432 assert_eq!(data.len(), 100);
433 }
434
435 #[test]
436 fn test_generate_noise() {
437 let generator = DataGenerator::new();
438 let data = generator.generate_raster(
439 10,
440 10,
441 RasterPattern::Noise {
442 min: 0.0,
443 max: 100.0,
444 },
445 );
446 assert_eq!(data.len(), 100);
447 assert!(data.iter().all(|&v| (0.0..=100.0).contains(&v)));
448 }
449
450 #[test]
451 fn test_generate_points() {
452 let generator = DataGenerator::new();
453 let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
454 let points = generator.generate_points(10, bounds);
455 assert_eq!(points.len(), 10);
456 assert!(points.iter().all(|p| p.x >= 0.0 && p.x <= 100.0));
457 assert!(points.iter().all(|p| p.y >= 0.0 && p.y <= 100.0));
458 }
459
460 #[test]
461 fn test_generate_grid() {
462 let generator = DataGenerator::new();
463 let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
464 let points = generator
465 .generate_grid(5, 5, bounds)
466 .expect("5x5 grid should succeed");
467 assert_eq!(points.len(), 25);
468 assert!(points.iter().all(|p| p.x.is_finite() && p.y.is_finite()));
469 }
470
471 #[test]
472 fn test_generate_grid_single_column_does_not_produce_nan_or_inf() {
473 let generator = DataGenerator::new();
474 let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
475 let points = generator
476 .generate_grid(5, 1, bounds)
477 .expect("single-column grid should succeed, not divide by zero");
478 assert_eq!(points.len(), 5);
479 assert!(
480 points.iter().all(|p| p.x.is_finite() && p.y.is_finite()),
481 "single-column grid must not produce NaN/inf coordinates: {points:?}"
482 );
483 assert!(points.iter().all(|p| (p.x - 0.0).abs() < 1e-9));
485 }
486
487 #[test]
488 fn test_generate_grid_single_row_does_not_produce_nan_or_inf() {
489 let generator = DataGenerator::new();
490 let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
491 let points = generator
492 .generate_grid(1, 5, bounds)
493 .expect("single-row grid should succeed, not divide by zero");
494 assert_eq!(points.len(), 5);
495 assert!(
496 points.iter().all(|p| p.x.is_finite() && p.y.is_finite()),
497 "single-row grid must not produce NaN/inf coordinates: {points:?}"
498 );
499 assert!(points.iter().all(|p| (p.y - 0.0).abs() < 1e-9));
500 }
501
502 #[test]
503 fn test_generate_grid_single_point() {
504 let generator = DataGenerator::new();
505 let bounds = Bounds::new(10.0, 20.0, 100.0, 100.0);
506 let points = generator
507 .generate_grid(1, 1, bounds)
508 .expect("1x1 grid should succeed");
509 assert_eq!(points.len(), 1);
510 assert!(points[0].x.is_finite() && points[0].y.is_finite());
511 assert!((points[0].x - 10.0).abs() < 1e-9);
512 assert!((points[0].y - 20.0).abs() < 1e-9);
513 }
514
515 #[test]
516 fn test_generate_grid_zero_rows_or_cols_errors_instead_of_panicking() {
517 let generator = DataGenerator::new();
518 let bounds = Bounds::new(0.0, 0.0, 100.0, 100.0);
519 assert!(generator.generate_grid(0, 5, bounds).is_err());
520 assert!(generator.generate_grid(5, 0, bounds).is_err());
521 assert!(generator.generate_grid(0, 0, bounds).is_err());
522 }
523
524 #[test]
525 fn test_generate_geojson() -> Result<()> {
526 use tempfile::NamedTempFile;
527
528 let temp_file = NamedTempFile::new()?;
529 let points = vec![Point { x: 0.0, y: 0.0 }, Point { x: 1.0, y: 1.0 }];
530
531 FileGenerator::generate_geojson(temp_file.path(), &points)?;
532
533 let content = std::fs::read_to_string(temp_file.path())?;
534 assert!(content.contains("FeatureCollection"));
535 assert!(content.contains("Point"));
536
537 Ok(())
538 }
539
540 #[test]
541 fn test_generate_geotiff_creates_nonempty_file() -> Result<()> {
542 let path = TempPath::new("nonempty.tif");
543
544 FileGenerator::generate_geotiff(&path, 32, 32)?;
545
546 let metadata = std::fs::metadata(&path)?;
547 assert!(metadata.is_file(), "output must be a regular file");
548 assert!(metadata.len() > 0, "generated GeoTIFF must be non-empty");
549
550 let bytes = std::fs::read(&path)?;
552 assert!(bytes.len() >= 4, "file too short to contain a TIFF header");
553 let is_tiff_le = bytes[0] == b'I'
554 && bytes[1] == b'I'
555 && bytes[3] == 0
556 && (bytes[2] == 42 || bytes[2] == 43); let is_tiff_be = bytes[0] == b'M' && bytes[1] == b'M' && (bytes[3] == 42 || bytes[3] == 43);
558 assert!(
559 is_tiff_le || is_tiff_be,
560 "file does not start with a valid TIFF magic sequence"
561 );
562
563 Ok(())
564 }
565
566 #[test]
567 fn test_generate_geotiff_rejects_zero_dimensions() {
568 let path = TempPath::new("zero.tif");
569 assert!(
570 FileGenerator::generate_geotiff(&path, 0, 16).is_err(),
571 "zero width should return an error"
572 );
573 assert!(
574 FileGenerator::generate_geotiff(&path, 16, 0).is_err(),
575 "zero height should return an error"
576 );
577 }
578}