Skip to content

Commit 7111062

Browse files
committed
fix: Fix min/max tile index calculation for some bounds
Add a method to the tile schema builder to set the bounds for which the tiles exist. This method when tested revieled a bug in the min/max tile index calcultion which resulted in wrong tiles being selected for the specified bounding box.
1 parent b59437b commit 7111062

File tree

2 files changed

+141
-17
lines changed

2 files changed

+141
-17
lines changed

galileo/src/tile_schema/builder.rs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ use super::schema::{TileSchema, VerticalDirection};
1414
#[derive(Debug)]
1515
pub struct TileSchemaBuilder {
1616
origin: Point2,
17-
bounds: Rect,
17+
world_bounds: Rect,
18+
tile_bounds: Rect,
1819
lods: Lods,
1920
tile_width: u32,
2021
tile_height: u32,
@@ -75,8 +76,8 @@ impl TileSchemaBuilder {
7576
pub fn build(self) -> Result<TileSchema, TileSchemaError> {
7677
// Resolution is bound by the maximum tile index that can be represented
7778
let min_resolution = f64::min(
78-
self.bounds.width() / self.tile_width as f64 / u64::MAX as f64,
79-
self.bounds.height() / self.tile_height as f64 / u64::MAX as f64,
79+
self.world_bounds.width() / self.tile_width as f64 / u64::MAX as f64,
80+
self.world_bounds.height() / self.tile_height as f64 / u64::MAX as f64,
8081
);
8182

8283
let lods = match self.lods {
@@ -85,7 +86,7 @@ impl TileSchemaBuilder {
8586
return Err(TileSchemaError::NoZLevelsProvided);
8687
}
8788

88-
let top_resolution = self.bounds.width() / self.tile_width as f64;
89+
let top_resolution = self.world_bounds.width() / self.tile_width as f64;
8990

9091
let max_z_level = *z_levels.iter().max().unwrap_or(&0);
9192
let mut lods = vec![f64::MAX; max_z_level as usize + 1];
@@ -161,7 +162,7 @@ impl TileSchemaBuilder {
161162

162163
Ok(TileSchema {
163164
origin: self.origin,
164-
bounds: self.bounds,
165+
bounds: self.tile_bounds,
165166
lods: Arc::new(lods),
166167
tile_width: self.tile_width,
167168
tile_height: self.tile_height,
@@ -184,7 +185,13 @@ impl TileSchemaBuilder {
184185

185186
Self {
186187
origin: Point2::new(-MAX_COORD_VALUE, MAX_COORD_VALUE),
187-
bounds: Rect::new(
188+
world_bounds: Rect::new(
189+
-MAX_COORD_VALUE,
190+
-MAX_COORD_VALUE,
191+
MAX_COORD_VALUE,
192+
MAX_COORD_VALUE,
193+
),
194+
tile_bounds: Rect::new(
188195
-MAX_COORD_VALUE,
189196
-MAX_COORD_VALUE,
190197
MAX_COORD_VALUE,
@@ -238,14 +245,14 @@ impl TileSchemaBuilder {
238245
}
239246

240247
/// Sets the origin point of the tiles.
241-
///
248+
///
242249
/// Origin point is set in projection coordinates (for example, in Mercator meters for Mercator projection).
243-
///
250+
///
244251
/// It is the point where the tile with index `(X: 0, Y: 0)` is located (for every Z index). If the schema
245252
/// uses direction of Y indices from top to bottom, the origin point will be at the left top angle of the
246253
/// tile. If the direction of Y indices is from bottom to top, the origin point will be at the left bottom
247254
/// point of the tile.
248-
///
255+
///
249256
/// ```
250257
/// # use galileo::tile_schema::TileSchemaBuilder;
251258
/// # use galileo::galileo_types::cartesian::Point2;
@@ -254,14 +261,37 @@ impl TileSchemaBuilder {
254261
/// .build()
255262
/// .expect("tile schema is properly defined");
256263
/// ```
257-
///
264+
///
258265
/// Note that origin point doesn't have to be inside the schema bounds. For example, the origin may point to
259266
/// the top left angle of the world map, but tiles might only be available for a specific region, and the
260-
/// bounds will only contain that region. In this case tiles may have indicies starting not from 0.
267+
/// bounds will only contain that region. In this case tiles may have indices starting not from 0.
261268
pub fn origin(mut self, origin: Point2) -> Self {
262269
self.origin = origin;
263270
self
264271
}
272+
273+
/// Sets a rectangle in projected coordinates for which tiles are available.
274+
///
275+
/// Tiles that lies outside of the bounds will not be requested from the source.
276+
///
277+
/// ```
278+
/// # use galileo::tile_schema::TileSchemaBuilder;
279+
/// # use galileo::galileo_types::cartesian::Rect;
280+
/// let tile_schema = TileSchemaBuilder::web_mercator(0..23)
281+
/// // only show tiles for Angola
282+
/// .bounds(Rect::new(1282761., -1975899., 2674573., -590691.))
283+
/// .build()
284+
/// .expect("tile schema is properly defined");
285+
/// ```
286+
///
287+
/// # Errors
288+
///
289+
/// 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 {
292+
self.tile_bounds = bounds;
293+
self
294+
}
265295
}
266296

267297
#[cfg(test)]

galileo/src/tile_schema/schema.rs

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,16 @@ impl TileSchema {
203203
let pix_bound = (self.bounds.x_max() - self.origin.x()) / resolution;
204204
let floored = pix_bound.floor();
205205
if (pix_bound - floored).abs() < 0.1 {
206-
(floored / self.tile_width as f64) as i32 - 1
206+
(pix_bound / self.tile_width as f64) as i32 - 1
207207
} else {
208-
(floored / self.tile_width as f64) as i32
208+
(pix_bound / self.tile_width as f64) as i32
209209
}
210210
}
211211

212212
fn min_y_index(&self, resolution: f64) -> i32 {
213213
match self.y_direction {
214214
VerticalDirection::TopToBottom => {
215-
((self.bounds.y_min() + self.origin.y()) / resolution / self.tile_height as f64)
215+
((self.origin.y() - self.bounds.y_max()) / resolution / self.tile_height as f64)
216216
.floor() as i32
217217
}
218218
VerticalDirection::BottomToTop => {
@@ -224,14 +224,15 @@ impl TileSchema {
224224

225225
fn max_y_index(&self, resolution: f64) -> i32 {
226226
let pix_bound = match self.y_direction {
227-
VerticalDirection::TopToBottom => (self.bounds.y_max() + self.origin.y()) / resolution,
227+
VerticalDirection::TopToBottom => (self.origin.y() - self.bounds.y_min()) / resolution,
228228
VerticalDirection::BottomToTop => (self.bounds.y_max() - self.origin.y()) / resolution,
229229
};
230+
230231
let floored = pix_bound.floor();
231232
if (pix_bound - floored).abs() < 0.1 {
232-
(floored / self.tile_height as f64) as i32 - 1
233+
(pix_bound / self.tile_width as f64) as i32 - 1
233234
} else {
234-
(floored / self.tile_height as f64) as i32
235+
(pix_bound / self.tile_width as f64) as i32
235236
}
236237
}
237238
}
@@ -397,4 +398,97 @@ mod tests {
397398
4
398399
);
399400
}
401+
402+
#[test]
403+
fn iter_tiles_origin_out_of_bounds() {
404+
let schema = TileSchema {
405+
origin: Point2::new(0.0, 0.0),
406+
bounds: Rect::new(1000.0, 1000.0, 2000.0, 2000.0),
407+
lods: Arc::new(vec![30.0, 10.0, 1.0]),
408+
tile_width: 10,
409+
tile_height: 10,
410+
y_direction: VerticalDirection::BottomToTop,
411+
wrap_x: false,
412+
};
413+
414+
let tiles: Vec<_> = schema
415+
.iter_tiles_over_bbox(10.0, Rect::new(0.0, 0.0, 500.0, 500.0))
416+
.unwrap()
417+
.collect();
418+
assert!(
419+
tiles.is_empty(),
420+
"Expected empty tiles iter, but got: {tiles:?}"
421+
);
422+
423+
let tiles: Vec<_> = schema
424+
.iter_tiles_over_bbox(10.0, Rect::new(900.0, 900.0, 1100.0, 1100.0))
425+
.unwrap()
426+
.collect();
427+
428+
assert_eq!(tiles.len(), 1);
429+
assert_eq!(tiles[0], WrappingTileIndex::new(10, 10, 1));
430+
431+
let tiles: Vec<_> = schema
432+
.iter_tiles_over_bbox(30.0, Rect::new(900.0, 900.0, 950.0, 950.0))
433+
.unwrap()
434+
.collect();
435+
436+
assert_eq!(tiles.len(), 1);
437+
assert_eq!(tiles[0], WrappingTileIndex::new(3, 3, 0));
438+
}
439+
440+
#[test]
441+
fn iter_tiles_origin_out_of_bounds_top_to_bottom() {
442+
let schema = TileSchema {
443+
origin: Point2::new(0.0, 3000.0),
444+
bounds: Rect::new(1000.0, 1000.0, 2000.0, 2000.0),
445+
lods: Arc::new(vec![30.0, 10.0, 1.0]),
446+
tile_width: 10,
447+
tile_height: 10,
448+
y_direction: VerticalDirection::TopToBottom,
449+
wrap_x: false,
450+
};
451+
452+
let tiles: Vec<_> = schema
453+
.iter_tiles_over_bbox(10.0, Rect::new(0.0, 0.0, 500.0, 500.0))
454+
.unwrap()
455+
.collect();
456+
assert!(
457+
tiles.is_empty(),
458+
"Expected empty tiles iter, but got: {tiles:?}"
459+
);
460+
461+
let tiles: Vec<_> = schema
462+
.iter_tiles_over_bbox(10.0, Rect::new(900.0, 900.0, 1099.0, 1099.0))
463+
.unwrap()
464+
.collect();
465+
466+
println!("{tiles:?}");
467+
assert_eq!(tiles.len(), 1);
468+
assert_eq!(tiles[0], WrappingTileIndex::new(10, 19, 1));
469+
470+
let tiles: Vec<_> = schema
471+
.iter_tiles_over_bbox(30.0, Rect::new(900.0, 900.0, 950.0, 950.0))
472+
.unwrap()
473+
.collect();
474+
475+
assert_eq!(tiles.len(), 1);
476+
assert_eq!(tiles[0], WrappingTileIndex::new(3, 6, 0));
477+
}
478+
479+
#[test]
480+
fn tile_bbox_origin_out_of_bounds() {
481+
let schema = TileSchema {
482+
origin: Point2::new(0.0, 0.0),
483+
bounds: Rect::new(1000.0, 1000.0, 2000.0, 2000.0),
484+
lods: Arc::new(vec![300.0, 100.0, 10.0, 1.0]),
485+
tile_width: 10,
486+
tile_height: 10,
487+
y_direction: VerticalDirection::BottomToTop,
488+
wrap_x: false,
489+
};
490+
491+
let bbox = schema.tile_bbox(WrappingTileIndex::new(10, 10, 2)).unwrap();
492+
assert_eq!(bbox, Rect::new(1000.0, 1000.0, 1100.0, 1100.0));
493+
}
400494
}

0 commit comments

Comments
 (0)