• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 19241805651

10 Nov 2025 06:22PM UTC coverage: 88.832%. First build
19241805651

Pull #1083

github

web-flow
Merge dac631b93 into 113de40ca
Pull Request #1083: feat: new gdal source workflow optimization

6213 of 7052 new or added lines in 71 files covered. (88.1%)

116189 of 130797 relevant lines covered (88.83%)

496404.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.38
/operators/src/source/csv.rs
1
use crate::engine::{
2
    CanonicOperatorName, InitializedVectorOperator, OperatorData, OperatorName, QueryContext,
3
    SourceOperator, TypedVectorQueryProcessor, VectorOperator, VectorQueryProcessor,
4
    VectorResultDescriptor,
5
};
6
use crate::engine::{QueryProcessor, WorkflowOperatorPath};
7
use crate::error;
8
use crate::optimization::OptimizationError;
9
use crate::util::{Result, safe_lock_mutex};
10
use async_trait::async_trait;
11
use csv::{Position, Reader, StringRecord};
12
use futures::stream::BoxStream;
13
use futures::task::{Context, Poll};
14
use futures::{Stream, StreamExt};
15
use geoengine_datatypes::collections::{
16
    BuilderProvider, GeoFeatureCollectionRowBuilder, MultiPointCollection, VectorDataType,
17
};
18
use geoengine_datatypes::dataset::NamedData;
19
use geoengine_datatypes::primitives::SpatialResolution;
20
use geoengine_datatypes::{
21
    primitives::{
22
        BoundingBox2D, ColumnSelection, Coordinate2D, TimeInterval, VectorQueryRectangle,
23
    },
24
    spatial_reference::SpatialReference,
25
};
26
use serde::{Deserialize, Serialize};
27
use snafu::{OptionExt, ResultExt, ensure};
28
use std::path::PathBuf;
29
use std::pin::Pin;
30
use std::sync::atomic::Ordering;
31
use std::sync::{Arc, Mutex};
32
use std::{fs::File, sync::atomic::AtomicBool};
33

34
/// Parameters for the CSV Source Operator
35
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
36
#[serde(rename_all = "camelCase")]
37
pub struct CsvSourceParameters {
38
    pub file_path: PathBuf,
39
    pub field_separator: char,
40
    pub geometry: CsvGeometrySpecification,
41
    #[serde(default)]
42
    pub time: CsvTimeSpecification,
43
}
44

45
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
46
#[serde(tag = "type", rename_all = "lowercase")]
47
pub enum CsvGeometrySpecification {
48
    #[allow(clippy::upper_case_acronyms)]
49
    XY { x: String, y: String },
50
}
51

52
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Default)]
53
pub enum CsvTimeSpecification {
54
    #[default]
55
    None,
56
}
57

58
enum ReaderState {
59
    Untouched(Reader<File>),
60
    OnGoing {
61
        header: ParsedHeader,
62
        records: csv::StringRecordsIntoIter<File>,
63
    },
64
    Error,
65
}
66

67
impl ReaderState {
68
    pub fn setup_once(&mut self, geometry_specification: CsvGeometrySpecification) -> Result<()> {
17✔
69
        if let ReaderState::Untouched(..) = self {
17✔
70
            // pass
7✔
71
        } else {
7✔
72
            return Ok(());
10✔
73
        }
74

75
        let old_state = std::mem::replace(self, ReaderState::Error);
7✔
76

77
        if let ReaderState::Untouched(mut csv_reader) = old_state {
7✔
78
            let header = CsvSourceStream::setup_read(geometry_specification, &mut csv_reader)?;
7✔
79

80
            let mut records = csv_reader.into_records();
6✔
81

82
            // consume the first row, which is the header
83
            if header.has_header {
6✔
84
                // TODO: throw error
6✔
85
                records.next();
6✔
86
            }
6✔
87

88
            *self = ReaderState::OnGoing { header, records }
6✔
89
        }
×
90

91
        Ok(())
6✔
92
    }
17✔
93
}
94

