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

geo-engine / geoengine / 5869096654

15 Aug 2023 02:59PM UTC coverage: 89.79% (+0.3%) from 89.481%
5869096654

push

github

web-flow
Merge pull request #851 from geo-engine/pg-dataset-metadata-mapping

Pg dataset metadata mapping

1982 of 1982 new or added lines in 9 files covered. (100.0%)

106148 of 118218 relevant lines covered (89.79%)

61246.75 hits per line

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

74.44
/services/src/api/model/datatypes.rs
1
use crate::error::{self, Result};
2
use crate::identifier;
3
use fallible_iterator::FallibleIterator;
4
use geoengine_datatypes::primitives::{
5
    AxisAlignedRectangle, MultiLineStringAccess, MultiPointAccess, MultiPolygonAccess,
6
};
7
use ordered_float::NotNan;
8
use postgres_types::{FromSql, ToSql};
9
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
10
use snafu::ResultExt;
11
use std::{
12
    collections::{BTreeMap, HashMap},
13
    fmt::{Debug, Formatter},
14
    str::FromStr,
15
};
16
use utoipa::{IntoParams, ToSchema};
17

18
use super::{PolygonOwned, PolygonRef};
19

20
identifier!(DataProviderId);
×
21

22
impl From<DataProviderId> for geoengine_datatypes::dataset::DataProviderId {
23
    fn from(value: DataProviderId) -> Self {
24✔
24
        Self(value.0)
24✔
25
    }
24✔
26
}
27

28
// Identifier for datasets managed by Geo Engine
29
identifier!(DatasetId);
×
30

31
impl From<DatasetId> for geoengine_datatypes::dataset::DatasetId {
32
    fn from(value: DatasetId) -> Self {
94✔
33
        Self(value.0)
94✔
34
    }
94✔
35
}
36

37
#[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
17✔
38
#[serde(rename_all = "camelCase", tag = "type")]
39
/// The identifier for loadable data. It is used in the source operators to get the loading info (aka parametrization)
40
/// for accessing the data. Internal data is loaded from datasets, external from `DataProvider`s.
41
pub enum DataId {
42
    #[serde(rename_all = "camelCase")]
43
    Internal {
44
        dataset_id: DatasetId,
45
    },
46
    External(ExternalDataId),
47
}
48

49
impl DataId {
50
    pub fn internal(&self) -> Option<DatasetId> {
51
        if let Self::Internal {
52
            dataset_id: dataset,
×
53
        } = self
×
54
        {
55
            return Some(*dataset);
×
56
        }
×
57
        None
×
58
    }
×
59

60
    pub fn external(&self) -> Option<ExternalDataId> {
61
        if let Self::External(id) = self {
18✔
62
            return Some(id.clone());
18✔
63
        }
×
64
        None
×
65
    }
18✔
66
}
67

68
impl From<DatasetId> for DataId {
69
    fn from(value: DatasetId) -> Self {
9✔
70
        Self::Internal { dataset_id: value }
9✔
71
    }
9✔
72
}
73

74
impl From<ExternalDataId> for DataId {
75
    fn from(value: ExternalDataId) -> Self {
×
76
        Self::External(value)
×
77
    }
×
78
}
79

80
impl From<ExternalDataId> for geoengine_datatypes::dataset::DataId {
81
    fn from(value: ExternalDataId) -> Self {
4✔
82
        Self::External(value.into())
4✔
83
    }
4✔
84
}
85

86
impl From<DatasetId> for geoengine_datatypes::dataset::DataId {
87
    fn from(value: DatasetId) -> Self {
47✔
88
        Self::Internal {
47✔
89
            dataset_id: value.into(),
47✔
90
        }
47✔
91
    }
47✔
92
}
93

94
impl From<ExternalDataId> for geoengine_datatypes::dataset::ExternalDataId {
95
    fn from(value: ExternalDataId) -> Self {
24✔
96
        Self {
24✔
97
            provider_id: value.provider_id.into(),
24✔
98
            layer_id: value.layer_id.into(),
24✔
99
        }
24✔
100
    }
24✔
101
}
102

103
impl From<geoengine_datatypes::dataset::DataId> for DataId {
104
    fn from(id: geoengine_datatypes::dataset::DataId) -> Self {
18✔
105
        match id {
18✔
106
            geoengine_datatypes::dataset::DataId::Internal { dataset_id } => Self::Internal {
2✔
107
                dataset_id: dataset_id.into(),
2✔
108
            },
2✔
109
            geoengine_datatypes::dataset::DataId::External(external_id) => {
16✔
110
                Self::External(external_id.into())
16✔
111
            }
112
        }
113
    }
18✔
114
}
115

116
impl From<DataId> for geoengine_datatypes::dataset::DataId {
117
    fn from(id: DataId) -> Self {
20✔
118
        match id {
20✔
119
            DataId::Internal { dataset_id } => Self::Internal {
×
120
                dataset_id: dataset_id.into(),
×
121
            },
×
122
            DataId::External(external_id) => Self::External(external_id.into()),
20✔
123
        }
124
    }
20✔
125
}
126

127
impl From<geoengine_datatypes::dataset::DatasetId> for DatasetId {
128
    fn from(id: geoengine_datatypes::dataset::DatasetId) -> Self {
18✔
129
        Self(id.0)
18✔
130
    }
18✔
131
}
132

133
/// The user-facing identifier for loadable data.
134
/// It can be resolved into a [`DataId`].
135
#[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize, Serialize)]
65✔
136
// TODO: Have separate type once `geoengine_datatypes::dataset::NamedData` is not part of the API anymore.
137
#[serde(
138
    from = "geoengine_datatypes::dataset::NamedData",
139
    into = "geoengine_datatypes::dataset::NamedData"
140
)]
141
pub struct NamedData {
142
    pub namespace: Option<String>,
143
    pub provider: Option<String>,
144
    pub name: String,
145
}
146

147
impl From<geoengine_datatypes::dataset::NamedData> for NamedData {
148
    fn from(
×
149
        geoengine_datatypes::dataset::NamedData {
×
150
            namespace,
×
151
            provider,
×
152
            name,
×
153
        }: geoengine_datatypes::dataset::NamedData,
×
154
    ) -> Self {
×
155
        Self {
×
156
            namespace,
×
157
            provider,
×
158
            name,
×
159
        }
×
160
    }
×
161
}
162

163
impl From<&geoengine_datatypes::dataset::NamedData> for NamedData {
164
    fn from(named_data: &geoengine_datatypes::dataset::NamedData) -> Self {
×
165
        Self::from(named_data.clone())
×
166
    }
×
167
}
168

169
impl From<NamedData> for geoengine_datatypes::dataset::NamedData {
170
    fn from(
118✔
171
        NamedData {
118✔
172
            namespace,
118✔
173
            provider,
118✔
174
            name,
118✔
175
        }: NamedData,
118✔
176
    ) -> Self {
118✔
177
        Self {
118✔
178
            namespace,
118✔
179
            provider,
118✔
180
            name,
118✔
181
        }
118✔
182
    }
118✔
183
}
184

185
impl From<&NamedData> for geoengine_datatypes::dataset::NamedData {
186
    fn from(named_data: &NamedData) -> Self {
61✔
187
        Self::from(named_data.clone())
61✔
188
    }
61✔
189
}
190

191
impl<'a> ToSchema<'a> for NamedData {
192
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
2✔
193
        use utoipa::openapi::*;
2✔
194
        (
2✔
195
            "NamedData",
2✔
196
            ObjectBuilder::new().schema_type(SchemaType::String).into(),
2✔
197
        )
2✔
198
    }
2✔
199
}
200

201
/// A (optionally namespaced) name for a `Dataset`.
202
/// It can be resolved into a [`DataId`] if you know the data provider.
203
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, IntoParams, ToSql, FromSql)]
709✔
204
pub struct DatasetName {
205
    pub namespace: Option<String>,
206
    pub name: String,
207
}
208

209
impl DatasetName {
210
    /// Canonicalize a name that reflects the system namespace and provider.
211
    fn canonicalize<S: Into<String> + PartialEq<&'static str>>(
7✔
212
        name: S,
7✔
213
        system_name: &'static str,
7✔
214
    ) -> Option<String> {
7✔
215
        if name == system_name {
7✔
216
            None
×
217
        } else {
218
            Some(name.into())
7✔
219
        }
220
    }
7✔
221

222
    pub fn new<S: Into<String>>(namespace: Option<String>, name: S) -> Self {
11✔
223
        Self {
11✔
224
            namespace,
11✔
225
            name: name.into(),
11✔
226
        }
11✔
227
    }
11✔
228
}
229

230
impl std::fmt::Display for DatasetName {
231
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6✔
232
        let d = geoengine_datatypes::dataset::NAME_DELIMITER;
6✔
233
        match (&self.namespace, &self.name) {
6✔
234
            (None, name) => write!(f, "{name}"),
3✔
235
            (Some(namespace), name) => {
3✔
236
                write!(f, "{namespace}{d}{name}")
3✔
237
            }
238
        }
239
    }
6✔
240
}
241

242
impl Serialize for DatasetName {
243
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
14✔
244
    where
14✔
245
        S: serde::Serializer,
14✔
246
    {
14✔
247
        let d = geoengine_datatypes::dataset::NAME_DELIMITER;
14✔
248
        let serialized = match (&self.namespace, &self.name) {
14✔
249
            (None, name) => name.to_string(),
9✔
250
            (Some(namespace), name) => {
5✔
251
                format!("{namespace}{d}{name}")
5✔
252
            }
253
        };
254

255
        serializer.serialize_str(&serialized)
14✔
256
    }
14✔
257
}
258

259
impl<'de> Deserialize<'de> for DatasetName {
260
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
19✔
261
    where
19✔
262
        D: serde::Deserializer<'de>,
19✔
263
    {
19✔
264
        deserializer.deserialize_str(DatasetNameDeserializeVisitor)
19✔
265
    }
19✔
266
}
267

268
struct DatasetNameDeserializeVisitor;
269

270
impl<'de> Visitor<'de> for DatasetNameDeserializeVisitor {
271
    type Value = DatasetName;
272

273
    /// always keep in sync with [`is_allowed_name_char`]
274
    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
×
275
        write!(
×
276
            formatter,
×
277
            "a string consisting of a namespace and name name, separated by a colon, only using alphanumeric characters, underscores & dashes"
×
278
        )
×
279
    }
×
280

281
    fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
19✔
282
    where
19✔
283
        E: serde::de::Error,
19✔
284
    {
19✔
285
        let mut strings = [None, None];
19✔
286
        let mut split = s.split(geoengine_datatypes::dataset::NAME_DELIMITER);
19✔
287

288
        for (buffer, part) in strings.iter_mut().zip(&mut split) {
26✔
289
            if part.is_empty() {
26✔
290
                return Err(E::custom("empty part in named data"));
×
291
            }
26✔
292

293
            if let Some(c) = part
26✔
294
                .matches(geoengine_datatypes::dataset::is_invalid_name_char)
26✔
295
                .next()
26✔
296
            {
297
                return Err(E::custom(format!("invalid character '{c}' in named data")));
×
298
            }
26✔
299

26✔
300
            *buffer = Some(part.to_string());
26✔
301
        }
302

303
        if split.next().is_some() {
19✔
304
            return Err(E::custom("named data must consist of at most two parts"));
×
305
        }
19✔
306

307
        match strings {
19✔
308
            [Some(namespace), Some(name)] => Ok(DatasetName {
7✔
309
                namespace: DatasetName::canonicalize(
7✔
310
                    namespace,
7✔
311
                    geoengine_datatypes::dataset::SYSTEM_NAMESPACE,
7✔
312
                ),
7✔
313
                name,
7✔
314
            }),
7✔
315
            [Some(name), None] => Ok(DatasetName {
12✔
316
                namespace: None,
12✔
317
                name,
12✔
318
            }),
12✔
319
            _ => Err(E::custom("empty named data")),
×
320
        }
321
    }
