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

Open-S2 / gis-tools / #62

03 May 2025 04:49AM UTC coverage: 93.846% (+0.8%) from 93.071%
#62

push

Mr Martian
migrate to stable

88819 of 94643 relevant lines covered (93.85%)

9015.08 hits per line

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

89.45
/rust/data_structures/src/tile.rs
1
use alloc::{
2
    collections::{BTreeMap, BTreeSet},
3
    string::{String, ToString},
4
    vec,
5
    vec::Vec,
6
};
7
use geometry::{
8
    CellId, ConvertFeature, ConvertVectorFeatureS2, ConvertVectorFeatureWM, S2CellId,
9
    SimplifyVectorGeometry, build_sq_dists, clip_features, convert,
10
};
11
use parsers::FeatureReader;
12
use s2json::{
13
    Axis, Face, Feature, JSONCollection, MValue, Projection, Properties, VectorFeature,
14
    VectorGeometry, VectorPoint,
15
};
16
use serde::{Deserialize, Serialize};
17

18
/// If a user creates metadata for a VectorFeature, it needs to define a get_layer function
19
pub trait HasLayer {
20
    /// Get the layer from metadata if it exists
21
    fn get_layer(&self) -> Option<String>;
22
}
23
impl HasLayer for () {
24
    fn get_layer(&self) -> Option<String> {
×
25
        None
×
26
    }
×
27
}
28
impl HasLayer for MValue {
29
    fn get_layer(&self) -> Option<String> {
×
30
        let layer = self.get("layer");
×
31
        match layer {
×
32
            Some(l) => l.to_prim()?.to_string(),
×
33
            _ => None,
×
34
        }
35
    }
×
36
}
37