95
pub struct CsvSourceStream {
96
    parameters: CsvSourceParameters,
97
    bbox: BoundingBox2D,
98
    chunk_size: usize,
99
    reader_state: Arc<Mutex<ReaderState>>,
100
    thread_is_computing: Arc<AtomicBool>,
101
    #[allow(clippy::option_option)]
102
    poll_result: Arc<Mutex<Option<Option<Result<MultiPointCollection>>>>>,
103
}
104

105
pub type CsvSource = SourceOperator<CsvSourceParameters>;
106

107
impl OperatorName for CsvSource {
108
    const TYPE_NAME: &'static str = "CsvSource";
109
}
110

111
impl OperatorData for CsvSourceParameters {
112
    fn data_names_collect(&self, _data_names: &mut Vec<NamedData>) {}
×
113
}
114

115
#[typetag::serde]
×
116
#[async_trait]
117
impl VectorOperator for CsvSource {
118
    async fn _initialize(
119
        self: Box<Self>,
120
        path: WorkflowOperatorPath,
121
        _context: &dyn crate::engine::ExecutionContext,
122
    ) -> Result<Box<dyn InitializedVectorOperator>> {
4✔
123
        let initialized_source = InitializedCsvSource {
124
            name: CanonicOperatorName::from(&self),
125
            path,
126
            result_descriptor: VectorResultDescriptor {
127
                data_type: VectorDataType::MultiPoint, // TODO: get as user input
128
                spatial_reference: SpatialReference::epsg_4326().into(), // TODO: get as user input
129
                columns: Default::default(), // TODO: get when source allows loading other columns
130
                time: None,
131
                bbox: None,
132
            },
133
            state: self.params,
134
        };
135

136
        Ok(initialized_source.boxed())
137
    }
4✔
138

139
    span_fn!(CsvSource);
140
}
141

142
pub struct InitializedCsvSource {
143
    name: CanonicOperatorName,
144
    path: WorkflowOperatorPath,
145
    result_descriptor: VectorResultDescriptor,
146
    state: CsvSourceParameters,
147
}
148

149
impl InitializedVectorOperator for InitializedCsvSource {
150
    fn query_processor(&self) -> Result<crate::engine::TypedVectorQueryProcessor> {
3✔
151
        Ok(TypedVectorQueryProcessor::MultiPoint(
3✔
152
            CsvSourceProcessor {
3✔
153
                params: self.state.clone(),
3✔
154
                result_descriptor: self.result_descriptor.clone(),
3✔
155
            }
3✔
156
            .boxed(),
3✔
157
        ))
3✔
158
    }
3✔
159

160
    fn result_descriptor(&self) -> &VectorResultDescriptor {
4✔
161
        &self.result_descriptor
4✔
162
    }
4✔
163

164
    fn canonic_name(&self) -> CanonicOperatorName {
×
165
        self.name.clone()
×
166
    }
×
167

168
    fn name(&self) -> &'static str {
3✔
169
        CsvSource::TYPE_NAME
3✔
170
    }
3✔
171

172
    fn path(&self) -> WorkflowOperatorPath {
3✔
173
        self.path.clone()
3✔
174
    }
3✔
175

NEW
176
    fn optimize(
×
NEW
177
        &self,
×
NEW
178
        _target_resolution: SpatialResolution,
×
NEW
179
    ) -> Result<Box<dyn VectorOperator>, OptimizationError> {
×
NEW
180
        Ok(CsvSource {
×
NEW
181
            params: self.state.clone(),
×
NEW
182
        }
×
NEW
183
        .boxed())
×
NEW
184
    }
×
185
}
186