19✔
322
}
323

324
impl From<NamedData> for DatasetName {
325
    fn from(
×
326
        NamedData {
×
327
            namespace,
×
328
            provider: _,
×
329
            name,
×
330
        }: NamedData,
×
331
    ) -> Self {
×
332
        Self { namespace, name }
×
333
    }
×
334
}
335

336
impl From<&NamedData> for DatasetName {
337
    fn from(named_data: &NamedData) -> Self {
×
338
        Self {
×
339
            namespace: named_data.namespace.clone(),
×
340
            name: named_data.name.clone(),
×
341
        }
×
342
    }
×
343
}
344

345
impl From<&geoengine_datatypes::dataset::NamedData> for DatasetName {
346
    fn from(named_data: &geoengine_datatypes::dataset::NamedData) -> Self {
32✔
347
        Self {
32✔
348
            namespace: named_data.namespace.clone(),
32✔
349
            name: named_data.name.clone(),
32✔
350
        }
32✔
351
    }
32✔
352
}
353

354
impl From<DatasetName> for NamedData {
355
    fn from(DatasetName { namespace, name }: DatasetName) -> Self {
5✔
356
        NamedData {
5✔
357
            namespace,
5✔
358
            provider: None,
5✔
359
            name,
5✔
360
        }
5✔
361
    }
5✔
362
}
363

364
impl From<DatasetName> for geoengine_datatypes::dataset::NamedData {
365
    fn from(DatasetName { namespace, name }: DatasetName) -> Self {
3✔
366
        geoengine_datatypes::dataset::NamedData {
3✔
367
            namespace,
3✔
368
            provider: None,
3✔
369
            name,
3✔
370
        }
3✔
371
    }
3✔
372
}
373

374
impl<'a> ToSchema<'a> for DatasetName {
375
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
2✔
376
        use utoipa::openapi::*;
2✔
377
        (
2✔
378
            "DatasetName",
2✔
379
            ObjectBuilder::new().schema_type(SchemaType::String).into(),
2✔
380
        )
2✔
381
    }
2✔
382
}
383

384
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, ToSchema, ToSql, FromSql)]
168✔
385
pub struct LayerId(pub String); // TODO: differentiate between internal layer ids (UUID) and external layer ids (String)
386

387
impl From<LayerId> for geoengine_datatypes::dataset::LayerId {
388
    fn from(value: LayerId) -> Self {
24✔
389
        Self(value.0)
24✔
390
    }
24✔
391
}
392

393
impl std::fmt::Display for LayerId {
394
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
79✔
395
        write!(f, "{}", self.0)
79✔
396
    }
79✔
397
}
398

399
#[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
33✔
400
#[serde(rename_all = "camelCase")]
401
pub struct ExternalDataId {
402
    pub provider_id: DataProviderId,
403
    pub layer_id: LayerId,
404
}
405

406
impl From<geoengine_datatypes::dataset::ExternalDataId> for ExternalDataId {
407
    fn from(id: geoengine_datatypes::dataset::ExternalDataId) -> Self {
16✔
408
        Self {
16✔
409
            provider_id: id.provider_id.into(),
16✔
410
            layer_id: id.layer_id.into(),
16✔
411
        }
16✔
412
    }
16✔
413
}
414

415
impl From<geoengine_datatypes::dataset::DataProviderId> for DataProviderId {
416
    fn from(id: geoengine_datatypes::dataset::DataProviderId) -> Self {
17✔
417
        Self(id.0)
17✔
418
    }
17✔
419
}
420

421
impl From<geoengine_datatypes::dataset::LayerId> for LayerId {
422
    fn from(id: geoengine_datatypes::dataset::LayerId) -> Self {
16✔
423
        Self(id.0)
16✔
424
    }
16✔
425
}
426

427
/// A spatial reference authority that is part of a spatial reference definition
428
#[derive(
429
    Debug,
×
430
    Copy,
431
    Clone,
×
432
    Eq,
433
    PartialEq,
37✔
434
    Ord,
×
435
    PartialOrd,
×
436
    Serialize,
×
437
    Deserialize,
×
438
    ToSchema,
2✔
439
    FromSql,
6,185✔
440
    ToSql,
188✔
441
)]
442
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
443
pub enum SpatialReferenceAuthority {
444
    Epsg,
445
    SrOrg,
446
    Iau2000,
447
    Esri,
448
}
449

450
impl From<geoengine_datatypes::spatial_reference::SpatialReferenceAuthority>
451
    for SpatialReferenceAuthority
452
{
453
    fn from(value: geoengine_datatypes::spatial_reference::SpatialReferenceAuthority) -> Self {
218✔
454
        match value {
218✔
455
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Epsg => Self::Epsg,
217✔
456
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::SrOrg => Self::SrOrg,
1✔
457
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Iau2000 => {
458
                Self::Iau2000
×
459
            }
460
            geoengine_datatypes::spatial_reference::SpatialReferenceAuthority::Esri => Self::Esri,
×
461
        }
462
    }
218✔
463
}
464

465
impl From<SpatialReferenceAuthority>
466
    for geoengine_datatypes::spatial_reference::SpatialReferenceAuthority
467
{
468
    fn from(value: SpatialReferenceAuthority) -> Self {
55✔
469
        match value {
55✔
470
            SpatialReferenceAuthority::Epsg => Self::Epsg,
55✔
471
            SpatialReferenceAuthority::SrOrg => Self::SrOrg,
×
472
            SpatialReferenceAuthority::Iau2000 => Self::Iau2000,
×
473
            SpatialReferenceAuthority::Esri => Self::Esri,
×
474
        }
475
    }
55✔
476
}
477

478
impl FromStr for SpatialReferenceAuthority {
479
    type Err = error::Error;
480

481
    fn from_str(s: &str) -> Result<Self, Self::Err> {
32✔
482
        Ok(match s {
32✔
483
            "EPSG" => SpatialReferenceAuthority::Epsg,
32✔
484
            "SR-ORG" => SpatialReferenceAuthority::SrOrg,
×
485
            "IAU2000" => SpatialReferenceAuthority::Iau2000,
×
486
            "ESRI" => SpatialReferenceAuthority::Esri,
×
487
            _ => {
488
                return Err(error::Error::InvalidSpatialReferenceString {
×
489
                    spatial_reference_string: s.into(),
×
490
                })
×
491
            }
492
        })
493
    }
32✔
494
}
495

496
impl std::fmt::Display for SpatialReferenceAuthority {
497
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44✔
498
        write!(
44✔
499
            f,
44✔
500
            "{}",
44✔
501
            match self {
44✔
502
                SpatialReferenceAuthority::Epsg => "EPSG",
44✔
503
                SpatialReferenceAuthority::SrOrg => "SR-ORG",
×
504
                SpatialReferenceAuthority::Iau2000 => "IAU2000",
×
505
                SpatialReferenceAuthority::Esri => "ESRI",
×
506
            }
507
        )
508
    }
44✔
509
}
510

511
/// A spatial reference consists of an authority and a code
512
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, FromSql, ToSql)]
3,947✔
513
pub struct SpatialReference {
514
    authority: SpatialReferenceAuthority,
515
    code: u32,
516
}
517

518
impl SpatialReference {
519
    pub fn proj_string(self) -> Result<String> {
520
        match self.authority {
×
521
            SpatialReferenceAuthority::Epsg | SpatialReferenceAuthority::Iau2000 => {
522
                Ok(format!("{}:{}", self.authority, self.code))
17✔
523
            }
524
            // poor-mans integration of Meteosat Second Generation
525
            SpatialReferenceAuthority::SrOrg if self.code == 81 => Ok("+proj=geos +lon_0=0 +h=35785831 +x_0=0 +y_0=0 +ellps=WGS84 +units=m +no_defs +type=crs".to_owned()),
×
526
            SpatialReferenceAuthority::SrOrg | SpatialReferenceAuthority::Esri => {
527
                Err(error::Error::ProjStringUnresolvable { spatial_ref: self })
×
528
                //TODO: we might need to look them up somehow! Best solution would be a registry where we can store user definexd srs strings.
529
            }
530
        }
531
    }
17✔
532

533
    /// Return the srs-string "authority:code"
534
    #[allow(clippy::trivially_copy_pass_by_ref)]
535
    pub fn srs_string(&self) -> String {
17✔
536
        format!("{}:{}", self.authority, self.code)
17✔
537
    }
17✔
538
}
539

540
impl<'a> ToSchema<'a> for SpatialReference {
541
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
2✔
542
        use utoipa::openapi::*;
2✔
543
        (
2✔
544
            "SpatialReference",
2✔
545
            ObjectBuilder::new().schema_type(SchemaType::String).into(),
2✔
546
        )
2✔
547
    }
2✔
548
}
549

550
impl From<geoengine_datatypes::spatial_reference::SpatialReference> for SpatialReference {
551
    fn from(value: geoengine_datatypes::spatial_reference::SpatialReference) -> Self {
218✔
552
        Self {
218✔
553
            authority: (*value.authority()).into(),
218✔
554
            code: value.code(),
218✔
555
        }
218✔
556
    }
218✔
557
}
558

559
impl From<SpatialReference> for geoengine_datatypes::spatial_reference::SpatialReference {
560
    fn from(value: SpatialReference) -> Self {
55✔
561
        geoengine_datatypes::spatial_reference::SpatialReference::new(
55✔
562
            value.authority.into(),
55✔
563
            value.code,
55✔
564
        )
55✔
565
    }
55✔
566
}
567

568
impl SpatialReference {
569
    pub fn new(authority: SpatialReferenceAuthority, code: u32) -> Self {
37✔
570
        Self { authority, code }
37✔
571
    }
37✔
572

573
    pub fn authority(&self) -> &SpatialReferenceAuthority {
1✔
574
        &self.authority
1✔
575
    }
1✔
576

577
    pub fn code(self) -> u32 {
1✔
578
        self.code
1✔
579
    }
1✔
580
}
581

582
impl FromStr for SpatialReference {
583
    type Err = error::Error;
584

585
    fn from_str(s: &str) -> Result<Self, Self::Err> {
32✔
586
        let mut split = s.split(':');
32✔
587

32✔
588
        match (split.next(), split.next(), split.next()) {
32✔
589
            (Some(authority), Some(code), None) => Ok(Self::new(
32✔
590
                authority.parse()?,
32✔
591
                code.parse::<u32>().context(error::ParseU32)?,
32✔
592
            )),
593
            _ => Err(error::Error::InvalidSpatialReferenceString {
×
594
                spatial_reference_string: s.into(),
×
595
            }),
×
596
        }
597
    }
32✔
598
}
599

600
impl std::fmt::Display for SpatialReference {
601
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9✔
602
        write!(f, "{}:{}", self.authority, self.code)
9✔
603
    }
9✔
604
}
605

606
impl Serialize for SpatialReference {
607
    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
1✔
608
    where
1✔
609
        S: Serializer,
1✔
610
    {
1✔
611
        serializer.serialize_str(&self.to_string())
1✔
612
    }
1✔
613
}
614

615
/// Helper struct for deserializing a `SpatialReferencce`
616
struct SpatialReferenceDeserializeVisitor;
617

