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

geo-engine / geoengine / 6022770913

30 Aug 2023 08:59AM UTC coverage: 89.934% (+0.09%) from 89.84%
6022770913

push

github

web-flow
Merge pull request #864 from geo-engine/compressed-vector-cache

Compressed-vector-cache

569 of 569 new or added lines in 6 files covered. (100.0%)

106288 of 118185 relevant lines covered (89.93%)

61263.93 hits per line

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

57.92
/operators/src/pro/cache/cache_chunks.rs
1
use super::cache_stream::CacheStream;
2
use super::error::CacheError;
3
use super::shared_cache::{
4
    CacheBackendElement, CacheBackendElementExt, CacheElement, CacheElementsContainer,
5
    CacheElementsContainerInfos, LandingZoneElementsContainer, VectorCacheQueryEntry,
6
    VectorLandingQueryEntry,
7
};
8
use crate::util::Result;
9
use arrow::array::StructArray;
10
use arrow::ipc::reader::FileReader;
11
use arrow::ipc::writer::{FileWriter, IpcWriteOptions};
12
use arrow::record_batch::RecordBatch;
13
use geoengine_datatypes::collections::FeatureCollectionInternals;
14
use geoengine_datatypes::primitives::VectorQueryRectangle;
15
use geoengine_datatypes::{
16
    collections::{
17
        FeatureCollection, FeatureCollectionInfos, FeatureCollectionModifications,
18
        GeometryCollection, IntoGeometryIterator,
19
    },
20
    primitives::{Geometry, MultiLineString, MultiPoint, MultiPolygon, NoGeometry},
21
    util::{arrow::ArrowTyped, ByteSize},
22
};
23
use std::collections::HashMap;
24
use std::io::Cursor;
25
use std::sync::Arc;
26

27
#[derive(Debug)]
×
28
pub enum CachedFeatures {
29
    NoGeometry(Arc<Vec<CompressedDataCollection>>),
30
    MultiPoint(Arc<Vec<CompressedMultiPointCollection>>),
31
    MultiLineString(Arc<Vec<CompressedMultiLineStringCollection>>),
32
    MultiPolygon(Arc<Vec<CompressedMultiPolygonCollection>>),
33
}
34

35
impl CachedFeatures {
36
    pub fn len(&self) -> usize {
×
37
        match self {
×
38
            CachedFeatures::NoGeometry(v) => v.len(),
×
39
            CachedFeatures::MultiPoint(v) => v.len(),
×
40
            CachedFeatures::MultiLineString(v) => v.len(),
×
41
            CachedFeatures::MultiPolygon(v) => v.len(),
×
42
        }
43
    }
×
44

45
    pub fn is_empty(&self) -> bool {
×
46
        self.len() == 0
×
47
    }
×
48

49
    pub fn is_expired(&self) -> bool {
1✔
50
        match self {
1✔
51
            CachedFeatures::NoGeometry(v) => v.iter().any(|c| c.cache_hint.is_expired()),
×
52
            CachedFeatures::MultiPoint(v) => v.iter().any(|c| c.cache_hint.is_expired()),
1✔
53
            CachedFeatures::MultiLineString(v) => v.iter().any(|c| c.cache_hint.is_expired()),
×
54
            CachedFeatures::MultiPolygon(v) => v.iter().any(|c| c.cache_hint.is_expired()),
×
55
        }
56
    }
1✔
57
}
58

59
impl ByteSize for CachedFeatures {
60
    fn heap_byte_size(&self) -> usize {
×
61
        // we need to use `byte_size` instead of `heap_byte_size` here, because `Vec` stores its data on the heap
×
62
        match self {
×
63
            CachedFeatures::NoGeometry(v) => v.iter().map(ByteSize::byte_size).sum(),
×
64
            CachedFeatures::MultiPoint(v) => v.iter().map(ByteSize::byte_size).sum(),
×
65
            CachedFeatures::MultiLineString(v) => v.iter().map(ByteSize::byte_size).sum(),
×
66
            CachedFeatures::MultiPolygon(v) => v.iter().map(ByteSize::byte_size).sum(),
×
67
        }
68
    }
×
69
}
70