187
impl CsvSourceStream {
188
    /// Creates a new `CsvSource`
189
    ///
190
    /// # Errors
191
    ///
192
    /// This constructor fails if the delimiter is not an ASCII character.
193
    /// Furthermore, there are IO errors from the reader.
194
    ///
195
    // TODO: include time interval, e.g. QueryRectangle parameter
196
    pub fn new(
7✔
197
        parameters: CsvSourceParameters,
7✔
198
        bbox: BoundingBox2D,
7✔
199
        chunk_size: usize,
7✔
200
    ) -> Result<Self> {
7✔
201
        ensure!(
7✔
202
            parameters.field_separator.is_ascii(),
7✔
203
            error::CsvSource {
×
204
                details: "Delimiter must be ASCII character"
×
205
            }
×
206
        );
207

208
        Ok(Self {
209
            reader_state: Arc::new(Mutex::new(ReaderState::Untouched(
7✔
210
                csv::ReaderBuilder::new()
7✔
211
                    .delimiter(parameters.field_separator as u8)
7✔
212
                    .has_headers(true)
7✔
213
                    .from_path(parameters.file_path.as_path())
7✔
214
                    .context(error::CsvSourceReader {})?,
7✔
215
            ))),
216
            thread_is_computing: Arc::new(AtomicBool::new(false)),
7✔
217
            poll_result: Arc::new(Mutex::new(None)),
7✔
218
            parameters,
7✔
219
            bbox,
7✔
220
            chunk_size,
7✔
221
        })
222
    }
7✔
223

224
    fn setup_read(
7✔
225
        geometry_specification: CsvGeometrySpecification,
7✔
226
        csv_reader: &mut Reader<File>,
7✔
227
    ) -> Result<ParsedHeader> {
7✔
228
        csv_reader
7✔
229
            .seek(Position::new())
7✔
230
            .context(error::CsvSourceReader {})?; // start at beginning
7✔
231

232
        ensure!(
7✔
233
            csv_reader.has_headers(),
7✔
234
            error::CsvSource {
×
235
                details: "CSV file must contain header",
×
236
            }
×
237
        );
238

239
        let header = csv_reader.headers().context(error::CsvSourceReader)?;
7✔
240

241
        let CsvGeometrySpecification::XY { x, y } = geometry_specification;
7✔
242
        let x_index = header
7✔
243
            .iter()
7✔
244
            .position(|v| v == x)
7✔
245
            .context(error::CsvSource {
7✔
246
                details: "Cannot find x index in csv header",
7✔
247
            })?;
7✔
248
        let y_index = header
7✔
249
            .iter()
7✔
250
            .position(|v| v == y)
14✔
251
            .context(error::CsvSource {
7✔
252
                details: "Cannot find y index in csv header",
7✔
253
            })?;
7✔
254

255
        Ok(ParsedHeader {
6✔
256
            has_header: true,
6✔
257
            x_index,
6✔
258
            y_index,
6✔
259
        })
6✔
260
    }
7✔
261

262
    /// Parse a single CSV row
263
    fn parse_row(header: &ParsedHeader, row: &StringRecord) -> Result<ParsedRow> {
17✔
264
        let x: f64 = row
17✔
265
            .get(header.x_index)
17✔
266
            .context(error::CsvSource {
17✔
267
                details: "Cannot find x index key",
17✔
268
            })?
17✔
269
            .parse()
17✔
270
            .map_err(|_error| error::Error::CsvSource {
17✔
271
                details: "Cannot parse x coordinate".to_string(),
×
272
            })?;
×
273
        let y: f64 = row
17✔
274
            .get(header.y_index)
17✔
275
            .context(error::CsvSource {
17✔
276
                details: "Cannot find y index key",
17✔
277
            })?
17✔
278
            .parse()
17✔
279
            .map_err(|_error| error::Error::CsvSource {
17✔
280
                details: "Cannot parse y coordinate".to_string(),
×
281
            })?;
×
282

283
        Ok(ParsedRow {
17✔
284
            coordinate: (x, y).into(),
17✔
285
            time_interval: TimeInterval::default(),
17✔
286
        })
17✔
287
    }
17✔
288
}
289