618
impl<'de> Visitor<'de> for SpatialReferenceDeserializeVisitor {
619
    type Value = SpatialReference;
620

621
    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
×
622
        formatter.write_str("a spatial reference in the form authority:code")
×
623
    }
×
624

625
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
16✔
626
    where
16✔
627
        E: serde::de::Error,
16✔
628
    {
16✔
629
        v.parse().map_err(serde::de::Error::custom)
16✔
630
    }
16✔
631
}
632

633
impl<'de> Deserialize<'de> for SpatialReference {
634
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
16✔
635
    where
16✔
636
        D: Deserializer<'de>,
16✔
637
    {
16✔
638
        deserializer.deserialize_str(SpatialReferenceDeserializeVisitor)
16✔
639
    }
16✔
640
}
641

642
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ToSchema)]
184✔
643
pub enum SpatialReferenceOption {
644
    SpatialReference(SpatialReference),
645
    Unreferenced,
646
}
647

648
impl From<geoengine_datatypes::spatial_reference::SpatialReferenceOption>
649
    for SpatialReferenceOption
650
{
651
    fn from(value: geoengine_datatypes::spatial_reference::SpatialReferenceOption) -> Self {
226✔
652
        match value {
226✔
653
            geoengine_datatypes::spatial_reference::SpatialReferenceOption::SpatialReference(s) => {
188✔
654
                Self::SpatialReference(s.into())
188✔
655
            }
656
            geoengine_datatypes::spatial_reference::SpatialReferenceOption::Unreferenced => {
657
                Self::Unreferenced
38✔
658
            }
659
        }
660
    }
226✔
661
}
662

663
impl From<SpatialReferenceOption>
664
    for geoengine_datatypes::spatial_reference::SpatialReferenceOption
665
{
666
    fn from(value: SpatialReferenceOption) -> Self {
63✔
667
        match value {
63✔
668
            SpatialReferenceOption::SpatialReference(sr) => Self::SpatialReference(sr.into()),
51✔
669
            SpatialReferenceOption::Unreferenced => Self::Unreferenced,
12✔
670
        }
671
    }
63✔
672
}
673

674
impl From<SpatialReference> for SpatialReferenceOption {
675
    fn from(spatial_reference: SpatialReference) -> Self {
8✔
676
        Self::SpatialReference(spatial_reference)
8✔
677
    }
8✔
678
}
679

680
impl std::fmt::Display for SpatialReferenceOption {
681
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11✔
682
        match self {
11✔
683
            SpatialReferenceOption::SpatialReference(p) => write!(f, "{p}"),
8✔
684
            SpatialReferenceOption::Unreferenced => Ok(()),
3✔
685
        }
686
    }
11✔
687
}
688

689
impl Serialize for SpatialReferenceOption {
690
    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
11✔
691
    where
11✔
692
        S: Serializer,
11✔
693
    {
11✔
694
        serializer.serialize_str(&self.to_string())
11✔
695
    }
11✔
696
}
697

698
/// Helper struct for deserializing a `SpatialReferenceOption`
699
struct SpatialReferenceOptionDeserializeVisitor;
700

701
impl<'de> Visitor<'de> for SpatialReferenceOptionDeserializeVisitor {
702
    type Value = SpatialReferenceOption;
703

704
    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
×
705
        formatter.write_str("a spatial reference in the form authority:code")
×
706
    }
×
707

708
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
8✔
709
    where
8✔
710
        E: serde::de::Error,
8✔
711
    {
8✔
712
        if v.is_empty() {
8✔
713
            return Ok(SpatialReferenceOption::Unreferenced);
×
714
        }
8✔
715

716
        let spatial_reference: SpatialReference = v.parse().map_err(serde::de::Error::custom)?;
8✔
717

718
        Ok(spatial_reference.into())
8✔
719
    }
8✔
720
}
721

722
impl<'de> Deserialize<'de> for SpatialReferenceOption {
723
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
8✔
724
    where
8✔
725
        D: Deserializer<'de>,
8✔
726
    {
8✔
727
        deserializer.deserialize_str(SpatialReferenceOptionDeserializeVisitor)
8✔
728
    }
8✔
729
}
730

731
impl From<Option<SpatialReference>> for SpatialReferenceOption {
732
    fn from(option: Option<SpatialReference>) -> Self {
×
733
        match option {
×
734
            Some(p) => SpatialReferenceOption::SpatialReference(p),
×
735
            None => SpatialReferenceOption::Unreferenced,
×
736
        }
737
    }
×
738
}
739

740
impl From<SpatialReferenceOption> for Option<SpatialReference> {
741
    fn from(option: SpatialReferenceOption) -> Self {
12✔
742
        match option {
12✔
743
            SpatialReferenceOption::SpatialReference(p) => Some(p),
12✔
744
            SpatialReferenceOption::Unreferenced => None,
×
745
        }
746
    }
12✔
747
}
748

749
impl ToSql for SpatialReferenceOption {
750
    fn to_sql(
200✔
751
        &self,
200✔
752
        ty: &postgres_types::Type,
200✔
753
        out: &mut bytes::BytesMut,
200✔
754
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>>
200✔
755
    where
200✔
756
        Self: Sized,
200✔
757
    {
200✔
758
        match self {
200✔
759
            SpatialReferenceOption::SpatialReference(sref) => sref.to_sql(ty, out),
162✔
760
            SpatialReferenceOption::Unreferenced => Ok(postgres_types::IsNull::Yes),
38✔
761
        }
762
    }
200✔
763

764
    fn accepts(ty: &postgres_types::Type) -> bool
4✔
765
    where
4✔
766
        Self: Sized,
4✔
767
    {
4✔
768
        <SpatialReference as ToSql>::accepts(ty)
4✔
769
    }
4✔
770

771
    fn to_sql_checked(
2✔
772
        &self,
2✔
773
        ty: &postgres_types::Type,
2✔
774
        out: &mut bytes::BytesMut,
2✔
775
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
2✔
776
        match self {
2✔
777
            SpatialReferenceOption::SpatialReference(sref) => sref.to_sql_checked(ty, out),
1✔
778
            SpatialReferenceOption::Unreferenced => Ok(postgres_types::IsNull::Yes),
1✔
779
        }
780
    }
2✔
781
}
782

783
impl<'a> FromSql<'a> for SpatialReferenceOption {
784
    fn from_sql(
59✔
785
        ty: &postgres_types::Type,
59✔
786
        raw: &'a [u8],
59✔
787
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
59✔
788
        Ok(SpatialReferenceOption::SpatialReference(
59✔
789
            SpatialReference::from_sql(ty, raw)?,
59✔
790
        ))
791
    }
59✔
792

793
    fn from_sql_null(
23✔
794
        _: &postgres_types::Type,
23✔
795
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
23✔
796
        Ok(SpatialReferenceOption::Unreferenced)
23✔
797
    }
23✔
798

799
    fn accepts(ty: &postgres_types::Type) -> bool {
1,237✔
800
        <SpatialReference as FromSql>::accepts(ty)
1,237✔
801
    }
1,237✔
802
}
803

804
/// An enum that contains all possible vector data types
805
#[derive(
806
    Debug,
×
807
    Ord,
×
808
    PartialOrd,
×
809
    Eq,
810
    PartialEq,
12✔
811
    Hash,
×
812
    Deserialize,
16✔
813
    Serialize,
5✔
814
    Copy,
815
    Clone,
72✔
816
    ToSchema,
2✔
817
    FromSql,
2,830✔
818
    ToSql,
98✔
819
)]
820
pub enum VectorDataType {
821
    Data,
822
    MultiPoint,
823
    MultiLineString,
824
    MultiPolygon,
825
}
826

827
impl From<geoengine_datatypes::collections::VectorDataType> for VectorDataType {
828
    fn from(value: geoengine_datatypes::collections::VectorDataType) -> Self {
81✔
829
        match value {
81✔
830
            geoengine_datatypes::collections::VectorDataType::Data => Self::Data,
16✔
831
            geoengine_datatypes::collections::VectorDataType::MultiPoint => Self::MultiPoint,
65✔
832
            geoengine_datatypes::collections::VectorDataType::MultiLineString => {
833
                Self::MultiLineString
×
834
            }
835
            geoengine_datatypes::collections::VectorDataType::MultiPolygon => Self::MultiPolygon,
×
836
        }
837
    }
81✔
838
}
839

840
impl From<VectorDataType> for geoengine_datatypes::collections::VectorDataType {
841
    fn from(value: VectorDataType) -> Self {
30✔
842
        match value {
30✔
843
            VectorDataType::Data => Self::Data,
4✔
844
            VectorDataType::MultiPoint => Self::MultiPoint,
26✔
845
            VectorDataType::MultiLineString => Self::MultiLineString,
×
846
            VectorDataType::MultiPolygon => Self::MultiPolygon,
×
847
        }
848
    }
30✔
849
}
850

851
#[derive(
852
    Clone,
×
853
    Copy,
854
    Debug,
×
855
    Deserialize,
70✔
856
    PartialEq,
91✔
857
    PartialOrd,
×
858
    Serialize,
21✔
859
    Default,
×
860
    ToSchema,
2✔
861
    ToSql,
1,122✔
862
    FromSql,
10,222✔
863
)]
864
pub struct Coordinate2D {
865
    pub x: f64,
866
    pub y: f64,
867
}
868

869
impl From<geoengine_datatypes::primitives::Coordinate2D> for Coordinate2D {
870
    fn from(coordinate: geoengine_datatypes::primitives::Coordinate2D) -> Self {
511✔
871
        Self {
511✔
872
            x: coordinate.x,
511✔
873
            y: coordinate.y,
511✔
874
        }
511✔
875
    }
511✔
876
}
877

878
impl From<Coordinate2D> for geoengine_datatypes::primitives::Coordinate2D {
879
    fn from(coordinate: Coordinate2D) -> Self {
157✔
880
        Self {
157✔
881
            x: coordinate.x,
157✔
882
            y: coordinate.y,
157✔
883
        }
157✔
884
    }
157✔
885
}
886

887
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Debug, ToSchema, ToSql, FromSql)]
1,632✔
888
#[serde(rename_all = "camelCase")]
889
/// A bounding box that includes all border points.
890
/// Note: may degenerate to a point!
891
pub struct BoundingBox2D {
892
    pub lower_left_coordinate: Coordinate2D,
893
    pub upper_right_coordinate: Coordinate2D,
894
}
895

896
impl From<geoengine_datatypes::primitives::BoundingBox2D> for BoundingBox2D {
897
    fn from(bbox: geoengine_datatypes::primitives::BoundingBox2D) -> Self {
38✔
898
        Self {
38✔
899
            lower_left_coordinate:
38✔
900
                geoengine_datatypes::primitives::AxisAlignedRectangle::lower_left(&bbox).into(),
38✔
901
            upper_right_coordinate:
38✔
902
                geoengine_datatypes::primitives::AxisAlignedRectangle::upper_right(&bbox).into(),
38✔
903
        }
38✔
904
    }
38✔
905
}
906

907
impl From<BoundingBox2D> for geoengine_datatypes::primitives::BoundingBox2D {
908
    fn from(bbox: BoundingBox2D) -> Self {
7✔
909
        Self::new_unchecked(
7✔
910
            bbox.lower_left_coordinate.into(),
7✔
911
            bbox.upper_right_coordinate.into(),
7✔
912
        )
7✔
913
    }
7✔
914
}
915

916
/// An object that composes the date and a timestamp with time zone.
917
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ToSchema)]
2✔
918
pub struct DateTime {
919
    datetime: chrono::DateTime<chrono::Utc>,
920
}
921