38
/// Tile Class to contain the tile information for splitting or simplifying
39
#[derive(Debug, Clone, PartialEq)]
40
pub struct Tile<M = (), P: Clone + Default = Properties, D: Clone + Default = MValue> {
41
    /// the tile id
42
    pub id: CellId,
43
    /// the tile's layers
44
    pub layers: BTreeMap<String, Layer<M, P, D>>,
45
    /// whether the tile feature geometry has been transformed
46
    pub transformed: bool,
47
}
48
impl<M: HasLayer + Clone, P: Clone + Default, D: Clone + Default> Tile<M, P, D> {
49
    /// Create a new Tile
50
    pub fn new(id: CellId) -> Self {
450✔
51
        Self { id, layers: BTreeMap::new(), transformed: false }
450✔
52
    }
450✔
53

54
    /// Returns the number of layers
55
    pub fn len(&self) -> usize {
9✔
56
        self.layers.len()
9✔
57
    }
9✔
58

59
    /// Returns true if the tile is empty of features
60
    pub fn is_empty(&self) -> bool {
837✔
61
        for layer in self.layers.values() {
837✔
62
            if !layer.features.is_empty() {
307✔
63
                return false;
307✔
64
            }
×
65
        }
66

67
        true
530✔
68
    }
837✔
69

70
    /// Add any reader to the tile
71
    pub fn add_reader<R>(&mut self, reader: R, layer: Option<String>)
×
72
    where
×
73
        R: FeatureReader<M, P, D>,
×
74
    {
×
75
        for feature in reader.iter() {
×
76
            self.add_feature(feature, layer.clone());
×
77
        }
×
78
    }
×
79

80
    /// Add a feature to the tile
81
    pub fn add_feature(&mut self, feature: VectorFeature<M, P, D>, layer: Option<String>) {
207✔
82
        let layer_name = feature
207✔
83
            .metadata
207✔
84
            .as_ref()
207✔
85
            .and_then(|meta| meta.get_layer()) // Get the layer from metadata if it exists
207✔
86
            .or(layer) // Fall back to the provided layer
207✔
87
            .unwrap_or_else(|| "default".to_string()); // Fall back to "default" if none found
207✔
88

207✔
89
        let layer = self.layers.entry(layer_name.clone()).or_insert(Layer::new(layer_name));
207✔
90
        layer.features.push(feature);
207✔
91
    }
207✔
92

93
    /// Simplify the geometry to have a tolerance which will be relative to the tile's zoom level.
94
    /// NOTE: This should be called after the tile has been split into children if that functionality
95
    /// is needed.
96
    pub fn transform(&mut self, tolerance: f64, maxzoom: Option<u8>) {
111✔
97
        if self.transformed || self.id.is_face() {
111✔
98
            self.transformed = true;
19✔
99
            return;
19✔
100
        }
92✔
101

92✔
102
        let (_, zoom, i, j) = self.id.to_face_ij();
92✔
103
        for layer in self.layers.values_mut() {
92✔
104
            for feature in layer.features.iter_mut() {
106✔
105
                feature.geometry.simplify(tolerance, zoom, maxzoom);
106✔
106
                feature.geometry.transform(zoom.into(), i as f64, j as f64)
106✔
107
            }
108
        }
109

110
        self.transformed = true;
92✔
111
    }
111✔
112

113
    /// split tile into 4 children
114
    pub fn split(&mut self, buffer: Option<f64>) -> TileChildren<M, P, D> {
108✔
115
        let buffer = buffer.unwrap_or(0.0625);
108✔
116
        let (face, zoom, i, j) = self.id.to_face_ij();
108✔
117
        let [bl_id, br_id, tl_id, tr_id] = S2CellId::children_ij(face, zoom, i, j);
108✔
118
        let mut children = TileChildren {
108✔
119
            bottom_left: Tile::new(bl_id),
108✔
120
            bottom_right: Tile::new(br_id),
108✔
121
            top_left: Tile::new(tl_id),
108✔
122
            top_right: Tile::new(tr_id),
108✔
123
        };
108✔
124
        let scale = (1 << zoom) as f64;
108✔
125
        let k1 = 0.;
108✔
126
        let k2 = 0.5;
108✔
127
        let k3 = 0.5;
108✔
128
        let k4 = 1.;
108✔
129
        let i = i as f64;
108✔
130
        let j = j as f64;
108✔
131

132
        let mut tl: Option<Vec<VectorFeature<M, P, D>>>;
133
        let mut bl: Option<Vec<VectorFeature<M, P, D>>>;
134
        let mut tr: Option<Vec<VectorFeature<M, P, D>>>;
135
        let mut br: Option<Vec<VectorFeature<M, P, D>>>;
136

137
        for (name, layer) in self.layers.iter_mut() {
108✔
138
            let features = &layer.features;
108✔
139
            let left = clip_features(features, scale, i - k1, i + k3, Axis::X, buffer);
108✔
140
            let right = clip_features(features, scale, i + k2, i + k4, Axis::X, buffer);
108✔
141

142
            if let Some(left) = left {
108✔
143
                bl = clip_features(&left, scale, j - k1, j + k3, Axis::Y, buffer);
52✔
144
                tl = clip_features(&left, scale, j + k2, j + k4, Axis::Y, buffer);
52✔
145
                if let Some(bl) = bl {
52✔
146
                    for d in bl {
78✔
147
                        children.bottom_left.add_feature(d, Some(name.to_string()));
40✔
148
                    }
40✔
149
                }
14✔
150
                if let Some(tl) = tl {
52✔
151
                    for d in tl {
52✔
152
                        children.top_left.add_feature(d, Some(name.to_string()));
26✔
153
                    }
26✔
154
                }
26✔
155
            }
56✔
156

157
            if let Some(right) = right {
108✔
158
                br = clip_features(&right, scale, j - k1, j + k3, Axis::Y, buffer);
75✔
159
                tr = clip_features(&right, scale, j + k2, j + k4, Axis::Y, buffer);
75✔
160
                if let Some(br) = br {
75✔
161
                    for d in br {
117✔
162
                        children.bottom_right.add_feature(d, Some(name.to_string()));
64✔
163
                    }
64✔
164
                }
22✔
165
                if let Some(tr) = tr {
75✔
166
                    for d in tr {
93✔
167
                        children.top_right.add_feature(d, Some(name.to_string()));
47✔
168
                    }
47✔
169
                }
29✔
170
            }
33✔
171
        }
172

173
        children
108✔
174
    }
108✔
175
}
176

177
/// The children of a tile
178
#[derive(Debug)]
179
pub struct TileChildren<M = (), P: Clone + Default = Properties, D: Clone + Default = MValue> {
180
    /// The bottom left child tile
181
    pub bottom_left: Tile<M, P, D>,
182
    /// The bottom right child tile
183
    pub bottom_right: Tile<M, P, D>,
184
    /// The top left child tile
185
    pub top_left: Tile<M, P, D>,
186
    /// The top right child tile
187
    pub top_right: Tile<M, P, D>,
188
}
189

190
/// Layer Class to contain the layer information for splitting or simplifying
191
#[derive(Debug, Clone, PartialEq)]
192
pub struct Layer<M = (), P: Clone + Default = Properties, D: Clone + Default = MValue> {
193
    /// the layer name
194
    pub name: String,
195
    /// the layer's features
196
    pub features: Vec<VectorFeature<M, P, D>>,
197
}
198
impl<M, P: Clone + Default, D: Clone + Default> Layer<M, P, D> {
199
    /// Create a new Layer
200
    pub fn new(name: String) -> Self {
207✔
201
        Self { name, features: vec![] }
207✔
202
    }
207✔
203
}
204

205
/// Options for creating a TileStore
206
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
207
pub struct TileStoreOptions {
208
    /// manually set the projection, otherwise it defaults to whatever the data type is
209
    pub projection: Option<Projection>,
210
    /// min zoom to generate data on
211
    pub minzoom: Option<u8>,
212
    /// max zoom level to cluster the points on
213
    pub maxzoom: Option<u8>,
214
    /// tile buffer on each side in pixels
215
    pub index_maxzoom: Option<u8>,
216
    /// simplification tolerance (higher means simpler). Default is 3.
217
    /// Note: this tolerance is measured against a 4_096x4_096 unit grid when applying simplification.
218
    pub tolerance: Option<f64>,
219
    /// tile buffer on each side so lines and polygons don't get clipped
220
    pub buffer: Option<f64>,
221
}
222

223
/// TileStore Class is a tile-lookup system that splits and simplifies as needed for each tile request */
224
#[derive(Debug, Clone, PartialEq)]
225
pub struct TileStore<
226
    M: HasLayer + Clone = (),
227
    P: Clone + Default = Properties,
228
    D: Clone + Default = MValue,
229
> {
230
    /// min zoom to preserve detail on
231
    pub minzoom: u8,
232
    /// max zoom to preserve detail on
233
    pub maxzoom: u8,
234
    /// store which faces are active. 0 face could be entire WM projection
235
    pub faces: BTreeSet<Face>,
236
    /// max zoom in the tile index
237
    pub index_maxzoom: u8,
238
    /// simplification tolerance (higher means simpler)
239
    pub tolerance: f64,
240
    /// tile buffer for lines and polygons
241
    pub buffer: f64,
242
    /// stores both WM and S2 tiles
243
    pub tiles: BTreeMap<CellId, Tile<M, P, D>>,
244
    /// projection to build tiles for
245
    pub projection: Projection,
246
}
247
impl<M: HasLayer + Clone, P: Clone + Default, D: Clone + Default> Default for TileStore<M, P, D> {
248
    fn default() -> Self {
1✔
249
        Self {
1✔
250
            minzoom: 0,
1✔
251
            maxzoom: 14,
1✔
252
            faces: BTreeSet::<Face>::new(),
1✔
253
            index_maxzoom: 4,
1✔
254
            tolerance: 3. / 4_096.,
1✔
255
            buffer: 0.0625,
1✔
256
            tiles: BTreeMap::<CellId, Tile<M, P, D>>::new(),
1✔
257
            projection: Projection::S2,
1✔
258
        }
1✔
259
    }
1✔
260
}
261
impl<M: HasLayer + Clone, P: Clone + Default, D: Clone + Default> TileStore<M, P, D>
262
where
263
    VectorFeature<M, P, D>: ConvertVectorFeatureWM<M, P, D> + ConvertVectorFeatureS2<M, P, D>,
264
    Feature<M, P, D>: ConvertFeature<M, P, D>,
