Fix merging multi-source tilejson (#653)

Improve merging of multiple tilejsons

Fix #652
This commit is contained in:
Yuri Astrakhan 2023-05-02 04:25:06 -04:00 committed by GitHub
parent 169223a705
commit 9583f32edb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 212 additions and 37 deletions

View File

@ -21,7 +21,7 @@ use itertools::Itertools;
use log::{debug, error}; use log::{debug, error};
use martin_tile_utils::{Encoding, Format, TileInfo}; use martin_tile_utils::{Encoding, Format, TileInfo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tilejson::TileJSON; use tilejson::{tilejson, TileJSON};
use crate::source::{Source, Sources, UrlQuery, Xyz}; use crate::source::{Source, Sources, UrlQuery, Xyz};
use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT};
@ -223,42 +223,95 @@ fn get_tiles_url(scheme: &str, host: &str, query_string: &str, tiles_path: &str)
} }
fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON {
let mut tilejson = sources if sources.len() == 1 {
.into_iter() let mut tj = sources[0].get_tilejson();
.map(Source::get_tilejson) tj.tiles = vec![tiles_url];
.reduce(|mut accum, tj| { return tj;
if let Some(minzoom) = tj.minzoom {
if let Some(a) = accum.minzoom {
if a > minzoom {
accum.minzoom = tj.minzoom;
} }
let mut attributions = vec![];
let mut descriptions = vec![];
let mut names = vec![];
let mut result = tilejson! {
tiles: vec![tiles_url],
};
for src in sources {
let tj = src.get_tilejson();
if let Some(vector_layers) = tj.vector_layers {
if let Some(ref mut a) = result.vector_layers {
a.extend(vector_layers);
} else { } else {
accum.minzoom = tj.minzoom; result.vector_layers = Some(vector_layers);
} }
} }
if let Some(maxzoom) = tj.maxzoom {
if let Some(a) = accum.maxzoom { if let Some(v) = tj.attribution {
if a < maxzoom { if !attributions.contains(&v) {
accum.maxzoom = tj.maxzoom; attributions.push(v);
}
} else {
accum.maxzoom = tj.maxzoom;
} }
} }
if let Some(bounds) = tj.bounds { if let Some(bounds) = tj.bounds {
if let Some(a) = accum.bounds { if let Some(a) = result.bounds {
accum.bounds = Some(a + bounds); result.bounds = Some(a + bounds);
} else { } else {
accum.bounds = tj.bounds; result.bounds = tj.bounds;
} }
} }
accum if result.center.is_none() {
}) // Use first found center. Averaging multiple centers might create a center in the middle of nowhere.
.expect("nonempty sources iter"); result.center = tj.center;
}
tilejson.tiles.push(tiles_url); if let Some(v) = tj.description {
tilejson if !descriptions.contains(&v) {
descriptions.push(v);
}
}
if let Some(maxzoom) = tj.maxzoom {
if let Some(a) = result.maxzoom {
if a < maxzoom {
result.maxzoom = tj.maxzoom;
}
} else {
result.maxzoom = tj.maxzoom;
}
}
if let Some(minzoom) = tj.minzoom {
if let Some(a) = result.minzoom {
if a > minzoom {
result.minzoom = tj.minzoom;
}
} else {
result.minzoom = tj.minzoom;
}
}
if let Some(name) = tj.name {
if !names.contains(&name) {
names.push(name);
}
}
}
if !attributions.is_empty() {
result.attribution = Some(attributions.join("\n"));
}
if !descriptions.is_empty() {
result.description = Some(descriptions.join("\n"));
}
if !names.is_empty() {
result.name = Some(names.join(","));
}
result
} }
#[route("/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")] #[route("/{source_ids}/{z}/{x}/{y}", method = "GET", method = "HEAD")]
@ -456,3 +509,113 @@ fn parse_x_rewrite_url(header: &HeaderValue) -> Option<String> {
.and_then(|header| header.parse::<Uri>().ok()) .and_then(|header| header.parse::<Uri>().ok())
.map(|uri| uri.path().to_owned()) .map(|uri| uri.path().to_owned())
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::source::{Source, Tile};
use crate::utils;
use async_trait::async_trait;
use std::collections::HashMap;
use tilejson::{tilejson, Bounds, VectorLayer};
#[derive(Debug, Clone)]
struct TestSource {
tj: TileJSON,
}
#[async_trait]
impl Source for TestSource {
fn get_tilejson(&self) -> TileJSON {
self.tj.clone()
}
fn get_tile_info(&self) -> TileInfo {
unimplemented!()
}
fn clone_source(&self) -> Box<dyn Source> {
unimplemented!()
}
fn is_valid_zoom(&self, _zoom: u8) -> bool {
unimplemented!()
}
fn support_url_query(&self) -> bool {
unimplemented!()
}
async fn get_tile(
&self,
_xyz: &Xyz,
_url_query: &Option<UrlQuery>,
) -> Result<Tile, utils::Error> {
unimplemented!()
}
}
#[test]
fn test_merge_tilejson() {
let url = "http://localhost:8888/foo/{z}/{x}/{y}".to_string();
let src1 = TestSource {
tj: tilejson! {
tiles: vec![],
name: "layer1".to_string(),
minzoom: 5,
maxzoom: 10,
bounds: Bounds::new(-10.0, -20.0, 10.0, 20.0),
vector_layers: vec![
VectorLayer::new("layer1".to_string(),
HashMap::from([
("a".to_string(), "x1".to_string()),
]))
],
},
};
let tj = merge_tilejson(vec![&src1], url.clone());
assert_eq!(
TileJSON {
tiles: vec![url.clone()],
..src1.tj.clone()
},
tj
);
let src2 = TestSource {
tj: tilejson! {
tiles: vec![],
name: "layer2".to_string(),
minzoom: 7,
maxzoom: 12,
bounds: Bounds::new(-20.0, -5.0, 5.0, 50.0),
vector_layers: vec![
VectorLayer::new("layer2".to_string(),
HashMap::from([
("b".to_string(), "x2".to_string()),
]))
],
},
};
let tj = merge_tilejson(vec![&src1, &src2], url.clone());
assert_eq!(tj.tiles, vec![url]);
assert_eq!(tj.name, Some("layer1,layer2".to_string()));
assert_eq!(tj.minzoom, Some(5));
assert_eq!(tj.maxzoom, Some(12));
assert_eq!(tj.bounds, Some(Bounds::new(-20.0, -20.0, 10.0, 50.0)));
assert_eq!(
tj.vector_layers,
Some(vec![
VectorLayer::new(
"layer1".to_string(),
HashMap::from([("a".to_string(), "x1".to_string())])
),
VectorLayer::new(
"layer2".to_string(),
HashMap::from([("b".to_string(), "x2".to_string())])
),
])
);
}
}

View File

@ -9,8 +9,20 @@
"fields": { "fields": {
"gid": "int4" "gid": "int4"
} }
},
{
"id": "points1",
"fields": {
"gid": "int4"
}
},
{
"id": "points2",
"fields": {
"gid": "int4"
}
} }
], ],
"description": "public.table_source.geom", "description": "public.table_source.geom\npublic.points1.geom\npublic.points2.geom",
"name": "table_source" "name": "table_source,points1,points2"
} }