922
impl FromStr for DateTime {
923
    type Err = geoengine_datatypes::primitives::DateTimeError;
924

925
    fn from_str(input: &str) -> Result<Self, Self::Err> {
×
926
        let date_time = chrono::DateTime::<chrono::FixedOffset>::from_str(input).map_err(|e| {
×
927
            Self::Err::DateParse {
×
928
                source: Box::new(e),
×
929
            }
×
930
        })?;
×
931

932
        Ok(date_time.into())
×
933
    }
×
934
}
935

936
impl From<chrono::DateTime<chrono::FixedOffset>> for DateTime {
937
    fn from(datetime: chrono::DateTime<chrono::FixedOffset>) -> Self {
×
938
        Self {
×
939
            datetime: datetime.into(),
×
940
        }
×
941
    }
×
942
}
943

944
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, ToSchema, FromSql, ToSql)]
2,933✔
945
#[serde(rename_all = "camelCase")]
946
pub enum FeatureDataType {
947
    Category,
948
    Int,
949
    Float,
950
    Text,
951
    Bool,
952
    DateTime,
953
}
954

955
impl From<geoengine_datatypes::primitives::FeatureDataType> for FeatureDataType {
956
    fn from(value: geoengine_datatypes::primitives::FeatureDataType) -> Self {
80✔
957
        match value {
80✔
958
            geoengine_datatypes::primitives::FeatureDataType::Category => Self::Category,
×
959
            geoengine_datatypes::primitives::FeatureDataType::Int => Self::Int,
27✔
960
            geoengine_datatypes::primitives::FeatureDataType::Float => Self::Float,
28✔
961
            geoengine_datatypes::primitives::FeatureDataType::Text => Self::Text,
25✔
962
            geoengine_datatypes::primitives::FeatureDataType::Bool => Self::Bool,
×
963
            geoengine_datatypes::primitives::FeatureDataType::DateTime => Self::DateTime,
×
964
        }
965
    }
80✔
966
}
967

968
impl From<FeatureDataType> for geoengine_datatypes::primitives::FeatureDataType {
969
    fn from(value: FeatureDataType) -> Self {
41✔
970
        match value {
41✔
971
            FeatureDataType::Category => Self::Category,
×
972
            FeatureDataType::Int => Self::Int,
15✔
973
            FeatureDataType::Float => Self::Float,
8✔
974
            FeatureDataType::Text => Self::Text,
18✔
975
            FeatureDataType::Bool => Self::Bool,
×
976
            FeatureDataType::DateTime => Self::DateTime,
×
977
        }
978
    }
41✔
979
}
980

981
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
276✔
982
#[serde(rename_all = "camelCase", tag = "type")]
983
pub enum Measurement {
984
    Unitless,
985
    Continuous(ContinuousMeasurement),
986
    Classification(ClassificationMeasurement),
987
}
988

989
impl From<geoengine_datatypes::primitives::Measurement> for Measurement {
990
    fn from(value: geoengine_datatypes::primitives::Measurement) -> Self {
229✔
991
        match value {
229✔
992
            geoengine_datatypes::primitives::Measurement::Unitless => Self::Unitless,
212✔
993
            geoengine_datatypes::primitives::Measurement::Continuous(cm) => {
14✔
994
                Self::Continuous(cm.into())
14✔
995
            }
996
            geoengine_datatypes::primitives::Measurement::Classification(cm) => {
3✔
997
                Self::Classification(cm.into())
3✔
998
            }
999
        }
1000
    }
229✔
1001
}
1002

1003
impl From<Measurement> for geoengine_datatypes::primitives::Measurement {
1004
    fn from(value: Measurement) -> Self {
106✔
1005
        match value {
106✔
1006
            Measurement::Unitless => Self::Unitless,
87✔
1007
            Measurement::Continuous(cm) => Self::Continuous(cm.into()),
17✔
1008
            Measurement::Classification(cm) => Self::Classification(cm.into()),
2✔
1009
        }
1010
    }
106✔
1011
}
1012

1013
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema, FromSql, ToSql)]
3,418✔
1014
pub struct ContinuousMeasurement {
1015
    pub measurement: String,
1016
    pub unit: Option<String>,
1017
}
1018

1019
impl From<geoengine_datatypes::primitives::ContinuousMeasurement> for ContinuousMeasurement {
1020
    fn from(value: geoengine_datatypes::primitives::ContinuousMeasurement) -> Self {
14✔
1021
        Self {
14✔
1022
            measurement: value.measurement,
14✔
1023
            unit: value.unit,
14✔
1024
        }
14✔
1025
    }
14✔
1026
}
1027

1028
impl From<ContinuousMeasurement> for geoengine_datatypes::primitives::ContinuousMeasurement {
1029
    fn from(value: ContinuousMeasurement) -> Self {
17✔
1030
        Self {
17✔
1031
            measurement: value.measurement,
17✔
1032
            unit: value.unit,
17✔
1033
        }
17✔
1034
    }
17✔
1035
}
1036

1037
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
2✔
1038
#[serde(
1039
    try_from = "SerializableClassificationMeasurement",
1040
    into = "SerializableClassificationMeasurement"
1041
)]
1042
pub struct ClassificationMeasurement {
1043
    pub measurement: String,
1044
    pub classes: HashMap<u8, String>,
1045
}
1046

1047
impl From<geoengine_datatypes::primitives::ClassificationMeasurement>
1048
    for ClassificationMeasurement
1049
{
1050
    fn from(value: geoengine_datatypes::primitives::ClassificationMeasurement) -> Self {
3✔
1051
        Self {
3✔
1052
            measurement: value.measurement,
3✔
1053
            classes: value.classes,
3✔
1054
        }
3✔
1055
    }
3✔
1056
}
1057

1058
impl From<ClassificationMeasurement>
1059
    for geoengine_datatypes::primitives::ClassificationMeasurement
1060
{
1061
    fn from(value: ClassificationMeasurement) -> Self {
2✔
1062
        Self {
2✔
1063
            measurement: value.measurement,
2✔
1064
            classes: value.classes,
2✔
1065
        }
2✔
1066
    }
2✔
1067
}
1068

1069
/// A type that is solely for serde's serializability.
1070
/// You cannot serialize floats as JSON map keys.
1071
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
×
1072
pub struct SerializableClassificationMeasurement {
1073
    pub measurement: String,
1074
    // use a BTreeMap to preserve the order of the keys
1075
    pub classes: BTreeMap<String, String>,
1076
}
1077

1078
impl From<ClassificationMeasurement> for SerializableClassificationMeasurement {
1079
    fn from(measurement: ClassificationMeasurement) -> Self {
×
1080
        let mut classes = BTreeMap::new();
×
1081
        for (k, v) in measurement.classes {
×
1082
            classes.insert(k.to_string(), v);
×
1083
        }
×
1084
        Self {
×
1085
            measurement: measurement.measurement,
×
1086
            classes,
×
1087
        }
×
1088
    }
×
1089
}
1090

1091
impl TryFrom<SerializableClassificationMeasurement> for ClassificationMeasurement {
1092
    type Error = <u8 as FromStr>::Err;
1093

1094
    fn try_from(measurement: SerializableClassificationMeasurement) -> Result<Self, Self::Error> {
×
1095
        let mut classes = HashMap::with_capacity(measurement.classes.len());
×
1096
        for (k, v) in measurement.classes {
×
1097
            classes.insert(k.parse::<u8>()?, v);
×
1098
        }
1099
        Ok(Self {
×
1100
            measurement: measurement.measurement,
×
1101
            classes,
×
1102
        })
×
1103
    }
×
1104
}
1105

1106
/// A partition of space that include the upper left but excludes the lower right coordinate
1107
#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Debug, ToSchema, FromSql, ToSql)]
2,271✔
1108
#[serde(rename_all = "camelCase")]
1109
pub struct SpatialPartition2D {
1110
    pub upper_left_coordinate: Coordinate2D,
1111
    pub lower_right_coordinate: Coordinate2D,
1112
}
1113

1114
impl From<geoengine_datatypes::primitives::SpatialPartition2D> for SpatialPartition2D {
1115
    fn from(value: geoengine_datatypes::primitives::SpatialPartition2D) -> Self {
133✔
1116
        Self {
133✔
1117
            upper_left_coordinate: value.upper_left().into(),
133✔
1118
            lower_right_coordinate: value.lower_right().into(),
133✔
1119
        }
133✔
1120
    }
133✔
1121
}
1122

1123
impl From<SpatialPartition2D> for geoengine_datatypes::primitives::SpatialPartition2D {
1124
    fn from(value: SpatialPartition2D) -> Self {
45✔
1125
        Self::new_unchecked(
45✔
1126
            value.upper_left_coordinate.into(),
45✔
1127
            value.lower_right_coordinate.into(),
45✔
1128
        )
45✔
1129
    }
45✔
1130
}
1131

1132
/// A spatio-temporal rectangle with a specified resolution
1133
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
6✔
1134
#[serde(rename_all = "camelCase")]
1135
#[aliases(
1136
    VectorQueryRectangle = QueryRectangle<BoundingBox2D>,
1137
    RasterQueryRectangle = QueryRectangle<SpatialPartition2D>,
1138
    PlotQueryRectangle = QueryRectangle<BoundingBox2D>)
1139
]
1140
pub struct QueryRectangle<SpatialBounds> {
1141
    pub spatial_bounds: SpatialBounds,
1142
    pub time_interval: TimeInterval,
1143
    pub spatial_resolution: SpatialResolution,
1144
}
1145

1146
/// The spatial resolution in SRS units
1147
#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema, ToSql, FromSql)]
2,271✔
1148
pub struct SpatialResolution {
1149
    pub x: f64,
1150
    pub y: f64,
1151
}
1152

1153
impl From<geoengine_datatypes::primitives::SpatialResolution> for SpatialResolution {
1154
    fn from(value: geoengine_datatypes::primitives::SpatialResolution) -> Self {
134✔
1155
        Self {
134✔
1156
            x: value.x,
134✔
1157
            y: value.y,
134✔
1158
        }
134✔
1159
    }
134✔
1160
}
1161

1162
impl From<SpatialResolution> for geoengine_datatypes::primitives::SpatialResolution {
1163
    fn from(value: SpatialResolution) -> Self {
37✔
1164
        Self {
37✔
1165
            x: value.x,
37✔
1166
            y: value.y,
37✔
1167
        }
37✔
1168
    }
37✔
1169
}
1170

1171
#[derive(
1172
    Clone, Copy, Serialize, PartialEq, Eq, PartialOrd, Ord, Debug, ToSchema, FromSql, ToSql,
3,814✔
1173
)]
1174
#[repr(C)]
1175
#[postgres(transparent)]
1176
pub struct TimeInstance(i64);
1177

1178
impl FromStr for TimeInstance {
1179
    type Err = geoengine_datatypes::primitives::DateTimeError;
1180

1181
    fn from_str(s: &str) -> Result<Self, Self::Err> {
×
1182
        let date_time = DateTime::from_str(s)?;
×
1183
        Ok(date_time.into())
×
1184
    }
×
1185
}
1186

1187
impl From<geoengine_datatypes::primitives::TimeInstance> for TimeInstance {
1188
    fn from(value: geoengine_datatypes::primitives::TimeInstance) -> Self {
578✔
1189
        Self(value.inner())
578✔
1190
    }
578✔
1191
}
1192

