stac_api/
search.rs

1use crate::{Error, Fields, GetItems, Items, Result, Sortby};
2use geojson::Geometry;
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use stac::{Bbox, Item};
6use std::ops::{Deref, DerefMut};
7
8/// The core parameters for STAC search are defined by OAFeat, and STAC adds a few parameters for convenience.
9#[derive(Clone, Default, Debug, Serialize, Deserialize)]
10pub struct Search {
11    /// Many fields are shared with [Items], so we re-use that structure.
12    #[serde(flatten)]
13    pub items: Items,
14
15    /// Searches items by performing intersection between their geometry and provided GeoJSON geometry.
16    ///
17    /// All GeoJSON geometry types must be supported.
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub intersects: Option<Geometry>,
20
21    /// Array of Item ids to return.
22    #[serde(skip_serializing_if = "Vec::is_empty", default)]
23    pub ids: Vec<String>,
24
25    /// Array of one or more Collection IDs that each matching Item must be in.
26    #[serde(skip_serializing_if = "Vec::is_empty", default)]
27    pub collections: Vec<String>,
28}
29
30/// GET parameters for the item search endpoint.
31#[derive(Clone, Default, Debug, Serialize, Deserialize)]
32pub struct GetSearch {
33    /// Many fields are shared with [Items], so we re-use that structure.
34    #[serde(flatten)]
35    pub items: GetItems,
36
37    /// Searches items by performing intersection between their geometry and provided GeoJSON geometry.
38    ///
39    /// All GeoJSON geometry types must be supported.
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub intersects: Option<String>,
42
43    /// Comma-delimited list of Item ids to return.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub ids: Option<String>,
46
47    /// Comma-delimited list of one or more Collection IDs that each matching Item must be in.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub collections: Option<String>,
50}
51
52impl Search {
53    /// Creates a new, empty search.
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// use stac_api::Search;
59    ///
60    /// let search = Search::new();
61    /// ```
62    pub fn new() -> Search {
63        Search::default()
64    }
65
66    /// Sets the ids field of this search.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use stac_api::Search;
72    /// let search = Search::new().ids(vec!["an-id".to_string()]);
73    /// ```
74    pub fn ids(mut self, ids: Vec<String>) -> Search {
75        self.ids = ids;
76        self
77    }
78
79    /// Sets the intersects of this search.
80    pub fn intersects(mut self, intersects: impl Into<Geometry>) -> Search {
81        self.intersects = Some(intersects.into());
82        self
83    }
84
85    /// Sets the collections of this search.
86    pub fn collections(mut self, collections: Vec<String>) -> Search {
87        self.collections = collections;
88        self
89    }
90
91    /// Sets the bbox of this search.
92    pub fn bbox(mut self, bbox: impl Into<Bbox>) -> Search {
93        self.items.bbox = Some(bbox.into());
94        self
95    }
96
97    /// Sets the datetime of this search.
98    pub fn datetime(mut self, datetime: impl ToString) -> Search {
99        self.items.datetime = Some(datetime.to_string());
100        self
101    }
102
103    /// Sets the limit of this search.
104    pub fn limit(mut self, limit: u64) -> Search {
105        self.items.limit = Some(limit);
106        self
107    }
108
109    /// Sets the sortby of this search.
110    pub fn sortby(mut self, sortby: Vec<Sortby>) -> Search {
111        self.items.sortby = sortby;
112        self
113    }
114
115    /// Sets the fields of this search.
116    pub fn fields(mut self, fields: Fields) -> Search {
117        self.items.fields = Some(fields);
118        self
119    }
120
121    /// Returns an error if this search is invalid, e.g. if both bbox and intersects are specified.
122    ///
123    /// Returns the search unchanged if it is valid.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use stac_api::Search;
129    /// use geojson::{Geometry, Value};
130    ///
131    /// let mut search = Search::default();
132    /// search.items.bbox =  Some(vec![-180.0, -90.0, 180.0, 80.0].try_into().unwrap());
133    /// search = search.valid().unwrap();
134    /// search.intersects = Some(Geometry::new(Value::Point(vec![0.0, 0.0])));
135    /// search.valid().unwrap_err();
136    /// ```
137    pub fn valid(mut self) -> Result<Search> {
138        self.items = self.items.valid()?;
139        if self.items.bbox.is_some() & self.intersects.is_some() {
140            Err(Error::SearchHasBboxAndIntersects(Box::new(self.clone())))
141        } else {
142            Ok(self)
143        }
144    }
145
146    /// Returns true if this item matches this search.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use stac::Item;
152    /// use stac_api::Search;
153    ///
154    /// let item = Item::new("an-id");
155    /// assert!(Search::new().matches(&item).unwrap());
156    /// assert!(!Search::new().ids(vec!["not-the-id".to_string()]).matches(&item).unwrap());
157    /// ```
158    pub fn matches(&self, item: &Item) -> Result<bool> {
159        Ok(self.collection_matches(item)
160            & self.id_matches(item)
161            & self.intersects_matches(item)?
162            & self.items.matches(item)?)
163    }
164
165    /// Returns true if this item's collection matches this search.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use stac_api::Search;
171    /// use stac::Item;
172    ///
173    /// let mut search = Search::new();
174    /// let mut item = Item::new("item-id");
175    /// assert!(search.collection_matches(&item));
176    /// search.collections = vec!["collection-id".to_string()];
177    /// assert!(!search.collection_matches(&item));
178    /// item.collection = Some("collection-id".to_string());
179    /// assert!(search.collection_matches(&item));
180    /// item.collection = Some("another-collection-id".to_string());
181    /// assert!(!search.collection_matches(&item));
182    /// ```
183    pub fn collection_matches(&self, item: &Item) -> bool {
184        if self.collections.is_empty() {
185            true
186        } else if let Some(collection) = item.collection.as_ref() {
187            self.collections.contains(collection)
188        } else {
189            false
190        }
191    }
192
193    /// Returns true if this item's id matches this search.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use stac_api::Search;
199    /// use stac::Item;
200    ///
201    /// let mut search = Search::new();
202    /// let mut item = Item::new("item-id");
203    /// assert!(search.id_matches(&item));
204    /// search.ids = vec!["item-id".to_string()];
205    /// assert!(search.id_matches(&item));
206    /// search.ids = vec!["another-id".to_string()];
207    /// assert!(!search.id_matches(&item));
208    /// ```
209    pub fn id_matches(&self, item: &Item) -> bool {
210        self.ids.is_empty() || self.ids.contains(&item.id)
211    }
212
213    /// Returns true if this item's geometry matches this search's intersects.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// # #[cfg(feature = "geo")]
219    /// # {
220    /// use stac_api::Search;
221    /// use stac::Item;
222    /// use geojson::{Geometry, Value};
223    ///
224    /// let mut search = Search::new();
225    /// let mut item = Item::new("item-id");
226    /// assert!(search.intersects_matches(&item).unwrap());
227    /// search.intersects = Some(Geometry::new(Value::Point(vec![-105.1, 41.1])));
228    /// assert!(!search.intersects_matches(&item).unwrap());
229    /// item.set_geometry(Geometry::new(Value::Point(vec![-105.1, 41.1])));
230    /// assert!(search.intersects_matches(&item).unwrap());
231    /// # }
232    /// ```
233    #[allow(unused_variables)]
234    pub fn intersects_matches(&self, item: &Item) -> Result<bool> {
235        if let Some(intersects) = self.intersects.clone() {
236            #[cfg(feature = "geo")]
237            {
238                let intersects: geo::Geometry = intersects.try_into().map_err(Box::new)?;
239                item.intersects(&intersects).map_err(Error::from)
240            }
241            #[cfg(not(feature = "geo"))]
242            {
243                Err(Error::FeatureNotEnabled("geo"))
244            }
245        } else {
246            Ok(true)
247        }
248    }
249
250    /// Converts this search's filter to cql2-json, if set.
251    pub fn into_cql2_json(mut self) -> Result<Search> {
252        self.items = self.items.into_cql2_json()?;
253        Ok(self)
254    }
255}
256
257impl TryFrom<Search> for GetSearch {
258    type Error = Error;
259
260    fn try_from(search: Search) -> Result<GetSearch> {
261        let get_items: GetItems = search.items.try_into()?;
262        let intersects = search
263            .intersects
264            .map(|intersects| serde_json::to_string(&intersects))
265            .transpose()?;
266        let collections = if search.collections.is_empty() {
267            None
268        } else {
269            Some(search.collections.join(","))
270        };
271        let ids = if search.ids.is_empty() {
272            None
273        } else {
274            Some(search.ids.join(","))
275        };
276        Ok(GetSearch {
277            items: get_items,
278            intersects,
279            ids,
280            collections,
281        })
282    }
283}
284
285impl TryFrom<GetSearch> for Search {
286    type Error = Error;
287
288    fn try_from(get_search: GetSearch) -> Result<Search> {
289        let items: Items = get_search.items.try_into()?;
290        let intersects = get_search
291            .intersects
292            .map(|intersects| serde_json::from_str(&intersects))
293            .transpose()?;
294        let collections = get_search
295            .collections
296            .map(|collections| collections.split(',').map(|s| s.to_string()).collect())
297            .unwrap_or_default();
298        let ids = get_search
299            .ids
300            .map(|ids| ids.split(',').map(|s| s.to_string()).collect())
301            .unwrap_or_default();
302        Ok(Search {
303            items,
304            intersects,
305            ids,
306            collections,
307        })
308    }
309}
310
311impl From<Items> for Search {
312    fn from(items: Items) -> Self {
313        Search {
314            items,
315            ..Default::default()
316        }
317    }
318}
319
320impl stac::Fields for Search {
321    fn fields(&self) -> &Map<String, Value> {
322        &self.items.additional_fields
323    }
324    fn fields_mut(&mut self) -> &mut Map<String, Value> {
325        &mut self.items.additional_fields
326    }
327}
328
329impl Deref for Search {
330    type Target = Items;
331    fn deref(&self) -> &Self::Target {
332        &self.items
333    }
334}
335
336impl DerefMut for Search {
337    fn deref_mut(&mut self) -> &mut Self::Target {
338        &mut self.items
339    }
340}