1use crate::cli::Commands;
2use crate::config_t_rex as t_rex;
3use crate::datasource::source_config_from_cli_arg;
4use bbox_core::cli::CommonCommands;
5use bbox_core::config::{
6 app_dir, error_exit, from_config_root_or_exit, ConfigError, DatasourceCfg, DsPostgisCfg,
7 NamedDatasourceCfg,
8};
9use bbox_core::service::ServiceConfig;
10use clap::{ArgMatches, FromArgMatches};
11use log::{info, warn};
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet};
15use std::convert::From;
16use std::num::NonZeroU16;
17use std::path::{Path, PathBuf};
18
19#[derive(Deserialize, Serialize, Default, Debug)]
20#[serde(default)]
21pub struct TileServiceCfg {
22 #[serde(rename = "grid")]
24 pub grids: Vec<GridCfg>,
25 #[serde(rename = "datasource")]
26 pub datasources: Vec<NamedDatasourceCfg>,
27 #[serde(rename = "tileset")]
29 pub tilesets: Vec<TileSetCfg>,
30 #[serde(rename = "tilestore")]
31 pub tilestores: Vec<TileCacheProviderCfg>,
32}
33
34#[derive(Deserialize, Serialize, Clone, Debug)]
36#[serde(deny_unknown_fields)]
37pub struct TileSetCfg {
38 pub name: String,
40 #[serde(default)]
44 pub tms: Vec<TilesetTmsCfg>,
45 #[serde(flatten)]
47 pub source: SourceParamCfg,
48 pub cache: Option<String>,
50 pub cache_format: Option<String>,
52 pub cache_limits: Option<CacheLimitCfg>,
54 #[serde(default)]
56 pub cache_control: Vec<CacheControlCfg>,
57}
58
59#[derive(Deserialize, Serialize, Debug)]
61#[serde(deny_unknown_fields)]
62pub struct GridCfg {
63 pub json: String,
65}
66
67impl GridCfg {
68 pub fn abs_path(&self) -> PathBuf {
69 app_dir(&self.json)
70 }
71}
72
73#[derive(Deserialize, Serialize, Clone, Debug)]
75#[serde(deny_unknown_fields)]
76pub struct TilesetTmsCfg {
77 pub id: String,
79 pub minzoom: Option<u8>,
81 pub maxzoom: Option<u8>,
85}
86
87#[derive(Deserialize, Serialize, Clone, Debug)]
89#[serde(deny_unknown_fields)]
90pub enum SourceParamCfg {
91 #[serde(rename = "wms_proxy")]
93 WmsHttp(WmsHttpSourceParamsCfg),
94 #[serde(rename = "map_service")]
96 WmsFcgi(WmsFcgiSourceParamsCfg),
97 #[serde(rename = "postgis")]
99 Postgis(PostgisSourceParamsCfg),
100 #[serde(rename = "mbtiles")]
102 Mbtiles(MbtilesStoreCfg),
103 #[serde(rename = "pmtiles")]
105 Pmtiles(PmtilesStoreCfg),
106}
107
108#[derive(Deserialize, Serialize, Clone, Debug)]
110#[serde(deny_unknown_fields)]
111pub struct WmsHttpSourceParamsCfg {
112 pub source: String,
114 pub layers: String,
115}
116
117#[derive(Deserialize, Serialize, Clone, Debug)]
119#[serde(deny_unknown_fields)]
120pub struct WmsFcgiSourceParamsCfg {
121 pub project: String,
122 pub suffix: String,
123 pub layers: String,
124 pub params: Option<String>,
126 pub tile_size: Option<NonZeroU16>,
129}
130
131#[derive(Deserialize, Serialize, Clone, Debug)]
133#[serde(deny_unknown_fields)]
134pub struct PostgisSourceParamsCfg {
135 pub datasource: Option<String>,
138 pub extent: Option<ExtentCfg>,
139 pub center: Option<(f64, f64)>,
143 pub start_zoom: Option<u8>,
145 pub attribution: Option<String>,
147 #[serde(default)]
149 pub postgis2: bool,
150 pub diagnostics: Option<TileDiagnosticsCfg>,
152 #[serde(rename = "layer")]
154 pub layers: Vec<VectorLayerCfg>,
155}
156
157#[derive(Deserialize, Serialize, Clone, Debug)]
158#[serde(deny_unknown_fields)]
159pub struct ExtentCfg {
160 pub minx: f64,
161 pub miny: f64,
162 pub maxx: f64,
163 pub maxy: f64,
164}
165
166#[derive(Deserialize, Serialize, Clone, Debug)]
167#[serde(deny_unknown_fields)]
168pub struct TileDiagnosticsCfg {
169 pub reference_size: Option<u64>,
171}
172
173#[derive(Deserialize, Serialize, Clone, Debug)]
175#[serde(deny_unknown_fields)]
176pub struct VectorLayerCfg {
177 pub name: String,
179 pub geometry_field: Option<String>,
181 pub geometry_type: Option<String>,
185 pub srid: Option<i32>,
187 #[serde(default)]
189 pub no_transform: bool,
190 pub fid_field: Option<String>,
192 pub table_name: Option<String>,
194 #[serde(default, rename = "query")]
196 pub queries: Vec<VectorLayerQueryCfg>,
197 pub minzoom: Option<u8>,
199 pub maxzoom: Option<u8>,
201 pub query_limit: Option<u32>,
203 #[serde(default = "default_tile_size")]
205 pub tile_size: u32,
206 pub buffer_size: Option<u32>,
208 #[serde(default)]
212 pub simplify: bool,
213 #[serde(default = "default_tolerance")]
215 pub tolerance: String,
216 #[serde(default)]
220 pub make_valid: bool,
221 #[serde(default)]
223 pub shift_longitude: bool,
224}
225
226fn default_tile_size() -> u32 {
227 4096
228}
229
230const DEFAULT_TOLERANCE: &str = "!pixel_width!/2";
231
232fn default_tolerance() -> String {
233 DEFAULT_TOLERANCE.to_string()
234}
235
236#[derive(Deserialize, Serialize, Clone, Debug)]
237#[serde(deny_unknown_fields)]
238pub struct VectorLayerQueryCfg {
239 pub minzoom: Option<u8>,
241 pub maxzoom: Option<u8>,
243 pub simplify: Option<bool>,
245 pub tolerance: Option<String>,
247 pub sql: Option<String>,
257}
258
259#[derive(Deserialize, Serialize, Clone, Debug)]
261#[serde(deny_unknown_fields)]
262pub struct CacheLimitCfg {
263 #[serde(default)]
264 pub minzoom: u8,
265 pub maxzoom: Option<u8>,
266}
267
268#[derive(Deserialize, Serialize, Clone, Debug)]
270#[serde(deny_unknown_fields)]
271pub struct CacheControlCfg {
272 pub max_age: u64,
274 pub minzoom: Option<u8>,
276 pub maxzoom: Option<u8>,
278}
279
280#[derive(Deserialize, Serialize, Clone, Debug)]
281#[serde(deny_unknown_fields)]
282pub struct TileCacheProviderCfg {
283 pub name: String,
285 pub compression: Option<StoreCompressionCfg>,
287 #[serde(flatten)]
290 pub cache: TileStoreCfg,
291}
292
293#[derive(Deserialize, Serialize, PartialEq, Clone, Debug)]
295pub enum StoreCompressionCfg {
296 None,
299 Gzip,
301 }
304
305#[derive(Deserialize, Serialize, Clone, Debug)]
307#[serde(deny_unknown_fields)]
308pub enum TileStoreCfg {
309 #[serde(rename = "files")]
311 Files(FileStoreCfg),
312 #[serde(rename = "s3")]
314 S3(S3StoreCfg),
315 #[serde(rename = "mbtiles")]
317 Mbtiles(MbtilesStoreCfg),
318 #[serde(rename = "pmtiles")]
320 Pmtiles(PmtilesStoreCfg),
321 #[serde(rename = "nostore")]
323 NoStore,
324}
325
326#[derive(Deserialize, Serialize, Clone, Debug)]
327#[serde(deny_unknown_fields)]
328pub struct FileStoreCfg {
329 pub base_dir: PathBuf,
331}
332
333impl FileStoreCfg {
334 pub fn abs_path(&self) -> PathBuf {
335 app_dir(&self.base_dir)
336 }
337}
338
339#[derive(Deserialize, Serialize, Clone, Debug)]
340#[serde(deny_unknown_fields)]
341pub struct S3StoreCfg {
342 pub path: String,
343 }
347
348#[derive(Deserialize, Serialize, Clone, Debug)]
349#[serde(deny_unknown_fields)]
350pub struct MbtilesStoreCfg {
351 pub path: PathBuf,
352}
353
354impl MbtilesStoreCfg {
355 pub fn abs_path(&self) -> PathBuf {
356 app_dir(&self.path)
357 }
358}
359
360#[derive(Deserialize, Serialize, Clone, Debug)]
361#[serde(deny_unknown_fields)]
362pub struct PmtilesStoreCfg {
363 pub path: PathBuf,
364}
365
366impl PmtilesStoreCfg {
367 pub fn abs_path(&self) -> PathBuf {
368 app_dir(&self.path)
369 }
370}
371
372impl TileStoreCfg {
373 pub fn from_cli_args(cli: &ArgMatches) -> Option<Self> {
374 let Ok(Commands::Seed(args)) = Commands::from_arg_matches(cli) else {
375 return None;
376 };
377 if let Some(path) = &args.tile_path {
378 let cache_cfg = TileStoreCfg::Files(FileStoreCfg {
379 base_dir: path.into(),
380 });
381 Some(cache_cfg)
382 } else if let Some(s3_path) = &args.s3_path {
383 let cache_cfg = TileStoreCfg::S3(S3StoreCfg {
384 path: s3_path.to_string(),
385 });
386 Some(cache_cfg)
387 } else if let Some(path) = &args.mb_path {
388 let cache_cfg = TileStoreCfg::Mbtiles(MbtilesStoreCfg { path: path.into() });
389 Some(cache_cfg)
390 } else if let Some(path) = &args.pm_path {
391 let cache_cfg = TileStoreCfg::Pmtiles(PmtilesStoreCfg { path: path.into() });
392 Some(cache_cfg)
393 } else if args.no_store {
394 Some(TileStoreCfg::NoStore)
395 } else {
396 None
397 }
398 }
399}
400
401impl ServiceConfig for TileServiceCfg {
402 fn initialize(cli: &ArgMatches) -> Result<Self, ConfigError> {
403 let mut cfg: TileServiceCfg = from_config_root_or_exit();
404
405 if let Some(t_rex_config) = cli.get_one::<PathBuf>("t_rex_config") {
407 let t_rex_cfg: t_rex::ApplicationCfg =
408 t_rex::read_config(t_rex_config.to_str().expect("invalid string"))
409 .unwrap_or_else(error_exit);
410 cfg = t_rex_cfg.into();
411 info!("Imported t-rex config:\n{}", cfg.as_toml());
412 }
413
414 if let Some(cache) = TileStoreCfg::from_cli_args(cli) {
415 cfg.tilestores.push(TileCacheProviderCfg {
416 name: "<cli>".to_string(),
417 compression: None,
418 cache,
419 });
420 }
421
422 let file_or_url =
424 if let Ok(CommonCommands::Serve(args)) = CommonCommands::from_arg_matches(cli) {
425 args.file_or_url
426 } else if let Ok(Commands::Seed(args)) = Commands::from_arg_matches(cli) {
427 args.file_or_url
428 } else {
429 None
430 };
431 if let Some(file_or_url) = file_or_url {
432 if let Some(source_cfg) = source_config_from_cli_arg(&file_or_url) {
433 let name = if let Some(name) = Path::new(&file_or_url).file_stem() {
434 name.to_string_lossy().to_string()
435 } else {
436 file_or_url.to_string()
437 };
438 info!("Adding tileset `{name}`");
439 let ts = TileSetCfg {
440 name,
441 tms: Vec::new(),
442 source: source_cfg,
443 cache: None,
444 cache_format: None,
445 cache_limits: None,
446 cache_control: Vec::new(),
447 };
448 cfg.tilesets.push(ts);
449 }
450 }
451 Ok(cfg)
452 }
453}
454
455impl TileServiceCfg {
456 pub fn as_toml(&self) -> String {
457 toml::to_string(&self).unwrap()
458 }
459}
460
461impl From<t_rex::ApplicationCfg> for TileServiceCfg {
462 fn from(t_rex_config: t_rex::ApplicationCfg) -> Self {
463 let re = Regex::new(r#"\) AS "\w+"$"#).expect("re");
464 let datasources = t_rex_config
465 .datasource
466 .into_iter()
467 .filter(|ds| {
468 if ds.path.is_some() {
469 warn!("Skipping GDAL datasource");
470 }
471 ds.dbconn.is_some()
472 })
473 .map(|ds| {
474 let datasource = DatasourceCfg::Postgis(DsPostgisCfg {
475 url: ds.dbconn.expect("dbconn"),
476 });
477 NamedDatasourceCfg {
478 name: ds.name.unwrap_or("default".to_string()),
479 datasource,
480 }
481 })
482 .collect();
483 let grids = if let Some(g) = &t_rex_config.grid.user {
484 warn!("User defined grid has to be configured manually");
485 vec![GridCfg {
486 json: format!("{}.json", g.srid),
487 }]
488 } else {
489 Vec::new()
490 };
491 let tms = if let Some(g) = &t_rex_config.grid.user {
492 format!("{}", g.srid)
493 } else {
494 match &t_rex_config.grid.predefined.as_deref() {
495 Some("wgs84") => "WorldCRS84Quad".to_string(),
496 Some("web_mercator") => "WebMercatorQuad".to_string(),
497 _ => "WebMercatorQuad".to_string(),
498 }
499 };
500 let tilestore = t_rex_config
501 .cache
502 .and_then(|cache| {
503 if let Some(fcache) = cache.file {
504 Some(TileStoreCfg::Files(FileStoreCfg {
505 base_dir: fcache.base.into(),
506 }))
507 } else {
508 None
509 }
510 })
511 .map(|cache| TileCacheProviderCfg {
512 name: "tilecache".to_string(),
513 compression: Some(StoreCompressionCfg::Gzip),
514 cache,
515 });
516 let cache_name = tilestore.as_ref().map(|ts| ts.name.clone());
517 let tilestores = if let Some(ts) = tilestore {
518 vec![ts]
519 } else {
520 Vec::new()
521 };
522 let tilesets = t_rex_config
523 .tilesets
524 .into_iter()
525 .map(|ts| {
526 let dsnames = ts
528 .layers
529 .iter()
530 .map(|l| l.datasource.clone())
531 .collect::<HashSet<_>>()
532 .into_iter()
533 .collect::<Vec<_>>();
534 if dsnames.len() > 1 {
535 warn!(
536 "Please group layers with datasources ({dsnames:?}) into separate tilesets"
537 )
538 }
539 let datasource = dsnames.first().expect("no datasource").clone();
540 let layers = ts
541 .layers
542 .into_iter()
543 .map(|l| {
544 let mut queries = l
545 .query
546 .into_iter()
547 .map(|q| VectorLayerQueryCfg {
548 minzoom: Some(q.minzoom),
549 maxzoom: q.maxzoom,
550 simplify: q.simplify,
551 tolerance: q.tolerance,
552 sql: q.sql,
553 })
554 .collect::<Vec<_>>();
555 let mut table_name = l.table_name.clone();
557 if let Some(table) = &l.table_name {
558 if table.starts_with("(SELECT ") {
559 let sql = Some(re.replace_all(table, ")").to_string());
560 queries.insert(
561 0,
562 VectorLayerQueryCfg {
563 minzoom: l.minzoom,
564 maxzoom: None,
565 simplify: Some(l.simplify),
566 tolerance: Some(l.tolerance.clone()),
567 sql,
568 },
569 );
570 table_name = None;
571 }
572 }
573 VectorLayerCfg {
574 name: l.name,
575 geometry_field: l.geometry_field,
576 geometry_type: l.geometry_type,
577 srid: l.srid,
578 no_transform: l.no_transform,
579 fid_field: l.fid_field,
580 table_name,
581 query_limit: l.query_limit,
582 queries,
583 minzoom: l.minzoom,
584 maxzoom: l.maxzoom,
585 tile_size: l.tile_size,
586 simplify: l.simplify,
587 tolerance: l.tolerance,
588 buffer_size: l.buffer_size,
589 make_valid: l.make_valid,
590 shift_longitude: l.shift_longitude,
591 }
592 })
593 .collect();
594 let pgcfg = PostgisSourceParamsCfg {
595 datasource,
596 extent: ts.extent.map(|ext| ExtentCfg {
597 maxx: ext.maxx,
598 maxy: ext.maxy,
599 minx: ext.minx,
600 miny: ext.miny,
601 }),
602 center: ts.center,
603 start_zoom: ts.start_zoom,
604 attribution: ts.attribution,
605 postgis2: false,
606 diagnostics: None,
607 layers,
608 };
609 TileSetCfg {
610 name: ts.name,
611 tms: vec![TilesetTmsCfg {
612 id: tms.clone(),
613 minzoom: ts.minzoom,
614 maxzoom: ts.maxzoom,
615 }],
616 source: SourceParamCfg::Postgis(pgcfg),
617 cache: cache_name.clone(),
618 cache_format: None,
619 cache_limits: ts.cache_limits.map(|l| CacheLimitCfg {
620 minzoom: l.minzoom,
621 maxzoom: l.maxzoom,
622 }),
623 cache_control: Vec::new(), }
625 })
626 .collect();
627 TileServiceCfg {
628 grids,
629 datasources,
630 tilesets,
631 tilestores,
632 }
633 }
634}
635
636static WORLD_EXTENT: ExtentCfg = ExtentCfg {
637 minx: -180.0,
638 miny: -90.0,
639 maxx: 180.0,
640 maxy: 90.0,
641};
642
643impl PostgisSourceParamsCfg {
644 pub fn attribution(&self) -> String {
645 self.attribution.clone().unwrap_or("".to_string())
646 }
647 pub fn get_extent(&self) -> &ExtentCfg {
648 self.extent.as_ref().unwrap_or(&WORLD_EXTENT)
649 }
650 pub fn get_center(&self) -> (f64, f64) {
651 if self.center.is_none() {
652 let ext = self.get_extent();
653 (
654 ext.maxx - (ext.maxx - ext.minx) / 2.0,
655 ext.maxy - (ext.maxy - ext.miny) / 2.0,
656 )
657 } else {
658 self.center.unwrap()
659 }
660 }
661 pub fn get_start_zoom(&self) -> u8 {
662 self.start_zoom.unwrap_or(2)
663 }
664}
665
666impl VectorLayerCfg {
667 pub fn minzoom(&self) -> u8 {
668 self.minzoom.unwrap_or(
669 self.queries
670 .iter()
671 .filter_map(|q| q.minzoom)
672 .min()
673 .unwrap_or(0),
674 )
675 }
676 pub fn maxzoom(&self, default: u8) -> u8 {
677 self.maxzoom.unwrap_or(
678 self.queries
679 .iter()
680 .map(|q| q.maxzoom.unwrap_or(default))
681 .max()
682 .unwrap_or(default),
683 )
684 }
685 pub fn zoom_steps(&self, tms_cfg: &[TilesetTmsCfg]) -> Vec<u8> {
687 let mut zoom_steps: Vec<u8> = self
688 .queries
689 .iter()
690 .filter(|q| q.sql.is_some())
691 .filter_map(|q| q.minzoom)
692 .chain(tms_cfg.iter().filter_map(|crs| crs.minzoom))
694 .chain(tms_cfg.iter().filter_map(|crs| crs.maxzoom.map(|z| z + 1)))
696 .filter(|z| *z >= self.minzoom())
697 .chain([self.minzoom()])
699 .collect::<HashSet<u8>>()
701 .into_iter()
702 .collect();
703 zoom_steps.sort();
704 zoom_steps
705 }
706 pub fn zoom_step_entry<T>(lookup: &HashMap<u8, T>, zoom: u8) -> Option<&T> {
708 let mut zooms = lookup.keys().cloned().collect::<Vec<_>>();
709 zooms.sort(); let z = zooms.into_iter().rev().find(|z| zoom >= *z);
711 z.as_ref().and_then(|z| lookup.get(z))
712 }
713 fn query_cfg<F>(&self, level: u8, check: F) -> Option<&VectorLayerQueryCfg>
715 where
716 F: Fn(&VectorLayerQueryCfg) -> bool,
717 {
718 let mut queries = self
719 .queries
720 .iter()
721 .map(|q| (q.minzoom.unwrap_or(0), q.maxzoom.unwrap_or(255), q))
722 .collect::<Vec<_>>();
723 queries.sort_by_key(|t| t.0);
724 let query = queries
726 .iter()
727 .rev()
728 .find(|q| level >= q.0 && level <= q.1 && check(q.2));
729 query.map(|q| q.2)
730 }
731 pub fn query(&self, level: u8) -> Option<&String> {
733 let query_cfg = self.query_cfg(level, |q| q.sql.is_some());
734 query_cfg.and_then(|q| q.sql.as_ref())
735 }
736 pub fn simplify(&self, level: u8) -> bool {
738 let query_cfg = self.query_cfg(level, |q| q.simplify.is_some());
739 query_cfg.and_then(|q| q.simplify).unwrap_or(self.simplify)
740 }
741 pub fn tolerance(&self, level: u8) -> &String {
743 let query_cfg = self.query_cfg(level, |q| q.tolerance.is_some());
744 query_cfg
745 .and_then(|q| q.tolerance.as_ref())
746 .unwrap_or(&self.tolerance)
747 }
748}
749
750#[cfg(test)]
984mod tests {
985 use super::*;
986 use toml::Value;
987
988 fn parse_config<'a, T: Deserialize<'a>>(toml: &str) -> Result<T, String> {
989 toml.parse::<Value>()
990 .and_then(|cfg| cfg.try_into::<T>())
991 .map_err(|err| format!("{err}"))
992 }
993
994 #[test]
995 fn zoom_steps() {
996 const CONFIG: &str = r#"
997 [[datasource]]
998 name = "osmdb"
999 [datasource.postgis]
1000 url = "postgres:///osmdb"
1001
1002 [[tileset]]
1003 name = "osm"
1004
1005 [tileset.postgis]
1006 datasource = "osmdb"
1007
1008 [[tileset.postgis.layer]]
1009 geometry_field = "geom"
1010 geometry_type = "POLYGON"
1011 name = "ocean"
1012 #srid = 8857 / 3857
1013
1014 [[tileset.postgis.layer.query]]
1015 minzoom = 0
1016 maxzoom = 2
1017 sql = """SELECT "geom" FROM "eq"."ocean_low""""
1018
1019 [[tileset.postgis.layer.query]]
1020 minzoom = 3
1021 maxzoom = 9
1022 sql = """SELECT "id","geom" FROM "merc"."ocean_low""""
1023
1024 [[tileset.postgis.layer.query]]
1025 minzoom = 10
1026 sql = """SELECT "id","geom" FROM "merc"."ocean""""
1027 "#;
1028 let cfg: TileServiceCfg = parse_config(CONFIG).unwrap();
1029 let SourceParamCfg::Postgis(ref source) = cfg.tilesets[0].source else {
1030 panic!("Wrong tileset source")
1031 };
1032 assert_eq!(source.layers.len(), 1);
1033 assert_eq!(source.layers[0].minzoom(), 0);
1034 assert_eq!(source.layers[0].maxzoom(42), 42);
1035 assert_eq!(source.layers[0].zoom_steps(&[]), vec![0, 3, 10]);
1036 }
1037
1038 #[test]
1039 fn zoom_min_max() {
1040 const CONFIG: &str = r#"
1041 [[tileset]]
1042 name = "osm"
1043
1044 [tileset.postgis]
1045 datasource = "osmdb"
1046
1047 [[tileset.postgis.layer]]
1048 geometry_field = "geom"
1049 geometry_type = "POLYGON"
1050 name = "ocean"
1051
1052 [[tileset.postgis.layer.query]]
1053 minzoom = 3
1054 maxzoom = 9
1055 sql = """SELECT "id","geom" FROM "merc"."ocean_low""""
1056
1057 [[tileset.postgis.layer.query]]
1058 minzoom = 10
1059 maxzoom = 24
1060 sql = """SELECT "id","geom" FROM "merc"."ocean""""
1061 "#;
1062 let cfg: TileServiceCfg = parse_config(CONFIG).unwrap();
1063 let SourceParamCfg::Postgis(ref source) = cfg.tilesets[0].source else {
1064 panic!("Wrong tileset source")
1065 };
1066 assert_eq!(source.layers[0].minzoom(), 3);
1067 assert_eq!(source.layers[0].maxzoom(42), 24);
1068 assert_eq!(source.layers[0].zoom_steps(&[]), vec![3, 10]);
1069 }
1070
1071 #[test]
1072 fn multi_crs_projected() {
1073 const CONFIG: &str = r#"
1074 [[grid]]
1075 json = "EqualEarthGreenwichWGS84Quad.json"
1076
1077 [[tileset]]
1078 name = "tracking"
1079
1080 [[tileset.tms]]
1081 id = "EqualEarthGreenwichWGS84Quad"
1082 maxzoom = 2
1083
1084 [[tileset.tms]]
1085 id = "WebMercatorQuad"
1086 minzoom = 3
1087
1088 [tileset.postgis]
1089 datasource = "tracking"
1090
1091 [[tileset.postgis.layer]]
1092 name = "waypoints"
1093 geometry_field = "geom"
1094 geometry_type = "POINT"
1095 srid = 4326
1096
1097 [[tileset.postgis.layer.query]]
1098 sql = """SELECT id, ts::TEXT, ST_Point(lon, lat, 4326) AS geom FROM gps.gpslog"""
1099 "#;
1100 let cfg: TileServiceCfg = parse_config(CONFIG).unwrap();
1101 let ts = &cfg.tilesets[0];
1102 let SourceParamCfg::Postgis(ref source) = ts.source else {
1103 panic!("Wrong tileset source")
1104 };
1105 assert_eq!(source.layers[0].minzoom(), 0);
1106 assert_eq!(source.layers[0].maxzoom(42), 42);
1107 assert_eq!(source.layers[0].zoom_steps(&[]), vec![0]);
1108 assert_eq!(source.layers[0].zoom_steps(&ts.tms), vec![0, 3]);
1109 }
1110
1111 #[test]
1112 fn multi_crs_unprojected() {
1113 const CONFIG: &str = r#"
1114 [[grid]]
1115 json = "EqualEarthGreenwichWGS84Quad.json"
1116
1117 [[tileset]]
1118 name = "ocean"
1119
1120 [[tileset.tms]]
1121 id = "EqualEarthGreenwichWGS84Quad"
1122 maxzoom = 2
1123
1124 [[tileset.tms]]
1125 id = "WebMercatorQuad"
1126 minzoom = 3
1127
1128 [tileset.postgis]
1129 datasource = "osmdb"
1130
1131 [[tileset.postgis.layer]]
1132 geometry_field = "geom"
1133 geometry_type = "POLYGON"
1134 name = "ocean"
1135 #srid = 8857 / 3857
1136
1137 [[tileset.postgis.layer.query]]
1138 minzoom = 0
1139 maxzoom = 2
1140 sql = """SELECT "geom" FROM "eq"."ocean_low""""
1141
1142 [[tileset.postgis.layer.query]]
1143 minzoom = 3
1144 maxzoom = 9
1145 sql = """SELECT "id","geom" FROM "merc"."ocean_low""""
1146 "#;
1147 let cfg: TileServiceCfg = parse_config(CONFIG).unwrap();
1148 let ts = &cfg.tilesets[0];
1149 let SourceParamCfg::Postgis(ref source) = ts.source else {
1150 panic!("Wrong tileset source")
1151 };
1152 assert_eq!(source.layers[0].minzoom(), 0);
1153 assert_eq!(source.layers[0].maxzoom(42), 9);
1154 assert_eq!(source.layers[0].zoom_steps(&[]), vec![0, 3]);
1155 assert_eq!(source.layers[0].zoom_steps(&ts.tms), vec![0, 3]);
1156 }
1157}