290
impl Stream for CsvSourceStream {
291
    type Item = Result<MultiPointCollection>;
292

293
    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
34✔
294
        // TODO: handle lock poisoning on multiple occasions
295

296
        if self.thread_is_computing.load(Ordering::Relaxed) {
34✔
297
            return Poll::Pending;
×
298
        }
34✔
299

300
        let mut poll_result = safe_lock_mutex(&self.poll_result);
34✔
301
        if let Some(x) = poll_result.take() {
34✔
302
            return Poll::Ready(x);
17✔
303
        }
17✔
304

305
        self.thread_is_computing.store(true, Ordering::Relaxed);
17✔
306

307
        let is_working = self.thread_is_computing.clone();
17✔
308
        let reader_state = self.reader_state.clone();
17✔
309
        let poll_result = self.poll_result.clone();
17✔
310

311
        let bbox = self.bbox;
17✔
312
        let chunk_size = self.chunk_size;
17✔
313
        let parameters = self.parameters.clone();
17✔
314
        let waker = cx.waker().clone();
17✔
315

316
        crate::util::spawn_blocking(move || {
17✔
317
            let mut csv_reader = safe_lock_mutex(&reader_state);
17✔
318
            let computation_result = || -> Result<Option<MultiPointCollection>> {
17✔
319
                // TODO: is clone necessary?
320
                let geometry_specification = parameters.geometry.clone();
17✔
321
                csv_reader.setup_once(geometry_specification)?;
17✔
322

323
                let (header, records) = match &mut *csv_reader {
16✔
324
                    ReaderState::OnGoing { header, records } => (header, records),
15✔
325
                    ReaderState::Error => return Ok(None),
1✔
326
                    ReaderState::Untouched(_) => unreachable!(),
×
327
                };
328

329
                let mut builder = MultiPointCollection::builder().finish_header();
15✔
330
                let mut number_of_entries = 0; // TODO: add size/len to builder
15✔
331

332
                while number_of_entries < chunk_size {
32✔
333
                    let Some(record) = records.next() else {
29✔
334
                        break;
11✔
335
                    };
336

337
                    let row = record.context(error::CsvSourceReader)?;
18✔
338
                    let parsed_row = CsvSourceStream::parse_row(header, &row)?;
17✔
339

340
                    // TODO: filter time
341
                    if bbox.contains_coordinate(&parsed_row.coordinate) {
17✔
342
                        builder.push_geometry(parsed_row.coordinate.into());
16✔
343
                        builder.push_time_interval(parsed_row.time_interval);
16✔
344
                        builder.finish_row();
16✔
345

16✔
346
                        number_of_entries += 1;
16✔
347
                    }
16✔
348
                }
349

350
                // TODO: is this the correct cancellation criterion?
351
                if number_of_entries > 0 {
14✔
352
                    let collection = builder.build()?;
8✔
353
                    Ok(Some(collection))
8✔
354
                } else {
355
                    Ok(None)
6✔
356
                }
357
            }();
17✔
358

359
            *safe_lock_mutex(&poll_result) = Some(match computation_result {
17✔
360
                Ok(Some(collection)) => Some(Ok(collection)),
8✔
361
                Ok(None) => None,
7✔
362
                Err(e) => Some(Err(e)),
2✔
363
            });
364
            is_working.store(false, Ordering::Relaxed);
17✔
365

366
            waker.wake();
17✔
367
        });
17✔
368

369
        Poll::Pending
17✔
370
    }
34✔
371
}
372

373
#[derive(Debug)]
374
struct CsvSourceProcessor {
375
    params: CsvSourceParameters,
376
    result_descriptor: VectorResultDescriptor,
377
}
378

379
#[async_trait]
380
impl QueryProcessor for CsvSourceProcessor {
381
    type Output = MultiPointCollection;
382
    type SpatialBounds = BoundingBox2D;
383
    type Selection = ColumnSelection;
384
    type ResultDescription = VectorResultDescriptor;
385

386
    async fn _query<'a>(
387
        &'a self,
388
        query: VectorQueryRectangle,
389
        _ctx: &'a dyn QueryContext,
390
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
4✔
391
        // TODO: properly handle chunk_size
392
        Ok(CsvSourceStream::new(self.params.clone(), query.spatial_bounds(), 10)?.boxed())
393
    }