71
#[derive(Debug)]
×
72
pub enum LandingZoneQueryFeatures {
73
    NoGeometry(Vec<CompressedDataCollection>),
74
    MultiPoint(Vec<CompressedMultiPointCollection>),
75
    MultiLineString(Vec<CompressedMultiLineStringCollection>),
76
    MultiPolygon(Vec<CompressedMultiPolygonCollection>),
77
}
78

79
impl LandingZoneQueryFeatures {
80
    pub fn len(&self) -> usize {
1✔
81
        match self {
1✔
82
            LandingZoneQueryFeatures::NoGeometry(v) => v.len(),
×
83
            LandingZoneQueryFeatures::MultiPoint(v) => v.len(),
1✔
84
            LandingZoneQueryFeatures::MultiLineString(v) => v.len(),
×
85
            LandingZoneQueryFeatures::MultiPolygon(v) => v.len(),
×
86
        }
87
    }
1✔
88

89
    pub fn is_empty(&self) -> bool {
1✔
90
        self.len() == 0
1✔
91
    }
1✔
92
}
93

94
impl ByteSize for LandingZoneQueryFeatures {
95
    fn heap_byte_size(&self) -> usize {
×
96
        // we need to use `byte_size` instead of `heap_byte_size` here, because `Vec` stores its data on the heap
×
97
        match self {
×
98
            LandingZoneQueryFeatures::NoGeometry(v) => v.iter().map(ByteSize::byte_size).sum(),
×
99
            LandingZoneQueryFeatures::MultiPoint(v) => v.iter().map(ByteSize::byte_size).sum(),
×
100
            LandingZoneQueryFeatures::MultiLineString(v) => v.iter().map(ByteSize::byte_size).sum(),
×
101
            LandingZoneQueryFeatures::MultiPolygon(v) => v.iter().map(ByteSize::byte_size).sum(),
×
102
        }
103
    }
×
104
}
105

106
impl From<LandingZoneQueryFeatures> for CachedFeatures {
107
    fn from(value: LandingZoneQueryFeatures) -> Self {
1✔
108
        match value {
1✔
109
            LandingZoneQueryFeatures::NoGeometry(v) => CachedFeatures::NoGeometry(Arc::new(v)),
×
110
            LandingZoneQueryFeatures::MultiPoint(v) => CachedFeatures::MultiPoint(Arc::new(v)),
1✔
111
            LandingZoneQueryFeatures::MultiLineString(v) => {
×
112
                CachedFeatures::MultiLineString(Arc::new(v))
×
113
            }
114
            LandingZoneQueryFeatures::MultiPolygon(v) => CachedFeatures::MultiPolygon(Arc::new(v)),
×
115
        }
116
    }
1✔
117
}
118

119
impl CacheElementsContainerInfos<VectorQueryRectangle> for CachedFeatures {
120
    fn is_expired(&self) -> bool {
×
121
        self.is_expired()
×
122
    }
×
123
}
124

125
impl<G> CacheElementsContainer<VectorQueryRectangle, CompressedFeatureCollection<G>>
126
    for CachedFeatures
127
where
128
    G: Geometry + ArrowTyped,
129
    CompressedFeatureCollection<G>: CacheBackendElementExt<CacheContainer = Self>,
130
{
131
    fn results_arc(&self) -> Option<Arc<Vec<CompressedFeatureCollection<G>>>> {
×
132
        CompressedFeatureCollection::<G>::results_arc(self)
×
133
    }
×
134
}
135

136
impl<G> LandingZoneElementsContainer<CompressedFeatureCollection<G>> for LandingZoneQueryFeatures
137
where
138
    G: Geometry + ArrowTyped,
