Skip to content

Commit 99e4a4b

Browse files
committed
feat: Make tile schema world bounds configurable
Add the `world_bounds` methods to the tile schema builder to decouple it from the tile bounds. This also fixes the wrapping tile indices calculation when tile bounds do not cover the whole earth.
1 parent 9b987ba commit 99e4a4b

2 files changed

Lines changed: 231 additions & 40 deletions

File tree

galileo/src/tile_schema/builder.rs

Lines changed: 124 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ enum Lods {
3030
}
3131

3232
/// Errors that can occur during building a [`TileSchema`].
33-
#[derive(Debug, thiserror::Error)]
33+
#[derive(Debug, thiserror::Error, PartialEq, Copy, Clone)]
3434
pub enum TileSchemaError {
3535
/// No zoom levels provided
3636
#[error("no zoom levels provided")]
@@ -69,11 +69,30 @@ pub enum TileSchemaError {
6969
/// Resolution of the `lower_level`
7070
lower_resolution: f64,
7171
},
72+
73+
/// Tile bounds have invalid value
74+
#[error("tile bounds have zero size or not finite: {0:?}")]
75+
InvalidTileBounds(Rect),
76+
77+
/// World bounds have invalid value
78+
#[error("world bounds have zero size or not finite: {0:?}")]
79+
InvalidWorldBounds(Rect),
7280
}
7381

