├── .Rbuildignore ├── .gitattributes ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ ├── cran-checks.yaml │ ├── pkgdown.yaml │ └── rhub.yaml ├── .gitignore ├── CRAN-SUBMISSION ├── DESCRIPTION ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── arcgeocode-package.R ├── core-batch-geocode.R ├── core-find-candidates.R ├── core-reverse-geocode.R ├── core-suggest.R ├── doc-data.R ├── extendr-wrappers.R ├── import-standalone-obj-type.R ├── import-standalone-types-check.R ├── list-geocoders.R ├── sysdata.rda ├── utils-camel-case.R ├── utils-checks.R ├── utils-crs.R ├── utils-geocoder-obj.R └── utils-iso-3166.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── configure ├── configure.win ├── cran-comments.md ├── data-raw ├── default_geocoder.R └── esri-spatial-ref.R ├── data ├── esri_wkids.rda └── world_geocoder.rda ├── dev ├── arcgisgeocode-yelp-timing.csv ├── batch-geocode.R ├── bench-marks.R ├── calcite-doc.R ├── custom-locator-res.R ├── geocode-csv.R ├── json.R ├── make-errors.R ├── shiny-app.R ├── shiny-reverse-geocode.R ├── suggest-find-candidates.R └── yelp-timing.csv ├── man ├── arcgisgeocode-package.Rd ├── esri_wkids.Rd ├── figures │ └── logo.svg ├── find_address_candidates.Rd ├── geocode_addresses.Rd ├── geocode_server.Rd ├── iso_3166_codes.Rd ├── list_geocoders.Rd ├── reverse_geocode.Rd ├── storage.Rd ├── suggest_places.Rd └── world_geocoder.Rd ├── pkgdown └── favicon │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon.ico ├── src ├── .gitignore ├── Makevars.in ├── Makevars.ucrt ├── Makevars.win.in ├── arcgisgeocode-win.def ├── entrypoint.c └── rust │ ├── Cargo.lock │ ├── Cargo.toml │ ├── src │ ├── batch_geocode.rs │ ├── find_candidates.rs │ ├── iso3166.rs │ ├── lib.rs │ ├── parse_custom_attrs.rs │ ├── reverse.rs │ └── suggest.rs │ ├── vendor-config.toml │ └── vendor.tar.xz ├── tests ├── testdata │ ├── batch-response-1.json │ ├── custom-locator.json │ └── public-locator-res.json ├── testthat.R └── testthat │ ├── test-obj_as_points.R │ └── test-search-extent.R └── tools ├── config.R └── msrv.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^arcgeocode\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^cran-comments\.md$ 6 | ^src/\.cargo$ 7 | ^\.github$ 8 | ^data-raw$ 9 | ^dev$ 10 | ^_pkgdown\.yml$ 11 | ^docs$ 12 | ^pkgdown$ 13 | ^tests/testdata$ 14 | ^README\.*$ 15 | ^src/rust/vendor$ 16 | ^\./src/\.cargo$ 17 | ^CRAN-SUBMISSION$ 18 | ^src/rust/target$ 19 | ^src/Makevars$ 20 | ^src/Makevars\.win$ 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://github.com/topepo/Cubist/blob/193c8b61933e2cef65658cf0e35aa25dcd25f3b6/.gitattributes 2 | # On GitHub Actions with Windows, git will use CRLF by default when checking out 3 | # the repo. If this file has CRLF line endings, R's check process will complain. 4 | configure.ac text eol=lf -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | 3 | 4 | on: 5 | push: 6 | branches: [main, master] 7 | pull_request: 8 | branches: [main, master] 9 | 10 | name: R-CMD-check 11 | 12 | jobs: 13 | R-CMD-check: 14 | runs-on: ${{ matrix.config.os }} 15 | 16 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | config: 22 | - {os: macos-latest, r: 'release'} 23 | - {os: windows-latest, r: 'release'} 24 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 25 | - {os: ubuntu-latest, r: 'release'} 26 | - {os: ubuntu-latest, r: 'oldrel-1'} 27 | - {os: ubuntu-latest, r: 'oldrel-2'} 28 | - {os: ubuntu-latest, r: 'oldrel-3'} 29 | 30 | env: 31 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 32 | R_KEEP_PKG_SOURCE: yes 33 | NOT_CRAN: ${{ matrix.config.r != 'release' }} 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - uses: r-lib/actions/setup-pandoc@v2 39 | 40 | - uses: r-lib/actions/setup-r@v2 41 | with: 42 | r-version: ${{ matrix.config.r }} 43 | http-user-agent: ${{ matrix.config.http-user-agent }} 44 | use-public-rspm: true 45 | 46 | - uses: r-lib/actions/setup-r-dependencies@v2 47 | with: 48 | extra-packages: any::rcmdcheck 49 | needs: check 50 | 51 | - uses: r-lib/actions/check-r-package@v2 52 | with: 53 | upload-snapshots: true 54 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 55 | -------------------------------------------------------------------------------- /.github/workflows/cran-checks.yaml: -------------------------------------------------------------------------------- 1 | name: Check CRAN status 2 | 3 | on: 4 | schedule: 5 | # Runs daily at 4:00 PM UTC (9:00 AM PST) 6 | - cron: '0 16 * * *' 7 | # allows for manually running of the check 8 | workflow_dispatch: 9 | 10 | jobs: 11 | check_cran_status: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Get CRAN checks 16 | uses: ricochet-rs/cran-checks/check-pkg@main 17 | with: 18 | pkg: arcgisgeocode 19 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | permissions: 23 | contents: write 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - uses: r-lib/actions/setup-pandoc@v2 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | with: 31 | use-public-rspm: true 32 | 33 | - uses: r-lib/actions/setup-r-dependencies@v2 34 | with: 35 | extra-packages: any::pkgdown, local::. 36 | needs: website 37 | 38 | - name: Build site 39 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 40 | shell: Rscript {0} 41 | 42 | - name: Deploy to GitHub pages 🚀 43 | if: github.event_name != 'pull_request' 44 | uses: JamesIves/github-pages-deploy-action@v4.5.0 45 | with: 46 | clean: false 47 | branch: gh-pages 48 | folder: docs 49 | -------------------------------------------------------------------------------- /.github/workflows/rhub.yaml: -------------------------------------------------------------------------------- 1 | # R-hub's generic GitHub Actions workflow file. It's canonical location is at 2 | # https://github.com/r-hub/actions/blob/v1/workflows/rhub.yaml 3 | # You can update this file to a newer version using the rhub2 package: 4 | # 5 | # rhub::rhub_setup() 6 | # 7 | # It is unlikely that you need to modify this file manually. 8 | 9 | name: R-hub 10 | run-name: "${{ github.event.inputs.id }}: ${{ github.event.inputs.name || format('Manually run by {0}', github.triggering_actor) }}" 11 | 12 | on: 13 | workflow_dispatch: 14 | inputs: 15 | config: 16 | description: 'A comma separated list of R-hub platforms to use.' 17 | type: string 18 | default: 'linux,windows,macos' 19 | name: 20 | description: 'Run name. You can leave this empty now.' 21 | type: string 22 | id: 23 | description: 'Unique ID. You can leave this empty now.' 24 | type: string 25 | 26 | jobs: 27 | 28 | setup: 29 | runs-on: ubuntu-latest 30 | outputs: 31 | containers: ${{ steps.rhub-setup.outputs.containers }} 32 | platforms: ${{ steps.rhub-setup.outputs.platforms }} 33 | 34 | steps: 35 | # NO NEED TO CHECKOUT HERE 36 | - uses: r-hub/actions/setup@v1 37 | with: 38 | config: ${{ github.event.inputs.config }} 39 | id: rhub-setup 40 | 41 | linux-containers: 42 | needs: setup 43 | if: ${{ needs.setup.outputs.containers != '[]' }} 44 | runs-on: ubuntu-latest 45 | name: ${{ matrix.config.label }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | config: ${{ fromJson(needs.setup.outputs.containers) }} 50 | container: 51 | image: ${{ matrix.config.container }} 52 | 53 | steps: 54 | - uses: r-hub/actions/checkout@v1 55 | - uses: r-hub/actions/platform-info@v1 56 | with: 57 | token: ${{ secrets.RHUB_TOKEN }} 58 | job-config: ${{ matrix.config.job-config }} 59 | - uses: r-hub/actions/setup-deps@v1 60 | with: 61 | token: ${{ secrets.RHUB_TOKEN }} 62 | job-config: ${{ matrix.config.job-config }} 63 | - uses: r-hub/actions/run-check@v1 64 | with: 65 | token: ${{ secrets.RHUB_TOKEN }} 66 | job-config: ${{ matrix.config.job-config }} 67 | 68 | other-platforms: 69 | needs: setup 70 | if: ${{ needs.setup.outputs.platforms != '[]' }} 71 | runs-on: ${{ matrix.config.os }} 72 | name: ${{ matrix.config.label }} 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | config: ${{ fromJson(needs.setup.outputs.platforms) }} 77 | 78 | steps: 79 | - uses: r-hub/actions/checkout@v1 80 | - uses: r-hub/actions/setup-r@v1 81 | with: 82 | job-config: ${{ matrix.config.job-config }} 83 | token: ${{ secrets.RHUB_TOKEN }} 84 | - uses: r-hub/actions/platform-info@v1 85 | with: 86 | token: ${{ secrets.RHUB_TOKEN }} 87 | job-config: ${{ matrix.config.job-config }} 88 | - uses: r-hub/actions/setup-deps@v1 89 | with: 90 | job-config: ${{ matrix.config.job-config }} 91 | token: ${{ secrets.RHUB_TOKEN }} 92 | - uses: r-hub/actions/run-check@v1 93 | with: 94 | job-config: ${{ matrix.config.job-config }} 95 | token: ${{ secrets.RHUB_TOKEN }} 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | autom4te.cache/ 2 | config.log 3 | configu.status 4 | configure~ 5 | src/vendor 6 | .Rproj.user 7 | docs 8 | src/rust/vendor 9 | src/Makevars 10 | src/Makevars.win 11 | -------------------------------------------------------------------------------- /CRAN-SUBMISSION: -------------------------------------------------------------------------------- 1 | Version: 0.2.2 2 | Date: 2024-09-26 19:20:02 UTC 3 | SHA: 4a1d951ca985ad090be34657738c160f4bf23566 4 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: arcgisgeocode 2 | Title: A Robust Interface to ArcGIS 'Geocoding Services' 3 | Version: 0.2.3 4 | Authors@R: 5 | person("Josiah", "Parry", , "josiah.parry@gmail.com", role = c("aut", "cre"), 6 | comment = c(ORCID = "0000-0001-9910-865X")) 7 | Description: A very fast and robust interface to ArcGIS 'Geocoding 8 | Services'. Provides capabilities for reverse geocoding, finding 9 | address candidates, character-by-character search autosuggestion, and 10 | batch geocoding. The public 'ArcGIS World Geocoder' is accessible for 11 | free use via 'arcgisgeocode' for all services except batch geocoding. 12 | 'arcgisgeocode' also integrates with 'arcgisutils' to provide access 13 | to custom locators or private 'ArcGIS World Geocoder' hosted on 14 | 'ArcGIS Enterprise'. Learn more in the 'Geocode service' API reference 15 | . 16 | License: Apache License (>= 2) 17 | URL: https://github.com/r-arcgis/arcgisgeocode, 18 | https://developers.arcgis.com/r-bridge/api-reference/arcgisgeocode 19 | Imports: 20 | arcgisutils (>= 0.3.0), 21 | cli, 22 | curl, 23 | httr2 (>= 1.1.1), 24 | jsonify, 25 | RcppSimdJson (>= 0.1.13), 26 | rlang (>= 1.1.0), 27 | sf 28 | Suggests: 29 | data.table, 30 | dplyr, 31 | testthat (>= 3.0.0) 32 | Config/rextendr/version: 0.3.1.9001 33 | Config/testthat/edition: 3 34 | Encoding: UTF-8 35 | Language: en 36 | Roxygen: list(markdown = TRUE) 37 | RoxygenNote: 7.3.2 38 | SystemRequirements: Cargo (Rust's package manager), rustc >= 1.67 39 | Depends: 40 | R (>= 4.2) 41 | LazyData: true 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print,GeocodeServer) 4 | export(default_geocoder) 5 | export(find_address_candidates) 6 | export(geocode_addresses) 7 | export(geocode_server) 8 | export(iso_3166_codes) 9 | export(list_geocoders) 10 | export(reverse_geocode) 11 | export(suggest_places) 12 | import(arcgisutils) 13 | useDynLib(arcgisgeocode, .registration = TRUE) 14 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # arcgisgeocode 0.2.3 2 | 3 | - Bumps version of extendr-api to address CRAN checks. 4 | 5 | # arcgisgeocode 0.2.2 6 | 7 | - Bumps version of extendr-api to address CRAN checks 8 | - Bumps version of httr2 due to regression [#34](https://github.com/R-ArcGIS/arcgisgeocode/issues/34) 9 | - Bug fixes with unexported objects and NA handling by [@elipousson] [#37](https://github.com/R-ArcGIS/arcgisgeocode/pull/37) 10 | 11 | # arcgisgeocode 0.2.1 12 | 13 | - Address CRAN error on MacOS oldrel 14 | 15 | # arcgisgeocode 0.2.0 16 | 17 | - The minimum version of R supported is R `4.2` 18 | - [#22](https://github.com/R-ArcGIS/arcgisgeocode/pull/22) Fixed a bug where the `default_geocoder()` would not work without attaching the entire package. See . 19 | - [#21](https://github.com/R-ArcGIS/arcgisgeocode/pull/21) Fixed a bug where custom locators did not parse and sort the results appropriately. 20 | 21 | # arcgisgeocode 0.1.0 22 | 23 | - Initial CRAN release 24 | -------------------------------------------------------------------------------- /R/arcgeocode-package.R: -------------------------------------------------------------------------------- 1 | #' @import arcgisutils 2 | #' @keywords internal 3 | "_PACKAGE" 4 | 5 | ## usethis namespace: start 6 | ## usethis namespace: end 7 | NULL 8 | 9 | 10 | #' Storing Geocoding Results 11 | #' 12 | #' The results of geocoding operations cannot be stored or 13 | #' persisted unless the `for_storage` argument is set to 14 | #' `TRUE`. The default argument value is `for_storage = FALSE`, which indicates the results of the operation can't be stored, but they can be temporarily displayed on a map, for instance. If you store the results, in a database, for example, you need to set this parameter to true. 15 | #' 16 | #' See [the official documentation](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm#ESRI_SECTION3_BBCB5704B46B4CDF8377749B873B1A7F) for more context. 17 | #' @name storage 18 | NULL 19 | -------------------------------------------------------------------------------- /R/core-find-candidates.R: -------------------------------------------------------------------------------- 1 | #' Find Address Candidates 2 | #' 3 | #' Given an address, returns geocode result candidates. 4 | #' 5 | #' @details 6 | #' Utilizes the [`/findAddressCandidates`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm) endpoint. 7 | #' 8 | #' The endpoint can only handle one request at a time. To 9 | #' make the operation as performant as possible, requests are sent in parallel 10 | #' using [`httr2::req_perform_parallel()`]. The JSON responses are then processed 11 | #' using Rust and returned as an sf object. 12 | #' 13 | #' @examples 14 | #' candidates_from_single <- find_address_candidates( 15 | #' single_line = "Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 30316, USA" 16 | #' ) 17 | #' 18 | #' candidates_from_parts <- find_address_candidates( 19 | #' address = c("Bellwood Coffee", "Joe's coffeehouse"), 20 | #' address2 = c("1366 Glenwood Ave SE", "510 Flat Shoals Ave"), 21 | #' city = "Atlanta", 22 | #' region = "GA", 23 | #' postal = "30316", 24 | #' country_code = "USA" 25 | #' ) 26 | #' 27 | #' str(candidates_from_parts) 28 | #' 29 | #' @param single_line a character vector of addresses to geocode. If provided 30 | #' other `address` fields cannot be used. If `address` is not provided, 31 | #' `single_line` must be. 32 | #' @param address a character vector of the first part of a street address. 33 | #' Typically used for the street name and house number. But can also be a place 34 | #' or building name. If `single_line` is not provided, `address` must be. 35 | #' @param address2 a character vector of the second part of a street address. 36 | #' Typically includes a house number, sub-unit, street, building, or place name. 37 | #' Optional. 38 | #' @param address3 a character vector of the third part of an address. Optional. 39 | #' @param neighborhood a character vector of the smallest administrative division 40 | #' associated with an address. Typically, a neighborhood or a section of a 41 | #' larger populated place. Optional. 42 | #' @param city a character vector of the next largest administrative division 43 | #' associated with an address, typically, a city or municipality. A city is a 44 | #' subdivision of a subregion or a region. Optional. 45 | #' @param subregion a character vector of the next largest administrative division 46 | #' associated with an address. Depending on the country, a subregion can 47 | #' represent a county, state, or province. Optional. 48 | #' @param region a character vector of the largest administrative division 49 | #' associated with an address, typically, a state or province. Optional. 50 | #' @param postal a character vector of the standard postal code for an address, 51 | #' typically, a three– to six-digit alphanumeric code. Optional. 52 | #' @param postal_ext a character vector of the postal code extension, such as 53 | #' the United States Postal Service ZIP+4 code, provides finer resolution or 54 | #' higher accuracy when also passing postal. Optional. 55 | #' @param max_locations the maximum number of results to return. The default is 56 | #' 15 with a maximum of 50. Optional. 57 | #' @param match_out_of_range set to `TRUE` by service by default. Matches locations Optional. 58 | #' @param source_country default `NULL`. An ISO 3166 country code. 59 | #' See [`iso_3166_codes()`] for valid ISO codes. Optional. 60 | #' @param magic_key a unique identifier returned from [`suggest_places()`]. 61 | #' When a `magic_key` is provided, results are returned faster. Optional. 62 | #' @inheritParams suggest_places 63 | #' @inheritParams reverse_geocode 64 | #' @returns 65 | #' An `sf` object with 60 columns. 66 | #' @export 67 | find_address_candidates <- function( 68 | single_line = NULL, 69 | address = NULL, 70 | address2 = NULL, 71 | address3 = NULL, 72 | neighborhood = NULL, 73 | city = NULL, 74 | subregion = NULL, 75 | region = NULL, 76 | postal = NULL, 77 | postal_ext = NULL, 78 | country_code = NULL, 79 | # Is search extent specified for each location or 80 | # as a whole? 81 | search_extent = NULL, # should be a bbox object 82 | location = NULL, # sfc_POINT or sfg 83 | category = NULL, # Needs validation 84 | crs = NULL, # validate 85 | max_locations = NULL, # max 50 86 | for_storage = FALSE, # warn 87 | match_out_of_range = NULL, 88 | location_type = NULL, 89 | lang_code = NULL, 90 | source_country = NULL, # iso code 91 | preferred_label_values = NULL, 92 | magic_key = NULL, 93 | geocoder = default_geocoder(), 94 | token = arc_token(), 95 | .progress = TRUE) { 96 | check_geocoder(geocoder, call = rlang::caller_env()) 97 | 98 | if (!"geocode" %in% capabilities(geocoder)) { 99 | arg <- rlang::caller_arg(geocoder) 100 | cli::cli_abort("{.arg {arg}} does not support the {.path /findAddressCandidates} endpoint") 101 | } 102 | 103 | # this also checks the token 104 | check_for_storage(for_storage, token, call = rlang::caller_env()) 105 | 106 | check_bool(.progress, allow_na = FALSE, allow_null = FALSE) 107 | 108 | # type checking for all character types 109 | # they can be either NULL or not. When not, they cannot have NA values 110 | check_character(single_line, allow_null = TRUE) 111 | check_character(address, allow_null = TRUE) 112 | check_character(address2, allow_null = TRUE) 113 | check_character(address3, allow_null = TRUE) 114 | check_character(neighborhood, allow_null = TRUE) 115 | check_character(city, allow_null = TRUE) 116 | check_character(subregion, allow_null = TRUE) 117 | check_character(region, allow_null = TRUE) 118 | check_character(postal, allow_null = TRUE) 119 | check_character(postal_ext, allow_null = TRUE) 120 | check_character(category, allow_null = TRUE) 121 | check_character(location_type, allow_null = TRUE) 122 | check_character(preferred_label_values, allow_null = TRUE) 123 | check_character(magic_key, allow_null = TRUE) 124 | 125 | # iso 3166 checks 126 | check_iso_3166(country_code, allow_null = TRUE, scalar = FALSE) 127 | check_iso_3166(lang_code, allow_null = TRUE, scalar = FALSE) 128 | check_iso_3166(source_country, allow_null = TRUE, scalar = FALSE) 129 | 130 | check_logical( 131 | match_out_of_range, 132 | allow_null = TRUE, 133 | allow_na = FALSE 134 | ) 135 | 136 | check_number_whole( 137 | max_locations, 138 | min = 1, 139 | max = 50, 140 | allow_null = TRUE, 141 | call = rlang::caller_env() 142 | ) 143 | 144 | # check that either single_line or address are not-null 145 | # all non-null values should be a scalar or the same length 146 | 147 | # It could be actually really easy to capture all of the arguments 148 | # except .progress, token, and geocoder to turn it into a data.frame 149 | # iterate through the rows and create the requests 150 | 151 | # NOTE when debugging inline 152 | # fml_nms <- rlang::fn_fmls_names(find_address_candidates) 153 | fml_nms <- rlang::fn_fmls_names() 154 | 155 | # get all values passed in 156 | all_args <- rlang::env_get_list(nms = fml_nms) 157 | 158 | # check to see if any are null 159 | null_args <- vapply(all_args, is.null, logical(1)) 160 | 161 | # these arguments are scalars and shold not be handled in a vectorized manner 162 | to_exclude <- c("crs", ".progress", "token", "geocoder", "for_storage", "search_extent") 163 | to_include <- !names(all_args) %in% to_exclude 164 | 165 | # fetches all non-null arguments. These will be turned into a dataframe 166 | non_null_vals <- all_args[to_include & !null_args] 167 | 168 | # validate the preferred_label_values 169 | if (!is.null(non_null_vals[["preferred_label_values"]])) { 170 | non_null_vals[["preferred_label_values"]] <- match_label_values( 171 | non_null_vals[["preferred_label_values"]], 172 | .multiple = TRUE 173 | ) 174 | } 175 | 176 | # validate location types 177 | if (!is.null(non_null_vals[["location_type"]])) { 178 | non_null_vals[["location_type"]] <- match_location_type( 179 | non_null_vals[["location_type"]], 180 | .multiple = TRUE 181 | ) 182 | } 183 | 184 | # check for locations 185 | location <- obj_as_points(location, allow_null = TRUE) 186 | 187 | # convert to esri json if not missing 188 | if (!is.null(location)) { 189 | in_crs <- sf::st_crs(location) 190 | in_sr <- validate_crs(in_crs, call = call)[[1]] 191 | non_null_vals[["location"]] <- as_esri_point_json(location, in_sr) 192 | } 193 | 194 | 195 | # check lengths 196 | ns <- lengths(non_null_vals) 197 | n_checks <- ns == max(ns) | ns == 1L 198 | 199 | if (!all(n_checks)) { 200 | cli::cli_abort( 201 | c( 202 | "All arguments must be the same length or scalar or length 1" 203 | ) 204 | ) 205 | } 206 | 207 | # handle outSR 208 | if (!is.null(crs)) { 209 | crs <- jsonify::to_json(validate_crs(crs)[[1]], unbox = TRUE) 210 | } 211 | 212 | # handle extent 213 | # only 1 extent per function call, this will not be vectorized 214 | check_extent( 215 | search_extent, 216 | arg = rlang::caller_arg(search_extent) 217 | ) 218 | 219 | if (!is.null(search_extent)) { 220 | extent_crs <- validate_crs( 221 | sf::st_crs(search_extent), 222 | call = rlang::current_call() 223 | )[[1]] 224 | 225 | extent_json_raw <- c( 226 | as.list(search_extent), 227 | spatialReference = list(extent_crs) 228 | ) 229 | search_extent <- jsonify::to_json(extent_json_raw, unbox = TRUE) 230 | } 231 | 232 | # create the base request 233 | b_req <- arc_base_req( 234 | geocoder[["url"]], 235 | token, 236 | path = "findAddressCandidates", 237 | query = c("f" = "json") 238 | ) 239 | 240 | # create a data frame to take advantage of auto-recycling 241 | params_df <- data.frame(non_null_vals) 242 | 243 | # how many requests we will have to make 244 | n <- nrow(params_df) 245 | 246 | # pre-allocate list 247 | all_reqs <- vector(mode = "list", length = n) 248 | 249 | for (i in seq_len(n)) { 250 | # capture params as a list 251 | params_i <- as.list(params_df[i, , drop = FALSE]) 252 | 253 | # convert the names to lowerCamel for endpoint 254 | names(params_i) <- to_lower_camel(names(params_i)) 255 | 256 | # store in list 257 | all_reqs[[i]] <- httr2::req_body_form( 258 | b_req, 259 | !!!params_i, 260 | outFields = "*", 261 | outSR = crs, 262 | searchExtent = search_extent 263 | ) 264 | } 265 | 266 | all_resps <- httr2::req_perform_parallel( 267 | all_reqs, 268 | on_error = "continue", 269 | progress = .progress 270 | ) 271 | 272 | all_resps <- httr2::req_perform_parallel( 273 | all_reqs, 274 | on_error = "continue", 275 | progress = .progress 276 | ) 277 | 278 | # Before we can process the responses, we must know if 279 | # the locator has custom fields. If so, we need to use 280 | # RcppSimdJson and _not_ the Rust based implementation 281 | use_custom_json_processing <- has_custom_fields(geocoder) 282 | 283 | # TODO Handle errors 284 | all_results <- lapply(all_resps, function(.resp) { 285 | string <- httr2::resp_body_string(.resp) 286 | parse_candidate_res(string) 287 | }) 288 | 289 | # combine all the results 290 | results <- rbind_results(all_results) 291 | 292 | # if any issues occured they would've happened here 293 | errors <- attr(results, "null_elements") 294 | n_errors <- length(errors) 295 | 296 | # if errors occurred attach as an attribute 297 | if (n_errors > 0) { 298 | attr(results, "error_requests") <- all_reqs[errors] 299 | 300 | # process resps and catch the errors 301 | error_messages <- lapply( 302 | all_resps[errors], 303 | function(.x) catch_error(httr2::resp_body_string(.x), rlang::caller_call(2)) 304 | ) 305 | 306 | # add a warning when n_errors > 0 307 | cli::cli_warn(c( 308 | "x" = "Issue{cli::qty(n_errors)}{?s} encountered when processing response{cli::qty(n_errors)}{?s} {cli::qty(n_errors)} {errors}", 309 | "i" = "access problem requests with {.code attr(result, \"error_requests\")}" 310 | )) 311 | 312 | # for each error message signal the condition 313 | for (cnd in error_messages) rlang::cnd_signal(cnd) 314 | } 315 | 316 | 317 | # # TODO handle errors!!! 318 | # successes <- httr2::resps_successes(all_resps) 319 | 320 | # all_results <- lapply(successes, function(.resp) { 321 | # string <- httr2::resp_body_string(.resp) 322 | # # string 323 | # parse_candidate_res(string) 324 | # }) 325 | 326 | # # combine together 327 | # res <- rbind_results(all_results) 328 | 329 | # # FIXME should the IDs be included as optional into `rbind_results()`? 330 | n_ids <- vapply(all_results, function(.x) nrow(.x) %||% 0L, integer(1)) 331 | ids <- rep.int(1:length(all_results), n_ids) 332 | 333 | # # cbind() is slow but not that bad? 334 | res <- cbind(input_id = ids, results) 335 | attr(res, "error_requests") <- all_reqs[errors] 336 | attr(res, "error_ids") <- errors 337 | res 338 | } 339 | 340 | 341 | parse_candidate_res <- function(string) { 342 | res_list <- parse_candidate_json(string) 343 | 344 | if (is.null(res_list)) { 345 | return(NULL) 346 | } 347 | res <- res_list[["attributes"]] 348 | res[["extents"]] <- res_list[["extents"]] 349 | 350 | # TODO sometimes the wkid isn't a standard EPSG code. 351 | # Then we need to add `ESRI:{wkid}` in front of it. 352 | # But how do we know? The spatialReference database could be handy here. 353 | # but thats so bigg..... 354 | crs_obj <- parse_wkid(res_list$sr$wkid) 355 | geometry <- sf::st_sfc(res_list[["locations"]], crs = crs_obj) 356 | 357 | # geometry 358 | sf::st_sf( 359 | res, 360 | geometry 361 | ) 362 | } 363 | -------------------------------------------------------------------------------- /R/core-reverse-geocode.R: -------------------------------------------------------------------------------- 1 | #' Reverse Geocode Locations 2 | #' 3 | #' Determines the address for a given point. 4 | #' 5 | #' @details 6 | #' This function utilizes the 7 | #' [`/reverseGeocode`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm) endpoint of a geocoding service. By default, it uses 8 | #' the public ArcGIS World Geocoder. 9 | #' 10 | #' - Intersection matches are only returned when `feature_types = "StreetInt"`. See [REST documentation for more](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm#ESRI_SECTION3_1FE6B6D350714E45B2707845ADA22E1E). 11 | #' 12 | #' ## Location Type 13 | #' 14 | #' - Specifies whether the output geometry shuold be the rooftop point or the 15 | #' street entrance location. 16 | #' - The `location_type` parameter changes the geometry's placement but does not 17 | #' change the attribute values of `X`, `Y`, or `DisplayX`, and `DisplayY`. 18 | #' 19 | #' ## Storage 20 | #' 21 | #' **Very Important** 22 | #' 23 | #' The argument `for_storage` is used to determine if the request allows you to 24 | #' persist the results of the query. It is important to note that there are 25 | #' contractual obligations to appropriately set this argument. **You cannot save 26 | #' or persist results** when `for_storage = FALSE` (the default). 27 | #' 28 | #' ## Execution 29 | #' 30 | #' The `/reverseGeocode` endpoint can only handle one address at a time. To 31 | #' make the operation as performant as possible, requests are sent in parallel 32 | #' using [`httr2::req_perform_parallel()`]. The JSON responses are then processed 33 | #' using Rust and returned as an sf object. 34 | #' 35 | #' @examples 36 | #' # Find addresses from locations 37 | #' reverse_geocode(c(-117.172, 34.052)) 38 | #' @param locations an `sfc_POINT` object of the locations to be reverse geocoded. 39 | #' @param crs the CRS of the returned geometries. Passed to `sf::st_crs()`. 40 | #' Ignored if `locations` is not an `sfc_POINT` object. 41 | #' @param ... unused. 42 | #' @param lang_code default `NULL`. An ISO 3166 country code. 43 | #' See [`iso_3166_codes()`] for valid ISO codes. Optional. 44 | #' @param feature_type limits the possible match types returned. Must be one of 45 | #' `"StreetInt"`, `"DistanceMarker"`, `"StreetAddress"`, `"StreetName"`, 46 | #' `"POI"`, `"Subaddress"`, `"PointAddress"`, `"Postal"`, or `"Locality"`. Optional. 47 | #' @param location_type default `"rooftop"`. Must be one of `"rooftop"` or `"street"`. 48 | #' Optional. 49 | #' @param preferred_label_values default NULL. Must be one of `"postalCity"` 50 | #' or `"localCity"`. Optional. 51 | #' @param for_storage default `FALSE`. Whether or not the results will be saved 52 | #' for long term storage. 53 | #' @param geocoder default [`default_geocoder()`]. 54 | #' @param .progress default `TRUE`. Whether a progress bar should be provided. 55 | #' @inheritParams arcgisutils::arc_base_req 56 | #' @export 57 | #' @return An sf object. 58 | reverse_geocode <- function( 59 | locations, 60 | crs = sf::st_crs(locations), 61 | ..., 62 | lang_code = NULL, 63 | feature_type = NULL, 64 | location_type = c("rooftop", "street"), 65 | preferred_label_values = c("postalCity", "localCity"), 66 | for_storage = FALSE, 67 | geocoder = default_geocoder(), 68 | token = arc_token(), 69 | .progress = TRUE) { 70 | # TODO ask users to verify their `for_storage` use 71 | # This is super important and can lead to contractual violations 72 | check_geocoder(geocoder, call = rlang::caller_env()) 73 | 74 | if (!"reversegeocode" %in% capabilities(geocoder)) { 75 | arg <- rlang::caller_arg(geocoder) 76 | cli::cli_abort("{.arg {arg}} does not support the {.path /reverseGeocode} endpoint") 77 | } 78 | 79 | check_for_storage(for_storage, token, call = rlang::caller_env()) 80 | 81 | check_bool(.progress) 82 | 83 | # check feature type if not missing 84 | if (!is.null(feature_type)) { 85 | rlang::arg_match( 86 | feature_type, 87 | c( 88 | "StreetInt", "DistanceMarker", "StreetAddress", 89 | "StreetName", "POI", "Subaddress", 90 | "PointAddress", "Postal", "Locality" 91 | ) 92 | ) 93 | } 94 | 95 | # verify location type argument 96 | location_type <- rlang::arg_match(location_type, values = c("rooftop", "street")) 97 | 98 | # verify label value arg 99 | preferred_label_values <- rlang::arg_match( 100 | preferred_label_values, 101 | values = c("postalCity", "localCity") 102 | ) 103 | 104 | # if locations is not an sfc object, we set to 4326 105 | # otherwise we validate output CRS 106 | if (!rlang::inherits_all(locations, c("sfc_POINT", "sfc"))) { 107 | crs <- 4326 108 | } else if (is.na(crs)) { 109 | cli::cli_warn( 110 | c( 111 | "!" = "{.arg crs} is set to {.cls NA}", 112 | "i" = "using {.code EPSG:4326}" 113 | ) 114 | ) 115 | crs <- 4326 116 | } 117 | 118 | # TODO use wk to use any wk_handle-able points 119 | # validates location input 120 | locations <- obj_as_points(locations) 121 | 122 | # ensure lang_code a single string 123 | check_string(lang_code, allow_null = TRUE) 124 | 125 | # if not missing and not valid, error 126 | if (!is.null(lang_code) && !is_iso3166(lang_code)) { 127 | cli::cli_abort( 128 | c( 129 | "{.arg lang_code} is not a recognized Country Code", 130 | "i" = "See {.fn iso_3166_codes} for ISO 3166 codes" 131 | ) 132 | ) 133 | } 134 | 135 | # get the JSON output 136 | out_crs <- validate_crs(crs)[[1]] 137 | 138 | # create list of provided parameters 139 | query_params <- compact(list( 140 | langCode = lang_code, 141 | outSR = jsonify::to_json(out_crs, unbox = TRUE), 142 | featureType = feature_type, 143 | forStorage = for_storage, 144 | locationType = location_type, 145 | preferredLabelValues = preferred_label_values 146 | )) 147 | 148 | # validate the input CRS 149 | in_crs <- validate_crs(sf::st_crs(locations))[[1]] 150 | 151 | # convert to EsriPoint JSON 152 | locs_json <- as_esri_point_json(locations, in_crs) 153 | 154 | b_req <- arc_base_req( 155 | geocoder[["url"]], 156 | token, 157 | path = "reverseGeocode" 158 | ) 159 | 160 | # allocate list to store requests 161 | all_reqs <- vector(mode = "list", length = length(locs_json)) 162 | 163 | # fill requests with for loop 164 | for (i in seq_along(locs_json)) { 165 | all_reqs[[i]] <- httr2::req_body_form( 166 | f = "json", 167 | b_req, 168 | !!!query_params, 169 | location = locs_json[[i]] 170 | ) 171 | } 172 | 173 | # Run requests in parallel 174 | all_resps <- httr2::req_perform_parallel( 175 | all_reqs, 176 | on_error = "continue", 177 | progress = .progress 178 | ) 179 | 180 | # TODO capture which locations had an error and either return 181 | # requests or points 182 | # also, should return missing values or IDs to associate with the points 183 | # so that they can be merged back on to the input locations? 184 | # TODO check for errors which will be encoded as json 185 | resps_json <- httr2::resps_data(all_resps, httr2::resp_body_string) 186 | 187 | # process the raw json using rust 188 | res_raw <- parse_rev_geocode_resp(resps_json) 189 | 190 | # TODO incorporate squish DF into arcgisutils. This is stopgap solution 191 | # https://github.com/R-ArcGIS/arcgislayers/pull/167 192 | res_attr <- data_frame(rbind_results(res_raw$attributes)) 193 | 194 | # cast into sf object 195 | res_sf <- sf::st_sf( 196 | res_attr, 197 | geometry = sf::st_sfc(res_raw[["geometry"]], crs = crs) 198 | ) 199 | 200 | res_sf 201 | # Return the errors as an attribute this will let people 202 | # handle the failures later on if they need to do an iterative / recursive 203 | # approach to it. 204 | # or use tokio.... 205 | # I'll try both 206 | } 207 | 208 | 209 | # notes ------------------------------------------------------------------- 210 | 211 | # We need to have an object for GeocoderService 212 | # Much like we have one for FeatureLayer etc 213 | # These store so much metadata that will be needed. 214 | # What is the workflow? 215 | # arc_open("geocoder-url") or `geocoder("url"/"id")` 216 | # We can use rust to create the JSON from the points 217 | # Or do we want to have Rust do it _all_? I think that might be nice.. 218 | # would need to inherit the arc_agent() and X-Esri-Authorization headerre 219 | -------------------------------------------------------------------------------- /R/core-suggest.R: -------------------------------------------------------------------------------- 1 | #' Search Suggestion 2 | #' 3 | #' This function returns candidate locations based on a partial search query. 4 | #' It is designed to be used in an interactive search experience in a client 5 | #' facing application. 6 | #' 7 | #' @details 8 | #' 9 | #' Unlike the other functions in this package, `suggest_places()` is not 10 | #' vectorized as it is intended to provide search suggestions for individual 11 | #' queries such as those made in a search bar. 12 | #' 13 | #' Utilizes the [`/suggest`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm) endpoint. 14 | #' 15 | #' @param text a scalar character of search key to generate a place suggestion. 16 | #' @param location an `sfc_POINT` object that centers the search. Optional. 17 | #' @param category a scalar character. Place or address type that can be used to 18 | #' filter suggest results. Optional. 19 | #' @param search_extent an object of class `bbox` that limits the search area. This is especially useful for applications in which a user will search for places and addresses within the current map extent. Optional. 20 | #' @param country_code default `NULL.` An ISO 3166 country code. 21 | #' See [`iso_3166_codes()`] for valid ISO codes. Optional. 22 | #' @param max_suggestions default `NULL`. The maximum number of suggestions to return. 23 | #' The service default is 5 with a maximum of 15. 24 | #' @inheritParams reverse_geocode 25 | #' @inheritParams find_address_candidates 26 | #' @returns 27 | #' A `data.frame` with 3 columns: `text`, `magic_key`, and `is_collection`. 28 | #' @export 29 | #' @examples 30 | #' # identify a search point 31 | #' location <- sf::st_sfc(sf::st_point(c(-84.34, 33.74)), crs = 4326) 32 | #' 33 | #' # create a search extent from it 34 | #' search_extent <- sf::st_bbox(sf::st_buffer(location, 10)) 35 | #' 36 | #' # find suggestions from it 37 | #' suggestions <- suggest_places( 38 | #' "bellwood", 39 | #' location, 40 | #' search_extent = search_extent 41 | #' ) 42 | #' 43 | #' # get address candidate information 44 | #' # using the text and the magic key 45 | #' find_address_candidates( 46 | #' suggestions$text, 47 | #' magic_key = suggestions$magic_key 48 | #' ) 49 | suggest_places <- function( 50 | text, 51 | location = NULL, 52 | category = NULL, 53 | search_extent = NULL, 54 | max_suggestions = NULL, 55 | country_code = NULL, 56 | preferred_label_values = NULL, 57 | geocoder = default_geocoder(), 58 | token = arc_token()) { 59 | if (!"suggest" %in% capabilities(geocoder)) { 60 | arg <- rlang::caller_arg(geocoder) 61 | cli::cli_abort("{.arg {arg}} does not support the {.path /suggest} endpoint") 62 | } 63 | 64 | location <- obj_as_points(location, allow_null = TRUE, call = rlang::caller_env()) 65 | # FIXME 66 | # Location should be able to be a length 2 numeric vector or an sfg POINT 67 | check_geocoder(geocoder, call = rlang::caller_env()) 68 | 69 | check_string(text) 70 | check_string(category, allow_null = TRUE) 71 | 72 | # searchExtent 73 | check_extent( 74 | search_extent, 75 | arg = rlang::caller_arg(search_extent) 76 | ) 77 | 78 | 79 | if (!is.null(search_extent)) { 80 | extent_crs <- validate_crs( 81 | sf::st_crs(search_extent), 82 | call = rlang::current_call() 83 | )[[1]] 84 | 85 | extent_json_raw <- c( 86 | as.list(search_extent), 87 | spatialReference = list(extent_crs) 88 | ) 89 | search_extent <- jsonify::to_json(extent_json_raw, unbox = TRUE) 90 | } 91 | 92 | check_number_whole( 93 | max_suggestions, 94 | min = 1, 95 | max = 15, 96 | allow_null = TRUE 97 | ) 98 | 99 | check_iso_3166(country_code, scalar = TRUE) 100 | 101 | check_string(preferred_label_values, allow_null = TRUE) 102 | 103 | if (!is.null(preferred_label_values)) { 104 | preferred_label_values <- match_label_values(preferred_label_values) 105 | } 106 | 107 | b_req <- arc_base_req( 108 | geocoder[["url"]], 109 | path = "suggest", query = list(f = "json") 110 | ) 111 | 112 | if (!is.null(location)) { 113 | in_sr <- validate_crs(sf::st_crs(location))[[1]] 114 | } else { 115 | in_sr <- NULL 116 | } 117 | 118 | # get the location as json 119 | if (!is.null(location)) { 120 | loc_json <- as_esri_point_json(location, in_sr) 121 | } else { 122 | loc_json <- NULL 123 | } 124 | 125 | req <- httr2::req_body_form( 126 | b_req, 127 | text = text, 128 | location = loc_json, 129 | category = category, 130 | maxSuggestions = max_suggestions, 131 | countryCode = country_code, 132 | preferredLabelValues = preferred_label_values 133 | ) 134 | 135 | resp <- httr2::req_perform(req) 136 | resp_string <- httr2::resp_body_string(resp) 137 | 138 | # capture the response 139 | res <- data_frame(parse_suggestions(resp_string)) 140 | 141 | # if there are more than 0 rows, no error occured 142 | if (nrow(res) > 0) { 143 | return(res) 144 | } else { 145 | # if there are 0 rows, an error occurred, capture and signal it 146 | # still return empty data frame 147 | rlang::cnd_signal(catch_error(resp_string, error_call = rlang::caller_env())) 148 | return(res) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /R/doc-data.R: -------------------------------------------------------------------------------- 1 | #' ArcGIS World Geocoder 2 | #' 3 | #' The [ArcGIS World Geocoder](https://www.esri.com/en-us/arcgis/products/arcgis-world-geocoder) 4 | #' is made publicly available for some uses. The `world_geocoder` object is used 5 | #' as the default `GeocodeServer` object in [`default_geocoder()`] when no 6 | #' authorization token is found. The [`find_address_candidates()`], 7 | #' [`reverse_geocode()`], and [`suggest_places()`] can be used without an 8 | #' authorization token. The [`geocode_addresses()`] funciton requires an 9 | #' authorization token to be used for batch geocoding. 10 | #' @returns an object of class `GeocodeServer` 11 | "world_geocoder" 12 | 13 | 14 | #' Esri well-known IDs 15 | #' 16 | #' An integer vector containing the WKIDs of Esri authority 17 | #' spatial references. 18 | #' Esri WKIDs were identified from the [`{arcgeocoder}`](https://cran.r-project.org/package=arcgeocoder) package from 19 | #' [@dieghernan](https://github.com/dieghernan). 20 | #' @returns a numeric vector of well-known IDs 21 | "esri_wkids" 22 | -------------------------------------------------------------------------------- /R/extendr-wrappers.R: -------------------------------------------------------------------------------- 1 | # Generated by extendr: Do not edit by hand 2 | 3 | # nolint start 4 | 5 | # 6 | # This file was created with the following call: 7 | # .Call("wrap__make_arcgisgeocode_wrappers", use_symbols = TRUE, package_name = "arcgisgeocode") 8 | 9 | #' @usage NULL 10 | #' @useDynLib arcgisgeocode, .registration = TRUE 11 | NULL 12 | 13 | as_esri_point_json <- function(x, sr) .Call(wrap__as_esri_point_json, x, sr) 14 | 15 | create_records <- function(object_id, single_line, address, address2, address3, neighborhood, city, subregion, region, postal, postal_ext, country_code, location, sr, n) .Call(wrap__create_records, object_id, single_line, address, address2, address3, neighborhood, city, subregion, region, postal, postal_ext, country_code, location, sr, n) 16 | 17 | parse_location_json <- function(x) .Call(wrap__parse_location_json, x) 18 | 19 | parse_candidate_json <- function(x) .Call(wrap__parse_candidate_json, x) 20 | 21 | is_iso3166 <- function(code) .Call(wrap__is_iso3166, code) 22 | 23 | iso_3166_2 <- function() .Call(wrap__iso_3166_2) 24 | 25 | iso_3166_3 <- function() .Call(wrap__iso_3166_3) 26 | 27 | iso_3166_names <- function() .Call(wrap__iso_3166_names) 28 | 29 | parse_custom_location_json_ <- function(x, to_fill) .Call(wrap__parse_custom_location_json_, x, to_fill) 30 | 31 | parse_rev_geocode_resp <- function(resps) .Call(wrap__parse_rev_geocode_resp, resps) 32 | 33 | parse_suggestions <- function(x) .Call(wrap__parse_suggestions, x) 34 | 35 | 36 | # nolint end 37 | -------------------------------------------------------------------------------- /R/import-standalone-obj-type.R: -------------------------------------------------------------------------------- 1 | # Standalone file: do not edit by hand 2 | # Source: 3 | # ---------------------------------------------------------------------- 4 | # 5 | # --- 6 | # repo: r-lib/rlang 7 | # file: standalone-obj-type.R 8 | # last-updated: 2023-05-01 9 | # license: https://unlicense.org 10 | # imports: rlang (>= 1.1.0) 11 | # --- 12 | # 13 | # ## Changelog 14 | # 15 | # 2023-05-01: 16 | # - `obj_type_friendly()` now only displays the first class of S3 objects. 17 | # 18 | # 2023-03-30: 19 | # - `stop_input_type()` now handles `I()` input literally in `arg`. 20 | # 21 | # 2022-10-04: 22 | # - `obj_type_friendly(value = TRUE)` now shows numeric scalars 23 | # literally. 24 | # - `stop_friendly_type()` now takes `show_value`, passed to 25 | # `obj_type_friendly()` as the `value` argument. 26 | # 27 | # 2022-10-03: 28 | # - Added `allow_na` and `allow_null` arguments. 29 | # - `NULL` is now backticked. 30 | # - Better friendly type for infinities and `NaN`. 31 | # 32 | # 2022-09-16: 33 | # - Unprefixed usage of rlang functions with `rlang::` to 34 | # avoid onLoad issues when called from rlang (#1482). 35 | # 36 | # 2022-08-11: 37 | # - Prefixed usage of rlang functions with `rlang::`. 38 | # 39 | # 2022-06-22: 40 | # - `friendly_type_of()` is now `obj_type_friendly()`. 41 | # - Added `obj_type_oo()`. 42 | # 43 | # 2021-12-20: 44 | # - Added support for scalar values and empty vectors. 45 | # - Added `stop_input_type()` 46 | # 47 | # 2021-06-30: 48 | # - Added support for missing arguments. 49 | # 50 | # 2021-04-19: 51 | # - Added support for matrices and arrays (#141). 52 | # - Added documentation. 53 | # - Added changelog. 54 | # 55 | # nocov start 56 | 57 | #' Return English-friendly type 58 | #' @param x Any R object. 59 | #' @param value Whether to describe the value of `x`. Special values 60 | #' like `NA` or `""` are always described. 61 | #' @param length Whether to mention the length of vectors and lists. 62 | #' @return A string describing the type. Starts with an indefinite 63 | #' article, e.g. "an integer vector". 64 | #' @noRd 65 | obj_type_friendly <- function(x, value = TRUE) { 66 | if (rlang::is_missing(x)) { 67 | return("absent") 68 | } 69 | 70 | if (is.object(x)) { 71 | if (inherits(x, "quosure")) { 72 | type <- "quosure" 73 | } else { 74 | type <- class(x)[[1L]] 75 | } 76 | return(sprintf("a <%s> object", type)) 77 | } 78 | 79 | if (!rlang::is_vector(x)) { 80 | return(.rlang_as_friendly_type(typeof(x))) 81 | } 82 | 83 | n_dim <- length(dim(x)) 84 | 85 | if (!n_dim) { 86 | if (!rlang::is_list(x) && length(x) == 1) { 87 | if (rlang::is_na(x)) { 88 | return(switch( 89 | typeof(x), 90 | logical = "`NA`", 91 | integer = "an integer `NA`", 92 | double = 93 | if (is.nan(x)) { 94 | "`NaN`" 95 | } else { 96 | "a numeric `NA`" 97 | }, 98 | complex = "a complex `NA`", 99 | character = "a character `NA`", 100 | .rlang_stop_unexpected_typeof(x) 101 | )) 102 | } 103 | 104 | show_infinites <- function(x) { 105 | if (x > 0) { 106 | "`Inf`" 107 | } else { 108 | "`-Inf`" 109 | } 110 | } 111 | str_encode <- function(x, width = 30, ...) { 112 | if (nchar(x) > width) { 113 | x <- substr(x, 1, width - 3) 114 | x <- paste0(x, "...") 115 | } 116 | encodeString(x, ...) 117 | } 118 | 119 | if (value) { 120 | if (is.numeric(x) && is.infinite(x)) { 121 | return(show_infinites(x)) 122 | } 123 | 124 | if (is.numeric(x) || is.complex(x)) { 125 | number <- as.character(round(x, 2)) 126 | what <- if (is.complex(x)) "the complex number" else "the number" 127 | return(paste(what, number)) 128 | } 129 | 130 | return(switch( 131 | typeof(x), 132 | logical = if (x) "`TRUE`" else "`FALSE`", 133 | character = { 134 | what <- if (nzchar(x)) "the string" else "the empty string" 135 | paste(what, str_encode(x, quote = "\"")) 136 | }, 137 | raw = paste("the raw value", as.character(x)), 138 | .rlang_stop_unexpected_typeof(x) 139 | )) 140 | } 141 | 142 | return(switch( 143 | typeof(x), 144 | logical = "a logical value", 145 | integer = "an integer", 146 | double = if (is.infinite(x)) show_infinites(x) else "a number", 147 | complex = "a complex number", 148 | character = if (nzchar(x)) "a string" else "\"\"", 149 | raw = "a raw value", 150 | .rlang_stop_unexpected_typeof(x) 151 | )) 152 | } 153 | 154 | if (length(x) == 0) { 155 | return(switch( 156 | typeof(x), 157 | logical = "an empty logical vector", 158 | integer = "an empty integer vector", 159 | double = "an empty numeric vector", 160 | complex = "an empty complex vector", 161 | character = "an empty character vector", 162 | raw = "an empty raw vector", 163 | list = "an empty list", 164 | .rlang_stop_unexpected_typeof(x) 165 | )) 166 | } 167 | } 168 | 169 | vec_type_friendly(x) 170 | } 171 | 172 | vec_type_friendly <- function(x, length = FALSE) { 173 | if (!rlang::is_vector(x)) { 174 | rlang::abort("`x` must be a vector.") 175 | } 176 | type <- typeof(x) 177 | n_dim <- length(dim(x)) 178 | 179 | add_length <- function(type) { 180 | if (length && !n_dim) { 181 | paste0(type, sprintf(" of length %s", length(x))) 182 | } else { 183 | type 184 | } 185 | } 186 | 187 | if (type == "list") { 188 | if (n_dim < 2) { 189 | return(add_length("a list")) 190 | } else if (is.data.frame(x)) { 191 | return("a data frame") 192 | } else if (n_dim == 2) { 193 | return("a list matrix") 194 | } else { 195 | return("a list array") 196 | } 197 | } 198 | 199 | type <- switch( 200 | type, 201 | logical = "a logical %s", 202 | integer = "an integer %s", 203 | numeric = , 204 | double = "a double %s", 205 | complex = "a complex %s", 206 | character = "a character %s", 207 | raw = "a raw %s", 208 | type = paste0("a ", type, " %s") 209 | ) 210 | 211 | if (n_dim < 2) { 212 | kind <- "vector" 213 | } else if (n_dim == 2) { 214 | kind <- "matrix" 215 | } else { 216 | kind <- "array" 217 | } 218 | out <- sprintf(type, kind) 219 | 220 | if (n_dim >= 2) { 221 | out 222 | } else { 223 | add_length(out) 224 | } 225 | } 226 | 227 | .rlang_as_friendly_type <- function(type) { 228 | switch( 229 | type, 230 | 231 | list = "a list", 232 | 233 | NULL = "`NULL`", 234 | environment = "an environment", 235 | externalptr = "a pointer", 236 | weakref = "a weak reference", 237 | S4 = "an S4 object", 238 | 239 | name = , 240 | symbol = "a symbol", 241 | language = "a call", 242 | pairlist = "a pairlist node", 243 | expression = "an expression vector", 244 | 245 | char = "an internal string", 246 | promise = "an internal promise", 247 | ... = "an internal dots object", 248 | any = "an internal `any` object", 249 | bytecode = "an internal bytecode object", 250 | 251 | primitive = , 252 | builtin = , 253 | special = "a primitive function", 254 | closure = "a function", 255 | 256 | type 257 | ) 258 | } 259 | 260 | .rlang_stop_unexpected_typeof <- function(x, call = rlang::caller_env()) { 261 | rlang::abort( 262 | sprintf("Unexpected type <%s>.", typeof(x)), 263 | call = call 264 | ) 265 | } 266 | 267 | #' Return OO type 268 | #' @param x Any R object. 269 | #' @return One of `"bare"` (for non-OO objects), `"S3"`, `"S4"`, 270 | #' `"R6"`, or `"R7"`. 271 | #' @noRd 272 | obj_type_oo <- function(x) { 273 | if (!is.object(x)) { 274 | return("bare") 275 | } 276 | 277 | class <- inherits(x, c("R6", "R7_object"), which = TRUE) 278 | 279 | if (class[[1]]) { 280 | "R6" 281 | } else if (class[[2]]) { 282 | "R7" 283 | } else if (isS4(x)) { 284 | "S4" 285 | } else { 286 | "S3" 287 | } 288 | } 289 | 290 | #' @param x The object type which does not conform to `what`. Its 291 | #' `obj_type_friendly()` is taken and mentioned in the error message. 292 | #' @param what The friendly expected type as a string. Can be a 293 | #' character vector of expected types, in which case the error 294 | #' message mentions all of them in an "or" enumeration. 295 | #' @param show_value Passed to `value` argument of `obj_type_friendly()`. 296 | #' @param ... Arguments passed to [abort()]. 297 | #' @inheritParams args_error_context 298 | #' @noRd 299 | stop_input_type <- function( 300 | x, 301 | what, 302 | ..., 303 | allow_na = FALSE, 304 | allow_null = FALSE, 305 | show_value = TRUE, 306 | arg = rlang::caller_arg(x), 307 | call = rlang::caller_env() 308 | ) { 309 | # From standalone-cli.R 310 | cli <- rlang::env_get_list( 311 | nms = c("format_arg", "format_code"), 312 | last = topenv(), 313 | default = function(x) sprintf("`%s`", x), 314 | inherit = TRUE 315 | ) 316 | 317 | if (allow_na) { 318 | what <- c(what, cli$format_code("NA")) 319 | } 320 | if (allow_null) { 321 | what <- c(what, cli$format_code("NULL")) 322 | } 323 | if (length(what)) { 324 | what <- oxford_comma(what) 325 | } 326 | if (inherits(arg, "AsIs")) { 327 | format_arg <- identity 328 | } else { 329 | format_arg <- cli$format_arg 330 | } 331 | 332 | message <- sprintf( 333 | "%s must be %s, not %s.", 334 | format_arg(arg), 335 | what, 336 | obj_type_friendly(x, value = show_value) 337 | ) 338 | 339 | rlang::abort(message, ..., call = call, arg = arg) 340 | } 341 | 342 | oxford_comma <- function(chr, sep = ", ", final = "or") { 343 | n <- length(chr) 344 | 345 | if (n < 2) { 346 | return(chr) 347 | } 348 | 349 | head <- chr[seq_len(n - 1)] 350 | last <- chr[n] 351 | 352 | head <- paste(head, collapse = sep) 353 | 354 | # Write a or b. But a, b, or c. 355 | if (n > 2) { 356 | paste0(head, sep, final, " ", last) 357 | } else { 358 | paste0(head, " ", final, " ", last) 359 | } 360 | } 361 | 362 | # nocov end 363 | -------------------------------------------------------------------------------- /R/import-standalone-types-check.R: -------------------------------------------------------------------------------- 1 | # Standalone file: do not edit by hand 2 | # Source: 3 | # ---------------------------------------------------------------------- 4 | # 5 | # --- 6 | # repo: r-lib/rlang 7 | # file: standalone-types-check.R 8 | # last-updated: 2023-03-13 9 | # license: https://unlicense.org 10 | # dependencies: standalone-obj-type.R 11 | # imports: rlang (>= 1.1.0) 12 | # --- 13 | # 14 | # ## Changelog 15 | # 16 | # 2023-03-13: 17 | # - Improved error messages of number checkers (@teunbrand) 18 | # - Added `allow_infinite` argument to `check_number_whole()` (@mgirlich). 19 | # - Added `check_data_frame()` (@mgirlich). 20 | # 21 | # 2023-03-07: 22 | # - Added dependency on rlang (>= 1.1.0). 23 | # 24 | # 2023-02-15: 25 | # - Added `check_logical()`. 26 | # 27 | # - `check_bool()`, `check_number_whole()`, and 28 | # `check_number_decimal()` are now implemented in C. 29 | # 30 | # - For efficiency, `check_number_whole()` and 31 | # `check_number_decimal()` now take a `NULL` default for `min` and 32 | # `max`. This makes it possible to bypass unnecessary type-checking 33 | # and comparisons in the default case of no bounds checks. 34 | # 35 | # 2022-10-07: 36 | # - `check_number_whole()` and `_decimal()` no longer treat 37 | # non-numeric types such as factors or dates as numbers. Numeric 38 | # types are detected with `is.numeric()`. 39 | # 40 | # 2022-10-04: 41 | # - Added `check_name()` that forbids the empty string. 42 | # `check_string()` allows the empty string by default. 43 | # 44 | # 2022-09-28: 45 | # - Removed `what` arguments. 46 | # - Added `allow_na` and `allow_null` arguments. 47 | # - Added `allow_decimal` and `allow_infinite` arguments. 48 | # - Improved errors with absent arguments. 49 | # 50 | # 51 | # 2022-09-16: 52 | # - Unprefixed usage of rlang functions with `rlang::` to 53 | # avoid onLoad issues when called from rlang (#1482). 54 | # 55 | # 2022-08-11: 56 | # - Added changelog. 57 | # 58 | # nocov start 59 | 60 | # Scalars ----------------------------------------------------------------- 61 | 62 | .standalone_types_check_dot_call <- .Call 63 | 64 | check_bool <- function(x, 65 | ..., 66 | allow_na = FALSE, 67 | allow_null = FALSE, 68 | arg = rlang::caller_arg(x), 69 | call = rlang::caller_env()) { 70 | if (!missing(x) && .standalone_types_check_dot_call(rlang::ffi_standalone_is_bool_1.0.7, x, allow_na, allow_null)) { 71 | return(invisible(NULL)) 72 | } 73 | 74 | stop_input_type( 75 | x, 76 | c("`TRUE`", "`FALSE`"), 77 | ..., 78 | allow_na = allow_na, 79 | allow_null = allow_null, 80 | arg = arg, 81 | call = call 82 | ) 83 | } 84 | 85 | check_string <- function(x, 86 | ..., 87 | allow_empty = TRUE, 88 | allow_na = FALSE, 89 | allow_null = FALSE, 90 | arg = rlang::caller_arg(x), 91 | call = rlang::caller_env()) { 92 | if (!missing(x)) { 93 | is_string <- .rlang_check_is_string( 94 | x, 95 | allow_empty = allow_empty, 96 | allow_na = allow_na, 97 | allow_null = allow_null 98 | ) 99 | if (is_string) { 100 | return(invisible(NULL)) 101 | } 102 | } 103 | 104 | stop_input_type( 105 | x, 106 | "a single string", 107 | ..., 108 | allow_na = allow_na, 109 | allow_null = allow_null, 110 | arg = arg, 111 | call = call 112 | ) 113 | } 114 | 115 | .rlang_check_is_string <- function(x, 116 | allow_empty, 117 | allow_na, 118 | allow_null) { 119 | if (rlang::is_string(x)) { 120 | if (allow_empty || !rlang::is_string(x, "")) { 121 | return(TRUE) 122 | } 123 | } 124 | 125 | if (allow_null && rlang::is_null(x)) { 126 | return(TRUE) 127 | } 128 | 129 | if (allow_na && (identical(x, NA) || identical(x, rlang::na_chr)) 130 | ) { 131 | return(TRUE) 132 | } 133 | 134 | FALSE 135 | } 136 | 137 | check_name <- function(x, 138 | ..., 139 | allow_null = FALSE, 140 | arg = rlang::caller_arg(x), 141 | call = rlang::caller_env()) { 142 | if (!missing(x)) { 143 | is_string <- .rlang_check_is_string( 144 | x, 145 | allow_empty = FALSE, 146 | allow_na = FALSE, 147 | allow_null = allow_null 148 | ) 149 | if (is_string) { 150 | return(invisible(NULL)) 151 | } 152 | } 153 | 154 | stop_input_type( 155 | x, 156 | "a valid name", 157 | ..., 158 | allow_na = FALSE, 159 | allow_null = allow_null, 160 | arg = arg, 161 | call = call 162 | ) 163 | } 164 | 165 | IS_NUMBER_true <- 0 166 | IS_NUMBER_false <- 1 167 | IS_NUMBER_oob <- 2 168 | 169 | check_number_decimal <- function( 170 | x, 171 | ..., 172 | min = NULL, 173 | max = NULL, 174 | allow_infinite = TRUE, 175 | allow_na = FALSE, 176 | allow_null = FALSE, 177 | arg = rlang::caller_arg(x), 178 | call = rlang::caller_env() 179 | ) { 180 | if (missing(x)) { 181 | exit_code <- IS_NUMBER_false 182 | } else if (0 == (exit_code <- .standalone_types_check_dot_call( 183 | rlang::ffi_standalone_check_number_1.0.7, 184 | x, 185 | allow_decimal = TRUE, 186 | min, 187 | max, 188 | allow_infinite, 189 | allow_na, 190 | allow_null 191 | ))) { 192 | return(invisible(NULL)) 193 | } 194 | 195 | .stop_not_number( 196 | x, 197 | ..., 198 | exit_code = exit_code, 199 | allow_decimal = TRUE, 200 | min = min, 201 | max = max, 202 | allow_na = allow_na, 203 | allow_null = allow_null, 204 | arg = arg, 205 | call = call 206 | ) 207 | } 208 | 209 | check_number_whole <- function(x, 210 | ..., 211 | min = NULL, 212 | max = NULL, 213 | allow_infinite = FALSE, 214 | allow_na = FALSE, 215 | allow_null = FALSE, 216 | arg = rlang::caller_arg(x), 217 | call = rlang::caller_env()) { 218 | if (missing(x)) { 219 | exit_code <- IS_NUMBER_false 220 | } else if (0 == (exit_code <- .standalone_types_check_dot_call( 221 | rlang::ffi_standalone_check_number_1.0.7, 222 | x, 223 | allow_decimal = FALSE, 224 | min, 225 | max, 226 | allow_infinite, 227 | allow_na, 228 | allow_null 229 | ))) { 230 | return(invisible(NULL)) 231 | } 232 | 233 | .stop_not_number( 234 | x, 235 | ..., 236 | exit_code = exit_code, 237 | allow_decimal = FALSE, 238 | min = min, 239 | max = max, 240 | allow_na = allow_na, 241 | allow_null = allow_null, 242 | arg = arg, 243 | call = call 244 | ) 245 | } 246 | 247 | .stop_not_number <- function(x, 248 | ..., 249 | exit_code, 250 | allow_decimal, 251 | min, 252 | max, 253 | allow_na, 254 | allow_null, 255 | arg, 256 | call) { 257 | if (allow_decimal) { 258 | what <- "a number" 259 | } else { 260 | what <- "a whole number" 261 | } 262 | 263 | if (exit_code == IS_NUMBER_oob) { 264 | min <- min %||% -Inf 265 | max <- max %||% Inf 266 | 267 | if (min > -Inf && max < Inf) { 268 | what <- sprintf("%s between %s and %s", what, min, max) 269 | } else if (x < min) { 270 | what <- sprintf("%s larger than or equal to %s", what, min) 271 | } else if (x > max) { 272 | what <- sprintf("%s smaller than or equal to %s", what, max) 273 | } else { 274 | rlang::abort("Unexpected state in OOB check", .internal = TRUE) 275 | } 276 | } 277 | 278 | stop_input_type( 279 | x, 280 | what, 281 | ..., 282 | allow_na = allow_na, 283 | allow_null = allow_null, 284 | arg = arg, 285 | call = call 286 | ) 287 | } 288 | 289 | check_symbol <- function(x, 290 | ..., 291 | allow_null = FALSE, 292 | arg = rlang::caller_arg(x), 293 | call = rlang::caller_env()) { 294 | if (!missing(x)) { 295 | if (rlang::is_symbol(x)) { 296 | return(invisible(NULL)) 297 | } 298 | if (allow_null && rlang::is_null(x)) { 299 | return(invisible(NULL)) 300 | } 301 | } 302 | 303 | stop_input_type( 304 | x, 305 | "a symbol", 306 | ..., 307 | allow_na = FALSE, 308 | allow_null = allow_null, 309 | arg = arg, 310 | call = call 311 | ) 312 | } 313 | 314 | check_arg <- function( 315 | x, 316 | ..., 317 | allow_null = FALSE, 318 | arg = rlang::caller_arg(x), 319 | call = rlang::caller_env() 320 | ) { 321 | if (!missing(x)) { 322 | if (rlang::is_symbol(x)) { 323 | return(invisible(NULL)) 324 | } 325 | if (allow_null && rlang::is_null(x)) { 326 | return(invisible(NULL)) 327 | } 328 | } 329 | 330 | stop_input_type( 331 | x, 332 | "an argument name", 333 | ..., 334 | allow_na = FALSE, 335 | allow_null = allow_null, 336 | arg = arg, 337 | call = call 338 | ) 339 | } 340 | 341 | check_call <- function( 342 | x, 343 | ..., 344 | allow_null = FALSE, 345 | arg = rlang::caller_arg(x), 346 | call = rlang::caller_env() 347 | ) { 348 | if (!missing(x)) { 349 | if (rlang::is_call(x)) { 350 | return(invisible(NULL)) 351 | } 352 | if (allow_null && rlang::is_null(x)) { 353 | return(invisible(NULL)) 354 | } 355 | } 356 | 357 | stop_input_type( 358 | x, 359 | "a defused call", 360 | ..., 361 | allow_na = FALSE, 362 | allow_null = allow_null, 363 | arg = arg, 364 | call = call 365 | ) 366 | } 367 | 368 | check_environment <- function( 369 | x, 370 | ..., 371 | allow_null = FALSE, 372 | arg = rlang::caller_arg(x), 373 | call = rlang::caller_env() 374 | ) { 375 | if (!missing(x)) { 376 | if (rlang::is_environment(x)) { 377 | return(invisible(NULL)) 378 | } 379 | if (allow_null && rlang::is_null(x)) { 380 | return(invisible(NULL)) 381 | } 382 | } 383 | 384 | stop_input_type( 385 | x, 386 | "an environment", 387 | ..., 388 | allow_na = FALSE, 389 | allow_null = allow_null, 390 | arg = arg, 391 | call = call 392 | ) 393 | } 394 | 395 | check_function <- function( 396 | x, 397 | ..., 398 | allow_null = FALSE, 399 | arg = rlang::caller_arg(x), 400 | call = rlang::caller_env() 401 | ) { 402 | if (!missing(x)) { 403 | if (rlang::is_function(x)) { 404 | return(invisible(NULL)) 405 | } 406 | if (allow_null && rlang::is_null(x)) { 407 | return(invisible(NULL)) 408 | } 409 | } 410 | 411 | stop_input_type( 412 | x, 413 | "a function", 414 | ..., 415 | allow_na = FALSE, 416 | allow_null = allow_null, 417 | arg = arg, 418 | call = call 419 | ) 420 | } 421 | 422 | check_closure <- function(x, 423 | ..., 424 | allow_null = FALSE, 425 | arg = rlang::caller_arg(x), 426 | call = rlang::caller_env()) { 427 | if (!missing(x)) { 428 | if (rlang::is_closure(x)) { 429 | return(invisible(NULL)) 430 | } 431 | if (allow_null && rlang::is_null(x)) { 432 | return(invisible(NULL)) 433 | } 434 | } 435 | 436 | stop_input_type( 437 | x, 438 | "an R function", 439 | ..., 440 | allow_na = FALSE, 441 | allow_null = allow_null, 442 | arg = arg, 443 | call = call 444 | ) 445 | } 446 | 447 | check_formula <- function( 448 | x, 449 | ..., 450 | allow_null = FALSE, 451 | arg = rlang::caller_arg(x), 452 | call = rlang::caller_env() 453 | ) { 454 | if (!missing(x)) { 455 | if (rlang::is_formula(x)) { 456 | return(invisible(NULL)) 457 | } 458 | if (allow_null && rlang::is_null(x)) { 459 | return(invisible(NULL)) 460 | } 461 | } 462 | 463 | stop_input_type( 464 | x, 465 | "a formula", 466 | ..., 467 | allow_na = FALSE, 468 | allow_null = allow_null, 469 | arg = arg, 470 | call = call 471 | ) 472 | } 473 | 474 | 475 | # Vectors ----------------------------------------------------------------- 476 | 477 | check_character <- function(x, 478 | ..., 479 | allow_null = FALSE, 480 | arg = rlang::caller_arg(x), 481 | call = rlang::caller_env()) { 482 | if (!missing(x)) { 483 | if (rlang::is_character(x)) { 484 | return(invisible(NULL)) 485 | } 486 | if (allow_null && rlang::is_null(x)) { 487 | return(invisible(NULL)) 488 | } 489 | } 490 | 491 | stop_input_type( 492 | x, 493 | "a character vector", 494 | ..., 495 | allow_na = FALSE, 496 | allow_null = allow_null, 497 | arg = arg, 498 | call = call 499 | ) 500 | } 501 | 502 | check_logical <- function(x, 503 | ..., 504 | allow_null = FALSE, 505 | arg = rlang::caller_arg(x), 506 | call = rlang::caller_env()) { 507 | if (!missing(x)) { 508 | if (rlang::is_logical(x)) { 509 | return(invisible(NULL)) 510 | } 511 | if (allow_null && rlang::is_null(x)) { 512 | return(invisible(NULL)) 513 | } 514 | } 515 | 516 | stop_input_type( 517 | x, 518 | "a logical vector", 519 | ..., 520 | allow_na = FALSE, 521 | allow_null = allow_null, 522 | arg = arg, 523 | call = call 524 | ) 525 | } 526 | 527 | check_data_frame <- function( 528 | x, 529 | ..., 530 | allow_null = FALSE, 531 | arg = rlang::caller_arg(x), 532 | call = rlang::caller_env() 533 | ) { 534 | if (!missing(x)) { 535 | if (is.data.frame(x)) { 536 | return(invisible(NULL)) 537 | } 538 | if (allow_null && rlang::is_null(x)) { 539 | return(invisible(NULL)) 540 | } 541 | } 542 | 543 | stop_input_type( 544 | x, 545 | "a data frame", 546 | ..., 547 | allow_null = allow_null, 548 | arg = arg, 549 | call = call 550 | ) 551 | } 552 | 553 | # nocov end 554 | -------------------------------------------------------------------------------- /R/list-geocoders.R: -------------------------------------------------------------------------------- 1 | #' List Available Geocoder Services 2 | #' 3 | #' Evaluates the logged in user from an authorization token and returns 4 | #' a data.frame containing the available geocoding services for the 5 | #' associated token. 6 | #' 7 | #' The `default_geocoder()` will return the ArcGIS World Geocoder if no 8 | #' token is available. `list_geocoder()` requires an authorization 9 | #' token. 10 | #' 11 | #' @returns a `data.frame` with columns `url`, `northLat`, `southLat`, 12 | #' `eastLon`, `westLon`, `name`, `suggest`, `zoomScale`, `placefinding`, `batch`. 13 | #' 14 | #' @inheritParams arcgisutils::arc_base_req 15 | #' @export 16 | #' @examples 17 | #' 18 | #' # Default geocoder object 19 | #' # ArcGIS World Geocoder b/c no token 20 | #' default_geocoder() 21 | #' 22 | #' # Requires an Authorization Token 23 | #' \dontrun{ 24 | #' list_geocoders() 25 | #' } 26 | list_geocoders <- function(token = arc_token()) { 27 | # capture current env for error propagation 28 | # There may be a reason to include it in the 29 | # function arguments but im not convinced yet 30 | call <- rlang::current_env() 31 | 32 | if (is.null(token)) { 33 | cli::cli_abort( 34 | "{.arg token} is {.code NULL}. Cannot search for geocoders." 35 | ) 36 | } 37 | # extract helper services 38 | self <- arc_self_meta(error_call = call) 39 | # extrac geocode metadata 40 | geocoder_metadata <- self[["helperServices"]][["geocode"]] 41 | 42 | if (is.null(geocoder_metadata)) { 43 | current_user <- self$user$username %||% "not signed in" 44 | 45 | cli::cli_abort( 46 | c( 47 | "No geocoder services found", 48 | ">" = "Current user: {.emph {current_user}}", 49 | ">" = "Portal: {.url {self$portalHostname}}", 50 | call = call 51 | ) 52 | ) 53 | } 54 | 55 | # use pillar -- should be done in arcgisutils 56 | data_frame(geocoder_metadata) 57 | } 58 | 59 | #' Provides a default GeocodeServer 60 | #' 61 | #' For users who have not signed into a private portal or ArcGIS Online, 62 | #' the public [ArcGIS World Geocoder](https://www.esri.com/en-us/arcgis/products/arcgis-world-geocoder) is used. Otherwise, the first available geocoding service associated 63 | #' with your authorization token is used. 64 | #' 65 | #' To manually create a `GeocodeServer` object, see [`geocode_server()`]. 66 | #' @inheritParams arc_token 67 | #' @export 68 | #' @rdname list_geocoders 69 | default_geocoder <- function(token = arc_token()) { 70 | if (is.null(token)) { 71 | return(world_geocoder) 72 | } 73 | 74 | res <- list_geocoders(token = token) 75 | 76 | if (nrow(res) > 1) { 77 | return(world_geocoder) 78 | } 79 | 80 | geocode_server(res[1, "url"]) 81 | } 82 | -------------------------------------------------------------------------------- /R/sysdata.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/R/sysdata.rda -------------------------------------------------------------------------------- /R/utils-camel-case.R: -------------------------------------------------------------------------------- 1 | #' Convert a character vector to lower camel case 2 | #' @param x a character vector 3 | #' @keywords internal 4 | #' @noRd 5 | to_lower_camel <- function(x, call = rlang::caller_env()) { 6 | check_character(x, call = call, allow_null = TRUE) 7 | if (is.null(x)) { 8 | return(NULL) 9 | } 10 | vapply(strsplit(x, "_"), .lower_camel_case, character(1)) 11 | } 12 | 13 | .lower_camel_case <- function(.x) { 14 | n <- length(.x) 15 | if (n == 1) { 16 | return(.x) 17 | } 18 | 19 | .x[2:n] <- tools::toTitleCase(.x[2:n]) 20 | paste(.x, collapse = "") 21 | } 22 | -------------------------------------------------------------------------------- /R/utils-checks.R: -------------------------------------------------------------------------------- 1 | #' 2 | #' @keywords internal 3 | #' @noRd 4 | check_for_storage <- function( 5 | for_storage, 6 | token, 7 | call = rlang::caller_env()) { 8 | check_bool(for_storage, call = call) 9 | # Tokens are required for reverseGeocoding for storage 10 | if (for_storage) { 11 | obj_check_token(token, call = call) 12 | inform_for_storage(call = call) 13 | } 14 | } 15 | 16 | match_location_type <- function( 17 | location_type, 18 | .multiple = FALSE, 19 | call = rlang::caller_env()) { 20 | # TODO maybe want to use error_arg for better message 21 | rlang::arg_match( 22 | location_type, 23 | values = c("rooftop", "street"), 24 | multiple = .multiple, 25 | error_call = call 26 | ) 27 | } 28 | 29 | match_label_values <- function( 30 | preferred_label_values, 31 | .multiple = FALSE, 32 | call = rlang::caller_env()) { 33 | rlang::arg_match( 34 | preferred_label_values, 35 | values = c("postalCity", "localCity"), 36 | multiple = .multiple, 37 | error_call = call 38 | ) 39 | } 40 | 41 | check_iso_3166 <- function( 42 | x, 43 | allow_null = TRUE, 44 | scalar = FALSE, 45 | arg = rlang::caller_arg(x), 46 | call = rlang::caller_env()) { 47 | if (is.null(x)) { 48 | return(invisible(NULL)) 49 | } 50 | 51 | if (scalar) { 52 | check_string(x, allow_null = allow_null) 53 | } else { 54 | check_character(x, allow_null = allow_null) 55 | } 56 | 57 | if (!all(is_iso3166(x))) { 58 | cli::cli_abort( 59 | c( 60 | "{.arg {arg}} is not a recognized Country Code", 61 | "i" = "See {.fn iso_3166_codes} for ISO 3166 codes" 62 | ), 63 | call = call 64 | ) 65 | } 66 | } 67 | 68 | 69 | check_locations <- function( 70 | locations, 71 | allow_null = TRUE, 72 | call = rlang::caller_env()) { 73 | if (is.null(locations)) { 74 | return(invisible(NULL)) 75 | } 76 | 77 | if (!rlang::inherits_all(locations, c("sfc_POINT", "sfc"))) { 78 | stop_input_type(locations, "sfc_POINT", call = call) 79 | } 80 | } 81 | 82 | check_extent <- function( 83 | extent, 84 | allow_null = TRUE, 85 | arg = rlang::caller_arg(extent), 86 | call = rlang::caller_call()) { 87 | if (is.null(extent)) { 88 | return(invisible(NULL)) 89 | } 90 | 91 | if (!rlang::inherits_all(extent, "bbox")) { 92 | stop_input_type(extent, "bbox", call = call) 93 | } 94 | } 95 | 96 | inform_for_storage <- function(call = rlang::caller_env()) { 97 | .freq <- getOption("arcgisgeocode.storage", default = "once") 98 | 99 | if (!identical(.freq, "never")) { 100 | cli::cli_inform( 101 | c( 102 | "!" = "{.arg for_storage} is set to {.code FALSE}, results cannot be persisted.", 103 | "*" = "See the {.href [official documentation](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm#ESRI_SECTION3_BBCB5704B46B4CDF8377749B873B1A7F)} for legal obligations or the {.help storage} help page.", 104 | "i" = "suppress this message by setting {.code options(\"arcgisgeocode.storage\" = \"never\")}" 105 | ), 106 | .frequency = "once", 107 | .frequency_id = "for_storage" 108 | ) 109 | } 110 | } 111 | 112 | # This function will convert a number of different representations 113 | # into points that can be used instead of sfc 114 | obj_as_points <- function( 115 | x, 116 | allow_null = TRUE, 117 | arg = rlang::caller_arg(x), 118 | call = rlang::caller_env()) { 119 | if (is.null(x) && allow_null) { 120 | return(NULL) 121 | } else if (rlang::inherits_any(x, "POINT")) { 122 | if (abs(x[1]) > 180 || abs(x[[2]]) > 90) { 123 | abort_4326(arg, call) 124 | } 125 | return(sf::st_sfc(x, crs = 4326)) 126 | } else if (rlang::inherits_any(x, "matrix") && is.numeric(x)) { 127 | if (max(abs(x[, 1]), na.rm = TRUE) > 180 || max(abs(x[, 2]), na.rm = TRUE) > 90) { 128 | abort_4326(arg, call) 129 | } 130 | return(sf::st_cast(sf::st_sfc(sf::st_multipoint(x), crs = 4326), "POINT")) 131 | } else if (is.numeric(x)) { 132 | if (abs(x[1]) > 180 || abs(x[[2]]) > 90) { 133 | abort_4326(arg, call) 134 | } else if (length(x) > 4) { 135 | cli::cli_abort("{arg} is {obj_type_friendly(x)} and cannot exceed 4 elements", call = call) 136 | } 137 | return(sf::st_sfc(sf::st_point(x), crs = 4326)) 138 | } else if (rlang::inherits_all(x, c("sfc_POINT", "sfc"))) { 139 | return(x) 140 | } else { 141 | cli::cli_abort(c( 142 | "{.arg {arg}} cannot be converted to a point", 143 | "i" = "found {obj_type_friendly(x)}" 144 | ), arg = arg, call = call) 145 | } 146 | } 147 | 148 | abort_4326 <- function(arg, call) { 149 | cli::cli_abort(c( 150 | "{.arg {arg}}, {obj_type_friendly(x)}, must be in EPSG:4326", 151 | ">" = "{.code longitude} values must be in the range [-180, 180]", 152 | " " = "and {.code latitude} values must be in the range [-90, 90]" 153 | ), call = call) 154 | } 155 | 156 | # 157 | # check_extent <- function( 158 | # x, 159 | # ..., 160 | # allow_null = FALSE, 161 | # arg = rlang::caller_arg(x), 162 | # call = rlang::caller_env() 163 | # ) { 164 | # if (!missing(x)) { 165 | # if (rlang::inherits_any(x, "bbox")) { 166 | # return(invisible(NULL)) 167 | # } 168 | # if (allow_null && rlang::is_null(x)) { 169 | # return(invisible(NULL)) 170 | # } 171 | # } 172 | # 173 | # stop_input_type( 174 | # x, 175 | # "a `bbox`", 176 | # ..., 177 | # allow_na = FALSE, 178 | # allow_null = allow_null, 179 | # arg = arg, 180 | # call = call 181 | # ) 182 | # } 183 | -------------------------------------------------------------------------------- /R/utils-crs.R: -------------------------------------------------------------------------------- 1 | #' Returns a CRS object from a WKID 2 | #' 3 | #' When an Esri WKID is returned, they are not recognized 4 | #' by {sf} without a prefixed authority e.g. `"Esri:102719"`. 5 | #' This function adds the Esri authority prefix if it is 6 | #' recognized as an Esri WKID. 7 | #' 8 | #' @details 9 | #' Esri WKIDs were identified from the [`{arcgeocoder}`](https://cran.r-project.org/package=arcgeocoder) package from 10 | #' [\@dieghernan](https://github.com/dieghernan). 11 | #' 12 | #' @param wkid an integer scalar 13 | #' @keywords internal 14 | #' @noRd 15 | parse_wkid <- function(wkid) { 16 | is_esri_srid <- any(wkid == arcgisgeocode::esri_wkids) 17 | if (is_esri_srid) { 18 | sf::st_crs(paste("ESRI", wkid, sep = ":")) 19 | } else { 20 | sf::st_crs(wkid) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /R/utils-geocoder-obj.R: -------------------------------------------------------------------------------- 1 | #' Create a GeocodeServer 2 | #' 3 | #' Create an object of class `GeocodeServer` from a URL. This object 4 | #' stores the service definition of the geocoding service as a list object. 5 | #' 6 | #' @param url the URL of a geocoding server. 7 | #' @inheritParams arcgisutils::arc_base_req 8 | #' @examples 9 | #' server_url <- "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" 10 | #' geocode_server(server_url) 11 | #' @export 12 | #' @returns an object of class `GeocodeServer`. 13 | geocode_server <- function(url, token = arc_token()) { 14 | b_req <- arc_base_req(url, token = token, query = c("f" = "json")) 15 | resp <- httr2::req_perform(b_req) 16 | jsn <- httr2::resp_body_string(resp) 17 | res <- RcppSimdJson::fparse(jsn) 18 | detect_errors(res) # check for any errors and report if thats the case 19 | res[["url"]] <- url 20 | structure(res, class = c("GeocodeServer", "list")) 21 | } 22 | 23 | world_geocoder_url <- "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" 24 | 25 | # Print method for the geocoder, must be exported 26 | #' @export 27 | print.GeocodeServer <- function(x, ...) { 28 | # if the description is "" we don't print it 29 | descrip <- x$serviceDescription 30 | if (!nzchar(descrip)) { 31 | descrip <- NULL 32 | } 33 | 34 | to_print <- compact( 35 | list( 36 | Description = descrip, 37 | Version = x$currentVersion, 38 | CRS = x[["spatialReference"]][["latestWkid"]] 39 | ) 40 | ) 41 | 42 | header <- "" 43 | body <- paste0(names(to_print), ": ", to_print) 44 | 45 | cat(header, body, sep = "\n") 46 | invisible(x) 47 | } 48 | 49 | # handy check function to see if the type is a GeocodeServer 50 | check_geocoder <- function(x, arg = rlang::caller_arg(x), call = rlang::caller_env()) { 51 | if (!rlang::inherits_any(x, "GeocodeServer")) { 52 | cli::cli_abort("Expected {.cls GeocodeServer}, {.arg {arg}} is {obj_type_friendly(x)}") 53 | } 54 | 55 | invisible(NULL) 56 | } 57 | 58 | #' Returns the capabilities of the geocoder as a character vector 59 | #' @keywords internal 60 | #' @noRd 61 | capabilities <- function(geocoder) { 62 | tolower(strsplit(geocoder[["capabilities"]], ",")[[1]]) 63 | } 64 | 65 | #' Determines if there are different fields in the geocoder object 66 | #' TRUE if there are fields that are not in the default world geocoder 67 | #' FALSE if there arent 68 | #' @keywords internal 69 | #' @noRd 70 | has_custom_fields <- function(x) { 71 | custom_fields <- setdiff( 72 | x$candidateFields$name, 73 | arcgisgeocode::world_geocoder$candidateFields$name 74 | ) 75 | 76 | length(custom_fields) > 0 77 | } 78 | -------------------------------------------------------------------------------- /R/utils-iso-3166.R: -------------------------------------------------------------------------------- 1 | #' ISO 3166 Country Codes 2 | #' 3 | #' Create a data.frame of ISO 3166 2 and 3 digit Country codes. 4 | #' 5 | #' @details 6 | #' Country codes provided by [`rust_iso3166`](https://docs.rs/rust_iso3166/latest/rust_iso3166/index.html). 7 | #' 8 | #' @returns a `data.frame` with columns `country`, `code_2`, `code_3`. 9 | #' @export 10 | #' @examples 11 | #' head(iso_3166_codes()) 12 | iso_3166_codes <- function() { 13 | codes <- data.frame( 14 | country = iso_3166_names(), 15 | code_2 = iso_3166_2(), 16 | code_3 = iso_3166_3() 17 | ) 18 | data_frame(codes) 19 | } 20 | 21 | 22 | #' Add `tbl` class to a data.frame 23 | #' 24 | #' When pillar is loaded, a data.frame will print like a tibble 25 | #' but will not inherit the tibble class. 26 | #' 27 | #' @noRd 28 | #' @keywords internal 29 | data_frame <- function(x, call = rlang::caller_env()) { 30 | check_data_frame(x, call = call) 31 | structure(x, class = c("tbl", "data.frame")) 32 | } 33 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | format: gfm 4 | --- 5 | 6 | 7 | 8 | ```{r, include = FALSE} 9 | knitr::opts_chunk$set( 10 | collapse = TRUE, 11 | comment = "#>", 12 | fig.path = "man/figures/README-", 13 | out.width = "100%" 14 | ) 15 | library(jsonify) 16 | ``` 17 | 18 | # arcgisgeocode 19 | 20 | 21 | [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 22 | [![CRAN status](https://www.r-pkg.org/badges/version/arcgisgeocode)](https://CRAN.R-project.org/package=arcgisgeocode) 23 | [![R-CMD-check](https://github.com/R-ArcGIS/arcgisgeocode/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/R-ArcGIS/arcgisgeocode/actions/workflows/R-CMD-check.yaml) 24 | 25 | 26 | arcgisgeocode provides access to ArcGIS geocoding services from R. It supports address candidate identification, batch geocoding, reverse geocoding, and autocomplete suggestions. 27 | 28 | ## Installation 29 | 30 | Install the package from CRAN 31 | 32 | ``` r 33 | # install from CRAN 34 | install.packages("arcgisgeocode") 35 | ``` 36 | 37 | You can also install the development version from r-universe as a binary for Mac, Windows, or Ubuntu from r-universe like so: 38 | 39 | ``` r 40 | # install from R-universe 41 | install.packages("arcgisgeocode", repos = "https://r-arcgis.r-universe.dev") 42 | ``` 43 | 44 | Or you can install the package from source which requires Rust to be available. Follow the [rustup instructions](https://rustup.rs/) to install Rust and verify your installation is compatible using [`rextendr::rust_sitrep()`](https://extendr.github.io/rextendr/dev/#sitrep). Then install the development version from GitHub: 45 | 46 | ``` r 47 | # install pak if not available 48 | if (!requireNamespace("pak")) install.packages("pak") 49 | 50 | # install development version of {arcgisgeocode} 51 | pak::pak("r-arcgis/arcgisgeocode") 52 | ``` 53 | 54 | ## Usage 55 | 56 | By default, the [ArcGIS World Geocoder](https://www.esri.com/en-us/arcgis/products/arcgis-world-geocoder) will be used. This geocoding server provides public access to the [`/findAddressCandidates`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm), [`/reverseGeocode`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm), and [`/suggest`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm) endpoints made available via the `find_address_candidates()`, `reverse_geocode()`, and `suggest_places()` functions respectively. 57 | 58 | The batch geocoding endpoint [`/geocodeAddresses`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-geocode-addresses.htm) is available via `geocode_addresses()`. However, this requires the use of an authorization token and may consume credits. 59 | 60 | Refer to the ArcGIS World Geocoder [official documentation](https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm) for additional information on use restrictions and licensing. For example, a valid token is required to [store the results](#important-storing-results) of geocoding transactions. 61 | 62 | ### Reverse geocoding 63 | 64 | Reverse geocoding takes a location and finds the associated address. 65 | 66 | ::: callout-tip 67 | A token _is not required_ to use this function. 68 | ::: 69 | 70 | ```{r, include = FALSE} 71 | library(pillar) 72 | options("arcgisgeocode.storage" = "never") 73 | ``` 74 | 75 | ```{r} 76 | library(arcgisgeocode) 77 | 78 | # Find addresses from locations 79 | rev_res <- reverse_geocode(c(-117.172, 34.052)) 80 | 81 | # preview results 82 | dplyr::glimpse(rev_res) 83 | ``` 84 | 85 | ### Address search 86 | 87 | The `find_address_candidates()` function returns geocoding candidate results. The function is vectorized over the input and will perform multiple requests in parallel. Each request geocodes one location at a time. 88 | 89 | One or more candidates are returned from the endpoint. You can limit the number of candidates using the `max_locations` argument (with a maximum of 50). 90 | 91 | ::: callout-tip 92 | A token _is not required_ to use this function. 93 | ::: 94 | 95 | ```{r} 96 | # Find addresses from address search 97 | candidates <- find_address_candidates( 98 | address = "esri", 99 | address2 = "380 new york street", 100 | city = "redlands", 101 | country_code = "usa", 102 | max_locations = 2 103 | ) 104 | 105 | dplyr::glimpse(candidates[, 1:10]) 106 | ``` 107 | 108 | ### Suggest locations 109 | 110 | Geocoding services can also provide a location suggestion based on a search term and, optionally, a location or extent. The `suggest_places()` function (`/suggest` endpoint) is intended to be used as part of a client-facing application that provides autocomplete suggestions. 111 | 112 | In this example we create a search extent around a single point and find suggestions based on the search term `"bellwood"`. 113 | 114 | ::: callout-tip 115 | A token _is not required_ to use this function. 116 | ::: 117 | 118 | ```{r} 119 | # identify a search point as a simple feature column 120 | location <- sf::st_sfc( 121 | sf::st_point(c(-84.34, 33.74)), 122 | crs = 4326 123 | ) 124 | 125 | # buffer and create a bbox object to search within the extent 126 | search_extent <- sf::st_bbox( 127 | sf::st_buffer(location, 10) 128 | ) 129 | 130 | # find suggestions within the bounding box 131 | suggestions <- suggest_places( 132 | "bellwood", 133 | location, 134 | search_extent = search_extent 135 | ) 136 | 137 | suggestions 138 | ``` 139 | 140 | The result is intended to be provided to `find_address_candidates()` to complete the geocoding process. The column `text` contains the address to geocode. The column `magic_key` is a special identifier that makes it much faster to fetch results. Pass this into the argument `magic_key`. 141 | 142 | ```{r} 143 | # get address candidate information 144 | # using the text and the magic key 145 | res <- find_address_candidates( 146 | suggestions$text, 147 | magic_key = suggestions$magic_key 148 | ) 149 | 150 | dplyr::glimpse(res[, 1:10]) 151 | ``` 152 | 153 | ## *Important*: Storing results 154 | 155 | By default, the argument `for_storage = FALSE` meaning that the results of the geocoding operation cannot be persisted. If you intend to persist the results of the geocoding operation, you must set `for_storage = TRUE`. 156 | 157 | To learn more about free and paid geocoding operations refer to the [storage parameter documentation](https://developers.arcgis.com/documentation/mapping-apis-and-services/geocoding/services/geocoding-service/#storage-parameter). 158 | 159 | ## Batch geocoding 160 | 161 | Many addresses can be geocoded very quickly using the `geocode_addresses()` function which calls the `/geocodeAddresses` endpoint. Note that this function requires an authorization token. `geocode_addresses()` sends the input addresses in chunks as parallel requests. 162 | 163 | Batch geocoding requires a signed in user. Load the [`{arcgisutils}`](https://github.com/r-arcgis/arcgisutils) to authorize and set your token. This example uses the [Geocoding Test Dataset](# https://datacatalog.urban.org/node/6158/revisions/14192/view) from the [Urban Institute](https://www.urban.org/). 164 | 165 | ::: callout-tip 166 | A token _is required_ to use this function with the World Geocoding Service. It may not be necessary if you are using a private ArcGIS Enterprise service. 167 | ::: 168 | 169 | ```{r, eval = FALSE} 170 | library(arcgisutils) 171 | library(arcgisgeocode) 172 | 173 | set_arc_token(auth_user()) 174 | 175 | # Example dataset from the Urban Institute 176 | fp <- "https://urban-data-catalog.s3.amazonaws.com/drupal-root-live/2020/02/25/geocoding_test_data.csv" 177 | 178 | to_geocode <- readr::read_csv(fp, readr::locale(encoding = "latin1")) 179 | 180 | geocoded <- to_geocode |> 181 | dplyr::reframe( 182 | geocode_addresses( 183 | address = address, 184 | city = city, 185 | region = state, 186 | postal = zip 187 | ) 188 | ) 189 | 190 | dplyr::glimpse(res[, 1:10]) 191 | ``` 192 | 193 | ## Using other locators 194 | 195 | `{arcgisgeocode}` can be used with other geocoding services, including custom locators hosted on ArcGIS Online or Enterprise. For example, we can use the [AddressNC](https://www.nconemap.gov/pages/addresses) geocoding service [available on ArcGIS Online](https://www.arcgis.com/home/item.html?id=247dfe30ec42476a96926ad9e35f725f). 196 | 197 | Create a new `GeocodeServer` object using `geocode_server()`. This geocoder can be passed into the `geocoder` argument to any of the geocoding functions. 198 | 199 | ```{r} 200 | address_nc <- geocode_server( 201 | "https://services.nconemap.gov/secure/rest/services/AddressNC/AddressNC_geocoder/GeocodeServer", 202 | token = NULL 203 | ) 204 | 205 | res <- find_address_candidates( 206 | address = "rowan coffee", 207 | city = "asheville", 208 | geocoder = address_nc 209 | ) 210 | 211 | dplyr::glimpse(res[, 1:10]) 212 | ``` 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # arcgisgeocode 5 | 6 | 7 | 8 | [![Lifecycle: 9 | experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) 10 | [![CRAN 11 | status](https://www.r-pkg.org/badges/version/arcgisgeocode.png)](https://CRAN.R-project.org/package=arcgisgeocode) 12 | [![R-CMD-check](https://github.com/R-ArcGIS/arcgisgeocode/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/R-ArcGIS/arcgisgeocode/actions/workflows/R-CMD-check.yaml) 13 | 14 | 15 | arcgisgeocode provides access to ArcGIS geocoding services from R. It 16 | supports address candidate identification, batch geocoding, reverse 17 | geocoding, and autocomplete suggestions. 18 | 19 | ## Installation 20 | 21 | Install the package from CRAN 22 | 23 | ``` r 24 | # install from CRAN 25 | install.packages("arcgisgeocode") 26 | ``` 27 | 28 | You can also install the development version from r-universe as a binary 29 | for Mac, Windows, or Ubuntu from r-universe like so: 30 | 31 | ``` r 32 | # install from R-universe 33 | install.packages("arcgisgeocode", repos = "https://r-arcgis.r-universe.dev") 34 | ``` 35 | 36 | Or you can install the package from source which requires Rust to be 37 | available. Follow the [rustup instructions](https://rustup.rs/) to 38 | install Rust and verify your installation is compatible using 39 | [`rextendr::rust_sitrep()`](https://extendr.github.io/rextendr/dev/#sitrep). 40 | Then install the development version from GitHub: 41 | 42 | ``` r 43 | # install pak if not available 44 | if (!requireNamespace("pak")) install.packages("pak") 45 | 46 | # install development version of {arcgisgeocode} 47 | pak::pak("r-arcgis/arcgisgeocode") 48 | ``` 49 | 50 | ## Usage 51 | 52 | By default, the [ArcGIS World 53 | Geocoder](https://www.esri.com/en-us/arcgis/products/arcgis-world-geocoder) 54 | will be used. This geocoding server provides public access to the 55 | [`/findAddressCandidates`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm), 56 | [`/reverseGeocode`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm), 57 | and 58 | [`/suggest`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm) 59 | endpoints made available via the `find_address_candidates()`, 60 | `reverse_geocode()`, and `suggest_places()` functions respectively. 61 | 62 | The batch geocoding endpoint 63 | [`/geocodeAddresses`](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-geocode-addresses.htm) 64 | is available via `geocode_addresses()`. However, this requires the use 65 | of an authorization token and may consume credits. 66 | 67 | Refer to the ArcGIS World Geocoder [official 68 | documentation](https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm) 69 | for additional information on use restrictions and licensing. For 70 | example, a valid token is required to [store the 71 | results](#important-storing-results) of geocoding transactions. 72 | 73 | ### Reverse geocoding 74 | 75 | Reverse geocoding takes a location and finds the associated address. 76 | 77 | > [!TIP] 78 | > 79 | > A token *is not required* to use this function. 80 | 81 | ``` r 82 | library(arcgisgeocode) 83 | 84 | # Find addresses from locations 85 | rev_res <- reverse_geocode(c(-117.172, 34.052)) 86 | 87 | # preview results 88 | dplyr::glimpse(rev_res) 89 | #> Rows: 1 90 | #> Columns: 23 91 | #> $ match_addr "600-620 Home Pl, Redlands, California, 92374" 92 | #> $ long_label "600-620 Home Pl, Redlands, CA, 92374, USA" 93 | #> $ short_label "600-620 Home Pl" 94 | #> $ addr_type "StreetAddress" 95 | #> $ type_field "" 96 | #> $ place_name "" 97 | #> $ add_num "608" 98 | #> $ address "608 Home Pl" 99 | #> $ block "" 100 | #> $ sector "" 101 | #> $ neighborhood "South Redlands" 102 | #> $ district "" 103 | #> $ city "Redlands" 104 | #> $ metro_area "" 105 | #> $ subregion "San Bernardino County" 106 | #> $ region "California" 107 | #> $ region_abbr "CA" 108 | #> $ territory "" 109 | #> $ postal "92374" 110 | #> $ postal_ext "" 111 | #> $ country_name "United States" 112 | #> $ country_code "USA" 113 | #> $ geometry POINT (-117.172 34.05204) 114 | ``` 115 | 116 | ### Address search 117 | 118 | The `find_address_candidates()` function returns geocoding candidate 119 | results. The function is vectorized over the input and will perform 120 | multiple requests in parallel. Each request geocodes one location at a 121 | time. 122 | 123 | One or more candidates are returned from the endpoint. You can limit the 124 | number of candidates using the `max_locations` argument (with a maximum 125 | of 50). 126 | 127 | > [!TIP] 128 | > 129 | > A token *is not required* to use this function. 130 | 131 | ``` r 132 | # Find addresses from address search 133 | candidates <- find_address_candidates( 134 | address = "esri", 135 | address2 = "380 new york street", 136 | city = "redlands", 137 | country_code = "usa", 138 | max_locations = 2 139 | ) 140 | 141 | dplyr::glimpse(candidates[, 1:10]) 142 | #> Rows: 2 143 | #> Columns: 11 144 | #> $ input_id 1, 1 145 | #> $ result_id NA, NA 146 | #> $ loc_name "World", "World" 147 | #> $ status "M", "M" 148 | #> $ score 100.00, 98.57 149 | #> $ match_addr "Esri", "380 New York St, Redlands, California, 92373" 150 | #> $ long_label "Esri, 380 New York St, Redlands, CA, 92373, USA", "380 Ne… 151 | #> $ short_label "Esri", "380 New York St" 152 | #> $ addr_type "POI", "PointAddress" 153 | #> $ type_field "Business Facility", NA 154 | #> $ geometry POINT (-117.1957 34.05609), POINT (-117.1948 34.05726)… 155 | ``` 156 | 157 | ### Suggest locations 158 | 159 | Geocoding services can also provide a location suggestion based on a 160 | search term and, optionally, a location or extent. The 161 | `suggest_places()` function (`/suggest` endpoint) is intended to be used 162 | as part of a client-facing application that provides autocomplete 163 | suggestions. 164 | 165 | In this example we create a search extent around a single point and find 166 | suggestions based on the search term `"bellwood"`. 167 | 168 | > [!TIP] 169 | > 170 | > A token *is not required* to use this function. 171 | 172 | ``` r 173 | # identify a search point as a simple feature column 174 | location <- sf::st_sfc( 175 | sf::st_point(c(-84.34, 33.74)), 176 | crs = 4326 177 | ) 178 | 179 | # buffer and create a bbox object to search within the extent 180 | search_extent <- sf::st_bbox( 181 | sf::st_buffer(location, 10) 182 | ) 183 | 184 | # find suggestions within the bounding box 185 | suggestions <- suggest_places( 186 | "bellwood", 187 | location, 188 | search_extent = search_extent 189 | ) 190 | 191 | suggestions 192 | #> # A data frame: 5 × 3 193 | #> text magic_key is_collection 194 | #> * 195 | #> 1 Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 3… dHA9MCN0… FALSE 196 | #> 2 Bellwood, Atlanta, GA, USA dHA9MCN0… FALSE 197 | #> 3 Bellwood Church, Atlanta, GA, USA dHA9MCN0… FALSE 198 | #> 4 Bellwood Yard, Atlanta, GA, USA dHA9MCN0… FALSE 199 | #> 5 Bellwood, IL, USA dHA9NCN0… FALSE 200 | ``` 201 | 202 | The result is intended to be provided to `find_address_candidates()` to 203 | complete the geocoding process. The column `text` contains the address 204 | to geocode. The column `magic_key` is a special identifier that makes it 205 | much faster to fetch results. Pass this into the argument `magic_key`. 206 | 207 | ``` r 208 | # get address candidate information 209 | # using the text and the magic key 210 | res <- find_address_candidates( 211 | suggestions$text, 212 | magic_key = suggestions$magic_key 213 | ) 214 | 215 | dplyr::glimpse(res[, 1:10]) 216 | #> Rows: 7 217 | #> Columns: 11 218 | #> $ input_id 1, 2, 3, 4, 5, 5, 5 219 | #> $ result_id NA, NA, NA, NA, NA, NA, NA 220 | #> $ loc_name NA, NA, NA, NA, NA, NA, NA 221 | #> $ status "M", "M", "M", "M", "T", "T", "T" 222 | #> $ score 100, 100, 100, 100, 100, 100, 100 223 | #> $ match_addr "Bellwood Coffee", "Bellwood, Atlanta, Georgia", "Bellwood… 224 | #> $ long_label "Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 30316… 225 | #> $ short_label "Bellwood Coffee", "Bellwood", "Bellwood Church", "Bellwoo… 226 | #> $ addr_type "POI", "Locality", "POI", "POI", "Locality", "Locality", "… 227 | #> $ type_field "Snacks", "City", "Church", "Building", "City", "City", "C… 228 | #> $ geometry POINT (-84.34273 33.74034), POINT (-84.41243 33.77455), PO… 229 | ``` 230 | 231 | ## *Important*: Storing results 232 | 233 | By default, the argument `for_storage = FALSE` meaning that the results 234 | of the geocoding operation cannot be persisted. If you intend to persist 235 | the results of the geocoding operation, you must set 236 | `for_storage = TRUE`. 237 | 238 | To learn more about free and paid geocoding operations refer to the 239 | [storage parameter 240 | documentation](https://developers.arcgis.com/documentation/mapping-apis-and-services/geocoding/services/geocoding-service/#storage-parameter). 241 | 242 | ## Batch geocoding 243 | 244 | Many addresses can be geocoded very quickly using the 245 | `geocode_addresses()` function which calls the `/geocodeAddresses` 246 | endpoint. Note that this function requires an authorization token. 247 | `geocode_addresses()` sends the input addresses in chunks as parallel 248 | requests. 249 | 250 | Batch geocoding requires a signed in user. Load the 251 | [`{arcgisutils}`](https://github.com/r-arcgis/arcgisutils) to authorize 252 | and set your token. This example uses the [Geocoding Test 253 | Dataset](#%20https://datacatalog.urban.org/node/6158/revisions/14192/view) 254 | from the [Urban Institute](https://www.urban.org/). 255 | 256 | > [!TIP] 257 | > 258 | > A token *is required* to use this function with the World Geocoding 259 | > Service. It may not be necessary if you are using a private ArcGIS 260 | > Enterprise service. 261 | 262 | ``` r 263 | library(arcgisutils) 264 | library(arcgisgeocode) 265 | 266 | set_arc_token(auth_user()) 267 | 268 | # Example dataset from the Urban Institute 269 | fp <- "https://urban-data-catalog.s3.amazonaws.com/drupal-root-live/2020/02/25/geocoding_test_data.csv" 270 | 271 | to_geocode <- readr::read_csv(fp, readr::locale(encoding = "latin1")) 272 | 273 | geocoded <- to_geocode |> 274 | dplyr::reframe( 275 | geocode_addresses( 276 | address = address, 277 | city = city, 278 | region = state, 279 | postal = zip 280 | ) 281 | ) 282 | 283 | dplyr::glimpse(res[, 1:10]) 284 | ``` 285 | 286 | ## Using other locators 287 | 288 | `{arcgisgeocode}` can be used with other geocoding services, including 289 | custom locators hosted on ArcGIS Online or Enterprise. For example, we 290 | can use the [AddressNC](https://www.nconemap.gov/pages/addresses) 291 | geocoding service [available on ArcGIS 292 | Online](https://www.arcgis.com/home/item.html?id=247dfe30ec42476a96926ad9e35f725f). 293 | 294 | Create a new `GeocodeServer` object using `geocode_server()`. This 295 | geocoder can be passed into the `geocoder` argument to any of the 296 | geocoding functions. 297 | 298 | ``` r 299 | address_nc <- geocode_server( 300 | "https://services.nconemap.gov/secure/rest/services/AddressNC/AddressNC_geocoder/GeocodeServer", 301 | token = NULL 302 | ) 303 | 304 | res <- find_address_candidates( 305 | address = "rowan coffee", 306 | city = "asheville", 307 | geocoder = address_nc 308 | ) 309 | 310 | dplyr::glimpse(res[, 1:10]) 311 | #> Rows: 2 312 | #> Columns: 11 313 | #> $ input_id 1, 1 314 | #> $ result_id NA, NA 315 | #> $ loc_name NA, NA 316 | #> $ status "T", "T" 317 | #> $ score 78, 78 318 | #> $ match_addr "ASHEVILLE", "ASHEVILLE" 319 | #> $ long_label "ASHEVILLE", "ASHEVILLE" 320 | #> $ short_label "ASHEVILLE", "ASHEVILLE" 321 | #> $ addr_type "Locality", "Locality" 322 | #> $ type_field "City", "City" 323 | #> $ geometry POINT (943428.1 681596.4), POINT (948500.3 631973.4)… 324 | ``` 325 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: ~ 2 | template: 3 | bootstrap: 5 4 | 5 | reference: 6 | 7 | - title: Geocoding Services 8 | contents: 9 | - geocode_addresses 10 | - reverse_geocode 11 | - find_address_candidates 12 | - suggest_places 13 | - storage 14 | 15 | - title: GeocodeServer objects 16 | contents: 17 | - geocode_server 18 | - default_geocoder 19 | - list_geocoders 20 | - world_geocoder 21 | 22 | - title: Utilities 23 | contents: 24 | - iso_3166_codes 25 | - esri_wkids -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | : "${R_HOME=`R RHOME`}" 3 | "${R_HOME}/bin/Rscript" tools/config.R 4 | -------------------------------------------------------------------------------- /configure.win: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" tools/config.R 3 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | This is a maintenance release to ensure that the package passes checks on R-devel. 2 | 3 | ## R CMD check results 4 | 5 | 0 errors | 0 warnings | 0 note 6 | 7 | ## tarball size 8 | 9 | This package vendors **Rust dependencies** resulting in a 13mb .xz file. Final installation size is 1.3mb 10 | 11 | 12 | ## Testing Environments 13 | 14 | GitHub Actions: 15 | 16 | - {os: macos-latest, r: 'release'}  17 | - {os: windows-latest, r: 'release'} 18 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 19 | - {os: ubuntu-latest, r: 'release'} 20 | - {os: ubuntu-latest, r: 'oldrel-1'} 21 | - {os: ubuntu-latest, r: 'oldrel-2'} 22 | 23 | ## Software Naming 24 | 25 | ArcGIS is a brand name and not the name of a specific software. 26 | 27 | The phrase 'Geocoding service' refers to a spefic API which can be considered software. This is quoted in the DESCRIPTION file. Additionally, 'ArcGIS World Geocoder' and 'ArcGIS Enterprise' are products and software offering which is why they are quoted. 28 | 29 | ## Use of \dontrun 30 | 31 | The use of dontrun is to ensure that the geocoding services are not called as they often require a user credential. And we do not want automatic testing to burden the public service. 32 | 33 | -------------------------------------------------------------------------------- /data-raw/default_geocoder.R: -------------------------------------------------------------------------------- 1 | ## code to prepare `default_geocoder` dataset goes here 2 | world_geocoder <- geocode_server(world_geocoder_url) 3 | 4 | remove_localized_names <- function(lst) { 5 | if (is.list(lst)) { 6 | lst <- lapply(lst, remove_localized_names) 7 | lst <- lst[!names(lst) %in% c("localizedNames", "recognizedNames")] 8 | } 9 | lst 10 | } 11 | 12 | # need to remove localized names because it bloats the package install size 13 | # by 7mb. Changes object size to 51kb from 7mb 14 | world_geocoder <- structure( 15 | remove_localized_names(world_geocoder), 16 | class = c("GeocodeServer", "list") 17 | ) 18 | 19 | usethis::use_data(world_geocoder, overwrite = TRUE, internal = TRUE) 20 | -------------------------------------------------------------------------------- /data-raw/esri-spatial-ref.R: -------------------------------------------------------------------------------- 1 | ## code to prepare `esri-spatial-ref` dataset goes here 2 | esri_wkids <- arcgeocoder::arc_spatial_references |> 3 | dplyr::filter( 4 | authority == "Esri" 5 | ) |> 6 | dplyr::pull(wkid) 7 | 8 | usethis::use_data(esri_wkids, overwrite = TRUE) 9 | -------------------------------------------------------------------------------- /data/esri_wkids.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/data/esri_wkids.rda -------------------------------------------------------------------------------- /data/world_geocoder.rda: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/data/world_geocoder.rda -------------------------------------------------------------------------------- /dev/arcgisgeocode-yelp-timing.csv: -------------------------------------------------------------------------------- 1 | expression,min,median,itr/sec,mem_alloc,gc/sec,n_itr,n_gc,total_time 2 | R-ArcGIS Bulk Address,4.66s,4.66s,0.21463022273143767, 16MB,0.21463022273143767,1,1,4.66s 3 | R-ArcGIS Single Address,1.04m,1.04m,0.01604269336891505,321MB,1.1550739225618836,1,72,1.04m 4 | -------------------------------------------------------------------------------- /dev/batch-geocode.R: -------------------------------------------------------------------------------- 1 | library(arcgisutils) 2 | library(arcgisgeocode) 3 | 4 | # read in the dataset 5 | boston_restaurants <- readr::read_csv( 6 | "~/downloads/boston-yelp-restaurants.csv" 7 | ) 8 | 9 | # extract the addresses 10 | addresses <- boston_restaurants$restaurant_address 11 | head(addresses) # preview them 12 | 13 | # authorize to AGOL using arcgisutils 14 | set_arc_token(auth_user()) 15 | 16 | # time both geocoding capabilities 17 | r_arcgis_bm <- bench::mark( 18 | "R-ArcGIS Bulk Address" = geocode_addresses(addresses), 19 | "R-ArcGIS Single Address" = find_address_candidates(addresses, max_locations = 1), 20 | iterations = 1, 21 | check = FALSE 22 | ) 23 | readr::write_csv(r_arcgis_bm[, 1:9], "dev/arcgisgeocode-yelp-timing.csv") 24 | 25 | 26 | # Community Packages ----------------------------------------------------- 27 | 28 | # read in the dataset 29 | boston_restaurants <- readr::read_csv( 30 | "~/downloads/boston-yelp-restaurants.csv" 31 | ) 32 | 33 | addresses <- boston_restaurants$restaurant_address 34 | 35 | bm <- bench::mark( 36 | tidygeocoder::geo(addresses, method = "arcgis"), 37 | arcgeocoder::arc_geo(addresses), 38 | check = FALSE, 39 | iterations = 1 40 | ) 41 | 42 | readr::write_csv(bm[, 1:9], "dev/yelp-timing.csv") 43 | 44 | 45 | tictoc::tic() 46 | geo_res <- geocode_addresses(addresses) 47 | (timing <- tictoc::toc()) -------------------------------------------------------------------------------- /dev/bench-marks.R: -------------------------------------------------------------------------------- 1 | library(magrittr) 2 | # library(arcgisgeocode) 3 | 4 | fp <- "https://urban-data-catalog.s3.amazonaws.com/drupal-root-live/2020/02/25/geocoding_test_data.csv" 5 | 6 | addresses <- readr::read_csv(fp) 7 | # set_arc_token(auth_user()) 8 | 9 | addresses %$% 10 | geocode_addresses( 11 | address = address, 12 | city = city, 13 | region = state, 14 | postal = zip, 15 | batch_size = 10 16 | ) -> geocoded 17 | 18 | attr(geocoded, "error_messages")[[1]] |> 19 | 20 | 21 | warns <- attr(geocoded, "error_requests")[[1]] |> 22 | httr2::req_perform() |> 23 | httr2::resp_body_string() |> 24 | RcppSimdJson::fparse() |> 25 | arcgisutils:::report_errors() 26 | 27 | 28 | warns <- attr(geocoded, "error_requests")[[1]] |> 29 | httr2::req_perform() |> 30 | httr2::resp_body_string() |> 31 | RcppSimdJson::fparse() |> 32 | arcgisutils:::report_errors() 33 | 34 | # system.time( 35 | # addresses %$% 36 | # geocode_addresses( 37 | # address = address, 38 | # city = city, 39 | # region = state, 40 | # postal = zip 41 | # ) -> geocoded 42 | # ) 43 | 44 | 45 | # Real geocoding ------------------------------------------------------ 46 | 47 | boston_restaurants <- readr::read_csv("/Users/josiahparry/Downloads/boston-yelp-restaurants.csv") 48 | 49 | system.time({ 50 | yelp_geocoded <- geocode_addresses( 51 | single_line = boston_restaurants$restaurant_address, 52 | ) Í 53 | }) 54 | 55 | 56 | # using public geocoder 57 | system.time({ 58 | yelp_candidates <- find_address_candidates( 59 | single_line = boston_restaurants$restaurant_address, 60 | max_locations = 1 61 | ) 62 | }) 63 | 64 | 65 | bos_311 <- readr::read_csv("/Users/josiahparry/Downloads/boston-311-2024.csv") |> 66 | dplyr::filter(!is.na(longitude), !is.na(latitude)) |> 67 | sf::st_as_sf(coords = c("longitude", "latitude"), crs = 4326) 68 | 69 | 70 | open_cases <- dplyr::filter(bos_311, case_status == "Open") 71 | 72 | system.time({ 73 | open_case_addresses <- reverse_geocode( 74 | open_cases$geometry 75 | ) 76 | }) 77 | -------------------------------------------------------------------------------- /dev/calcite-doc.R: -------------------------------------------------------------------------------- 1 | # Current limitation is that the images that are used are relative to the package lib.loc / help so what might be idea lis to extract the image and base64 encode them using {b64} 2 | pkg_to_mdx <- function(pkg, out_path = paste0(pkg, ".qmd"), ...) { 3 | tmp <- tempfile() 4 | base_doc <- tools::pkg2HTML(pkg, out = tmp, include_description = FALSE) 5 | 6 | # read in html 7 | og <- rvest::read_html(tmp) 8 | 9 | # get all of the elements 10 | all_elements <- og |> 11 | rvest::html_elements("main") |> 12 | rvest::html_children() 13 | 14 | # get reference positions 15 | reference_starts <- which(rvest::html_name(all_elements) == "h2" & !is.na(rvest::html_attr(all_elements, "id"))) 16 | 17 | # count how many elements there are in the html file 18 | n <- length(all_elements) 19 | 20 | # identify the reference section ending positions 21 | reference_ends <- (reference_starts + diff(c(reference_starts, n))) - 1 22 | reference_ends[length(reference_ends)] <- length(all_elements) 23 | 24 | # extract all of the reference doc 25 | all_references <- Map( 26 | function(.x, .y) { 27 | # create a new html div with a "reference class" 28 | new_div <- rvest::read_html('
') |> 29 | rvest::html_element("div") 30 | 31 | # identify all of the children from the reference section 32 | children <- all_elements[.x:.y] 33 | 34 | # for each of the children add it to the div 35 | for (child in children) { 36 | xml2::xml_add_child(new_div, child) 37 | } 38 | # return the div 39 | new_div 40 | }, 41 | reference_starts, reference_ends 42 | ) 43 | 44 | all_mds <- unlist(lapply(all_references, html_to_md)) 45 | 46 | # add a quarto yaml header 47 | # adds the TOC to mimic the R function 48 | yaml_header <- glue::glue("--- 49 | title: {pkg} 50 | ---") 51 | # cleaned <- gsub("<(http[^>]+|[^>]+@[^>]+)>", "\\1", all_mds, perl = TRUE) 52 | # cleaned <- stringr::str_remove_all(cleaned, 'style\\s*=\\s*(["\'])(.*?)\\1') |> 53 | # stringr::str_replace_all("><", ">\n<") |> 54 | # stringr::str_replace_all(">([^<])", ">\n\\1") |> 55 | # stringr::str_replace_all("([^>])<", "\\1\n<") 56 | cleaned <- gsub("<(http[^>]+|[^>]+@[^>]+)>", "\\1", all_mds, perl = TRUE) 57 | 58 | cleaned <- cleaned |> 59 | stringr::str_remove_all('style\\s*=\\s*(["\'])(.*?)\\1') |> 60 | stringr::str_replace_all("(?<", ">\n<") |> 61 | stringr::str_replace_all("(?([^<])", ">\n\\1") |> 62 | stringr::str_replace_all("([^>])(? 63 | stringr::str_replace_all("]*>[\\s\\S]*?", function(x) gsub("\n", "", x)) |> 64 | stringr::str_replace_all(" \n<-", " <-") 65 | 66 | # c(yaml_header, all_mds) 67 | # write the file out 68 | brio::write_lines( 69 | # this removes the complicated brackets for jsx 70 | c(yaml_header, cleaned), 71 | out_path 72 | ) 73 | } 74 | 75 | # converts a reference to github flavored markdown 76 | # this doesnt create the ::: classes though so it wont be compatible 77 | # with JSX... 78 | html_to_md <- function(reference) { 79 | pandoc::pandoc_convert( 80 | text = reference, 81 | from = "html", 82 | to = "gfm", 83 | args = "--wrap=none" 84 | ) 85 | } 86 | 87 | # pkg_to_mdx("arcgisutils", "~/github/misc-esri/dev-docs/arcgisutils.mdx") 88 | # file.edit("~/github/misc-esri/dev-docs/arcgisutils.mdx") 89 | 90 | # the results of this will not be perfect so first it needs to be linted with prettier..maybe not 91 | pkg_to_mdx("arcgislayers", "~/github/misc-esri/dev-docs/arcgislayers.mdx") 92 | pkg_to_mdx("arcgisutils", "~/github/misc-esri/dev-docs/arcgisutils.mdx") 93 | pkg_to_mdx("arcpbf", "~/github/misc-esri/dev-docs/arcpbf.mdx") 94 | pkg_to_mdx("arcgisgeocode", "~/github/misc-esri/dev-docs/arcgisgeocode.mdx") 95 | -------------------------------------------------------------------------------- /dev/custom-locator-res.R: -------------------------------------------------------------------------------- 1 | custom_json <- brio::read_file("tests/testdata/custom-locator.json") 2 | known_json <- brio::read_file("tests/testdata/public-locator-res.json") 3 | 4 | known_res <- RcppSimdJson::fparse(known_json) 5 | 6 | cur <- parse_location_json(jsn) |> 7 | dplyr::glimpse() 8 | -------------------------------------------------------------------------------- /dev/geocode-csv.R: -------------------------------------------------------------------------------- 1 | library(readr) 2 | library(arcgis) 3 | library(arcgisgeocode) 4 | 5 | customer_addresses <- readr::read_csv( 6 | "/Users/josiahparry/Downloads/Atlanta_customers.csv" 7 | ) 8 | 9 | # preview 10 | # dplyr::glimpse(customer_addresses) 11 | 12 | # authorize 13 | set_arc_token(auth_user()) 14 | 15 | # geocode by fields 16 | geocoding_res <- customer_addresses |> 17 | dplyr::mutate( 18 | geocoded = geocode_addresses( 19 | address = ADDRESS, 20 | city = CITY, 21 | region = STATE, 22 | postal = as.character(ZIP) 23 | ) 24 | ) |> 25 | tidyr::unnest(geocoded) 26 | 27 | geocoding_res |> 28 | dplyr::glimpse() 29 | -------------------------------------------------------------------------------- /dev/json.R: -------------------------------------------------------------------------------- 1 | x <- '{ 2 | "spatialReference": { 3 | "wkid": 4326, 4 | "latestWkid": 4326 5 | }, 6 | "candidates": [ 7 | { 8 | "address": "Starbucks", 9 | "location": { 10 | "x": 151.20641, 11 | "y": -33.8756 12 | }, 13 | "score": 100, 14 | "attributes": { 15 | "type": "Coffee Shop", 16 | "city": "Sydney", 17 | "region": "New South Wales" 18 | }, 19 | "extent": { 20 | "xmin": 151.20141, 21 | "ymin": -33.8806, 22 | "xmax": 151.21141, 23 | "ymax": -33.8706 24 | } 25 | }, 26 | { 27 | "address": "Starbucks", 28 | "location": { 29 | "x": 151.20555, 30 | "y": -33.86506 31 | }, 32 | "score": 100, 33 | "attributes": { 34 | "type": "Coffee Shop", 35 | "city": "Sydney", 36 | "region": "New South Wales" 37 | }, 38 | "extent": { 39 | "xmin": 151.20055, 40 | "ymin": -33.87006, 41 | "xmax": 151.21055, 42 | "ymax": -33.86006 43 | } 44 | }, 45 | { 46 | "address": "Starbucks", 47 | "location": { 48 | "x": 151.20908, 49 | "y": -33.87374 50 | }, 51 | "score": 100, 52 | "attributes": { 53 | "type": "Coffee Shop", 54 | "city": "Sydney", 55 | "region": "New South Wales" 56 | }, 57 | "extent": { 58 | "xmin": 151.20408, 59 | "ymin": -33.87874, 60 | "xmax": 151.21408, 61 | "ymax": -33.86874 62 | } 63 | }, 64 | { 65 | "address": "Starbucks", 66 | "location": { 67 | "x": 151.20697, 68 | "y": -33.87176 69 | }, 70 | "score": 100, 71 | "attributes": { 72 | "type": "Coffee Shop", 73 | "city": "Sydney", 74 | "region": "New South Wales" 75 | }, 76 | "extent": { 77 | "xmin": 151.20197, 78 | "ymin": -33.87676, 79 | "xmax": 151.21197, 80 | "ymax": -33.86676 81 | } 82 | }, 83 | { 84 | "address": "Starbucks", 85 | "location": { 86 | "x": 151.24979, 87 | "y": -33.89205 88 | }, 89 | "score": 100, 90 | "attributes": { 91 | "type": "Coffee Shop", 92 | "city": "Sydney", 93 | "region": "New South Wales" 94 | }, 95 | "extent": { 96 | "xmin": 151.24479, 97 | "ymin": -33.89705, 98 | "xmax": 151.25479, 99 | "ymax": -33.88705 100 | } 101 | }, 102 | { 103 | "address": "Starbucks", 104 | "location": { 105 | "x": 151.25052, 106 | "y": -33.89092 107 | }, 108 | "score": 100, 109 | "attributes": { 110 | "type": "Coffee Shop", 111 | "city": "Sydney", 112 | "region": "New South Wales" 113 | }, 114 | "extent": { 115 | "xmin": 151.24552, 116 | "ymin": -33.89592, 117 | "xmax": 151.25552, 118 | "ymax": -33.88592 119 | } 120 | }, 121 | { 122 | "address": "Starbucks", 123 | "location": { 124 | "x": 151.10432, 125 | "y": -33.87427 126 | }, 127 | "score": 100, 128 | "attributes": { 129 | "type": "Coffee Shop", 130 | "city": "Sydney", 131 | "region": "New South Wales" 132 | }, 133 | "extent": { 134 | "xmin": 151.09932, 135 | "ymin": -33.87927, 136 | "xmax": 151.10932, 137 | "ymax": -33.86927 138 | } 139 | }, 140 | { 141 | "address": "Starbucks", 142 | "location": { 143 | "x": 151.10238, 144 | "y": -33.87722 145 | }, 146 | "score": 100, 147 | "attributes": { 148 | "type": "Coffee Shop", 149 | "city": "Sydney", 150 | "region": "New South Wales" 151 | }, 152 | "extent": { 153 | "xmin": 151.09738, 154 | "ymin": -33.88222, 155 | "xmax": 151.10738, 156 | "ymax": -33.87222 157 | } 158 | }, 159 | { 160 | "address": "Starbucks", 161 | "location": { 162 | "x": 151.18427, 163 | "y": -33.79591 164 | }, 165 | "score": 100, 166 | "attributes": { 167 | "type": "Coffee Shop", 168 | "city": "Sydney", 169 | "region": "New South Wales" 170 | }, 171 | "extent": { 172 | "xmin": 151.17927, 173 | "ymin": -33.80091, 174 | "xmax": 151.18927, 175 | "ymax": -33.79091 176 | } 177 | }, 178 | { 179 | "address": "Starbucks", 180 | "location": { 181 | "x": 151.19993, 182 | "y": -33.88424 183 | }, 184 | "score": 100, 185 | "attributes": { 186 | "type": "Coffee Shop", 187 | "city": "Sydney", 188 | "region": "New South Wales" 189 | }, 190 | "extent": { 191 | "xmin": 151.19493, 192 | "ymin": -33.88924, 193 | "xmax": 151.20493, 194 | "ymax": -33.87924 195 | } 196 | } 197 | ] 198 | }' 199 | 200 | str(parse_candidate_json(x), 2) 201 | -------------------------------------------------------------------------------- /dev/make-errors.R: -------------------------------------------------------------------------------- 1 | find_address_candidates( 2 | single_line = "Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 30316, USA" 3 | ) 4 | # TODO: `magic_key` should only be provided when single line is 5 | # all other arguments should be ignored 6 | find_address_candidates(magic_key = "123") 7 | 8 | 9 | # this should create an error for having different lengths 10 | find_address_candidates( 11 | single_line = "Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 30316, USA", 12 | postal = c("30312", "30000"), 13 | region = c("CA", "GA", "TX") 14 | ) 15 | 16 | find_address_candidates( 17 | "Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 30316, USA", 18 | category = "billy joe armstrong" 19 | ) 20 | 21 | 22 | # suggests --------------------------------------------------------------- 23 | library(sf) 24 | 25 | # identify a search point 26 | location <- st_sfc(st_point(c(-84.34, 33.74)), crs = 4326) 27 | 28 | # create a search extent from it 29 | search_extent <- st_bbox(st_buffer(location, 10)) 30 | 31 | 32 | # too many values for the search location 33 | suggestions <- suggest_places( 34 | "bellwood", 35 | c(-84.34, 33.74, 3.14159, 1, 0), 36 | search_extent = search_extent 37 | ) 38 | 39 | 40 | suggest_places( 41 | "bellwood", 42 | c(-84.34, 33.74), 43 | search_extent = list(search_extent, search_extent) 44 | ) 45 | 46 | 47 | suggest_places("m", category = "!!!!^&*(&%(%fastest of food vroom vroom") 48 | 49 | 50 | # reverse geocode -------------------------------------------------------- 51 | 52 | reverse_geocode(c(-84.34, 33.74), location_type = "sdfouhw23495ghfadsofhv") 53 | reverse_geocode(c(-84.34, 33.74), location_type = c("street", "rooftop", "rooftop"), crs = 3857) 54 | -------------------------------------------------------------------------------- /dev/shiny-app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | library(leaflet) 4 | library(htmltools) 5 | library(arcgisgeocode) 6 | 7 | # Define the UI layout using {bslib} 8 | ui <- page_sidebar( 9 | card( 10 | leafletOutput("map", width = "100%", height = "100%"), 11 | ), 12 | sidebar = sidebar( 13 | textInput( 14 | "search_value", 15 | label = "Search", 16 | ), 17 | uiOutput("suggests") 18 | ) 19 | ) 20 | 21 | server <- function(input, output, session) { 22 | # this reactively gets the center of the map thats in screen 23 | bounds <- reactive({ 24 | loc <- input$map_center 25 | if ( 26 | !is.null(loc) 27 | ) { 28 | c(loc[[1]], loc[[2]]) 29 | } 30 | }) 31 | 32 | # extract the search text 33 | search_text <- reactive({ 34 | input$search_value 35 | }) 36 | 37 | # whenever the search text changes 38 | observeEvent(search_text(), { 39 | # extract search text 40 | txt <- search_text() 41 | 42 | # get map center 43 | bnds <- bounds() 44 | 45 | # as long as search text and bounds aren't null 46 | if (nzchar(txt) && !is.null(bnds)) { 47 | places <- suggest_places( 48 | search_text(), 49 | location = bnds 50 | ) 51 | 52 | # add the suggestions as a fake-drop down 53 | output$suggests <- renderUI({ 54 | make_suggestion_list(places) 55 | }) 56 | 57 | if (nrow(places) > 0) { 58 | # then pass the suggestions to find_address_candidates 59 | search_results <- find_address_candidates( 60 | places$text, 61 | magic_key = places$magic_key, 62 | max_locations = 1, 63 | for_storage = FALSE 64 | ) 65 | 66 | # update map with new markers 67 | leafletProxy( 68 | "map", 69 | data = sf::st_geometry(search_results) 70 | ) |> 71 | clearMarkers() |> 72 | addMarkers() 73 | } 74 | } 75 | 76 | # clear the gt table 77 | if (!nzchar(txt)) { 78 | output$suggests <- NULL 79 | 80 | # remove the markers 81 | leafletProxy("map") |> 82 | clearMarkers() 83 | } 84 | }) 85 | 86 | output$map <- renderLeaflet({ 87 | leaflet() |> 88 | # use esri canvas 89 | addProviderTiles(providers$Esri.WorldGrayCanvas) |> 90 | setView(lat = 42.3601, lng = -71.0589, zoom = 14) 91 | }) 92 | } 93 | 94 | # helper function 95 | make_suggestion_list <- function(suggestions) { 96 | ul <- tag("ul", c("class" = "list-group shadow-lg")) 97 | lis <- lapply(suggestions$text, \(.x) { 98 | htmltools::tag( 99 | "li", 100 | c("class" = "list-group-item list-group-item-action border-light text-sm", .x) 101 | ) 102 | }) 103 | 104 | tagSetChildren(ul, lis) 105 | } 106 | 107 | 108 | shinyApp(ui, server) 109 | -------------------------------------------------------------------------------- /dev/shiny-reverse-geocode.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(bslib) 3 | library(leaflet) 4 | library(arcgisgeocode) 5 | 6 | ui <- page_fillable( 7 | card( 8 | card_title(textOutput("rev_result")), 9 | leafletOutput("map", width = "100%", height = "100%"), 10 | ) 11 | ) 12 | 13 | server <- function(input, output, session) { 14 | # This reactive expression represents the palette function, 15 | # which changes as the user makes selections in UI. 16 | 17 | observeEvent(input$map_click, { 18 | # get the click location 19 | click <- input$map_click 20 | 21 | # extract the x and y coordinate 22 | x <- click$lng 23 | y <- click$lat 24 | loc <- c(x, y) 25 | dput(loc) # print to console 26 | 27 | geocoded <- reverse_geocode(loc) 28 | 29 | output$rev_result <- renderText(geocoded$long_label) 30 | 31 | leafletProxy("map", data = sf::st_geometry(geocoded)) |> 32 | clearMarkers() |> 33 | addMarkers() 34 | }) 35 | 36 | output$map <- renderLeaflet({ 37 | # Use leaflet() here, and only include aspects of the map that 38 | # won't need to change dynamically (at least, not unless the 39 | # entire map is being torn down and recreated). 40 | leaflet() |> 41 | # use esri canvas 42 | addProviderTiles(providers$Esri.WorldGrayCanvas) |> 43 | setView(lat = 42.3601, lng = -71.0589, zoom = 14) 44 | }) 45 | } 46 | 47 | shinyApp(ui, server) 48 | 49 | make_label <- function(geocoded) { 50 | name <- geocoded$place_name 51 | 52 | if (!nzchar(name)) { 53 | return(geocoded$long_label) 54 | } else { 55 | label <- paste0( 56 | name, 57 | "\n", 58 | geocoded$long_label 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dev/suggest-find-candidates.R: -------------------------------------------------------------------------------- 1 | library(sf) 2 | 3 | # identify a search point 4 | location <- st_sfc(st_point(c(-84.34, 33.74)), crs = 4326) 5 | 6 | # create a search extent from it 7 | search_extent <- st_bbox(st_buffer(location, 10)) 8 | 9 | # find suggestions from it 10 | suggestions <- suggest_places( 11 | "bellwood", 12 | c(-84.34, 33.74), 13 | search_extent = search_extent 14 | ) 15 | 16 | # get address candidate information 17 | # using the text and the magic key 18 | find_address_candidates( 19 | suggestions$text, 20 | magic_key = suggestions$magic_key 21 | ) 22 | -------------------------------------------------------------------------------- /dev/yelp-timing.csv: -------------------------------------------------------------------------------- 1 | expression,min,median,itr/sec,mem_alloc,gc/sec,n_itr,n_gc,total_time 2 | "tidygeocoder::geo(addresses, method = ""arcgis"")",16.5m,16.5m,0.0010079042700814767,169MB,0.10079042700814766,1,100,16.5m 3 | arcgeocoder::arc_geo(addresses),21.1m,21.1m,7.909762914354787e-4,169MB,0.016610502120145053,1,21,21.1m 4 | -------------------------------------------------------------------------------- /man/arcgisgeocode-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/arcgeocode-package.R 3 | \docType{package} 4 | \name{arcgisgeocode-package} 5 | \alias{arcgisgeocode} 6 | \alias{arcgisgeocode-package} 7 | \title{arcgisgeocode: A Robust Interface to ArcGIS 'Geocoding Services'} 8 | \description{ 9 | A very fast and robust interface to ArcGIS 'Geocoding Services'. Provides capabilities for reverse geocoding, finding address candidates, character-by-character search autosuggestion, and batch geocoding. The public 'ArcGIS World Geocoder' is accessible for free use via 'arcgisgeocode' for all services except batch geocoding. 'arcgisgeocode' also integrates with 'arcgisutils' to provide access to custom locators or private 'ArcGIS World Geocoder' hosted on 'ArcGIS Enterprise'. Learn more in the 'Geocode service' API reference \url{https://developers.arcgis.com/rest/geocode/api-reference/overview-world-geocoding-service.htm}. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://github.com/r-arcgis/arcgisgeocode} 15 | \item \url{https://r.esri.com/arcgisgeocode/} 16 | } 17 | 18 | } 19 | \author{ 20 | \strong{Maintainer}: Josiah Parry \email{josiah.parry@gmail.com} (\href{https://orcid.org/0000-0001-9910-865X}{ORCID}) 21 | 22 | } 23 | \keyword{internal} 24 | -------------------------------------------------------------------------------- /man/esri_wkids.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/doc-data.R 3 | \docType{data} 4 | \name{esri_wkids} 5 | \alias{esri_wkids} 6 | \title{Esri well-known IDs} 7 | \format{ 8 | An object of class \code{integer} of length 2886. 9 | } 10 | \usage{ 11 | esri_wkids 12 | } 13 | \value{ 14 | a numeric vector of well-known IDs 15 | } 16 | \description{ 17 | An integer vector containing the WKIDs of Esri authority 18 | spatial references. 19 | Esri WKIDs were identified from the \href{https://cran.r-project.org/package=arcgeocoder}{\code{{arcgeocoder}}} package from 20 | \href{https://github.com/dieghernan}{@dieghernan}. 21 | } 22 | \keyword{datasets} 23 | -------------------------------------------------------------------------------- /man/find_address_candidates.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/core-find-candidates.R 3 | \name{find_address_candidates} 4 | \alias{find_address_candidates} 5 | \title{Find Address Candidates} 6 | \usage{ 7 | find_address_candidates( 8 | single_line = NULL, 9 | address = NULL, 10 | address2 = NULL, 11 | address3 = NULL, 12 | neighborhood = NULL, 13 | city = NULL, 14 | subregion = NULL, 15 | region = NULL, 16 | postal = NULL, 17 | postal_ext = NULL, 18 | country_code = NULL, 19 | search_extent = NULL, 20 | location = NULL, 21 | category = NULL, 22 | crs = NULL, 23 | max_locations = NULL, 24 | for_storage = FALSE, 25 | match_out_of_range = NULL, 26 | location_type = NULL, 27 | lang_code = NULL, 28 | source_country = NULL, 29 | preferred_label_values = NULL, 30 | magic_key = NULL, 31 | geocoder = default_geocoder(), 32 | token = arc_token(), 33 | .progress = TRUE 34 | ) 35 | } 36 | \arguments{ 37 | \item{single_line}{a character vector of addresses to geocode. If provided 38 | other \code{address} fields cannot be used. If \code{address} is not provided, 39 | \code{single_line} must be.} 40 | 41 | \item{address}{a character vector of the first part of a street address. 42 | Typically used for the street name and house number. But can also be a place 43 | or building name. If \code{single_line} is not provided, \code{address} must be.} 44 | 45 | \item{address2}{a character vector of the second part of a street address. 46 | Typically includes a house number, sub-unit, street, building, or place name. 47 | Optional.} 48 | 49 | \item{address3}{a character vector of the third part of an address. Optional.} 50 | 51 | \item{neighborhood}{a character vector of the smallest administrative division 52 | associated with an address. Typically, a neighborhood or a section of a 53 | larger populated place. Optional.} 54 | 55 | \item{city}{a character vector of the next largest administrative division 56 | associated with an address, typically, a city or municipality. A city is a 57 | subdivision of a subregion or a region. Optional.} 58 | 59 | \item{subregion}{a character vector of the next largest administrative division 60 | associated with an address. Depending on the country, a subregion can 61 | represent a county, state, or province. Optional.} 62 | 63 | \item{region}{a character vector of the largest administrative division 64 | associated with an address, typically, a state or province. Optional.} 65 | 66 | \item{postal}{a character vector of the standard postal code for an address, 67 | typically, a three– to six-digit alphanumeric code. Optional.} 68 | 69 | \item{postal_ext}{a character vector of the postal code extension, such as 70 | the United States Postal Service ZIP+4 code, provides finer resolution or 71 | higher accuracy when also passing postal. Optional.} 72 | 73 | \item{country_code}{default \code{NULL.} An ISO 3166 country code. 74 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 75 | 76 | \item{search_extent}{an object of class \code{bbox} that limits the search area. This is especially useful for applications in which a user will search for places and addresses within the current map extent. Optional.} 77 | 78 | \item{location}{an \code{sfc_POINT} object that centers the search. Optional.} 79 | 80 | \item{category}{a scalar character. Place or address type that can be used to 81 | filter suggest results. Optional.} 82 | 83 | \item{crs}{the CRS of the returned geometries. Passed to \code{sf::st_crs()}. 84 | Ignored if \code{locations} is not an \code{sfc_POINT} object.} 85 | 86 | \item{max_locations}{the maximum number of results to return. The default is 87 | 15 with a maximum of 50. Optional.} 88 | 89 | \item{for_storage}{default \code{FALSE}. Whether or not the results will be saved 90 | for long term storage.} 91 | 92 | \item{match_out_of_range}{set to \code{TRUE} by service by default. Matches locations Optional.} 93 | 94 | \item{location_type}{default \code{"rooftop"}. Must be one of \code{"rooftop"} or \code{"street"}. 95 | Optional.} 96 | 97 | \item{lang_code}{default \code{NULL}. An ISO 3166 country code. 98 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 99 | 100 | \item{source_country}{default \code{NULL}. An ISO 3166 country code. 101 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 102 | 103 | \item{preferred_label_values}{default NULL. Must be one of \code{"postalCity"} 104 | or \code{"localCity"}. Optional.} 105 | 106 | \item{magic_key}{a unique identifier returned from \code{\link[=suggest_places]{suggest_places()}}. 107 | When a \code{magic_key} is provided, results are returned faster. Optional.} 108 | 109 | \item{geocoder}{default \code{\link[=default_geocoder]{default_geocoder()}}.} 110 | 111 | \item{token}{an object of class \code{httr2_token} as generated by \code{\link[arcgisutils:auth_code]{auth_code()}} 112 | or related function} 113 | 114 | \item{.progress}{default \code{TRUE}. Whether a progress bar should be provided.} 115 | } 116 | \value{ 117 | An \code{sf} object with 60 columns. 118 | } 119 | \description{ 120 | Given an address, returns geocode result candidates. 121 | } 122 | \details{ 123 | Utilizes the \href{https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm}{\verb{/findAddressCandidates}} endpoint. 124 | 125 | The endpoint can only handle one request at a time. To 126 | make the operation as performant as possible, requests are sent in parallel 127 | using \code{\link[httr2:req_perform_parallel]{httr2::req_perform_parallel()}}. The JSON responses are then processed 128 | using Rust and returned as an sf object. 129 | } 130 | \examples{ 131 | candidates_from_single <- find_address_candidates( 132 | single_line = "Bellwood Coffee, 1366 Glenwood Ave SE, Atlanta, GA, 30316, USA" 133 | ) 134 | 135 | candidates_from_parts <- find_address_candidates( 136 | address = c("Bellwood Coffee", "Joe's coffeehouse"), 137 | address2 = c("1366 Glenwood Ave SE", "510 Flat Shoals Ave"), 138 | city = "Atlanta", 139 | region = "GA", 140 | postal = "30316", 141 | country_code = "USA" 142 | ) 143 | 144 | str(candidates_from_parts) 145 | 146 | } 147 | -------------------------------------------------------------------------------- /man/geocode_addresses.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/core-batch-geocode.R 3 | \name{geocode_addresses} 4 | \alias{geocode_addresses} 5 | \title{Batch Geocode Addresses} 6 | \usage{ 7 | geocode_addresses( 8 | single_line = NULL, 9 | address = NULL, 10 | address2 = NULL, 11 | address3 = NULL, 12 | neighborhood = NULL, 13 | city = NULL, 14 | subregion = NULL, 15 | region = NULL, 16 | postal = NULL, 17 | postal_ext = NULL, 18 | country_code = NULL, 19 | location = NULL, 20 | search_extent = NULL, 21 | category = NULL, 22 | crs = NULL, 23 | max_locations = NULL, 24 | for_storage = FALSE, 25 | match_out_of_range = NULL, 26 | location_type = NULL, 27 | lang_code = NULL, 28 | source_country = NULL, 29 | preferred_label_values = NULL, 30 | batch_size = NULL, 31 | geocoder = default_geocoder(), 32 | token = arc_token(), 33 | .progress = TRUE 34 | ) 35 | } 36 | \arguments{ 37 | \item{single_line}{a character vector of addresses to geocode. If provided 38 | other \code{address} fields cannot be used. If \code{address} is not provided, 39 | \code{single_line} must be.} 40 | 41 | \item{address}{a character vector of the first part of a street address. 42 | Typically used for the street name and house number. But can also be a place 43 | or building name. If \code{single_line} is not provided, \code{address} must be.} 44 | 45 | \item{address2}{a character vector of the second part of a street address. 46 | Typically includes a house number, sub-unit, street, building, or place name. 47 | Optional.} 48 | 49 | \item{address3}{a character vector of the third part of an address. Optional.} 50 | 51 | \item{neighborhood}{a character vector of the smallest administrative division 52 | associated with an address. Typically, a neighborhood or a section of a 53 | larger populated place. Optional.} 54 | 55 | \item{city}{a character vector of the next largest administrative division 56 | associated with an address, typically, a city or municipality. A city is a 57 | subdivision of a subregion or a region. Optional.} 58 | 59 | \item{subregion}{a character vector of the next largest administrative division 60 | associated with an address. Depending on the country, a subregion can 61 | represent a county, state, or province. Optional.} 62 | 63 | \item{region}{a character vector of the largest administrative division 64 | associated with an address, typically, a state or province. Optional.} 65 | 66 | \item{postal}{a character vector of the standard postal code for an address, 67 | typically, a three– to six-digit alphanumeric code. Optional.} 68 | 69 | \item{postal_ext}{a character vector of the postal code extension, such as 70 | the United States Postal Service ZIP+4 code, provides finer resolution or 71 | higher accuracy when also passing postal. Optional.} 72 | 73 | \item{country_code}{default \code{NULL.} An ISO 3166 country code. 74 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 75 | 76 | \item{location}{an \code{sfc_POINT} object that centers the search. Optional.} 77 | 78 | \item{search_extent}{an object of class \code{bbox} that limits the search area. This is especially useful for applications in which a user will search for places and addresses within the current map extent. Optional.} 79 | 80 | \item{category}{a scalar character. Place or address type that can be used to 81 | filter suggest results. Optional.} 82 | 83 | \item{crs}{the CRS of the returned geometries. Passed to \code{sf::st_crs()}. 84 | Ignored if \code{locations} is not an \code{sfc_POINT} object.} 85 | 86 | \item{max_locations}{the maximum number of results to return. The default is 87 | 15 with a maximum of 50. Optional.} 88 | 89 | \item{for_storage}{default \code{FALSE}. Whether or not the results will be saved 90 | for long term storage.} 91 | 92 | \item{match_out_of_range}{set to \code{TRUE} by service by default. Matches locations Optional.} 93 | 94 | \item{location_type}{default \code{"rooftop"}. Must be one of \code{"rooftop"} or \code{"street"}. 95 | Optional.} 96 | 97 | \item{lang_code}{default \code{NULL}. An ISO 3166 country code. 98 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 99 | 100 | \item{source_country}{default \code{NULL}. An ISO 3166 country code. 101 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 102 | 103 | \item{preferred_label_values}{default NULL. Must be one of \code{"postalCity"} 104 | or \code{"localCity"}. Optional.} 105 | 106 | \item{batch_size}{the number of addresses to geocode per 107 | request. Uses the suggested batch size property of the 108 | \code{geocoder}.} 109 | 110 | \item{geocoder}{default \code{\link[=default_geocoder]{default_geocoder()}}.} 111 | 112 | \item{token}{an object of class \code{httr2_token} as generated by \code{\link[arcgisutils:auth_code]{auth_code()}} 113 | or related function} 114 | 115 | \item{.progress}{default \code{TRUE}. Whether a progress bar should be provided.} 116 | } 117 | \value{ 118 | an \code{sf} object 119 | } 120 | \description{ 121 | Gecocode a vector of addresses in batches. 122 | } 123 | \details{ 124 | Addresses are partitioned into batches of up to \code{batch_size} 125 | elements. The batches are then sent to the geocoding service 126 | in parallel using \code{\link[httr2:req_perform_parallel]{httr2::req_perform_parallel()}}. 127 | The JSON responses are then processed 128 | using Rust and returned as an sf object. 129 | 130 | Utilizes the \href{https://developers.arcgis.com/rest/geocode/api-reference/geocoding-geocode-addresses.htm}{\verb{/geocodeAddresses}} endpoint. 131 | } 132 | \examples{ 133 | # Example dataset from the Urban Institute 134 | \dontrun{ 135 | fp <- paste0( 136 | "https://urban-data-catalog.s3.amazonaws.com/", 137 | "drupal-root-live/2020/02/25/geocoding_test_data.csv" 138 | ) 139 | to_geocode <- read.csv(fp) 140 | geocode_addresses( 141 | address = to_geocode$address, 142 | city = to_geocode$city, 143 | region = to_geocode$state, 144 | postal = to_geocode$zip 145 | ) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /man/geocode_server.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-geocoder-obj.R 3 | \name{geocode_server} 4 | \alias{geocode_server} 5 | \title{Create a GeocodeServer} 6 | \usage{ 7 | geocode_server(url, token = arc_token()) 8 | } 9 | \arguments{ 10 | \item{url}{the URL of a geocoding server.} 11 | 12 | \item{token}{an object of class \code{httr2_token} as generated by \code{\link[arcgisutils:auth_code]{auth_code()}} 13 | or related function} 14 | } 15 | \value{ 16 | an object of class \code{GeocodeServer}. 17 | } 18 | \description{ 19 | Create an object of class \code{GeocodeServer} from a URL. This object 20 | stores the service definition of the geocoding service as a list object. 21 | } 22 | \examples{ 23 | server_url <- "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer" 24 | geocode_server(server_url) 25 | } 26 | -------------------------------------------------------------------------------- /man/iso_3166_codes.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-iso-3166.R 3 | \name{iso_3166_codes} 4 | \alias{iso_3166_codes} 5 | \title{ISO 3166 Country Codes} 6 | \usage{ 7 | iso_3166_codes() 8 | } 9 | \value{ 10 | a \code{data.frame} with columns \code{country}, \code{code_2}, \code{code_3}. 11 | } 12 | \description{ 13 | Create a data.frame of ISO 3166 2 and 3 digit Country codes. 14 | } 15 | \details{ 16 | Country codes provided by \href{https://docs.rs/rust_iso3166/latest/rust_iso3166/index.html}{\code{rust_iso3166}}. 17 | } 18 | \examples{ 19 | head(iso_3166_codes()) 20 | } 21 | -------------------------------------------------------------------------------- /man/list_geocoders.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/list-geocoders.R 3 | \name{list_geocoders} 4 | \alias{list_geocoders} 5 | \alias{default_geocoder} 6 | \title{List Available Geocoder Services} 7 | \usage{ 8 | list_geocoders(token = arc_token()) 9 | 10 | default_geocoder(token = arc_token()) 11 | } 12 | \arguments{ 13 | \item{token}{an object of class \code{httr2_token} as generated by \code{\link[arcgisutils:auth_code]{auth_code()}} 14 | or related function} 15 | } 16 | \value{ 17 | a \code{data.frame} with columns \code{url}, \code{northLat}, \code{southLat}, 18 | \code{eastLon}, \code{westLon}, \code{name}, \code{suggest}, \code{zoomScale}, \code{placefinding}, \code{batch}. 19 | } 20 | \description{ 21 | Evaluates the logged in user from an authorization token and returns 22 | a data.frame containing the available geocoding services for the 23 | associated token. 24 | 25 | For users who have not signed into a private portal or ArcGIS Online, 26 | the public \href{https://www.esri.com/en-us/arcgis/products/arcgis-world-geocoder}{ArcGIS World Geocoder} is used. Otherwise, the first available geocoding service associated 27 | with your authorization token is used. 28 | } 29 | \details{ 30 | The \code{default_geocoder()} will return the ArcGIS World Geocoder if no 31 | token is available. \code{list_geocoder()} requires an authorization 32 | token. 33 | 34 | To manually create a \code{GeocodeServer} object, see \code{\link[=geocode_server]{geocode_server()}}. 35 | } 36 | \examples{ 37 | 38 | # Default geocoder object 39 | # ArcGIS World Geocoder b/c no token 40 | default_geocoder() 41 | 42 | # Requires an Authorization Token 43 | \dontrun{ 44 | list_geocoders() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /man/reverse_geocode.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/core-reverse-geocode.R 3 | \name{reverse_geocode} 4 | \alias{reverse_geocode} 5 | \title{Reverse Geocode Locations} 6 | \usage{ 7 | reverse_geocode( 8 | locations, 9 | crs = sf::st_crs(locations), 10 | ..., 11 | lang_code = NULL, 12 | feature_type = NULL, 13 | location_type = c("rooftop", "street"), 14 | preferred_label_values = c("postalCity", "localCity"), 15 | for_storage = FALSE, 16 | geocoder = default_geocoder(), 17 | token = arc_token(), 18 | .progress = TRUE 19 | ) 20 | } 21 | \arguments{ 22 | \item{locations}{an \code{sfc_POINT} object of the locations to be reverse geocoded.} 23 | 24 | \item{crs}{the CRS of the returned geometries. Passed to \code{sf::st_crs()}. 25 | Ignored if \code{locations} is not an \code{sfc_POINT} object.} 26 | 27 | \item{...}{unused.} 28 | 29 | \item{lang_code}{default \code{NULL}. An ISO 3166 country code. 30 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 31 | 32 | \item{feature_type}{limits the possible match types returned. Must be one of 33 | \code{"StreetInt"}, \code{"DistanceMarker"}, \code{"StreetAddress"}, \code{"StreetName"}, 34 | \code{"POI"}, \code{"Subaddress"}, \code{"PointAddress"}, \code{"Postal"}, or \code{"Locality"}. Optional.} 35 | 36 | \item{location_type}{default \code{"rooftop"}. Must be one of \code{"rooftop"} or \code{"street"}. 37 | Optional.} 38 | 39 | \item{preferred_label_values}{default NULL. Must be one of \code{"postalCity"} 40 | or \code{"localCity"}. Optional.} 41 | 42 | \item{for_storage}{default \code{FALSE}. Whether or not the results will be saved 43 | for long term storage.} 44 | 45 | \item{geocoder}{default \code{\link[=default_geocoder]{default_geocoder()}}.} 46 | 47 | \item{token}{an object of class \code{httr2_token} as generated by \code{\link[arcgisutils:auth_code]{auth_code()}} 48 | or related function} 49 | 50 | \item{.progress}{default \code{TRUE}. Whether a progress bar should be provided.} 51 | } 52 | \value{ 53 | An sf object. 54 | } 55 | \description{ 56 | Determines the address for a given point. 57 | } 58 | \details{ 59 | This function utilizes the 60 | \href{https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm}{\verb{/reverseGeocode}} endpoint of a geocoding service. By default, it uses 61 | the public ArcGIS World Geocoder. 62 | \itemize{ 63 | \item Intersection matches are only returned when \code{feature_types = "StreetInt"}. See \href{https://developers.arcgis.com/rest/geocode/api-reference/geocoding-reverse-geocode.htm#ESRI_SECTION3_1FE6B6D350714E45B2707845ADA22E1E}{REST documentation for more}. 64 | } 65 | \subsection{Location Type}{ 66 | \itemize{ 67 | \item Specifies whether the output geometry shuold be the rooftop point or the 68 | street entrance location. 69 | \item The \code{location_type} parameter changes the geometry's placement but does not 70 | change the attribute values of \code{X}, \code{Y}, or \code{DisplayX}, and \code{DisplayY}. 71 | } 72 | } 73 | 74 | \subsection{Storage}{ 75 | 76 | \strong{Very Important} 77 | 78 | The argument \code{for_storage} is used to determine if the request allows you to 79 | persist the results of the query. It is important to note that there are 80 | contractual obligations to appropriately set this argument. \strong{You cannot save 81 | or persist results} when \code{for_storage = FALSE} (the default). 82 | } 83 | 84 | \subsection{Execution}{ 85 | 86 | The \verb{/reverseGeocode} endpoint can only handle one address at a time. To 87 | make the operation as performant as possible, requests are sent in parallel 88 | using \code{\link[httr2:req_perform_parallel]{httr2::req_perform_parallel()}}. The JSON responses are then processed 89 | using Rust and returned as an sf object. 90 | } 91 | } 92 | \examples{ 93 | # Find addresses from locations 94 | reverse_geocode(c(-117.172, 34.052)) 95 | } 96 | -------------------------------------------------------------------------------- /man/storage.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/arcgeocode-package.R 3 | \name{storage} 4 | \alias{storage} 5 | \title{Storing Geocoding Results} 6 | \description{ 7 | The results of geocoding operations cannot be stored or 8 | persisted unless the \code{for_storage} argument is set to 9 | \code{TRUE}. The default argument value is \code{for_storage = FALSE}, which indicates the results of the operation can't be stored, but they can be temporarily displayed on a map, for instance. If you store the results, in a database, for example, you need to set this parameter to true. 10 | } 11 | \details{ 12 | See \href{https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm#ESRI_SECTION3_BBCB5704B46B4CDF8377749B873B1A7F}{the official documentation} for more context. 13 | } 14 | -------------------------------------------------------------------------------- /man/suggest_places.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/core-suggest.R 3 | \name{suggest_places} 4 | \alias{suggest_places} 5 | \title{Search Suggestion} 6 | \usage{ 7 | suggest_places( 8 | text, 9 | location = NULL, 10 | category = NULL, 11 | search_extent = NULL, 12 | max_suggestions = NULL, 13 | country_code = NULL, 14 | preferred_label_values = NULL, 15 | geocoder = default_geocoder(), 16 | token = arc_token() 17 | ) 18 | } 19 | \arguments{ 20 | \item{text}{a scalar character of search key to generate a place suggestion.} 21 | 22 | \item{location}{an \code{sfc_POINT} object that centers the search. Optional.} 23 | 24 | \item{category}{a scalar character. Place or address type that can be used to 25 | filter suggest results. Optional.} 26 | 27 | \item{search_extent}{an object of class \code{bbox} that limits the search area. This is especially useful for applications in which a user will search for places and addresses within the current map extent. Optional.} 28 | 29 | \item{max_suggestions}{default \code{NULL}. The maximum number of suggestions to return. 30 | The service default is 5 with a maximum of 15.} 31 | 32 | \item{country_code}{default \code{NULL.} An ISO 3166 country code. 33 | See \code{\link[=iso_3166_codes]{iso_3166_codes()}} for valid ISO codes. Optional.} 34 | 35 | \item{preferred_label_values}{default NULL. Must be one of \code{"postalCity"} 36 | or \code{"localCity"}. Optional.} 37 | 38 | \item{geocoder}{default \code{\link[=default_geocoder]{default_geocoder()}}.} 39 | 40 | \item{token}{an object of class \code{httr2_token} as generated by \code{\link[arcgisutils:auth_code]{auth_code()}} 41 | or related function} 42 | } 43 | \value{ 44 | A \code{data.frame} with 3 columns: \code{text}, \code{magic_key}, and \code{is_collection}. 45 | } 46 | \description{ 47 | This function returns candidate locations based on a partial search query. 48 | It is designed to be used in an interactive search experience in a client 49 | facing application. 50 | } 51 | \details{ 52 | Unlike the other functions in this package, \code{suggest_places()} is not 53 | vectorized as it is intended to provide search suggestions for individual 54 | queries such as those made in a search bar. 55 | 56 | Utilizes the \href{https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm}{\verb{/suggest}} endpoint. 57 | } 58 | \examples{ 59 | # identify a search point 60 | location <- sf::st_sfc(sf::st_point(c(-84.34, 33.74)), crs = 4326) 61 | 62 | # create a search extent from it 63 | search_extent <- sf::st_bbox(sf::st_buffer(location, 10)) 64 | 65 | # find suggestions from it 66 | suggestions <- suggest_places( 67 | "bellwood", 68 | location, 69 | search_extent = search_extent 70 | ) 71 | 72 | # get address candidate information 73 | # using the text and the magic key 74 | find_address_candidates( 75 | suggestions$text, 76 | magic_key = suggestions$magic_key 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /man/world_geocoder.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/doc-data.R 3 | \docType{data} 4 | \name{world_geocoder} 5 | \alias{world_geocoder} 6 | \title{ArcGIS World Geocoder} 7 | \format{ 8 | An object of class \code{GeocodeServer} (inherits from \code{list}) of length 12. 9 | } 10 | \usage{ 11 | world_geocoder 12 | } 13 | \value{ 14 | an object of class \code{GeocodeServer} 15 | } 16 | \description{ 17 | The \href{https://www.esri.com/en-us/arcgis/products/arcgis-world-geocoder}{ArcGIS World Geocoder} 18 | is made publicly available for some uses. The \code{world_geocoder} object is used 19 | as the default \code{GeocodeServer} object in \code{\link[=default_geocoder]{default_geocoder()}} when no 20 | authorization token is found. The \code{\link[=find_address_candidates]{find_address_candidates()}}, 21 | \code{\link[=reverse_geocode]{reverse_geocode()}}, and \code{\link[=suggest_places]{suggest_places()}} can be used without an 22 | authorization token. The \code{\link[=geocode_addresses]{geocode_addresses()}} funciton requires an 23 | authorization token to be used for batch geocoding. 24 | } 25 | \keyword{datasets} 26 | -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /pkgdown/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /pkgdown/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/pkgdown/favicon/favicon.ico -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | *.dll 4 | target 5 | .cargo 6 | -------------------------------------------------------------------------------- /src/Makevars.in: -------------------------------------------------------------------------------- 1 | TARGET_DIR = ./rust/target 2 | LIBDIR = $(TARGET_DIR)/@LIBDIR@ 3 | STATLIB = $(LIBDIR)/libarcgisgeocode.a 4 | PKG_LIBS = -L$(LIBDIR) -larcgisgeocode 5 | 6 | all: $(SHLIB) rust_clean 7 | 8 | .PHONY: $(STATLIB) 9 | 10 | $(SHLIB): $(STATLIB) 11 | 12 | CARGOTMP = $(CURDIR)/.cargo 13 | VENDOR_DIR = $(CURDIR)/vendor 14 | 15 | 16 | # RUSTFLAGS appends --print=native-static-libs to ensure that 17 | # the correct linkers are used. Use this for debugging if need. 18 | # 19 | # CRAN note: Cargo and Rustc versions are reported during 20 | # configure via tools/msrv.R. 21 | # 22 | # When the NOT_CRAN flag is *not* set, the vendor.tar.xz, if present, 23 | # is unzipped and used for offline compilation. 24 | $(STATLIB): 25 | 26 | # Check if NOT_CRAN is false and unzip vendor.tar.xz if so 27 | if [ "$(NOT_CRAN)" != "true" ]; then \ 28 | if [ -f ./rust/vendor.tar.xz ]; then \ 29 | tar xf rust/vendor.tar.xz && \ 30 | mkdir -p $(CARGOTMP) && \ 31 | cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ 32 | fi; \ 33 | fi 34 | 35 | export CARGO_HOME=$(CARGOTMP) && \ 36 | export PATH="$(PATH):$(HOME)/.cargo/bin" && \ 37 | RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --lib @PROFILE@ --manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR) 38 | 39 | # Always clean up CARGOTMP 40 | rm -Rf $(CARGOTMP); 41 | 42 | rust_clean: $(SHLIB) 43 | rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ 44 | 45 | clean: 46 | rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) 47 | -------------------------------------------------------------------------------- /src/Makevars.ucrt: -------------------------------------------------------------------------------- 1 | # Rtools42 doesn't have the linker in the location that cargo expects, so we 2 | # need to overwrite it via configuration. 3 | CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe 4 | 5 | include Makevars.win 6 | -------------------------------------------------------------------------------- /src/Makevars.win.in: -------------------------------------------------------------------------------- 1 | TARGET = $(subst 64,x86_64,$(subst 32,i686,$(WIN)))-pc-windows-gnu 2 | 3 | TARGET_DIR = ./rust/target 4 | LIBDIR = $(TARGET_DIR)/$(TARGET)/@LIBDIR@ 5 | STATLIB = $(LIBDIR)/libarcgisgeocode.a 6 | PKG_LIBS = -L$(LIBDIR) -larcgisgeocode -lws2_32 -ladvapi32 -luserenv -lbcrypt -lntdll 7 | 8 | all: $(SHLIB) rust_clean 9 | 10 | .PHONY: $(STATLIB) 11 | 12 | $(SHLIB): $(STATLIB) 13 | 14 | CARGOTMP = $(CURDIR)/.cargo 15 | VENDOR_DIR = vendor 16 | 17 | $(STATLIB): 18 | mkdir -p $(TARGET_DIR)/libgcc_mock 19 | touch $(TARGET_DIR)/libgcc_mock/libgcc_eh.a 20 | 21 | if [ "$(NOT_CRAN)" != "true" ]; then \ 22 | if [ -f ./rust/vendor.tar.xz ]; then \ 23 | tar xf rust/vendor.tar.xz && \ 24 | mkdir -p $(CARGOTMP) && \ 25 | cp rust/vendor-config.toml $(CARGOTMP)/config.toml; \ 26 | fi; \ 27 | fi 28 | 29 | # Build the project using Cargo with additional flags 30 | export CARGO_HOME=$(CARGOTMP) && \ 31 | export LIBRARY_PATH="$(LIBRARY_PATH);$(CURDIR)/$(TARGET_DIR)/libgcc_mock" && \ 32 | RUSTFLAGS="$(RUSTFLAGS) --print=native-static-libs" cargo build @CRAN_FLAGS@ --target=$(TARGET) --lib @PROFILE@ --manifest-path=rust/Cargo.toml --target-dir=$(TARGET_DIR) 33 | 34 | # Always clean up CARGOTMP 35 | rm -Rf $(CARGOTMP); 36 | 37 | rust_clean: $(SHLIB) 38 | rm -Rf $(CARGOTMP) $(VENDOR_DIR) @CLEAN_TARGET@ 39 | 40 | clean: 41 | rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) $(TARGET_DIR) 42 | -------------------------------------------------------------------------------- /src/arcgisgeocode-win.def: -------------------------------------------------------------------------------- 1 | EXPORTS 2 | R_init_arcgisgeocode 3 | -------------------------------------------------------------------------------- /src/entrypoint.c: -------------------------------------------------------------------------------- 1 | // We need to forward routine registration from C to Rust 2 | // to avoid the linker removing the static library. 3 | 4 | void R_init_arcgisgeocode_extendr(void *dll); 5 | 6 | void R_init_arcgisgeocode(void *dll) { 7 | R_init_arcgisgeocode_extendr(dll); 8 | } 9 | -------------------------------------------------------------------------------- /src/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = 'arcgisgeocode' 3 | publish = false 4 | version = '0.1.0' 5 | edition = '2021' 6 | rust-version = "1.65.0" 7 | 8 | [lib] 9 | crate-type = ['staticlib'] 10 | name = 'arcgisgeocode' 11 | 12 | [dependencies] 13 | extendr-api = { version = "0.8.0", features = ["serde"] } 14 | rust_iso3166 = "0.1.12" 15 | serde = "*" 16 | serde_esri = { git = "https://github.com/josiahparry/serde_esri" } 17 | serde_json = "*" 18 | serde_with = { version = "*" } 19 | 20 | [profile.release] 21 | lto = true 22 | codegen-units = 1 23 | -------------------------------------------------------------------------------- /src/rust/src/batch_geocode.rs: -------------------------------------------------------------------------------- 1 | use crate::find_candidates::Attributes as GeocodeAttrs; 2 | use crate::{as_sfg, parse_sr}; 3 | use extendr_api::prelude::*; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_esri::{geometry::EsriPoint, spatial_reference::SpatialReference}; 6 | use serde_with::skip_serializing_none; 7 | 8 | #[derive(Debug, Deserialize, Serialize, Clone)] 9 | pub struct GeocodeAdddressesResults { 10 | #[serde(rename = "spatialReference")] 11 | pub spatial_reference: SpatialReference, 12 | pub locations: Vec, 13 | } 14 | 15 | #[skip_serializing_none] 16 | #[derive(Debug, Deserialize, Serialize, Clone)] 17 | pub struct Location { 18 | pub address: Option, 19 | pub location: Option, 20 | pub score: f64, 21 | pub attributes: GeocodeAttrs, 22 | } 23 | 24 | #[skip_serializing_none] 25 | #[derive(Debug, Deserialize, Serialize, Clone)] 26 | pub struct Address { 27 | objectid: i32, 28 | #[serde(rename = "singleLine")] 29 | single_line: Option, 30 | address: Option, 31 | address2: Option, 32 | address3: Option, 33 | neighborhood: Option, 34 | city: Option, 35 | subregion: Option, 36 | region: Option, 37 | postal: Option, 38 | #[serde(rename = "postalExt")] 39 | postal_ext: Option, 40 | #[serde(rename = "countryCode")] 41 | country_code: Option, 42 | location: Option, 43 | } 44 | 45 | #[derive(Debug, Deserialize, Serialize, Clone)] 46 | pub struct Record { 47 | attributes: Address, 48 | } 49 | 50 | #[derive(Debug, Deserialize, Serialize, Clone)] 51 | pub struct Records { 52 | records: Vec, 53 | } 54 | 55 | // TODO have an argument for object ID 56 | #[extendr] 57 | pub fn create_records( 58 | object_id: Integers, 59 | single_line: Nullable, 60 | address: Nullable, 61 | address2: Nullable, 62 | address3: Nullable, 63 | neighborhood: Nullable, 64 | city: Nullable, 65 | subregion: Nullable, 66 | region: Nullable, 67 | postal: Nullable, 68 | postal_ext: Nullable, 69 | country_code: Nullable, 70 | location: Nullable, 71 | sr: Robj, 72 | n: i32, 73 | ) -> String { 74 | let n = n as usize; 75 | let spatial_ref = parse_sr(sr); 76 | let mut record_vec: Vec = Vec::with_capacity(n); 77 | 78 | for i in 0..n { 79 | let single = match single_line { 80 | NotNull(ref a) => { 81 | let s = a.elt(i); 82 | Some(s.to_string()) 83 | } 84 | Null => None, 85 | }; 86 | 87 | let addr = match address { 88 | NotNull(ref a) => { 89 | let s = a.elt(i); 90 | Some(s.to_string()) 91 | } 92 | Null => None, 93 | }; 94 | 95 | let addr2 = match address2 { 96 | NotNull(ref a) => { 97 | let s = a.elt(i); 98 | Some(s.to_string()) 99 | } 100 | Null => None, 101 | }; 102 | 103 | let addr3 = match address3 { 104 | NotNull(ref a) => { 105 | let s = a.elt(i); 106 | Some(s.to_string()) 107 | } 108 | Null => None, 109 | }; 110 | 111 | let nbh = match neighborhood { 112 | NotNull(ref a) => { 113 | let s = a.elt(i); 114 | Some(s.to_string()) 115 | } 116 | Null => None, 117 | }; 118 | 119 | let cty = match city { 120 | NotNull(ref a) => { 121 | let s = a.elt(i); 122 | Some(s.to_string()) 123 | } 124 | Null => None, 125 | }; 126 | 127 | let sub = match subregion { 128 | NotNull(ref a) => { 129 | let s = a.elt(i); 130 | Some(s.to_string()) 131 | } 132 | Null => None, 133 | }; 134 | 135 | let reg = match region { 136 | NotNull(ref a) => { 137 | let s = a.elt(i); 138 | Some(s.to_string()) 139 | } 140 | Null => None, 141 | }; 142 | 143 | let post = match postal { 144 | NotNull(ref a) => { 145 | let s = a.elt(i); 146 | Some(s.to_string()) 147 | } 148 | Null => None, 149 | }; 150 | 151 | let postx = match postal_ext { 152 | NotNull(ref a) => { 153 | let s = a.elt(i); 154 | Some(s.to_string()) 155 | } 156 | Null => None, 157 | }; 158 | 159 | let cc = match country_code { 160 | NotNull(ref a) => { 161 | let s = a.elt(i); 162 | Some(s.to_string()) 163 | } 164 | Null => None, 165 | }; 166 | 167 | let loc = match location { 168 | // If not-null that means the spatial ref also has to not be null 169 | NotNull(ref a) => { 170 | let loc = Doubles::try_from(a.elt(i).unwrap()).unwrap(); 171 | let p = EsriPoint { 172 | x: loc[0].inner(), 173 | y: loc[1].inner(), 174 | z: None, 175 | m: None, 176 | spatialReference: Some(spatial_ref.clone().unwrap()), 177 | }; 178 | 179 | Some(p) 180 | } 181 | Null => None, 182 | }; 183 | 184 | let oid = object_id[i].inner(); 185 | let record = Address { 186 | objectid: oid, 187 | single_line: single, 188 | address: addr, 189 | address2: addr2, 190 | address3: addr3, 191 | neighborhood: nbh, 192 | city: cty, 193 | subregion: sub, 194 | region: reg, 195 | postal: post, 196 | postal_ext: postx, 197 | country_code: cc, 198 | location: loc, 199 | }; 200 | 201 | // push record into vec 202 | record_vec.push(Record { attributes: record }); 203 | } 204 | 205 | let recs = Records { 206 | records: record_vec, 207 | }; 208 | 209 | serde_json::to_string(&recs).unwrap() 210 | } 211 | 212 | 213 | #[derive(Debug, Clone, Deserialize, Serialize)] 214 | struct ErrorCode { 215 | code: i32, 216 | extended_code: Option, 217 | message: Option, 218 | details: Option> 219 | } 220 | #[derive(Debug, Clone, Deserialize, Serialize)] 221 | struct ErrorMsg { 222 | error: ErrorCode 223 | } 224 | 225 | #[extendr] 226 | pub fn parse_location_json(x: &str) -> Robj { 227 | let parsed = serde_json::from_str::(x); 228 | 229 | match parsed { 230 | Ok(p) => { 231 | let n = p.locations.len(); 232 | let mut location_res = List::new(n); 233 | 234 | let location_attrs = p 235 | .locations 236 | .into_iter() 237 | .enumerate() 238 | .map(|(i, pi)| { 239 | if let Some(loc) = pi.location { 240 | let _ = location_res.set_elt(i, as_sfg(loc)); 241 | } else { 242 | let empty_point = Doubles::from_values([Rfloat::na(), Rfloat::na()]) 243 | .into_robj() 244 | .set_class(&["XY", "POINT", "sfg"]) 245 | .unwrap() 246 | .to_owned(); 247 | 248 | let _ = location_res.set_elt(i, empty_point); 249 | } 250 | pi.attributes 251 | }) 252 | .collect::>(); 253 | 254 | let res = location_attrs.into_dataframe().unwrap(); 255 | let location_attrs = res.as_robj().clone(); 256 | 257 | list!( 258 | attributes = location_attrs, 259 | locations = location_res, 260 | sr = extendr_api::serializer::to_robj(&p.spatial_reference).unwrap() 261 | ) 262 | .into_robj() 263 | } 264 | Err(_) => { 265 | match serde_json::from_str::(x) { 266 | Ok(e) => { 267 | let err = e.error; 268 | let fmt = format!("Error occured parsing response:\n{}: {} {}", err.code, err.message.unwrap_or("".into()), err.details.unwrap_or(vec![]).join(" ")); 269 | eprintln!("{fmt}"); 270 | } 271 | Err(e) => { 272 | eprintln!("Error occured parsing : {:?}", e.to_string()); 273 | } 274 | } 275 | ().into_robj() 276 | }, 277 | } 278 | } 279 | 280 | extendr_module! { 281 | mod batch_geocode; 282 | fn create_records; 283 | fn parse_location_json; 284 | } 285 | -------------------------------------------------------------------------------- /src/rust/src/find_candidates.rs: -------------------------------------------------------------------------------- 1 | use crate::as_sfg; 2 | use extendr_api::{prelude::*, Attributes as ExtendrAttr}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_esri::{geometry::EsriPoint, spatial_reference::SpatialReference}; 5 | use serde_with::{serde_as, NoneAsEmptyString}; 6 | 7 | #[serde_as] 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | #[serde(rename_all = "camelCase")] 10 | pub struct FindCandidatesResponse { 11 | pub spatial_reference: SpatialReference, 12 | pub candidates: Vec, 13 | } 14 | 15 | #[serde_as] 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct Candidate { 19 | pub address: Option, 20 | pub location: EsriPoint, 21 | pub score: f64, 22 | pub attributes: Attributes, 23 | pub extent: Extent, 24 | } 25 | 26 | #[serde_as] 27 | #[derive(Debug, Clone, Serialize, Deserialize, IntoDataFrameRow)] 28 | #[serde(rename_all = "camelCase")] 29 | pub struct Attributes { 30 | #[serde(rename = "ResultID")] 31 | pub result_id: Option, 32 | 33 | #[serde(rename = "Loc_name")] 34 | pub loc_name: Option, 35 | 36 | #[serde_as(as = "NoneAsEmptyString")] 37 | #[serde(rename = "Status")] 38 | pub status: Option, 39 | 40 | #[serde(rename = "Score")] 41 | pub score: Option, 42 | 43 | #[serde(rename = "Match_addr")] 44 | #[serde_as(as = "NoneAsEmptyString")] 45 | pub match_addr: Option, 46 | 47 | #[serde(rename = "LongLabel")] 48 | #[serde_as(as = "NoneAsEmptyString")] 49 | pub long_label: Option, 50 | 51 | #[serde(rename = "ShortLabel")] 52 | #[serde_as(as = "NoneAsEmptyString")] 53 | pub short_label: Option, 54 | 55 | #[serde(rename = "Addr_type")] 56 | #[serde_as(as = "NoneAsEmptyString")] 57 | pub addr_type: Option, 58 | 59 | #[serde(rename = "Type")] 60 | #[serde_as(as = "NoneAsEmptyString")] 61 | pub type_field: Option, 62 | 63 | #[serde(rename = "PlaceName")] 64 | #[serde_as(as = "NoneAsEmptyString")] 65 | pub place_name: Option, 66 | 67 | #[serde(rename = "Place_addr")] 68 | #[serde_as(as = "NoneAsEmptyString")] 69 | pub place_addr: Option, 70 | 71 | #[serde(rename = "Phone")] 72 | #[serde_as(as = "NoneAsEmptyString")] 73 | pub phone: Option, 74 | 75 | #[serde(rename = "URL")] 76 | #[serde_as(as = "NoneAsEmptyString")] 77 | pub url: Option, 78 | 79 | #[serde(rename = "Rank")] 80 | pub rank: Option, 81 | 82 | #[serde(rename = "AddBldg")] 83 | #[serde_as(as = "NoneAsEmptyString")] 84 | pub add_bldg: Option, 85 | 86 | #[serde_as(as = "NoneAsEmptyString")] 87 | #[serde(rename = "AddNum")] 88 | pub add_num: Option, 89 | 90 | #[serde_as(as = "NoneAsEmptyString")] 91 | #[serde(rename = "AddNumFrom")] 92 | pub add_num_from: Option, 93 | 94 | #[serde(rename = "AddNumTo")] 95 | #[serde_as(as = "NoneAsEmptyString")] 96 | pub add_num_to: Option, 97 | 98 | #[serde(rename = "AddRange")] 99 | #[serde_as(as = "NoneAsEmptyString")] 100 | pub add_range: Option, 101 | 102 | #[serde(rename = "Side")] 103 | #[serde_as(as = "NoneAsEmptyString")] 104 | pub side: Option, 105 | 106 | #[serde(rename = "StPreDir")] 107 | #[serde_as(as = "NoneAsEmptyString")] 108 | pub st_pre_dir: Option, 109 | 110 | #[serde(rename = "StPreType")] 111 | #[serde_as(as = "NoneAsEmptyString")] 112 | pub st_pre_type: Option, 113 | 114 | #[serde(rename = "StName")] 115 | #[serde_as(as = "NoneAsEmptyString")] 116 | pub st_name: Option, 117 | 118 | #[serde(rename = "StType")] 119 | #[serde_as(as = "NoneAsEmptyString")] 120 | pub st_type: Option, 121 | 122 | #[serde(rename = "StDir")] 123 | #[serde_as(as = "NoneAsEmptyString")] 124 | pub st_dir: Option, 125 | 126 | #[serde(rename = "BldgType")] 127 | #[serde_as(as = "NoneAsEmptyString")] 128 | pub bldg_type: Option, 129 | 130 | #[serde(rename = "BldgName")] 131 | #[serde_as(as = "NoneAsEmptyString")] 132 | pub bldg_name: Option, 133 | 134 | #[serde(rename = "LevelType")] 135 | #[serde_as(as = "NoneAsEmptyString")] 136 | pub level_type: Option, 137 | 138 | #[serde(rename = "LevelName")] 139 | #[serde_as(as = "NoneAsEmptyString")] 140 | pub level_name: Option, 141 | 142 | #[serde(rename = "UnitType")] 143 | #[serde_as(as = "NoneAsEmptyString")] 144 | pub unit_type: Option, 145 | 146 | #[serde(rename = "UnitName")] 147 | #[serde_as(as = "NoneAsEmptyString")] 148 | pub unit_name: Option, 149 | 150 | #[serde(rename = "SubAddr")] 151 | #[serde_as(as = "NoneAsEmptyString")] 152 | pub sub_addr: Option, 153 | 154 | #[serde(rename = "StAddr")] 155 | #[serde_as(as = "NoneAsEmptyString")] 156 | pub st_addr: Option, 157 | 158 | #[serde(rename = "Block")] 159 | #[serde_as(as = "NoneAsEmptyString")] 160 | pub block: Option, 161 | 162 | #[serde(rename = "Sector")] 163 | #[serde_as(as = "NoneAsEmptyString")] 164 | pub sector: Option, 165 | 166 | #[serde(rename = "Nbrhd")] 167 | #[serde_as(as = "NoneAsEmptyString")] 168 | pub nbrhd: Option, 169 | 170 | #[serde(rename = "District")] 171 | #[serde_as(as = "NoneAsEmptyString")] 172 | pub district: Option, 173 | 174 | #[serde(rename = "City")] 175 | #[serde_as(as = "NoneAsEmptyString")] 176 | pub city: Option, 177 | 178 | #[serde(rename = "MetroArea")] 179 | #[serde_as(as = "NoneAsEmptyString")] 180 | pub metro_area: Option, 181 | 182 | #[serde(rename = "Subregion")] 183 | #[serde_as(as = "NoneAsEmptyString")] 184 | pub subregion: Option, 185 | 186 | #[serde(rename = "Region")] 187 | #[serde_as(as = "NoneAsEmptyString")] 188 | pub region: Option, 189 | 190 | #[serde_as(as = "NoneAsEmptyString")] 191 | #[serde(rename = "RegionAbbr")] 192 | pub region_abbr: Option, 193 | 194 | #[serde_as(as = "NoneAsEmptyString")] 195 | #[serde(rename = "Territory")] 196 | pub territory: Option, 197 | 198 | #[serde_as(as = "NoneAsEmptyString")] 199 | #[serde(rename = "Zone")] 200 | pub zone: Option, 201 | 202 | #[serde_as(as = "NoneAsEmptyString")] 203 | #[serde(rename = "Postal")] 204 | pub postal: Option, 205 | 206 | #[serde(rename = "PostalExt")] 207 | #[serde_as(as = "NoneAsEmptyString")] 208 | pub postal_ext: Option, 209 | 210 | #[serde_as(as = "NoneAsEmptyString")] 211 | #[serde(rename = "Country")] 212 | pub country: Option, 213 | 214 | #[serde_as(as = "NoneAsEmptyString")] 215 | #[serde(rename = "CntryName")] 216 | pub cntry_name: Option, 217 | 218 | #[serde_as(as = "NoneAsEmptyString")] 219 | #[serde(rename = "LangCode")] 220 | pub lang_code: Option, 221 | 222 | #[serde(rename = "Distance")] 223 | pub distance: Option, 224 | 225 | #[serde(rename = "X")] 226 | pub x: Option, 227 | 228 | #[serde(rename = "Y")] 229 | pub y: Option, 230 | 231 | #[serde(rename = "DisplayX")] 232 | pub display_x: Option, 233 | 234 | #[serde(rename = "DisplayY")] 235 | pub display_y: Option, 236 | 237 | #[serde(rename = "Xmin")] 238 | pub xmin: Option, 239 | 240 | #[serde(rename = "Xmax")] 241 | pub xmax: Option, 242 | 243 | #[serde(rename = "Ymin")] 244 | pub ymin: Option, 245 | 246 | #[serde(rename = "Ymax")] 247 | pub ymax: Option, 248 | 249 | #[serde(rename = "ExInfo")] 250 | #[serde_as(as = "NoneAsEmptyString")] 251 | pub ex_info: Option, 252 | } 253 | 254 | #[serde_as] 255 | #[derive(Debug, Clone, Serialize, Deserialize)] 256 | #[serde(rename_all = "camelCase")] 257 | pub struct Extent { 258 | pub xmin: f64, 259 | pub ymin: f64, 260 | pub xmax: f64, 261 | pub ymax: f64, 262 | } 263 | 264 | #[extendr] 265 | pub fn parse_candidate_json(x: &str) -> Robj { 266 | let parsed = serde_json::from_str::(x); 267 | 268 | match parsed { 269 | Ok(p) => { 270 | let n = p.candidates.len(); 271 | 272 | if n == 0 { 273 | return ().into_robj(); 274 | } 275 | let mut extent_res = List::new(n); 276 | let mut location_res = List::new(n); 277 | 278 | let candidate_attrs = p 279 | .candidates 280 | .into_iter() 281 | .enumerate() 282 | .map(|(i, pi)| { 283 | let _ = location_res.set_elt(i, as_sfg(pi.location)); 284 | 285 | let Extent { 286 | xmin, 287 | ymin, 288 | xmax, 289 | ymax, 290 | } = pi.extent; 291 | 292 | let extent = Doubles::from_values([xmin, ymin, xmax, ymax]) 293 | .into_robj() 294 | .set_attrib("names", ["xmin", "ymin", "xmax", "ymax"]) 295 | .unwrap() 296 | .to_owned(); 297 | 298 | let _ = extent_res.set_elt(i, extent); 299 | 300 | pi.attributes 301 | }) 302 | .collect::>(); 303 | 304 | let res = candidate_attrs.into_dataframe().unwrap(); 305 | let candidate_attrs = res.as_robj().clone(); 306 | 307 | list!( 308 | attributes = candidate_attrs, 309 | extents = extent_res, 310 | locations = location_res, 311 | sr = extendr_api::serializer::to_robj(&p.spatial_reference).unwrap() 312 | ) 313 | .into_robj() 314 | } 315 | Err(_) => { 316 | // rprintln!("{:?}", e); 317 | // rprintln!("{x}"); 318 | ().into_robj() 319 | } 320 | } 321 | } 322 | 323 | extendr_module! { 324 | mod find_candidates; 325 | fn parse_candidate_json; 326 | } 327 | -------------------------------------------------------------------------------- /src/rust/src/iso3166.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | 3 | 4 | #[extendr] 5 | fn is_iso3166(code: Strings) -> Logicals { 6 | code 7 | .into_iter() 8 | .map(|c| { 9 | if c.is_na() { 10 | Rbool::na() 11 | } else { 12 | Rbool::from(is_iso3166_scalar(c.as_str())) 13 | } 14 | }) 15 | .collect::() 16 | } 17 | 18 | fn is_iso3166_scalar(code: &str) -> bool { 19 | let code = code.to_uppercase(); 20 | //https://developers.arcgis.com/rest/geocode/api-reference/geocode-coverage.htm#GUID-D61FB53E-32DF-4E0E-A1CC-473BA38A23C0 21 | let non_iso_valid = ["EUR", "NCY", "PLI", "RKS", "SPI"]; 22 | 23 | // check these first 24 | if non_iso_valid.contains(&code.as_str()) { 25 | return true; 26 | } 27 | 28 | // check the rest 29 | let alpha2 = rust_iso3166::from_alpha2(&code); 30 | 31 | if alpha2.is_some() { 32 | return true 33 | } 34 | 35 | let alpha3 = rust_iso3166::from_alpha3(&code); 36 | 37 | if alpha3.is_some() { 38 | return true 39 | } 40 | 41 | false 42 | } 43 | 44 | #[extendr] 45 | fn iso_3166_2() -> Strings { 46 | let iso = rust_iso3166::ALL; 47 | iso 48 | .iter() 49 | .map(|c| c.alpha2) 50 | .collect::() 51 | } 52 | 53 | #[extendr] 54 | fn iso_3166_3() -> Strings { 55 | let iso = rust_iso3166::ALL; 56 | iso 57 | .iter() 58 | .map(|c| c.alpha3) 59 | .collect::() 60 | } 61 | 62 | #[extendr] 63 | fn iso_3166_names() -> Strings { 64 | let iso = rust_iso3166::ALL; 65 | iso 66 | .iter() 67 | .map(|c| c.name) 68 | .collect::() 69 | } 70 | 71 | extendr_module! { 72 | mod iso3166; 73 | fn is_iso3166; 74 | fn iso_3166_2; 75 | fn iso_3166_3; 76 | fn iso_3166_names; 77 | } -------------------------------------------------------------------------------- /src/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::{deserializer::from_robj, prelude::*}; 2 | use serde_esri::{geometry::EsriPoint, spatial_reference::SpatialReference}; 3 | use serde_json::to_string; 4 | use std::sync::Arc; 5 | 6 | mod batch_geocode; 7 | mod find_candidates; 8 | mod iso3166; 9 | mod parse_custom_attrs; 10 | mod reverse; 11 | mod suggest; 12 | 13 | extendr_module! { 14 | mod arcgisgeocode; 15 | fn as_esri_point_json; 16 | use batch_geocode; 17 | use find_candidates; 18 | use iso3166; 19 | use parse_custom_attrs; 20 | use reverse; 21 | use suggest; 22 | } 23 | 24 | fn parse_sr(sr: Robj) -> Option { 25 | let sr: Result = from_robj(&sr); 26 | sr.ok() 27 | } 28 | 29 | fn sfc_point_to_esri_point(pnts: List, sr: SpatialReference) -> Vec> { 30 | let sr = Arc::new(sr); 31 | 32 | if !pnts.inherits("sfc_POINT") { 33 | throw_r_error("Expected `sfc_POINT`") 34 | } 35 | 36 | let esri_pnts = pnts 37 | .into_iter() 38 | .map(|(_, pi)| { 39 | // TODO if None, return None, unwrap here is unsafe 40 | let crds: Doubles = Doubles::try_from(pi).unwrap(); 41 | 42 | if crds.len() < 2 { 43 | None 44 | } else { 45 | let pnt = EsriPoint { 46 | x: crds[0].inner(), 47 | y: crds[1].inner(), 48 | z: None, 49 | m: None, 50 | spatialReference: Some(sr.as_ref().clone()), 51 | }; 52 | Some(pnt) 53 | } 54 | }) 55 | .collect::>(); 56 | 57 | esri_pnts 58 | } 59 | 60 | // #[extendr] 61 | // fn reverse_geocode_rs( 62 | // service_url: &str, 63 | // locations: List, 64 | // crs: Robj, 65 | // lang: Option<&str>, 66 | // for_storage: Option, 67 | // feature_type: Option<&str>, 68 | // location_type: Option<&str>, 69 | // preferred_label_values: Option<&str>, 70 | // token: Option, 71 | // ) -> Strings { 72 | // // create a url 73 | // let service_url = Url::parse(service_url).unwrap(); 74 | 75 | // // extract spatial reference 76 | // let sr = Arc::new(parse_sr(crs).unwrap()); 77 | 78 | // let ftype = feature_type.map_or(None, |f| match f { 79 | // "StreetInt" => Some(FeatureType::StreetInt), 80 | // "DistanceMarker" => Some(FeatureType::DistanceMarker), 81 | // "StreetAddress" => Some(FeatureType::StreetAddress), 82 | // "StreetName" => Some(FeatureType::StreetName), 83 | // "POI" => Some(FeatureType::POI), 84 | // "Subaddress" => Some(FeatureType::Subaddress), 85 | // "PointAddress" => Some(FeatureType::PointAddress), 86 | // "Postal" => Some(FeatureType::Postal), 87 | // "Locality" => Some(FeatureType::Locality), 88 | // _ => None, 89 | // }); 90 | 91 | // let ltype = location_type.map_or(None, |l| match l { 92 | // "Rooftop" => Some(LocationType::Rooftop), 93 | // "Street" => Some(LocationType::Street), 94 | // _ => None, 95 | // }); 96 | 97 | // let pref_lab_vals = preferred_label_values.map_or(None, |p| match p { 98 | // "PostalCity" => Some(PreferredLabelValues::PostalCity), 99 | // "LocalCity" => Some(PreferredLabelValues::LocalCity), 100 | // _ => None, 101 | // }); 102 | 103 | // // get the locations as esri points 104 | // let locs = sfc_point_to_esri_point(locations, sr.as_ref().clone()); 105 | 106 | // // allocate params vec 107 | // let mut params = Vec::with_capacity(locs.len()); 108 | 109 | // // fill in the params vec 110 | // for (_, loc) in locs.into_iter().enumerate() { 111 | // let param = ReverseGeocodeParams { 112 | // location: loc.unwrap(), 113 | // out_sr: sr.as_ref().clone(), 114 | // lang_code: lang.map_or(None, |l| Some(String::from(l))), 115 | // for_storage: for_storage, 116 | // feature_types: ftype.clone(), 117 | // location_type: ltype.clone(), 118 | // preferred_label_values: pref_lab_vals.clone(), 119 | // }; 120 | 121 | // params.push(param); 122 | // } 123 | // // res_list 124 | // // create new runtime 125 | // let rt = Runtime::new().unwrap(); 126 | 127 | // // run the reverse geocode in parallel 128 | // let res = rt.block_on(reverse_geocode_(service_url, params, token)); 129 | 130 | // res.into_iter() 131 | // .map(|r| { 132 | // let rr = r.unwrap(); 133 | // let json = serde_json::to_string(&rr).unwrap(); 134 | // json 135 | // }) 136 | // .collect::() 137 | // } 138 | 139 | // convert an EsriPoint to an sfg 140 | fn as_sfg(x: EsriPoint) -> Robj { 141 | let coord = Doubles::from_values([x.x, x.y]); 142 | coord 143 | .into_robj() 144 | .set_class(&["XY", "POINT", "sfg"]) 145 | .unwrap() 146 | .to_owned() 147 | } 148 | 149 | #[extendr] 150 | fn as_esri_point_json(x: List, sr: Robj) -> Strings { 151 | let res = sfc_point_to_esri_point(x, parse_sr(sr).unwrap()); 152 | res.into_iter() 153 | .map(|pi| match pi { 154 | Some(p) => { 155 | let json = to_string(&p).unwrap(); 156 | Rstr::from_string(&json) 157 | } 158 | None => Rstr::na(), 159 | }) 160 | .collect::() 161 | } 162 | -------------------------------------------------------------------------------- /src/rust/src/parse_custom_attrs.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use extendr_api::prelude::*; 4 | use extendr_api::serializer::to_robj; 5 | use serde_json::{de::from_str, Value}; 6 | 7 | // Takes a dataframe and creates a hashmap of column names and types 8 | // these are used to match and insert into the elements of a dataframe 9 | fn make_df_type_map(x: &List) -> HashMap { 10 | let mut map = HashMap::new(); 11 | x.iter().for_each(|(key, val)| { 12 | map.insert(String::from(key), val.rtype()); 13 | }); 14 | map 15 | } 16 | 17 | // Function to insert into a data.frame. Requires lots of matching a bit messy 18 | fn insert_into_df(x: &mut List, key: &str, rtype: &Rtype, val: Value, i: usize) { 19 | match rtype { 20 | Rtype::Logicals => { 21 | let col = x.dollar(key).unwrap(); 22 | let mut col_to_insert = Logicals::try_from(col).unwrap(); 23 | let _ = col_to_insert.set_elt(i, Rbool::from(val.as_bool())); 24 | } 25 | Rtype::Integers => { 26 | let col = x.dollar(key).unwrap(); 27 | let mut col_to_insert = Integers::try_from(col).unwrap(); 28 | let to_insert = match val.as_i64() { 29 | Some(v) => Rint::from(v as i32), 30 | None => Rint::na(), 31 | }; 32 | let _ = col_to_insert.set_elt(i, to_insert); 33 | } 34 | Rtype::Doubles => { 35 | let col = x.dollar(key).unwrap(); 36 | let mut col_to_insert = Doubles::try_from(col).unwrap(); 37 | let _ = col_to_insert.set_elt(i, Rfloat::from(val.as_f64())); 38 | } 39 | Rtype::Strings => { 40 | let col = x.dollar(key).unwrap(); 41 | let mut col_to_insert = Strings::try_from(col).unwrap(); 42 | let to_insert = match val.as_str() { 43 | Some(v) => Rstr::from(v), 44 | None => Rstr::na(), 45 | }; 46 | 47 | let _ = col_to_insert.set_elt(i, to_insert); 48 | } 49 | _ => unimplemented!(), 50 | }; 51 | } 52 | 53 | // Takes a data.frame (list) that is modified by reference 54 | // it must be completely pre-allocated otherwise a panic occurs 55 | #[extendr] 56 | fn parse_custom_location_json_(x: &str, to_fill: List) -> Robj { 57 | let col_maps = make_df_type_map(&to_fill); 58 | let mut to_fill = to_fill; 59 | 60 | let mut res: Value = from_str(x).unwrap(); 61 | 62 | let res = res.as_object_mut(); 63 | 64 | let locs = res.filter(|xi| xi.contains_key("locations")); 65 | 66 | if locs.is_none() { 67 | return ().into_robj(); 68 | } 69 | // rprintln!("Did we find locations?: {:?}", locs.is_some()); 70 | 71 | // create bindings to set from inside the scope of the iterator 72 | let mut res_locs = ().into_robj(); 73 | let mut res_sr = ().into_robj(); 74 | locs.into_iter().for_each(|li| { 75 | let sr = li.get("spatialReference"); 76 | res_sr = match to_robj(&sr) { 77 | Ok(r) => r, 78 | Err(_) => ().into_robj(), 79 | }; 80 | 81 | let r = li 82 | .get("locations") 83 | .unwrap() 84 | .as_array() 85 | .unwrap() 86 | .into_iter() 87 | .enumerate() 88 | .map(|(i, loc)| { 89 | let _r = loc 90 | .get("attributes") 91 | .unwrap() 92 | .as_object() 93 | .into_iter() 94 | .for_each(|lli| { 95 | lli.into_iter().for_each(|li| { 96 | // FIXME this should not be cloned!!! 97 | let key = li.0.as_str(); 98 | let val = li.1.clone(); 99 | // if this is None then we have an unexpected value 100 | // it is skipped 101 | let ctype = col_maps.get(key); 102 | match ctype { 103 | Some(c) => { 104 | let _inserted = insert_into_df(&mut to_fill, key, c, val, i); 105 | } 106 | None => (), 107 | }; 108 | }); 109 | }); 110 | 111 | let location_field = loc.get("location"); 112 | 113 | match location_field { 114 | Some(lf) => match lf.as_object() { 115 | Some(l) => { 116 | let xi = Rfloat::from(l.get("x").unwrap().as_f64()); 117 | let yi = Rfloat::from(l.get("y").unwrap().as_f64()); 118 | Doubles::from_values([xi, yi]) 119 | .into_robj() 120 | .set_class(&["XY", "POINT", "sfg"]) 121 | .unwrap() 122 | .to_owned() 123 | } 124 | None => Doubles::from_values([Rfloat::na(), Rfloat::na()]) 125 | .into_robj() 126 | .set_class(&["XY", "POINT", "sfg"]) 127 | .unwrap() 128 | .to_owned(), 129 | }, 130 | None => Doubles::from_values([Rfloat::na(), Rfloat::na()]) 131 | .into_robj() 132 | .set_class(&["XY", "POINT", "sfg"]) 133 | .unwrap() 134 | .to_owned(), 135 | } 136 | }) 137 | .collect::(); 138 | res_locs = r.into(); 139 | }); 140 | list!(attributes = to_fill, locations = res_locs, sr = res_sr).into() 141 | } 142 | 143 | extendr_module! { 144 | mod parse_custom_attrs; 145 | fn parse_custom_location_json_; 146 | } 147 | -------------------------------------------------------------------------------- /src/rust/src/reverse.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_esri::{geometry::EsriPoint, spatial_reference::SpatialReference}; 4 | 5 | // use reqwest::Url; 6 | // use std::result::Result; 7 | // use std::sync::Arc; 8 | 9 | // pub async fn reverse_geocode_( 10 | // service_url: Url, 11 | // params: Vec, 12 | // token: Option, 13 | // ) -> Vec> { 14 | // let url = Arc::new(service_url); 15 | // let client = reqwest::Client::new(); 16 | // let token = Arc::new(token.unwrap_or("".to_string())); 17 | 18 | // let mut tasks = Vec::with_capacity(params.len()); 19 | 20 | // // create a task for each param 21 | // for param in params { 22 | // let task = client 23 | // .clone() 24 | // .post(url.as_ref().clone()) 25 | // .query(&[("f", "json")]) 26 | // .form(¶m.as_form_body()) 27 | // .header("X-Esri-Authorization", token.as_ref().clone()) 28 | // .send(); 29 | 30 | // tasks.push(tokio::spawn(task)); 31 | // } 32 | 33 | // // create a vector to store the output 34 | // let mut outputs = Vec::with_capacity(tasks.len()); 35 | 36 | // // capture the output of each task 37 | // for task in tasks { 38 | // let task_res = task.await.unwrap(); 39 | // match task_res { 40 | // Ok(res) => { 41 | // let res = res.json::().await; 42 | // outputs.push(res); 43 | // } 44 | // Err(e) => outputs.push(Err(e)), 45 | // } 46 | // } 47 | 48 | // outputs 49 | // } 50 | 51 | #[derive(Debug, Clone, Serialize, Deserialize)] 52 | pub struct ReverseGeocodeParams { 53 | pub location: EsriPoint, 54 | #[serde(rename = "outSR")] 55 | pub out_sr: SpatialReference, 56 | #[serde(rename = "langCode")] 57 | pub lang_code: Option, 58 | #[serde(rename = "forStorage")] 59 | pub for_storage: Option, 60 | #[serde(rename = "featureTypes")] 61 | pub feature_types: Option, 62 | #[serde(rename = "locationType")] 63 | pub location_type: Option, 64 | #[serde(rename = "preferredLabelValues")] 65 | pub preferred_label_values: Option, 66 | } 67 | 68 | // use serde_json::to_string; 69 | // use std::collections::HashMap; 70 | 71 | // impl ReverseGeocodeParams { 72 | // pub fn as_form_body(self) -> HashMap<&'static str, String> { 73 | // let mut map = HashMap::new(); 74 | // map.insert("location", to_string(&self.location).unwrap()); 75 | 76 | // // insert spatialReference 77 | // map.insert("outSR", to_string(&self.out_sr).unwrap()); 78 | 79 | // // inserts langCode if present 80 | // self.lang_code 81 | // .map(|lang_code| map.insert("langCode", to_string(&lang_code).unwrap())); 82 | 83 | // // inserts forStorage if present 84 | // self.for_storage 85 | // .map(|for_storage| map.insert("forStorage", to_string(&for_storage).unwrap())); 86 | 87 | // // inserts locationType if present 88 | // self.location_type 89 | // .map(|location_type| map.insert("locationType", to_string(&location_type).unwrap())); 90 | 91 | // // inserts preferredLabelValues if present 92 | // self.preferred_label_values.map(|preferred_label_values| { 93 | // map.insert( 94 | // "preferredLabelValues", 95 | // to_string(&preferred_label_values).unwrap(), 96 | // ) 97 | // }); 98 | 99 | // map 100 | // } 101 | 102 | // pub fn _new(x: f64, y: f64) -> Self { 103 | // ReverseGeocodeParams { 104 | // location: EsriPoint { 105 | // x, 106 | // y, 107 | // z: None, 108 | // m: None, 109 | // spatialReference: None, 110 | // }, 111 | // out_sr: SpatialReference { 112 | // wkid: Some(3857), 113 | // latest_wkid: None, 114 | // vcs_wkid: None, 115 | // latest_vcs_wkid: None, 116 | // wkt: None, 117 | // }, 118 | // lang_code: None, 119 | // for_storage: None, 120 | // feature_types: None, 121 | // location_type: None, 122 | // preferred_label_values: None, 123 | // } 124 | // } 125 | // } 126 | 127 | impl Default for ReverseGeocodeParams { 128 | fn default() -> Self { 129 | ReverseGeocodeParams { 130 | location: EsriPoint { 131 | x: 0.0, 132 | y: 0.0, 133 | z: None, 134 | m: None, 135 | spatialReference: None, 136 | }, 137 | out_sr: SpatialReference { 138 | wkid: Some(4326), 139 | latest_wkid: None, 140 | vcs_wkid: None, 141 | latest_vcs_wkid: None, 142 | wkt: None, 143 | }, 144 | lang_code: None, 145 | for_storage: None, 146 | feature_types: None, 147 | location_type: None, 148 | preferred_label_values: None, 149 | } 150 | } 151 | } 152 | 153 | #[derive(Debug, Clone, Serialize, Deserialize)] 154 | #[serde(rename_all = "camelCase")] 155 | pub enum LocationType { 156 | Rooftop, 157 | Street, 158 | } 159 | 160 | #[derive(Debug, Clone, Serialize, Deserialize)] 161 | #[serde(rename_all = "camelCase")] 162 | pub enum PreferredLabelValues { 163 | PostalCity, 164 | LocalCity, 165 | } 166 | 167 | #[derive(Debug, Clone, Serialize, Deserialize)] 168 | pub enum FeatureType { 169 | StreetInt, 170 | DistanceMarker, 171 | StreetAddress, 172 | StreetName, 173 | POI, 174 | Subaddress, 175 | PointAddress, 176 | Postal, 177 | Locality, 178 | } 179 | 180 | // Expected Response from the /reverseGeocode Endpoint 181 | #[derive(Debug, Clone, Serialize, Deserialize)] 182 | #[serde(rename_all = "camelCase")] 183 | pub struct ReverseGeocodeResponse { 184 | pub address: Address, 185 | pub location: EsriPoint, 186 | } 187 | 188 | #[derive(Default, Debug, Clone, Serialize, Deserialize, IntoDataFrameRow)] 189 | #[serde(rename_all = "camelCase")] 190 | pub struct Address { 191 | #[serde(rename = "Match_addr")] 192 | pub match_addr: String, 193 | #[serde(rename = "LongLabel")] 194 | pub long_label: String, 195 | #[serde(rename = "ShortLabel")] 196 | pub short_label: String, 197 | #[serde(rename = "Addr_type")] 198 | pub addr_type: String, 199 | #[serde(rename = "Type")] 200 | pub type_field: String, 201 | #[serde(rename = "PlaceName")] 202 | pub place_name: String, 203 | #[serde(rename = "AddNum")] 204 | pub add_num: String, 205 | #[serde(rename = "Address")] 206 | pub address: String, 207 | #[serde(rename = "Block")] 208 | pub block: String, 209 | #[serde(rename = "Sector")] 210 | pub sector: String, 211 | #[serde(rename = "Neighborhood")] 212 | pub neighborhood: String, 213 | #[serde(rename = "District")] 214 | pub district: String, 215 | #[serde(rename = "City")] 216 | pub city: String, 217 | #[serde(rename = "MetroArea")] 218 | pub metro_area: String, 219 | #[serde(rename = "Subregion")] 220 | pub subregion: String, 221 | #[serde(rename = "Region")] 222 | pub region: String, 223 | #[serde(rename = "RegionAbbr")] 224 | pub region_abbr: String, 225 | #[serde(rename = "Territory")] 226 | pub territory: String, 227 | #[serde(rename = "Postal")] 228 | pub postal: String, 229 | #[serde(rename = "PostalExt")] 230 | pub postal_ext: String, 231 | #[serde(rename = "CntryName")] 232 | pub country_name: String, 233 | #[serde(rename = "CountryCode")] 234 | pub country_code: String, 235 | } 236 | 237 | #[extendr] 238 | pub fn parse_rev_geocode_resp(resps: Strings) -> List { 239 | let mut res_geo = List::new(resps.len()); 240 | 241 | let res_attrs = resps 242 | .into_iter() 243 | .enumerate() 244 | .map(|(i, ri)| { 245 | let resp = serde_json::from_str::(ri.as_str()); 246 | let res = match resp { 247 | Ok(r) => { 248 | // let res = to_robj(&r.address).unwrap().as_list().unwrap(); 249 | let _ = res_geo.set_elt(i, crate::as_sfg(r.location)); 250 | vec![r.address].into_dataframe().unwrap().as_robj().clone() 251 | // res.into_robj() 252 | } 253 | Err(_) => ().into_robj(), 254 | }; 255 | res 256 | }) 257 | .collect::() 258 | .into(); 259 | 260 | List::from_names_and_values(&["attributes", "geometry"], [res_attrs, res_geo]).unwrap() 261 | } 262 | 263 | extendr_module! { 264 | mod reverse; 265 | fn parse_rev_geocode_resp; 266 | } 267 | -------------------------------------------------------------------------------- /src/rust/src/suggest.rs: -------------------------------------------------------------------------------- 1 | use extendr_api::prelude::*; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, IntoDataFrameRow)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct Suggestion { 7 | pub text: String, 8 | pub magic_key: String, 9 | pub is_collection: bool, 10 | } 11 | 12 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 13 | pub struct Suggestions { 14 | pub suggestions: Vec, 15 | } 16 | 17 | #[extendr] 18 | pub fn parse_suggestions(x: &str) -> Robj { 19 | let sugg = serde_json::from_str::(x); 20 | match sugg { 21 | Ok(s) => Dataframe::try_from_values(s.suggestions).unwrap().as_robj().clone(), 22 | Err(_) => Dataframe::try_from_values(Suggestions::default().suggestions).unwrap().as_robj().clone(), 23 | } 24 | } 25 | 26 | extendr_module! { 27 | mod suggest; 28 | fn parse_suggestions; 29 | } 30 | -------------------------------------------------------------------------------- /src/rust/vendor-config.toml: -------------------------------------------------------------------------------- 1 | [source.crates-io] 2 | replace-with = "vendored-sources" 3 | 4 | [source."git+https://github.com/josiahparry/serde_esri"] 5 | git = "https://github.com/josiahparry/serde_esri" 6 | replace-with = "vendored-sources" 7 | 8 | [source.vendored-sources] 9 | directory = "vendor" 10 | -------------------------------------------------------------------------------- /src/rust/vendor.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R-ArcGIS/arcgisgeocode/afdbbd1d7d31582573b118b52f490a409b09c494/src/rust/vendor.tar.xz -------------------------------------------------------------------------------- /tests/testdata/custom-locator.json: -------------------------------------------------------------------------------- 1 | { 2 | "spatialReference": { 3 | "wkid": 102646, 4 | "latestWkid": 2230 5 | }, 6 | "locations": [ 7 | { 8 | "address": "", 9 | "score": 0, 10 | "attributes": { 11 | "ResultID": 1, 12 | "Loc_name": "", 13 | "Status": "U", 14 | "Score": 0, 15 | "Match_addr": "", 16 | "LongLabel": "", 17 | "ShortLabel": "", 18 | "Addr_type": "", 19 | "Type": "", 20 | "PlaceName": "", 21 | "Place_addr": "", 22 | "Phone": "", 23 | "URL": "", 24 | "Rank": 0, 25 | "AddBldg": "", 26 | "AddNum": "", 27 | "AddNumFrom": "", 28 | "AddNumTo": "", 29 | "AddRange": "", 30 | "Side": "", 31 | "StPreDir": "", 32 | "StPreType": "", 33 | "StName": "", 34 | "StType": "", 35 | "StDir": "", 36 | "BldgType": "", 37 | "BldgName": "", 38 | "LevelType": "", 39 | "LevelName": "", 40 | "UnitType": "", 41 | "UnitName": "", 42 | "SubAddr": "", 43 | "StAddr": "", 44 | "Block": "", 45 | "Sector": "", 46 | "Nbrhd": "", 47 | "District": "", 48 | "City": "", 49 | "MetroArea": "", 50 | "Subregion": "", 51 | "Region": "", 52 | "RegionAbbr": "", 53 | "Territory": "", 54 | "Zone": "", 55 | "Postal": "", 56 | "PostalExt": "", 57 | "Country": "", 58 | "CntryName": "", 59 | "LangCode": "", 60 | "Distance": 0, 61 | "X": 0, 62 | "Y": 0, 63 | "DisplayX": 0, 64 | "DisplayY": 0, 65 | "Xmin": 0, 66 | "Xmax": 0, 67 | "Ymin": 0, 68 | "Ymax": 0, 69 | "ExInfo": "", 70 | "USNG": "", 71 | "BEAT": 0, 72 | "TRACT": 0 73 | } 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | # This file is part of the standard setup for testthat. 2 | # It is recommended that you do not modify it. 3 | # 4 | # Where should you do additional test configuration? 5 | # Learn more about the roles of various files in: 6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview 7 | # * https://testthat.r-lib.org/articles/special-files.html 8 | 9 | # library(testthat) 10 | # library(arcgisgeocode) 11 | 12 | # test_check("arcgisgeocode") 13 | -------------------------------------------------------------------------------- /tests/testthat/test-obj_as_points.R: -------------------------------------------------------------------------------- 1 | test_that("check_points() handles inputs correctly", { 2 | expected <- sf::st_sfc(sf::st_point(c(-117.172, 34.052)), crs = 4326) 3 | 4 | expect_identical( 5 | obj_as_points(c(-117.172, 34.052)), 6 | expected 7 | ) 8 | 9 | expect_equal( 10 | obj_as_points(matrix(c(-117.172, 34.052), ncol = 2)), 11 | expected, 12 | ignore_attr = TRUE 13 | ) 14 | 15 | expect_no_error( 16 | obj_as_points( 17 | matrix( 18 | rep(c(-117.172, 34.052), 3), 19 | ncol = 2, byrow = TRUE 20 | ) 21 | ) 22 | ) 23 | 24 | expect_identical( 25 | obj_as_points(sf::st_point(c(-117.172, 34.052))), 26 | expected 27 | ) 28 | 29 | # error when a list is provided 30 | expect_error(obj_as_points(list())) 31 | 32 | # error when values are out of bounds 33 | expect_error(obj_as_points(c(-181, 90))) 34 | expect_error(obj_as_points(c(-180, 91))) 35 | 36 | # error when allow_null = FALSE 37 | expect_error(obj_as_points(NULL, allow_null = FALSE)) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/testthat/test-search-extent.R: -------------------------------------------------------------------------------- 1 | bbox <- sf::st_point(c(-122.307, 47.654)) |> 2 | sf::st_sfc(crs = 4326) |> 3 | sf::st_transform(3857) |> 4 | sf::st_buffer(5000) |> 5 | sf::st_transform(4326) |> 6 | sf::st_bbox() 7 | 8 | test_that("find_address_candidate(): search_extent is respected", { 9 | expect_no_error( 10 | find_address_candidates( 11 | "4130 Roosevelt Way NE", 12 | search_extent = bbox 13 | ) 14 | ) 15 | }) 16 | 17 | test_that("find_address_candidate(): search_extent must be a bbox", { 18 | expect_error( 19 | find_address_candidates( 20 | "4130 Roosevelt Way NE", 21 | search_extent = list(bbox) 22 | ) 23 | ) 24 | }) 25 | 26 | 27 | test_that("geocode_addresses(): search_extent is respected", { 28 | skip_on_ci() 29 | skip_if(!interactive(), "Must be done manually") 30 | 31 | set_arc_token(auth_user()) 32 | expect_no_error( 33 | geocode_addresses( 34 | "4130 Roosevelt Way NE", 35 | search_extent = bbox 36 | ) 37 | ) 38 | }) 39 | 40 | test_that("geocode_addresses(): search_extent must be a bbox", { 41 | set_arc_token(auth_user()) 42 | expect_error( 43 | geocode_addresses( 44 | "4130 Roosevelt Way NE", 45 | search_extent = list(bbox) 46 | ) 47 | ) 48 | }) 49 | 50 | test_that("suggest_places(): search_extent is respected", { 51 | skip_on_ci() 52 | skip_if(!interactive(), "Must be done manually") 53 | 54 | set_arc_token(auth_user()) 55 | expect_no_error( 56 | suggest_places( 57 | "espresso", 58 | search_extent = bbox 59 | ) 60 | ) 61 | }) 62 | 63 | test_that("suggest_places(): search_extent must be a bbox", { 64 | set_arc_token(auth_user()) 65 | expect_error( 66 | suggest_places( 67 | "4130 Roosevelt Way NE", 68 | search_extent = list(bbox) 69 | ) 70 | ) 71 | }) 72 | -------------------------------------------------------------------------------- /tools/config.R: -------------------------------------------------------------------------------- 1 | # check the packages MSRV first 2 | source("tools/msrv.R") 3 | 4 | # check DEBUG and NOT_CRAN environment variables 5 | env_debug <- Sys.getenv("DEBUG") 6 | env_not_cran <- Sys.getenv("NOT_CRAN") 7 | 8 | # check if the vendored zip file exists 9 | vendor_exists <- file.exists("src/rust/vendor.tar.xz") 10 | 11 | is_not_cran <- env_not_cran != "" 12 | is_debug <- env_debug != "" 13 | 14 | if (is_debug) { 15 | # if we have DEBUG then we set not cran to true 16 | # CRAN is always release build 17 | is_not_cran <- TRUE 18 | message("Creating DEBUG build.") 19 | } 20 | 21 | if (!is_not_cran) { 22 | message("Building for CRAN.") 23 | } 24 | 25 | # we set cran flags only if NOT_CRAN is empty and if 26 | # the vendored crates are present. 27 | .cran_flags <- ifelse( 28 | !is_not_cran && vendor_exists, 29 | "-j 2 --offline", 30 | "" 31 | ) 32 | 33 | # when DEBUG env var is present we use `--debug` build 34 | .profile <- ifelse(is_debug, "", "--release") 35 | .clean_targets <- ifelse(is_debug, "", "$(TARGET_DIR)") 36 | 37 | # when we are using a debug build we need to use target/debug instead of target/release 38 | .libdir <- ifelse(is_debug, "debug", "release") 39 | 40 | # read in the Makevars.in file 41 | is_windows <- .Platform[["OS.type"]] == "windows" 42 | 43 | # if windows we replace in the Makevars.win.in 44 | mv_fp <- ifelse( 45 | is_windows, 46 | "src/Makevars.win.in", 47 | "src/Makevars.in" 48 | ) 49 | 50 | # set the output file 51 | mv_ofp <- ifelse( 52 | is_windows, 53 | "src/Makevars.win", 54 | "src/Makevars" 55 | ) 56 | 57 | # delete the existing Makevars{.win} 58 | if (file.exists(mv_ofp)) { 59 | message("Cleaning previous `", mv_ofp, "`.") 60 | invisible(file.remove(mv_ofp)) 61 | } 62 | 63 | # read as a single string 64 | mv_txt <- readLines(mv_fp) 65 | 66 | # replace placeholder values 67 | new_txt <- gsub("@CRAN_FLAGS@", .cran_flags, mv_txt) |> 68 | gsub("@PROFILE@", .profile, x = _) |> 69 | gsub("@CLEAN_TARGET@", .clean_targets, x = _) |> 70 | gsub("@LIBDIR@", .libdir, x = _) 71 | 72 | message("Writing `", mv_ofp, "`.") 73 | con <- file(mv_ofp, open = "wb") 74 | writeLines(new_txt, con, sep = "\n") 75 | close(con) 76 | 77 | message("`tools/config.R` has finished.") 78 | -------------------------------------------------------------------------------- /tools/msrv.R: -------------------------------------------------------------------------------- 1 | # read the DESCRIPTION file 2 | desc <- read.dcf("DESCRIPTION") 3 | 4 | if (!"SystemRequirements" %in% colnames(desc)) { 5 | fmt <- c( 6 | "`SystemRequirements` not found in `DESCRIPTION`.", 7 | "Please specify `SystemRequirements: Cargo (Rust's package manager), rustc`" 8 | ) 9 | stop(paste(fmt, collapse = "\n")) 10 | } 11 | 12 | # extract system requirements 13 | sysreqs <- desc[, "SystemRequirements"] 14 | 15 | # check that cargo and rustc is found 16 | if (!grepl("cargo", sysreqs, ignore.case = TRUE)) { 17 | stop("You must specify `Cargo (Rust's package manager)` in your `SystemRequirements`") 18 | } 19 | 20 | if (!grepl("rustc", sysreqs, ignore.case = TRUE)) { 21 | stop("You must specify `Cargo (Rust's package manager), rustc` in your `SystemRequirements`") 22 | } 23 | 24 | # split into parts 25 | parts <- strsplit(sysreqs, ", ")[[1]] 26 | 27 | # identify which is the rustc 28 | rustc_ver <- parts[grepl("rustc", parts)] 29 | 30 | # perform checks for the presence of rustc and cargo on the OS 31 | no_cargo_msg <- c( 32 | "----------------------- [CARGO NOT FOUND]--------------------------", 33 | "The 'cargo' command was not found on the PATH. Please install Cargo", 34 | "from: https://www.rust-lang.org/tools/install", 35 | "", 36 | "Alternatively, you may install Cargo from your OS package manager:", 37 | " - Debian/Ubuntu: apt-get install cargo", 38 | " - Fedora/CentOS: dnf install cargo", 39 | " - macOS: brew install rust", 40 | "-------------------------------------------------------------------" 41 | ) 42 | 43 | no_rustc_msg <- c( 44 | "----------------------- [RUST NOT FOUND]---------------------------", 45 | "The 'rustc' compiler was not found on the PATH. Please install", 46 | paste(rustc_ver, "or higher from:"), 47 | "https://www.rust-lang.org/tools/install", 48 | "", 49 | "Alternatively, you may install Rust from your OS package manager:", 50 | " - Debian/Ubuntu: apt-get install rustc", 51 | " - Fedora/CentOS: dnf install rustc", 52 | " - macOS: brew install rust", 53 | "-------------------------------------------------------------------" 54 | ) 55 | 56 | # Add {user}/.cargo/bin to path before checking 57 | new_path <- paste0( 58 | Sys.getenv("PATH"), 59 | ":", 60 | paste0(Sys.getenv("HOME"), "/.cargo/bin") 61 | ) 62 | 63 | # set the path with the new path 64 | Sys.setenv("PATH" = new_path) 65 | 66 | # check for rustc installation 67 | rustc_version <- tryCatch( 68 | system("rustc --version", intern = TRUE), 69 | error = function(e) { 70 | stop(paste(no_rustc_msg, collapse = "\n")) 71 | } 72 | ) 73 | 74 | # check for cargo installation 75 | cargo_version <- tryCatch( 76 | system("cargo --version", intern = TRUE), 77 | error = function(e) { 78 | stop(paste(no_cargo_msg, collapse = "\n")) 79 | } 80 | ) 81 | 82 | # helper function to extract versions 83 | extract_semver <- function(ver) { 84 | if (grepl("\\d+\\.\\d+(\\.\\d+)?", ver)) { 85 | sub(".*?(\\d+\\.\\d+(\\.\\d+)?).*", "\\1", ver) 86 | } else { 87 | NA 88 | } 89 | } 90 | 91 | # get the MSRV 92 | msrv <- extract_semver(rustc_ver) 93 | 94 | # extract current version 95 | current_rust_version <- extract_semver(rustc_version) 96 | 97 | # perform check 98 | if (!is.na(msrv)) { 99 | # -1 when current version is later 100 | # 0 when they are the same 101 | # 1 when MSRV is newer than current 102 | is_msrv <- utils::compareVersion(msrv, current_rust_version) 103 | if (is_msrv == 1) { 104 | fmt <- paste0( 105 | "\n------------------ [UNSUPPORTED RUST VERSION]------------------\n", 106 | "- Minimum supported Rust version is %s.\n", 107 | "- Installed Rust version is %s.\n", 108 | "---------------------------------------------------------------" 109 | ) 110 | stop(sprintf(fmt, msrv, current_rust_version)) 111 | } 112 | } 113 | 114 | # print the versions 115 | versions_fmt <- "Using %s\nUsing %s" 116 | message(sprintf(versions_fmt, cargo_version, rustc_version)) 117 | --------------------------------------------------------------------------------