Skip to content

Commit 20f477a

Browse files
committed
refactor: Construct tile schema through builder
Introduce a `TileSchemaBuilder` to validate the tile schema parameters before it is used by the map. This commit makes the tile schema parameters private so it is harder to construct an invalid schema that will break everything. The builder validates parameters when `.build()` is called.
1 parent 729f360 commit 20f477a

File tree

6 files changed

+294
-117
lines changed

6 files changed

+294
-117
lines changed

galileo/examples/vector_tiles.rs

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ use galileo::layer::vector_tile_layer::VectorTileLayerBuilder;
1010
use galileo::layer::VectorTileLayer;
1111
use galileo::render::text::text_service::TextService;
1212
use galileo::render::text::RustybuzzRasterizer;
13-
use galileo::tile_schema::{TileIndex, TileSchema, VerticalDirection};
13+
use galileo::tile_schema::{TileIndex, TileSchema, TileSchemaBuilder};
1414
use galileo::{Map, MapBuilder};
1515
use galileo_egui::{EguiMap, EguiMapState};
16-
use galileo_types::cartesian::{Point2, Rect};
1716
use parking_lot::RwLock;
1817

1918
#[cfg(not(target_arch = "wasm32"))]
@@ -156,25 +155,8 @@ fn gray_style() -> VectorTileStyle {
156155
}
157156

158157
fn tile_schema() -> TileSchema {
159-
const ORIGIN: Point2 = Point2::new(-20037508.342787, 20037508.342787);
160-
const TOP_RESOLUTION: f64 = 156543.03392800014 / 16.0;
161-
162-
let mut lods = vec![0.0, 0.0, TOP_RESOLUTION];
163-
for i in 3..16 {
164-
lods.push(lods[i - 3] / 2.0);
165-
}
166-
167-
TileSchema {
168-
origin: ORIGIN,
169-
bounds: Rect::new(
170-
-20037508.342787,
171-
-20037508.342787,
172-
20037508.342787,
173-
20037508.342787,
174-
),
175-
lods: lods.into_iter().collect(),
176-
tile_width: 1024,
177-
tile_height: 1024,
178-
y_direction: VerticalDirection::TopToBottom,
179-
}
158+
TileSchemaBuilder::web_mercator(2..16)
159+
.with_rect_tile_size(1024)
160+
.build()
161+
.expect("invalid tile schema")
180162
}