1193
impl From<TimeInstance> for geoengine_datatypes::primitives::TimeInstance {
1194
    fn from(value: TimeInstance) -> Self {
224✔
1195
        geoengine_datatypes::primitives::TimeInstance::from_millis_unchecked(value.inner())
224✔
1196
    }
224✔
1197
}
1198

1199
impl From<DateTime> for TimeInstance {
1200
    fn from(datetime: DateTime) -> Self {
×
1201
        Self::from(&datetime)
×
1202
    }
×
1203
}
1204

1205
impl From<&DateTime> for TimeInstance {
1206
    fn from(datetime: &DateTime) -> Self {
×
1207
        geoengine_datatypes::primitives::TimeInstance::from_millis_unchecked(
×
1208
            datetime.datetime.timestamp_millis(),
×
1209
        )
×
1210
        .into()
×
1211
    }
×
1212
}
1213

1214
impl TimeInstance {
1215
    pub const fn inner(self) -> i64 {
224✔
1216
        self.0
224✔
1217
    }
224✔
1218
}
1219

1220
impl<'de> Deserialize<'de> for TimeInstance {
1221
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
16✔
1222
    where
16✔
1223
        D: serde::Deserializer<'de>,
16✔
1224
    {
16✔
1225
        struct IsoStringOrUnixTimestamp;
16✔
1226

16✔
1227
        impl<'de> serde::de::Visitor<'de> for IsoStringOrUnixTimestamp {
16✔
1228
            type Value = TimeInstance;
16✔
1229

16✔
1230
            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
16✔
1231
                formatter.write_str("RFC 3339 timestamp string or Unix timestamp integer")
×
1232
            }
×
1233

16✔
1234
            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
16✔
1235
            where
×
1236
                E: serde::de::Error,
×
1237
            {
×
1238
                TimeInstance::from_str(value).map_err(E::custom)
×
1239
            }
×
1240

16✔
1241
            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
16✔
1242
            where
16✔
1243
                E: serde::de::Error,
16✔
1244
            {
16✔
1245
                geoengine_datatypes::primitives::TimeInstance::from_millis(v)
16✔
1246
                    .map(Into::into)
16✔
1247
                    .map_err(E::custom)
16✔
1248
            }
16✔
1249

16✔
1250
            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
16✔
1251
            where
16✔
1252
                E: serde::de::Error,
16✔
1253
            {
16✔
1254
                Self::visit_i64(self, v as i64)
16✔
1255
            }
16✔
1256
        }
16✔
1257

16✔
1258
        deserializer.deserialize_any(IsoStringOrUnixTimestamp)
16✔
1259
    }
16✔
1260
}
1261

1262
/// A time granularity.
1263
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, FromSql, ToSql)]
3,656✔
1264
#[serde(rename_all = "camelCase")]
1265
pub enum TimeGranularity {
1266
    Millis,
1267
    Seconds,
1268
    Minutes,
1269
    Hours,
1270
    Days,
1271
    Months,
1272
    Years,
1273
}
1274

1275
impl From<geoengine_datatypes::primitives::TimeGranularity> for TimeGranularity {
1276
    fn from(value: geoengine_datatypes::primitives::TimeGranularity) -> Self {
69✔
1277
        match value {
69✔
1278
            geoengine_datatypes::primitives::TimeGranularity::Millis => Self::Millis,
11✔
1279
            geoengine_datatypes::primitives::TimeGranularity::Seconds => Self::Seconds,
×
1280
            geoengine_datatypes::primitives::TimeGranularity::Minutes => Self::Minutes,
×
1281
            geoengine_datatypes::primitives::TimeGranularity::Hours => Self::Hours,
×
1282
            geoengine_datatypes::primitives::TimeGranularity::Days => Self::Days,
×
1283
            geoengine_datatypes::primitives::TimeGranularity::Months => Self::Months,
58✔
1284
            geoengine_datatypes::primitives::TimeGranularity::Years => Self::Years,
×
1285
        }
1286
    }
69✔
1287
}
1288

1289
impl From<TimeGranularity> for geoengine_datatypes::primitives::TimeGranularity {
1290
    fn from(value: TimeGranularity) -> Self {
32✔
1291
        match value {
32✔
1292
            TimeGranularity::Millis => Self::Millis,
8✔
1293
            TimeGranularity::Seconds => Self::Seconds,
×
1294
            TimeGranularity::Minutes => Self::Minutes,
×
1295
            TimeGranularity::Hours => Self::Hours,
×
1296
            TimeGranularity::Days => Self::Days,
×
1297
            TimeGranularity::Months => Self::Months,
24✔
1298
            TimeGranularity::Years => Self::Years,
×
1299
        }
1300
    }
32✔
1301
}
1302

1303
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema, FromSql, ToSql)]
1,487✔
1304
pub struct TimeStep {
1305
    pub granularity: TimeGranularity,
1306
    pub step: u32, // TODO: ensure on deserialization it is > 0
1307
}
1308

1309
impl From<geoengine_datatypes::primitives::TimeStep> for TimeStep {
1310
    fn from(value: geoengine_datatypes::primitives::TimeStep) -> Self {
69✔
1311
        Self {
69✔
1312
            granularity: value.granularity.into(),
69✔
1313
            step: value.step,
69✔
1314
        }
69✔
1315
    }
69✔
1316
}
1317

1318
impl From<TimeStep> for geoengine_datatypes::primitives::TimeStep {
1319
    fn from(value: TimeStep) -> Self {
32✔
1320
        Self {
32✔
1321
            granularity: value.granularity.into(),
32✔
1322
            step: value.step,
32✔
1323
        }
32✔
1324
    }
32✔
1325
}
1326

1327
/// Stores time intervals in ms in close-open semantic [start, end)
1328
#[derive(Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ToSql, FromSql)]
5,349✔
1329
pub struct TimeInterval {
1330
    start: TimeInstance,
1331
    end: TimeInstance,
1332
}
1333

1334
impl<'a> ToSchema<'a> for TimeInterval {
1335
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
2✔
1336
        use utoipa::openapi::*;
2✔
1337
        (
2✔
1338
            "TimeInterval",
2✔
1339
            ObjectBuilder::new().schema_type(SchemaType::String).into(),
2✔
1340
        )
2✔
1341
    }
2✔
1342
}
1343

1344
impl From<TimeInterval> for geoengine_datatypes::primitives::TimeInterval {
1345
    fn from(value: TimeInterval) -> Self {
108✔
1346
        geoengine_datatypes::primitives::TimeInterval::new_unchecked::<
108✔
1347
            geoengine_datatypes::primitives::TimeInstance,
108✔
1348
            geoengine_datatypes::primitives::TimeInstance,
108✔
1349
        >(value.start.into(), value.end.into())
108✔
1350
    }
108✔
1351
}
1352

1353
impl From<geoengine_datatypes::primitives::TimeInterval> for TimeInterval {
1354
    fn from(value: geoengine_datatypes::primitives::TimeInterval) -> Self {
276✔
1355
        Self {
276✔
1356
            start: value.start().into(),
276✔
1357
            end: value.end().into(),
276✔
1358
        }
276✔
1359
    }
276✔
1360
}
1361

1362
impl core::fmt::Debug for TimeInterval {
1363
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
×
1364
        write!(
×
1365
            f,
×
1366
            "TimeInterval [{}, {})",
×
1367
            self.start.inner(),
×
1368
            &self.end.inner()
×
1369
        )
×
1370
    }
×
1371
}
1372

1373
#[derive(
1374
    Debug,
×
1375
    Ord,
×
1376
    PartialOrd,
×
1377
    Eq,
1378
    PartialEq,
16✔
1379
    Hash,
×
1380
    Deserialize,
8✔
1381
    Serialize,
7✔
1382
    Copy,
1383
    Clone,
129✔
1384
    ToSchema,
2✔
1385
    FromSql,
7,854✔
1386
    ToSql,
295✔
1387
)]
1388
pub enum RasterDataType {
1389
    U8,
1390
    U16,
1391
    U32,
1392
    U64,
1393
    I8,
1394
    I16,
1395
    I32,
1396
    I64,
1397
    F32,
1398
    F64,
1399
}
1400

1401
impl From<geoengine_datatypes::raster::RasterDataType> for RasterDataType {
1402
    fn from(value: geoengine_datatypes::raster::RasterDataType) -> Self {
150✔
1403
        match value {
150✔
1404
            geoengine_datatypes::raster::RasterDataType::U8 => Self::U8,
150✔
1405
            geoengine_datatypes::raster::RasterDataType::U16 => Self::U16,
×
1406
            geoengine_datatypes::raster::RasterDataType::U32 => Self::U32,
×
1407
            geoengine_datatypes::raster::RasterDataType::U64 => Self::U64,
×
1408
            geoengine_datatypes::raster::RasterDataType::I8 => Self::I8,
×
1409
            geoengine_datatypes::raster::RasterDataType::I16 => Self::I16,
×
1410
            geoengine_datatypes::raster::RasterDataType::I32 => Self::I32,
×
1411
            geoengine_datatypes::raster::RasterDataType::I64 => Self::I64,
×
1412
            geoengine_datatypes::raster::RasterDataType::F32 => Self::F32,
×
1413
            geoengine_datatypes::raster::RasterDataType::F64 => Self::F64,
×
1414
        }
1415
    }
150✔
1416
}
1417

1418
impl From<RasterDataType> for geoengine_datatypes::raster::RasterDataType {
1419
    fn from(value: RasterDataType) -> Self {
45✔
1420
        match value {
45✔
1421
            RasterDataType::U8 => Self::U8,
45✔
1422
            RasterDataType::U16 => Self::U16,
×
1423
            RasterDataType::U32 => Self::U32,
×
1424
            RasterDataType::U64 => Self::U64,
×
1425
            RasterDataType::I8 => Self::I8,
×
1426
            RasterDataType::I16 => Self::I16,
×
1427
            RasterDataType::I32 => Self::I32,
×
1428
            RasterDataType::I64 => Self::I64,
×
1429
            RasterDataType::F32 => Self::F32,
×
1430
            RasterDataType::F64 => Self::F64,
×
1431
        }
1432
    }
45✔
1433
}
1434

1435
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
×
1436
#[serde(rename_all = "UPPERCASE")]
1437
pub enum ResamplingMethod {
1438
    Nearest,
1439
    Average,
1440
    Bilinear,
1441
    Cubic,
1442
    CubicSpline,
1443
    Lanczos,
1444
}
1445

1446
impl std::fmt::Display for ResamplingMethod {
1447
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19✔
1448
        match self {
19✔
1449
            ResamplingMethod::Nearest => write!(f, "NEAREST"),
19✔
1450
            ResamplingMethod::Average => write!(f, "AVERAGE"),
×
1451
            ResamplingMethod::Bilinear => write!(f, "BILINEAR"),
×
1452
            ResamplingMethod::Cubic => write!(f, "CUBIC"),
×
1453
            ResamplingMethod::CubicSpline => write!(f, "CUBICSPLINE"),
×
1454
            ResamplingMethod::Lanczos => write!(f, "LANCZOS"),
×
1455
        }
1456
    }
19✔
1457
}
1458

1459
/// `RgbaColor` defines a 32 bit RGB color with alpha value
1460
#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
429✔
1461
pub struct RgbaColor(pub [u8; 4]);
1462

