diff --git a/martin-tile-utils/src/lib.rs b/martin-tile-utils/src/lib.rs index 9dd4008c1..a14f1a322 100644 --- a/martin-tile-utils/src/lib.rs +++ b/martin-tile-utils/src/lib.rs @@ -8,6 +8,9 @@ use std::fmt::{Display, Formatter, Result}; /// circumference of the earth in meters pub const EARTH_CIRCUMFERENCE: f64 = 40_075_016.685_578_5; +/// circumference of the earth in degrees +pub const EARTH_CIRCUMFERENCE_DEGREES: u32 = 360; + /// radius of the earth in meters pub const EARTH_RADIUS: f64 = EARTH_CIRCUMFERENCE / 2.0 / PI; diff --git a/martin/src/pg/query_tables.rs b/martin/src/pg/query_tables.rs index 1efa2ce2c..440ad9065 100644 --- a/martin/src/pg/query_tables.rs +++ b/martin/src/pg/query_tables.rs @@ -1,3 +1,4 @@ +use martin_tile_utils::EARTH_CIRCUMFERENCE_DEGREES; use std::collections::HashMap; use futures::pin_mut; @@ -171,19 +172,32 @@ pub async fn table_to_query( let extent = info.extent.unwrap_or(DEFAULT_EXTENT); let buffer = info.buffer.unwrap_or(DEFAULT_BUFFER); + let margin = f64::from(buffer) / f64::from(extent); + // When calculating the bounding box to search within, a few considerations must be made when + // using a margin. The ST_TileEnvelope margin parameter is for use with SRID 3857. + // For SRID 4326, ST_Expand is used and provided with SRID 4326 specific units (degrees). + // If the table uses a non-standard SRID, it will fall back to existing behavior. + // + // For more context, if SRID 4326 were to be used with ST_TileEnvelope and margin + // parameter, the resultant bounding box for tiles on the antimeridian would be calculated + // incorrectly. For example, with a margin of 2 units, the antimeridian edge would transform + // from -180 to +178. This results in a bbox that stretches from the easternmost edge of a tile + // (plus margin) around the map to the westernmost edge of the tile (minus margin). The + // resulting bbox covers none of the original tile. In contrast, for this example, ST_Expand + // will result in a westernmost edge (minus margin) of -182. let bbox_search = if buffer == 0 { - "ST_TileEnvelope($1::integer, $2::integer, $3::integer)".to_string() - } else if pool.supports_tile_margin() { - let margin = f64::from(buffer) / f64::from(extent); - format!("ST_TileEnvelope($1::integer, $2::integer, $3::integer, margin => {margin})") + format!("ST_Transform(ST_TileEnvelope($1::integer, $2::integer, $3::integer), {srid})") + } else if pool.supports_tile_margin() && srid == 3857 { + format!( + "ST_Transform(ST_TileEnvelope($1::integer, $2::integer, $3::integer, margin => {margin}), {srid})" + ) + } else if srid == 4326 { + format!( + "ST_Expand(ST_Transform(ST_TileEnvelope($1::integer, $2::integer, $3::integer), {srid}), ({margin} * {EARTH_CIRCUMFERENCE_DEGREES}) / 2^$1::integer)" + ) } else { - // TODO: we should use ST_Expand here, but it may require a bit more math work, - // so might not be worth it as it is only used for PostGIS < v3.1. - // v3.1 has been out for 2+ years (december 2020) - // let val = EARTH_CIRCUMFERENCE * buffer as f64 / extent as f64; - // format!("ST_Expand(ST_TileEnvelope($1::integer, $2::integer, $3::integer), {val}/2^$1::integer)") - "ST_TileEnvelope($1::integer, $2::integer, $3::integer)".to_string() + format!("ST_Transform(ST_TileEnvelope($1::integer, $2::integer, $3::integer), {srid})") }; let limit_clause = max_feature_count.map_or(String::new(), |v| format!("LIMIT {v}")); @@ -204,7 +218,7 @@ FROM ( FROM {schema}.{table} WHERE - {geometry_column} && ST_Transform({bbox_search}, {srid}) + {geometry_column} && {bbox_search} {limit_clause} ) AS tile; " diff --git a/martin/tests/pg_server_test.rs b/martin/tests/pg_server_test.rs index 77cb4d895..8f8b4b446 100644 --- a/martin/tests/pg_server_test.rs +++ b/martin/tests/pg_server_test.rs @@ -66,6 +66,9 @@ postgres: MixPoints: content_type: application/x-protobuf description: a description from comment on table + antimeridian: + content_type: application/x-protobuf + description: public.antimeridian.geom auto_table: content_type: application/x-protobuf description: autodetect.auto_table.geom diff --git a/martin/tests/pg_table_source_test.rs b/martin/tests/pg_table_source_test.rs index 36dc4e949..361c10938 100644 --- a/martin/tests/pg_table_source_test.rs +++ b/martin/tests/pg_table_source_test.rs @@ -26,6 +26,9 @@ async fn table_source() { MixPoints: content_type: application/x-protobuf description: a description from comment on table + antimeridian: + content_type: application/x-protobuf + description: public.antimeridian.geom auto_table: content_type: application/x-protobuf description: autodetect.auto_table.geom @@ -124,6 +127,22 @@ async fn table_source() { properties: gid: int4 "); + + let source3 = table(&mock, "points3857"); + assert_yaml_snapshot!(source3, @r" + schema: public + table: points3857 + srid: 3857 + geometry_column: geom + bounds: + - -161.40590777554058 + - -81.50727021609012 + - 172.51549126768532 + - 84.2440187164111 + geometry_type: POINT + properties: + gid: int4 + "); } #[actix_rt::test] diff --git a/tests/expected/auto/antimeridian_4_0_4.pbf b/tests/expected/auto/antimeridian_4_0_4.pbf new file mode 100644 index 000000000..1f1fb480d Binary files /dev/null and b/tests/expected/auto/antimeridian_4_0_4.pbf differ diff --git a/tests/expected/auto/antimeridian_4_0_4.pbf.geojson b/tests/expected/auto/antimeridian_4_0_4.pbf.geojson new file mode 100644 index 000000000..cec58e965 --- /dev/null +++ b/tests/expected/auto/antimeridian_4_0_4.pbf.geojson @@ -0,0 +1,40 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + 3641, + 2200 + ], + [ + 3641, + 1448 + ], + [ + 2731, + 1448 + ], + [ + 2731, + 2200 + ], + [ + 3641, + 2200 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "gid": 3, + "source_mvt_layer": "antimeridian" + }, + "type": "Feature" + } + ], + "name": "merged", + "type": "FeatureCollection" +} diff --git a/tests/expected/auto/antimeridian_4_0_4.pbf.headers b/tests/expected/auto/antimeridian_4_0_4.pbf.headers new file mode 100644 index 000000000..4856bae77 --- /dev/null +++ b/tests/expected/auto/antimeridian_4_0_4.pbf.headers @@ -0,0 +1,5 @@ +content-encoding: gzip +content-length: 79 +content-type: application/x-protobuf +etag: "171363189529507097744074158008402788299" +vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers diff --git a/tests/expected/auto/antimeridian_4_0_5.pbf b/tests/expected/auto/antimeridian_4_0_5.pbf new file mode 100644 index 000000000..4e5eb3b05 Binary files /dev/null and b/tests/expected/auto/antimeridian_4_0_5.pbf differ diff --git a/tests/expected/auto/antimeridian_4_0_5.pbf.geojson b/tests/expected/auto/antimeridian_4_0_5.pbf.geojson new file mode 100644 index 000000000..d1c950322 --- /dev/null +++ b/tests/expected/auto/antimeridian_4_0_5.pbf.geojson @@ -0,0 +1,74 @@ +{ + "features": [ + { + "geometry": { + "coordinates": [ + [ + [ + 546, + 2636 + ], + [ + 546, + 3228 + ], + [ + 1456, + 3228 + ], + [ + 1456, + 2636 + ], + [ + 546, + 2636 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "gid": 1, + "source_mvt_layer": "antimeridian" + }, + "type": "Feature" + }, + { + "geometry": { + "coordinates": [ + [ + [ + 546, + 2636 + ], + [ + 546, + 3228 + ], + [ + 4096, + 3228 + ], + [ + 4096, + 2636 + ], + [ + 546, + 2636 + ] + ] + ], + "type": "Polygon" + }, + "properties": { + "gid": 2, + "source_mvt_layer": "antimeridian" + }, + "type": "Feature" + } + ], + "name": "merged", + "type": "FeatureCollection" +} diff --git a/tests/expected/auto/antimeridian_4_0_5.pbf.headers b/tests/expected/auto/antimeridian_4_0_5.pbf.headers new file mode 100644 index 000000000..1b1ece7cb --- /dev/null +++ b/tests/expected/auto/antimeridian_4_0_5.pbf.headers @@ -0,0 +1,5 @@ +content-encoding: gzip +content-length: 109 +content-type: application/x-protobuf +etag: "11410412592260844908993902889337130111" +vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers diff --git a/tests/expected/auto/catalog_auto.json b/tests/expected/auto/catalog_auto.json index b659b5188..a3c3d4775 100644 --- a/tests/expected/auto/catalog_auto.json +++ b/tests/expected/auto/catalog_auto.json @@ -48,6 +48,10 @@ "content_type": "application/x-protobuf", "description": "a description from comment on table" }, + "antimeridian": { + "content_type": "application/x-protobuf", + "description": "public.antimeridian.geom" + }, "auto_table": { "content_type": "application/x-protobuf", "description": "autodetect.auto_table.geom" diff --git a/tests/expected/auto/save_config.yaml b/tests/expected/auto/save_config.yaml index b132bfe83..3fc6ef13f 100644 --- a/tests/expected/auto/save_config.yaml +++ b/tests/expected/auto/save_config.yaml @@ -32,6 +32,19 @@ postgres: properties: Gid: int4 TABLE: text + antimeridian: + schema: public + table: antimeridian + srid: 4326 + geometry_column: geom + bounds: + - -182.0 + - 51.0 + - -160.0 + - 62.0 + geometry_type: GEOMETRY + properties: + gid: int4 auto_table: schema: autodetect table: auto_table diff --git a/tests/expected/martin-cp/flat-with-hash_save_config.yaml b/tests/expected/martin-cp/flat-with-hash_save_config.yaml index 00e3e0047..69392ec5b 100644 --- a/tests/expected/martin-cp/flat-with-hash_save_config.yaml +++ b/tests/expected/martin-cp/flat-with-hash_save_config.yaml @@ -31,6 +31,19 @@ postgres: properties: Gid: int4 TABLE: text + antimeridian: + schema: public + table: antimeridian + srid: 4326 + geometry_column: geom + bounds: + - -182.0 + - 51.0 + - -160.0 + - 62.0 + geometry_type: GEOMETRY + properties: + gid: int4 auto_table: schema: autodetect table: auto_table diff --git a/tests/expected/martin-cp/flat_save_config.yaml b/tests/expected/martin-cp/flat_save_config.yaml index 00e3e0047..69392ec5b 100644 --- a/tests/expected/martin-cp/flat_save_config.yaml +++ b/tests/expected/martin-cp/flat_save_config.yaml @@ -31,6 +31,19 @@ postgres: properties: Gid: int4 TABLE: text + antimeridian: + schema: public + table: antimeridian + srid: 4326 + geometry_column: geom + bounds: + - -182.0 + - 51.0 + - -160.0 + - 62.0 + geometry_type: GEOMETRY + properties: + gid: int4 auto_table: schema: autodetect table: auto_table diff --git a/tests/expected/martin-cp/normalized_save_config.yaml b/tests/expected/martin-cp/normalized_save_config.yaml index 00e3e0047..69392ec5b 100644 --- a/tests/expected/martin-cp/normalized_save_config.yaml +++ b/tests/expected/martin-cp/normalized_save_config.yaml @@ -31,6 +31,19 @@ postgres: properties: Gid: int4 TABLE: text + antimeridian: + schema: public + table: antimeridian + srid: 4326 + geometry_column: geom + bounds: + - -182.0 + - 51.0 + - -160.0 + - 62.0 + geometry_type: GEOMETRY + properties: + gid: int4 auto_table: schema: autodetect table: auto_table diff --git a/tests/fixtures/tables/antimeridian.sql b/tests/fixtures/tables/antimeridian.sql new file mode 100644 index 000000000..1f4321dde --- /dev/null +++ b/tests/fixtures/tables/antimeridian.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS antimeridian; + +CREATE TABLE antimeridian +( + gid serial PRIMARY KEY, + geom GEOMETRY (GEOMETRY, 4326) +); + +INSERT INTO antimeridian (geom) VALUES ( + GEOMFROMEWKT('SRID=4326;POLYGON((-177 51, -172 51, -172 53, -177 53, -177 51))') +); +INSERT INTO antimeridian (geom) VALUES ( + GEOMFROMEWKT('SRID=4326;POLYGON((-182 51, -177 51, -177 53, -182 53, -182 51))') +); +INSERT INTO antimeridian (geom) VALUES ( + GEOMFROMEWKT('SRID=4326;POLYGON ((-160 60, -165 60, -165 62, -160 62, -160 60))') +); + +CREATE INDEX ON antimeridian USING gist (geom); diff --git a/tests/test.sh b/tests/test.sh index fd2bd9284..01a9ce2a4 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -374,6 +374,10 @@ test_png rgba_u8_nodata_1_0_0 rgba_u8_nodata/1/0/0 >&2 echo "***** Test server response for table source with empty SRID *****" test_pbf points_empty_srid_0_0_0 points_empty_srid/0/0/0 +>&2 echo "***** Test server response for table source with antimeridian geometries *****" +test_pbf antimeridian_4_0_4 antimeridian/4/0/4 +test_pbf antimeridian_4_0_5 antimeridian/4/0/5 + >&2 echo "***** Test server response for comments *****" test_jsn tbl_comment MixPoints test_jsn fnc_comment function_Mixed_Name