139
    CompressedFeatureCollection<G>: CacheBackendElementExt<LandingZoneContainer = Self>,
140
{
141
    fn insert_element(
×
142
        &mut self,
×
143
        element: CompressedFeatureCollection<G>,
×
144
    ) -> Result<(), super::error::CacheError> {
×
145
        CompressedFeatureCollection::<G>::move_element_into_landing_zone(element, self)
×
146
    }
×
147

148
    fn create_empty() -> Self {
1✔
149
        CompressedFeatureCollection::<G>::create_empty_landing_zone()
1✔
150
    }
1✔
151
}
152

153
impl<G> CacheBackendElement for CompressedFeatureCollection<G>
154
where
155
    G: Geometry + ArrowTyped + ArrowTyped + Sized,
156
{
157
    type Query = VectorQueryRectangle;
158

159
    fn cache_hint(&self) -> geoengine_datatypes::primitives::CacheHint {
×
160
        self.cache_hint
×
161
    }
×
162

163
    fn typed_canonical_operator_name(
×
164
        key: crate::engine::CanonicOperatorName,
×
165
    ) -> super::shared_cache::TypedCanonicOperatorName {
×
166
        super::shared_cache::TypedCanonicOperatorName::Vector(key)
×
167
    }
×
168

169
    fn update_stored_query(&self, _query: &mut Self::Query) -> Result<(), CacheError> {
×
170
        // In this case, the elements of the cache are vector data chunks.
×
171
        // Unlike raster data, chunks have no guaranteed extent (spatial or temporal) other than the limits of the query itself.
×
172
        // If a vector element has a larger extent than the query, then the bbox computed for the collection is larger than the query bbox.
×
173
        // However, there may be a point that is outside the query bbox but inside the collection bbox. As it is not in the query bbox, it must not be returned as the result of the query.
×
174
        // So the query is not updated.
×
175
        Ok(())
×
176
    }
×
177

178
    fn intersects_query(&self, query: &Self::Query) -> bool {
36✔
179
        // If the chunk has no time bounds it must be empty so we can skip the temporal check and return true.
36✔
180
        let temporal_hit = self
36✔
181
            .time_interval
36✔
182
            .map_or(true, |tb| tb.intersects(&query.time_interval));
36✔
183

36✔
184
        // If the chunk has no spatial bounds it is either an empty collection or a no geometry collection.
36✔
185
        let spatial_hit = self
36✔
186
            .spatial_bounds
36✔
187
            .map_or(true, |sb| sb.intersects_bbox(&query.spatial_bounds));
36✔
188

36✔
189
        temporal_hit && spatial_hit
36✔
190
    }
36✔
191
}
192

193
macro_rules! impl_cache_element_subtype {
194
    ($g:ty, $variant:ident) => {
195
        impl CacheBackendElementExt for CompressedFeatureCollection<$g> {
196
            type LandingZoneContainer = LandingZoneQueryFeatures;
197
            type CacheContainer = CachedFeatures;
198

199
            fn move_element_into_landing_zone(
18✔
200
                self,
18✔
201
                landing_zone: &mut LandingZoneQueryFeatures,
18✔
202
            ) -> Result<(), super::error::CacheError> {
18✔
203
                match landing_zone {
18✔
204
                    LandingZoneQueryFeatures::$variant(v) => {
18✔
205
                        v.push(self);
18✔
206
                        Ok(())
18✔
207
                    }
208
                    _ => Err(super::error::CacheError::InvalidTypeForInsertion),
×
209
                }
210
            }
18✔
211

212
            fn create_empty_landing_zone() -> LandingZoneQueryFeatures {
3✔
213
                LandingZoneQueryFeatures::$variant(Vec::new())
3✔
214
            }
3✔
215

216
            fn results_arc(cache_elements_container: &CachedFeatures) -> Option<Arc<Vec<Self>>> {
217
                if let CachedFeatures::$variant(v) = cache_elements_container {
×
218
                    Some(v.clone())
×
219
                } else {
220
                    None
×
221
                }
222
            }
×
223

224
            fn landing_zone_to_cache_entry(
1✔
225
                landing_zone_entry: VectorLandingQueryEntry,
1✔
226
            ) -> VectorCacheQueryEntry {
1✔
227
                landing_zone_entry.into()
1✔
228
            }
1✔
229
        }
230
    };
231
}
232
impl_cache_element_subtype!(NoGeometry, NoGeometry);
233
impl_cache_element_subtype!(MultiPoint, MultiPoint);
234
impl_cache_element_subtype!(MultiLineString, MultiLineString);
235
impl_cache_element_subtype!(MultiPolygon, MultiPolygon);
236

