Skip to content

Commit

Permalink
feat: add gdal
Browse files Browse the repository at this point in the history
  • Loading branch information
gadomski committed Apr 11, 2024
1 parent cda0b00 commit ab7aeaf
Show file tree
Hide file tree
Showing 14 changed files with 530 additions and 11 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,30 @@ jobs:
- "-p stac-async"
- "-p stac-cli"
- "-p stac-validate"
steps:
- uses: actions/checkout@v4
- name: Set up Rust cache
uses: Swatinem/rust-cache@v2
- name: Test
run: cargo test ${{ matrix.args }}
ubuntu-with-gdal:
runs-on: ubuntu-latest
strategy:
matrix:
args:
- "-p stac -p stac-cli -F gdal"
- "--all-features"
steps:
- uses: actions/checkout@v4
- name: Set up Rust cache
uses: Swatinem/rust-cache@v2
- name: Install GDAL
run: |
sudo apt-get update
sudo apt-get install libgdal-dev
- name: Test
run: cargo test ${{ matrix.args }}

windows:
runs-on: windows-latest
steps:
Expand Down
3 changes: 3 additions & 0 deletions stac-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ license = "MIT OR Apache-2.0"
keywords = ["geospatial", "stac", "metadata", "geo", "raster"]
categories = ["science", "data-structures"]

[features]
gdal = ["stac/gdal"]

[dependencies]
clap = { version = "4", features = ["derive"] }
console = "0.15"
Expand Down
20 changes: 19 additions & 1 deletion stac-cli/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub enum Command {
/// Use compact representation for the output.
#[arg(short, long)]
compact: bool,

/// Don't use GDAL for item creation.
///
/// Automatically set to true if this crate is compiled without GDAL.
#[arg(long)]
disable_gdal: bool,
},