1463
impl ToSql for RgbaColor {
1464
    fn to_sql(
499✔
1465
        &self,
499✔
1466
        ty: &postgres_types::Type,
499✔
1467
        w: &mut bytes::BytesMut,
499✔
1468
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
499✔
1469
        let tuple = self.0.map(i16::from);
499✔
1470

1471
        let postgres_types::Kind::Domain(inner_type) = ty.kind() else {
499✔
1472
            return Err(Box::new(crate::error::Error::UnexpectedInvalidDbTypeConversion));
×
1473
        };
1474

1475
        <[i16; 4] as ToSql>::to_sql(&tuple, inner_type, w)
499✔
1476
    }
499✔
1477

1478
    fn accepts(ty: &postgres_types::Type) -> bool {
2✔
1479
        if ty.name() != "RgbaColor" {
2✔
1480
            return false;
×
1481
        }
2✔
1482
        let postgres_types::Kind::Domain(inner_type) = ty.kind() else {
2✔
1483
            return false;
×
1484
        };
1485

1486
        <[i16; 4] as ToSql>::accepts(inner_type)
2✔
1487
    }
2✔
1488

1489
    postgres_types::to_sql_checked!();
1490
}
1491

1492
impl<'a> FromSql<'a> for RgbaColor {
1493
    fn from_sql(
178✔
1494
        ty: &postgres_types::Type,
178✔
1495
        raw: &'a [u8],
178✔
1496
    ) -> Result<RgbaColor, Box<dyn std::error::Error + Sync + Send>> {
178✔
1497
        let array_ty = match ty.kind() {
178✔
1498
            postgres_types::Kind::Domain(inner_type) => inner_type,
177✔
1499
            _ => ty,
1✔
1500
        };
1501

1502
        let tuple = <[i16; 4] as FromSql>::from_sql(array_ty, raw)?;
178✔
1503

1504
        Ok(RgbaColor(tuple.map(|v| v as u8)))
712✔
1505
    }
178✔
1506

1507
    fn accepts(ty: &postgres_types::Type) -> bool {
17,147✔
1508
        type Target = [i16; 4];
17,147✔
1509

17,147✔
1510
        if <Target as FromSql>::accepts(ty) {
17,147✔
1511
            return true;
1✔
1512
        }
17,146✔
1513

17,146✔
1514
        if ty.name() != "RgbaColor" {
17,146✔
1515
            return false;
×
1516
        }
17,146✔
1517
        let postgres_types::Kind::Domain(inner_type) = ty.kind() else {
17,146✔
1518
            return false;
×
1519
        };
1520

1521
        <Target as FromSql>::accepts(inner_type)
17,146✔
1522
    }
17,147✔
1523
}
1524

1525
// manual implementation utoipa generates an integer field
1526
impl<'a> ToSchema<'a> for RgbaColor {
1527
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
2✔
1528
        use utoipa::openapi::*;
2✔
1529
        (
2✔
1530
            "RgbaColor",
2✔
1531
            ArrayBuilder::new()
2✔
1532
                .items(ObjectBuilder::new().schema_type(SchemaType::Integer))
2✔
1533
                .min_items(Some(4))
2✔
1534
                .max_items(Some(4))
2✔
1535
                .into(),
2✔
1536
        )
2✔
1537
    }
2✔
1538
}
1539

1540
impl From<geoengine_datatypes::operations::image::RgbaColor> for RgbaColor {
1541
    fn from(color: geoengine_datatypes::operations::image::RgbaColor) -> Self {
359✔
1542
        Self(color.into_inner())
359✔
1543
    }
359✔
1544
}
1545

1546
impl From<RgbaColor> for geoengine_datatypes::operations::image::RgbaColor {
1547
    fn from(color: RgbaColor) -> Self {
44✔
1548
        Self::new(color.0[0], color.0[1], color.0[2], color.0[3])
44✔
1549
    }
44✔
1550
}
1551

1552
/// A container type for breakpoints that specify a value to color mapping
1553
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSql, FromSql)]
7,877✔
1554
pub struct Breakpoint {
1555
    pub value: NotNanF64,
1556
    pub color: RgbaColor,
1557
}
1558

1559
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
404✔
1560
pub struct NotNanF64(NotNan<f64>);
1561

1562
impl From<NotNan<f64>> for NotNanF64 {
1563
    fn from(value: NotNan<f64>) -> Self {
406✔
1564
        Self(value)
406✔
1565
    }
406✔
1566
}
1567

1568
impl From<NotNanF64> for NotNan<f64> {
1569
    fn from(value: NotNanF64) -> Self {
3✔
1570
        value.0
3✔
1571
    }
3✔
1572
}
1573

1574
impl ToSql for NotNanF64 {
1575
    fn to_sql(
416✔
1576
        &self,
416✔
1577
        ty: &postgres_types::Type,
416✔
1578
        w: &mut bytes::BytesMut,
416✔
1579
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
416✔
1580
        <f64 as ToSql>::to_sql(&self.0.into_inner(), ty, w)
416✔
1581
    }
416✔
1582

1583
    fn accepts(ty: &postgres_types::Type) -> bool {
2✔
1584
        <f64 as ToSql>::accepts(ty)
2✔
1585
    }
2✔
1586

1587
    postgres_types::to_sql_checked!();
1588
}
1589

1590
impl<'a> FromSql<'a> for NotNanF64 {
1591
    fn from_sql(
111✔
1592
        ty: &postgres_types::Type,
111✔
1593
        raw: &'a [u8],
111✔
1594
    ) -> Result<NotNanF64, Box<dyn std::error::Error + Sync + Send>> {
111✔
1595
        let value = <f64 as FromSql>::from_sql(ty, raw)?;
111✔
1596

1597
        Ok(NotNanF64(value.try_into()?))
111✔
1598
    }
111✔
1599

1600
    fn accepts(ty: &postgres_types::Type) -> bool {
2,480✔
1601
        <f64 as FromSql>::accepts(ty)
2,480✔
1602
    }
2,480✔
1603
}
1604

1605
// manual implementation because of NotNan
1606
impl<'a> ToSchema<'a> for Breakpoint {
1607
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
2✔
1608
        use utoipa::openapi::*;
2✔
1609
        (
2✔
1610
            "Breakpoint",
2✔
1611
            ObjectBuilder::new()
2✔
1612
                .property("value", Object::with_type(SchemaType::Number))
2✔
1613
                .property("color", Ref::from_schema_name("RgbaColor"))
2✔
1614
                .into(),
2✔
1615
        )
2✔
1616
    }
2✔
1617
}
1618

1619
impl From<geoengine_datatypes::operations::image::Breakpoint> for Breakpoint {
1620
    fn from(breakpoint: geoengine_datatypes::operations::image::Breakpoint) -> Self {
122✔
1621
        Self {
122✔
1622
            value: breakpoint.value.into(),
122✔
1623
            color: breakpoint.color.into(),
122✔
1624
        }
122✔
1625
    }
122✔
1626
}
1627

1628
#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSchema)]
40✔
1629
#[serde(untagged, rename_all = "camelCase", into = "OverUnderColors")]
1630
pub enum DefaultColors {
1631
    #[serde(rename_all = "camelCase")]
1632
    DefaultColor { default_color: RgbaColor },
1633
    #[serde(rename_all = "camelCase")]
1634
    OverUnder(OverUnderColors),
1635
}
1636

1637
#[derive(Copy, Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSchema)]
40✔
1638
#[serde(rename_all = "camelCase")]
1639
pub struct OverUnderColors {
1640
    pub over_color: RgbaColor,
1641
    pub under_color: RgbaColor,
1642
}
1643

1644
impl From<DefaultColors> for OverUnderColors {
1645
    fn from(value: DefaultColors) -> Self {
×
1646
        match value {
×
1647
            DefaultColors::DefaultColor { default_color } => Self {
×
1648
                over_color: default_color,
×
1649
                under_color: default_color,
×
1650
            },
×
1651
            DefaultColors::OverUnder(over_under) => over_under,
×
1652
        }
1653
    }
×
1654
}
1655

1656
impl From<DefaultColors> for geoengine_datatypes::operations::image::DefaultColors {
1657
    fn from(value: DefaultColors) -> Self {
×
1658
        match value {
×
1659
            DefaultColors::DefaultColor { default_color } => Self::DefaultColor {
×
1660
                default_color: default_color.into(),
×
1661
            },
×
1662
            DefaultColors::OverUnder(OverUnderColors {
1663
                over_color,
×
1664
                under_color,
×
1665
            }) => Self::OverUnder {
×
1666
                over_color: over_color.into(),
×
1667
                under_color: under_color.into(),
×
1668
            },
×
1669
        }
1670
    }
×
1671
}
1672

1673
impl From<geoengine_datatypes::operations::image::DefaultColors> for DefaultColors {
1674
    fn from(value: geoengine_datatypes::operations::image::DefaultColors) -> Self {
61✔
1675
        match value {
61✔
1676
            geoengine_datatypes::operations::image::DefaultColors::DefaultColor {
1677
                default_color,
×
1678
            } => Self::DefaultColor {
×
1679
                default_color: default_color.into(),
×
1680
            },
×
1681
            geoengine_datatypes::operations::image::DefaultColors::OverUnder {
1682
                over_color,
61✔
1683
                under_color,
61✔
1684
            } => Self::OverUnder(OverUnderColors {
61✔
1685
                over_color: over_color.into(),
61✔
1686
                under_color: under_color.into(),
61✔
1687
            }),
61✔
1688
        }
1689
    }
61✔
1690
}
1691

1692
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSchema)]
56✔
1693
#[serde(rename_all = "camelCase")]
1694
pub struct LinearGradient {
1695
    pub breakpoints: Vec<Breakpoint>,
1696
    pub no_data_color: RgbaColor,
1697
    #[serde(flatten)]
1698
    pub color_fields: DefaultColors,
1699
}
1700

1701
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSchema)]
2✔
1702
#[serde(rename_all = "camelCase")]
1703
pub struct LogarithmicGradient {
1704
    pub breakpoints: Vec<Breakpoint>,
1705
    pub no_data_color: RgbaColor,
1706
    #[serde(flatten)]
1707
    pub color_fields: DefaultColors,
1708
}
1709

1710
/// A colorizer specifies a mapping between raster values and an output image
1711
/// There are different variants that perform different kinds of mapping.
1712
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, ToSchema)]
47✔
1713
#[serde(rename_all = "camelCase", tag = "type")]
1714
pub enum Colorizer {
1715
    #[serde(rename_all = "camelCase")]
1716
    LinearGradient(LinearGradient),
1717
    #[serde(rename_all = "camelCase")]
1718
    LogarithmicGradient(LogarithmicGradient),
1719
    #[serde(rename_all = "camelCase")]
1720
    Palette {
1721
        colors: Palette,
1722
        no_data_color: RgbaColor,
1723
        default_color: RgbaColor,
1724
    },
1725
    Rgba,
1726
}
1727

1728
impl From<geoengine_datatypes::operations::image::Colorizer> for Colorizer {
1729
    fn from(v: geoengine_datatypes::operations::image::Colorizer) -> Self {
65✔
1730
        match v {
65✔
1731
            geoengine_datatypes::operations::image::Colorizer::LinearGradient {
1732
                breakpoints,
61✔
1733
                no_data_color,
61✔
1734
                default_colors: color_fields,
61✔
1735
            } => Self::LinearGradient(LinearGradient {
61✔
1736
                breakpoints: breakpoints
61✔
1737
                    .into_iter()
61✔
1738
                    .map(Into::into)
61✔
1739
                    .collect::<Vec<Breakpoint>>(),
61✔
1740
                no_data_color: no_data_color.into(),
61✔
1741
                color_fields: color_fields.into(),
61✔
1742
            }),
61✔
1743
            geoengine_datatypes::operations::image::Colorizer::LogarithmicGradient {
1744
                breakpoints,
×
1745
                no_data_color,
×
1746
                default_colors: color_fields,
×
1747
            } => Self::LogarithmicGradient(LogarithmicGradient {
×
1748
                breakpoints: breakpoints
×
1749
                    .into_iter()
×
1750
                    .map(Into::into)
×
1751
                    .collect::<Vec<Breakpoint>>(),
×
1752
                no_data_color: no_data_color.into(),
×
1753
                color_fields: color_fields.into(),
×
1754
            }),
×
1755
            geoengine_datatypes::operations::image::Colorizer::Palette {
1756
                colors,
1✔
1757
                no_data_color,
1✔
1758
                default_color,
1✔
1759
            } => Self::Palette {
1✔
1760
                colors: colors.into(),
1✔
1761
                no_data_color: no_data_color.into(),
1✔
1762
                default_color: default_color.into(),
1✔
1763
            },
1✔
1764
            geoengine_datatypes::operations::image::Colorizer::Rgba => Self::Rgba,
3✔
1765
        }
1766
    }
65✔
1767
}
1768

