oxigdal_stac/lib.rs
1//! STAC (SpatioTemporal Asset Catalog) support for OxiGDAL.
2//!
3//! This crate provides Pure Rust implementation of the STAC specification,
4//! enabling cloud-native geospatial workflows with catalogs, collections,
5//! and items.
6//!
7//! # Features
8//!
9//! - **STAC 1.0.0 Specification**: Full compliance with STAC 1.0.0
10//! - **Core Models**: Catalog, Collection, Item, and Asset
11//! - **Extensions**: EO (Electro-Optical) and Projection extensions
12//! - **STAC API Client**: Async HTTP client for searching STAC APIs
13//! - **Builder Patterns**: Fluent APIs for easy object creation
14//! - **Validation**: Comprehensive validation of STAC objects
15//! - **Pure Rust**: No C/Fortran dependencies
16//!
17//! # Example
18//!
19//! ```rust
20//! use oxigdal_stac::{ItemBuilder, Asset};
21//! use chrono::Utc;
22//!
23//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
24//! // Create a STAC Item
25//! let item = ItemBuilder::new("my-item")
26//! .datetime(Utc::now())
27//! .bbox(-122.5, 37.5, -122.0, 38.0)
28//! .simple_asset("visual", "https://example.com/image.tif")
29//! .build()?;
30//!
31//! // Serialize to JSON
32//! let json = serde_json::to_string_pretty(&item)?;
33//! println!("{}", json);
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! # STAC API Search
39//!
40//! ```rust,no_run
41//! # #[cfg(all(feature = "reqwest", feature = "async"))]
42//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
43//! use oxigdal_stac::StacClient;
44//!
45//! // Create a STAC API client
46//! let client = StacClient::new("https://earth-search.aws.element84.com/v1")?;
47//!
48//! // Search for items
49//! let results = client.search()
50//! .collections(vec!["sentinel-2-l2a"])
51//! .bbox([-122.5, 37.5, -122.0, 38.0])
52//! .limit(10)
53//! .execute()
54//! .await?;
55//!
56//! println!("Found {} items", results.features.len());
57//! # Ok(())
58//! # }
59//! ```
60
61#![deny(missing_docs)]
62#![cfg_attr(not(feature = "std"), no_std)]
63// Allow collapsible matches for clear STAC field handling
64#![allow(clippy::collapsible_match)]
65#![allow(clippy::collapsible_if)]
66// Allow dead code for future STAC extensions
67#![allow(dead_code)]
68// Allow stripping prefix manually for URL handling
69#![allow(clippy::manual_strip)]
70// Allow method name conflicts for builder patterns
71#![allow(clippy::should_implement_trait)]
72
73// Re-export common types
74pub use chrono;
75pub use geojson;
76pub use serde_json;
77
78// Core modules
79pub mod aggregation;
80pub mod api;
81pub mod asset;
82pub mod builder;
83pub mod catalog;
84pub mod client;
85pub mod collection;
86pub mod collection_aggregation;
87pub mod cql2;
88pub mod error;
89pub mod extensions;
90pub mod item;
91pub mod transaction;
92
93#[cfg(feature = "reqwest")]
94pub mod pagination;
95
96#[cfg(feature = "reqwest")]
97pub mod search;
98
99// Public exports
100pub use aggregation::{
101 Aggregation, AggregationRequest, AggregationResponse, AggregationResult, Bucket,
102};
103pub use api::{
104 CollectionSummary, CollectionsList, ConformanceDeclaration, FieldsSpec, ItemCollection,
105 LandingPage, SearchContext as ApiSearchContext, SearchRequest as ApiSearchRequest,
106 SortDirection as ApiSortDirection, SortField,
107};
108pub use asset::{Asset, media_types, roles};
109pub use builder::{CatalogBuilder, CollectionBuilder, ItemBuilder};
110pub use catalog::Catalog;
111pub use collection::{Collection, Extent, Provider, SpatialExtent, TemporalExtent};
112pub use collection_aggregation::{CollectionAggregator, CollectionStats, NumericStats};
113pub use cql2::{Cql2Filter, Cql2Operand};
114pub use error::{Result, StacError};
115pub use extensions::{
116 eo::{Band, CommonBandName, EoExtension},
117 projection::{ProjectionExtension, epsg_codes},
118 sar::{FrequencyBand, ObservationDirection, Polarization, SarExtension},
119 scientific::{Publication, ScientificExtension},
120 timestamps::TimestampsExtension,
121 version::VersionExtension,
122 view::ViewExtension,
123};
124pub use item::{Item, ItemProperties, Link, link_rel};
125pub use transaction::{StacItemStore, TransactionOp, TransactionResult};
126
127#[cfg(feature = "reqwest")]
128pub use pagination::{CursorPagination, PagePagination, Paginator, TokenPagination};
129
130#[cfg(feature = "reqwest")]
131pub use search::{SearchContext, SearchParams, SearchResults, SortBy, SortDirection, StacClient};
132
133/// STAC version supported by this crate.
134pub const STAC_VERSION: &str = "1.0.0";
135
136/// Helper function to create a bounding box from coordinates.
137///
138/// # Arguments
139///
140/// * `west` - Western longitude
141/// * `south` - Southern latitude
142/// * `east` - Eastern longitude
143/// * `north` - Northern latitude
144///
145/// # Returns
146///
147/// Bounding box vector [west, south, east, north]
148///
149/// # Example
150///
151/// ```
152/// use oxigdal_stac::bbox;
153///
154/// let bbox = bbox(-122.5, 37.5, -122.0, 38.0);
155/// assert_eq!(bbox, vec![-122.5, 37.5, -122.0, 38.0]);
156/// ```
157pub fn bbox(west: f64, south: f64, east: f64, north: f64) -> Vec<f64> {
158 vec![west, south, east, north]
159}
160
161/// Helper function to create a GeoJSON Point geometry.
162///
163/// # Arguments
164///
165/// * `lon` - Longitude
166/// * `lat` - Latitude
167///
168/// # Returns
169///
170/// GeoJSON Point geometry
171///
172/// # Example
173///
174/// ```
175/// use oxigdal_stac::point_geometry;
176///
177/// let geometry = point_geometry(-122.0, 37.0);
178/// assert_eq!(geometry.value, geojson::GeometryValue::new_point([-122.0, 37.0]));
179/// ```
180pub fn point_geometry(lon: f64, lat: f64) -> geojson::Geometry {
181 geojson::Geometry::new_point([lon, lat])
182}
183
184/// Helper function to create a GeoJSON Polygon geometry from a bounding box.
185///
186/// # Arguments
187///
188/// * `west` - Western longitude
189/// * `south` - Southern latitude
190/// * `east` - Eastern longitude
191/// * `north` - Northern latitude
192///
193/// # Returns
194///
195/// GeoJSON Polygon geometry
196///
197/// # Example
198///
199/// ```
200/// use oxigdal_stac::bbox_to_polygon;
201///
202/// let geometry = bbox_to_polygon(-122.5, 37.5, -122.0, 38.0);
203/// match geometry.value {
204/// geojson::GeometryValue::Polygon { coordinates: _ } => (),
205/// _ => panic!("Expected Polygon"),
206/// }
207/// ```
208pub fn bbox_to_polygon(west: f64, south: f64, east: f64, north: f64) -> geojson::Geometry {
209 let polygon = vec![vec![
210 vec![west, south],
211 vec![east, south],
212 vec![east, north],
213 vec![west, north],
214 vec![west, south], // Close the ring
215 ]];
216 geojson::Geometry::new_polygon(polygon)
217}
218
219#[cfg(test)]
220#[allow(clippy::panic)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_bbox_helper() {
226 let bbox = bbox(-122.5, 37.5, -122.0, 38.0);
227 assert_eq!(bbox, vec![-122.5, 37.5, -122.0, 38.0]);
228 }
229
230 #[test]
231 fn test_point_geometry() {
232 let geometry = point_geometry(-122.0, 37.0);
233 assert_eq!(
234 geometry.value,
235 geojson::GeometryValue::new_point([-122.0, 37.0])
236 );
237 }
238
239 #[test]
240 fn test_bbox_to_polygon() {
241 let geometry = bbox_to_polygon(-122.5, 37.5, -122.0, 38.0);
242 match geometry.value {
243 geojson::GeometryValue::Polygon {
244 coordinates: coords,
245 } => {
246 assert_eq!(coords.len(), 1);
247 assert_eq!(coords[0].len(), 5); // 4 corners + close
248 }
249 _ => panic!("Expected Polygon"),
250 }
251 }
252
253 #[test]
254 fn test_stac_version() {
255 assert_eq!(STAC_VERSION, "1.0.0");
256 }
257}