feat: add bounds to tilejson endpoints (#260) (h/t @jaspervercnocke)

This commit is contained in:
Stepan Kuzmin 2021-10-10 12:09:56 +03:00 committed by GitHub
parent d22cc7772b
commit 40b0a0c26a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 130 additions and 11 deletions

View File

@ -97,6 +97,7 @@ jobs:
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/table_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/points1_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/points2_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/points3857_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/function_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/function_source_query_params.sql
env:

View File

@ -32,6 +32,7 @@ jobs:
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/table_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/points1_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/points2_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/points3857_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/function_source.sql
psql -h $POSTGRES_HOST -p $POSTGRES_PORT -U postgres -d test -f tests/fixtures/function_source_query_params.sql
env:

12
Cargo.lock generated
View File

@ -1246,6 +1246,7 @@ dependencies = [
"log",
"native-tls",
"num_cpus",
"postgis",
"postgres",
"postgres-native-tls",
"postgres-protocol",
@ -1641,6 +1642,17 @@ dependencies = [
"plotters-backend",
]
[[package]]
name = "postgis"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52406590b7a682cadd0f0339c43905eb323568e84a2e97e855ef92645e0ec09"
dependencies = [
"byteorder",
"bytes 1.1.0",
"postgres-types",
]
[[package]]
name = "postgres"
version = "0.19.2"

View File

@ -29,6 +29,7 @@ num_cpus = "1.13"
postgres = { version = "0.19.1", features = ["with-time-0_2", "with-uuid-0_8", "with-serde_json-1"] }
postgres-native-tls = "0.5.0"
postgres-protocol = "0.6.2"
postgis = "0.9.0"
r2d2 = "0.8"
r2d2_postgres = "0.18"
semver = "1.0"

View File

@ -16,6 +16,7 @@ fn mock_table_source(schema: &str, table: &str) -> TableSource {
table: table.to_owned(),
id_column: None,
geometry_column: "geom".to_owned(),
bounds: None,
srid: 3857,
extent: Some(4096),
buffer: Some(64),

View File

@ -47,6 +47,21 @@ impl CompositeSource {
format!("{} {}", bounds_cte, tile_query)
}
pub fn get_bounds(&self) -> Option<Vec<f32>> {
self.table_sources
.iter()
.filter_map(|table_source| table_source.bounds.as_ref())
.map(|bounds| bounds.to_vec())
.reduce(|a, b| {
vec![
if a[0] < b[0] { a[0] } else { b[0] },
if a[1] < b[1] { a[1] } else { b[1] },
if a[2] > b[2] { a[2] } else { b[2] },
if a[3] > b[3] { a[3] } else { b[3] },
]
})
}
}
impl Source for CompositeSource {
@ -60,6 +75,10 @@ impl Source for CompositeSource {
tilejson_builder.scheme("xyz");
tilejson_builder.name(&self.id);
if let Some(bounds) = self.get_bounds() {
tilejson_builder.bounds(bounds);
};
Ok(tilejson_builder.finalize())
}

View File

@ -19,7 +19,8 @@ pub fn mock_table_sources() -> Option<TableSources> {
table: "table_source".to_owned(),
id_column: None,
geometry_column: "geom".to_owned(),
srid: 3857,
bounds: Some(vec![-180.0, -90.0, 180.0, 90.0]),
srid: 4326,
extent: Some(4096),
buffer: Some(64),
clip_geom: Some(true),
@ -33,7 +34,8 @@ pub fn mock_table_sources() -> Option<TableSources> {
table: "points1".to_owned(),
id_column: None,
geometry_column: "geom".to_owned(),
srid: 3857,
bounds: Some(vec![-180.0, -90.0, 180.0, 90.0]),
srid: 4326,
extent: Some(4096),
buffer: Some(64),
clip_geom: Some(true),
@ -47,6 +49,22 @@ pub fn mock_table_sources() -> Option<TableSources> {
table: "points2".to_owned(),
id_column: None,
geometry_column: "geom".to_owned(),
bounds: Some(vec![-180.0, -90.0, 180.0, 90.0]),
srid: 4326,
extent: Some(4096),
buffer: Some(64),
clip_geom: Some(true),
geometry_type: None,
properties: HashMap::new(),
};
let table_source3857 = TableSource {
id: "public.points3857".to_owned(),
schema: "public".to_owned(),
table: "points3857".to_owned(),
id_column: None,
geometry_column: "geom".to_owned(),
bounds: Some(vec![-180.0, -90.0, 180.0, 90.0]),
srid: 3857,
extent: Some(4096),
buffer: Some(64),
@ -59,6 +77,7 @@ pub fn mock_table_sources() -> Option<TableSources> {
table_sources.insert("public.table_source".to_owned(), Box::new(source));
table_sources.insert("public.points1".to_owned(), Box::new(table_source1));
table_sources.insert("public.points2".to_owned(), Box::new(table_source2));
table_sources.insert("public.points3857".to_owned(), Box::new(table_source3857));
Some(table_sources)
}

4
src/scripts/get_bounds.sql Executable file
View File

@ -0,0 +1,4 @@
SELECT
ST_Transform (ST_SetSRID (ST_Extent ({geometry_column}), {srid}), 4326) AS bounds
FROM
{id}

View File

@ -6,7 +6,7 @@ use tilejson::{TileJSON, TileJSONBuilder};
use crate::db::Connection;
use crate::source::{Query, Source, Tile, Xyz};
use crate::utils::{get_bounds_cte, get_srid_bounds, json_to_hashmap, prettify_error, tilebbox};
use crate::utils;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TableSource {
@ -16,6 +16,7 @@ pub struct TableSource {
pub id_column: Option<String>,
pub geometry_column: String,
pub srid: u32,
pub bounds: Option<Vec<f32>>,
pub extent: Option<u32>,
pub buffer: Option<u32>,
pub clip_geom: Option<bool>,
@ -27,7 +28,7 @@ pub type TableSources = HashMap<String, Box<TableSource>>;
impl TableSource {
pub fn get_geom_query(&self, xyz: &Xyz) -> String {
let mercator_bounds = tilebbox(xyz);
let mercator_bounds = utils::tilebbox(xyz);
let properties = if self.properties.is_empty() {
"".to_string()
@ -73,8 +74,8 @@ impl TableSource {
}
pub fn build_tile_query(&self, xyz: &Xyz) -> String {
let srid_bounds = get_srid_bounds(self.srid, xyz);
let bounds_cte = get_bounds_cte(srid_bounds);
let srid_bounds = utils::get_srid_bounds(self.srid, xyz);
let bounds_cte = utils::get_bounds_cte(srid_bounds);
let tile_query = self.get_tile_query(xyz);
format!("{} {}", bounds_cte, tile_query)
@ -92,6 +93,10 @@ impl Source for TableSource {
tilejson_builder.scheme("xyz");
tilejson_builder.name(&self.id);
if let Some(bounds) = &self.bounds {
tilejson_builder.bounds(bounds.to_vec());
};
Ok(tilejson_builder.finalize())
}
@ -106,7 +111,7 @@ impl Source for TableSource {
let tile: Tile = conn
.query_one(tile_query.as_str(), &[])
.map(|row| row.get("st_asmvt"))
.map_err(prettify_error("Can't get table source tile"))?;
.map_err(utils::prettify_error("Can't get table source tile"))?;
Ok(tile)
}
@ -121,7 +126,7 @@ pub fn get_table_sources(conn: &mut Connection) -> Result<TableSources, io::Erro
let rows = conn
.query(include_str!("scripts/get_table_sources.sql"), &[])
.map_err(prettify_error("Can't get table sources"))?;
.map_err(utils::prettify_error("Can't get table sources"))?;
for row in &rows {
let schema: String = row.get("f_table_schema");
@ -142,7 +147,16 @@ pub fn get_table_sources(conn: &mut Connection) -> Result<TableSources, io::Erro
continue;
}
let properties = json_to_hashmap(&row.get("properties"));
let bounds_query = utils::get_source_bounds(&id, srid as u32, &geometry_column);
let bounds: Option<Vec<f32>> = conn
.query_one(bounds_query.as_str(), &[])
.map(|row| row.get("bounds"))
.ok()
.flatten()
.and_then(utils::polygon_to_bbox);
let properties = utils::json_to_hashmap(&row.get("properties"));
let source = TableSource {
id: id.to_string(),
@ -150,6 +164,7 @@ pub fn get_table_sources(conn: &mut Connection) -> Result<TableSources, io::Erro
table,
id_column: None,
geometry_column,
bounds,
srid: srid as u32,
extent: Some(DEFAULT_EXTENT),
buffer: Some(DEFAULT_BUFFER),

View File

@ -1,6 +1,7 @@
use std::collections::HashMap;
use crate::source::{Query, Xyz};
use postgis::{ewkb, LineString, Point, Polygon};
use postgres::types::Json;
use serde_json::Value;
@ -66,3 +67,28 @@ pub fn get_srid_bounds(srid: u32, xyz: &Xyz) -> String {
mercator_bounds = tilebbox(xyz),
)
}
pub fn get_source_bounds(id: &str, srid: u32, geometry_column: &str) -> String {
format!(
include_str!("scripts/get_bounds.sql"),
id = id,
srid = srid,
geometry_column = geometry_column,
)
}
pub fn polygon_to_bbox(polygon: ewkb::Polygon) -> Option<Vec<f32>> {
polygon.rings().next().and_then(|linestring| {
let mut points = linestring.points();
if let (Some(bottom_left), Some(top_right)) = (points.next(), points.nth(1)) {
Some(vec![
bottom_left.x() as f32,
bottom_left.y() as f32,
top_right.x() as f32,
top_right.y() as f32,
])
} else {
None
}
})
}

19
tests/fixtures/points3857_source.sql vendored Normal file
View File

@ -0,0 +1,19 @@
CREATE TABLE points3857(gid SERIAL PRIMARY KEY, geom GEOMETRY(POINT, 3857));
INSERT INTO points3857
SELECT
generate_series(1, 10000) as id,
(
ST_DUMP(
ST_GENERATEPOINTS(
ST_TRANSFORM(
ST_GEOMFROMTEXT('POLYGON ((-179 89, 179 89, 179 -89, -179 -89, -179 89))', 4326),
3857
),
10000
)
)
).geom;
CREATE INDEX ON points3857 USING GIST(geom);
CLUSTER points3857_geom_idx ON points3857;

View File

@ -14,3 +14,4 @@ psql --dbname="$POSTGRES_DB" -f /fixtures/function_source_query_params.sql
psql --dbname="$POSTGRES_DB" -f /fixtures/points1_source.sql
psql --dbname="$POSTGRES_DB" -f /fixtures/points2_source.sql
psql --dbname="$POSTGRES_DB" -f /fixtures/points3857_source.sql

View File

@ -102,7 +102,7 @@ async fn test_get_composite_source_ok() {
assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
let req = test::TestRequest::get()
.uri("/public.points1,public.points2.json")
.uri("/public.points1,public.points2,public.points3857.json")
.to_request();
let response = test::call_service(&mut app, req).await;
@ -124,7 +124,7 @@ async fn test_get_composite_source_tile_ok() {
assert_eq!(response.status(), http::StatusCode::NOT_FOUND);
let req = test::TestRequest::get()
.uri("/public.points1,public.points2/0/0/0.pbf")
.uri("/public.points1,public.points2,public.points3857/0/0/0.pbf")
.to_request();
let response = test::call_service(&mut app, req).await;