1769
/// A map from value to color
1770
///
1771
/// It is assumed that is has at least one and at most 256 entries.
1772
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
2✔
1773
#[serde(try_from = "SerializablePalette", into = "SerializablePalette")]
1774
pub struct Palette(pub HashMap<NotNan<f64>, RgbaColor>);
1775

1776
impl From<geoengine_datatypes::operations::image::Palette> for Palette {
1777
    fn from(palette: geoengine_datatypes::operations::image::Palette) -> Self {
1✔
1778
        Self(
1✔
1779
            palette
1✔
1780
                .into_inner()
1✔
1781
                .into_iter()
1✔
1782
                .map(|(value, color)| (value, color.into()))
17✔
1783
                .collect(),
1✔
1784
        )
1✔
1785
    }
1✔
1786
}
1787

1788
/// A type that is solely for serde's serializability.
1789
/// You cannot serialize floats as JSON map keys.
1790
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1✔
1791
pub struct SerializablePalette(HashMap<String, RgbaColor>);
1792

1793
impl From<Palette> for SerializablePalette {
1794
    fn from(palette: Palette) -> Self {
×
1795
        Self(
×
1796
            palette
×
1797
                .0
×
1798
                .into_iter()
×
1799
                .map(|(k, v)| (k.to_string(), v))
×
1800
                .collect(),
×
1801
        )
×
1802
    }
×
1803
}
1804

1805
impl TryFrom<SerializablePalette> for Palette {
1806
    type Error = <NotNan<f64> as FromStr>::Err;
1807

1808
    fn try_from(palette: SerializablePalette) -> Result<Self, Self::Error> {
1✔
1809
        let mut inner = HashMap::<NotNan<f64>, RgbaColor>::with_capacity(palette.0.len());
1✔
1810
        for (k, v) in palette.0 {
257✔
1811
            inner.insert(k.parse()?, v);
256✔
1812
        }
1813
        Ok(Self(inner))
1✔
1814
    }
1✔
1815
}
1816

1817
#[derive(
1818
    Debug,
×
1819
    Clone,
26✔
1820
    Serialize,
×
1821
    Deserialize,
×
1822
    PartialEq,
12✔
1823
    Hash,
×
1824
    Eq,
1825
    PartialOrd,
×
1826
    Ord,
×
1827
    ToSchema,
2✔
1828
    FromSql,
3,626✔
1829
    ToSql,
56✔
1830
)]
1831
pub struct RasterPropertiesKey {
1832
    pub domain: Option<String>,
1833
    pub key: String,
1834
}
1835

1836
impl From<geoengine_datatypes::raster::RasterPropertiesKey> for RasterPropertiesKey {
1837
    fn from(value: geoengine_datatypes::raster::RasterPropertiesKey) -> Self {
8✔
1838
        Self {
8✔
1839
            domain: value.domain,
8✔
1840
            key: value.key,
8✔
1841
        }
8✔
1842
    }
8✔
1843
}
1844

1845
impl From<RasterPropertiesKey> for geoengine_datatypes::raster::RasterPropertiesKey {
1846
    fn from(value: RasterPropertiesKey) -> Self {
16✔
1847
        Self {
16✔
1848
            domain: value.domain,
16✔
1849
            key: value.key,
16✔
1850
        }
16✔
1851
    }
16✔
1852
}
1853

1854
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema, FromSql, ToSql)]
1,783✔
1855
pub enum RasterPropertiesEntryType {
1856
    Number,
1857
    String,
1858
}
1859

1860
impl From<geoengine_datatypes::raster::RasterPropertiesEntryType> for RasterPropertiesEntryType {
1861
    fn from(value: geoengine_datatypes::raster::RasterPropertiesEntryType) -> Self {
4✔
1862
        match value {
4✔
1863
            geoengine_datatypes::raster::RasterPropertiesEntryType::Number => Self::Number,
×
1864
            geoengine_datatypes::raster::RasterPropertiesEntryType::String => Self::String,
4✔
1865
        }
1866
    }
4✔
1867
}
1868

1869
impl From<RasterPropertiesEntryType> for geoengine_datatypes::raster::RasterPropertiesEntryType {
1870
    fn from(value: RasterPropertiesEntryType) -> Self {
8✔
1871
        match value {
8✔
1872
            RasterPropertiesEntryType::Number => Self::Number,
×
1873
            RasterPropertiesEntryType::String => Self::String,
8✔
1874
        }
1875
    }
8✔
1876
}
1877

1878
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, ToSchema, FromSql, ToSql)]
5,664✔
1879
pub struct DateTimeParseFormat {
1880
    fmt: String,
1881
    has_tz: bool,
1882
    has_time: bool,
1883
}
1884

1885
impl From<geoengine_datatypes::primitives::DateTimeParseFormat> for DateTimeParseFormat {
1886
    fn from(value: geoengine_datatypes::primitives::DateTimeParseFormat) -> Self {
66✔
1887
        Self {
66✔
1888
            fmt: value._to_parse_format().to_string(),
66✔
1889
            has_tz: value.has_tz(),
66✔
1890
            has_time: value.has_time(),
66✔
1891
        }
66✔
1892
    }
66✔
1893
}
1894

1895
impl From<DateTimeParseFormat> for geoengine_datatypes::primitives::DateTimeParseFormat {
1896
    fn from(value: DateTimeParseFormat) -> Self {
28✔
1897
        Self::custom(value.fmt)
28✔
1898
    }
28✔
1899
}
1900

1901
impl DateTimeParseFormat {
1902
    // this is used as default value
1903
    pub fn unix() -> Self {
×
1904
        geoengine_datatypes::primitives::DateTimeParseFormat::unix().into()
×
1905
    }
×
1906
}
1907

1908
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
2✔
1909
pub struct NoGeometry;
1910

1911
impl From<geoengine_datatypes::primitives::NoGeometry> for NoGeometry {
1912
    fn from(_: geoengine_datatypes::primitives::NoGeometry) -> Self {
×
1913
        Self {}
×
1914
    }
×
1915
}
1916

1917
impl From<NoGeometry> for geoengine_datatypes::primitives::NoGeometry {
1918
    fn from(_: NoGeometry) -> Self {
×
1919
        Self {}
×
1920
    }
×
1921
}
1922

1923
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
9✔
1924
pub struct MultiPoint {
1925
    pub coordinates: Vec<Coordinate2D>,
1926
}
1927

1928
impl From<geoengine_datatypes::primitives::MultiPoint> for MultiPoint {
1929
    fn from(value: geoengine_datatypes::primitives::MultiPoint) -> Self {
1✔
1930
        Self {
1✔
1931
            coordinates: value.points().iter().map(|x| (*x).into()).collect(),
2✔
1932
        }
1✔
1933
    }
1✔
1934
}
1935

1936
impl From<MultiPoint> for geoengine_datatypes::primitives::MultiPoint {
1937
    fn from(value: MultiPoint) -> Self {
2✔
1938
        Self::new(value.coordinates.into_iter().map(Into::into).collect()).unwrap()
2✔
1939
    }
2✔
1940
}
1941

1942
impl ToSql for MultiPoint {
1943
    fn to_sql(
5✔
1944
        &self,
5✔
1945
        ty: &postgres_types::Type,
5✔
1946
        w: &mut bytes::BytesMut,
5✔
1947
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
5✔
1948
        let postgres_types::Kind::Array(member_type) = ty.kind() else {
5✔
1949
             panic!("expected array type");
×
1950
        };
1951

1952
        let dimension = postgres_protocol::types::ArrayDimension {
5✔
1953
            len: self.coordinates.len() as i32,
5✔
1954
            lower_bound: 1, // arrays are one-indexed
5✔
1955
        };
5✔
1956

5✔
1957
        postgres_protocol::types::array_to_sql(
5✔
1958
            Some(dimension),
5✔
1959
            member_type.oid(),
5✔
1960
            self.coordinates.iter(),
5✔
1961
            |c, w| {
10✔
1962
                postgres_protocol::types::point_to_sql(c.x, c.y, w);
10✔
1963

10✔
1964
                Ok(postgres_protocol::IsNull::No)
10✔
1965
            },
10✔
1966
            w,
5✔
1967
        )?;
5✔
1968

1969
        Ok(postgres_types::IsNull::No)
5✔
1970
    }
5✔
1971

1972
    fn accepts(ty: &postgres_types::Type) -> bool {
1✔
1973
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
1✔
1974
            return false;
×
1975
        };
1976

1977
        matches!(inner_type, &postgres_types::Type::POINT)
1✔
1978
    }
1✔
1979

1980
    postgres_types::to_sql_checked!();
1981
}
1982

1983
impl<'a> FromSql<'a> for MultiPoint {
1984
    fn from_sql(
5✔
1985
        _ty: &postgres_types::Type,
5✔
1986
        raw: &'a [u8],
5✔
1987
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
5✔
1988
        let array = postgres_protocol::types::array_from_sql(raw)?;
5✔
1989
        if array.dimensions().count()? > 1 {
5✔
1990
            return Err("array contains too many dimensions".into());
×
1991
        }
5✔
1992

1993
        let coordinates = array
5✔
1994
            .values()
5✔
1995
            .map(|raw| {
10✔
1996
                let Some(raw) = raw else {
10✔
1997
                    return Err("array contains NULL values".into());
×
1998
                };
1999
                let point = postgres_protocol::types::point_from_sql(raw)?;
10✔
2000
                Ok(Coordinate2D {
10✔
2001
                    x: point.x(),
10✔
2002
                    y: point.y(),
10✔
2003
                })
10✔
2004
            })
10✔
2005
            .collect()?;
5✔
2006

2007
        Ok(Self { coordinates })
5✔
2008
    }
5✔
2009

2010
    fn accepts(ty: &postgres_types::Type) -> bool {
158✔
2011
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
158✔
2012
            return false;
×
2013
        };
2014

2015
        matches!(inner_type, &postgres_types::Type::POINT)
158✔
2016
    }
158✔
2017
}
2018

2019
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
2✔
2020
pub struct MultiLineString {
2021
    pub coordinates: Vec<Vec<Coordinate2D>>,
2022
}
2023

2024
impl From<geoengine_datatypes::primitives::MultiLineString> for MultiLineString {
2025
    fn from(value: geoengine_datatypes::primitives::MultiLineString) -> Self {
×
2026
        Self {
×
2027
            coordinates: value
×
2028
                .lines()
×
2029
                .iter()
×
2030
                .map(|x| x.iter().map(|x| (*x).into()).collect())
×
2031
                .collect(),
×
2032
        }
×
2033
    }
×
2034
}
2035

