@@ -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
347341fn 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) ]
363390async 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) ]
570604mod 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) => {
0 commit comments