geocoding_async/
geoadmin.rs

1//! The [GeoAdmin](https://api3.geo.admin.ch) provider for geocoding in Switzerland exclusively.
2//!
3//! Based on the [Search API](https://api3.geo.admin.ch/services/sdiservices.html#search)
4//! and [Identify Features API](https://api3.geo.admin.ch/services/sdiservices.html#identify-features)
5//!
6//! While GeoAdmin API is free, please respect their fair usage policy.
7//!
8//! ### Example
9//!
10//! ```
11//! # tokio_test::block_on(async {
12//! use geocoding_async::{GeoAdmin, Forward, Point};
13//!
14//! let geoadmin = GeoAdmin::new();
15//! let address = "Seftigenstrasse 264, 3084 Wabern";
16//! let res = geoadmin.forward(&address).await;
17//! assert_eq!(res.unwrap(), vec![Point::new(7.451352119445801, 46.92793655395508)]);
18//! # });
19//! ```
20use crate::Deserialize;
21use crate::GeocodingError;
22use crate::InputBounds;
23use crate::Point;
24use crate::UA_STRING;
25use crate::{Client, HeaderMap, HeaderValue, USER_AGENT};
26use crate::{Forward, Reverse};
27use num_traits::{Float, Pow};
28use std::fmt::Debug;
29
30/// An instance of the GeoAdmin geocoding service
31pub struct GeoAdmin {
32    client: Client,
33    endpoint: String,
34    sr: String,
35}
36
37/// An instance of a parameter builder for GeoAdmin geocoding
38pub struct GeoAdminParams<'a, T>
39where
40    T: Float + Debug,
41{
42    searchtext: &'a str,
43    origins: &'a str,
44    bbox: Option<&'a InputBounds<T>>,
45    limit: Option<u8>,
46}
47
48impl<'a, T> GeoAdminParams<'a, T>
49where
50    T: Float + Debug,
51{
52    /// Create a new GeoAdmin parameter builder
53    /// # Example:
54    ///
55    /// ```
56    /// use geocoding_async::{GeoAdmin, InputBounds, Point};
57    /// use geocoding_async::geoadmin::{GeoAdminParams};
58    ///
59    /// let bbox = InputBounds::new(
60    ///     (7.4513398, 46.92792859),
61    ///     (7.4513662, 46.9279467),
62    /// );
63    /// let params = GeoAdminParams::new(&"Seftigenstrasse Bern")
64    ///     .with_origins("address")
65    ///     .with_bbox(&bbox)
66    ///     .build();
67    /// ```
68    pub fn new(searchtext: &'a str) -> GeoAdminParams<'a, T> {
69        GeoAdminParams {
70            searchtext,
71            origins: "zipcode,gg25,district,kantone,gazetteer,address,parcel",
72            bbox: None,
73            limit: Some(50),
74        }
75    }
76
77    /// Set the `origins` property
78    pub fn with_origins(&mut self, origins: &'a str) -> &mut Self {
79        self.origins = origins;
80        self
81    }
82
83    /// Set the `bbox` property
84    pub fn with_bbox(&mut self, bbox: &'a InputBounds<T>) -> &mut Self {
85        self.bbox = Some(bbox);
86        self
87    }
88
89    /// Set the `limit` property
90    pub fn with_limit(&mut self, limit: u8) -> &mut Self {
91        self.limit = Some(limit);
92        self
93    }
94
95    /// Build and return an instance of GeoAdminParams
96    pub fn build(&self) -> GeoAdminParams<'a, T> {
97        GeoAdminParams {
98            searchtext: self.searchtext,
99            origins: self.origins,
100            bbox: self.bbox,
101            limit: self.limit,
102        }
103    }
104}
105
106impl GeoAdmin {
107    /// Create a new GeoAdmin geocoding instance using the default endpoint and sr
108    pub fn new() -> Self {
109        GeoAdmin::default()
110    }
111
112    /// Set a custom endpoint of a GeoAdmin geocoding instance
113    ///
114    /// Endpoint should include a trailing slash (i.e. "https://api3.geo.admin.ch/rest/services/api/")
115    pub fn with_endpoint(mut self, endpoint: &str) -> Self {
116        endpoint.clone_into(&mut self.endpoint);
117        self
118    }
119
120    /// Set a custom sr of a GeoAdmin geocoding instance
121    ///
122    /// Supported values: 21781 (LV03), 2056 (LV95), 4326 (WGS84) and 3857 (Web Pseudo-Mercator)
123    pub fn with_sr(mut self, sr: &str) -> Self {
124        sr.clone_into(&mut self.sr);
125        self
126    }
127
128    /// A forward-geocoding search of a location, returning a full detailed response
129    ///
130    /// Accepts an [`GeoAdminParams`](struct.GeoAdminParams.html) struct for specifying
131    /// options, including what origins to response and whether to filter
132    /// by a bounding box.
133    ///
134    /// Please see [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#search) for details.
135    ///
136    /// This method passes the `format` parameter to the API.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// # tokio_test::block_on(async {
142    /// use geocoding_async::{GeoAdmin, InputBounds, Point};
143    /// use geocoding_async::geoadmin::{GeoAdminParams, GeoAdminForwardResponse};
144    ///
145    /// let geoadmin = GeoAdmin::new();
146    /// let bbox = InputBounds::new(
147    ///     (7.4513398, 46.92792859),
148    ///     (7.4513662, 46.9279467),
149    /// );
150    /// let params = GeoAdminParams::new(&"Seftigenstrasse Bern")
151    ///     .with_origins("address")
152    ///     .with_bbox(&bbox)
153    ///     .build();
154    /// let res: GeoAdminForwardResponse<f64> = geoadmin.forward_full(&params).await.unwrap();
155    /// let result = &res.features[0];
156    /// assert_eq!(
157    ///     result.properties.label,
158    ///     "Seftigenstrasse 264 <b>3084 Wabern</b>",
159    /// );
160    /// # });
161    /// ```
162    pub async fn forward_full<T>(
163        &self,
164        params: &GeoAdminParams<'_, T>,
165    ) -> Result<GeoAdminForwardResponse<T>, GeocodingError>
166    where
167        T: Float + Debug,
168        for<'de> T: Deserialize<'de>,
169    {
170        // For lifetime issues
171        let bbox;
172        let limit;
173
174        let mut query = vec![
175            ("searchText", params.searchtext),
176            ("type", "locations"),
177            ("origins", params.origins),
178            ("sr", &self.sr),
179            ("geometryFormat", "geojson"),
180        ];
181
182        if let Some(bb) = params.bbox.cloned().as_mut() {
183            if ["4326", "3857"].contains(&self.sr.as_str()) {
184                *bb = InputBounds::new(
185                    wgs84_to_lv03(&bb.minimum_lonlat),
186                    wgs84_to_lv03(&bb.maximum_lonlat),
187                );
188            }
189            bbox = String::from(*bb);
190            query.push(("bbox", &bbox));
191        }
192
193        if let Some(lim) = params.limit {
194            limit = lim.to_string();
195            query.push(("limit", &limit));
196        }
197
198        let resp = self
199            .client
200            .get(&format!("{}SearchServer", self.endpoint))
201            .query(&query)
202            .send()
203            .await?
204            .error_for_status()?;
205        let res: GeoAdminForwardResponse<T> = resp.json().await?;
206        Ok(res)
207    }
208}
209
210impl Default for GeoAdmin {
211    fn default() -> Self {
212        let mut headers = HeaderMap::new();
213        headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING));
214        let client = Client::builder()
215            .default_headers(headers)
216            .build()
217            .expect("Couldn't build a client!");
218        GeoAdmin {
219            client,
220            endpoint: "https://api3.geo.admin.ch/rest/services/api/".to_string(),
221            sr: "4326".to_string(),
222        }
223    }
224}
225
226impl<T> Forward<T> for GeoAdmin
227where
228    T: Float + Debug,
229    for<'de> T: Deserialize<'de>,
230{
231    /// A forward-geocoding lookup of an address. Please see [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#search) for details.
232    ///
233    /// This method passes the `type`,  `origins`, `limit` and `sr` parameter to the API.
234    async fn forward(&self, place: &str) -> Result<Vec<Point<T>>, GeocodingError> {
235        let resp = self
236            .client
237            .get(&format!("{}SearchServer", self.endpoint))
238            .query(&[
239                ("searchText", place),
240                ("type", "locations"),
241                ("origins", "address"),
242                ("limit", "1"),
243                ("sr", &self.sr),
244                ("geometryFormat", "geojson"),
245            ])
246            .send()
247            .await?
248            .error_for_status()?;
249        let res: GeoAdminForwardResponse<T> = resp.json().await?;
250        // return easting & northing consistent
251        let results = if ["2056", "21781"].contains(&self.sr.as_str()) {
252            res.features
253                .iter()
254                .map(|feature| Point::new(feature.properties.y, feature.properties.x)) // y = west-east, x = north-south
255                .collect()
256        } else {
257            res.features
258                .iter()
259                .map(|feature| Point::new(feature.properties.x, feature.properties.y)) // x = west-east, y = north-south
260                .collect()
261        };
262        Ok(results)
263    }
264}
265
266impl<T> Reverse<T> for GeoAdmin
267where
268    T: Float + Debug,
269    for<'de> T: Deserialize<'de>,
270{
271    /// A reverse lookup of a point. More detail on the format of the
272    /// returned `String` can be found [here](https://api3.geo.admin.ch/services/sdiservices.html#identify-features)
273    ///
274    /// This method passes the `format` parameter to the API.
275    async fn reverse(&self, point: &Point<T>) -> Result<Option<String>, GeocodingError> {
276        let resp = self
277            .client
278            .get(&format!("{}MapServer/identify", self.endpoint))
279            .query(&[
280                (
281                    "geometry",
282                    format!(
283                        "{},{}",
284                        point.x().to_f64().unwrap(),
285                        point.y().to_f64().unwrap()
286                    )
287                    .as_str(),
288                ),
289                ("geometryType", "esriGeometryPoint"),
290                ("layers", "all:ch.bfs.gebaeude_wohnungs_register"),
291                ("mapExtent", "0,0,100,100"),
292                ("imageDisplay", "100,100,100"),
293                ("tolerance", "50"),
294                ("geometryFormat", "geojson"),
295                ("sr", &self.sr),
296                ("lang", "en"),
297            ])
298            .send()
299            .await?
300            .error_for_status()?;
301        let res: GeoAdminReverseResponse = resp.json().await?;
302        if !res.results.is_empty() {
303            let properties = &res.results[0].properties;
304            let address = format!(
305                "{}, {} {}",
306                properties.strname_deinr, properties.dplz4, properties.dplzname
307            );
308            Ok(Some(address))
309        } else {
310            Ok(None)
311        }
312    }
313}
314
315// Approximately transform Point from WGS84 to LV03
316//
317// See [the documentation](https://www.swisstopo.admin.ch/content/swisstopo-internet/en/online/calculation-services/_jcr_content/contentPar/tabs/items/documents_publicatio/tabPar/downloadlist/downloadItems/19_1467104393233.download/ch1903wgs84_e.pdf) for more details
318fn wgs84_to_lv03<T>(p: &Point<T>) -> Point<T>
319where
320    T: Float + Debug,
321{
322    let lambda = (p.x().to_f64().unwrap() * 3600.0 - 26782.5) / 10000.0;
323    let phi = (p.y().to_f64().unwrap() * 3600.0 - 169028.66) / 10000.0;
324    let x = 2600072.37 + 211455.93 * lambda
325        - 10938.51 * lambda * phi
326        - 0.36 * lambda * phi.pow(2)
327        - 44.54 * lambda.pow(3);
328    let y = 1200147.07 + 308807.95 * phi + 3745.25 * lambda.pow(2) + 76.63 * phi.pow(2)
329        - 194.56 * lambda.pow(2) * phi
330        + 119.79 * phi.pow(3);
331    Point::new(
332        T::from(x - 2000000.0).unwrap(),
333        T::from(y - 1000000.0).unwrap(),
334    )
335}
336/// The top-level full JSON (GeoJSON Feature Collection) response returned by a forward-geocoding request
337///
338/// See [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#search) for more details
339///
340///```json
341///{
342///     "type": "FeatureCollection",
343///     "features": [
344///         {
345///             "properties": {
346///                 "origin": "address",
347///                 "geom_quadindex": "021300220302203002031",
348///                 "weight": 1512,
349///                 "zoomlevel": 10,
350///                 "lon": 7.451352119445801,
351///                 "detail": "seftigenstrasse 264 3084 wabern 355 koeniz ch be",
352///                 "rank": 7,
353///                 "lat": 46.92793655395508,
354///                 "num": 264,
355///                 "y": 2600968.75,
356///                 "x": 1197427.0,
357///                 "label": "Seftigenstrasse 264 <b>3084 Wabern</b>"
358///                 "id": 1420809,
359///             }
360///         }
361///     ]
362/// }
363///```
364#[derive(Debug, Deserialize)]
365pub struct GeoAdminForwardResponse<T>
366where
367    T: Float + Debug,
368{
369    pub features: Vec<GeoAdminForwardLocation<T>>,
370}
371
372/// A forward geocoding location
373#[derive(Debug, Deserialize)]
374pub struct GeoAdminForwardLocation<T>
375where
376    T: Float + Debug,
377{
378    pub properties: ForwardLocationProperties<T>,
379}
380
381/// Forward Geocoding location attributes
382#[derive(Clone, Debug, Deserialize)]
383pub struct ForwardLocationProperties<T> {
384    pub origin: String,
385    pub geom_quadindex: String,
386    pub weight: u32,
387    pub rank: u32,
388    pub detail: String,
389    pub lat: T,
390    pub lon: T,
391    pub num: Option<usize>,
392    pub x: T,
393    pub y: T,
394    pub label: String,
395    pub zoomlevel: u32,
396}
397
398/// The top-level full JSON (GeoJSON FeatureCollection) response returned by a reverse-geocoding request
399///
400/// See [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#identify-features) for more details
401///
402///```json
403/// {
404///     "results": [
405///         {
406///             "type": "Feature"
407///             "id": "1272199_0"
408///             "attributes": {
409///                 "xxx": "xxx",
410///                 "...": "...",
411///             },
412///             "layerBodId": "ch.bfs.gebaeude_wohnungs_register",
413///             "layerName": "Register of Buildings and Dwellings",
414///         }
415///     ]
416/// }
417///```
418#[derive(Debug, Deserialize)]
419pub struct GeoAdminReverseResponse {
420    pub results: Vec<GeoAdminReverseLocation>,
421}
422
423/// A reverse geocoding result
424#[derive(Debug, Deserialize)]
425pub struct GeoAdminReverseLocation {
426    #[serde(rename = "featureId")]
427    pub feature_id: String,
428    #[serde(rename = "layerBodId")]
429    pub layer_bod_id: String,
430    #[serde(rename = "layerName")]
431    pub layer_name: String,
432    pub properties: ReverseLocationAttributes,
433}
434
435/// Reverse geocoding result attributes
436#[derive(Clone, Debug, Deserialize)]
437pub struct ReverseLocationAttributes {
438    pub egid: Option<String>,
439    pub ggdenr: u32,
440    pub ggdename: String,
441    pub gdekt: String,
442    pub edid: Option<String>,
443    pub egaid: u32,
444    pub deinr: Option<String>,
445    pub dplz4: u32,
446    pub dplzname: String,
447    pub egrid: Option<String>,
448    pub esid: u32,
449    pub strname: Vec<String>,
450    pub strsp: Vec<String>,
451    pub strname_deinr: String,
452    pub label: String,
453}
454
455#[cfg(test)]
456mod test {
457    use super::*;
458
459    #[tokio::test]
460    async fn new_with_sr_forward_test() {
461        let geoadmin = GeoAdmin::new().with_sr("2056");
462        let address = "Seftigenstrasse 264, 3084 Wabern";
463        let res = geoadmin.forward(&address).await;
464        assert_eq!(res.unwrap(), vec![Point::new(2_600_968.75, 1_197_427.0)]);
465    }
466
467    #[tokio::test]
468    async fn new_with_endpoint_forward_test() {
469        let geoadmin =
470            GeoAdmin::new().with_endpoint("https://api3.geo.admin.ch/rest/services/api/");
471        let address = "Seftigenstrasse 264, 3084 Wabern";
472        let res = geoadmin.forward(&address).await;
473        assert_eq!(
474            res.unwrap(),
475            vec![Point::new(7.451352119445801, 46.92793655395508)]
476        );
477    }
478
479    #[tokio::test]
480    async fn with_sr_forward_full_test() {
481        let geoadmin = GeoAdmin::new().with_sr("2056");
482        let bbox = InputBounds::new((2_600_967.75, 1_197_426.0), (2_600_969.75, 1_197_428.0));
483        let params = GeoAdminParams::new(&"Seftigenstrasse Bern")
484            .with_origins("address")
485            .with_bbox(&bbox)
486            .build();
487        let res: GeoAdminForwardResponse<f64> = geoadmin.forward_full(&params).await.unwrap();
488        let result = &res.features[0];
489        assert_eq!(
490            result.properties.label,
491            "Seftigenstrasse 264 <b>3084 Wabern</b>",
492        );
493    }
494
495    #[tokio::test]
496    async fn forward_full_test() {
497        let geoadmin = GeoAdmin::new();
498        let bbox = InputBounds::new((7.4513398, 46.92792859), (7.4513662, 46.9279467));
499        let params = GeoAdminParams::new(&"Seftigenstrasse Bern")
500            .with_origins("address")
501            .with_bbox(&bbox)
502            .build();
503        let res: GeoAdminForwardResponse<f64> = geoadmin.forward_full(&params).await.unwrap();
504        let result = &res.features[0];
505        assert_eq!(
506            result.properties.label,
507            "Seftigenstrasse 264 <b>3084 Wabern</b>",
508        );
509    }
510
511    #[tokio::test]
512    async fn forward_test() {
513        let geoadmin = GeoAdmin::new();
514        let address = "Seftigenstrasse 264, 3084 Wabern";
515        let res = geoadmin.forward(&address).await;
516        assert_eq!(
517            res.unwrap(),
518            vec![Point::new(7.451352119445801, 46.92793655395508)]
519        );
520    }
521
522    #[tokio::test]
523    async fn with_sr_reverse_test() {
524        let geoadmin = GeoAdmin::new().with_sr("2056");
525        let p = Point::new(2_600_968.75, 1_197_427.0);
526        let res = geoadmin.reverse(&p).await;
527        assert_eq!(
528            res.unwrap(),
529            Some("Seftigenstrasse 264, 3084 Wabern".to_string()),
530        );
531    }
532
533    #[tokio::test]
534    #[ignore = "https://github.com/georust/geocoding/pull/45#issuecomment-1592395700"]
535    async fn reverse_test() {
536        let geoadmin = GeoAdmin::new();
537        let p = Point::new(7.451352119445801, 46.92793655395508);
538        let res = geoadmin.reverse(&p).await;
539        assert_eq!(
540            res.unwrap(),
541            Some("Seftigenstrasse 264, 3084 Wabern".to_string()),
542        );
543    }
544}