4✔
394

395
    fn result_descriptor(&self) -> &VectorResultDescriptor {
13✔
396
        &self.result_descriptor
13✔
397
    }
13✔
398
}
399

400
#[derive(Clone, Copy, Debug)]
401
struct ParsedHeader {
402
    pub has_header: bool,
403
    pub x_index: usize,
404
    pub y_index: usize,
405
}
406

407
struct ParsedRow {
408
    pub coordinate: Coordinate2D,
409
    pub time_interval: TimeInterval,
410
    // TODO: fields
411
}
412

413
#[cfg(test)]
414
mod tests {
415
    use super::*;
416
    use crate::engine::MockExecutionContext;
417
    use geoengine_datatypes::{
418
        collections::{FeatureCollectionInfos, ToGeoJson},
419
        util::test::TestDefault,
420
    };
421
    use std::io::{Seek, SeekFrom, Write};
422

423
    #[test]
424
    fn it_deserializes() {
1✔
425
        let json_string = r#"
1✔
426
            {
1✔
427
                "type": "CsvSource",
1✔
428
                "params": {
1✔
429
                    "filePath": "/foo/bar.csv",
1✔
430
                    "fieldSeparator": ",",
1✔
431
                    "geometry": {
1✔
432
                        "type": "xy",
1✔
433
                        "x": "x",
1✔
434
                        "y": "y"
1✔
435
                    }
1✔
436
                }
1✔
437
            }"#;
1✔
438

439
        let operator: CsvSource = serde_json::from_str(json_string).unwrap();
1✔
440

441
        assert_eq!(
1✔
442
            operator,
443
            CsvSource {
1✔
444
                params: CsvSourceParameters {
1✔
445
                    file_path: "/foo/bar.csv".into(),
1✔
446
                    field_separator: ',',
1✔
447
                    geometry: CsvGeometrySpecification::XY {
1✔
448
                        x: "x".into(),
1✔
449
                        y: "y".into()
1✔
450
                    },
1✔
451
                    time: CsvTimeSpecification::None,
1✔
452
                },
1✔
453
            }
1✔
454
        );
455
    }
1✔
456

457
    #[tokio::test]
458
    async fn read_points() {
1✔
459
        let mut fake_file = tempfile::NamedTempFile::new().unwrap();
1✔
460
        write!(
1✔
461
            fake_file,
1✔
462
            "\
1✔
463
x,y
1✔
464
0,1
1✔
465
2,3
1✔
466
4,5
1✔
467
"
1✔
468
        )
469
        .unwrap();
1✔
470
        fake_file.seek(SeekFrom::Start(0)).unwrap();
1✔
471

472
        let mut csv_source = CsvSourceStream::new(
1✔
473
            CsvSourceParameters {
1✔
474
                file_path: fake_file.path().into(),
1✔
475
                field_separator: ',',
1✔
476
                geometry: CsvGeometrySpecification::XY {
1✔
477
                    x: "x".into(),
1✔
478
                    y: "y".into(),
1✔
479
                },
1✔
480
                time: CsvTimeSpecification::None,
1✔
481
            },
1✔
482
            BoundingBox2D::new_unchecked((0., 0.).into(), (5., 5.).into()),
1✔
483
            2,
484
        )
485
        .unwrap();
1✔
486

487
        assert_eq!(csv_source.next().await.unwrap().unwrap().len(), 2);
1✔
488
        assert_eq!(csv_source.next().await.unwrap().unwrap().len(), 1);
1✔
489
        assert!(csv_source.next().await.is_none());
1✔
490
    }
1✔
491

492
    #[tokio::test]
493
    async fn erroneous_point_rows() {
1✔
494
        let mut fake_file = tempfile::NamedTempFile::new().unwrap();
1✔
495
        write!(
1✔
496
            fake_file,
1✔
497
            "\
1✔
498
x,y
1✔
499
0,1
1✔
500
CORRUPT
1✔
501
4,5
1✔
502
"
1✔
503
        )
504
        .unwrap();
1✔
505
        fake_file.seek(SeekFrom::Start(0)).unwrap();
1✔
506

507
        let mut csv_source = CsvSourceStream::new(
1✔
508
            CsvSourceParameters {
1✔
509
                file_path: fake_file.path().into(),
1✔
510
                field_separator: ',',
1✔
511
                geometry: CsvGeometrySpecification::XY {
1✔
512
                    x: "x".into(),
1✔
513
                    y: "y".into(),
1✔
514
                },
1✔
515
                time: CsvTimeSpecification::None,
1✔
516
            },
1✔
517
            BoundingBox2D::new_unchecked((0., 0.).into(), (5., 5.).into()),
1✔
518
            1,
519
        )
520
        .unwrap();
1✔
521

522
        assert_eq!(csv_source.next().await.unwrap().unwrap().len(), 1);
1✔
523
        assert!(csv_source.next().await.unwrap().is_err());
1✔
524
        assert_eq!(csv_source.next().await.unwrap().unwrap().len(), 1);
1✔
525
        assert!(csv_source.next().await.is_none());
1✔
526
    }
1✔
527

528
    #[tokio::test]
529
    async fn corrupt_point_header() {
1✔
530
        let mut fake_file = tempfile::NamedTempFile::new().unwrap();
1✔
531
        write!(
1✔
532
            fake_file,
1✔
533
            "\
1✔
534
x,z
1✔
535
0,1
1✔
536
2,3
1✔
537
4,5
1✔
538
"
1✔
539
        )
540
        .unwrap();
1✔
541
        fake_file.seek(SeekFrom::Start(0)).unwrap();
1✔
542

543
        let mut csv_source = CsvSourceStream::new(
1✔
544
            CsvSourceParameters {
1✔
545
                file_path: fake_file.path().into(),
1✔
546
                field_separator: ',',
1✔
547
                geometry: CsvGeometrySpecification::XY {
1✔
548
                    x: "x".into(),
1✔
549
                    y: "y".into(),
1✔
550
                },
1✔
551
                time: CsvTimeSpecification::None,
1✔
552
            },
1✔
553
            BoundingBox2D::new_unchecked((0., 0.).into(), (5., 5.).into()),
1✔
554
            1,
555
        )
556
        .unwrap();
1✔
557

558
        assert!(csv_source.next().await.unwrap().is_err());
1✔
559
        assert!(csv_source.next().await.is_none());
1✔
560
    }
1✔
561

562
    #[tokio::test]
563
    async fn processor() {
1✔
564
        let mut fake_file = tempfile::NamedTempFile::new().unwrap();
1✔
565
        write!(
1✔
566
            fake_file,
1✔
567
            "\
1✔
568
x,y
1✔
569
0,1
1✔
570
2,3
1✔
571
4,5
1✔
572
"
1✔
573
        )
574
        .unwrap();
1✔
575
        fake_file.seek(SeekFrom::Start(0)).unwrap();
1✔
576

577
        let params = CsvSourceParameters {
1✔
578
            file_path: fake_file.path().into(),
1✔
579
            field_separator: ',',
1✔
580
            geometry: CsvGeometrySpecification::XY {
1✔
581
                x: "x".into(),
1✔
582
                y: "y".into(),
1✔
583
            },
1✔
584
            time: CsvTimeSpecification::None,
1✔
585
        };
1✔
586

587
        let p = CsvSourceProcessor {
1✔
588
            params,
1✔
589
            result_descriptor: VectorResultDescriptor {
1✔
590
                data_type: VectorDataType::MultiPoint,
1✔
591
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
592
                columns: Default::default(),
1✔
593
                time: None,
1✔
594
                bbox: None,
1✔
595
            },
1✔
596
        };
1✔
597

598
        let query = VectorQueryRectangle::new(
1✔
599
            BoundingBox2D::new_unchecked(Coordinate2D::new(0., 0.), Coordinate2D::new(3., 3.)),
1✔
600
            TimeInterval::new_unchecked(0, 1),
1✔
601
            ColumnSelection::all(),
1✔
602
        );
603
        let ecx = MockExecutionContext::test_default();
1✔
604
        let ctx = ecx.mock_query_context((10 * 8 * 2).into());
1✔
605

606
        let r: Vec<Result<MultiPointCollection>> =
1✔
607
            p.query(query, &ctx).await.unwrap().collect().await;
1✔
608

609
        assert_eq!(r.len(), 1);
1✔
610

611
        assert_eq!(
1✔
612
            serde_json::from_str::<serde_json::Value>(&r[0].as_ref().unwrap().to_geo_json())
1✔
613
                .unwrap(),
1✔
614
            serde_json::json!({
1✔
615
                "type": "FeatureCollection",
1✔
616
                "features": [{
1✔
617
                    "type": "Feature",
1✔
618
                    "geometry": {
1✔
619
                        "type": "Point",
1✔
620
                        "coordinates": [0.0, 1.0]
1✔
621
                    },
1✔
622
                    "properties": {},
1✔
623
                    "when": {
1✔
624
                        "start": "-262143-01-01T00:00:00+00:00",
1✔
625
                        "end": "+262142-12-31T23:59:59.999+00:00",
1✔
626
                        "type": "Interval"
1✔
627
                    }
1✔
628
                }, {
1✔
629
                    "type": "Feature",
1✔
630
                    "geometry": {
1✔
631
                        "type": "Point",
1✔
632
                        "coordinates": [2.0, 3.0]
1✔
633
                    },
1✔
634
                    "properties": {},
1✔
635
                    "when": {
1✔
636
                        "start": "-262143-01-01T00:00:00+00:00",
1✔
637
                        "end": "+262142-12-31T23:59:59.999+00:00",
1✔
638
                        "type": "Interval"
1✔
639
                    }
1✔
640
                }]
1✔
641
            })
1✔
642
        );
1✔
643
    }
1✔
644

645
    #[test]
646
    fn operator() {
1✔
647
        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
1✔
648
        write!(
1✔
649
            temp_file,
1✔
650
            "\
1✔
651
x;y
1✔
652
0;1
1✔
653
2;3
1✔
654
4;5
1✔
655
"
1✔
656
        )
657
        .unwrap();
1✔
658
        temp_file.seek(SeekFrom::Start(0)).unwrap();
1✔
659

660
        let params = CsvSourceParameters {
1✔
661
            file_path: temp_file.path().into(),
1✔
662
            field_separator: ';',
1✔
663
            geometry: CsvGeometrySpecification::XY {
1✔
664
                x: "x".into(),
1✔
665
                y: "y".into(),
1✔
666
            },
1✔
667
            time: CsvTimeSpecification::None,
1✔
668
        };
1✔
669

670
        let operator = CsvSource { params }.boxed();
1✔
671

672
        let operator_json = serde_json::to_value(&operator).unwrap();
1✔
673

674
        assert_eq!(
1✔
675
            operator_json,
676
            serde_json::json!({
1✔
677
                "type": "CsvSource",
1✔
678
                "params": {
1✔
679
                    "filePath": temp_file.path(),
1✔
680
                    "fieldSeparator": ";",
1✔
681
                    "geometry": {
1✔
682
                        "type": "xy",
1✔
683
                        "x": "x",
1✔
684
                        "y": "y"
1✔
685
                    },
686
                    "time": "None"
1✔
687
                }
688
            })
689
        );
690

691
        let _operator: Box<dyn VectorOperator> = serde_json::from_value(operator_json).unwrap();
1✔
692
    }
1✔
693
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc