bbox_feature_server/
endpoints.rs

1use crate::filter_params::FilterParams;
2use crate::inventory::Inventory;
3use crate::service::FeatureService;
4use actix_web::{web, Error, HttpRequest, HttpResponse};
5use bbox_core::api::OgcApiInventory;
6use bbox_core::endpoints::absurl;
7use bbox_core::ogcapi::{ApiLink, CoreCollections};
8use bbox_core::service::ServiceEndpoints;
9use bbox_core::templates::{create_env_embedded, html_accepted, render_endpoint};
10use minijinja::{context, Environment};
11use once_cell::sync::Lazy;
12use std::collections::HashMap;
13
14/// the feature collections in the dataset
15async fn collections(
16    _ogcapi: web::Data<OgcApiInventory>,
17    inventory: web::Data<Inventory>,
18    req: HttpRequest,
19) -> Result<HttpResponse, Error> {
20    let collections = CoreCollections {
21        links: vec![ApiLink {
22            href: absurl(&req, "/collections.json"),
23            rel: Some("self".to_string()),
24            type_: Some("application/json".to_string()),
25            title: Some("this document".to_string()),
26            hreflang: None,
27            length: None,
28        }],
29        //TODO: include also collections from other services
30        collections: inventory.collections(), //TODO: convert urls with absurl (?)
31    };
32    if html_accepted(&req).await {
33        render_endpoint(
34            &TEMPLATES,
35            "collections.html",
36            context!(cur_menu=>"Collections", collections => &collections),
37        )
38        .await
39    } else {
40        Ok(HttpResponse::Ok().json(collections))
41    }
42}
43
44/// describe the feature collection with id `collectionId`
45async fn collection(
46    inventory: web::Data<Inventory>,
47    req: HttpRequest,
48    collection_id: web::Path<String>,
49) -> Result<HttpResponse, Error> {
50    if let Some(collection) = inventory.core_collection(&collection_id) {
51        if html_accepted(&req).await {
52            render_endpoint(
53                &TEMPLATES,
54                "collection.html",
55                context!(cur_menu=>"Collections", collection => &collection),
56            )
57            .await
58        } else {
59            Ok(HttpResponse::Ok().json(collection))
60        }
61    } else {
62        Ok(HttpResponse::NotFound().finish())
63    }
64}
65
66/// describe the queryables available in the collection with id `collectionId`
67async fn queryables(
68    inventory: web::Data<Inventory>,
69    req: HttpRequest,
70    collection_id: web::Path<String>,
71) -> Result<HttpResponse, Error> {
72    if let Some(queryables) = inventory.collection_queryables(&collection_id).await {
73        if html_accepted(&req).await {
74            render_endpoint(
75                &TEMPLATES,
76                "queryables.html",
77                context!(cur_menu=>"Collections", queryables => &queryables),
78            )
79            .await
80        } else {
81            Ok(HttpResponse::Ok()
82                .content_type("application/geo+json")
83                .json(queryables))
84        }
85    } else {
86        Ok(HttpResponse::NotFound().finish())
87    }
88}
89
90/// fetch features
91async fn features(
92    inventory: web::Data<Inventory>,
93    req: HttpRequest,
94    collection_id: web::Path<String>,
95) -> Result<HttpResponse, Error> {
96    if let Some(collection) = inventory.core_collection(&collection_id) {
97        let mut filters: HashMap<String, String> =
98            match serde_urlencoded::from_str::<Vec<(String, String)>>(req.query_string()) {
99                Ok(f) => f
100                    .iter()
101                    .map(|k| (k.0.to_lowercase(), k.1.to_owned()))
102                    .collect(),
103                Err(_e) => return Ok(HttpResponse::BadRequest().finish()),
104            };
105
106        let bbox = filters.remove("bbox");
107        let datetime = filters.remove("datetime");
108
109        let offset = if let Some(offset_str) = filters.get("offset") {
110            match offset_str.parse::<u32>() {
111                Ok(o) => {
112                    filters.remove("offset");
113                    Some(o)
114                }
115                Err(_e) => return Ok(HttpResponse::BadRequest().finish()),
116            }
117        } else {
118            None
119        };
120        let limit = if let Some(limit_str) = filters.get("limit") {
121            match limit_str.parse::<u32>() {
122                Ok(o) => {
123                    filters.remove("limit");
124                    Some(o)
125                }
126                Err(_e) => return Ok(HttpResponse::BadRequest().finish()),
127            }
128        } else {
129            None
130        };
131
132        let fp = FilterParams {
133            offset,
134            limit,
135            bbox,
136            datetime,
137            filters,
138        };
139
140        if let Some(features) = inventory.collection_items(&collection_id, &fp).await {
141            if html_accepted(&req).await {
142                render_endpoint(
143                    &TEMPLATES,
144                    "features.html",
145                    context!(cur_menu=>"Collections", collection => &collection, features => &features),
146                ).await
147            } else {
148                Ok(HttpResponse::Ok()
149                    .content_type("application/geo+json")
150                    .json(features))
151            }
152        } else {
153            Ok(HttpResponse::NotFound().finish())
154        }
155    } else {
156        Ok(HttpResponse::NotFound().finish())
157    }
158}
159
160/// fetch a single feature
161async fn feature(
162    inventory: web::Data<Inventory>,
163    req: HttpRequest,
164    path: web::Path<(String, String)>,
165) -> Result<HttpResponse, Error> {
166    let (collection_id, feature_id) = path.into_inner();
167    if let Some(collection) = inventory.core_collection(&collection_id) {
168        if let Some(feature) = inventory.collection_item(&collection_id, &feature_id).await {
169            if html_accepted(&req).await {
170                render_endpoint(
171                    &TEMPLATES,
172                    "feature.html",
173                    context!(cur_menu=>"Collections", collection => &collection, feature => &feature),
174                ).await
175            } else {
176                Ok(HttpResponse::Ok()
177                    .content_type("application/geo+json")
178                    .json(feature))
179            }
180        } else {
181            Ok(HttpResponse::NotFound().finish())
182        }
183    } else {
184        Ok(HttpResponse::NotFound().finish())
185    }
186}
187
188#[cfg(feature = "html")]
189#[derive(rust_embed::RustEmbed)]
190#[folder = "templates/"]
191struct Templates;
192
193#[cfg(not(feature = "html"))]
194type Templates = bbox_core::templates::NoTemplates;
195
196static TEMPLATES: Lazy<Environment<'static>> = Lazy::new(create_env_embedded::<Templates>);
197
198impl ServiceEndpoints for FeatureService {
199    fn register_endpoints(&self, cfg: &mut web::ServiceConfig) {
200        cfg.app_data(web::Data::new(self.inventory.clone()))
201            .service(web::resource("/collections").route(web::get().to(collections)))
202            .service(web::resource("/collections.json").route(web::get().to(collections)))
203            .service(
204                web::resource("/collections/{collectionId}.json").route(web::get().to(collection)),
205            )
206            .service(web::resource("/collections/{collectionId}").route(web::get().to(collection)))
207            .service(
208                web::resource("/collections/{collectionId}/queryables.json")
209                    .route(web::get().to(queryables)),
210            )
211            .service(
212                web::resource("/collections/{collectionId}/queryables")
213                    .route(web::get().to(queryables)),
214            )
215            .service(
216                web::resource("/collections/{collectionId}/items").route(web::get().to(features)),
217            )
218            .service(
219                web::resource("/collections/{collectionId}/items.json")
220                    .route(web::get().to(features)),
221            )
222            .service(
223                web::resource("/collections/{collectionId}/items/{featureId}.json")
224                    .route(web::get().to(feature)),
225            )
226            .service(
227                web::resource("/collections/{collectionId}/items/{featureId}")
228                    .route(web::get().to(feature)),
229            );
230    }
231}