Skip to content

Commit 5363825

Browse files
committed
feat: Support custom z-levels in tile schemas
1 parent 7026656 commit 5363825

1 file changed

Lines changed: 155 additions & 22 deletions

File tree

galileo/src/tile_schema/builder.rs

Lines changed: 155 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use core::f64;
44
use std::sync::Arc;
55

6+
use ahash::HashMap;
67
use galileo_types::cartesian::{Point2, Rect};
78

89
use super::schema::{TileSchema, VerticalDirection};
@@ -23,17 +24,18 @@ pub struct TileSchemaBuilder {
2324
#[derive(Debug)]
2425
enum Lods {
2526
Logarithmic(Vec<u32>),
27+
Custom(HashMap<u32, f64>),
2628
}
2729

2830
/// Errors that can occur during building a [`TileSchema`].
2931
#[derive(Debug, thiserror::Error)]
3032
pub enum TileSchemaError {
3133
/// No zoom levels provided
32-
#[error("No zoom levels provided")]
34+
#[error("no zoom levels provided")]
3335
NoZLevelsProvided,
3436

3537
/// Invalid tile size
36-
#[error("Invalid tile size: {width}x{height}")]
38+
#[error("invalid tile size: {width}x{height}")]
3739
InvalidTileSize {
3840
/// Tile width
3941
width: u32,
@@ -45,18 +47,37 @@ pub enum TileSchemaError {
4547
///
4648
/// If the resolution is too small, it means that the tile indices would exceed the maximum
4749
/// representable value (u64::MAX).
48-
#[error("Resolution too small at z-level {z_level}: {resolution}")]
50+
#[error("resolution too small at z-level {z_level}: {resolution}")]
4951
ResolutionTooSmall {
5052
/// Z-level where resolution is too small
5153
z_level: u32,
5254
/// The resolution value that is too small
5355
resolution: f64,
5456
},
57+
58+
/// Z-level resolutions are not decreasing
59+
#[error("resolution at z-level {upper_level} ({upper_resolution}) cannot be smaller than resolution at z-level {lower_level} ({lower_resolution})")]
60+
NotSortedZLevels {
61+
/// Smaller z-level value
62+
upper_level: u32,
63+
/// Resolution of the `upper_level`
64+
upper_resolution: f64,
65+
/// Larger z-level value
66+
lower_level: u32,
67+
/// Resolution of the `lower_level`
68+
lower_resolution: f64,
69+
},
5570
}
5671

5772
impl TileSchemaBuilder {
5873
/// Create a new builder with default parameters.
5974
pub fn build(self) -> Result<TileSchema, TileSchemaError> {
75+
// Resolution is bound by the maximum tile index that can be represented
76+
let min_resolution = f64::min(
77+
self.bounds.width() / self.tile_width as f64 / u64::MAX as f64,
78+
self.bounds.height() / self.tile_height as f64 / u64::MAX as f64,
79+
);
80+
6081
let lods = match self.lods {
6182
Lods::Logarithmic(z_levels) => {
6283
if z_levels.is_empty() {
@@ -65,12 +86,6 @@ impl TileSchemaBuilder {
6586

6687
let top_resolution = self.bounds.width() / self.tile_width as f64;
6788

68-
// Resolution is bound by the maximum tile index that can be represented
69-
let min_resolution = f64::min(
70-
self.bounds.width() / self.tile_width as f64 / u64::MAX as f64,
71-
self.bounds.height() / self.tile_height as f64 / u64::MAX as f64,
72-
);
73-
7489
let max_z_level = *z_levels.iter().max().unwrap_or(&0);
7590
let mut lods = vec![f64::MAX; max_z_level as usize + 1];
7691

@@ -93,6 +108,45 @@ impl TileSchemaBuilder {
93108
}
94109
}
95110

111+
lods
112+
}
113+
Lods::Custom(z_levels) => {
114+
if z_levels.is_empty() {
115+
return Err(TileSchemaError::NoZLevelsProvided);
116+
}
117+
118+
let max_z_level = *z_levels.keys().max().unwrap_or(&0);
119+
let mut lods = vec![f64::MAX; max_z_level as usize + 1];
120+
121+
for i in 0..lods.len() {
122+
match z_levels.get(&(i as u32)) {
123+
Some(&resolution) => {
124+
if resolution < min_resolution {
125+
return Err(TileSchemaError::ResolutionTooSmall {
126+
z_level: i as u32,
127+
resolution,
128+
});
129+
}
130+
131+
if i > 0 && lods[i - 1] < resolution {
132+
return Err(TileSchemaError::NotSortedZLevels {
133+
upper_level: i as u32 - 1,
134+
upper_resolution: lods[i - 1],
135+
lower_level: i as u32,
136+
lower_resolution: resolution,
137+
});
138+
}
139+
140+
lods[i] = resolution
141+
}
142+
None => {
143+
if i > 0 {
144+
lods[i] = lods[i - 1];
145+
}
146+
}
147+
}
148+
}
149+
96150
lods
97151
}
98152
};
@@ -154,6 +208,16 @@ impl TileSchemaBuilder {
154208

155209
self
156210
}
211+
212+
/// Sets the z-levels with specified resolution from the iterator.
213+
///
214+
/// Z-levels are given as tuples of `(z-index, resolution)`. Smaller z-indexes must correspond
215+
/// to larger resolution values. If z-levels are not sorted correctly, building the tile schema
216+
/// would result in [`TileSchemaError::NotSortedZLevels`] error.
217+
pub fn with_z_levels(mut self, z_levels: impl IntoIterator<Item = (u32, f64)>) -> Self {
218+
self.lods = Lods::Custom(z_levels.into_iter().collect());
219+
self
220+
}
157221
}
158222

159223
#[cfg(test)]
@@ -163,15 +227,17 @@ mod tests {
163227
use super::*;
164228
use crate::tile_schema::VerticalDirection;
165229

230+
const TOP_RESOLUTION: f64 = 156543.03392802345;
231+
166232
#[test]
167233
fn schema_builder_normal_web_mercator() {
168234
let schema = TileSchemaBuilder::web_mercator(0..=20).build().unwrap();
169235
assert_eq!(schema.lods.len(), 21);
170236

171-
assert_abs_diff_eq!(schema.lods[0], 156543.03392802345);
237+
assert_abs_diff_eq!(schema.lods[0], TOP_RESOLUTION);
172238

173239
for z in 1..=20 {
174-
let expected = 156543.03392802345 / 2f64.powi(z);
240+
let expected = TOP_RESOLUTION / 2f64.powi(z);
175241
assert_abs_diff_eq!(schema.lods[z as usize], expected);
176242
}
177243

@@ -208,8 +274,8 @@ mod tests {
208274
let schema = TileSchemaBuilder::web_mercator(5..=10).build().unwrap();
209275
assert_eq!(schema.lods.len(), 11);
210276

211-
assert_abs_diff_eq!(schema.lods[5], 156543.03392802345 / 2f64.powi(5));
212-
assert_abs_diff_eq!(schema.lods[10], 156543.03392802345 / 2f64.powi(10));
277+
assert_abs_diff_eq!(schema.lods[5], TOP_RESOLUTION / 2f64.powi(5));
278+
assert_abs_diff_eq!(schema.lods[10], TOP_RESOLUTION / 2f64.powi(10));
213279
}
214280

215281
#[test]
@@ -254,30 +320,30 @@ mod tests {
254320

255321
assert_eq!(schema.lods[0], f64::MAX);
256322

257-
assert_abs_diff_eq!(schema.lods[1], 156543.03392802345 / 2f64.powi(1));
258-
assert_abs_diff_eq!(schema.lods[2], 156543.03392802345 / 2f64.powi(2));
259-
assert_abs_diff_eq!(schema.lods[3], 156543.03392802345 / 2f64.powi(3));
323+
assert_abs_diff_eq!(schema.lods[1], TOP_RESOLUTION / 2f64.powi(1));
324+
assert_abs_diff_eq!(schema.lods[2], TOP_RESOLUTION / 2f64.powi(2));
325+
assert_abs_diff_eq!(schema.lods[3], TOP_RESOLUTION / 2f64.powi(3));
260326

261-
let expected_level_3 = 156543.03392802345 / 2f64.powi(3);
327+
let expected_level_3 = TOP_RESOLUTION / 2f64.powi(3);
262328
assert_abs_diff_eq!(schema.lods[4], expected_level_3);
263329

264-
assert_abs_diff_eq!(schema.lods[5], 156543.03392802345 / 2f64.powi(5));
330+
assert_abs_diff_eq!(schema.lods[5], TOP_RESOLUTION / 2f64.powi(5));
265331
}
266332

267333
#[test]
268334
fn skipped_multiple_middle_z_levels_use_previous_value() {
269335
let schema = TileSchemaBuilder::web_mercator([0, 1, 5]).build().unwrap();
270336
assert_eq!(schema.lods.len(), 6);
271337

272-
assert_abs_diff_eq!(schema.lods[0], 156543.03392802345 / 2f64.powi(0));
273-
assert_abs_diff_eq!(schema.lods[1], 156543.03392802345 / 2f64.powi(1));
338+
assert_abs_diff_eq!(schema.lods[0], TOP_RESOLUTION / 2f64.powi(0));
339+
assert_abs_diff_eq!(schema.lods[1], TOP_RESOLUTION / 2f64.powi(1));
274340

275-
let expected_level_1 = 156543.03392802345 / 2f64.powi(1);
341+
let expected_level_1 = TOP_RESOLUTION / 2f64.powi(1);
276342
for z in 2..5 {
277343
assert_abs_diff_eq!(schema.lods[z], expected_level_1);
278344
}
279345

280-
assert_abs_diff_eq!(schema.lods[5], 156543.03392802345 / 2f64.powi(5));
346+
assert_abs_diff_eq!(schema.lods[5], TOP_RESOLUTION / 2f64.powi(5));
281347
}
282348

283349
#[test]
@@ -300,4 +366,71 @@ mod tests {
300366
result
301367
);
302368
}
369+
370+
#[test]
371+
fn custom_z_levels_equivalent_to_logarithmic() {
372+
let mut lods = vec![];
373+
const LEVELS: u32 = 32;
374+
375+
for i in 0..=LEVELS {
376+
lods.push((i, TOP_RESOLUTION / 2f64.powi(i as i32)));
377+
}
378+
379+
let tile_schema = TileSchemaBuilder::web_mercator(0..0)
380+
.with_z_levels(lods)
381+
.build()
382+
.unwrap();
383+
384+
assert_eq!(
385+
tile_schema.lods,
386+
TileSchemaBuilder::web_mercator(0..=LEVELS)
387+
.build()
388+
.unwrap()
389+
.lods,
390+
);
391+
}
392+
393+
#[test]
394+
fn custom_z_levels_check_for_min_resolution() {
395+
let result = TileSchemaBuilder::web_mercator(0..0)
396+
.with_z_levels([(0, TOP_RESOLUTION), (1, TOP_RESOLUTION / 2f64.powi(65))])
397+
.build();
398+
assert!(
399+
matches!(
400+
result,
401+
Err(TileSchemaError::ResolutionTooSmall { z_level: 1, .. })
402+
),
403+
"Unexpected schema build result: {result:?}"
404+
);
405+
}
406+
407+
#[test]
408+
fn custom_z_levels_must_be_sorted() {
409+
let mut lods = vec![];
410+
const LEVELS: u32 = 32;
411+
412+
for i in 0..=LEVELS {
413+
lods.push((i, TOP_RESOLUTION / 2f64.powi(i as i32)));
414+
}
415+
416+
lods.swap(1, 2);
417+
lods[1].0 = 1;
418+
lods[2].0 = 2;
419+
420+
let result = TileSchemaBuilder::web_mercator(0..0)
421+
.with_z_levels(lods)
422+
.build();
423+
424+
assert!(
425+
matches!(
426+
result,
427+
Err(TileSchemaError::NotSortedZLevels {
428+
upper_level: 1,
429+
lower_level: 2,
430+
..
431+
})
432+
),
433+
"Unexpected schema build result: {result:?}"
434+
)
435+
}
303436
}

0 commit comments

Comments
 (0)