237
/// Our own tile stream that "owns" the data (more precisely a reference to the data)
238

239
pub trait CacheElementSpatialBounds {
240
    fn filter_cache_element_entries(
241
        &self,
242
        query_rect: &VectorQueryRectangle,
243
    ) -> Result<Self, CacheError>
244
    where
245
        Self: Sized;
246

247
    fn cache_element_spatial_bounds(
248
        &self,
249
    ) -> Option<geoengine_datatypes::primitives::BoundingBox2D>
250
    where
251
        Self: Sized;
252
}
253

254
impl CacheElementSpatialBounds for FeatureCollection<NoGeometry> {
255
    fn filter_cache_element_entries(
×
256
        &self,
×
257
        query_rect: &VectorQueryRectangle,
×
258
    ) -> Result<Self, CacheError> {
×
259
        let time_filter_bools = self
×
260
            .time_intervals()
×
261
            .iter()
×
262
            .map(|t| t.intersects(&query_rect.time_interval))
×
263
            .collect::<Vec<bool>>();
×
264
        self.filter(time_filter_bools)
×
265
            .map_err(|_err| CacheError::CouldNotFilterResults)
×
266
    }
×
267

268
    fn cache_element_spatial_bounds(
×
269
        &self,
×
270
    ) -> Option<geoengine_datatypes::primitives::BoundingBox2D> {
×
271
        None
×
272
    }
×
273
}
274

275
macro_rules! impl_cache_result_check {
276
    ($t:ty) => {
277
        impl<'a> CacheElementSpatialBounds for FeatureCollection<$t>
278
        where
279
            FeatureCollection<$t>: GeometryCollection,
280
        {
281
            fn filter_cache_element_entries(
×
282
                &self,
×
283
                query_rect: &VectorQueryRectangle,
×
284
            ) -> Result<Self, CacheError> {
×
285
                let geoms_filter_bools = self.geometries().map(|g| {
×
286
                    g.bbox()
287
                        .map(|bbox| bbox.intersects_bbox(&query_rect.spatial_bounds))
288
                        .unwrap_or(false)
289
                });
×
290

×
291
                let time_filter_bools = self
×
292
                    .time_intervals()
×
293
                    .iter()
×
294
                    .map(|t| t.intersects(&query_rect.time_interval));
×
295

×
296
                let filter_bools = geoms_filter_bools
×
297
                    .zip(time_filter_bools)
×
298
                    .map(|(g, t)| g && t)
×
299
                    .collect::<Vec<bool>>();
×
300

×
301
                self.filter(filter_bools)
×
302
                    .map_err(|_err| CacheError::CouldNotFilterResults)
×
303
            }
×
304

305
            fn cache_element_spatial_bounds(
45✔
306
                &self,
45✔
307
            ) -> Option<geoengine_datatypes::primitives::BoundingBox2D> {
45✔
308
                self.bbox()
45✔
309
            }
45✔
310
        }
311
    };
312
}
313

314
impl_cache_result_check!(MultiPoint);
×
315
impl_cache_result_check!(MultiLineString);
×
316
impl_cache_result_check!(MultiPolygon);
×
317

