Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,3 @@ jobs:
testJobCreationInputRef
ogc-api-features: true
ogc-api-features-container-tag: 1.9-teamengine-6.0.0-RC2
ogc-api-features-ignore: |-
validateFeaturesWithLimitResponse_NumberMatched
validateFeaturesResponse_NumberMatched
validateFeaturesWithBoundingBoxResponse_NumberMatched
validateFeaturesWithLimitResponse_NumberMatched
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed fields to status database model for OGC API - Processes.
- Consolidate API definition for OGC cite validation.
- Decouple drivers from app state.
- Set default item type of collection as `feature`.
- Define numeric feature id as `u64`.
- Remove default Crs implementation.

## [0.3.0] - 2025-04-05

Expand Down
7 changes: 3 additions & 4 deletions examples/data-loader/src/geojson.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ pub async fn load(args: Args) -> anyhow::Result<()> {
// Create collection
let collection = Collection {
id: args.collection.to_owned(),
item_type: Some("Feature".to_string()),
extent: geojson
.bbox
.map(|bbox| Extent {
Expand All @@ -35,13 +34,13 @@ pub async fn load(args: Args) -> anyhow::Result<()> {
.try_into()
.unwrap_or_else(|_| [-180.0, -90.0, 180.0, 90.0].into()),
],
crs: Crs::default(),
crs: Some(Crs::default2d()),
},
..Default::default()
})
.or_else(|| Some(Extent::default())),
crs: vec![Crs::default(), Crs::from_epsg(3857)],
storage_crs: Some(Crs::default()),
crs: vec![Crs::default2d(), Crs::from_epsg(3857)],
storage_crs: Some(Crs::default2d()),
#[cfg(feature = "stac")]
assets: crate::asset::load_asset_from_path(&args.input).await?,
..Default::default()
Expand Down
4 changes: 2 additions & 2 deletions examples/data-loader/src/ogr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ pub async fn load(mut args: Args) -> Result<(), anyhow::Error> {
let collection = Collection {
id: args.collection.to_owned(),
crs: Vec::from_iter(HashSet::from([
Crs::default(),
Crs::default2d(),
storage_crs.clone(),
Crs::from_epsg(3857),
])),
extent: layer.try_get_extent()?.map(|e| Extent {
spatial: SpatialExtent {
bbox: vec![Bbox::Bbox2D([e.MinX, e.MinY, e.MaxX, e.MaxY])],
crs: storage_crs.to_owned(),
crs: Some(storage_crs.to_owned()),
},
..Default::default()
}),
Expand Down
2 changes: 1 addition & 1 deletion examples/data-loader/src/osm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub async fn load(args: Args) -> Result<(), anyhow::Error> {
// Create collection
let collection = Collection {
id: args.collection.to_owned(),
crs: vec![Crs::default()],
crs: vec![Crs::default2d()],
..Default::default()
};
// db.delete_collection(&collection.id).await?;
Expand Down
10 changes: 5 additions & 5 deletions ogcapi-drivers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,21 @@ pub trait FeatureTransactions: Send + Sync {

async fn read_feature(
&self,
collection: &str,
collection_id: &str,
id: &str,
crs: &Crs,
) -> anyhow::Result<Option<Feature>>;
async fn update_feature(&self, feature: &Feature) -> anyhow::Result<()>;

async fn delete_feature(&self, collection: &str, id: &str) -> anyhow::Result<()>;
async fn delete_feature(&self, collection_id: &str, id: &str) -> anyhow::Result<()>;

async fn list_items(
&self,
collection: &str,
collection_id: &str,
query: &FeatureQuery,
) -> anyhow::Result<FeatureCollection>;

async fn queryables(&self, _collection: &str) -> anyhow::Result<Queryables> {
async fn queryables(&self, _collection_id: &str) -> anyhow::Result<Queryables> {
// Default to nothing is queryable
Ok(Queryables {
queryables: Default::default(),
Expand All @@ -89,7 +89,7 @@ pub trait EdrQuerier: Send + Sync {
collection_id: &str,
query_type: &QueryType,
query: &EdrQuery,
) -> anyhow::Result<FeatureCollection>;
) -> anyhow::Result<(FeatureCollection, Crs)>;
}

/// Trait for `Processes` jobs
Expand Down
9 changes: 7 additions & 2 deletions ogcapi-drivers/src/postgres/collection.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ogcapi_types::common::{Collection, Collections, Query};
use ogcapi_types::common::{Collection, Collections, Crs, Query};

use crate::CollectionTransactions;

Expand Down Expand Up @@ -47,9 +47,14 @@ impl CollectionTransactions for Db {
.execute(&mut *tx)
.await?;

let srid = collection
.storage_crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default2d().as_srid());
sqlx::query("SELECT UpdateGeometrySRID('items', $1, 'geom', $2)")
.bind(&collection.id)
.bind(collection.storage_crs.clone().unwrap_or_default().as_srid())
.bind(srid)
.execute(&mut *tx)
.await?;

Expand Down
85 changes: 66 additions & 19 deletions ogcapi-drivers/src/postgres/edr.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use sqlx::types::Json;

use ogcapi_types::{
common::{Crs, Exception},
edr::{Query, QueryType},
features::{Feature, FeatureCollection},
};
Expand All @@ -16,27 +17,49 @@ impl EdrQuerier for Db {
collection_id: &str,
query_type: &QueryType,
query: &Query,
) -> anyhow::Result<FeatureCollection> {
let srid: i32 = query.crs.as_srid();

let c = self.read_collection(collection_id).await?;
let storage_srid = c.unwrap().storage_crs.unwrap_or_default().as_srid();
) -> anyhow::Result<(FeatureCollection, Crs)> {
let collection = self.read_collection(collection_id).await?;
let storage_srid = match collection {
Some(collection) => match collection.storage_crs.map(|crs| crs.as_srid()) {
Some(srid) => srid,
None => {
sqlx::query_scalar(&format!(
"SELECT Find_SRID('items', '{collection_id}', 'geom')"
))
.fetch_one(&self.pool)
.await?
}
},
None => return Err(Exception::new_from_status(404).into()),
};

let mut geometry_type = query.coords.split('(').next().unwrap().to_uppercase();
geometry_type.retain(|c| !c.is_whitespace());

let spatial_predicate = match &query_type {
let (spatial_predicate, srid) = match &query_type {
QueryType::Position | QueryType::Area | QueryType::Trajectory => {
if geometry_type.ends_with('Z') || geometry_type.ends_with('M') {
format!(
let srid: i32 = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default3d().as_srid());
let predicate = format!(
"ST_3DIntersects(geom, ST_Transform(ST_GeomFromEWKT('SRID={};{}'), {}))",
srid, query.coords, storage_srid
)
);
(predicate, srid)
} else {
format!(
let srid: i32 = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default2d().as_srid());
let predicate = format!(
"ST_Intersects(geom, ST_Transform(ST_GeomFromEWKT('SRID={};{}'), {}))",
srid, query.coords, storage_srid
)
);
(predicate, srid)
}
}
QueryType::Radius => {
Expand All @@ -56,26 +79,49 @@ impl EdrQuerier for Db {
.expect("Failed to parse & convert distance");

if geometry_type.ends_with('Z') || geometry_type.ends_with('M') {
format!(
let srid: i32 = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default3d().as_srid());
let predicate = format!(
"ST_3DDWithin(geom, ST_Transform(ST_GeomFromEWKT('SRID={};{}'), {}))",
srid, query.coords, storage_srid
)
);
(predicate, srid)
} else {
format!(
let srid: i32 = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default2d().as_srid());
let predicate = format!(
"ST_DWithin(ST_Transform(geom, 4326)::geography, ST_Transform(ST_GeomFromEWKT('SRID={};{}'), 4326)::geography, {}, false)",
srid, query.coords, distance
)
);
(predicate, srid)
}
}
QueryType::Cube => {
let bbox: Vec<&str> = query.coords.split(',').collect();
if bbox.len() == 4 {
format!(
let srid: i32 = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default2d().as_srid());
let predicate = format!(
"ST_Intersects(geom, ST_Transform(ST_MakeEnvelope({}, {}), {})",
query.coords, srid, storage_srid
)
);
(predicate, srid)
} else {
format!(
let srid: i32 = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default3d().as_srid());
let predicate = format!(
"ST_3DIntersects(
geom,
ST_Transform(
Expand All @@ -87,7 +133,8 @@ impl EdrQuerier for Db {
)
)",
bbox[0], bbox[1], bbox[2], bbox[3], bbox[4], bbox[5], srid, storage_srid
)
);
(predicate, srid)
}
}
qt => unimplemented!("{qt:?}"),
Expand Down Expand Up @@ -142,6 +189,6 @@ impl EdrQuerier for Db {
let mut fc = FeatureCollection::new(features);
fc.number_matched = Some(number_matched);

Ok(fc)
Ok((fc, Crs::from_srid(srid)))
}
}
49 changes: 35 additions & 14 deletions ogcapi-drivers/src/postgres/feature.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ogcapi_types::{
common::{Authority, Bbox, Crs, Datetime, IntervalDatetime},
common::{Authority, Bbox, Crs, Datetime, Exception, IntervalDatetime},
features::{Feature, FeatureCollection, Query},
};

Expand Down Expand Up @@ -133,25 +133,39 @@ impl FeatureTransactions for Db {

async fn list_items(
&self,
collection: &str,
collection_id: &str,
query: &Query,
) -> anyhow::Result<FeatureCollection> {
let mut where_conditions = vec!["TRUE".to_owned()];

// bbox
if let Some(bbox) = query.bbox.as_ref() {
// crs
let bbox_crs = query.bbox_crs.clone().unwrap_or_else(|| match bbox {
Bbox::Bbox2D(_) => Crs::default2d(),
Bbox::Bbox3D(_) => Crs::default3d(),
});

// coordinate system axis order (OGC and Postgis is lng, lat | EPSG is lat, lng)
let order = match query.bbox_crs.authority {
let order = match bbox_crs.authority {
Authority::OGC => [0, 1, 2, 3],
Authority::EPSG => [1, 0, 3, 2],
};

let c = self.read_collection(collection).await?;
let storage_srid = c
.expect("collection exists")
.storage_crs
.unwrap_or_default()
.as_srid();
let collection = self.read_collection(collection_id).await?;
let storage_srid = match collection {
Some(collection) => match collection.storage_crs.map(|crs| crs.as_srid()) {
Some(srid) => srid,
None => {
sqlx::query_scalar(&format!(
"SELECT Find_SRID('items', '{collection_id}', 'geom')"
))
.fetch_one(&self.pool)
.await?
}
},
None => return Err(Exception::new_from_status(404).into()),
};

// TODO: handle antimeridian (lower > upper on axis 1)
let intersection = match bbox {
Expand All @@ -161,7 +175,7 @@ impl FeatureTransactions for Db {
bbox[order[1]],
bbox[order[2]],
bbox[order[3]],
query.bbox_crs.as_srid()
bbox_crs.as_srid()
),
Bbox::Bbox3D(bbox) => format!(
// FIXME: ensure proper height/box transformation handling
Expand All @@ -181,7 +195,7 @@ impl FeatureTransactions for Db {
x2 = bbox[order[2] + 1],
y2 = bbox[order[3] + 1],
z2 = bbox[5],
srid = query.bbox_crs.as_srid()
srid = bbox_crs.as_srid()
),
};

Expand Down Expand Up @@ -260,20 +274,27 @@ impl FeatureTransactions for Db {
// count
let number_matched: (i64,) = sqlx::query_as(&format!(
r#"
SELECT count(*) FROM items."{collection}"
SELECT count(*) FROM items."{collection_id}"
WHERE {conditions}
"#,
))
.fetch_one(&self.pool)
.await?;

// srid
let srid = query
.crs
.as_ref()
.map(|crs| crs.as_srid())
.unwrap_or_else(|| Crs::default2d().as_srid());

// fetch
let features: Option<sqlx::types::Json<Vec<Feature>>> = sqlx::query_scalar(&format!(
r#"
SELECT array_to_json(array_agg(row_to_json(t)))
FROM (
SELECT {ROWS}
FROM items."{collection}" items JOIN meta.collections meta
FROM items."{collection_id}" items JOIN meta.collections meta
ON items.collection = meta.id
WHERE {conditions}
LIMIT {}
Expand All @@ -285,7 +306,7 @@ impl FeatureTransactions for Db {
.map_or_else(|| String::from("NULL"), |l| l.to_string()),
query.offset.unwrap_or(0)
))
.bind(query.crs.as_srid())
.bind(srid)
.fetch_one(&self.pool)
.await?;

Expand Down
Loading
Loading