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}