318
impl<G> CacheElement for FeatureCollection<G>
319
where
320
    G: Geometry + ArrowTyped + 'static,
321
    CompressedFeatureCollection<G>: CacheBackendElementExt<
322
        Query = VectorQueryRectangle,
323
        LandingZoneContainer = LandingZoneQueryFeatures,
324
        CacheContainer = CachedFeatures,
325
    >,
326
    FeatureCollection<G>: ByteSize + CacheElementSpatialBounds,
327
{
328
    type StoredCacheElement = CompressedFeatureCollection<G>;
329
    type Query = VectorQueryRectangle;
330
    type ResultStream =
331
        CacheStream<CompressedFeatureCollection<G>, FeatureCollection<G>, VectorQueryRectangle>;
332

333
    fn into_stored_element(self) -> Self::StoredCacheElement {
×
334
        CompressedFeatureCollection::<G>::from_collection(self)
×
335
            .expect("Compressing the feature collection should not fail")
×
336
    }
×
337

338
    fn from_stored_element_ref(stored: &Self::StoredCacheElement) -> Result<Self, CacheError> {
×
339
        stored.to_collection()
×
340
    }
×
341

342
    fn result_stream(
×
343
        stored_data: Arc<Vec<Self::StoredCacheElement>>,
×
344
        query: Self::Query,
×
345
    ) -> Self::ResultStream {
×
346
        CacheStream::new(stored_data, query)
×
347
    }
×
348
}
349

350
pub type CompressedFeatureCollection<G> = CompressedFeatureCollectionImpl<G, Lz4FlexCompression>;
351
pub type CompressedDataCollection = CompressedFeatureCollection<NoGeometry>;
352
pub type CompressedMultiPointCollection = CompressedFeatureCollection<MultiPoint>;
353
pub type CompressedMultiLineStringCollection = CompressedFeatureCollection<MultiLineString>;
354
pub type CompressedMultiPolygonCollection = CompressedFeatureCollection<MultiPolygon>;
355

356
#[derive(Debug)]
×
357
pub struct CompressedFeatureCollectionImpl<G, C> {
358
    data: Vec<u8>,
359
    spatial_bounds: Option<geoengine_datatypes::primitives::BoundingBox2D>,
360
    time_interval: Option<geoengine_datatypes::primitives::TimeInterval>,
361
    types: HashMap<String, geoengine_datatypes::primitives::FeatureDataType>,
362
    cache_hint: geoengine_datatypes::primitives::CacheHint,
363
    collection_type: std::marker::PhantomData<G>,
364
    compression_type: std::marker::PhantomData<C>,
365
}
366

367
impl<G, C> ByteSize for CompressedFeatureCollectionImpl<G, C> {
368
    fn heap_byte_size(&self) -> usize {
×
369
        self.data.heap_byte_size()
×
370
    }
×
371
}
372

373
impl<G, C> CompressedFeatureCollectionImpl<G, C>
374
where
375
    FeatureCollection<G>: FeatureCollectionInfos + CacheElementSpatialBounds,
376
    C: ChunkCompression,
377
{
378
    pub fn from_collection(collection: FeatureCollection<G>) -> Result<Self, CacheError> {
45✔
379
        let spatial_bounds = collection.cache_element_spatial_bounds();
45✔
380
        let time_interval = collection.time_bounds();
45✔
381

45✔
382
        let FeatureCollectionInternals {
45✔
383
            table,
45✔
384
            types,
45✔
385
            collection_type,
45✔
386
            cache_hint,
45✔
387
        } = collection.into();
45✔
388

389
        let data = C::compress_array(table)?;
45✔
390

391
        let compressed: CompressedFeatureCollectionImpl<G, C> = CompressedFeatureCollectionImpl {
45✔
392
            data,
45✔
393
            spatial_bounds,
45✔
394
            time_interval,
45✔
395
            types,
45✔
396
            cache_hint,
45✔
397
            collection_type,
45✔
398
            compression_type: Default::default(),
45✔
399
        };
45✔
400

45✔
401
        Ok(compressed)
45✔
402
    }
45✔
403

404
    pub fn to_collection(&self) -> Result<FeatureCollection<G>, CacheError> {
×
405
        let table = C::decompress_array(&self.data)?;
×
406

407
        let collection_internals = FeatureCollectionInternals {
×
408
            table,
×
409
            types: self.types.clone(),
×
410
            collection_type: self.collection_type,
×
411
            cache_hint: self.cache_hint,
×
412
        };
×
413

×
414
        let collection = FeatureCollection::from(collection_internals);
×
415

×
416
        Ok(collection)
×
417
    }
×
418
}
419