265
{
266
    /// Create a new TileStore
267
    pub fn new(data: JSONCollection<M, P, D>, options: TileStoreOptions) -> Self {
13✔
268
        let mut tile_store = Self {
13✔
269
            minzoom: options.minzoom.unwrap_or(0),
13✔
270
            maxzoom: options.maxzoom.unwrap_or(14),
13✔
271
            faces: BTreeSet::<Face>::new(),
13✔
272
            index_maxzoom: options.index_maxzoom.unwrap_or(4),
13✔
273
            tolerance: options.tolerance.unwrap_or(3.) / 4_096.,
13✔
274
            buffer: options.buffer.unwrap_or(64.),
13✔
275
            tiles: BTreeMap::<CellId, Tile<M, P, D>>::new(),
13✔
276
            projection: options.projection.unwrap_or(Projection::S2),
13✔
277
        };
13✔
278
        // sanity check
13✔
279
        debug_assert!(
13✔
280
            tile_store.minzoom <= tile_store.maxzoom
13✔
281
                && tile_store.maxzoom > 0
13✔
282
                && tile_store.maxzoom <= 20,
13✔
283
            "maxzoom should be in the 0-20 range"
×
284
        );
285
        // convert features
286
        let features: Vec<VectorFeature<M, P, D>> =
13✔
287
            convert(tile_store.projection, &data, Some(true), Some(true));
13✔
288
        features.into_iter().for_each(|feature| tile_store.add_feature(feature));
27✔
289
        for i in 0..6 {
91✔
290
            tile_store.split_tile(CellId::from_face(i), None, None);
78✔
291
        }
78✔
292

293
        tile_store
13✔
294
    }
13✔
295

296
    /// Add a feature to the tile store
297
    fn add_feature(&mut self, mut feature: VectorFeature<M, P, D>) {
27✔
298
        let face: u8 = feature.face.into();
27✔
299
        let tile = self.tiles.entry(CellId::from_face(face)).or_insert_with(|| {
27✔
300
            self.faces.insert(feature.face);
16✔
301
            Tile::new(CellId::from_face(face))
16✔
302
        });
27✔
303
        // Prep Douglas-Peucker simplification by setting t-values.
27✔
304
        build_sq_dists(&mut feature.geometry, self.tolerance, Some(self.maxzoom));
27✔
305

27✔
306
        tile.add_feature(feature, None);
27✔
307
    }
27✔
308

309
    /// Split tiles given a range
310
    fn split_tile(&mut self, start_id: CellId, end_id: Option<CellId>, end_zoom: Option<u8>) {
275✔
311
        let TileStore { buffer, tiles, tolerance, maxzoom, index_maxzoom, .. } = self;
275✔
312
        let end_zoom = end_zoom.unwrap_or(*maxzoom);
275✔
313
        let mut stack: Vec<CellId> = vec![start_id];
275✔
314
        // avoid recursion by using a processing queue
315
        while !stack.is_empty() {
982✔
316
            // find our next tile to split
317
            let stack_id = stack.pop();
707✔
318
            if stack_id.is_none() {
707✔
319
                break;
×
320
            }
707✔
321
            let tile = tiles.get_mut(&stack_id.unwrap());
707✔
322
            // if the tile we need does not exist, is empty, or already transformed, skip it
707✔
323
            if tile.is_none() {
707✔
324
                continue;
62✔
325
            }
645✔
326
            let tile = tile.unwrap();
645✔
327
            if tile.is_empty() || tile.transformed {
645✔
328
                continue;
451✔
329
            }
194✔
330
            let tile_zoom = tile.id.level();
194✔
331
            // 1: stop tiling if we reached a defined end
194✔
332
            // 2: stop tiling if it's the first-pass tiling, and we reached max zoom for indexing
194✔
333
            // 3: stop at currently needed maxzoom OR current tile does not include child
194✔
334
            if tile_zoom >= *maxzoom || // 1
194✔
335
                (end_id.is_none() && tile_zoom >= *index_maxzoom) || // 2
158✔
336
                (end_id.is_some() && (tile_zoom > end_zoom || !tile.id.contains(end_id.unwrap())))
108✔
337
            {
338
                continue;
86✔
339
            }
108✔
340

108✔
341
            // split the tile
108✔
342
            let TileChildren {
108✔
343
                bottom_left: bl_id,
108✔
344
                bottom_right: br_id,
108✔
345
                top_left: tl_id,
108✔
346
                top_right: tr_id,
108✔
347
            } = tile.split(Some(*buffer));
108✔
348
            // now that the tile has been split, we can transform it
108✔
349
            tile.transform(*tolerance, Some(*maxzoom));
108✔
350
            // push the new features to the stack
108✔
351
            stack.extend(vec![bl_id.id, br_id.id, tl_id.id, tr_id.id]);
108✔
352
            // store the children
108✔
353
            tiles.insert(bl_id.id, bl_id);
108✔
354
            tiles.insert(br_id.id, br_id);
108✔
355
            tiles.insert(tl_id.id, tl_id);
108✔
356
            tiles.insert(tr_id.id, tr_id);
108✔
357
        }
358
    }
275✔
359

360
    /// Get a tile
361
    pub fn get_tile(&mut self, id: CellId) -> Option<&Tile<M, P, D>> {
247✔
362
        let zoom = id.level();
247✔
363
        let face = id.face();
247✔
364
        // If the zoom is out of bounds, return nothing
247✔
365
        if !(0..=20).contains(&zoom) || !self.faces.contains(&face.into()) {
247✔
366
            return None;
50✔
367
        }
197✔
368

197✔
369
        // we want to find the closest tile to the data.
197✔
370
        let mut p_id = id;
197✔
371
        while !self.tiles.contains_key(&p_id) && !p_id.is_face() {
197✔
372
            p_id = p_id.parent(None);
×
373
        }
×
374
        // split as necessary, the algorithm will know if the tile is already split
375
        self.split_tile(p_id, Some(id), Some(zoom));
197✔
376

197✔
377
        // grab the tile and split it if necessary
197✔
378
        self.tiles.get(&id)
197✔
379
    }
247✔
380
}
381

