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 match self.intersects.clone() {
236 Some(intersects) => {
237 #[cfg(feature = "geo")]
238 {
239 let intersects: geo::Geometry = intersects.try_into().map_err(Box::new)?;
240 item.intersects(&intersects).map_err(Error::from)
241 }
242 #[cfg(not(feature = "geo"))]
243 {
244 Err(Error::FeatureNotEnabled("geo"))
245 }
246 }
247 _ => Ok(true),
248 }
249 }
250
251 /// Converts this search's filter to cql2-json, if set.
252 pub fn into_cql2_json(mut self) -> Result<Search> {
253 self.items = self.items.into_cql2_json()?;
254 Ok(self)
255 }
256}
257
258impl TryFrom<Search> for GetSearch {
259 type Error = Error;
260
261 fn try_from(search: Search) -> Result<GetSearch> {
262 let get_items: GetItems = search.items.try_into()?;
263 let intersects = search
264 .intersects
265 .map(|intersects| serde_json::to_string(&intersects))
266 .transpose()?;
267 let collections = if search.collections.is_empty() {
268 None
269 } else {
270 Some(search.collections.join(","))
271 };
272 let ids = if search.ids.is_empty() {
273 None
274 } else {
275 Some(search.ids.join(","))
276 };
277 Ok(GetSearch {
278 items: get_items,
279 intersects,
280 ids,
281 collections,
282 })
283 }
284}
285
286impl TryFrom<GetSearch> for Search {
287 type Error = Error;
288
289 fn try_from(get_search: GetSearch) -> Result<Search> {
290 let items: Items = get_search.items.try_into()?;
291 let intersects = get_search
292 .intersects
293 .map(|intersects| serde_json::from_str(&intersects))
294 .transpose()?;
295 let collections = get_search
296 .collections
297 .map(|collections| collections.split(',').map(|s| s.to_string()).collect())
298 .unwrap_or_default();
299 let ids = get_search
300 .ids
301 .map(|ids| ids.split(',').map(|s| s.to_string()).collect())
302 .unwrap_or_default();
303 Ok(Search {
304 items,
305 intersects,
306 ids,
307 collections,
308 })
309 }
310}
311
312impl From<Items> for Search {
313 fn from(items: Items) -> Self {
314 Search {
315 items,
316 ..Default::default()
317 }
318 }
319}
320
321impl stac::Fields for Search {
322 fn fields(&self) -> &Map<String, Value> {
323 &self.items.additional_fields
324 }
325 fn fields_mut(&mut self) -> &mut Map<String, Value> {
326 &mut self.items.additional_fields
327 }
328}
329
330impl Deref for Search {
331 type Target = Items;
332 fn deref(&self) -> &Self::Target {
333 &self.items
334 }
335}
336
337impl DerefMut for Search {
338 fn deref_mut(&mut self) -> &mut Self::Target {
339 &mut self.items
340 }
341}