2036
impl From<MultiLineString> for geoengine_datatypes::primitives::MultiLineString {
2037
    fn from(value: MultiLineString) -> Self {
×
2038
        Self::new(
×
2039
            value
×
2040
                .coordinates
×
2041
                .into_iter()
×
2042
                .map(|x| x.into_iter().map(Into::into).collect())
×
2043
                .collect(),
×
2044
        )
×
2045
        .unwrap()
×
2046
    }
×
2047
}
2048

2049
impl ToSql for MultiLineString {
2050
    fn to_sql(
2✔
2051
        &self,
2✔
2052
        ty: &postgres_types::Type,
2✔
2053
        w: &mut bytes::BytesMut,
2✔
2054
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
2✔
2055
        let postgres_types::Kind::Array(member_type) = ty.kind() else {
2✔
2056
             panic!("expected array type");
×
2057
        };
2058

2059
        let dimension = postgres_protocol::types::ArrayDimension {
2✔
2060
            len: self.coordinates.len() as i32,
2✔
2061
            lower_bound: 1, // arrays are one-indexed
2✔
2062
        };
2✔
2063

2✔
2064
        postgres_protocol::types::array_to_sql(
2✔
2065
            Some(dimension),
2✔
2066
            member_type.oid(),
2✔
2067
            self.coordinates.iter(),
2✔
2068
            |coordinates, w| {
2✔
2069
                postgres_protocol::types::path_to_sql(
4✔
2070
                    false,
4✔
2071
                    coordinates.iter().map(|p| (p.x, p.y)),
8✔
2072
                    w,
4✔
2073
                )?;
4✔
2074

2075
                Ok(postgres_protocol::IsNull::No)
4✔
2076
            },
4✔
2077
            w,
2✔
2078
        )?;
2✔
2079

2080
        Ok(postgres_types::IsNull::No)
2✔
2081
    }
2✔
2082

2083
    fn accepts(ty: &postgres_types::Type) -> bool {
1✔
2084
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
1✔
2085
            return false;
×
2086
        };
2087

2088
        matches!(inner_type, &postgres_types::Type::PATH)
1✔
2089
    }
1✔
2090

2091
    postgres_types::to_sql_checked!();
2092
}
2093

2094
impl<'a> FromSql<'a> for MultiLineString {
2095
    fn from_sql(
2✔
2096
        _ty: &postgres_types::Type,
2✔
2097
        raw: &'a [u8],
2✔
2098
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
2✔
2099
        let array = postgres_protocol::types::array_from_sql(raw)?;
2✔
2100
        if array.dimensions().count()? > 1 {
2✔
2101
            return Err("array contains too many dimensions".into());
×
2102
        }
2✔
2103

2104
        let coordinates = array
2✔
2105
            .values()
2✔
2106
            .map(|raw| {
4✔
2107
                let Some(raw) = raw else {
4✔
2108
                    return Err("array contains NULL values".into());
×
2109
                };
2110
                let path = postgres_protocol::types::path_from_sql(raw)?;
4✔
2111

2112
                let coordinates = path
4✔
2113
                    .points()
4✔
2114
                    .map(|point| {
8✔
2115
                        Ok(Coordinate2D {
8✔
2116
                            x: point.x(),
8✔
2117
                            y: point.y(),
8✔
2118
                        })
8✔
2119
                    })
8✔
2120
                    .collect()?;
4✔
2121
                Ok(coordinates)
4✔
2122
            })
4✔
2123
            .collect()?;
2✔
2124

2125
        Ok(Self { coordinates })
2✔
2126
    }
2✔
2127

2128
    fn accepts(ty: &postgres_types::Type) -> bool {
158✔
2129
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
158✔
2130
            return false;
×
2131
        };
2132

2133
        matches!(inner_type, &postgres_types::Type::PATH)
158✔
2134
    }
158✔
2135
}
2136

2137
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
2✔
2138
pub struct MultiPolygon {
2139
    pub polygons: Vec<Vec<Vec<Coordinate2D>>>,
2140
}
2141

2142
impl From<geoengine_datatypes::primitives::MultiPolygon> for MultiPolygon {
2143
    fn from(value: geoengine_datatypes::primitives::MultiPolygon) -> Self {
×
2144
        Self {
×
2145
            polygons: value
×
2146
                .polygons()
×
2147
                .iter()
×
2148
                .map(|x| {
×
2149
                    x.iter()
×
2150
                        .map(|y| y.iter().map(|y| (*y).into()).collect())
×
2151
                        .collect()
×
2152
                })
×
2153
                .collect(),
×
2154
        }
×
2155
    }
×
2156
}
2157

2158
impl From<MultiPolygon> for geoengine_datatypes::primitives::MultiPolygon {
2159
    fn from(value: MultiPolygon) -> Self {
×
2160
        Self::new(
×
2161
            value
×
2162
                .polygons
×
2163
                .iter()
×
2164
                .map(|x| {
×
2165
                    x.iter()
×
2166
                        .map(|y| y.iter().map(|y| (*y).into()).collect())
×
2167
                        .collect()
×
2168
                })
×
2169
                .collect(),
×
2170
        )
×
2171
        .unwrap()
×
2172
    }
×
2173
}
2174

2175
impl ToSql for MultiPolygon {
2176
    fn to_sql(
2✔
2177
        &self,
2✔
2178
        ty: &postgres_types::Type,
2✔
2179
        w: &mut bytes::BytesMut,
2✔
2180
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
2✔
2181
        let postgres_types::Kind::Array(member_type) = ty.kind() else {
2✔
2182
             panic!("expected array type");
×
2183
        };
2184

2185
        let dimension = postgres_protocol::types::ArrayDimension {
2✔
2186
            len: self.polygons.len() as i32,
2✔
2187
            lower_bound: 1, // arrays are one-indexed
2✔
2188
        };
2✔
2189

2✔
2190
        postgres_protocol::types::array_to_sql(
2✔
2191
            Some(dimension),
2✔
2192
            member_type.oid(),
2✔
2193
            self.polygons.iter(),
2✔
2194
            |rings, w| match <PolygonRef as ToSql>::to_sql(&PolygonRef { rings }, member_type, w)? {
4✔
2195
                postgres_types::IsNull::No => Ok(postgres_protocol::IsNull::No),
4✔
2196
                postgres_types::IsNull::Yes => Ok(postgres_protocol::IsNull::Yes),
×
2197
            },
4✔
2198
            w,
2✔
2199
        )?;
2✔
2200

2201
        Ok(postgres_types::IsNull::No)
2✔
2202
    }
2✔
2203

2204
    fn accepts(ty: &postgres_types::Type) -> bool {
1✔
2205
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
1✔
2206
            return false;
×
2207
        };
2208

2209
        <PolygonRef as ToSql>::accepts(inner_type)
1✔
2210
    }
1✔
2211

2212
    postgres_types::to_sql_checked!();
2213
}
2214

2215
impl<'a> FromSql<'a> for MultiPolygon {
2216
    fn from_sql(
2✔
2217
        ty: &postgres_types::Type,
2✔
2218
        raw: &'a [u8],
2✔
2219
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
2✔
2220
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
2✔
2221
            return Err("inner type is not of type array".into());
×
2222
        };
2223

2224
        let array = postgres_protocol::types::array_from_sql(raw)?;
2✔
2225
        if array.dimensions().count()? > 1 {
2✔
2226
            return Err("array contains too many dimensions".into());
×
2227
        }
2✔
2228

2229
        let polygons = array
2✔
2230
            .values()
2✔
2231
            .map(|raw| {
4✔
2232
                let Some(raw) = raw else {
4✔
2233
                    return Err("array contains NULL values".into());
×
2234
                };
2235
                let polygon = <PolygonOwned as FromSql>::from_sql(inner_type, raw)?;
4✔
2236
                Ok(polygon.rings)
4✔
2237
            })
4✔
2238
            .collect()?;
2✔
2239

2240
        Ok(Self { polygons })
2✔
2241
    }
2✔
2242

2243
    fn accepts(ty: &postgres_types::Type) -> bool {
158✔
2244
        let postgres_types::Kind::Array(inner_type) = ty.kind() else {
158✔
2245
            return false;
×
2246
        };
2247

2248
        inner_type.name() == "Polygon"
158✔
2249
    }
158✔
2250
}
2251

2252
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
13✔
2253
pub struct StringPair((String, String));
2254

2255
impl<'a> ToSchema<'a> for StringPair {
2256
    fn schema() -> (&'a str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
6✔
2257
        use utoipa::openapi::*;
6✔
2258
        (
6✔
2259
            "StringPair",
6✔
2260
            ArrayBuilder::new()
6✔
2261
                .items(Object::with_type(SchemaType::String))
6✔
2262
                .min_items(Some(2))
6✔
2263
                .max_items(Some(2))
6✔
2264
                .into(),
6✔
2265
        )
6✔
2266
    }
6✔
2267
}
2268

2269
impl ToSql for StringPair {
2270
    fn to_sql(
10✔
2271
        &self,
10✔
2272
        ty: &postgres_types::Type,
10✔
2273
        w: &mut bytes::BytesMut,
10✔
2274
    ) -> Result<postgres_types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
10✔
2275
        // unpack domain type
2276
        let ty = match ty.kind() {
10✔
2277
            postgres_types::Kind::Domain(ty) => ty,
10✔
2278
            _ => ty,
×
2279
        };
2280

2281
        let (a, b) = &self.0;
10✔
2282
        <[&String; 2] as ToSql>::to_sql(&[a, b], ty, w)
10✔
2283
    }
10✔
2284

2285
    fn accepts(ty: &postgres_types::Type) -> bool {
1✔
2286
        // unpack domain type
2287
        let ty = match ty.kind() {
1✔
2288
            postgres_types::Kind::Domain(ty) => ty,
1✔
2289
            _ => ty,
×
2290
        };
2291

2292
        <[String; 2] as ToSql>::accepts(ty)
1✔
2293
    }
1✔
2294

2295
    postgres_types::to_sql_checked!();
2296
}
2297

2298
impl<'a> FromSql<'a> for StringPair {
2299
    fn from_sql(
10✔
2300
        ty: &postgres_types::Type,
10✔
2301
        raw: &'a [u8],
10✔
2302
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
10✔
2303
        // unpack domain type
2304
        let ty = match ty.kind() {
10✔
2305
            postgres_types::Kind::Domain(ty) => ty,
9✔
2306
            _ => ty,
1✔
2307
        };
2308

2309
        let [a, b] = <[String; 2] as FromSql>::from_sql(ty, raw)?;
10✔
2310

2311
        Ok(Self((a, b)))
10✔
2312
    }
10✔
2313

2314
    fn accepts(ty: &postgres_types::Type) -> bool {
591✔
2315
        // unpack domain type
2316
        let ty = match ty.kind() {
591✔
2317
            postgres_types::Kind::Domain(ty) => ty,
590✔
2318
            _ => ty,
1✔
2319
        };
2320

2321
        <[String; 2] as FromSql>::accepts(ty)
591✔
2322
    }
591✔
2323
}
2324

2325
impl From<(String, String)> for StringPair {
2326
    fn from(value: (String, String)) -> Self {
40✔
2327
        Self(value)
40✔
2328
    }
40✔
2329
}
2330

2331
impl From<StringPair> for (String, String) {
2332
    fn from(value: StringPair) -> Self {
8✔
2333
        value.0
8✔
2334
    }
8✔
2335
}
2336

2337
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize, ToSchema)]
2✔
2338
pub enum PlotOutputFormat {
2339
    JsonPlain,
2340
    JsonVega,
2341
    ImagePng,
2342
}
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