galileo/examples/vector_tiles_labels.rs

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ use galileo::layer::vector_tile_layer::style::{
77
use galileo::layer::vector_tile_layer::{VectorTileLayer, VectorTileLayerBuilder};
88
use galileo::render::text::text_service::TextService;
99
use galileo::render::text::{FontWeight, RustybuzzRasterizer, TextStyle};
10-
use galileo::tile_schema::{TileIndex, TileSchema, VerticalDirection};
10+
use galileo::tile_schema::{TileIndex, TileSchema, TileSchemaBuilder};
1111
use galileo::{Color, MapBuilder};
12-
use galileo_types::cartesian::{Point2, Rect};
1312

1413
#[cfg(not(target_arch = "wasm32"))]
1514
fn main() {
@@ -97,25 +96,8 @@ fn default_style() -> VectorTileStyle {
9796
}
9897

9998
fn tile_schema() -> TileSchema {
100-
const ORIGIN: Point2 = Point2::new(-20037508.342787, 20037508.342787);
101-
const TOP_RESOLUTION: f64 = 156543.03392800014 / 16.0;
102-
103-
let mut lods = vec![0.0, 0.0, TOP_RESOLUTION];
104-
for i in 3..16 {
105-
lods.push(lods[i - 3] / 2.0);
106-
}
107-
108-
TileSchema {
109-
origin: ORIGIN,
110-
bounds: Rect::new(
111-
-20037508.342787,
112-
-20037508.342787,
113-
20037508.342787,
114-
20037508.342787,
115-
),
116-
lods: lods.into_iter().collect(),
117-
tile_width: 1024,
118-
tile_height: 1024,
119-
y_direction: VerticalDirection::TopToBottom,
120-
}
99+
TileSchemaBuilder::web_mercator(2..16)
100+
.with_rect_tile_size(1024)
101+
.build()
102+
.expect("invalid tile schema")
121103
}

galileo/src/tile_schema/builder.rs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
//! Builder for [`TileSchema`].
2+
3+
use core::f64;
4+
5+
use galileo_types::cartesian::{Point2, Rect};
6+
7+
use super::schema::{TileSchema, VerticalDirection};
8+
9+
/// Builder for [`TileSchema`].
10+
///
11+
/// The builder validates all the input parameters and guarantees that the created schema is valid.
12+
#[derive(Debug)]
13+
pub struct TileSchemaBuilder {
14+
origin: Point2,
15+
bounds: Rect,
16+
lods: Lods,
17+
tile_width: u32,
18+
tile_height: u32,
19+
y_direction: VerticalDirection,
20+
}
21+
22+
#[derive(Debug)]
23+
enum Lods {
24+
Logarithmic(Vec<u32>),
25+
}
26+
27+
/// Errors that can occur during building a [`TileSchema`].
28+
#[derive(Debug, thiserror::Error)]
29+
pub enum TileSchemaError {
30+
/// No zoom levels provided
31+
#[error("No zoom levels provided")]
32+
NoZLevelsProvided,
33+
34+
/// Invalid tile size
35+
#[error("Invalid tile size: {width}x{height}")]
36+
InvalidTileSize {
37+
/// Tile width
38+
width: u32,
39+
/// Tile height
40+
height: u32,
41+
},
42+
}
43+
44+
impl TileSchemaBuilder {
45+
/// Create a new builder with default parameters.
46+
pub fn build(self) -> Result<TileSchema, TileSchemaError> {
47+
let lods = match self.lods {
48+
Lods::Logarithmic(z_levels) => {
49+
if z_levels.is_empty() {
50+
return Err(TileSchemaError::NoZLevelsProvided);
51+
}
52+
53+
let top_resolution = self.bounds.width() / self.tile_width as f64;
54+
55+
let max_z_level = *z_levels.iter().max().unwrap_or(&0);
56+
let mut lods = vec![f64::NAN; max_z_level as usize + 1];
57+
58+
for z in z_levels {
59+
let resolution = top_resolution / f64::powi(2.0, z as i32);
60+
lods[z as usize] = resolution;
61+
}
62+
63+
lods
64+
}
65+
};
66+
67+
if self.tile_width == 0 || self.tile_height == 0 {
68+
return Err(TileSchemaError::InvalidTileSize {
69+
width: self.tile_width,
70+
height: self.tile_height,
71+
});
72+
}
73+
74+
Ok(TileSchema {
75+
origin: self.origin,
76+
bounds: self.bounds,
77+
lods,
78+
tile_width: self.tile_width,
79+
tile_height: self.tile_height,
80+
y_direction: self.y_direction,
81+
})
82+
}
83+
84+
/// Standard Web Mercator based tile scheme (used, for example, by OSM and Google maps).
85+
pub fn web_mercator(z_levels: impl IntoIterator<Item = u32>) -> Self {
86+
const TILE_SIZE: u32 = 256;
87+
88+
Self::web_mercator_base()
89+
.with_logarithmic_z_levels(z_levels)
90+
.with_rect_tile_size(TILE_SIZE)
91+
}
92+
93+
fn web_mercator_base() -> Self {
94+
const MAX_COORD_VALUE: f64 = 20037508.342787;
95+
96+
Self {
97+
origin: Point2::new(-MAX_COORD_VALUE, MAX_COORD_VALUE),
98+
bounds: Rect::new(
99+
-MAX_COORD_VALUE,
100+
-MAX_COORD_VALUE,
101+
MAX_COORD_VALUE,
102+
MAX_COORD_VALUE,
103+
),
104+
lods: Lods::Logarithmic(Vec::new()),
105+
tile_width: 0,
106+
tile_height: 0,
107+
y_direction: VerticalDirection::TopToBottom,
108+
}
109+
}
110+
111+
/// Set both tile width and height to `tile_size`.
112+
pub fn with_rect_tile_size(mut self, tile_size: u32) -> Self {
113+
self.tile_width = tile_size;
114+
self.tile_height = tile_size;
115+
116+
self
117+
}
118+
119+
fn with_logarithmic_z_levels(mut self, z_levels: impl IntoIterator<Item = u32>) -> Self {
120+
self.lods = Lods::Logarithmic(z_levels.into_iter().collect());
121+
122+
self
123+
}
124+
}
125+
126+
#[cfg(test)]
127+
mod tests {
128+
use approx::assert_abs_diff_eq;
129+
130+
use super::*;
131+
use crate::tile_schema::VerticalDirection;
132+
133+
#[test]
134+
fn schema_builder_normal_web_mercator() {
135+
let schema = TileSchemaBuilder::web_mercator(0..=20).build().unwrap();
136+
assert_eq!(schema.lods.len(), 21);
137+
138+
assert_abs_diff_eq!(schema.lods[0], 156543.03392802345);
139+
140+
for z in 1..=20 {
141+
let expected = 156543.03392802345 / 2f64.powi(z);
142+
assert_abs_diff_eq!(schema.lods[z as usize], expected);
143+
}
144+
145+
assert_eq!(schema.tile_width, 256);
146+
assert_eq!(schema.tile_height, 256);
147+
assert_eq!(
148+
schema.origin,
149+
Point2::new(-20037508.342787, 20037508.342787)
150+
);
151+
assert_eq!(
152+
schema.bounds,
153+
Rect::new(
154+
-20037508.342787,
155+
-20037508.342787,
156+
20037508.342787,
157+
20037508.342787
158+
)
159+
);
160+
assert_eq!(schema.y_direction, VerticalDirection::TopToBottom);
161+
}
162+
163+
#[test]
164+
fn schema_builder_no_z_levels() {
165+
let result = TileSchemaBuilder::web_mercator(std::iter::empty()).build();
166+
assert!(
167+
matches!(result, Err(TileSchemaError::NoZLevelsProvided)),
168+
"Got {:?}",
169+
result
170+
);
171+
}
172+
173+
#[test]
174+
fn skipping_first_z_levels() {
175+
let schema = TileSchemaBuilder::web_mercator(5..=10).build().unwrap();
176+
assert_eq!(schema.lods.len(), 11);
177+
178+
assert_abs_diff_eq!(schema.lods[5], 156543.03392802345 / 2f64.powi(5));
179+
assert_abs_diff_eq!(schema.lods[10], 156543.03392802345 / 2f64.powi(10));
180+
}
181+
182+
#[test]
183+
fn zero_tile_size() {
184+
let result = TileSchemaBuilder::web_mercator(0..=20)
185+
.with_rect_tile_size(0)
186+
.build();
187+
assert!(
188+
matches!(
189+
result,
190+
Err(TileSchemaError::InvalidTileSize {
191+
width: 0,
192+
height: 0
193+
})
194+
),
195+
"Got {:?}",
196+
result
197+
);
198+
}
199+
}

galileo/src/tile_schema/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! [`TileSchema`] is used by tile layers to calculate [tile indices](TileIndex) needed for a given ['MapView'].
2+
3+
mod builder;
4+
mod schema;
5+
mod tile_index;
6+
7+
pub use builder::{TileSchemaBuilder, TileSchemaError};
8+
pub use schema::{TileSchema, VerticalDirection};
9+
pub use tile_index::{TileIndex, WrappingTileIndex};

0 commit comments

Comments
 (0)