├── .github ├── pull_request_template.md └── workflows │ └── test.yml ├── .gitignore ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── bors.toml └── src ├── geoadmin.rs ├── lib.rs ├── opencage.rs └── openstreetmap.rs /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - [ ] I agree to follow the project's [code of conduct](https://github.com/georust/geo/blob/master/CODE_OF_CONDUCT.md). 2 | - [ ] I added an entry to `CHANGES.md` if knowledge of this change could be valuable to users. 3 | --- 4 | 5 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Run tests 3 | jobs: 4 | # The `ci-result` job doesn't actually test anything - it just aggregates the 5 | # overall build status for bors, otherwise our bors.toml would need an entry 6 | # for each individual job produced by the job-matrix. 7 | # 8 | # Ref: https://github.com/rust-lang/crater/blob/9ab6f9697c901c4a44025cf0a39b73ad5b37d198/.github/workflows/bors.yml#L125-L149 9 | # 10 | # ALL THE SUBSEQUENT JOBS NEED THEIR `name` ADDED TO THE `needs` SECTION OF THIS JOB! 11 | ci-result: 12 | name: ci result 13 | runs-on: ubuntu-latest 14 | needs: 15 | - geocoding 16 | steps: 17 | - name: Mark the job as a success 18 | if: success() 19 | run: exit 0 20 | - name: Mark the job as a failure 21 | if: "!success()" 22 | run: exit 1 23 | 24 | geocoding: 25 | name: geocoding 26 | runs-on: ubuntu-latest 27 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 28 | defaults: 29 | run: 30 | working-directory: . 31 | strategy: 32 | matrix: 33 | container_image: 34 | # We aim to support rust-stable plus (at least) the prior 3 releases, 35 | # giving us about 6 months of coverage. 36 | # 37 | # Minimum supported rust version (MSRV) 38 | - "georust/geo-ci:proj-9.4.0-rust-1.75" 39 | # Two most recent releases - we omit older ones for expedient CI (TBD) 40 | container: 41 | image: ${{ matrix.container_image }} 42 | steps: 43 | - name: Install clippy and rustfmt 44 | run: rustup component add clippy rustfmt 45 | - name: Checkout repository 46 | uses: actions/checkout@v3 47 | - name: Check formatting 48 | run: cargo fmt --check 49 | - name: Build (--no-default-features) 50 | run: cargo build --no-default-features 51 | - name: Test (--no-default-features) 52 | run: cargo test --no-default-features 53 | - name: Build (default features) 54 | run: cargo build 55 | - name: Test (default features) 56 | run: cargo test 57 | - name: Build (--all-features) 58 | run: cargo build --all-features 59 | - name: Test (--all-features) 60 | run: cargo test --all-features 61 | - name: Clippy 62 | run: cargo clippy --all-features --all-targets 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## unreleased 4 | 5 | - Remove `chrono` dependency 6 | - update geo-types to 0.7.8 7 | - bump MSRV to 1.69 to keep CI happy 8 | 9 | ### Breaking Changes 10 | 11 | Due to previous security issues caused by the `chrono` crate 12 | the `NaiveDateTime` was replaces by a `UnixTime` type: 13 | 14 | ```diff 15 | - use chrono::NaiveDateTime; 16 | - use geocoding::opencage::Timestamp; 17 | + use geocoding::opencage::{Timestamp, UnixTime}; 18 | 19 | let created_http = "Mon, 16 May 2022 14:52:47 GMT".to_string(); 20 | 21 | let ts_in_seconds = 1_652_712_767_i64; 22 | - let created_unix = NaiveDateTime::from_timestamp(ts_in_seconds, 0); 23 | + let created_unix = UnixTime::from_seconds(ts_in_seconds); 24 | 25 | let timestamp = Timestamp { created_http, created_unix }; 26 | 27 | + assert_eq!(ts_in_seconds, created_unix.as_seconds()); 28 | ``` 29 | 30 | ## 0.4.0 31 | - Update CI to use same Rust versions as geo 32 | - Switch GeoAdmin API to WGS84 33 | - 34 | - Migrate to Github Actions 35 | - Update tests and dependencies 36 | - Update geo-types 37 | - Derive Debug where necessary 38 | - Fix OpenCage schema 39 | 40 | ## 0.3.1 41 | 42 | - Allow usage of `rustls-tls` feature 43 | 44 | ## 0.3.0 45 | 46 | - Update reqwest and hyper 47 | - 48 | - Upgrade geo-types 49 | - 50 | - Allow optional parameters for Opencage 51 | - 52 | - Derive `Clone` for Opencage results 53 | - 54 | 55 | ## 0.2.0 56 | 57 | - Made Opencage and Openstreetmap responses/results serializable so it's easier to store them afterwards 58 | - 59 | - Replace Failure with Thiserror 60 | - 61 | - Update geo-types to 0.5 62 | - 63 | - Update reqwest and hyper 64 | - 65 | 66 | 67 | ## 0.1.0 68 | 69 | - Added OpenStreetMap provider 70 | - 71 | - Fixes to keep up with OpenCage schema updates 72 | - 73 | - Switch to 2018 edition, use of `Failure`, more robust OpenCage schema definition, more ergonomic specification of bounds for OpenCage 74 | - https://github.com/georust/geocoding/pull/15 75 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # The GeoRust Code of Conduct 2 | 3 | This document is based on, and aims to track the [Rust Code of Conduct](https://www.rust-lang.org/conduct.html) 4 | 5 | ## Conduct 6 | 7 | **Contact**: [mods@georust.org](mailto:mods@georust.org) 8 | 9 | * We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other similar characteristic. 10 | * On IRC, please avoid using overtly sexual nicknames or other nicknames that might detract from a friendly, safe and welcoming environment for all. 11 | * Please be kind and courteous. There's no need to be mean or rude. 12 | * Respect that people have differences of opinion and that every design or implementation choice carries a trade-off and numerous costs. There is seldom a right answer. 13 | * Please keep unstructured critique to a minimum. If you have solid ideas you want to experiment with, make a fork and see how it works. 14 | * We will exclude you from interaction if you insult, demean or harass anyone. That is not welcome behavior. We interpret the term "harassment" as including the definition in the Citizen Code of Conduct; if you have any lack of clarity about what might be included in that concept, please read their definition. In particular, we don't tolerate behavior that excludes people in socially marginalized groups. 15 | * Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the channel ops or any of the [GeoRust moderation team][mod_team] immediately. Whether you're a regular contributor or a newcomer, we care about making this community a safe place for you and we've got your back. 16 | * Likewise any spamming, trolling, flaming, baiting or other attention-stealing behavior is not welcome. 17 | 18 | ## Moderation 19 | 20 | 21 | These are the policies for upholding our community's standards of conduct. If you feel that a thread needs moderation, please contact the [GeoRust moderation team][mod_team]. 22 | 23 | 1. Remarks that violate the Rust standards of conduct, including hateful, hurtful, oppressive, or exclusionary remarks, are not allowed. (Cursing is allowed, but never targeting another user, and never in a hateful manner.) 24 | 2. Remarks that moderators find inappropriate, whether listed in the code of conduct or not, are also not allowed. 25 | 3. Moderators will first respond to such remarks with a warning. 26 | 4. If the warning is unheeded, the user will be "kicked," i.e., kicked out of the communication channel to cool off. 27 | 5. If the user comes back and continues to make trouble, they will be banned, i.e., indefinitely excluded. 28 | 6. Moderators may choose at their discretion to un-ban the user if it was a first offense and they offer the offended party a genuine apology. 29 | 7. If a moderator bans someone and you think it was unjustified, please take it up with that moderator, or with a different moderator, **in private**. Complaints about bans in-channel are not allowed. 30 | 8. Moderators are held to a higher standard than other community members. If a moderator creates an inappropriate situation, they should expect less leeway than others. 31 | 32 | In the GeoRust community we strive to go the extra step to look out for each other. Don't just aim to be technically unimpeachable, try to be your best self. In particular, avoid flirting with offensive or sensitive issues, particularly if they're off-topic; this all too often leads to unnecessary fights, hurt feelings, and damaged trust; worse, it can drive people away from the community entirely. 33 | 34 | And if someone takes issue with something you said or did, resist the urge to be defensive. Just stop doing what it was they complained about and apologize. Even if you feel you were misinterpreted or unfairly accused, chances are good there was something you could've communicated better — remember that it's your responsibility to make your fellow Rustaceans comfortable. Everyone wants to get along and we are all here first and foremost because we want to talk about cool technology. You will find that people will be eager to assume good intent and forgive as long as you earn their trust. 35 | 36 | *Adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling) as well as the [Contributor Covenant v1.3.0](https://www.contributor-covenant.org/version/1/3/0/).* 37 | 38 | [mod_team]: mailto:mods@georust.org 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "geocoding" 3 | description = "Geocoding library for Rust" 4 | version = "0.4.0" 5 | authors = ["The Georust Developers "] 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/georust/geocoding" 8 | keywords = ["gecoding", "geo", "gis", "geospatial"] 9 | readme = "README.md" 10 | edition = "2018" 11 | rust-version = "1.75" 12 | 13 | [dependencies] 14 | thiserror = "1.0" 15 | geo-types = "0.7.8" 16 | num-traits = "0.2" 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | reqwest = { version = "0.11", default-features = false, features = [ 20 | "default-tls", 21 | "blocking", 22 | "json", 23 | ] } 24 | hyper = "0.14.11" 25 | 26 | [features] 27 | default = ["reqwest/default"] 28 | rustls-tls = ["reqwest/rustls-tls"] 29 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 The rust-geocoding Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geocoding 2 | 3 | Rust utilities to enrich addresses, cities, countries, and landmarks 4 | with geographic coordinates through third-party geocoding web services. 5 | Project is in a very early stage. 6 | 7 | [API Documentation](https://docs.rs/geocoding) 8 | 9 | ## License 10 | 11 | Licensed under either of 12 | 13 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 14 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 15 | 16 | at your option. 17 | 18 | ### Contribution 19 | 20 | Unless you explicitly state otherwise, any contribution intentionally 21 | submitted for inclusion in the work by you, as defined in the Apache-2.0 22 | license, shall be dual licensed as above, without any additional terms or 23 | conditions. 24 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | "ci result", 3 | ] 4 | -------------------------------------------------------------------------------- /src/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 | //! use geocoding::{GeoAdmin, Forward, Point}; 12 | //! 13 | //! let geoadmin = GeoAdmin::new(); 14 | //! let address = "Seftigenstrasse 264, 3084 Wabern"; 15 | //! let res = geoadmin.forward(&address); 16 | //! assert_eq!(res.unwrap(), vec![Point::new(7.451352119445801, 46.92793655395508)]); 17 | //! ``` 18 | use crate::Deserialize; 19 | use crate::GeocodingError; 20 | use crate::InputBounds; 21 | use crate::Point; 22 | use crate::UA_STRING; 23 | use crate::{Client, HeaderMap, HeaderValue, USER_AGENT}; 24 | use crate::{Forward, Reverse}; 25 | use num_traits::{Float, Pow}; 26 | use std::fmt::Debug; 27 | 28 | /// An instance of the GeoAdmin geocoding service 29 | pub struct GeoAdmin { 30 | client: Client, 31 | endpoint: String, 32 | sr: String, 33 | } 34 | 35 | /// An instance of a parameter builder for GeoAdmin geocoding 36 | pub struct GeoAdminParams<'a, T> 37 | where 38 | T: Float + Debug, 39 | { 40 | searchtext: &'a str, 41 | origins: &'a str, 42 | bbox: Option<&'a InputBounds>, 43 | limit: Option, 44 | } 45 | 46 | impl<'a, T> GeoAdminParams<'a, T> 47 | where 48 | T: Float + Debug, 49 | { 50 | /// Create a new GeoAdmin parameter builder 51 | /// # Example: 52 | /// 53 | /// ``` 54 | /// use geocoding::{GeoAdmin, InputBounds, Point}; 55 | /// use geocoding::geoadmin::{GeoAdminParams}; 56 | /// 57 | /// let bbox = InputBounds::new( 58 | /// (7.4513398, 46.92792859), 59 | /// (7.4513662, 46.9279467), 60 | /// ); 61 | /// let params = GeoAdminParams::new(&"Seftigenstrasse Bern") 62 | /// .with_origins("address") 63 | /// .with_bbox(&bbox) 64 | /// .build(); 65 | /// ``` 66 | pub fn new(searchtext: &'a str) -> GeoAdminParams<'a, T> { 67 | GeoAdminParams { 68 | searchtext, 69 | origins: "zipcode,gg25,district,kantone,gazetteer,address,parcel", 70 | bbox: None, 71 | limit: Some(50), 72 | } 73 | } 74 | 75 | /// Set the `origins` property 76 | pub fn with_origins(&mut self, origins: &'a str) -> &mut Self { 77 | self.origins = origins; 78 | self 79 | } 80 | 81 | /// Set the `bbox` property 82 | pub fn with_bbox(&mut self, bbox: &'a InputBounds) -> &mut Self { 83 | self.bbox = Some(bbox); 84 | self 85 | } 86 | 87 | /// Set the `limit` property 88 | pub fn with_limit(&mut self, limit: u8) -> &mut Self { 89 | self.limit = Some(limit); 90 | self 91 | } 92 | 93 | /// Build and return an instance of GeoAdminParams 94 | pub fn build(&self) -> GeoAdminParams<'a, T> { 95 | GeoAdminParams { 96 | searchtext: self.searchtext, 97 | origins: self.origins, 98 | bbox: self.bbox, 99 | limit: self.limit, 100 | } 101 | } 102 | } 103 | 104 | impl GeoAdmin { 105 | /// Create a new GeoAdmin geocoding instance using the default endpoint and sr 106 | pub fn new() -> Self { 107 | GeoAdmin::default() 108 | } 109 | 110 | /// Set a custom endpoint of a GeoAdmin geocoding instance 111 | /// 112 | /// Endpoint should include a trailing slash (i.e. "https://api3.geo.admin.ch/rest/services/api/") 113 | pub fn with_endpoint(mut self, endpoint: &str) -> Self { 114 | self.endpoint = endpoint.to_owned(); 115 | self 116 | } 117 | 118 | /// Set a custom sr of a GeoAdmin geocoding instance 119 | /// 120 | /// Supported values: 21781 (LV03), 2056 (LV95), 4326 (WGS84) and 3857 (Web Pseudo-Mercator) 121 | pub fn with_sr(mut self, sr: &str) -> Self { 122 | self.sr = sr.to_owned(); 123 | self 124 | } 125 | 126 | /// A forward-geocoding search of a location, returning a full detailed response 127 | /// 128 | /// Accepts an [`GeoAdminParams`](struct.GeoAdminParams.html) struct for specifying 129 | /// options, including what origins to response and whether to filter 130 | /// by a bounding box. 131 | /// 132 | /// Please see [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#search) for details. 133 | /// 134 | /// This method passes the `format` parameter to the API. 135 | /// 136 | /// # Examples 137 | /// 138 | /// ``` 139 | /// use geocoding::{GeoAdmin, InputBounds, Point}; 140 | /// use geocoding::geoadmin::{GeoAdminParams, GeoAdminForwardResponse}; 141 | /// 142 | /// let geoadmin = GeoAdmin::new(); 143 | /// let bbox = InputBounds::new( 144 | /// (7.4513398, 46.92792859), 145 | /// (7.4513662, 46.9279467), 146 | /// ); 147 | /// let params = GeoAdminParams::new(&"Seftigenstrasse Bern") 148 | /// .with_origins("address") 149 | /// .with_bbox(&bbox) 150 | /// .build(); 151 | /// let res: GeoAdminForwardResponse = geoadmin.forward_full(¶ms).unwrap(); 152 | /// let result = &res.features[0]; 153 | /// assert_eq!( 154 | /// result.properties.label, 155 | /// "Seftigenstrasse 264 3084 Wabern", 156 | /// ); 157 | /// ``` 158 | pub fn forward_full( 159 | &self, 160 | params: &GeoAdminParams, 161 | ) -> Result, GeocodingError> 162 | where 163 | T: Float + Debug, 164 | for<'de> T: Deserialize<'de>, 165 | { 166 | // For lifetime issues 167 | let bbox; 168 | let limit; 169 | 170 | let mut query = vec![ 171 | ("searchText", params.searchtext), 172 | ("type", "locations"), 173 | ("origins", params.origins), 174 | ("sr", &self.sr), 175 | ("geometryFormat", "geojson"), 176 | ]; 177 | 178 | if let Some(bb) = params.bbox.cloned().as_mut() { 179 | if ["4326", "3857"].contains(&self.sr.as_str()) { 180 | *bb = InputBounds::new( 181 | wgs84_to_lv03(&bb.minimum_lonlat), 182 | wgs84_to_lv03(&bb.maximum_lonlat), 183 | ); 184 | } 185 | bbox = String::from(*bb); 186 | query.push(("bbox", &bbox)); 187 | } 188 | 189 | if let Some(lim) = params.limit { 190 | limit = lim.to_string(); 191 | query.push(("limit", &limit)); 192 | } 193 | 194 | let resp = self 195 | .client 196 | .get(format!("{}SearchServer", self.endpoint)) 197 | .query(&query) 198 | .send()? 199 | .error_for_status()?; 200 | let res: GeoAdminForwardResponse = resp.json()?; 201 | Ok(res) 202 | } 203 | } 204 | 205 | impl Default for GeoAdmin { 206 | fn default() -> Self { 207 | let mut headers = HeaderMap::new(); 208 | headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING)); 209 | let client = Client::builder() 210 | .default_headers(headers) 211 | .build() 212 | .expect("Couldn't build a client!"); 213 | GeoAdmin { 214 | client, 215 | endpoint: "https://api3.geo.admin.ch/rest/services/api/".to_string(), 216 | sr: "4326".to_string(), 217 | } 218 | } 219 | } 220 | 221 | impl Forward for GeoAdmin 222 | where 223 | T: Float + Debug, 224 | for<'de> T: Deserialize<'de>, 225 | { 226 | /// A forward-geocoding lookup of an address. Please see [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#search) for details. 227 | /// 228 | /// This method passes the `type`, `origins`, `limit` and `sr` parameter to the API. 229 | fn forward(&self, place: &str) -> Result>, GeocodingError> { 230 | let resp = self 231 | .client 232 | .get(format!("{}SearchServer", self.endpoint)) 233 | .query(&[ 234 | ("searchText", place), 235 | ("type", "locations"), 236 | ("origins", "address"), 237 | ("limit", "1"), 238 | ("sr", &self.sr), 239 | ("geometryFormat", "geojson"), 240 | ]) 241 | .send()? 242 | .error_for_status()?; 243 | let res: GeoAdminForwardResponse = resp.json()?; 244 | // return easting & northing consistent 245 | let results = if ["2056", "21781"].contains(&self.sr.as_str()) { 246 | res.features 247 | .iter() 248 | .map(|feature| Point::new(feature.properties.y, feature.properties.x)) // y = west-east, x = north-south 249 | .collect() 250 | } else { 251 | res.features 252 | .iter() 253 | .map(|feature| Point::new(feature.properties.x, feature.properties.y)) // x = west-east, y = north-south 254 | .collect() 255 | }; 256 | Ok(results) 257 | } 258 | } 259 | 260 | impl Reverse for GeoAdmin 261 | where 262 | T: Float + Debug, 263 | for<'de> T: Deserialize<'de>, 264 | { 265 | /// A reverse lookup of a point. More detail on the format of the 266 | /// returned `String` can be found [here](https://api3.geo.admin.ch/services/sdiservices.html#identify-features) 267 | /// 268 | /// This method passes the `format` parameter to the API. 269 | fn reverse(&self, point: &Point) -> Result, GeocodingError> { 270 | let resp = self 271 | .client 272 | .get(format!("{}MapServer/identify", self.endpoint)) 273 | .query(&[ 274 | ( 275 | "geometry", 276 | format!( 277 | "{},{}", 278 | point.x().to_f64().unwrap(), 279 | point.y().to_f64().unwrap() 280 | ) 281 | .as_str(), 282 | ), 283 | ("geometryType", "esriGeometryPoint"), 284 | ("layers", "all:ch.bfs.gebaeude_wohnungs_register"), 285 | ("mapExtent", "0,0,100,100"), 286 | ("imageDisplay", "100,100,100"), 287 | ("tolerance", "50"), 288 | ("geometryFormat", "geojson"), 289 | ("sr", &self.sr), 290 | ("lang", "en"), 291 | ]) 292 | .send()? 293 | .error_for_status()?; 294 | let res: GeoAdminReverseResponse = resp.json()?; 295 | if !res.results.is_empty() { 296 | let properties = &res.results[0].properties; 297 | let address = format!( 298 | "{}, {} {}", 299 | properties.strname_deinr, properties.dplz4, properties.dplzname 300 | ); 301 | Ok(Some(address)) 302 | } else { 303 | Ok(None) 304 | } 305 | } 306 | } 307 | 308 | // Approximately transform Point from WGS84 to LV03 309 | // 310 | // 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 311 | fn wgs84_to_lv03(p: &Point) -> Point 312 | where 313 | T: Float + Debug, 314 | { 315 | let lambda = (p.x().to_f64().unwrap() * 3600.0 - 26782.5) / 10000.0; 316 | let phi = (p.y().to_f64().unwrap() * 3600.0 - 169028.66) / 10000.0; 317 | let x = 2600072.37 + 211455.93 * lambda 318 | - 10938.51 * lambda * phi 319 | - 0.36 * lambda * phi.pow(2) 320 | - 44.54 * lambda.pow(3); 321 | let y = 1200147.07 + 308807.95 * phi + 3745.25 * lambda.pow(2) + 76.63 * phi.pow(2) 322 | - 194.56 * lambda.pow(2) * phi 323 | + 119.79 * phi.pow(3); 324 | Point::new( 325 | T::from(x - 2000000.0).unwrap(), 326 | T::from(y - 1000000.0).unwrap(), 327 | ) 328 | } 329 | /// The top-level full JSON (GeoJSON Feature Collection) response returned by a forward-geocoding request 330 | /// 331 | /// See [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#search) for more details 332 | /// 333 | ///```json 334 | ///{ 335 | /// "type": "FeatureCollection", 336 | /// "features": [ 337 | /// { 338 | /// "properties": { 339 | /// "origin": "address", 340 | /// "geom_quadindex": "021300220302203002031", 341 | /// "weight": 1512, 342 | /// "zoomlevel": 10, 343 | /// "lon": 7.451352119445801, 344 | /// "detail": "seftigenstrasse 264 3084 wabern 355 koeniz ch be", 345 | /// "rank": 7, 346 | /// "lat": 46.92793655395508, 347 | /// "num": 264, 348 | /// "y": 2600968.75, 349 | /// "x": 1197427.0, 350 | /// "label": "Seftigenstrasse 264 3084 Wabern" 351 | /// "id": 1420809, 352 | /// } 353 | /// } 354 | /// ] 355 | /// } 356 | ///``` 357 | #[derive(Debug, Deserialize)] 358 | pub struct GeoAdminForwardResponse 359 | where 360 | T: Float + Debug, 361 | { 362 | pub features: Vec>, 363 | } 364 | 365 | /// A forward geocoding location 366 | #[derive(Debug, Deserialize)] 367 | pub struct GeoAdminForwardLocation 368 | where 369 | T: Float + Debug, 370 | { 371 | pub properties: ForwardLocationProperties, 372 | } 373 | 374 | /// Forward Geocoding location attributes 375 | #[derive(Clone, Debug, Deserialize)] 376 | pub struct ForwardLocationProperties { 377 | pub origin: String, 378 | pub geom_quadindex: String, 379 | pub weight: u32, 380 | pub rank: u32, 381 | pub detail: String, 382 | pub lat: T, 383 | pub lon: T, 384 | pub num: Option, 385 | pub x: T, 386 | pub y: T, 387 | pub label: String, 388 | pub zoomlevel: u32, 389 | } 390 | 391 | /// The top-level full JSON (GeoJSON FeatureCollection) response returned by a reverse-geocoding request 392 | /// 393 | /// See [the documentation](https://api3.geo.admin.ch/services/sdiservices.html#identify-features) for more details 394 | /// 395 | ///```json 396 | /// { 397 | /// "results": [ 398 | /// { 399 | /// "type": "Feature" 400 | /// "id": "1272199_0" 401 | /// "attributes": { 402 | /// "xxx": "xxx", 403 | /// "...": "...", 404 | /// }, 405 | /// "layerBodId": "ch.bfs.gebaeude_wohnungs_register", 406 | /// "layerName": "Register of Buildings and Dwellings", 407 | /// } 408 | /// ] 409 | /// } 410 | ///``` 411 | #[derive(Debug, Deserialize)] 412 | pub struct GeoAdminReverseResponse { 413 | pub results: Vec, 414 | } 415 | 416 | /// A reverse geocoding result 417 | #[derive(Debug, Deserialize)] 418 | pub struct GeoAdminReverseLocation { 419 | #[serde(rename = "featureId")] 420 | pub feature_id: String, 421 | #[serde(rename = "layerBodId")] 422 | pub layer_bod_id: String, 423 | #[serde(rename = "layerName")] 424 | pub layer_name: String, 425 | pub properties: ReverseLocationAttributes, 426 | } 427 | 428 | /// Reverse geocoding result attributes 429 | #[derive(Clone, Debug, Deserialize)] 430 | pub struct ReverseLocationAttributes { 431 | pub egid: Option, 432 | pub ggdenr: u32, 433 | pub ggdename: String, 434 | pub gdekt: String, 435 | pub edid: Option, 436 | pub egaid: u32, 437 | pub deinr: Option, 438 | pub dplz4: u32, 439 | pub dplzname: String, 440 | pub egrid: Option, 441 | pub esid: u32, 442 | pub strname: Vec, 443 | pub strsp: Vec, 444 | pub strname_deinr: String, 445 | pub label: String, 446 | } 447 | 448 | #[cfg(test)] 449 | mod test { 450 | use super::*; 451 | 452 | #[test] 453 | fn new_with_sr_forward_test() { 454 | let geoadmin = GeoAdmin::new().with_sr("2056"); 455 | let address = "Seftigenstrasse 264, 3084 Wabern"; 456 | let res = geoadmin.forward(address); 457 | assert_eq!(res.unwrap(), vec![Point::new(2_600_968.75, 1_197_427.0)]); 458 | } 459 | 460 | #[test] 461 | fn new_with_endpoint_forward_test() { 462 | let geoadmin = 463 | GeoAdmin::new().with_endpoint("https://api3.geo.admin.ch/rest/services/api/"); 464 | let address = "Seftigenstrasse 264, 3084 Wabern"; 465 | let res = geoadmin.forward(address); 466 | assert_eq!( 467 | res.unwrap(), 468 | vec![Point::new(7.451352119445801, 46.92793655395508)] 469 | ); 470 | } 471 | 472 | #[test] 473 | fn with_sr_forward_full_test() { 474 | let geoadmin = GeoAdmin::new().with_sr("2056"); 475 | let bbox = InputBounds::new((2_600_967.75, 1_197_426.0), (2_600_969.75, 1_197_428.0)); 476 | let params = GeoAdminParams::new("Seftigenstrasse Bern") 477 | .with_origins("address") 478 | .with_bbox(&bbox) 479 | .build(); 480 | let res: GeoAdminForwardResponse = geoadmin.forward_full(¶ms).unwrap(); 481 | let result = &res.features[0]; 482 | assert_eq!( 483 | result.properties.label, 484 | "Seftigenstrasse 264 3084 Wabern", 485 | ); 486 | } 487 | 488 | #[test] 489 | fn forward_full_test() { 490 | let geoadmin = GeoAdmin::new(); 491 | let bbox = InputBounds::new((7.4513398, 46.92792859), (7.4513662, 46.9279467)); 492 | let params = GeoAdminParams::new("Seftigenstrasse Bern") 493 | .with_origins("address") 494 | .with_bbox(&bbox) 495 | .build(); 496 | let res: GeoAdminForwardResponse = geoadmin.forward_full(¶ms).unwrap(); 497 | let result = &res.features[0]; 498 | assert_eq!( 499 | result.properties.label, 500 | "Seftigenstrasse 264 3084 Wabern", 501 | ); 502 | } 503 | 504 | #[test] 505 | fn forward_test() { 506 | let geoadmin = GeoAdmin::new(); 507 | let address = "Seftigenstrasse 264, 3084 Wabern"; 508 | let res = geoadmin.forward(address); 509 | assert_eq!( 510 | res.unwrap(), 511 | vec![Point::new(7.451352119445801, 46.92793655395508)] 512 | ); 513 | } 514 | 515 | #[test] 516 | fn with_sr_reverse_test() { 517 | let geoadmin = GeoAdmin::new().with_sr("2056"); 518 | let p = Point::new(2_600_968.75, 1_197_427.0); 519 | let res = geoadmin.reverse(&p); 520 | assert_eq!( 521 | res.unwrap(), 522 | Some("Seftigenstrasse 264, 3084 Wabern".to_string()), 523 | ); 524 | } 525 | 526 | #[test] 527 | #[ignore = "https://github.com/georust/geocoding/pull/45#issuecomment-1592395700"] 528 | fn reverse_test() { 529 | let geoadmin = GeoAdmin::new(); 530 | let p = Point::new(7.451352119445801, 46.92793655395508); 531 | let res = geoadmin.reverse(&p); 532 | assert_eq!( 533 | res.unwrap(), 534 | Some("Seftigenstrasse 264, 3084 Wabern".to_string()), 535 | ); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides forward– and reverse-geocoding functionality for Rust. 2 | //! Over time, a variety of providers will be added. Each provider may implement one or both 3 | //! of the `Forward` and `Reverse` traits, which provide forward– and reverse-geocoding methods. 4 | //! 5 | //! Note that for the `reverse` method, the return type is simply `Option`, 6 | //! as this is the lowest common denominator reverse-geocoding result. 7 | //! Individual providers may implement additional methods, which return more 8 | //! finely-structured and/or extensive data, and enable more specific query tuning. 9 | //! Coordinate data are specified using the [`Point`](struct.Point.html) struct, which has several 10 | //! convenient `From` implementations to allow for easy construction using primitive types. 11 | //! 12 | //! ### A note on Coordinate Order 13 | //! While individual providers may specify coordinates in either `[Longitude, Latitude]` **or** 14 | //! `[Latitude, Longitude`] order, 15 | //! `Geocoding` **always** requires [`Point`](struct.Point.html) data in `[Longitude, Latitude]` (`x, y`) order, 16 | //! and returns data in that order. 17 | //! 18 | //! ### Usage of rustls 19 | //! 20 | //! If you like to use [rustls](https://github.com/ctz/rustls) instead of OpenSSL 21 | //! you can enable the `rustls-tls` feature in your `Cargo.toml`: 22 | //! 23 | //!```toml 24 | //![dependencies] 25 | //!geocoding = { version = "*", default-features = false, features = ["rustls-tls"] } 26 | //!``` 27 | 28 | static UA_STRING: &str = "Rust-Geocoding"; 29 | 30 | pub use geo_types::{Coord, Point}; 31 | use num_traits::Float; 32 | use reqwest::blocking::Client; 33 | use reqwest::header::ToStrError; 34 | use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; 35 | use serde::de::DeserializeOwned; 36 | use serde::{Deserialize, Serialize}; 37 | use std::fmt::Debug; 38 | use std::num::ParseIntError; 39 | use thiserror::Error; 40 | 41 | // The OpenCage geocoding provider 42 | pub mod opencage; 43 | pub use crate::opencage::Opencage; 44 | 45 | // The OpenStreetMap Nominatim geocoding provider 46 | pub mod openstreetmap; 47 | pub use crate::openstreetmap::Openstreetmap; 48 | 49 | // The GeoAdmin geocoding provider 50 | pub mod geoadmin; 51 | pub use crate::geoadmin::GeoAdmin; 52 | 53 | /// Errors that can occur during geocoding operations 54 | #[derive(Error, Debug)] 55 | pub enum GeocodingError { 56 | #[error("Forward geocoding failed")] 57 | Forward, 58 | #[error("Reverse geocoding failed")] 59 | Reverse, 60 | #[error("HTTP request error")] 61 | Request(#[from] reqwest::Error), 62 | #[error("Error converting headers to String")] 63 | HeaderConversion(#[from] ToStrError), 64 | #[error("Error converting int to String")] 65 | ParseInt(#[from] ParseIntError), 66 | } 67 | 68 | /// Reverse-geocode a coordinate. 69 | /// 70 | /// This trait represents the most simple and minimal implementation 71 | /// available from a given geocoding provider: some address formatted as Option. 72 | /// 73 | /// Examples 74 | /// 75 | /// ``` 76 | /// use geocoding::{Opencage, Point, Reverse}; 77 | /// 78 | /// let p = Point::new(2.12870, 41.40139); 79 | /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 80 | /// let res = oc.reverse(&p).unwrap(); 81 | /// assert_eq!( 82 | /// res, 83 | /// Some("Carrer de Calatrava, 64, 08017 Barcelona, Spain".to_string()) 84 | /// ); 85 | /// ``` 86 | pub trait Reverse 87 | where 88 | T: Float + Debug, 89 | { 90 | // NOTE TO IMPLEMENTERS: Point coordinates are lon, lat (x, y) 91 | // You may have to provide these coordinates in reverse order, 92 | // depending on the provider's requirements (see e.g. OpenCage) 93 | fn reverse(&self, point: &Point) -> Result, GeocodingError>; 94 | } 95 | 96 | /// Forward-geocode a coordinate. 97 | /// 98 | /// This trait represents the most simple and minimal implementation available 99 | /// from a given geocoding provider: It returns a `Vec` of zero or more `Points`. 100 | /// 101 | /// Examples 102 | /// 103 | /// ``` 104 | /// use geocoding::{Coord, Forward, Opencage, Point}; 105 | /// 106 | /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 107 | /// let address = "Schwabing, München"; 108 | /// let res: Vec> = oc.forward(address).unwrap(); 109 | /// assert_eq!( 110 | /// res, 111 | /// vec![Point(Coord { x: 11.5884858, y: 48.1700887 })] 112 | /// ); 113 | /// ``` 114 | pub trait Forward 115 | where 116 | T: Float + Debug, 117 | { 118 | // NOTE TO IMPLEMENTERS: while returned provider point data may not be in 119 | // lon, lat (x, y) order, Geocoding requires this order in its output Point 120 | // data. Please pay attention when using returned data to construct Points 121 | fn forward(&self, address: &str) -> Result>, GeocodingError>; 122 | } 123 | 124 | /// Used to specify a bounding box to search within when forward-geocoding 125 | /// 126 | /// - `minimum` refers to the **bottom-left** or **south-west** corner of the bounding box 127 | /// - `maximum` refers to the **top-right** or **north-east** corner of the bounding box. 128 | #[derive(Copy, Clone, Debug)] 129 | pub struct InputBounds 130 | where 131 | T: Float + Debug, 132 | { 133 | pub minimum_lonlat: Point, 134 | pub maximum_lonlat: Point, 135 | } 136 | 137 | impl InputBounds 138 | where 139 | T: Float + Debug, 140 | { 141 | /// Create a new `InputBounds` struct by passing 2 `Point`s defining: 142 | /// - minimum (bottom-left) longitude and latitude coordinates 143 | /// - maximum (top-right) longitude and latitude coordinates 144 | pub fn new(minimum_lonlat: U, maximum_lonlat: U) -> InputBounds 145 | where 146 | U: Into>, 147 | { 148 | InputBounds { 149 | minimum_lonlat: minimum_lonlat.into(), 150 | maximum_lonlat: maximum_lonlat.into(), 151 | } 152 | } 153 | } 154 | 155 | /// Convert borrowed input bounds into the correct String representation 156 | impl From> for String 157 | where 158 | T: Float + Debug, 159 | { 160 | fn from(ip: InputBounds) -> String { 161 | // Return in lon, lat order 162 | format!( 163 | "{},{},{},{}", 164 | ip.minimum_lonlat.x().to_f64().unwrap(), 165 | ip.minimum_lonlat.y().to_f64().unwrap(), 166 | ip.maximum_lonlat.x().to_f64().unwrap(), 167 | ip.maximum_lonlat.y().to_f64().unwrap() 168 | ) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/opencage.rs: -------------------------------------------------------------------------------- 1 | //! The [OpenCage Geocoding](https://opencagedata.com/) provider. 2 | //! 3 | //! Geocoding methods are implemented on the [`Opencage`](struct.Opencage.html) struct. 4 | //! Please see the [API documentation](https://opencagedata.com/api) for details. 5 | //! Note that rate limits apply to the free tier: 6 | //! there is a [rate-limit](https://opencagedata.com/api#rate-limiting) of 1 request per second, 7 | //! and a quota of calls allowed per 24-hour period. The remaining daily quota can be retrieved 8 | //! using the [`remaining_calls()`](struct.Opencage.html#method.remaining_calls) method. If you 9 | //! are a paid tier user, this value will not be updated, and will remain `None`. 10 | //! ### A Note on Coordinate Order 11 | //! This provider's API documentation shows all coordinates in `[Latitude, Longitude]` order. 12 | //! However, `Geocoding` requires input `Point` coordinate order as `[Longitude, Latitude]` 13 | //! `(x, y)`, and returns coordinates with that order. 14 | //! 15 | //! ### Example 16 | //! 17 | //! ``` 18 | //! use geocoding::{Opencage, Point, Reverse}; 19 | //! 20 | //! let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 21 | //! oc.parameters.language = Some("fr"); 22 | //! let p = Point::new(2.12870, 41.40139); 23 | //! let res = oc.reverse(&p); 24 | //! // "Carrer de Calatrava, 64, 08017 Barcelone, Espagne" 25 | //! println!("{:?}", res.unwrap()); 26 | //! ``` 27 | use crate::DeserializeOwned; 28 | use crate::GeocodingError; 29 | use crate::InputBounds; 30 | use crate::Point; 31 | use crate::UA_STRING; 32 | use crate::{Client, HeaderMap, HeaderValue, USER_AGENT}; 33 | use crate::{Deserialize, Serialize}; 34 | use crate::{Forward, Reverse}; 35 | use num_traits::Float; 36 | use serde::Deserializer; 37 | use std::collections::HashMap; 38 | use std::fmt::Debug; 39 | use std::sync::{Arc, Mutex}; 40 | 41 | macro_rules! add_optional_param { 42 | ($query:expr, $param:expr, $name:expr) => { 43 | if let Some(p) = $param { 44 | $query.push(($name, p)) 45 | } 46 | }; 47 | } 48 | 49 | // Please see the [API documentation](https://opencagedata.com/api#forward-opt) for details. 50 | #[derive(Default)] 51 | pub struct Parameters<'a> { 52 | pub language: Option<&'a str>, 53 | pub countrycode: Option<&'a str>, 54 | pub limit: Option<&'a str>, 55 | } 56 | 57 | impl<'a> Parameters<'a> { 58 | fn as_query(&self) -> Vec<(&'a str, &'a str)> { 59 | let mut query = vec![]; 60 | add_optional_param!(query, self.language, "language"); 61 | add_optional_param!(query, self.countrycode, "countrycode"); 62 | add_optional_param!(query, self.limit, "limit"); 63 | query 64 | } 65 | } 66 | 67 | pub fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result 68 | where 69 | D: Deserializer<'de>, 70 | { 71 | #[derive(Deserialize)] 72 | #[serde(untagged)] 73 | enum StringOrInt { 74 | String(String), 75 | Int(i32), 76 | } 77 | 78 | match StringOrInt::deserialize(deserializer)? { 79 | StringOrInt::String(s) => Ok(s), 80 | StringOrInt::Int(i) => Ok(i.to_string()), 81 | } 82 | } 83 | 84 | // OpenCage has a custom rate-limit header, indicating remaining calls 85 | // header! { (XRatelimitRemaining, "X-RateLimit-Remaining") => [i32] } 86 | static XRL: &str = "x-ratelimit-remaining"; 87 | /// Use this constant if you don't need to restrict a `forward_full` call with a bounding box 88 | pub static NOBOX: Option> = None::>; 89 | 90 | /// An instance of the Opencage Geocoding service 91 | pub struct Opencage<'a> { 92 | api_key: String, 93 | client: Client, 94 | endpoint: String, 95 | pub parameters: Parameters<'a>, 96 | remaining: Arc>>, 97 | } 98 | 99 | impl Opencage<'_> { 100 | /// Create a new OpenCage geocoding instance 101 | pub fn new(api_key: String) -> Self { 102 | let mut headers = HeaderMap::new(); 103 | headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING)); 104 | let client = Client::builder() 105 | .default_headers(headers) 106 | .build() 107 | .expect("Couldn't build a client!"); 108 | 109 | let parameters = Parameters::default(); 110 | Opencage { 111 | api_key, 112 | client, 113 | parameters, 114 | endpoint: "https://api.opencagedata.com/geocode/v1/json".to_string(), 115 | remaining: Arc::new(Mutex::new(None)), 116 | } 117 | } 118 | /// Retrieve the remaining API calls in your daily quota 119 | /// 120 | /// Initially, this value is `None`. Any OpenCage API call using a "Free Tier" key 121 | /// will update this value to reflect the remaining quota for the API key. 122 | /// See the [API docs](https://opencagedata.com/api#rate-limiting) for details. 123 | pub fn remaining_calls(&self) -> Option { 124 | *self.remaining.lock().unwrap() 125 | } 126 | /// A reverse lookup of a point, returning an annotated response. 127 | /// 128 | /// This method passes the `no_record` parameter to the API. 129 | /// 130 | /// # Examples 131 | /// 132 | ///``` 133 | /// use geocoding::{Opencage, Point}; 134 | /// 135 | /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 136 | /// let p = Point::new(2.12870, 41.40139); 137 | /// // a full `OpencageResponse` struct 138 | /// let res = oc.reverse_full(&p).unwrap(); 139 | /// // responses may include multiple results 140 | /// let first_result = &res.results[0]; 141 | /// assert_eq!( 142 | /// first_result.components["road"], 143 | /// "Carrer de Calatrava" 144 | /// ); 145 | ///``` 146 | pub fn reverse_full(&self, point: &Point) -> Result, GeocodingError> 147 | where 148 | T: Float + DeserializeOwned + Debug, 149 | { 150 | let q = format!( 151 | "{}, {}", 152 | // OpenCage expects lat, lon order 153 | (&point.y().to_f64().unwrap().to_string()), 154 | &point.x().to_f64().unwrap().to_string() 155 | ); 156 | let mut query = vec![ 157 | ("q", q.as_str()), 158 | ("key", &self.api_key), 159 | ("no_annotations", "0"), 160 | ("no_record", "1"), 161 | ]; 162 | query.extend(self.parameters.as_query()); 163 | 164 | let resp = self 165 | .client 166 | .get(&self.endpoint) 167 | .query(&query) 168 | .send()? 169 | .error_for_status()?; 170 | // it's OK to index into this vec, because reverse-geocoding only returns a single result 171 | if let Some(headers) = resp.headers().get::<_>(XRL) { 172 | let mut lock = self.remaining.try_lock(); 173 | if let Ok(ref mut mutex) = lock { 174 | // not ideal, but typed headers are currently impossible in 0.9.x 175 | let h = headers.to_str()?; 176 | let h: i32 = h.parse()?; 177 | **mutex = Some(h) 178 | } 179 | } 180 | let res: OpencageResponse = resp.json()?; 181 | Ok(res) 182 | } 183 | /// A forward-geocoding lookup of an address, returning an annotated response. 184 | /// 185 | /// it is recommended that you restrict the search space by passing a 186 | /// [bounding box](struct.InputBounds.html) to search within. 187 | /// If you don't need or want to restrict the search using a bounding box (usually not recommended), you 188 | /// may pass the [`NOBOX`](static.NOBOX.html) static value instead. 189 | /// 190 | /// Please see [the documentation](https://opencagedata.com/api#ambiguous-results) for details 191 | /// of best practices in order to obtain good-quality results. 192 | /// 193 | /// This method passes the `no_record` parameter to the API. 194 | /// 195 | /// # Examples 196 | /// 197 | ///``` 198 | /// use geocoding::{Opencage, InputBounds, Point}; 199 | /// 200 | /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 201 | /// let address = "UCL Centre for Advanced Spatial Analysis"; 202 | /// // Optionally restrict the search space using a bounding box. 203 | /// // The first point is the bottom-left corner, the second is the top-right. 204 | /// let bbox = InputBounds::new( 205 | /// Point::new(-0.13806939125061035, 51.51989264641164), 206 | /// Point::new(-0.13427138328552246, 51.52319711775629), 207 | /// ); 208 | /// let res = oc.forward_full(&address, bbox).unwrap(); 209 | /// let first_result = &res.results[0]; 210 | /// // the first result is correct 211 | /// assert!(first_result.formatted.contains("90 Tottenham Court Road")); 212 | ///``` 213 | /// 214 | /// ``` 215 | /// // You can pass NOBOX if you don't need bounds. 216 | /// use geocoding::{Opencage, InputBounds, Point}; 217 | /// use geocoding::opencage::{NOBOX}; 218 | /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 219 | /// let address = "Moabit, Berlin"; 220 | /// let res = oc.forward_full(&address, NOBOX).unwrap(); 221 | /// let first_result = &res.results[0]; 222 | /// assert_eq!( 223 | /// first_result.formatted, 224 | /// "Moabit, Berlin, Germany" 225 | /// ); 226 | /// ``` 227 | /// 228 | /// ``` 229 | /// // There are several ways to construct a Point, such as from a tuple 230 | /// use geocoding::{Opencage, InputBounds, Point}; 231 | /// let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 232 | /// let address = "UCL Centre for Advanced Spatial Analysis"; 233 | /// let bbox = InputBounds::new( 234 | /// (-0.13806939125061035, 51.51989264641164), 235 | /// (-0.13427138328552246, 51.52319711775629), 236 | /// ); 237 | /// let res = oc.forward_full(&address, bbox).unwrap(); 238 | /// let first_result = &res.results[0]; 239 | /// assert!( 240 | /// first_result.formatted.contains( 241 | /// "90 Tottenham Court Road" 242 | /// )); 243 | /// ``` 244 | pub fn forward_full( 245 | &self, 246 | place: &str, 247 | bounds: U, 248 | ) -> Result, GeocodingError> 249 | where 250 | T: Float + DeserializeOwned + Debug, 251 | U: Into>>, 252 | { 253 | let ann = String::from("0"); 254 | let record = String::from("1"); 255 | // we need this to avoid lifetime inconvenience 256 | let bd; 257 | let mut query = vec![ 258 | ("q", place), 259 | ("key", &self.api_key), 260 | ("no_annotations", &ann), 261 | ("no_record", &record), 262 | ]; 263 | 264 | // If search bounds are passed, use them 265 | if let Some(bds) = bounds.into() { 266 | bd = String::from(bds); 267 | query.push(("bounds", &bd)); 268 | } 269 | query.extend(self.parameters.as_query()); 270 | 271 | let resp = self 272 | .client 273 | .get(&self.endpoint) 274 | .query(&query) 275 | .send()? 276 | .error_for_status()?; 277 | if let Some(headers) = resp.headers().get::<_>(XRL) { 278 | let mut lock = self.remaining.try_lock(); 279 | if let Ok(ref mut mutex) = lock { 280 | // not ideal, but typed headers are currently impossible in 0.9.x 281 | let h = headers.to_str()?; 282 | let h: i32 = h.parse()?; 283 | **mutex = Some(h) 284 | } 285 | } 286 | let res: OpencageResponse = resp.json()?; 287 | Ok(res) 288 | } 289 | } 290 | 291 | impl Reverse for Opencage<'_> 292 | where 293 | T: Float + DeserializeOwned + Debug, 294 | { 295 | /// A reverse lookup of a point. More detail on the format of the 296 | /// returned `String` can be found [here](https://blog.opencagedata.com/post/99059889253/good-looking-addresses-solving-the-berlin-berlin) 297 | /// 298 | /// This method passes the `no_annotations` and `no_record` parameters to the API. 299 | fn reverse(&self, point: &Point) -> Result, GeocodingError> { 300 | let q = format!( 301 | "{}, {}", 302 | // OpenCage expects lat, lon order 303 | (&point.y().to_f64().unwrap().to_string()), 304 | &point.x().to_f64().unwrap().to_string() 305 | ); 306 | let mut query = vec![ 307 | ("q", q.as_str()), 308 | ("key", &self.api_key), 309 | ("no_annotations", "1"), 310 | ("no_record", "1"), 311 | ]; 312 | query.extend(self.parameters.as_query()); 313 | 314 | let resp = self 315 | .client 316 | .get(&self.endpoint) 317 | .query(&query) 318 | .send()? 319 | .error_for_status()?; 320 | if let Some(headers) = resp.headers().get::<_>(XRL) { 321 | let mut lock = self.remaining.try_lock(); 322 | if let Ok(ref mut mutex) = lock { 323 | // not ideal, but typed headers are currently impossible in 0.9.x 324 | let h = headers.to_str()?; 325 | let h: i32 = h.parse()?; 326 | **mutex = Some(h) 327 | } 328 | } 329 | let res: OpencageResponse = resp.json()?; 330 | // it's OK to index into this vec, because reverse-geocoding only returns a single result 331 | let address = &res.results[0]; 332 | Ok(Some(address.formatted.to_string())) 333 | } 334 | } 335 | 336 | impl Forward for Opencage<'_> 337 | where 338 | T: Float + DeserializeOwned + Debug, 339 | { 340 | /// A forward-geocoding lookup of an address. Please see [the documentation](https://opencagedata.com/api#ambiguous-results) for details 341 | /// of best practices in order to obtain good-quality results. 342 | /// 343 | /// This method passes the `no_annotations` and `no_record` parameters to the API. 344 | fn forward(&self, place: &str) -> Result>, GeocodingError> { 345 | let mut query = vec![ 346 | ("q", place), 347 | ("key", &self.api_key), 348 | ("no_annotations", "1"), 349 | ("no_record", "1"), 350 | ]; 351 | query.extend(self.parameters.as_query()); 352 | 353 | let resp = self 354 | .client 355 | .get(&self.endpoint) 356 | .query(&query) 357 | .send()? 358 | .error_for_status()?; 359 | if let Some(headers) = resp.headers().get::<_>(XRL) { 360 | let mut lock = self.remaining.try_lock(); 361 | if let Ok(ref mut mutex) = lock { 362 | // not ideal, but typed headers are currently impossible in 0.9.x 363 | let h = headers.to_str()?; 364 | let h: i32 = h.parse()?; 365 | **mutex = Some(h) 366 | } 367 | } 368 | let res: OpencageResponse = resp.json()?; 369 | Ok(res 370 | .results 371 | .iter() 372 | .map(|res| Point::new(res.geometry["lng"], res.geometry["lat"])) 373 | .collect()) 374 | } 375 | } 376 | 377 | /// The top-level full JSON response returned by a forward-geocoding request 378 | /// 379 | /// See [the documentation](https://opencagedata.com/api#response) for more details 380 | /// 381 | ///```json 382 | /// { 383 | /// "documentation": "https://opencagedata.com/api", 384 | /// "licenses": [ 385 | /// { 386 | /// "name": "CC-BY-SA", 387 | /// "url": "http://creativecommons.org/licenses/by-sa/3.0/" 388 | /// }, 389 | /// { 390 | /// "name": "ODbL", 391 | /// "url": "http://opendatacommons.org/licenses/odbl/summary/" 392 | /// } 393 | /// ], 394 | /// "rate": { 395 | /// "limit": 2500, 396 | /// "remaining": 2499, 397 | /// "reset": 1523318400 398 | /// }, 399 | /// "results": [ 400 | /// { 401 | /// "annotations": { 402 | /// "DMS": { 403 | /// "lat": "41° 24' 5.06412'' N", 404 | /// "lng": "2° 7' 43.40064'' E" 405 | /// }, 406 | /// "MGRS": "31TDF2717083684", 407 | /// "Maidenhead": "JN11bj56ki", 408 | /// "Mercator": { 409 | /// "x": 236968.295, 410 | /// "y": 5043465.71 411 | /// }, 412 | /// "OSM": { 413 | /// "edit_url": "https://www.openstreetmap.org/edit?way=355421084#map=17/41.40141/2.12872", 414 | /// "url": "https://www.openstreetmap.org/?mlat=41.40141&mlon=2.12872#map=17/41.40141/2.12872" 415 | /// }, 416 | /// "callingcode": 34, 417 | /// "currency": { 418 | /// "alternate_symbols": [ 419 | /// 420 | /// ], 421 | /// "decimal_mark": ",", 422 | /// "html_entity": "€", 423 | /// "iso_code": "EUR", 424 | /// "iso_numeric": 978, 425 | /// "name": "Euro", 426 | /// "smallest_denomination": 1, 427 | /// "subunit": "Cent", 428 | /// "subunit_to_unit": 100, 429 | /// "symbol": "€", 430 | /// "symbol_first": 1, 431 | /// "thousands_separator": "." 432 | /// }, 433 | /// "flag": "🇪🇸", 434 | /// "geohash": "sp3e82yhdvd7p5x1mbdv", 435 | /// "qibla": 110.53, 436 | /// "sun": { 437 | /// "rise": { 438 | /// "apparent": 1523251260, 439 | /// "astronomical": 1523245440, 440 | /// "civil": 1523249580, 441 | /// "nautical": 1523247540 442 | /// }, 443 | /// "set": { 444 | /// "apparent": 1523298360, 445 | /// "astronomical": 1523304180, 446 | /// "civil": 1523300040, 447 | /// "nautical": 1523302080 448 | /// } 449 | /// }, 450 | /// "timezone": { 451 | /// "name": "Europe/Madrid", 452 | /// "now_in_dst": 1, 453 | /// "offset_sec": 7200, 454 | /// "offset_string": 200, 455 | /// "short_name": "CEST" 456 | /// }, 457 | /// "what3words": { 458 | /// "words": "chins.pictures.passes" 459 | /// } 460 | /// }, 461 | /// "bounds": { 462 | /// "northeast": { 463 | /// "lat": 41.4015815, 464 | /// "lng": 2.128952 465 | /// }, 466 | /// "southwest": { 467 | /// "lat": 41.401227, 468 | /// "lng": 2.1284918 469 | /// } 470 | /// }, 471 | /// "components": { 472 | /// "ISO_3166-1_alpha-2": "ES", 473 | /// "_type": "building", 474 | /// "city": "Barcelona", 475 | /// "city_district": "Sarrià - Sant Gervasi", 476 | /// "country": "Spain", 477 | /// "country_code": "es", 478 | /// "county": "BCN", 479 | /// "house_number": "64", 480 | /// "political_union": "European Union", 481 | /// "postcode": "08017", 482 | /// "road": "Carrer de Calatrava", 483 | /// "state": "Catalonia", 484 | /// "suburb": "les Tres Torres" 485 | /// }, 486 | /// "confidence": 10, 487 | /// "formatted": "Carrer de Calatrava, 64, 08017 Barcelona, Spain", 488 | /// "geometry": { 489 | /// "lat": 41.4014067, 490 | /// "lng": 2.1287224 491 | /// } 492 | /// } 493 | /// ], 494 | /// "status": { 495 | /// "code": 200, 496 | /// "message": "OK" 497 | /// }, 498 | /// "stay_informed": { 499 | /// "blog": "https://blog.opencagedata.com", 500 | /// "twitter": "https://twitter.com/opencagedata" 501 | /// }, 502 | /// "thanks": "For using an OpenCage Data API", 503 | /// "timestamp": { 504 | /// "created_http": "Mon, 09 Apr 2018 12:33:01 GMT", 505 | /// "created_unix": 1523277181 506 | /// }, 507 | /// "total_results": 1 508 | /// } 509 | ///``` 510 | #[derive(Debug, Serialize, Deserialize)] 511 | pub struct OpencageResponse 512 | where 513 | T: Float, 514 | { 515 | pub documentation: String, 516 | pub licenses: Vec>, 517 | pub rate: Option>, 518 | pub results: Vec>, 519 | pub status: Status, 520 | pub stay_informed: HashMap, 521 | pub thanks: String, 522 | pub timestamp: Timestamp, 523 | pub total_results: i32, 524 | } 525 | 526 | /// A forward geocoding result 527 | #[derive(Debug, Clone, Serialize, Deserialize)] 528 | pub struct Results 529 | where 530 | T: Float, 531 | { 532 | pub annotations: Option>, 533 | pub bounds: Option>, 534 | pub components: HashMap, 535 | pub confidence: i8, 536 | pub formatted: String, 537 | pub geometry: HashMap, 538 | } 539 | 540 | /// Annotations pertaining to the geocoding result 541 | #[derive(Debug, Clone, Serialize, Deserialize)] 542 | pub struct Annotations 543 | where 544 | T: Float, 545 | { 546 | pub dms: Option>, 547 | pub mgrs: Option, 548 | pub maidenhead: Option, 549 | pub mercator: Option>, 550 | pub osm: Option>, 551 | pub callingcode: i16, 552 | pub currency: Option, 553 | pub flag: String, 554 | pub geohash: String, 555 | pub qibla: T, 556 | pub sun: Sun, 557 | pub timezone: Timezone, 558 | pub what3words: HashMap, 559 | } 560 | 561 | /// Currency metadata 562 | #[derive(Debug, Clone, Serialize, Deserialize)] 563 | pub struct Currency { 564 | pub alternate_symbols: Option>, 565 | pub decimal_mark: String, 566 | pub html_entity: String, 567 | pub iso_code: String, 568 | #[serde(deserialize_with = "deserialize_string_or_int")] 569 | pub iso_numeric: String, 570 | pub name: String, 571 | pub smallest_denomination: i16, 572 | pub subunit: String, 573 | pub subunit_to_unit: i16, 574 | pub symbol: String, 575 | pub symbol_first: i16, 576 | pub thousands_separator: String, 577 | } 578 | 579 | /// Sunrise and sunset metadata 580 | #[derive(Debug, Clone, Serialize, Deserialize)] 581 | pub struct Sun { 582 | pub rise: HashMap, 583 | pub set: HashMap, 584 | } 585 | 586 | /// Timezone metadata 587 | #[derive(Debug, Clone, Serialize, Deserialize)] 588 | pub struct Timezone { 589 | pub name: String, 590 | pub now_in_dst: i16, 591 | pub offset_sec: i32, 592 | #[serde(deserialize_with = "deserialize_string_or_int")] 593 | pub offset_string: String, 594 | #[serde(deserialize_with = "deserialize_string_or_int")] 595 | pub short_name: String, 596 | } 597 | 598 | /// HTTP status metadata 599 | #[derive(Debug, Serialize, Deserialize)] 600 | pub struct Status { 601 | pub message: String, 602 | pub code: i16, 603 | } 604 | 605 | /// Timestamp metadata 606 | #[derive(Debug, Serialize, Deserialize)] 607 | pub struct Timestamp { 608 | pub created_http: String, 609 | pub created_unix: UnixTime, 610 | } 611 | 612 | /// Primitive unix timestamp 613 | #[derive(Debug, Clone, Copy, Serialize, Deserialize)] 614 | pub struct UnixTime(i64); 615 | 616 | impl UnixTime { 617 | pub const fn as_seconds(self) -> i64 { 618 | self.0 619 | } 620 | pub const fn from_seconds(seconds: i64) -> Self { 621 | Self(seconds) 622 | } 623 | } 624 | 625 | /// Bounding-box metadata 626 | #[derive(Debug, Clone, Serialize, Deserialize)] 627 | pub struct Bounds 628 | where 629 | T: Float, 630 | { 631 | pub northeast: HashMap, 632 | pub southwest: HashMap, 633 | } 634 | 635 | #[cfg(test)] 636 | mod test { 637 | use super::*; 638 | use crate::Coord; 639 | 640 | #[test] 641 | fn reverse_test() { 642 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 643 | let p = Point::new(2.12870, 41.40139); 644 | let res = oc.reverse(&p); 645 | assert_eq!( 646 | res.unwrap(), 647 | Some("Carrer de Calatrava, 64, 08017 Barcelona, Spain".to_string()) 648 | ); 649 | } 650 | 651 | #[test] 652 | fn reverse_test_with_params() { 653 | let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 654 | oc.parameters.language = Some("fr"); 655 | let p = Point::new(2.12870, 41.40139); 656 | let res = oc.reverse(&p); 657 | assert_eq!( 658 | res.unwrap(), 659 | Some("Carrer de Calatrava, 64, 08017 Barcelone, Espagne".to_string()) 660 | ); 661 | } 662 | #[test] 663 | #[allow(deprecated)] 664 | fn forward_test() { 665 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 666 | let address = "Schwabing, München"; 667 | let res = oc.forward(address); 668 | assert_eq!( 669 | res.unwrap(), 670 | vec![Point(Coord { 671 | x: 11.5884858, 672 | y: 48.1700887 673 | })] 674 | ); 675 | } 676 | #[test] 677 | fn reverse_full_test() { 678 | let mut oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 679 | oc.parameters.language = Some("fr"); 680 | let p = Point::new(2.12870, 41.40139); 681 | let res = oc.reverse_full(&p).unwrap(); 682 | let first_result = &res.results[0]; 683 | assert_eq!(first_result.components["road"], "Carrer de Calatrava"); 684 | } 685 | #[test] 686 | fn forward_full_test() { 687 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 688 | let address = "UCL Centre for Advanced Spatial Analysis"; 689 | let bbox = InputBounds { 690 | minimum_lonlat: Point::new(-0.13806939125061035, 51.51989264641164), 691 | maximum_lonlat: Point::new(-0.13427138328552246, 51.52319711775629), 692 | }; 693 | let res = oc.forward_full(address, bbox).unwrap(); 694 | let first_result = &res.results[0]; 695 | assert!(first_result.formatted.contains("UCL")); 696 | } 697 | #[test] 698 | fn forward_full_test_floats() { 699 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 700 | let address = "UCL Centre for Advanced Spatial Analysis"; 701 | let bbox = InputBounds::new( 702 | Point::new(-0.13806939125061035, 51.51989264641164), 703 | Point::new(-0.13427138328552246, 51.52319711775629), 704 | ); 705 | let res = oc.forward_full(address, bbox).unwrap(); 706 | let first_result = &res.results[0]; 707 | assert!( 708 | first_result.formatted.contains("UCL") 709 | && first_result.formatted.contains("90 Tottenham Court Road") 710 | ); 711 | } 712 | #[test] 713 | fn forward_full_test_pointfrom() { 714 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 715 | let address = "UCL Centre for Advanced Spatial Analysis"; 716 | let bbox = InputBounds::new( 717 | Point::from((-0.13806939125061035, 51.51989264641164)), 718 | Point::from((-0.13427138328552246, 51.52319711775629)), 719 | ); 720 | let res = oc.forward_full(address, bbox).unwrap(); 721 | let first_result = &res.results[0]; 722 | assert!( 723 | first_result.formatted.contains("UCL") 724 | && first_result.formatted.contains("90 Tottenham Court Road") 725 | ); 726 | } 727 | #[test] 728 | fn forward_full_test_pointinto() { 729 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 730 | let address = "UCL Centre for Advanced Spatial Analysis"; 731 | let bbox = InputBounds::new( 732 | (-0.13806939125061035, 51.51989264641164), 733 | (-0.13427138328552246, 51.52319711775629), 734 | ); 735 | let res = oc.forward_full(address, bbox).unwrap(); 736 | let first_result = &res.results[0]; 737 | assert!(first_result 738 | .formatted 739 | .contains("Tottenham Court Road, London")); 740 | } 741 | #[test] 742 | fn forward_full_test_nobox() { 743 | let oc = Opencage::new("dcdbf0d783374909b3debee728c7cc10".to_string()); 744 | let address = "Moabit, Berlin, Germany"; 745 | let res = oc.forward_full(address, NOBOX).unwrap(); 746 | let first_result = &res.results[0]; 747 | assert_eq!(first_result.formatted, "Moabit, Berlin, Germany"); 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /src/openstreetmap.rs: -------------------------------------------------------------------------------- 1 | //! The [OpenStreetMap Nominatim](https://nominatim.org/) provider. 2 | //! 3 | //! Geocoding methods are implemented on the [`Openstreetmap`](struct.Openstreetmap.html) struct. 4 | //! Please see the [API documentation](https://nominatim.org/release-docs/develop/) for details. 5 | //! 6 | //! While OpenStreetMap's Nominatim API is free, see the [Nominatim Usage Policy](https://operations.osmfoundation.org/policies/nominatim/) 7 | //! for details on usage requirements, including a maximum of 1 request per second. 8 | //! 9 | //! ### Example 10 | //! 11 | //! ``` 12 | //! use geocoding::{Openstreetmap, Forward, Point}; 13 | //! 14 | //! let osm = Openstreetmap::new(); 15 | //! let address = "Schwabing, München"; 16 | //! let res = osm.forward(&address); 17 | //! assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]); 18 | //! ``` 19 | use crate::GeocodingError; 20 | use crate::InputBounds; 21 | use crate::Point; 22 | use crate::UA_STRING; 23 | use crate::{Client, HeaderMap, HeaderValue, USER_AGENT}; 24 | use crate::{Deserialize, Serialize}; 25 | use crate::{Forward, Reverse}; 26 | use num_traits::Float; 27 | use std::fmt::Debug; 28 | 29 | /// An instance of the Openstreetmap geocoding service 30 | pub struct Openstreetmap { 31 | client: Client, 32 | endpoint: String, 33 | } 34 | 35 | /// An instance of a parameter builder for Openstreetmap geocoding 36 | pub struct OpenstreetmapParams<'a, T> 37 | where 38 | T: Float + Debug, 39 | { 40 | query: &'a str, 41 | addressdetails: bool, 42 | viewbox: Option<&'a InputBounds>, 43 | } 44 | 45 | impl<'a, T> OpenstreetmapParams<'a, T> 46 | where 47 | T: Float + Debug, 48 | { 49 | /// Create a new OpenStreetMap parameter builder 50 | /// # Example: 51 | /// 52 | /// ``` 53 | /// use geocoding::{Openstreetmap, InputBounds, Point}; 54 | /// use geocoding::openstreetmap::{OpenstreetmapParams}; 55 | /// 56 | /// let viewbox = InputBounds::new( 57 | /// (-0.13806939125061035, 51.51989264641164), 58 | /// (-0.13427138328552246, 51.52319711775629), 59 | /// ); 60 | /// let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis") 61 | /// .with_addressdetails(true) 62 | /// .with_viewbox(&viewbox) 63 | /// .build(); 64 | /// ``` 65 | pub fn new(query: &'a str) -> OpenstreetmapParams<'a, T> { 66 | OpenstreetmapParams { 67 | query, 68 | addressdetails: false, 69 | viewbox: None, 70 | } 71 | } 72 | 73 | /// Set the `addressdetails` property 74 | pub fn with_addressdetails(&mut self, addressdetails: bool) -> &mut Self { 75 | self.addressdetails = addressdetails; 76 | self 77 | } 78 | 79 | /// Set the `viewbox` property 80 | pub fn with_viewbox(&mut self, viewbox: &'a InputBounds) -> &mut Self { 81 | self.viewbox = Some(viewbox); 82 | self 83 | } 84 | 85 | /// Build and return an instance of OpenstreetmapParams 86 | pub fn build(&self) -> OpenstreetmapParams<'a, T> { 87 | OpenstreetmapParams { 88 | query: self.query, 89 | addressdetails: self.addressdetails, 90 | viewbox: self.viewbox, 91 | } 92 | } 93 | } 94 | 95 | impl Openstreetmap { 96 | /// Create a new Openstreetmap geocoding instance using the default endpoint 97 | pub fn new() -> Self { 98 | Openstreetmap::new_with_endpoint("https://nominatim.openstreetmap.org/".to_string()) 99 | } 100 | 101 | /// Create a new Openstreetmap geocoding instance with a custom endpoint. 102 | /// 103 | /// Endpoint should include a trailing slash (i.e. "https://nominatim.openstreetmap.org/") 104 | pub fn new_with_endpoint(endpoint: String) -> Self { 105 | let mut headers = HeaderMap::new(); 106 | headers.insert(USER_AGENT, HeaderValue::from_static(UA_STRING)); 107 | let client = Client::builder() 108 | .default_headers(headers) 109 | .build() 110 | .expect("Couldn't build a client!"); 111 | Openstreetmap { client, endpoint } 112 | } 113 | 114 | /// A forward-geocoding lookup of an address, returning a full detailed response 115 | /// 116 | /// Accepts an [`OpenstreetmapParams`](struct.OpenstreetmapParams.html) struct for specifying 117 | /// options, including whether to include address details in the response and whether to filter 118 | /// by a bounding box. 119 | /// 120 | /// Please see [the documentation](https://nominatim.org/release-docs/develop/api/Search/) for details. 121 | /// 122 | /// This method passes the `format` parameter to the API. 123 | /// 124 | /// # Examples 125 | /// 126 | /// ``` 127 | /// use geocoding::{Openstreetmap, InputBounds, Point}; 128 | /// use geocoding::openstreetmap::{OpenstreetmapParams, OpenstreetmapResponse}; 129 | /// 130 | /// let osm = Openstreetmap::new(); 131 | /// let viewbox = InputBounds::new( 132 | /// (-0.13806939125061035, 51.51989264641164), 133 | /// (-0.13427138328552246, 51.52319711775629), 134 | /// ); 135 | /// let params = OpenstreetmapParams::new(&"UCL Centre for Advanced Spatial Analysis") 136 | /// .with_addressdetails(true) 137 | /// .with_viewbox(&viewbox) 138 | /// .build(); 139 | /// let res: OpenstreetmapResponse = osm.forward_full(¶ms).unwrap(); 140 | /// let result = res.features[0].properties.clone(); 141 | /// assert!(result.display_name.contains("Tottenham Court Road")); 142 | /// ``` 143 | pub fn forward_full( 144 | &self, 145 | params: &OpenstreetmapParams, 146 | ) -> Result, GeocodingError> 147 | where 148 | T: Float + Debug, 149 | for<'de> T: Deserialize<'de>, 150 | { 151 | let format = String::from("geojson"); 152 | let addressdetails = String::from(if params.addressdetails { "1" } else { "0" }); 153 | // For lifetime issues 154 | let viewbox; 155 | 156 | let mut query = vec![ 157 | (&"q", params.query), 158 | (&"format", &format), 159 | (&"addressdetails", &addressdetails), 160 | ]; 161 | 162 | if let Some(vb) = params.viewbox { 163 | viewbox = String::from(*vb); 164 | query.push((&"viewbox", &viewbox)); 165 | } 166 | 167 | let resp = self 168 | .client 169 | .get(format!("{}search", self.endpoint)) 170 | .query(&query) 171 | .send()? 172 | .error_for_status()?; 173 | let res: OpenstreetmapResponse = resp.json()?; 174 | Ok(res) 175 | } 176 | } 177 | 178 | impl Default for Openstreetmap { 179 | fn default() -> Self { 180 | Self::new() 181 | } 182 | } 183 | 184 | impl Forward for Openstreetmap 185 | where 186 | T: Float + Debug, 187 | for<'de> T: Deserialize<'de>, 188 | { 189 | /// A forward-geocoding lookup of an address. Please see [the documentation](https://nominatim.org/release-docs/develop/api/Search/) for details. 190 | /// 191 | /// This method passes the `format` parameter to the API. 192 | fn forward(&self, place: &str) -> Result>, GeocodingError> { 193 | let resp = self 194 | .client 195 | .get(format!("{}search", self.endpoint)) 196 | .query(&[(&"q", place), (&"format", &String::from("geojson"))]) 197 | .send()? 198 | .error_for_status()?; 199 | let res: OpenstreetmapResponse = resp.json()?; 200 | Ok(res 201 | .features 202 | .iter() 203 | .map(|res| Point::new(res.geometry.coordinates.0, res.geometry.coordinates.1)) 204 | .collect()) 205 | } 206 | } 207 | 208 | impl Reverse for Openstreetmap 209 | where 210 | T: Float + Debug, 211 | for<'de> T: Deserialize<'de>, 212 | { 213 | /// A reverse lookup of a point. More detail on the format of the 214 | /// returned `String` can be found [here](https://nominatim.org/release-docs/develop/api/Reverse/) 215 | /// 216 | /// This method passes the `format` parameter to the API. 217 | fn reverse(&self, point: &Point) -> Result, GeocodingError> { 218 | let resp = self 219 | .client 220 | .get(format!("{}reverse", self.endpoint)) 221 | .query(&[ 222 | (&"lon", &point.x().to_f64().unwrap().to_string()), 223 | (&"lat", &point.y().to_f64().unwrap().to_string()), 224 | (&"format", &String::from("geojson")), 225 | ]) 226 | .send()? 227 | .error_for_status()?; 228 | let res: OpenstreetmapResponse = resp.json()?; 229 | let address = &res.features[0]; 230 | Ok(Some(address.properties.display_name.to_string())) 231 | } 232 | } 233 | 234 | /// The top-level full GeoJSON response returned by a forward-geocoding request 235 | /// 236 | /// See [the documentation](https://nominatim.org/release-docs/develop/api/Search/#geojson) for more details 237 | /// 238 | ///```json 239 | ///{ 240 | /// "type": "FeatureCollection", 241 | /// "licence": "Data © OpenStreetMap contributors, ODbL 1.0. https://osm.org/copyright", 242 | /// "features": [ 243 | /// { 244 | /// "type": "Feature", 245 | /// "properties": { 246 | /// "place_id": 263681481, 247 | /// "osm_type": "way", 248 | /// "osm_id": 355421084, 249 | /// "display_name": "64, Carrer de Calatrava, les Tres Torres, Sarrià - Sant Gervasi, Barcelona, BCN, Catalonia, 08017, Spain", 250 | /// "place_rank": 30, 251 | /// "category": "building", 252 | /// "type": "apartments", 253 | /// "importance": 0.7409999999999999, 254 | /// "address": { 255 | /// "house_number": "64", 256 | /// "road": "Carrer de Calatrava", 257 | /// "suburb": "les Tres Torres", 258 | /// "city_district": "Sarrià - Sant Gervasi", 259 | /// "city": "Barcelona", 260 | /// "county": "BCN", 261 | /// "state": "Catalonia", 262 | /// "postcode": "08017", 263 | /// "country": "Spain", 264 | /// "country_code": "es" 265 | /// } 266 | /// }, 267 | /// "bbox": [ 268 | /// 2.1284918, 269 | /// 41.401227, 270 | /// 2.128952, 271 | /// 41.4015815 272 | /// ], 273 | /// "geometry": { 274 | /// "type": "Point", 275 | /// "coordinates": [ 276 | /// 2.12872241167437, 277 | /// 41.40140675 278 | /// ] 279 | /// } 280 | /// } 281 | /// ] 282 | ///} 283 | ///``` 284 | #[derive(Debug, Serialize, Deserialize)] 285 | pub struct OpenstreetmapResponse 286 | where 287 | T: Float + Debug, 288 | { 289 | pub r#type: String, 290 | pub licence: String, 291 | pub features: Vec>, 292 | } 293 | 294 | /// A geocoding result 295 | #[derive(Debug, Serialize, Deserialize)] 296 | pub struct OpenstreetmapResult 297 | where 298 | T: Float + Debug, 299 | { 300 | pub r#type: String, 301 | pub properties: ResultProperties, 302 | pub bbox: (T, T, T, T), 303 | pub geometry: ResultGeometry, 304 | } 305 | 306 | /// Geocoding result properties 307 | #[derive(Clone, Debug, Serialize, Deserialize)] 308 | pub struct ResultProperties { 309 | pub place_id: u64, 310 | pub osm_type: String, 311 | pub osm_id: u64, 312 | pub display_name: String, 313 | pub place_rank: u64, 314 | pub category: String, 315 | pub r#type: String, 316 | pub importance: f64, 317 | pub address: Option, 318 | } 319 | 320 | /// Address details in the result object 321 | #[derive(Clone, Debug, Serialize, Deserialize)] 322 | pub struct AddressDetails { 323 | pub city: Option, 324 | pub city_district: Option, 325 | pub construction: Option, 326 | pub continent: Option, 327 | pub country: Option, 328 | pub country_code: Option, 329 | pub house_number: Option, 330 | pub neighbourhood: Option, 331 | pub postcode: Option, 332 | pub public_building: Option, 333 | pub state: Option, 334 | pub suburb: Option, 335 | pub road: Option, 336 | pub village: Option, 337 | } 338 | 339 | /// A geocoding result geometry 340 | #[derive(Debug, Serialize, Deserialize)] 341 | pub struct ResultGeometry 342 | where 343 | T: Float + Debug, 344 | { 345 | pub r#type: String, 346 | pub coordinates: (T, T), 347 | } 348 | 349 | #[cfg(test)] 350 | mod test { 351 | use super::*; 352 | 353 | #[test] 354 | fn new_with_endpoint_forward_test() { 355 | let osm = 356 | Openstreetmap::new_with_endpoint("https://nominatim.openstreetmap.org/".to_string()); 357 | let address = "Schwabing, München"; 358 | let res = osm.forward(address); 359 | assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]); 360 | } 361 | 362 | #[test] 363 | fn forward_full_test() { 364 | let osm = Openstreetmap::new(); 365 | let viewbox = InputBounds::new( 366 | (-0.13806939125061035, 51.51989264641164), 367 | (-0.13427138328552246, 51.52319711775629), 368 | ); 369 | let params = OpenstreetmapParams::new("UCL Centre for Advanced Spatial Analysis") 370 | .with_addressdetails(true) 371 | .with_viewbox(&viewbox) 372 | .build(); 373 | let res: OpenstreetmapResponse = osm.forward_full(¶ms).unwrap(); 374 | let result = res.features[0].properties.clone(); 375 | assert!(result.display_name.contains("Tottenham Court Road")); 376 | assert_eq!(result.address.unwrap().city.unwrap(), "London"); 377 | } 378 | 379 | #[test] 380 | fn forward_test() { 381 | let osm = Openstreetmap::new(); 382 | let address = "Schwabing, München"; 383 | let res = osm.forward(address); 384 | assert_eq!(res.unwrap(), vec![Point::new(11.5884858, 48.1700887)]); 385 | } 386 | 387 | #[test] 388 | fn reverse_test() { 389 | let osm = Openstreetmap::new(); 390 | let p = Point::new(2.12870, 41.40139); 391 | let res = osm.reverse(&p); 392 | assert!(res 393 | .unwrap() 394 | .unwrap() 395 | .contains("Barcelona, Barcelonès, Barcelona, Catalunya")); 396 | } 397 | } 398 | --------------------------------------------------------------------------------