382
/// A trait for transforming a geometry from the 0->1 coordinate system to a tile coordinate system
383
pub trait TransformVectorGeometry<M: Clone + Default = MValue> {
384
    /// Transform the geometry from the 0->1 coordinate system to a tile coordinate system
385
    fn transform(&mut self, zoom: f64, ti: f64, tj: f64);
386
}
387
impl<M: Clone + Default> TransformVectorGeometry<M> for VectorGeometry<M> {
388
    /// Transform the geometry from the 0->1 coordinate system to a tile coordinate system
389
    fn transform(&mut self, zoom: f64, ti: f64, tj: f64) {
106✔
390
        let zoom = (1 << (zoom as u64)) as f64;
106✔
391
        match self {
106✔
392
            VectorGeometry::Point(p) => p.coordinates.transform(zoom, ti, tj),
69✔
393
            VectorGeometry::LineString(l) | VectorGeometry::MultiPoint(l) => {
12✔
394
                l.coordinates.iter_mut().for_each(|p| p.transform(zoom, ti, tj))
12✔
395
            }
396
            VectorGeometry::MultiLineString(l) | VectorGeometry::Polygon(l) => l
25✔
397
                .coordinates
25✔
398
                .iter_mut()
25✔
399
                .for_each(|l| l.iter_mut().for_each(|p| p.transform(zoom, ti, tj))),
160✔
400
            VectorGeometry::MultiPolygon(l) => l.coordinates.iter_mut().for_each(|p| {
×
401
                p.iter_mut().for_each(|l| l.iter_mut().for_each(|p| p.transform(zoom, ti, tj)))
×
402
            }),
×
403
        }
404
    }
106✔
405
}
406
impl<M: Clone + Default> TransformVectorGeometry<M> for VectorPoint<M> {
407
    /// Transform the point from the 0->1 coordinate system to a tile coordinate system
408
    fn transform(&mut self, zoom: f64, ti: f64, tj: f64) {
244✔
409
        self.x = self.x * zoom - ti;
244✔
410
        self.y = self.y * zoom - tj;
244✔
411
    }
244✔
412
}
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