420
pub trait ChunkCompression {
421
    fn compress_array(collection: StructArray) -> Result<Vec<u8>, CacheError>
422
    where
423
        Self: Sized;
424

425
    fn decompress_array(compressed: &[u8]) -> Result<StructArray, CacheError>
426
    where
427
        Self: Sized;
428

429
    fn array_to_bytes(table: StructArray) -> Result<Vec<u8>, CacheError> {
45✔
430
        let record_batch = RecordBatch::from(&table);
45✔
431

432
        let mut file_writer = FileWriter::try_new_with_options(
45✔
433
            Vec::new(),
45✔
434
            record_batch.schema().as_ref(),
45✔
435
            IpcWriteOptions::default(), // TODO: Add ".try_with_compression(COMPRESSION)?," once arrow provides a way to ensure that the decompressed data has the same size as the original data.
45✔
436
        )
45✔
437
        .map_err(|source| CacheError::CouldNotWriteElementToBytes { source })?;
45✔
438
        file_writer
45✔
439
            .write(&record_batch)
45✔
440
            .map_err(|source| CacheError::CouldNotWriteElementToBytes { source })?;
45✔
441
        file_writer
45✔
442
            .finish()
45✔
443
            .map_err(|source| CacheError::CouldNotWriteElementToBytes { source })?;
45✔
444

445
        file_writer
45✔
446
            .into_inner()
45✔
447
            .map_err(|source| CacheError::CouldNotWriteElementToBytes { source })
45✔
448
    }
45✔
449

450
    fn bytes_to_array(bytes: &[u8]) -> Result<StructArray, CacheError> {
×
451
        let mut reader = FileReader::try_new(Cursor::new(bytes), None)
×
452
            .map_err(|source| CacheError::CouldNotReadElementFromBytes { source })?;
×
453
        let record_batch = reader
×
454
            .next()
×
455
            .expect("We only call next once so there must be a record batch")
×
456
            .map_err(|source| CacheError::CouldNotReadElementFromBytes { source })?;
×
457

458
        Ok(StructArray::from(record_batch))
×
459
    }
×
460
}
461

462
#[derive(Debug, Copy, Clone)]
×
463
pub struct Lz4FlexCompression;
464

465
impl ChunkCompression for Lz4FlexCompression {
466
    fn compress_array(collection: StructArray) -> Result<Vec<u8>, CacheError>
45✔
467
    where
45✔
468
        Self: Sized,
45✔
469
    {
45✔
470
        let bytes = Self::array_to_bytes(collection)?;
45✔
471
        let compressed = lz4_flex::compress_prepend_size(&bytes);
45✔
472
        Ok(compressed)
45✔
473
    }
45✔
474

475
    fn decompress_array(compressed: &[u8]) -> Result<StructArray, CacheError>
×
476
    where
×
477
        Self: Sized,
×
478
    {
×
479
        let bytes = lz4_flex::decompress_size_prepended(compressed)
×
480
            .map_err(|source| CacheError::CouldNotDecompressElement { source })?;
×
481
        let array = Self::bytes_to_array(&bytes)?;
×
482
        Ok(array)
×
483
    }
×
484
}
485

