diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d311c96a..ebfc85c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 83250044..8154db40 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -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: diff --git a/Cargo.lock b/Cargo.lock index f1cf2c71..429b3420 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 01476c5b..0f21d2b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/benches/sources.rs b/benches/sources.rs index 3ee442d4..08d65e33 100644 --- a/benches/sources.rs +++ b/benches/sources.rs @@ -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), diff --git a/src/composite_source.rs b/src/composite_source.rs index cb43b9b1..b08ab49b 100644 --- a/src/composite_source.rs +++ b/src/composite_source.rs @@ -47,6 +47,21 @@ impl CompositeSource { format!("{} {}", bounds_cte, tile_query) } + + pub fn get_bounds(&self) -> Option> { + 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()) } diff --git a/src/dev.rs b/src/dev.rs index a5614a84..517b7513 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -19,7 +19,8 @@ pub fn mock_table_sources() -> Option { 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 { 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 { 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 { 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) } diff --git a/src/scripts/get_bounds.sql b/src/scripts/get_bounds.sql new file mode 100755 index 00000000..660470bd --- /dev/null +++ b/src/scripts/get_bounds.sql @@ -0,0 +1,4 @@ +SELECT + ST_Transform (ST_SetSRID (ST_Extent ({geometry_column}), {srid}), 4326) AS bounds +FROM + {id} diff --git a/src/table_source.rs b/src/table_source.rs index 806eec23..1d2ed934 100644 --- a/src/table_source.rs +++ b/src/table_source.rs @@ -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, pub geometry_column: String, pub srid: u32, + pub bounds: Option>, pub extent: Option, pub buffer: Option, pub clip_geom: Option, @@ -27,7 +28,7 @@ pub type TableSources = HashMap>; 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 Result> = 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 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> { + 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 + } + }) +} diff --git a/tests/fixtures/points3857_source.sql b/tests/fixtures/points3857_source.sql new file mode 100644 index 00000000..b95f3826 --- /dev/null +++ b/tests/fixtures/points3857_source.sql @@ -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; \ No newline at end of file diff --git a/tests/initdb-martin.sh b/tests/initdb-martin.sh index ece7a467..29427d08 100755 --- a/tests/initdb-martin.sh +++ b/tests/initdb-martin.sh @@ -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 diff --git a/tests/server_test.rs b/tests/server_test.rs index e0c9dc1c..cb937cb1 100644 --- a/tests/server_test.rs +++ b/tests/server_test.rs @@ -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;