/// Searches a STAC API.
Expand Down Expand Up @@ -143,9 +149,21 @@ impl Command {
role,
allow_relative_paths,
compact,
mut disable_gdal,
} => {
let id = id.unwrap_or_else(|| infer_id(&href));
crate::commands::item(id, href, key, role, allow_relative_paths, compact)
if !cfg!(feature = "gdal") {
disable_gdal = true;
}
crate::commands::item(
id,
href,
key,
role,
allow_relative_paths,
compact,
disable_gdal,
)
}
Search {
href,
Expand Down
2 changes: 2 additions & 0 deletions stac-cli/src/commands/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ pub fn item(
roles: Vec<String>,
allow_relative_paths: bool,
compact: bool,
disable_gdal: bool,
) -> Result<()> {
let mut asset = Asset::new(href);
asset.roles = roles;
let item = Builder::new(id)
.asset(key, asset)
.canonicalize_paths(!allow_relative_paths)
.enable_gdal(!disable_gdal)
.into_item()?;
if compact {
println!("{}", serde_json::to_string(&item)?);
Expand Down
5 changes: 5 additions & 0 deletions stac/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ keywords = ["geospatial", "stac", "metadata", "geo", "raster"]
categories = ["science", "data-structures"]

[features]
gdal = ["dep:gdal", "dep:log"]
geo = ["dep:geo", "dep:geojson"]
reqwest = ["dep:reqwest"]
schemars = ["dep:schemars"]

[dependencies]
chrono = "0.4"
gdal = { version = "0.16", optional = true, features = [
"bindgen",
] } # we use bindgen b/c the gdal crate doesn't support GDAL 3.8 as of this writing
geo = { version = "0.28", optional = true }
geojson = { version = "0.24", optional = true }
log = { version = "0.4", optional = true }
reqwest = { version = "0.12", optional = true, features = ["json", "blocking"] }
schemars = { version = "0.8", optional = true }
serde = { version = "1", features = ["derive"] }
Expand Down
Binary file added stac/assets/dataset_geo.tif
Binary file not shown.
76 changes: 76 additions & 0 deletions stac/src/bounds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/// Two-dimensional bounds.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bounds {
/// Minimum x value.
pub xmin: f64,
/// Minimum y value.
pub ymin: f64,
/// Maximum x value.
pub xmax: f64,
/// Maximum y value.
pub ymax: f64,
}

impl Bounds {
/// Creates a new bounds object.
///
/// # Examples
///
/// ```
/// use stac::Bounds;
/// let bounds = Bounds::new(1., 2., 3., 4.);
/// ```
pub fn new(xmin: f64, ymin: f64, xmax: f64, ymax: f64) -> Bounds {
Bounds {
xmin,
ymin,
xmax,
ymax,
}
}

/// Returns true if the minimum bound values are smaller than the maximum.
///
/// This doesn't currently handle antimeridian-crossing bounds.
///
/// # Examples
///
/// ```
/// use stac::Bounds;
/// let bounds = Bounds::default();
/// assert!(!bounds.is_valid());
/// let bounds = Bounds::new(1., 2., 3., 4.);
/// assert!(bounds.is_valid());
/// ```
pub fn is_valid(&self) -> bool {
self.xmin < self.xmax && self.ymin < self.ymax
}

/// Updates these bounds with another bounds' values.
///
/// # Examples
///
/// ```
/// use stac::Bounds;
/// let mut bounds = Bounds::new(1., 1., 2., 2.);
/// bounds.update(Bounds::new(0., 0., 1.5, 1.5));
/// assert_eq!(bounds, Bounds::new(0., 0., 2., 2.));
/// ```
pub fn update(&mut self, other: Bounds) {
self.xmin = self.xmin.min(other.xmin);
self.ymin = self.ymin.min(other.ymin);
self.xmax = self.xmax.max(other.xmax);
self.ymax = self.ymax.max(other.ymax);
}
}

impl Default for Bounds {
fn default() -> Self {
Bounds {
xmin: f64::MAX,
ymin: f64::MAX,
xmax: f64::MIN,
ymax: f64::MIN,
}
}
}
13 changes: 13 additions & 0 deletions stac/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ pub enum Error {
#[error(transparent)]
ChronoParse(#[from] chrono::ParseError),

/// [gdal::errors::GdalError]
#[cfg(feature = "gdal")]
#[error(transparent)]
GdalError(#[from] gdal::errors::GdalError),

/// GDAL is not enabled.
#[error("gdal is not enabled")]
GdalNotEnabled,

/// [geojson::Error]
#[cfg(feature = "geo")]
#[error(transparent)]
Expand Down Expand Up @@ -77,6 +86,10 @@ pub enum Error {
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),

/// [std::num::TryFromIntError]
#[error(transparent)]
TryFromInt(#[from] std::num::TryFromIntError),

/// Returned when the `type` field of a STAC object does not equal `"Feature"`, `"Catalog"`, or `"Collection"`.
#[error("unknown \"type\": {0}")]
UnknownType(String),
Expand Down
29 changes: 25 additions & 4 deletions stac/src/extensions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
//! assert!(item.has_extension::<Projection>());
//!
//! // Get extension information
//! let mut projection: Projection = item.extension().unwrap();
//! let mut projection: Projection = item.extension().unwrap().unwrap();
//! println!("epsg: {}", projection.epsg.unwrap());
//!
//! // Set extension information
Expand Down Expand Up @@ -121,16 +121,36 @@ pub trait Extensions: Fields {

/// Gets an extension's data.
///
/// Returns `Ok(None)` if the object doesn't have the given extension.
///
/// # Examples
///
/// ```
/// use stac::{Item, extensions::{Projection, Extensions}};
/// let item: Item = stac::read("data/extensions-collection/proj-example/proj-example.json").unwrap();
/// let projection: Projection = item.extension().unwrap();
/// let projection: Projection = item.extension().unwrap().unwrap();
/// assert_eq!(projection.epsg.unwrap(), 32614);
/// ```
fn extension<E: Extension>(&self) -> Result<E> {
self.fields_with_prefix(E::PREFIX)
fn extension<E: Extension>(&self) -> Result<Option<E>> {
if self.has_extension::<E>() {
self.fields_with_prefix(E::PREFIX).map(|v| Some(v))
} else {
Ok(None)
}
}

/// Adds an extension's identifer to this object.
///
/// # Examples
///
/// ```
/// use stac::{Item, extensions::{Projection, Extensions}};
/// let mut item = Item::new("an-id");
/// item.add_extension::<Projection>();
/// ```
fn add_extension<E: Extension>(&mut self) {
self.extensions_mut().push(E::IDENTIFIER.to_string());
self.extensions_mut().dedup();
}

/// Sets an extension's data and adds its schema to this object's `extensions`.
Expand All @@ -148,6 +168,7 @@ pub trait Extensions: Fields {
fn set_extension<E: Extension>(&mut self, extension: E) -> Result<()> {
self.remove_extension::<E>();
self.extensions_mut().push(E::IDENTIFIER.to_string());
self.extensions_mut().dedup();
self.set_fields_with_prefix(E::PREFIX, extension)
}

Expand Down
74 changes: 72 additions & 2 deletions stac/src/extensions/projection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use serde_json::{Map, Value};
use super::Extension;

/// The projection extension fields.
#[derive(Debug, Serialize, Deserialize, Default)]
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Clone)]
pub struct Projection {
/// EPSG code of the datasource
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -45,7 +45,7 @@ pub struct Projection {
}

/// This object represents the centroid of the Item Geometry.
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Centroid {
/// The latitude of the centroid.
pub lat: f64,
Expand All @@ -54,6 +54,76 @@ pub struct Centroid {
pub lon: f64,
}

impl Projection {
/// Returns this projection's bounds in WGS84.
///
/// Requires one of the crs fields to be set (epsg, wkt2, or projjson) as well as a bbox.
///
/// # Examples
///
/// ```
/// use stac::extensions::Projection;
/// let projection = Projection {
/// epsg: Some(32621),
/// bbox: Some(vec![
/// 373185.0,
/// 8019284.949381611,
/// 639014.9492102272,
/// 8286015.0
/// ]),
/// ..Default::default()
/// };
/// let bounds = projection.wgs84_bounds().unwrap().unwrap();
/// ```
#[cfg(feature = "gdal")]
pub fn wgs84_bounds(&self) -> crate::Result<Option<crate::Bounds>> {
use gdal::spatial_ref::{CoordTransform, SpatialRef};

if let Some(bbox) = self.bbox.as_ref() {
if bbox.len() != 4 {
return Ok(None);
}
if let Some(spatial_ref) = self.spatial_ref()? {
let wgs84 = SpatialRef::from_epsg(4326)?;
let coord_transform = CoordTransform::new(&spatial_ref, &wgs84)?;
let bounds =
coord_transform.transform_bounds(&[bbox[0], bbox[1], bbox[2], bbox[3]], 21)?;
let round = |n: f64| (n * 10_000_000.).round() / 10_000_000.;
Ok(Some(crate::Bounds::new(
round(bounds[0]),
round(bounds[1]),
round(bounds[2]),
round(bounds[3]),
)))
} else {
Ok(None)
}
} else {
Ok(None)
}
}

#[cfg(feature = "gdal")]
fn spatial_ref(&self) -> crate::Result<Option<gdal::spatial_ref::SpatialRef>> {
use crate::Error;
use gdal::spatial_ref::SpatialRef;

if let Some(epsg) = self.epsg {
SpatialRef::from_epsg(epsg.try_into()?)
.map(Some)
.map_err(Error::from)
} else if let Some(wkt) = self.wkt2.as_ref() {
SpatialRef::from_wkt(&wkt).map(Some).map_err(Error::from)
} else if let Some(projjson) = self.projjson.clone() {
SpatialRef::from_definition(&serde_json::to_string(&Value::Object(projjson))?)
.map(Some)
.map_err(Error::from)
} else {
Ok(None)
}
}
}

impl Extension for Projection {
const IDENTIFIER: &'static str =
"https://stac-extensions.github.io/projection/v1.1.0/schema.json";
Expand Down
Loading

0 comments on commit ab7aeaf

Please sign in to comment.