Skip to content

Commit d2cdec9

Browse files
todtbpre-commit-ci[bot]CommanderStorm
authored
feat(martin-cp): infer default bounds from configured sources for better performance (#2385)
#2212 - Adds more dynamic default bounds (bbox) to `martin-cp` - If no bounds passed as args, and a single source exists with defined bounds, use them. Fallback to global as previously done. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Frank Elsinga <[email protected]>
1 parent 2f49d82 commit d2cdec9

File tree

6 files changed

+477
-26
lines changed

6 files changed

+477
-26
lines changed

martin/src/bin/martin-cp.rs

Lines changed: 155 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,7 @@ async fn start(copy_args: CopierArgs) -> MartinCpResult<()> {
195195
run_tile_copy(copy_args.copy, sources).await
196196
}
197197

198-
fn check_bboxes(args: &CopyArgs) -> MartinCpResult<Vec<Bounds>> {
199-
let boxes = if args.bbox.is_empty() {
200-
vec![Bounds::MAX_TILED]
201-
} else {
202-
args.bbox.clone()
203-
};
204-
198+
fn check_bboxes(boxes: Vec<Bounds>) -> MartinCpResult<Vec<Bounds>> {
205199
for bb in &boxes {
206200
let allowed_lon = Bounds::MAX_TILED.left..=Bounds::MAX_TILED.right;
207201
if !allowed_lon.contains(&bb.left) || !allowed_lon.contains(&bb.right) {
@@ -345,20 +339,53 @@ fn iterate_tiles(tiles: Vec<TileRect>) -> impl Iterator<Item = TileCoord> {
345339
}
346340

347341
fn check_sources(args: &CopyArgs, state: &ServerState) -> Result<String, MartinCpError> {
348-
if let Some(source) = &args.source {
349-
Ok(source.clone())
342+
if let Some(source_id) = &args.source {
343+
Ok(source_id.clone())
350344
} else {
351-
let sources = state.tiles.source_names();
352-
if let Some(source) = sources.first() {
353-
if sources.len() > 1 {
354-
return Err(MartinCpError::MultipleSources(sources.join(", ")));
345+
let source_ids = state.tiles.source_names();
346+
if let Some(source_id) = source_ids.first() {
347+
if source_ids.len() > 1 {
348+
return Err(MartinCpError::MultipleSources(source_ids.join(", ")));
355349
}
356-
Ok(source.clone())
350+
Ok(source_id.clone())
357351
} else {
358352
Err(MartinCpError::NoSources)
359353
}
360354
}
361355
}
356+
357+
fn default_bounds(src: &DynTileSource) -> Vec<Bounds> {
358+
if src.sources.is_empty() {
359+
vec![Bounds::MAX_TILED]
360+
} else {
361+
let mut source_bounds = src
362+
.sources
363+
.iter()
364+
.map(|source| source.get_tilejson().bounds.unwrap_or(Bounds::MAX_TILED))
365+
.collect::<Vec<Bounds>>();
366+
367+
source_bounds.dedup_by_key(|bounds| bounds.to_string());
368+
369+
if source_bounds.is_empty() {
370+
info!(
371+
"No configured bounds for source, using: {}",
372+
Bounds::MAX_TILED
373+
);
374+
vec![Bounds::MAX_TILED]
375+
} else {
376+
info!(
377+
"No bbox specified, using source bounds: {}",
378+
source_bounds
379+
.iter()
380+
.map(|s| format!("[{s}]"))
381+
.collect::<Vec<String>>()
382+
.join(", ")
383+
);
384+
source_bounds
385+
}
386+
}
387+
}
388+
362389
#[expect(clippy::too_many_lines)]
363390
async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()> {
364391
let output_file = &args.output_file;
@@ -372,23 +399,30 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()>
372399
);
373400
}
374401

375-
let source = check_sources(&args, &state)?;
402+
let source_id = check_sources(&args, &state)?;
376403

377404
let src = DynTileSource::new(
378405
&state.tiles,
379-
&source,
406+
&source_id,
380407
None,
381408
args.url_query.as_deref().unwrap_or_default(),
382409
Some(parse_encoding(args.encoding.as_str())?),
383410
None,
384411
None,
385412
None,
386413
)?;
414+
415+
let inferred_bboxes = if args.bbox.is_empty() {
416+
default_bounds(&src)
417+
} else {
418+
args.bbox.clone()
419+
};
420+
let bboxes = check_bboxes(inferred_bboxes)?;
421+
387422
// parallel async below uses move, so we must only use copyable types
388423
let src = &src;
389424

390425
let zooms = get_zooms(&args);
391-
let bboxes = check_bboxes(&args)?;
392426
let tiles = compute_tile_ranges(&bboxes, &zooms);
393427
let mbt = Mbtiles::new(output_file)?;
394428
let mut conn = mbt.open_or_new().await?;
@@ -406,7 +440,7 @@ async fn run_tile_copy(args: CopyArgs, state: ServerState) -> MartinCpResult<()>
406440
"Copying {} {} tiles from {} to {}",
407441
progress.total,
408442
src.info,
409-
source,
443+
source_id,
410444
args.output_file.display()
411445
);
412446

@@ -568,12 +602,111 @@ async fn main() {
568602

569603
#[cfg(test)]
570604
mod tests {
605+
use super::*;
606+
use async_trait::async_trait;
607+
use insta::assert_yaml_snapshot;
608+
use martin::TileSources;
609+
use martin_core::tiles::{MartinCoreResult, Source, UrlQuery};
610+
use martin_tile_utils::{Encoding, Format};
611+
use rstest::{fixture, rstest};
571612
use std::str::FromStr;
613+
use tilejson::{TileJSON, tilejson};
572614

573-
use insta::assert_yaml_snapshot;
574-
use rstest::rstest;
615+
#[derive(Debug, Clone)]
616+
pub struct MockSource {
617+
pub id: &'static str,
618+
pub tj: TileJSON,
619+
pub data: TileData,
620+
}
575621

576-
use super::*;
622+
#[async_trait]
623+
impl Source for MockSource {
624+
fn get_id(&self) -> &str {
625+
self.id
626+
}
627+
628+
fn get_tilejson(&self) -> &TileJSON {
629+
&self.tj
630+
}
631+
632+
fn get_tile_info(&self) -> TileInfo {
633+
TileInfo::new(Format::Mvt, Encoding::Uncompressed)
634+
}
635+
636+
fn clone_source(&self) -> BoxedSource {
637+
Box::new(self.clone())
638+
}
639+
640+
async fn get_tile(
641+
&self,
642+
_xyz: TileCoord,
643+
_url_query: Option<&UrlQuery>,
644+
) -> MartinCoreResult<TileData> {
645+
Ok(self.data.clone())
646+
}
647+
}
648+
649+
#[fixture]
650+
fn many_sources() -> TileSources {
651+
TileSources::new(vec![vec![
652+
Box::new(MockSource {
653+
id: "test_source",
654+
tj: tilejson! { tiles: vec![], bounds: Bounds::from_str("-110.0,20.0,-120.0,80.0").unwrap() },
655+
data: Vec::default(),
656+
}),
657+
Box::new(MockSource {
658+
id: "test_source2",
659+
tj: tilejson! { tiles: vec![], bounds: Bounds::from_str("-130.0,40.0,-170.0,10.0").unwrap() },
660+
data: Vec::default(),
661+
}),
662+
Box::new(MockSource {
663+
id: "unrequested_source",
664+
tj: tilejson! { tiles: vec![], bounds: Bounds::from_str("-150.0,40.0,-120.0,10.0").unwrap() },
665+
data: Vec::default(),
666+
}),
667+
Box::new(MockSource {
668+
id: "unbounded_source",
669+
tj: tilejson! { tiles: vec![] },
670+
data: Vec::default(),
671+
}),
672+
]])
673+
}
674+
675+
#[fixture]
676+
fn one_source() -> TileSources {
677+
TileSources::new(vec![vec![Box::new(MockSource {
678+
id: "test_source",
679+
tj: tilejson! { tiles: vec![], bounds: Bounds::from_str("-120.0,30.0,-110.0,40.0").unwrap() },
680+
data: Vec::default(),
681+
})]])
682+
}
683+
684+
#[fixture]
685+
fn source_wo_bounds() -> TileSources {
686+
TileSources::new(vec![vec![Box::new(MockSource {
687+
id: "test_source",
688+
tj: tilejson! { tiles: vec![] },
689+
data: Vec::default(),
690+
})]])
691+
}
692+
693+
#[rstest]
694+
#[case::one_source(one_source(), "test_source", vec![Bounds::from_str("-120.0,30.0,-110.0,40.0").unwrap()])]
695+
#[case::many_sources(many_sources(), "test_source,test_source2", vec![Bounds::from_str("-110.0,20.0,-120.0,80.0").unwrap(), Bounds::from_str("-130.0,40.0,-170.0,10.0").unwrap()])]
696+
#[case::many_sources_rev(many_sources(), "test_source2,test_source", vec![Bounds::from_str("-130.0,40.0,-170.0,10.0").unwrap(), Bounds::from_str("-110.0,20.0,-120.0,80.0").unwrap()])]
697+
#[case::many_sources_only_unbounded(many_sources(), "unbounded_source", vec![Bounds::MAX_TILED])]
698+
#[case::many_sources_bounded_and_unbounded(many_sources(), "test_source,unbounded_source", vec![Bounds::from_str("-110.0,20.0,-120.0,80.0").unwrap(), Bounds::MAX_TILED])]
699+
#[case::many_sources_bounded_and_unbounded_rev(many_sources(), "unbounded_source,test_source", vec![Bounds::MAX_TILED, Bounds::from_str("-110.0,20.0,-120.0,80.0").unwrap()])]
700+
#[case::source_wo_bounds(source_wo_bounds(), "test_source", vec![Bounds::MAX_TILED])]
701+
fn test_default_bounds(
702+
#[case] src: TileSources,
703+
#[case] ids: &str,
704+
#[case] expected: Vec<Bounds>,
705+
) {
706+
let dts = DynTileSource::new(&src, ids, None, "", None, None, None, None).unwrap();
707+
708+
assert_eq!(default_bounds(&dts), expected);
709+
}
577710

578711
#[test]
579712
fn test_compute_tile_ranges() {
@@ -611,7 +744,6 @@ mod tests {
611744
}
612745

613746
#[rstest]
614-
#[case("", Ok(Bounds::MAX_TILED.to_string()))]
615747
#[case("-180.0,-85.05112877980659,180.0,85.0511287798066", Ok(Bounds::MAX_TILED.to_string()))]
616748
#[case("-120.0,30.0,-110.0,40.0", Ok("-120.0,30.0,-110.0,40.0".to_string()))]
617749
#[case("-190.0,30.0,-110.0,40.0", Err("longitude".to_string()))]
@@ -627,10 +759,7 @@ mod tests {
627759
vec![Bounds::from_str(bbox_str).unwrap()]
628760
};
629761

630-
let result = check_bboxes(&CopyArgs {
631-
bbox: bbox_vec,
632-
..Default::default()
633-
});
762+
let result = check_bboxes(bbox_vec);
634763

635764
match expected {
636765
Ok(expected_str) => {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[INFO ] cp_no-bbox has an unrecognized metadata value foo={"bar":"foo"}
2+
id: cp_no-bbox
3+
tile_info:
4+
format: mvt
5+
encoding: gzip
6+
tilejson:
7+
tilejson: 3.0.0
8+
tiles: []
9+
vector_layers:
10+
- id: table_source
11+
fields:
12+
gid: int4
13+
bounds:
14+
- -2.0
15+
- -1.0
16+
- 142.84131509869133
17+
- 45.0
18+
maxzoom: 6
19+
minzoom: 0
20+
name: table_source
21+
foo: '{"bar":"foo"}'
22+
format: pbf
23+
generator: martin-cp v0.0.0
24+
agg_tiles_hash: 7323D1D8A07A7176998822DB65E08567
25+

0 commit comments

Comments
 (0)