486
#[cfg(test)]
487
mod tests {
488
    use crate::pro::cache::{
489
        cache_chunks::{CachedFeatures, LandingZoneQueryFeatures},
490
        shared_cache::{
491
            CacheBackendElement, CacheBackendElementExt, CacheQueryMatch, VectorCacheQueryEntry,
492
            VectorLandingQueryEntry,
493
        },
494
    };
495

496
    use super::CompressedFeatureCollection;
497
    use geoengine_datatypes::{
498
        collections::MultiPointCollection,
499
        primitives::{
500
            BoundingBox2D, CacheHint, FeatureData, MultiPoint, SpatialResolution, TimeInterval,
501
            VectorQueryRectangle,
502
        },
503
    };
504
    use std::{collections::HashMap, sync::Arc};
505

506
    fn create_test_collection() -> Vec<CompressedFeatureCollection<MultiPoint>> {
4✔
507
        let mut data = Vec::new();
4✔
508

509
        for x in 0..9 {
40✔
510
            let mut points = Vec::new();
36✔
511
            let mut strngs = Vec::new();
36✔
512
            for i in x..x + 2 {
72✔
513
                let p = MultiPoint::new(vec![(f64::from(i), f64::from(i)).into()]).unwrap();
72✔
514
                points.push(p);
72✔
515
                strngs.push(format!("test {i}"));
72✔
516
            }
72✔
517

518
            let collection = MultiPointCollection::from_data(
36✔
519
                points,
36✔
520
                vec![TimeInterval::default(); 2],
36✔
521
                HashMap::<String, FeatureData>::from([(
36✔
522
                    "strings".to_owned(),
36✔
523
                    FeatureData::Text(strngs),
36✔
524
                )]),
36✔
525
                CacheHint::default(),
36✔
526
            )
36✔
527
            .unwrap();
36✔
528

36✔
529
            let compressed_collection =
36✔
530
                CompressedFeatureCollection::from_collection(collection).unwrap();
36✔
531
            data.push(compressed_collection);
36✔
532
        }
533
        data
4✔
534
    }
4✔
535

536
    #[test]
1✔
537
    fn create_empty_landing_zone() {
1✔
538
        let landing_zone = CompressedFeatureCollection::<MultiPoint>::create_empty_landing_zone();
1✔
539
        assert!(landing_zone.is_empty());
1✔
540
        if let LandingZoneQueryFeatures::MultiPoint(v) = landing_zone {
1✔
541
            assert!(v.is_empty());
1✔
542
        } else {
543
            panic!("wrong type");
×
544
        }
545
    }
1✔
546

547
    #[test]
1✔
548
    fn move_element_to_landing_zone() {
1✔
549
        let mut landing_zone =
1✔
550
            CompressedFeatureCollection::<MultiPoint>::create_empty_landing_zone();
1✔
551
        let col = create_test_collection();
1✔
552
        for c in col {
10✔
553
            c.move_element_into_landing_zone(&mut landing_zone).unwrap();
9✔
554
        }
9✔
555
        if let LandingZoneQueryFeatures::MultiPoint(v) = landing_zone {
1✔
556
            assert_eq!(v.len(), 9);
1✔
557
        } else {
558
            panic!("wrong type");
×
559
        }
560
    }
1✔
561

562
    #[test]
1✔
563
    fn landing_zone_to_cache_entry() {
1✔
564
        let cols = create_test_collection();
1✔
565
        let query = VectorQueryRectangle {
1✔
566
            spatial_bounds: BoundingBox2D::new_unchecked((0., 0.).into(), (1., 1.).into()),
1✔
567
            time_interval: Default::default(),
1✔
568
            spatial_resolution: SpatialResolution::zero_point_one(),
1✔
569
        };
1✔
570
        let mut lq =
1✔
571
            VectorLandingQueryEntry::create_empty::<CompressedFeatureCollection<MultiPoint>>(query);
1✔
572
        for c in cols {
10✔
573
            c.move_element_into_landing_zone(lq.elements_mut()).unwrap();
9✔
574
        }
9✔
575
        let mut cache_entry =
1✔
576
            CompressedFeatureCollection::<MultiPoint>::landing_zone_to_cache_entry(lq);
1✔
577
        assert_eq!(cache_entry.query(), &query);
1✔
578
        assert!(cache_entry.elements_mut().is_expired());
1✔
579
    }
1✔
580

581
    #[test]
1✔
582
    fn cache_element_hit() {
1✔
583
        let cols = create_test_collection();
1✔
584

1✔
585
        // elemtes are all fully contained
1✔
586
        let query = VectorQueryRectangle {
1✔
587
            spatial_bounds: BoundingBox2D::new_unchecked((0., 0.).into(), (12., 12.).into()),
1✔
588
            time_interval: Default::default(),
1✔
589
            spatial_resolution: SpatialResolution::one(),
1✔
590
        };
1✔
591

592
        for c in &cols {
10✔
593
            assert!(c.intersects_query(&query));
9✔
594
        }
595

596
        // first element is not contained
597
        let query = VectorQueryRectangle {
1✔
598
            spatial_bounds: BoundingBox2D::new_unchecked((2., 2.).into(), (10., 10.).into()),
1✔
599
            time_interval: Default::default(),
1✔
600
            spatial_resolution: SpatialResolution::one(),
1✔
601
        };
1✔
602
        assert!(!cols[0].intersects_query(&query));
1✔
603
        for c in &cols[1..] {
8✔
604
            assert!(c.intersects_query(&query));
8✔
605
        }
606

607
        // all elements are not contained
608
        let query = VectorQueryRectangle {
1✔
609
            spatial_bounds: BoundingBox2D::new_unchecked((13., 13.).into(), (26., 26.).into()),
1✔
610
            time_interval: Default::default(),
1✔
611
            spatial_resolution: SpatialResolution::one(),
1✔
612
        };
1✔
613
        for col in &cols {
10✔
614
            assert!(!col.intersects_query(&query));
9✔
615
        }
616
    }
1✔
617

618
    #[test]
1✔
619
    fn cache_entry_matches() {
1✔
620
        let cols = create_test_collection();
1✔
621

1✔
622
        let cache_entry_bounds = VectorQueryRectangle {
1✔
623
            spatial_bounds: BoundingBox2D::new_unchecked((1., 1.).into(), (11., 11.).into()),
1✔
624
            time_interval: Default::default(),
1✔
625
            spatial_resolution: SpatialResolution::one(),
1✔
626
        };
1✔
627

1✔
628
        let cache_query_entry = VectorCacheQueryEntry {
1✔
629
            query: cache_entry_bounds,
1✔
630
            elements: CachedFeatures::MultiPoint(Arc::new(cols)),
1✔
631
        };
1✔
632

1✔
633
        // query is equal
1✔
634
        let query = cache_entry_bounds;
1✔
635
        assert!(cache_query_entry.query().is_match(&query));
1✔
636

637
        // query is fully contained
638
        let query2 = VectorQueryRectangle {
1✔
639
            spatial_bounds: BoundingBox2D::new_unchecked((2., 2.).into(), (10., 10.).into()),
1✔
640
            time_interval: Default::default(),
1✔
641
            spatial_resolution: SpatialResolution::one(),
1✔
642
        };
1✔
643
        assert!(cache_query_entry.query().is_match(&query2));
1✔
644

645
        // query is exceeds cached bounds
646
        let query3 = VectorQueryRectangle {
1✔
647
            spatial_bounds: BoundingBox2D::new_unchecked((0., 0.).into(), (8., 8.).into()),
1✔
648
            time_interval: Default::default(),
1✔
649
            spatial_resolution: SpatialResolution::one(),
1✔
650
        };
1✔
651
        assert!(!cache_query_entry.query().is_match(&query3));
1✔
652
    }
1✔
653
}
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