7482
impl TileSchemaBuilder {
7583
/// Create a new builder with default parameters.
7684
pub fn build(self) -> Result<TileSchema, TileSchemaError> {
85+
if !self.tile_bounds.width().is_normal() || !self.tile_bounds.height().is_normal() {
86+
return Err(TileSchemaError::InvalidTileBounds(self.tile_bounds));
87+
}
88+
89+
if self.wrap_x
90+
&& matches!(self.lods, Lods::Logarithmic(_))
91+
&& (!self.world_bounds.width().is_normal() || !self.world_bounds.height().is_normal())
92+
{
93+
return Err(TileSchemaError::InvalidWorldBounds(self.tile_bounds));
94+
}
95+
7796
// Resolution is bound by the maximum tile index that can be represented
7897
let min_resolution = f64::min(
7998
self.world_bounds.width() / self.tile_width as f64 / u64::MAX as f64,
@@ -162,7 +181,8 @@ impl TileSchemaBuilder {
162181

163182
Ok(TileSchema {
164183
origin: self.origin,
165-
bounds: self.tile_bounds,
184+
tile_bounds: self.tile_bounds,
185+
world_bounds: self.world_bounds,
166186
lods: Arc::new(lods),
167187
tile_width: self.tile_width,
168188
tile_height: self.tile_height,
@@ -236,7 +256,7 @@ impl TileSchemaBuilder {
236256
/// or increasing x coordinate by the whole number of bounding box widths. This produces an effect of
237257
/// horizontally infinite map, where a user can pan as log as they want to the right or left.
238258
///
239-
/// Note, that for wrapping to work property, bounds of the tile schema should cover the whole globe.
259+
/// Note, that for wrapping to work property, world bounds of the tile schema should cover the whole globe.
240260
/// This is not enforced in `.build()` method validatation since tile schema is agnostic to the CRS
241261
/// it will be used for.
242262
pub fn wrap_x(mut self, shall_wrap: bool) -> Self {
@@ -262,40 +282,69 @@ impl TileSchemaBuilder {
262282
/// .expect("tile schema is properly defined");
263283
/// ```
264284
///
265-
/// Note that origin point doesn't have to be inside the schema bounds. For example, the origin may point to
285+
/// Note that origin point doesn't have to be inside the tile bounds. For example, the origin may point to
266286
/// the top left angle of the world map, but tiles might only be available for a specific region, and the
267287
/// bounds will only contain that region. In this case tiles may have indices starting not from 0.
268288
pub fn origin(mut self, origin: Point2) -> Self {
269289
self.origin = origin;
270290
self
271291
}
272292

273-
/// Sets a rectangle in projected coordinates for which tiles are available.
293+
/// Sets the rectangle in projected coordinates for which tiles are available.
274294
///
275-
/// Tiles that lies outside of the bounds will not be requested from the source.
295+
/// Tiles that lie outside of the bounds will not be requested from the source.
276296
///
277297
/// ```
278298
/// # use galileo::tile_schema::TileSchemaBuilder;
279299
/// # use galileo::galileo_types::cartesian::Rect;
280300
/// let tile_schema = TileSchemaBuilder::web_mercator(0..23)
281301
/// // only show tiles for Angola
282-
/// .bounds(Rect::new(1282761., -1975899., 2674573., -590691.))
302+
/// .tile_bounds(Rect::new(1282761., -1975899., 2674573., -590691.))
283303
/// .build()
284304
/// .expect("tile schema is properly defined");
285305
/// ```
286306
///
287307
/// # Errors
288308
///
289309
/// If either width or height of the bounds rectangle is `0`, `NaN` or `Infinity`, building the tile schema
290-
/// will return an error `TileSchemaError::InvalidBounds`.
291-
pub fn bounds(mut self, bounds: Rect) -> Self {
310+
/// will return an error [`TileSchemaError::InvalidTileBounds`].
311+
pub fn tile_bounds(mut self, bounds: Rect) -> Self {
292312
self.tile_bounds = bounds;
293313
self
294314
}
315+
316+
/// Sets the rectangle in projected coordinates, which includes the whole globe as defined by the target
317+
/// projection.
318+
///
319+
/// World bounds are used to calculate x coordinate of tiles when wrapping around 180 parallel, and to
320+
/// calculate resolution levels for logarithmic z-levels. If wrapping is not used and z-levels are set
321+
/// manually, this parameter is not required for correct calculations of the tile indices.
322+
///
323+
/// ```
324+
/// # use galileo::tile_schema::TileSchemaBuilder;
325+
/// # use galileo::galileo_types::cartesian::Rect;
326+
/// let tile_schema = TileSchemaBuilder::web_mercator(0..23)
327+
/// // square WebMercator projetion bounds
328+
/// .world_bounds(Rect::new(-20037508.342787, -20037508.342787, 20037508.342787, 20037508.342787))
329+
/// .build()
330+
/// .expect("tile schema is properly defined");
331+
/// ```
332+
///
333+
/// # Errors
334+
///
335+
/// If either width or height of the bounds rectangle is `0`, `NaN` or `Infinity`, building the tile schema
336+
/// will return an error `TileSchemaError::InvalidWorldBounds`. This check is skipped if neither wrapping nor
337+
/// logarithmic z-levels are used for the schema.
338+
pub fn world_bounds(mut self, bounds: Rect) -> Self {
339+
self.world_bounds = bounds;
340+
self
341+
}
295342
}
296343

297344
#[cfg(test)]
298345
mod tests {
346+
use core::f64;
347+
299348
use approx::assert_abs_diff_eq;
300349

301350
use super::*;
@@ -322,7 +371,7 @@ mod tests {
322371
Point2::new(-20037508.342787, 20037508.342787)
323372
);
324373
assert_eq!(
325-
schema.bounds,
374+
schema.tile_bounds,
326375
Rect::new(
327376
-20037508.342787,
328377
-20037508.342787,
@@ -507,4 +556,69 @@ mod tests {
507556
"Unexpected schema build result: {result:?}"
508557
)
509558
}
559+
560+
#[test]
561+
fn invalid_tile_bounds_return_error() {
562+
let to_check = [
563+
Rect::new(0.0, 0.0, 0.0, 1000.0),
564+
Rect::new(0.0, 0.0, 1000.0, 0.0),
565+
Rect::new(0.0, 0.0, f64::NAN, 1000.0),
566+
Rect::new(0.0, 0.0, 0.0, f64::INFINITY),
567+
Rect::new(f64::NEG_INFINITY, 0.0, 1000.0, 1000.0),
568+
];
569+
570+
for bounds in to_check {
571+
let result = TileSchemaBuilder::web_mercator(0..18)
572+
.tile_bounds(bounds)
573+
.build();
574+
assert!(
575+
matches!(result, Err(TileSchemaError::InvalidTileBounds(_))),
576+
"Error not returned for tile bounds: {bounds:?}"
577+
);
578+
}
579+
}
580+
581+
#[test]
582+
fn invalid_world_bounds_return_error() {
583+
let to_check = [
584+
Rect::new(0.0, 0.0, 0.0, 1000.0),
585+
Rect::new(0.0, 0.0, 1000.0, 0.0),
586+
Rect::new(0.0, 0.0, f64::NAN, 1000.0),
587+
Rect::new(0.0, 0.0, 0.0, f64::INFINITY),
588+
Rect::new(f64::NEG_INFINITY, 0.0, 1000.0, 1000.0),
589+
];
590+
591+
for bounds in to_check {
592+
let result = TileSchemaBuilder::web_mercator(0..18)
593+
.world_bounds(bounds)
594+
.build();
595+
assert!(
596+
matches!(result, Err(TileSchemaError::InvalidWorldBounds(_))),
597+
"Error not returned for world bounds: {bounds:?}"
598+
);
599+
}
600+
}
601+
602+
#[test]
603+
fn invalid_world_bounds_skipped_if_not_needed() {
604+
let to_check = [
605+
Rect::new(0.0, 0.0, 0.0, 1000.0),
606+
Rect::new(0.0, 0.0, 1000.0, 0.0),
607+
Rect::new(0.0, 0.0, f64::NAN, 1000.0),
608+
Rect::new(0.0, 0.0, 0.0, f64::INFINITY),
609+
Rect::new(f64::NEG_INFINITY, 0.0, 1000.0, 1000.0),
610+
];
611+
612+
for bounds in to_check {
613+
let result = TileSchemaBuilder::web_mercator(0..18)
614+
.world_bounds(bounds)
615+
.wrap_x(false)
616+
.with_z_levels([(0, 1000.0), (1, 500.0)])
617+
.build();
618+
assert!(
619+
result.is_ok(),
620+
"Error returned for world bounds: {bounds:?}"
621+
);
622+
}
623+
}
510624
}

0 commit comments

Comments
 (0)