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

geo-engine / geoengine / 6035497100

31 Aug 2023 08:32AM UTC coverage: 90.035% (+0.1%) from 89.934%
6035497100

push

github

web-flow
Merge pull request #868 from geo-engine/update-2023-08-29

Update 2023 08 29

171 of 171 new or added lines in 36 files covered. (100.0%)

106387 of 118162 relevant lines covered (90.03%)

61276.9 hits per line

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

74.33
/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 {
23✔
24
        Self(value.0)
23✔
25
    }
23✔
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 {
17✔
62
            return Some(id.clone());
17✔
63
        }
×
64
        None
×
65
    }
17✔
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 {
23✔
96
        Self {
23✔
97
            provider_id: value.provider_id.into(),
23✔
98
            layer_id: value.layer_id.into(),
23✔
99
        }
23✔
100
    }
23✔
101
}
102

103
impl From<geoengine_datatypes::dataset::DataId> for DataId {
104
    fn from(id: geoengine_datatypes::dataset::DataId) -> Self {
17✔
105
        match id {
17✔
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) => {
15✔
110
                Self::External(external_id.into())
15✔
111
            }
112
        }
113
    }
17✔
114
}
115

116
impl From<DataId> for geoengine_datatypes::dataset::DataId {
117
    fn from(id: DataId) -> Self {
19✔
118
        match id {
19✔
119
            DataId::Internal { dataset_id } => Self::Internal {
×
120
                dataset_id: dataset_id.into(),
×
121
            },
×
122
            DataId::External(external_id) => Self::External(external_id.into()),
19✔
123
        }
124
    }
19✔
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)]
714✔
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>
21✔
261
    where
21✔
262
        D: serde::Deserializer<'de>,
21✔
263
    {
21✔
264
        deserializer.deserialize_str(DatasetNameDeserializeVisitor)
21✔
265
    }
21✔
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>
21✔
282
    where
21✔
283
        E: serde::de::Error,
21✔
284
    {
21✔
285
        let mut strings = [None, None];
21✔
286
        let mut split = s.split(geoengine_datatypes::dataset::NAME_DELIMITER);
21✔
287

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

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

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

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

307
        match strings {
21✔
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 {
14✔
316
                namespace: None,
14✔
317
                name,
14✔
318
            }),
14✔
319
            _ => Err(E::custom("empty named data")),
×
320
        }
321
    }
21✔
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 {
33✔
347
        Self {
33✔
348
            namespace: named_data.namespace.clone(),
33✔
349
            name: named_data.name.clone(),
33✔
350
        }
33✔
351
    }
33✔
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 {
23✔
389
        Self(value.0)
23✔
390
    }
23✔
391
}
392

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

399
#[derive(Debug, Clone, Hash, Eq, PartialEq, Deserialize, Serialize, ToSchema)]
32✔
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 {
15✔
408
        Self {
15✔
409
            provider_id: id.provider_id.into(),
15✔
410
            layer_id: id.layer_id.into(),
15✔
411
        }
15✔
412
    }
15✔
413
}
414

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

421
impl From<geoengine_datatypes::dataset::LayerId> for LayerId {
422
    fn from(id: geoengine_datatypes::dataset::LayerId) -> Self {
15✔
423
        Self(id.0)
15✔
424
    }
15✔
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> {
34✔
482
        Ok(match s {
34✔
483
            "EPSG" => SpatialReferenceAuthority::Epsg,
34✔
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
    }
34✔
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 {
39✔
570
        Self { authority, code }
39✔
571
    }
39✔
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> {
34✔
586
        let mut split = s.split(':');
34✔
587

34✔
588
        match (split.next(), split.next(), split.next()) {
34✔
589
            (Some(authority), Some(code), None) => Ok(Self::new(
34✔
590
                authority.parse()?,
34✔
591
                code.parse::<u32>().context(error::ParseU32)?,
34✔
592
            )),
593
            _ => Err(error::Error::InvalidSpatialReferenceString {
×
594
                spatial_reference_string: s.into(),
×
595
            }),
×
596
        }
597
    }
34✔
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 {
10✔
676
        Self::SpatialReference(spatial_reference)
10✔
677
    }
10✔
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>
10✔
709
    where
10✔
710
        E: serde::de::Error,
10✔
711
    {
10✔
712
        if v.is_empty() {
10✔
713
            return Ok(SpatialReferenceOption::Unreferenced);
×
714
        }
10✔
715

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

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

722
impl<'de> Deserialize<'de> for SpatialReferenceOption {
723
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
10✔
724
    where
10✔
725
        D: Deserializer<'de>,
10✔
726
    {
10✔
727
        deserializer.deserialize_str(SpatialReferenceOptionDeserializeVisitor)
10✔
728
    }
10✔
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,
24✔
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(
×
1473
                crate::error::Error::UnexpectedInvalidDbTypeConversion,
×
1474
            ));
×
1475
        };
1476

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

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

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

1491
    postgres_types::to_sql_checked!();
1492
}
1493

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1589
    postgres_types::to_sql_checked!();
1590
}
1591

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

1599
        Ok(NotNanF64(value.try_into()?))
111✔
1600
    }
111✔
1601

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1971
        Ok(postgres_types::IsNull::No)
5✔
1972
    }
5✔
1973

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

1979
        matches!(inner_type, &postgres_types::Type::POINT)
1✔
1980
    }
1✔
1981

1982
    postgres_types::to_sql_checked!();
1983
}
1984

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

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

2009
        Ok(Self { coordinates })
5✔
2010
    }
5✔
2011

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

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

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

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

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

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

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

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

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

2082
        Ok(postgres_types::IsNull::No)
2✔
2083
    }
2✔
2084

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

2090
        matches!(inner_type, &postgres_types::Type::PATH)
1✔
2091
    }
1✔
2092

2093
    postgres_types::to_sql_checked!();
2094
}
2095

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

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

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

2127
        Ok(Self { coordinates })
2✔
2128
    }
2✔
2129

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

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

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

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

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

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

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

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

2203
        Ok(postgres_types::IsNull::No)
2✔
2204
    }
2✔
2205

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

2211
        <PolygonRef as ToSql>::accepts(inner_type)
1✔
2212
    }
1✔
2213

2214
    postgres_types::to_sql_checked!();
2215
}
2216

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

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

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

2242
        Ok(Self { polygons })
2✔
2243
    }
2✔
2244

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

2250
        inner_type.name() == "Polygon"
158✔
2251
    }
158✔
2252
}
2253

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

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

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

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

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

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

2297
    postgres_types::to_sql_checked!();
2298
}
2299

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

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

2313
        Ok(Self((a, b)))
10✔
2314
    }
10✔
2315

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

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

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

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

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