diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..4782add50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug, triage me' +assignees: '' + +--- + +Thanks for stopping by to let us know something could be better! + +--- +**PLEASE READ** + +If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. + +Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). + +If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). + +Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. + +--- + +Please be sure to include as much information as possible: + +#### Environment details + +1. Specify the API at the beginning of the title (for example, "Places: ...") +2. OS type and version +3. Library version and other environment information + +#### Steps to reproduce + + 1. ? + +#### Code example + +```python +# example +``` + +#### Stack trace +``` +# example +``` + +Following these steps will guarantee the quickest resolution possible. + +Thanks! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..39c3c5abe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,35 @@ +--- +name: Feature request +about: Suggest an idea for this library +title: '' +labels: 'type: feature request, triage me' +assignees: '' + +--- + +Thanks for stopping by to let us know something could be better! + +--- +**PLEASE READ** + +If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. + +Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). + +If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). + +Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. + +--- + + **Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + + **Describe the solution you'd like** +A clear and concise description of what you want to happen. + + **Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + + **Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 000000000..495a5c99b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,21 @@ +--- +name: Support request +about: If you have a support contract with Google, please create an issue in the Google + Cloud Support console. +title: '' +labels: 'triage me, type: question' +assignees: '' + +--- + +**PLEASE READ** + +If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/). This will ensure a timely response. + +Discover additional support services for the Google Maps Platform, including developer communities, technical guidance, and expert support at the Google Maps Platform [support resources page](https://developers.google.com/maps/support/). + +If your bug or feature request is not related to this particular library, please visit the Google Maps Platform [issue trackers](https://developers.google.com/maps/support/#issue_tracker). + +Check for answers on StackOverflow with the [google-maps](http://stackoverflow.com/questions/tagged/google-maps) tag. + +--- diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..4d7d59e9b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,12 @@ +--- +name: Pull request +about: Create a pull request +label: 'triage me' +--- +Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass +- [ ] Code coverage does not decrease (if any source code was changed) +- [ ] Appropriate docs were updated (if necessary) + +Fixes # 🦕 diff --git a/.github/blunderbuss.yml b/.github/blunderbuss.yml new file mode 100644 index 000000000..334554212 --- /dev/null +++ b/.github/blunderbuss.yml @@ -0,0 +1,4 @@ +assign_issues: + - arriolac +assign_prs: + - arriolac diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..009707d4c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +Thank you for opening a Pull Request! + +--- + +Before submitting your PR, there are a few things you can do to make sure it goes smoothly: +- [ ] Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea +- [ ] Ensure the tests and linter pass +- [ ] Code coverage does not decrease (if any source code was changed) +- [ ] Appropriate docs were updated (if necessary) + +Fixes # 🦕 diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..8ed0e080f --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,60 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 120 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 180 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pinned + - "type: bug" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: "stale" + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. Please comment here if it is still valid so that we can + reprioritize. Thank you! + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +closeComment: > + Closing this. Please reopen if you believe it should be addressed. Thank you for your contribution. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 10 + +# Limit to only `issues` or `pulls` +only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..601216aaf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A workflow that runs tests on every new pull request +name: Run unit tests + +on: + repository_dispatch: + types: [test] + pull_request: + branches: ['*'] + push: + branches: ['*'] + +jobs: + run-unit-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Run tests + run: ./gradlew check test jacocoTestReport --stacktrace + + - name: Send test results to CodeCov + run: bash <(curl -s https://codecov.io/bash) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..301561dc7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,54 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A workflow that pushes artifacts to Sonatype +name: Publish build to Sonatype + +on: + push: + tags: + - '*' + repository_dispatch: + types: [publish] + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Build and Publish + run: | + echo "Create .gpg key" + echo $GPG_KEY_ARMOR | base64 --decode > ./release.asc + gpg --quiet --output $GITHUB_WORKSPACE/release.gpg --dearmor ./release.asc + + SONATYPE_PASSWORD_ESCAPED=$(printf '%s\n' "$SONATYPE_PASSWORD" | sed -e 's/[\/&]/\\&/g') + sed -i -e "s,sonatypeUsername=,sonatypeUsername=$SONATYPE_USERNAME,g" gradle.properties + sed -i -e "s,sonatypePassword=,sonatypePassword=$SONATYPE_PASSWORD_ESCAPED,g" gradle.properties + sed -i -e "s,githubPassword=,githubPassword=$GITHUB_PASSWORD,g" gradle.properties + sed -i -e "s,signing.keyId=,signing.keyId=$GPG_KEY_ID,g" gradle.properties + sed -i -e "s,signing.password=,signing.password=$GPG_PASSWORD,g" gradle.properties + sed -i -e "s,signing.secretKeyRingFile=,signing.secretKeyRingFile=$GITHUB_WORKSPACE/release.gpg,g" gradle.properties + ./gradlew build publish --warn --stacktrace + env: + GPG_KEY_ARMOR: "${{ secrets.SYNCED_GPG_KEY_ARMOR }}" + GPG_KEY_ID: ${{ secrets.SYNCED_GPG_KEY_ID }} + GPG_PASSWORD: ${{ secrets.SYNCED_GPG_KEY_PASSWORD }} + SONATYPE_PASSWORD: '${{ secrets.SYNCED_SONATYPE_PASSWORD }}' + SONATYPE_USERNAME: ${{ secrets.SYNCED_SONATYPE_USERNAME }} + GITHUB_PASSWORD: '${{ secrets.SYNCED_GITHUB_TOKEN_REPO }}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..bebedf461 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Release +on: + push: + branches: [ master ] +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v2 + with: + extra_plugins: | + "@semantic-release/commit-analyzer" + "@semantic-release/release-notes-generator" + "@google/semantic-release-replace-plugin" + "@semantic-release/git + "@semantic-release/github + env: + GH_TOKEN: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} diff --git a/.gitignore b/.gitignore index 5fe09cfc4..a33bdb9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,19 @@ -build/ +/build/ +/out/ +/bin/ +/com/ .gradle/ +.nb-gradle/ +.nb-gradle-properties *.iml .idea/ +.settings/ +.project +.classpath +.vscode/ +.DS_Store +hs_err_*.log +local.properties + +*.gpg +*.bak diff --git a/.releaserc b/.releaserc new file mode 100644 index 000000000..470ee025f --- /dev/null +++ b/.releaserc @@ -0,0 +1,17 @@ +branches: + - master +plugins: + - "@semantic-release/commit-analyzer" + - "@semantic-release/release-notes-generator" + - - "@google/semantic-release-replace-plugin" + - replacements: + - files: + - "./gradle.properties" + from: "version=.*" + to: "version=${nextRelease.version}" + - - "@semantic-release/git" + - assets: + - "./gradle.properties" + - "@semantic-release/github" +options: + debug: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 35d2da37c..000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: java -jdk: -- oraclejdk7 -- openjdk7 -notifications: - email: - - mdr-eng@google.com -after_success: -- ./gradlew jacocoTestReport coveralls -env: - global: - - secure: TOW8DiUzKHdpa/ZL9yaAvzvzAsTfJ6+bW4y2JfukpLT4Qj0V6e6fFPlBwfOvGYR9NAGLpw7DxdNJcSn1sTxIdwvwm5YkriMoSNpHh7IMUs9vx5d/k9ZwR4RQ+XyVzm3oskoxOZUX3HnKvP3nJkGvLgTxsDBJw4FcpuU9V4yTEK4= - - secure: JyoUWkUDIR8O6Fm0QAzmb67OqlMceExmu6GJG2Z/zNSELr8t0mkvKkZ9ApQ0rUxkimUdc5SDY541DCGbwDzmp4ZE1Jd47olq0YJNSUqFvNmdmGZOM+aUfN5jREl9c4R4XPw4GxFhjym8GUPXQTOjxV18nqPyt6aJxN8sq4062u0= diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..b630f4fc4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,785 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased](https://github.com/googlemaps/google-maps-services-java/compare/v0.11.0...HEAD) + +## [v0.11.0](https://github.com/googlemaps/google-maps-services-java/compare/v0.10.2...v0.11.0) - 2020-01-21 + +### Bug Fixes + +- increase stale bot window ([22a2b4c](https://github.com/googlemaps/google-maps-services-java/commit/22a2b4cb8d6530e59494b9b25681bbae33d5ee60)) +- added path exception when there is no center or no zoom (#650) ([47acc92](https://github.com/googlemaps/google-maps-services-java/commit/47acc92)), closes [#650](https://github.com/googlemaps/google-maps-services-java/issues/650) +- added primary school as a poi AddressType to silence SafeEnumAdapter … (#646) ([6500947](https://github.com/googlemaps/google-maps-services-java/commit/6500947)), closes [#646](https://github.com/googlemaps/google-maps-services-java/issues/646) +- typo in FieldMask lon to lng ([#654](https://github.com/googlemaps/google-maps-services-java/issues/654)) ([244d188](https://github.com/googlemaps/google-maps-services-java/commit/244d188a229fdbde29bc397228a2cc1ca28946d6)) + +### Features + +- add support for experience id. ([#647](https://github.com/googlemaps/google-maps-services-java/issues/647)) ([b229806](https://github.com/googlemaps/google-maps-services-java/commit/b229806526c9a1e0b98a71889a209446a1035d36)) + +## [v0.10.2](https://github.com/googlemaps/google-maps-services-java/compare/v0.10.1...v0.10.2) - 2019-12-10 + +### Merged + +- fix: Add MAX_ROUTE_LENGTH_EXCEEDED Exception [`#632`](https://github.com/googlemaps/google-maps-services-java/pull/632) +- fix: Add more AddressComponentTypes [`#633`](https://github.com/googlemaps/google-maps-services-java/pull/633) +- automate publish to staging repository with additional nexus plugins [`#610`](https://github.com/googlemaps/google-maps-services-java/pull/610) +- mark additional deprecations for alt_id and scope in PlaceDetails [`#613`](https://github.com/googlemaps/google-maps-services-java/pull/613) +- update okhttp dependency [`#614`](https://github.com/googlemaps/google-maps-services-java/pull/614) +- direct users to https://www.javadoc.io/doc/com.google.maps/google-maps-services [`#612`](https://github.com/googlemaps/google-maps-services-java/pull/612) +- add additional place types and deprecate non supported [`#608`](https://github.com/googlemaps/google-maps-services-java/pull/608) +- deprecation warning for place fields: `alt_id`, `id`, `reference`, and `scope` [`#605`](https://github.com/googlemaps/google-maps-services-java/pull/605) + +### Commits + +- add stale config [`f7b2116`](https://github.com/googlemaps/google-maps-services-java/commit/f7b211626318b6e5ee079a5e211b66720fd3f639) +- update issue templates [`e6273e3`](https://github.com/googlemaps/google-maps-services-java/commit/e6273e39ee33bb4e84fb3c055c170ebed443d298) +- modify stale config [`fd856b8`](https://github.com/googlemaps/google-maps-services-java/commit/fd856b89de01bd0d64b194a2f1c2ad5333b1d778) + +## [v0.10.1](https://github.com/googlemaps/google-maps-services-java/compare/v0.10.0...v0.10.1) - 2019-09-23 + +### Merged + +- Fixes issue where deps were not being added to pom.xml [`#606`](https://github.com/googlemaps/google-maps-services-java/pull/606) +- Add tourist_attraction address type [`#601`](https://github.com/googlemaps/google-maps-services-java/pull/601) +- add changelog [`#598`](https://github.com/googlemaps/google-maps-services-java/pull/598) +- add subfields to mask values for place details request [`#597`](https://github.com/googlemaps/google-maps-services-java/pull/597) +- add github issue templates [`#595`](https://github.com/googlemaps/google-maps-services-java/pull/595) +- Include plus_code in PlaceDetailsRequest.FieldMask [`#594`](https://github.com/googlemaps/google-maps-services-java/pull/594) + +### Commits + +- update changelog for 0.10.1 [`943c0d9`](https://github.com/googlemaps/google-maps-services-java/commit/943c0d972301bb5bbf0980cd0951665eec6a5b30) +- add contributor and stackoverflow badges [`47b5c1c`](https://github.com/googlemaps/google-maps-services-java/commit/47b5c1cfca0ed093a81a3c11e97a6d19adb32892) + +## [v0.10.0](https://github.com/googlemaps/google-maps-services-java/compare/v0.9.4...v0.10.0) - 2019-08-27 + +### Merged + +- Updates to build/release and v0.10.0 version rev [`#592`](https://github.com/googlemaps/google-maps-services-java/pull/592) +- Making serializable PlusCode [`#591`](https://github.com/googlemaps/google-maps-services-java/pull/591) +- Upgrades build process to use maven-publish plugin [`#590`](https://github.com/googlemaps/google-maps-services-java/pull/590) +- add userRatingsTotal to PlaceDetails, add unit tests [`#587`](https://github.com/googlemaps/google-maps-services-java/pull/587) +- Add an overloaded method to enable query and location parameters [`#588`](https://github.com/googlemaps/google-maps-services-java/pull/588) + +### Commits + +- Update JavaDoc [`ec0f0a8`](https://github.com/googlemaps/google-maps-services-java/commit/ec0f0a827f6e61b464c67e54d1a2c752ec05141d) +- Opening up v0.9.5 for development [`68993f0`](https://github.com/googlemaps/google-maps-services-java/commit/68993f053bc9324e3a4dc597eeedaf0a3bfcef7d) + +## [v0.9.4](https://github.com/googlemaps/google-maps-services-java/compare/v0.9.3...v0.9.4) - 2019-08-07 + +### Merged + +- Releasing v0.9.4 [`#586`](https://github.com/googlemaps/google-maps-services-java/pull/586) +- Gradle downgrade 5.5.1 -> 5.0 for broken signing [`#585`](https://github.com/googlemaps/google-maps-services-java/pull/585) +- Versions version update [`#584`](https://github.com/googlemaps/google-maps-services-java/pull/584) +- Version update [`#582`](https://github.com/googlemaps/google-maps-services-java/pull/582) +- #572: updated the access modifier for response inner classes in Api Classes [`#581`](https://github.com/googlemaps/google-maps-services-java/pull/581) +- Fix: Format java files that break tests using gradlew check [`#573`](https://github.com/googlemaps/google-maps-services-java/pull/573) +- Add user_ratings_total to the PlaceSearchResult [`#571`](https://github.com/googlemaps/google-maps-services-java/pull/571) +- Add Serializable definition to AutoCompletePrediction$Term class [`#569`](https://github.com/googlemaps/google-maps-services-java/pull/569) +- [NOT URGENT] add new textSearchQuery (overload) method with test [`#567`](https://github.com/googlemaps/google-maps-services-java/pull/567) +- Validate TextSearchRequest without query if type is set [`#566`](https://github.com/googlemaps/google-maps-services-java/pull/566) +- Add implementation of Serializable to AutocompletePrediction [`#565`](https://github.com/googlemaps/google-maps-services-java/pull/565) +- Add Light Rail Station into PlaceType [`#562`](https://github.com/googlemaps/google-maps-services-java/pull/562) +- Add meal_delivery AddressComponentType [`#561`](https://github.com/googlemaps/google-maps-services-java/pull/561) + +### Commits + +- add new textSearchQuery method with test [`96a6a56`](https://github.com/googlemaps/google-maps-services-java/commit/96a6a561cbae878404fac4c4b6e3413134632c34) +- Add user_ratings_total response field to PlaceSearchResult class [`a8762d8`](https://github.com/googlemaps/google-maps-services-java/commit/a8762d8474fd516a08f84444a4a6ff5e19c2c1d3) +- OKHttp3 v4.0.1 -> v3.14.2 due to warnings [`d8f69b8`](https://github.com/googlemaps/google-maps-services-java/commit/d8f69b89c91dc498fabfaf259085dfae30d92437) + +## [v0.9.3](https://github.com/googlemaps/google-maps-services-java/compare/v0.9.2...v0.9.3) - 2019-03-20 + +### Merged + +- Version uplift [`#555`](https://github.com/googlemaps/google-maps-services-java/pull/555) +- Add new address and address component types [`#554`](https://github.com/googlemaps/google-maps-services-java/pull/554) + +### Commits + +- Releasing version 0.9.3 [`bc26e75`](https://github.com/googlemaps/google-maps-services-java/commit/bc26e75c6232ca3d69f17c2a09e1543d9a23e538) +- Opening up v0.9.3 for development [`027b37d`](https://github.com/googlemaps/google-maps-services-java/commit/027b37d3fcbec30ea8995cf3f5f6704eb9bbb6e9) +- Linking to v0.9.2 javadoc [`c5bc2e7`](https://github.com/googlemaps/google-maps-services-java/commit/c5bc2e74aa49addd8ca47bfe5756b728da656df9) + +## [v0.9.2](https://github.com/googlemaps/google-maps-services-java/compare/v0.9.1...v0.9.2) - 2019-02-28 + +### Merged + +- Reverting Gradle version. [`#550`](https://github.com/googlemaps/google-maps-services-java/pull/550) +- ./gradlew depUp [`#549`](https://github.com/googlemaps/google-maps-services-java/pull/549) +- Fixing Google Java Style [`#543`](https://github.com/googlemaps/google-maps-services-java/pull/543) +- adding a default no-arg constructor to EncodedPolyline [`#542`](https://github.com/googlemaps/google-maps-services-java/pull/542) +- TextSearch: add region parameter [`#538`](https://github.com/googlemaps/google-maps-services-java/pull/538) +- Upgrade to commons-lang3 [`#537`](https://github.com/googlemaps/google-maps-services-java/pull/537) +- ./gradlew depUp [`#536`](https://github.com/googlemaps/google-maps-services-java/pull/536) +- ./gradlew googleJavaFormat [`#530`](https://github.com/googlemaps/google-maps-services-java/pull/530) +- Added support to Place IDs [`#526`](https://github.com/googlemaps/google-maps-services-java/pull/526) +- ./gradlew dependencyUpdates [`#529`](https://github.com/googlemaps/google-maps-services-java/pull/529) +- Looks like case of DateTimeFormat is locale dependant. [`#528`](https://github.com/googlemaps/google-maps-services-java/pull/528) +- Formatting and making the tests pass [`#527`](https://github.com/googlemaps/google-maps-services-java/pull/527) +- Adds a method for setting the departure time as "now" [`#525`](https://github.com/googlemaps/google-maps-services-java/pull/525) +- Fix a small formatting mistake in the docs. [`#523`](https://github.com/googlemaps/google-maps-services-java/pull/523) +- Update README.md [`#522`](https://github.com/googlemaps/google-maps-services-java/pull/522) + +### Commits + +- Clean up [`6097ff3`](https://github.com/googlemaps/google-maps-services-java/commit/6097ff3f08c9676f84157269a24e208c5d72c89d) +- Added a method to make waypoints from Place IDs [`d2d05a7`](https://github.com/googlemaps/google-maps-services-java/commit/d2d05a7cf124bec97e09a6cd0004d08f95004b25) +- updating google java format to 0.8-SNAPSHOT [`de357b7`](https://github.com/googlemaps/google-maps-services-java/commit/de357b77feaa536b287cfbe8a14b13d87825910d) + +## [v0.9.1](https://github.com/googlemaps/google-maps-services-java/compare/v0.9.0...v0.9.1) - 2018-11-28 + +### Merged + +- Force Jacoco version 0.8.2 for OpenJDK 11 compatibility [`#517`](https://github.com/googlemaps/google-maps-services-java/pull/517) +- Fix Google Error Prone warnings [`#513`](https://github.com/googlemaps/google-maps-services-java/pull/513) +- DirectionsLeg, TransitDetails: use ZonedDateTime instead of LocalDateTime [`#516`](https://github.com/googlemaps/google-maps-services-java/pull/516) +- Versions update [`#515`](https://github.com/googlemaps/google-maps-services-java/pull/515) +- Add StaticMapsRequest.path(EncodedPolyline) [`#511`](https://github.com/googlemaps/google-maps-services-java/pull/511) +- Added Maps Static API in README.md [`#507`](https://github.com/googlemaps/google-maps-services-java/pull/507) +- Fixed typo in field [`#506`](https://github.com/googlemaps/google-maps-services-java/pull/506) + +### Commits + +- Add missing @Override annotations [`7c7a72c`](https://github.com/googlemaps/google-maps-services-java/commit/7c7a72c8f70e2967346eed971d9fd7bda237f1d4) +- Avoid implicit use of default charset [`88204f6`](https://github.com/googlemaps/google-maps-services-java/commit/88204f6406c355b6da2596b63efcf7bbd472391d) +- Updates and making tests pass [`c9f89bc`](https://github.com/googlemaps/google-maps-services-java/commit/c9f89bc525923b334f8a5a6eac428e7dce0775fc) + +## [v0.9.0](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.11...v0.9.0) - 2018-09-24 + +### Merged + +- Version 0.9 [`#503`](https://github.com/googlemaps/google-maps-services-java/pull/503) +- Java 1.8 is now the minimum supported version [`#502`](https://github.com/googlemaps/google-maps-services-java/pull/502) +- Dependencies update, reformat, and fixup [`#501`](https://github.com/googlemaps/google-maps-services-java/pull/501) +- Migrate to java.time [`#421`](https://github.com/googlemaps/google-maps-services-java/pull/421) +- SessionToken is now serializable [`#495`](https://github.com/googlemaps/google-maps-services-java/pull/495) + +### Commits + +- Java 1.8 is the new minimum [`ab013ba`](https://github.com/googlemaps/google-maps-services-java/commit/ab013bac3a4871329f77ca67a37a6d2ce3d2c0e6) +- Tidyup [`839fbd1`](https://github.com/googlemaps/google-maps-services-java/commit/839fbd1c8cfdac75ded5dca678f7ab452d4d5e9a) +- Opening up v0.2.12 development [`5809326`](https://github.com/googlemaps/google-maps-services-java/commit/5809326ee8da2dc60dce9a3116eea71cd59516cd) + +## [v0.2.11](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.10...v0.2.11) - 2018-08-31 + +### Merged + +- Updated dependencies [`#494`](https://github.com/googlemaps/google-maps-services-java/pull/494) +- Enabling setting SessionToken for placeDetails [`#493`](https://github.com/googlemaps/google-maps-services-java/pull/493) +- Opening up v0.2.11 for development [`#490`](https://github.com/googlemaps/google-maps-services-java/pull/490) + +### Commits + +- Release v0.2.11 [`a22237e`](https://github.com/googlemaps/google-maps-services-java/commit/a22237e9fb0c39d3b8e9e7f4c308788a4c8d776c) +- Update README.md [`4fb6af0`](https://github.com/googlemaps/google-maps-services-java/commit/4fb6af021a2019688ed057f1e87adafd36799354) +- Updating link to v0.2.10 Javadoc [`6894d11`](https://github.com/googlemaps/google-maps-services-java/commit/6894d11c2d9f8cb7c19d51b2b5b4925ac395bc2e) + +## [v0.2.10](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.9...v0.2.10) - 2018-08-15 + +### Merged + +- Releasing version 0.2.10 [`#489`](https://github.com/googlemaps/google-maps-services-java/pull/489) +- Updating dependencies [`#486`](https://github.com/googlemaps/google-maps-services-java/pull/486) +- Cleaning up [`#485`](https://github.com/googlemaps/google-maps-services-java/pull/485) +- ./gradlew googleJavaFormat [`#484`](https://github.com/googlemaps/google-maps-services-java/pull/484) +- Replace "baseUrlForTesting()" with usage-neutral "baseUrlOverride()" [`#471`](https://github.com/googlemaps/google-maps-services-java/pull/471) +- Tidying up [`#483`](https://github.com/googlemaps/google-maps-services-java/pull/483) +- Fix for #478 [`#482`](https://github.com/googlemaps/google-maps-services-java/pull/482) +- PlaceDetails: add adr_address support [`#480`](https://github.com/googlemaps/google-maps-services-java/pull/480) +- Remove Radar-related test resource files [`#481`](https://github.com/googlemaps/google-maps-services-java/pull/481) +- Remove Radar Search support [`#469`](https://github.com/googlemaps/google-maps-services-java/pull/469) +- ./gradlew googleJavaFormat [`#476`](https://github.com/googlemaps/google-maps-services-java/pull/476) +- Add non-stopover waypoint support [`#468`](https://github.com/googlemaps/google-maps-services-java/pull/468) +- Change *RequestHandler Builder signatures to do chainable calls [`#470`](https://github.com/googlemaps/google-maps-services-java/pull/470) +- Supply -html4 to javadoc with JDK 10 and later [`#472`](https://github.com/googlemaps/google-maps-services-java/pull/472) +- Update gradle dependency keywords [`#474`](https://github.com/googlemaps/google-maps-services-java/pull/474) +- Fix an NPE in DirectionsStep.toString() [`#475`](https://github.com/googlemaps/google-maps-services-java/pull/475) +- Opening up v0.2.10 for development [`#466`](https://github.com/googlemaps/google-maps-services-java/pull/466) + +### Commits + +- Remove support for Radar Search, which is now past EOL as of June 30, 2018 [`e814a89`](https://github.com/googlemaps/google-maps-services-java/commit/e814a89befde30059c1b7b058ba3bc7ff2771b1f) +- Add non-stopover Waypoint support. [`e42752f`](https://github.com/googlemaps/google-maps-services-java/commit/e42752f7af776ae33b74640975535d7abce52850) +- Add regression test for DirectionsResult.toString() [`7c60f67`](https://github.com/googlemaps/google-maps-services-java/commit/7c60f67445f8465575945525e89dc124a0743119) + +## [v0.2.9](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.8...v0.2.9) - 2018-07-06 + +### Merged + +- Releasing v0.2.9 [`#465`](https://github.com/googlemaps/google-maps-services-java/pull/465) +- Add okHttpClientBuilder() for customizing the OkHttpClient [`#464`](https://github.com/googlemaps/google-maps-services-java/pull/464) +- Add "continent" AddressType and AddressComponentType [`#463`](https://github.com/googlemaps/google-maps-services-java/pull/463) +- Add value-based toString()s for model objects [`#452`](https://github.com/googlemaps/google-maps-services-java/pull/452) +- Add Plus Code support to GeocodingResult and PlaceDetails [`#459`](https://github.com/googlemaps/google-maps-services-java/pull/459) +- Tidying up tests [`#461`](https://github.com/googlemaps/google-maps-services-java/pull/461) +- Reverting SLF4J version bump. [`#460`](https://github.com/googlemaps/google-maps-services-java/pull/460) +- Add note on GeoApiContext reuse in Javadoc [`#456`](https://github.com/googlemaps/google-maps-services-java/pull/456) +- Have OkHttpRequestHandler evict connectionPool on shutdown [`#455`](https://github.com/googlemaps/google-maps-services-java/pull/455) +- Deprecate NearbySearchRequest.type(PlaceType...) [`#454`](https://github.com/googlemaps/google-maps-services-java/pull/454) +- PlaceDetails: make types an AddressType[] instead of String[] [`#453`](https://github.com/googlemaps/google-maps-services-java/pull/453) +- Adjust link in README so Markdown inspectors do not complain [`#451`](https://github.com/googlemaps/google-maps-services-java/pull/451) +- Improve EnumsTest unit test for AddressType and AddressComponentType [`#444`](https://github.com/googlemaps/google-maps-services-java/pull/444) +- Bringing dependencies up to date. [`#449`](https://github.com/googlemaps/google-maps-services-java/pull/449) +- ./gradlew googleJavaFormat [`#448`](https://github.com/googlemaps/google-maps-services-java/pull/448) +- Replace bogus @url Javadoc tag with HTML link [`#447`](https://github.com/googlemaps/google-maps-services-java/pull/447) +- Minor code hygiene suggestions [`#446`](https://github.com/googlemaps/google-maps-services-java/pull/446) +- Add Javadoc for DirectionsApiRequest static methods [`#443`](https://github.com/googlemaps/google-maps-services-java/pull/443) +- LocalTestServerContext: use "expected[s]" as assertion parameter name [`#445`](https://github.com/googlemaps/google-maps-services-java/pull/445) +- Add more AddressTypes and AddressComponentTypes [`#437`](https://github.com/googlemaps/google-maps-services-java/pull/437) +- New AddressComponentTypes: general_contractor, food, store, etc... [`#436`](https://github.com/googlemaps/google-maps-services-java/pull/436) +- Add missing fields to PlaceDetails.Review. [`#432`](https://github.com/googlemaps/google-maps-services-java/pull/432) + +### Fixed + +- Reverting SLF4J version bump. [`#457`](https://github.com/googlemaps/google-maps-services-java/issues/457) + +### Commits + +- Merge upstream master [`eb72951`](https://github.com/googlemaps/google-maps-services-java/commit/eb72951102dc178ede794048459f17834219e71e) +- EnumsTest: improve source code and results readability [`9cb1014`](https://github.com/googlemaps/google-maps-services-java/commit/9cb1014c0b706a60a6f94458176774a189fb53c0) +- EnumsTest: add missing entries for AddressType [`d32d598`](https://github.com/googlemaps/google-maps-services-java/commit/d32d598146cec3034e59966febb40f3dad0d6da9) + +## [v0.2.8](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.7...v0.2.8) - 2018-06-25 + +### Merged + +- Find place from text, and place details field masks. [`#424`](https://github.com/googlemaps/google-maps-services-java/pull/424) +- Add new types to AddressType enum. [`#428`](https://github.com/googlemaps/google-maps-services-java/pull/428) +- ./gradlew googleJavaFormat [`#423`](https://github.com/googlemaps/google-maps-services-java/pull/423) +- Add support for scale parameter on custom icons to be able to use hi-DPI icons when map scale > 1 [`#419`](https://github.com/googlemaps/google-maps-services-java/pull/419) + +### Commits + +- Find place by text, and place details field masks. [`acf74b4`](https://github.com/googlemaps/google-maps-services-java/commit/acf74b404ed81dd993dff1ef5b80be1673d86519) +- Adding LocationBias parameter [`bca82fe`](https://github.com/googlemaps/google-maps-services-java/commit/bca82fe5a8eb544fef629b680df6df1d526c1af8) +- Field masks differ between place details and find place from text. [`3620c27`](https://github.com/googlemaps/google-maps-services-java/commit/3620c27f7c5b9a829106c585abda79c652632aa2) + +## [v0.2.7](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.6...v0.2.7) - 2018-04-10 + +### Merged + +- Releasing version 0.2.7 [`#415`](https://github.com/googlemaps/google-maps-services-java/pull/415) +- README update, and googleJavaFormat fix [`#414`](https://github.com/googlemaps/google-maps-services-java/pull/414) +- Adding Static Maps API [`#413`](https://github.com/googlemaps/google-maps-services-java/pull/413) +- Fix for #360 [`#410`](https://github.com/googlemaps/google-maps-services-java/pull/410) +- close response on retry [`#409`](https://github.com/googlemaps/google-maps-services-java/pull/409) +- gradlew googleJavaFormat [`#406`](https://github.com/googlemaps/google-maps-services-java/pull/406) +- Added missing enums fields [`#405`](https://github.com/googlemaps/google-maps-services-java/pull/405) +- added museum to address type [`#404`](https://github.com/googlemaps/google-maps-services-java/pull/404) + +### Commits + +- Filling in StaticMapRequest, along with an additional AddressType [`2486d00`](https://github.com/googlemaps/google-maps-services-java/commit/2486d003a18a183595d3c6ceb2e509c53a4899d4) +- Initial Static Maps implementation [`593b194`](https://github.com/googlemaps/google-maps-services-java/commit/593b1943151441fa070339e94527eb6e4d3c667c) +- Adding tests [`98ad5b3`](https://github.com/googlemaps/google-maps-services-java/commit/98ad5b3003a0b8bf9dfd966903a8015cfa266974) + +## [v0.2.6](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.5...v0.2.6) - 2018-01-09 + +### Merged + +- Releasing version 0.2.6 [`#402`](https://github.com/googlemaps/google-maps-services-java/pull/402) +- Increase default QPS from 10 to 50 [`#401`](https://github.com/googlemaps/google-maps-services-java/pull/401) +- Set ComponentFilter's Constructor Public [`#397`](https://github.com/googlemaps/google-maps-services-java/pull/397) +- Revert "Revert "Made the constructor public"" [`#395`](https://github.com/googlemaps/google-maps-services-java/pull/395) +- Revert "Made the constructor public" [`#394`](https://github.com/googlemaps/google-maps-services-java/pull/394) +- Made the constructor public [`#393`](https://github.com/googlemaps/google-maps-services-java/pull/393) +- Add java.io.Serializable for inner classes, as it was only added in t… [`#389`](https://github.com/googlemaps/google-maps-services-java/pull/389) + +### Commits + +- Add java.io.Serializable for inner classes, as it was only added in top-level ones. [`887f288`](https://github.com/googlemaps/google-maps-services-java/commit/887f2888b0d0c7e82436e4afcf79f5bcbbc85e5c) +- Made ComponentFilter's Class Constructor Public [`eac7f77`](https://github.com/googlemaps/google-maps-services-java/commit/eac7f77100f3d46d83637bdc7a23720a1fa99b67) +- Made ComponentFilter's Construtor Private [`ca28a78`](https://github.com/googlemaps/google-maps-services-java/commit/ca28a78dd76406bf957a33d5535a9883f7aba900) + +## [v0.2.5](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.4...v0.2.5) - 2017-11-15 + +### Merged + +- Version 0.2.5 release [`#387`](https://github.com/googlemaps/google-maps-services-java/pull/387) +- ./gradlew googleJavaFormat [`#386`](https://github.com/googlemaps/google-maps-services-java/pull/386) +- Adding strictbounds to Places Autocomplete [`#385`](https://github.com/googlemaps/google-maps-services-java/pull/385) +- AddressType: add night_club [`#384`](https://github.com/googlemaps/google-maps-services-java/pull/384) +- AddressType: add travel_agency [`#382`](https://github.com/googlemaps/google-maps-services-java/pull/382) +- AddressType: add shoe_store [`#381`](https://github.com/googlemaps/google-maps-services-java/pull/381) +- AddressType: add more types observed by steveetl [`#380`](https://github.com/googlemaps/google-maps-services-java/pull/380) +- added java.io.Serializable to the response model #366 [`#367`](https://github.com/googlemaps/google-maps-services-java/pull/367) +- AddressType: add beauty care types [`#378`](https://github.com/googlemaps/google-maps-services-java/pull/378) +- AddressType: add STADIUM [`#379`](https://github.com/googlemaps/google-maps-services-java/pull/379) +- AddressType: add CASINO and PARKING [`#377`](https://github.com/googlemaps/google-maps-services-java/pull/377) +- Opening up v0.2.5 development [`#373`](https://github.com/googlemaps/google-maps-services-java/pull/373) +- Update Javadoc link for v0.2.4 [`#374`](https://github.com/googlemaps/google-maps-services-java/pull/374) +- Suppress unchecked warning in GeoApiContextTest [`#372`](https://github.com/googlemaps/google-maps-services-java/pull/372) +- Adding required import, and googleJavaFormat [`#370`](https://github.com/googlemaps/google-maps-services-java/pull/370) +- ResponseBody in OkHttpPendingResult was not being closed. [`#368`](https://github.com/googlemaps/google-maps-services-java/pull/368) +- added a shutdown method to GeoApiContext which stops RateLimitExecutorDelayThread #261 [`#365`](https://github.com/googlemaps/google-maps-services-java/pull/365) + +### Commits + +- added java.io.Serializable to the response model [`7bce9dd`](https://github.com/googlemaps/google-maps-services-java/commit/7bce9dd3c129d48e21572f41de157abf407ae171) +- remove comment on serialVersionUID [`8157552`](https://github.com/googlemaps/google-maps-services-java/commit/815755229ba54053f425a335abdb436307b1324f) +- ./gradlew googleJavaFormat [`d36f5ff`](https://github.com/googlemaps/google-maps-services-java/commit/d36f5ff5e94ea33e343848a8bc6fcd1c73cdcbb5) + +## [v0.2.4](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.3...v0.2.4) - 2017-10-06 + +### Merged + +- Rolling 0.2.4 release [`#358`](https://github.com/googlemaps/google-maps-services-java/pull/358) +- Tidyup warnings [`#357`](https://github.com/googlemaps/google-maps-services-java/pull/357) +- Upgrade Gradle [`#356`](https://github.com/googlemaps/google-maps-services-java/pull/356) +- ./gradlew googleJavaFormat [`#354`](https://github.com/googlemaps/google-maps-services-java/pull/354) +- Replace Guava dep with local RateLimiter implementation copy-paste [`#351`](https://github.com/googlemaps/google-maps-services-java/pull/351) +- .gitignore: add /out/ to support IntelliJ IDEA [`#352`](https://github.com/googlemaps/google-maps-services-java/pull/352) + +### Commits + +- Copy Guava's RateLimiter implementation locally and remove Guava dependency [`9622a59`](https://github.com/googlemaps/google-maps-services-java/commit/9622a59b0c919fa6334d0966952760d16ada532f) +- Opening up v0.2.4 development [`7785c57`](https://github.com/googlemaps/google-maps-services-java/commit/7785c57c1563d14ff6afb555a0efaed8173785ec) +- Updating Javadoc link for v0.2.3 [`8ca6e15`](https://github.com/googlemaps/google-maps-services-java/commit/8ca6e1550537f85f574ce7eaff7dcb635b685939) + +## [v0.2.3](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.2...v0.2.3) - 2017-09-13 + +### Merged + +- Releasing version 0.2.3 [`#346`](https://github.com/googlemaps/google-maps-services-java/pull/346) +- Cleaning up README.md's markdown [`#345`](https://github.com/googlemaps/google-maps-services-java/pull/345) +- ./gradlew googleJavaFormat [`#343`](https://github.com/googlemaps/google-maps-services-java/pull/343) +- adding proxy authentication in GeoApiContext [`#337`](https://github.com/googlemaps/google-maps-services-java/pull/337) +- Add note about paging delay [`#339`](https://github.com/googlemaps/google-maps-services-java/pull/339) +- Documenting GAE usage [`#335`](https://github.com/googlemaps/google-maps-services-java/pull/335) +- Opening up development on v0.2.3 [`#333`](https://github.com/googlemaps/google-maps-services-java/pull/333) + +### Commits + +- adding proxy authentication in GeoApiContext - googleJavaFormat [`bd5303d`](https://github.com/googlemaps/google-maps-services-java/commit/bd5303d9f8ee1d6694030848bbd45aba77b89e00) +- Documenting new GAE usage [`ddb5363`](https://github.com/googlemaps/google-maps-services-java/commit/ddb536333c515b6d9b909e60e1334587560c5cf6) +- Javadoc for v0.2.2 [`69fd291`](https://github.com/googlemaps/google-maps-services-java/commit/69fd2913acaec29ab8039c97e569777a8828ff6c) + +## [v0.2.2](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.1...v0.2.2) - 2017-08-15 + +### Merged + +- Releasing v0.2.2 [`#332`](https://github.com/googlemaps/google-maps-services-java/pull/332) +- Adding handling for AutocompletePrediction's structured formatting. [`#330`](https://github.com/googlemaps/google-maps-services-java/pull/330) +- guava v22 and v23 reintroduced java 7 support [`#331`](https://github.com/googlemaps/google-maps-services-java/pull/331) +- LatLng serialisation constructor. [`#328`](https://github.com/googlemaps/google-maps-services-java/pull/328) +- ./gradlew googleJavaFormat [`#327`](https://github.com/googlemaps/google-maps-services-java/pull/327) +- Javadoc: Rephrase to match Google Java Style Guide (classes P) [`#321`](https://github.com/googlemaps/google-maps-services-java/pull/321) +- Javadoc: Rephrase to match Google Java Style Guide (classes G) [`#319`](https://github.com/googlemaps/google-maps-services-java/pull/319) +- Javadoc: Rephrase to match Google Java Style Guide (classes Q-S) [`#322`](https://github.com/googlemaps/google-maps-services-java/pull/322) +- Javadoc: Rephrase to match Google Java Style Guide (classes T) [`#323`](https://github.com/googlemaps/google-maps-services-java/pull/323) +- Javadoc: Rephrase to match Google Java Style Guide (classes U-Z) [`#324`](https://github.com/googlemaps/google-maps-services-java/pull/324) +- Javadoc: Rephrase to match Google Java Style Guide (classes H-O) [`#320`](https://github.com/googlemaps/google-maps-services-java/pull/320) +- Javadoc: Use "latitude/longitude" instead of "latitude,longitude" everywhere [`#325`](https://github.com/googlemaps/google-maps-services-java/pull/325) +- Use Oxford commas in Javadoc [`#317`](https://github.com/googlemaps/google-maps-services-java/pull/317) +- Javadoc: Rephrase to match Google Java Style Guide (classes D-F) [`#318`](https://github.com/googlemaps/google-maps-services-java/pull/318) +- Dropping truncated JavaDoc [`#316`](https://github.com/googlemaps/google-maps-services-java/pull/316) +- Javadoc: Rephrase to match Google Java Style Guide (classes A-C) [`#315`](https://github.com/googlemaps/google-maps-services-java/pull/315) +- Javadoc: Update links and fix typos [`#314`](https://github.com/googlemaps/google-maps-services-java/pull/314) +- README: Point "javadoc" link at the new v0.2.1 javadocs [`#311`](https://github.com/googlemaps/google-maps-services-java/pull/311) +- Update build.gradle - Changed guava to v20 [`#308`](https://github.com/googlemaps/google-maps-services-java/pull/308) + +### Commits + +- Cleaning up Javadoc, and fixing class name typo. (Whups) [`7459bd2`](https://github.com/googlemaps/google-maps-services-java/commit/7459bd26441a60e3943f8e7ae8ddd74241afda5c) +- Use Oxford commas everywhere in Javadoc [`f12ce8a`](https://github.com/googlemaps/google-maps-services-java/commit/f12ce8a4057bcef0e8beb2653ccae712caa05277) +- Adding in unicode characters for MatchedSubstring offset and length. [`432c133`](https://github.com/googlemaps/google-maps-services-java/commit/432c1332c86a6f5a26a96686417e24c4be205032) + +## [v0.2.1](https://github.com/googlemaps/google-maps-services-java/compare/v0.2.0...v0.2.1) - 2017-08-02 + +### Merged + +- ./gradlew googleJavaFormat [`#305`](https://github.com/googlemaps/google-maps-services-java/pull/305) +- Update GeoApiContext.java - Exception using GAE [`#304`](https://github.com/googlemaps/google-maps-services-java/pull/304) +- Upgrade mockwebserver to 3.8.1 from OkHttp distribution [`#301`](https://github.com/googlemaps/google-maps-services-java/pull/301) +- Suppress deprecation warnings for radarSearchQuery [`#302`](https://github.com/googlemaps/google-maps-services-java/pull/302) +- README: Make central repo links do searches for this lib [`#300`](https://github.com/googlemaps/google-maps-services-java/pull/300) +- Add .nb-gradle files to .gitignore [`#299`](https://github.com/googlemaps/google-maps-services-java/pull/299) +- Spell 'adapter' consistently [`#298`](https://github.com/googlemaps/google-maps-services-java/pull/298) +- Update documentation publishing instructions [`#296`](https://github.com/googlemaps/google-maps-services-java/pull/296) +- GeocodingApiTest: fix missing quote in javadoc [`#294`](https://github.com/googlemaps/google-maps-services-java/pull/294) + +### Commits + +- Upgrade mockwebserver to 3.8.1 [`7d2282b`](https://github.com/googlemaps/google-maps-services-java/commit/7d2282be775badcabf6bd4af045ac9a6e090cfea) +- spell 'adapter' consistently [`5069f8a`](https://github.com/googlemaps/google-maps-services-java/commit/5069f8a8cba3f268731fd6e40bfd74c1f2abda03) +- README: Make central repo links do searches for this lib instead of going to main home page. [`d2e3245`](https://github.com/googlemaps/google-maps-services-java/commit/d2e3245992bde7b41b35a42977bb1270aa9fa76e) + +## [v0.2.0](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.22...v0.2.0) - 2017-07-25 + +### Merged + +- Landing Okhttp3 [`#293`](https://github.com/googlemaps/google-maps-services-java/pull/293) + +### Commits + +- Releasing Version 0.2.0 [`af0ca26`](https://github.com/googlemaps/google-maps-services-java/commit/af0ca2621ff8157a2be9f63988aa102f9613d6cd) +- Re-opening v0.2 in preparation for landing OkHttp3. [`b1e495e`](https://github.com/googlemaps/google-maps-services-java/commit/b1e495e376126138814da82fa771ea5b602cf981) + +## [v0.1.22](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.21...v0.1.22) - 2017-07-24 + +### Merged + +- Applying `google-java-format` to the codebase. [`#291`](https://github.com/googlemaps/google-maps-services-java/pull/291) +- Fix issue where Runnable runs on the incorrect thread [`#290`](https://github.com/googlemaps/google-maps-services-java/pull/290) + +### Fixed + +- A quick respin to fix https://github.com/googlemaps/google-maps-services-java/issues/292 [`#292`](https://github.com/googlemaps/google-maps-services-java/issues/292) + +### Commits + +- Reformat the static imports correctly [`94d8c8b`](https://github.com/googlemaps/google-maps-services-java/commit/94d8c8b1bf9a7963190ef90e8a80f2f338727f15) +- Making google-java-format easier to use. [`c9c006c`](https://github.com/googlemaps/google-maps-services-java/commit/c9c006cf226acb76ce75264a5b7701aebe9faa42) +- Runnable runs on the delegate ExecutorService [`eb81d7e`](https://github.com/googlemaps/google-maps-services-java/commit/eb81d7e318d41ee021f8addce7371904fd803dcd) + +## [v0.1.21](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.20...v0.1.21) - 2017-07-03 + +### Merged + +- Marking Radar Search as deprecated [`#284`](https://github.com/googlemaps/google-maps-services-java/pull/284) +- Converting tests to local server based. [`#282`](https://github.com/googlemaps/google-maps-services-java/pull/282) +- Converting Integration tests to Local Server tests [`#278`](https://github.com/googlemaps/google-maps-services-java/pull/278) +- Fixing license declaration [`#277`](https://github.com/googlemaps/google-maps-services-java/pull/277) +- Making PlaceDetailsRequest#Response public [`#276`](https://github.com/googlemaps/google-maps-services-java/pull/276) +- Adding undocumented address types. [`#275`](https://github.com/googlemaps/google-maps-services-java/pull/275) +- Testing Kita Ward. [`#274`](https://github.com/googlemaps/google-maps-services-java/pull/274) +- Test fix [`#273`](https://github.com/googlemaps/google-maps-services-java/pull/273) +- Replacing hand rolled rate limiter with Gauva's Rate Limiter. [`#272`](https://github.com/googlemaps/google-maps-services-java/pull/272) +- Adding example configuration for SLF4J to the README [`#271`](https://github.com/googlemaps/google-maps-services-java/pull/271) +- Making tests pass [`#269`](https://github.com/googlemaps/google-maps-services-java/pull/269) +- Add light_rail_station enum type [`#268`](https://github.com/googlemaps/google-maps-services-java/pull/268) +- Fix for issues 218,170 - aded instance creator for EncodedPolyline to use with Gson [`#260`](https://github.com/googlemaps/google-maps-services-java/pull/260) + +### Commits + +- Converting Places API Integration tests to local server tests. [`72edd9c`](https://github.com/googlemaps/google-maps-services-java/commit/72edd9c12c6726ff755f9cd1c58cd7689dacc125) +- Moving large string blobs to resource files [`21d2be5`](https://github.com/googlemaps/google-maps-services-java/commit/21d2be5eb9bf8a40b60079708ef78310e694090a) +- Converted last API surfaces to localtests. [`9379324`](https://github.com/googlemaps/google-maps-services-java/commit/9379324ef7b95a38f089b63e26e901b5c70f9aaf) + +## [v0.1.20](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.19...v0.1.20) - 2017-04-13 + +### Merged + +- Solving performance problems in parallel processing [`#259`](https://github.com/googlemaps/google-maps-services-java/pull/259) +- Fixing concurrency problem [`#255`](https://github.com/googlemaps/google-maps-services-java/pull/255) +- Fixing up ordering of signature calculation. [`#253`](https://github.com/googlemaps/google-maps-services-java/pull/253) + +### Commits + +- Created test for parallel signatures and cloned mac for each signature to avoid concurrency problems [`d9f55f9`](https://github.com/googlemaps/google-maps-services-java/commit/d9f55f91cac641407665a18f012ccb6a88db4445) +- Set maxQps on OKHttp dispatcher to not be limited to the default configuration [`fe422e9`](https://github.com/googlemaps/google-maps-services-java/commit/fe422e943eebc76e7592fa20e89b8f94f4d1fe48) +- Also catching exceptions during parallel signature to be more assertive [`4c41b2c`](https://github.com/googlemaps/google-maps-services-java/commit/4c41b2cbc8ab68d3e603d069a4e7ab5687496936) + +## [v0.1.19](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.18...v0.1.19) - 2017-03-28 + +### Merged + +- Version 0.1.19 release [`#252`](https://github.com/googlemaps/google-maps-services-java/pull/252) +- Fix for https://github.com/googlemaps/google-maps-services-java/issues/248 [`#251`](https://github.com/googlemaps/google-maps-services-java/pull/251) + +### Commits + +- Opening up development for v0.1.19 [`e2b474d`](https://github.com/googlemaps/google-maps-services-java/commit/e2b474d6a5191167251b83e77b1405651e775d23) +- Updating Javadoc link [`f9ade4e`](https://github.com/googlemaps/google-maps-services-java/commit/f9ade4ee593578c5d41903cf7c7a78f6f942e44f) + +## [v0.1.18](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.17...v0.1.18) - 2017-03-24 + +### Merged + +- Creating version 0.1.18 [`#247`](https://github.com/googlemaps/google-maps-services-java/pull/247) +- Updating gradle version [`#246`](https://github.com/googlemaps/google-maps-services-java/pull/246) +- Updating versions and making tests pass [`#245`](https://github.com/googlemaps/google-maps-services-java/pull/245) +- Make PendingResult#await throw specific Exception [`#238`](https://github.com/googlemaps/google-maps-services-java/pull/238) +- Linkify travis badge and mavencentral badge in README [`#239`](https://github.com/googlemaps/google-maps-services-java/pull/239) +- Update NearbySearchRequest.java [`#243`](https://github.com/googlemaps/google-maps-services-java/pull/243) +- Update build.gradle to require 1.7 [`#236`](https://github.com/googlemaps/google-maps-services-java/pull/236) +- Update TextSearchRequest.java [`#221`](https://github.com/googlemaps/google-maps-services-java/pull/221) +- Fixing tests [`#223`](https://github.com/googlemaps/google-maps-services-java/pull/223) +- Correct usage of GeoApiContext [`#222`](https://github.com/googlemaps/google-maps-services-java/pull/222) +- Support `nearestRoads` API call [`#217`](https://github.com/googlemaps/google-maps-services-java/pull/217) +- Add support for address type 'postal_code_prefix'. [`#215`](https://github.com/googlemaps/google-maps-services-java/pull/215) +- Tidying up headers and imports [`#213`](https://github.com/googlemaps/google-maps-services-java/pull/213) +- Save waypoints [`#204`](https://github.com/googlemaps/google-maps-services-java/pull/204) +- Fixing up broken merge [`#212`](https://github.com/googlemaps/google-maps-services-java/pull/212) +- Change Java Util Logging to SLF4J and add SLF4J simple logger for tests [`#186`](https://github.com/googlemaps/google-maps-services-java/pull/186) +- Update Javadoc reference [`#211`](https://github.com/googlemaps/google-maps-services-java/pull/211) +- Open up Version 0.1.18 for development [`#210`](https://github.com/googlemaps/google-maps-services-java/pull/210) + +### Commits + +- Add waypoint optimization tests [`01f6a29`](https://github.com/googlemaps/google-maps-services-java/commit/01f6a29fa3250c22fdf93d77f9c5c75648387873) +- Remove unused ExceptionResult [`fa50c98`](https://github.com/googlemaps/google-maps-services-java/commit/fa50c988c614cf3540a1e79b74736ac3f35ce9f0) +- Support nearestRoads method from Roads API [`0829c7e`](https://github.com/googlemaps/google-maps-services-java/commit/0829c7e6cbd128faf2457b63bcef27730a76a9ff) + +## [v0.1.17](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.16...v0.1.17) - 2016-11-30 + +### Merged + +- Version 0.1.17 [`#209`](https://github.com/googlemaps/google-maps-services-java/pull/209) +- Adding Custom Parameter handling [`#208`](https://github.com/googlemaps/google-maps-services-java/pull/208) +- Keep using ThreadPoolExecutor but increase number of threads [`#199`](https://github.com/googlemaps/google-maps-services-java/pull/199) +- Add waypoints method that takes in an array of LatLng [`#205`](https://github.com/googlemaps/google-maps-services-java/pull/205) +- Add Light Rail Station to AddressType [`#197`](https://github.com/googlemaps/google-maps-services-java/pull/197) +- Update documentation links that were outdated. [`#194`](https://github.com/googlemaps/google-maps-services-java/pull/194) +- Add support for multiple type for PlacesApi [`#192`](https://github.com/googlemaps/google-maps-services-java/pull/192) +- Allow specific exception types to be retried or not retried [`#189`](https://github.com/googlemaps/google-maps-services-java/pull/189) + +### Commits + +- Allow specific exception types to be retried or not retried. [`5f55a2b`](https://github.com/googlemaps/google-maps-services-java/commit/5f55a2b7b6a1b06390a779f56afe5a4d7546cfc4) +- Refactor reading test response file into a TestUtils. [`3f324a7`](https://github.com/googlemaps/google-maps-services-java/commit/3f324a744c65d27d7beee4e4ce6982611ec78342) +- Adding custom parameter pass through [`272734d`](https://github.com/googlemaps/google-maps-services-java/commit/272734d793df6708b1aafa7db4b0b5441f109efd) + +## [v0.1.16](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.15...v0.1.16) - 2016-10-06 + +### Merged + +- Checking if OVER_QUERY_LIMIT is caused because Daily limit. In that c… [`#188`](https://github.com/googlemaps/google-maps-services-java/pull/188) +- Allow retries to be limited by number of retries, not time. [`#185`](https://github.com/googlemaps/google-maps-services-java/pull/185) +- Adding support for the Geolocation API call. [`#164`](https://github.com/googlemaps/google-maps-services-java/pull/164) +- Adding support for maneuver in the directions api. [`#50`](https://github.com/googlemaps/google-maps-services-java/pull/50) +- Fix poor resource management code (fixes #179) [`#181`](https://github.com/googlemaps/google-maps-services-java/pull/181) +- Add support for the 'permanently_closed' attribute in Place responses. [`#177`](https://github.com/googlemaps/google-maps-services-java/pull/177) +- Set RateLimitExecutor thread name. [`#174`](https://github.com/googlemaps/google-maps-services-java/pull/174) +- Cleaning up broken tests [`#167`](https://github.com/googlemaps/google-maps-services-java/pull/167) +- Adds client-id example to README.md [`#166`](https://github.com/googlemaps/google-maps-services-java/pull/166) + +### Fixed + +- Merge pull request #181 from ben-manes/master [`#179`](https://github.com/googlemaps/google-maps-services-java/issues/179) +- Fix poor resource management code (fixes #179) [`#179`](https://github.com/googlemaps/google-maps-services-java/issues/179) + +### Commits + +- Issue Fixes [`5281ed9`](https://github.com/googlemaps/google-maps-services-java/commit/5281ed9894a15c38fdddd9249cc8ca179d54763b) +- Pull Request Comments low hanging fruit. [`b2c718a`](https://github.com/googlemaps/google-maps-services-java/commit/b2c718a94828dfbb262105f46f8025efdb669902) +- ApiConfig now controls GET vs POST. [`f98913a`](https://github.com/googlemaps/google-maps-services-java/commit/f98913ab06475477298b3dfafea882883427fd5f) + +## [v0.1.15](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.12...v0.1.15) - 2016-06-07 + +### Merged + +- Version 0.1.15 release [`#159`](https://github.com/googlemaps/google-maps-services-java/pull/159) +- Break fixes [`#158`](https://github.com/googlemaps/google-maps-services-java/pull/158) +- App engine support [`#154`](https://github.com/googlemaps/google-maps-services-java/pull/154) +- Increasing the precision of LatLng's toUrlValue [`#153`](https://github.com/googlemaps/google-maps-services-java/pull/153) +- Adding canonical literals for AddressComponentType [`#152`](https://github.com/googlemaps/google-maps-services-java/pull/152) +- Upgrade OkHttp [`#144`](https://github.com/googlemaps/google-maps-services-java/pull/144) + +### Fixed + +- Upgrading OkHTTP to current. [`#143`](https://github.com/googlemaps/google-maps-services-java/issues/143) + +### Commits + +- Adding support for Google App Engine [`e59c5d3`](https://github.com/googlemaps/google-maps-services-java/commit/e59c5d3e8116aa2393c7ed4cc0d53ad3669066f2) +- Adding unit test for canonical literals [`65e6997`](https://github.com/googlemaps/google-maps-services-java/commit/65e69970f23b649b15530232c513fe35cea4db17) +- Adding copyright notices. [`7755784`](https://github.com/googlemaps/google-maps-services-java/commit/7755784e0072403dd2c6e28db3877843ac20b177) + +## [v0.1.12](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.11...v0.1.12) - 2016-03-24 + +### Merged + +- Dropping flaky tests. [`#142`](https://github.com/googlemaps/google-maps-services-java/pull/142) +- Fix places autocomplete types parameter and introduced correct type enum [`#140`](https://github.com/googlemaps/google-maps-services-java/pull/140) +- distance matrix request - add traffic_model to request and durationInTraffic to response [`#139`](https://github.com/googlemaps/google-maps-services-java/pull/139) +- Commenting out Flaky test [`#136`](https://github.com/googlemaps/google-maps-services-java/pull/136) +- Bug fixes and cleanups [`#135`](https://github.com/googlemaps/google-maps-services-java/pull/135) + +### Commits + +- Fix for https://github.com/googlemaps/google-maps-services-java/issues/75 [`99b8bd8`](https://github.com/googlemaps/google-maps-services-java/commit/99b8bd86f2f2cd639600447c05d2d9603ab58b76) +- Making Places API return AddressTypes instead of Strings. [`d567486`](https://github.com/googlemaps/google-maps-services-java/commit/d5674861aee3d96e0f7952f88afdc4feca94550e) +- Changing local_icon to localIcon, and reverting AddressType change. [`54b685e`](https://github.com/googlemaps/google-maps-services-java/commit/54b685ec3cd1cb71bd57765625b68c65934df861) + +## [v0.1.11](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.9...v0.1.11) - 2016-02-18 + +### Merged + +- Adding most of Places API, plus tidy ups. [`#133`](https://github.com/googlemaps/google-maps-services-java/pull/133) +- Fixing analytics. [`#130`](https://github.com/googlemaps/google-maps-services-java/pull/130) +- Making the fares tests pass. [`#129`](https://github.com/googlemaps/google-maps-services-java/pull/129) +- NPE check for route fares, and a better test for permanently closed. [`#128`](https://github.com/googlemaps/google-maps-services-java/pull/128) +- Adding tests: UTF8 return parsing and permanently closed [`#127`](https://github.com/googlemaps/google-maps-services-java/pull/127) +- Making Travis compile again. [`#125`](https://github.com/googlemaps/google-maps-services-java/pull/125) +- Tidyup tests + better javadoc. [`#124`](https://github.com/googlemaps/google-maps-services-java/pull/124) +- Introducing Geocoded Waypoints into Directions API result. [`#118`](https://github.com/googlemaps/google-maps-services-java/pull/118) +- Places API test fixes [`#115`](https://github.com/googlemaps/google-maps-services-java/pull/115) + +### Commits + +- Simplified Directions API to just one result type [`f1f73ba`](https://github.com/googlemaps/google-maps-services-java/commit/f1f73badbd38da3245c25fca8f50dfc32cca4912) +- Splitting the full result from just the routes. [`13e24c1`](https://github.com/googlemaps/google-maps-services-java/commit/13e24c1bfffc112c53fcd0e8d48f876a2d68854d) +- Introducing GeocodedWaypointStatus. [`b05e5ae`](https://github.com/googlemaps/google-maps-services-java/commit/b05e5aebbac63179d18569e76a79c896b9e91f40) + +## [v0.1.9](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.8...v0.1.9) - 2015-11-10 + +### Merged + +- Added traffic_model parameter to directions requests [`#112`](https://github.com/googlemaps/google-maps-services-java/pull/112) +- Adding Subway Station [`#111`](https://github.com/googlemaps/google-maps-services-java/pull/111) +- Places API release - version 0.1.8 [`#110`](https://github.com/googlemaps/google-maps-services-java/pull/110) + +### Commits + +- version bump [`dfa0a2f`](https://github.com/googlemaps/google-maps-services-java/commit/dfa0a2fa2ed9e1ef5974239dadedd20a14aa90a8) + +## [v0.1.8](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.7...v0.1.8) - 2015-10-14 + +### Merged + +- Fixing javadoc errors. [`#107`](https://github.com/googlemaps/google-maps-services-java/pull/107) +- Adding Places API [`#106`](https://github.com/googlemaps/google-maps-services-java/pull/106) +- Update README.md [`#103`](https://github.com/googlemaps/google-maps-services-java/pull/103) +- Making tests happy. [`#100`](https://github.com/googlemaps/google-maps-services-java/pull/100) +- Added channel parameter to request (issue #77) [`#97`](https://github.com/googlemaps/google-maps-services-java/pull/97) +- Multiple failed test fixes [`#89`](https://github.com/googlemaps/google-maps-services-java/pull/89) +- For issue #87 NumberFormatException when parsing SpeedLimit [`#88`](https://github.com/googlemaps/google-maps-services-java/pull/88) + +### Commits + +- Fix for issue #87 NumberFormatException when parsing SpeedLimit response for nonmetric countries. Changed SpeedLimit.speedLimit property from long to double. Added unit tests. [`17342b0`](https://github.com/googlemaps/google-maps-services-java/commit/17342b0b455da698ee3e2d2bddb1d2fbabbb9aa5) +- Code tidyup [`e469d56`](https://github.com/googlemaps/google-maps-services-java/commit/e469d5623eb1ebd50fe9a9105bc56677b77c2550) +- Enable using API Keys as systemProperties. [`7e030c4`](https://github.com/googlemaps/google-maps-services-java/commit/7e030c493295061ee2c92288b60b621d9aec3bcb) + +## [v0.1.7](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.6...v0.1.7) - 2015-05-05 + +### Merged + +- Added postal_code_prefix (Closes #48) [`#86`](https://github.com/googlemaps/google-maps-services-java/pull/86) +- Fixes vehicle type field name [`#81`](https://github.com/googlemaps/google-maps-services-java/pull/81) +- Added some returning but not documented AddressType. [`#78`](https://github.com/googlemaps/google-maps-services-java/pull/78) +- Updated build rules to execute M4W + keyed reqs together [`#80`](https://github.com/googlemaps/google-maps-services-java/pull/80) + +### Fixed + +- Merge pull request #86 from markmcd/prefix [`#48`](https://github.com/googlemaps/google-maps-services-java/issues/48) +- Added postal_code_prefix (Closes #48) [`#48`](https://github.com/googlemaps/google-maps-services-java/issues/48) + +### Commits + +- Bumped timeouts for Raods API tests [`57fe118`](https://github.com/googlemaps/google-maps-services-java/commit/57fe118ba46572349f5ac33063be15e332add23c) +- JavaDoc in README to 0.1.6 [`270be23`](https://github.com/googlemaps/google-maps-services-java/commit/270be23a25851763530b6faf18545da6db9d3043) +- Adding @NicolasPoirier to AUTH/CONTRIB [`835fd60`](https://github.com/googlemaps/google-maps-services-java/commit/835fd6092ed26a2730b5304da71fe0c6d739c767) + +## [v0.1.6](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.5...v0.1.6) - 2015-03-04 + +### Merged + +- Clarified support text [`#44`](https://github.com/googlemaps/google-maps-services-java/pull/44) +- Revert support level to Java 7 [`#66`](https://github.com/googlemaps/google-maps-services-java/pull/66) +- Add transit details [`#72`](https://github.com/googlemaps/google-maps-services-java/pull/72) +- Added place ID support to geocoding [`#73`](https://github.com/googlemaps/google-maps-services-java/pull/73) +- Fixed up polyline doc [`#74`](https://github.com/googlemaps/google-maps-services-java/pull/74) +- Fixed direction steps field name [`#71`](https://github.com/googlemaps/google-maps-services-java/pull/71) +- Adds Proxy support to GeoApiContext [`#67`](https://github.com/googlemaps/google-maps-services-java/pull/67) +- Fixed broken tests [`#64`](https://github.com/googlemaps/google-maps-services-java/pull/64) +- final on String in model [`#63`](https://github.com/googlemaps/google-maps-services-java/pull/63) +- :moneybag: Transit Fares in Directions & DistanceMatrix [`#60`](https://github.com/googlemaps/google-maps-services-java/pull/60) +- Fixed broken test caused by API data [`#61`](https://github.com/googlemaps/google-maps-services-java/pull/61) +- Added ApiConfig for more configurable API endpoints [`#58`](https://github.com/googlemaps/google-maps-services-java/pull/58) + +### Commits + +- :car: Added Roads API [`a69e13c`](https://github.com/googlemaps/google-maps-services-java/commit/a69e13c5ec38020a607c5b2562b84ee197c7d2ce) +- Optimized lookup() method, adding more types. [`b0b1301`](https://github.com/googlemaps/google-maps-services-java/commit/b0b1301e7664d6a395d4d44ac64b8ad075bcf170) +- Added transit fares to directions API [`efb08a9`](https://github.com/googlemaps/google-maps-services-java/commit/efb08a9838cc75e37fd2169f3ad7515c1c9a584d) + +## [v0.1.5](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.4...v0.1.5) - 2015-01-19 + +### Merged + +- :wrench: Updated GSON dep to working version [`#57`](https://github.com/googlemaps/google-maps-services-java/pull/57) +- Change req to Java 1.6 [`#56`](https://github.com/googlemaps/google-maps-services-java/pull/56) +- Response parsing and dependency on StandardCharsets class [`#46`](https://github.com/googlemaps/google-maps-services-java/pull/46) + +### Commits + +- dependency on java.nio removed [`07a3098`](https://github.com/googlemaps/google-maps-services-java/commit/07a30986894a2b5735181623265c1ce46d8621f9) +- Adding support for maneuver in the directions api. [`0f7117d`](https://github.com/googlemaps/google-maps-services-java/commit/0f7117d685056db5954c8ffb57529b2f6e3d34cc) +- a bit smaller buffer [`267eb3d`](https://github.com/googlemaps/google-maps-services-java/commit/267eb3d530524a79197df2fed2d8afd8a1eef2f4) + +## [v0.1.4](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.3...v0.1.4) - 2014-11-04 + +### Merged + +- Minor cleanups, refactor RateLimitExecutorService. [`#41`](https://github.com/googlemaps/google-maps-services-java/pull/41) +- Add @nutsiepully, github usernames to CONTRIBUTORS [`#43`](https://github.com/googlemaps/google-maps-services-java/pull/43) +- Porting code to be compatible with Java 1.6 [`#42`](https://github.com/googlemaps/google-maps-services-java/pull/42) +- Fixing .gitignore to remove iml and intellij idea files. [`#40`](https://github.com/googlemaps/google-maps-services-java/pull/40) +- Make DistanceMatrixElementStatus an enum. [`#39`](https://github.com/googlemaps/google-maps-services-java/pull/39) +- Add extra safety to unmarshaling to enums. [`#38`](https://github.com/googlemaps/google-maps-services-java/pull/38) + +### Commits + +- Incorporating formatting feedback. [`98785ca`](https://github.com/googlemaps/google-maps-services-java/commit/98785ca551959b8e77fdfec10ea8955fe3878011) +- :sparkle: clarified support text [`2c4a7cc`](https://github.com/googlemaps/google-maps-services-java/commit/2c4a7cc2c9829c51726260e925fe61d4128b97d8) +- javadoc -> 0.1.3 [`1d144d1`](https://github.com/googlemaps/google-maps-services-java/commit/1d144d1796a9c60dec37d379ca695fa2fa9e3d47) + +## [v0.1.3](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.2...v0.1.3) - 2014-10-03 + +### Merged + +- Added logging to the rate limiting test to aid debugging if it fails again [`#36`](https://github.com/googlemaps/google-maps-services-java/pull/36) +- Don't throw exceptions when the server returns something we're not expecting. [`#34`](https://github.com/googlemaps/google-maps-services-java/pull/34) +- Fix indentation. [`#33`](https://github.com/googlemaps/google-maps-services-java/pull/33) +- Better handling of more address types and address component types [`#32`](https://github.com/googlemaps/google-maps-services-java/pull/32) +- Fixing a typo in ReadMe.md [`#29`](https://github.com/googlemaps/google-maps-services-java/pull/29) +- A bit more Maps for Business -> Maps for Work renaming. [`#27`](https://github.com/googlemaps/google-maps-services-java/pull/27) +- Updates to the ReadMe. Cleaning up the original documentation, and clarifying some things around support/contribution. [`#26`](https://github.com/googlemaps/google-maps-services-java/pull/26) +- Remove elevation LatLng join code [`#25`](https://github.com/googlemaps/google-maps-services-java/pull/25) + +### Commits + +- Fix indentation. Remove unneeded import/impl of UrlValue. [`3f46121`](https://github.com/googlemaps/google-maps-services-java/commit/3f46121a8cacbe3c4abfc1cf1b2828b270ef8dcf) +- Updates to the ReadMe file [`989be08`](https://github.com/googlemaps/google-maps-services-java/commit/989be08cc55e370884a81e4a5bd4ef2987749762) +- Add instructions for generating and pushing javadoc. [`be0b96e`](https://github.com/googlemaps/google-maps-services-java/commit/be0b96e16c522d5b03f2c10524646340acfe25cf) + +## [v0.1.2](https://github.com/googlemaps/google-maps-services-java/compare/v0.1.1...v0.1.2) - 2014-09-16 + +### Merged + +- :vertical_traffic_light: added over_query_limit to retry logic [`#22`](https://github.com/googlemaps/google-maps-services-java/pull/22) +- :curly_loop: fixed long-running threads [`#21`](https://github.com/googlemaps/google-maps-services-java/pull/21) + +### Commits + +- :curly_loop: extracted threadFactory [`14c7341`](https://github.com/googlemaps/google-maps-services-java/commit/14c7341afe6fdf9b30be9be808f0be2cb199f8c1) +- added over_query_limit to retry logic [`c074753`](https://github.com/googlemaps/google-maps-services-java/commit/c0747535ff208a846f2f77d0babc4530a922c6a9) +- :curly_loop: updated comment regarding thread termination [`d5af2a1`](https://github.com/googlemaps/google-maps-services-java/commit/d5af2a1764973e384c0da63e85adf8e3d75b285e) + +## v0.1.1 - 2014-09-12 + +### Merged + +- Fixed signing rules to allow travis to run [`#20`](https://github.com/googlemaps/google-maps-services-java/pull/20) +- Added maven deployment tasks [`#19`](https://github.com/googlemaps/google-maps-services-java/pull/19) +- Updated partial match test to use a working query [`#18`](https://github.com/googlemaps/google-maps-services-java/pull/18) +- Update README.md [`#16`](https://github.com/googlemaps/google-maps-services-java/pull/16) +- Enable test coverage reports with jacoco [`#13`](https://github.com/googlemaps/google-maps-services-java/pull/13) +- Don't use local hostname for MockWebServer's base URL. [`#14`](https://github.com/googlemaps/google-maps-services-java/pull/14) +- added NOT_FOUND exception (closes #10) [`#11`](https://github.com/googlemaps/google-maps-services-java/pull/11) +- :construction: fixed javadoc warnings [`#9`](https://github.com/googlemaps/google-maps-services-java/pull/9) +- Updating copyrights and license [`#8`](https://github.com/googlemaps/google-maps-services-java/pull/8) +- Set up tests to run with client ID too [`#7`](https://github.com/googlemaps/google-maps-services-java/pull/7) +- Moved QPS args from constructor to setter. [`#6`](https://github.com/googlemaps/google-maps-services-java/pull/6) +- Added README [`#2`](https://github.com/googlemaps/google-maps-services-java/pull/2) +- Version is now specified in build file [`#4`](https://github.com/googlemaps/google-maps-services-java/pull/4) +- Adds copyright string to each file [`#5`](https://github.com/googlemaps/google-maps-services-java/pull/5) +- Adding files for license, (c) and contributors [`#3`](https://github.com/googlemaps/google-maps-services-java/pull/3) +- Migrated build to gradle [`#1`](https://github.com/googlemaps/google-maps-services-java/pull/1) + +### Fixed + +- Merge pull request #11 from markmcd/issue-10 [`#10`](https://github.com/googlemaps/google-maps-services-java/issues/10) +- added NOT_FOUND exception (closes #10) [`#10`](https://github.com/googlemaps/google-maps-services-java/issues/10) + +### Commits + +- Initial import of Java code [`b577f30`](https://github.com/googlemaps/google-maps-services-java/commit/b577f300c18b88674a375018bfc4c20daca1f24b) +- Using the /* */ format instead of // [`0816981`](https://github.com/googlemaps/google-maps-services-java/commit/081698119beb67d76d0808db23bbc5fb615b08a8) +- :gift: Added gradle wrapper [`d669a10`](https://github.com/googlemaps/google-maps-services-java/commit/d669a10855a019db6332f3cbaf354d2d24e26a22) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..f8b12cb55 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,63 @@ +# Google Open Source Community Guidelines + +At Google, we recognize and celebrate the creativity and collaboration of open +source contributors and the diversity of skills, experiences, cultures, and +opinions they bring to the projects and communities they participate in. + +Every one of Google's open source projects and communities are inclusive +environments, based on treating all individuals respectfully, regardless of +gender identity and expression, sexual orientation, disabilities, +neurodiversity, physical appearance, body size, ethnicity, nationality, race, +age, religion, or similar personal characteristic. + +We value diverse opinions, but we value respectful behavior more. + +Respectful behavior includes: + +* Being considerate, kind, constructive, and helpful. +* Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or + physically threatening behavior, speech, and imagery. +* Not engaging in unwanted physical contact. + +Some Google open source projects [may adopt][] an explicit project code of +conduct, which may have additional detailed expectations for participants. Most +of those projects will use our [modified Contributor Covenant][]. + +[may adopt]: https://opensource.google/docs/releasing/preparing/#conduct +[modified Contributor Covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/ + +## Resolve peacefully + +We do not believe that all conflict is necessarily bad; healthy debate and +disagreement often yields positive results. However, it is never okay to be +disrespectful. + +If you see someone behaving disrespectfully, you are encouraged to address the +behavior directly with those involved. Many issues can be resolved quickly and +easily, and this gives people more control over the outcome of their dispute. +If you are unable to resolve the matter for any reason, or if the behavior is +threatening or harassing, report it. We are dedicated to providing an +environment where participants feel welcome and safe. + +## Reporting problems + +Some Google open source projects may adopt a project-specific code of conduct. +In those cases, a Google employee will be identified as the Project Steward, +who will receive and handle reports of code of conduct violations. In the event +that a project hasn’t identified a Project Steward, you can report problems by +emailing opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is +taken. The identity of the reporter will be omitted from the details of the +report supplied to the accused. In potentially harmful situations, such as +ongoing harassment or threats to anyone's safety, we may take action without +notice. + +*This document was adapted from the [IndieWeb Code of Conduct][] and can also +be found at .* + +[IndieWeb Code of Conduct]: https://indieweb.org/code-of-conduct diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 45342bf2f..170b986fd 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -6,11 +6,21 @@ # Please keep the list sorted by first name. +Amy Boyd @amyboyd +Brantley Wells @gitbrantley Brett Morgan @domesticmouse Chris Broadfoot @broady +Christopher Arriola @arriolac +Dan O'Meara @danoscarmike +Darshit Patel @darshitpp Dave Holmes @dh-- +Ismael Blesa @ismamai +Malcolm Windsor @mwindsor-beoped Mark McDonald @markmcd Nicolas Poirier @NicolasPoirier +Pavel Smagin @psmagin Pulkit Bhuwalka @nutsiepully Romain Sertelon @rsertelon +Sam Lukes @slukes Sipos Tamas @onlyonce +Stephan Schroevers @Stephan202 diff --git a/README.md b/README.md index db38c7af9..da58f95c6 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,49 @@ Java Client for Google Maps Services ==================================== -![Build Status](https://travis-ci.org/googlemaps/google-maps-services-java.svg) ![Maven Central Version](http://img.shields.io/maven-central/v/com.google.maps/google-maps-services.svg) [![Coverage Status](https://img.shields.io/coveralls/googlemaps/google-maps-services-java.svg)](https://coveralls.io/r/googlemaps/google-maps-services-java) +[![Build Status](https://travis-ci.org/googlemaps/google-maps-services-java.svg)](https://travis-ci.org/googlemaps/google-maps-services-java) +[![Coverage Status](https://img.shields.io/coveralls/googlemaps/google-maps-services-java.svg)](https://coveralls.io/r/googlemaps/google-maps-services-java) +[![Maven Central Version](http://img.shields.io/maven-central/v/com.google.maps/google-maps-services.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.google.maps%22%20a%3A%22google-maps-services%22) +[![Javadocs](https://www.javadoc.io/badge/com.google.maps/google-maps-services.svg)](https://www.javadoc.io/doc/com.google.maps/google-maps-services) +![GitHub contributors](https://img.shields.io/github/contributors/googlemaps/google-maps-services-java?color=green) +[![Stack Exchange questions](https://img.shields.io/stackexchange/stackoverflow/t/google-maps?color=orange&label=google-maps&logo=stackoverflow)](https://stackoverflow.com/questions/tagged/google-maps) ## Description Use Java? Want to [geocode][Geocoding API] something? Looking for [directions][Directions API]? Maybe [matrices of directions][Distance Matrix API]? This library brings the [Google Maps API Web Services] to your server-side Java application. -![Analytics](https://ga-beacon.appspot.com/UA-12846745-20/google-maps-services-java/readme?pixel) +![Analytics](https://maps-ga-beacon.appspot.com/UA-12846745-20/google-maps-services-java/readme?pixel) -The Java Client for Google Maps Services is a Java Client library for the following Google Maps +The Java Client for Google Maps Services is a Java Client library for the following Google Maps APIs: - - [Directions API] - - [Distance Matrix API] - - [Elevation API] - - [Geocoding API] - - [Time Zone API] - - [Roads API] +- [Directions API] +- [Distance Matrix API] +- [Elevation API] +- [Geocoding API] +- [Maps Static API] +- [Places API] +- [Roads API] +- [Time Zone API] Keep in mind that the same [terms and conditions](https://developers.google.com/maps/terms) apply to usage of the APIs when they're accessed through this library. +## Intended usage of this library + +The Java Client for Google Maps Services is designed for use in server applications. This library +is not intended for use inside of an Android app, due to the potential for loss of API keys. + +If you are building a mobile application, you will need to introduce a proxy server to act as +intermediary between your mobile application and the [Google Maps API Web Services]. The Java +Client for Google Maps Services would make an excellent choice as the basis for such a proxy server. + +Please see [Making the most of the Google Maps Web Service APIs] for more detail. + +Looking for our Android [Maps](https://developers.google.com/maps/documentation/android-sdk/intro) or +[Places](https://developers.google.com/places/android-sdk/intro) SDKs? + ## Support This library is community supported. We're comfortable enough with the stability and features of @@ -38,109 +59,134 @@ contribute, please read [How to Contribute][contrib]. ## Requirements - - Java 1.7 or later. - - A Google Maps API key. +- Java 1.8 or later. +- A Google Maps API key. ### API keys +Each Google Maps Web Service request requires an API key. API keys are generated in the 'Credentials' page of the 'APIs & Services' tab of Google Cloud console. + +For even more information on getting started with Google Maps Platform and generating/restricting an API key, see [Get Started with Google Maps Platform](https://developers.google.com/maps/gmp-get-started) in our docs. -Each Google Maps Web Service requires an API key or Client ID. API keys are -freely available with a Google Account at -https://developers.google.com/console. To generate a server key for -your project: - - 1. Visit https://developers.google.com/console and log in with - a Google Account. - 1. Select an existing project, or create a new project. - 1. Click **Enable an API**. - 1. Browse for the API, and set its status to "On". The Java Client for Google Maps Services - accesses the following APIs: - * Directions API - * Distance Matrix API - * Elevation API - * Geocoding API - * Time Zone API - 1. Once you've enabled the APIs, click **Credentials** from the left navigation of the Developer - Console. - 1. In the "Public API access", click **Create new Key**. - 1. Choose **Server Key**. - 1. If you'd like to restrict requests to a specific IP address, do so now. - 1. Click **Create**. - -Your API key should be 40 characters long, and begin with `AIza`. - -**Important:** This key should be kept secret on your server. +Important: This key should be kept secret on your server. ## Installation You can add the library to your project via Maven or Gradle. +**Note:** Since 0.1.18 there is now a dependency on [SLF4J](https://www.slf4j.org/). You need to add +one of the adapter dependencies that makes sense for your logging setup. In the configuration +samples below we are integrating +[slf4j-nop](https://search.maven.org/#artifactdetails%7Corg.slf4j%7Cslf4j-nop%7C1.7.25%7Cjar), +but there are others like +[slf4j-log4j12](https://search.maven.org/#artifactdetails%7Corg.slf4j%7Cslf4j-log4j12%7C1.7.25%7Cjar) +and [slf4j-jdk14](https://search.maven.org/#artifactdetails%7Corg.slf4j%7Cslf4j-jdk14%7C1.7.25%7Cjar) +that will make more sense in other configurations. This will stop a warning message being emitted +when you start using `google-maps-services`. + ### Maven + ```xml - com.google.maps - google-maps-services - (insert latest version) + com.google.maps + google-maps-services + (insert latest version) + + + org.slf4j + slf4j-simple + 1.7.25 ``` ### Gradle + ```groovy repositories { mavenCentral() } dependencies { - compile 'com.google.maps:google-maps-services:(insert latest version)' - ... + implementation 'com.google.maps:google-maps-services:(insert latest version)' + implementation 'org.slf4j:slf4j-simple:1.7.25' } ``` -You can find the latest version at the top of this README or by searching -[Maven Central](https://search.maven.org/) or [Gradle, Please](http://gradleplease.appspot.com/). +You can find the latest version at the top of this README or by [searching +Maven Central](https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22google-maps-services%22) or [Gradle, Please](http://gradleplease.appspot.com/#google-maps-services). ## Developer Documentation +View the [javadoc](https://www.javadoc.io/doc/com.google.maps/google-maps-services). -View the [javadoc](https://googlemaps.github.io/google-maps-services-java/v0.1.7/javadoc). - -Additional documentation for the included web services is available at +Additional documentation for the included web services is available at https://developers.google.com/maps/. - - [Directions API] - - [Distance Matrix API] - - [Elevation API] - - [Geocoding API] - - [Time Zone API] +- [Directions API] +- [Distance Matrix API] +- [Elevation API] +- [Geocoding API] +- [Maps Static API] +- [Places API] +- [Roads API] +- [Time Zone API] ## Usage -This example uses the [Geocoding API]. +This example uses the [Geocoding API] with an API key: ```java -GeoApiContext context = new GeoApiContext().setApiKey("AIza..."); +GeoApiContext context = new GeoApiContext.Builder() + .apiKey("AIza...") + .build(); GeocodingResult[] results = GeocodingApi.geocode(context, "1600 Amphitheatre Parkway Mountain View, CA 94043").await(); -System.out.println(results[0].formattedAddress); +Gson gson = new GsonBuilder().setPrettyPrinting().create(); +System.out.println(gson.toJson(results[0].addressComponents)); ``` -For more usage examples, check out [the tests](src/test/java/com/google/maps/). +The `GeoApiContext` is designed to be a [Singleton](https://en.wikipedia.org/wiki/Singleton_pattern) +in your application. Please instantiate one on application startup, and continue to use it for the +life of your application. This will enable proper QPS enforcement across all of your requests. + +At the end of the execution, call the `shutdown()` method of `GeoApiContext`, +otherwise the thread will remain instantiated in memory. + +For more usage examples, check out [the tests](src/test/java/com/google/maps). ## Features +### Google App Engine Support + +You can use this client library on Google App Engine with a single code change. + +```java +new GeoApiContext.Builder(new GaeRequestHandler.Builder()) + .apiKey("AIza...") + .build(); +``` + +The `new GaeRequestHandler.Builder()` argument to `GeoApiContext.Builder`'s `requestHandlerBuilder` +tells the Java Client for Google Maps Services to utilise the appropriate calls for making HTTP +requests from Google App Engine, instead of the default [OkHttp3](https://square.github.io/okhttp/) +based strategy. + ### Rate Limiting Never sleep between requests again! By default, requests are sent at the expected rate limits for -each web service, typically 10 queries per second for free users. If you want to speed up or slow -down requests, you can do that too, using `new GeoApiContext().setQueryRateLimit(qps)`. +each web service, typically 50 queries per second for free users. If you want to speed up or slow +down requests, you can do that too, using `new GeoApiContext.Builder().queryRateLimit(qps).build()`. +Note that you still need to manually handle the [delay between the initial request and successive pages](https://developers.google.com/places/web-service/search#PlaceSearchPaging) when you're paging through multiple result sets. ### Retry on Failure Automatically retry when intermittent failures occur. That is, when any of the retriable 5xx errors are returned from the API. -### Keys *and* Client IDs +To alter or disable automatic retries, see these methods in `GeoApiContext`: -Maps API for Work customers can use their [client ID and secret][clientid] to authenticate. Free -customers can use their [API key][apikey], too. +- `.disableRetries()` +- `.maxRetries()` +- `.retryTimeout()` +- `.setIfExceptionIsAllowedToRetry()` ### POJOs @@ -185,38 +231,22 @@ req.setCallback(new PendingResult.Callback() { $ ./gradlew jar # Run the tests - $ API_KEY=AIza.... ./gradlew test - - # Run the tests with enterprise credentials. - $ CLIENT_ID=... CLIENT_SECRET=... ./gradlew test + $ ./gradlew test - # Generate documentation - $ ./gradlew javadoc - - # Publish documentation - $ ./gradlew javadoc - $ git checkout gh-pages - $ rm -rf javadoc - $ mkdir $VERSION - $ mv build/docs/javadoc $VERSION - $ git add $VERSION/javadoc - $ git add latest - $ git commit - $ git push origin gh-pages [apikey]: https://developers.google.com/maps/faq#keysystem [clientid]: https://developers.google.com/maps/documentation/business/webservices/auth [contrib]: https://github.com/googlemaps/google-maps-services-java/blob/master/CONTRIB.md [Directions API]: https://developers.google.com/maps/documentation/directions +[directions-key]: https://developers.google.com/maps/documentation/directions/get-api-key#key +[directions-client-id]: https://developers.google.com/maps/documentation/directions/get-api-key#client-id [Distance Matrix API]: https://developers.google.com/maps/documentation/distancematrix [Elevation API]: https://developers.google.com/maps/documentation/elevation [Geocoding API]: https://developers.google.com/maps/documentation/geocoding -[Google Maps API Web Services]: https://developers.google.com/maps/documentation/webservices/ +[Google Maps API Web Services]: https://developers.google.com/maps/apis-by-platform#web_service_apis [issues]: https://github.com/googlemaps/google-maps-services-java/issues +[Maps Static API]: https://developers.google.com/maps/documentation/maps-static/ +[Places API]: https://developers.google.com/places/web-service/ [Time Zone API]: https://developers.google.com/maps/documentation/timezone [Roads API]: https://developers.google.com/maps/documentation/roads - - - - - +[Making the most of the Google Maps Web Service APIs]: https://maps-apis.googleblog.com/2016/09/making-most-of-google-maps-web-service.html diff --git a/build.gradle b/build.gradle index 71b6e3fbc..a9166f32c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,27 +1,38 @@ import org.apache.tools.ant.filters.ReplaceTokens +import java.time.Duration + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.3' + } +} + + +plugins { + id 'com.github.ben-manes.versions' version '0.22.0' + id 'com.github.sherter.google-java-format' version '0.8' + id "de.marcphilipp.nexus-publish" version "0.3.0" + id 'io.codearte.nexus-staging' version '0.21.1' +} apply plugin: 'java' -apply plugin: 'maven' +apply plugin: 'java-library' +apply plugin: 'maven-publish' apply plugin: 'signing' apply plugin: 'jacoco' apply plugin: 'com.github.kt3k.coveralls' +apply plugin: 'com.github.sherter.google-java-format' group = 'com.google.maps' -sourceCompatibility = 1.6 +sourceCompatibility = 1.8 repositories { mavenCentral() } -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.0.0' - } -} - task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' from 'build/docs/javadoc' @@ -38,19 +49,20 @@ artifacts { archives sourcesJar } -repositories { - mavenCentral() -} - dependencies { - compile 'com.google.code.gson:gson:2.3.1' - compile 'com.squareup.okhttp:okhttp:2.0.0' - compile 'joda-time:joda-time:2.4' - - testCompile 'junit:junit:4.11' - testCompile 'org.mockito:mockito-core:1.9.5' - testCompile 'com.google.mockwebserver:mockwebserver:20130706' - testCompile 'org.apache.httpcomponents:httpclient:4.3.5' + compileOnly 'com.google.appengine:appengine-api-1.0-sdk:1.9.76' + api 'com.squareup.okhttp3:okhttp:3.14.4' + api 'com.google.code.gson:gson:2.8.6' + api 'io.opencensus:opencensus-api:0.25.0' + implementation 'org.slf4j:slf4j-api:1.7.26' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.0.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:3.14.2' + testImplementation 'org.apache.httpcomponents:httpclient:4.5.9' + testImplementation 'org.slf4j:slf4j-simple:1.7.26' + testImplementation 'org.apache.commons:commons-lang3:3.9' + testImplementation 'org.json:json:20180813' + testImplementation 'io.opencensus:opencensus-impl:0.25.0' } task updateVersion(type: Copy) { @@ -60,8 +72,33 @@ task updateVersion(type: Copy) { compileJava.source = "build/filtered/src/main/java" compileJava.dependsOn updateVersion +compileJava { + options.compilerArgs << "-Xlint:deprecation" +} + +compileTestJava { + options.compilerArgs << "-Xlint:deprecation" +} + +// Propagate API Key system properties to test tasks +tasks.withType(Test) { + systemProperty 'api.key', System.getProperty('api.key') + systemProperty 'client.id', System.getProperty('client.id') + systemProperty 'client.secret', System.getProperty('client.secret') +} + +java { + withJavadocJar() + withSourcesJar() +} + javadoc { exclude '**/internal/**' + def currentJavaVersion = org.gradle.api.JavaVersion.current() +} + +jacoco { + toolVersion = "0.8.2" // non-default version 0.8.2 required for OpenJDK 11 compatibility } jacocoTestReport { @@ -71,61 +108,102 @@ jacocoTestReport { } } -uploadArchives { - repositories { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { - authentication(userName: sonatypeUsername, password: sonatypePassword) - } +ext.artifactId = 'google-maps-services' - pom.artifactId = 'google-maps-services' - pom.project { - name 'Java Client for Google Maps Services' - packaging 'jar' - description 'Use the Google Maps API Web Services in Java! ' + - 'https://developers.google.com/maps/documentation/webservices/' - url 'https://github.com/googlemaps/google-maps-services-java' +publishing { + publications { + MapsJavaUtils(MavenPublication) { + pom { + name = 'Java Client for Google Maps Platform Web Services' + description = 'Use the Google Maps Platform Web Services in Java! ' + + 'https://developers.google.com/maps/documentation/webservices/' + url = 'https://github.com/googlemaps/google-maps-services-java' scm { - url 'scm:git@github.com:googlemaps/google-maps-services-java.git' - connection 'scm:git@github.com:googlemaps/google-maps-services-java.git' - developerConnection 'scm:git@github.com:googlemaps/google-maps-services-java.git' + url = 'scm:git@github.com:googlemaps/google-maps-services-java.git' + connection = 'scm:git@github.com:googlemaps/google-maps-services-java.git' + developerConnection = 'scm:git@github.com:googlemaps/google-maps-services-java.git' } licenses { license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - distribution 'repo' + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution = 'repo' } } + organization { + name = 'Google Inc' + url = 'http://developers.google.com/maps' + } + developers { developer { - id 'markmcd' - name 'Mark McDonald' - url 'https://google.com/+MarkMcDonald0' + id = 'markmcd' + name = 'Mark McDonald' } developer { - id 'domesticmouse' - name 'Brett Morgan' - url 'https://google.com/+BrettMorgan' + id = 'domesticmouse' + name = 'Brett Morgan' } developer { - id 'broady' - name 'Chris Broadfoot' - url 'https://google.com/+ChristopherBroadfoot' + id = 'broady' + name = 'Chris Broadfoot' + } + developer { + id = 'chrisarriola' + name = 'Christopher Arriola' } } } + groupId group + artifactId project.ext.artifactId + version version + from components.java + } + gpr(MavenPublication) { + groupId group + artifactId project.ext.artifactId + version version + from(components.java) + } + } + repositories { + maven { + url "https://oss.sonatype.org/service/local/staging/deploy/maven2" + credentials { + username sonatypeUsername + password sonatypePassword + } + } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/googlemaps/google-maps-services-java") + credentials { + username = 'googlemaps-bot' + password = githubPassword + } } } } -if (ext.'signing.secretKeyRingFile') { - signing { - sign configurations.archives +nexusPublishing { + repositories { + sonatype { + username = sonatypeUsername + password = sonatypePassword + clientTimeout = Duration.ofSeconds(120) + } } } + +nexusStaging { + username = sonatypeUsername + password = sonatypePassword + packageGroup = "com.google.maps" +} + +signing { + sign publishing.publications.MapsJavaUtils +} diff --git a/gradle.properties b/gradle.properties index be203b1a5..676860575 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=0.1.8-SNAPSHOT +version=0.18.0 # variables required to allow build.gradle to parse, # override in ~/.gradle/gradle.properties @@ -9,3 +9,4 @@ signing.secretKeyRingFile= sonatypeUsername= sonatypePassword= +githubPassword= diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 637a41681..a1df9e107 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Aug 15 22:13:57 EST 2014 +#Mon Sep 21 20:20:36 EST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/src/main/java/com/google/maps/DirectionsApi.java b/src/main/java/com/google/maps/DirectionsApi.java index 9dcfee544..415ece739 100644 --- a/src/main/java/com/google/maps/DirectionsApi.java +++ b/src/main/java/com/google/maps/DirectionsApi.java @@ -19,46 +19,67 @@ import com.google.maps.internal.ApiConfig; import com.google.maps.internal.ApiResponse; import com.google.maps.internal.StringJoin.UrlValue; +import com.google.maps.model.DirectionsResult; import com.google.maps.model.DirectionsRoute; +import com.google.maps.model.GeocodedWaypoint; /** - *

The Google Directions API is a service that calculates directions between locations - * using an HTTP request. You can search for directions for several modes of transportation, - * include transit, driving, walking or cycling. Directions may specify origins, destinations - * and waypoints either as text strings (e.g. "Chicago, IL" or "Darwin, NT, Australia") or as - * latitude/longitude coordinates. The Directions API can return multi-part directions using - * a series of waypoints. - *

See documentation. + * The Google Directions API is a service that calculates directions between locations using an HTTP + * request. You can search for directions for several modes of transportation, include transit, + * driving, walking, or cycling. Directions may specify origins, destinations, and waypoints, either + * as text strings (e.g. "Chicago, IL" or "Darwin, NT, Australia") or as latitude/longitude + * coordinates. The Directions API can return multi-part directions using a series of waypoints. + * + *

See the Directions + * API Developer's Guide for more information. */ public class DirectionsApi { static final ApiConfig API_CONFIG = new ApiConfig("/maps/api/directions/json"); - private DirectionsApi() { - } + private DirectionsApi() {} + /** + * Creates a new DirectionsApiRequest using the given context, with all attributes at their + * default values. + * + * @param context Context that the DirectionsApiRequest will be executed against + * @return A newly constructed DirectionsApiRequest between the given points. + */ public static DirectionsApiRequest newRequest(GeoApiContext context) { return new DirectionsApiRequest(context); } - public static DirectionsApiRequest getDirections(GeoApiContext context, - String origin, - String destination) { - return newRequest(context).origin(origin).destination(destination); + /** + * Creates a new DirectionsApiRequest between the given origin and destination, using the defaults + * for all other options. + * + * @param context Context that the DirectionsApiRequest will be executed against + * @param origin Origin address as text + * @param destination Destination address as text + * @return A newly constructed DirectionsApiRequest between the given points. + */ + public static DirectionsApiRequest getDirections( + GeoApiContext context, String origin, String destination) { + return new DirectionsApiRequest(context).origin(origin).destination(destination); } - static class Response implements ApiResponse { + public static class Response implements ApiResponse { public String status; public String errorMessage; + public GeocodedWaypoint[] geocodedWaypoints; public DirectionsRoute[] routes; @Override public boolean successful() { - return "OK".equals(status) || "ZERO_RESULTS".equals(status); + return "OK".equals(status); } @Override - public DirectionsRoute[] getResult() { - return routes; + public DirectionsResult getResult() { + DirectionsResult result = new DirectionsResult(); + result.geocodedWaypoints = geocodedWaypoints; + result.routes = routes; + return result; } @Override @@ -71,31 +92,29 @@ public ApiException getError() { } /** - * Directions may be calculated that adhere to certain restrictions. This is configured by - * calling {@link com.google.maps.DirectionsApiRequest#avoid} or - * {@link com.google.maps.DistanceMatrixApiRequest#avoid}. + * Directions may be calculated that adhere to certain restrictions. This is configured by calling + * {@link com.google.maps.DirectionsApiRequest#avoid} or {@link + * com.google.maps.DistanceMatrixApiRequest#avoid}. * - * @see - * Restrictions in the Directions API - * @see - * Restrictions in the Distance Matrix API> + * @see + * Restrictions in the Directions API + * @see + * Distance Matrix API Request Parameters */ public enum RouteRestriction implements UrlValue { - /** - * {@code TOLLS} indicates that the calculated route should avoid toll roads/bridges. - */ + /** Indicates that the calculated route should avoid toll roads/bridges. */ TOLLS("tolls"), - /** - * {@code HIGHWAYS} indicates that the calculated route should avoid highways. - */ + /** Indicates that the calculated route should avoid highways. */ HIGHWAYS("highways"), - /** - * {@code FERRIES} indicates that the calculated route should avoid ferries. - */ - FERRIES("ferries"); + /** Indicates that the calculated route should avoid ferries. */ + FERRIES("ferries"), + + /** Indicates that the calculated route should avoid indoor areas. */ + INDOOR("indoor"); private final String restriction; diff --git a/src/main/java/com/google/maps/DirectionsApiRequest.java b/src/main/java/com/google/maps/DirectionsApiRequest.java index 006bb6f37..c10186cf2 100644 --- a/src/main/java/com/google/maps/DirectionsApiRequest.java +++ b/src/main/java/com/google/maps/DirectionsApiRequest.java @@ -16,29 +16,28 @@ package com.google.maps; import static com.google.maps.internal.StringJoin.join; +import static java.util.Objects.requireNonNull; -import com.google.maps.DirectionsApi.RouteRestriction; -import com.google.maps.model.DirectionsRoute; +import com.google.maps.model.DirectionsResult; import com.google.maps.model.LatLng; +import com.google.maps.model.TrafficModel; import com.google.maps.model.TransitMode; import com.google.maps.model.TransitRoutingPreference; import com.google.maps.model.TravelMode; import com.google.maps.model.Unit; +import java.time.Instant; -import org.joda.time.ReadableInstant; - -/** - * Request for the Directions API. - */ +/** Request for the Directions API. */ public class DirectionsApiRequest - extends PendingResultBase { - private boolean optimizeWaypoints; - private String[] waypoints; + extends PendingResultBase { - DirectionsApiRequest(GeoApiContext context) { + public DirectionsApiRequest(GeoApiContext context) { super(context, DirectionsApi.API_CONFIG, DirectionsApi.Response.class); } + protected boolean optimizeWaypoints; + protected Waypoint[] waypoints; + @Override protected void validateRequest() { if (!params().containsKey("origin")) { @@ -47,42 +46,77 @@ protected void validateRequest() { if (!params().containsKey("destination")) { throw new IllegalArgumentException("Request must contain 'destination'"); } - if (TravelMode.TRANSIT.toString().equals(params().get("mode")) - && (params().containsKey("arrival_time") && params().containsKey("departure_time"))) { + if (params().containsKey("arrival_time") && params().containsKey("departure_time")) { throw new IllegalArgumentException( "Transit request must not contain both a departureTime and an arrivalTime"); } + if (params().containsKey("traffic_model") && !params().containsKey("departure_time")) { + throw new IllegalArgumentException( + "Specifying a traffic model requires that departure time be provided."); + } } /** - * The address or textual latitude/longitude value from which you wish to calculate directions. - * If you pass an address as a string, the Directions service will geocode the string and convert + * The address or textual latitude/longitude value from which you wish to calculate directions. If + * you pass an address as a location, the Directions service will geocode the location and convert * it to a latitude/longitude coordinate to calculate directions. If you pass coordinates, ensure * that no space exists between the latitude and longitude values. + * + * @param origin The starting location for the Directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest origin(String origin) { return param("origin", origin); } /** - * The address or textual latitude/longitude value from which you wish to calculate directions. - * If you pass an address as a string, the Directions service will geocode the string and convert + * The address or textual latitude/longitude value from which you wish to calculate directions. If + * you pass an address as a location, the Directions service will geocode the location and convert * it to a latitude/longitude coordinate to calculate directions. If you pass coordinates, ensure * that no space exists between the latitude and longitude values. + * + * @param destination The ending location for the Directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest destination(String destination) { return param("destination", destination); } /** - * The origin, as a latitude,longitude location. + * The Place ID value from which you wish to calculate directions. + * + * @param originPlaceId The starting location Place ID for the Directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest originPlaceId(String originPlaceId) { + return param("origin", prefixPlaceId(originPlaceId)); + } + + /** + * The Place ID value from which you wish to calculate directions. + * + * @param destinationPlaceId The ending location Place ID for the Directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest destinationPlaceId(String destinationPlaceId) { + return param("destination", prefixPlaceId(destinationPlaceId)); + } + + /** + * The origin, as a latitude/longitude location. + * + * @param origin The starting location for the Directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest origin(LatLng origin) { return origin(origin.toString()); } /** - * The destination, as a latitude,longitude location. + * The destination, as a latitude/longitude location. + * + * @param destination The ending location for the Directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest destination(LatLng destination) { return destination(destination.toString()); @@ -94,6 +128,7 @@ public DirectionsApiRequest destination(LatLng destination) { * either a {@code departureTime} or an {@code arrivalTime}. * * @param mode The travel mode to request directions for. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest mode(TravelMode mode) { return param("mode", mode); @@ -102,15 +137,18 @@ public DirectionsApiRequest mode(TravelMode mode) { /** * Indicates that the calculated route(s) should avoid the indicated features. * - * @param restrictions one or more of {@link RouteRestriction#TOLLS}, - * {@link RouteRestriction#HIGHWAYS}, {@link RouteRestriction#FERRIES} + * @param restrictions one or more of {@link DirectionsApi.RouteRestriction} objects. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ - public DirectionsApiRequest avoid(RouteRestriction... restrictions) { + public DirectionsApiRequest avoid(DirectionsApi.RouteRestriction... restrictions) { return param("avoid", join('|', restrictions)); } /** * Specifies the unit system to use when displaying results. + * + * @param units The preferred units for displaying distances. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest units(Unit units) { return param("units", units); @@ -118,6 +156,7 @@ public DirectionsApiRequest units(Unit units) { /** * @param region The region code, specified as a ccTLD ("top-level domain") two-character value. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest region(String region) { return param("region", region); @@ -127,43 +166,124 @@ public DirectionsApiRequest region(String region) { * Set the arrival time for a Transit directions request. * * @param time The arrival time to calculate directions for. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ - public DirectionsApiRequest arrivalTime(ReadableInstant time) { - return param("arrival_time", Long.toString(time.getMillis() / 1000L)); + public DirectionsApiRequest arrivalTime(Instant time) { + return param("arrival_time", Long.toString(time.toEpochMilli() / 1000L)); } /** - * Set the departure time for a Transit directions request. If not provided, "now" is assumed. + * Set the departure time for a transit or driving directions request. If both departure time and + * traffic model are not provided, then "now" is assumed. If traffic model is supplied, then + * departure time must be specified. Duration in traffic will only be returned if the departure + * time is specified. * * @param time The departure time to calculate directions for. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ - public DirectionsApiRequest departureTime(ReadableInstant time) { - return param("departure_time", Long.toString(time.getMillis() / 1000L)); + public DirectionsApiRequest departureTime(Instant time) { + return param("departure_time", Long.toString(time.toEpochMilli() / 1000L)); + } + + /** + * Set the departure time for a transit or driving directions request as the current time. If + * traffic model is supplied, then departure time must be specified. Duration in traffic will only + * be returned if the departure time is specified. + * + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest departureTimeNow() { + return param("departure_time", "now"); } /** * Specifies a list of waypoints. Waypoints alter a route by routing it through the specified - * location(s). A waypoint is specified as either a latitude/longitude coordinate or as an - * address which will be geocoded. Waypoints are only supported for driving, walking and - * bicycling directions. + * location(s). A waypoint is specified as either a latitude/longitude coordinate or as an address + * which will be geocoded. Waypoints are only supported for driving, walking and bicycling + * directions. * - *

For more information on waypoints, see - * - * Using Waypoints in Routes. + *

For more information on waypoints, see Using + * Waypoints in Routes. + * + * @param waypoints The waypoints to add to this directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ - public DirectionsApiRequest waypoints(String... waypoints) { + public DirectionsApiRequest waypoints(Waypoint... waypoints) { if (waypoints == null || waypoints.length == 0) { + this.waypoints = new Waypoint[0]; + param("waypoints", ""); return this; - } else if (waypoints.length == 1) { - return param("waypoints", waypoints[0]); } else { - return param("waypoints", (optimizeWaypoints ? "optimize:true|" : "") + join('|', waypoints)); + this.waypoints = waypoints; + String[] waypointStrs = new String[waypoints.length]; + for (int i = 0; i < waypoints.length; i++) { + waypointStrs[i] = waypoints[i].toString(); + } + param("waypoints", (optimizeWaypoints ? "optimize:true|" : "") + join('|', waypointStrs)); + return this; + } + } + + /** + * Specifies the list of waypoints as String addresses. If any of the Strings are Place IDs, you + * must prefix them with {@code place_id:}. + * + *

See {@link #prefixPlaceId(String)}. + * + *

See {@link #waypoints(Waypoint...)}. + * + * @param waypoints The waypoints to add to this directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest waypoints(String... waypoints) { + Waypoint[] objWaypoints = new Waypoint[waypoints.length]; + for (int i = 0; i < waypoints.length; i++) { + objWaypoints[i] = new Waypoint(waypoints[i]); + } + return waypoints(objWaypoints); + } + + /** + * Specifies the list of waypoints as Plade ID Strings, prefixing them as required by the API. + * + *

See {@link #prefixPlaceId(String)}. + * + *

See {@link #waypoints(Waypoint...)}. + * + * @param waypoints The waypoints to add to this directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest waypointsFromPlaceIds(String... waypoints) { + Waypoint[] objWaypoints = new Waypoint[waypoints.length]; + for (int i = 0; i < waypoints.length; i++) { + objWaypoints[i] = new Waypoint(prefixPlaceId(waypoints[i])); + } + return waypoints(objWaypoints); + } + + /** + * The list of waypoints as latitude/longitude locations. + * + *

See {@link #waypoints(Waypoint...)}. + * + * @param waypoints The waypoints to add to this directions request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest waypoints(LatLng... waypoints) { + Waypoint[] objWaypoints = new Waypoint[waypoints.length]; + for (int i = 0; i < waypoints.length; i++) { + objWaypoints[i] = new Waypoint(waypoints[i]); } + return waypoints(objWaypoints); } /** * Allow the Directions service to optimize the provided route by rearranging the waypoints in a * more efficient order. + * + * @param optimize Whether to optimize waypoints. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest optimizeWaypoints(boolean optimize) { optimizeWaypoints = optimize; @@ -178,6 +298,9 @@ public DirectionsApiRequest optimizeWaypoints(boolean optimize) { * If set to true, specifies that the Directions service may provide more than one route * alternative in the response. Note that providing route alternatives may increase the response * time from the server. + * + * @param alternateRoutes whether to return alternate routes. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest alternatives(boolean alternateRoutes) { if (alternateRoutes) { @@ -190,17 +313,106 @@ public DirectionsApiRequest alternatives(boolean alternateRoutes) { /** * Specifies one or more preferred modes of transit. This parameter may only be specified for * requests where the mode is transit. + * + * @param transitModes The preferred transit modes. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest transitMode(TransitMode... transitModes) { return param("transit_mode", join('|', transitModes)); } /** - * Specifies preferences for transit requests. Using this parameter, - * you can bias the options returned, rather than accepting the default best route chosen by - * the API. + * Specifies preferences for transit requests. Using this parameter, you can bias the options + * returned, rather than accepting the default best route chosen by the API. + * + * @param pref The transit routing preferences for this request. + * @return Returns this {@code DirectionsApiRequest} for call chaining. */ public DirectionsApiRequest transitRoutingPreference(TransitRoutingPreference pref) { return param("transit_routing_preference", pref); } + + /** + * Specifies the traffic model to use when requesting future driving directions. Once set, you + * must specify a departure time. + * + * @param trafficModel The traffic model for estimating driving time. + * @return Returns this {@code DirectionsApiRequest} for call chaining. + */ + public DirectionsApiRequest trafficModel(TrafficModel trafficModel) { + return param("traffic_model", trafficModel); + } + + /** + * Helper method for prefixing a Place ID, as specified by the API. + * + * @param placeId The Place ID to be prefixed. + * @return Returns the Place ID prefixed with {@code place_id:}. + */ + public String prefixPlaceId(String placeId) { + return "place_id:" + placeId; + } + + public static class Waypoint { + /** The location of this waypoint, expressed as an API-recognized location. */ + private String location; + /** Whether this waypoint is a stopover waypoint. */ + private boolean isStopover; + + /** + * Constructs a stopover Waypoint using a String address. + * + * @param location Any address or location recognized by the Google Maps API. + */ + public Waypoint(String location) { + this(location, true); + } + + /** + * Constructs a Waypoint using a String address. + * + * @param location Any address or location recognized by the Google Maps API. + * @param isStopover Whether this waypoint is a stopover waypoint. + */ + public Waypoint(String location, boolean isStopover) { + requireNonNull(location, "address may not be null"); + this.location = location; + this.isStopover = isStopover; + } + + /** + * Constructs a stopover Waypoint using a Latlng location. + * + * @param location The LatLng coordinates of this waypoint. + */ + public Waypoint(LatLng location) { + this(location, true); + } + + /** + * Constructs a Waypoint using a LatLng location. + * + * @param location The LatLng coordinates of this waypoint. + * @param isStopover Whether this waypoint is a stopover waypoint. + */ + public Waypoint(LatLng location, boolean isStopover) { + requireNonNull(location, "location may not be null"); + this.location = location.toString(); + this.isStopover = isStopover; + } + + /** + * Gets the String representation of this Waypoint, as an API request parameter fragment. + * + * @return The HTTP parameter fragment representing this waypoint. + */ + @Override + public String toString() { + if (isStopover) { + return location; + } else { + return "via:" + location; + } + } + } } diff --git a/src/main/java/com/google/maps/DistanceMatrixApi.java b/src/main/java/com/google/maps/DistanceMatrixApi.java index d74d30504..9035a08f9 100644 --- a/src/main/java/com/google/maps/DistanceMatrixApi.java +++ b/src/main/java/com/google/maps/DistanceMatrixApi.java @@ -22,40 +22,38 @@ import com.google.maps.model.DistanceMatrixRow; /** - * The Google Distance Matrix API is a service that provides travel distance and time for a - * matrix of origins and destinations. The information returned is based on the recommended route - * between start and end points, as calculated by the Google Maps API, - * and consists of rows containing duration and distance values for each pair. + * The Google Distance Matrix API is a service that provides travel distance and time for a matrix + * of origins and destinations. The information returned is based on the recommended route between + * start and end points, as calculated by the Google Maps API, and consists of rows containing + * duration and distance values for each pair. * *

This service does not return detailed route information. Route information can be obtained by - * passing the desired single origin and destination to the Directions API, - * {@link com.google.maps.DirectionsApi}. + * passing the desired single origin and destination to the Directions API, using {@link + * com.google.maps.DirectionsApi}. * - *

Note: Use of the Distance Matrix API must relate to the display of - * information on a Google Map; for example, to determine origin-destination pairs that fall - * within a specific driving time from one another, before requesting and displaying those - * destinations on a map. Use of the service in an application that doesn't display a Google map - * is prohibited. + *

Note: You can display Distance Matrix API results on a Google Map, or without + * a map. If you want to display Distance Matrix API results on a map, then these results must be + * displayed on a Google Map. It is prohibited to use Distance Matrix API data on a map that is not + * a Google map. * - * @see Distance - * Matrix Documentation + * @see Distance Matrix + * API Documentation */ public class DistanceMatrixApi { static final ApiConfig API_CONFIG = new ApiConfig("/maps/api/distancematrix/json"); - private DistanceMatrixApi() { - } + private DistanceMatrixApi() {} public static DistanceMatrixApiRequest newRequest(GeoApiContext context) { return new DistanceMatrixApiRequest(context); } - public static DistanceMatrixApiRequest getDistanceMatrix(GeoApiContext context, String[] origins, - String[] destinations) { + public static DistanceMatrixApiRequest getDistanceMatrix( + GeoApiContext context, String[] origins, String[] destinations) { return newRequest(context).origins(origins).destinations(destinations); } - static class Response implements ApiResponse { + public static class Response implements ApiResponse { public String status; public String errorMessage; public String[] originAddresses; @@ -80,6 +78,4 @@ public DistanceMatrix getResult() { return new DistanceMatrix(originAddresses, destinationAddresses, rows); } } - - } diff --git a/src/main/java/com/google/maps/DistanceMatrixApiRequest.java b/src/main/java/com/google/maps/DistanceMatrixApiRequest.java index f50bb9b7e..1f1e63439 100644 --- a/src/main/java/com/google/maps/DistanceMatrixApiRequest.java +++ b/src/main/java/com/google/maps/DistanceMatrixApiRequest.java @@ -21,16 +21,14 @@ import com.google.maps.DistanceMatrixApi.Response; import com.google.maps.model.DistanceMatrix; import com.google.maps.model.LatLng; +import com.google.maps.model.TrafficModel; import com.google.maps.model.TransitMode; import com.google.maps.model.TransitRoutingPreference; import com.google.maps.model.TravelMode; import com.google.maps.model.Unit; +import java.time.Instant; -import org.joda.time.ReadableInstant; - -/** - * A request to the Distance Matrix API. - */ +/** A request to the Distance Matrix API. */ public class DistanceMatrixApiRequest extends PendingResultBase { @@ -46,18 +44,18 @@ protected void validateRequest() { if (!params().containsKey("destinations")) { throw new IllegalArgumentException("Request must contain 'destinations'"); } - if (TravelMode.TRANSIT.toString().equals(params().get("mode")) - && (params().containsKey("arrival_time") && params().containsKey("departure_time"))) { + if (params().containsKey("arrival_time") && params().containsKey("departure_time")) { throw new IllegalArgumentException( "Transit request must not contain both a departureTime and an arrivalTime"); } } /** - * One or more addresses from which to calculate distance and time. The service will geocode - * the string and convert it to a latitude/longitude coordinate to calculate directions. + * One or more addresses from which to calculate distance and time. The service will geocode the + * strings and convert them to latitude/longitude coordinates to calculate directions. * - * @param origins String to geocode and use as an origin point (e.g. "New York, NY") + * @param origins Strings to geocode and use as an origin point (e.g. "New York, NY") + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest origins(String... origins) { return param("origins", join('|', origins)); @@ -66,18 +64,19 @@ public DistanceMatrixApiRequest origins(String... origins) { /** * One or more latitude/longitude values from which to calculate distance and time. * - * @param points The origin points. + * @param points The origin points. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest origins(LatLng... points) { return param("origins", join('|', points)); } - /** * One or more addresses to which to calculate distance and time. The service will geocode the - * string and convert it to a latitude/longitude coordinate to calculate directions. + * strings and convert them to latitude/longitude coordinates to calculate directions. * - * @param destinations String to geocode and use as a destination point (e.g. "New Jersey, NY") + * @param destinations Strings to geocode and use as a destination point (e.g. "Jersey City, NJ") + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest destinations(String... destinations) { return param("destinations", join('|', destinations)); @@ -87,6 +86,7 @@ public DistanceMatrixApiRequest destinations(String... destinations) { * One or more latitude/longitude values to which to calculate distance and time. * * @param points The destination points. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest destinations(LatLng... points) { return param("destinations", join('|', points)); @@ -95,10 +95,11 @@ public DistanceMatrixApiRequest destinations(LatLng... points) { /** * Specifies the mode of transport to use when calculating directions. * - *

Note that Distance Matrix requests only support {@link TravelMode#DRIVING}, - * {@link TravelMode#WALKING} and {@link TravelMode#BICYCLING}. - - * @param mode One of the travel modes supported by the Distance Matrix API. + *

Note that Distance Matrix requests only support {@link TravelMode#DRIVING}, {@link + * TravelMode#WALKING}, {@link TravelMode#BICYCLING} and {@link TravelMode#TRANSIT}. + * + * @param mode One of the travel modes supported by the Distance Matrix API. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest mode(TravelMode mode) { if (TravelMode.DRIVING.equals(mode) @@ -107,15 +108,15 @@ public DistanceMatrixApiRequest mode(TravelMode mode) { || TravelMode.TRANSIT.equals(mode)) { return param("mode", mode); } - throw new IllegalArgumentException("Distance Matrix API travel modes must be Driving, " - + "Transit, Walking or Bicycling"); + throw new IllegalArgumentException( + "Distance Matrix API travel modes must be Driving, Transit, Walking or Bicycling"); } /** * Introduces restrictions to the route. Only one restriction can be specified. * - * @param restriction One of {@link RouteRestriction#TOLLS}, {@link RouteRestriction#FERRIES} or - * {@link RouteRestriction#HIGHWAYS}. + * @param restriction A {@link RouteRestriction} object. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest avoid(RouteRestriction restriction) { return param("avoid", restriction); @@ -125,11 +126,11 @@ public DistanceMatrixApiRequest avoid(RouteRestriction restriction) { * Specifies the unit system to use when expressing distance as text. Distance Matrix results * contain text within distance fields to indicate the distance of the calculated route. * - * @see Unit systems in the Distance Matrix - * API - * - * @param unit One of {@link Unit#METRIC}, {@link Unit#IMPERIAL}. + * @param unit One of {@link Unit#METRIC} or {@link Unit#IMPERIAL}. + * @see + * Unit systems in the Distance Matrix API + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest units(Unit unit) { return param("units", unit); @@ -139,44 +140,66 @@ public DistanceMatrixApiRequest units(Unit unit) { * Specifies the desired time of departure. * *

The departure time may be specified in two cases: - *

  • For requests where the travel mode is transit: You can optionally specify one of - * departure_time or arrival_time. If neither time is specified, the departure_time defaults - * to now (that is, the departure time defaults to the current time).
  • - *
  • For requests where the travel mode is driving: Google Maps API for Work customers can - * specify the departure_time to receive trip duration considering current traffic conditions. - * The departure_time must be set to within a few minutes of the current time.
  • + * + *
      + *
    • For requests where the travel mode is transit: You can optionally specify one of + * departure_time or arrival_time. If neither time is specified, the departure_time defaults + * to now. (That is, the departure time defaults to the current time.) + *
    • For requests where the travel mode is driving: Google Maps API for Work customers can + * specify the departure_time to receive trip duration considering current traffic + * conditions. The departure_time must be set to within a few minutes of the current time. *
    * *

    Setting the parameter to null will remove it from the API request. * - * @param departureTime The time of departure. + * @param departureTime The time of departure. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ - public DistanceMatrixApiRequest departureTime(ReadableInstant departureTime) { - return param("departure_time", Long.toString(departureTime.getMillis() / 1000L)); + public DistanceMatrixApiRequest departureTime(Instant departureTime) { + return param("departure_time", Long.toString(departureTime.toEpochMilli() / 1000L)); + } + + /** + * Specifies the assumptions to use when calculating time in traffic. This parameter may only be + * specified when the travel mode is driving and the request includes a departure_time. + * + * @param trafficModel The traffic model to use in estimating time in traffic. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. + */ + public DistanceMatrixApiRequest trafficModel(TrafficModel trafficModel) { + return param("traffic_model", trafficModel); } /** * Specifies the desired time of arrival for transit requests. You can specify either * departure_time or arrival_time, but not both. + * + * @param arrivalTime The preferred arrival time. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ - public DistanceMatrixApiRequest arrivalTime(ReadableInstant arrivalTime) { - return param("arrival_time", Long.toString(arrivalTime.getMillis() / 1000L)); + public DistanceMatrixApiRequest arrivalTime(Instant arrivalTime) { + return param("arrival_time", Long.toString(arrivalTime.toEpochMilli() / 1000L)); } /** * Specifies one or more preferred modes of transit. This parameter may only be specified for * requests where the mode is transit. + * + * @param transitModes The preferred transit modes. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest transitModes(TransitMode... transitModes) { return param("transit_mode", join('|', transitModes)); } /** - * Specifies preferences for transit requests. Using this parameter, - * you can bias the options returned, rather than accepting the default best route chosen by - * the API. + * Specifies preferences for transit requests. Using this parameter, you can bias the options + * returned, rather than accepting the default best route chosen by the API. + * + * @param pref The transit routing preference for this distance matrix. + * @return Returns this {@code DistanceMatrixApiRequest} for call chaining. */ public DistanceMatrixApiRequest transitRoutingPreference(TransitRoutingPreference pref) { return param("transit_routing_preference", pref); } -} \ No newline at end of file +} diff --git a/src/main/java/com/google/maps/ElevationApi.java b/src/main/java/com/google/maps/ElevationApi.java index 54c73e4e4..adad922c3 100644 --- a/src/main/java/com/google/maps/ElevationApi.java +++ b/src/main/java/com/google/maps/ElevationApi.java @@ -26,51 +26,72 @@ import com.google.maps.model.LatLng; /** - *

    The Google Elevation API provides you a simple interface to query locations - * on the earth for elevation data. Additionally, you may request sampled elevation - * data along paths, allowing you to calculate elevation changes along routes. - *

    See documentation. + * The Google Elevation API provides a simple interface to query locations on the earth for + * elevation data. Additionally, you may request sampled elevation data along paths, allowing you to + * calculate elevation changes along routes. + * + *

    See the Google Maps + * Elevation API documentation. */ public class ElevationApi { private static final ApiConfig API_CONFIG = new ApiConfig("/maps/api/elevation/json"); - private ElevationApi() { - } + private ElevationApi() {} /** - * See documentation. + * Gets a list of elevations for a list of points. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param points The points to retrieve elevations for. + * @return The elevations as a {@link PendingResult}. */ - public static PendingResult getByPoints(GeoApiContext context, - LatLng... points) { - return context.get(API_CONFIG, MultiResponse.class, - "locations", shortestParam(points)); + public static PendingResult getByPoints( + GeoApiContext context, LatLng... points) { + return context.get(API_CONFIG, MultiResponse.class, "locations", shortestParam(points)); } /** - * See documentation. + * See + * documentation. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param samples The number of samples to retrieve heights along {@code path}. + * @param path The path to sample. + * @return The elevations as a {@link PendingResult}. */ - public static PendingResult getByPath(GeoApiContext context, - int samples, - LatLng... path) { - return context.get(API_CONFIG, MultiResponse.class, - "samples", String.valueOf(samples), - "path", shortestParam(path)); + public static PendingResult getByPath( + GeoApiContext context, int samples, LatLng... path) { + return context.get( + API_CONFIG, + MultiResponse.class, + "samples", + String.valueOf(samples), + "path", + shortestParam(path)); } /** - * See documentation. + * See + * documentation. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param samples The number of samples to retrieve heights along {@code encodedPolyline}. + * @param encodedPolyline The path to sample as an encoded polyline. + * @return The elevations as a {@link PendingResult}. */ - public static PendingResult getByPath(GeoApiContext context, - int samples, - EncodedPolyline encodedPolyline) { - return context.get(API_CONFIG, MultiResponse.class, - "samples", String.valueOf(samples), - "path", "enc:" + encodedPolyline.getEncodedPath()); + public static PendingResult getByPath( + GeoApiContext context, int samples, EncodedPolyline encodedPolyline) { + return context.get( + API_CONFIG, + MultiResponse.class, + "samples", + String.valueOf(samples), + "path", + "enc:" + encodedPolyline.getEncodedPath()); } /** - * Chooses the shortest param (only a guess, since the - * length is different after URL encoding). + * Chooses the shortest param (only a guess, since the length is different after URL encoding). */ private static String shortestParam(LatLng[] points) { String joined = join('|', points); @@ -78,14 +99,15 @@ private static String shortestParam(LatLng[] points) { return joined.length() < encoded.length() ? joined : encoded; } - /** - * Retrieve the elevation of a single point. + /** + * Retrieves the elevation of a single location. * - *

    For more detail, please see the - * documentation. + * @param context The {@link GeoApiContext} to make requests through. + * @param location The location to retrieve the elevation for. + * @return The elevation as a {@link PendingResult}. */ - public static PendingResult getByPoint(GeoApiContext context, LatLng point) { - return context.get(API_CONFIG, SingularResponse.class, "locations", point.toString()); + public static PendingResult getByPoint(GeoApiContext context, LatLng location) { + return context.get(API_CONFIG, SingularResponse.class, "locations", location.toString()); } private static class SingularResponse implements ApiResponse { @@ -113,14 +135,16 @@ public ApiException getError() { } /** - * Retrieve the elevations of an encoded polyline path. + * Retrieves the elevations of an encoded polyline path. * - *

    See documentation. + * @param context The {@link GeoApiContext} to make requests through. + * @param encodedPolyline The encoded polyline to retrieve elevations for. + * @return The elevations as a {@link PendingResult}. */ - public static PendingResult getByPoints(GeoApiContext context, - EncodedPolyline encodedPolyline) { - return context.get(API_CONFIG, MultiResponse.class, - "locations", "enc:" + encodedPolyline.getEncodedPath()); + public static PendingResult getByPoints( + GeoApiContext context, EncodedPolyline encodedPolyline) { + return context.get( + API_CONFIG, MultiResponse.class, "locations", "enc:" + encodedPolyline.getEncodedPath()); } private static class MultiResponse implements ApiResponse { diff --git a/src/main/java/com/google/maps/FindPlaceFromTextRequest.java b/src/main/java/com/google/maps/FindPlaceFromTextRequest.java new file mode 100644 index 000000000..667738b18 --- /dev/null +++ b/src/main/java/com/google/maps/FindPlaceFromTextRequest.java @@ -0,0 +1,215 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.internal.StringJoin; +import com.google.maps.internal.StringJoin.UrlValue; +import com.google.maps.model.FindPlaceFromText; +import com.google.maps.model.LatLng; +import com.google.maps.model.PlacesSearchResult; + +public class FindPlaceFromTextRequest + extends PendingResultBase< + FindPlaceFromText, FindPlaceFromTextRequest, FindPlaceFromTextRequest.Response> { + + static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/place/findplacefromtext/json") + .fieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .supportsClientId(false); + + public FindPlaceFromTextRequest(GeoApiContext context) { + super(context, API_CONFIG, Response.class); + } + + public enum InputType implements UrlValue { + TEXT_QUERY("textquery"), + PHONE_NUMBER("phonenumber"); + + private final String inputType; + + InputType(final String inputType) { + this.inputType = inputType; + } + + @Override + public String toUrlValue() { + return this.inputType; + } + } + + /** + * The text input specifying which place to search for (for example, a name, address, or phone + * number). + * + * @param input The text input. + * @return Returns {@code FindPlaceFromTextRequest} for call chaining. + */ + public FindPlaceFromTextRequest input(String input) { + return param("input", input); + } + + /** + * The type of input. + * + * @param inputType The input type. + * @return Returns {@code FindPlaceFromTextRequest} for call chaining. + */ + public FindPlaceFromTextRequest inputType(InputType inputType) { + return param("inputtype", inputType); + } + + /** + * The fields specifying the types of place data to return. + * + * @param fields The fields to return. + * @return Returns {@code FindPlaceFromTextRequest} for call chaining. + */ + public FindPlaceFromTextRequest fields(FieldMask... fields) { + return param("fields", StringJoin.join(',', fields)); + } + + /** + * Prefer results in a specified area, by specifying either a radius plus lat/lng, or two lat/lng + * pairs representing the points of a rectangle. + * + * @param locationBias The location bias for this request. + * @return Returns {@code FindPlaceFromTextRequest} for call chaining. + */ + public FindPlaceFromTextRequest locationBias(LocationBias locationBias) { + return param("locationbias", locationBias); + } + + @Override + protected void validateRequest() { + if (!params().containsKey("input")) { + throw new IllegalArgumentException("Request must contain 'input'."); + } + if (!params().containsKey("inputtype")) { + throw new IllegalArgumentException("Request must contain 'inputType'."); + } + } + + public static class Response implements ApiResponse { + + public String status; + public PlacesSearchResult candidates[]; + public String errorMessage; + + @Override + public boolean successful() { + return "OK".equals(status) || "ZERO_RESULTS".equals(status); + } + + @Override + public FindPlaceFromText getResult() { + FindPlaceFromText result = new FindPlaceFromText(); + result.candidates = candidates; + return result; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(status, errorMessage); + } + } + + public enum FieldMask implements UrlValue { + BUSINESS_STATUS("business_status"), + FORMATTED_ADDRESS("formatted_address"), + GEOMETRY("geometry"), + ICON("icon"), + ID("id"), + NAME("name"), + OPENING_HOURS("opening_hours"), + @Deprecated + PERMANENTLY_CLOSED("permanently_closed"), + PHOTOS("photos"), + PLACE_ID("place_id"), + PRICE_LEVEL("price_level"), + RATING("rating"), + TYPES("types"); + + private final String field; + + FieldMask(final String field) { + this.field = field; + } + + @Override + public String toUrlValue() { + return field; + } + } + + public interface LocationBias extends UrlValue {} + + public static class LocationBiasIP implements LocationBias { + @Override + public String toUrlValue() { + return "ipbias"; + } + } + + public static class LocationBiasPoint implements LocationBias { + private final LatLng point; + + public LocationBiasPoint(LatLng point) { + this.point = point; + } + + @Override + public String toUrlValue() { + return "point:" + point.toUrlValue(); + } + } + + public static class LocationBiasCircular implements LocationBias { + private final LatLng center; + private final int radius; + + public LocationBiasCircular(LatLng center, int radius) { + this.center = center; + this.radius = radius; + } + + @Override + public String toUrlValue() { + return "circle:" + radius + "@" + center.toUrlValue(); + } + } + + public static class LocationBiasRectangular implements LocationBias { + private final LatLng southWest; + private final LatLng northEast; + + public LocationBiasRectangular(LatLng southWest, LatLng northEast) { + this.southWest = southWest; + this.northEast = northEast; + } + + @Override + public String toUrlValue() { + return "rectangle:" + southWest.toUrlValue() + "|" + northEast.toUrlValue(); + } + } +} diff --git a/src/main/java/com/google/maps/GaeRequestHandler.java b/src/main/java/com/google/maps/GaeRequestHandler.java new file mode 100644 index 000000000..ff026db32 --- /dev/null +++ b/src/main/java/com/google/maps/GaeRequestHandler.java @@ -0,0 +1,169 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.appengine.api.urlfetch.FetchOptions; +import com.google.appengine.api.urlfetch.HTTPHeader; +import com.google.appengine.api.urlfetch.HTTPMethod; +import com.google.appengine.api.urlfetch.HTTPRequest; +import com.google.appengine.api.urlfetch.URLFetchService; +import com.google.appengine.api.urlfetch.URLFetchServiceFactory; +import com.google.gson.FieldNamingPolicy; +import com.google.maps.GeoApiContext.RequestHandler; +import com.google.maps.internal.ApiResponse; +import com.google.maps.internal.ExceptionsAllowedToRetry; +import com.google.maps.internal.GaePendingResult; +import com.google.maps.internal.HttpHeaders; +import com.google.maps.metrics.RequestMetrics; +import java.net.MalformedURLException; +import java.net.Proxy; +import java.net.URL; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A strategy for handling URL requests using Google App Engine's URL Fetch API. + * + * @see com.google.maps.GeoApiContext.RequestHandler + */ +public class GaeRequestHandler implements GeoApiContext.RequestHandler { + private static final Logger LOG = LoggerFactory.getLogger(GaeRequestHandler.class.getName()); + private final URLFetchService client = URLFetchServiceFactory.getURLFetchService(); + + /* package */ GaeRequestHandler() {} + + @Override + public > PendingResult handle( + String hostName, + String url, + String userAgent, + String experienceIdHeaderValue, + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeout, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics) { + FetchOptions fetchOptions = FetchOptions.Builder.withDeadline(10); + HTTPRequest req; + try { + req = new HTTPRequest(new URL(hostName + url), HTTPMethod.POST, fetchOptions); + if (experienceIdHeaderValue != null) { + req.setHeader( + new HTTPHeader(HttpHeaders.X_GOOG_MAPS_EXPERIENCE_ID, experienceIdHeaderValue)); + } + } catch (MalformedURLException e) { + LOG.error("Request: {}{}", hostName, url, e); + throw (new RuntimeException(e)); + } + + return new GaePendingResult<>( + req, + client, + clazz, + fieldNamingPolicy, + errorTimeout, + maxRetries, + exceptionsAllowedToRetry, + metrics); + } + + @Override + public > PendingResult handlePost( + String hostName, + String url, + String payload, + String userAgent, + String experienceIdHeaderValue, + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeout, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics) { + FetchOptions fetchOptions = FetchOptions.Builder.withDeadline(10); + HTTPRequest req = null; + try { + req = new HTTPRequest(new URL(hostName + url), HTTPMethod.POST, fetchOptions); + req.setHeader(new HTTPHeader("Content-Type", "application/json; charset=utf-8")); + if (experienceIdHeaderValue != null) { + req.setHeader( + new HTTPHeader(HttpHeaders.X_GOOG_MAPS_EXPERIENCE_ID, experienceIdHeaderValue)); + } + req.setPayload(payload.getBytes(UTF_8)); + } catch (MalformedURLException e) { + LOG.error("Request: {}{}", hostName, url, e); + throw (new RuntimeException(e)); + } + + return new GaePendingResult<>( + req, + client, + clazz, + fieldNamingPolicy, + errorTimeout, + maxRetries, + exceptionsAllowedToRetry, + metrics); + } + + @Override + public void shutdown() { + // do nothing + } + + /** Builder strategy for constructing {@code GaeRequestHandler}. */ + public static class Builder implements GeoApiContext.RequestHandler.Builder { + + @Override + public Builder connectTimeout(long timeout, TimeUnit unit) { + throw new RuntimeException("connectTimeout not implemented for Google App Engine"); + } + + @Override + public Builder readTimeout(long timeout, TimeUnit unit) { + throw new RuntimeException("readTimeout not implemented for Google App Engine"); + } + + @Override + public Builder writeTimeout(long timeout, TimeUnit unit) { + throw new RuntimeException("writeTimeout not implemented for Google App Engine"); + } + + @Override + public Builder queriesPerSecond(int maxQps) { + throw new RuntimeException("queriesPerSecond not implemented for Google App Engine"); + } + + @Override + public Builder proxy(Proxy proxy) { + throw new RuntimeException("setProxy not implemented for Google App Engine"); + } + + @Override + public Builder proxyAuthentication(String proxyUserName, String proxyUserPassword) { + throw new RuntimeException("setProxyAuthentication not implemented for Google App Engine"); + } + + @Override + public RequestHandler build() { + return new GaeRequestHandler(); + } + } +} diff --git a/src/main/java/com/google/maps/GeoApiContext.java b/src/main/java/com/google/maps/GeoApiContext.java index 2f23931fa..222dacdff 100644 --- a/src/main/java/com/google/maps/GeoApiContext.java +++ b/src/main/java/com/google/maps/GeoApiContext.java @@ -16,92 +16,304 @@ package com.google.maps; import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.errors.OverQueryLimitException; import com.google.maps.internal.ApiConfig; import com.google.maps.internal.ApiResponse; -import com.google.maps.internal.ExceptionResult; -import com.google.maps.internal.OkHttpPendingResult; -import com.google.maps.internal.RateLimitExecutorService; +import com.google.maps.internal.ExceptionsAllowedToRetry; +import com.google.maps.internal.HttpHeaders; +import com.google.maps.internal.StringJoin; import com.google.maps.internal.UrlSigner; - -import com.squareup.okhttp.Dispatcher; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; - +import com.google.maps.metrics.NoOpRequestMetricsReporter; +import com.google.maps.metrics.RequestMetrics; +import com.google.maps.metrics.RequestMetricsReporter; +import java.io.Closeable; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.Proxy; import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; /** * The entry point for making requests against the Google Geo APIs. + * + *

    Construct this object by using the enclosed {@link GeoApiContext.Builder}. + * + *

    GeoApiContexts should be shared

    + * + * GeoApiContext works best when you create a single GeoApiContext instance, or one per API key, and + * reuse it for all your Google Geo API queries. This is because each GeoApiContext manages its own + * thread pool, back-end client, and other resources. + * + *

    When you are finished with a GeoApiContext object, you must call {@link #shutdown()} on it to + * release its resources. */ -public class GeoApiContext { - private static final String VERSION = "@VERSION@"; // Populated by the build script +public class GeoApiContext implements Closeable { + + private static final String VERSION = "@VERSION@"; // Populated by the build script private static final String USER_AGENT = "GoogleGeoApiClientJava/" + VERSION; private static final int DEFAULT_BACKOFF_TIMEOUT_MILLIS = 60 * 1000; // 60s - private String baseUrlOverride; - private String apiKey; - private String clientId; - private UrlSigner urlSigner; - private final OkHttpClient client = new OkHttpClient(); - private final RateLimitExecutorService rateLimitExecutorService; + private final RequestHandler requestHandler; + private final String apiKey; + private final String baseUrlOverride; + private final String channel; + private final String clientId; + private final long errorTimeout; + private final ExceptionsAllowedToRetry exceptionsAllowedToRetry; + private final Integer maxRetries; + private final UrlSigner urlSigner; + private String experienceIdHeaderValue; + private final RequestMetricsReporter requestMetricsReporter; + + /* package */ + GeoApiContext( + RequestHandler requestHandler, + String apiKey, + String baseUrlOverride, + String channel, + String clientId, + long errorTimeout, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + Integer maxRetries, + UrlSigner urlSigner, + RequestMetricsReporter requestMetricsReporter, + String... experienceIdHeaderValue) { + this.requestHandler = requestHandler; + this.apiKey = apiKey; + this.baseUrlOverride = baseUrlOverride; + this.channel = channel; + this.clientId = clientId; + this.errorTimeout = errorTimeout; + this.exceptionsAllowedToRetry = exceptionsAllowedToRetry; + this.maxRetries = maxRetries; + this.urlSigner = urlSigner; + this.requestMetricsReporter = requestMetricsReporter; + setExperienceId(experienceIdHeaderValue); + } + + /** + * standard Java API to reclaim resources + * + * @throws IOException + */ + @Override + public void close() throws IOException { + shutdown(); + } + + /** + * The service provider interface that enables requests to be handled via switchable back ends. + * There are supplied implementations of this interface for both OkHttp and Google App Engine's + * URL Fetch API. + * + * @see OkHttpRequestHandler + * @see GaeRequestHandler + */ + public interface RequestHandler { + + > PendingResult handle( + String hostName, + String url, + String userAgent, + String experienceIdHeaderValue, + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeout, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics); + + > PendingResult handlePost( + String hostName, + String url, + String payload, + String userAgent, + String experienceIdHeaderValue, + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeout, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics); + + void shutdown(); + + /** Builder pattern for {@code GeoApiContext.RequestHandler}. */ + interface Builder { + + Builder connectTimeout(long timeout, TimeUnit unit); + + Builder readTimeout(long timeout, TimeUnit unit); + + Builder writeTimeout(long timeout, TimeUnit unit); + + Builder queriesPerSecond(int maxQps); + + Builder proxy(Proxy proxy); + + Builder proxyAuthentication(String proxyUserName, String proxyUserPassword); + + RequestHandler build(); + } + } + + /** + * Sets the value for the HTTP header field name {@link HttpHeaders#X_GOOG_MAPS_EXPERIENCE_ID} to + * be used on subsequent API calls. Calling this method with {@code null} is equivalent to calling + * {@link #clearExperienceId()}. + * + * @param experienceId The experience ID if set, otherwise null + */ + public void setExperienceId(String... experienceId) { + if (experienceId == null || experienceId.length == 0) { + experienceIdHeaderValue = null; + return; + } + experienceIdHeaderValue = StringJoin.join(",", experienceId); + } + + /** @return Returns the experience ID if set, otherwise, null */ + public String getExperienceId() { + return experienceIdHeaderValue; + } - private static final Logger LOG = Logger.getLogger(GeoApiContext.class.getName()); - private long errorTimeout = DEFAULT_BACKOFF_TIMEOUT_MILLIS; + /** + * Clears the experience ID if set the HTTP header field {@link + * HttpHeaders#X_GOOG_MAPS_EXPERIENCE_ID} will be omitted from subsequent calls. + */ + public void clearExperienceId() { + experienceIdHeaderValue = null; + } - public GeoApiContext() { - rateLimitExecutorService = new RateLimitExecutorService(); - client.setDispatcher(new Dispatcher(rateLimitExecutorService)); + /** + * Shut down this GeoApiContext instance, reclaiming resources. After shutdown() has been called, + * no further queries may be done against this instance. + */ + public void shutdown() { + requestHandler.shutdown(); } - > PendingResult get(ApiConfig config, Class clazz, - Map params) { + > PendingResult get( + ApiConfig config, Class clazz, Map> params) { + if (channel != null && !channel.isEmpty() && !params.containsKey("channel")) { + params.put("channel", Collections.singletonList(channel)); + } + StringBuilder query = new StringBuilder(); - for (Map.Entry param : params.entrySet()) { - query.append('&').append(param.getKey()).append("="); - try { - query.append(URLEncoder.encode(param.getValue(), "UTF-8")); - } catch (UnsupportedEncodingException e) { - return new ExceptionResult(e); + for (Map.Entry> param : params.entrySet()) { + List values = param.getValue(); + for (String value : values) { + query.append('&').append(param.getKey()).append("="); + try { + query.append(URLEncoder.encode(value, "UTF-8")); + } catch (UnsupportedEncodingException e) { + // This should never happen. UTF-8 support is required for every Java implementation. + throw new IllegalStateException(e); + } } } - return getWithPath(clazz, config.fieldNamingPolicy, config.hostName, config.path, - config.supportsClientId, query.toString()); + return getWithPath( + clazz, + config.fieldNamingPolicy, + config.hostName, + config.path, + config.supportsClientId, + query.toString(), + requestMetricsReporter.newRequest(config.path)); } - > PendingResult get(ApiConfig config, Class clazz, - String... params) { + > PendingResult get( + ApiConfig config, Class clazz, String... params) { if (params.length % 2 != 0) { throw new IllegalArgumentException("Params must be matching key/value pairs."); } StringBuilder query = new StringBuilder(); - for (int i = 0; i < params.length; i++) { + boolean channelSet = false; + for (int i = 0; i < params.length; i += 2) { + if (params[i].equals("channel")) { + channelSet = true; + } query.append('&').append(params[i]).append('='); - i++; // URL-encode the parameter. try { - query.append(URLEncoder.encode(params[i], "UTF-8")); + query.append(URLEncoder.encode(params[i + 1], "UTF-8")); } catch (UnsupportedEncodingException e) { - return new ExceptionResult(e); + // This should never happen. UTF-8 support is required for every Java implementation. + throw new IllegalStateException(e); } } - return getWithPath(clazz, config.fieldNamingPolicy, config.hostName, config.path, - config.supportsClientId, query.toString()); + // Channel can be supplied per-request or per-context. We prioritize it from the request, + // so if it's not provided there, provide it here + if (!channelSet && channel != null && !channel.isEmpty()) { + query.append("&channel=").append(channel); + } + + return getWithPath( + clazz, + config.fieldNamingPolicy, + config.hostName, + config.path, + config.supportsClientId, + query.toString(), + requestMetricsReporter.newRequest(config.path)); } - private > PendingResult getWithPath(Class clazz, - FieldNamingPolicy fieldNamingPolicy, String hostName, String path, - boolean canUseClientId, String encodedPath) { + > PendingResult post( + ApiConfig config, Class clazz, Map> params) { + + checkContext(config.supportsClientId); + + StringBuilder url = new StringBuilder(config.path); + if (config.supportsClientId && clientId != null) { + url.append("?client=").append(clientId); + } else { + url.append("?key=").append(apiKey); + } + + if (config.supportsClientId && urlSigner != null) { + String signature = urlSigner.getSignature(url.toString()); + url.append("&signature=").append(signature); + } + + String hostName = config.hostName; + if (baseUrlOverride != null) { + hostName = baseUrlOverride; + } + + return requestHandler.handlePost( + hostName, + url.toString(), + params.get("_payload").get(0), + USER_AGENT, + experienceIdHeaderValue, + clazz, + config.fieldNamingPolicy, + errorTimeout, + maxRetries, + exceptionsAllowedToRetry, + requestMetricsReporter.newRequest(config.path)); + } + + private > PendingResult getWithPath( + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + String hostName, + String path, + boolean canUseClientId, + String encodedPath, + RequestMetrics metrics) { checkContext(canUseClientId); if (!encodedPath.startsWith("&")) { throw new IllegalArgumentException("encodedPath must start with &"); @@ -115,33 +327,31 @@ private > PendingResult getWithPath(Class claz } url.append(encodedPath); - if (canUseClientId && clientId != null) { - try { - String signature = urlSigner.getSignature(url.toString()); - url.append("&signature=").append(signature); - } catch (Exception e) { - return new ExceptionResult(e); - } + if (canUseClientId && urlSigner != null) { + String signature = urlSigner.getSignature(url.toString()); + url.append("&signature=").append(signature); } if (baseUrlOverride != null) { hostName = baseUrlOverride; } - Request req = new Request.Builder() - .get() - .header("User-Agent", USER_AGENT) - .url(hostName + url).build(); - - LOG.log(Level.INFO, "Request: {0}", hostName + url); - - return new OkHttpPendingResult(req, client, clazz, fieldNamingPolicy, errorTimeout); + return requestHandler.handle( + hostName, + url.toString(), + USER_AGENT, + experienceIdHeaderValue, + clazz, + fieldNamingPolicy, + errorTimeout, + maxRetries, + exceptionsAllowedToRetry, + metrics); } private void checkContext(boolean canUseClientId) { if (urlSigner == null && apiKey == null) { - throw new IllegalStateException( - "Must provide either API key or Maps for Work credentials."); + throw new IllegalStateException("Must provide either API key or Maps for Work credentials."); } else if (!canUseClientId && apiKey == null) { throw new IllegalStateException( "API does not support client ID & secret - you must provide a key"); @@ -151,93 +361,278 @@ private void checkContext(boolean canUseClientId) { } } - /** - * Override the base URL of the API endpoint. Useful only for testing. - * @param baseUrl The URL to use, without a trailing slash, e.g. https://maps.googleapis.com - */ - GeoApiContext setBaseUrlForTesting(String baseUrl) { - baseUrlOverride = baseUrl; - return this; - } + /** The Builder for {@code GeoApiContext}. */ + public static class Builder { + + private RequestHandler.Builder builder; + + private String apiKey; + private String baseUrlOverride; + private String channel; + private String clientId; + private long errorTimeout = DEFAULT_BACKOFF_TIMEOUT_MILLIS; + private ExceptionsAllowedToRetry exceptionsAllowedToRetry = new ExceptionsAllowedToRetry(); + private Integer maxRetries; + private UrlSigner urlSigner; + private RequestMetricsReporter requestMetricsReporter = new NoOpRequestMetricsReporter(); + private String[] experienceIdHeaderValue; + + /** Builder pattern for the enclosing {@code GeoApiContext}. */ + public Builder() { + requestHandlerBuilder(new OkHttpRequestHandler.Builder()); + } - public GeoApiContext setApiKey(String apiKey) { - this.apiKey = apiKey; - return this; - } + public Builder(RequestHandler.Builder builder) { + requestHandlerBuilder(builder); + } - public GeoApiContext setEnterpriseCredentials(String clientId, String cryptographicSecret) { - this.clientId = clientId; - this.urlSigner = new UrlSigner(cryptographicSecret); - return this; - } + /** + * Changes the RequestHandler.Builder strategy to change between the {@code + * OkHttpRequestHandler} and the {@code GaeRequestHandler}. + * + * @param builder The {@code RequestHandler.Builder} to use for {@link #build()} + * @return Returns this builder for call chaining. + * @see OkHttpRequestHandler + * @see GaeRequestHandler + */ + public Builder requestHandlerBuilder(RequestHandler.Builder builder) { + this.builder = builder; + this.exceptionsAllowedToRetry.add(OverQueryLimitException.class); + return this; + } - /** - * Sets the default connect timeout for new connections. A value of 0 means no timeout. - * - * @see java.net.URLConnection#setConnectTimeout(int) - */ - public GeoApiContext setConnectTimeout(long timeout, TimeUnit unit) { - client.setConnectTimeout(timeout, unit); - return this; - } + /** + * Overrides the base URL of the API endpoint. Useful for testing or certain international usage + * scenarios. + * + * @param baseUrl The URL to use, without a trailing slash, e.g. https://maps.googleapis.com + * @return Returns this builder for call chaining. + */ + Builder baseUrlOverride(String baseUrl) { + baseUrlOverride = baseUrl; + return this; + } - /** - * Sets the default read timeout for new connections. A value of 0 means no timeout. - * - * @see java.net.URLConnection#setReadTimeout(int) - */ - public GeoApiContext setReadTimeout(long timeout, TimeUnit unit) { - client.setReadTimeout(timeout, unit); - return this; - } + /** + * Older name for {@link #baseUrlOverride(String)}. This was used back when testing was the only + * use case foreseen for this. + * + * @deprecated Use baseUrlOverride(String) instead. + * @param baseUrl The URL to use, without a trailing slash, e.g. https://maps.googleapis.com + * @return Returns this builder for call chaining. + */ + @Deprecated + Builder baseUrlForTesting(String baseUrl) { + return baseUrlOverride(baseUrl); + } - /** - * Sets the default write timeout for new connections. A value of 0 means no timeout. - */ - public GeoApiContext setWriteTimeout(long timeout, TimeUnit unit) { - client.setWriteTimeout(timeout, unit); - return this; - } + /** + * Sets the API Key to use for authorizing requests. + * + * @param apiKey The API Key to use. + * @return Returns this builder for call chaining. + */ + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } - /** - * Sets the time limit for which retry-able errors will be retried. Defaults to 60 seconds. Set - * to zero to disable. - */ - public GeoApiContext setRetryTimeout(long timeout, TimeUnit unit) { - this.errorTimeout = unit.toMillis(timeout); - return this; - } + /** + * Sets the ClientID/Secret pair to use for authorizing requests. Most users should use {@link + * #apiKey(String)} instead. + * + * @param clientId The Client ID to use. + * @param cryptographicSecret The Secret to use. + * @return Returns this builder for call chaining. + */ + public Builder enterpriseCredentials(String clientId, String cryptographicSecret) { + this.clientId = clientId; + try { + this.urlSigner = new UrlSigner(cryptographicSecret); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException(e); + } + return this; + } - /** - * Sets the maximum number of queries that will be executed during a 1 second interval. - * The default is 10. A minimum interval between requests will also be enforced, - * set to 1/(2 * {@code maxQps}). - */ - public GeoApiContext setQueryRateLimit(int maxQps) { - rateLimitExecutorService.setQueriesPerSecond(maxQps); - return this; - } + /** + * Sets the default channel for requests (can be overridden by requests). Only useful for Google + * Maps for Work clients. + * + * @param channel The channel to use for analytics + * @return Returns this builder for call chaining. + */ + public Builder channel(String channel) { + this.channel = channel; + return this; + } - /** - * Sets the rate at which queries are executed. - * - * @param maxQps The maximum number of queries to execute per second. - * @param minimumInterval The minimum amount of time, in milliseconds, to pause between requests. - * Note that this pause only occurs if the amount of time between requests has not elapsed - * naturally. - */ - public GeoApiContext setQueryRateLimit(int maxQps, int minimumInterval) { - rateLimitExecutorService.setQueriesPerSecond(maxQps, minimumInterval); - return this; - } + /** + * Sets the default connect timeout for new connections. A value of 0 means no timeout. + * + * @see java.net.URLConnection#setConnectTimeout(int) + * @param timeout The connect timeout period in {@code unit}s. + * @param unit The connect timeout time unit. + * @return Returns this builder for call chaining. + */ + public Builder connectTimeout(long timeout, TimeUnit unit) { + builder.connectTimeout(timeout, unit); + return this; + } - /** - * Sets the proxy for new connections. - * - * @param proxy The proxy to be used by the underlying HTTP client. - */ - public GeoApiContext setProxy(Proxy proxy) { - client.setProxy(proxy == null ? Proxy.NO_PROXY : proxy); - return this; + /** + * Sets the default read timeout for new connections. A value of 0 means no timeout. + * + * @see java.net.URLConnection#setReadTimeout(int) + * @param timeout The read timeout period in {@code unit}s. + * @param unit The read timeout time unit. + * @return Returns this builder for call chaining. + */ + public Builder readTimeout(long timeout, TimeUnit unit) { + builder.readTimeout(timeout, unit); + return this; + } + + /** + * Sets the default write timeout for new connections. A value of 0 means no timeout. + * + * @param timeout The write timeout period in {@code unit}s. + * @param unit The write timeout time unit. + * @return Returns this builder for call chaining. + */ + public Builder writeTimeout(long timeout, TimeUnit unit) { + builder.writeTimeout(timeout, unit); + return this; + } + + /** + * Sets the cumulative time limit for which retry-able errors will be retried. Defaults to 60 + * seconds. Set to zero to retry requests forever. + * + *

    This operates separately from the count-based {@link #maxRetries(Integer)}. + * + * @param timeout The retry timeout period in {@code unit}s. + * @param unit The retry timeout time unit. + * @return Returns this builder for call chaining. + */ + public Builder retryTimeout(long timeout, TimeUnit unit) { + this.errorTimeout = unit.toMillis(timeout); + return this; + } + + /** + * Sets the maximum number of times each retry-able errors will be retried. Set this to null to + * not have a max number. Set this to zero to disable retries. + * + *

    This operates separately from the time-based {@link #retryTimeout(long, TimeUnit)}. + * + * @param maxRetries The maximum number of times to retry. + * @return Returns this builder for call chaining. + */ + public Builder maxRetries(Integer maxRetries) { + this.maxRetries = maxRetries; + return this; + } + + /** + * Disables retries completely, by setting max retries to 0 and retry timeout to 0. + * + * @return Returns this builder for call chaining. + */ + public Builder disableRetries() { + maxRetries(0); + retryTimeout(0, TimeUnit.MILLISECONDS); + return this; + } + + /** + * Sets the maximum number of queries that will be executed during a 1 second interval. The + * default is 50. A minimum interval between requests will also be enforced, set to 1/(2 * + * {@code maxQps}). + * + * @param maxQps The maximum queries per second. + * @return Returns this builder for call chaining. + */ + public Builder queryRateLimit(int maxQps) { + builder.queriesPerSecond(maxQps); + return this; + } + + /** + * Allows specific API exceptions to be retried or not retried. + * + * @param exception The {@code ApiException} to allow or deny being re-tried. + * @param allowedToRetry Whether to allow or deny re-trying {@code exception}. + * @return Returns this builder for call chaining. + */ + public Builder setIfExceptionIsAllowedToRetry( + Class exception, boolean allowedToRetry) { + if (allowedToRetry) { + exceptionsAllowedToRetry.add(exception); + } else { + exceptionsAllowedToRetry.remove(exception); + } + return this; + } + + /** + * Sets the proxy for new connections. + * + * @param proxy The proxy to be used by the underlying HTTP client. + * @return Returns this builder for call chaining. + */ + public Builder proxy(Proxy proxy) { + builder.proxy(proxy == null ? Proxy.NO_PROXY : proxy); + return this; + } + + /** + * set authentication for proxy + * + * @param proxyUserName username for proxy authentication + * @param proxyUserPassword username for proxy authentication + * @return Returns this builder for call chaining. + */ + public Builder proxyAuthentication(String proxyUserName, String proxyUserPassword) { + builder.proxyAuthentication(proxyUserName, proxyUserPassword); + return this; + } + + /** + * Sets the value for the HTTP header field name {@link HttpHeaders#X_GOOG_MAPS_EXPERIENCE_ID} + * HTTP header value for the field name on subsequent API calls. + * + * @param experienceId The experience ID + * @return Returns this builder for call chaining. + */ + public Builder experienceId(String... experienceId) { + this.experienceIdHeaderValue = experienceId; + return this; + } + + public Builder requestMetricsReporter(RequestMetricsReporter requestMetricsReporter) { + this.requestMetricsReporter = requestMetricsReporter; + return this; + } + + /** + * Converts this builder into a {@code GeoApiContext}. + * + * @return Returns the built {@code GeoApiContext}. + */ + public GeoApiContext build() { + return new GeoApiContext( + builder.build(), + apiKey, + baseUrlOverride, + channel, + clientId, + errorTimeout, + exceptionsAllowedToRetry, + maxRetries, + urlSigner, + requestMetricsReporter, + experienceIdHeaderValue); + } } } diff --git a/src/main/java/com/google/maps/GeocodingApi.java b/src/main/java/com/google/maps/GeocodingApi.java index c0513fe74..b760ad15c 100644 --- a/src/main/java/com/google/maps/GeocodingApi.java +++ b/src/main/java/com/google/maps/GeocodingApi.java @@ -15,36 +15,39 @@ package com.google.maps; -import static com.google.maps.internal.StringJoin.join; - import com.google.maps.errors.ApiException; import com.google.maps.internal.ApiResponse; -import com.google.maps.internal.StringJoin.UrlValue; import com.google.maps.model.GeocodingResult; import com.google.maps.model.LatLng; /** - *

    Geocoding is the process of converting addresses - * (like "1600 Amphitheatre Parkway, Mountain View, CA") into geographic coordinates - * (like latitude 37.423021 and longitude -122.083739), which you can use to place markers or - * position the map. - *

    Reverse geocoding is the process of converting geographic coordinates into a human-readable - * address. - *

    See documentation. + * Geocoding is the process of converting addresses (like "1600 Amphitheatre Parkway, Mountain View, + * CA") into geographic coordinates (like latitude 37.423021 and longitude -122.083739), which you + * can use to place markers or position the map. Reverse geocoding is the process of converting + * geographic coordinates into a human-readable address. + * + * @see Geocoding + * documentation */ public class GeocodingApi { - private GeocodingApi() { - } + private GeocodingApi() {} /** - * create a new Geocoding API request. + * Creates a new Geocoding API request. + * + * @param context The {@link GeoApiContext} to make requests through. + * @return Returns the request, ready to run. */ public static GeocodingApiRequest newRequest(GeoApiContext context) { return new GeocodingApiRequest(context); } /** - * Request the latitude and longitude of an {@code address}. + * Requests the latitude and longitude of an {@code address}. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param address The address to geocode. + * @return Returns the request, ready to run. */ public static GeocodingApiRequest geocode(GeoApiContext context, String address) { GeocodingApiRequest request = new GeocodingApiRequest(context); @@ -53,7 +56,11 @@ public static GeocodingApiRequest geocode(GeoApiContext context, String address) } /** - * Request the street address of a {@code location}. + * Requests the street address of a {@code location}. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param location The location to reverse geocode. + * @return Returns the request, ready to run. */ public static GeocodingApiRequest reverseGeocode(GeoApiContext context, LatLng location) { GeocodingApiRequest request = new GeocodingApiRequest(context); @@ -61,7 +68,7 @@ public static GeocodingApiRequest reverseGeocode(GeoApiContext context, LatLng l return request; } - static class Response implements ApiResponse { + public static class Response implements ApiResponse { public String status; public String errorMessage; public GeocodingResult[] results; @@ -84,68 +91,4 @@ public ApiException getError() { return ApiException.from(status, errorMessage); } } - - /** - * This class represents a component filter for a geocode request. In a geocoding response, the - * Google Geocoding API can return address results restricted to a specific area. The restriction - * is specified using the components filter. - * - *

    Please see - * - * Component Filtering for more detail. - */ - public static class ComponentFilter implements UrlValue { - private final String component; - private final String value; - - ComponentFilter(String component, String value) { - this.component = component; - this.value = value; - } - - @Override - public String toString() { - return toUrlValue(); - } - - @Override - public String toUrlValue() { - return join(':', component, value); - } - - /** - * {@code route} matches long or short name of a route. - */ - public static ComponentFilter route(String route) { - return new ComponentFilter("route", route); - } - - /** - * {@code locality} matches against both locality and sublocality types. - */ - public static ComponentFilter locality(String locality) { - return new ComponentFilter("locality", locality); - } - - /** - * {@code administrativeArea} matches all the administrative area levels. - */ - public static ComponentFilter administrativeArea(String administrativeArea) { - return new ComponentFilter("administrative_area", administrativeArea); - } - - /** - * {@code postalCode} matches postal code and postal code prefix. - */ - public static ComponentFilter postalCode(String postalCode) { - return new ComponentFilter("postal_code", postalCode); - } - - /** - * {@code country} matches a country name or a two letter ISO 3166-1 country code. - */ - public static ComponentFilter country(String country) { - return new ComponentFilter("country" , country); - } - } } diff --git a/src/main/java/com/google/maps/GeocodingApiRequest.java b/src/main/java/com/google/maps/GeocodingApiRequest.java index 9945674fc..4a4d251ef 100644 --- a/src/main/java/com/google/maps/GeocodingApiRequest.java +++ b/src/main/java/com/google/maps/GeocodingApiRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 Google Inc. All rights reserved. + * Copyright 2016 Google Inc. All rights reserved. * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this @@ -19,13 +19,12 @@ import com.google.maps.internal.ApiConfig; import com.google.maps.model.AddressType; +import com.google.maps.model.ComponentFilter; import com.google.maps.model.GeocodingResult; import com.google.maps.model.LatLng; import com.google.maps.model.LocationType; -/** - * Request for the Geocoding API. - */ +/** A request for the Geocoding API. */ public class GeocodingApiRequest extends PendingResultBase { @@ -38,92 +37,118 @@ public GeocodingApiRequest(GeoApiContext context) { @Override protected void validateRequest() { // Must not have both address and latlng. - if (params().containsKey("latlng") && params().containsKey("address") + if (params().containsKey("latlng") + && params().containsKey("address") && params().containsKey("place_id")) { - throw new IllegalArgumentException("Request must contain only one of 'address', 'latlng' " - + "or 'place_id'."); + throw new IllegalArgumentException( + "Request must contain only one of 'address', 'latlng' or 'place_id'."); } // Must contain at least one of place_id, address, latlng, and components; - if (!params().containsKey("latlng") && !params().containsKey("address") - && !params().containsKey("components") && !params().containsKey("place_id")) { + if (!params().containsKey("latlng") + && !params().containsKey("address") + && !params().containsKey("components") + && !params().containsKey("place_id")) { throw new IllegalArgumentException( "Request must contain at least one of 'address', 'latlng', 'place_id' and 'components'."); } } /** - * Create a forward geocode for {@code address}. + * Creates a forward geocode for {@code address}. + * + * @param address The address to geocode. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest address(String address) { return param("address", address); } /** - * Create a forward geocode for {@code placeId}. + * Creates a forward geocode for {@code placeId}. + * + * @param placeId The Place ID to geocode. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest place(String placeId) { return param("place_id", placeId); } /** - * Create a reverse geocode for {@code latlng}. + * Creates a reverse geocode for {@code latlng}. + * + * @param latlng The location to reverse geocode. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest latlng(LatLng latlng) { return param("latlng", latlng); } /** - * Set the bounding box of the viewport within which to bias geocode results more prominently. - * This parameter will only influence, not fully restrict, results from the geocoder. ( + * Sets the bounding box of the viewport within which to bias geocode results more prominently. + * This parameter will only influence, not fully restrict, results from the geocoder. + * + *

    For more information see + * Viewport Biasing. * - *

    For more information see - * Viewports - * documentation. + * @param southWestBound The South West bound of the bounding box. + * @param northEastBound The North East bound of the bounding box. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest bounds(LatLng southWestBound, LatLng northEastBound) { return param("bounds", join('|', southWestBound, northEastBound)); } /** - * Set the region code, specified as a ccTLD ("top-level domain") two-character value. This + * Sets the region code, specified as a ccTLD ("top-level domain") two-character value. This * parameter will only influence, not fully restrict, results from the geocoder. * - *

    For more information see - * - * Region Codes. + *

    For more information see Region + * Biasing. + * + * @param region The region code to influence results. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest region(String region) { return param("region", region); } /** - * Set the component filters. Each component filter consists of a component:value pair and will + * Sets the component filters. Each component filter consists of a component:value pair and will * fully restrict the results from the geocoder. * - *

    For more information see - * + *

    For more information see * Component Filtering. + * + * @param filters Component filters to apply to the request. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ - public GeocodingApiRequest components(GeocodingApi.ComponentFilter... filters) { + public GeocodingApiRequest components(ComponentFilter... filters) { return param("components", join('|', filters)); } /** - * Set the result type. Specifying a type will restrict the results to this type. If multiple + * Sets the result type. Specifying a type will restrict the results to this type. If multiple * types are specified, the API will return all addresses that match any of the types. + * + * @param resultTypes The result types to restrict to. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest resultType(AddressType... resultTypes) { return param("result_type", join('|', resultTypes)); } /** - * Set the location type. Specifying a type will restrict the results to this type. If multiple + * Sets the location type. Specifying a type will restrict the results to this type. If multiple * types are specified, the API will return all addresses that match any of the types. + * + * @param locationTypes The location types to restrict to. + * @return Returns this {@code GeocodingApiRequest} for call chaining. */ public GeocodingApiRequest locationType(LocationType... locationTypes) { return param("location_type", join('|', locationTypes)); } - } - diff --git a/src/main/java/com/google/maps/GeolocationApi.java b/src/main/java/com/google/maps/GeolocationApi.java new file mode 100644 index 000000000..48ef2e5c7 --- /dev/null +++ b/src/main/java/com/google/maps/GeolocationApi.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.model.GeolocationPayload; +import com.google.maps.model.GeolocationResult; +import com.google.maps.model.LatLng; + +/* + * The Google Maps Geolocation API returns a location and accuracy radius based on information + * about cell towers and WiFi nodes that the mobile client can detect. + * + *

    Please see the + * Geolocation API for more detail. + * + * + */ +public class GeolocationApi { + private static final String API_BASE_URL = "https://www.googleapis.com"; + + static final ApiConfig GEOLOCATION_API_CONFIG = + new ApiConfig("/geolocation/v1/geolocate") + .hostName(API_BASE_URL) + .supportsClientId(false) + .fieldNamingPolicy(FieldNamingPolicy.IDENTITY) + .requestVerb("POST"); + + private GeolocationApi() {} + + public static PendingResult geolocate( + GeoApiContext context, GeolocationPayload payload) { + return new GeolocationApiRequest(context).Payload(payload).CreatePayload(); + } + + public static GeolocationApiRequest newRequest(GeoApiContext context) { + return new GeolocationApiRequest(context); + } + + public static class Response implements ApiResponse { + public int code = 200; + public String message = "OK"; + public double accuracy = -1.0; + public LatLng location = null; + public String domain = null; + public String reason = null; + public String debugInfo = null; + + @Override + public boolean successful() { + return code == 200; + } + + @Override + public GeolocationResult getResult() { + GeolocationResult result = new GeolocationResult(); + result.accuracy = accuracy; + result.location = location; + return result; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(reason, message); + } + } +} diff --git a/src/main/java/com/google/maps/GeolocationApiRequest.java b/src/main/java/com/google/maps/GeolocationApiRequest.java new file mode 100644 index 000000000..ae84d45bb --- /dev/null +++ b/src/main/java/com/google/maps/GeolocationApiRequest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.Gson; +import com.google.maps.model.CellTower; +import com.google.maps.model.GeolocationPayload; +import com.google.maps.model.GeolocationPayload.GeolocationPayloadBuilder; +import com.google.maps.model.GeolocationResult; +import com.google.maps.model.WifiAccessPoint; + +/** A request for the Geolocation API. */ +public class GeolocationApiRequest + extends PendingResultBase { + + private GeolocationPayload payload = null; + private GeolocationPayloadBuilder builder = null; + + GeolocationApiRequest(GeoApiContext context) { + super(context, GeolocationApi.GEOLOCATION_API_CONFIG, GeolocationApi.Response.class); + builder = new GeolocationPayload.GeolocationPayloadBuilder(); + } + + @Override + protected void validateRequest() { + if (this.payload.considerIp != null + && !this.payload.considerIp + && this.payload.wifiAccessPoints != null + && this.payload.wifiAccessPoints.length < 2) { + throw new IllegalArgumentException("Request must contain two or more 'Wifi Access Points'"); + } + } + + public GeolocationApiRequest HomeMobileCountryCode(int newHomeMobileCountryCode) { + this.builder.HomeMobileCountryCode(newHomeMobileCountryCode); + return this; + } + + public GeolocationApiRequest HomeMobileNetworkCode(int newHomeMobileNetworkCode) { + this.builder.HomeMobileNetworkCode(newHomeMobileNetworkCode); + return this; + } + + public GeolocationApiRequest RadioType(String newRadioType) { + this.builder.RadioType(newRadioType); + return this; + } + + public GeolocationApiRequest Carrier(String newCarrier) { + this.builder.Carrier(newCarrier); + return this; + } + + public GeolocationApiRequest ConsiderIp(boolean newConsiderIp) { + this.builder.ConsiderIp(newConsiderIp); + return this; + } + + public GeolocationApiRequest CellTowers(CellTower[] newCellTowers) { + this.builder.CellTowers(newCellTowers); + return this; + } + + public GeolocationApiRequest AddCellTower(CellTower newCellTower) { + this.builder.AddCellTower(newCellTower); + return this; + } + + public GeolocationApiRequest WifiAccessPoints(WifiAccessPoint[] newWifiAccessPoints) { + this.builder.WifiAccessPoints(newWifiAccessPoints); + return this; + } + + public GeolocationApiRequest AddWifiAccessPoint(WifiAccessPoint newWifiAccessPoint) { + this.builder.AddWifiAccessPoint(newWifiAccessPoint); + return this; + } + + public GeolocationApiRequest Payload(GeolocationPayload payload) { + this.payload = payload; + return this; + } + + public GeolocationApiRequest CreatePayload() { + if (this.payload == null) { + // if the payload has not been set, create it + this.payload = this.builder.createGeolocationPayload(); + } else { + // use the payload that has been explicitly set by the Payload method above + } + Gson gson = new Gson(); + String jsonPayload = gson.toJson(this.payload); + return param("_payload", jsonPayload); + } +} diff --git a/src/main/java/com/google/maps/ImageResult.java b/src/main/java/com/google/maps/ImageResult.java new file mode 100644 index 000000000..8083760b5 --- /dev/null +++ b/src/main/java/com/google/maps/ImageResult.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiResponse; +import java.io.Serializable; + +/** {@code ImageResult} is the object returned from API end points that return images. */ +public class ImageResult implements Serializable { + + public ImageResult(String contentType, byte[] imageData) { + this.imageData = imageData; + this.contentType = contentType; + } + + private static final long serialVersionUID = 1L; + + /** The image data from the Photos API call. */ + public final byte[] imageData; + + /** The Content-Type header of the returned result. */ + public final String contentType; + + /** + * ImageResult.Response is a type system hack to enable API endpoints to return a + * ImageResult. + */ + public static class Response implements ApiResponse { + @Override + public boolean successful() { + return true; + } + + @Override + public ApiException getError() { + return null; + } + + @Override + public ImageResult getResult() { + return null; + } + } +} diff --git a/src/main/java/com/google/maps/NearbySearchRequest.java b/src/main/java/com/google/maps/NearbySearchRequest.java new file mode 100644 index 000000000..406d060f9 --- /dev/null +++ b/src/main/java/com/google/maps/NearbySearchRequest.java @@ -0,0 +1,230 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static com.google.maps.internal.StringJoin.join; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.model.LatLng; +import com.google.maps.model.PlaceType; +import com.google.maps.model.PlacesSearchResponse; +import com.google.maps.model.PlacesSearchResult; +import com.google.maps.model.PriceLevel; +import com.google.maps.model.RankBy; + +/** + * A Nearby + * Search request. + */ +public class NearbySearchRequest + extends PendingResultBase< + PlacesSearchResponse, NearbySearchRequest, NearbySearchRequest.Response> { + + static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/place/nearbysearch/json") + .fieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + + /** + * Constructs a new {@code NearbySearchRequest}. + * + * @param context The {@code GeoApiContext} to make requests through. + */ + public NearbySearchRequest(GeoApiContext context) { + super(context, API_CONFIG, Response.class); + } + + /** + * Specifies the latitude/longitude around which to retrieve place information. + * + * @param location The location to use as the center of the Nearby Search. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest location(LatLng location) { + return param("location", location); + } + + /** + * Specifies the distance (in meters) within which to return place results. The maximum allowed + * radius is 50,000 meters. Note that radius must not be included if {@code rankby=DISTANCE} is + * specified. + * + * @param distance The distance in meters around the {@link #location(LatLng)} to search. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest radius(int distance) { + if (distance > 50000) { + throw new IllegalArgumentException("The maximum allowed radius is 50,000 meters."); + } + return param("radius", String.valueOf(distance)); + } + + /** + * Specifies the order in which results are listed. + * + * @param ranking The rank by method. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest rankby(RankBy ranking) { + return param("rankby", ranking); + } + + /** + * Specifies a term to be matched against all content that Google has indexed for this place. This + * includes but is not limited to name, type, and address, as well as customer reviews and other + * third-party content. + * + * @param keyword The keyword to search for. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest keyword(String keyword) { + return param("keyword", keyword); + } + + /** + * Restricts to places that are at least this price level. + * + * @param priceLevel The price level to set as minimum. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest minPrice(PriceLevel priceLevel) { + return param("minprice", priceLevel); + } + + /** + * Restricts to places that are at most this price level. + * + * @param priceLevel The price level to set as maximum. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest maxPrice(PriceLevel priceLevel) { + return param("maxprice", priceLevel); + } + + /** + * Specifies one or more terms to be matched against the names of places, separated by spaces. + * + * @param name Search for Places with this name. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest name(String name) { + return param("name", name); + } + + /** + * Restricts to only those places that are open for business at the time the query is sent. + * + * @param openNow Whether to restrict to places that are open. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest openNow(boolean openNow) { + return param("opennow", String.valueOf(openNow)); + } + + /** + * Returns the next 20 results from a previously run search. Setting {@code pageToken} will + * execute a search with the same parameters used previously — all parameters other than {@code + * pageToken} will be ignored. + * + * @param nextPageToken The page token from a previous result. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest pageToken(String nextPageToken) { + return param("pagetoken", nextPageToken); + } + + /** + * Restricts the results to places matching the specified type. + * + * @param type The {@link PlaceType} to restrict results to. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + public NearbySearchRequest type(PlaceType type) { + return param("type", type); + } + + /** + * Restricts the results to places matching the specified type. Provides support for multiple + * types. + * + * @deprecated Multiple search types are ignored by the Places API. + * @param types The {@link PlaceType}s to restrict results to. + * @return Returns this {@code NearbyApiRequest} for call chaining. + */ + @Deprecated + public NearbySearchRequest type(PlaceType... types) { + return param("type", join('|', types)); + } + + @Override + protected void validateRequest() { + + // If pagetoken is included, all other parameters are ignored. + if (params().containsKey("pagetoken")) { + return; + } + + // radius must not be included if rankby=distance + if (params().containsKey("rankby") + && params().get("rankby").get(0).equals(RankBy.DISTANCE.toString()) + && params().containsKey("radius")) { + throw new IllegalArgumentException("Request must not contain radius with rankby=distance"); + } + + // If rankby=distance is specified, then one or more of keyword, name, or type is required. + if (params().containsKey("rankby") + && params().get("rankby").get(0).equals(RankBy.DISTANCE.toString()) + && !params().containsKey("keyword") + && !params().containsKey("name") + && !params().containsKey("type")) { + throw new IllegalArgumentException( + "With rankby=distance is specified, then one or more of keyword, name, or type is required"); + } + } + + public static class Response implements ApiResponse { + + public String status; + public String htmlAttributions[]; + public PlacesSearchResult results[]; + public String nextPageToken; + public String errorMessage; + + @Override + public boolean successful() { + return "OK".equals(status) || "ZERO_RESULTS".equals(status); + } + + @Override + public PlacesSearchResponse getResult() { + PlacesSearchResponse result = new PlacesSearchResponse(); + result.htmlAttributions = htmlAttributions; + result.results = results; + result.nextPageToken = nextPageToken; + return result; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(status, errorMessage); + } + } +} diff --git a/src/main/java/com/google/maps/OkHttpRequestHandler.java b/src/main/java/com/google/maps/OkHttpRequestHandler.java new file mode 100644 index 000000000..35a4d6c65 --- /dev/null +++ b/src/main/java/com/google/maps/OkHttpRequestHandler.java @@ -0,0 +1,205 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.GeoApiContext.RequestHandler; +import com.google.maps.internal.ApiResponse; +import com.google.maps.internal.ExceptionsAllowedToRetry; +import com.google.maps.internal.HttpHeaders; +import com.google.maps.internal.OkHttpPendingResult; +import com.google.maps.internal.RateLimitExecutorService; +import com.google.maps.metrics.RequestMetrics; +import java.io.IOException; +import java.net.Proxy; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import okhttp3.Authenticator; +import okhttp3.Credentials; +import okhttp3.Dispatcher; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.Route; + +/** + * A strategy for handling URL requests using OkHttp. + * + * @see com.google.maps.GeoApiContext.RequestHandler + */ +public class OkHttpRequestHandler implements GeoApiContext.RequestHandler { + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private final OkHttpClient client; + private final ExecutorService executorService; + + /* package */ OkHttpRequestHandler(OkHttpClient client, ExecutorService executorService) { + this.client = client; + this.executorService = executorService; + } + + @Override + public > PendingResult handle( + String hostName, + String url, + String userAgent, + String experienceIdHeaderValue, + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeout, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics) { + Request.Builder builder = new Request.Builder().get().header("User-Agent", userAgent); + if (experienceIdHeaderValue != null) { + builder = builder.header(HttpHeaders.X_GOOG_MAPS_EXPERIENCE_ID, experienceIdHeaderValue); + } + Request req = builder.url(hostName + url).build(); + + return new OkHttpPendingResult<>( + req, + client, + clazz, + fieldNamingPolicy, + errorTimeout, + maxRetries, + exceptionsAllowedToRetry, + metrics); + } + + @Override + public > PendingResult handlePost( + String hostName, + String url, + String payload, + String userAgent, + String experienceIdHeaderValue, + Class clazz, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeout, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics) { + RequestBody body = RequestBody.create(JSON, payload); + Request.Builder builder = new Request.Builder().post(body).header("User-Agent", userAgent); + + if (experienceIdHeaderValue != null) { + builder = builder.header(HttpHeaders.X_GOOG_MAPS_EXPERIENCE_ID, experienceIdHeaderValue); + } + Request req = builder.url(hostName + url).build(); + + return new OkHttpPendingResult<>( + req, + client, + clazz, + fieldNamingPolicy, + errorTimeout, + maxRetries, + exceptionsAllowedToRetry, + metrics); + } + + @Override + public void shutdown() { + executorService.shutdown(); + client.connectionPool().evictAll(); + } + + /** Builder strategy for constructing an {@code OkHTTPRequestHandler}. */ + public static class Builder implements GeoApiContext.RequestHandler.Builder { + private final OkHttpClient.Builder builder; + private final RateLimitExecutorService rateLimitExecutorService; + private final Dispatcher dispatcher; + + public Builder() { + builder = new OkHttpClient.Builder(); + rateLimitExecutorService = new RateLimitExecutorService(); + dispatcher = new Dispatcher(rateLimitExecutorService); + builder.dispatcher(dispatcher); + } + + @Override + public Builder connectTimeout(long timeout, TimeUnit unit) { + builder.connectTimeout(timeout, unit); + return this; + } + + @Override + public Builder readTimeout(long timeout, TimeUnit unit) { + builder.readTimeout(timeout, unit); + return this; + } + + @Override + public Builder writeTimeout(long timeout, TimeUnit unit) { + builder.writeTimeout(timeout, unit); + return this; + } + + @Override + public Builder queriesPerSecond(int maxQps) { + dispatcher.setMaxRequests(maxQps); + dispatcher.setMaxRequestsPerHost(maxQps); + rateLimitExecutorService.setQueriesPerSecond(maxQps); + return this; + } + + @Override + public Builder proxy(Proxy proxy) { + builder.proxy(proxy); + return this; + } + + @Override + public Builder proxyAuthentication(String proxyUserName, String proxyUserPassword) { + final String userName = proxyUserName; + final String password = proxyUserPassword; + + builder.proxyAuthenticator( + new Authenticator() { + @Override + public Request authenticate(Route route, Response response) throws IOException { + String credential = Credentials.basic(userName, password); + return response + .request() + .newBuilder() + .header("Proxy-Authorization", credential) + .build(); + } + }); + return this; + } + + /** + * Gets a reference to the OkHttpClient.Builder used to build the OkHttpRequestHandler's + * internal OkHttpClient. This allows you to fully customize the OkHttpClient that the resulting + * OkHttpRequestHandler will make HTTP requests through. + * + * @return OkHttpClient.Builder that will produce the OkHttpClient used by the + * OkHttpRequestHandler built by this. + */ + public OkHttpClient.Builder okHttpClientBuilder() { + return builder; + } + + @Override + public RequestHandler build() { + OkHttpClient client = builder.build(); + return new OkHttpRequestHandler(client, rateLimitExecutorService); + } + } +} diff --git a/src/main/java/com/google/maps/PendingResult.java b/src/main/java/com/google/maps/PendingResult.java index cff9a02ac..1c9acc285 100644 --- a/src/main/java/com/google/maps/PendingResult.java +++ b/src/main/java/com/google/maps/PendingResult.java @@ -15,52 +15,66 @@ package com.google.maps; +import com.google.maps.errors.ApiException; +import java.io.IOException; + /** - * Represents a pending result from an API call. + * A pending result from an API call. * * @param the type of the result object. */ public interface PendingResult { /** - * Performs the request asynchronously, calling onResult or onFailure after - * the request has been completed. + * Performs the request asynchronously, calling {@link + * com.google.maps.PendingResult.Callback#onResult onResult} or {@link + * com.google.maps.PendingResult.Callback#onFailure onFailure} after the request has been + * completed. + * + * @param callback The callback to call on completion. */ - public void setCallback(Callback callback); + void setCallback(Callback callback); /** * Performs the request synchronously. * * @return The result. - * @throws Exception + * @throws ApiException Thrown if the API Returned result is an error. + * @throws InterruptedException Thrown when a thread is waiting, sleeping, or otherwise occupied, + * and the thread is interrupted. + * @throws IOException Thrown when an I/O exception of some sort has occurred. */ - public T await() throws Exception; + T await() throws ApiException, InterruptedException, IOException; /** - * Performs the request synchronously, ignoring exceptions - * while performing the request and errors returned by the server. + * Performs the request synchronously, ignoring exceptions while performing the request and errors + * returned by the server. * * @return The result, or null if there was any error or exception ignored. */ - public T awaitIgnoreError(); + T awaitIgnoreError(); - /** - * Attempt to cancel the request. - */ - public void cancel(); + /** Attempts to cancel the request. */ + void cancel(); /** * The callback interface the API client code needs to implement to handle API results. + * + * @param The type of the result object. */ - public interface Callback { + interface Callback { /** * Called when the request was successfully completed. + * + * @param result The result of the call. */ void onResult(T result); /** * Called when there was an error performing the request. + * + * @param e The exception describing the failure. */ void onFailure(Throwable e); } diff --git a/src/main/java/com/google/maps/PendingResultBase.java b/src/main/java/com/google/maps/PendingResultBase.java index d8d86695a..b59a95862 100644 --- a/src/main/java/com/google/maps/PendingResultBase.java +++ b/src/main/java/com/google/maps/PendingResultBase.java @@ -15,12 +15,15 @@ package com.google.maps; +import com.google.maps.errors.ApiException; import com.google.maps.internal.ApiConfig; import com.google.maps.internal.ApiResponse; import com.google.maps.internal.StringJoin.UrlValue; - +import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -29,13 +32,12 @@ *

    {@code T} is the class of the result, {@code A} is the actual base class of this abstract * class, and R is the type of the request. */ -abstract class PendingResultBase, - R extends ApiResponse> +abstract class PendingResultBase, R extends ApiResponse> implements PendingResult { private final GeoApiContext context; private final ApiConfig config; - private HashMap params = new HashMap(); + private HashMap> params = new HashMap<>(); private PendingResult delegate; private Class responseClass; @@ -51,10 +53,9 @@ public final void setCallback(Callback callback) { } @Override - public final T await() throws Exception { + public final T await() throws ApiException, InterruptedException, IOException { PendingResult request = makeRequest(); - T result = request.await(); - return result; + return request.await(); } @Override @@ -76,41 +77,96 @@ private PendingResult makeRequest() { "'await', 'awaitIgnoreError' or 'setCallback' was already called."); } validateRequest(); - delegate = context.get(config, responseClass, params); - return delegate; + switch (config.requestVerb) { + case "GET": + return delegate = context.get(config, responseClass, params); + case "POST": + return delegate = context.post(config, responseClass, params); + default: + throw new IllegalStateException( + String.format("Unexpected request method '%s'", config.requestVerb)); + } } protected abstract void validateRequest(); - protected A param(String key, String val) { - params.put(key, val); - - @SuppressWarnings("unchecked") // safe by specification - A is the actual class of this instance + private A getInstance() { + @SuppressWarnings("unchecked") A result = (A) this; return result; } + protected A param(String key, String val) { + // Enforce singleton parameter semantics for most API surfaces + params.put(key, new ArrayList()); + return paramAddToList(key, val); + } + + protected A param(String key, int val) { + return this.param(key, Integer.toString(val)); + } + protected A param(String key, UrlValue val) { - params.put(key, val.toString()); + if (val != null) { + return this.param(key, val.toUrlValue()); + } + return getInstance(); + } - @SuppressWarnings("unchecked") // safe by specification - A is the actual class of this instance - A result = (A) this; - return result; + protected A paramAddToList(String key, String val) { + // Multiple parameter values required to support Static Maps API paths and markers. + if (params.get(key) == null) { + params.put(key, new ArrayList()); + } + params.get(key).add(val); + return getInstance(); + } + + protected A paramAddToList(String key, UrlValue val) { + if (val != null) { + return this.paramAddToList(key, val.toUrlValue()); + } + return getInstance(); } - protected Map params() { + protected Map> params() { return Collections.unmodifiableMap(params); } /** - * The language in which to return results. Note that we often update supported languages so - * this list may not be exhaustive. + * The language in which to return results. Note that we often update supported languages so this + * list may not be exhaustive. * - * @see List of supported - * domain languages - * @param language The language code, e.g. "en-AU" or "es" + * @param language The language code, e.g. "en-AU" or "es". + * @see List of supported domain + * languages + * @return Returns the request for call chaining. */ public final A language(String language) { return param("language", language); } + + /** + * A channel to pass with the request. channel is used by Google Maps API for Work users to be + * able to track usage across different applications with the same clientID. See Premium Plan + * Usage Rates and Limits. + * + * @param channel String to pass with the request for analytics. + * @return Returns the request for call chaining. + */ + public A channel(String channel) { + return param("channel", channel); + } + + /** + * Custom parameter. For advanced usage only. + * + * @param parameter The name of the custom parameter. + * @param value The value of the custom parameter. + * @return Returns the request for call chaining. + */ + public A custom(String parameter, String value) { + return param(parameter, value); + } } diff --git a/src/main/java/com/google/maps/PhotoRequest.java b/src/main/java/com/google/maps/PhotoRequest.java new file mode 100644 index 000000000..f60abf2d6 --- /dev/null +++ b/src/main/java/com/google/maps/PhotoRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.maps.internal.ApiConfig; + +/** + * A Place + * Photo request. + */ +public class PhotoRequest + extends PendingResultBase { + + static final ApiConfig API_CONFIG = new ApiConfig("/maps/api/place/photo"); + + public PhotoRequest(GeoApiContext context) { + super(context, API_CONFIG, ImageResult.Response.class); + } + + @Override + protected void validateRequest() { + if (!params().containsKey("photoreference")) { + throw new IllegalArgumentException("Request must contain 'photoReference'."); + } + if (!params().containsKey("maxheight") && !params().containsKey("maxwidth")) { + throw new IllegalArgumentException("Request must contain 'maxHeight' or 'maxWidth'."); + } + } + + /** + * Sets the photoReference for this request. + * + * @param photoReference A string identifier that uniquely identifies a photo. Photo references + * are returned from either a Place Search or Place Details request. + * @return Returns the configured PhotoRequest. + */ + public PhotoRequest photoReference(String photoReference) { + return param("photoreference", photoReference); + } + + /** + * Sets the maxHeight for this request. + * + * @param maxHeight The maximum desired height, in pixels, of the image returned by the Place + * Photos service. + * @return Returns the configured PhotoRequest. + */ + public PhotoRequest maxHeight(int maxHeight) { + return param("maxheight", String.valueOf(maxHeight)); + } + + /** + * Sets the maxWidth for this request. + * + * @param maxWidth The maximum desired width, in pixels, of the image returned by the Place Photos + * service. + * @return Returns the configured PhotoRequest. + */ + public PhotoRequest maxWidth(int maxWidth) { + return param("maxwidth", String.valueOf(maxWidth)); + } +} diff --git a/src/main/java/com/google/maps/PlaceAutocompleteRequest.java b/src/main/java/com/google/maps/PlaceAutocompleteRequest.java new file mode 100644 index 000000000..dcb1eaeec --- /dev/null +++ b/src/main/java/com/google/maps/PlaceAutocompleteRequest.java @@ -0,0 +1,228 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static com.google.maps.internal.StringJoin.join; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.internal.StringJoin.UrlValue; +import com.google.maps.model.AutocompletePrediction; +import com.google.maps.model.ComponentFilter; +import com.google.maps.model.LatLng; +import com.google.maps.model.PlaceAutocompleteType; +import java.io.Serializable; +import java.util.UUID; + +/** + * A Place + * Autocomplete request. + */ +public class PlaceAutocompleteRequest + extends PendingResultBase< + AutocompletePrediction[], PlaceAutocompleteRequest, PlaceAutocompleteRequest.Response> { + + static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/place/autocomplete/json") + .fieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + + protected PlaceAutocompleteRequest(GeoApiContext context) { + super(context, API_CONFIG, Response.class); + } + + /** SessionToken represents an Autocomplete session. */ + public static final class SessionToken implements UrlValue, Serializable { + + private static final long serialVersionUID = 1L; + + private UUID uuid; + + /** This constructor creates a new session. */ + public SessionToken() { + uuid = UUID.randomUUID(); + } + + /** + * Construct a session that is a continuation of a previous session. + * + * @param uuid The universally unique identifier for this session. + */ + public SessionToken(UUID uuid) { + this.uuid = uuid; + } + + /** + * Retrieve the universally unique identifier for this session. This enables you to recreate the + * session token in a later context. + * + * @return Returns the universally unique identifier for this session. + */ + public UUID getUUID() { + return uuid; + } + + @Override + public String toUrlValue() { + return uuid.toString(); + } + } + + /** + * Sets the SessionToken for this request. Using session token makes sure the autocomplete is + * priced per session, instead of per keystroke. + * + * @param sessionToken Session Token is the session identifier. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest sessionToken(SessionToken sessionToken) { + return param("sessiontoken", sessionToken); + } + + /** + * Sets the text string on which to search. The Places service will return candidate matches based + * on this string and order results based on their perceived relevance. + * + * @param input The input text to autocomplete. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest input(String input) { + return param("input", input); + } + + /** + * The character position in the input term at which the service uses text for predictions. For + * example, if the input is 'Googl' and the completion point is 3, the service will match on + * 'Goo'. The offset should generally be set to the position of the text caret. If no offset is + * supplied, the service will use the entire term. + * + * @param offset The character offset position of the user's cursor. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest offset(int offset) { + return param("offset", String.valueOf(offset)); + } + + /** + * The origin point from which to calculate straight-line distance to the destination (returned as + * {@link AutocompletePrediction#distanceMeters}). If this value is omitted, straight-line + * distance will not be returned. + * + * @param origin The {@link LatLng} origin point from which to calculate distance. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest origin(LatLng origin) { + return param("origin", origin); + } + + /** + * The point around which you wish to retrieve place information. + * + * @param location The {@link LatLng} location to center this autocomplete search. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest location(LatLng location) { + return param("location", location); + } + + /** + * The distance (in meters) within which to return place results. Note that setting a radius + * biases results to the indicated area, but may not fully restrict results to the specified area. + * + * @param radius The radius over which to bias results. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest radius(int radius) { + return param("radius", String.valueOf(radius)); + } + + /** + * Restricts the results to places matching the specified type. + * + * @param type The type to restrict results to. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + * @deprecated Please use {@code types} instead. + */ + public PlaceAutocompleteRequest type(PlaceAutocompleteType type) { + return this.types(type); + } + + /** + * Restricts the results to places matching the specified type. + * + * @param types The type to restrict results to. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest types(PlaceAutocompleteType types) { + return param("types", types); + } + + /** + * A grouping of places to which you would like to restrict your results. Currently, you can use + * components to filter by country. + * + * @param filters The component filter to restrict results with. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest components(ComponentFilter... filters) { + return param("components", join('|', filters)); + } + + /** + * StrictBounds returns only those places that are strictly within the region defined by location + * and radius. This is a restriction, rather than a bias, meaning that results outside this region + * will not be returned even if they match the user input. + * + * @param strictBounds Whether to strictly bound results. + * @return Returns this {@code PlaceAutocompleteRequest} for call chaining. + */ + public PlaceAutocompleteRequest strictBounds(boolean strictBounds) { + return param("strictbounds", Boolean.toString(strictBounds)); + } + + @Override + protected void validateRequest() { + if (!params().containsKey("input")) { + throw new IllegalArgumentException("Request must contain 'input'."); + } + } + + public static class Response implements ApiResponse { + public String status; + public AutocompletePrediction predictions[]; + public String errorMessage; + + @Override + public boolean successful() { + return "OK".equals(status) || "ZERO_RESULTS".equals(status); + } + + @Override + public AutocompletePrediction[] getResult() { + return predictions; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(status, errorMessage); + } + } +} diff --git a/src/main/java/com/google/maps/PlaceDetailsRequest.java b/src/main/java/com/google/maps/PlaceDetailsRequest.java new file mode 100644 index 000000000..c5aa1a8ca --- /dev/null +++ b/src/main/java/com/google/maps/PlaceDetailsRequest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.internal.StringJoin; +import com.google.maps.internal.StringJoin.UrlValue; +import com.google.maps.model.PlaceDetails; + +/** + * A Place + * Details request. + */ +public class PlaceDetailsRequest + extends PendingResultBase { + + static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/place/details/json") + .fieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + + public PlaceDetailsRequest(GeoApiContext context) { + super(context, API_CONFIG, Response.class); + } + + /** + * Specifies the Place ID to get Place Details for. Required. + * + * @param placeId The Place ID to retrieve details for. + * @return Returns this {@code PlaceDetailsRequest} for call chaining. + */ + public PlaceDetailsRequest placeId(String placeId) { + return param("placeid", placeId); + } + + /** + * Sets the SessionToken for this request. Use this for Place Details requests that are called + * following an autocomplete request in the same user session. Optional. + * + * @param sessionToken Session Token is the session identifier. + * @return Returns this {@code PlaceDetailsRequest} for call chaining. + */ + public PlaceDetailsRequest sessionToken(PlaceAutocompleteRequest.SessionToken sessionToken) { + return param("sessiontoken", sessionToken); + } + + /** + * Sets the Region for this request. The region code, specified as a ccTLD (country code top-level + * domain) two-character value. Most ccTLD codes are identical to ISO 3166-1 codes, with some + * exceptions. This parameter will only influence, not fully restrict, results. + * + * @param region The region code. + * @return Returns this {@code PlaceDetailsRequest} for call chaining. + */ + public PlaceDetailsRequest region(String region) { + return param("region", region); + } + + /** + * Specifies the field masks of the details to be returned by PlaceDetails. + * + * @param fields The Field Masks of the fields to return. + * @return Returns this {@code PlaceDetailsRequest} for call chaining. + */ + public PlaceDetailsRequest fields(FieldMask... fields) { + return param("fields", StringJoin.join(',', fields)); + } + + @Override + protected void validateRequest() { + if (!params().containsKey("placeid")) { + throw new IllegalArgumentException("Request must contain 'placeId'."); + } + } + + public static class Response implements ApiResponse { + public String status; + public PlaceDetails result; + public String[] htmlAttributions; + public String errorMessage; + + @Override + public boolean successful() { + return "OK".equals(status) || "ZERO_RESULTS".equals(status); + } + + @Override + public PlaceDetails getResult() { + if (result != null) { + result.htmlAttributions = htmlAttributions; + } + return result; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(status, errorMessage); + } + } + + public enum FieldMask implements UrlValue { + ADDRESS_COMPONENT("address_component"), + ADR_ADDRESS("adr_address"), + @Deprecated + ALT_ID("alt_id"), + BUSINESS_STATUS("business_status"), + FORMATTED_ADDRESS("formatted_address"), + FORMATTED_PHONE_NUMBER("formatted_phone_number"), + GEOMETRY("geometry"), + GEOMETRY_LOCATION("geometry/location"), + GEOMETRY_LOCATION_LAT("geometry/location/lat"), + GEOMETRY_LOCATION_LNG("geometry/location/lng"), + GEOMETRY_VIEWPORT("geometry/viewport"), + GEOMETRY_VIEWPORT_NORTHEAST("geometry/viewport/northeast"), + GEOMETRY_VIEWPORT_NORTHEAST_LAT("geometry/viewport/northeast/lat"), + GEOMETRY_VIEWPORT_NORTHEAST_LNG("geometry/viewport/northeast/lng"), + GEOMETRY_VIEWPORT_SOUTHWEST("geometry/viewport/southwest"), + GEOMETRY_VIEWPORT_SOUTHWEST_LAT("geometry/viewport/southwest/lat"), + GEOMETRY_VIEWPORT_SOUTHWEST_LNG("geometry/viewport/southwest/lng"), + ICON("icon"), + @Deprecated + ID("id"), + INTERNATIONAL_PHONE_NUMBER("international_phone_number"), + NAME("name"), + OPENING_HOURS("opening_hours"), + @Deprecated + PERMANENTLY_CLOSED("permanently_closed"), + USER_RATINGS_TOTAL("user_ratings_total"), + PHOTOS("photos"), + PLACE_ID("place_id"), + PLUS_CODE("plus_code"), + PRICE_LEVEL("price_level"), + RATING("rating"), + @Deprecated + REFERENCE("reference"), + REVIEW("review"), + @Deprecated + SCOPE("scope"), + TYPES("types"), + URL("url"), + UTC_OFFSET("utc_offset"), + VICINITY("vicinity"), + WEBSITE("website"); + + private final String field; + + FieldMask(final String field) { + this.field = field; + } + + @Override + public String toUrlValue() { + return field; + } + } +} diff --git a/src/main/java/com/google/maps/PlacesApi.java b/src/main/java/com/google/maps/PlacesApi.java new file mode 100644 index 000000000..aa968c93b --- /dev/null +++ b/src/main/java/com/google/maps/PlacesApi.java @@ -0,0 +1,227 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.maps.model.LatLng; +import com.google.maps.model.PlaceType; + +/** + * Performs a text search for places. The Google Places API enables you to get data from the same + * database used by Google Maps and Google+ Local. Places features more than 100 million businesses + * and points of interest that are updated frequently through owner-verified listings and + * user-moderated contributions. + * + *

    See also: Places API Web Service + * documentation. + */ +public class PlacesApi { + + private PlacesApi() {} + + /** + * Performs a search for nearby Places. + * + * @param context The context on which to make Geo API requests. + * @param location The latitude/longitude around which to retrieve place information. + * @return Returns a NearbySearchRequest that can be configured and executed. + */ + public static NearbySearchRequest nearbySearchQuery(GeoApiContext context, LatLng location) { + NearbySearchRequest request = new NearbySearchRequest(context); + request.location(location); + return request; + } + + /** + * Retrieves the next page of Nearby Search results. The nextPageToken, returned in a + * PlacesSearchResponse when there are more pages of results, encodes all of the original Nearby + * Search Request parameters, which are thus not required on this call. + * + * @param context The context on which to make Geo API requests. + * @param nextPageToken The nextPageToken returned as part of a PlacesSearchResponse. + * @return Returns a NearbySearchRequest that can be executed. + */ + public static NearbySearchRequest nearbySearchNextPage( + GeoApiContext context, String nextPageToken) { + NearbySearchRequest request = new NearbySearchRequest(context); + request.pageToken(nextPageToken); + return request; + } + + /** + * Performs a search for Places using a text query; for example, "pizza in New York" or "shoe + * stores near Ottawa". + * + * @param context The context on which to make Geo API requests. + * @param query The text string on which to search, for example: "restaurant". + * @return Returns a TextSearchRequest that can be configured and executed. + */ + public static TextSearchRequest textSearchQuery(GeoApiContext context, String query) { + TextSearchRequest request = new TextSearchRequest(context); + request.query(query); + return request; + } + + /** + * Performs a search for Places using a text query; for example, "pizza in New York" or "shoe + * stores near Ottawa". + * + * @param context The context on which to make Geo API requests. + * @param query The text string on which to search, for example: "restaurant". + * @param location The latitude/longitude around which to retrieve place information. + * @return Returns a TextSearchRequest that can be configured and executed. + */ + public static TextSearchRequest textSearchQuery( + GeoApiContext context, String query, LatLng location) { + TextSearchRequest request = new TextSearchRequest(context); + request.query(query); + request.location(location); + return request; + } + + /** + * Performs a search for Places using a PlaceType parameter. + * + * @param context The context on which to make Geo API requests. + * @param type Restricts the results to places matching the specified PlaceType. + * @return Returns a TextSearchRequest that can be configured and executed. + */ + public static TextSearchRequest textSearchQuery(GeoApiContext context, PlaceType type) { + TextSearchRequest request = new TextSearchRequest(context); + request.type(type); + return request; + } + + /** + * Retrieves the next page of Text Search results. The nextPageToken, returned in a + * PlacesSearchResponse when there are more pages of results, encodes all of the original Text + * Search Request parameters, which are thus not required on this call. + * + * @param context The context on which to make Geo API requests. + * @param nextPageToken The nextPageToken returned as part of a PlacesSearchResponse. + * @return Returns a TextSearchRequest that can be executed. + */ + public static TextSearchRequest textSearchNextPage(GeoApiContext context, String nextPageToken) { + TextSearchRequest request = new TextSearchRequest(context); + request.pageToken(nextPageToken); + return request; + } + + /** + * Requests the details of a Place. + * + *

    We are only enabling looking up Places by placeId as the older Place identifier, reference, + * is deprecated. Please see the + * deprecation warning. + * + * @param context The context on which to make Geo API requests. + * @param placeId The PlaceID to request details on. + * @param sessionToken The Session Token for this request. + * @return Returns a PlaceDetailsRequest that you can configure and execute. + */ + public static PlaceDetailsRequest placeDetails( + GeoApiContext context, String placeId, PlaceAutocompleteRequest.SessionToken sessionToken) { + PlaceDetailsRequest request = new PlaceDetailsRequest(context); + request.placeId(placeId); + request.sessionToken(sessionToken); + return request; + } + + /** + * Requests the details of a Place. + * + *

    We are only enabling looking up Places by placeId as the older Place identifier, reference, + * is deprecated. Please see the + * deprecation warning. + * + * @param context The context on which to make Geo API requests. + * @param placeId The PlaceID to request details on. + * @return Returns a PlaceDetailsRequest that you can configure and execute. + */ + public static PlaceDetailsRequest placeDetails(GeoApiContext context, String placeId) { + PlaceDetailsRequest request = new PlaceDetailsRequest(context); + request.placeId(placeId); + return request; + } + + /** + * Requests a Photo from a PhotoReference. + * + *

    Note: If you want to use a Photo in a web browser, please retrieve the photos for a place + * via our + * JavaScript Places Library. Likewise, on Android, Places Photos can be retrieved using the + * Google Places API for + * Android. + * + * @param context The context on which to make Geo API requests. + * @param photoReference The reference to the photo to retrieve. + * @return Returns a PhotoRequest that you can execute. + */ + public static PhotoRequest photo(GeoApiContext context, String photoReference) { + PhotoRequest request = new PhotoRequest(context); + request.photoReference(photoReference); + return request; + } + + /** + * Creates a new Places Autocomplete request for a given input. The Place Autocomplete service can + * match on full words as well as substrings. Applications can therefore send queries as the user + * types, to provide on-the-fly place predictions. + * + * @param context The context on which to make Geo API requests. + * @param input input is the text string on which to search. + * @param sessionToken Session token, to make sure requests are billed per session, instead of per + * character. + * @return Returns a PlaceAutocompleteRequest that you can configure and execute. + */ + public static PlaceAutocompleteRequest placeAutocomplete( + GeoApiContext context, String input, PlaceAutocompleteRequest.SessionToken sessionToken) { + PlaceAutocompleteRequest request = new PlaceAutocompleteRequest(context); + request.input(input); + request.sessionToken(sessionToken); + return request; + } + + /** + * Allows you to add on-the-fly geographic query predictions to your application. + * + * @param context The context on which to make Geo API requests. + * @param input input is the text string on which to search. + * @return Returns a QueryAutocompleteRequest that you can configure and execute. + */ + public static QueryAutocompleteRequest queryAutocomplete(GeoApiContext context, String input) { + QueryAutocompleteRequest request = new QueryAutocompleteRequest(context); + request.input(input); + return request; + } + + /** + * Find places using either search text, or a phone number. + * + * @param context The context on which to make Geo API requests. + * @param input The input to search on. + * @param inputType Whether the input is search text, or a phone number. + * @return Returns a FindPlaceFromTextRequest that you can configure and execute. + */ + public static FindPlaceFromTextRequest findPlaceFromText( + GeoApiContext context, String input, FindPlaceFromTextRequest.InputType inputType) { + FindPlaceFromTextRequest request = new FindPlaceFromTextRequest(context); + request.input(input).inputType(inputType); + return request; + } +} diff --git a/src/main/java/com/google/maps/QueryAutocompleteRequest.java b/src/main/java/com/google/maps/QueryAutocompleteRequest.java new file mode 100644 index 000000000..cec5efa18 --- /dev/null +++ b/src/main/java/com/google/maps/QueryAutocompleteRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.model.AutocompletePrediction; +import com.google.maps.model.LatLng; + +/** + * A Query + * Autocomplete request. + */ +public class QueryAutocompleteRequest + extends PendingResultBase< + AutocompletePrediction[], QueryAutocompleteRequest, QueryAutocompleteRequest.Response> { + + static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/place/queryautocomplete/json") + .fieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + + protected QueryAutocompleteRequest(GeoApiContext context) { + super(context, API_CONFIG, Response.class); + } + + @Override + protected void validateRequest() { + if (!params().containsKey("input")) { + throw new IllegalArgumentException("Request must contain 'input'."); + } + } + + /** + * The text string on which to search. The Places service will return candidate matches based on + * this string and order results based on their perceived relevance. + * + * @param input The input text to autocomplete. + * @return Returns this {@code QueryAutocompleteRequest} for call chaining. + */ + public QueryAutocompleteRequest input(String input) { + return param("input", input); + } + + /** + * The character position in the input term at which the service uses text for predictions. For + * example, if the input is 'Googl' and the completion point is 3, the service will match on + * 'Goo'. The offset should generally be set to the position of the text caret. If no offset is + * supplied, the service will use the entire term. + * + * @param offset The character offset to search from. + * @return Returns this {@code QueryAutocompleteRequest} for call chaining. + */ + public QueryAutocompleteRequest offset(int offset) { + return param("offset", String.valueOf(offset)); + } + + /** + * The point around which you wish to retrieve place information. + * + * @param location The location point around which to search. + * @return Returns this {@code QueryAutocompleteRequest} for call chaining. + */ + public QueryAutocompleteRequest location(LatLng location) { + return param("location", location); + } + + /** + * The distance (in meters) within which to return place results. Note that setting a radius + * biases results to the indicated area, but may not fully restrict results to the specified area. + * + * @param radius The radius around which to bias results. + * @return Returns this {@code QueryAutocompleteRequest} for call chaining. + */ + public QueryAutocompleteRequest radius(int radius) { + return param("radius", String.valueOf(radius)); + } + + public static class Response implements ApiResponse { + public String status; + public AutocompletePrediction predictions[]; + public String errorMessage; + + @Override + public boolean successful() { + return "OK".equals(status) || "ZERO_RESULTS".equals(status); + } + + @Override + public AutocompletePrediction[] getResult() { + return predictions; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(status, errorMessage); + } + } +} diff --git a/src/main/java/com/google/maps/RoadsApi.java b/src/main/java/com/google/maps/RoadsApi.java index bed99bb63..67f72340c 100644 --- a/src/main/java/com/google/maps/RoadsApi.java +++ b/src/main/java/com/google/maps/RoadsApi.java @@ -37,12 +37,20 @@ public class RoadsApi { static final String API_BASE_URL = "https://roads.googleapis.com"; - static final ApiConfig ROADS_API_CONFIG = new ApiConfig("/v1/snapToRoads") - .hostName(API_BASE_URL) - .supportsClientId(false) - .fieldNamingPolicy(FieldNamingPolicy.IDENTITY); + static final ApiConfig SNAP_TO_ROADS_API_CONFIG = + new ApiConfig("/v1/snapToRoads") + .hostName(API_BASE_URL) + .supportsClientId(false) + .fieldNamingPolicy(FieldNamingPolicy.IDENTITY); + + static final ApiConfig SPEEDS_API_CONFIG = + new ApiConfig("/v1/speedLimits") + .hostName(API_BASE_URL) + .supportsClientId(false) + .fieldNamingPolicy(FieldNamingPolicy.IDENTITY); - static final ApiConfig SPEEDS_API_CONFIG = new ApiConfig("/v1/speedLimits") + static final ApiConfig NEAREST_ROADS_API_CONFIG = + new ApiConfig("/v1/nearestRoads") .hostName(API_BASE_URL) .supportsClientId(false) .fieldNamingPolicy(FieldNamingPolicy.IDENTITY); @@ -50,31 +58,40 @@ public class RoadsApi { private RoadsApi() {} /** - * Takes up to 100 GPS points collected along a route, and returns a similar set of data with - * the points snapped to the most likely roads the vehicle was traveling along. + * Takes up to 100 GPS points collected along a route, and returns a similar set of data with the + * points snapped to the most likely roads the vehicle was traveling along. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param path The collected GPS points as a path. + * @return Returns the snapped points as a {@link PendingResult}. */ - public static PendingResult snapToRoads(GeoApiContext context, - LatLng... path) { - return context.get(ROADS_API_CONFIG, RoadsResponse.class, "path", join('|', path)); + public static PendingResult snapToRoads(GeoApiContext context, LatLng... path) { + return context.get(SNAP_TO_ROADS_API_CONFIG, RoadsResponse.class, "path", join('|', path)); } /** - * Takes up to 100 GPS points collected along a route, and returns a similar set of data with - * the points snapped to the most likely roads the vehicle was traveling along. Additionally, - * you can request that the points be interpolated, resulting in a path that smoothly follows - * the geometry of the road. + * Takes up to 100 GPS points collected along a route, and returns a similar set of data with the + * points snapped to the most likely roads the vehicle was traveling along. Additionally, you can + * request that the points be interpolated, resulting in a path that smoothly follows the geometry + * of the road. * + * @param context The {@link GeoApiContext} to make requests through. * @param interpolate Whether to interpolate a path to include all points forming the full - * road-geometry. When true, additional interpolated points will also be returned, - * resulting in a path that smoothly follows the geometry of the road, - * even around corners and through tunnels. + * road-geometry. When true, additional interpolated points will also be returned, resulting + * in a path that smoothly follows the geometry of the road, even around corners and through + * tunnels. * @param path The path to be snapped. + * @return Returns the snapped points as a {@link PendingResult}. */ - public static PendingResult snapToRoads(GeoApiContext context, - boolean interpolate, LatLng... path) { - return context.get(ROADS_API_CONFIG, RoadsResponse.class, - "path", join('|', path), - "interpolate", String.valueOf(interpolate)); + public static PendingResult snapToRoads( + GeoApiContext context, boolean interpolate, LatLng... path) { + return context.get( + SNAP_TO_ROADS_API_CONFIG, + RoadsResponse.class, + "path", + join('|', path), + "interpolate", + String.valueOf(interpolate)); } /** @@ -83,8 +100,13 @@ public static PendingResult snapToRoads(GeoApiContext context, * *

    Note: The accuracy of speed limit data returned by the Google Maps Roads API cannot be * guaranteed. Speed limit data provided is not real-time, and may be estimated, inaccurate, - * incomplete, and/or outdated. Inaccuracies in our data may be reported through the Google Map Maker service. + * incomplete, and/or outdated. Inaccuracies in our data may be reported through + * Google Maps Feedback. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param path The collected GPS points as a path. + * @return Returns the speed limits as a {@link PendingResult}. */ public static PendingResult speedLimits(GeoApiContext context, LatLng... path) { return context.get(SPEEDS_API_CONFIG, SpeedsResponse.class, "path", join('|', path)); @@ -95,15 +117,17 @@ public static PendingResult speedLimits(GeoApiContext context, Lat * *

    Note: The accuracy of speed limit data returned by the Google Maps Roads API cannot be * guaranteed. Speed limit data provided is not real-time, and may be estimated, inaccurate, - * incomplete, and/or outdated. Inaccuracies in our data may be reported through the Google Map Maker service. + * incomplete, and/or outdated. Inaccuracies in our data may be reported through + * Google Maps Feedback. * - * @param placeIds The Place ID of the road segment. Place IDs are returned by the - * {@link #snapToRoads(GeoApiContext, com.google.maps.model.LatLng...)} method. You can pass up - * to 100 placeIds with each request. + * @param context The {@link GeoApiContext} to make requests through. + * @param placeIds The Place ID of the road segment. Place IDs are returned by the {@link + * #snapToRoads(GeoApiContext, com.google.maps.model.LatLng...)} method. You can pass up to + * 100 placeIds with each request. + * @return Returns the speed limits as a {@link PendingResult}. */ - public static PendingResult speedLimits(GeoApiContext context, - String... placeIds) { + public static PendingResult speedLimits(GeoApiContext context, String... placeIds) { String[] placeParams = new String[2 * placeIds.length]; int i = 0; for (String placeId : placeIds) { @@ -116,14 +140,30 @@ public static PendingResult speedLimits(GeoApiContext context, /** * Returns the result of snapping the provided points to roads and retrieving the speed limits. - * This is useful for interactive applications where you need to + * + * @param context The {@link GeoApiContext} to make requests through. + * @param path The collected GPS points as a path. + * @return Returns the snapped points and speed limits as a {@link PendingResult}. */ - public static PendingResult snappedSpeedLimits(GeoApiContext context, - LatLng... path) { + public static PendingResult snappedSpeedLimits( + GeoApiContext context, LatLng... path) { return context.get(SPEEDS_API_CONFIG, CombinedResponse.class, "path", join('|', path)); } - private static class RoadsResponse implements ApiResponse { + /** + * Takes up to 100 GPS points, and returns the closest road segment for each point. The points + * passed do not need to be part of a continuous path. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param points The sequence of points to be aligned to nearest roads + * @return Returns the snapped points as a {@link PendingResult}. + */ + public static PendingResult nearestRoads( + GeoApiContext context, LatLng... points) { + return context.get(NEAREST_ROADS_API_CONFIG, RoadsResponse.class, "points", join('|', points)); + } + + public static class RoadsResponse implements ApiResponse { private SnappedPoint[] snappedPoints; private ApiError error; @@ -143,7 +183,7 @@ public ApiException getError() { } } - private static class SpeedsResponse implements ApiResponse { + public static class SpeedsResponse implements ApiResponse { private SpeedLimit[] speedLimits; private ApiError error; @@ -163,7 +203,7 @@ public ApiException getError() { } } - private static class CombinedResponse implements ApiResponse { + public static class CombinedResponse implements ApiResponse { private SnappedPoint[] snappedPoints; private SpeedLimit[] speedLimits; private ApiError error; diff --git a/src/main/java/com/google/maps/StaticMapsApi.java b/src/main/java/com/google/maps/StaticMapsApi.java new file mode 100644 index 000000000..3a31644b6 --- /dev/null +++ b/src/main/java/com/google/maps/StaticMapsApi.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.maps.model.Size; + +public class StaticMapsApi { + + private StaticMapsApi() {} + + /** + * Create a new {@code StaticMapRequest}. + * + * @param context The {@code GeoApiContext} to make this request through. + * @param size The size of the static map. + * @return Returns a new {@code StaticMapRequest} with configured size. + */ + public static StaticMapsRequest newRequest(GeoApiContext context, Size size) { + return new StaticMapsRequest(context).size(size); + } +} diff --git a/src/main/java/com/google/maps/StaticMapsRequest.java b/src/main/java/com/google/maps/StaticMapsRequest.java new file mode 100644 index 000000000..327a84780 --- /dev/null +++ b/src/main/java/com/google/maps/StaticMapsRequest.java @@ -0,0 +1,470 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.StringJoin; +import com.google.maps.internal.StringJoin.UrlValue; +import com.google.maps.model.EncodedPolyline; +import com.google.maps.model.LatLng; +import com.google.maps.model.Size; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class StaticMapsRequest + extends PendingResultBase { + + static final ApiConfig API_CONFIG = new ApiConfig("/maps/api/staticmap"); + + public StaticMapsRequest(GeoApiContext context) { + super(context, API_CONFIG, ImageResult.Response.class); + } + + @Override + protected void validateRequest() { + if (!((params().containsKey("center") && params().containsKey("zoom")) + || params().containsKey("markers") + || params().containsKey("path"))) { + throw new IllegalArgumentException( + "Request must contain 'center' and 'zoom' if 'markers' or 'path' aren't present."); + } + if (!params().containsKey("size")) { + throw new IllegalArgumentException("Request must contain 'size'."); + } + } + + /** + * center (required if markers not present) defines the center of the map, + * equidistant from all edges of the map. + * + * @param location The location of the center of the map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest center(LatLng location) { + return param("center", location); + } + + /** + * center (required if markers not present) defines the center of the map, + * equidistant from all edges of the map. + * + * @param location The location of the center of the map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest center(String location) { + return param("center", location); + } + + /** + * zoom (required if markers not present) defines the zoom level of the map, which + * determines the magnification level of the map. + * + * @param zoom The zoom level of the region. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest zoom(int zoom) { + return param("zoom", zoom); + } + + /** + * size defines the rectangular dimensions of the map image. + * + * @param size The Size of the static map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest size(Size size) { + return param("size", size); + } + + /** + * scale affects the number of pixels that are returned. Setting scale + * to 2 returns twice as many pixels as scale set to 1 while retaining the same + * coverage area and level of detail (i.e. the contents of the map doesn't change). + * + * @param scale The scale of the static map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest scale(int scale) { + return param("scale", scale); + } + + public enum ImageFormat implements UrlValue { + png("png"), + png8("png8"), + png32("png32"), + gif("gif"), + jpg("jpg"), + jpgBaseline("jpg-baseline"); + + private final String format; + + ImageFormat(String format) { + this.format = format; + } + + @Override + public String toUrlValue() { + return format; + } + } + + /** + * format defines the format of the resulting image. By default, the Google Static + * Maps API creates PNG images. There are several possible formats including GIF, JPEG and PNG + * types. + * + * @param format The format of the static map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest format(ImageFormat format) { + return param("format", format); + } + + public enum StaticMapType implements UrlValue { + roadmap, + satellite, + terrain, + hybrid; + + @Override + public String toUrlValue() { + return this.name(); + } + } + + /** + * maptype defines the type of map to construct. + * + * @param maptype The map type of the static map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest maptype(StaticMapType maptype) { + return param("maptype", maptype); + } + + /** + * region defines the appropriate borders to display, based on geo-political + * sensitivities. Accepts a region code specified as a two-character ccTLD ('top-level domain') + * value. + * + * @param region The region of the static map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest region(String region) { + return param("region", region); + } + + public static class Markers implements UrlValue { + + public enum MarkersSize implements UrlValue { + tiny, + mid, + small, + normal; + + @Override + public String toUrlValue() { + return this.name(); + } + } + + public enum CustomIconAnchor implements UrlValue { + top, + bottom, + left, + right, + center, + topleft, + topright, + bottomleft, + bottomright; + + @Override + public String toUrlValue() { + return this.name(); + } + } + + private MarkersSize size; + private String color; + private String label; + private String customIconURL; + private CustomIconAnchor anchorPoint; + private Integer scale; + private final List locations = new ArrayList<>(); + + /** + * Specifies the size of marker. If no size parameter is set, the marker will appear in its + * default (normal) size. + * + * @param size The size of the markers. + */ + public void size(MarkersSize size) { + this.size = size; + } + + /** + * Specifies a 24-bit color (example: color=0xFFFFCC) or a predefined color from the set {black, + * brown, green, purple, yellow, blue, gray, orange, red, white}. + * + * @param color The color of the markers. + */ + public void color(String color) { + this.color = color; + } + + private static final Pattern labelPattern = Pattern.compile("^[A-Z0-9]$"); + + /** + * Specifies a single uppercase alphanumeric character from the set {A-Z, 0-9}. + * + * @param label The label to add to markers. + */ + public void label(String label) { + if (!labelPattern.matcher(label).matches()) { + throw new IllegalArgumentException( + "Label '" + label + "' doesn't match acceptable label pattern."); + } + + this.label = label; + } + + /** + * Set a custom icon for these markers. + * + * @param url URL for the custom icon. + * @param anchorPoint The anchor point for this custom icon. + */ + public void customIcon(String url, CustomIconAnchor anchorPoint) { + this.customIconURL = url; + this.anchorPoint = anchorPoint; + } + + /** + * Set a custom icon for these markers. + * + * @param url URL for the custom icon. + * @param anchorPoint The anchor point for this custom icon. + * @param scale Set the image density scale (1, 2, or 4) of the custom icon provided. + */ + public void customIcon(String url, CustomIconAnchor anchorPoint, int scale) { + this.customIconURL = url; + this.anchorPoint = anchorPoint; + this.scale = scale; + } + + /** + * Add the location of a marker. At least one is required. + * + * @param location The location of the added marker. + */ + public void addLocation(String location) { + locations.add(location); + } + + /** + * Add the location of a marker. At least one is required. + * + * @param location The location of the added marker. + */ + public void addLocation(LatLng location) { + locations.add(location.toUrlValue()); + } + + @Override + public String toUrlValue() { + List urlParts = new ArrayList<>(); + + if (customIconURL != null) { + urlParts.add("icon:" + customIconURL); + } + + if (anchorPoint != null) { + urlParts.add("anchor:" + anchorPoint.toUrlValue()); + } + + if (scale != null) { + urlParts.add("scale:" + scale); + } + + if (size != null && size != MarkersSize.normal) { + urlParts.add("size:" + size.toUrlValue()); + } + + if (color != null) { + urlParts.add("color:" + color); + } + + if (label != null) { + urlParts.add("label:" + label); + } + + urlParts.addAll(locations); + + return StringJoin.join('|', urlParts.toArray(new String[urlParts.size()])); + } + } + + /** + * markers parameter defines a set of one or more markers (map pins) at a set of + * locations. Each marker defined within a single markers declaration must exhibit the same visual + * style; if you wish to display markers with different styles, you will need to supply multiple + * markers parameters with separate style information. + * + * @param markers A group of markers with the same style. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest markers(Markers markers) { + return paramAddToList("markers", markers); + } + + public static class Path implements UrlValue { + + private int weight; + private String color; + private String fillcolor; + private boolean geodesic; + private final List points = new ArrayList<>(); + + /** + * Specifies the thickness of the path in pixels. If no weight parameter is set, the path will + * appear in its default thickness (5 pixels). + * + * @param weight The thickness of the path in pixels. + */ + public void weight(int weight) { + this.weight = weight; + } + + /** + * Specifies a 24-bit color (example: color=0xFFFFCC) or a predefined color from the set {black, + * brown, green, purple, yellow, blue, gray, orange, red, white}. + * + * @param color The color of the path. + */ + public void color(String color) { + this.color = color; + } + + /** + * Specifies a 24-bit color (example: color=0xFFFFCC) or a predefined color from the set {black, + * brown, green, purple, yellow, blue, gray, orange, red, white}. + * + * @param color The fill color. + */ + public void fillcolor(String color) { + this.fillcolor = color; + } + + /** + * Indicates that the requested path should be interpreted as a geodesic line that follows the + * curvature of the earth. + * + * @param geodesic Whether the path is geodesic. + */ + public void geodesic(boolean geodesic) { + this.geodesic = geodesic; + } + + /** + * Add a point to the path. At least two are required. + * + * @param point The point to add. + */ + public void addPoint(String point) { + points.add(point); + } + + /** + * Add a point to the path. At least two are required. + * + * @param point The point to add. + */ + public void addPoint(LatLng point) { + points.add(point.toUrlValue()); + } + + @Override + public String toUrlValue() { + List urlParts = new ArrayList<>(); + + if (weight > 0) { + urlParts.add("weight:" + weight); + } + + if (color != null) { + urlParts.add("color:" + color); + } + + if (fillcolor != null) { + urlParts.add("fillcolor:" + fillcolor); + } + + if (geodesic) { + urlParts.add("geodesic:" + geodesic); + } + + urlParts.addAll(points); + + return StringJoin.join('|', urlParts.toArray(new String[urlParts.size()])); + } + } + + /** + * The path parameter defines a set of one or more locations connected by a path to + * overlay on the map image. + * + * @param path A path to render atop the map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest path(Path path) { + return paramAddToList("path", path); + } + + /** + * The path parameter defines a set of one or more locations connected by a path to + * overlay on the map image. This variant of the method accepts the path as an EncodedPolyline. + * + * @param path A path to render atop the map, as an EncodedPolyline. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest path(EncodedPolyline path) { + return paramAddToList("path", "enc:" + path.getEncodedPath()); + } + + /** + * visible instructs the Google Static Maps API service to construct a map such that + * the existing locations remain visible. + * + * @param visibleLocation The location to be made visible in the requested Static Map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest visible(LatLng visibleLocation) { + return param("visible", visibleLocation); + } + + /** + * visible instructs the Google Static Maps API service to construct a map such that + * the existing locations remain visible. + * + * @param visibleLocation The location to be made visible in the requested Static Map. + * @return Returns this {@code StaticMapsRequest} for call chaining. + */ + public StaticMapsRequest visible(String visibleLocation) { + return param("visible", visibleLocation); + } +} diff --git a/src/main/java/com/google/maps/TextSearchRequest.java b/src/main/java/com/google/maps/TextSearchRequest.java new file mode 100644 index 000000000..0f73f6d5b --- /dev/null +++ b/src/main/java/com/google/maps/TextSearchRequest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import com.google.gson.FieldNamingPolicy; +import com.google.maps.errors.ApiException; +import com.google.maps.internal.ApiConfig; +import com.google.maps.internal.ApiResponse; +import com.google.maps.model.LatLng; +import com.google.maps.model.PlaceType; +import com.google.maps.model.PlacesSearchResponse; +import com.google.maps.model.PlacesSearchResult; +import com.google.maps.model.PriceLevel; +import com.google.maps.model.RankBy; + +/** + * A Text + * Search request. + */ +public class TextSearchRequest + extends PendingResultBase { + + static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/place/textsearch/json") + .fieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES); + + public TextSearchRequest(GeoApiContext context) { + super(context, API_CONFIG, Response.class); + } + + /** + * Specifies the text string on which to search, for example: {@code "restaurant"}. + * + * @param query The query string to search for. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest query(String query) { + return param("query", query); + } + + /** + * Specifies the latitude/longitude around which to retrieve place information. + * + * @param location The location of the center of the search. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest location(LatLng location) { + return param("location", location); + } + + /** + * Region used to influence search results. This parameter will only influence, not fully + * restrict, search results. If more relevant results exist outside of the specified region, they + * may be included. When this parameter is used, the country name is omitted from the resulting + * formatted_address for results in the specified region. + * + * @param region The ccTLD two-letter code of the region. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest region(String region) { + return param("region", region); + } + + /** + * Specifies the distance (in meters) within which to bias place results. + * + * @param radius The radius of the search bias. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest radius(int radius) { + if (radius > 50000) { + throw new IllegalArgumentException("The maximum allowed radius is 50,000 meters."); + } + return param("radius", String.valueOf(radius)); + } + + /** + * Restricts to places that are at least this price level. + * + * @param priceLevel The minimum price level to restrict results with. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest minPrice(PriceLevel priceLevel) { + return param("minprice", priceLevel); + } + + /** + * Restricts to places that are at most this price level. + * + * @param priceLevel The maximum price leve to restrict results with. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest maxPrice(PriceLevel priceLevel) { + return param("maxprice", priceLevel); + } + + /** + * Specifies one or more terms to be matched against the names of places, separated with space + * characters. + * + * @param name The name to search for. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest name(String name) { + return param("name", name); + } + + /** + * Restricts to only those places that are open for business at the time the query is sent. + * + * @param openNow Whether to restrict this search to open places. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest openNow(boolean openNow) { + return param("opennow", String.valueOf(openNow)); + } + + /** + * Returns the next 20 results from a previously run search. Setting pageToken will execute a + * search with the same parameters used previously — all parameters other than pageToken will be + * ignored. + * + * @param nextPageToken A {@code pageToken} from a prior result. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest pageToken(String nextPageToken) { + return param("pagetoken", nextPageToken); + } + + /** + * Specifies the order in which results are listed. + * + * @param ranking The rank by method. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest rankby(RankBy ranking) { + return param("rankby", ranking); + } + + /** + * Restricts the results to places matching the specified type. + * + * @param type The type of place to restrict the results with. + * @return Returns this {@code TextSearchRequest} for call chaining. + */ + public TextSearchRequest type(PlaceType type) { + return param("type", type); + } + + @Override + protected void validateRequest() { + + // All other parameters are ignored if pagetoken is specified. + if (params().containsKey("pagetoken")) { + return; + } + + if (!params().containsKey("query") && !params().containsKey("type")) { + throw new IllegalArgumentException( + "Request must contain 'query' or a 'pageToken'. If a 'type' is specified 'query' becomes optional."); + } + + if (params().containsKey("location") && !params().containsKey("radius")) { + throw new IllegalArgumentException( + "Request must contain 'radius' parameter when it contains a 'location' parameter."); + } + } + + public static class Response implements ApiResponse { + + public String status; + public String htmlAttributions[]; + public PlacesSearchResult results[]; + public String nextPageToken; + public String errorMessage; + + @Override + public boolean successful() { + return "OK".equals(status) || "ZERO_RESULTS".equals(status); + } + + @Override + public PlacesSearchResponse getResult() { + PlacesSearchResponse result = new PlacesSearchResponse(); + result.htmlAttributions = htmlAttributions; + result.results = results; + result.nextPageToken = nextPageToken; + return result; + } + + @Override + public ApiException getError() { + if (successful()) { + return null; + } + return ApiException.from(status, errorMessage); + } + } +} diff --git a/src/main/java/com/google/maps/TimeZoneApi.java b/src/main/java/com/google/maps/TimeZoneApi.java index bd9c2e29d..38f76d1dd 100644 --- a/src/main/java/com/google/maps/TimeZoneApi.java +++ b/src/main/java/com/google/maps/TimeZoneApi.java @@ -20,30 +20,38 @@ import com.google.maps.internal.ApiConfig; import com.google.maps.internal.ApiResponse; import com.google.maps.model.LatLng; - import java.util.TimeZone; /** - *

    The Google Time Zone API provides a simple interface to request the time zone - * for a location on the earth. - *

    See documentation. + * The Google Time Zone API provides a simple interface to request the time zone for a location on + * the earth. + * + *

    See the Time Zone API + * documentation. */ public class TimeZoneApi { - private static final ApiConfig API_CONFIG = new ApiConfig("/maps/api/timezone/json") - .fieldNamingPolicy(FieldNamingPolicy.IDENTITY); + private static final ApiConfig API_CONFIG = + new ApiConfig("/maps/api/timezone/json").fieldNamingPolicy(FieldNamingPolicy.IDENTITY); - private TimeZoneApi() { - } + private TimeZoneApi() {} /** - * Retrieve the {@link java.util.TimeZone} for the given location. + * Retrieves the {@link java.util.TimeZone} for the given location. + * + * @param context The {@link GeoApiContext} to make requests through. + * @param location The location for which to retrieve a time zone. + * @return Returns the time zone as a {@link PendingResult}. */ public static PendingResult getTimeZone(GeoApiContext context, LatLng location) { - return context.get(API_CONFIG, Response.class, - "location", location.toString(), + return context.get( + API_CONFIG, + Response.class, + "location", + location.toString(), // Java has its own lookup for time -> DST, so we really only need to fetch the TZ id. // "timestamp" is, in effect, ignored. - "timestamp", "0"); + "timestamp", + "0"); } private static class Response implements ApiResponse { diff --git a/src/main/java/com/google/maps/errors/AccessNotConfiguredException.java b/src/main/java/com/google/maps/errors/AccessNotConfiguredException.java index a24391724..fb057ac39 100644 --- a/src/main/java/com/google/maps/errors/AccessNotConfiguredException.java +++ b/src/main/java/com/google/maps/errors/AccessNotConfiguredException.java @@ -21,6 +21,8 @@ */ public class AccessNotConfiguredException extends ApiException { + private static final long serialVersionUID = -9167434506751721386L; + public AccessNotConfiguredException(String errorMessage) { super(errorMessage); } diff --git a/src/main/java/com/google/maps/errors/ApiError.java b/src/main/java/com/google/maps/errors/ApiError.java index a54821a27..5a93a4d40 100644 --- a/src/main/java/com/google/maps/errors/ApiError.java +++ b/src/main/java/com/google/maps/errors/ApiError.java @@ -15,9 +15,7 @@ package com.google.maps.errors; -/** - * An error returned by the API, including some extra information for aiding in debugging. - */ +/** An error returned by the API, including some extra information for aiding in debugging. */ public class ApiError { public int code; public String message; diff --git a/src/main/java/com/google/maps/errors/ApiException.java b/src/main/java/com/google/maps/errors/ApiException.java index 6685049fe..b262456f4 100644 --- a/src/main/java/com/google/maps/errors/ApiException.java +++ b/src/main/java/com/google/maps/errors/ApiException.java @@ -16,8 +16,8 @@ package com.google.maps.errors; /** - * ApiException and it's descendants represent an error returned by the remote API. API errors - * are determined by the {@code status} field returned in any of the Geo API responses. + * ApiException and its descendants represent an error returned by the remote API. API errors are + * determined by the {@code status} field returned in any of the Geo API responses. */ public class ApiException extends Exception { private static final long serialVersionUID = -6550606366694345191L; @@ -27,31 +27,39 @@ protected ApiException(String message) { } /** - * Construct the appropriate ApiException from the response. If the response was successful, - * this method will return null. + * Construct the appropriate ApiException from the response. If the response was successful, this + * method will return null. * - * @param status The status field returned from the API + * @param status The status field returned from the API * @param errorMessage The error message returned from the API * @return The appropriate ApiException based on the status or null if no error occurred. */ public static ApiException from(String status, String errorMessage) { // Classic Geo API error formats if ("OK".equals(status)) { - return null; + return null; } else if ("INVALID_REQUEST".equals(status)) { - return new InvalidRequestException(errorMessage); + return new InvalidRequestException(errorMessage); } else if ("MAX_ELEMENTS_EXCEEDED".equals(status)) { - return new MaxElementsExceededException(errorMessage); + return new MaxElementsExceededException(errorMessage); + } else if ("MAX_ROUTE_LENGTH_EXCEEDED".equals(status)) { + return new MaxRouteLengthExceededException(errorMessage); + } else if ("MAX_WAYPOINTS_EXCEEDED".equals(status)) { + return new MaxWaypointsExceededException(errorMessage); } else if ("NOT_FOUND".equals(status)) { - return new NotFoundException(errorMessage); + return new NotFoundException(errorMessage); } else if ("OVER_QUERY_LIMIT".equals(status)) { - return new OverQueryLimitException(errorMessage); + if ("You have exceeded your daily request quota for this API. If you did not set a custom daily request quota, verify your project has an active billing account: http://g.co/dev/maps-no-account" + .equalsIgnoreCase(errorMessage)) { + return new OverDailyLimitException(errorMessage); + } + return new OverQueryLimitException(errorMessage); } else if ("REQUEST_DENIED".equals(status)) { - return new RequestDeniedException(errorMessage); + return new RequestDeniedException(errorMessage); } else if ("UNKNOWN_ERROR".equals(status)) { - return new UnknownErrorException(errorMessage); + return new UnknownErrorException(errorMessage); } else if ("ZERO_RESULTS".equals(status)) { - return new ZeroResultsException(errorMessage); + return new ZeroResultsException(errorMessage); } // New-style Geo API error formats @@ -65,10 +73,24 @@ public static ApiException from(String status, String errorMessage) { return new RequestDeniedException(errorMessage); } + // Geolocation Errors + if ("keyInvalid".equals(status)) { + return new AccessNotConfiguredException(errorMessage); + } else if ("dailyLimitExceeded".equals(status)) { + return new OverDailyLimitException(errorMessage); + } else if ("userRateLimitExceeded".equals(status)) { + return new OverQueryLimitException(errorMessage); + } else if ("notFound".equals(status)) { + return new NotFoundException(errorMessage); + } else if ("parseError".equals(status)) { + return new InvalidRequestException(errorMessage); + } else if ("invalid".equals(status)) { + return new InvalidRequestException(errorMessage); + } + // We've hit an unknown error. This is not a state we should hit, // but we don't want to crash a user's application if we introduce a new error. - return new UnknownErrorException("An unexpected error occurred. " - + "Status: " + status + ", " - + "Message: " + errorMessage); + return new UnknownErrorException( + "An unexpected error occurred. Status: " + status + ", Message: " + errorMessage); } } diff --git a/src/main/java/com/google/maps/errors/InvalidRequestException.java b/src/main/java/com/google/maps/errors/InvalidRequestException.java index a8e2af98c..b72051d01 100644 --- a/src/main/java/com/google/maps/errors/InvalidRequestException.java +++ b/src/main/java/com/google/maps/errors/InvalidRequestException.java @@ -15,9 +15,7 @@ package com.google.maps.errors; -/** - * Indicates that the API received a malformed request. - */ +/** Indicates that the API received a malformed request. */ public class InvalidRequestException extends ApiException { private static final long serialVersionUID = -5682669561780594333L; diff --git a/src/main/java/com/google/maps/errors/MaxElementsExceededException.java b/src/main/java/com/google/maps/errors/MaxElementsExceededException.java index e5ea2c0fb..7137b1a60 100644 --- a/src/main/java/com/google/maps/errors/MaxElementsExceededException.java +++ b/src/main/java/com/google/maps/errors/MaxElementsExceededException.java @@ -18,7 +18,8 @@ /** * Indicates that the product of origins and destinations exceeds the per-query limit. * - * @see Limits + * @see + * Limits */ public class MaxElementsExceededException extends ApiException { diff --git a/src/main/java/com/google/maps/internal/ExceptionResult.java b/src/main/java/com/google/maps/errors/MaxRouteLengthExceededException.java similarity index 50% rename from src/main/java/com/google/maps/internal/ExceptionResult.java rename to src/main/java/com/google/maps/errors/MaxRouteLengthExceededException.java index d3b1a1af5..5b033e405 100644 --- a/src/main/java/com/google/maps/internal/ExceptionResult.java +++ b/src/main/java/com/google/maps/errors/MaxRouteLengthExceededException.java @@ -13,36 +13,22 @@ * permissions and limitations under the License. */ -package com.google.maps.internal; - -import com.google.maps.PendingResult; +package com.google.maps.errors; /** - * This class centralizes failure handling, independent of the calling style. + * Indicates that the requested route is too long and cannot be processed. + * + *

    This error occurs when more complex directions are returned. Try reducing the number of + * waypoints, turns, or instructions. + * + * @see + * Status Codes */ -public class ExceptionResult implements PendingResult { - private final Exception exception; - - public ExceptionResult(Exception exception) { - this.exception = exception; - } +public class MaxRouteLengthExceededException extends ApiException { - @Override - public void setCallback(Callback callback) { - callback.onFailure(exception); - } - - @Override - public T await() throws Exception { - throw exception; - } - - @Override - public T awaitIgnoreError() { - return null; - } + private static final long serialVersionUID = 5926526363472768479L; - @Override - public void cancel() { + public MaxRouteLengthExceededException(String errorMessage) { + super(errorMessage); } } diff --git a/src/main/java/com/google/maps/errors/MaxWaypointsExceededException.java b/src/main/java/com/google/maps/errors/MaxWaypointsExceededException.java new file mode 100644 index 000000000..4b93e1b0d --- /dev/null +++ b/src/main/java/com/google/maps/errors/MaxWaypointsExceededException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.errors; + +/** + * Indicates that too many waypoints were provided in the request. + * + * @see + * Status Codes + */ +public class MaxWaypointsExceededException extends ApiException { + private static final long serialVersionUID = 1L; + + public MaxWaypointsExceededException(String errorMessage) { + super(errorMessage); + } +} diff --git a/src/main/java/com/google/maps/errors/NotFoundException.java b/src/main/java/com/google/maps/errors/NotFoundException.java index 68937e106..052864fa1 100644 --- a/src/main/java/com/google/maps/errors/NotFoundException.java +++ b/src/main/java/com/google/maps/errors/NotFoundException.java @@ -1,8 +1,23 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package com.google.maps.errors; /** - * Indicates at least one of the locations specified in the request's origin, destination, - * or waypoints could not be geocoded. + * Indicates at least one of the locations specified in the request's origin, destination, or + * waypoints could not be geocoded. */ public class NotFoundException extends ApiException { diff --git a/src/main/java/com/google/maps/errors/OverDailyLimitException.java b/src/main/java/com/google/maps/errors/OverDailyLimitException.java index 74d8d578f..9f7a28cf7 100644 --- a/src/main/java/com/google/maps/errors/OverDailyLimitException.java +++ b/src/main/java/com/google/maps/errors/OverDailyLimitException.java @@ -15,11 +15,11 @@ package com.google.maps.errors; -/** - * Indicates that the requesting account has exceeded daily quota. - */ +/** Indicates that the requesting account has exceeded its daily quota. */ public class OverDailyLimitException extends ApiException { + private static final long serialVersionUID = 9172790459877314621L; + public OverDailyLimitException(String errorMessage) { super(errorMessage); } diff --git a/src/main/java/com/google/maps/errors/OverQueryLimitException.java b/src/main/java/com/google/maps/errors/OverQueryLimitException.java index 98fd6f1d3..ded8bd084 100644 --- a/src/main/java/com/google/maps/errors/OverQueryLimitException.java +++ b/src/main/java/com/google/maps/errors/OverQueryLimitException.java @@ -15,9 +15,7 @@ package com.google.maps.errors; -/** - * Indicates that the requesting account has exceeded short-term quota. - */ +/** Indicates that the requesting account has exceeded its short-term quota. */ public class OverQueryLimitException extends ApiException { private static final long serialVersionUID = -6888513535435397042L; diff --git a/src/main/java/com/google/maps/errors/RequestDeniedException.java b/src/main/java/com/google/maps/errors/RequestDeniedException.java index 2fad493c2..b3f9aca93 100644 --- a/src/main/java/com/google/maps/errors/RequestDeniedException.java +++ b/src/main/java/com/google/maps/errors/RequestDeniedException.java @@ -15,9 +15,7 @@ package com.google.maps.errors; -/** - * Indicates that the API denied the request. Check the message for more detail. - */ +/** Indicates that the API denied the request. Check the message for more detail. */ public class RequestDeniedException extends ApiException { private static final long serialVersionUID = -1434641617962369958L; diff --git a/src/main/java/com/google/maps/errors/ZeroResultsException.java b/src/main/java/com/google/maps/errors/ZeroResultsException.java index cc11907e8..5b93c5174 100644 --- a/src/main/java/com/google/maps/errors/ZeroResultsException.java +++ b/src/main/java/com/google/maps/errors/ZeroResultsException.java @@ -18,10 +18,9 @@ /** * Indicates that no results were returned. * - *

    In some cases, this will be treated as a success - * state and you will only see an empty array. For time zone data, it means that no time zone - * information could be found for the specified position or time. Confirm that the request is for - * a location on land, and not over water. + *

    In some cases, this will be treated as a success state and you will only see an empty array. + * For time zone data, it means that no time zone information could be found for the specified + * position or time. Confirm that the request is for a location on land, and not over water. */ public class ZeroResultsException extends ApiException { diff --git a/src/main/java/com/google/maps/internal/ApiConfig.java b/src/main/java/com/google/maps/internal/ApiConfig.java index 676df3663..14e8100d0 100644 --- a/src/main/java/com/google/maps/internal/ApiConfig.java +++ b/src/main/java/com/google/maps/internal/ApiConfig.java @@ -1,15 +1,29 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package com.google.maps.internal; import com.google.gson.FieldNamingPolicy; -/** - * API configuration builder. Defines fields that are variable per-API. - */ +/** API configuration builder. Defines fields that are variable per-API. */ public class ApiConfig { public String path; public FieldNamingPolicy fieldNamingPolicy = FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; public String hostName = "https://maps.googleapis.com"; public boolean supportsClientId = true; + public String requestVerb = "GET"; public ApiConfig(String path) { this.path = path; @@ -29,4 +43,9 @@ public ApiConfig supportsClientId(boolean supportsClientId) { this.supportsClientId = supportsClientId; return this; } + + public ApiConfig requestVerb(String requestVerb) { + this.requestVerb = requestVerb; + return this; + } } diff --git a/src/main/java/com/google/maps/internal/ApiResponse.java b/src/main/java/com/google/maps/internal/ApiResponse.java index 46cc2f390..cb931c8fc 100644 --- a/src/main/java/com/google/maps/internal/ApiResponse.java +++ b/src/main/java/com/google/maps/internal/ApiResponse.java @@ -17,9 +17,7 @@ import com.google.maps.errors.ApiException; -/** - * All Geo API responses implement this Interface. - */ +/** All Geo API responses implement this Interface. */ public interface ApiResponse { boolean successful(); diff --git a/src/main/java/com/google/maps/internal/DayOfWeekAdapter.java b/src/main/java/com/google/maps/internal/DayOfWeekAdapter.java new file mode 100644 index 000000000..02857dd7c --- /dev/null +++ b/src/main/java/com/google/maps/internal/DayOfWeekAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.google.maps.model.OpeningHours.Period.OpenClose.DayOfWeek; +import java.io.IOException; + +/** + * This class handles conversion from JSON to {@link DayOfWeek}. + * + *

    Please see GSON + * Type Adapter for more detail. + */ +public class DayOfWeekAdapter extends TypeAdapter { + + @Override + public DayOfWeek read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + + if (reader.peek() == JsonToken.NUMBER) { + int day = reader.nextInt(); + + switch (day) { + case 0: + return DayOfWeek.SUNDAY; + case 1: + return DayOfWeek.MONDAY; + case 2: + return DayOfWeek.TUESDAY; + case 3: + return DayOfWeek.WEDNESDAY; + case 4: + return DayOfWeek.THURSDAY; + case 5: + return DayOfWeek.FRIDAY; + case 6: + return DayOfWeek.SATURDAY; + } + } + + return DayOfWeek.UNKNOWN; + } + + /** This method is not implemented. */ + @Override + public void write(JsonWriter writer, DayOfWeek value) throws IOException { + throw new UnsupportedOperationException("Unimplemented method"); + } +} diff --git a/src/main/java/com/google/maps/internal/DistanceAdapter.java b/src/main/java/com/google/maps/internal/DistanceAdapter.java index da0850499..4e73ebf41 100644 --- a/src/main/java/com/google/maps/internal/DistanceAdapter.java +++ b/src/main/java/com/google/maps/internal/DistanceAdapter.java @@ -20,15 +20,14 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import com.google.maps.model.Distance; - import java.io.IOException; /** * This class handles conversion from JSON to {@link Distance}. * - *

    Please see - * {@url https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/TypeAdapter.html} - * for more detail. + *

    Please see GSON + * Type Adapter for more detail. */ public class DistanceAdapter extends TypeAdapter { @@ -36,10 +35,10 @@ public class DistanceAdapter extends TypeAdapter { * Read a distance object from a Directions API result and convert it to a {@link Distance}. * *

    We are expecting to receive something akin to the following: + * *

        * {
    -   *   "value": 207,
    -       "text": "0.1 mi"
    +   *   "value": 207, "text": "0.1 mi"
        * }
        * 
    */ @@ -60,20 +59,15 @@ public Distance read(JsonReader reader) throws IOException { } else if (name.equals("value")) { distance.inMeters = reader.nextLong(); } - } reader.endObject(); return distance; } - /** - * This method is not implemented. - */ + /** This method is not implemented. */ @Override public void write(JsonWriter writer, Distance value) throws IOException { throw new UnsupportedOperationException("Unimplemented method"); } - } - diff --git a/src/main/java/com/google/maps/internal/DurationAdapter.java b/src/main/java/com/google/maps/internal/DurationAdapter.java index 1fb8bf34b..086d59952 100644 --- a/src/main/java/com/google/maps/internal/DurationAdapter.java +++ b/src/main/java/com/google/maps/internal/DurationAdapter.java @@ -19,16 +19,16 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; +import com.google.maps.model.Distance; import com.google.maps.model.Duration; - import java.io.IOException; /** * This class handles conversion from JSON to {@link Distance}. * - *

    Please see - * {@url https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/TypeAdapter.html} - * for more detail. + *

    Please see GSON + * Type Adapter for more detail. */ public class DurationAdapter extends TypeAdapter { @@ -36,10 +36,11 @@ public class DurationAdapter extends TypeAdapter { * Read a distance object from a Directions API result and convert it to a {@link Distance}. * *

    We are expecting to receive something akin to the following: + * *

        * {
        *   "value": 207,
    -       "text": "0.1 mi"
    +   *   "text": "0.1 mi"
        * }
        * 
    */ @@ -60,20 +61,15 @@ public Duration read(JsonReader reader) throws IOException { } else if (name.equals("value")) { duration.inSeconds = reader.nextLong(); } - } reader.endObject(); return duration; } - /** - * This method is not implemented. - */ + /** This method is not implemented. */ @Override public void write(JsonWriter writer, Duration value) throws IOException { throw new UnsupportedOperationException("Unimplemented method"); } - } - diff --git a/src/main/java/com/google/maps/internal/EncodedPolylineInstanceCreator.java b/src/main/java/com/google/maps/internal/EncodedPolylineInstanceCreator.java new file mode 100644 index 000000000..fcec2492d --- /dev/null +++ b/src/main/java/com/google/maps/internal/EncodedPolylineInstanceCreator.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 by https://github.com/ArielY15 + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.gson.InstanceCreator; +import com.google.maps.model.EncodedPolyline; +import java.lang.reflect.Type; + +public class EncodedPolylineInstanceCreator implements InstanceCreator { + private String points; + + public EncodedPolylineInstanceCreator(String points) { + this.points = points; + } + + @Override + public EncodedPolyline createInstance(Type type) { + return new EncodedPolyline(points); + } +} diff --git a/src/main/java/com/google/maps/internal/ExceptionsAllowedToRetry.java b/src/main/java/com/google/maps/internal/ExceptionsAllowedToRetry.java new file mode 100644 index 000000000..ad7c395d7 --- /dev/null +++ b/src/main/java/com/google/maps/internal/ExceptionsAllowedToRetry.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.maps.errors.ApiException; +import java.util.HashSet; + +public final class ExceptionsAllowedToRetry extends HashSet> { + + private static final long serialVersionUID = 5283992240187266422L; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("ExceptionsAllowedToRetry["); + + Object[] array = toArray(); + for (int i = 0; i < array.length; i++) { + sb.append(array[i]); + if (i < array.length - 1) { + sb.append(", "); + } + } + + sb.append(']'); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/internal/FareAdapter.java b/src/main/java/com/google/maps/internal/FareAdapter.java index 08d3e6de4..116b1be51 100644 --- a/src/main/java/com/google/maps/internal/FareAdapter.java +++ b/src/main/java/com/google/maps/internal/FareAdapter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package com.google.maps.internal; import com.google.gson.TypeAdapter; @@ -5,14 +20,11 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import com.google.maps.model.Fare; - import java.io.IOException; import java.math.BigDecimal; import java.util.Currency; -/** - * This class handles conversion from JSON to {@link com.google.maps.model.Fare}. - */ +/** This class handles conversion from JSON to {@link com.google.maps.model.Fare}. */ public class FareAdapter extends TypeAdapter { /** @@ -39,6 +51,9 @@ public Fare read(JsonReader reader) throws IOException { } else if ("value".equals(key)) { // this relies on nextString() being able to coerce raw numbers to strings fare.value = new BigDecimal(reader.nextString()); + } else { + // Be forgiving of unexpected values + reader.skipValue(); } } reader.endObject(); @@ -46,9 +61,7 @@ public Fare read(JsonReader reader) throws IOException { return fare; } - /** - * This method is not implemented. - */ + /** This method is not implemented. */ @Override public void write(JsonWriter out, Fare value) throws IOException { throw new UnsupportedOperationException("Unimplemented method"); diff --git a/src/main/java/com/google/maps/internal/GaePendingResult.java b/src/main/java/com/google/maps/internal/GaePendingResult.java new file mode 100644 index 000000000..9f6b2a13d --- /dev/null +++ b/src/main/java/com/google/maps/internal/GaePendingResult.java @@ -0,0 +1,266 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.appengine.api.urlfetch.HTTPHeader; +import com.google.appengine.api.urlfetch.HTTPRequest; +import com.google.appengine.api.urlfetch.HTTPResponse; +import com.google.appengine.api.urlfetch.URLFetchService; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.maps.GeolocationApi; +import com.google.maps.ImageResult; +import com.google.maps.PendingResult; +import com.google.maps.errors.ApiException; +import com.google.maps.errors.UnknownErrorException; +import com.google.maps.metrics.RequestMetrics; +import com.google.maps.model.AddressComponentType; +import com.google.maps.model.AddressType; +import com.google.maps.model.Distance; +import com.google.maps.model.Duration; +import com.google.maps.model.EncodedPolyline; +import com.google.maps.model.Fare; +import com.google.maps.model.LatLng; +import com.google.maps.model.LocationType; +import com.google.maps.model.OpeningHours.Period.OpenClose.DayOfWeek; +import com.google.maps.model.PlaceDetails.Review.AspectRating.RatingType; +import com.google.maps.model.PriceLevel; +import com.google.maps.model.TravelMode; +import com.google.maps.model.VehicleType; +import java.io.IOException; +import java.nio.charset.Charset; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A PendingResult backed by a HTTP call executed by Google App Engine URL Fetch capability, a + * deserialization step using Gson, and a retry policy. + * + *

    {@code T} is the type of the result of this pending result, and {@code R} is the type of the + * request. + */ +public class GaePendingResult> implements PendingResult { + private final HTTPRequest request; + private final URLFetchService client; + private final Class responseClass; + private final FieldNamingPolicy fieldNamingPolicy; + private final Integer maxRetries; + private final ExceptionsAllowedToRetry exceptionsAllowedToRetry; + private final RequestMetrics metrics; + + private long errorTimeOut; + private int retryCounter = 0; + private long cumulativeSleepTime = 0; + private Future call; + + private static final Logger LOG = LoggerFactory.getLogger(GaePendingResult.class.getName()); + private static final List RETRY_ERROR_CODES = Arrays.asList(500, 503, 504); + + /** + * @param request HTTP request to execute. + * @param client The client used to execute the request. + * @param responseClass Model class to unmarshal JSON body content. + * @param fieldNamingPolicy FieldNamingPolicy for unmarshaling JSON. + * @param errorTimeOut Number of milliseconds to re-send erroring requests. + * @param maxRetries Number of times allowed to re-send erroring requests. + */ + public GaePendingResult( + HTTPRequest request, + URLFetchService client, + Class responseClass, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeOut, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics) { + this.request = request; + this.client = client; + this.responseClass = responseClass; + this.fieldNamingPolicy = fieldNamingPolicy; + this.errorTimeOut = errorTimeOut; + this.maxRetries = maxRetries; + this.exceptionsAllowedToRetry = exceptionsAllowedToRetry; + this.metrics = metrics; + + metrics.startNetwork(); + this.call = client.fetchAsync(request); + } + + @Override + public void setCallback(Callback callback) { + throw new RuntimeException("setCallback not implemented for Google App Engine"); + } + + @Override + public T await() throws ApiException, IOException, InterruptedException { + try { + HTTPResponse result = call.get(); + metrics.endNetwork(); + return parseResponse(this, result); + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else { + // According to + // https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/urlfetch/URLFetchService + // all exceptions should be subclass of IOException so this should not happen. + throw new UnknownErrorException("Unexpected exception from " + e.getMessage()); + } + } + } + + @Override + public T awaitIgnoreError() { + try { + return await(); + } catch (Exception e) { + return null; + } + } + + @Override + public void cancel() { + call.cancel(true); + } + + @SuppressWarnings("unchecked") + private T parseResponse(GaePendingResult request, HTTPResponse response) + throws IOException, ApiException, InterruptedException { + try { + T result = parseResponseInternal(request, response); + metrics.endRequest(null, response.getResponseCode(), retryCounter); + return result; + } catch (Exception e) { + metrics.endRequest(e, response.getResponseCode(), retryCounter); + throw e; + } + } + + @SuppressWarnings("unchecked") + private T parseResponseInternal(GaePendingResult request, HTTPResponse response) + throws IOException, ApiException, InterruptedException { + if (shouldRetry(response)) { + // Retry is a blocking method, but that's OK. If we're here, we're either in an await() + // call, which is blocking anyway, or we're handling a callback in a separate thread. + return request.retry(); + } + + byte[] bytes = response.getContent(); + R resp; + + String contentType = null; + for (HTTPHeader header : response.getHeaders()) { + if (header.getName().equalsIgnoreCase("Content-Type")) { + contentType = header.getValue(); + } + } + + if (contentType != null + && contentType.startsWith("image") + && responseClass == ImageResult.Response.class + && response.getResponseCode() == 200) { + ImageResult result = new ImageResult(contentType, bytes); + return (T) result; + } + + Gson gson = + new GsonBuilder() + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()) + .registerTypeAdapter(Distance.class, new DistanceAdapter()) + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .registerTypeAdapter(Fare.class, new FareAdapter()) + .registerTypeAdapter(LatLng.class, new LatLngAdapter()) + .registerTypeAdapter( + AddressComponentType.class, new SafeEnumAdapter<>(AddressComponentType.UNKNOWN)) + .registerTypeAdapter(AddressType.class, new SafeEnumAdapter<>(AddressType.UNKNOWN)) + .registerTypeAdapter(TravelMode.class, new SafeEnumAdapter<>(TravelMode.UNKNOWN)) + .registerTypeAdapter(LocationType.class, new SafeEnumAdapter<>(LocationType.UNKNOWN)) + .registerTypeAdapter(RatingType.class, new SafeEnumAdapter<>(RatingType.UNKNOWN)) + .registerTypeAdapter(VehicleType.class, new SafeEnumAdapter<>(VehicleType.OTHER)) + .registerTypeAdapter(DayOfWeek.class, new DayOfWeekAdapter()) + .registerTypeAdapter(PriceLevel.class, new PriceLevelAdapter()) + .registerTypeAdapter(Instant.class, new InstantAdapter()) + .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()) + .registerTypeAdapter(GeolocationApi.Response.class, new GeolocationResponseAdapter()) + .registerTypeAdapter(EncodedPolyline.class, new EncodedPolylineInstanceCreator("")) + .setFieldNamingPolicy(fieldNamingPolicy) + .create(); + + // Attempt to de-serialize before checking the HTTP status code, as there may be JSON in the + // body that we can use to provide a more descriptive exception. + try { + resp = gson.fromJson(new String(bytes, "utf8"), responseClass); + } catch (JsonSyntaxException e) { + // Check HTTP status for a more suitable exception + if (response.getResponseCode() > 399) { + // Some of the APIs return 200 even when the API request fails, as long as the transport + // mechanism succeeds. In these cases, INVALID_RESPONSE, etc are handled by the Gson + // parsing. + throw new IOException( + String.format( + "Server Error: %d %s", + response.getResponseCode(), + new String(response.getContent(), Charset.defaultCharset()))); + } + + // Otherwise just cough up the syntax exception. + throw e; + } + + if (resp.successful()) { + // Return successful responses + return resp.getResult(); + } else { + ApiException e = resp.getError(); + if (shouldRetry(e)) { + // Retry over_query_limit errors + return request.retry(); + } else { + // Throw anything else, including OQLs if we've spent too much time retrying + throw e; + } + } + } + + private T retry() throws IOException, ApiException, InterruptedException { + retryCounter++; + LOG.info("Retrying request. Retry #{}", retryCounter); + metrics.startNetwork(); + this.call = client.fetchAsync(request); + return this.await(); + } + + private boolean shouldRetry(HTTPResponse response) { + return RETRY_ERROR_CODES.contains(response.getResponseCode()) + && cumulativeSleepTime < errorTimeOut + && (maxRetries == null || retryCounter < maxRetries); + } + + private boolean shouldRetry(ApiException exception) { + return exceptionsAllowedToRetry.contains(exception.getClass()) + && cumulativeSleepTime < errorTimeOut + && (maxRetries == null || retryCounter < maxRetries); + } +} diff --git a/src/main/java/com/google/maps/internal/GeolocationResponseAdapter.java b/src/main/java/com/google/maps/internal/GeolocationResponseAdapter.java new file mode 100644 index 000000000..5102a4c0d --- /dev/null +++ b/src/main/java/com/google/maps/internal/GeolocationResponseAdapter.java @@ -0,0 +1,133 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.google.maps.GeolocationApi; +import java.io.IOException; + +public class GeolocationResponseAdapter extends TypeAdapter { + /** + * Reads in a JSON object to create a Geolocation Response. See: + * https://developers.google.com/maps/documentation/geolocation/intro#responses + * + *

    Success Case: + * + *

    +   *   {
    +   *     "location": {
    +   *       "lat": 51.0,
    +   *       "lng": -0.1
    +   *     },
    +   *     "accuracy": 1200.4
    +   *   }
    +   * 
    + * + * Error Case: The response contains an object with a single error object with the following keys: + * + *

    code: This is the same as the HTTP status of the response. {@code message}: A short + * description of the error. {@code errors}: A list of errors which occurred. Each error contains + * an identifier for the type of error (the reason) and a short description (the message). For + * example, sending invalid JSON will return the following error: + * + *

    +   *   {
    +   *     "error": {
    +   *       "errors": [ {
    +   *           "domain": "geolocation",
    +   *           "reason": "notFound",
    +   *           "message": "Not Found",
    +   *           "debugInfo": "status: ZERO_RESULTS\ncom.google.api.server.core.Fault: Immu...
    +   *       }],
    +   *       "code": 404,
    +   *       "message": "Not Found"
    +   *     }
    +   *   }
    +   * 
    + */ + @Override + public GeolocationApi.Response read(JsonReader reader) throws IOException { + + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + GeolocationApi.Response response = new GeolocationApi.Response(); + LatLngAdapter latLngAdapter = new LatLngAdapter(); + + reader.beginObject(); // opening { + while (reader.hasNext()) { + String name = reader.nextName(); + // two different objects could be returned a success object containing "location" and + // "accuracy" keys or an error object containing an "error" key + if (name.equals("location")) { + // we already have a parser for the LatLng object so lets use that + response.location = latLngAdapter.read(reader); + } else if (name.equals("accuracy")) { + response.accuracy = reader.nextDouble(); + } else if (name.equals("error")) { + reader.beginObject(); // the error key leads to another object... + while (reader.hasNext()) { + String errName = reader.nextName(); + // ...with keys "errors", "code" and "message" + if (errName.equals("code")) { + response.code = reader.nextInt(); + } else if (errName.equals("message")) { + response.message = reader.nextString(); + } else if (errName.equals("errors")) { + reader.beginArray(); // its plural because its an array of errors... + while (reader.hasNext()) { + reader.beginObject(); // ...and each error array element is an object... + while (reader.hasNext()) { + errName = reader.nextName(); + // ...with keys "reason", "domain", "debugInfo", "location", "locationType", and + // "message" (again) + if (errName.equals("reason")) { + response.reason = reader.nextString(); + } else if (errName.equals("domain")) { + response.domain = reader.nextString(); + } else if (errName.equals("debugInfo")) { + response.debugInfo = reader.nextString(); + } else if (errName.equals("message")) { + // have this already + reader.nextString(); + } else if (errName.equals("location")) { + reader.nextString(); + } else if (errName.equals("locationType")) { + reader.nextString(); + } + } + reader.endObject(); + } + reader.endArray(); + } + } + reader.endObject(); // closing } + } + } + reader.endObject(); + return response; + } + + /** Not supported. */ + @Override + public void write(JsonWriter out, GeolocationApi.Response value) throws IOException { + throw new UnsupportedOperationException("Unimplemented method."); + } +} diff --git a/src/main/java/com/google/maps/internal/HttpHeaders.java b/src/main/java/com/google/maps/internal/HttpHeaders.java new file mode 100644 index 000000000..844d8ceed --- /dev/null +++ b/src/main/java/com/google/maps/internal/HttpHeaders.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +/** Contains HTTP header name constants. */ +public final class HttpHeaders { + + /** The HTTP {@code X-Goog-Maps-Experience-ID} header field name. */ + public static final String X_GOOG_MAPS_EXPERIENCE_ID = "X-Goog-Maps-Experience-ID"; +} diff --git a/src/main/java/com/google/maps/internal/InstantAdapter.java b/src/main/java/com/google/maps/internal/InstantAdapter.java new file mode 100644 index 000000000..d31b2cdb7 --- /dev/null +++ b/src/main/java/com/google/maps/internal/InstantAdapter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.time.Instant; + +/** This class handles conversion from JSON to {@link Instant}. */ +public class InstantAdapter extends TypeAdapter { + + /** Read a time from the Places API and convert to a {@link Instant} */ + @Override + public Instant read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + + if (reader.peek() == JsonToken.NUMBER) { + // Number is the number of seconds since Epoch. + return Instant.ofEpochMilli(reader.nextLong() * 1000L); + } + + throw new UnsupportedOperationException("Unsupported format"); + } + + /** This method is not implemented. */ + @Override + public void write(JsonWriter out, Instant value) throws IOException { + throw new UnsupportedOperationException("Unimplemented method"); + } +} diff --git a/src/main/java/com/google/maps/internal/LatLngAdapter.java b/src/main/java/com/google/maps/internal/LatLngAdapter.java index e2ced1da7..10dd89081 100644 --- a/src/main/java/com/google/maps/internal/LatLngAdapter.java +++ b/src/main/java/com/google/maps/internal/LatLngAdapter.java @@ -20,25 +20,22 @@ import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import com.google.maps.model.LatLng; - import java.io.IOException; -/** - * Handle conversion from varying types of latitude and longitude representations. - */ +/** Handle conversion from varying types of latitude and longitude representations. */ public class LatLngAdapter extends TypeAdapter { - /** + /** * Reads in a JSON object and try to create a LatLng in one of the following formats. - * - *
    {
    -  *   "lat" : -33.8353684,
    -  *   "lng" : 140.8527069
    -  * }
    -  *
    -  * {
    -  *   "latitude": -33.865257570508334,
    -  *   "longitude": 151.19287000481452
    -  * }
    + * + *
    {
    +   *   "lat" : -33.8353684,
    +   *   "lng" : 140.8527069
    +   * }
    +   *
    +   * {
    +   *   "latitude": -33.865257570508334,
    +   *   "longitude": 151.19287000481452
    +   * }
    */ @Override public LatLng read(JsonReader reader) throws IOException { @@ -72,9 +69,7 @@ public LatLng read(JsonReader reader) throws IOException { } } - /** - * Not supported. - */ + /** Not supported. */ @Override public void write(JsonWriter out, LatLng value) throws IOException { throw new UnsupportedOperationException("Unimplemented method."); diff --git a/src/main/java/com/google/maps/internal/LocalTimeAdapter.java b/src/main/java/com/google/maps/internal/LocalTimeAdapter.java new file mode 100644 index 000000000..407d7e624 --- /dev/null +++ b/src/main/java/com/google/maps/internal/LocalTimeAdapter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** This class handles conversion from JSON to {@link LocalTime}. */ +public class LocalTimeAdapter extends TypeAdapter { + /** Read a time from the Places API and convert to a {@link LocalTime} */ + @Override + public LocalTime read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + + if (reader.peek() == JsonToken.STRING) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HHmm"); + return LocalTime.parse(reader.nextString(), dtf); + } + + throw new UnsupportedOperationException("Unsupported format"); + } + + /** This method is not implemented. */ + @Override + public void write(JsonWriter out, LocalTime value) throws IOException { + throw new UnsupportedOperationException("Unimplemented method"); + } +} diff --git a/src/main/java/com/google/maps/internal/OkHttpPendingResult.java b/src/main/java/com/google/maps/internal/OkHttpPendingResult.java index 9f50b809a..2f68ef54f 100644 --- a/src/main/java/com/google/maps/internal/OkHttpPendingResult.java +++ b/src/main/java/com/google/maps/internal/OkHttpPendingResult.java @@ -19,31 +19,43 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; +import com.google.maps.GeolocationApi; +import com.google.maps.ImageResult; import com.google.maps.PendingResult; import com.google.maps.errors.ApiException; -import com.google.maps.errors.OverQueryLimitException; -import com.google.maps.model.*; - -import com.squareup.okhttp.Call; -import com.squareup.okhttp.Callback; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; - -import org.joda.time.DateTime; - -import java.io.ByteArrayOutputStream; +import com.google.maps.metrics.RequestMetrics; +import com.google.maps.model.AddressComponentType; +import com.google.maps.model.AddressType; +import com.google.maps.model.Distance; +import com.google.maps.model.Duration; +import com.google.maps.model.Fare; +import com.google.maps.model.LatLng; +import com.google.maps.model.LocationType; +import com.google.maps.model.OpeningHours.Period.OpenClose.DayOfWeek; +import com.google.maps.model.PlaceDetails.Review.AspectRating.RatingType; +import com.google.maps.model.PriceLevel; +import com.google.maps.model.TravelMode; +import com.google.maps.model.VehicleType; import java.io.IOException; -import java.io.InputStream; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; -import java.util.logging.Logger; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * A PendingResult backed by a HTTP call executed by OkHttp, a deserialization step using Gson, - * rate limiting and a retry policy. + * A PendingResult backed by a HTTP call executed by OkHttp, a deserialization step using Gson, rate + * limiting and a retry policy. * *

    {@code T} is the type of the result of this pending result, and {@code R} is the type of the * request. @@ -54,31 +66,47 @@ public class OkHttpPendingResult> private final OkHttpClient client; private final Class responseClass; private final FieldNamingPolicy fieldNamingPolicy; + private final Integer maxRetries; + private final RequestMetrics metrics; private Call call; private Callback callback; private long errorTimeOut; private int retryCounter = 0; private long cumulativeSleepTime = 0; + private ExceptionsAllowedToRetry exceptionsAllowedToRetry; - private static final Logger LOG = Logger.getLogger(OkHttpPendingResult.class.getName()); - private static final List RETRY_ERROR_CODES = Arrays.asList(500, 503, 504); + private static final Logger LOG = LoggerFactory.getLogger(OkHttpPendingResult.class.getName()); + private static final List RETRY_ERROR_CODES = Arrays.asList(500, 503, 504); /** - * @param request HTTP request to execute. - * @param client The client used to execute the request. - * @param responseClass Model class to unmarshal JSON body content. + * @param request HTTP request to execute. + * @param client The client used to execute the request. + * @param responseClass Model class to unmarshal JSON body content. * @param fieldNamingPolicy FieldNamingPolicy for unmarshaling JSON. - * @param errorTimeOut Number of milliseconds to re-send erroring requests. + * @param errorTimeOut Number of milliseconds to re-send erroring requests. + * @param maxRetries Number of times allowed to re-send erroring requests. + * @param exceptionsAllowedToRetry The exceptions to retry. */ - public OkHttpPendingResult(Request request, OkHttpClient client, Class responseClass, - FieldNamingPolicy fieldNamingPolicy, long errorTimeOut) { + public OkHttpPendingResult( + Request request, + OkHttpClient client, + Class responseClass, + FieldNamingPolicy fieldNamingPolicy, + long errorTimeOut, + Integer maxRetries, + ExceptionsAllowedToRetry exceptionsAllowedToRetry, + RequestMetrics metrics) { this.request = request; this.client = client; this.responseClass = responseClass; this.fieldNamingPolicy = fieldNamingPolicy; this.errorTimeOut = errorTimeOut; + this.maxRetries = maxRetries; + this.exceptionsAllowedToRetry = exceptionsAllowedToRetry; + this.metrics = metrics; + metrics.startNetwork(); this.call = client.newCall(request); } @@ -88,20 +116,19 @@ public void setCallback(Callback callback) { call.enqueue(this); } - /** - * Preserve a request/response pair through an asynchronous callback. - */ + /** Preserve a request/response pair through an asynchronous callback. */ private class QueuedResponse { private final OkHttpPendingResult request; private final Response response; - private final Exception e; + private final IOException e; public QueuedResponse(OkHttpPendingResult request, Response response) { this.request = request; this.response = response; this.e = null; } - public QueuedResponse(OkHttpPendingResult request, Exception e) { + + public QueuedResponse(OkHttpPendingResult request, IOException e) { this.request = request; this.response = null; this.e = e; @@ -109,7 +136,7 @@ public QueuedResponse(OkHttpPendingResult request, Exception e) { } @Override - public T await() throws Exception { + public T await() throws ApiException, IOException, InterruptedException { // Handle sleeping for retried requests if (retryCounter > 0) { // 0.5 * (1.5 ^ i) represents an increased sleep time of 1.5x per iteration, @@ -120,8 +147,10 @@ public T await() throws Exception { // Generate a jitter value between -delaySecs / 2 and +delaySecs / 2 long delayMillis = (long) (delaySecs * (Math.random() + 0.5) * 1000); - LOG.config(String.format("Sleeping between errors for %dms (retry #%d, already slept %dms)", - delayMillis, retryCounter, cumulativeSleepTime)); + LOG.debug( + String.format( + "Sleeping between errors for %dms (retry #%d, already slept %dms)", + delayMillis, retryCounter, cumulativeSleepTime)); cumulativeSleepTime += delayMillis; try { Thread.sleep(delayMillis); @@ -130,27 +159,31 @@ public T await() throws Exception { } } - final BlockingQueue waiter = new ArrayBlockingQueue(1); + final BlockingQueue waiter = new ArrayBlockingQueue<>(1); final OkHttpPendingResult parent = this; // This callback will be called on another thread, handled by the RateLimitExecutorService. // Calling call.execute() directly would bypass the rate limiting. - call.enqueue(new com.squareup.okhttp.Callback() { - @Override - public void onFailure(Request request, IOException e) { - waiter.add(new QueuedResponse(parent, e)); - } - - @Override - public void onResponse(Response response) throws IOException { - waiter.add(new QueuedResponse(parent, response)); - } - }); + call.enqueue( + new okhttp3.Callback() { + @Override + public void onFailure(Call call, IOException e) { + metrics.endNetwork(); + waiter.add(new QueuedResponse(parent, e)); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + metrics.endNetwork(); + waiter.add(new QueuedResponse(parent, response)); + } + }); QueuedResponse r = waiter.take(); if (r.response != null) { return parseResponse(r.request, r.response); } else { + metrics.endRequest(r.e, 0, retryCounter); throw r.e; } } @@ -170,14 +203,17 @@ public void cancel() { } @Override - public void onFailure(Request request, IOException ioe) { + public void onFailure(Call call, IOException ioe) { + metrics.endNetwork(); if (callback != null) { + metrics.endRequest(ioe, 0, retryCounter); callback.onFailure(ioe); } } @Override - public void onResponse(Response response) throws IOException { + public void onResponse(Call call, Response response) throws IOException { + metrics.endNetwork(); if (callback != null) { try { callback.onResult(parseResponse(this, response)); @@ -187,29 +223,73 @@ public void onResponse(Response response) throws IOException { } } - private T parseResponse(OkHttpPendingResult request, Response response) throws Exception { - if (RETRY_ERROR_CODES.contains(response.code()) && cumulativeSleepTime < errorTimeOut) { - // Retry is a blocking method, but that's OK. If we're here, we're either in an await() - // call, which is blocking anyway, or we're handling a callback in a separate thread. - return request.retry(); + @SuppressWarnings("unchecked") + private T parseResponse(OkHttpPendingResult request, Response response) + throws ApiException, InterruptedException, IOException { + try { + T result = parseResponseInternal(request, response); + metrics.endRequest(null, response.code(), retryCounter); + return result; + } catch (Exception e) { + metrics.endRequest(e, response.code(), retryCounter); + throw e; } + } - Gson gson = new GsonBuilder() - .registerTypeAdapter(DateTime.class, new DateTimeAdapter()) - .registerTypeAdapter(Distance.class, new DistanceAdapter()) - .registerTypeAdapter(Duration.class, new DurationAdapter()) - .registerTypeAdapter(Fare.class, new FareAdapter()) - .registerTypeAdapter(LatLng.class, new LatLngAdapter()) - .registerTypeAdapter(AddressComponentType.class, - new SafeEnumAdapter(AddressComponentType.UNKNOWN)) - .registerTypeAdapter(AddressType.class, new SafeEnumAdapter(AddressType.UNKNOWN)) - .registerTypeAdapter(TravelMode.class, new SafeEnumAdapter(TravelMode.UNKNOWN)) - .registerTypeAdapter(LocationType.class, new SafeEnumAdapter(LocationType.UNKNOWN)) - .setFieldNamingPolicy(fieldNamingPolicy) - .create(); - - byte[] bytes = getBytes(response); + @SuppressWarnings("unchecked") + private T parseResponseInternal(OkHttpPendingResult request, Response response) + throws ApiException, InterruptedException, IOException { + if (shouldRetry(response)) { + // since we are retrying the request we must close the response + response.close(); + + // Retry is a blocking method, but that's OK. If we're here, we're either in an await() + // call, which is blocking anyway, or we're handling a callback in a separate thread. + return request.retry(); + } + + byte[] bytes; + try (ResponseBody body = response.body()) { + bytes = body.bytes(); + } R resp; + String contentType = response.header("Content-Type"); + + if (contentType != null + && contentType.startsWith("image") + && responseClass == ImageResult.Response.class + && response.code() == 200) { + ImageResult result = new ImageResult(contentType, bytes); + return (T) result; + } + + Gson gson = + new GsonBuilder() + .registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()) + .registerTypeAdapter(Distance.class, new DistanceAdapter()) + .registerTypeAdapter(Duration.class, new DurationAdapter()) + .registerTypeAdapter(Fare.class, new FareAdapter()) + .registerTypeAdapter(LatLng.class, new LatLngAdapter()) + .registerTypeAdapter( + AddressComponentType.class, + new SafeEnumAdapter(AddressComponentType.UNKNOWN)) + .registerTypeAdapter( + AddressType.class, new SafeEnumAdapter(AddressType.UNKNOWN)) + .registerTypeAdapter( + TravelMode.class, new SafeEnumAdapter(TravelMode.UNKNOWN)) + .registerTypeAdapter( + LocationType.class, new SafeEnumAdapter(LocationType.UNKNOWN)) + .registerTypeAdapter( + RatingType.class, new SafeEnumAdapter(RatingType.UNKNOWN)) + .registerTypeAdapter( + VehicleType.class, new SafeEnumAdapter(VehicleType.OTHER)) + .registerTypeAdapter(DayOfWeek.class, new DayOfWeekAdapter()) + .registerTypeAdapter(PriceLevel.class, new PriceLevelAdapter()) + .registerTypeAdapter(Instant.class, new InstantAdapter()) + .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter()) + .registerTypeAdapter(GeolocationApi.Response.class, new GeolocationResponseAdapter()) + .setFieldNamingPolicy(fieldNamingPolicy) + .create(); // Attempt to de-serialize before checking the HTTP status code, as there may be JSON in the // body that we can use to provide a more descriptive exception. @@ -221,8 +301,8 @@ private T parseResponse(OkHttpPendingResult request, Response response) th // Some of the APIs return 200 even when the API request fails, as long as the transport // mechanism succeeds. In these cases, INVALID_RESPONSE, etc are handled by the Gson // parsing. - throw new IOException(String.format("Server Error: %d %s", response.code(), - response.message())); + throw new IOException( + String.format("Server Error: %d %s", response.code(), response.message())); } // Otherwise just cough up the syntax exception. @@ -234,32 +314,31 @@ private T parseResponse(OkHttpPendingResult request, Response response) th return resp.getResult(); } else { ApiException e = resp.getError(); - if (e instanceof OverQueryLimitException && cumulativeSleepTime < errorTimeOut) { - // Retry over_query_limit errors + if (shouldRetry(e)) { return request.retry(); } else { - // Throw anything else, including OQLs if we've spent too much time retrying throw e; } } } - private byte[] getBytes(Response response) throws IOException { - InputStream in = response.body().byteStream(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - int bytesRead; - byte[] data = new byte[8192]; - while ((bytesRead = in.read(data, 0, data.length)) != -1) { - buffer.write(data, 0, bytesRead); - } - buffer.flush(); - return buffer.toByteArray(); - } - - private T retry() throws Exception { + private T retry() throws ApiException, InterruptedException, IOException { retryCounter++; LOG.info("Retrying request. Retry #" + retryCounter); + metrics.startNetwork(); this.call = client.newCall(request); return this.await(); } + + private boolean shouldRetry(Response response) { + return RETRY_ERROR_CODES.contains(response.code()) + && cumulativeSleepTime < errorTimeOut + && (maxRetries == null || retryCounter < maxRetries); + } + + private boolean shouldRetry(ApiException exception) { + return exceptionsAllowedToRetry.contains(exception.getClass()) + && cumulativeSleepTime < errorTimeOut + && (maxRetries == null || retryCounter < maxRetries); + } } diff --git a/src/main/java/com/google/maps/internal/PolylineEncoding.java b/src/main/java/com/google/maps/internal/PolylineEncoding.java index 9b6c7c2e9..ee2306625 100644 --- a/src/main/java/com/google/maps/internal/PolylineEncoding.java +++ b/src/main/java/com/google/maps/internal/PolylineEncoding.java @@ -16,7 +16,6 @@ package com.google.maps.internal; import com.google.maps.model.LatLng; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -24,18 +23,17 @@ /** * Utility class that encodes and decodes Polylines. * - *

    See {@url https://developers.google.com/maps/documentation/utilities/polylinealgorithm} for - * detailed description of this format. + *

    See + * https://developers.google.com/maps/documentation/utilities/polylinealgorithm for detailed + * description of this format. */ public class PolylineEncoding { - /** - * Decodes an encoded path string into a sequence of LatLngs. - */ + /** Decodes an encoded path string into a sequence of LatLngs. */ public static List decode(final String encodedPath) { int len = encodedPath.length(); - final List path = new ArrayList(len / 2); + final List path = new ArrayList<>(len / 2); int index = 0; int lat = 0; int lng = 0; @@ -66,14 +64,12 @@ public static List decode(final String encodedPath) { return path; } - /** - * Encodes a sequence of LatLngs into an encoded path string. - */ + /** Encodes a sequence of LatLngs into an encoded path string. */ public static String encode(final List path) { long lastLat = 0; long lastLng = 0; - final StringBuffer result = new StringBuffer(); + final StringBuilder result = new StringBuilder(); for (final LatLng point : path) { long lat = Math.round(point.lat * 1e5); @@ -91,7 +87,7 @@ public static String encode(final List path) { return result.toString(); } - private static void encode(long v, StringBuffer result) { + private static void encode(long v, StringBuilder result) { v = v < 0 ? ~(v << 1) : v << 1; while (v >= 0x20) { result.append(Character.toChars((int) ((0x20 | (v & 0x1f)) + 63))); @@ -100,9 +96,7 @@ private static void encode(long v, StringBuffer result) { result.append(Character.toChars((int) (v + 63))); } - /** - * Encodes an array of LatLngs into an encoded path string. - */ + /** Encodes an array of LatLngs into an encoded path string. */ public static String encode(LatLng[] path) { return encode(Arrays.asList(path)); } diff --git a/src/main/java/com/google/maps/internal/PriceLevelAdapter.java b/src/main/java/com/google/maps/internal/PriceLevelAdapter.java new file mode 100644 index 000000000..377d2e4eb --- /dev/null +++ b/src/main/java/com/google/maps/internal/PriceLevelAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.google.maps.model.PriceLevel; +import java.io.IOException; + +/** + * This class handles conversion from JSON to {@link PriceLevel}. + * + *

    Please see GSON + * Type Adapter for more detail. + */ +public class PriceLevelAdapter extends TypeAdapter { + + @Override + public PriceLevel read(JsonReader reader) throws IOException { + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + return null; + } + + if (reader.peek() == JsonToken.NUMBER) { + int priceLevel = reader.nextInt(); + + switch (priceLevel) { + case 0: + return PriceLevel.FREE; + case 1: + return PriceLevel.INEXPENSIVE; + case 2: + return PriceLevel.MODERATE; + case 3: + return PriceLevel.EXPENSIVE; + case 4: + return PriceLevel.VERY_EXPENSIVE; + } + } + + return PriceLevel.UNKNOWN; + } + + /** This method is not implemented. */ + @Override + public void write(JsonWriter writer, PriceLevel value) throws IOException { + throw new UnsupportedOperationException("Unimplemented method"); + } +} diff --git a/src/main/java/com/google/maps/internal/RateLimitExecutorService.java b/src/main/java/com/google/maps/internal/RateLimitExecutorService.java index 6d76f6993..1c94b0b33 100644 --- a/src/main/java/com/google/maps/internal/RateLimitExecutorService.java +++ b/src/main/java/com/google/maps/internal/RateLimitExecutorService.java @@ -15,8 +15,8 @@ package com.google.maps.internal; +import com.google.maps.internal.ratelimiter.RateLimiter; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; @@ -24,101 +24,71 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.logging.Level; -import java.util.logging.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -/** - * Rate Limit Policy for Google Maps Web Services APIs. - */ +/** Rate Limit Policy for Google Maps Web Services APIs. */ public class RateLimitExecutorService implements ExecutorService, Runnable { - private static final Logger LOG = Logger.getLogger(RateLimitExecutorService.class.getName()); - private static final int DEFAULT_QUERIES_PER_SECOND = 10; - private static final int SECOND = 1000; - private static final int HALF_SECOND = SECOND / 2; + private static final Logger LOG = + LoggerFactory.getLogger(RateLimitExecutorService.class.getName()); + private static final int DEFAULT_QUERIES_PER_SECOND = 50; // It's important we set Ok's second arg to threadFactory(.., true) to ensure the threads are // killed when the app exits. For synchronous requests this is ideal but it means any async // requests still pending after termination will be killed. - private final ExecutorService delegate = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, - TimeUnit.SECONDS, new LinkedBlockingQueue(), - threadFactory("Rate Limited Dispatcher", true)); + private final ExecutorService delegate = + new ThreadPoolExecutor( + Runtime.getRuntime().availableProcessors(), + Integer.MAX_VALUE, + 60, + TimeUnit.SECONDS, + new SynchronousQueue(), + threadFactory("Rate Limited Dispatcher", true)); - private final BlockingQueue queue = new LinkedBlockingQueue(); + private final BlockingQueue queue = new LinkedBlockingQueue<>(); + private final RateLimiter rateLimiter = + RateLimiter.create(DEFAULT_QUERIES_PER_SECOND, 1, TimeUnit.SECONDS); - private volatile int queriesPerSecond; - private volatile int minimumDelay; - private LinkedList sentTimes = new LinkedList(); - private long lastSentTime = 0; + final Thread delayThread; public RateLimitExecutorService() { setQueriesPerSecond(DEFAULT_QUERIES_PER_SECOND); - Thread delayThread = new Thread(this); + delayThread = new Thread(this); delayThread.setDaemon(true); + delayThread.setName("RateLimitExecutorDelayThread"); delayThread.start(); } public void setQueriesPerSecond(int maxQps) { - this.queriesPerSecond = maxQps; - this.minimumDelay = HALF_SECOND / queriesPerSecond; + this.rateLimiter.setRate(maxQps); } - public void setQueriesPerSecond(int maxQps, int minimumInterval) { - this.queriesPerSecond = maxQps; - this.minimumDelay = minimumInterval; - - LOG.log(Level.INFO, "Configuring rate limit at QPS: " + maxQps + ", minimum delay " - + minimumInterval + "ms between requests"); - } - - /** - * Main loop. - */ + /** Main loop. */ @Override public void run() { try { while (!delegate.isShutdown()) { - long now = System.currentTimeMillis(); - long oneSecondAgo = now - SECOND; - + this.rateLimiter.acquire(); Runnable r = queue.take(); - - long requiredSeparationDelay = lastSentTime + minimumDelay - now; - if (requiredSeparationDelay > 0) { - Thread.sleep(requiredSeparationDelay); - } - - // Purge any sent times older than a second - while (sentTimes.size() > 0 && sentTimes.peekFirst() < oneSecondAgo) { - sentTimes.pop(); - } - - long delay = 0; - if (sentTimes.size() > 0) { - delay = sentTimes.peekFirst() + SECOND - System.currentTimeMillis(); - } - - if (sentTimes.size() < queriesPerSecond || delay <= 0) { + if (!delegate.isShutdown()) { delegate.execute(r); - lastSentTime = now; - sentTimes.add(lastSentTime); - } else { - queue.add(r); - Thread.sleep(delay); } } } catch (InterruptedException ie) { - LOG.log(Level.INFO, "Interrupted", ie); + LOG.info("Interrupted", ie); } } private static ThreadFactory threadFactory(final String name, final boolean daemon) { return new ThreadFactory() { - @Override public Thread newThread(Runnable runnable) { + @Override + public Thread newThread(Runnable runnable) { Thread result = new Thread(runnable, name); result.setDaemon(daemon); return result; @@ -131,16 +101,33 @@ public void execute(Runnable runnable) { queue.add(runnable); } - // Everything below here is straight delegation. - @Override public void shutdown() { delegate.shutdown(); + // we need this to break out of queue.take() + execute( + new Runnable() { + @Override + public void run() { + // do nothing + } + }); } + // Everything below here is straight delegation. + @Override public List shutdownNow() { - return delegate.shutdownNow(); + List tasks = delegate.shutdownNow(); + // we need this to break out of queue.take() + execute( + new Runnable() { + @Override + public void run() { + // do nothing + } + }); + return tasks; } @Override @@ -180,14 +167,15 @@ public List> invokeAll(Collection> callables } @Override - public List> invokeAll(Collection> callables, long l, - TimeUnit timeUnit) throws InterruptedException { + public List> invokeAll( + Collection> callables, long l, TimeUnit timeUnit) + throws InterruptedException { return delegate.invokeAll(callables, l, timeUnit); } @Override - public T invokeAny(Collection> callables) throws InterruptedException, - ExecutionException { + public T invokeAny(Collection> callables) + throws InterruptedException, ExecutionException { return delegate.invokeAny(callables); } @@ -196,5 +184,4 @@ public T invokeAny(Collection> callables, long l, Time throws InterruptedException, ExecutionException, TimeoutException { return delegate.invokeAny(callables, l, timeUnit); } - } diff --git a/src/main/java/com/google/maps/internal/SafeEnumAdapter.java b/src/main/java/com/google/maps/internal/SafeEnumAdapter.java index c4ce257f5..19ab6848c 100644 --- a/src/main/java/com/google/maps/internal/SafeEnumAdapter.java +++ b/src/main/java/com/google/maps/internal/SafeEnumAdapter.java @@ -19,27 +19,26 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; - import java.io.IOException; import java.util.Locale; -import java.util.logging.Logger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A {@link com.google.gson.TypeAdapter} that maps case-insensitive values to an enum type. If the * value is not found, an UNKNOWN value is returned, and logged. This allows the server to return * values this client doesn't yet know about. + * * @param the enum type to map values to. */ public class SafeEnumAdapter> extends TypeAdapter { - private static final Logger LOG = Logger.getLogger(SafeEnumAdapter.class.getName()); + private static final Logger LOG = LoggerFactory.getLogger(SafeEnumAdapter.class.getName()); private final Class clazz; private final E unknownValue; - /** - * @param unknownValue the value to return if the value cannot be found. - */ + /** @param unknownValue the value to return if the value cannot be found. */ public SafeEnumAdapter(E unknownValue) { if (unknownValue == null) throw new IllegalArgumentException(); @@ -62,7 +61,7 @@ public E read(JsonReader reader) throws IOException { try { return Enum.valueOf(clazz, value.toUpperCase(Locale.ENGLISH)); } catch (IllegalArgumentException iae) { - LOG.warning(String.format("Unknown type for enum %s: '%s'", clazz.getName(), value)); + LOG.warn("Unknown type for enum {}: '{}'", clazz.getName(), value); return unknownValue; } } diff --git a/src/main/java/com/google/maps/internal/StringJoin.java b/src/main/java/com/google/maps/internal/StringJoin.java index dd97fb71d..9af4e0b10 100644 --- a/src/main/java/com/google/maps/internal/StringJoin.java +++ b/src/main/java/com/google/maps/internal/StringJoin.java @@ -15,26 +15,27 @@ package com.google.maps.internal; -/** - * Utility class to join strings. - */ +import java.util.Objects; + +/** Utility class to join strings. */ public class StringJoin { /** - * Marker Interface to enable the URL Value enums in {@link com.google.maps.DirectionsApi} to - * be string joinable. + * Marker Interface to enable the URL Value enums in {@link com.google.maps.DirectionsApi} to be + * string joinable. */ public interface UrlValue { - /** - * @return the object, represented as a URL value (not URL encoded). - */ - String toUrlValue(); + /** @return the object, represented as a URL value (not URL encoded). */ + String toUrlValue(); } - private StringJoin() { - } + private StringJoin() {} public static String join(char delim, String... parts) { + return join(new String(new char[] {delim}), parts); + } + + public static String join(CharSequence delim, String... parts) { StringBuilder result = new StringBuilder(); for (int i = 0; i < parts.length; i++) { if (i != 0) { @@ -45,11 +46,26 @@ public static String join(char delim, String... parts) { return result.toString(); } + public static String join(char delim, Object... parts) { + return join(new String(new char[] {delim}), parts); + } + + public static String join(CharSequence delim, Object... parts) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < parts.length; i++) { + if (i != 0) { + result.append(delim); + } + result.append(Objects.toString(parts[i])); + } + return result.toString(); + } + public static String join(char delim, UrlValue... parts) { String[] strings = new String[parts.length]; int i = 0; for (UrlValue part : parts) { - strings[i++] = part.toString(); + strings[i++] = part.toUrlValue(); } return join(delim, strings); diff --git a/src/main/java/com/google/maps/internal/UrlSigner.java b/src/main/java/com/google/maps/internal/UrlSigner.java index cf8eea6df..5d305d430 100644 --- a/src/main/java/com/google/maps/internal/UrlSigner.java +++ b/src/main/java/com/google/maps/internal/UrlSigner.java @@ -15,24 +15,26 @@ package com.google.maps.internal; -import okio.ByteString; +import static java.nio.charset.StandardCharsets.UTF_8; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; - import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import okio.ByteString; /** * Utility class for supporting Maps for Work Digital signatures. * - *

    See {@url https://developers.google.com/maps/documentation/business/webservices/auth#digital_signatures} - * for more detail on this protocol. + *

    See Using a + * client ID for more detail on this protocol. */ public class UrlSigner { - private final SecretKeySpec key; + private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1"; + private final Mac mac; - public UrlSigner(final String keyString) { + public UrlSigner(final String keyString) throws NoSuchAlgorithmException, InvalidKeyException { // Convert from URL-safe base64 to regular base64. String base64 = keyString.replace('-', '+').replace('_', '/'); @@ -41,18 +43,23 @@ public UrlSigner(final String keyString) { // NOTE: don't log the exception, in case some of the private key leaks to an end-user. throw new IllegalArgumentException("Private key is invalid."); } - this.key = new SecretKeySpec(decodedKey.toByteArray(), "HmacSHA1"); + + mac = Mac.getInstance(ALGORITHM_HMAC_SHA1); + mac.init(new SecretKeySpec(decodedKey.toByteArray(), ALGORITHM_HMAC_SHA1)); } - /** - * Generate url safe HmacSHA1 of a path. - */ - public String getSignature(String path) - throws NoSuchAlgorithmException, InvalidKeyException { - // TODO(macd): add test - Mac mac = Mac.getInstance("HmacSHA1"); - mac.init(key); - byte[] digest = mac.doFinal(path.getBytes()); + /** Generate url safe HmacSHA1 of a path. */ + public String getSignature(String path) { + byte[] digest = getMac().doFinal(path.getBytes(UTF_8)); return ByteString.of(digest).base64().replace('+', '-').replace('/', '_'); } + + private Mac getMac() { + // Mac is not thread-safe. Requires a new clone for each signature. + try { + return (Mac) mac.clone(); + } catch (CloneNotSupportedException e) { + throw new IllegalStateException(e); + } + } } diff --git a/src/main/java/com/google/maps/internal/DateTimeAdapter.java b/src/main/java/com/google/maps/internal/ZonedDateTimeAdapter.java similarity index 70% rename from src/main/java/com/google/maps/internal/DateTimeAdapter.java rename to src/main/java/com/google/maps/internal/ZonedDateTimeAdapter.java index c09643919..e1685d57d 100644 --- a/src/main/java/com/google/maps/internal/DateTimeAdapter.java +++ b/src/main/java/com/google/maps/internal/ZonedDateTimeAdapter.java @@ -19,25 +19,25 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; - -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; - import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; /** - * This class handles conversion from JSON to {@link DateTime}s. + * This class handles conversion from JSON to {@link ZonedDateTime}s. * - *

    Please see - * {@url https://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/TypeAdapter.html} + *

    Please see TypeAdapter * for more detail. */ -public class DateTimeAdapter extends TypeAdapter { +public class ZonedDateTimeAdapter extends TypeAdapter { /** - * Read a Time object from a Directions API result and convert it to a {@link DateTime}. + * Read a Time object from a Directions API result and convert it to a {@link ZonedDateTime}. * *

    We are expecting to receive something akin to the following: + * *

        * {
        *   "text" : "4:27pm",
    @@ -47,7 +47,7 @@ public class DateTimeAdapter extends TypeAdapter {
        * 
    */ @Override - public DateTime read(JsonReader reader) throws IOException { + public ZonedDateTime read(JsonReader reader) throws IOException { if (reader.peek() == JsonToken.NULL) { reader.nextNull(); return null; @@ -60,27 +60,23 @@ public DateTime read(JsonReader reader) throws IOException { while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("text")) { - // Ignore the human readable rendering. + // Ignore the human-readable rendering. reader.nextString(); } else if (name.equals("time_zone")) { timeZoneId = reader.nextString(); } else if (name.equals("value")) { secondsSinceEpoch = reader.nextLong(); } - } reader.endObject(); - return new DateTime(secondsSinceEpoch * 1000, DateTimeZone.forID(timeZoneId)); + return ZonedDateTime.ofInstant( + Instant.ofEpochMilli(secondsSinceEpoch * 1000), ZoneId.of(timeZoneId)); } - /** - * This method is not implemented. - */ + /** This method is not implemented. */ @Override - public void write(JsonWriter writer, DateTime value) throws IOException { + public void write(JsonWriter writer, ZonedDateTime value) throws IOException { throw new UnsupportedOperationException("Unimplemented method"); } - } - diff --git a/src/main/java/com/google/maps/internal/ratelimiter/LongMath.java b/src/main/java/com/google/maps/internal/ratelimiter/LongMath.java new file mode 100644 index 000000000..797806f68 --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/LongMath.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +/** + * A class for arithmetic on values of type {@code long}. + * + *

    This is a minimal port of Google Guava's com.google.common.math.LongMath, just sufficient to + * implement the ratelimiter classes. + */ +public final class LongMath { + private LongMath() {} + + /** + * Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case + * {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively. + */ + /* Suppress warnings instead of "fixing" because this is code imported from Guava. */ + @SuppressWarnings("ShortCircuitBoolean") + public static long saturatedAdd(long a, long b) { + long naiveSum = a + b; + if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) { + // If a and b have different signs or a has the same sign as the result then there was no + // overflow, return. + return naiveSum; + } + // we did over/under flow, if the sign is negative we should return MAX otherwise MIN + return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1); + } +} diff --git a/src/main/java/com/google/maps/internal/ratelimiter/Platform.java b/src/main/java/com/google/maps/internal/ratelimiter/Platform.java new file mode 100644 index 000000000..45ccd3487 --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/Platform.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2009 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +import java.util.Locale; + +/** + * Methods factored out so that they can be emulated differently in GWT. + * + *

    This is a minimal port of Google Guava's com.google.common.base.Platform, sufficient to + * implement the ratelimiter classes. + * + * @author Jesse Wilson + */ +final class Platform { + private Platform() {} + + /** Calls {@link System#nanoTime()}. */ + static long systemNanoTime() { + return System.nanoTime(); + } + + static String formatCompact4Digits(double value) { + return String.format(Locale.ROOT, "%.4g", value); + } +} diff --git a/src/main/java/com/google/maps/internal/ratelimiter/Preconditions.java b/src/main/java/com/google/maps/internal/ratelimiter/Preconditions.java new file mode 100644 index 000000000..24151222d --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/Preconditions.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +/** + * Static convenience methods that help a method or constructor check whether it was invoked + * correctly (that is, whether its preconditions were met). + * + *

    This is a minimal port of Google Guava's com.google.common.base.Preconditions necessary to + * implement the ratelimiter classes. + */ +public final class Preconditions { + private Preconditions() {} + + /** + * Ensures the truth of an expression involving one or more parameters to the calling method. + * + * @param expression a boolean expression + * @param errorMessageTemplate a template for the exception message should the check fail. The + * message is formed by replacing each {@code %s} placeholder in the template with an + * argument. These are matched by position - the first {@code %s} gets {@code + * errorMessageArgs[0]}, etc. Unmatched arguments will be appended to the formatted message in + * square braces. Unmatched placeholders will be left as-is. + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. + * @throws IllegalArgumentException if {@code expression} is false + */ + public static void checkArgument( + boolean expression, String errorMessageTemplate, Object... errorMessageArgs) { + if (!expression) { + throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs)); + } + } + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null. + * + * @param reference an object reference + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + /** + * Ensures that an object reference passed as a parameter to the calling method is not null. + * + * @param reference an object reference + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static T checkNotNull(T reference, Object errorMessage) { + if (reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + return reference; + } + + /** + * Ensures the truth of an expression involving the state of the calling instance, but not + * involving any parameters to the calling method. + * + * @param expression a boolean expression + * @param errorMessage the exception message to use if the check fails; will be converted to a + * string using {@link String#valueOf(Object)} + * @throws IllegalStateException if {@code expression} is false + */ + public static void checkState(boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalStateException(String.valueOf(errorMessage)); + } + } + + /** + * Substitutes each {@code %s} in {@code template} with an argument. These are matched by + * position: the first {@code %s} gets {@code args[0]}, etc. If there are more arguments than + * placeholders, the unmatched arguments will be appended to the end of the formatted message in + * square braces. + * + * @param template a string containing 0 or more {@code %s} placeholders. null is treated as + * "null". + * @param args the arguments to be substituted into the message template. Arguments are converted + * to strings using {@link String#valueOf(Object)}. Arguments can be null. + */ + static String format(String template, Object... args) { + template = String.valueOf(template); // null -> "null" + + args = args == null ? new Object[] {"(Object[])null"} : args; + + // start substituting the arguments into the '%s' placeholders + StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); + int templateStart = 0; + int i = 0; + while (i < args.length) { + int placeholderStart = template.indexOf("%s", templateStart); + if (placeholderStart == -1) { + break; + } + builder.append(template, templateStart, placeholderStart); + builder.append(args[i++]); + templateStart = placeholderStart + 2; + } + builder.append(template, templateStart, template.length()); + + // if we run out of placeholders, append the extra args in square braces + if (i < args.length) { + builder.append(" ["); + builder.append(args[i++]); + while (i < args.length) { + builder.append(", "); + builder.append(args[i++]); + } + builder.append(']'); + } + + return builder.toString(); + } +} diff --git a/src/main/java/com/google/maps/internal/ratelimiter/RateLimiter.java b/src/main/java/com/google/maps/internal/ratelimiter/RateLimiter.java new file mode 100644 index 000000000..7db4840f8 --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/RateLimiter.java @@ -0,0 +1,450 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +import static com.google.maps.internal.ratelimiter.Preconditions.checkArgument; +import static com.google.maps.internal.ratelimiter.Preconditions.checkNotNull; +import static java.lang.Math.max; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.maps.internal.ratelimiter.SmoothRateLimiter.SmoothBursty; +import com.google.maps.internal.ratelimiter.SmoothRateLimiter.SmoothWarmingUp; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * A rate limiter. Conceptually, a rate limiter distributes permits at a configurable rate. Each + * {@link #acquire()} blocks if necessary until a permit is available, and then takes it. Once + * acquired, permits need not be released. + * + *

    Rate limiters are often used to restrict the rate at which some physical or logical resource + * is accessed. This is in contrast to {@link java.util.concurrent.Semaphore} which restricts the + * number of concurrent accesses instead of the rate (note though that concurrency and rate are + * closely related, e.g. see Little's + * Law). + * + *

    A {@code RateLimiter} is defined primarily by the rate at which permits are issued. Absent + * additional configuration, permits will be distributed at a fixed rate, defined in terms of + * permits per second. Permits will be distributed smoothly, with the delay between individual + * permits being adjusted to ensure that the configured rate is maintained. + * + *

    It is possible to configure a {@code RateLimiter} to have a warmup period during which time + * the permits issued each second steadily increases until it hits the stable rate. + * + *

    As an example, imagine that we have a list of tasks to execute, but we don't want to submit + * more than 2 per second: + * + *

    {@code
    + * final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is "2 permits per second"
    + * void submitTasks(List tasks, Executor executor) {
    + *   for (Runnable task : tasks) {
    + *     rateLimiter.acquire(); // may wait
    + *     executor.execute(task);
    + *   }
    + * }
    + * }
    + * + *

    As another example, imagine that we produce a stream of data, and we want to cap it at 5kb per + * second. This could be accomplished by requiring a permit per byte, and specifying a rate of 5000 + * permits per second: + * + *

    {@code
    + * final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second
    + * void submitPacket(byte[] packet) {
    + *   rateLimiter.acquire(packet.length);
    + *   networkService.send(packet);
    + * }
    + * }
    + * + *

    It is important to note that the number of permits requested never affects the + * throttling of the request itself (an invocation to {@code acquire(1)} and an invocation to {@code + * acquire(1000)} will result in exactly the same throttling, if any), but it affects the throttling + * of the next request. I.e., if an expensive task arrives at an idle RateLimiter, it will be + * granted immediately, but it is the next request that will experience extra throttling, + * thus paying for the cost of the expensive task. + * + *

    Note: {@code RateLimiter} does not provide fairness guarantees. + * + * @author Dimitris Andreou + * @since 13.0 + */ +public abstract class RateLimiter { + /** + * Creates a {@code RateLimiter} with the specified stable throughput, given as "permits per + * second" (commonly referred to as QPS, queries per second). + * + *

    The returned {@code RateLimiter} ensures that on average no more than {@code + * permitsPerSecond} are issued during any given second, with sustained requests being smoothly + * spread over each second. When the incoming request rate exceeds {@code permitsPerSecond} the + * rate limiter will release one permit every {@code (1.0 / permitsPerSecond)} seconds. When the + * rate limiter is unused, bursts of up to {@code permitsPerSecond} permits will be allowed, with + * subsequent requests being smoothly limited at the stable rate of {@code permitsPerSecond}. + * + * @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in how many + * permits become available per second + * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero + */ + public static RateLimiter create(double permitsPerSecond) { + /* + * The default RateLimiter configuration can save the unused permits of up to one second. This + * is to avoid unnecessary stalls in situations like this: A RateLimiter of 1qps, and 4 threads, + * all calling acquire() at these moments: + * + * T0 at 0 seconds + * T1 at 1.05 seconds + * T2 at 2 seconds + * T3 at 3 seconds + * + * Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, and T3 would also + * have to sleep till 3.05 seconds. + */ + return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer()); + } + + static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) { + RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); + rateLimiter.setRate(permitsPerSecond); + return rateLimiter; + } + + /** + * Creates a {@code RateLimiter} with the specified stable throughput, given as "permits per + * second" (commonly referred to as QPS, queries per second), and a warmup period, + * during which the {@code RateLimiter} smoothly ramps up its rate, until it reaches its maximum + * rate at the end of the period (as long as there are enough requests to saturate it). Similarly, + * if the {@code RateLimiter} is left unused for a duration of {@code warmupPeriod}, it + * will gradually return to its "cold" state, i.e. it will go through the same warming up process + * as when it was first created. + * + *

    The returned {@code RateLimiter} is intended for cases where the resource that actually + * fulfills the requests (e.g., a remote server) needs "warmup" time, rather than being + * immediately accessed at the stable (maximum) rate. + * + *

    The returned {@code RateLimiter} starts in a "cold" state (i.e. the warmup period will + * follow), and if it is left unused for long enough, it will return to that state. + * + * @param permitsPerSecond the rate of the returned {@code RateLimiter}, measured in how many + * permits become available per second + * @param warmupPeriod the duration of the period where the {@code RateLimiter} ramps up its rate, + * before reaching its stable (maximum) rate + * @param unit the time unit of the warmupPeriod argument + * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero or {@code + * warmupPeriod} is negative + */ + public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) { + checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod); + return create( + permitsPerSecond, warmupPeriod, unit, 3.0, SleepingStopwatch.createFromSystemTimer()); + } + + static RateLimiter create( + double permitsPerSecond, + long warmupPeriod, + TimeUnit unit, + double coldFactor, + SleepingStopwatch stopwatch) { + RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor); + rateLimiter.setRate(permitsPerSecond); + return rateLimiter; + } + + /** + * The underlying timer; used both to measure elapsed time and sleep as necessary. A separate + * object to facilitate testing. + */ + private final SleepingStopwatch stopwatch; + + // Can't be initialized in the constructor because mocks don't call the constructor. + private volatile Object mutexDoNotUseDirectly; + + private Object mutex() { + Object mutex = mutexDoNotUseDirectly; + if (mutex == null) { + synchronized (this) { + mutex = mutexDoNotUseDirectly; + if (mutex == null) { + mutexDoNotUseDirectly = mutex = new Object(); + } + } + } + return mutex; + } + + RateLimiter(SleepingStopwatch stopwatch) { + this.stopwatch = checkNotNull(stopwatch); + } + + /** + * Updates the stable rate of this {@code RateLimiter}, that is, the {@code permitsPerSecond} + * argument provided in the factory method that constructed the {@code RateLimiter}. Currently + * throttled threads will not be awakened as a result of this invocation, thus they do not + * observe the new rate; only subsequent requests will. + * + *

    Note though that, since each request repays (by waiting, if necessary) the cost of the + * previous request, this means that the very next request after an invocation to {@code + * setRate} will not be affected by the new rate; it will pay the cost of the previous request, + * which is in terms of the previous rate. + * + *

    The behavior of the {@code RateLimiter} is not modified in any other way, e.g. if the {@code + * RateLimiter} was configured with a warmup period of 20 seconds, it still has a warmup period of + * 20 seconds after this method invocation. + * + * @param permitsPerSecond the new stable rate of this {@code RateLimiter} + * @throws IllegalArgumentException if {@code permitsPerSecond} is negative or zero + */ + public final void setRate(double permitsPerSecond) { + checkArgument( + permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive"); + synchronized (mutex()) { + doSetRate(permitsPerSecond, stopwatch.readMicros()); + } + } + + abstract void doSetRate(double permitsPerSecond, long nowMicros); + + /** + * Returns the stable rate (as {@code permits per seconds}) with which this {@code RateLimiter} is + * configured with. The initial value of this is the same as the {@code permitsPerSecond} argument + * passed in the factory method that produced this {@code RateLimiter}, and it is only updated + * after invocations to {@linkplain #setRate}. + */ + public final double getRate() { + synchronized (mutex()) { + return doGetRate(); + } + } + + abstract double doGetRate(); + + /** + * Acquires a single permit from this {@code RateLimiter}, blocking until the request can be + * granted. Tells the amount of time slept, if any. + * + *

    This method is equivalent to {@code acquire(1)}. + * + * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited + * @since 16.0 (present in 13.0 with {@code void} return type}) + */ + public double acquire() { + return acquire(1); + } + + /** + * Acquires the given number of permits from this {@code RateLimiter}, blocking until the request + * can be granted. Tells the amount of time slept, if any. + * + * @param permits the number of permits to acquire + * @return time spent sleeping to enforce rate, in seconds; 0.0 if not rate-limited + * @throws IllegalArgumentException if the requested number of permits is negative or zero + * @since 16.0 (present in 13.0 with {@code void} return type}) + */ + public double acquire(int permits) { + long microsToWait = reserve(permits); + stopwatch.sleepMicrosUninterruptibly(microsToWait); + return 1.0 * microsToWait / SECONDS.toMicros(1L); + } + + /** + * Reserves the given number of permits from this {@code RateLimiter} for future use, returning + * the number of microseconds until the reservation can be consumed. + * + * @return time in microseconds to wait until the resource can be acquired, never negative + */ + final long reserve(int permits) { + checkPermits(permits); + synchronized (mutex()) { + return reserveAndGetWaitLength(permits, stopwatch.readMicros()); + } + } + + /** + * Acquires a permit from this {@code RateLimiter} if it can be obtained without exceeding the + * specified {@code timeout}, or returns {@code false} immediately (without waiting) if the permit + * would not have been granted before the timeout expired. + * + *

    This method is equivalent to {@code tryAcquire(1, timeout, unit)}. + * + * @param timeout the maximum time to wait for the permit. Negative values are treated as zero. + * @param unit the time unit of the timeout argument + * @return {@code true} if the permit was acquired, {@code false} otherwise + * @throws IllegalArgumentException if the requested number of permits is negative or zero + */ + public boolean tryAcquire(long timeout, TimeUnit unit) { + return tryAcquire(1, timeout, unit); + } + + /** + * Acquires permits from this {@link RateLimiter} if it can be acquired immediately without delay. + * + *

    This method is equivalent to {@code tryAcquire(permits, 0, anyUnit)}. + * + * @param permits the number of permits to acquire + * @return {@code true} if the permits were acquired, {@code false} otherwise + * @throws IllegalArgumentException if the requested number of permits is negative or zero + * @since 14.0 + */ + public boolean tryAcquire(int permits) { + return tryAcquire(permits, 0, MICROSECONDS); + } + + /** + * Acquires a permit from this {@link RateLimiter} if it can be acquired immediately without + * delay. + * + *

    This method is equivalent to {@code tryAcquire(1)}. + * + * @return {@code true} if the permit was acquired, {@code false} otherwise + * @since 14.0 + */ + public boolean tryAcquire() { + return tryAcquire(1, 0, MICROSECONDS); + } + + /** + * Acquires the given number of permits from this {@code RateLimiter} if it can be obtained + * without exceeding the specified {@code timeout}, or returns {@code false} immediately (without + * waiting) if the permits would not have been granted before the timeout expired. + * + * @param permits the number of permits to acquire + * @param timeout the maximum time to wait for the permits. Negative values are treated as zero. + * @param unit the time unit of the timeout argument + * @return {@code true} if the permits were acquired, {@code false} otherwise + * @throws IllegalArgumentException if the requested number of permits is negative or zero + */ + public boolean tryAcquire(int permits, long timeout, TimeUnit unit) { + long timeoutMicros = max(unit.toMicros(timeout), 0); + checkPermits(permits); + long microsToWait; + synchronized (mutex()) { + long nowMicros = stopwatch.readMicros(); + if (!canAcquire(nowMicros, timeoutMicros)) { + return false; + } else { + microsToWait = reserveAndGetWaitLength(permits, nowMicros); + } + } + stopwatch.sleepMicrosUninterruptibly(microsToWait); + return true; + } + + private boolean canAcquire(long nowMicros, long timeoutMicros) { + return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros; + } + + /** + * Reserves next ticket and returns the wait time that the caller must wait for. + * + * @return the required wait time, never negative + */ + final long reserveAndGetWaitLength(int permits, long nowMicros) { + long momentAvailable = reserveEarliestAvailable(permits, nowMicros); + return max(momentAvailable - nowMicros, 0); + } + + /** + * Returns the earliest time that permits are available (with one caveat). + * + * @return the time that permits are available, or, if permits are available immediately, an + * arbitrary past or present time + */ + abstract long queryEarliestAvailable(long nowMicros); + + /** + * Reserves the requested number of permits and returns the time that those permits can be used + * (with one caveat). + * + * @return the time that the permits may be used, or, if the permits may be used immediately, an + * arbitrary past or present time + */ + abstract long reserveEarliestAvailable(int permits, long nowMicros); + + @Override + public String toString() { + return String.format(Locale.ROOT, "RateLimiter[stableRate=%3.1fqps]", getRate()); + } + + abstract static class SleepingStopwatch { + /** Constructor for use by subclasses. */ + protected SleepingStopwatch() {} + + /* + * We always hold the mutex when calling this. + */ + protected abstract long readMicros(); + + protected abstract void sleepMicrosUninterruptibly(long micros); + + public static SleepingStopwatch createFromSystemTimer() { + return new SleepingStopwatch() { + final Stopwatch stopwatch = Stopwatch.createStarted(); + + @Override + protected long readMicros() { + return stopwatch.elapsed(MICROSECONDS); + } + + @Override + protected void sleepMicrosUninterruptibly(long micros) { + if (micros > 0) { + sleepUninterruptibly(micros, MICROSECONDS); + } + } + }; + } + } + + private static void checkPermits(int permits) { + checkArgument(permits > 0, "Requested permits (%s) must be positive", permits); + } + + /** Invokes {@code unit.}{@link TimeUnit#sleep(long) sleep(sleepFor)} uninterruptibly. */ + private static void sleepUninterruptibly(long sleepFor, TimeUnit unit) { + boolean interrupted = false; + try { + long remainingNanos = unit.toNanos(sleepFor); + long end = System.nanoTime() + remainingNanos; + while (true) { + try { + // TimeUnit.sleep() treats negative timeouts just like zero. + NANOSECONDS.sleep(remainingNanos); + return; + } catch (InterruptedException e) { + interrupted = true; + remainingNanos = end - System.nanoTime(); + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } +} diff --git a/src/main/java/com/google/maps/internal/ratelimiter/SmoothRateLimiter.java b/src/main/java/com/google/maps/internal/ratelimiter/SmoothRateLimiter.java new file mode 100644 index 000000000..346358227 --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/SmoothRateLimiter.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +import static java.lang.Math.min; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.TimeUnit; + +abstract class SmoothRateLimiter extends RateLimiter { + /* + * How is the RateLimiter designed, and why? + * + * The primary feature of a RateLimiter is its "stable rate", the maximum rate that is should + * allow at normal conditions. This is enforced by "throttling" incoming requests as needed, i.e. + * compute, for an incoming request, the appropriate throttle time, and make the calling thread + * wait as much. + * + * The simplest way to maintain a rate of QPS is to keep the timestamp of the last granted + * request, and ensure that (1/QPS) seconds have elapsed since then. For example, for a rate of + * QPS=5 (5 tokens per second), if we ensure that a request isn't granted earlier than 200ms after + * the last one, then we achieve the intended rate. If a request comes and the last request was + * granted only 100ms ago, then we wait for another 100ms. At this rate, serving 15 fresh permits + * (i.e. for an acquire(15) request) naturally takes 3 seconds. + * + * It is important to realize that such a RateLimiter has a very superficial memory of the past: + * it only remembers the last request. What if the RateLimiter was unused for a long period of + * time, then a request arrived and was immediately granted? This RateLimiter would immediately + * forget about that past underutilization. This may result in either underutilization or + * overflow, depending on the real world consequences of not using the expected rate. + * + * Past underutilization could mean that excess resources are available. Then, the RateLimiter + * should speed up for a while, to take advantage of these resources. This is important when the + * rate is applied to networking (limiting bandwidth), where past underutilization typically + * translates to "almost empty buffers", which can be filled immediately. + * + * On the other hand, past underutilization could mean that "the server responsible for handling + * the request has become less ready for future requests", i.e. its caches become stale, and + * requests become more likely to trigger expensive operations (a more extreme case of this + * example is when a server has just booted, and it is mostly busy with getting itself up to + * speed). + * + * To deal with such scenarios, we add an extra dimension, that of "past underutilization", + * modeled by "storedPermits" variable. This variable is zero when there is no underutilization, + * and it can grow up to maxStoredPermits, for sufficiently large underutilization. So, the + * requested permits, by an invocation acquire(permits), are served from: + * + * - stored permits (if available) + * + * - fresh permits (for any remaining permits) + * + * How this works is best explained with an example: + * + * For a RateLimiter that produces 1 token per second, every second that goes by with the + * RateLimiter being unused, we increase storedPermits by 1. Say we leave the RateLimiter unused + * for 10 seconds (i.e., we expected a request at time X, but we are at time X + 10 seconds before + * a request actually arrives; this is also related to the point made in the last paragraph), thus + * storedPermits becomes 10.0 (assuming maxStoredPermits >= 10.0). At that point, a request of + * acquire(3) arrives. We serve this request out of storedPermits, and reduce that to 7.0 (how + * this is translated to throttling time is discussed later). Immediately after, assume that an + * acquire(10) request arriving. We serve the request partly from storedPermits, using all the + * remaining 7.0 permits, and the remaining 3.0, we serve them by fresh permits produced by the + * rate limiter. + * + * We already know how much time it takes to serve 3 fresh permits: if the rate is + * "1 token per second", then this will take 3 seconds. But what does it mean to serve 7 stored + * permits? As explained above, there is no unique answer. If we are primarily interested to deal + * with underutilization, then we want stored permits to be given out /faster/ than fresh ones, + * because underutilization = free resources for the taking. If we are primarily interested to + * deal with overflow, then stored permits could be given out /slower/ than fresh ones. Thus, we + * require a (different in each case) function that translates storedPermits to throtting time. + * + * This role is played by storedPermitsToWaitTime(double storedPermits, double permitsToTake). The + * underlying model is a continuous function mapping storedPermits (from 0.0 to maxStoredPermits) + * onto the 1/rate (i.e. intervals) that is effective at the given storedPermits. "storedPermits" + * essentially measure unused time; we spend unused time buying/storing permits. Rate is + * "permits / time", thus "1 / rate = time / permits". Thus, "1/rate" (time / permits) times + * "permits" gives time, i.e., integrals on this function (which is what storedPermitsToWaitTime() + * computes) correspond to minimum intervals between subsequent requests, for the specified number + * of requested permits. + * + * Here is an example of storedPermitsToWaitTime: If storedPermits == 10.0, and we want 3 permits, + * we take them from storedPermits, reducing them to 7.0, and compute the throttling for these as + * a call to storedPermitsToWaitTime(storedPermits = 10.0, permitsToTake = 3.0), which will + * evaluate the integral of the function from 7.0 to 10.0. + * + * Using integrals guarantees that the effect of a single acquire(3) is equivalent to { + * acquire(1); acquire(1); acquire(1); }, or { acquire(2); acquire(1); }, etc, since the integral + * of the function in [7.0, 10.0] is equivalent to the sum of the integrals of [7.0, 8.0], [8.0, + * 9.0], [9.0, 10.0] (and so on), no matter what the function is. This guarantees that we handle + * correctly requests of varying weight (permits), /no matter/ what the actual function is - so we + * can tweak the latter freely. (The only requirement, obviously, is that we can compute its + * integrals). + * + * Note well that if, for this function, we chose a horizontal line, at height of exactly (1/QPS), + * then the effect of the function is non-existent: we serve storedPermits at exactly the same + * cost as fresh ones (1/QPS is the cost for each). We use this trick later. + * + * If we pick a function that goes /below/ that horizontal line, it means that we reduce the area + * of the function, thus time. Thus, the RateLimiter becomes /faster/ after a period of + * underutilization. If, on the other hand, we pick a function that goes /above/ that horizontal + * line, then it means that the area (time) is increased, thus storedPermits are more costly than + * fresh permits, thus the RateLimiter becomes /slower/ after a period of underutilization. + * + * Last, but not least: consider a RateLimiter with rate of 1 permit per second, currently + * completely unused, and an expensive acquire(100) request comes. It would be nonsensical to just + * wait for 100 seconds, and /then/ start the actual task. Why wait without doing anything? A much + * better approach is to /allow/ the request right away (as if it was an acquire(1) request + * instead), and postpone /subsequent/ requests as needed. In this version, we allow starting the + * task immediately, and postpone by 100 seconds future requests, thus we allow for work to get + * done in the meantime instead of waiting idly. + * + * This has important consequences: it means that the RateLimiter doesn't remember the time of the + * _last_ request, but it remembers the (expected) time of the _next_ request. This also enables + * us to tell immediately (see tryAcquire(timeout)) whether a particular timeout is enough to get + * us to the point of the next scheduling time, since we always maintain that. And what we mean by + * "an unused RateLimiter" is also defined by that notion: when we observe that the + * "expected arrival time of the next request" is actually in the past, then the difference (now - + * past) is the amount of time that the RateLimiter was formally unused, and it is that amount of + * time which we translate to storedPermits. (We increase storedPermits with the amount of permits + * that would have been produced in that idle time). So, if rate == 1 permit per second, and + * arrivals come exactly one second after the previous, then storedPermits is _never_ increased -- + * we would only increase it for arrivals _later_ than the expected one second. + */ + + /** + * This implements the following function where coldInterval = coldFactor * stableInterval. + * + *

    +   *          ^ throttling
    +   *          |
    +   *    cold  +                  /
    +   * interval |                 /.
    +   *          |                / .
    +   *          |               /  .   ← "warmup period" is the area of the trapezoid between
    +   *          |              /   .     thresholdPermits and maxPermits
    +   *          |             /    .
    +   *          |            /     .
    +   *          |           /      .
    +   *   stable +----------/  WARM .
    +   * interval |          .   UP  .
    +   *          |          . PERIOD.
    +   *          |          .       .
    +   *        0 +----------+-------+--------------→ storedPermits
    +   *          0 thresholdPermits maxPermits
    +   * 
    + * + * Before going into the details of this particular function, let's keep in mind the basics: + * + *
      + *
    1. The state of the RateLimiter (storedPermits) is a vertical line in this figure. + *
    2. When the RateLimiter is not used, this goes right (up to maxPermits) + *
    3. When the RateLimiter is used, this goes left (down to zero), since if we have + * storedPermits, we serve from those first + *
    4. When _unused_, we go right at a constant rate! The rate at which we move to the right is + * chosen as maxPermits / warmupPeriod. This ensures that the time it takes to go from 0 to + * maxPermits is equal to warmupPeriod. + *
    5. When _used_, the time it takes, as explained in the introductory class note, is equal to + * the integral of our function, between X permits and X-K permits, assuming we want to + * spend K saved permits. + *
    + * + *

    In summary, the time it takes to move to the left (spend K permits), is equal to the area of + * the function of width == K. + * + *

    Assuming we have saturated demand, the time to go from maxPermits to thresholdPermits is + * equal to warmupPeriod. And the time to go from thresholdPermits to 0 is warmupPeriod/2. (The + * reason that this is warmupPeriod/2 is to maintain the behavior of the original implementation + * where coldFactor was hard coded as 3.) + * + *

    It remains to calculate thresholdsPermits and maxPermits. + * + *

      + *
    • The time to go from thresholdPermits to 0 is equal to the integral of the function + * between 0 and thresholdPermits. This is thresholdPermits * stableIntervals. By (5) it is + * also equal to warmupPeriod/2. Therefore + *
      + * thresholdPermits = 0.5 * warmupPeriod / stableInterval + *
      + *
    • The time to go from maxPermits to thresholdPermits is equal to the integral of the + * function between thresholdPermits and maxPermits. This is the area of the pictured + * trapezoid, and it is equal to 0.5 * (stableInterval + coldInterval) * (maxPermits - + * thresholdPermits). It is also equal to warmupPeriod, so + *
      + * maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval) + *
      + *
    + */ + static final class SmoothWarmingUp extends SmoothRateLimiter { + private final long warmupPeriodMicros; + /** + * The slope of the line from the stable interval (when permits == 0), to the cold interval + * (when permits == maxPermits) + */ + private double slope; + + private double thresholdPermits; + private double coldFactor; + + SmoothWarmingUp( + SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit, double coldFactor) { + super(stopwatch); + this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod); + this.coldFactor = coldFactor; + } + + @Override + void doSetRate(double permitsPerSecond, double stableIntervalMicros) { + double oldMaxPermits = maxPermits; + double coldIntervalMicros = stableIntervalMicros * coldFactor; + thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros; + maxPermits = + thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros); + slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits); + if (oldMaxPermits == Double.POSITIVE_INFINITY) { + // if we don't special-case this, we would get storedPermits == NaN, below + storedPermits = 0.0; + } else { + storedPermits = + (oldMaxPermits == 0.0) + ? maxPermits // initial state is cold + : storedPermits * maxPermits / oldMaxPermits; + } + } + + @Override + long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { + double availablePermitsAboveThreshold = storedPermits - thresholdPermits; + long micros = 0; + // measuring the integral on the right part of the function (the climbing line) + if (availablePermitsAboveThreshold > 0.0) { + double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake); + double length = + permitsToTime(availablePermitsAboveThreshold) + + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake); + micros = (long) (permitsAboveThresholdToTake * length / 2.0); + permitsToTake -= permitsAboveThresholdToTake; + } + // measuring the integral on the left part of the function (the horizontal line) + micros += (long) (stableIntervalMicros * permitsToTake); + return micros; + } + + private double permitsToTime(double permits) { + return stableIntervalMicros + permits * slope; + } + + @Override + double coolDownIntervalMicros() { + return warmupPeriodMicros / maxPermits; + } + } + + /** + * This implements a "bursty" RateLimiter, where storedPermits are translated to zero throttling. + * The maximum number of permits that can be saved (when the RateLimiter is unused) is defined in + * terms of time, in this sense: if a RateLimiter is 2qps, and this time is specified as 10 + * seconds, we can save up to 2 * 10 = 20 permits. + */ + static final class SmoothBursty extends SmoothRateLimiter { + /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */ + final double maxBurstSeconds; + + SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) { + super(stopwatch); + this.maxBurstSeconds = maxBurstSeconds; + } + + @Override + void doSetRate(double permitsPerSecond, double stableIntervalMicros) { + double oldMaxPermits = this.maxPermits; + maxPermits = maxBurstSeconds * permitsPerSecond; + if (oldMaxPermits == Double.POSITIVE_INFINITY) { + // if we don't special-case this, we would get storedPermits == NaN, below + storedPermits = maxPermits; + } else { + storedPermits = + (oldMaxPermits == 0.0) + ? 0.0 // initial state + : storedPermits * maxPermits / oldMaxPermits; + } + } + + @Override + long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { + return 0L; + } + + @Override + double coolDownIntervalMicros() { + return stableIntervalMicros; + } + } + + /** The currently stored permits. */ + double storedPermits; + + /** The maximum number of stored permits. */ + double maxPermits; + + /** + * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits + * per second has a stable interval of 200ms. + */ + double stableIntervalMicros; + + /** + * The time when the next request (no matter its size) will be granted. After granting a request, + * this is pushed further in the future. Large requests push this further than small requests. + */ + private long nextFreeTicketMicros = 0L; // could be either in the past or future + + private SmoothRateLimiter(SleepingStopwatch stopwatch) { + super(stopwatch); + } + + @Override + final void doSetRate(double permitsPerSecond, long nowMicros) { + resync(nowMicros); + double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond; + this.stableIntervalMicros = stableIntervalMicros; + doSetRate(permitsPerSecond, stableIntervalMicros); + } + + abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros); + + @Override + final double doGetRate() { + return SECONDS.toMicros(1L) / stableIntervalMicros; + } + + @Override + final long queryEarliestAvailable(long nowMicros) { + return nextFreeTicketMicros; + } + + @Override + final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { + resync(nowMicros); + long returnValue = nextFreeTicketMicros; + double storedPermitsToSpend = min(requiredPermits, this.storedPermits); + double freshPermits = requiredPermits - storedPermitsToSpend; + long waitMicros = + storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + + (long) (freshPermits * stableIntervalMicros); + + this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); + this.storedPermits -= storedPermitsToSpend; + return returnValue; + } + + /** + * Translates a specified portion of our currently stored permits which we want to spend/acquire, + * into a throttling time. Conceptually, this evaluates the integral of the underlying function we + * use, for the range of [(storedPermits - permitsToTake), storedPermits]. + * + *

    This always holds: {@code 0 <= permitsToTake <= storedPermits} + */ + abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake); + + /** + * Returns the number of microseconds during cool down that we have to wait to get a new permit. + */ + abstract double coolDownIntervalMicros(); + + /** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */ + void resync(long nowMicros) { + // if nextFreeTicket is in the past, resync to now + if (nowMicros > nextFreeTicketMicros) { + double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); + storedPermits = min(maxPermits, storedPermits + newPermits); + nextFreeTicketMicros = nowMicros; + } + } +} diff --git a/src/main/java/com/google/maps/internal/ratelimiter/Stopwatch.java b/src/main/java/com/google/maps/internal/ratelimiter/Stopwatch.java new file mode 100644 index 000000000..167dcc24b --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/Stopwatch.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2008 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +import static com.google.maps.internal.ratelimiter.Preconditions.checkNotNull; +import static com.google.maps.internal.ratelimiter.Preconditions.checkState; +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.MICROSECONDS; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.concurrent.TimeUnit; + +/** + * An object that measures elapsed time in nanoseconds. It is useful to measure elapsed time using + * this class instead of direct calls to {@link System#nanoTime} for a few reasons: + * + *

      + *
    • An alternate time source can be substituted, for testing or performance reasons. + *
    • As documented by {@code nanoTime}, the value returned has no absolute meaning, and can only + * be interpreted as relative to another timestamp returned by {@code nanoTime} at a different + * time. {@code Stopwatch} is a more effective abstraction because it exposes only these + * relative values, not the absolute ones. + *
    + * + *

    Basic usage: + * + *

    {@code
    + * Stopwatch stopwatch = Stopwatch.createStarted();
    + * doSomething();
    + * stopwatch.stop(); // optional
    + *
    + * long millis = stopwatch.elapsed(MILLISECONDS);
    + *
    + * log.info("time: " + stopwatch); // formatted string like "12.3 ms"
    + * }
    + * + *

    Stopwatch methods are not idempotent; it is an error to start or stop a stopwatch that is + * already in the desired state. + * + *

    When testing code that uses this class, use {@link #createUnstarted(Ticker)} or {@link + * #createStarted(Ticker)} to supply a fake or mock ticker. This allows you to simulate any valid + * behavior of the stopwatch. + * + *

    Note: This class is not thread-safe. + * + *

    Warning for Android users: a stopwatch with default behavior may not continue to keep + * time while the device is asleep. Instead, create one like this: + * + *

    {@code
    + * Stopwatch.createStarted(
    + *      new Ticker() {
    + *        public long read() {
    + *          return android.os.SystemClock.elapsedRealtimeNanos();
    + *        }
    + *      });
    + * }
    + * + * @author Kevin Bourrillion + */ +public final class Stopwatch { + private final Ticker ticker; + private boolean isRunning; + private long elapsedNanos; + private long startTick; + + /** + * Creates (but does not start) a new stopwatch using {@link System#nanoTime} as its time source. + */ + public static Stopwatch createUnstarted() { + return new Stopwatch(); + } + + /** Creates (but does not start) a new stopwatch, using the specified time source. */ + public static Stopwatch createUnstarted(Ticker ticker) { + return new Stopwatch(ticker); + } + + /** Creates (and starts) a new stopwatch using {@link System#nanoTime} as its time source. */ + public static Stopwatch createStarted() { + return new Stopwatch().start(); + } + + /** Creates (and starts) a new stopwatch, using the specified time source. */ + public static Stopwatch createStarted(Ticker ticker) { + return new Stopwatch(ticker).start(); + } + + Stopwatch() { + this.ticker = Ticker.systemTicker(); + } + + Stopwatch(Ticker ticker) { + this.ticker = checkNotNull(ticker, "ticker"); + } + + /** + * Returns {@code true} if {@link #start()} has been called on this stopwatch, and {@link #stop()} + * has not been called since the last call to {@code start()}. + */ + public boolean isRunning() { + return isRunning; + } + + /** + * Starts the stopwatch. + * + * @return this {@code Stopwatch} instance + * @throws IllegalStateException if the stopwatch is already running. + */ + public Stopwatch start() { + checkState(!isRunning, "This stopwatch is already running."); + isRunning = true; + startTick = ticker.read(); + return this; + } + + /** + * Stops the stopwatch. Future reads will return the fixed duration that had elapsed up to this + * point. + * + * @return this {@code Stopwatch} instance + * @throws IllegalStateException if the stopwatch is already stopped. + */ + public Stopwatch stop() { + long tick = ticker.read(); + checkState(isRunning, "This stopwatch is already stopped."); + isRunning = false; + elapsedNanos += tick - startTick; + return this; + } + + /** + * Sets the elapsed time for this stopwatch to zero, and places it in a stopped state. + * + * @return this {@code Stopwatch} instance + */ + public Stopwatch reset() { + elapsedNanos = 0; + isRunning = false; + return this; + } + + private long elapsedNanos() { + return isRunning ? ticker.read() - startTick + elapsedNanos : elapsedNanos; + } + + /** + * Returns the current elapsed time shown on this stopwatch, expressed in the desired time unit, + * with any fraction rounded down. + * + *

    Note that the overhead of measurement can be more than a microsecond, so it is generally not + * useful to specify {@link TimeUnit#NANOSECONDS} precision here. + */ + public long elapsed(TimeUnit desiredUnit) { + return desiredUnit.convert(elapsedNanos(), NANOSECONDS); + } + + /** Returns a string representation of the current elapsed time. */ + @Override + public String toString() { + long nanos = elapsedNanos(); + + TimeUnit unit = chooseUnit(nanos); + double value = (double) nanos / NANOSECONDS.convert(1, unit); + + // Too bad this functionality is not exposed as a regular method call + return Platform.formatCompact4Digits(value) + " " + abbreviate(unit); + } + + private static TimeUnit chooseUnit(long nanos) { + if (DAYS.convert(nanos, NANOSECONDS) > 0) { + return DAYS; + } + if (HOURS.convert(nanos, NANOSECONDS) > 0) { + return HOURS; + } + if (MINUTES.convert(nanos, NANOSECONDS) > 0) { + return MINUTES; + } + if (SECONDS.convert(nanos, NANOSECONDS) > 0) { + return SECONDS; + } + if (MILLISECONDS.convert(nanos, NANOSECONDS) > 0) { + return MILLISECONDS; + } + if (MICROSECONDS.convert(nanos, NANOSECONDS) > 0) { + return MICROSECONDS; + } + return NANOSECONDS; + } + + private static String abbreviate(TimeUnit unit) { + switch (unit) { + case NANOSECONDS: + return "ns"; + case MICROSECONDS: + return "\u03bcs"; // μs + case MILLISECONDS: + return "ms"; + case SECONDS: + return "s"; + case MINUTES: + return "min"; + case HOURS: + return "h"; + case DAYS: + return "d"; + default: + throw new AssertionError(); + } + } +} diff --git a/src/main/java/com/google/maps/internal/ratelimiter/Ticker.java b/src/main/java/com/google/maps/internal/ratelimiter/Ticker.java new file mode 100644 index 000000000..c3d5880d8 --- /dev/null +++ b/src/main/java/com/google/maps/internal/ratelimiter/Ticker.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.internal.ratelimiter; + +/** + * A time source; returns a time value representing the number of nanoseconds elapsed since some + * fixed but arbitrary point in time. Note that most users should use {@link Stopwatch} instead of + * interacting with this class directly. + * + *

    Warning: this interface can only be used to measure elapsed time, not wall time. + * + * @author Kevin Bourrillion + */ +public abstract class Ticker { + /** Constructor for use by subclasses. */ + protected Ticker() {} + + /** Returns the number of nanoseconds elapsed since this ticker's fixed point of reference. */ + public abstract long read(); + + /** A ticker that reads the current time using {@link System#nanoTime}. */ + public static Ticker systemTicker() { + return SYSTEM_TICKER; + } + + private static final Ticker SYSTEM_TICKER = + new Ticker() { + @Override + public long read() { + return Platform.systemNanoTime(); + } + }; +} diff --git a/src/main/java/com/google/maps/metrics/NoOpRequestMetrics.java b/src/main/java/com/google/maps/metrics/NoOpRequestMetrics.java new file mode 100644 index 000000000..750261305 --- /dev/null +++ b/src/main/java/com/google/maps/metrics/NoOpRequestMetrics.java @@ -0,0 +1,13 @@ +package com.google.maps.metrics; + +/** A no-op implementation that does nothing */ +final class NoOpRequestMetrics implements RequestMetrics { + + NoOpRequestMetrics(String requestName) {} + + public void startNetwork() {} + + public void endNetwork() {} + + public void endRequest(Exception exception, int httpStatusCode, long retryCount) {} +} diff --git a/src/main/java/com/google/maps/metrics/NoOpRequestMetricsReporter.java b/src/main/java/com/google/maps/metrics/NoOpRequestMetricsReporter.java new file mode 100644 index 000000000..cb0cf71c1 --- /dev/null +++ b/src/main/java/com/google/maps/metrics/NoOpRequestMetricsReporter.java @@ -0,0 +1,11 @@ +package com.google.maps.metrics; + +/** A no-op implementation that does nothing */ +public final class NoOpRequestMetricsReporter implements RequestMetricsReporter { + + public NoOpRequestMetricsReporter() {} + + public RequestMetrics newRequest(String requestName) { + return new NoOpRequestMetrics(requestName); + } +} diff --git a/src/main/java/com/google/maps/metrics/OpenCensusMetrics.java b/src/main/java/com/google/maps/metrics/OpenCensusMetrics.java new file mode 100644 index 000000000..b6be49050 --- /dev/null +++ b/src/main/java/com/google/maps/metrics/OpenCensusMetrics.java @@ -0,0 +1,128 @@ +package com.google.maps.metrics; + +import io.opencensus.stats.Aggregation; +import io.opencensus.stats.Aggregation.Count; +import io.opencensus.stats.Aggregation.Distribution; +import io.opencensus.stats.BucketBoundaries; +import io.opencensus.stats.Measure.MeasureLong; +import io.opencensus.stats.Stats; +import io.opencensus.stats.View; +import io.opencensus.stats.ViewManager; +import io.opencensus.tags.TagKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/* + * OpenCensus metrics which are measured for every request. + */ +public final class OpenCensusMetrics { + private OpenCensusMetrics() {} + + public static final class Tags { + private Tags() {} + + public static final TagKey REQUEST_NAME = TagKey.create("request_name"); + public static final TagKey HTTP_CODE = TagKey.create("http_code"); + public static final TagKey API_STATUS = TagKey.create("api_status"); + } + + public static final class Measures { + private Measures() {} + + public static final MeasureLong LATENCY = + MeasureLong.create( + "maps.googleapis.com/measure/client/latency", + "Total time between library method called and results returned", + "ms"); + + public static final MeasureLong NETWORK_LATENCY = + MeasureLong.create( + "maps.googleapis.com/measure/client/network_latency", + "Network time inside the library", + "ms"); + + public static final MeasureLong RETRY_COUNT = + MeasureLong.create( + "maps.googleapis.com/measure/client/retry_count", + "How many times any request was retried", + "1"); + } + + private static final class Aggregations { + private Aggregations() {} + + private static final Aggregation COUNT = Count.create(); + + private static final Aggregation DISTRIBUTION_INTEGERS_10 = + Distribution.create( + BucketBoundaries.create( + Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0))); + + // every bucket is ~25% bigger = 20 * 2^(N/3) + private static final Aggregation DISTRIBUTION_LATENCY = + Distribution.create( + BucketBoundaries.create( + Arrays.asList( + 0.0, 20.0, 25.2, 31.7, 40.0, 50.4, 63.5, 80.0, 100.8, 127.0, 160.0, 201.6, + 254.0, 320.0, 403.2, 508.0, 640.0, 806.3, 1015.9, 1280.0, 1612.7, 2031.9, + 2560.0, 3225.4, 4063.7))); + } + + public static final class Views { + private Views() {} + + private static final List fields = + tags(Tags.REQUEST_NAME, Tags.HTTP_CODE, Tags.API_STATUS); + + public static final View REQUEST_COUNT = + View.create( + View.Name.create("maps.googleapis.com/client/request_count"), + "Request counts", + Measures.LATENCY, + Aggregations.COUNT, + fields); + + public static final View REQUEST_LATENCY = + View.create( + View.Name.create("maps.googleapis.com/client/request_latency"), + "Latency in msecs", + Measures.LATENCY, + Aggregations.DISTRIBUTION_LATENCY, + fields); + + public static final View NETWORK_LATENCY = + View.create( + View.Name.create("maps.googleapis.com/client/network_latency"), + "Network latency in msecs (internal)", + Measures.NETWORK_LATENCY, + Aggregations.DISTRIBUTION_LATENCY, + fields); + + public static final View RETRY_COUNT = + View.create( + View.Name.create("maps.googleapis.com/client/retry_count"), + "Retries per request", + Measures.RETRY_COUNT, + Aggregations.DISTRIBUTION_INTEGERS_10, + fields); + } + + public static void registerAllViews() { + registerAllViews(Stats.getViewManager()); + } + + public static void registerAllViews(ViewManager viewManager) { + View[] views_to_register = + new View[] { + Views.REQUEST_COUNT, Views.REQUEST_LATENCY, Views.NETWORK_LATENCY, Views.RETRY_COUNT + }; + for (View view : views_to_register) { + viewManager.registerView(view); + } + } + + private static List tags(TagKey... items) { + return Collections.unmodifiableList(Arrays.asList(items)); + } +} diff --git a/src/main/java/com/google/maps/metrics/OpenCensusRequestMetrics.java b/src/main/java/com/google/maps/metrics/OpenCensusRequestMetrics.java new file mode 100644 index 000000000..eb6fa694b --- /dev/null +++ b/src/main/java/com/google/maps/metrics/OpenCensusRequestMetrics.java @@ -0,0 +1,75 @@ +package com.google.maps.metrics; + +import io.opencensus.stats.StatsRecorder; +import io.opencensus.tags.TagContext; +import io.opencensus.tags.TagValue; +import io.opencensus.tags.Tagger; + +/** An OpenCensus logger that generates success and latency metrics. */ +final class OpenCensusRequestMetrics implements RequestMetrics { + private final String requestName; + private final Tagger tagger; + private final StatsRecorder statsRecorder; + + private long requestStart; + private long networkStart; + private long networkTime; + private boolean finished; + + OpenCensusRequestMetrics(String requestName, Tagger tagger, StatsRecorder statsRecorder) { + this.requestName = requestName; + this.tagger = tagger; + this.statsRecorder = statsRecorder; + this.requestStart = milliTime(); + this.networkStart = milliTime(); + this.networkTime = 0; + this.finished = false; + } + + @Override + public void startNetwork() { + this.networkStart = milliTime(); + } + + @Override + public void endNetwork() { + this.networkTime += milliTime() - this.networkStart; + } + + @Override + public void endRequest(Exception exception, int httpStatusCode, long retryCount) { + // multiple endRequest are ignored + if (this.finished) { + return; + } + this.finished = true; + long requestTime = milliTime() - this.requestStart; + + TagContext tagContext = + tagger + .currentBuilder() + .putLocal(OpenCensusMetrics.Tags.REQUEST_NAME, TagValue.create(requestName)) + .putLocal( + OpenCensusMetrics.Tags.HTTP_CODE, TagValue.create(Integer.toString(httpStatusCode))) + .putLocal(OpenCensusMetrics.Tags.API_STATUS, TagValue.create(exceptionName(exception))) + .build(); + statsRecorder + .newMeasureMap() + .put(OpenCensusMetrics.Measures.LATENCY, requestTime) + .put(OpenCensusMetrics.Measures.NETWORK_LATENCY, this.networkTime) + .put(OpenCensusMetrics.Measures.RETRY_COUNT, retryCount) + .record(tagContext); + } + + private String exceptionName(Exception exception) { + if (exception == null) { + return ""; + } else { + return exception.getClass().getName(); + } + } + + private long milliTime() { + return System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/google/maps/metrics/OpenCensusRequestMetricsReporter.java b/src/main/java/com/google/maps/metrics/OpenCensusRequestMetricsReporter.java new file mode 100644 index 000000000..363909d82 --- /dev/null +++ b/src/main/java/com/google/maps/metrics/OpenCensusRequestMetricsReporter.java @@ -0,0 +1,19 @@ +package com.google.maps.metrics; + +import io.opencensus.stats.Stats; +import io.opencensus.stats.StatsRecorder; +import io.opencensus.tags.Tagger; +import io.opencensus.tags.Tags; + +/** An OpenCensus logger that generates success and latency metrics. */ +public final class OpenCensusRequestMetricsReporter implements RequestMetricsReporter { + private static final Tagger tagger = Tags.getTagger(); + private static final StatsRecorder statsRecorder = Stats.getStatsRecorder(); + + public OpenCensusRequestMetricsReporter() {} + + @Override + public RequestMetrics newRequest(String requestName) { + return new OpenCensusRequestMetrics(requestName, tagger, statsRecorder); + } +} diff --git a/src/main/java/com/google/maps/metrics/RequestMetrics.java b/src/main/java/com/google/maps/metrics/RequestMetrics.java new file mode 100644 index 000000000..35fab11e8 --- /dev/null +++ b/src/main/java/com/google/maps/metrics/RequestMetrics.java @@ -0,0 +1,27 @@ +package com.google.maps.metrics; + +/** + * A type to report common metrics shared among all request types. + * + *

    If a request retries, there will be multiple calls to all methods below. Ignore any endRequest + * after the first one. For example: + * + *

      + *
    1. constructor - request starts + *
    2. startNetwork / endNetwork - original request + *
    3. startNetwork / endNetwork - retried request + *
    4. endRequest - request finished (retry) + *
    5. endRequest - request finished (original) + *
    + * + *

    The following metrics can be computed: Total queries, successful queries, total latency, + * network latency + */ +public interface RequestMetrics { + + void startNetwork(); + + void endNetwork(); + + void endRequest(Exception exception, int httpStatusCode, long retryCount); +} diff --git a/src/main/java/com/google/maps/metrics/RequestMetricsReporter.java b/src/main/java/com/google/maps/metrics/RequestMetricsReporter.java new file mode 100644 index 000000000..9f52c4c8f --- /dev/null +++ b/src/main/java/com/google/maps/metrics/RequestMetricsReporter.java @@ -0,0 +1,7 @@ +package com.google.maps.metrics; + +/** A type to report common metrics shared among all request types. */ +public interface RequestMetricsReporter { + + RequestMetrics newRequest(String requestName); +} diff --git a/src/main/java/com/google/maps/model/AddressComponent.java b/src/main/java/com/google/maps/model/AddressComponent.java index 8b07eb408..896751b6c 100644 --- a/src/main/java/com/google/maps/model/AddressComponent.java +++ b/src/main/java/com/google/maps/model/AddressComponent.java @@ -15,29 +15,44 @@ package com.google.maps.model; +import static com.google.maps.internal.StringJoin.join; + +import java.io.Serializable; + /** * The parts of an address. * - *

    See here for more - * detail. + *

    See Address + * Types and Address Component Types in the Google Maps Geocoding API + * Developer's Guide for more detail. */ -public class AddressComponent { - /** - * {@code longName} is the full text description or name of the address component as returned by - * the Geocoder. - */ +public class AddressComponent implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The full text description or name of the address component as returned by the Geocoder. */ public String longName; /** - * {@code shortName} is an abbreviated textual name for the address component, if available. For - * example, an address component for the state of Alaska may have a longName of "Alaska" and a - * shortName of "AK" using the 2-letter postal abbreviation. + * An abbreviated textual name for the address component, if available. For example, an address + * component for the state of Alaska may have a longName of "Alaska" and a shortName of "AK" using + * the 2-letter postal abbreviation. */ public String shortName; - /** - * This indicates the type of each part of the address. Examples include street number or country. - */ + /** Indicates the type of each part of the address. Examples include street number or country. */ public AddressComponentType[] types; + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[AddressComponent: "); + sb.append("\"").append(longName).append("\""); + if (shortName != null) { + sb.append(" (\"").append(shortName).append("\")"); + } + sb.append(" (").append(join(", ", (Object[]) types)).append(")"); + sb.append("]"); + return sb.toString(); + } } diff --git a/src/main/java/com/google/maps/model/AddressComponentType.java b/src/main/java/com/google/maps/model/AddressComponentType.java index a85fa74d6..aa0d565d7 100644 --- a/src/main/java/com/google/maps/model/AddressComponentType.java +++ b/src/main/java/com/google/maps/model/AddressComponentType.java @@ -16,209 +16,288 @@ package com.google.maps.model; /** - * The Adress Component types. Please see - * Address Component - * Types for more detail. + * The Address Component types. Please see Address Types and + * Address Component Types for more detail. */ public enum AddressComponentType { - /** - * {@code STREET_ADDRESS} indicates a precise street address. - */ - STREET_ADDRESS, + /** A precise street address. */ + STREET_ADDRESS("street_address"), - /** - * {@code ROUTE} indicates a named route (such as "US 101"). - */ - ROUTE, + /** A named route (such as "US 101"). */ + ROUTE("route"), - /** - * {@code INTERSECTION} indicates a major intersection, usually of two major roads. - */ - INTERSECTION, + /** A major intersection, usually of two major roads. */ + INTERSECTION("intersection"), - /** - * {@code POLITICAL} indicates a political entity. Usually, this type indicates a polygon of - * some civil administration. - */ - POLITICAL, + /** A continent. */ + CONTINENT("continent"), - /** - * {@code COUNTRY} indicates the national political entity, and is typically the highest order - * type returned by the Geocoder. - */ - COUNTRY, + /** A political entity. Usually, this type indicates a polygon of some civil administration. */ + POLITICAL("political"), - /** - * {@code ADMINISTRATIVE_AREA_LEVEL_1} indicates a first-order civil entity below the country - * level. Within the United States, these administrative levels are states. Not all nations - * exhibit these administrative levels. - */ - ADMINISTRATIVE_AREA_LEVEL_1, + /** A national political entity, typically the highest order type returned by the Geocoder. */ + COUNTRY("country"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_2} indicates a second-order civil entity below the country - * level. Within the United States, these administrative levels are counties. Not all nations - * exhibit these administrative levels. + * A first-order civil entity below the country level. Within the United States, these + * administrative levels are states. Not all nations exhibit these administrative levels. */ - ADMINISTRATIVE_AREA_LEVEL_2, + ADMINISTRATIVE_AREA_LEVEL_1("administrative_area_level_1"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_3} indicates a third-order civil entity below the country - * level. This type indicates a minor civil division. Not all nations exhibit these - * administrative levels. + * A second-order civil entity below the country level. Within the United States, these + * administrative levels are counties. Not all nations exhibit these administrative levels. */ - ADMINISTRATIVE_AREA_LEVEL_3, + ADMINISTRATIVE_AREA_LEVEL_2("administrative_area_level_2"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_4} indicates a fourth-order civil entity below the country - * level. This type indicates a minor civil division. Not all nations exhibit these - * administrative levels. + * A third-order civil entity below the country level. This type indicates a minor civil division. + * Not all nations exhibit these administrative levels. */ - ADMINISTRATIVE_AREA_LEVEL_4, + ADMINISTRATIVE_AREA_LEVEL_3("administrative_area_level_3"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_5} indicates a fifth-order civil entity below the country - * level. This type indicates a minor civil division. Not all nations exhibit these - * administrative levels. + * A fourth-order civil entity below the country level. This type indicates a minor civil + * division. Not all nations exhibit these administrative levels. */ - ADMINISTRATIVE_AREA_LEVEL_5, + ADMINISTRATIVE_AREA_LEVEL_4("administrative_area_level_4"), /** - * {@code COLLOQUIAL_AREA} indicates a commonly-used alternative name for the entity. + * A fifth-order civil entity below the country level. This type indicates a minor civil division. + * Not all nations exhibit these administrative levels. */ - COLLOQUIAL_AREA, + ADMINISTRATIVE_AREA_LEVEL_5("administrative_area_level_5"), + + /** A commonly-used alternative name for the entity. */ + COLLOQUIAL_AREA("colloquial_area"), + + /** An incorporated city or town political entity. */ + LOCALITY("locality"), /** - * {@code LOCALITY} indicates an incorporated city or town political entity. + * A specific type of Japanese locality, used to facilitate distinction between multiple locality + * components within a Japanese address. */ - LOCALITY, + WARD("ward"), /** - * {@code SUBLOCALITY} indicates a first-order civil entity below a locality. For some locations - * may receive one of the additional types: sublocality_level_1 to sublocality_level_5. Each - * sublocality level is a civil entity. Larger numbers indicate a smaller geographic area. + * A first-order civil entity below a locality. For some locations may receive one of the + * additional types: sublocality_level_1 to sublocality_level_5. Each sublocality level is a civil + * entity. Larger numbers indicate a smaller geographic area. */ - SUBLOCALITY, - SUBLOCALITY_LEVEL_1, - SUBLOCALITY_LEVEL_2, - SUBLOCALITY_LEVEL_3, - SUBLOCALITY_LEVEL_4, - SUBLOCALITY_LEVEL_5, + SUBLOCALITY("sublocality"), + SUBLOCALITY_LEVEL_1("sublocality_level_1"), + SUBLOCALITY_LEVEL_2("sublocality_level_2"), + SUBLOCALITY_LEVEL_3("sublocality_level_3"), + SUBLOCALITY_LEVEL_4("sublocality_level_4"), + SUBLOCALITY_LEVEL_5("sublocality_level_5"), + /** A named neighborhood. */ + NEIGHBORHOOD("neighborhood"), - /** - * {@code NEIGHBORHOOD} indicates a named neighborhood. - */ - NEIGHBORHOOD, + /** A named location, usually a building or collection of buildings with a common name. */ + PREMISE("premise"), /** - * {@code PREMISE} indicates a named location, usually a building or collection of buildings - * with a common name. + * A first-order entity below a named location, usually a singular building within a collection of + * buildings with a common name. */ - PREMISE, + SUBPREMISE("subpremise"), - /** - * {@code SUBPREMISE} indicates a first-order entity below a named location, usually a singular - * building within a collection of buildings with a common name - */ - SUBPREMISE, + /** A postal code as used to address postal mail within the country. */ + POSTAL_CODE("postal_code"), - /** - * {@code POSTAL_CODE} indicates a postal code as used to address postal mail within the - * country. - */ - POSTAL_CODE, + /** A postal code prefix as used to address postal mail within the country. */ + POSTAL_CODE_PREFIX("postal_code_prefix"), - /** - * {@code POSTAL_CODE_PREFIX} indicates a postal code prefix as used to address postal mail - * within the country. - */ - POSTAL_CODE_PREFIX, + /** A postal code suffix as used to address postal mail within the country. */ + POSTAL_CODE_SUFFIX("postal_code_suffix"), - /** - * {@code POSTAL_CODE_SUFFIX} indicates a postal code suffix as used to address postal mail within the - * country. - */ - POSTAL_CODE_SUFFIX, + /** A prominent natural feature. */ + NATURAL_FEATURE("natural_feature"), - /** - * {@code NATURAL_FEATURE} indicates a prominent natural feature. - */ - NATURAL_FEATURE, + /** An airport. */ + AIRPORT("airport"), - /** - * {@code AIRPORT} indicates an airport. - */ - AIRPORT, + /** A named park. */ + PARK("park"), /** - * {@code PARK} indicates a named park. + * A named point of interest. Typically, these "POI"s are prominent local entities that don't + * easily fit in another category, such as "Empire State Building" or "Statue of Liberty." */ - PARK, + POINT_OF_INTEREST("point_of_interest"), - /** - * {@code POINT_OF_INTEREST} indicates a named point of interest. Typically, these "POI"s are - * prominent local entities that don't easily fit in another category, such as "Empire State - * Building" or "Statue of Liberty." - */ - POINT_OF_INTEREST, + /** The floor of a building address. */ + FLOOR("floor"), - /** - * {@code FLOOR} indicates the floor of a building address. - */ - FLOOR, + /** Typically indicates a place that has not yet been categorized. */ + ESTABLISHMENT("establishment"), - /** - * {@code ESTABLISHMENT} typically indicates a place that has not yet been categorized. - */ - ESTABLISHMENT, + /** A parking lot or parking structure. */ + PARKING("parking"), - /** - * {@code PARKING} indicates a parking lot or parking structure. - */ - PARKING, + /** A specific postal box. */ + POST_BOX("post_box"), /** - * {@code POST_BOX} indicates a specific postal box. + * A grouping of geographic areas, such as locality and sublocality, used for mailing addresses in + * some countries. */ - POST_BOX, + POSTAL_TOWN("postal_town"), - /** - * {@code POSTAL_TOWN} indicates a grouping of geographic areas, such as locality and - * sublocality, used for mailing addresses in some countries. - */ - POSTAL_TOWN, + /** The room of a building address. */ + ROOM("room"), - /** - * {@code ROOM} indicates the room of a building address. - */ - ROOM, + /** The precise street number of an address. */ + STREET_NUMBER("street_number"), - /** - * {@code STREET_NUMBER} indicates the precise street number. - */ - STREET_NUMBER, + /** The location of a bus stop. */ + BUS_STATION("bus_station"), - /** - * {@code BUS_STATION} indicates the location of a bus stop. - */ - BUS_STATION, + /** The location of a train station. */ + TRAIN_STATION("train_station"), - /** - * {@code TRAIN_STATION} indicates the location of a train station. - */ - TRAIN_STATION, + /** The location of a subway station. */ + SUBWAY_STATION("subway_station"), - /** - * {@code TRANSIT_STATION} indicates the location of a transit station. - */ - TRANSIT_STATION, + /** The location of a transit station. */ + TRANSIT_STATION("transit_station"), + + /** The location of a light rail station. */ + LIGHT_RAIL_STATION("light_rail_station"), + + /** A general contractor. */ + GENERAL_CONTRACTOR("general_contractor"), + + /** A food service establishment. */ + FOOD("food"), + + /** A real-estate agency. */ + REAL_ESTATE_AGENCY("real_estate_agency"), + + /** A car-rental establishment. */ + CAR_RENTAL("car_rental"), + + /** A travel agency. */ + TRAVEL_AGENCY("travel_agency"), + + /** An electronics store. */ + ELECTRONICS_STORE("electronics_store"), + + /** A home goods store. */ + HOME_GOODS_STORE("home_goods_store"), + + /** A school. */ + SCHOOL("school"), + + /** A store. */ + STORE("store"), + + /** A shopping mall. */ + SHOPPING_MALL("shopping_mall"), + + /** A lodging establishment. */ + LODGING("lodging"), + + /** An art gallery. */ + ART_GALLERY("art_gallery"), + + /** A lawyer. */ + LAWYER("lawyer"), + + /** A restaurant. */ + RESTAURANT("restaurant"), + + /** A bar. */ + BAR("bar"), + + /** A take-away meal establishment. */ + MEAL_TAKEAWAY("meal_takeaway"), + + /** A clothing store. */ + CLOTHING_STORE("clothing_store"), + + /** A local government office. */ + LOCAL_GOVERNMENT_OFFICE("local_government_office"), + + /** A finance establishment. */ + FINANCE("finance"), + + /** A moving company. */ + MOVING_COMPANY("moving_company"), + + /** A storage establishment. */ + STORAGE("storage"), + + /** A cafe. */ + CAFE("cafe"), + + /** A car repair establishment. */ + CAR_REPAIR("car_repair"), + + /** A health service provider. */ + HEALTH("health"), + + /** An insurance agency. */ + INSURANCE_AGENCY("insurance_agency"), + + /** A painter. */ + PAINTER("painter"), + + /** An archipelago. */ + ARCHIPELAGO("archipelago"), + + /** A museum. */ + MUSEUM("museum"), + + /** A campground. */ + CAMPGROUND("campground"), + + /** An RV park. */ + RV_PARK("rv_park"), + + /** A meal delivery establishment. */ + MEAL_DELIVERY("meal_delivery"), + + /** A primary school. */ + PRIMARY_SCHOOL("primary_school"), + + /** A secondary school. */ + SECONDARY_SCHOOL("secondary_school"), + + /** A town square. */ + TOWN_SQUARE("town_square"), + + /** Tourist Attraction */ + TOURIST_ATTRACTION("tourist_attraction"), + + /** Plus code */ + PLUS_CODE("plus_code"), + + /** DRUGSTORE */ + DRUGSTORE("drugstore"), /** * Indicates an unknown address component type returned by the server. The Java Client for Google * Maps Services should be updated to support the new value. */ - UNKNOWN -} + UNKNOWN("unknown"); + + private final String addressComponentType; + + AddressComponentType(final String addressComponentType) { + this.addressComponentType = addressComponentType; + } + @Override + public String toString() { + return addressComponentType; + } + + public String toCanonicalLiteral() { + return toString(); + } +} diff --git a/src/main/java/com/google/maps/model/AddressType.java b/src/main/java/com/google/maps/model/AddressType.java index 5c46dbe3a..08157bc98 100644 --- a/src/main/java/com/google/maps/model/AddressType.java +++ b/src/main/java/com/google/maps/model/AddressType.java @@ -15,99 +15,93 @@ package com.google.maps.model; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; - import com.google.maps.internal.StringJoin.UrlValue; /** - * The Address types. Please see - * Address - * Types for more detail. - * Some addresses contain additional place categories. Please see - * Places + * The Address types. Please see Address Types and + * Address Component Types for more detail. Some addresses contain additional place categories. + * Please see Place Types for + * more detail. */ public enum AddressType implements UrlValue { - /** - * {@code STREET_ADDRESS} indicates a precise street address. - */ + /** A precise street address. */ STREET_ADDRESS("street_address"), - /** - * {@code ROUTE} indicates a named route (such as "US 101"). - */ + /** A precise street number. */ + STREET_NUMBER("street_number"), + + /** The floor in the address of the building. */ + FLOOR("floor"), + + /** The room in the address of the building */ + ROOM("room"), + + /** A specific mailbox. */ + POST_BOX("post_box"), + + /** A named route (such as "US 101"). */ ROUTE("route"), - /** - * {@code INTERSECTION} indicates a major intersection, usually of two major roads. - */ + /** A major intersection, usually of two major roads. */ INTERSECTION("intersection"), - /** - * {@code POLITICAL} indicates a political entity. Usually, this type indicates a polygon of - * some civil administration. - */ + /** A continent. */ + CONTINENT("continent"), + + /** A political entity. Usually, this type indicates a polygon of some civil administration. */ POLITICAL("political"), - /** - * {@code COUNTRY} indicates the national political entity, and is typically the highest order - * type returned by the Geocoder. - */ + /** The national political entity, typically the highest order type returned by the Geocoder. */ COUNTRY("country"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_1} indicates a first-order civil entity below the country - * level. Within the United States, these administrative levels are states. Not all nations - * exhibit these administrative levels. + * A first-order civil entity below the country level. Within the United States, these + * administrative levels are states. Not all nations exhibit these administrative levels. */ ADMINISTRATIVE_AREA_LEVEL_1("administrative_area_level_1"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_2} indicates a second-order civil entity below the country - * level. Within the United States, these administrative levels are counties. Not all nations - * exhibit these administrative levels. + * A second-order civil entity below the country level. Within the United States, these + * administrative levels are counties. Not all nations exhibit these administrative levels. */ ADMINISTRATIVE_AREA_LEVEL_2("administrative_area_level_2"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_3} indicates a third-order civil entity below the country - * level. This type indicates a minor civil division. Not all nations exhibit these - * administrative levels. + * A third-order civil entity below the country level. This type indicates a minor civil division. + * Not all nations exhibit these administrative levels. */ ADMINISTRATIVE_AREA_LEVEL_3("administrative_area_level_3"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_4} indicates a fourth-order civil entity below the country - * level. This type indicates a minor civil division. Not all nations exhibit these - * administrative levels. + * A fourth-order civil entity below the country level. This type indicates a minor civil + * division. Not all nations exhibit these administrative levels. */ ADMINISTRATIVE_AREA_LEVEL_4("administrative_area_level_4"), /** - * {@code ADMINISTRATIVE_AREA_LEVEL_5} indicates a fifth-order civil entity below the country - * level. This type indicates a minor civil division. Not all nations exhibit these - * administrative levels. + * A fifth-order civil entity below the country level. This type indicates a minor civil division. + * Not all nations exhibit these administrative levels. */ ADMINISTRATIVE_AREA_LEVEL_5("administrative_area_level_5"), - /** - * {@code COLLOQUIAL_AREA} indicates a commonly-used alternative name for the entity. - */ + /** A commonly-used alternative name for the entity. */ COLLOQUIAL_AREA("colloquial_area"), + /** An incorporated city or town political entity. */ + LOCALITY("locality"), + /** - * {@code LOCALITY} indicates an incorporated city or town political entity. + * A specific type of Japanese locality, used to facilitate distinction between multiple locality + * components within a Japanese address. */ - LOCALITY("locality"), + WARD("ward"), /** - * {@code SUBLOCALITY} indicates a first-order civil entity below a locality. For some locations - * may receive one of the additional types: sublocality_level_1 to sublocality_level_5. Each - * sublocality level is a civil entity. Larger numbers indicate a smaller geographic area. + * A first-order civil entity below a locality. Some locations may receive one of the additional + * types: {@code SUBLOCALITY_LEVEL_1} to {@code SUBLOCALITY_LEVEL_5}. Each sublocality level is a + * civil entity. Larger numbers indicate a smaller geographic area. */ SUBLOCALITY("sublocality"), SUBLOCALITY_LEVEL_1("sublocality_level_1"), @@ -116,97 +110,360 @@ public enum AddressType implements UrlValue { SUBLOCALITY_LEVEL_4("sublocality_level_4"), SUBLOCALITY_LEVEL_5("sublocality_level_5"), - /** - * {@code NEIGHBORHOOD} indicates a named neighborhood. - */ + /** A named neighborhood. */ NEIGHBORHOOD("neighborhood"), - /** - * {@code PREMISE} indicates a named location, usually a building or collection of buildings - * with a common name. - */ + /** A named location, usually a building or collection of buildings with a common name. */ PREMISE("premise"), /** - * {@code SUBPREMISE} indicates a first-order entity below a named location, usually a singular - * building within a collection of buildings with a common name + * A first-order entity below a named location, usually a singular building within a collection of + * buildings with a common name. */ SUBPREMISE("subpremise"), - /** - * {@code POSTAL_CODE} indicates a postal code as used to address postal mail within the - * country. - */ + /** A postal code as used to address postal mail within the country. */ POSTAL_CODE("postal_code"), - /** - * {@code NATURAL_FEATURE} indicates a prominent natural feature. - */ + /** A postal code prefix as used to address postal mail within the country. */ + POSTAL_CODE_PREFIX("postal_code_prefix"), + + /** A postal code prefix as used to address postal mail within the country. */ + POSTAL_CODE_SUFFIX("postal_code_suffix"), + + /* Plus code */ + PLUS_CODE("plus_code"), + + /** A prominent natural feature. */ NATURAL_FEATURE("natural_feature"), - /** - * {@code AIRPORT} indicates an airport. - */ + /** An airport. */ AIRPORT("airport"), - - /** - * {@code UNIVERSITY} indicates a university. - */ + /** A university. */ UNIVERSITY("university"), - /** - * {@code PARK} indicates a named park. - */ + /** A named park. */ PARK("park"), + /** A museum. */ + MUSEUM("museum"), + /** - * {@code POINT_OF_INTEREST} indicates a named point of interest. Typically, these "POI"s are - * prominent local entities that don't easily fit in another category, such as "Empire State - * Building" or "Statue of Liberty." + * A named point of interest. Typically, these "POI"s are prominent local entities that don't + * easily fit in another category, such as "Empire State Building" or "Statue of Liberty." */ POINT_OF_INTEREST("point_of_interest"), - /** - * {@code ESTABLISHMENT} typically indicates a place that has not yet been categorized. - */ + /** A place that has not yet been categorized. */ ESTABLISHMENT("establishment"), - /** - * {@code BUS_STATION} indicates the location of a bus stop. - */ + /** The location of a bus stop. */ BUS_STATION("bus_station"), - /** - * {@code TRAIN_STATION} indicates the location of a train station. - */ + /** The location of a train station. */ TRAIN_STATION("train_station"), - /** - * {@code TRANSIT_STATION} indicates the location of a transit station. - */ + /** The location of a subway station. */ + SUBWAY_STATION("subway_station"), + + /** The location of a transit station. */ TRANSIT_STATION("transit_station"), - /** - * {@code CHURCH} indicates the location of a church. - */ + /** The location of a light rail station. */ + LIGHT_RAIL_STATION("light_rail_station"), + + /** The location of a church. */ CHURCH("church"), - /** - * {@code FINANCE} indicates the location of a finance institute. - */ + /** The location of a primary school. */ + PRIMARY_SCHOOL("primary_school"), + + /** The location of a secondary school. */ + SECONDARY_SCHOOL("secondary_school"), + + /** The location of a finance institute. */ FINANCE("finance"), - /** - * {@code POST_OFFICE} indicates the location of a post office. - */ + /** The location of a post office. */ POST_OFFICE("post_office"), - + + /** The location of a place of worship. */ + PLACE_OF_WORSHIP("place_of_worship"), + /** - * {@code PLACE_OF_WORSHIP} indicates the location of a place of worship. + * A grouping of geographic areas, such as locality and sublocality, used for mailing addresses in + * some countries. */ - PLACE_OF_WORSHIP("place_of_worship"), - + POSTAL_TOWN("postal_town"), + + /** Currently not a documented return type. */ + SYNAGOGUE("synagogue"), + + /** Currently not a documented return type. */ + FOOD("food"), + + /** Currently not a documented return type. */ + GROCERY_OR_SUPERMARKET("grocery_or_supermarket"), + + /** Currently not a documented return type. */ + STORE("store"), + + /** The location of a drugstore. */ + DRUGSTORE("drugstore"), + + /** Currently not a documented return type. */ + LAWYER("lawyer"), + + /** Currently not a documented return type. */ + HEALTH("health"), + + /** Currently not a documented return type. */ + INSURANCE_AGENCY("insurance_agency"), + + /** Currently not a documented return type. */ + GAS_STATION("gas_station"), + + /** Currently not a documented return type. */ + CAR_DEALER("car_dealer"), + + /** Currently not a documented return type. */ + CAR_REPAIR("car_repair"), + + /** Currently not a documented return type. */ + MEAL_TAKEAWAY("meal_takeaway"), + + /** Currently not a documented return type. */ + FURNITURE_STORE("furniture_store"), + + /** Currently not a documented return type. */ + HOME_GOODS_STORE("home_goods_store"), + + /** Currently not a documented return type. */ + SHOPPING_MALL("shopping_mall"), + + /** Currently not a documented return type. */ + GYM("gym"), + + /** Currently not a documented return type. */ + ACCOUNTING("accounting"), + + /** Currently not a documented return type. */ + MOVING_COMPANY("moving_company"), + + /** Currently not a documented return type. */ + LODGING("lodging"), + + /** Currently not a documented return type. */ + STORAGE("storage"), + + /** Currently not a documented return type. */ + CASINO("casino"), + + /** Currently not a documented return type. */ + PARKING("parking"), + + /** Currently not a documented return type. */ + STADIUM("stadium"), + + /** Currently not a documented return type. */ + TRAVEL_AGENCY("travel_agency"), + + /** Currently not a documented return type. */ + NIGHT_CLUB("night_club"), + + /** Currently not a documented return type. */ + BEAUTY_SALON("beauty_salon"), + + /** Currently not a documented return type. */ + HAIR_CARE("hair_care"), + + /** Currently not a documented return type. */ + SPA("spa"), + + /** Currently not a documented return type. */ + SHOE_STORE("shoe_store"), + + /** Currently not a documented return type. */ + BAKERY("bakery"), + + /** Currently not a documented return type. */ + PHARMACY("pharmacy"), + + /** Currently not a documented return type. */ + SCHOOL("school"), + + /** Currently not a documented return type. */ + BOOK_STORE("book_store"), + + /** Currently not a documented return type. */ + DEPARTMENT_STORE("department_store"), + + /** Currently not a documented return type. */ + RESTAURANT("restaurant"), + + /** Currently not a documented return type. */ + REAL_ESTATE_AGENCY("real_estate_agency"), + + /** Currently not a documented return type. */ + BAR("bar"), + + /** Currently not a documented return type. */ + DOCTOR("doctor"), + + /** Currently not a documented return type. */ + HOSPITAL("hospital"), + + /** Currently not a documented return type. */ + FIRE_STATION("fire_station"), + + /** Currently not a documented return type. */ + SUPERMARKET("supermarket"), + + /** Currently not a documented return type. */ + CITY_HALL("city_hall"), + + /** Currently not a documented return type. */ + LOCAL_GOVERNMENT_OFFICE("local_government_office"), + + /** Currently not a documented return type. */ + ATM("atm"), + + /** Currently not a documented return type. */ + BANK("bank"), + + /** Currently not a documented return type. */ + LIBRARY("library"), + + /** Currently not a documented return type. */ + CAR_WASH("car_wash"), + + /** Currently not a documented return type. */ + HARDWARE_STORE("hardware_store"), + + /** Currently not a documented return type. */ + AMUSEMENT_PARK("amusement_park"), + + /** Currently not a documented return type. */ + AQUARIUM("aquarium"), + + /** Currently not a documented return type. */ + ART_GALLERY("art_gallery"), + + /** Currently not a documented return type. */ + BICYCLE_STORE("bicycle_store"), + + /** Currently not a documented return type. */ + BOWLING_ALLEY("bowling_alley"), + + /** Currently not a documented return type. */ + CAFE("cafe"), + + /** Currently not a documented return type. */ + CAMPGROUND("campground"), + + /** Currently not a documented return type. */ + CAR_RENTAL("car_rental"), + + /** Currently not a documented return type. */ + CEMETERY("cemetery"), + + /** Currently not a documented return type. */ + CLOTHING_STORE("clothing_store"), + + /** Currently not a documented return type. */ + CONVENIENCE_STORE("convenience_store"), + + /** Currently not a documented return type. */ + COURTHOUSE("courthouse"), + + /** Currently not a documented return type. */ + DENTIST("dentist"), + + /** Currently not a documented return type. */ + ELECTRICIAN("electrician"), + + /** Currently not a documented return type. */ + ELECTRONICS_STORE("electronics_store"), + + /** Currently not a documented return type. */ + EMBASSY("embassy"), + + /** Currently not a documented return type. */ + FLORIST("florist"), + + /** Currently not a documented return type. */ + FUNERAL_HOME("funeral_home"), + + /** Currently not a documented return type. */ + GENERAL_CONTRACTOR("general_contractor"), + + /** Currently not a documented return type. */ + HINDU_TEMPLE("hindu_temple"), + + /** Currently not a documented return type. */ + JEWELRY_STORE("jewelry_store"), + + /** Currently not a documented return type. */ + LAUNDRY("laundry"), + + /** Currently not a documented return type. */ + LIQUOR_STORE("liquor_store"), + + /** Currently not a documented return type. */ + LOCKSMITH("locksmith"), + + /** Currently not a documented return type. */ + MEAL_DELIVERY("meal_delivery"), + + /** Currently not a documented return type. */ + MOSQUE("mosque"), + + /** Currently not a documented return type. */ + MOVIE_RENTAL("movie_rental"), + + /** Currently not a documented return type. */ + MOVIE_THEATER("movie_theater"), + + /** Currently not a documented return type. */ + PAINTER("painter"), + + /** Currently not a documented return type. */ + PET_STORE("pet_store"), + + /** Currently not a documented return type. */ + PHYSIOTHERAPIST("physiotherapist"), + + /** Currently not a documented return type. */ + PLUMBER("plumber"), + + /** Currently not a documented return type. */ + POLICE("police"), + + /** Currently not a documented return type. */ + ROOFING_CONTRACTOR("roofing_contractor"), + + /** Currently not a documented return type. */ + RV_PARK("rv_park"), + + /** Currently not a documented return type. */ + TAXI_STAND("taxi_stand"), + + /** Currently not a documented return type. */ + VETERINARY_CARE("veterinary_care"), + + /** Currently not a documented return type. */ + ZOO("zoo"), + + /** An archipelago. */ + ARCHIPELAGO("archipelago"), + + /** A tourist attraction */ + TOURIST_ATTRACTION("tourist_attraction"), + + /** Currently not a documented return type. */ + TOWN_SQUARE("town_square"), + /** * Indicates an unknown address type returned by the server. The Java Client for Google Maps * Services should be updated to support the new value. @@ -224,6 +481,10 @@ public String toString() { return addressType; } + public String toCanonicalLiteral() { + return toString(); + } + @Override public String toUrlValue() { if (this == UNKNOWN) { @@ -231,6 +492,4 @@ public String toUrlValue() { } return addressType; } - } - diff --git a/src/main/java/com/google/maps/model/AutocompletePrediction.java b/src/main/java/com/google/maps/model/AutocompletePrediction.java new file mode 100644 index 000000000..8e8850b4a --- /dev/null +++ b/src/main/java/com/google/maps/model/AutocompletePrediction.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; + +/** + * Represents a single Autocomplete result returned from the Google Places API Web Service. + * + *

    Please see Query + * Autocomplete Responses for more detail. + */ +public class AutocompletePrediction implements Serializable { + + private static final long serialVersionUID = 1L; + + /** Description of the matched prediction. */ + public String description; + + /** The Place ID of the place. */ + public String placeId; + + /** + * An array indicating the type of the address component. + * + *

    Please see supported + * types for a list of types that can be returned. + */ + public String types[]; + + /** + * An array of terms identifying each section of the returned description. (A section of the + * description is generally terminated with a comma.) Each entry in the array has a value field, + * containing the text of the term, and an offset field, defining the start position of this term + * in the description, measured in Unicode characters. + */ + public Term terms[]; + + /** + * The distance in meters of the place from the {@link + * com.google.maps.PlaceAutocompleteRequest#origin(LatLng)}. Optional. + */ + public Integer distanceMeters; + + /** + * Describes the location of the entered term in the prediction result text, so that the term can + * be highlighted if desired. + */ + public static class MatchedSubstring implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The length of the matched substring, measured in Unicode characters. */ + public int length; + + /** The start position of the matched substring, measured in Unicode characters. */ + public int offset; + + @Override + public String toString() { + return String.format("(offset=%d, length=%d)", offset, length); + } + } + + /** + * The locations of the entered term in the prediction result text, so that the term can be + * highlighted if desired. + */ + public MatchedSubstring matchedSubstrings[]; + + /** A description of how the autocomplete query matched the returned result. */ + public AutocompleteStructuredFormatting structuredFormatting; + + /** + * Identifies each section of the returned description. (A section of the description is generally + * terminated with a comma.) + */ + public static class Term implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The start position of this term in the description, measured in Unicode characters. */ + public int offset; + + /** The text of the matched term. */ + public String value; + + @Override + public String toString() { + return String.format("(offset=%d, value=%s)", offset, value); + } + } + + @Override + public String toString() { + return String.format( + "[AutocompletePrediction: \"%s\", placeId=%s, types=%s, terms=%s, " + + "matchedSubstrings=%s, structuredFormatting=%s]", + description, + placeId, + Arrays.toString(types), + Arrays.toString(terms), + Arrays.toString(matchedSubstrings), + Objects.toString(structuredFormatting)); + } +} diff --git a/src/main/java/com/google/maps/model/AutocompleteStructuredFormatting.java b/src/main/java/com/google/maps/model/AutocompleteStructuredFormatting.java new file mode 100644 index 000000000..c2a3052ad --- /dev/null +++ b/src/main/java/com/google/maps/model/AutocompleteStructuredFormatting.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; +import java.util.Arrays; + +/** The structured formatting info for a {@link com.google.maps.model.AutocompletePrediction}. */ +public class AutocompleteStructuredFormatting implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The main text of a prediction, usually the name of the place. */ + public String mainText; + + /** Where the query matched the returned main text. */ + public AutocompletePrediction.MatchedSubstring mainTextMatchedSubstrings[]; + + /** The secondary text of a prediction, usually the location of the place. */ + public String secondaryText; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("("); + sb.append("\"").append(mainText).append("\""); + sb.append(" at ").append(Arrays.toString(mainTextMatchedSubstrings)); + if (secondaryText != null) { + sb.append(", secondaryText=\"").append(secondaryText).append("\""); + } + sb.append(")"); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/Bounds.java b/src/main/java/com/google/maps/model/Bounds.java index 43cfaa083..b26c036c5 100644 --- a/src/main/java/com/google/maps/model/Bounds.java +++ b/src/main/java/com/google/maps/model/Bounds.java @@ -15,10 +15,19 @@ package com.google.maps.model; -/** - * The north east and south west points that delineate the outer bounds of a map. - */ -public class Bounds { +import java.io.Serializable; + +/** The northeast and southwest points that delineate the outer bounds of a map. */ +public class Bounds implements Serializable { + + private static final long serialVersionUID = 1L; + /** The northeast corner of the bounding box. */ public LatLng northeast; + /** The southwest corner of the bounding box. */ public LatLng southwest; + + @Override + public String toString() { + return String.format("[%s, %s]", northeast, southwest); + } } diff --git a/src/main/java/com/google/maps/model/CellTower.java b/src/main/java/com/google/maps/model/CellTower.java new file mode 100644 index 000000000..2b39905d2 --- /dev/null +++ b/src/main/java/com/google/maps/model/CellTower.java @@ -0,0 +1,152 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** + * A cell tower object. + * + *

    The Geolocation API request body's cellTowers array contains zero or more cell tower objects. + * + *

    Please see Cell + * Tower Object for more detail. + */ +public class CellTower implements Serializable { + + private static final long serialVersionUID = 1L; + + public CellTower() {} + + // constructor only used by the builder class below + private CellTower( + Integer _cellId, + Integer _locationAreaCode, + Integer _mobileCountryCode, + Integer _mobileNetworkCode, + Integer _age, + Integer _signalStrength, + Integer _timingAdvance) { + this.cellId = _cellId; + this.locationAreaCode = _locationAreaCode; + this.mobileCountryCode = _mobileCountryCode; + this.mobileNetworkCode = _mobileNetworkCode; + this.age = _age; + this.signalStrength = _signalStrength; + this.timingAdvance = _timingAdvance; + } + /** + * Unique identifier of the cell (required). On GSM, this is the Cell ID (CID); CDMA networks use + * the Base Station ID (BID). WCDMA networks use the UTRAN/GERAN Cell Identity (UC-Id), which is a + * 32-bit value concatenating the Radio Network Controller (RNC) and Cell ID. Specifying only the + * 16-bit Cell ID value in WCDMA networks may return inaccurate results. + */ + public Integer cellId = null; + /** + * The Location Area Code (LAC) for GSM and WCDMAnetworks or The Network ID (NID) for CDMA + * networks (required). + */ + public Integer locationAreaCode = null; + /** The cell tower's Mobile Country Code (MCC) (required). */ + public Integer mobileCountryCode = null; + /** + * The cell tower's Mobile Network Code (required). This is the MNC for GSM and WCDMA; CDMA uses + * the System ID (SID). + */ + public Integer mobileNetworkCode = null; + /* The following optional fields are not currently used, but may be included if values are available. */ + /** + * The number of milliseconds since this cell was primary. If age is 0, the cellId represents a + * current measurement. + */ + public Integer age = null; + /** Radio signal strength measured in dBm. */ + public Integer signalStrength = null; + /** The timing advance value. */ + public Integer timingAdvance = null; + + @Override + public String toString() { + return String.format( + "[CellTower: cellId=%s, locationAreaCode=%s, mobileCountryCode=%s, " + + "mobileNetworkCode=%s, age=%s, signalStrength=%s, timingAdvance=%s]", + cellId, + locationAreaCode, + mobileCountryCode, + mobileNetworkCode, + age, + signalStrength, + timingAdvance); + } + + public static class CellTowerBuilder { + private Integer _cellId = null; + private Integer _locationAreaCode = null; + private Integer _mobileCountryCode = null; + private Integer _mobileNetworkCode = null; + private Integer _age = null; + private Integer _signalStrength = null; + private Integer _timingAdvance = null; + + // create the actual cell tower + public CellTower createCellTower() { + return new CellTower( + _cellId, + _locationAreaCode, + _mobileCountryCode, + _mobileNetworkCode, + _age, + _signalStrength, + _timingAdvance); + } + + public CellTowerBuilder CellId(int newCellId) { + this._cellId = newCellId; + return this; + } + + public CellTowerBuilder LocationAreaCode(int newLocationAreaCode) { + this._locationAreaCode = newLocationAreaCode; + return this; + } + + public CellTowerBuilder MobileCountryCode(int newMobileCountryCode) { + this._mobileCountryCode = newMobileCountryCode; + return this; + } + + public CellTowerBuilder MobileNetworkCode(int newMobileNetworkCode) { + this._mobileNetworkCode = newMobileNetworkCode; + return this; + } + + public CellTowerBuilder Age(int newAge) { + this._age = newAge; + return this; + } + + public CellTowerBuilder SignalStrength(int newSignalStrength) { + this._signalStrength = newSignalStrength; + return this; + } + + public CellTowerBuilder TimingAdvance(int newTimingAdvance) { + this._timingAdvance = newTimingAdvance; + return this; + } + } +} diff --git a/src/main/java/com/google/maps/model/ComponentFilter.java b/src/main/java/com/google/maps/model/ComponentFilter.java new file mode 100644 index 000000000..0c5cc887a --- /dev/null +++ b/src/main/java/com/google/maps/model/ComponentFilter.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import static com.google.maps.internal.StringJoin.join; + +import com.google.maps.internal.StringJoin; + +/** + * A component filter for a geocode request. In a geocoding response, the Google Geocoding API can + * return address results restricted to a specific area. The restriction is specified using the + * components filter. + * + *

    Please see Component + * Filtering for more detail. + */ +public class ComponentFilter implements StringJoin.UrlValue { + public final String component; + public final String value; + + /** + * Constructs a component filter. + * + * @param component The component to filter. + * @param value The value of the filter. + */ + public ComponentFilter(String component, String value) { + this.component = component; + this.value = value; + } + + @Override + public String toString() { + return toUrlValue(); + } + + @Override + public String toUrlValue() { + return join(':', component, value); + } + + /** + * Matches long or short name of a route. + * + * @param route The name of the route to filter on. + * @return Returns a {@link ComponentFilter}. + */ + public static ComponentFilter route(String route) { + return new ComponentFilter("route", route); + } + + /** + * Matches against both locality and sublocality types. + * + * @param locality The locality to filter on. + * @return Returns a {@link ComponentFilter}. + */ + public static ComponentFilter locality(String locality) { + return new ComponentFilter("locality", locality); + } + + /** + * Matches all the administrative area levels. + * + * @param administrativeArea The administrative area to filter on. + * @return Returns a {@link ComponentFilter}. + */ + public static ComponentFilter administrativeArea(String administrativeArea) { + return new ComponentFilter("administrative_area", administrativeArea); + } + + /** + * Matches postal code or postal code prefix. + * + * @param postalCode The postal code to filter on. + * @return Returns a {@link ComponentFilter}. + */ + public static ComponentFilter postalCode(String postalCode) { + return new ComponentFilter("postal_code", postalCode); + } + + /** + * Matches a country name or a two letter ISO 3166-1 country code. + * + * @param country The country to filter on. + * @return Returns a {@link ComponentFilter}. + */ + public static ComponentFilter country(String country) { + return new ComponentFilter("country", country); + } +} diff --git a/src/main/java/com/google/maps/model/DirectionsLeg.java b/src/main/java/com/google/maps/model/DirectionsLeg.java index 11ef397be..e60978797 100644 --- a/src/main/java/com/google/maps/model/DirectionsLeg.java +++ b/src/main/java/com/google/maps/model/DirectionsLeg.java @@ -15,86 +15,104 @@ package com.google.maps.model; -import org.joda.time.DateTime; +import java.io.Serializable; +import java.time.ZonedDateTime; /** * A component of a Directions API result. * - * See the Legs + *

    See the Legs * documentation for more detail. */ -public class DirectionsLeg { +public class DirectionsLeg implements Serializable { + + private static final long serialVersionUID = 1L; /** - * {@code steps[]} contains an array of steps denoting information about each separate step of the - * leg of the journey. + * Contains an array of steps denoting information about each separate step of this leg of the + * journey. */ public DirectionsStep[] steps; - /** - * {@code distance} indicates the total distance covered by this leg. - */ + /** The total distance covered by this leg. */ public Distance distance; - /** - * {@code duration} indicates the total duration of this leg - */ + /** The total duration of this leg. */ public Duration duration; /** - * {@code durationInTraffic} indicates the total duration of this leg, taking into account current - * traffic conditions. The duration in traffic will only be returned if all of the following are - * true: + * The total duration of this leg, taking into account current traffic conditions. The duration in + * traffic will only be returned if all of the following are true: + * *

      *
    1. The directions request includes a departureTime parameter set to a value within a few - * minutes of the current time.
    2. - *
    3. The request includes a valid Maps for Work client and signature parameter.
    4. - *
    5. Traffic conditions are available for the requested route.
    6. - *
    7. The directions request does not include stopover waypoints.
    8. + * minutes of the current time. + *
    9. The request includes a valid Maps for Work client and signature parameter. + *
    10. Traffic conditions are available for the requested route. + *
    11. The directions request does not include stopover waypoints. *
    */ public Duration durationInTraffic; /** - * {@code arrivalTime} contains the estimated time of arrival for this leg. This property is only - * returned for transit directions. + * The estimated time of arrival for this leg. This property is only returned for transit + * directions. */ - public DateTime arrivalTime; + public ZonedDateTime arrivalTime; /** - * {@code departureTime} contains the estimated time of departure for this leg. The departureTime - * is only available for transit directions. + * The estimated time of departure for this leg. The departureTime is only available for transit + * directions. */ - public DateTime departureTime; - + public ZonedDateTime departureTime; /** - * {@code startLocation} contains the latitude/longitude coordinates of the origin of this leg. - * Because the Directions API calculates directions between locations by using the nearest - * transportation option (usually a road) at the start and end points, startLocation may be - * different than the provided origin of this leg if, for example, a road is not near the origin. + * The latitude/longitude coordinates of the origin of this leg. Because the Directions API + * calculates directions between locations by using the nearest transportation option (usually a + * road) at the start and end points, startLocation may be different from the provided origin of + * this leg if, for example, a road is not near the origin. */ public LatLng startLocation; /** - * {@code endLocation} contains the latitude/longitude coordinates of the given destination of - * this leg. Because the Directions API calculates directions between locations by using the - * nearest transportation option (usually a road) at the start and end points, endLocation may be - * different than the provided destination of this leg if, for example, a road is not near the - * destination. + * The latitude/longitude coordinates of the given destination of this leg. Because the Directions + * API calculates directions between locations by using the nearest transportation option (usually + * a road) at the start and end points, endLocation may be different than the provided destination + * of this leg if, for example, a road is not near the destination. */ public LatLng endLocation; /** - * {@code startAddress} contains the human-readable address (typically a street address) - * reflecting the start location of this leg. + * The human-readable address (typically a street address) reflecting the start location of this + * leg. */ public String startAddress; /** - * {@code endAddress} contains the human-readable address (typically a street address) reflecting - * the end location of this leg. + * The human-readable address (typically a street address) reflecting the end location of this + * leg. */ public String endAddress; + @Override + public String toString() { + StringBuilder sb = + new StringBuilder( + String.format( + "[DirectionsLeg: \"%s\" -> \"%s\" (%s -> %s)", + startAddress, endAddress, startLocation, endLocation)); + if (departureTime != null) { + sb.append(", departureTime=").append(departureTime); + } + if (arrivalTime != null) { + sb.append(", arrivalTime=").append(arrivalTime); + } + if (durationInTraffic != null) { + sb.append(", durationInTraffic=").append(durationInTraffic); + } + sb.append(", duration=").append(duration); + sb.append(", distance=").append(distance); + sb.append(": ").append(steps.length).append(" steps]"); + return sb.toString(); + } } diff --git a/src/main/java/com/google/maps/model/DirectionsResult.java b/src/main/java/com/google/maps/model/DirectionsResult.java new file mode 100644 index 000000000..153f84f99 --- /dev/null +++ b/src/main/java/com/google/maps/model/DirectionsResult.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** + * DirectionsResult represents a result from the Google Directions API Web Service. + * + *

    Please see + * Directions API for more detail. + */ +public class DirectionsResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * Details about the geocoding of origin, destination, and waypoints. See + * Geocoded Waypoints for more detail. + */ + public GeocodedWaypoint geocodedWaypoints[]; + + /** + * Routes from the origin to the destination. See Routes for + * more detail. + */ + public DirectionsRoute routes[]; +} diff --git a/src/main/java/com/google/maps/model/DirectionsRoute.java b/src/main/java/com/google/maps/model/DirectionsRoute.java index 6ac86d8df..7fbada10e 100644 --- a/src/main/java/com/google/maps/model/DirectionsRoute.java +++ b/src/main/java/com/google/maps/model/DirectionsRoute.java @@ -15,63 +15,77 @@ package com.google.maps.model; +import java.io.Serializable; +import java.util.Arrays; + /** * A Directions API result. When the Directions API returns results, it places them within a routes * array. Even if the service returns no results (such as if the origin and/or destination doesn't * exist) it still returns an empty routes array. * - *

    Please see Routes for - * more detail. + *

    Please see + * Routes for more detail. */ -public class DirectionsRoute { +public class DirectionsRoute implements Serializable { + + private static final long serialVersionUID = 1L; /** - * {@code summary} contains a short textual description for the route, suitable for naming and - * disambiguating the route from alternatives. + * A short textual description for the route, suitable for naming and disambiguating the route + * from alternatives. */ public String summary; /** - * {@code legs} contains information about a leg of the route, between two locations within the - * given route. A separate leg will be present for each waypoint or destination specified. (A - * route with no waypoints will contain exactly one leg within the legs array.) + * Information about legs of the route, between locations within the route. A separate leg will be + * present for each waypoint or destination specified. (A route with no waypoints will contain + * exactly one leg within the legs array.) */ public DirectionsLeg[] legs; /** - * {@code waypointOrder} contains an array indicating the order of any waypoints in the - * calculated route. This waypoints may be reordered if the request was passed - * {@code optimize:true} within its {@code waypoints} parameter. + * Indicates the order of any waypoints in the calculated route. This waypoints may be reordered + * if the request was passed {@code optimize:true} within its {@code waypoints} parameter. */ public int[] waypointOrder; - /** - * {@code overviewPolyline} contains an object holding an array of encoded points that represent - * an approximate (smoothed) path of the resulting directions. - */ + /** An approximate (smoothed) path of the resulting directions. */ public EncodedPolyline overviewPolyline; - /** - * {@code bounds} contains the viewport bounding box of the overview_polyline. - */ + /** The viewport bounding box of the overview_polyline. */ public Bounds bounds; /** - * {@code copyrights} contains the copyrights text to be displayed for this route. You must - * handle and display this information yourself. + * Copyrights text to be displayed for this route. You must handle and display this information + * yourself. */ public String copyrights; /** - * {@code fare} contains information about the fare (that is, the ticket costs) on this route. - * This property is only returned for transit directions, and only for routes where fare - * information is available for all transit legs. + * Information about the fare (that is, the ticket costs) on this route. This property is only + * returned for transit directions, and only for routes where fare information is available for + * all transit legs. */ public Fare fare; /** - * {@code warnings} contains an array of warnings to be displayed when showing these directions. - * You must handle and display these warnings yourself. + * Warnings to be displayed when showing these directions. You must handle and display these + * warnings yourself. */ public String[] warnings; + + @Override + public String toString() { + String str = + String.format( + "[DirectionsRoute: \"%s\", %d legs, waypointOrder=%s, bounds=%s", + summary, legs.length, Arrays.toString(waypointOrder), bounds); + if (fare != null) { + str = str + ", fare=" + fare; + } + if (warnings != null && warnings.length > 0) { + str = str + ", " + warnings.length + " warnings"; + } + str = str + "]"; + return str; + } } diff --git a/src/main/java/com/google/maps/model/DirectionsStep.java b/src/main/java/com/google/maps/model/DirectionsStep.java index d35a76b20..40542acbf 100644 --- a/src/main/java/com/google/maps/model/DirectionsStep.java +++ b/src/main/java/com/google/maps/model/DirectionsStep.java @@ -15,75 +15,91 @@ package com.google.maps.model; +import java.io.Serializable; + /** * Each element in the steps of a {@link DirectionsLeg} defines a single step of the calculated * directions. A step is the most atomic unit of a direction's route, containing a single step - * describing a specific, single instruction on the journey. E.g. "Turn left at W. 4th St." - * The step not only describes the instruction but also contains distance and duration information - * relating to how this step relates to the following step. For example, a step denoted as - * "Merge onto I-80 West" may contain a duration of "37 miles" and "40 minutes," indicating - * that the next step is 37 miles/40 minutes from this step. + * describing a specific, single instruction on the journey. E.g. "Turn left at W. 4th St." The step + * not only describes the instruction but also contains distance and duration information relating + * to how this step relates to the following step. For example, a step denoted as "Merge onto I-80 + * West" may contain a duration of "37 miles" and "40 minutes," indicating that the next step is 37 + * miles/40 minutes from this step. * *

    When using the Directions API to search for transit directions, the steps array will include - * additional - * Transit Details in the form of a {@code transitDetails} array. If the directions include - * multiple modes of transportation, detailed directions will be provided for walking or driving - * steps in a {@code steps} array. For example, a walking step will include directions from - * the start and end locations: "Walk to Innes Ave & Fitch St". That step will include detailed - * walking directions for that route in the {@code steps} array, such as: "Head north-west", - * "Turn left onto Arelious Walker", and "Turn left onto Innes Ave". + * additional Transit + * Details in the form of a {@code transitDetails} array. If the directions include multiple + * modes of transportation, detailed directions will be provided for walking or driving steps in a + * {@code steps} array. For example, a walking step will include directions from the start and end + * locations: "Walk to Innes Ave & Fitch St". That step will include detailed walking directions + * for that route in the {@code steps} array, such as: "Head north-west", "Turn left onto Arelious + * Walker", and "Turn left onto Innes Ave". */ -public class DirectionsStep { +public class DirectionsStep implements Serializable { - /** - * {@code htmlInstructions} contains formatted instructions for this step, presented as an - * HTML text string. - */ + private static final long serialVersionUID = 1L; + + /** Formatted instructions for this step, presented as an HTML text string. */ public String htmlInstructions; - /** - * {@code distance} contains the distance covered by this step until the next step. - */ + /** The distance covered by this step until the next step. */ public Distance distance; /** - * {@code duration} contains the typical time required to perform the step, until the next step. + * The maneuver required to move ahead. E.g., turn-left. Please note, this field is undocumented, + * and thus should not be relied upon. */ + @Deprecated public String maneuver; + + /** The typical time required to perform the step, until the next step. */ public Duration duration; - /** - * {@code startLocation} contains the location of the starting point of this step. - */ + /** The location of the starting point of this step. */ public LatLng startLocation; - /** - * {@code endLocation} contains the location of the last point of this step. - */ + /** The location of the last point of this step. */ public LatLng endLocation; /** - * {@code steps} contains detailed directions for walking or driving steps in transit - * directions. Substeps are only available when travelMode is set to "transit". + * Detailed directions for walking or driving steps in transit directions. Substeps are only + * available when travelMode is set to "transit". */ public DirectionsStep[] steps; - /** - * {@code polyline} is the path of this step. - */ + /** The path of this step. */ public EncodedPolyline polyline; /** - * {@code travelMode} is the travel mode of this step. See - * Travel + * The travel mode of this step. See Travel * Modes for more detail. */ public TravelMode travelMode; /** - * {@code transitDetails} contains transit specific information. This field is only returned with - * travel_mode is set to "transit". - * See - * Transit Details for more detail. + * Transit-specific information. This field is only returned when travel_mode is set to "transit". + * See Transit + * Details for more detail. */ public TransitDetails transitDetails; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[DirectionsStep: "); + sb.append("\"").append(htmlInstructions).append("\""); + sb.append(String.format(" (%s -> %s", startLocation, endLocation)).append(")"); + sb.append(" ").append(travelMode); + sb.append(", duration=").append(duration); + sb.append(", distance=").append(distance); + if (steps != null && steps.length > 0) { + sb.append(", ").append(steps.length).append(" substeps"); + } + if (transitDetails != null) { + sb.append(", transitDetails=").append(transitDetails); + } + sb.append("]"); + return sb.toString(); + } } diff --git a/src/main/java/com/google/maps/model/Distance.java b/src/main/java/com/google/maps/model/Distance.java index f42c39b25..c54d68653 100644 --- a/src/main/java/com/google/maps/model/Distance.java +++ b/src/main/java/com/google/maps/model/Distance.java @@ -14,20 +14,23 @@ */ package com.google.maps.model; -/** - * The distance component for Directions API results. - */ -public class Distance { + +import java.io.Serializable; + +/** The distance component for Directions API results. */ +public class Distance implements Serializable { + + private static final long serialVersionUID = 1L; /** - * This is the numeric distance, always in meters. This is intended to be used only in - * algorithmic situations, e.g. sorting results by some user specified metric. + * The numeric distance, in meters. This is intended to be used only in algorithmic situations, + * e.g. sorting results by some user specified metric. */ public long inMeters; /** - * This is the human friendly distance. This is rounded and in an appropriate unit for the - * request. The units can be overriden with a request parameter. + * The human-friendly distance. This is rounded and in an appropriate unit for the request. The + * units can be overridden with a request parameter. */ public String humanReadable; @@ -36,4 +39,3 @@ public String toString() { return humanReadable; } } - diff --git a/src/main/java/com/google/maps/model/DistanceMatrix.java b/src/main/java/com/google/maps/model/DistanceMatrix.java index 107c049ba..b21fa8d01 100644 --- a/src/main/java/com/google/maps/model/DistanceMatrix.java +++ b/src/main/java/com/google/maps/model/DistanceMatrix.java @@ -15,37 +15,47 @@ package com.google.maps.model; +import java.io.Serializable; + /** * A complete result from a Distance Matrix API call. * - * @see Distance Matrix Results + * @see + * Distance Matrix Results */ -public class DistanceMatrix { +public class DistanceMatrix implements Serializable { + + private static final long serialVersionUID = 1L; /** - * {@code originAddresses} contains an array of addresses as returned by the API from your - * original request. These are formatted by the geocoder and localized according to the - * language parameter passed with the request. + * Origin addresses as returned by the API from your original request. These are formatted by the + * geocoder and localized according to the language parameter passed with the request. */ public final String[] originAddresses; /** - * {@code destinationAddresses} contains an array of addresses as returned by the API from your - * original request. As with {@link #originAddresses}, these are localized if appropriate. + * Destination addresses as returned by the API from your original request. As with {@link + * #originAddresses}, these are localized if appropriate. */ public final String[] destinationAddresses; /** - * {@code rows} contains an array of elements, which in turn each contain a status, duration, - * and distance element. + * An array of elements, each of which in turn contains a status, duration, and distance element. */ public final DistanceMatrixRow[] rows; - public DistanceMatrix(String[] originAddresses, String[] destinationAddresses, - DistanceMatrixRow[] rows) { + public DistanceMatrix( + String[] originAddresses, String[] destinationAddresses, DistanceMatrixRow[] rows) { this.originAddresses = originAddresses; this.destinationAddresses = destinationAddresses; this.rows = rows; } + + @Override + public String toString() { + return String.format( + "DistanceMatrix: %d origins x %d destinations, %d rows", + originAddresses.length, destinationAddresses.length, rows.length); + } } diff --git a/src/main/java/com/google/maps/model/DistanceMatrixElement.java b/src/main/java/com/google/maps/model/DistanceMatrixElement.java index f3148524f..a8373226d 100644 --- a/src/main/java/com/google/maps/model/DistanceMatrixElement.java +++ b/src/main/java/com/google/maps/model/DistanceMatrixElement.java @@ -15,34 +15,60 @@ package com.google.maps.model; +import java.io.Serializable; + /** - * A single result corresponding to a origin/destination pair in a Distance Matrix response. + * A single result corresponding to an origin/destination pair in a Distance Matrix response. * *

    Be sure to check the status for each element, as a matrix response can have a mix of * successful and failed elements depending on the connectivity of the origin and destination. */ -public class DistanceMatrixElement { +public class DistanceMatrixElement implements Serializable { + + private static final long serialVersionUID = 1L; /** - * {@code status} indicates the status of the request for this origin/destination pair. + * The status of the request for this origin/destination pair. * - * Will be one of {@link com.google.maps.model.DistanceMatrixElementStatus}. + *

    Will be one of {@link com.google.maps.model.DistanceMatrixElementStatus}. */ public DistanceMatrixElementStatus status; - /** - * {@code duration} indicates the total duration of this leg - */ + /** The total duration of this leg. */ public Duration duration; /** - * {@code distance} indicates the total distance covered by this leg. + * The length of time to travel this route, based on current and historical traffic conditions. + * The duration in traffic will only be returned if all of the following are true: + * + *

      + *
    1. The request includes a departureTime parameter. + *
    2. The request includes a valid API key or a valid Google Maps APIs Premium Plan client ID + * and signature. + *
    3. Traffic conditions are available for the requested route. + *
    4. The mode parameter is set to driving. + *
    */ + public Duration durationInTraffic; + + /** {@code distance} indicates the total distance covered by this leg. */ public Distance distance; - /** - * {@code fare} indicates the contains information about the fare (that is, the ticket costs) on - * this route. - */ + /** {@code fare} contains information about the fare (that is, the ticket costs) on this route. */ public Fare fare; + + @Override + public String toString() { + String str = + String.format( + "[DistanceMatrixElement %s distance=%s, duration=%s", status, distance, duration); + if (durationInTraffic != null) { + str = str + ", durationInTraffic=" + durationInTraffic; + } + if (fare != null) { + str = str + ", fare=" + fare; + } + str = str + "]"; + return str; + } } diff --git a/src/main/java/com/google/maps/model/DistanceMatrixElementStatus.java b/src/main/java/com/google/maps/model/DistanceMatrixElementStatus.java index dfe794a7a..89635e2a5 100644 --- a/src/main/java/com/google/maps/model/DistanceMatrixElementStatus.java +++ b/src/main/java/com/google/maps/model/DistanceMatrixElementStatus.java @@ -18,23 +18,17 @@ /** * The status result for a single {@link com.google.maps.model.DistanceMatrixElement}. * - * @see Documentation on status codes + * @see + * Documentation on status codes */ public enum DistanceMatrixElementStatus { - /** - * {@code OK} indicates the response contains a valid result. - */ + /** Indicates that the response contains a valid result. */ OK, - /** - * {@code NOT_FOUND} indicates that the origin and/or destination of this pairing could not be - * geocoded. - */ + /** Indicates that the origin and/or destination of this pairing could not be geocoded. */ NOT_FOUND, - /** - * {@code ZERO_RESULTS} indicates no route could be found between the origin and destination. - */ + /** Indicates that no route could be found between the origin and destination. */ ZERO_RESULTS } diff --git a/src/main/java/com/google/maps/model/DistanceMatrixRow.java b/src/main/java/com/google/maps/model/DistanceMatrixRow.java index 97a5eafd5..d22209c90 100644 --- a/src/main/java/com/google/maps/model/DistanceMatrixRow.java +++ b/src/main/java/com/google/maps/model/DistanceMatrixRow.java @@ -15,14 +15,21 @@ package com.google.maps.model; +import java.io.Serializable; + /** * Represents a single row in a Distance Matrix API response. A row represents the results for a * single origin. */ -public class DistanceMatrixRow { +public class DistanceMatrixRow implements Serializable { + + private static final long serialVersionUID = 1L; - /** - * {@code elements} contains the results for this row, or individual origin. - */ + /** The results for this row, or individual origin. */ public DistanceMatrixElement[] elements; + + @Override + public String toString() { + return String.format("[DistanceMatrixRow %d elements]", elements.length); + } } diff --git a/src/main/java/com/google/maps/model/Duration.java b/src/main/java/com/google/maps/model/Duration.java index 3678f3b3c..d75cfb09f 100644 --- a/src/main/java/com/google/maps/model/Duration.java +++ b/src/main/java/com/google/maps/model/Duration.java @@ -14,20 +14,21 @@ */ package com.google.maps.model; -/** - * The duration component for Directions API results. - */ -public class Duration { + +import java.io.Serializable; + +/** The duration component for Directions API results. */ +public class Duration implements Serializable { + + private static final long serialVersionUID = 1L; /** - * This is the numeric duration, in seconds. This is intended to be used only in - * algorithmic situations, e.g. sorting results by some user specified metric. + * The numeric duration, in seconds. This is intended to be used only in algorithmic situations, + * e.g. sorting results by some user specified metric. */ public long inSeconds; - /** - * This is the human friendly duration. Use this for display purposes. - */ + /** The human-friendly duration. Use this for display purposes. */ public String humanReadable; @Override @@ -35,4 +36,3 @@ public String toString() { return humanReadable; } } - diff --git a/src/main/java/com/google/maps/model/ElevationResult.java b/src/main/java/com/google/maps/model/ElevationResult.java index 1571c85f9..46e4b8ec2 100644 --- a/src/main/java/com/google/maps/model/ElevationResult.java +++ b/src/main/java/com/google/maps/model/ElevationResult.java @@ -15,11 +15,26 @@ package com.google.maps.model; +import java.io.Serializable; + /** * An Elevation API result. + * + *

    Units are in meters, per https://developers.google.com/maps/documentation/elevation/start. */ -public class ElevationResult { +public class ElevationResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** Elevation in meters. */ public double elevation; + /** Location of the elevation data. */ public LatLng location; + /** Maximum distance between data points from which the elevation was interpolated, in meters. */ public double resolution; + + @Override + public String toString() { + return String.format("(%s, %f m, resolution=%f m)", location, elevation, resolution); + } } diff --git a/src/main/java/com/google/maps/model/EncodedPolyline.java b/src/main/java/com/google/maps/model/EncodedPolyline.java index f10e6e893..07f75ddeb 100644 --- a/src/main/java/com/google/maps/model/EncodedPolyline.java +++ b/src/main/java/com/google/maps/model/EncodedPolyline.java @@ -16,19 +16,25 @@ package com.google.maps.model; import com.google.maps.internal.PolylineEncoding; - +import java.io.Serializable; import java.util.List; /** * Encoded Polylines are used by the API to represent paths. * - *

    See Encoded Polyline Algorithm for more - * detail on the protocol. + *

    See + * Encoded Polyline Algorithm for more detail on the protocol. */ -public class EncodedPolyline { +public class EncodedPolyline implements Serializable { + + private static final long serialVersionUID = 1L; + private final String points; + public EncodedPolyline() { + this.points = null; + } + /** * @param encodedPoints A string representation of a path, encoded with the Polyline Algorithm. */ @@ -36,9 +42,7 @@ public EncodedPolyline(String encodedPoints) { this.points = encodedPoints; } - /** - * @param points A path as a collection of {@code LatLng} points. - */ + /** @param points A path as a collection of {@code LatLng} points. */ public EncodedPolyline(List points) { this.points = PolylineEncoding.encode(points); } @@ -50,4 +54,11 @@ public String getEncodedPath() { public List decodePath() { return PolylineEncoding.decode(points); } + + // Use the encoded point representation; decoding to get an alternate representation for + // individual points would be expensive. + @Override + public String toString() { + return String.format("[EncodedPolyline: %s]", points); + } } diff --git a/src/main/java/com/google/maps/model/Fare.java b/src/main/java/com/google/maps/model/Fare.java index 1c0c7072e..8f2a3338e 100644 --- a/src/main/java/com/google/maps/model/Fare.java +++ b/src/main/java/com/google/maps/model/Fare.java @@ -1,23 +1,42 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package com.google.maps.model; +import java.io.Serializable; import java.math.BigDecimal; import java.util.Currency; /** * A representation of ticket cost for use on public transit. * - * See the Routes - * Documentation for more detail. + *

    See the + * Routes Documentation for more detail. */ -public class Fare { +public class Fare implements Serializable { - /** - * {@code currency} contains the currency indicating the currency that the amount is expressed in. - */ + private static final long serialVersionUID = 1L; + + /** The currency that the amount is expressed in. */ public Currency currency; - /** - * {@code value} contains the total fare amount, in the currency specified in {@link #currency}. - */ + /** The total fare amount, in the currency specified in {@link #currency}. */ public BigDecimal value; + + @Override + public String toString() { + return String.format("%s %s", value, currency); + } } diff --git a/src/main/java/com/google/maps/model/FindPlaceFromText.java b/src/main/java/com/google/maps/model/FindPlaceFromText.java new file mode 100644 index 000000000..075a29bc2 --- /dev/null +++ b/src/main/java/com/google/maps/model/FindPlaceFromText.java @@ -0,0 +1,30 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +public class FindPlaceFromText implements Serializable { + + private static final long serialVersionUID = 1L; + + public PlacesSearchResult candidates[]; + + @Override + public String toString() { + return String.format("[FindPlaceFromText %d candidates]", candidates.length); + } +} diff --git a/src/main/java/com/google/maps/model/GeocodedWaypoint.java b/src/main/java/com/google/maps/model/GeocodedWaypoint.java new file mode 100644 index 000000000..6b523afbb --- /dev/null +++ b/src/main/java/com/google/maps/model/GeocodedWaypoint.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * A point in a Directions API response; either the origin, one of the requested waypoints, or the + * destination. Please see + * Geocoded Waypoints for more detail. + */ +public class GeocodedWaypoint implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The status code resulting from the geocoding operation for this waypoint. */ + public GeocodedWaypointStatus geocoderStatus; + + /** + * Indicates that the geocoder did not return an exact match for the original request, though it + * was able to match part of the requested address. + */ + public boolean partialMatch; + + /** A unique identifier for this waypoint that can be used with other Google APIs. */ + public String placeId; + + /** The address types of the geocoding result used for calculating directions. */ + public AddressType types[]; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[GeocodedWaypoint"); + sb.append(" ").append(geocoderStatus); + if (partialMatch) { + sb.append(" ").append("PARTIAL MATCH"); + } + sb.append(" placeId=").append(placeId); + sb.append(", types=").append(Arrays.toString(types)); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/GeocodedWaypointStatus.java b/src/main/java/com/google/maps/model/GeocodedWaypointStatus.java new file mode 100644 index 000000000..75a954373 --- /dev/null +++ b/src/main/java/com/google/maps/model/GeocodedWaypointStatus.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +/** + * The status result for a single {@link com.google.maps.model.GeocodedWaypoint}. + * + * @see + * Documentation on status codes + */ +public enum GeocodedWaypointStatus { + /** Indicates the response contains a valid result. */ + OK, + + /** Indicates no route could be found between the origin and destination. */ + ZERO_RESULTS +} diff --git a/src/main/java/com/google/maps/model/GeocodingResult.java b/src/main/java/com/google/maps/model/GeocodingResult.java index b6f15b311..e096ba217 100644 --- a/src/main/java/com/google/maps/model/GeocodingResult.java +++ b/src/main/java/com/google/maps/model/GeocodingResult.java @@ -15,50 +15,51 @@ package com.google.maps.model; -/** - * Result from a Geocoding API call. - */ -public class GeocodingResult { +import java.io.Serializable; +import java.util.Arrays; - /** - * {@code addressComponents} is an array containing the separate address components. - */ +/** A result from a Geocoding API call. */ +public class GeocodingResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The separate address components in this result. */ public AddressComponent[] addressComponents; /** - * {@code formattedAddress} is the human-readable address of this location. Often this address is - * equivalent to the "postal address," which sometimes differs from country to country. (Note - * that some countries, such as the United Kingdom, do not allow distribution of true postal - * addresses due to licensing restrictions.) This address is generally composed of one or more - * address components. For example, the address "111 8th Avenue, New York, NY" contains separate - * address components for "111" (the street number, "8th Avenue" (the route), "New York" (the - * city) and "NY" (the US state). These address components contain additional information. + * The human-readable address of this location. + * + *

    Often this address is equivalent to the "postal address," which sometimes differs from + * country to country. (Note that some countries, such as the United Kingdom, do not allow + * distribution of true postal addresses due to licensing restrictions.) This address is generally + * composed of one or more address components. For example, the address "111 8th Avenue, New York, + * NY" contains separate address components for "111" (the street number, "8th Avenue" (the + * route), "New York" (the city) and "NY" (the US state). These address components contain + * additional information. */ public String formattedAddress; /** - * {@code postcodeLocalities} is an array denoting all the localities contained in a postal code. - * This is only present when the result is a postal code that contains multiple localities. + * All the localities contained in a postal code. This is only present when the result is a postal + * code that contains multiple localities. */ public String[] postcodeLocalities; - /** - * {@code geometry} contains location information. - */ + /** Location information for this result. */ public Geometry geometry; /** - * The {@code types} array indicates the type of the returned result. This array contains a set - * of zero or more tags identifying the type of feature returned in the result. For example, a - * geocode of "Chicago" returns "locality" which indicates that "Chicago" is a city, and also - * returns "political" which indicates it is a political entity. + * The types of the returned result. This array contains a set of zero or more tags identifying + * the type of feature returned in the result. For example, a geocode of "Chicago" returns + * "locality" which indicates that "Chicago" is a city, and also returns "political" which + * indicates it is a political entity. */ public AddressType[] types; /** - * {@code partialMatch} indicates that the geocoder did not return an exact match for the - * original request, though it was able to match part of the requested address. You may wish to - * examine the original request for misspellings and/or an incomplete address. + * Indicates that the geocoder did not return an exact match for the original request, though it + * was able to match part of the requested address. You may wish to examine the original request + * for misspellings and/or an incomplete address. * *

    Partial matches most often occur for street addresses that do not exist within the locality * you pass in the request. Partial matches may also be returned when a request matches two or @@ -69,8 +70,27 @@ public class GeocodingResult { */ public boolean partialMatch; - /** - * {@code placeId} is a unique identifier for a place. - */ + /** A unique identifier for this place. */ public String placeId; + + /** The Plus Code identifier for this place. */ + public PlusCode plusCode; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[GeocodingResult"); + if (partialMatch) { + sb.append(" PARTIAL MATCH"); + } + sb.append(" placeId=").append(placeId); + sb.append(" ").append(geometry); + sb.append(", formattedAddress=").append(formattedAddress); + sb.append(", types=").append(Arrays.toString(types)); + sb.append(", addressComponents=").append(Arrays.toString(addressComponents)); + if (postcodeLocalities != null && postcodeLocalities.length > 0) { + sb.append(", postcodeLocalities=").append(Arrays.toString(postcodeLocalities)); + } + sb.append("]"); + return sb.toString(); + } } diff --git a/src/main/java/com/google/maps/model/GeolocationPayload.java b/src/main/java/com/google/maps/model/GeolocationPayload.java new file mode 100644 index 000000000..640169816 --- /dev/null +++ b/src/main/java/com/google/maps/model/GeolocationPayload.java @@ -0,0 +1,188 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import static com.google.maps.internal.StringJoin.join; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Request body. + * + *

    Please see Geolocation + * Requests and Request Body + * for more detail. + * + *

    The following fields are supported, and all fields are optional: + */ +public class GeolocationPayload implements Serializable { + + private static final long serialVersionUID = 1L; + + public GeolocationPayload() {} + + // constructor only used by the builder class below + private GeolocationPayload( + Integer _homeMobileCountryCode, + Integer _homeMobileNetworkCode, + String _radioType, + String _carrier, + Boolean _considerIp, + CellTower[] _cellTowers, + WifiAccessPoint[] _wifiAccessPoints) { + homeMobileCountryCode = _homeMobileCountryCode; + homeMobileNetworkCode = _homeMobileNetworkCode; + radioType = _radioType; + carrier = _carrier; + considerIp = _considerIp; + cellTowers = _cellTowers; + wifiAccessPoints = _wifiAccessPoints; + } + /** The mobile country code (MCC) for the device's home network. */ + public Integer homeMobileCountryCode = null; + /** The mobile network code (MNC) for the device's home network. */ + public Integer homeMobileNetworkCode = null; + /** + * The mobile radio type. Supported values are {@code "lte"}, {@code "gsm"}, {@code "cdma"}, and + * {@code "wcdma"}. While this field is optional, it should be included if a value is available, + * for more accurate results. + */ + public String radioType = null; + /** The carrier name. */ + public String carrier = null; + /** + * Specifies whether to fall back to IP geolocation if wifi and cell tower signals are not + * available. Note that the IP address in the request header may not be the IP of the device. + * Defaults to true. Set considerIp to false to disable fall back. + */ + public Boolean considerIp = null; + /** An array of cell tower objects. See {@link com.google.maps.model.CellTower}. */ + public CellTower[] cellTowers; + /** An array of WiFi access point objects. See {@link com.google.maps.model.WifiAccessPoint}. */ + public WifiAccessPoint[] wifiAccessPoints; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[GeolocationPayload"); + List elements = new ArrayList<>(); + if (homeMobileCountryCode != null) { + elements.add("homeMobileCountryCode=" + homeMobileCountryCode); + } + if (homeMobileNetworkCode != null) { + elements.add("homeMobileNetworkCode=" + homeMobileNetworkCode); + } + if (radioType != null) { + elements.add("radioType=" + radioType); + } + if (carrier != null) { + elements.add("carrier=" + carrier); + } + elements.add("considerIp=" + considerIp); + if (cellTowers != null && cellTowers.length > 0) { + elements.add("cellTowers=" + Arrays.toString(cellTowers)); + } + if (wifiAccessPoints != null && wifiAccessPoints.length > 0) { + elements.add("wifiAccessPoints=" + Arrays.toString(wifiAccessPoints)); + } + sb.append(join(", ", elements)); + sb.append("]"); + return sb.toString(); + } + + public static class GeolocationPayloadBuilder { + private Integer _homeMobileCountryCode = null; + private Integer _homeMobileNetworkCode = null; + private String _radioType = null; + private String _carrier = null; + private Boolean _considerIp = null; + private CellTower[] _cellTowers = null; + private List _addedCellTowers = new ArrayList<>(); + private WifiAccessPoint[] _wifiAccessPoints = null; + private List _addedWifiAccessPoints = new ArrayList<>(); + + public GeolocationPayload createGeolocationPayload() { + // if wifi access points have been added individually... + if (!_addedWifiAccessPoints.isEmpty()) { + // ...use them as our list of access points by converting the list to an array + _wifiAccessPoints = _addedWifiAccessPoints.toArray(new WifiAccessPoint[0]); + } // otherwise we will simply use the array set outright + + // same logic as above for cell towers + if (!_addedCellTowers.isEmpty()) { + _cellTowers = _addedCellTowers.toArray(new CellTower[0]); + } + + return new GeolocationPayload( + _homeMobileCountryCode, + _homeMobileNetworkCode, + _radioType, + _carrier, + _considerIp, + _cellTowers, + _wifiAccessPoints); + } + + public GeolocationPayloadBuilder HomeMobileCountryCode(int newHomeMobileCountryCode) { + this._homeMobileCountryCode = newHomeMobileCountryCode; + return this; + } + + public GeolocationPayloadBuilder HomeMobileNetworkCode(int newHomeMobileNetworkCode) { + this._homeMobileNetworkCode = newHomeMobileNetworkCode; + return this; + } + + public GeolocationPayloadBuilder RadioType(String newRadioType) { + this._radioType = newRadioType; + return this; + } + + public GeolocationPayloadBuilder Carrier(String newCarrier) { + this._carrier = newCarrier; + return this; + } + + public GeolocationPayloadBuilder ConsiderIp(boolean newConsiderIp) { + this._considerIp = newConsiderIp; + return this; + } + + public GeolocationPayloadBuilder CellTowers(CellTower[] newCellTowers) { + this._cellTowers = newCellTowers; + return this; + } + + public GeolocationPayloadBuilder AddCellTower(CellTower newCellTower) { + this._addedCellTowers.add(newCellTower); + return this; + } + + public GeolocationPayloadBuilder WifiAccessPoints(WifiAccessPoint[] newWifiAccessPoints) { + this._wifiAccessPoints = newWifiAccessPoints; + return this; + } + + public GeolocationPayloadBuilder AddWifiAccessPoint(WifiAccessPoint newWifiAccessPoint) { + this._addedWifiAccessPoints.add(newWifiAccessPoint); + return this; + } + } +} diff --git a/src/main/java/com/google/maps/model/GeolocationResult.java b/src/main/java/com/google/maps/model/GeolocationResult.java new file mode 100644 index 000000000..45e7e097d --- /dev/null +++ b/src/main/java/com/google/maps/model/GeolocationResult.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** + * A Geolocation API result. + * + *

    A successful geolocation request will return a result defining a location and radius. + * + *

    Please see Geolocation + * responses for more detail. + */ +public class GeolocationResult implements Serializable { + + private static final long serialVersionUID = 1L; + /** The user’s estimated latitude and longitude. */ + public LatLng location; + /** + * The accuracy of the estimated location, in meters. This represents the radius of a circle + * around the returned {@code location}. + */ + public double accuracy; + + @Override + public String toString() { + return String.format("%s, accuracy=%s m", location, accuracy); + } +} diff --git a/src/main/java/com/google/maps/model/Geometry.java b/src/main/java/com/google/maps/model/Geometry.java index db9a43711..87b8ba972 100644 --- a/src/main/java/com/google/maps/model/Geometry.java +++ b/src/main/java/com/google/maps/model/Geometry.java @@ -15,32 +15,38 @@ package com.google.maps.model; -/** - * The Geometry of a Geocoding Result. - */ -public class Geometry { +import java.io.Serializable; + +/** The geometry of a Geocoding result. */ +public class Geometry implements Serializable { + + private static final long serialVersionUID = 1L; /** - * {@code bounds} (optionally returned) stores the bounding box which can fully contain the - * returned result. Note that these bounds may not match the recommended viewport. (For example, - * San Francisco includes the Farallon islands, which are technically part of the city, but - * probably should not be returned in the viewport.) + * The bounding box which can fully contain the returned result (optionally returned). Note that + * these bounds may not match the recommended viewport. (For example, San Francisco includes the + * Farallon islands, which are technically part of the city, but probably should not be returned + * in the viewport.) */ public Bounds bounds; /** - * {@code location} contains the geocoded {@code latitude,longitude} value. For normal address - * lookups, this field is typically the most important. + * The geocoded latitude/longitude value. For normal address lookups, this field is typically the + * most important. */ public LatLng location; - /** - * The level of certainty of this geocoding result. - */ + /** The level of certainty of this geocoding result. */ public LocationType locationType; /** - * {@code viewport} contains the recommended viewport for displaying the returned result. - * Generally the viewport is used to frame a result when displaying it to a user. + * The recommended viewport for displaying the returned result. Generally the viewport is used to + * frame a result when displaying it to a user. */ public Bounds viewport; + + @Override + public String toString() { + return String.format( + "[Geometry: %s (%s) bounds=%s, viewport=%s]", location, locationType, bounds, viewport); + } } diff --git a/src/main/java/com/google/maps/model/LatLng.java b/src/main/java/com/google/maps/model/LatLng.java index 64126690a..6d0febd1d 100644 --- a/src/main/java/com/google/maps/model/LatLng.java +++ b/src/main/java/com/google/maps/model/LatLng.java @@ -16,32 +16,35 @@ package com.google.maps.model; import com.google.maps.internal.StringJoin.UrlValue; - +import java.io.Serializable; import java.util.Locale; +import java.util.Objects; -/** - * A place on Earth, represented by a Latitude/Longitude pair. - */ -public class LatLng implements UrlValue { +/** A place on Earth, represented by a latitude/longitude pair. */ +public class LatLng implements UrlValue, Serializable { - /** - * The latitude of this location. - */ + private static final long serialVersionUID = 1L; + + /** The latitude of this location. */ public double lat; - /** - * The longitude of this location. - */ + /** The longitude of this location. */ public double lng; /** - * Construct a location with a latitude longitude pair. + * Constructs a location with a latitude/longitude pair. + * + * @param lat The latitude of this location. + * @param lng The longitude of this location. */ public LatLng(double lat, double lng) { this.lat = lat; this.lng = lng; } + /** Serialisation constructor. */ + public LatLng() {} + @Override public String toString() { return toUrlValue(); @@ -50,6 +53,19 @@ public String toString() { @Override public String toUrlValue() { // Enforce Locale to English for double to string conversion - return String.format(Locale.ENGLISH, "%f,%f", lat, lng); + return String.format(Locale.ENGLISH, "%.8f,%.8f", lat, lng); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LatLng latLng = (LatLng) o; + return Double.compare(latLng.lat, lat) == 0 && Double.compare(latLng.lng, lng) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(lat, lng); } } diff --git a/src/main/java/com/google/maps/model/LocationType.java b/src/main/java/com/google/maps/model/LocationType.java index c68731d0c..529bf6f72 100644 --- a/src/main/java/com/google/maps/model/LocationType.java +++ b/src/main/java/com/google/maps/model/LocationType.java @@ -18,34 +18,31 @@ import com.google.maps.internal.StringJoin.UrlValue; /** - * Location types for a reverse geocoding request. Please see - * for - * more detail. + * Location types for a reverse geocoding request. Please see Reverse + * Geocoding for more detail. */ public enum LocationType implements UrlValue { /** - * {@code ROOFTOP} restricts the results to addresses for which we have location information - * accurate down to street address precision. + * Restricts the results to addresses for which we have location information accurate down to + * street address precision. */ ROOFTOP, /** - * {@code RANGE_INTERPOLATED} restricts the results to those that reflect an approximation - * (usually on a road) interpolated between two precise points (such as intersections). An - * interpolated range generally indicates that rooftop geocodes are unavailable for a street - * address. + * Restricts the results to those that reflect an approximation (usually on a road) interpolated + * between two precise points (such as intersections). An interpolated range generally indicates + * that rooftop geocodes are unavailable for a street address. */ RANGE_INTERPOLATED, /** - * {@code GEOMETRIC_CENTER} restricts the results to geometric centers of a location such as a - * polyline (for example, a street) or polygon (region). + * Restricts the results to geometric centers of a location such as a polyline (for example, a + * street) or polygon (region). */ GEOMETRIC_CENTER, - /** - * {@code APPROXIMATE} restricts the results to those that are characterized as approximate. - */ + /** Restricts the results to those that are characterized as approximate. */ APPROXIMATE, /** diff --git a/src/main/java/com/google/maps/model/OpeningHours.java b/src/main/java/com/google/maps/model/OpeningHours.java new file mode 100644 index 000000000..c8ae0df3c --- /dev/null +++ b/src/main/java/com/google/maps/model/OpeningHours.java @@ -0,0 +1,124 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; +import java.time.LocalTime; +import java.util.Arrays; + +/** + * Opening hours for a Place Details result. Please see Place Details + * Results for more details. + */ +public class OpeningHours implements Serializable { + + private static final long serialVersionUID = 1L; + /** + * Whether the place is open at the current time. + * + *

    Note: this field will be null if it isn't present in the response. + */ + public Boolean openNow; + + /** The opening hours for a Place for a single day. */ + public static class Period implements Serializable { + + private static final long serialVersionUID = 1L; + + public static class OpenClose implements Serializable { + + private static final long serialVersionUID = 1L; + + public enum DayOfWeek { + SUNDAY("Sunday"), + MONDAY("Monday"), + TUESDAY("Tuesday"), + WEDNESDAY("Wednesday"), + THURSDAY("Thursday"), + FRIDAY("Friday"), + SATURDAY("Saturday"), + + /** + * Indicates an unknown day of week type returned by the server. The Java Client for Google + * Maps Services should be updated to support the new value. + */ + UNKNOWN("Unknown"); + + private DayOfWeek(String name) { + this.name = name; + } + + private final String name; + + public String getName() { + return name; + } + } + + /** Day that this Open/Close pair is for. */ + public Period.OpenClose.DayOfWeek day; + + /** Time that this Open or Close happens at. */ + public LocalTime time; + + @Override + public String toString() { + return String.format("%s %s", day, time); + } + } + + /** When the Place opens. */ + public Period.OpenClose open; + + /** When the Place closes. */ + public Period.OpenClose close; + + @Override + public String toString() { + return String.format("%s - %s", open, close); + } + } + + /** Opening periods covering seven days, starting from Sunday, in chronological order. */ + public Period[] periods; + + /** + * The formatted opening hours for each day of the week, as an array of seven strings; for + * example, {@code "Monday: 8:30 am – 5:30 pm"}. + */ + public String[] weekdayText; + + /** + * Indicates that the place has permanently shut down. + * + *

    Note: this field will be null if it isn't present in the response. + */ + public Boolean permanentlyClosed; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[OpeningHours:"); + if (permanentlyClosed != null && permanentlyClosed) { + sb.append(" permanentlyClosed"); + } + if (openNow != null && openNow) { + sb.append(" openNow"); + } + sb.append(" ").append(Arrays.toString(periods)); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/Photo.java b/src/main/java/com/google/maps/model/Photo.java new file mode 100644 index 000000000..ae16a5cb2 --- /dev/null +++ b/src/main/java/com/google/maps/model/Photo.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** + * Describes a photo available with a Search Result. + * + *

    Please see Place Photos + * for more details. + */ +public class Photo implements Serializable { + + private static final long serialVersionUID = 1L; + /** Used to identify the photo when you perform a Photo request. */ + public String photoReference; + + /** The maximum height of the image. */ + public int height; + + /** The maximum width of the image. */ + public int width; + + /** Attributions about this listing which must be displayed to the user. */ + public String[] htmlAttributions; + + @Override + public String toString() { + String str = String.format("[Photo %s (%d x %d)", photoReference, width, height); + if (htmlAttributions != null && htmlAttributions.length > 0) { + str = str + " " + htmlAttributions.length + " attributions"; + } + str = str + "]"; + return str; + } +} diff --git a/src/main/java/com/google/maps/model/PlaceAutocompleteType.java b/src/main/java/com/google/maps/model/PlaceAutocompleteType.java new file mode 100644 index 000000000..3e10c3471 --- /dev/null +++ b/src/main/java/com/google/maps/model/PlaceAutocompleteType.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import com.google.maps.internal.StringJoin; + +/** + * Used by the Places API to restrict the autocomplete results to places matching the specified + * type. + */ +public enum PlaceAutocompleteType implements StringJoin.UrlValue { + GEOCODE("geocode"), + ADDRESS("address"), + ESTABLISHMENT("establishment"), + REGIONS("(regions)"), + CITIES("(cities)"); + + PlaceAutocompleteType(final String placeType) { + this.placeType = placeType; + } + + private final String placeType; + + @Override + public String toUrlValue() { + return placeType; + } + + @Override + public String toString() { + return placeType; + } +} diff --git a/src/main/java/com/google/maps/model/PlaceDetails.java b/src/main/java/com/google/maps/model/PlaceDetails.java new file mode 100644 index 000000000..6ea1da91e --- /dev/null +++ b/src/main/java/com/google/maps/model/PlaceDetails.java @@ -0,0 +1,293 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; +import java.net.URL; +import java.time.Instant; +import java.util.Arrays; + +/** + * The result of a Place Details request. A Place Details request returns more comprehensive + * information about the indicated place such as its complete address, phone number, user rating, + * and reviews. + * + *

    See + * Place Details Results for more detail. + */ +public class PlaceDetails implements Serializable { + + private static final long serialVersionUID = 1L; + + /** A list of separate address components that comprise the address of this place. */ + public AddressComponent[] addressComponents; + + /** A representation of the place's address in the adr microformat. */ + public String adrAddress; + + /** The human-readable address of this place. */ + public String formattedAddress; + + /** The place's phone number in its local format. */ + public String formattedPhoneNumber; + + /** The location of the Place. */ + public Geometry geometry; + + /** + * The URL of a suggested icon which may be displayed to the user when indicating this result on a + * map. + */ + public URL icon; + + /** + * The place's phone number in international format. International format includes the country + * code, and is prefixed with the plus (+) sign. + */ + public String internationalPhoneNumber; + + /** The human-readable name for the returned result. */ + public String name; + + /** The opening hours for the place. */ + public OpeningHours openingHours; + + /** A list of photos associated with this place, each containing a reference to an image. */ + public Photo[] photos; + + /** A textual identifier that uniquely identifies this place. */ + public String placeId; + + /** The scope of the placeId. */ + @Deprecated public PlaceIdScope scope; + + /** The Plus Code location identifier for this place. */ + public PlusCode plusCode; + + /** Whether the place has permanently closed. */ + public boolean permanentlyClosed; + + /** The number of user reviews for this place */ + public int userRatingsTotal; + + @Deprecated + public static class AlternatePlaceIds implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * The alternative placeId. The most likely reason for a place to have an alternative place ID + * is if your application adds a place and receives an application-scoped place ID, then later + * receives a Google-scoped place ID after passing the moderation process. + */ + public String placeId; + + /** + * The scope of an alternative place ID will always be APP, indicating that the alternative + * place ID is recognised by your application only. + */ + public PlaceIdScope scope; + + @Override + public String toString() { + return String.format("%s (%s)", placeId, scope); + } + } + + /** + * An optional array of alternative place IDs for the place, with a scope related to each + * alternative ID. + */ + public AlternatePlaceIds[] altIds; + + /** + * The price level of the place. The exact amount indicated by a specific value will vary from + * region to region. + */ + public PriceLevel priceLevel; + + /** The place's rating, from 1.0 to 5.0, based on aggregated user reviews. */ + public float rating; + + public static class Review implements Serializable { + + private static final long serialVersionUID = 1L; + + public static class AspectRating implements Serializable { + + private static final long serialVersionUID = 1L; + + public enum RatingType { + APPEAL, + ATMOSPHERE, + DECOR, + FACILITIES, + FOOD, + OVERALL, + QUALITY, + SERVICE, + + /** + * Indicates an unknown rating type returned by the server. The Java Client for Google Maps + * Services should be updated to support the new value. + */ + UNKNOWN + } + + /** The name of the aspect that is being rated. */ + public RatingType type; + + /** The user's rating for this particular aspect, from 0 to 3. */ + public int rating; + } + + /** + * A list of AspectRating objects, each of which provides a rating of a single attribute of the + * establishment. + * + *

    Note: this is a Premium Data + * field available to the Google Places API for Work customers. + */ + public AspectRating[] aspects; + + /** + * The name of the user who submitted the review. Anonymous reviews are attributed to "A Google + * user". + */ + public String authorName; + + /** The URL of the user's Google+ profile, if available. */ + public URL authorUrl; + + /** An IETF language code indicating the language used in the user's review. */ + public String language; + + /** The URL of the user's Google+ profile photo, if available. */ + public String profilePhotoUrl; + + /** The user's overall rating for this place. This is a whole number, ranging from 1 to 5. */ + public int rating; + + /** The relative time that the review was submitted. */ + public String relativeTimeDescription; + + /** + * The user's review. When reviewing a location with Google Places, text reviews are considered + * optional. + */ + public String text; + + /** The time that the review was submitted. */ + public Instant time; + } + + /** + * An array of up to five reviews. If a language parameter was specified in the Place Details + * request, the Places Service will bias the results to prefer reviews written in that language. + */ + public Review[] reviews; + + /** Feature types describing the given result. */ + public AddressType[] types; + + /** + * The URL of the official Google page for this place. This will be the establishment's Google+ + * page if the Google+ page exists, otherwise it will be the Google-owned page that contains the + * best available information about the place. Applications must link to or embed this page on any + * screen that shows detailed results about the place to the user. + */ + public URL url; + + /** The number of minutes this place’s current timezone is offset from UTC. */ + public int utcOffset; + + /** + * A simplified address for the place, including the street name, street number, and locality, but + * not the province/state, postal code, or country. + */ + public String vicinity; + + /** The authoritative website for this place, such as a business's homepage. */ + public URL website; + + /** Attributions about this listing which must be displayed to the user. */ + public String[] htmlAttributions; + + /** The status of the business (i.e. operational, temporarily closed, etc.). */ + public String businessStatus; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[PlaceDetails: "); + sb.append("\"").append(name).append("\""); + sb.append(" ").append(placeId).append(" (").append(scope).append(")"); + sb.append(" address=\"").append(formattedAddress).append("\""); + sb.append(" geometry=").append(geometry); + if (vicinity != null) { + sb.append(", vicinity=").append(vicinity); + } + if (types != null && types.length > 0) { + sb.append(", types=").append(Arrays.toString(types)); + } + if (altIds != null && altIds.length > 0) { + sb.append(", altIds=").append(Arrays.toString(altIds)); + } + if (formattedPhoneNumber != null) { + sb.append(", phone=").append(formattedPhoneNumber); + } + if (internationalPhoneNumber != null) { + sb.append(", internationalPhoneNumber=").append(internationalPhoneNumber); + } + if (url != null) { + sb.append(", url=").append(url); + } + if (website != null) { + sb.append(", website=").append(website); + } + if (icon != null) { + sb.append(", icon"); + } + if (openingHours != null) { + sb.append(", openingHours"); + sb.append(", utcOffset=").append(utcOffset); + } + if (priceLevel != null) { + sb.append(", priceLevel=").append(priceLevel); + } + sb.append(", rating=").append(rating); + if (permanentlyClosed) { + sb.append(", permanentlyClosed"); + } + if (userRatingsTotal > 0) { + sb.append(", userRatingsTotal=").append(userRatingsTotal); + } + if (photos != null && photos.length > 0) { + sb.append(", ").append(photos.length).append(" photos"); + } + if (reviews != null && reviews.length > 0) { + sb.append(", ").append(reviews.length).append(" reviews"); + } + if (htmlAttributions != null && htmlAttributions.length > 0) { + sb.append(", ").append(htmlAttributions.length).append(" htmlAttributions"); + } + if (businessStatus != null) { + sb.append(", businessStatus=").append(businessStatus); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/PlaceIdScope.java b/src/main/java/com/google/maps/model/PlaceIdScope.java new file mode 100644 index 000000000..3dfbeab9a --- /dev/null +++ b/src/main/java/com/google/maps/model/PlaceIdScope.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +/** The scope of a Place ID returned from the Google Places API Web Service. */ +@Deprecated +public enum PlaceIdScope { + /** + * Indicates the place ID is recognised by your application only. This is because your application + * added the place, and the place has not yet passed the moderation process. + */ + APP, + /** Indicates the place ID is available to other applications and on Google Maps. */ + GOOGLE +} diff --git a/src/main/java/com/google/maps/model/PlaceType.java b/src/main/java/com/google/maps/model/PlaceType.java new file mode 100644 index 000000000..222015747 --- /dev/null +++ b/src/main/java/com/google/maps/model/PlaceType.java @@ -0,0 +1,147 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import com.google.maps.internal.StringJoin; + +/** Used by the Places API to restrict the results to places matching the specified type. */ +public enum PlaceType implements StringJoin.UrlValue { + ACCOUNTING("accounting"), + AIRPORT("airport"), + AMUSEMENT_PARK("amusement_park"), + AQUARIUM("aquarium"), + ART_GALLERY("art_gallery"), + ATM("atm"), + BAKERY("bakery"), + BANK("bank"), + BAR("bar"), + BEAUTY_SALON("beauty_salon"), + BICYCLE_STORE("bicycle_store"), + BOOK_STORE("book_store"), + BOWLING_ALLEY("bowling_alley"), + BUS_STATION("bus_station"), + CAFE("cafe"), + CAMPGROUND("campground"), + CAR_DEALER("car_dealer"), + CAR_RENTAL("car_rental"), + CAR_REPAIR("car_repair"), + CAR_WASH("car_wash"), + CASINO("casino"), + CEMETERY("cemetery"), + CHURCH("church"), + CITY_HALL("city_hall"), + CLOTHING_STORE("clothing_store"), + CONVENIENCE_STORE("convenience_store"), + COURTHOUSE("courthouse"), + DENTIST("dentist"), + DEPARTMENT_STORE("department_store"), + DOCTOR("doctor"), + DRUGSTORE("drugstore"), + ELECTRICIAN("electrician"), + ELECTRONICS_STORE("electronics_store"), + EMBASSY("embassy"), + @Deprecated + ESTABLISHMENT("establishment"), + @Deprecated + FINANCE("finance"), + FIRE_STATION("fire_station"), + FLORIST("florist"), + @Deprecated + FOOD("food"), + FUNERAL_HOME("funeral_home"), + FURNITURE_STORE("furniture_store"), + GAS_STATION("gas_station"), + @Deprecated + GENERAL_CONTRACTOR("general_contractor"), + GROCERY_OR_SUPERMARKET("grocery_or_supermarket"), + GYM("gym"), + HAIR_CARE("hair_care"), + HARDWARE_STORE("hardware_store"), + @Deprecated + HEALTH("health"), + HINDU_TEMPLE("hindu_temple"), + HOME_GOODS_STORE("home_goods_store"), + HOSPITAL("hospital"), + INSURANCE_AGENCY("insurance_agency"), + JEWELRY_STORE("jewelry_store"), + LAUNDRY("laundry"), + LAWYER("lawyer"), + LIBRARY("library"), + LIGHT_RAIL_STATION("light_rail_station"), + LIQUOR_STORE("liquor_store"), + LOCAL_GOVERNMENT_OFFICE("local_government_office"), + LOCKSMITH("locksmith"), + LODGING("lodging"), + MEAL_DELIVERY("meal_delivery"), + MEAL_TAKEAWAY("meal_takeaway"), + MOSQUE("mosque"), + MOVIE_RENTAL("movie_rental"), + MOVIE_THEATER("movie_theater"), + MOVING_COMPANY("moving_company"), + MUSEUM("museum"), + NIGHT_CLUB("night_club"), + PAINTER("painter"), + PARK("park"), + PARKING("parking"), + PET_STORE("pet_store"), + PHARMACY("pharmacy"), + PHYSIOTHERAPIST("physiotherapist"), + @Deprecated + PLACE_OF_WORSHIP("place_of_worship"), + PLUMBER("plumber"), + POLICE("police"), + POST_OFFICE("post_office"), + PRIMARY_SCHOOL("primary_school"), + REAL_ESTATE_AGENCY("real_estate_agency"), + RESTAURANT("restaurant"), + ROOFING_CONTRACTOR("roofing_contractor"), + RV_PARK("rv_park"), + SCHOOL("school"), + SECONDARY_SCHOOL("secondary_school"), + SHOE_STORE("shoe_store"), + SHOPPING_MALL("shopping_mall"), + SPA("spa"), + STADIUM("stadium"), + STORAGE("storage"), + STORE("store"), + SUBWAY_STATION("subway_station"), + SUPERMARKET("supermarket"), + SYNAGOGUE("synagogue"), + TAXI_STAND("taxi_stand"), + TOURIST_ATTRACTION("tourist_attraction"), + TRAIN_STATION("train_station"), + TRANSIT_STATION("transit_station"), + TRAVEL_AGENCY("travel_agency"), + UNIVERSITY("university"), + VETERINARY_CARE("veterinary_care"), + ZOO("zoo"); + + PlaceType(final String placeType) { + this.placeType = placeType; + } + + private final String placeType; + + @Override + public String toUrlValue() { + return placeType; + } + + @Override + public String toString() { + return placeType; + } +} diff --git a/src/main/java/com/google/maps/model/PlacesSearchResponse.java b/src/main/java/com/google/maps/model/PlacesSearchResponse.java new file mode 100644 index 000000000..ccf58ae06 --- /dev/null +++ b/src/main/java/com/google/maps/model/PlacesSearchResponse.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** + * The response from a Places Search request. + * + *

    Please see Places Search + * Response for more detail. + */ +public class PlacesSearchResponse implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The list of Search Results. */ + public PlacesSearchResult results[]; + + /** Attributions about this listing which must be displayed to the user. */ + public String htmlAttributions[]; + + /** + * A token that can be used to request up to 20 additional results. This field will be null if + * there are no further results. The maximum number of results that can be returned is 60. + * + *

    Note: There is a short delay between when this response is issued, and when nextPageToken + * will become valid to execute. + */ + public String nextPageToken; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[PlacesSearchResponse: "); + sb.append(results.length).append(" results"); + if (nextPageToken != null) { + sb.append(", nextPageToken=").append(nextPageToken); + } + if (htmlAttributions != null && htmlAttributions.length > 0) { + sb.append(", ").append(htmlAttributions.length).append(" htmlAttributions"); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/PlacesSearchResult.java b/src/main/java/com/google/maps/model/PlacesSearchResult.java new file mode 100644 index 000000000..6efbbf64b --- /dev/null +++ b/src/main/java/com/google/maps/model/PlacesSearchResult.java @@ -0,0 +1,118 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; +import java.net.URL; +import java.util.Arrays; + +/** + * A single result in the search results returned from the Google Places API Web Service. + * + *

    Please see Place Search + * Results for more detail. + */ +public class PlacesSearchResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The human-readable address of this place. */ + public String formattedAddress; + + /** + * Geometry information about the result, generally including the location (geocode) of the place + * and (optionally) the viewport identifying its general area of coverage. + */ + public Geometry geometry; + + /** + * The human-readable name for the returned result. For establishment results, this is usually the + * business name. + */ + public String name; + + /** + * The URL of a recommended icon which may be displayed to the user when indicating this result. + */ + public URL icon; + + /** A textual identifier that uniquely identifies a place. */ + public String placeId; + + /** The scope of the placeId. */ + @Deprecated public PlaceIdScope scope; + + /** The place's rating, from 1.0 to 5.0, based on aggregated user reviews. */ + public float rating; + + /** Feature types describing the given result. */ + public String types[]; + + /** Information on when the place is open. */ + public OpeningHours openingHours; + + /** Photo objects associated with this place, each containing a reference to an image. */ + public Photo photos[]; + + /** A feature name of a nearby location. */ + public String vicinity; + + /** Indicates that the place has permanently shut down. */ + public boolean permanentlyClosed; + + /** The number of user reviews for this place */ + public int userRatingsTotal; + + /** The status of the business (i.e. operational, temporarily closed, etc.). */ + public String businessStatus; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[PlacesSearchResult: "); + sb.append("\"").append(name).append("\""); + sb.append(", \"").append(formattedAddress).append("\""); + sb.append(", geometry=").append(geometry); + sb.append(", placeId=").append(placeId); + if (vicinity != null) { + sb.append(", vicinity=").append(vicinity); + } + if (types != null && types.length > 0) { + sb.append(", types=").append(Arrays.toString(types)); + } + sb.append(", rating=").append(rating); + if (icon != null) { + sb.append(", icon"); + } + if (openingHours != null) { + sb.append(", openingHours"); + } + if (photos != null && photos.length > 0) { + sb.append(", ").append(photos.length).append(" photos"); + } + if (permanentlyClosed) { + sb.append(", permanentlyClosed"); + } + if (userRatingsTotal > 0) { + sb.append(", userRatingsTotal=").append(userRatingsTotal); + } + if (businessStatus != null) { + sb.append(", businessStatus=").append(businessStatus); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/PlusCode.java b/src/main/java/com/google/maps/model/PlusCode.java new file mode 100644 index 000000000..bc129c01a --- /dev/null +++ b/src/main/java/com/google/maps/model/PlusCode.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** A Plus Code encoded location reference. */ +public class PlusCode implements Serializable { + + private static final long serialVersionUID = 1L; + + /** The global Plus Code identifier. */ + public String globalCode; + + /** The compound Plus Code identifier. May be null for locations in remote areas. */ + public String compoundCode; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[PlusCode: "); + sb.append(globalCode); + if (compoundCode != null) { + sb.append(", compoundCode=").append(compoundCode); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/PriceLevel.java b/src/main/java/com/google/maps/model/PriceLevel.java new file mode 100644 index 000000000..1dc3571aa --- /dev/null +++ b/src/main/java/com/google/maps/model/PriceLevel.java @@ -0,0 +1,52 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import com.google.maps.internal.StringJoin; + +/** Used by Places API to restrict search results to those within a given price range. */ +public enum PriceLevel implements StringJoin.UrlValue { + FREE("0"), + INEXPENSIVE("1"), + MODERATE("2"), + EXPENSIVE("3"), + VERY_EXPENSIVE("4"), + + /** + * Indicates an unknown price level type returned by the server. The Java Client for Google Maps + * Services should be updated to support the new value. + */ + UNKNOWN("Unknown"); + + private final String priceLevel; + + PriceLevel(final String priceLevel) { + this.priceLevel = priceLevel; + } + + @Override + public String toString() { + return priceLevel; + } + + @Override + public String toUrlValue() { + if (this == UNKNOWN) { + throw new UnsupportedOperationException("Shouldn't use PriceLevel.UNKNOWN in a request."); + } + return priceLevel; + } +} diff --git a/src/main/java/com/google/maps/model/RankBy.java b/src/main/java/com/google/maps/model/RankBy.java new file mode 100644 index 000000000..7b618e0af --- /dev/null +++ b/src/main/java/com/google/maps/model/RankBy.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import com.google.maps.internal.StringJoin; + +/** Used by the Places API to specify the order in which results are listed. */ +public enum RankBy implements StringJoin.UrlValue { + PROMINENCE("prominence"), + DISTANCE("distance"); + + private final String ranking; + + RankBy(String ranking) { + this.ranking = ranking; + } + + @Override + public String toString() { + return ranking; + } + + @Override + public String toUrlValue() { + return ranking; + } +} diff --git a/src/main/java/com/google/maps/model/Size.java b/src/main/java/com/google/maps/model/Size.java new file mode 100644 index 000000000..fd3376334 --- /dev/null +++ b/src/main/java/com/google/maps/model/Size.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import com.google.maps.internal.StringJoin; +import java.io.Serializable; + +public class Size implements StringJoin.UrlValue, Serializable { + private static final long serialVersionUID = 1L; + + /** The width of this Size. */ + public int width; + + /** The height of this Size. */ + public int height; + + /** + * Constructs a Size with a height/width pair. + * + * @param height The height of this Size. + * @param width The width of this Size. + */ + public Size(int width, int height) { + this.width = width; + this.height = height; + } + + /** Serialization constructor. */ + public Size() {} + + @Override + public String toString() { + return toUrlValue(); + } + + @Override + public String toUrlValue() { + return String.format("%dx%d", width, height); + } +} diff --git a/src/main/java/com/google/maps/model/SnappedPoint.java b/src/main/java/com/google/maps/model/SnappedPoint.java index 1927d1cdf..b07b30186 100644 --- a/src/main/java/com/google/maps/model/SnappedPoint.java +++ b/src/main/java/com/google/maps/model/SnappedPoint.java @@ -15,32 +15,36 @@ package com.google.maps.model; -/** - * A point that has been snapped to a road by the Roads API. - */ -public class SnappedPoint { - /** - * {@code location} contains a latitude and longitude value representing the snapped location. - */ +import java.io.Serializable; + +/** A point that has been snapped to a road by the Roads API. */ +public class SnappedPoint implements Serializable { + + private static final long serialVersionUID = 1L; + /** A latitude/longitude value representing the snapped location. */ public LatLng location; /** - * {@code originalIndex} is an integer that indicates the corresponding value in the original - * request. Each value in the request should map to a snapped value in the response. However, - * if you've set interpolate=true, then it's possible that the response will contain more - * coordinates than the request. Interpolated values will not have an originalIndex. These - * values are indexed from 0, so a point with an originalIndex of 4 will be the snapped value - * of the 5th lat/lng passed to the path parameter. + * The index of the corresponding value in the original request. Each value in the request should + * map to a snapped value in the response. However, if you've set interpolate=true, then it's + * possible that the response will contain more coordinates than the request. Interpolated values + * will not have an originalIndex. These values are indexed from 0, so a point with an + * originalIndex of 4 will be the snapped value of the 5th lat/lng passed to the path parameter. * - *

    A point that was not on the original path, or when interpolate=false will have an + *

    A point that was not on the original path, or when interpolate=false, will have an * originalIndex of -1. */ public int originalIndex = -1; /** - * {@code placeId} is a unique identifier for a place. All placeIds returned by the Roads API - * will correspond to road segments. The placeId can be passed to the speedLimit method - * to determine the speed limit along that road segment. + * A unique identifier for a place. All placeIds returned by the Roads API will correspond to road + * segments. The placeId can be passed to the speedLimit method to determine the speed limit along + * that road segment. */ public String placeId; + + @Override + public String toString() { + return String.format("[%s, placeId=%s, originalIndex=%s]", location, placeId, originalIndex); + } } diff --git a/src/main/java/com/google/maps/model/SnappedSpeedLimitResponse.java b/src/main/java/com/google/maps/model/SnappedSpeedLimitResponse.java index adc51d714..d82dc00ad 100644 --- a/src/main/java/com/google/maps/model/SnappedSpeedLimitResponse.java +++ b/src/main/java/com/google/maps/model/SnappedSpeedLimitResponse.java @@ -1,13 +1,43 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package com.google.maps.model; -/** - * A combined snap-to-roads and speed limit response. - */ -public class SnappedSpeedLimitResponse { +import java.io.Serializable; + +/** A combined snap-to-roads and speed limit response. */ +public class SnappedSpeedLimitResponse implements Serializable { + + private static final long serialVersionUID = 1L; /** Speed limit results. */ public SpeedLimit[] speedLimits; /** Snap-to-road results. */ public SnappedPoint[] snappedPoints; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[SnappedSpeedLimitResponse:"); + if (speedLimits != null && speedLimits.length > 0) { + sb.append(" ").append(speedLimits.length).append(" speedLimits"); + } + if (snappedPoints != null && snappedPoints.length > 0) { + sb.append(" ").append(snappedPoints.length).append(" speedLimits"); + } + sb.append("]"); + return sb.toString(); + } } diff --git a/src/main/java/com/google/maps/model/SpeedLimit.java b/src/main/java/com/google/maps/model/SpeedLimit.java index 7ec1641bd..1e7ab1721 100644 --- a/src/main/java/com/google/maps/model/SpeedLimit.java +++ b/src/main/java/com/google/maps/model/SpeedLimit.java @@ -15,27 +15,32 @@ package com.google.maps.model; -/** - * A speed limit result from the Roads API. - */ -public class SpeedLimit { +import java.io.Serializable; + +/** A speed limit result from the Roads API. */ +public class SpeedLimit implements Serializable { + + private static final long serialVersionUID = 1L; /** - * {@code placeId} is a unique identifier for a place. All placeIds returned by the Roads API - * will correspond to road segments. + * A unique identifier for a place. All placeIds returned by the Roads API will correspond to road + * segments. */ public String placeId; /** - * {@code speedLimit} is the speed limit for that road segment, specified in kilometers per hour. + * The speed limit for that road segment, specified in kilometers per hour. * *

    To obtain the speed in miles per hour, use {@link #speedLimitMph()}. */ - public long speedLimit; + public double speedLimit; - /** - * Returns the speed limit in miles per hour (MPH). - */ + /** @return Returns the speed limit in miles per hour (MPH). */ public long speedLimitMph() { return Math.round(speedLimit * 0.621371); } + + @Override + public String toString() { + return String.format("[%.0f km/h, placeId=%s]", speedLimit, placeId); + } } diff --git a/src/main/java/com/google/maps/model/StopDetails.java b/src/main/java/com/google/maps/model/StopDetails.java index 861efa0bd..8a17f9755 100644 --- a/src/main/java/com/google/maps/model/StopDetails.java +++ b/src/main/java/com/google/maps/model/StopDetails.java @@ -15,21 +15,27 @@ package com.google.maps.model; +import java.io.Serializable; + /** * The stop/station. * - *

    See - * Transit details for more detail. + *

    See Transit + * details for more detail. */ -public class StopDetails { +public class StopDetails implements Serializable { + + private static final long serialVersionUID = 1L; - /** - * The name of the transit station/stop. eg. "Union Square". - */ + /** The name of the transit station/stop. E.g. {@code "Union Square"}. */ public String name; - /** - * The location of the transit station/stop, represented as a lat and lng field. - */ + /** The location of the transit station/stop. */ public LatLng location; + + @Override + public String toString() { + return String.format("%s (%s)", name, location); + } } diff --git a/src/test/java/com/google/maps/KeyOnlyAuthenticatedTest.java b/src/main/java/com/google/maps/model/TrafficModel.java similarity index 53% rename from src/test/java/com/google/maps/KeyOnlyAuthenticatedTest.java rename to src/main/java/com/google/maps/model/TrafficModel.java index 396fa2274..b5547689d 100644 --- a/src/test/java/com/google/maps/KeyOnlyAuthenticatedTest.java +++ b/src/main/java/com/google/maps/model/TrafficModel.java @@ -13,26 +13,24 @@ * permissions and limitations under the License. */ -package com.google.maps; +package com.google.maps.model; -import org.junit.Ignore; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; +import com.google.maps.internal.StringJoin.UrlValue; +import java.util.Locale; -import java.util.Collection; +/** Specifies traffic prediction model when requesting future directions. */ +public enum TrafficModel implements UrlValue { + BEST_GUESS, + OPTIMISTIC, + PESSIMISTIC; -/** - * A specific authenticated test that will never attempt to use client ID and secret credentials - * to run. - */ -@RunWith(Parameterized.class) @Ignore -public class KeyOnlyAuthenticatedTest extends AuthenticatedTest { - protected KeyOnlyAuthenticatedTest() { + @Override + public String toString() { + return name().toLowerCase(Locale.ENGLISH); } - @Parameters - public static Collection contexts() { - return contexts(false); + @Override + public String toUrlValue() { + return toString(); } } diff --git a/src/main/java/com/google/maps/model/TransitAgency.java b/src/main/java/com/google/maps/model/TransitAgency.java index de260f7a2..283720a62 100644 --- a/src/main/java/com/google/maps/model/TransitAgency.java +++ b/src/main/java/com/google/maps/model/TransitAgency.java @@ -15,26 +15,39 @@ package com.google.maps.model; +import java.io.Serializable; + /** * The operator of a line. * - *

    See - * Transit details for more detail. + *

    See Transit + * Details for more detail. */ -public class TransitAgency { +public class TransitAgency implements Serializable { + + private static final long serialVersionUID = 1L; - /** - * {@code name} contains the name of the transit agency. - */ + /** The name of the transit agency. */ public String name; - /** - * {@code url} contains the URL for the transit agency. - */ + /** The URL for the transit agency. */ public String url; - /** - * {@code phone} contains the phone number of the transit agency. - */ + /** The phone number of the transit agency. */ public String phone; -} \ No newline at end of file + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[TransitAgency: "); + sb.append(name); + if (url != null) { + sb.append(", url=").append(url); + } + if (phone != null) { + sb.append(", phone=").append(phone); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/com/google/maps/model/TransitDetails.java b/src/main/java/com/google/maps/model/TransitDetails.java index a3e652e81..ff510f3aa 100644 --- a/src/main/java/com/google/maps/model/TransitDetails.java +++ b/src/main/java/com/google/maps/model/TransitDetails.java @@ -15,61 +15,69 @@ package com.google.maps.model; -import org.joda.time.DateTime; +import java.io.Serializable; +import java.time.ZonedDateTime; /** * Transit directions return additional information that is not relevant for other modes of - * transportation. These additional properties are exposed through the {@code transit_details} - * object, returned as a field of an element in the {@code steps} array. From the - * {@code TransitDetails} object you can access additional information about the transit stop, - * transit line and transit agency. + * transportation. These additional properties are exposed through the {@code TransitDetails} + * object, returned as a field of an element in the {@code steps} array. From the {@code + * TransitDetails} object you can access additional information about the transit stop, transit + * line, and transit agency. */ -public class TransitDetails { +public class TransitDetails implements Serializable { - /** - * {@code arrivalStop} contains information about the arrival stop/station for this part of the - * trip. - */ + private static final long serialVersionUID = 1L; + + /** Information about the arrival stop/station for this part of the trip. */ public StopDetails arrivalStop; - /** - * {@code departureStop} contains information about the departure stop/station for this part of the - * trip. - */ + /** Information about the departure stop/station for this part of the trip. */ public StopDetails departureStop; - /** - * {@code arrivalTime} contains the arrival time for this leg of the journey. - */ - public DateTime arrivalTime; + /** The arrival time for this leg of the journey. */ + public ZonedDateTime arrivalTime; - /** - * {@code departureTime} contains the departure time for this leg of the journey. - */ - public DateTime departureTime; + /** The departure time for this leg of the journey. */ + public ZonedDateTime departureTime; /** - * {@code headsign} specifies the direction in which to travel on this line, as it is marked on - * the vehicle or at the departure stop. This will often be the terminus station. + * The direction in which to travel on this line, as it is marked on the vehicle or at the + * departure stop. This will often be the terminus station. */ public String headsign; /** - * {@code headway} specifies the expected number of seconds between departures from the same stop - * at this time. For example, with a headway value of 600, you would expect a ten minute wait if - * you should miss your bus. + * The expected number of seconds between departures from the same stop at this time. For example, + * with a headway value of 600, you would expect a ten minute wait if you should miss your bus. */ public long headway; /** - * {@code numStops} contains the number of stops in this step, counting the arrival stop, but not - * the departure stop. For example, if your directions involve leaving from Stop A, passing - * through stops B and C, and arriving at stop D, {@code numStops} will return 3. + * The number of stops in this step, counting the arrival stop, but not the departure stop. For + * example, if your directions involve leaving from Stop A, passing through stops B and C, and + * arriving at stop D, {@code numStops} will equal 3. */ public int numStops; - /** - * {@code line} contains information about the transit line used in this step. - */ + /** Information about the transit line used in this step. */ public TransitLine line; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("["); + sb.append(departureStop).append(" at ").append(departureTime); + sb.append(" -> "); + sb.append(arrivalStop).append(" at ").append(arrivalTime); + if (headsign != null) { + sb.append(" (").append(headsign).append(" )"); + } + if (line != null) { + sb.append(" on ").append(line); + } + sb.append(", ").append(numStops).append(" stops"); + sb.append(", headway=").append(headway).append(" s"); + sb.append("]"); + return sb.toString(); + } } diff --git a/src/main/java/com/google/maps/model/TransitLine.java b/src/main/java/com/google/maps/model/TransitLine.java index 35d5448b2..becd05012 100644 --- a/src/main/java/com/google/maps/model/TransitLine.java +++ b/src/main/java/com/google/maps/model/TransitLine.java @@ -15,55 +15,54 @@ package com.google.maps.model; +import java.io.Serializable; + /** * The transit line used in a step. * - *

    See - * Transit details for more detail. + *

    See Transit + * Details for more detail. */ -public class TransitLine { +public class TransitLine implements Serializable { - /** - * {@code name} contains the full name of this transit line. eg. "7 Avenue Express". - */ + private static final long serialVersionUID = 1L; + + /** The full name of this transit line. E.g. {@code "7 Avenue Express"}. */ public String name; /** - * {@code shortName} contains the short name of this transit line. This will normally be a line - * number, such as "M7" or "355". + * The short name of this transit line. This will normally be a line number, such as {@code "M7"} + * or {@code "355"}. */ public String shortName; /** - * {@code color} contains the color commonly used in signage for this transit line. The color will - * be specified as a hex string such as: #FF0033. + * The color commonly used in signage for this transit line. The color will be specified as a hex + * string, such as {@code "#FF0033"}. */ public String color; - /** - * {@code agencies} contains an array of TransitAgency objects that each provide information about - * the operator of the line. - */ + /** Information about the operator(s) of this transit line. */ public TransitAgency[] agencies; - /** - * {@code url} contains the URL for this transit line as provided by the transit agency. - */ + /** The URL for this transit line as provided by the transit agency. */ public String url; - /** - * {@code icon} contains the URL for the icon associated with this line. - */ + /** The URL for the icon associated with this transit line. */ public String icon; /** - * {@code textColor} contains the color of text commonly used for signage of this line. The color - * will be specified as a hex string. + * The color of text commonly used for signage of this transit line. The color will be specified + * as a hex string, such as {@code "#FF0033"}. */ public String textColor; - /** - * {@code vehicle} contains the type of vehicle used on this line. - */ + /** The type of vehicle used on this transit line. */ public Vehicle vehicle; + + @Override + public String toString() { + return String.format("%s \"%s\"", shortName, name); + } } diff --git a/src/main/java/com/google/maps/model/TransitMode.java b/src/main/java/com/google/maps/model/TransitMode.java index 0e1ac60d5..d2f072f45 100644 --- a/src/main/java/com/google/maps/model/TransitMode.java +++ b/src/main/java/com/google/maps/model/TransitMode.java @@ -1,18 +1,31 @@ +/* + * Copyright 2015 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + package com.google.maps.model; import com.google.maps.internal.StringJoin.UrlValue; - import java.util.Locale; -/** - * You may specify transit mode when requesting transit directions or distances. - */ +/** You may specify transit mode when requesting transit directions or distances. */ public enum TransitMode implements UrlValue { - BUS, SUBWAY, TRAIN, TRAM, + BUS, + SUBWAY, + TRAIN, + TRAM, - /** - * Indicates preferred travel by train, tram, light rail and subway. - */ + /** Indicates preferred travel by train, tram, light rail and subway. */ RAIL; @Override diff --git a/src/main/java/com/google/maps/model/TransitRoutingPreference.java b/src/main/java/com/google/maps/model/TransitRoutingPreference.java index 6ac2492bd..f1ea85985 100644 --- a/src/main/java/com/google/maps/model/TransitRoutingPreference.java +++ b/src/main/java/com/google/maps/model/TransitRoutingPreference.java @@ -1,14 +1,12 @@ package com.google.maps.model; import com.google.maps.internal.StringJoin.UrlValue; - import java.util.Locale; -/** - * Indicate user preference when requesting transit directions. - */ +/** Indicates user preference when requesting transit directions. */ public enum TransitRoutingPreference implements UrlValue { - LESS_WALKING, FEWER_TRANSFERS; + LESS_WALKING, + FEWER_TRANSFERS; @Override public String toString() { @@ -18,4 +16,5 @@ public String toString() { @Override public String toUrlValue() { return name().toLowerCase(Locale.ENGLISH); - }} + } +} diff --git a/src/main/java/com/google/maps/model/TravelMode.java b/src/main/java/com/google/maps/model/TravelMode.java index a7b7e72ab..f8ebdba69 100644 --- a/src/main/java/com/google/maps/model/TravelMode.java +++ b/src/main/java/com/google/maps/model/TravelMode.java @@ -16,20 +16,22 @@ package com.google.maps.model; import com.google.maps.internal.StringJoin.UrlValue; - import java.util.Locale; /** * You may specify the transportation mode to use for calulating directions. Directions are * calculating as driving directions by default. * - * @see Directions API travel modes - * @see Distance Matrix API travel modes + * @see + * Directions API travel modes + * @see Distance + * Matrix API Intro */ public enum TravelMode implements UrlValue { - DRIVING, WALKING, BICYCLING, TRANSIT, + DRIVING, + WALKING, + BICYCLING, + TRANSIT, /** * Indicates an unknown travel mode returned by the server. The Java Client for Google Maps @@ -49,4 +51,4 @@ public String toUrlValue() { } return name().toLowerCase(Locale.ENGLISH); } -} \ No newline at end of file +} diff --git a/src/main/java/com/google/maps/model/Unit.java b/src/main/java/com/google/maps/model/Unit.java index 518d5081f..fff60311d 100644 --- a/src/main/java/com/google/maps/model/Unit.java +++ b/src/main/java/com/google/maps/model/Unit.java @@ -16,14 +16,12 @@ package com.google.maps.model; import com.google.maps.internal.StringJoin.UrlValue; - import java.util.Locale; -/** - * Units of measurement. - */ +/** Units of measurement. */ public enum Unit implements UrlValue { - METRIC, IMPERIAL; + METRIC, + IMPERIAL; @Override public String toString() { diff --git a/src/main/java/com/google/maps/model/Vehicle.java b/src/main/java/com/google/maps/model/Vehicle.java index f12d84c05..59c616e64 100644 --- a/src/main/java/com/google/maps/model/Vehicle.java +++ b/src/main/java/com/google/maps/model/Vehicle.java @@ -15,28 +15,36 @@ package com.google.maps.model; +import java.io.Serializable; + /** * The vehicle used on a line. - *

    - *

    See - * Transit details for more detail. + * + *

    See Transit + * details for more detail. */ -public class Vehicle { +public class Vehicle implements Serializable { - /** - * {@code name} contains the name of the vehicle on this line. eg. "Subway." - */ + private static final long serialVersionUID = 1L; + + /** The name of the vehicle on this line. E.g. {@code "Subway"}. */ public String name; /** - * {@code type} contains the type of vehicle that runs on this line. See the - * {@link com.google.maps.model.VehicleType VehicleType} documentation for a complete list of - * supported values. + * The type of vehicle that runs on this line. See the {@link com.google.maps.model.VehicleType + * VehicleType} documentation for a complete list of supported values. */ public VehicleType type; - /** - * {@code icon} contains the URL for an icon associated with this vehicle type. - */ + /** The URL for an icon associated with this vehicle type. */ public String icon; -} \ No newline at end of file + + /** The URL for an icon based on the local transport signage. */ + public String localIcon; + + @Override + public String toString() { + return String.format("%s (%s)", name, type); + } +} diff --git a/src/main/java/com/google/maps/model/VehicleType.java b/src/main/java/com/google/maps/model/VehicleType.java index 48f0482ca..3bfa81351 100644 --- a/src/main/java/com/google/maps/model/VehicleType.java +++ b/src/main/java/com/google/maps/model/VehicleType.java @@ -18,64 +18,45 @@ /** * The vehicle types. * - *

    See + *

    See * Vehicle Type for more detail. */ public enum VehicleType { - /** - * Rail. - */ + /** Rail. */ RAIL, - /** - * Light rail transit. - */ + /** Light rail transit. */ METRO_RAIL, - /** - * Underground light rail. - */ + /** Underground light rail. */ SUBWAY, - /** - * Above ground light rail. - */ + /** Above ground light rail. */ TRAM, - /** - * Monorail. - */ + /** Monorail. */ MONORAIL, - /** - * Heavy rail. - */ + /** Heavy rail. */ HEAVY_RAIL, - /** - * Commuter rail. - */ + /** Commuter rail. */ COMMUTER_TRAIN, - /** - * High speed train. - */ + /** High speed train. */ HIGH_SPEED_TRAIN, - /** - * Bus. - */ + /** Long distance train. */ + LONG_DISTANCE_TRAIN, + + /** Bus. */ BUS, - /** - * Intercity bus. - */ + /** Intercity bus. */ INTERCITY_BUS, - /** - * Trolleybus. - */ + /** Trolleybus. */ TROLLEYBUS, /** @@ -84,9 +65,7 @@ public enum VehicleType { */ SHARE_TAXI, - /** - * Ferry. - */ + /** Ferry. */ FERRY, /** @@ -95,9 +74,7 @@ public enum VehicleType { */ CABLE_CAR, - /** - * An aerial cable car. - */ + /** An aerial cable car. */ GONDOLA_LIFT, /** @@ -105,9 +82,7 @@ public enum VehicleType { * cars, with each car acting as a counterweight for the other. */ FUNICULAR, - - /** - * All other vehicles will return this type. - */ + + /** All other vehicles will return this type. */ OTHER } diff --git a/src/main/java/com/google/maps/model/WifiAccessPoint.java b/src/main/java/com/google/maps/model/WifiAccessPoint.java new file mode 100644 index 000000000..d0275d516 --- /dev/null +++ b/src/main/java/com/google/maps/model/WifiAccessPoint.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps.model; + +import java.io.Serializable; + +/** + * A WiFi access point. + * + *

    The request body's {@code wifiAccessPoints} array must contain two or more WiFi access point + * objects. {@code macAddress} is required; all other fields are optional. + * + *

    Please see + * WiFi Access Point Objects for more detail. + */ +public class WifiAccessPoint implements Serializable { + + private static final long serialVersionUID = 1L; + + public WifiAccessPoint() {} + + // constructor only used by the builder class below + private WifiAccessPoint( + String _macAddress, + Integer _signalStrength, + Integer _age, + Integer _channel, + Integer _signalToNoiseRatio) { + macAddress = _macAddress; + signalStrength = _signalStrength; + age = _age; + channel = _channel; + signalToNoiseRatio = _signalToNoiseRatio; + } + /** + * The MAC address of the WiFi node (required). Separators must be {@code :} (colon) and hex + * digits must use uppercase. + */ + public String macAddress; + /** The current signal strength measured in dBm. */ + public Integer signalStrength = null; + /** The number of milliseconds since this access point was detected. */ + public Integer age = null; + /** The channel over which the client is communicating with the access point. */ + public Integer channel = null; + /** The current signal to noise ratio measured in dB. */ + public Integer signalToNoiseRatio = null; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("[WifiAccessPoint:"); + if (macAddress != null) { + sb.append(" macAddress=").append(macAddress); + } + if (signalStrength != null) { + sb.append(" signalStrength=").append(signalStrength); + } + if (age != null) { + sb.append(" age=").append(age); + } + if (channel != null) { + sb.append(" channel=").append(channel); + } + if (signalToNoiseRatio != null) { + sb.append(" signalToNoiseRatio=").append(signalToNoiseRatio); + } + sb.append("]"); + return sb.toString(); + } + + public static class WifiAccessPointBuilder { + private String _macAddress = null; + private Integer _signalStrength = null; + private Integer _age = null; + private Integer _channel = null; + private Integer _signalToNoiseRatio = null; + + // create the actual wifi access point + public WifiAccessPoint createWifiAccessPoint() { + return new WifiAccessPoint(_macAddress, _signalStrength, _age, _channel, _signalToNoiseRatio); + } + + public WifiAccessPointBuilder MacAddress(String newMacAddress) { + this._macAddress = newMacAddress; + return this; + } + + public WifiAccessPointBuilder SignalStrength(int newSignalStrength) { + this._signalStrength = newSignalStrength; + return this; + } + + public WifiAccessPointBuilder Age(int newAge) { + this._age = newAge; + return this; + } + + public WifiAccessPointBuilder Channel(int newChannel) { + this._channel = newChannel; + return this; + } + + public WifiAccessPointBuilder SignalToNoiseRatio(int newSignalToNoiseRatio) { + this._signalToNoiseRatio = newSignalToNoiseRatio; + return this; + } + } +} diff --git a/src/test/java/com/google/maps/AuthenticatedTest.java b/src/test/java/com/google/maps/AuthenticatedTest.java deleted file mode 100644 index 7f09896b1..000000000 --- a/src/test/java/com/google/maps/AuthenticatedTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2014 Google Inc. All rights reserved. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this - * file except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.google.maps; - -import org.junit.Ignore; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * Base class for tests requiring automated authentication. - * - *

    Sub-classes need to implement a 1-arg constructor that takes a GeoApiContext that will be - * supplied with an appropriate API key or client ID and secret set. The {@code RunWith - * (Paramaterized.class)} annotation will then ensure that each test that inherits will be run - * for each context returned from {@link #contexts()}. That is, - * if an API_KEY and CLIENT_ID+CLIENT_SECRET are found, each test will be run twice and supplied - * a context with the appropriate authentication tokens set. - */ -@RunWith(Parameterized.class) @Ignore -public class AuthenticatedTest { - protected AuthenticatedTest() { - } - - public static Collection contexts(boolean supportsClientId) { - Collection contexts = new ArrayList(); - - // Travis can't run authorized tests from pull requests. - // http://docs.travis-ci.com/user/pull-requests/#Security-Restrictions-when-testing-Pull-Requests - if (System.getenv("TRAVIS_PULL_REQUEST") != null - && !"false".equals(System.getenv("TRAVIS_PULL_REQUEST"))) { - return contexts; - } - - if (System.getenv("API_KEY") != null) { - GeoApiContext context = new GeoApiContext() - .setApiKey(System.getenv("API_KEY")); - contexts.add(new Object[]{context}); - } - - if (supportsClientId - && (!(System.getenv("CLIENT_ID") == null - || System.getenv("CLIENT_SECRET") == null))) { - GeoApiContext context = new GeoApiContext() - .setEnterpriseCredentials(System.getenv("CLIENT_ID"), System.getenv("CLIENT_SECRET")); - contexts.add(new Object[]{context}); - } - - if (contexts.size() == 0) { - throw new IllegalArgumentException("No credentials found! Set the API_KEY or CLIENT_ID and " - + "CLIENT_SECRET environment variables to run tests requiring authentication."); - } - - return contexts; - } - - @Parameters - public static Collection contexts() { - return contexts(true); - } -} diff --git a/src/test/java/com/google/maps/DirectionsApiTest.java b/src/test/java/com/google/maps/DirectionsApiTest.java index 960d88808..8baa37522 100644 --- a/src/test/java/com/google/maps/DirectionsApiTest.java +++ b/src/test/java/com/google/maps/DirectionsApiTest.java @@ -15,319 +15,574 @@ package com.google.maps; +import static com.google.maps.TestUtils.retrieveBody; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import com.google.maps.DirectionsApi.RouteRestriction; import com.google.maps.errors.NotFoundException; -import com.google.maps.model.DirectionsRoute; +import com.google.maps.model.AddressType; +import com.google.maps.model.DirectionsResult; +import com.google.maps.model.GeocodedWaypointStatus; +import com.google.maps.model.LatLng; +import com.google.maps.model.TrafficModel; import com.google.maps.model.TransitMode; import com.google.maps.model.TransitRoutingPreference; import com.google.maps.model.TravelMode; import com.google.maps.model.Unit; - -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.util.concurrent.TimeUnit; - -@Category(LargeTests.class) -public class DirectionsApiTest extends AuthenticatedTest { +@Category(MediumTests.class) +public class DirectionsApiTest { - private GeoApiContext context; + private final String getDirectionsResponse; + private final String builderResponse; + private final String responseTimesArePopulatedCorrectly; - public DirectionsApiTest(GeoApiContext context) { - this.context = context - .setQueryRateLimit(3) - .setConnectTimeout(1, TimeUnit.SECONDS) - .setReadTimeout(1, TimeUnit.SECONDS) - .setWriteTimeout(1, TimeUnit.SECONDS); + public DirectionsApiTest() { + getDirectionsResponse = retrieveBody("GetDirectionsResponse.json"); + builderResponse = retrieveBody("DirectionsApiBuilderResponse.json"); + responseTimesArePopulatedCorrectly = retrieveBody("ResponseTimesArePopulatedCorrectly.json"); } @Test public void testGetDirections() throws Exception { - DirectionsRoute[] routes = DirectionsApi.getDirections(context, "Sydney, AU", - "Melbourne, AU").await(); - assertNotNull(routes); - assertNotNull(routes[0]); - assertThat(routes[0].overviewPolyline.decodePath().size(), not(0)); - assertEquals("Sydney NSW, Australia", routes[0].legs[0].startAddress); - assertEquals("Melbourne VIC, Australia", routes[0].legs[0].endAddress); + try (LocalTestServerContext sc = new LocalTestServerContext(getDirectionsResponse)) { + DirectionsResult result = + DirectionsApi.getDirections(sc.context, "Sydney, AU", "Melbourne, AU").await(); + + assertNotNull(result); + assertNotNull(result.toString(), "result.toString() succeeded"); + assertNotNull(result.geocodedWaypoints); + assertNotNull(Arrays.toString(result.geocodedWaypoints)); + assertEquals(2, result.geocodedWaypoints.length); + assertEquals("ChIJP3Sa8ziYEmsRUKgyFmh9AQM", result.geocodedWaypoints[0].placeId); + assertEquals("ChIJ90260rVG1moRkM2MIXVWBAQ", result.geocodedWaypoints[1].placeId); + assertNotNull(result.routes); + assertNotNull(Arrays.toString(result.routes)); + assertEquals(1, result.routes.length); + assertNotNull(result.routes[0]); + assertEquals("M31 and National Highway M31", result.routes[0].summary); + assertThat(result.routes[0].overviewPolyline.decodePath().size(), not(0)); + assertEquals(1, result.routes[0].legs.length); + assertEquals("Melbourne VIC, Australia", result.routes[0].legs[0].endAddress); + assertEquals("Sydney NSW, Australia", result.routes[0].legs[0].startAddress); + + sc.assertParamValue("Sydney, AU", "origin"); + sc.assertParamValue("Melbourne, AU", "destination"); + } } @Test public void testBuilder() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .mode(TravelMode.BICYCLING) - .avoid(RouteRestriction.HIGHWAYS, RouteRestriction.TOLLS, RouteRestriction.FERRIES) - .units(Unit.METRIC) - .region("au") - .origin("Sydney") - .destination("Melbourne").await(); - - assertNotNull(routes); - assertNotNull(routes[0]); - } - - @Test - public void testTravelModeRoundTrip() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .mode(TravelMode.BICYCLING) - .origin("Town Hall, Sydney") - .destination("Parramatta, NSW").await(); - - assertNotNull(routes); - assertNotNull(routes[0]); - assertEquals(TravelMode.BICYCLING, routes[0].legs[0].steps[0].travelMode); + try (LocalTestServerContext sc = new LocalTestServerContext(builderResponse)) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .mode(TravelMode.BICYCLING) + .avoid( + DirectionsApi.RouteRestriction.HIGHWAYS, + DirectionsApi.RouteRestriction.TOLLS, + DirectionsApi.RouteRestriction.FERRIES) + .units(Unit.METRIC) + .region("au") + .origin("Sydney") + .destination("Melbourne") + .await(); + + assertNotNull(result.routes); + assertEquals(1, result.routes.length); + + sc.assertParamValue(TravelMode.BICYCLING.toUrlValue(), "mode"); + sc.assertParamValue( + DirectionsApi.RouteRestriction.HIGHWAYS.toUrlValue() + + "|" + + DirectionsApi.RouteRestriction.TOLLS.toUrlValue() + + "|" + + DirectionsApi.RouteRestriction.FERRIES.toUrlValue(), + "avoid"); + sc.assertParamValue(Unit.METRIC.toUrlValue(), "units"); + sc.assertParamValue("au", "region"); + sc.assertParamValue("Sydney", "origin"); + sc.assertParamValue("Melbourne", "destination"); + } } @Test public void testResponseTimesArePopulatedCorrectly() throws Exception { - DateTime now = new DateTime(); - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .mode(TravelMode.TRANSIT) - .origin("Town Hall, Sydney") - .destination("Parramatta, NSW") - .departureTime(now) - .await(); - - assertNotNull(routes); - assertNotNull(routes[0]); - assertNotNull(routes[0].legs); - assertNotNull(routes[0].legs[0]); - assertNotNull(routes[0].legs[0].arrivalTime); - assertNotNull(routes[0].legs[0].departureTime); + try (LocalTestServerContext sc = + new LocalTestServerContext(responseTimesArePopulatedCorrectly)) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .mode(TravelMode.TRANSIT) + .origin("483 George St, Sydney NSW 2000, Australia") + .destination("182 Church St, Parramatta NSW 2150, Australia") + .await(); + + assertEquals(1, result.routes.length); + assertEquals(1, result.routes[0].legs.length); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("h:mm a"); + assertEquals("1:54 pm", fmt.format(result.routes[0].legs[0].arrivalTime).toLowerCase()); + assertEquals("1:21 pm", fmt.format(result.routes[0].legs[0].departureTime).toLowerCase()); + + sc.assertParamValue(TravelMode.TRANSIT.toUrlValue(), "mode"); + sc.assertParamValue("483 George St, Sydney NSW 2000, Australia", "origin"); + sc.assertParamValue("182 Church St, Parramatta NSW 2150, Australia", "destination"); + } } /** * A simple query from Toronto to Montreal. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal} + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal} */ @Test public void testTorontoToMontreal() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Toronto") - .destination("Montreal").await(); + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context).origin("Toronto").destination("Montreal").await(); - assertNotNull(routes); + sc.assertParamValue("Toronto", "origin"); + sc.assertParamValue("Montreal", "destination"); + } } /** * Going from Toronto to Montreal by bicycle, avoiding highways. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal&avoid=highways&mode=bicycling} + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Toronto&destination=Montreal&avoid=highways&mode=bicycling} */ @Test public void testTorontoToMontrealByBicycleAvoidingHighways() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Toronto") - .destination("Montreal") - .avoid(RouteRestriction.HIGHWAYS) - .mode(TravelMode.BICYCLING) - .await(); - - assertNotNull(routes); + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Toronto") + .destination("Montreal") + .avoid(DirectionsApi.RouteRestriction.HIGHWAYS) + .mode(TravelMode.BICYCLING) + .await(); + + sc.assertParamValue("Toronto", "origin"); + sc.assertParamValue("Montreal", "destination"); + sc.assertParamValue(RouteRestriction.HIGHWAYS.toUrlValue(), "avoid"); + sc.assertParamValue(TravelMode.BICYCLING.toUrlValue(), "mode"); + } + } + + @Test + public void testSanFranciscoToSeattleByBicycleAvoidingIndoor() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("San Francisco") + .destination("Seattle") + .avoid(RouteRestriction.INDOOR) + .mode(TravelMode.BICYCLING) + .await(); + + sc.assertParamValue("San Francisco", "origin"); + sc.assertParamValue("Seattle", "destination"); + sc.assertParamValue(RouteRestriction.INDOOR.toUrlValue(), "avoid"); + sc.assertParamValue(TravelMode.BICYCLING.toUrlValue(), "mode"); + } } /** * Brooklyn to Queens by public transport. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Brooklyn&destination=Queens&departure_time=1343641500&mode=transit} + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Brooklyn&destination=Queens&departure_time=1343641500&mode=transit} */ @Test public void testBrooklynToQueensByTransit() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Brooklyn") - .destination("Queens") - .departureTime(new DateTime(1343641500)) - .mode(TravelMode.TRANSIT) - .await(); - - assertNotNull(routes); + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Brooklyn") + .destination("Queens") + .mode(TravelMode.TRANSIT) + .await(); + + sc.assertParamValue("Brooklyn", "origin"); + sc.assertParamValue("Queens", "destination"); + sc.assertParamValue(TravelMode.TRANSIT.toUrlValue(), "mode"); + } } /** * Boston to Concord, via Charlestown and Lexington. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Boston,MA&destination=Concord,MA&waypoints=Charlestown,MA|Lexington,MA + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Boston,MA&destination=Concord,MA&waypoints=Charlestown,MA|Lexington,MA} */ @Test - public void testBostonToConcordViaCharlestownAndLexignton() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Boston,MA") - .destination("Concord,MA") - .waypoints("Charlestown,MA", "Lexington,MA") - .await(); - - assertNotNull(routes); + public void testBostonToConcordViaCharlestownAndLexington() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Boston,MA") + .destination("Concord,MA") + .waypoints("Charlestown,MA", "Lexington,MA") + .await(); + + sc.assertParamValue("Boston,MA", "origin"); + sc.assertParamValue("Concord,MA", "destination"); + sc.assertParamValue("Charlestown,MA|Lexington,MA", "waypoints"); + } } /** - * A wine tour around Adelaide in South Australia. This shows off how to get Directions Web - * Service API to find the shortest path amongst a set of way points. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Adelaide,SA&destination=Adelaide,SA&waypoints=optimize:true|Barossa+Valley,SA|Clare,SA|Connawarra,SA|McLaren+Vale,SA} + * Boston to Concord, via Charlestown and Lexington, using non-stopover waypoints. + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Boston,MA&destination=Concord,MA&waypoints=via:Charlestown,MA|via:Lexington,MA} */ @Test - public void testAdelaideWineTour() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Adelaide,SA") - .destination("Adelaide,SA") - .optimizeWaypoints(true) - .waypoints("Barossa Valley, SA", "Clare, SA", "Connawarra, SA", - "McLaren Vale, SA") - .await(); - - assertNotNull(routes); - assertEquals(1, routes.length); - - // optimize:true returns the waypoint_order of the optimized route. - // "waypoint_order": [ 1, 0, 2, 3 ] - assertNotNull(routes[0].waypointOrder); - assertEquals(1, routes[0].waypointOrder[0]); - assertEquals(0, routes[0].waypointOrder[1]); - assertEquals(2, routes[0].waypointOrder[2]); - assertEquals(3, routes[0].waypointOrder[3]); + public void testBostonToConcordViaCharlestownAndLexingtonNonStopover() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Boston,MA") + .destination("Concord,MA") + .waypoints( + new DirectionsApiRequest.Waypoint("Charlestown,MA", false), + new DirectionsApiRequest.Waypoint("Lexington,MA", false)) + .await(); + + sc.assertParamValue("Boston,MA", "origin"); + sc.assertParamValue("Concord,MA", "destination"); + sc.assertParamValue("via:Charlestown,MA|via:Lexington,MA", "waypoints"); + } } /** - * Toledo to Madrid, in Spain. This showcases region biasing results. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Toledo&destination=Madrid®ion=es} + * Boston to Concord, via Charlestown and Lexington, but using exact latitude and longitude + * coordinates for the waypoints. + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Boston,MA&destination=Concord,MA&waypoints=42.379322,-71.063384|42.444303,-71.229087} */ @Test - public void testToledoToMadridInSpain() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Toledo") - .destination("Madrid") - .region("es") - .await(); - - assertNotNull(routes); + public void testBostonToConcordViaCharlestownAndLexingtonLatLng() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Boston,MA") + .destination("Concord,MA") + .waypoints(new LatLng(42.379322, -71.063384), new LatLng(42.444303, -71.229087)) + .await(); + + sc.assertParamValue("Boston,MA", "origin"); + sc.assertParamValue("Concord,MA", "destination"); + sc.assertParamValue("42.37932200,-71.06338400|42.44430300,-71.22908700", "waypoints"); + } } /** - * This is the same query above, without region biasing. It returns no routes. - * {@url http://maps.googleapis.com/maps/api/directions/json?origin=Toledo&destination=Madrid} + * Boston to Concord, via Charlestown and Lexington, but using exact latitude and longitude + * coordinates for the waypoints, using non-stopover waypoints. + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Boston,MA&destination=Concord,MA&waypoints=via:42.379322,-71.063384|via:42.444303,-71.229087} */ @Test - public void testToledoToMadridNotSpain() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Toledo") - .destination("Madrid") - .await(); - - assertNotNull(routes); - assertEquals(0, routes.length); + public void testBostonToConcordViaCharlestownAndLexingtonLatLngNonStopoever() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Boston,MA") + .destination("Concord,MA") + .waypoints( + new DirectionsApiRequest.Waypoint(new LatLng(42.379322, -71.063384), false), + new DirectionsApiRequest.Waypoint(new LatLng(42.444303, -71.229087), false)) + .await(); + + sc.assertParamValue("Boston,MA", "origin"); + sc.assertParamValue("Concord,MA", "destination"); + sc.assertParamValue("via:42.37932200,-71.06338400|via:42.44430300,-71.22908700", "waypoints"); + } } /** - * Test the language parameter. + * Toledo to Madrid, in Spain. This showcases region biasing results. + * + *

    {@code + * http://maps.googleapis.com/maps/api/directions/json?origin=Toledo&destination=Madrid®ion=es} */ @Test - public void testLanguageParameter() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Toledo") - .destination("Madrid") - .region("es") - .language("es") - .await(); - - assertNotNull(routes); + public void testToledoToMadridInSpain() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsApi.newRequest(sc.context) + .origin("Toledo") + .destination("Madrid") + .region("es") + .await(); + + sc.assertParamValue("Toledo", "origin"); + sc.assertParamValue("Madrid", "destination"); + sc.assertParamValue("es", "region"); + } } - /** - * Testing the alternatives param. - */ + /** Test the language parameter. */ @Test - public void testAlternatives() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Sydney Town Hall") - .destination("Parramatta Town Hall") - .alternatives(true) - .await(); - - assertNotNull(routes); - assertTrue(routes.length > 1); + public void testLanguageParameter() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin("Toledo") + .destination("Madrid") + .region("es") + .language("es") + .await(); + + sc.assertParamValue("Toledo", "origin"); + sc.assertParamValue("Madrid", "destination"); + sc.assertParamValue("es", "region"); + sc.assertParamValue("es", "language"); + + assertNotNull(result.toString()); + } } - /** - * Test fares are returned for transit requests that support them. - */ + /** Tests the {@code traffic_model} and {@code duration_in_traffic} parameters. */ @Test - public void testFares() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Fisherman's Wharf, San Francisco") - .destination("Union Square, San Francisco") - .mode(TravelMode.TRANSIT) - .departureTime(new DateTime(2015, 1, 1, 19, 0, DateTimeZone.UTC)) - .await(); - - // Just in case we get a walking route or something silly - for (DirectionsRoute route : routes) { - if (route.fare.value != null && "USD".equals(route.fare.currency.getCurrencyCode())) { - return; - } + public void testTrafficModel() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin("48 Pirrama Road, Pyrmont NSW 2009") + .destination("182 Church St, Parramatta NSW 2150") + .mode(TravelMode.DRIVING) + .departureTime(Instant.now().plus(Duration.ofMinutes(2))) + .trafficModel(TrafficModel.PESSIMISTIC) + .await(); + + sc.assertParamValue("48 Pirrama Road, Pyrmont NSW 2009", "origin"); + sc.assertParamValue("182 Church St, Parramatta NSW 2150", "destination"); + sc.assertParamValue(TravelMode.DRIVING.toUrlValue(), "mode"); + sc.assertParamValue(TrafficModel.PESSIMISTIC.toUrlValue(), "traffic_model"); + + assertNotNull(result.toString()); } - fail("Fare data not found in any route"); } - /** - * Test transit without arrival or departure times specified. - */ + /** Test transit without arrival or departure times specified. */ @Test public void testTransitWithoutSpecifyingTime() throws Exception { - DirectionsApi.newRequest(context) - .origin("Fisherman's Wharf, San Francisco") - .destination("Union Square, San Francisco") - .mode(TravelMode.TRANSIT) - .await(); - - // Since this test may run at different times-of-day, it's entirely valid to return zero - // routes, but the main thing to catch is that no exception is thrown. + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin("Fisherman's Wharf, San Francisco") + .destination("Union Square, San Francisco") + .mode(TravelMode.TRANSIT) + .await(); + + sc.assertParamValue("Fisherman's Wharf, San Francisco", "origin"); + sc.assertParamValue("Union Square, San Francisco", "destination"); + sc.assertParamValue(TravelMode.TRANSIT.toUrlValue(), "mode"); + + assertNotNull(result.toString()); + } } - /** - * Test the extended transit parameters: mode and routing preference. - */ + /** Test the extended transit parameters: mode and routing preference. */ @Test public void testTransitParams() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Fisherman's Wharf, San Francisco") - .destination("Union Square, San Francisco") - .mode(TravelMode.TRANSIT) - .transitMode(TransitMode.BUS, TransitMode.TRAM) - .transitRoutingPreference(TransitRoutingPreference.LESS_WALKING) - .await(); - - assertTrue(routes.length > 0); + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin("Fisherman's Wharf, San Francisco") + .destination("Union Square, San Francisco") + .mode(TravelMode.TRANSIT) + .transitMode(TransitMode.BUS, TransitMode.TRAM) + .transitRoutingPreference(TransitRoutingPreference.LESS_WALKING) + .await(); + + sc.assertParamValue("Fisherman's Wharf, San Francisco", "origin"); + sc.assertParamValue("Union Square, San Francisco", "destination"); + sc.assertParamValue(TravelMode.TRANSIT.toUrlValue(), "mode"); + sc.assertParamValue( + TransitMode.BUS.toUrlValue() + "|" + TransitMode.TRAM.toUrlValue(), "transit_mode"); + sc.assertParamValue( + TransitRoutingPreference.LESS_WALKING.toUrlValue(), "transit_routing_preference"); + + assertNotNull(result.toString()); + } + } + + @Test + public void testTravelModeWalking() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .mode(TravelMode.WALKING) + .origin("483 George St, Sydney NSW 2000, Australia") + .destination("182 Church St, Parramatta NSW 2150, Australia") + .await(); + + assertNotNull(result.toString()); + assertNotNull(result.routes); + assertNotNull(result.routes[0]); + + sc.assertParamValue(TravelMode.WALKING.toUrlValue(), "mode"); + sc.assertParamValue("483 George St, Sydney NSW 2000, Australia", "origin"); + sc.assertParamValue("182 Church St, Parramatta NSW 2150, Australia", "destination"); + + assertNotNull(result.toString()); + } } @Test(expected = NotFoundException.class) public void testNotFound() throws Exception { - DirectionsRoute[] routes = DirectionsApi.getDirections(context, "fksjdhgf", "faldfdaf").await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "{\n" + + " \"geocoded_waypoints\" : [\n" + + " {\n" + + " \"geocoder_status\" : \"ZERO_RESULTS\"\n" + + " },\n" + + " {\n" + + " \"geocoder_status\" : \"ZERO_RESULTS\"\n" + + " }\n" + + " ],\n" + + " \"routes\" : [],\n" + + " \"status\" : \"NOT_FOUND\"\n" + + "}")) { + DirectionsApi.getDirections(sc.context, "fksjdhgf", "faldfdaf").await(); + } } - /** - * Test transit details. - */ + /** Test GeocodedWaypoints results. */ + @Test + public void testGeocodedWaypoints() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "{" + + " \"geocoded_waypoints\" : [\n" + + " {\n" + + " \"geocoder_status\" : \"OK\"\n" + + " },\n" + + " {\n" + + " \"geocoder_status\" : \"OK\",\n" + + " \"types\" : [\"route\"]\n" + + " }\n" + + " ],\n" + + " \"routes\": [{}],\n" + + " \"status\": \"OK\"\n" + + "}")) { + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin("48 Pirrama Rd, Pyrmont NSW") + .destination("Airport Dr, Sydney NSW") + .mode(TravelMode.DRIVING) + .await(); + + assertNotNull(result.toString()); + assertNotNull(result.geocodedWaypoints); + assertEquals(2, result.geocodedWaypoints.length); + assertEquals(GeocodedWaypointStatus.OK, result.geocodedWaypoints[0].geocoderStatus); + assertEquals(GeocodedWaypointStatus.OK, result.geocodedWaypoints[1].geocoderStatus); + assertEquals(AddressType.ROUTE, result.geocodedWaypoints[1].types[0]); + + assertNotNull(result.toString()); + } + } + + /** Tests that calling {@code optimizeWaypoints(true)} works in either order. */ @Test - public void testTransitDetails() throws Exception { - DirectionsRoute[] routes = DirectionsApi.newRequest(context) - .origin("Bibliotheque Francois Mitterrand, Paris") - .destination("Pyramides, Paris") - .mode(TravelMode.TRANSIT) - .departureTime(new DateTime(2015, 2, 15, 11, 0, DateTimeZone.UTC)) - .await(); - - assertNotNull(routes[0].legs[0].steps[0].transitDetails); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.arrivalStop); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.arrivalTime); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.departureStop); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.departureTime); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.line); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.line.agencies); - assertNotNull(routes[0].legs[0].steps[0].transitDetails.line.vehicle); + public void testOptimizeWaypointsBeforeWaypoints() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + List waypoints = getOptimizationWaypoints(); + LatLng origin = waypoints.get(0); + LatLng destination = waypoints.get(1); + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin(origin) + .destination(destination) + .departureTime(Instant.now()) + .optimizeWaypoints(true) + .waypoints(waypoints.subList(2, waypoints.size()).toArray(new LatLng[0])) + .await(); + + sc.assertParamValue(origin.toUrlValue(), "origin"); + sc.assertParamValue(destination.toUrlValue(), "destination"); + sc.assertParamValue( + "optimize:true|" + + waypoints.get(2).toUrlValue() + + "|" + + waypoints.get(3).toUrlValue() + + "|" + + waypoints.get(4).toUrlValue() + + "|" + + waypoints.get(5).toUrlValue(), + "waypoints"); + + assertNotNull(result.toString()); + } + } + + /** Tests that calling {@code optimizeWaypoints(true)} works in either order. */ + @Test + public void testOptimizeWaypointsAfterWaypoints() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext("{\"routes\": [{}],\"status\": \"OK\"}")) { + List waypoints = getOptimizationWaypoints(); + LatLng origin = waypoints.get(0); + LatLng destination = waypoints.get(1); + DirectionsResult result = + DirectionsApi.newRequest(sc.context) + .origin(origin) + .destination(destination) + .departureTime(Instant.now()) + .waypoints(waypoints.subList(2, waypoints.size()).toArray(new LatLng[0])) + .optimizeWaypoints(true) + .await(); + + sc.assertParamValue(origin.toUrlValue(), "origin"); + sc.assertParamValue(destination.toUrlValue(), "destination"); + sc.assertParamValue( + "optimize:true|" + + waypoints.get(2).toUrlValue() + + "|" + + waypoints.get(3).toUrlValue() + + "|" + + waypoints.get(4).toUrlValue() + + "|" + + waypoints.get(5).toUrlValue(), + "waypoints"); + + assertNotNull(result.toString()); + } + } + + /** Coordinates in Mexico City. */ + private List getOptimizationWaypoints() { + List waypoints = new ArrayList<>(); + waypoints.add(new LatLng(19.431676, -99.133999)); + waypoints.add(new LatLng(19.427915, -99.138939)); + waypoints.add(new LatLng(19.435436, -99.139145)); + waypoints.add(new LatLng(19.396436, -99.157176)); + waypoints.add(new LatLng(19.427705, -99.198858)); + waypoints.add(new LatLng(19.425869, -99.160716)); + return waypoints; } } diff --git a/src/test/java/com/google/maps/DistanceMatrixApiIntegrationTest.java b/src/test/java/com/google/maps/DistanceMatrixApiIntegrationTest.java deleted file mode 100644 index 06381684f..000000000 --- a/src/test/java/com/google/maps/DistanceMatrixApiIntegrationTest.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright 2014 Google Inc. All rights reserved. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this - * file except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.google.maps; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import com.google.maps.DirectionsApi.RouteRestriction; -import com.google.maps.model.DistanceMatrix; -import com.google.maps.model.DistanceMatrixElement; -import com.google.maps.model.DistanceMatrixElementStatus; -import com.google.maps.model.DistanceMatrixRow; -import com.google.maps.model.TransitMode; -import com.google.maps.model.TransitRoutingPreference; -import com.google.maps.model.TravelMode; -import com.google.maps.model.Unit; - -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.junit.Test; -import org.junit.experimental.categories.Category; - -import java.util.concurrent.TimeUnit; - -@Category(LargeTests.class) -public class DistanceMatrixApiIntegrationTest extends AuthenticatedTest { - - private GeoApiContext context; - - public DistanceMatrixApiIntegrationTest(GeoApiContext context) { - this.context = context - .setConnectTimeout(1, TimeUnit.SECONDS) - .setReadTimeout(1, TimeUnit.SECONDS) - .setWriteTimeout(1, TimeUnit.SECONDS); - } - - @Test - public void testGetDistanceMatrixWithBasicStringParams() throws Exception { - String[] origins = new String[] { - "Perth, Australia", "Sydney, Australia", "Melbourne, Australia", - "Adelaide, Australia", "Brisbane, Australia", "Darwin, Australia", - "Hobart, Australia", "Canberra, Australia" - }; - String[] destinations = new String[] { - "Uluru, Australia", "Kakadu, Australia", "Blue Mountains, Australia", - "Bungle Bungles, Australia", "The Pinnacles, Australia" - }; - DistanceMatrix matrix = - DistanceMatrixApi.getDistanceMatrix(context, origins, destinations).await(); - - // Rows length will match the number of origin elements, regardless of whether they're routable. - assertEquals(8, matrix.rows.length); - assertEquals(5, matrix.rows[0].elements.length); - assertEquals(DistanceMatrixElementStatus.OK, matrix.rows[0].elements[0].status); - } - - @Test - public void testNewRequestWithAllPossibleParams() throws Exception { - String[] origins = new String[] { - "Perth, Australia", "Sydney, Australia", "Melbourne, Australia", - "Adelaide, Australia", "Brisbane, Australia", "Darwin, Australia", - "Hobart, Australia", "Canberra, Australia" - }; - String[] destinations = new String[] { - "Uluru, Australia", "Kakadu, Australia", "Blue Mountains, Australia", - "Bungle Bungles, Australia", "The Pinnacles, Australia" - }; - - DistanceMatrix matrix = DistanceMatrixApi.newRequest(context) - .origins(origins) - .destinations(destinations) - .mode(TravelMode.DRIVING) - .language("en-AU") - .avoid(RouteRestriction.TOLLS) - .units(Unit.IMPERIAL) - .departureTime(new DateTime().plusMinutes(2)) // this is ignored when an API key is used - .await(); - - assertEquals(8, matrix.rows.length); - assertEquals(5, matrix.rows[0].elements.length); - assertTrue(matrix.rows[0].elements[0].distance.humanReadable.endsWith("mi")); - } - - /** - * Test the language parameter. - * - *

    Sample request: - * - * origins: Vancouver BC|Seattle, destinations: San Francisco|Victoria BC, mode: bicycling, - * language: french. - */ - @Test - public void testLanguageParameter() throws Exception { - DistanceMatrix matrix = DistanceMatrixApi.newRequest(context) - .origins("Vancouver BC", "Seattle") - .destinations("San Francisco", "Victoria BC") - .mode(TravelMode.BICYCLING) - .language("fr-FR") - .await(); - - assertNotNull(matrix); - } - - @Test - public void testTransitData() throws Exception { - DistanceMatrix matrix = DistanceMatrixApi.newRequest(context) - .origins("Fisherman's Wharf, San Francisco", "Union Square, San Francisco") - .destinations("Mikkeller Bar, San Francisco", "Moscone Center, San Francisco") - .mode(TravelMode.TRANSIT) - .departureTime(new DateTime(2015, 1, 1, 19, 0, DateTimeZone.UTC)) - .await(); - - assertNotNull(matrix); - - for (DistanceMatrixRow row : matrix.rows) { - for (DistanceMatrixElement cell : row.elements) { - if (cell.fare != null) { - assertEquals("USD", cell.fare.currency.getCurrencyCode()); - assertNotNull(cell.fare.value); - return; - } - } - } - - fail("No fare information found in a transit search."); - } - - /** - * Test transit without arrival or departure times specified. - */ - @Test - public void testTransitWithoutSpecifyingTime() throws Exception { - DistanceMatrixApi.newRequest(context) - .origins("Fisherman's Wharf, San Francisco", "Union Square, San Francisco") - .destinations("Mikkeller Bar, San Francisco", "Moscone Center, San Francisco") - .mode(TravelMode.TRANSIT) - .await(); - - // Since this test may run at different times-of-day, it's entirely valid to return zero - // routes, but the main thing to catch is that no exception is thrown. - } - - @Test - public void testTransitWithArrivalTime() throws Exception { - DistanceMatrix matrix = DistanceMatrixApi.newRequest(context) - .origins("Fisherman's Wharf, San Francisco", "Union Square, San Francisco") - .destinations("Mikkeller Bar, San Francisco", "Moscone Center, San Francisco") - .mode(TravelMode.TRANSIT) - .arrivalTime(new DateTime(2015, 1, 1, 19, 0, DateTimeZone.UTC)) - .await(); - - assertNotNull(matrix); - assertEquals(DistanceMatrixElementStatus.OK, matrix.rows[0].elements[0].status); - } - - /** - * Test the extended transit parameters: mode and routing preference. - */ - @Test - public void testTransitParams() throws Exception { - DistanceMatrix matrix = DistanceMatrixApi.newRequest(context) - .origins("Fisherman's Wharf, San Francisco", "Union Square, San Francisco") - .destinations("Mikkeller Bar, San Francisco", "Moscone Center, San Francisco") - .mode(TravelMode.TRANSIT) - .transitModes(TransitMode.RAIL, TransitMode.TRAM) - .transitRoutingPreference(TransitRoutingPreference.LESS_WALKING) - .arrivalTime(new DateTime(2015, 1, 1, 19, 0, DateTimeZone.UTC)) - .await(); - - assertNotNull(matrix); - assertEquals(DistanceMatrixElementStatus.OK, matrix.rows[0].elements[0].status); - } -} diff --git a/src/test/java/com/google/maps/DistanceMatrixApiTest.java b/src/test/java/com/google/maps/DistanceMatrixApiTest.java index 8fa37d1fe..59160159b 100644 --- a/src/test/java/com/google/maps/DistanceMatrixApiTest.java +++ b/src/test/java/com/google/maps/DistanceMatrixApiTest.java @@ -15,67 +15,201 @@ package com.google.maps; +import static com.google.maps.TestUtils.retrieveBody; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNotNull; +import com.google.maps.DirectionsApi.RouteRestriction; +import com.google.maps.model.DistanceMatrix; +import com.google.maps.model.DistanceMatrixElementStatus; import com.google.maps.model.LatLng; -import com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; - -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; +import com.google.maps.model.TrafficModel; +import com.google.maps.model.TravelMode; +import com.google.maps.model.Unit; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import org.apache.commons.lang3.StringUtils; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.net.URI; -import java.util.List; - @Category(MediumTests.class) public class DistanceMatrixApiTest { - private GeoApiContext context = new GeoApiContext().setApiKey("AIzaFakeKey"); + private final String getDistanceMatrixWithBasicStringParams; + + public DistanceMatrixApiTest() { + getDistanceMatrixWithBasicStringParams = + retrieveBody("GetDistanceMatrixWithBasicStringParams.json"); + } @Test public void testLatLngOriginDestinations() throws Exception { - MockResponse response = new MockResponse(); - response.setBody(""); - MockWebServer server = new MockWebServer(); - server.enqueue(response); - server.play(); - context.setBaseUrlForTesting("http://127.0.0.1:" + server.getPort()); - - DistanceMatrixApi.newRequest(context) - .origins(new LatLng(-31.9522, 115.8589), - new LatLng(-37.8136, 144.9631)) - .destinations(new LatLng(-25.344677, 131.036692), - new LatLng(-13.092297, 132.394057)) - .awaitIgnoreError(); - - List actualParams = - parseQueryParamsFromRequestLine(server.takeRequest().getRequestLine()); - assertParamValue("-31.952200,115.858900|-37.813600,144.963100", "origins", actualParams); - assertParamValue("-25.344677,131.036692|-13.092297,132.394057", "destinations", actualParams); - - server.shutdown(); + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + DistanceMatrixApi.newRequest(sc.context) + .origins(new LatLng(-31.9522, 115.8589), new LatLng(-37.8136, 144.9631)) + .destinations(new LatLng(-25.344677, 131.036692), new LatLng(-13.092297, 132.394057)) + .awaitIgnoreError(); + + sc.assertParamValue("-31.95220000,115.85890000|-37.81360000,144.96310000", "origins"); + sc.assertParamValue("-25.34467700,131.03669200|-13.09229700,132.39405700", "destinations"); + } + } + + @Test + public void testGetDistanceMatrixWithBasicStringParams() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(getDistanceMatrixWithBasicStringParams)) { + String[] origins = + new String[] { + "Perth, Australia", "Sydney, Australia", "Melbourne, Australia", + "Adelaide, Australia", "Brisbane, Australia", "Darwin, Australia", + "Hobart, Australia", "Canberra, Australia" + }; + String[] destinations = + new String[] { + "Uluru, Australia", + "Kakadu, Australia", + "Blue Mountains, Australia", + "Bungle Bungles, Australia", + "The Pinnacles, Australia" + }; + DistanceMatrix matrix = + DistanceMatrixApi.getDistanceMatrix(sc.context, origins, destinations).await(); + + assertNotNull(matrix.toString()); + assertNotNull(Arrays.toString(matrix.rows)); + + // Rows length will match the number of origin elements, regardless of whether they're + // routable. + assertEquals(8, matrix.rows.length); + assertEquals(5, matrix.rows[0].elements.length); + assertEquals(DistanceMatrixElementStatus.OK, matrix.rows[0].elements[0].status); + + assertEquals("Perth WA, Australia", matrix.originAddresses[0]); + assertEquals("Sydney NSW, Australia", matrix.originAddresses[1]); + assertEquals("Melbourne VIC, Australia", matrix.originAddresses[2]); + assertEquals("Adelaide SA, Australia", matrix.originAddresses[3]); + assertEquals("Brisbane QLD, Australia", matrix.originAddresses[4]); + assertEquals("Darwin NT, Australia", matrix.originAddresses[5]); + assertEquals("Hobart TAS 7000, Australia", matrix.originAddresses[6]); + assertEquals("Canberra ACT 2601, Australia", matrix.originAddresses[7]); + + assertEquals("Uluru, Petermann NT 0872, Australia", matrix.destinationAddresses[0]); + assertEquals("Kakadu NT 0822, Australia", matrix.destinationAddresses[1]); + assertEquals("Blue Mountains, New South Wales, Australia", matrix.destinationAddresses[2]); + assertEquals( + "Purnululu National Park, Western Australia 6770, Australia", + matrix.destinationAddresses[3]); + assertEquals("Pinnacles Drive, Cervantes WA 6511, Australia", matrix.destinationAddresses[4]); + + sc.assertParamValue(StringUtils.join(origins, "|"), "origins"); + sc.assertParamValue(StringUtils.join(destinations, "|"), "destinations"); + } } - private void assertParamValue(String expectedValue, String paramName, List params) - throws Exception { - boolean paramFound = false; - for (NameValuePair pair : params) { - if (pair.getName().equals(paramName)) { - paramFound = true; - assertEquals(expectedValue, pair.getValue()); - } + @Test + public void testNewRequestWithAllPossibleParams() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + String[] origins = + new String[] { + "Perth, Australia", "Sydney, Australia", "Melbourne, Australia", + "Adelaide, Australia", "Brisbane, Australia", "Darwin, Australia", + "Hobart, Australia", "Canberra, Australia" + }; + String[] destinations = + new String[] { + "Uluru, Australia", + "Kakadu, Australia", + "Blue Mountains, Australia", + "Bungle Bungles, Australia", + "The Pinnacles, Australia" + }; + + DistanceMatrixApi.newRequest(sc.context) + .origins(origins) + .destinations(destinations) + .mode(TravelMode.DRIVING) + .language("en-AU") + .avoid(RouteRestriction.TOLLS) + .units(Unit.IMPERIAL) + .departureTime( + Instant.now().plus(Duration.ofMinutes(2))) // this is ignored when an API key is used + .await(); + + sc.assertParamValue(StringUtils.join(origins, "|"), "origins"); + sc.assertParamValue(StringUtils.join(destinations, "|"), "destinations"); + sc.assertParamValue(TravelMode.DRIVING.toUrlValue(), "mode"); + sc.assertParamValue("en-AU", "language"); + sc.assertParamValue(RouteRestriction.TOLLS.toUrlValue(), "avoid"); + sc.assertParamValue(Unit.IMPERIAL.toUrlValue(), "units"); } - assertTrue(paramFound); } - private List parseQueryParamsFromRequestLine(String requestLine) throws Exception { - // Extract the URL part from the HTTP request line - String[] chunks = requestLine.split("\\s"); - String url = chunks[1]; + /** + * Test the language parameter. + * + *

    Sample request: + * origins: Vancouver BC|Seattle, destinations: San Francisco|Victoria BC, mode: bicycling, + * language: french. + */ + @Test + public void testLanguageParameter() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + String[] origins = new String[] {"Vancouver BC", "Seattle"}; + String[] destinations = new String[] {"San Francisco", "Victoria BC"}; + DistanceMatrixApi.newRequest(sc.context) + .origins(origins) + .destinations(destinations) + .mode(TravelMode.BICYCLING) + .language("fr-FR") + .await(); + + sc.assertParamValue(StringUtils.join(origins, "|"), "origins"); + sc.assertParamValue(StringUtils.join(destinations, "|"), "destinations"); + sc.assertParamValue(TravelMode.BICYCLING.toUrlValue(), "mode"); + sc.assertParamValue("fr-FR", "language"); + } + } - return URLEncodedUtils.parse(new URI(url), "UTF-8"); + /** Test transit without arrival or departure times specified. */ + @Test + public void testTransitWithoutSpecifyingTime() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + String[] origins = + new String[] {"Fisherman's Wharf, San Francisco", "Union Square, San Francisco"}; + String[] destinations = + new String[] {"Mikkeller Bar, San Francisco", "Moscone Center, San Francisco"}; + DistanceMatrixApi.newRequest(sc.context) + .origins(origins) + .destinations(destinations) + .mode(TravelMode.TRANSIT) + .await(); + + sc.assertParamValue(StringUtils.join(origins, "|"), "origins"); + sc.assertParamValue(StringUtils.join(destinations, "|"), "destinations"); + sc.assertParamValue(TravelMode.TRANSIT.toUrlValue(), "mode"); + } + } + + /** Test duration in traffic with traffic model set. */ + @Test + public void testDurationInTrafficWithTrafficModel() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + final long ONE_HOUR_MILLIS = 60 * 60 * 1000; + DistanceMatrixApi.newRequest(sc.context) + .origins("Fisherman's Wharf, San Francisco") + .destinations("San Francisco International Airport, San Francisco, CA") + .mode(TravelMode.DRIVING) + .trafficModel(TrafficModel.PESSIMISTIC) + .departureTime(Instant.ofEpochMilli(System.currentTimeMillis() + ONE_HOUR_MILLIS)) + .await(); + + sc.assertParamValue("Fisherman's Wharf, San Francisco", "origins"); + sc.assertParamValue("San Francisco International Airport, San Francisco, CA", "destinations"); + sc.assertParamValue(TravelMode.DRIVING.toUrlValue(), "mode"); + sc.assertParamValue(TrafficModel.PESSIMISTIC.toUrlValue(), "traffic_model"); + } } } diff --git a/src/test/java/com/google/maps/ElevationApiIntegrationTest.java b/src/test/java/com/google/maps/ElevationApiIntegrationTest.java deleted file mode 100644 index e6a17f5d4..000000000 --- a/src/test/java/com/google/maps/ElevationApiIntegrationTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2014 Google Inc. All rights reserved. - * - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this - * file except in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF - * ANY KIND, either express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -package com.google.maps; - -import static com.google.maps.model.LatLngAssert.assertEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import com.google.maps.model.ElevationResult; -import com.google.maps.model.EncodedPolyline; -import com.google.maps.model.LatLng; - -import org.junit.Test; -import org.junit.experimental.categories.Category; - -import java.util.List; -import java.util.concurrent.TimeUnit; - -@Category(LargeTests.class) -public class ElevationApiIntegrationTest extends AuthenticatedTest { - - public static final double SYDNEY_ELEVATION = 19.11174774169922; - public static final double SYDNEY_POINT_ELEVATION = 19.10829925537109; - public static final double MELBOURNE_ELEVATION = 25.49982643127441; - private static final double EPSILON = .00001; - private static final LatLng SYDNEY = new LatLng(-33.867487, 151.206990); - private static final LatLng MELBOURNE = new LatLng(-37.814107, 144.963280); - private static final EncodedPolyline SYD_MELB_ROUTE = new EncodedPolyline( - "rvumEis{y[`NsfA~tAbF`bEj^h{@{KlfA~eA~`AbmEghAt~D|e@jlRpO~yH_\\v}LjbBh~FdvCxu@`nCplDbcBf_B|w" - + "BhIfhCnqEb~D~jCn_EngApdEtoBbfClf@t_CzcCpoEr_Gz_DxmAphDjjBxqCviEf}B|pEvsEzbE~qGfpExjBlqCx}" - + "BvmLb`FbrQdpEvkAbjDllD|uDldDj`Ef|AzcEx_Gtm@vuI~xArwD`dArlFnhEzmHjtC~eDluAfkC|eAdhGpJh}N_m" - + "ArrDlr@h|HzjDbsAvy@~~EdTxpJje@jlEltBboDjJdvKyZpzExrAxpHfg@pmJg[tgJuqBnlIarAh}DbN`hCeOf_Ib" - + "xA~uFt|A|xEt_ArmBcN|sB|h@b_DjOzbJ{RlxCcfAp~AahAbqG~Gr}AerA`dCwlCbaFo]twKt{@bsG|}A~fDlvBvz" - + "@tw@rpD_r@rqB{PvbHek@vsHlh@ptNtm@fkD[~xFeEbyKnjDdyDbbBtuA|~Br|Gx_AfxCt}CjnHv`Ew\\lnBdrBfq" - + "BraD|{BldBxpG|]jqC`mArcBv]rdAxgBzdEb{InaBzyC}AzaEaIvrCzcAzsCtfD~qGoPfeEh]h`BxiB`e@`kBxfAv" - + "^pyA`}BhkCdoCtrC~bCxhCbgEplKrk@tiAteBwAxbCwuAnnCc]b{FjrDdjGhhGzfCrlDruBzSrnGhvDhcFzw@n{@z" - + "xAf}Fd{IzaDnbDjoAjqJjfDlbIlzAraBxrB}K~`GpuD~`BjmDhkBp{@r_AxCrnAjrCx`AzrBj{B|r@~qBbdAjtDnv" - + "CtNzpHxeApyC|GlfM`fHtMvqLjuEtlDvoFbnCt|@xmAvqBkGreFm~@hlHw|AltC}NtkGvhBfaJ|~@riAxuC~gErwC" - + "ttCzjAdmGuF`iFv`AxsJftD|nDr_QtbMz_DheAf~Buy@rlC`i@d_CljC`gBr|H|nAf_Fh{G|mE~kAhgKviEpaQnu@" - + "zwAlrA`G~gFnvItz@j{Cng@j{D{]`tEftCdcIsPz{DddE~}PlnE|dJnzG`eG`mF|aJdqDvoAwWjzHv`H`wOtjGzeX" - + "hhBlxErfCf{BtsCjpEjtD|}Aja@xnAbdDt|ErMrdFh{CzgAnlCnr@`wEM~mE`bA`uD|MlwKxmBvuFlhB|sN`_@fvB" - + "p`CxhCt_@loDsS|eDlmChgFlqCbjCxk@vbGxmCjbMba@rpBaoClcCk_DhgEzYdzBl\\vsA_JfGztAbShkGtEhlDzh" - + "C~w@hnB{e@yF}`D`_Ayx@~vGqn@l}CafC"); - private GeoApiContext context; - - public ElevationApiIntegrationTest(GeoApiContext context) { - this.context = context - .setQueryRateLimit(3) - .setConnectTimeout(1, TimeUnit.SECONDS) - .setReadTimeout(1, TimeUnit.SECONDS) - .setWriteTimeout(1, TimeUnit.SECONDS); - } - - @Test - public void testGetPoint() throws Exception { - ElevationResult result = ElevationApi.getByPoint(context, SYDNEY).await(); - - assertNotNull(result); - assertEquals(SYDNEY_POINT_ELEVATION, result.elevation, EPSILON); - } - - @Test - public void testGetPoints() throws Exception { - ElevationResult[] results = ElevationApi.getByPoints(context, SYDNEY, MELBOURNE).await(); - - assertNotNull(results); - assertEquals(2, results.length); - assertEquals(SYDNEY_ELEVATION, results[0].elevation, EPSILON); - assertEquals(MELBOURNE_ELEVATION, results[1].elevation, EPSILON); - } - - @Test - public void testGetPath() throws Exception { - ElevationResult[] results = ElevationApi.getByPath(context, 10, SYDNEY, MELBOURNE).await(); - - assertNotNull(results); - assertEquals(10, results.length); - assertEquals(SYDNEY_ELEVATION, results[0].elevation, EPSILON); - assertEquals(MELBOURNE_ELEVATION, results[9].elevation, EPSILON); - } - - @Test - public void testDirectionsAlongPath() throws Exception { - ElevationResult[] elevation = ElevationApi.getByPath(context, 100, SYD_MELB_ROUTE).await(); - assertEquals(100, elevation.length); - - List overviewPolylinePath = SYD_MELB_ROUTE.decodePath(); - LatLng lastDirectionsPoint = overviewPolylinePath.get(overviewPolylinePath.size() - 1); - LatLng lastElevationPoint = elevation[elevation.length - 1].location; - - assertEquals(lastDirectionsPoint, lastElevationPoint, EPSILON); - } -} diff --git a/src/test/java/com/google/maps/ElevationApiTest.java b/src/test/java/com/google/maps/ElevationApiTest.java index f38d66c60..968e40522 100644 --- a/src/test/java/com/google/maps/ElevationApiTest.java +++ b/src/test/java/com/google/maps/ElevationApiTest.java @@ -15,67 +15,264 @@ package com.google.maps; +import static com.google.maps.TestUtils.retrieveBody; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + import com.google.maps.errors.InvalidRequestException; import com.google.maps.errors.RequestDeniedException; +import com.google.maps.model.ElevationResult; import com.google.maps.model.EncodedPolyline; import com.google.maps.model.LatLng; -import com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; - -import org.junit.After; +import com.google.maps.model.LatLngAssert; +import java.util.Collections; +import java.util.List; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.util.Arrays; - @Category(MediumTests.class) public class ElevationApiTest { - private MockWebServer server = new MockWebServer(); - private GeoApiContext context = new GeoApiContext().setApiKey("AIzaFakeKey"); + private static final double SYDNEY_ELEVATION = 19.11174774169922; + private static final double SYDNEY_POINT_ELEVATION = 19.10829925537109; + private static final double MELBOURNE_ELEVATION = 25.49982643127441; + private static final double EPSILON = .00001; + private static final LatLng SYDNEY = new LatLng(-33.867487, 151.206990); + private static final LatLng MELBOURNE = new LatLng(-37.814107, 144.963280); + private static final EncodedPolyline SYD_MELB_ROUTE = + new EncodedPolyline( + "rvumEis{y[`NsfA~tAbF`bEj^h{@{KlfA~eA~`AbmEghAt~D|e@jlRpO~yH_\\v}LjbBh~FdvCxu@`nCplDbcBf_B|w" + + "BhIfhCnqEb~D~jCn_EngApdEtoBbfClf@t_CzcCpoEr_Gz_DxmAphDjjBxqCviEf}B|pEvsEzbE~qGfpExjBlqCx}" + + "BvmLb`FbrQdpEvkAbjDllD|uDldDj`Ef|AzcEx_Gtm@vuI~xArwD`dArlFnhEzmHjtC~eDluAfkC|eAdhGpJh}N_m" + + "ArrDlr@h|HzjDbsAvy@~~EdTxpJje@jlEltBboDjJdvKyZpzExrAxpHfg@pmJg[tgJuqBnlIarAh}DbN`hCeOf_Ib" + + "xA~uFt|A|xEt_ArmBcN|sB|h@b_DjOzbJ{RlxCcfAp~AahAbqG~Gr}AerA`dCwlCbaFo]twKt{@bsG|}A~fDlvBvz" + + "@tw@rpD_r@rqB{PvbHek@vsHlh@ptNtm@fkD[~xFeEbyKnjDdyDbbBtuA|~Br|Gx_AfxCt}CjnHv`Ew\\lnBdrBfq" + + "BraD|{BldBxpG|]jqC`mArcBv]rdAxgBzdEb{InaBzyC}AzaEaIvrCzcAzsCtfD~qGoPfeEh]h`BxiB`e@`kBxfAv" + + "^pyA`}BhkCdoCtrC~bCxhCbgEplKrk@tiAteBwAxbCwuAnnCc]b{FjrDdjGhhGzfCrlDruBzSrnGhvDhcFzw@n{@z" + + "xAf}Fd{IzaDnbDjoAjqJjfDlbIlzAraBxrB}K~`GpuD~`BjmDhkBp{@r_AxCrnAjrCx`AzrBj{B|r@~qBbdAjtDnv" + + "CtNzpHxeApyC|GlfM`fHtMvqLjuEtlDvoFbnCt|@xmAvqBkGreFm~@hlHw|AltC}NtkGvhBfaJ|~@riAxuC~gErwC" + + "ttCzjAdmGuF`iFv`AxsJftD|nDr_QtbMz_DheAf~Buy@rlC`i@d_CljC`gBr|H|nAf_Fh{G|mE~kAhgKviEpaQnu@" + + "zwAlrA`G~gFnvItz@j{Cng@j{D{]`tEftCdcIsPz{DddE~}PlnE|dJnzG`eG`mF|aJdqDvoAwWjzHv`H`wOtjGzeX" + + "hhBlxErfCf{BtsCjpEjtD|}Aja@xnAbdDt|ErMrdFh{CzgAnlCnr@`wEM~mE`bA`uD|MlwKxmBvuFlhB|sN`_@fvB" + + "p`CxhCt_@loDsS|eDlmChgFlqCbjCxk@vbGxmCjbMba@rpBaoClcCk_DhgEzYdzBl\\vsA_JfGztAbShkGtEhlDzh" + + "C~w@hnB{e@yF}`D`_Ayx@~vGqn@l}CafC"); - private void setMockBaseUrl() { - context.setBaseUrlForTesting("http://127.0.0.1:" + server.getPort()); - } + private final String directionsAlongPath; - @After - public void tearDown() throws Exception { - // Need to shut the server down here as we're using expected exceptions - server.shutdown(); + public ElevationApiTest() { + directionsAlongPath = retrieveBody("DirectionsAlongPath.json"); } @Test(expected = InvalidRequestException.class) public void testGetByPointThrowsInvalidRequestExceptionFromResponse() throws Exception { - // Queue up an invalid response - MockResponse errorResponse = new MockResponse(); - errorResponse.setBody("" - + "{\n" - + " \"routes\" : [],\n" - + " \"status\" : \"INVALID_REQUEST\"\n" - + "}"); - server.enqueue(errorResponse); - server.play(); - - setMockBaseUrl(); - // This should throw the InvalidRequestException - ElevationApi.getByPoint(context, new LatLng(0, 0)).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "{\n \"routes\" : [],\n \"status\" : \"INVALID_REQUEST\"\n}")) { + + // This should throw the InvalidRequestException + ElevationApi.getByPoint(sc.context, new LatLng(0, 0)).await(); + } } @Test(expected = RequestDeniedException.class) public void testGetByPointsThrowsRequestDeniedExceptionFromResponse() throws Exception { - // Queue up an invalid response - MockResponse errorResponse = new MockResponse(); - errorResponse.setBody("" - + "{\n" - + " \"routes\" : [],\n" - + " \"status\" : \"REQUEST_DENIED\",\n" - + " \"errorMessage\" : \"Can't do the thing\"\n" - + "}"); - server.enqueue(errorResponse); - server.play(); - - setMockBaseUrl(); - // This should throw the RequestDeniedException - ElevationApi.getByPoints(context, new EncodedPolyline(Arrays.asList(new LatLng(0, 0)))).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "" + + "{\n" + + " \"routes\" : [],\n" + + " \"status\" : \"REQUEST_DENIED\",\n" + + " \"errorMessage\" : \"Can't do the thing\"\n" + + "}")) { + + // This should throw the RequestDeniedException + ElevationApi.getByPoints( + sc.context, new EncodedPolyline(Collections.singletonList(new LatLng(0, 0)))) + .await(); + } + } + + @Test + public void testGetPoint() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"elevation\" : 19.10829925537109,\n" + + " \"location\" : {\n" + + " \"lat\" : -33.867487,\n" + + " \"lng\" : 151.20699\n" + + " },\n" + + " \"resolution\" : 4.771975994110107\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + ElevationResult result = ElevationApi.getByPoint(sc.context, SYDNEY).await(); + + assertNotNull(result); + assertNotNull(result.toString()); + assertEquals(SYDNEY_POINT_ELEVATION, result.elevation, EPSILON); + + sc.assertParamValue(SYDNEY.toUrlValue(), "locations"); + } + } + + @Test + public void testGetPoints() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"elevation\" : 19.11174774169922,\n" + + " \"location\" : {\n" + + " \"lat\" : -33.86749,\n" + + " \"lng\" : 151.20699\n" + + " },\n" + + " \"resolution\" : 4.771975994110107\n" + + " },\n" + + " {\n" + + " \"elevation\" : 25.49982643127441,\n" + + " \"location\" : {\n" + + " \"lat\" : -37.81411,\n" + + " \"lng\" : 144.96328\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + ElevationResult[] results = ElevationApi.getByPoints(sc.context, SYDNEY, MELBOURNE).await(); + + assertNotNull(results); + assertEquals(2, results.length); + assertEquals(SYDNEY_ELEVATION, results[0].elevation, EPSILON); + assertEquals(MELBOURNE_ELEVATION, results[1].elevation, EPSILON); + + sc.assertParamValue("enc:xvumEur{y[jyaWdnbe@", "locations"); + } + } + + @Test + public void testGetPath() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"elevation\" : 19.11174774169922,\n" + + " \"location\" : {\n" + + " \"lat\" : -33.86749,\n" + + " \"lng\" : 151.20699\n" + + " },\n" + + " \"resolution\" : 4.771975994110107\n" + + " },\n" + + " {\n" + + " \"elevation\" : 456.7416381835938,\n" + + " \"location\" : {\n" + + " \"lat\" : -34.32145720949158,\n" + + " \"lng\" : 150.5433152252451\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 677.8786010742188,\n" + + " \"location\" : {\n" + + " \"lat\" : -34.77180578055915,\n" + + " \"lng\" : 149.8724504366625\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 672.6239624023438,\n" + + " \"location\" : {\n" + + " \"lat\" : -35.21843425947625,\n" + + " \"lng\" : 149.1942540405992\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 1244.74755859375,\n" + + " \"location\" : {\n" + + " \"lat\" : -35.66123890186951,\n" + + " \"lng\" : 148.5085849619781\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 317.3624572753906,\n" + + " \"location\" : {\n" + + " \"lat\" : -36.10011364524662,\n" + + " \"lng\" : 147.815302885111\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 797.5011596679688,\n" + + " \"location\" : {\n" + + " \"lat\" : -36.53495008485245,\n" + + " \"lng\" : 147.1142685138642\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 684.0189819335938,\n" + + " \"location\" : {\n" + + " \"lat\" : -36.9656374532439,\n" + + " \"lng\" : 146.4053438519865\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 351.05712890625,\n" + + " \"location\" : {\n" + + " \"lat\" : -37.39206260399896,\n" + + " \"lng\" : 145.6883925043725\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " },\n" + + " {\n" + + " \"elevation\" : 25.49982643127441,\n" + + " \"location\" : {\n" + + " \"lat\" : -37.81411,\n" + + " \"lng\" : 144.96328\n" + + " },\n" + + " \"resolution\" : 152.7032318115234\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + ElevationResult[] results = ElevationApi.getByPath(sc.context, 10, SYDNEY, MELBOURNE).await(); + + assertNotNull(results); + assertEquals(10, results.length); + assertEquals(SYDNEY_ELEVATION, results[0].elevation, EPSILON); + assertEquals(MELBOURNE_ELEVATION, results[9].elevation, EPSILON); + + sc.assertParamValue("10", "samples"); + sc.assertParamValue("enc:xvumEur{y[jyaWdnbe@", "path"); + } + } + + @Test + public void testDirectionsAlongPath() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(directionsAlongPath)) { + ElevationResult[] elevation = ElevationApi.getByPath(sc.context, 100, SYD_MELB_ROUTE).await(); + assertEquals(100, elevation.length); + + List overviewPolylinePath = SYD_MELB_ROUTE.decodePath(); + LatLng lastDirectionsPoint = overviewPolylinePath.get(overviewPolylinePath.size() - 1); + LatLng lastElevationPoint = elevation[elevation.length - 1].location; + + LatLngAssert.assertEquals(lastDirectionsPoint, lastElevationPoint, EPSILON); + + sc.assertParamValue("100", "samples"); + sc.assertParamValue("enc:" + SYD_MELB_ROUTE.getEncodedPath(), "path"); + } } } diff --git a/src/test/java/com/google/maps/GeoApiContextTest.java b/src/test/java/com/google/maps/GeoApiContextTest.java index d8bce7b27..5d9d1bc61 100644 --- a/src/test/java/com/google/maps/GeoApiContextTest.java +++ b/src/test/java/com/google/maps/GeoApiContextTest.java @@ -15,65 +15,91 @@ package com.google.maps; +import static com.google.maps.TestUtils.findLastThreadByName; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; +import com.google.maps.errors.OverQueryLimitException; import com.google.maps.internal.ApiConfig; import com.google.maps.internal.ApiResponse; +import com.google.maps.internal.HttpHeaders; import com.google.maps.model.GeocodingResult; -import com.google.mockwebserver.MockResponse; -import com.google.mockwebserver.MockWebServer; -import com.google.mockwebserver.RecordedRequest; - -import org.junit.Test; -import org.junit.experimental.categories.Category; - import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; +import okhttp3.Headers; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; @Category(MediumTests.class) public class GeoApiContextTest { - private MockWebServer server = new MockWebServer(); - private GeoApiContext context = new GeoApiContext() - .setApiKey("AIza...") - .setQueryRateLimit(500, 0); + private MockWebServer server; + private GeoApiContext.Builder builder; + + @Before + public void Setup() { + server = new MockWebServer(); + builder = new GeoApiContext.Builder().apiKey("AIza...").queryRateLimit(500); + } + + @After + @SuppressWarnings("CatchAndPrintStackTrace") + public void Teardown() { + try { + server.shutdown(); + } catch (IOException e) { + e.printStackTrace(); + } + } private void setMockBaseUrl() { - context.setBaseUrlForTesting("http://127.0.0.1:" + server.getPort()); + builder.baseUrlOverride("http://127.0.0.1:" + server.getPort()); } + @SuppressWarnings("unchecked") @Test public void testGetIncludesDefaultUserAgent() throws Exception { // Set up a mock request - ApiResponse fakeResponse = mock(ApiResponse.class); + ApiResponse fakeResponse = mock(ApiResponse.class); String path = "/"; - Map params = new HashMap(1); - params.put("key", "value"); + Map> params = new HashMap<>(); + params.put("key", Collections.singletonList("value")); // Set up the fake web server server.enqueue(new MockResponse()); - server.play(); + server.start(); setMockBaseUrl(); // Build & execute the request using our context - context.get(new ApiConfig(path), fakeResponse.getClass(), params).awaitIgnoreError(); + builder.build().get(new ApiConfig(path), fakeResponse.getClass(), params).awaitIgnoreError(); // Read the headers server.shutdown(); RecordedRequest request = server.takeRequest(); - List headers = request.getHeaders(); + Headers headers = request.getHeaders(); boolean headerFound = false; - for (String header : headers) { - if (header.startsWith("User-Agent: ")) { + for (String headerName : headers.names()) { + if (headerName.equals("User-Agent")) { headerFound = true; - assertTrue("User agent not in correct format", - header.matches("User-Agent: GoogleGeoApiClientJava/[^\\s]+")); + String headerValue = headers.get(headerName); + assertTrue( + "User agent not in correct format", + headerValue.matches("GoogleGeoApiClientJava/[^\\s]+")); } } @@ -83,63 +109,95 @@ public void testGetIncludesDefaultUserAgent() throws Exception { @Test public void testErrorResponseRetries() throws Exception { // Set up mock responses - MockResponse errorResponse = new MockResponse(); - errorResponse.setStatus("HTTP/1.1 500 Internal server error"); - errorResponse.setBody("Uh-oh. Server Error."); - MockResponse goodResponse = new MockResponse(); - goodResponse.setResponseCode(200); - goodResponse.setBody("{\n" - + " \"results\" : [\n" - + " {\n" - + " \"address_components\" : [\n" - + " {\n" - + " \"long_name\" : \"1600\",\n" - + " \"short_name\" : \"1600\",\n" - + " \"types\" : [ \"street_number\" ]\n" - + " }\n" - + " ],\n" - + " \"formatted_address\" : \"1600 Amphitheatre Parkway, Mountain View, " - + "CA 94043, USA\",\n" - + " \"geometry\" : {\n" - + " \"location\" : {\n" - + " \"lat\" : 37.4220033,\n" - + " \"lng\" : -122.0839778\n" - + " },\n" - + " \"location_type\" : \"ROOFTOP\",\n" - + " \"viewport\" : {\n" - + " \"northeast\" : {\n" - + " \"lat\" : 37.4233522802915,\n" - + " \"lng\" : -122.0826288197085\n" - + " },\n" - + " \"southwest\" : {\n" - + " \"lat\" : 37.4206543197085,\n" - + " \"lng\" : -122.0853267802915\n" - + " }\n" - + " }\n" - + " },\n" - + " \"types\" : [ \"street_address\" ]\n" - + " }\n" - + " ],\n" - + " \"status\" : \"OK\"\n" - + "}"); + MockResponse errorResponse = createMockBadResponse(); + MockResponse goodResponse = createMockGoodResponse(); server.enqueue(errorResponse); server.enqueue(goodResponse); - server.play(); + server.start(); // Build the context under test setMockBaseUrl(); // Execute - GeocodingResult[] result = context.get(new ApiConfig("/"), GeocodingApi.Response.class, - "k", "v").await(); + GeocodingResult[] result = + builder.build().get(new ApiConfig("/"), GeocodingApi.Response.class, "k", "v").await(); assertEquals(1, result.length); - assertEquals("1600 Amphitheatre Parkway, Mountain View, CA 94043, USA", - result[0].formattedAddress); + assertEquals( + "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA", result[0].formattedAddress); server.shutdown(); } + @Test(expected = IOException.class) + public void testSettingMaxRetries() throws Exception { + MockResponse errorResponse = createMockBadResponse(); + MockResponse goodResponse = createMockGoodResponse(); + + // Set up the fake web server + server.enqueue(errorResponse); + server.enqueue(errorResponse); + server.enqueue(errorResponse); + server.enqueue(goodResponse); + server.start(); + setMockBaseUrl(); + + // This should limit the number of retries, ensuring that the success response is NOT returned. + builder.maxRetries(2); + + builder.build().get(new ApiConfig("/"), GeocodingApi.Response.class, "k", "v").await(); + } + + private MockResponse createMockGoodResponse() { + MockResponse response = new MockResponse(); + response.setResponseCode(200); + response.setBody( + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"1600\",\n" + + " \"short_name\" : \"1600\",\n" + + " \"types\" : [ \"street_number\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"1600 Amphitheatre Parkway, Mountain View, " + + "CA 94043, USA\",\n" + + " \"geometry\" : {\n" + + " \"location\" : {\n" + + " \"lat\" : 37.4220033,\n" + + " \"lng\" : -122.0839778\n" + + " },\n" + + " \"location_type\" : \"ROOFTOP\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 37.4233522802915,\n" + + " \"lng\" : -122.0826288197085\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 37.4206543197085,\n" + + " \"lng\" : -122.0853267802915\n" + + " }\n" + + " }\n" + + " },\n" + + " \"types\" : [ \"street_address\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}"); + + return response; + } + + private MockResponse createMockBadResponse() { + MockResponse response = new MockResponse(); + response.setStatus("HTTP/1.1 500 Internal server error"); + response.setBody("Uh-oh. Server Error."); + + return response; + } + @Test(expected = IOException.class) public void testRetryCanBeDisabled() throws Exception { // Set up 2 mock responses, an error that shouldn't be retried and a success @@ -150,20 +208,17 @@ public void testRetryCanBeDisabled() throws Exception { MockResponse goodResponse = new MockResponse(); goodResponse.setResponseCode(200); - goodResponse.setBody("{\n" - + " \"results\" : [],\n" - + " \"status\" : \"ZERO_RESULTS\"\n" - + "}"); + goodResponse.setBody("{\n \"results\" : [],\n \"status\" : \"ZERO_RESULTS\"\n}"); server.enqueue(goodResponse); - server.play(); + server.start(); setMockBaseUrl(); // This should disable the retry, ensuring that the success response is NOT returned - context.setRetryTimeout(0, TimeUnit.MILLISECONDS); + builder.disableRetries(); // We should get the error response here, not the success response. - context.get(new ApiConfig("/"), GeocodingApi.Response.class, "k", "v").await(); + builder.build().get(new ApiConfig("/"), GeocodingApi.Response.class, "k", "v").await(); } @Test @@ -176,14 +231,14 @@ public void testRetryEventuallyReturnsTheRightException() throws Exception { for (int i = 0; i < 10; i++) { server.enqueue(errorResponse); } - server.play(); + server.start(); // Wire the mock web server to the context setMockBaseUrl(); - context.setRetryTimeout(5, TimeUnit.SECONDS); + builder.retryTimeout(5, TimeUnit.SECONDS); try { - context.get(new ApiConfig("/"), GeocodingApi.Response.class, "k", "v").await(); + builder.build().get(new ApiConfig("/"), GeocodingApi.Response.class, "k", "v").await(); } catch (IOException ioe) { // Ensure the message matches the status line in the mock responses. assertEquals("Server Error: 500 Internal server error", ioe.getMessage()); @@ -202,15 +257,157 @@ public void testQueryParamsHaveOrderPreserved() throws Exception { response.setBody("{}"); server.enqueue(response); - server.play(); + server.start(); setMockBaseUrl(); - context.get(new ApiConfig("/"), GeocodingApi.Response.class, - "a", "1", "a", "2", "a", "3").awaitIgnoreError(); + builder + .build() + .get(new ApiConfig("/"), GeocodingApi.Response.class, "a", "1", "a", "2", "a", "3") + .awaitIgnoreError(); server.shutdown(); RecordedRequest request = server.takeRequest(); String path = request.getPath(); assertTrue(path.contains("a=1&a=2&a=3")); } + + @Test + public void testToggleIfExceptionIsAllowedToRetry() throws Exception { + // Enqueue some error responses, although only the first should be used because the response's + // exception is not allowed to be retried. + MockResponse overQueryLimitResponse = new MockResponse(); + overQueryLimitResponse.setStatus("HTTP/1.1 400 Internal server error"); + overQueryLimitResponse.setBody(TestUtils.retrieveBody("OverQueryLimitResponse.json")); + server.enqueue(overQueryLimitResponse); + server.enqueue(overQueryLimitResponse); + server.enqueue(overQueryLimitResponse); + server.start(); + + builder.retryTimeout(1, TimeUnit.MILLISECONDS); + builder.maxRetries(10); + builder.setIfExceptionIsAllowedToRetry(OverQueryLimitException.class, false); + + setMockBaseUrl(); + + try { + builder + .build() + .get(new ApiConfig("/"), GeocodingApi.Response.class, "any-key", "any-value") + .await(); + } catch (OverQueryLimitException e) { + assertEquals(1, server.getRequestCount()); + return; + } + + fail("OverQueryLimitException was expected but not observed."); + } + + @Test + public void testSingleExperienceId() { + final String experienceId = "experienceId"; + final GeoApiContext context = builder.experienceId(experienceId).build(); + assertEquals(experienceId, context.getExperienceId()); + } + + @Test + public void testMultipleExperienceId() { + final String experienceId1 = "experienceId1"; + final String experienceId2 = "experienceId2"; + final GeoApiContext context = builder.experienceId(experienceId1, experienceId2).build(); + assertEquals(experienceId1 + "," + experienceId2, context.getExperienceId()); + } + + @Test + public void testNoExperienceId() { + final GeoApiContext context = builder.build(); + assertNull(context.getExperienceId()); + } + + @Test + public void testClearingExperienceId() { + final String experienceId = "experienceId"; + final GeoApiContext context = builder.experienceId(experienceId).build(); + assertEquals(experienceId, context.getExperienceId()); + + context.clearExperienceId(); + assertNull(context.getExperienceId()); + } + + @Test + public void testExperienceIdIsInHeader() throws Exception { + final String experienceId = "exp1"; + final RecordedRequest request = makeMockRequest(experienceId); + assertEquals(experienceId, request.getHeader(HttpHeaders.X_GOOG_MAPS_EXPERIENCE_ID)); + } + + @Test + public void testExperienceIdNotInHeader() throws Exception { + final RecordedRequest request = makeMockRequest(); + final String value = request.getHeader(HttpHeaders.X_GOOG_MAPS_EXPERIENCE_ID); + assertNull(value); + } + + @Test + public void testExperienceIdSample() { + // [START maps_experience_id] + final String experienceId = UUID.randomUUID().toString(); + + // instantiate context with experience id + final GeoApiContext context = + new GeoApiContext.Builder().apiKey("AIza-Maps-API-Key").experienceId(experienceId).build(); + + // clear the current experience id + context.clearExperienceId(); + + // set a new experience id + final String otherExperienceId = UUID.randomUUID().toString(); + context.setExperienceId(experienceId, otherExperienceId); + + // make API request, the client will set the header + // X-GOOG-MAPS-EXPERIENCE-ID: experienceId,otherExperienceId + + // get current experience id + final String ids = context.getExperienceId(); + // [END maps_experience_id] + + assertEquals(experienceId + "," + otherExperienceId, ids); + } + + @SuppressWarnings("unchecked") + private RecordedRequest makeMockRequest(String... experienceId) throws Exception { + // Set up a mock request + ApiResponse fakeResponse = mock(ApiResponse.class); + String path = "/"; + Map> params = new HashMap<>(); + params.put("key", Collections.singletonList("value")); + + // Set up the fake web server + server.enqueue(new MockResponse()); + server.start(); + setMockBaseUrl(); + + // Build & execute the request using our context + final GeoApiContext context = builder.experienceId(experienceId).build(); + context.get(new ApiConfig(path), fakeResponse.getClass(), params).awaitIgnoreError(); + + // Read the header + server.shutdown(); + return server.takeRequest(); + } + + @Test + public void testShutdown() throws InterruptedException { + GeoApiContext context = builder.build(); + final Thread delayThread = findLastThreadByName("RateLimitExecutorDelayThread"); + assertNotNull( + "Delay thread should be created in constructor of RateLimitExecutorService", delayThread); + assertTrue( + "Delay thread should start in constructor of RateLimitExecutorService", + delayThread.isAlive()); + // this is needed to make sure that delay thread has reached queue.take() + delayThread.join(10); + context.shutdown(); + delayThread.join(10); + assertFalse(delayThread.isAlive()); + } } diff --git a/src/test/java/com/google/maps/GeocodingApiTest.java b/src/test/java/com/google/maps/GeocodingApiTest.java index 4c71664c5..3f03f6f40 100644 --- a/src/test/java/com/google/maps/GeocodingApiTest.java +++ b/src/test/java/com/google/maps/GeocodingApiTest.java @@ -15,81 +15,111 @@ package com.google.maps; -import static com.google.maps.GeocodingApi.ComponentFilter.administrativeArea; -import static com.google.maps.GeocodingApi.ComponentFilter.country; +import static com.google.maps.TestUtils.retrieveBody; +import static com.google.maps.model.ComponentFilter.administrativeArea; +import static com.google.maps.model.ComponentFilter.country; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.maps.GeocodingApi.ComponentFilter; import com.google.maps.model.AddressComponentType; import com.google.maps.model.AddressType; +import com.google.maps.model.ComponentFilter; import com.google.maps.model.GeocodingResult; import com.google.maps.model.LatLng; import com.google.maps.model.LocationType; - +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; +@Category(MediumTests.class) +public class GeocodingApiTest { -@Category(LargeTests.class) -public class GeocodingApiTest extends AuthenticatedTest { + private static final double EPSILON = 0.005; + private static String simpleGeocodeResponse; + private static String placeGeocodeResponse; + private static String reverseGeocodeResponse; + private static String simpleReverseGeocodeResponse; + private static String utfResultGeocodeResponse; + private static String reverseGeocodeWithKitaWardResponse; + private static String geocodeLibraryType; - public static final double EPSILON = 0.000001; + public GeocodingApiTest() { + simpleGeocodeResponse = retrieveBody("SimpleGeocodeResponse.json"); + placeGeocodeResponse = retrieveBody("PlaceGeocodeResponse.json"); + reverseGeocodeResponse = retrieveBody("ReverseGeocodeResponse.json"); + simpleReverseGeocodeResponse = retrieveBody("SimpleReverseGeocodeResponse.json"); + utfResultGeocodeResponse = retrieveBody("UtfResultGeocodeResponse.json"); + reverseGeocodeWithKitaWardResponse = retrieveBody("ReverseGeocodeWithKitaWardResponse.json"); + geocodeLibraryType = retrieveBody("GeocodeLibraryType.json"); + } - private GeoApiContext context; + @Test + public void testGeocodeLibraryType() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geocodeLibraryType)) { + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).address("80 FR").await(); - public GeocodingApiTest(GeoApiContext context) { - this.context = context - .setQueryRateLimit(3) - .setConnectTimeout(1, TimeUnit.SECONDS) - .setReadTimeout(1, TimeUnit.SECONDS) - .setWriteTimeout(1, TimeUnit.SECONDS); + assertEquals(1, results.length); + assertEquals(3, results[0].types.length); + assertEquals(AddressType.ESTABLISHMENT, results[0].types[0]); + assertEquals(AddressType.LIBRARY, results[0].types[1]); + assertEquals(AddressType.POINT_OF_INTEREST, results[0].types[2]); + assertNotNull(Arrays.toString(results)); + } } @Test public void testSimpleGeocode() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context).address("Sydney").await(); - checkSydneyResult(results); + try (LocalTestServerContext sc = new LocalTestServerContext(simpleGeocodeResponse)) { + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).address("Sydney").await(); + checkSydneyResult(results); + assertNotNull(Arrays.toString(results)); + + sc.assertParamValue("Sydney", "address"); + } } @Test public void testPlaceGeocode() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .place("ChIJP3Sa8ziYEmsRUKgyFmh9AQM") - .await(); - checkSydneyResult(results); + try (LocalTestServerContext sc = new LocalTestServerContext(placeGeocodeResponse)) { + String placeID = "ChIJP3Sa8ziYEmsRUKgyFmh9AQM"; + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).place(placeID).await(); + checkSydneyResult(results); + + sc.assertParamValue(placeID, "place_id"); + } } @Test public void testAsync() throws Exception { - final List resps = new ArrayList(); - - PendingResult.Callback callback = - new PendingResult.Callback() { - @Override - public void onResult(GeocodingResult[] result) { - resps.add(result); - } - - @Override - public void onFailure(Throwable e) { - fail("Got error when expected success."); - } - }; - GeocodingApi.newRequest(context).address("Sydney").setCallback(callback); - - Thread.sleep(2500); - - assertFalse(resps.isEmpty()); - assertNotNull(resps.get(0)); - checkSydneyResult(resps.get(0)); + try (LocalTestServerContext sc = new LocalTestServerContext(simpleGeocodeResponse)) { + final List resps = new ArrayList<>(); + + PendingResult.Callback callback = + new PendingResult.Callback() { + @Override + public void onResult(GeocodingResult[] result) { + resps.add(result); + } + + @Override + public void onFailure(Throwable e) { + fail("Got error when expected success."); + } + }; + GeocodingApi.newRequest(sc.context).address("Sydney").setCallback(callback); + + Thread.sleep(2500); + + assertFalse(resps.isEmpty()); + assertNotNull(resps.get(0)); + checkSydneyResult(resps.get(0)); + + sc.assertParamValue("Sydney", "address"); + } } private void checkSydneyResult(GeocodingResult[] results) { @@ -98,178 +128,997 @@ private void checkSydneyResult(GeocodingResult[] results) { assertNotNull(results[0].geometry.location); assertEquals(-33.8674869, results[0].geometry.location.lat, EPSILON); assertEquals(151.2069902, results[0].geometry.location.lng, EPSILON); - assertEquals("ChIJP3Sa8ziYEmsRUKgyFmh9AQM", results[0].placeId); assertEquals(LocationType.APPROXIMATE, results[0].geometry.locationType); } - @Test - public void testBadKey() throws Exception { - GeoApiContext badContext = new GeoApiContext() - .setApiKey("AIza........."); - - GeocodingResult[] results = GeocodingApi.newRequest(badContext).address("Sydney") - .awaitIgnoreError(); - assertNull(results); - - try { - results = GeocodingApi.newRequest(badContext).address("Sydney").await(); - assertNull(results); - fail("Expected exception REQUEST_DENIED"); - } catch (Exception e) { - assertEquals("The provided API key is invalid.", e.getMessage()); - } - } - @Test public void testReverseGeocode() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .latlng(new LatLng(-33.8674869, 151.2069902)).await(); + try (LocalTestServerContext sc = new LocalTestServerContext(reverseGeocodeResponse)) { + LatLng latlng = new LatLng(-33.8674869, 151.2069902); + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).latlng(latlng).await(); - assertTrue("Address didn't contain 'Sydney'", - results[0].formattedAddress.contains("Sydney")); + assertEquals(10, results.length); + assertEquals("343 George St, Sydney NSW 2000, Australia", results[0].formattedAddress); + assertEquals( + "York St Near Barrack St, Sydney NSW 2017, Australia", results[1].formattedAddress); + assertEquals("Sydney NSW 2000, Australia", results[2].formattedAddress); + + sc.assertParamValue(latlng.toUrlValue(), "latlng"); + } } /** - * Simple geocode sample: - * + * Simple geocode sample: * Address Geocode for "1600 Amphitheatre Parkway, Mountain View, CA". */ @Test public void testGeocodeTheGoogleplex() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .address("1600 Amphitheatre Parkway, Mountain View, CA").await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Google Building 41\",\n" + + " \"short_name\" : \"Google Bldg 41\",\n" + + " \"types\" : [ \"premise\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"1600\",\n" + + " \"short_name\" : \"1600\",\n" + + " \"types\" : [ \"street_number\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Amphitheatre Parkway\",\n" + + " \"short_name\" : \"Amphitheatre Pkwy\",\n" + + " \"types\" : [ \"route\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Mountain View\",\n" + + " \"short_name\" : \"Mountain View\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Santa Clara County\",\n" + + " \"short_name\" : \"Santa Clara County\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"California\",\n" + + " \"short_name\" : \"CA\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"94043\",\n" + + " \"short_name\" : \"94043\",\n" + + " \"types\" : [ \"postal_code\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Google Bldg 41, 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 37.4228642,\n" + + " \"lng\" : -122.0851557\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 37.4221145,\n" + + " \"lng\" : -122.0859841\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 37.4224082,\n" + + " \"lng\" : -122.0856086\n" + + " },\n" + + " \"location_type\" : \"ROOFTOP\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 37.4238383302915,\n" + + " \"lng\" : -122.0842209197085\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 37.4211403697085,\n" + + " \"lng\" : -122.0869188802915\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJxQvW8wK6j4AR3ukttGy3w2s\",\n" + + " \"types\" : [ \"premise\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + String address = "1600 Amphitheatre Parkway, Mountain View, CA"; + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).address(address).await(); - assertNotNull(results); - assertEquals("1600 Amphitheatre Parkway, Mountain View, CA 94043, USA", - results[0].formattedAddress); + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals( + "Google Bldg 41, 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA", + results[0].formattedAddress); + sc.assertParamValue(address, "address"); + } } /** - * Address geocode with bounds: - * + * Address geocode with bounds: * Winnetka within (34.172684,-118.604794) - (34.236144,-118.500938). */ @Test public void testGeocodeWithBounds() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context).address("Winnetka") - .bounds(new LatLng(34.172684, -118.604794), new LatLng(34.236144, -118.500938)).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Winnetka\",\n" + + " \"short_name\" : \"Winnetka\",\n" + + " \"types\" : [ \"neighborhood\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Los Angeles\",\n" + + " \"short_name\" : \"Los Angeles\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Los Angeles County\",\n" + + " \"short_name\" : \"Los Angeles County\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"California\",\n" + + " \"short_name\" : \"CA\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Winnetka, Los Angeles, CA, USA\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 34.2355209,\n" + + " \"lng\" : -118.5534191\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 34.1854649,\n" + + " \"lng\" : -118.588536\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 34.2048586,\n" + + " \"lng\" : -118.5739621\n" + + " },\n" + + " \"location_type\" : \"APPROXIMATE\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 34.2355209,\n" + + " \"lng\" : -118.5534191\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 34.1854649,\n" + + " \"lng\" : -118.588536\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJ0fd4S_KbwoAR2hRDrsr3HmQ\",\n" + + " \"types\" : [ \"neighborhood\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context) + .address("Winnetka") + .bounds(new LatLng(34.172684, -118.604794), new LatLng(34.236144, -118.500938)) + .await(); - assertNotNull(results); - assertEquals("Winnetka, Los Angeles, CA, USA", results[0].formattedAddress); + assertNotNull(Arrays.toString(results)); + + assertEquals("Winnetka, Los Angeles, CA, USA", results[0].formattedAddress); + assertEquals("ChIJ0fd4S_KbwoAR2hRDrsr3HmQ", results[0].placeId); + + sc.assertParamValue("Winnetka", "address"); + sc.assertParamValue("34.17268400,-118.60479400|34.23614400,-118.50093800", "bounds"); + } } /** - * Geocode with region biasing: - * Geocode for + * Toledo in Spain. */ @Test public void testGeocodeWithRegionBiasing() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context).address("Toledo").region("es") - .await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Toledo\",\n" + + " \"short_name\" : \"Toledo\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Toledo\",\n" + + " \"short_name\" : \"TO\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Castile-La Mancha\",\n" + + " \"short_name\" : \"CM\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Spain\",\n" + + " \"short_name\" : \"ES\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Toledo, Spain\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 39.88605099999999,\n" + + " \"lng\" : -3.9192423\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 39.8383676,\n" + + " \"lng\" : -4.0796176\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 39.8628316,\n" + + " \"lng\" : -4.027323099999999\n" + + " },\n" + + " \"location_type\" : \"APPROXIMATE\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 39.88605099999999,\n" + + " \"lng\" : -3.9192423\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 39.8383676,\n" + + " \"lng\" : -4.0796176\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJ8f21C60Lag0R_q11auhbf8Y\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context).address("Toledo").region("es").await(); - assertNotNull(results); - assertEquals("Toledo, Toledo, Spain", results[0].formattedAddress); + assertNotNull(Arrays.toString(results)); + + assertNotNull(results); + assertEquals("Toledo, Spain", results[0].formattedAddress); + assertEquals(AddressType.LOCALITY, results[0].types[0]); + assertEquals(AddressType.POLITICAL, results[0].types[1]); + + sc.assertParamValue("Toledo", "address"); + sc.assertParamValue("es", "region"); + } } /** - * Geocode with component filtering: - * + * Geocode with component filtering: * Geocoding "santa cruz" with country set to ES. */ @Test public void testGeocodeWithComponentFilter() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context).address("santa cruz") - .components(GeocodingApi.ComponentFilter.country("ES")).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Santa Cruz de Tenerife\",\n" + + " \"short_name\" : \"Santa Cruz de Tenerife\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Santa Cruz de Tenerife\",\n" + + " \"short_name\" : \"TF\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Canary Islands\",\n" + + " \"short_name\" : \"CN\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Spain\",\n" + + " \"short_name\" : \"ES\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Santa Cruz de Tenerife, Spain\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 28.487616,\n" + + " \"lng\" : -16.2356646\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 28.4280248,\n" + + " \"lng\" : -16.3370045\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 28.4636296,\n" + + " \"lng\" : -16.2518467\n" + + " },\n" + + " \"location_type\" : \"APPROXIMATE\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 28.487616,\n" + + " \"lng\" : -16.2356646\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 28.4280248,\n" + + " \"lng\" : -16.3370045\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJcUElzOzMQQwRLuV30nMUEUM\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context) + .address("santa cruz") + .components(ComponentFilter.country("ES")) + .await(); - assertNotNull(results); - assertEquals("Santa Cruz de Tenerife, Santa Cruz de Tenerife, Spain", - results[0].formattedAddress); + assertNotNull(Arrays.toString(results)); + + assertEquals("Santa Cruz de Tenerife, Spain", results[0].formattedAddress); + assertEquals("ChIJcUElzOzMQQwRLuV30nMUEUM", results[0].placeId); + + sc.assertParamValue("country:ES", "components"); + sc.assertParamValue("santa cruz", "address"); + } } /** - * Geocode with multiple component filters: - * + * Geocode with multiple component filters: * Geocoding Torun, with administrative area of "TX" and country of "US". */ @Test public void testGeocodeWithMultipleComponentFilters() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context).address("Torun") - .components(administrativeArea("TX"), country("US")).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Texas\",\n" + + " \"short_name\" : \"TX\",\n" + + " \"types\" : [\n" + + " \"administrative_area_level_1\",\n" + + " \"establishment\",\n" + + " \"point_of_interest\",\n" + + " \"political\"\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Texas, USA\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 36.5007041,\n" + + " \"lng\" : -93.5080389\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 25.8371638,\n" + + " \"lng\" : -106.6456461\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 31.9685988,\n" + + " \"lng\" : -99.9018131\n" + + " },\n" + + " \"location_type\" : \"APPROXIMATE\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 36.5018864,\n" + + " \"lng\" : -93.5080389\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 25.83819,\n" + + " \"lng\" : -106.6452951\n" + + " }\n" + + " }\n" + + " },\n" + + " \"partial_match\" : true,\n" + + " \"place_id\" : \"ChIJSTKCCzZwQIYRPN4IGI8c6xY\",\n" + + " \"types\" : [\n" + + " \"administrative_area_level_1\",\n" + + " \"establishment\",\n" + + " \"point_of_interest\",\n" + + " \"political\"\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context) + .address("Torun") + .components(administrativeArea("TX"), country("US")) + .await(); - assertNotNull(results); - assertEquals("Texas, USA", results[0].formattedAddress); + assertNotNull(Arrays.toString(results)); + + assertEquals("Texas, USA", results[0].formattedAddress); + assertEquals(true, results[0].partialMatch); + assertEquals("ChIJSTKCCzZwQIYRPN4IGI8c6xY", results[0].placeId); + + sc.assertParamValue("administrative_area:TX|country:US", "components"); + sc.assertParamValue("Torun", "address"); + } } /** - * Making a request using just components filter: - * + * Making a request using just components filter: * Searching for a route of Annegatan, in the administrative area of Helsinki, and the country of * Finland . */ @Test public void testGeocodeWithJustComponents() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context).components( - GeocodingApi.ComponentFilter.route("Annegatan"), - GeocodingApi.ComponentFilter.administrativeArea("Helsinki"), - GeocodingApi.ComponentFilter.country("Finland")).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Annankatu\",\n" + + " \"short_name\" : \"Annankatu\",\n" + + " \"types\" : [ \"route\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Helsinki\",\n" + + " \"short_name\" : \"HKI\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Finland\",\n" + + " \"short_name\" : \"FI\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"00101\",\n" + + " \"short_name\" : \"00101\",\n" + + " \"types\" : [ \"postal_code\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Annankatu, 00101 Helsinki, Finland\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 60.168997,\n" + + " \"lng\" : 24.9433353\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 60.16226160000001,\n" + + " \"lng\" : 24.9332897\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 60.1657808,\n" + + " \"lng\" : 24.938451\n" + + " },\n" + + " \"location_type\" : \"GEOMETRIC_CENTER\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 60.168997,\n" + + " \"lng\" : 24.9433353\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 60.16226160000001,\n" + + " \"lng\" : 24.9332897\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"EiBBbm5hbmthdHUsIDAwMTAxIEhlbHNpbmtpLCBTdW9taQ\",\n" + + " \"types\" : [ \"route\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context) + .components( + ComponentFilter.route("Annegatan"), + ComponentFilter.administrativeArea("Helsinki"), + ComponentFilter.country("Finland")) + .await(); - assertNotNull(results); - assertTrue(results[0].formattedAddress.startsWith("Annegatan")); + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals("Annankatu, 00101 Helsinki, Finland", results[0].formattedAddress); + assertEquals("EiBBbm5hbmthdHUsIDAwMTAxIEhlbHNpbmtpLCBTdW9taQ", results[0].placeId); + + sc.assertParamValue( + "route:Annegatan|administrative_area:Helsinki|country:Finland", "components"); + } } /** - * Simple reverse geocoding. - * - * Reverse geocode (40.714224,-73.961452). + * Simple reverse geocoding. Reverse + * geocode (40.714224,-73.961452). */ @Test public void testSimpleReverseGeocode() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .latlng(new LatLng(40.714224, -73.961452)).await(); + try (LocalTestServerContext sc = new LocalTestServerContext(simpleReverseGeocodeResponse)) { + LatLng latlng = new LatLng(40.714224, -73.961452); + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).latlng(latlng).await(); - assertNotNull(results); - assertEquals("277 Bedford Avenue, Brooklyn, NY 11211, USA", results[0].formattedAddress); - assertEquals("277", results[0].addressComponents[0].longName); - assertEquals("277", results[0].addressComponents[0].shortName); - assertEquals(AddressComponentType.STREET_NUMBER, - results[0].addressComponents[0].types[0]); - assertEquals(AddressType.STREET_ADDRESS, results[0].types[0]); + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals("277 Bedford Ave, Brooklyn, NY 11211, USA", results[0].formattedAddress); + assertEquals("277", results[0].addressComponents[0].longName); + assertEquals("277", results[0].addressComponents[0].shortName); + assertEquals(AddressComponentType.STREET_NUMBER, results[0].addressComponents[0].types[0]); + assertEquals(AddressType.STREET_ADDRESS, results[0].types[0]); + + sc.assertParamValue(latlng.toUrlValue(), "latlng"); + } } /** - * Reverse geocode restricted by type: - * + * Reverse geocode restricted by type: * Reverse Geocode (40.714224,-73.961452) with location type of ROOFTOP and result type of * street_address. */ @Test public void testReverseGeocodeRestrictedByType() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .latlng(new LatLng(40.714224, -73.961452)).locationType(LocationType.ROOFTOP) - .resultType(AddressType.STREET_ADDRESS).await(); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"277\",\n" + + " \"short_name\" : \"277\",\n" + + " \"types\" : [ \"street_number\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Bedford Avenue\",\n" + + " \"short_name\" : \"Bedford Ave\",\n" + + " \"types\" : [ \"route\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Williamsburg\",\n" + + " \"short_name\" : \"Williamsburg\",\n" + + " \"types\" : [ \"neighborhood\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Brooklyn\",\n" + + " \"short_name\" : \"Brooklyn\",\n" + + " \"types\" : [ \"political\", \"sublocality\", \"sublocality_level_1\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Kings County\",\n" + + " \"short_name\" : \"Kings County\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"New York\",\n" + + " \"short_name\" : \"NY\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"11211\",\n" + + " \"short_name\" : \"11211\",\n" + + " \"types\" : [ \"postal_code\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"277 Bedford Ave, Brooklyn, NY 11211, USA\",\n" + + " \"geometry\" : {\n" + + " \"location\" : {\n" + + " \"lat\" : 40.7142205,\n" + + " \"lng\" : -73.9612903\n" + + " },\n" + + " \"location_type\" : \"ROOFTOP\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 40.71556948029149,\n" + + " \"lng\" : -73.95994131970849\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 40.7128715197085,\n" + + " \"lng\" : -73.9626392802915\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJd8BlQ2BZwokRAFUEcm_qrcA\",\n" + + " \"types\" : [ \"street_address\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + LatLng latlng = new LatLng(40.714224, -73.961452); + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context) + .latlng(latlng) + .locationType(LocationType.ROOFTOP) + .resultType(AddressType.STREET_ADDRESS) + .await(); - assertNotNull(results); + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals("277 Bedford Ave, Brooklyn, NY 11211, USA", results[0].formattedAddress); + assertEquals(LocationType.ROOFTOP, results[0].geometry.locationType); + assertEquals("ChIJd8BlQ2BZwokRAFUEcm_qrcA", results[0].placeId); + + sc.assertParamValue(latlng.toUrlValue(), "latlng"); + sc.assertParamValue(LocationType.ROOFTOP.toUrlValue(), "location_type"); + sc.assertParamValue(AddressType.STREET_ADDRESS.toUrlValue(), "result_type"); + } + } + + /** Testing UTF8 result parsing. */ + @Test + public void testUtfResult() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(utfResultGeocodeResponse)) { + LatLng location = new LatLng(46.8023388, 1.6551867); + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).latlng(location).await(); + assertEquals("1 Rue Fernand Raynaud, 36000 Châteauroux, France", results[0].formattedAddress); + sc.assertParamValue(location.toUrlValue(), "latlng"); + } } /** - * Testing partial match. + * Testing custom parameter pass through. + * + *

    See + * Address Geocoding in the Google Maps APIs for the reasoning behind this usage. */ @Test - public void testPartialMatch() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .address("Pirrama Pyrmont").await(); + public void testCustomParameterPassThrough() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"Google Building 41\",\n" + + " \"short_name\" : \"Google Bldg 41\",\n" + + " \"types\" : [ \"premise\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"1600\",\n" + + " \"short_name\" : \"1600\",\n" + + " \"types\" : [ \"street_number\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Amphitheatre Parkway\",\n" + + " \"short_name\" : \"Amphitheatre Pkwy\",\n" + + " \"types\" : [ \"route\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Mountain View\",\n" + + " \"short_name\" : \"Mountain View\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Santa Clara County\",\n" + + " \"short_name\" : \"Santa Clara County\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"California\",\n" + + " \"short_name\" : \"CA\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"94043\",\n" + + " \"short_name\" : \"94043\",\n" + + " \"types\" : [ \"postal_code\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"Google Bldg 41, 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA\",\n" + + " \"geometry\" : {\n" + + " \"bounds\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 37.4228642,\n" + + " \"lng\" : -122.0851557\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 37.4221145,\n" + + " \"lng\" : -122.0859841\n" + + " }\n" + + " },\n" + + " \"location\" : {\n" + + " \"lat\" : 37.4224082,\n" + + " \"lng\" : -122.0856086\n" + + " },\n" + + " \"location_type\" : \"ROOFTOP\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 37.4238383302915,\n" + + " \"lng\" : -122.0842209197085\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 37.4211403697085,\n" + + " \"lng\" : -122.0869188802915\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJxQvW8wK6j4AR3ukttGy3w2s\",\n" + + " \"types\" : [ \"premise\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + String address = "1600 Amphitheatre Parkway, Mountain View, CA"; + GeocodingResult[] results = + GeocodingApi.newRequest(sc.context) + .address(address) + .custom("new_forward_geocoder", "true") + .await(); - assertNotNull(results); - assertTrue(results[0].partialMatch); + assertNotNull(Arrays.toString(results)); + assertEquals( + "Google Bldg 41, 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA", + results[0].formattedAddress); + + sc.assertParamValue(address, "address"); + sc.assertParamValue("true", "new_forward_geocoder"); + } } + /** Testing Kita Ward reverse geocode. */ @Test - public void testUtfResult() throws Exception { - GeocodingResult[] results = GeocodingApi.newRequest(context) - .components(ComponentFilter.postalCode("96766")) - .await(); + public void testReverseGeocodeWithKitaWard() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(reverseGeocodeWithKitaWardResponse)) { + LatLng location = new LatLng(35.03937, 135.729243); + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).latlng(location).await(); + + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals( + "Japan, 〒603-8361 Kyōto-fu, Kyōto-shi, Kita-ku, Kinkakujichō, 1 北山鹿苑寺金閣寺", + results[0].formattedAddress); + assertEquals("Kita Ward", results[3].addressComponents[0].shortName); + assertEquals("Kita Ward", results[3].addressComponents[0].longName); + assertEquals(AddressComponentType.LOCALITY, results[3].addressComponents[0].types[0]); + assertEquals(AddressComponentType.POLITICAL, results[3].addressComponents[0].types[1]); + assertEquals(AddressComponentType.WARD, results[3].addressComponents[0].types[2]); + + sc.assertParamValue(location.toUrlValue(), "latlng"); + } + } + + /** Testing supported Address Types for Geocoding. */ + @Test + public void testSupportedAddressTypesFood() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"21800\",\n" + + " \"short_name\" : \"21800\",\n" + + " \"types\" : [ \"street_number\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"West Eleven Mile Road\",\n" + + " \"short_name\" : \"W Eleven Mile Rd\",\n" + + " \"types\" : [ \"route\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Southfield\",\n" + + " \"short_name\" : \"Southfield\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Oakland County\",\n" + + " \"short_name\" : \"Oakland County\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Michigan\",\n" + + " \"short_name\" : \"MI\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"48076\",\n" + + " \"short_name\" : \"48076\",\n" + + " \"types\" : [ \"postal_code\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"21800 W Eleven Mile Rd, Southfield, MI 48076, USA\",\n" + + " \"geometry\" : {\n" + + " \"location\" : {\n" + + " \"lat\" : 42.4879618,\n" + + " \"lng\" : -83.2595228\n" + + " },\n" + + " \"location_type\" : \"ROOFTOP\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 42.4893107802915,\n" + + " \"lng\" : -83.25817381970849\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 42.4866128197085,\n" + + " \"lng\" : -83.26087178029151\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJ6zOD5dy3JIgRgsMEeGna5dI\",\n" + + " \"types\" : [\n" + + " \"establishment\",\n" + + " \"food\",\n" + + " \"grocery_or_supermarket\",\n" + + " \"point_of_interest\",\n" + + " \"store\"\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + String address = "Noah's Marketplace, 21800 W Eleven Mile Rd"; + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).address(address).await(); + + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals(AddressType.ESTABLISHMENT, results[0].types[0]); + assertEquals(AddressType.FOOD, results[0].types[1]); + assertEquals(AddressType.GROCERY_OR_SUPERMARKET, results[0].types[2]); + assertEquals(AddressType.POINT_OF_INTEREST, results[0].types[3]); + assertEquals(AddressType.STORE, results[0].types[4]); + + sc.assertParamValue(address, "address"); + } + } - assertEquals("Līhuʻe, HI 96766, USA", results[0].formattedAddress); + /** Testing supported Address Types for Geocoding - Synagogue. */ + @Test + public void testSupportedAddressTypesSynagogue() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"results\" : [\n" + + " {\n" + + " \"address_components\" : [\n" + + " {\n" + + " \"long_name\" : \"15620\",\n" + + " \"short_name\" : \"15620\",\n" + + " \"types\" : [ \"street_number\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"West 10 Mile Road\",\n" + + " \"short_name\" : \"W 10 Mile Rd\",\n" + + " \"types\" : [ \"route\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Southfield\",\n" + + " \"short_name\" : \"Southfield\",\n" + + " \"types\" : [ \"locality\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Oakland County\",\n" + + " \"short_name\" : \"Oakland County\",\n" + + " \"types\" : [ \"administrative_area_level_2\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"Michigan\",\n" + + " \"short_name\" : \"MI\",\n" + + " \"types\" : [ \"administrative_area_level_1\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"United States\",\n" + + " \"short_name\" : \"US\",\n" + + " \"types\" : [ \"country\", \"political\" ]\n" + + " },\n" + + " {\n" + + " \"long_name\" : \"48075\",\n" + + " \"short_name\" : \"48075\",\n" + + " \"types\" : [ \"postal_code\" ]\n" + + " }\n" + + " ],\n" + + " \"formatted_address\" : \"15620 W 10 Mile Rd, Southfield, MI 48075, USA\",\n" + + " \"geometry\" : {\n" + + " \"location\" : {\n" + + " \"lat\" : 42.4742111,\n" + + " \"lng\" : -83.2046522\n" + + " },\n" + + " \"location_type\" : \"ROOFTOP\",\n" + + " \"viewport\" : {\n" + + " \"northeast\" : {\n" + + " \"lat\" : 42.4755600802915,\n" + + " \"lng\" : -83.20330321970849\n" + + " },\n" + + " \"southwest\" : {\n" + + " \"lat\" : 42.4728621197085,\n" + + " \"lng\" : -83.20600118029151\n" + + " }\n" + + " }\n" + + " },\n" + + " \"place_id\" : \"ChIJn5hABPnIJIgRr_d3wqujFS0\",\n" + + " \"types\" : [ \"establishment\", \"place_of_worship\", \"point_of_interest\", \"synagogue\" ]\n" + + " }\n" + + " ],\n" + + " \"status\" : \"OK\"\n" + + "}\n")) { + String address = "Ahavas Olam, 15620 W. Ten Mile Road"; + GeocodingResult[] results = GeocodingApi.newRequest(sc.context).address(address).await(); + + assertNotNull(results); + assertNotNull(Arrays.toString(results)); + assertEquals(AddressType.ESTABLISHMENT, results[0].types[0]); + assertEquals(AddressType.PLACE_OF_WORSHIP, results[0].types[1]); + assertEquals(AddressType.POINT_OF_INTEREST, results[0].types[2]); + assertEquals(AddressType.SYNAGOGUE, results[0].types[3]); + + sc.assertParamValue(address, "address"); + } } } diff --git a/src/test/java/com/google/maps/GeolocationApiTest.java b/src/test/java/com/google/maps/GeolocationApiTest.java new file mode 100644 index 000000000..8388e17c0 --- /dev/null +++ b/src/test/java/com/google/maps/GeolocationApiTest.java @@ -0,0 +1,438 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static com.google.maps.TestUtils.retrieveBody; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.google.maps.errors.InvalidRequestException; +import com.google.maps.errors.NotFoundException; +import com.google.maps.model.CellTower; +import com.google.maps.model.GeolocationPayload; +import com.google.maps.model.GeolocationResult; +import com.google.maps.model.WifiAccessPoint; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category(LargeTests.class) +public class GeolocationApiTest { + + private final String geolocationDocSample; + private final String geolocationMinimumWifi; + private final String geolocationBasic; + private final String geolocationMaximumWifi; + private final String geolocationMinimumCellTower; + private final String geolocationAlternatePayloadBuilder; + private final String geolocationMaximumCellTower; + + public GeolocationApiTest() { + geolocationDocSample = retrieveBody("GeolocationDocSampleResponse.json"); + geolocationMinimumWifi = retrieveBody("GeolocationMinimumWifiResponse.json"); + geolocationBasic = retrieveBody("GeolocationBasicResponse.json"); + geolocationMaximumWifi = retrieveBody("GeolocationMaximumWifiResponse.json"); + geolocationMinimumCellTower = retrieveBody("GeolocationMinimumCellTowerResponse.json"); + geolocationAlternatePayloadBuilder = retrieveBody("GeolocationAlternatePayloadBuilder.json"); + geolocationMaximumCellTower = retrieveBody("GeolocationMaximumCellTower.json"); + } + + @Test + public void testDocSampleGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationDocSample)) { + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .HomeMobileCountryCode(310) + .HomeMobileNetworkCode(260) + .RadioType("gsm") + .Carrier("T-Mobile") + .AddCellTower( + new CellTower.CellTowerBuilder() + .CellId(39627456) + .LocationAreaCode(40495) + .MobileCountryCode(310) + .MobileNetworkCode(260) + .Age(0) + .SignalStrength(-95) + .createCellTower()) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("01:23:45:67:89:AB") + .SignalStrength(-65) + .SignalToNoiseRatio(8) + .Channel(8) + .Age(0) + .createWifiAccessPoint()) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("01:23:45:67:89:AC") + .SignalStrength(4) + .SignalToNoiseRatio(4) + .Age(0) + .createWifiAccessPoint()) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + assertEquals(310, body.get("homeMobileCountryCode")); + assertEquals(260, body.get("homeMobileNetworkCode")); + assertEquals("gsm", body.get("radioType")); + assertEquals("T-Mobile", body.get("carrier")); + assertEquals("accuracy", 1145.0, result.accuracy, 0.00001); + assertEquals("lat", 37.4248297, result.location.lat, 0.00001); + assertEquals("lng", -122.07346549999998, result.location.lng, 0.00001); + } + } + + @Test + public void testMinimumWifiGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationMinimumWifi)) { + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:6b:11") + .createWifiAccessPoint()) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:6b:10") + .createWifiAccessPoint()) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + JSONArray wifiAccessPoints = body.getJSONArray("wifiAccessPoints"); + assertEquals("94:b4:0f:ff:6b:11", wifiAccessPoints.getJSONObject(0).get("macAddress")); + assertEquals("94:b4:0f:ff:6b:10", wifiAccessPoints.getJSONObject(1).get("macAddress")); + assertEquals("accuracy", 150.0, result.accuracy, 0.001); + assertEquals("lat", 37.3989885, result.location.lat, 0.001); + assertEquals("lng", -122.0585196, result.location.lng, 0.001); + } + } + + @Test + public void testBasicGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationBasic)) { + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("92:68:c3:f8:76:47") + .SignalStrength(-42) + .SignalToNoiseRatio(68) + .createWifiAccessPoint()) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:6b:11") + .SignalStrength(-55) + .SignalToNoiseRatio(55) + .createWifiAccessPoint()) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + JSONArray wifiAccessPoints = body.getJSONArray("wifiAccessPoints"); + JSONObject wifi0 = wifiAccessPoints.getJSONObject(0); + JSONObject wifi1 = wifiAccessPoints.getJSONObject(1); + assertEquals("92:68:c3:f8:76:47", wifi0.get("macAddress")); + assertEquals(-42, wifi0.get("signalStrength")); + assertEquals(68, wifi0.get("signalToNoiseRatio")); + assertEquals("94:b4:0f:ff:6b:11", wifi1.get("macAddress")); + assertEquals(-55, wifi1.get("signalStrength")); + assertEquals(55, wifi1.get("signalToNoiseRatio")); + assertEquals("accuracy", 150.0, result.accuracy, 0.00001); + assertEquals("lat", 37.3989885, result.location.lat, 0.00001); + assertEquals("lng", -122.0585196, result.location.lng, 0.00001); + } + } + + @Test + public void testAlternateWifiSetterGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationBasic)) { + WifiAccessPoint[] wifiAccessPoints = new WifiAccessPoint[2]; + wifiAccessPoints[0] = + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:6b:11") + .createWifiAccessPoint(); + wifiAccessPoints[1] = + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:6b:10") + .createWifiAccessPoint(); + + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .WifiAccessPoints(wifiAccessPoints) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + JSONArray wifiAccessPointsResponse = body.getJSONArray("wifiAccessPoints"); + JSONObject wifi0 = wifiAccessPointsResponse.getJSONObject(0); + JSONObject wifi1 = wifiAccessPointsResponse.getJSONObject(1); + assertEquals("94:b4:0f:ff:6b:11", wifi0.get("macAddress")); + assertEquals("94:b4:0f:ff:6b:10", wifi1.get("macAddress")); + assertEquals("accuracy", 150.0, result.accuracy, 0.001); + assertEquals("lat", 37.3989885, result.location.lat, 0.001); + assertEquals("lng", -122.0585196, result.location.lng, 0.001); + } + } + + @Test + public void testMaximumWifiGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationMaximumWifi)) { + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .HomeMobileCountryCode(310) + .HomeMobileNetworkCode(410) + .RadioType("gsm") + .Carrier("Vodafone") + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:88:31") + .SignalStrength(-61) + .SignalToNoiseRatio(49) + .Channel(40) + .Age(0) + .createWifiAccessPoint()) + .AddWifiAccessPoint( + new WifiAccessPoint.WifiAccessPointBuilder() + .MacAddress("94:b4:0f:ff:88:30") + .SignalStrength(-64) + .SignalToNoiseRatio(46) + .Channel(40) + .Age(0) + .createWifiAccessPoint()) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + assertEquals(310, body.get("homeMobileCountryCode")); + assertEquals(410, body.get("homeMobileNetworkCode")); + assertEquals("gsm", body.get("radioType")); + assertEquals("Vodafone", body.get("carrier")); + JSONArray wifiAccessPointsResponse = body.getJSONArray("wifiAccessPoints"); + JSONObject wifi0 = wifiAccessPointsResponse.getJSONObject(0); + assertEquals("94:b4:0f:ff:88:31", wifi0.get("macAddress")); + assertEquals(-61, wifi0.get("signalStrength")); + assertEquals(49, wifi0.get("signalToNoiseRatio")); + assertEquals(40, wifi0.get("channel")); + assertEquals(0, wifi0.get("age")); + JSONObject wifi1 = wifiAccessPointsResponse.getJSONObject(1); + assertEquals("94:b4:0f:ff:88:30", wifi1.get("macAddress")); + assertEquals(-64, wifi1.get("signalStrength")); + assertEquals(46, wifi1.get("signalToNoiseRatio")); + assertEquals(40, wifi1.get("channel")); + assertEquals(0, wifi1.get("age")); + assertEquals("accuracy", 25.0, result.accuracy, 0.00001); + assertEquals("lat", 37.3990122, result.location.lat, 0.00001); + assertEquals("lng", -122.0583656, result.location.lng, 0.00001); + } + } + + @Test + public void testMinimumCellTowerGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationMinimumCellTower)) { + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .AddCellTower( + new CellTower.CellTowerBuilder() + .CellId(39627456) + .LocationAreaCode(40495) + .MobileCountryCode(310) + .MobileNetworkCode(260) + .createCellTower()) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + JSONObject cellTower = body.getJSONArray("cellTowers").getJSONObject(0); + assertEquals(39627456, cellTower.get("cellId")); + assertEquals(40495, cellTower.get("locationAreaCode")); + assertEquals(310, cellTower.get("mobileCountryCode")); + assertEquals(260, cellTower.get("mobileNetworkCode")); + assertEquals("accuracy", 658.0, result.accuracy, 0.00001); + assertEquals("lat", 37.42659, result.location.lat, 0.00001); + assertEquals("lng", -122.07266190000001, result.location.lng, 0.00001); + } + } + + @Test + public void testAlternatePayloadBuilderGeolocation() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(geolocationAlternatePayloadBuilder)) { + GeolocationPayload payload = + new GeolocationPayload.GeolocationPayloadBuilder() + .ConsiderIp(false) + .AddCellTower( + new CellTower.CellTowerBuilder() + .CellId(39627456) + .LocationAreaCode(40495) + .MobileCountryCode(310) + .MobileNetworkCode(260) + .createCellTower()) + .createGeolocationPayload(); + + GeolocationResult result = GeolocationApi.geolocate(sc.context, payload).await(); + assertNotNull(result.toString()); + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + JSONObject cellTower = body.getJSONArray("cellTowers").getJSONObject(0); + assertEquals(39627456, cellTower.get("cellId")); + assertEquals(40495, cellTower.get("locationAreaCode")); + assertEquals(310, cellTower.get("mobileCountryCode")); + assertEquals(260, cellTower.get("mobileNetworkCode")); + assertEquals("accuracy", 658.0, result.accuracy, 0.00001); + assertEquals("lat", 37.42659, result.location.lat, 0.00001); + assertEquals("lng", -122.07266190000001, result.location.lng, 0.00001); + } + } + + @Test + public void testMaximumCellTowerGeolocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationMaximumCellTower)) { + GeolocationResult result = + GeolocationApi.newRequest(sc.context) + .ConsiderIp(false) + .HomeMobileCountryCode(310) + .HomeMobileNetworkCode(260) + .RadioType("gsm") + .Carrier("Vodafone") + .AddCellTower( + new CellTower.CellTowerBuilder() + .CellId(39627456) + .LocationAreaCode(40495) + .MobileCountryCode(310) + .MobileNetworkCode(260) + .Age(0) + .SignalStrength(-103) + .TimingAdvance(15) + .createCellTower()) + .CreatePayload() + .await(); + + assertNotNull(result.toString()); + + JSONObject body = sc.requestBody(); + assertEquals(false, body.get("considerIp")); + assertEquals(310, body.get("homeMobileCountryCode")); + assertEquals(260, body.get("homeMobileNetworkCode")); + assertEquals("gsm", body.get("radioType")); + assertEquals("Vodafone", body.get("carrier")); + JSONObject cellTower = body.getJSONArray("cellTowers").getJSONObject(0); + assertEquals(39627456, cellTower.get("cellId")); + assertEquals(40495, cellTower.get("locationAreaCode")); + assertEquals(310, cellTower.get("mobileCountryCode")); + assertEquals(260, cellTower.get("mobileNetworkCode")); + assertEquals(0, cellTower.get("age")); + assertEquals(-103, cellTower.get("signalStrength")); + assertEquals(15, cellTower.get("timingAdvance")); + assertEquals("accuracy", 1145.0, result.accuracy, 0.00001); + assertEquals("lat", 37.4248297, result.location.lat, 0.00001); + assertEquals("lng", -122.07346549999998, result.location.lng, 0.00001); + } + } + + @Test + public void testNoPayloadGeolocation0() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationBasic)) { + GeolocationPayload payload = + new GeolocationPayload.GeolocationPayloadBuilder().createGeolocationPayload(); + + GeolocationResult result = GeolocationApi.geolocate(sc.context, payload).await(); + assertNotNull(result); + assertNotNull(result.toString()); + assertNotNull(result.location); + } + } + + @Test + public void testNoPayloadGeolocation1() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(geolocationBasic)) { + GeolocationResult result = GeolocationApi.newRequest(sc.context).CreatePayload().await(); + + assertNotNull(result); + assertNotNull(result.toString()); + assertNotNull(result.location); + } + } + + @Test(expected = NotFoundException.class) + public void testNotFoundGeolocation() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "" + + "{\n" + + " \"error\": {\n" + + " \"errors\": [\n" + + " {\n" + + " \"domain\": \"geolocation\",\n" + + " \"reason\": \"notFound\"" + + " }\n" + + " ],\n" + + " \"code\": 404\n" + + " }\n" + + "}")) { + GeolocationApi.newRequest(sc.context).ConsiderIp(false).CreatePayload().await(); + } + } + + @Test(expected = InvalidRequestException.class) + public void testInvalidArgumentGeolocation() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext( + "" + + "{\n" + + " \"error\": {\n" + + " \"errors\": [\n" + + " {\n" + + " \"domain\": \"global\",\n" + + " \"reason\": \"parseError\",\n" + + " \"message\": \"Parse Error\"\n" + + " }\n" + + " ],\n" + + " \"code\": 400,\n" + + " \"message\": \"Parse Error\"\n" + + " }\n" + + "}")) { + GeolocationApi.newRequest(sc.context).HomeMobileCountryCode(-310).CreatePayload().await(); + } + } +} diff --git a/src/test/java/com/google/maps/LargeTests.java b/src/test/java/com/google/maps/LargeTests.java index 785fc62db..cad4f25b8 100644 --- a/src/test/java/com/google/maps/LargeTests.java +++ b/src/test/java/com/google/maps/LargeTests.java @@ -15,8 +15,5 @@ package com.google.maps; -/** - * Large tests in this project generally use remote network calls. - */ -public interface LargeTests { -} +/** Large tests in this project generally use remote network calls. */ +public interface LargeTests {} diff --git a/src/test/java/com/google/maps/LocalTestServerContext.java b/src/test/java/com/google/maps/LocalTestServerContext.java new file mode 100644 index 000000000..3b4626cdb --- /dev/null +++ b/src/test/java/com/google/maps/LocalTestServerContext.java @@ -0,0 +1,143 @@ +/* + * Copyright 2017 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.List; +import javax.imageio.ImageIO; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import okio.Buffer; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.json.JSONObject; + +/** Local test mock server for unit tests. */ +public class LocalTestServerContext implements AutoCloseable { + + private final MockWebServer server; + public final GeoApiContext context; + private RecordedRequest request = null; + private List params = null; + + LocalTestServerContext(BufferedImage image) throws IOException { + this.server = new MockWebServer(); + Buffer buffer = new Buffer(); + ImageIO.write(image, "png", buffer.outputStream()); + MockResponse response = new MockResponse(); + response.setHeader("Content-Type", "image/png"); + response.setBody(buffer); + server.enqueue(response); + server.start(); + + this.context = + new GeoApiContext.Builder() + .apiKey("AIzaFakeKey") + .baseUrlOverride("http://127.0.0.1:" + server.getPort()) + .build(); + } + + LocalTestServerContext(String responseBody) throws IOException { + this.server = new MockWebServer(); + MockResponse response = new MockResponse(); + response.setHeader("Content-Type", "application/json"); + response.setBody(responseBody); + server.enqueue(response); + server.start(); + + this.context = + new GeoApiContext.Builder() + .apiKey("AIzaFakeKey") + .baseUrlOverride("http://127.0.0.1:" + server.getPort()) + .build(); + } + + private List parseQueryParamsFromRequestLine(String requestLine) + throws URISyntaxException { + // Extract the URL part from the HTTP request line + String[] chunks = requestLine.split("\\s", -1); + String url = chunks[1]; + + return URLEncodedUtils.parse(new URI(url), Charset.forName("UTF-8")); + } + + private void takeRequest() throws InterruptedException { + if (this.request == null) this.request = server.takeRequest(); + } + + public JSONObject requestBody() throws InterruptedException { + this.takeRequest(); + + return new JSONObject(request.getBody().readUtf8()); + } + + private List actualParams() throws InterruptedException, URISyntaxException { + this.takeRequest(); + return parseQueryParamsFromRequestLine(request.getRequestLine()); + } + + public String path() throws InterruptedException { + this.takeRequest(); + return request.getPath().split("\\?", -1)[0]; + } + + void assertParamValue(String expected, String paramName) + throws URISyntaxException, InterruptedException { + if (this.params == null) { + this.params = this.actualParams(); + } + boolean paramFound = false; + for (NameValuePair pair : params) { + if (pair.getName().equals(paramName)) { + paramFound = true; + assertEquals(expected, pair.getValue()); + } + } + assertTrue(paramFound); + } + + void assertParamValues(List expecteds, String paramName) + throws URISyntaxException, InterruptedException { + if (this.params == null) { + this.params = this.actualParams(); + } + int paramsFound = 0; + for (NameValuePair pair : params) { + if (pair.getName().equals(paramName)) { + assertEquals(expecteds.get(paramsFound), pair.getValue()); + paramsFound++; + } + } + assertEquals(paramsFound, expecteds.size()); + } + + @Override + public void close() { + try { + server.shutdown(); + } catch (IOException e) { + System.err.println("Failed to close server: " + e); + } + } +} diff --git a/src/test/java/com/google/maps/MediumTests.java b/src/test/java/com/google/maps/MediumTests.java index be1a5a8eb..72405d12c 100644 --- a/src/test/java/com/google/maps/MediumTests.java +++ b/src/test/java/com/google/maps/MediumTests.java @@ -15,8 +15,5 @@ package com.google.maps; -/** - * Medium tests may use local sockets or files, but nothing remote. - */ -public interface MediumTests { -} +/** Medium tests may use local sockets or files, but nothing remote. */ +public interface MediumTests {} diff --git a/src/test/java/com/google/maps/PlacesApiTest.java b/src/test/java/com/google/maps/PlacesApiTest.java new file mode 100644 index 000000000..cdba990c1 --- /dev/null +++ b/src/test/java/com/google/maps/PlacesApiTest.java @@ -0,0 +1,1053 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static com.google.maps.TestUtils.retrieveBody; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.maps.FindPlaceFromTextRequest.InputType; +import com.google.maps.FindPlaceFromTextRequest.LocationBiasCircular; +import com.google.maps.FindPlaceFromTextRequest.LocationBiasIP; +import com.google.maps.FindPlaceFromTextRequest.LocationBiasPoint; +import com.google.maps.FindPlaceFromTextRequest.LocationBiasRectangular; +import com.google.maps.PlaceAutocompleteRequest.SessionToken; +import com.google.maps.PlaceDetailsRequest.FieldMask; +import com.google.maps.model.AddressComponentType; +import com.google.maps.model.AddressType; +import com.google.maps.model.AutocompletePrediction; +import com.google.maps.model.AutocompletePrediction.MatchedSubstring; +import com.google.maps.model.AutocompleteStructuredFormatting; +import com.google.maps.model.ComponentFilter; +import com.google.maps.model.FindPlaceFromText; +import com.google.maps.model.LatLng; +import com.google.maps.model.OpeningHours.Period; +import com.google.maps.model.OpeningHours.Period.OpenClose.DayOfWeek; +import com.google.maps.model.Photo; +import com.google.maps.model.PlaceAutocompleteType; +import com.google.maps.model.PlaceDetails; +import com.google.maps.model.PlaceDetails.Review.AspectRating.RatingType; +import com.google.maps.model.PlaceType; +import com.google.maps.model.PlacesSearchResponse; +import com.google.maps.model.PlacesSearchResult; +import com.google.maps.model.PriceLevel; +import com.google.maps.model.RankBy; +import java.net.URI; +import java.time.LocalTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import org.junit.Test; + +public class PlacesApiTest { + + private static final String GOOGLE_SYDNEY = "ChIJN1t_tDeuEmsRUsoyG83frY4"; + private static final String QUAY_PLACE_ID = "ChIJ02qnq0KuEmsRHUJF4zo1x4I"; + private static final String PERMANENTLY_CLOSED_PLACE_ID = "ChIJZQvy3jAbdkgR9avxegjoCe0"; + private static final String QUERY_AUTOCOMPLETE_INPUT = "pizza near par"; + private static final LatLng SYDNEY = new LatLng(-33.8650, 151.2094); + + private final String autocompletePredictionStructuredFormatting; + private final String placeDetailResponseBody; + private final String placeDetailResponseBodyForPermanentlyClosedPlace; + private final String quayResponseBody; + private final String queryAutocompleteResponseBody; + private final String queryAutocompleteWithPlaceIdResponseBody; + private final String textSearchResponseBody; + private final String textSearchPizzaInNYCbody; + private final String placesApiTextSearch; + private final String placesApiPhoto; + private final String placesApiPizzaInNewYork; + private final String placesApiDetailsInFrench; + private final String placesApiNearbySearchRequestByKeyword; + private final String placesApiNearbySearchRequestByName; + private final String placesApiNearbySearchRequestByType; + private final String placesApiPlaceAutocomplete; + private final String placesApiPlaceAutocompleteWithType; + private final String placesApiKitaWard; + private final String findPlaceFromTextMuseumOfContemporaryArt; + + public PlacesApiTest() { + autocompletePredictionStructuredFormatting = + retrieveBody("AutocompletePredictionStructuredFormatting.json"); + placeDetailResponseBody = retrieveBody("PlaceDetailsResponse.json"); + placeDetailResponseBodyForPermanentlyClosedPlace = + retrieveBody("PlaceDetailsResponseForPermanentlyClosedPlace.json"); + quayResponseBody = retrieveBody("PlaceDetailsQuay.json"); + queryAutocompleteResponseBody = retrieveBody("QueryAutocompleteResponse.json"); + queryAutocompleteWithPlaceIdResponseBody = + retrieveBody("QueryAutocompleteResponseWithPlaceID.json"); + textSearchResponseBody = retrieveBody("TextSearchResponse.json"); + textSearchPizzaInNYCbody = retrieveBody("TextSearchPizzaInNYC.json"); + placesApiTextSearch = retrieveBody("PlacesApiTextSearchResponse.json"); + placesApiPhoto = retrieveBody("PlacesApiPhotoResponse.json"); + placesApiPizzaInNewYork = retrieveBody("PlacesApiPizzaInNewYorkResponse.json"); + placesApiDetailsInFrench = retrieveBody("PlacesApiDetailsInFrenchResponse.json"); + placesApiNearbySearchRequestByKeyword = + retrieveBody("PlacesApiNearbySearchRequestByKeywordResponse.json"); + placesApiNearbySearchRequestByName = + retrieveBody("PlacesApiNearbySearchRequestByNameResponse.json"); + placesApiNearbySearchRequestByType = + retrieveBody("PlacesApiNearbySearchRequestByTypeResponse.json"); + placesApiPlaceAutocomplete = retrieveBody("PlacesApiPlaceAutocompleteResponse.json"); + placesApiPlaceAutocompleteWithType = + retrieveBody("PlacesApiPlaceAutocompleteWithTypeResponse.json"); + placesApiKitaWard = retrieveBody("placesApiKitaWardResponse.json"); + findPlaceFromTextMuseumOfContemporaryArt = + retrieveBody("FindPlaceFromTextMuseumOfContemporaryArt.json"); + } + + @Test + public void testPlaceDetailsRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + PlacesApi.placeDetails(sc.context, GOOGLE_SYDNEY).await(); + + sc.assertParamValue(GOOGLE_SYDNEY, "placeid"); + } + } + + @Test + public void testAutocompletePredictionStructuredFormatting() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(autocompletePredictionStructuredFormatting)) { + SessionToken session = new SessionToken(); + final AutocompletePrediction[] predictions = + PlacesApi.placeAutocomplete(sc.context, "1", session).await(); + + assertNotNull(predictions); + assertNotNull(Arrays.toString(predictions)); + assertEquals(1, predictions.length); + final AutocompletePrediction prediction = predictions[0]; + assertNotNull(prediction); + assertEquals("1033 Princes Highway, Heathmere, Victoria, Australia", prediction.description); + final AutocompleteStructuredFormatting structuredFormatting = prediction.structuredFormatting; + assertNotNull(structuredFormatting); + assertEquals("1033 Princes Highway", structuredFormatting.mainText); + assertEquals("Heathmere, Victoria, Australia", structuredFormatting.secondaryText); + assertEquals(1, structuredFormatting.mainTextMatchedSubstrings.length); + final MatchedSubstring matchedSubstring = structuredFormatting.mainTextMatchedSubstrings[0]; + assertNotNull(matchedSubstring); + assertEquals(1, matchedSubstring.length); + assertEquals(0, matchedSubstring.offset); + } + } + + @Test + public void testPlaceDetailsLookupGoogleSydney() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placeDetailResponseBody)) { + PlaceDetails placeDetails = + PlacesApi.placeDetails(sc.context, GOOGLE_SYDNEY) + .fields( + PlaceDetailsRequest.FieldMask.PLACE_ID, + PlaceDetailsRequest.FieldMask.NAME, + PlaceDetailsRequest.FieldMask.TYPES) + .await(); + + sc.assertParamValue(GOOGLE_SYDNEY, "placeid"); + sc.assertParamValue("place_id,name,types", "fields"); + + assertNotNull(placeDetails); + assertNotNull(placeDetails.toString()); + + // Address + assertNotNull(placeDetails.addressComponents); + assertEquals(placeDetails.addressComponents[0].longName, "5"); + assertEquals(placeDetails.addressComponents[0].types.length, 0); + assertEquals(placeDetails.addressComponents[1].longName, "48"); + assertEquals(placeDetails.addressComponents[1].types[0], AddressComponentType.STREET_NUMBER); + assertEquals(placeDetails.addressComponents[2].longName, "Pirrama Road"); + assertEquals(placeDetails.addressComponents[2].shortName, "Pirrama Rd"); + assertEquals(placeDetails.addressComponents[2].types[0], AddressComponentType.ROUTE); + assertEquals(placeDetails.addressComponents[3].shortName, "Pyrmont"); + assertEquals(placeDetails.addressComponents[3].types[0], AddressComponentType.LOCALITY); + assertEquals(placeDetails.addressComponents[3].types[1], AddressComponentType.POLITICAL); + assertEquals(placeDetails.addressComponents[4].longName, "New South Wales"); + assertEquals(placeDetails.addressComponents[4].shortName, "NSW"); + assertEquals( + placeDetails.addressComponents[4].types[0], + AddressComponentType.ADMINISTRATIVE_AREA_LEVEL_1); + assertEquals(placeDetails.addressComponents[4].types[1], AddressComponentType.POLITICAL); + assertEquals(placeDetails.addressComponents[5].longName, "Australia"); + assertEquals(placeDetails.addressComponents[5].shortName, "AU"); + assertEquals(placeDetails.addressComponents[5].types[0], AddressComponentType.COUNTRY); + assertEquals(placeDetails.addressComponents[5].types[1], AddressComponentType.POLITICAL); + assertEquals(placeDetails.addressComponents[6].shortName, "2009"); + assertEquals(placeDetails.addressComponents[6].types[0], AddressComponentType.POSTAL_CODE); + assertNotNull(placeDetails.formattedAddress); + assertEquals(placeDetails.formattedAddress, "5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia"); + assertNotNull(placeDetails.vicinity); + assertEquals(placeDetails.vicinity, "5 48 Pirrama Road, Pyrmont"); + + // Phone numbers + assertNotNull(placeDetails.formattedPhoneNumber); + assertEquals(placeDetails.formattedPhoneNumber, "(02) 9374 4000"); + assertNotNull(placeDetails.internationalPhoneNumber); + assertEquals(placeDetails.internationalPhoneNumber, "+61 2 9374 4000"); + + // Geometry + assertNotNull(placeDetails.geometry); + assertNotNull(placeDetails.geometry.location); + assertEquals(placeDetails.geometry.location.lat, -33.866611, 0.001); + assertEquals(placeDetails.geometry.location.lng, 151.195832, 0.001); + + // URLs + assertNotNull(placeDetails.icon); + assertEquals( + placeDetails.icon.toURI(), + new URI("https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png")); + assertNotNull(placeDetails.url); + assertEquals( + placeDetails.url.toURI(), + new URI("https://plus.google.com/111337342022929067349/about?hl=en-US")); + assertNotNull(placeDetails.website); + assertEquals( + placeDetails.website.toURI(), + new URI("https://www.google.com.au/about/careers/locations/sydney/")); + + // Name + assertNotNull(placeDetails.name); + assertEquals(placeDetails.name, "Google"); + + // Opening Hours + assertNotNull(placeDetails.openingHours); + assertNotNull(placeDetails.openingHours.openNow); + assertTrue(placeDetails.openingHours.openNow); + assertNotNull(placeDetails.openingHours.periods); + assertEquals(placeDetails.openingHours.periods.length, 5); + + { + Period monday = placeDetails.openingHours.periods[0]; + Period tuesday = placeDetails.openingHours.periods[1]; + Period wednesday = placeDetails.openingHours.periods[2]; + Period thursday = placeDetails.openingHours.periods[3]; + Period friday = placeDetails.openingHours.periods[4]; + + assertEquals(DayOfWeek.MONDAY, monday.open.day); + LocalTime opening = LocalTime.of(8, 30); + LocalTime closing5pm = LocalTime.of(17, 0); + LocalTime closing530pm = LocalTime.of(17, 30); + + assertEquals(opening, monday.open.time); + assertEquals(DayOfWeek.MONDAY, monday.close.day); + assertEquals(closing530pm, monday.close.time); + + assertEquals(DayOfWeek.TUESDAY, tuesday.open.day); + assertEquals(opening, tuesday.open.time); + assertEquals(DayOfWeek.TUESDAY, tuesday.close.day); + assertEquals(closing530pm, tuesday.close.time); + + assertEquals(DayOfWeek.WEDNESDAY, wednesday.open.day); + assertEquals(opening, wednesday.open.time); + assertEquals(DayOfWeek.WEDNESDAY, wednesday.close.day); + assertEquals(closing530pm, wednesday.close.time); + + assertEquals(DayOfWeek.THURSDAY, thursday.open.day); + assertEquals(opening, thursday.open.time); + assertEquals(DayOfWeek.THURSDAY, thursday.close.day); + assertEquals(closing530pm, thursday.close.time); + + assertEquals(DayOfWeek.FRIDAY, friday.open.day); + assertEquals(opening, friday.open.time); + assertEquals(DayOfWeek.FRIDAY, friday.close.day); + assertEquals(closing5pm, friday.close.time); + } + + assertNotNull(placeDetails.openingHours.weekdayText); + assertEquals(placeDetails.openingHours.weekdayText[0], "Monday: 8:30 am – 5:30 pm"); + assertEquals(placeDetails.openingHours.weekdayText[1], "Tuesday: 8:30 am – 5:30 pm"); + assertEquals(placeDetails.openingHours.weekdayText[2], "Wednesday: 8:30 am – 5:30 pm"); + assertEquals(placeDetails.openingHours.weekdayText[3], "Thursday: 8:30 am – 5:30 pm"); + assertEquals(placeDetails.openingHours.weekdayText[4], "Friday: 8:30 am – 5:00 pm"); + assertEquals(placeDetails.openingHours.weekdayText[5], "Saturday: Closed"); + assertEquals(placeDetails.openingHours.weekdayText[6], "Sunday: Closed"); + assertEquals(placeDetails.utcOffset, 600); + + // Photos + assertNotNull(placeDetails.photos); + Photo photo = placeDetails.photos[0]; + assertNotNull(photo); + assertNotNull(photo.photoReference); + assertNotNull(photo.htmlAttributions); + assertNotNull(photo.htmlAttributions[0]); + + // Reviews + assertNotNull(placeDetails.reviews); + PlaceDetails.Review review = placeDetails.reviews[0]; + assertNotNull(review); + assertNotNull(review.authorName); + assertEquals("Danielle Lonnon", review.authorName); + assertNotNull(review.authorUrl); + assertEquals( + new URI("https://plus.google.com/118257578392162991040"), review.authorUrl.toURI()); + assertNotNull(review.profilePhotoUrl); + assertEquals("https://lh5.googleusercontent.com/photo.jpg", review.profilePhotoUrl); + assertNotNull(review.language); + assertEquals("en", review.language); + assertNotNull(review.relativeTimeDescription); + assertEquals("a month ago", review.relativeTimeDescription); + assertEquals(5, review.rating); + assertNotNull(review.text); + assertTrue(review.text.startsWith("As someone who works in the theatre,")); + assertNotNull(review.aspects); + PlaceDetails.Review.AspectRating aspect = review.aspects[0]; + assertNotNull(aspect); + assertEquals(3, aspect.rating); + assertNotNull(aspect.type); + assertEquals(RatingType.OVERALL, aspect.type); + assertEquals(1425790392, review.time.toEpochMilli() / 1000); + assertEquals( + "2015-03-08 04:53 am", + DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm a") + .withZone(ZoneOffset.UTC) + .format(review.time) + .toLowerCase()); + + // Place ID + assertNotNull(placeDetails.placeId); + assertEquals(placeDetails.placeId, GOOGLE_SYDNEY); + assertNotNull(placeDetails.types); + assertEquals(placeDetails.types[0], AddressType.ESTABLISHMENT); + assertEquals(placeDetails.rating, 4.4, 0.1); + + // Permanently closed: + assertFalse(placeDetails.permanentlyClosed); + } + } + + @Test + public void testPlaceDetailsLookupPermanentlyClosedPlace() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(placeDetailResponseBodyForPermanentlyClosedPlace)) { + PlaceDetails placeDetails = + PlacesApi.placeDetails(sc.context, PERMANENTLY_CLOSED_PLACE_ID).await(); + assertNotNull(placeDetails); + assertNotNull(placeDetails.toString()); + assertTrue(placeDetails.permanentlyClosed); + } + } + + @Test + public void testPlaceDetailsLookupReturnsUserRatingsTotal() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placeDetailResponseBody)) { + PlaceDetails placeDetails = PlacesApi.placeDetails(sc.context, GOOGLE_SYDNEY).await(); + + assertNotNull(placeDetails); + assertNotNull(placeDetails.toString()); + assertEquals(GOOGLE_SYDNEY, placeDetails.placeId); + assertEquals(98, placeDetails.userRatingsTotal); + } + } + + @Test + public void testPlaceDetailsLookupQuay() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(quayResponseBody)) { + PlaceDetails placeDetails = PlacesApi.placeDetails(sc.context, QUAY_PLACE_ID).await(); + assertNotNull(placeDetails); + assertNotNull(placeDetails.toString()); + assertNotNull(placeDetails.priceLevel); + assertEquals(PriceLevel.VERY_EXPENSIVE, placeDetails.priceLevel); + assertNotNull(placeDetails.photos); + Photo photo = placeDetails.photos[0]; + assertEquals(1944, photo.height); + assertEquals(2592, photo.width); + assertEquals( + "James Prendergast", + photo.htmlAttributions[0]); + assertEquals( + "CmRdAAAATDVdhv0RdMEZlvO2jNE_EXXZZnCWvenfvLmWCsYqVtCFxZiasbcv1X0CNDTkpaCtrurGzVxTVt8Fqc7egdA7VyFeq1VFaq1GiFatWrFAUm_H0CN9u2wbfjb1Zf0NL9QiEhCj6I5O2h6eFH_2sa5hyVaEGhTdn8b7RWD-2W64OrT3mFGjzzLWlQ", + photo.photoReference); + } + } + + @Test + public void testQueryAutocompleteRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(10, 20); + PlacesApi.queryAutocomplete(sc.context, QUERY_AUTOCOMPLETE_INPUT) + .offset(10) + .location(location) + .radius(5000) + .language("en") + .await(); + + sc.assertParamValue(QUERY_AUTOCOMPLETE_INPUT, "input"); + sc.assertParamValue("10", "offset"); + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue("5000", "radius"); + sc.assertParamValue("en", "language"); + } + } + + @Test + public void testQueryAutocompletePizzaNearPar() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(queryAutocompleteResponseBody)) { + AutocompletePrediction[] predictions = + PlacesApi.queryAutocomplete(sc.context, QUERY_AUTOCOMPLETE_INPUT).await(); + + assertNotNull(predictions); + assertEquals(predictions.length, 5); + assertNotNull(Arrays.toString(predictions)); + + AutocompletePrediction prediction = predictions[0]; + assertNotNull(prediction); + assertNotNull(prediction.description); + assertEquals("pizza near Paris, France", prediction.description); + + assertEquals(3, prediction.matchedSubstrings.length); + AutocompletePrediction.MatchedSubstring matchedSubstring = prediction.matchedSubstrings[0]; + assertEquals(5, matchedSubstring.length); + assertEquals(0, matchedSubstring.offset); + + assertEquals(4, prediction.terms.length); + AutocompletePrediction.Term term = prediction.terms[0]; + assertEquals(0, term.offset); + assertEquals("pizza", term.value); + } + } + + @Test + public void testQueryAutocompleteWithPlaceId() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(queryAutocompleteWithPlaceIdResponseBody)) { + AutocompletePrediction[] predictions = + PlacesApi.queryAutocomplete(sc.context, QUERY_AUTOCOMPLETE_INPUT).await(); + + assertNotNull(predictions); + assertEquals(predictions.length, 1); + assertNotNull(Arrays.toString(predictions)); + + AutocompletePrediction prediction = predictions[0]; + assertNotNull(prediction); + assertNotNull(prediction.description); + assertEquals( + "Bondi Pizza, Campbell Parade, Sydney, New South Wales, Australia", + prediction.description); + + assertEquals(2, prediction.matchedSubstrings.length); + AutocompletePrediction.MatchedSubstring matchedSubstring = prediction.matchedSubstrings[0]; + assertEquals(5, matchedSubstring.length); + assertEquals(6, matchedSubstring.offset); + + assertEquals(5, prediction.terms.length); + AutocompletePrediction.Term term = prediction.terms[0]; + assertEquals(0, term.offset); + assertEquals("Bondi Pizza", term.value); + + assertEquals("ChIJv0wpwp6tEmsR0Glcf5tugrk", prediction.placeId); + } + } + + @Test + public void testTextSearchRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(10, 20); + PlacesApi.textSearchQuery(sc.context, "Google Sydney") + .location(location) + .region("AU") + .radius(3000) + .minPrice(PriceLevel.INEXPENSIVE) + .maxPrice(PriceLevel.VERY_EXPENSIVE) + .name("name") + .openNow(true) + .rankby(RankBy.DISTANCE) + .type(PlaceType.AIRPORT) + .await(); + + sc.assertParamValue("Google Sydney", "query"); + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue("AU", "region"); + sc.assertParamValue(String.valueOf(3000), "radius"); + sc.assertParamValue(String.valueOf(1), "minprice"); + sc.assertParamValue(String.valueOf(4), "maxprice"); + sc.assertParamValue("name", "name"); + sc.assertParamValue("true", "opennow"); + sc.assertParamValue(RankBy.DISTANCE.toString(), "rankby"); + sc.assertParamValue(PlaceType.AIRPORT.toString(), "type"); + } + } + + @Test + public void testTextSearchRequestWithLocation() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(10, 20); + PlacesApi.textSearchQuery(sc.context, "Google Sydney", location) + .region("AU") + .radius(3000) + .minPrice(PriceLevel.INEXPENSIVE) + .maxPrice(PriceLevel.VERY_EXPENSIVE) + .name("name") + .openNow(true) + .rankby(RankBy.DISTANCE) + .type(PlaceType.AIRPORT) + .await(); + + sc.assertParamValue("Google Sydney", "query"); + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue("AU", "region"); + sc.assertParamValue(String.valueOf(3000), "radius"); + sc.assertParamValue(String.valueOf(1), "minprice"); + sc.assertParamValue(String.valueOf(4), "maxprice"); + sc.assertParamValue("name", "name"); + sc.assertParamValue("true", "opennow"); + sc.assertParamValue(RankBy.DISTANCE.toString(), "rankby"); + sc.assertParamValue(PlaceType.AIRPORT.toString(), "type"); + } + } + + @Test + public void testTextSearchRequestWithType() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(-33.866611, 151.195832); + PlacesSearchResponse results = + PlacesApi.textSearchQuery(sc.context, PlaceType.ZOO) + .location(location) + .radius(500) + .await(); + + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue(String.valueOf(500), "radius"); + sc.assertParamValue(PlaceType.ZOO.toString(), "type"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTextSearchLocationWithoutRadius() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(10, 20); + PlacesApi.textSearchQuery(sc.context, "query").location(location).await(); + } + } + + @Test + public void testTextSearchResponse() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(textSearchResponseBody)) { + PlacesSearchResponse results = PlacesApi.textSearchQuery(sc.context, "Google Sydney").await(); + + assertNotNull(results); + assertNotNull(results.results); + assertEquals(1, results.results.length); + assertNotNull(results.toString()); + + PlacesSearchResult result = results.results[0]; + assertNotNull(result.formattedAddress); + assertEquals("5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", result.formattedAddress); + assertNotNull(result.geometry); + assertNotNull(result.geometry.location); + assertEquals(-33.866611, result.geometry.location.lat, 0.0001); + assertEquals(151.195832, result.geometry.location.lng, 0.0001); + assertNotNull(result.icon); + assertEquals( + new URI("https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png"), + result.icon.toURI()); + assertNotNull(result.name); + assertEquals("Google", result.name); + assertNotNull(result.openingHours); + assertFalse(result.openingHours.openNow); + assertNotNull(result.photos); + + assertEquals(1, result.photos.length); + Photo photo = result.photos[0]; + assertNotNull(photo); + assertEquals(2322, photo.height); + assertEquals(4128, photo.width); + assertNotNull(photo.htmlAttributions); + assertEquals(1, photo.htmlAttributions.length); + assertEquals( + "William Stewart", + photo.htmlAttributions[0]); + assertEquals( + "CmRdAAAAa43ZeiQvF4n-Yv5UnEGcIe0KjdTzzTH4g-g1GuKgWas0g8W7793eFDGxkrG4Z5i_Jua0Z-" + + "Ib88IuYe2iVAZ0W3Q7wUrp4A2mux4BjZmakLFkTkPj_OZ7ek3vSGnrzqExEhBqB3AIn82lmf38RnVSFH1CGhSWrvzN30A_" + + "ABGNScuiYEU70wau3w", + photo.photoReference); + + assertNotNull(result.placeId); + assertEquals("ChIJN1t_tDeuEmsRUsoyG83frY4", result.placeId); + assertEquals(4.4, result.rating, 0.0001); + assertNotNull(result.types); + assertNotNull(result.types[0]); + assertEquals("establishment", result.types[0]); + } + } + + @Test + public void testTextSearchNYC() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(textSearchPizzaInNYCbody)) { + PlacesSearchResponse results = + PlacesApi.textSearchQuery(sc.context, "Pizza in New York").await(); + assertNotNull(results.toString()); + assertNotNull(results.nextPageToken); + assertEquals( + "CuQB1wAAANI17eHXt1HpqbLjkj7T5Ti69DEAClo02Qampg7Q6W_O_krFbge7hnTtDR7oVF3asex" + + "HcGnUtR1ZKjroYd4BTCXxSGPi9LEkjJ0P_zVE7byjEBcHvkdxB6nCHKHAgVNGqe0ZHuwSYKlr3C1-" + + "kuellMYwMlg3WSe69bJr1Ck35uToNZkUGvo4yjoYxNFRn1lABEnjPskbMdyHAjUDwvBDxzgGxpd8t" + + "0EzA9UOM8Y1jqWnZGJM7u8gacNFcI4prr0Doh9etjY1yHrgGYI4F7lKPbfLQKiks_wYzoHbcAcdbB" + + "jkEhAxDHC0XXQ16thDAlwVbEYaGhSaGDw5sHbaZkG9LZIqbcas0IJU8w", + results.nextPageToken); + } + } + + @Test + public void testPhotoRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("")) { + final String photoReference = "Photo Reference"; + final int width = 200; + final int height = 100; + + PlacesApi.photo(sc.context, photoReference) + .maxWidth(width) + .maxHeight(height) + .awaitIgnoreError(); + + sc.assertParamValue(photoReference, "photoreference"); + sc.assertParamValue(String.valueOf(width), "maxwidth"); + sc.assertParamValue(String.valueOf(height), "maxheight"); + } + } + + @Test + public void testNearbySearchRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(10, 20); + PlacesApi.nearbySearchQuery(sc.context, location) + .radius(5000) + .rankby(RankBy.PROMINENCE) + .keyword("keyword") + .language("en") + .minPrice(PriceLevel.INEXPENSIVE) + .maxPrice(PriceLevel.EXPENSIVE) + .name("name") + .openNow(true) + .type(PlaceType.AIRPORT) + .pageToken("next-page-token") + .await(); + + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue("5000", "radius"); + sc.assertParamValue(RankBy.PROMINENCE.toString(), "rankby"); + sc.assertParamValue("keyword", "keyword"); + sc.assertParamValue("en", "language"); + sc.assertParamValue(PriceLevel.INEXPENSIVE.toString(), "minprice"); + sc.assertParamValue(PriceLevel.EXPENSIVE.toString(), "maxprice"); + sc.assertParamValue("name", "name"); + sc.assertParamValue("true", "opennow"); + sc.assertParamValue(PlaceType.AIRPORT.toString(), "type"); + sc.assertParamValue("next-page-token", "pagetoken"); + } + } + + @Test + @SuppressWarnings("deprecation") // Testing a deprecated method + public void testNearbySearchRequestWithMultipleType() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + LatLng location = new LatLng(10, 20); + PlacesApi.nearbySearchQuery(sc.context, location) + .type(PlaceType.AIRPORT, PlaceType.BANK) + .await(); + + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue(PlaceType.AIRPORT.toString() + "|" + PlaceType.BANK.toString(), "type"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNearbySearchRadiusAndRankbyDistance() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("")) { + LatLng location = new LatLng(10, 20); + PlacesApi.nearbySearchQuery(sc.context, location) + .radius(5000) + .rankby(RankBy.DISTANCE) + .await(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testNearbySearchRankbyDistanceWithoutKeywordNameOrType() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("")) { + LatLng location = new LatLng(10, 20); + PlacesApi.nearbySearchQuery(sc.context, location).rankby(RankBy.DISTANCE).await(); + } + } + + @Test + public void testPlaceAutocompleteRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext("{\"status\" : \"OK\"}")) { + SessionToken session = new SessionToken(); + LatLng location = new LatLng(10, 20); + PlacesApi.placeAutocomplete(sc.context, "Sydney Town Hall", session) + .offset(4) + .origin(location) + .location(location) + .radius(5000) + .types(PlaceAutocompleteType.ESTABLISHMENT) + .components(ComponentFilter.country("AU")) + .await(); + + sc.assertParamValue("Sydney Town Hall", "input"); + sc.assertParamValue(Integer.toString(4), "offset"); + sc.assertParamValue(location.toUrlValue(), "origin"); + sc.assertParamValue(location.toUrlValue(), "location"); + sc.assertParamValue("5000", "radius"); + sc.assertParamValue(PlaceAutocompleteType.ESTABLISHMENT.toString(), "types"); + sc.assertParamValue(ComponentFilter.country("AU").toString(), "components"); + sc.assertParamValue(session.toUrlValue(), "sessiontoken"); + } + } + + @Test + public void testTextSearch() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiTextSearch)) { + PlacesSearchResponse response = + PlacesApi.textSearchQuery(sc.context, "Google Sydney").await(); + + sc.assertParamValue("Google Sydney", "query"); + + assertNotNull(response.toString()); + assertEquals(1, response.results.length); + PlacesSearchResult result = response.results[0]; + assertEquals("5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", result.formattedAddress); + assertEquals("ChIJN1t_tDeuEmsRUsoyG83frY4", result.placeId); + assertEquals("OPERATIONAL", result.businessStatus); + } + } + + @Test + public void testPhoto() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiPhoto)) { + PlaceDetails placeDetails = PlacesApi.placeDetails(sc.context, GOOGLE_SYDNEY).await(); + + sc.assertParamValue("ChIJN1t_tDeuEmsRUsoyG83frY4", "placeid"); + + assertNotNull(placeDetails.toString()); + assertEquals(10, placeDetails.photos.length); + assertEquals( + "CmRaAAAA-N3w5YTMXWautuDW7IZgX9knz_2fNyyUpCWpvYdVEVb8RurBiisMKvr7AFxMW8dsu2yakYoqjW-IYSFk2cylXVM_c50cCxfm7MlgjPErFxumlcW1bLNOe--SwLYmWlvkEhDxjz75xRqim-CkVlwFyp7sGhTs1fE02MZ6GQcc-TugrepSaeWapA", + placeDetails.photos[0].photoReference); + assertEquals(1365, placeDetails.photos[0].height); + assertEquals(2048, placeDetails.photos[0].width); + } + } + + @Test + public void testPizzaInNewYorkPagination() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiPizzaInNewYork)) { + PlacesSearchResponse response = + PlacesApi.textSearchQuery(sc.context, "Pizza in New York").await(); + + sc.assertParamValue("Pizza in New York", "query"); + + assertNotNull(response.toString()); + assertEquals(20, response.results.length); + assertEquals( + "CvQB6AAAAPQLwX6KjvGbOw81Y7aYVhXRlHR8M60aCRXFDM9eyflac4BjE5MaNxTj_1T429x3H2kzBd-ztTFXCSu1CPh3kY44Gu0gmL-xfnArnPE9-BgfqXTpgzGPZNeCltB7m341y4LnU-NE2omFPoDWIrOPIyHnyi05Qol9eP2wKW7XPUhMlHvyl9MeVgZ8COBZKvCdENHbhBD1MN1lWlada6A9GPFj06cCp1aqRGW6v98-IHcIcM9RcfMcS4dLAFm6TsgLq4tpeU6E1kSzhrvDiLMBXdJYFlI0qJmytd2wS3vD0t3zKgU6Im_mY-IJL7AwAqhugBIQ8k0X_n6TnacL9BExELBaixoUo8nPOwWm0Nx02haufF2dY0VL-tg", + response.nextPageToken); + } + } + + @Test + public void testPlaceDetailsInFrench() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiDetailsInFrench)) { + PlaceDetails details = + PlacesApi.placeDetails(sc.context, "ChIJ442GNENu5kcRGYUrvgqHw88").language("fr").await(); + + sc.assertParamValue("ChIJ442GNENu5kcRGYUrvgqHw88", "placeid"); + sc.assertParamValue("fr", "language"); + + assertNotNull(details.toString()); + assertEquals("ChIJ442GNENu5kcRGYUrvgqHw88", details.placeId); + assertEquals( + "35 Rue du Chevalier de la Barre, 75018 Paris, France", details.formattedAddress); + assertEquals("Sacré-Cœur", details.name); + } + } + + @Test + public void testNearbySearchRequestByKeyword() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(placesApiNearbySearchRequestByKeyword)) { + PlacesSearchResponse response = + PlacesApi.nearbySearchQuery(sc.context, SYDNEY).radius(10000).keyword("pub").await(); + + sc.assertParamValue("10000", "radius"); + sc.assertParamValue("pub", "keyword"); + sc.assertParamValue(SYDNEY.toUrlValue(), "location"); + + assertEquals(20, response.results.length); + } + } + + @Test + public void testNearbySearchRequestByName() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(placesApiNearbySearchRequestByName)) { + PlacesSearchResponse response = + PlacesApi.nearbySearchQuery(sc.context, SYDNEY) + .radius(10000) + .name("Sydney Town Hall") + .await(); + + sc.assertParamValue("Sydney Town Hall", "name"); + sc.assertParamValue(SYDNEY.toUrlValue(), "location"); + sc.assertParamValue("10000", "radius"); + + assertEquals("Sydney Town Hall", response.results[0].name); + } + } + + @Test + public void testNearbySearchRequestByType() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(placesApiNearbySearchRequestByType)) { + PlacesSearchResponse response = + PlacesApi.nearbySearchQuery(sc.context, SYDNEY).radius(10000).type(PlaceType.BAR).await(); + + sc.assertParamValue(SYDNEY.toUrlValue(), "location"); + sc.assertParamValue("10000", "radius"); + sc.assertParamValue(PlaceType.BAR.toUrlValue(), "type"); + + assertEquals(20, response.results.length); + } + } + + @Test + public void testNearbySearchRequestByTypeReturnsUserRatingsTotal() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(placesApiNearbySearchRequestByType)) { + PlacesSearchResponse response = + PlacesApi.nearbySearchQuery(sc.context, SYDNEY).radius(10000).type(PlaceType.BAR).await(); + + assertEquals(20, response.results.length); + assertEquals(563, response.results[0].userRatingsTotal); + } + } + + @Test + public void testPlaceAutocomplete() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiPlaceAutocomplete)) { + SessionToken session = new SessionToken(); + AutocompletePrediction[] predictions = + PlacesApi.placeAutocomplete(sc.context, "Sydney Town Ha", session).await(); + + sc.assertParamValue("Sydney Town Ha", "input"); + sc.assertParamValue(session.toUrlValue(), "sessiontoken"); + + assertEquals(5, predictions.length); + assertTrue(predictions[0].description.contains("Town Hall")); + } + } + + @Test + public void testPlaceAutocompleteWithType() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(placesApiPlaceAutocompleteWithType)) { + SessionToken session = new SessionToken(); + AutocompletePrediction[] predictions = + PlacesApi.placeAutocomplete(sc.context, "po", session) + .components(ComponentFilter.country("nz")) + .types(PlaceAutocompleteType.REGIONS) + .await(); + + sc.assertParamValue("po", "input"); + sc.assertParamValue("country:nz", "components"); + sc.assertParamValue("(regions)", "types"); + sc.assertParamValue(session.toUrlValue(), "sessiontoken"); + + assertNotNull(Arrays.toString(predictions)); + assertEquals(5, predictions.length); + for (AutocompletePrediction prediction : predictions) { + for (int j = 0; j < prediction.types.length; j++) { + assertFalse(prediction.types[j].equals("route")); + assertFalse(prediction.types[j].equals("zoo")); + } + } + } + } + + @Test + public void testPlaceAutocompleteWithStrictBounds() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiPlaceAutocomplete)) { + SessionToken session = new SessionToken(); + PlacesApi.placeAutocomplete(sc.context, "Amoeba", session) + .types(PlaceAutocompleteType.ESTABLISHMENT) + .location(new LatLng(37.76999, -122.44696)) + .radius(500) + .strictBounds(true) + .await(); + + sc.assertParamValue("Amoeba", "input"); + sc.assertParamValue("establishment", "types"); + sc.assertParamValue("37.76999000,-122.44696000", "location"); + sc.assertParamValue("500", "radius"); + sc.assertParamValue("true", "strictbounds"); + sc.assertParamValue(session.toUrlValue(), "sessiontoken"); + } + } + + @Test + public void testKitaWard() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(placesApiKitaWard)) { + String query = "Kita Ward, Kyoto, Kyoto Prefecture, Japan"; + PlacesSearchResponse response = PlacesApi.textSearchQuery(sc.context, query).await(); + + sc.assertParamValue(query, "query"); + + assertEquals( + "Kita Ward, Kyoto, Kyoto Prefecture, Japan", response.results[0].formattedAddress); + assertTrue(Arrays.asList(response.results[0].types).contains("ward")); + } + } + + @Test + public void testFindPlaceFromText() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(findPlaceFromTextMuseumOfContemporaryArt)) { + + String input = "Museum of Contemporary Art Australia"; + + FindPlaceFromText response = + PlacesApi.findPlaceFromText(sc.context, input, InputType.TEXT_QUERY) + .fields( + FindPlaceFromTextRequest.FieldMask.BUSINESS_STATUS, + FindPlaceFromTextRequest.FieldMask.PHOTOS, + FindPlaceFromTextRequest.FieldMask.FORMATTED_ADDRESS, + FindPlaceFromTextRequest.FieldMask.NAME, + FindPlaceFromTextRequest.FieldMask.RATING, + FindPlaceFromTextRequest.FieldMask.OPENING_HOURS, + FindPlaceFromTextRequest.FieldMask.GEOMETRY) + .locationBias(new LocationBiasIP()) + .await(); + + sc.assertParamValue(input, "input"); + sc.assertParamValue("textquery", "inputtype"); + sc.assertParamValue( + "business_status,photos,formatted_address,name,rating,opening_hours,geometry", "fields"); + sc.assertParamValue("ipbias", "locationbias"); + + assertNotNull(response); + PlacesSearchResult candidate = response.candidates[0]; + assertNotNull(candidate); + assertEquals("140 George St, The Rocks NSW 2000, Australia", candidate.formattedAddress); + LatLng location = candidate.geometry.location; + assertEquals(-33.8599358, location.lat, 0.00001); + assertEquals(151.2090295, location.lng, 0.00001); + assertEquals("Museum of Contemporary Art Australia", candidate.name); + assertEquals(true, candidate.openingHours.openNow); + Photo photo = candidate.photos[0]; + assertEquals( + "CmRaAAAAXBZe3QrziBst5oTCPUzL4LSgSuWYMctBNRu8bOP4TfwD0aU80YemnnbhjWdFfMX-kkh5h9NhFJky6fW5Ivk_G9fc11GekI0HOCDASZH3qRJmUBsdw0MWoCDZmwQAg-dVEhBb0aLoJXzoZ8cXWEceB9omGhRrX24jI3VnSEQUmInfYoAwSX4OPw", + photo.photoReference); + assertEquals(2268, photo.height); + assertEquals(4032, photo.width); + assertEquals(4.4, candidate.rating, 0.01); + } + } + + @Test + public void testFindPlaceFromTextPoint() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(findPlaceFromTextMuseumOfContemporaryArt)) { + + String input = "Museum of Contemporary Art Australia"; + + PlacesApi.findPlaceFromText(sc.context, input, InputType.TEXT_QUERY) + .fields( + FindPlaceFromTextRequest.FieldMask.PHOTOS, + FindPlaceFromTextRequest.FieldMask.FORMATTED_ADDRESS, + FindPlaceFromTextRequest.FieldMask.NAME, + FindPlaceFromTextRequest.FieldMask.RATING, + FindPlaceFromTextRequest.FieldMask.OPENING_HOURS, + FindPlaceFromTextRequest.FieldMask.GEOMETRY) + .locationBias(new LocationBiasPoint(new LatLng(1, 2))) + .await(); + + sc.assertParamValue(input, "input"); + sc.assertParamValue("textquery", "inputtype"); + sc.assertParamValue("photos,formatted_address,name,rating,opening_hours,geometry", "fields"); + sc.assertParamValue("point:1.00000000,2.00000000", "locationbias"); + } + } + + @Test + public void testFindPlaceFromTextCircular() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(findPlaceFromTextMuseumOfContemporaryArt)) { + + String input = "Museum of Contemporary Art Australia"; + + PlacesApi.findPlaceFromText(sc.context, input, InputType.TEXT_QUERY) + .fields( + FindPlaceFromTextRequest.FieldMask.PHOTOS, + FindPlaceFromTextRequest.FieldMask.FORMATTED_ADDRESS, + FindPlaceFromTextRequest.FieldMask.NAME, + FindPlaceFromTextRequest.FieldMask.RATING, + FindPlaceFromTextRequest.FieldMask.OPENING_HOURS, + FindPlaceFromTextRequest.FieldMask.GEOMETRY) + .locationBias(new LocationBiasCircular(new LatLng(1, 2), 3000)) + .await(); + + sc.assertParamValue(input, "input"); + sc.assertParamValue("textquery", "inputtype"); + sc.assertParamValue("photos,formatted_address,name,rating,opening_hours,geometry", "fields"); + sc.assertParamValue("circle:3000@1.00000000,2.00000000", "locationbias"); + } + } + + @Test + public void testFindPlaceFromTextRectangular() throws Exception { + try (LocalTestServerContext sc = + new LocalTestServerContext(findPlaceFromTextMuseumOfContemporaryArt)) { + + String input = "Museum of Contemporary Art Australia"; + + PlacesApi.findPlaceFromText(sc.context, input, InputType.TEXT_QUERY) + .fields( + FindPlaceFromTextRequest.FieldMask.PHOTOS, + FindPlaceFromTextRequest.FieldMask.FORMATTED_ADDRESS, + FindPlaceFromTextRequest.FieldMask.NAME, + FindPlaceFromTextRequest.FieldMask.RATING, + FindPlaceFromTextRequest.FieldMask.OPENING_HOURS, + FindPlaceFromTextRequest.FieldMask.GEOMETRY) + .locationBias(new LocationBiasRectangular(new LatLng(1, 2), new LatLng(3, 4))) + .await(); + + sc.assertParamValue(input, "input"); + sc.assertParamValue("textquery", "inputtype"); + sc.assertParamValue("photos,formatted_address,name,rating,opening_hours,geometry", "fields"); + sc.assertParamValue("rectangle:1.00000000,2.00000000|3.00000000,4.00000000", "locationbias"); + } + } + + @Test + public void testPlaceDetailsWithBusinessStatus() throws Exception { + final String jsonString = retrieveBody("PlaceDetailsResponseWithBusinessStatus.json"); + final LocalTestServerContext server = new LocalTestServerContext(jsonString); + final PlaceDetails placeDetails = PlacesApi.placeDetails(server.context, "testPlaceId").await(); + assertNotNull(placeDetails); + assertEquals("OPERATIONAL", placeDetails.businessStatus); + } + + @Test + public void testPlaceDetailsRequestHasFieldMask() throws Exception { + final String jsonString = retrieveBody("PlaceDetailsResponseWithBusinessStatus.json"); + final LocalTestServerContext server = new LocalTestServerContext(jsonString); + + PlacesApi.placeDetails(server.context, "testPlaceId").fields(FieldMask.BUSINESS_STATUS).await(); + + server.assertParamValue("business_status", "fields"); + } +} diff --git a/src/test/java/com/google/maps/RoadsApiIntegrationTest.java b/src/test/java/com/google/maps/RoadsApiIntegrationTest.java index ada356138..b9c975293 100644 --- a/src/test/java/com/google/maps/RoadsApiIntegrationTest.java +++ b/src/test/java/com/google/maps/RoadsApiIntegrationTest.java @@ -15,128 +15,185 @@ package com.google.maps; -import static org.hamcrest.CoreMatchers.either; -import static org.hamcrest.CoreMatchers.is; +import static com.google.maps.TestUtils.retrieveBody; +import static com.google.maps.internal.StringJoin.join; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import com.google.maps.model.LatLng; import com.google.maps.model.SnappedPoint; import com.google.maps.model.SnappedSpeedLimitResponse; import com.google.maps.model.SpeedLimit; - +import java.util.Arrays; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.util.concurrent.TimeUnit; - @Category(LargeTests.class) -public class RoadsApiIntegrationTest extends KeyOnlyAuthenticatedTest { - private GeoApiContext context; - - public RoadsApiIntegrationTest(GeoApiContext context) { - this.context = context - .setConnectTimeout(2, TimeUnit.SECONDS) - .setReadTimeout(2, TimeUnit.SECONDS) - .setWriteTimeout(2 , TimeUnit.SECONDS); +public class RoadsApiIntegrationTest { + + private final String snapToRoadResponse; + private final String speedLimitsResponse; + private final String speedLimitsUSAResponse; + private final String speedLimitsWithPlaceIdsResponse; + private final String snappedSpeedLimitResponse; + private final String nearestRoadsResponse; + + public RoadsApiIntegrationTest() { + snapToRoadResponse = retrieveBody("RoadsApiSnapToRoadResponse.json"); + speedLimitsResponse = retrieveBody("RoadsApiSpeedLimitsResponse.json"); + speedLimitsUSAResponse = retrieveBody("RoadsApiSpeedLimitsUSAResponse.json"); + speedLimitsWithPlaceIdsResponse = retrieveBody("RoadsApiSpeedLimitsWithPlaceIds.json"); + snappedSpeedLimitResponse = retrieveBody("RoadsApiSnappedSpeedLimitResponse.json"); + nearestRoadsResponse = retrieveBody("RoadsApiNearestRoadsResponse.json"); } @Test public void testSnapToRoad() throws Exception { - SnappedPoint[] points = RoadsApi.snapToRoads(context, - false, - new LatLng(-33.865382, 151.192861), - new LatLng(-33.865837, 151.193376), - new LatLng(-33.866745, 151.19373), - new LatLng(-33.867128, 151.19344), - new LatLng(-33.867547, 151.193676), - new LatLng(-33.867841, 151.194137), - new LatLng(-33.868224, 151.194116)).await(); - - assertNotNull(points); - assertEquals(7, points.length); - assertNotNull(points[0].location.lat); - assertNotNull(points[0].location.lng); - assertNotNull(points[0].placeId); + try (LocalTestServerContext sc = new LocalTestServerContext(snapToRoadResponse)) { + LatLng[] path = + new LatLng[] { + new LatLng(-33.865382, 151.192861), + new LatLng(-33.865837, 151.193376), + new LatLng(-33.866745, 151.19373), + new LatLng(-33.867128, 151.19344), + new LatLng(-33.867547, 151.193676), + new LatLng(-33.867841, 151.194137), + new LatLng(-33.868224, 151.194116) + }; + SnappedPoint[] points = RoadsApi.snapToRoads(sc.context, false, path).await(); + + assertNotNull(Arrays.toString(points)); + sc.assertParamValue(join('|', path), "path"); + sc.assertParamValue("false", "interpolate"); + assertEquals(7, points.length); + assertEquals(-33.86523340256843, points[0].location.lat, 0.0001); + assertEquals(151.19288612197704, points[0].location.lng, 0.0001); + assertEquals("ChIJjXkMCDauEmsRp5xab4Ske6k", points[0].placeId); + } } @Test - public void testSnapToRoadProvidesOriginalIndexWithInterpolation() throws Exception { - SnappedPoint[] points = RoadsApi.snapToRoads(context, - false, - new LatLng(-33.865382, 151.192861), - new LatLng(-33.865837, 151.193376), - new LatLng(-33.866745, 151.19373), - new LatLng(-33.867128, 151.19344), - new LatLng(-33.867547, 151.193676), - new LatLng(-33.867841, 151.194137), - new LatLng(-33.868224, 151.194116)).await(); - - int currentIndex = 0; - // Interpolated points need to have an incrementing originalIndex, or -1. - for (SnappedPoint point : points) { - assertThat(point.originalIndex, either(is(-1)).or(is(currentIndex))); - if (point.originalIndex != -1) { - currentIndex++; + public void testSpeedLimitsWithLatLngs() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(speedLimitsResponse)) { + LatLng[] path = + new LatLng[] { + new LatLng(-33.865382, 151.192861), + new LatLng(-33.865837, 151.193376), + new LatLng(-33.866745, 151.19373), + new LatLng(-33.867128, 151.19344), + new LatLng(-33.867547, 151.193676), + new LatLng(-33.867841, 151.194137), + new LatLng(-33.868224, 151.194116) + }; + SpeedLimit[] speeds = RoadsApi.speedLimits(sc.context, path).await(); + + assertNotNull(Arrays.toString(speeds)); + assertEquals("/v1/speedLimits", sc.path()); + sc.assertParamValue(join('|', path), "path"); + assertEquals(7, speeds.length); + + for (SpeedLimit speed : speeds) { + assertNotNull(speed.placeId); + assertEquals(40.0, speed.speedLimit, 0.001); } } - - assertEquals(7, currentIndex); // 7 latlngs, but we ++ after each, so index=7 } @Test - public void testSpeedLimitsWithLatLngs() throws Exception { - SpeedLimit[] speeds = RoadsApi.speedLimits(context, - new LatLng(-33.865382, 151.192861), - new LatLng(-33.865837, 151.193376), - new LatLng(-33.866745, 151.19373), - new LatLng(-33.867128, 151.19344), - new LatLng(-33.867547, 151.193676), - new LatLng(-33.867841, 151.194137), - new LatLng(-33.868224, 151.194116)).await(); - - assertNotNull(speeds); - assertEquals(7, speeds.length); - - for (SpeedLimit speed : speeds) { - assertNotNull(speed.placeId); - assertTrue(speed.speedLimit > 0); + public void testSpeedLimitsWithUsaLatLngs() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(speedLimitsUSAResponse)) { + LatLng[] path = + new LatLng[] { + new LatLng(33.777489, -84.397805), + new LatLng(33.777550, -84.395700), + new LatLng(33.776900, -84.393110), + new LatLng(33.776860, -84.389550), + new LatLng(33.775491, -84.388797), + new LatLng(33.773250, -84.388840), + new LatLng(33.771991, -84.388840) + }; + SpeedLimit[] speeds = RoadsApi.speedLimits(sc.context, path).await(); + + assertNotNull(Arrays.toString(speeds)); + assertEquals("/v1/speedLimits", sc.path()); + sc.assertParamValue(join('|', path), "path"); + assertEquals(7, speeds.length); + + for (SpeedLimit speed : speeds) { + assertNotNull(speed.placeId); + assertTrue(speed.speedLimit > 0); + } } } @Test public void testSpeedLimitsWithPlaceIds() throws Exception { - SpeedLimit[] speeds = RoadsApi.speedLimits(context, - "ChIJOXE4GDauEmsRbeangKX--a0", - "ChIJOXE4GDauEmsRbeangKX--a0", - "ChIJua_ZPTauEmsRwK6LHmdHDH4").await(); - - assertNotNull(speeds); - assertEquals(3, speeds.length); - assertEquals("ChIJua_ZPTauEmsRwK6LHmdHDH4", speeds[2].placeId); - - for (SpeedLimit speed : speeds) { - assertTrue(speed.speedLimit > 0); + try (LocalTestServerContext sc = new LocalTestServerContext(speedLimitsWithPlaceIdsResponse)) { + String[] placeIds = + new String[] { + "ChIJrfDjZYoE9YgRLpb3bOhcPno", + "ChIJyU-E2mEE9YgRftyNXxcfQYw", + "ChIJc0BrC2EE9YgR71DvaFzNgrA" + }; + SpeedLimit[] speeds = RoadsApi.speedLimits(sc.context, placeIds).await(); + + assertNotNull(Arrays.toString(speeds)); + assertEquals("/v1/speedLimits", sc.path()); + assertEquals(3, speeds.length); + assertEquals("ChIJc0BrC2EE9YgR71DvaFzNgrA", speeds[2].placeId); + + for (SpeedLimit speed : speeds) { + assertTrue(speed.speedLimit > 0); + } } } @Test public void testSnappedSpeedLimitRequest() throws Exception { - SnappedSpeedLimitResponse response = RoadsApi.snappedSpeedLimits(context, - new LatLng(-33.865382, 151.192861), - new LatLng(-33.865837, 151.193376), - new LatLng(-33.866745, 151.19373), - new LatLng(-33.867128, 151.19344), - new LatLng(-33.867547, 151.193676), - new LatLng(-33.867841, 151.194137), - new LatLng(-33.868224, 151.194116)).await(); - - SnappedPoint[] points = response.snappedPoints; - SpeedLimit[] speeds = response.speedLimits; - - assertEquals(7, points.length); - assertEquals(7, speeds.length); + try (LocalTestServerContext sc = new LocalTestServerContext(snappedSpeedLimitResponse)) { + LatLng[] path = + new LatLng[] { + new LatLng(-33.865382, 151.192861), + new LatLng(-33.865837, 151.193376), + new LatLng(-33.866745, 151.19373), + new LatLng(-33.867128, 151.19344), + new LatLng(-33.867547, 151.193676), + new LatLng(-33.867841, 151.194137), + new LatLng(-33.868224, 151.194116) + }; + SnappedSpeedLimitResponse response = RoadsApi.snappedSpeedLimits(sc.context, path).await(); + + assertNotNull(response.toString()); + assertEquals("/v1/speedLimits", sc.path()); + sc.assertParamValue(join('|', path), "path"); + assertEquals(path.length, response.snappedPoints.length); + assertEquals(path.length, response.speedLimits.length); + } + } + + @Test + public void testNearestRoads() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(nearestRoadsResponse)) { + LatLng[] path = + new LatLng[] { + new LatLng(-33.865382, 151.192861), + new LatLng(-33.865837, 151.193376), + new LatLng(-33.866745, 151.19373), + new LatLng(-33.867128, 151.19344), + new LatLng(-33.867547, 151.193676), + new LatLng(-33.867841, 151.194137), + new LatLng(-33.868224, 151.194116) + }; + SnappedPoint[] points = RoadsApi.nearestRoads(sc.context, path).await(); + + assertNotNull(Arrays.toString(points)); + assertEquals("/v1/nearestRoads", sc.path()); + sc.assertParamValue(join('|', path), "points"); + assertEquals(13, points.length); + assertEquals(-33.86543615612047, points[0].location.lat, 0.0001); + assertEquals(151.1930101572747, points[0].location.lng, 0.0001); + assertEquals("ChIJ0XXACjauEmsRUduC5Wd9ARM", points[0].placeId); + } } } diff --git a/src/test/java/com/google/maps/SmallTests.java b/src/test/java/com/google/maps/SmallTests.java index e9c2e72ee..2c73bc1b6 100644 --- a/src/test/java/com/google/maps/SmallTests.java +++ b/src/test/java/com/google/maps/SmallTests.java @@ -15,8 +15,5 @@ package com.google.maps; -/** - * Small tests run very quickly. - */ -public interface SmallTests { -} +/** Small tests run very quickly. */ +public interface SmallTests {} diff --git a/src/test/java/com/google/maps/StaticMapsApiTest.java b/src/test/java/com/google/maps/StaticMapsApiTest.java new file mode 100644 index 000000000..bc00b3064 --- /dev/null +++ b/src/test/java/com/google/maps/StaticMapsApiTest.java @@ -0,0 +1,273 @@ +/* + * Copyright 2018 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.google.maps.StaticMapsRequest.ImageFormat; +import com.google.maps.StaticMapsRequest.Markers; +import com.google.maps.StaticMapsRequest.Markers.CustomIconAnchor; +import com.google.maps.StaticMapsRequest.Markers.MarkersSize; +import com.google.maps.StaticMapsRequest.Path; +import com.google.maps.StaticMapsRequest.StaticMapType; +import com.google.maps.model.EncodedPolyline; +import com.google.maps.model.LatLng; +import com.google.maps.model.Size; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; +import javax.imageio.ImageIO; +import org.junit.Test; + +public class StaticMapsApiTest { + + private final int WIDTH = 640; + private final int HEIGHT = 480; + private final LatLng MELBOURNE = new LatLng(-37.8136, 144.9630); + private final LatLng SYDNEY = new LatLng(-33.8688, 151.2093); + /** This encoded path matches the exact [MELBOURNE, SYDNEY] points. */ + private final String MELBOURNE_TO_SYDNEY_ENCODED_POLYLINE = "~mxeFwaxsZ_naWk~be@"; + + private final BufferedImage IMAGE = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB); + + @Test + public void testGetSydneyStaticMap() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + req.center("Google Sydney"); + req.zoom(16); + ByteArrayInputStream bais = new ByteArrayInputStream(req.await().imageData); + BufferedImage img = ImageIO.read(bais); + + sc.assertParamValue("640x480", "size"); + sc.assertParamValue("Google Sydney", "center"); + sc.assertParamValue("16", "zoom"); + + assertNotNull(img); + assertEquals(WIDTH, img.getWidth()); + assertEquals(HEIGHT, img.getHeight()); + } + } + + @Test + public void testGetSydneyLatLngStaticMap() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + req.center(SYDNEY); + req.zoom(16); + req.await(); + + sc.assertParamValue("640x480", "size"); + sc.assertParamValue("-33.86880000,151.20930000", "center"); + sc.assertParamValue("16", "zoom"); + } + } + + @Test + public void testRequest() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + req.center("Sydney"); + req.zoom(16); + req.scale(2); + req.format(ImageFormat.png32); + req.maptype(StaticMapType.hybrid); + req.region("AU"); + req.visible("Melbourne"); + req.await(); + + sc.assertParamValue("640x480", "size"); + sc.assertParamValue("Sydney", "center"); + sc.assertParamValue("16", "zoom"); + sc.assertParamValue("2", "scale"); + sc.assertParamValue("png32", "format"); + sc.assertParamValue("hybrid", "maptype"); + sc.assertParamValue("AU", "region"); + sc.assertParamValue("Melbourne", "visible"); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateRequest_noCenter() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + req.zoom(16); + req.await(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateRequest_noZoom() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + req.center("Google Sydney"); + req.await(); + } + } + + @Test + public void testValidateRequest_noCenterAndNoZoomWithMarkers() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + + Markers markers = new Markers(); + markers.size(MarkersSize.small); + markers.customIcon("http://not.a/real/url", CustomIconAnchor.bottomleft, 2); + markers.color("blue"); + markers.label("A"); + markers.addLocation("Melbourne"); + markers.addLocation(SYDNEY); + req.markers(markers); + req.await(); + } + } + + @Test + public void testValidateRequest_noCenterAndNoZoomWithPath() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + Path path = new Path(); + path.color("green"); + path.fillcolor("0xAACCEE"); + path.weight(3); + path.geodesic(true); + path.addPoint("Melbourne"); + path.addPoint(SYDNEY); + req.path(path); + req.await(); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testValidateRequest_noSize() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, null); + req.center("Google Sydney"); + req.zoom(16); + req.await(); + } + } + + @Test + public void testMarkerAndPath() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + Markers markers = new Markers(); + markers.size(MarkersSize.small); + markers.customIcon("http://not.a/real/url", CustomIconAnchor.bottomleft, 2); + markers.color("blue"); + markers.label("A"); + markers.addLocation("Melbourne"); + markers.addLocation(SYDNEY); + req.markers(markers); + + Path path = new Path(); + path.color("green"); + path.fillcolor("0xAACCEE"); + path.weight(3); + path.geodesic(true); + path.addPoint("Melbourne"); + path.addPoint(SYDNEY); + req.path(path); + + req.await(); + + sc.assertParamValue( + "icon:http://not.a/real/url|anchor:bottomleft|scale:2|size:small|color:blue|label:A|Melbourne|-33.86880000,151.20930000", + "markers"); + sc.assertParamValue( + "weight:3|color:green|fillcolor:0xAACCEE|geodesic:true|Melbourne|-33.86880000,151.20930000", + "path"); + } + } + + @Test + public void testMarkerAndPathAsEncodedPolyline() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + Markers markers = new Markers(); + markers.size(MarkersSize.small); + markers.customIcon("http://not.a/real/url", CustomIconAnchor.bottomleft, 2); + markers.color("blue"); + markers.label("A"); + markers.addLocation("Melbourne"); + markers.addLocation(SYDNEY); + req.markers(markers); + + List points = new ArrayList<>(); + points.add(MELBOURNE); + points.add(SYDNEY); + EncodedPolyline path = new EncodedPolyline(points); + req.path(path); + + req.await(); + + sc.assertParamValue( + "icon:http://not.a/real/url|anchor:bottomleft|scale:2|size:small|color:blue|label:A|Melbourne|-33.86880000,151.20930000", + "markers"); + sc.assertParamValue("enc:" + MELBOURNE_TO_SYDNEY_ENCODED_POLYLINE, "path"); + } + } + + @Test + public void testBrooklynBridgeNYMarkers() throws Exception { + try (LocalTestServerContext sc = new LocalTestServerContext(IMAGE)) { + StaticMapsRequest req = StaticMapsApi.newRequest(sc.context, new Size(WIDTH, HEIGHT)); + req.center("Brooklyn Bridge, New York, NY"); + req.zoom(13); + req.maptype(StaticMapType.roadmap); + { + Markers markers = new Markers(); + markers.color("blue"); + markers.label("S"); + markers.addLocation(new LatLng(40.702147, -74.015794)); + req.markers(markers); + } + { + Markers markers = new Markers(); + markers.color("green"); + markers.label("G"); + markers.addLocation(new LatLng(40.711614, -74.012318)); + req.markers(markers); + } + { + Markers markers = new Markers(); + markers.color("red"); + markers.label("C"); + markers.addLocation(new LatLng(40.718217, -73.998284)); + req.markers(markers); + } + + req.await(); + + sc.assertParamValue("640x480", "size"); + sc.assertParamValue("Brooklyn Bridge, New York, NY", "center"); + sc.assertParamValue("13", "zoom"); + sc.assertParamValue("roadmap", "maptype"); + + List expected = new ArrayList<>(); + expected.add("color:blue|label:S|40.70214700,-74.01579400"); + expected.add("color:green|label:G|40.71161400,-74.01231800"); + expected.add("color:red|label:C|40.71821700,-73.99828400"); + sc.assertParamValues(expected, "markers"); + } + } +} diff --git a/src/test/java/com/google/maps/TestUtils.java b/src/test/java/com/google/maps/TestUtils.java new file mode 100644 index 000000000..993e867f6 --- /dev/null +++ b/src/test/java/com/google/maps/TestUtils.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.google.maps; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +public class TestUtils { + public static String retrieveBody(String filename) { + InputStream input = TestUtils.class.getResourceAsStream(filename); + try (Scanner s = new java.util.Scanner(input, StandardCharsets.UTF_8.name())) { + s.useDelimiter("\\A"); + String body = s.next(); + + if (body == null || body.length() == 0) { + throw new IllegalArgumentException( + "filename '" + filename + "' resulted in null or empty body"); + } + return body; + } + } + + public static Thread findLastThreadByName(String name) { + ThreadGroup currentThreadGroup = Thread.currentThread().getThreadGroup(); + Thread[] threads = new Thread[1000]; + currentThreadGroup.enumerate(threads); + Thread delayThread = null; + for (Thread thread : threads) { + if (thread == null) break; + if (thread.getName().equals(name)) { + delayThread = thread; + } + } + return delayThread; + } +} diff --git a/src/test/java/com/google/maps/TimeZoneApiTest.java b/src/test/java/com/google/maps/TimeZoneApiTest.java index d7c1f3a4e..cd0015c02 100644 --- a/src/test/java/com/google/maps/TimeZoneApiTest.java +++ b/src/test/java/com/google/maps/TimeZoneApiTest.java @@ -21,59 +21,57 @@ import static org.junit.Assert.assertTrue; import com.google.maps.errors.ZeroResultsException; -import com.google.maps.model.GeocodingResult; import com.google.maps.model.LatLng; - -import org.junit.Before; -import org.junit.Test; -import org.junit.experimental.categories.Category; - import java.util.Date; import java.util.TimeZone; -import java.util.concurrent.TimeUnit; - -@Category(LargeTests.class) -public class TimeZoneApiTest extends AuthenticatedTest { - - private GeoApiContext context; - - private LatLng sydney; - - public TimeZoneApiTest(GeoApiContext context) { - this.context = context.setQueryRateLimit(3) - .setConnectTimeout(1, TimeUnit.SECONDS) - .setReadTimeout(1, TimeUnit.SECONDS) - .setWriteTimeout(1, TimeUnit.SECONDS); - } - - @Before - public void setUp() throws Exception { - if (sydney == null) { - GeocodingResult[] results = GeocodingApi.geocode(context, "Sydney").await(); - sydney = results[0].geometry.location; - } - } +import org.junit.Test; +import org.junit.experimental.categories.Category; +@Category(MediumTests.class) +public class TimeZoneApiTest { @Test public void testGetTimeZone() throws Exception { - TimeZone tz = TimeZoneApi.getTimeZone(context, sydney).await(); - assertNotNull(tz); - assertEquals(TimeZone.getTimeZone("Australia/Sydney"), tz); - - // GMT+10 - assertEquals(36000000, tz.getRawOffset()); - // DST is +1h - assertEquals(3600000, tz.getDSTSavings()); - - assertTrue(tz.inDaylightTime(new Date(1388494800000L))); + try (LocalTestServerContext sc = + new LocalTestServerContext( + "\n" + + "{\n" + + " \"dstOffset\" : 0,\n" + + " \"rawOffset\" : 36000,\n" + + " \"status\" : \"OK\",\n" + + " \"timeZoneId\" : \"Australia/Sydney\",\n" + + " \"timeZoneName\" : \"Australian Eastern Standard Time\"\n" + + "}\n")) { + LatLng sydney = new LatLng(-33.8688, 151.2093); + TimeZone tz = TimeZoneApi.getTimeZone(sc.context, sydney).await(); + + assertNotNull(tz); + assertEquals(TimeZone.getTimeZone("Australia/Sydney"), tz); + + // GMT+10 + assertEquals(36000000, tz.getRawOffset()); + // DST is +1h + assertEquals(3600000, tz.getDSTSavings()); + + assertTrue(tz.inDaylightTime(new Date(1388494800000L))); + + sc.assertParamValue(sydney.toUrlValue(), "location"); + } } @Test(expected = ZeroResultsException.class) public void testNoResult() throws Exception { - TimeZone resp = TimeZoneApi.getTimeZone(context, new LatLng(0, 0)).awaitIgnoreError(); - assertNull(resp); + try (LocalTestServerContext sc = + new LocalTestServerContext("\n{\n \"status\" : \"ZERO_RESULTS\"\n}\n")) { + TimeZone resp = TimeZoneApi.getTimeZone(sc.context, new LatLng(0, 0)).awaitIgnoreError(); + assertNull(resp); - TimeZoneApi.getTimeZone(context, new LatLng(0, 0)).await(); + sc.assertParamValue("0.00000000,0.00000000", "location"); + + try (LocalTestServerContext sc2 = + new LocalTestServerContext("\n{\n \"status\" : \"ZERO_RESULTS\"\n}\n")) { + TimeZoneApi.getTimeZone(sc2.context, new LatLng(0, 0)).await(); + } + } } } diff --git a/src/test/java/com/google/maps/internal/PolylineEncodingTest.java b/src/test/java/com/google/maps/internal/PolylineEncodingTest.java index 3333c205b..ea3cd7dc5 100644 --- a/src/test/java/com/google/maps/internal/PolylineEncodingTest.java +++ b/src/test/java/com/google/maps/internal/PolylineEncodingTest.java @@ -20,15 +20,11 @@ import com.google.maps.SmallTests; import com.google.maps.model.LatLng; - +import java.util.List; import org.junit.Test; import org.junit.experimental.categories.Category; -import java.util.List; - -/** - * Test case for {@link PolylineEncoding}. - */ +/** Test case for {@link PolylineEncoding}. */ @Category(SmallTests.class) public class PolylineEncodingTest { @@ -37,28 +33,26 @@ public class PolylineEncodingTest { private static final LatLng MELBOURNE = new LatLng(-37.814130, 144.963180); private static final String SYD_MELB_ROUTE = "rvumEis{y[`NsfA~tAbF`bEj^h{@{KlfA~eA~`AbmEghAt~D|e@jlRpO~yH_\\v}LjbBh~FdvCxu@`nCplDbcBf_B|w" - + "BhIfhCnqEb~D~jCn_EngApdEtoBbfClf@t_CzcCpoEr_Gz_DxmAphDjjBxqCviEf}B|pEvsEzbE~qGfpExjBlqCx}" - + "BvmLb`FbrQdpEvkAbjDllD|uDldDj`Ef|AzcEx_Gtm@vuI~xArwD`dArlFnhEzmHjtC~eDluAfkC|eAdhGpJh}N_m" - + "ArrDlr@h|HzjDbsAvy@~~EdTxpJje@jlEltBboDjJdvKyZpzExrAxpHfg@pmJg[tgJuqBnlIarAh}DbN`hCeOf_Ib" - + "xA~uFt|A|xEt_ArmBcN|sB|h@b_DjOzbJ{RlxCcfAp~AahAbqG~Gr}AerA`dCwlCbaFo]twKt{@bsG|}A~fDlvBvz" - + "@tw@rpD_r@rqB{PvbHek@vsHlh@ptNtm@fkD[~xFeEbyKnjDdyDbbBtuA|~Br|Gx_AfxCt}CjnHv`Ew\\lnBdrBfq" - + "BraD|{BldBxpG|]jqC`mArcBv]rdAxgBzdEb{InaBzyC}AzaEaIvrCzcAzsCtfD~qGoPfeEh]h`BxiB`e@`kBxfAv" - + "^pyA`}BhkCdoCtrC~bCxhCbgEplKrk@tiAteBwAxbCwuAnnCc]b{FjrDdjGhhGzfCrlDruBzSrnGhvDhcFzw@n{@z" - + "xAf}Fd{IzaDnbDjoAjqJjfDlbIlzAraBxrB}K~`GpuD~`BjmDhkBp{@r_AxCrnAjrCx`AzrBj{B|r@~qBbdAjtDnv" - + "CtNzpHxeApyC|GlfM`fHtMvqLjuEtlDvoFbnCt|@xmAvqBkGreFm~@hlHw|AltC}NtkGvhBfaJ|~@riAxuC~gErwC" - + "ttCzjAdmGuF`iFv`AxsJftD|nDr_QtbMz_DheAf~Buy@rlC`i@d_CljC`gBr|H|nAf_Fh{G|mE~kAhgKviEpaQnu@" - + "zwAlrA`G~gFnvItz@j{Cng@j{D{]`tEftCdcIsPz{DddE~}PlnE|dJnzG`eG`mF|aJdqDvoAwWjzHv`H`wOtjGzeX" - + "hhBlxErfCf{BtsCjpEjtD|}Aja@xnAbdDt|ErMrdFh{CzgAnlCnr@`wEM~mE`bA`uD|MlwKxmBvuFlhB|sN`_@fvB" - + "p`CxhCt_@loDsS|eDlmChgFlqCbjCxk@vbGxmCjbMba@rpBaoClcCk_DhgEzYdzBl\\vsA_JfGztAbShkGtEhlDzh" - + "C~w@hnB{e@yF}`D`_Ayx@~vGqn@l}CafC"; - + + "BhIfhCnqEb~D~jCn_EngApdEtoBbfClf@t_CzcCpoEr_Gz_DxmAphDjjBxqCviEf}B|pEvsEzbE~qGfpExjBlqCx}" + + "BvmLb`FbrQdpEvkAbjDllD|uDldDj`Ef|AzcEx_Gtm@vuI~xArwD`dArlFnhEzmHjtC~eDluAfkC|eAdhGpJh}N_m" + + "ArrDlr@h|HzjDbsAvy@~~EdTxpJje@jlEltBboDjJdvKyZpzExrAxpHfg@pmJg[tgJuqBnlIarAh}DbN`hCeOf_Ib" + + "xA~uFt|A|xEt_ArmBcN|sB|h@b_DjOzbJ{RlxCcfAp~AahAbqG~Gr}AerA`dCwlCbaFo]twKt{@bsG|}A~fDlvBvz" + + "@tw@rpD_r@rqB{PvbHek@vsHlh@ptNtm@fkD[~xFeEbyKnjDdyDbbBtuA|~Br|Gx_AfxCt}CjnHv`Ew\\lnBdrBfq" + + "BraD|{BldBxpG|]jqC`mArcBv]rdAxgBzdEb{InaBzyC}AzaEaIvrCzcAzsCtfD~qGoPfeEh]h`BxiB`e@`kBxfAv" + + "^pyA`}BhkCdoCtrC~bCxhCbgEplKrk@tiAteBwAxbCwuAnnCc]b{FjrDdjGhhGzfCrlDruBzSrnGhvDhcFzw@n{@z" + + "xAf}Fd{IzaDnbDjoAjqJjfDlbIlzAraBxrB}K~`GpuD~`BjmDhkBp{@r_AxCrnAjrCx`AzrBj{B|r@~qBbdAjtDnv" + + "CtNzpHxeApyC|GlfM`fHtMvqLjuEtlDvoFbnCt|@xmAvqBkGreFm~@hlHw|AltC}NtkGvhBfaJ|~@riAxuC~gErwC" + + "ttCzjAdmGuF`iFv`AxsJftD|nDr_QtbMz_DheAf~Buy@rlC`i@d_CljC`gBr|H|nAf_Fh{G|mE~kAhgKviEpaQnu@" + + "zwAlrA`G~gFnvItz@j{Cng@j{D{]`tEftCdcIsPz{DddE~}PlnE|dJnzG`eG`mF|aJdqDvoAwWjzHv`H`wOtjGzeX" + + "hhBlxErfCf{BtsCjpEjtD|}Aja@xnAbdDt|ErMrdFh{CzgAnlCnr@`wEM~mE`bA`uD|MlwKxmBvuFlhB|sN`_@fvB" + + "p`CxhCt_@loDsS|eDlmChgFlqCbjCxk@vbGxmCjbMba@rpBaoClcCk_DhgEzYdzBl\\vsA_JfGztAbShkGtEhlDzh" + + "C~w@hnB{e@yF}`D`_Ayx@~vGqn@l}CafC"; @Test public void testPolylineEncodingRoundTrip() throws Exception { List points = PolylineEncoding.decode(SYD_MELB_ROUTE); String encodedPath = PolylineEncoding.encode(points); assertEquals(SYD_MELB_ROUTE, encodedPath); - } @Test diff --git a/src/test/java/com/google/maps/internal/RateLimitExecutorServiceTest.java b/src/test/java/com/google/maps/internal/RateLimitExecutorServiceTest.java index 7a3726202..2716fccdd 100644 --- a/src/test/java/com/google/maps/internal/RateLimitExecutorServiceTest.java +++ b/src/test/java/com/google/maps/internal/RateLimitExecutorServiceTest.java @@ -16,43 +16,46 @@ package com.google.maps.internal; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import com.google.maps.MediumTests; - -import org.junit.Test; -import org.junit.experimental.categories.Category; - import java.util.AbstractMap; import java.util.Date; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import java.util.logging.Logger; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Category(MediumTests.class) public class RateLimitExecutorServiceTest { - private static final Logger log = Logger.getLogger(RateLimitExecutorServiceTest.class.getName()); + private static final Logger LOG = + LoggerFactory.getLogger(RateLimitExecutorServiceTest.class.getName()); @Test public void testRateLimitDoesNotExceedSuppliedQps() throws Exception { int qps = 10; RateLimitExecutorService service = new RateLimitExecutorService(); - service.setQueriesPerSecond(qps, 50); - final ConcurrentHashMap executedTimestamps = new ConcurrentHashMap(); + service.setQueriesPerSecond(qps); + final ConcurrentHashMap executedTimestamps = new ConcurrentHashMap<>(); for (int i = 0; i < 100; i++) { - Runnable emptyTask = new Runnable() { - @Override - public void run() { - int nearestSecond = (int) (new Date().getTime() / 1000); - if (executedTimestamps.containsKey(nearestSecond)) { - executedTimestamps.put(nearestSecond, executedTimestamps.get(nearestSecond) + 1); - } else { - executedTimestamps.put(nearestSecond, 1); - } - } - }; + Runnable emptyTask = + new Runnable() { + @Override + public void run() { + int nearestSecond = (int) (new Date().getTime() / 1000); + if (executedTimestamps.containsKey(nearestSecond)) { + executedTimestamps.put(nearestSecond, executedTimestamps.get(nearestSecond) + 1); + } else { + executedTimestamps.put(nearestSecond, 1); + } + } + }; service.execute(emptyTask); } @@ -68,9 +71,11 @@ public void run() { for (Integer timestamp : executedTimestamps.keySet()) { Integer actualQps = executedTimestamps.get(timestamp); // Logging QPS here to detect if a previous iteration had qps-1 and this is qps+1. - log.info(String.format("Timestamp(%d) logged %d queries (target of %d qps)", - timestamp, actualQps, qps)); - assertTrue(String.format("Expected <= %d queries in a second, got %d.", qps, actualQps), + LOG.info( + String.format( + "Timestamp(%d) logged %d queries (target of %d qps)", timestamp, actualQps, qps)); + assertTrue( + String.format("Expected <= %d queries in a second, got %d.", qps, actualQps), actualQps <= qps); } // Check that we executed every request @@ -86,4 +91,20 @@ private static int countTotalRequests(AbstractMap hashMap) { } return counter; } + + @Test + public void testDelayThreadIsStoppedAfterShutdownIsCalled() throws InterruptedException { + RateLimitExecutorService service = new RateLimitExecutorService(); + final Thread delayThread = service.delayThread; + assertNotNull( + "Delay thread should be created in constructor of RateLimitExecutorService", delayThread); + assertTrue( + "Delay thread should start in constructor of RateLimitExecutorService", + delayThread.isAlive()); + // this is needed to make sure that delay thread has reached queue.take() + delayThread.join(10); + service.shutdown(); + delayThread.join(10); + assertFalse(delayThread.isAlive()); + } } diff --git a/src/test/java/com/google/maps/internal/UrlSignerTest.java b/src/test/java/com/google/maps/internal/UrlSignerTest.java index c510e7b11..a7c0698ef 100644 --- a/src/test/java/com/google/maps/internal/UrlSignerTest.java +++ b/src/test/java/com/google/maps/internal/UrlSignerTest.java @@ -15,18 +15,22 @@ package com.google.maps.internal; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import com.google.maps.SmallTests; - +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import okio.ByteString; - import org.junit.Test; import org.junit.experimental.categories.Category; -/** - * Test case for {@link UrlSigner}. - */ +/** Test case for {@link UrlSigner}. */ @Category(SmallTests.class) public class UrlSignerTest { @@ -34,11 +38,13 @@ public class UrlSignerTest { // HMAC_SHA1("key", "The quick brown fox jumps over the lazy dog") // = 0xde7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 private static final String MESSAGE = "The quick brown fox jumps over the lazy dog"; - private static final String SIGNING_KEY = ByteString.of("key".getBytes()) - .base64().replace('+', '-').replace('/', '_'); - private static final String SIGNATURE = ByteString.of( - hexStringToByteArray("de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9")).base64() - .replace('+', '-').replace('/', '_'); + private static final String SIGNING_KEY = + ByteString.of("key".getBytes(UTF_8)).base64().replace('+', '-').replace('/', '_'); + private static final String SIGNATURE = + ByteString.of(hexStringToByteArray("de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9")) + .base64() + .replace('+', '-') + .replace('/', '_'); @Test public void testUrlSigner() throws Exception { @@ -46,15 +52,44 @@ public void testUrlSigner() throws Exception { assertEquals(SIGNATURE, urlSigner.getSignature(MESSAGE)); } + @Test + public void testMustSupportParallelSignatures() throws Exception { + int attempts = 100; + ExecutorService executor = Executors.newFixedThreadPool(attempts); + + final UrlSigner urlSigner = new UrlSigner(SIGNING_KEY); + final List fails = Collections.synchronizedList(new ArrayList()); + + for (int i = 0; i < attempts; i++) { + executor.execute( + new Runnable() { + @Override + public void run() { + try { + if (!SIGNATURE.equals(urlSigner.getSignature(MESSAGE))) { + fails.add(true); + } + } catch (Exception e) { + fails.add(true); + } + } + }); + } + + executor.shutdown(); + executor.awaitTermination(20, TimeUnit.SECONDS); + + assertTrue(fails.isEmpty()); + } + // Helper code from http://stackoverflow.com/questions/140131/ - private static byte[] hexStringToByteArray(String s) { + private static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); + data[i / 2] = + (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); } return data; } - } diff --git a/src/test/java/com/google/maps/metrics/OpenCensusTest.java b/src/test/java/com/google/maps/metrics/OpenCensusTest.java new file mode 100644 index 000000000..1efc90926 --- /dev/null +++ b/src/test/java/com/google/maps/metrics/OpenCensusTest.java @@ -0,0 +1,122 @@ +package com.google.maps; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.maps.internal.ApiConfig; +import com.google.maps.metrics.OpenCensusMetrics; +import com.google.maps.metrics.OpenCensusRequestMetricsReporter; +import com.google.maps.model.GeocodingResult; +import io.opencensus.stats.AggregationData; +import io.opencensus.stats.Stats; +import io.opencensus.stats.View; +import io.opencensus.stats.ViewData; +import io.opencensus.tags.TagValue; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category(MediumTests.class) +public class OpenCensusTest { + + private MockWebServer server; + private GeoApiContext context; + + @Before + public void Setup() { + server = new MockWebServer(); + context = + new GeoApiContext.Builder() + .apiKey("AIza...") + .requestMetricsReporter(new OpenCensusRequestMetricsReporter()) + .baseUrlOverride("http://127.0.0.1:" + server.getPort()) + .build(); + OpenCensusMetrics.registerAllViews(); + } + + @After + @SuppressWarnings("CatchAndPrintStackTrace") + public void Teardown() { + try { + server.shutdown(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private MockResponse mockResponse(int code, String status, int delayMs) { + MockResponse response = new MockResponse(); + response.setResponseCode(code); + if (status != null) { + response.setBody("{\"results\" : [{}], \"status\" : \"" + status + "\" }"); + } + response.setBodyDelay(delayMs, TimeUnit.MILLISECONDS); + return response; + } + + private void sleep(int milliseconds) { + try { + TimeUnit.MILLISECONDS.sleep(milliseconds); + } catch (Exception e) { + } + } + + private Map.Entry, AggregationData> getMetric(String name) { + sleep(10); + ViewData viewData = Stats.getViewManager().getView(View.Name.create(name)); + Map, AggregationData> values = viewData.getAggregationMap(); + assertEquals(1, values.size()); + for (Map.Entry, AggregationData> entry : values.entrySet()) { + return entry; + } + return null; + } + + @Test + public void testSuccess() throws Exception { + server.enqueue(mockResponse(500, "OK", 100)); // retry 1 + server.enqueue(mockResponse(500, "OK", 100)); // retry 2 + server.enqueue(mockResponse(200, "OK", 300)); // succeed + + GeocodingResult[] result = + context.get(new ApiConfig("/path"), GeocodingApi.Response.class, "k", "v").await(); + assertEquals(1, result.length); + + List tags = + Arrays.asList(TagValue.create(""), TagValue.create("200"), TagValue.create("/path")); + + Map.Entry, AggregationData> latencyMetric = + getMetric("maps.googleapis.com/client/request_latency"); + assertNotNull(latencyMetric); + assertEquals(tags, latencyMetric.getKey()); + AggregationData.DistributionData latencyDist = + (AggregationData.DistributionData) latencyMetric.getValue(); + assertEquals(1, latencyDist.getCount()); + assertTrue(latencyDist.getMean() > 500); + + Map.Entry, AggregationData> retryMetric = + getMetric("maps.googleapis.com/client/retry_count"); + assertNotNull(retryMetric); + assertEquals(tags, retryMetric.getKey()); + AggregationData.DistributionData retryDist = + (AggregationData.DistributionData) retryMetric.getValue(); + assertEquals(1, retryDist.getCount()); + assertEquals(2.0, retryDist.getMean(), 0.1); + + Map.Entry, AggregationData> countMetric = + getMetric("maps.googleapis.com/client/request_count"); + assertNotNull(countMetric); + assertEquals(tags, countMetric.getKey()); + AggregationData.CountData count = (AggregationData.CountData) countMetric.getValue(); + assertEquals(1, count.getCount()); + } +} diff --git a/src/test/java/com/google/maps/model/EnumsTest.java b/src/test/java/com/google/maps/model/EnumsTest.java index e1399d627..a77d85d73 100644 --- a/src/test/java/com/google/maps/model/EnumsTest.java +++ b/src/test/java/com/google/maps/model/EnumsTest.java @@ -15,11 +15,18 @@ package com.google.maps.model; -import com.google.maps.SmallTests; - import static com.google.maps.internal.StringJoin.UrlValue; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import com.google.maps.SmallTests; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -32,7 +39,271 @@ public void testUnknown() throws Exception { assertCannotGetUrlValue(AddressType.UNKNOWN); assertCannotGetUrlValue(LocationType.UNKNOWN); assertCannotGetUrlValue(TravelMode.UNKNOWN); + } + + @Test + public void testCanonicalLiteralsForAddressType() { + Map addressTypeToLiteralMap = new HashMap(); + // Short alias just to avoid line wrapping in the below code + Map m = addressTypeToLiteralMap; + m.put(AddressType.STREET_ADDRESS, "street_address"); + m.put(AddressType.ROUTE, "route"); + m.put(AddressType.INTERSECTION, "intersection"); + m.put(AddressType.POLITICAL, "political"); + m.put(AddressType.COUNTRY, "country"); + m.put(AddressType.CONTINENT, "continent"); + m.put(AddressType.ADMINISTRATIVE_AREA_LEVEL_1, "administrative_area_level_1"); + m.put(AddressType.ADMINISTRATIVE_AREA_LEVEL_2, "administrative_area_level_2"); + m.put(AddressType.ADMINISTRATIVE_AREA_LEVEL_3, "administrative_area_level_3"); + m.put(AddressType.ADMINISTRATIVE_AREA_LEVEL_4, "administrative_area_level_4"); + m.put(AddressType.ADMINISTRATIVE_AREA_LEVEL_5, "administrative_area_level_5"); + m.put(AddressType.COLLOQUIAL_AREA, "colloquial_area"); + m.put(AddressType.LOCALITY, "locality"); + m.put(AddressType.WARD, "ward"); + m.put(AddressType.SUBLOCALITY, "sublocality"); + m.put(AddressType.SUBLOCALITY_LEVEL_1, "sublocality_level_1"); + m.put(AddressType.SUBLOCALITY_LEVEL_2, "sublocality_level_2"); + m.put(AddressType.SUBLOCALITY_LEVEL_3, "sublocality_level_3"); + m.put(AddressType.SUBLOCALITY_LEVEL_4, "sublocality_level_4"); + m.put(AddressType.SUBLOCALITY_LEVEL_5, "sublocality_level_5"); + m.put(AddressType.NEIGHBORHOOD, "neighborhood"); + m.put(AddressType.PREMISE, "premise"); + m.put(AddressType.SUBPREMISE, "subpremise"); + m.put(AddressType.POSTAL_CODE, "postal_code"); + m.put(AddressType.PLUS_CODE, "plus_code"); + m.put(AddressType.NATURAL_FEATURE, "natural_feature"); + m.put(AddressType.AIRPORT, "airport"); + m.put(AddressType.PARK, "park"); + m.put(AddressType.POINT_OF_INTEREST, "point_of_interest"); + m.put(AddressType.POST_OFFICE, "post_office"); + m.put(AddressType.PLACE_OF_WORSHIP, "place_of_worship"); + m.put(AddressType.BUS_STATION, "bus_station"); + m.put(AddressType.TRAIN_STATION, "train_station"); + m.put(AddressType.SUBWAY_STATION, "subway_station"); + m.put(AddressType.TRANSIT_STATION, "transit_station"); + m.put(AddressType.CHURCH, "church"); + m.put(AddressType.PRIMARY_SCHOOL, "primary_school"); + m.put(AddressType.SECONDARY_SCHOOL, "secondary_school"); + m.put(AddressType.FINANCE, "finance"); + m.put(AddressType.ESTABLISHMENT, "establishment"); + m.put(AddressType.POSTAL_TOWN, "postal_town"); + m.put(AddressType.UNIVERSITY, "university"); + m.put(AddressType.STREET_NUMBER, "street_number"); + m.put(AddressType.FLOOR, "floor"); + m.put(AddressType.ROOM, "room"); + m.put(AddressType.POST_BOX, "post_box"); + m.put(AddressType.POSTAL_CODE_PREFIX, "postal_code_prefix"); + m.put(AddressType.POSTAL_CODE_SUFFIX, "postal_code_suffix"); + m.put(AddressType.MUSEUM, "museum"); + m.put(AddressType.LIGHT_RAIL_STATION, "light_rail_station"); + m.put(AddressType.SYNAGOGUE, "synagogue"); + m.put(AddressType.FOOD, "food"); + m.put(AddressType.GROCERY_OR_SUPERMARKET, "grocery_or_supermarket"); + m.put(AddressType.STORE, "store"); + m.put(AddressType.DRUGSTORE, "drugstore"); + m.put(AddressType.LAWYER, "lawyer"); + m.put(AddressType.HEALTH, "health"); + m.put(AddressType.INSURANCE_AGENCY, "insurance_agency"); + m.put(AddressType.GAS_STATION, "gas_station"); + m.put(AddressType.CAR_DEALER, "car_dealer"); + m.put(AddressType.CAR_REPAIR, "car_repair"); + m.put(AddressType.MEAL_TAKEAWAY, "meal_takeaway"); + m.put(AddressType.FURNITURE_STORE, "furniture_store"); + m.put(AddressType.HOME_GOODS_STORE, "home_goods_store"); + m.put(AddressType.SHOPPING_MALL, "shopping_mall"); + m.put(AddressType.GYM, "gym"); + m.put(AddressType.ACCOUNTING, "accounting"); + m.put(AddressType.MOVING_COMPANY, "moving_company"); + m.put(AddressType.LODGING, "lodging"); + m.put(AddressType.STORAGE, "storage"); + m.put(AddressType.CASINO, "casino"); + m.put(AddressType.PARKING, "parking"); + m.put(AddressType.STADIUM, "stadium"); + m.put(AddressType.TRAVEL_AGENCY, "travel_agency"); + m.put(AddressType.NIGHT_CLUB, "night_club"); + m.put(AddressType.BEAUTY_SALON, "beauty_salon"); + m.put(AddressType.HAIR_CARE, "hair_care"); + m.put(AddressType.SPA, "spa"); + m.put(AddressType.SHOE_STORE, "shoe_store"); + m.put(AddressType.BAKERY, "bakery"); + m.put(AddressType.PHARMACY, "pharmacy"); + m.put(AddressType.SCHOOL, "school"); + m.put(AddressType.BOOK_STORE, "book_store"); + m.put(AddressType.DEPARTMENT_STORE, "department_store"); + m.put(AddressType.RESTAURANT, "restaurant"); + m.put(AddressType.REAL_ESTATE_AGENCY, "real_estate_agency"); + m.put(AddressType.BAR, "bar"); + m.put(AddressType.DOCTOR, "doctor"); + m.put(AddressType.HOSPITAL, "hospital"); + m.put(AddressType.FIRE_STATION, "fire_station"); + m.put(AddressType.SUPERMARKET, "supermarket"); + m.put(AddressType.CITY_HALL, "city_hall"); + m.put(AddressType.LOCAL_GOVERNMENT_OFFICE, "local_government_office"); + m.put(AddressType.ATM, "atm"); + m.put(AddressType.BANK, "bank"); + m.put(AddressType.LIBRARY, "library"); + m.put(AddressType.CAR_WASH, "car_wash"); + m.put(AddressType.HARDWARE_STORE, "hardware_store"); + m.put(AddressType.AMUSEMENT_PARK, "amusement_park"); + m.put(AddressType.AQUARIUM, "aquarium"); + m.put(AddressType.ART_GALLERY, "art_gallery"); + m.put(AddressType.BICYCLE_STORE, "bicycle_store"); + m.put(AddressType.BOWLING_ALLEY, "bowling_alley"); + m.put(AddressType.CAFE, "cafe"); + m.put(AddressType.CAMPGROUND, "campground"); + m.put(AddressType.CAR_RENTAL, "car_rental"); + m.put(AddressType.CEMETERY, "cemetery"); + m.put(AddressType.CLOTHING_STORE, "clothing_store"); + m.put(AddressType.CONVENIENCE_STORE, "convenience_store"); + m.put(AddressType.COURTHOUSE, "courthouse"); + m.put(AddressType.DENTIST, "dentist"); + m.put(AddressType.ELECTRICIAN, "electrician"); + m.put(AddressType.ELECTRONICS_STORE, "electronics_store"); + m.put(AddressType.EMBASSY, "embassy"); + m.put(AddressType.FLORIST, "florist"); + m.put(AddressType.FUNERAL_HOME, "funeral_home"); + m.put(AddressType.GENERAL_CONTRACTOR, "general_contractor"); + m.put(AddressType.HINDU_TEMPLE, "hindu_temple"); + m.put(AddressType.JEWELRY_STORE, "jewelry_store"); + m.put(AddressType.LAUNDRY, "laundry"); + m.put(AddressType.LIQUOR_STORE, "liquor_store"); + m.put(AddressType.LOCKSMITH, "locksmith"); + m.put(AddressType.MEAL_DELIVERY, "meal_delivery"); + m.put(AddressType.MOSQUE, "mosque"); + m.put(AddressType.MOVIE_RENTAL, "movie_rental"); + m.put(AddressType.MOVIE_THEATER, "movie_theater"); + m.put(AddressType.PAINTER, "painter"); + m.put(AddressType.PET_STORE, "pet_store"); + m.put(AddressType.PHYSIOTHERAPIST, "physiotherapist"); + m.put(AddressType.PLUMBER, "plumber"); + m.put(AddressType.POLICE, "police"); + m.put(AddressType.ROOFING_CONTRACTOR, "roofing_contractor"); + m.put(AddressType.RV_PARK, "rv_park"); + m.put(AddressType.TAXI_STAND, "taxi_stand"); + m.put(AddressType.VETERINARY_CARE, "veterinary_care"); + m.put(AddressType.ZOO, "zoo"); + m.put(AddressType.ARCHIPELAGO, "archipelago"); + m.put(AddressType.TOURIST_ATTRACTION, "tourist_attraction"); + m.put(AddressType.TOWN_SQUARE, "town_square"); + + for (Map.Entry addressTypeLiteralPair : + addressTypeToLiteralMap.entrySet()) { + assertEquals( + addressTypeLiteralPair.getValue(), addressTypeLiteralPair.getKey().toCanonicalLiteral()); + } + List enumsMinusUnknown = new ArrayList<>(Arrays.asList(AddressType.values())); + enumsMinusUnknown.remove(AddressType.UNKNOWN); + List onlyInTest = setdiff(addressTypeToLiteralMap.keySet(), enumsMinusUnknown); + List onlyInEnum = setdiff(enumsMinusUnknown, addressTypeToLiteralMap.keySet()); + assertEquals( + "Unexpected enum elements: Only in test: " + onlyInTest + ". Only in enum: " + onlyInEnum, + addressTypeToLiteralMap.size() + 1, // 1 for unknown + AddressType.values().length); + } + + @Test + public void testCanonicalLiteralsForAddressComponentType() { + Map addressComponentTypeToLiteralMap = + new HashMap(); + // Short alias just to avoid line wrapping in the below code + Map m = addressComponentTypeToLiteralMap; + m.put(AddressComponentType.STREET_ADDRESS, "street_address"); + m.put(AddressComponentType.ROUTE, "route"); + m.put(AddressComponentType.INTERSECTION, "intersection"); + m.put(AddressComponentType.POLITICAL, "political"); + m.put(AddressComponentType.COUNTRY, "country"); + m.put(AddressComponentType.CONTINENT, "continent"); + m.put(AddressComponentType.ADMINISTRATIVE_AREA_LEVEL_1, "administrative_area_level_1"); + m.put(AddressComponentType.ADMINISTRATIVE_AREA_LEVEL_2, "administrative_area_level_2"); + m.put(AddressComponentType.ADMINISTRATIVE_AREA_LEVEL_3, "administrative_area_level_3"); + m.put(AddressComponentType.ADMINISTRATIVE_AREA_LEVEL_4, "administrative_area_level_4"); + m.put(AddressComponentType.ADMINISTRATIVE_AREA_LEVEL_5, "administrative_area_level_5"); + m.put(AddressComponentType.COLLOQUIAL_AREA, "colloquial_area"); + m.put(AddressComponentType.LOCALITY, "locality"); + m.put(AddressComponentType.WARD, "ward"); + m.put(AddressComponentType.SUBLOCALITY, "sublocality"); + m.put(AddressComponentType.SUBLOCALITY_LEVEL_1, "sublocality_level_1"); + m.put(AddressComponentType.SUBLOCALITY_LEVEL_2, "sublocality_level_2"); + m.put(AddressComponentType.SUBLOCALITY_LEVEL_3, "sublocality_level_3"); + m.put(AddressComponentType.SUBLOCALITY_LEVEL_4, "sublocality_level_4"); + m.put(AddressComponentType.SUBLOCALITY_LEVEL_5, "sublocality_level_5"); + m.put(AddressComponentType.NEIGHBORHOOD, "neighborhood"); + m.put(AddressComponentType.PREMISE, "premise"); + m.put(AddressComponentType.SUBPREMISE, "subpremise"); + m.put(AddressComponentType.POSTAL_CODE, "postal_code"); + m.put(AddressComponentType.POST_BOX, "post_box"); + m.put(AddressComponentType.POSTAL_CODE_PREFIX, "postal_code_prefix"); + m.put(AddressComponentType.POSTAL_CODE_SUFFIX, "postal_code_suffix"); + m.put(AddressComponentType.NATURAL_FEATURE, "natural_feature"); + m.put(AddressComponentType.AIRPORT, "airport"); + m.put(AddressComponentType.PARK, "park"); + m.put(AddressComponentType.FLOOR, "floor"); + m.put(AddressComponentType.PARKING, "parking"); + m.put(AddressComponentType.POINT_OF_INTEREST, "point_of_interest"); + m.put(AddressComponentType.BUS_STATION, "bus_station"); + m.put(AddressComponentType.TRAIN_STATION, "train_station"); + m.put(AddressComponentType.SUBWAY_STATION, "subway_station"); + m.put(AddressComponentType.TRANSIT_STATION, "transit_station"); + m.put(AddressComponentType.LIGHT_RAIL_STATION, "light_rail_station"); + m.put(AddressComponentType.ESTABLISHMENT, "establishment"); + m.put(AddressComponentType.POSTAL_TOWN, "postal_town"); + m.put(AddressComponentType.ROOM, "room"); + m.put(AddressComponentType.STREET_NUMBER, "street_number"); + m.put(AddressComponentType.GENERAL_CONTRACTOR, "general_contractor"); + m.put(AddressComponentType.FOOD, "food"); + m.put(AddressComponentType.REAL_ESTATE_AGENCY, "real_estate_agency"); + m.put(AddressComponentType.CAR_RENTAL, "car_rental"); + m.put(AddressComponentType.STORE, "store"); + m.put(AddressComponentType.SHOPPING_MALL, "shopping_mall"); + m.put(AddressComponentType.LODGING, "lodging"); + m.put(AddressComponentType.TRAVEL_AGENCY, "travel_agency"); + m.put(AddressComponentType.ELECTRONICS_STORE, "electronics_store"); + m.put(AddressComponentType.HOME_GOODS_STORE, "home_goods_store"); + m.put(AddressComponentType.SCHOOL, "school"); + m.put(AddressComponentType.ART_GALLERY, "art_gallery"); + m.put(AddressComponentType.LAWYER, "lawyer"); + m.put(AddressComponentType.RESTAURANT, "restaurant"); + m.put(AddressComponentType.BAR, "bar"); + m.put(AddressComponentType.MEAL_TAKEAWAY, "meal_takeaway"); + m.put(AddressComponentType.CLOTHING_STORE, "clothing_store"); + m.put(AddressComponentType.LOCAL_GOVERNMENT_OFFICE, "local_government_office"); + m.put(AddressComponentType.FINANCE, "finance"); + m.put(AddressComponentType.MOVING_COMPANY, "moving_company"); + m.put(AddressComponentType.STORAGE, "storage"); + m.put(AddressComponentType.CAFE, "cafe"); + m.put(AddressComponentType.CAR_REPAIR, "car_repair"); + m.put(AddressComponentType.HEALTH, "health"); + m.put(AddressComponentType.INSURANCE_AGENCY, "insurance_agency"); + m.put(AddressComponentType.PAINTER, "painter"); + m.put(AddressComponentType.ARCHIPELAGO, "archipelago"); + m.put(AddressComponentType.MUSEUM, "museum"); + m.put(AddressComponentType.RV_PARK, "rv_park"); + m.put(AddressComponentType.CAMPGROUND, "campground"); + m.put(AddressComponentType.MEAL_DELIVERY, "meal_delivery"); + m.put(AddressComponentType.PRIMARY_SCHOOL, "primary_school"); + m.put(AddressComponentType.SECONDARY_SCHOOL, "secondary_school"); + m.put(AddressComponentType.TOWN_SQUARE, "town_square"); + m.put(AddressComponentType.TOURIST_ATTRACTION, "tourist_attraction"); + m.put(AddressComponentType.PLUS_CODE, "plus_code"); + m.put(AddressComponentType.DRUGSTORE, "drugstore"); + for (Map.Entry AddressComponentTypeLiteralPair : + addressComponentTypeToLiteralMap.entrySet()) { + assertEquals( + AddressComponentTypeLiteralPair.getValue(), + AddressComponentTypeLiteralPair.getKey().toCanonicalLiteral()); + } + List enumsMinusUnknown = + new ArrayList<>(Arrays.asList(AddressComponentType.values())); + enumsMinusUnknown.remove(AddressComponentType.UNKNOWN); + List onlyInTest = + setdiff(addressComponentTypeToLiteralMap.keySet(), enumsMinusUnknown); + List onlyInEnum = + setdiff(enumsMinusUnknown, addressComponentTypeToLiteralMap.keySet()); + assertEquals( + "Unexpected enum elements: Only in test: " + onlyInTest + ". Only in enum: " + onlyInEnum, + addressComponentTypeToLiteralMap.size() + 1, // 1 for unknown + AddressComponentType.values().length); } private static void assertCannotGetUrlValue(T unknown) { @@ -44,4 +315,10 @@ private static void assertCannotGetUrlValue(T unknown) { // Expected. } } + + private static List setdiff(Collection a, Collection b) { + List out = new ArrayList<>(a); + out.removeAll(b); + return out; + } } diff --git a/src/test/java/com/google/maps/model/LatLngAssert.java b/src/test/java/com/google/maps/model/LatLngAssert.java index 6ae414c62..5042133d0 100644 --- a/src/test/java/com/google/maps/model/LatLngAssert.java +++ b/src/test/java/com/google/maps/model/LatLngAssert.java @@ -17,13 +17,10 @@ import org.junit.Assert; -/** - * Testing infrastructure for {@see LatLng}. - */ +/** Testing infrastructure for {@see LatLng}. */ public class LatLngAssert { - private LatLngAssert() { - } + private LatLngAssert() {} public static void assertEquals(LatLng a, LatLng b, double epsilon) { Assert.assertEquals(a.lat, b.lat, epsilon); diff --git a/src/test/resources/com/google/maps/AutocompletePredictionStructuredFormatting.json b/src/test/resources/com/google/maps/AutocompletePredictionStructuredFormatting.json new file mode 100644 index 000000000..a444a8f33 --- /dev/null +++ b/src/test/resources/com/google/maps/AutocompletePredictionStructuredFormatting.json @@ -0,0 +1,50 @@ +{ + "predictions" : [ + { + "description" : "1033 Princes Highway, Heathmere, Victoria, Australia", + "id" : "7416118f95efcce4c73df632b077ffbbae4998a5", + "matched_substrings" : [ + { + "length" : 1, + "offset" : 0 + } + ], + "place_id" : "ChIJJS6K87yNnaoRXPN-iCJ0bJs", + "reference" : "ClRMAAAAoJx3vtMNpjfM766x_5YghMpc_zsDSWJ2qxcNWeKTvBgWanr9F4obKq_1R3lNWndCEuHjszqAnC2PjvHwCOpcKghwVDVQq9M5WK_RmwUDQZISEL2bAGMhWuyV7zML8JOdo44aFIoljjHi2IkPqQ4DQ2nGe-YBGCD9", + "structured_formatting" : { + "main_text" : "1033 Princes Highway", + "main_text_matched_substrings" : [ + { + "length" : 1, + "offset" : 0 + } + ], + "secondary_text" : "Heathmere, Victoria, Australia" + }, + "terms" : [ + { + "offset" : 0, + "value" : "1033" + }, + { + "offset" : 5, + "value" : "Princes Highway" + }, + { + "offset" : 22, + "value" : "Heathmere" + }, + { + "offset" : 33, + "value" : "Victoria" + }, + { + "offset" : 43, + "value" : "Australia" + } + ], + "types" : [ "street_address", "geocode" ] + } + ], + "status" : "OK" +} diff --git a/src/test/resources/com/google/maps/DirectionsAlongPath.json b/src/test/resources/com/google/maps/DirectionsAlongPath.json new file mode 100644 index 000000000..7e6d872ef --- /dev/null +++ b/src/test/resources/com/google/maps/DirectionsAlongPath.json @@ -0,0 +1,805 @@ +{ + "results": [ + { + "elevation": 19.30763816833496, + "location": { + "lat": -33.86746, + "lng": 151.20709 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 13.63531494140625, + "location": { + "lat": -33.93418870760096, + "lng": 151.2048109521581 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 22.67794799804688, + "location": { + "lat": -33.93611348767503, + "lng": 151.1179499439254 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 25.5423412322998, + "location": { + "lat": -33.94179099529389, + "lng": 151.0248274293727 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 12.80434608459473, + "location": { + "lat": -33.93988322412878, + "lng": 150.931696721774 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 65.37848663330078, + "location": { + "lat": -33.98155290568498, + "lng": 150.8655331001279 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 67.65006256103516, + "location": { + "lat": -34.04267539606656, + "lng": 150.8155353482151 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 99.75529479980469, + "location": { + "lat": -34.10401673708059, + "lng": 150.7623006399985 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 130.8006286621094, + "location": { + "lat": -34.17605223765933, + "lng": 150.7294790364836 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 193.3520812988281, + "location": { + "lat": -34.23305557476613, + "lng": 150.6668251612414 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 334.890625, + "location": { + "lat": -34.29700428521164, + "lng": 150.6171258096405 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 485.6658325195312, + "location": { + "lat": -34.35004990313182, + "lng": 150.5499644903976 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 603.6323852539062, + "location": { + "lat": -34.41241440700556, + "lng": 150.4952284048991 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 675.9644165039062, + "location": { + "lat": -34.4425741790337, + "lng": 150.4096270725346 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 658.4600219726562, + "location": { + "lat": -34.47487561563866, + "lng": 150.3242324292161 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 641.0990600585938, + "location": { + "lat": -34.54012551377705, + "lng": 150.2781951681288 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 694.635986328125, + "location": { + "lat": -34.60508569100411, + "lng": 150.229317759589 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 683.1285400390625, + "location": { + "lat": -34.63561797380088, + "lng": 150.147123900314 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 616.5374145507812, + "location": { + "lat": -34.66987222591835, + "lng": 150.0636921407473 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 633.684814453125, + "location": { + "lat": -34.72187320232963, + "lng": 149.9942253301121 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 646.775146484375, + "location": { + "lat": -34.743529011363, + "lng": 149.9060607952434 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 656.656005859375, + "location": { + "lat": -34.7338380169955, + "lng": 149.8157680392622 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 682.2879028320312, + "location": { + "lat": -34.77210959328304, + "lng": 149.7456782656556 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 680.8931884765625, + "location": { + "lat": -34.7832929886516, + "lng": 149.6528497589898 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 706.8088989257812, + "location": { + "lat": -34.80673626202241, + "lng": 149.5671441481079 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 727.0847778320312, + "location": { + "lat": -34.81135441254698, + "lng": 149.4748764248162 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 729.56591796875, + "location": { + "lat": -34.8216651455622, + "lng": 149.3821142460862 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 627.5591430664062, + "location": { + "lat": -34.80186730894788, + "lng": 149.2919867131641 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 619.4981079101562, + "location": { + "lat": -34.7875348306532, + "lng": 149.2024032949869 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 576.5390014648438, + "location": { + "lat": -34.81288310427085, + "lng": 149.1153681417341 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 647.2942504882812, + "location": { + "lat": -34.83132236414112, + "lng": 149.0274384225476 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 554.2569580078125, + "location": { + "lat": -34.81571513403354, + "lng": 148.9393149032084 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 527.8458862304688, + "location": { + "lat": -34.78635978622274, + "lng": 148.8557881679657 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 586.5067749023438, + "location": { + "lat": -34.76745588966663, + "lng": 148.7675650274979 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 520.2943725585938, + "location": { + "lat": -34.80467055620268, + "lng": 148.6906520279299 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 499.1740112304688, + "location": { + "lat": -34.81021493018572, + "lng": 148.6064691468771 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 531.8472900390625, + "location": { + "lat": -34.80496951098601, + "lng": 148.5130627933494 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 386.6448364257812, + "location": { + "lat": -34.81641481079129, + "lng": 148.4204208554565 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 285.3293151855469, + "location": { + "lat": -34.81837143946927, + "lng": 148.3277303062406 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 334.1597595214844, + "location": { + "lat": -34.87074129947463, + "lng": 148.2603507834795 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 322.6882019042969, + "location": { + "lat": -34.90957545829352, + "lng": 148.1787275023388 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 228.2541961669922, + "location": { + "lat": -34.97216511599062, + "lng": 148.1428830649462 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 262.6902770996094, + "location": { + "lat": -35.04018484475662, + "lng": 148.1083624817241 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 215.5640106201172, + "location": { + "lat": -35.10330250149316, + "lng": 148.0614841554738 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 255.7534332275391, + "location": { + "lat": -35.14370728056297, + "lng": 147.9828802039315 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 323.3035278320312, + "location": { + "lat": -35.16387704544926, + "lng": 147.8962708213743 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 292.5396118164062, + "location": { + "lat": -35.19264418422875, + "lng": 147.819233233818 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 229.7057495117188, + "location": { + "lat": -35.24995530328722, + "lng": 147.759766967826 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 249.7723846435547, + "location": { + "lat": -35.30173071741712, + "lng": 147.6907110899704 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 285.0657958984375, + "location": { + "lat": -35.35771005392223, + "lng": 147.6587574936025 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 414.4817199707031, + "location": { + "lat": -35.42739859717759, + "lng": 147.6359161487572 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 383.9430847167969, + "location": { + "lat": -35.48551703499704, + "lng": 147.5732820137972 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 346.4050598144531, + "location": { + "lat": -35.55464686491273, + "lng": 147.5343630171953 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 298.9681091308594, + "location": { + "lat": -35.61686403410487, + "lng": 147.4864667121691 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 276.23583984375, + "location": { + "lat": -35.66800970866227, + "lng": 147.4173724086339 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 288.7292175292969, + "location": { + "lat": -35.69858525987858, + "lng": 147.3309629877854 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 291.6640625, + "location": { + "lat": -35.76198440962613, + "lng": 147.2879959994351 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 301.1924438476562, + "location": { + "lat": -35.82302076537403, + "lng": 147.2368530279028 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 268.1228637695312, + "location": { + "lat": -35.87993886641533, + "lng": 147.1784480103171 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 264.8238220214844, + "location": { + "lat": -35.91575112873619, + "lng": 147.1053279493547 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 195.01171875, + "location": { + "lat": -35.92848814594905, + "lng": 147.0133718704994 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 221.6598510742188, + "location": { + "lat": -35.99763788065796, + "lng": 146.9919283632397 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 170.7210388183594, + "location": { + "lat": -36.06274814599447, + "lng": 146.9443464803252 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 156.9263305664062, + "location": { + "lat": -36.10791161250052, + "lng": 146.8793117774146 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 160.6368560791016, + "location": { + "lat": -36.08216108899676, + "lng": 146.791424792249 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 192.1187133789062, + "location": { + "lat": -36.09478012137878, + "lng": 146.6990218443442 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 229.2239837646484, + "location": { + "lat": -36.14875008812928, + "lng": 146.6313742464744 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 217.0271759033203, + "location": { + "lat": -36.16667405203486, + "lng": 146.542015594317 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 196.7459106445312, + "location": { + "lat": -36.20040696375381, + "lng": 146.4616367372034 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 163.3575897216797, + "location": { + "lat": -36.26550527472256, + "lng": 146.4096523018605 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 155.342041015625, + "location": { + "lat": -36.33473356340676, + "lng": 146.3779072778888 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 151.7095184326172, + "location": { + "lat": -36.39368985502084, + "lng": 146.3338489588857 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 159.3297119140625, + "location": { + "lat": -36.43279405447412, + "lng": 146.2548693194679 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 178.6408538818359, + "location": { + "lat": -36.47254077525418, + "lng": 146.1810937405232 + }, + "resolution": 1221.625854492188 + }, + { + "elevation": 174.1010894775391, + "location": { + "lat": -36.50209154565792, + "lng": 146.0921804414509 + }, + "resolution": 1221.625854492188 + }, + { + "elevation": 176.1711883544922, + "location": { + "lat": -36.55318572207888, + "lng": 146.0270619642694 + }, + "resolution": 1221.625854492188 + }, + { + "elevation": 192.8383331298828, + "location": { + "lat": -36.58068739645458, + "lng": 145.9405445365641 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 196.5705108642578, + "location": { + "lat": -36.60004083342037, + "lng": 145.8524401189636 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 185.3541564941406, + "location": { + "lat": -36.62383329585564, + "lng": 145.7624977977077 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 189.3662567138672, + "location": { + "lat": -36.66718286754705, + "lng": 145.6833754964888 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 176.9458923339844, + "location": { + "lat": -36.72523774478365, + "lng": 145.6203106790113 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 191.1588439941406, + "location": { + "lat": -36.77390071575151, + "lng": 145.5583808108557 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 190.0719299316406, + "location": { + "lat": -36.79953629048179, + "lng": 145.4726208374354 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 251.2273559570312, + "location": { + "lat": -36.83492170950757, + "lng": 145.3870666184597 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 186.9484100341797, + "location": { + "lat": -36.86601888447853, + "lng": 145.2985466452995 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 154.44140625, + "location": { + "lat": -36.91810954144704, + "lng": 145.228457593862 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 168.3538665771484, + "location": { + "lat": -36.97597292239033, + "lng": 145.1688516659076 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 152.7733459472656, + "location": { + "lat": -37.02064152105597, + "lng": 145.1065442755132 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 205.2728881835938, + "location": { + "lat": -37.0966355473582, + "lng": 145.0926706344683 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 232.0518951416016, + "location": { + "lat": -37.17290416911895, + "lng": 145.077242539156 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 245.8816528320312, + "location": { + "lat": -37.24759995164867, + "lng": 145.0533485684018 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 337.6495666503906, + "location": { + "lat": -37.32267999335172, + "lng": 145.0412333831331 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 363.9090270996094, + "location": { + "lat": -37.39392122874037, + "lng": 145.0186364431291 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 336.326171875, + "location": { + "lat": -37.46285875964947, + "lng": 144.9755824884678 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 225.3623809814453, + "location": { + "lat": -37.5362375403782, + "lng": 144.9483133577448 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 176.9776916503906, + "location": { + "lat": -37.60518203922306, + "lng": 144.9703434568565 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 114.5188903808594, + "location": { + "lat": -37.67481088443494, + "lng": 144.9846098878599 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 69.74943542480469, + "location": { + "lat": -37.69489825647631, + "lng": 144.9072009653025 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 39.91798400878906, + "location": { + "lat": -37.74259617351967, + "lng": 144.932710993307 + }, + "resolution": 610.8129272460938 + }, + { + "elevation": 31.31394958496094, + "location": { + "lat": -37.81413000000006, + "lng": 144.9631799999999 + }, + "resolution": 610.8129272460938 + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/DirectionsApiBuilderResponse.json b/src/test/resources/com/google/maps/DirectionsApiBuilderResponse.json new file mode 100644 index 000000000..a726f2924 --- /dev/null +++ b/src/test/resources/com/google/maps/DirectionsApiBuilderResponse.json @@ -0,0 +1,17 @@ +{ + "routes": [ + { + "legs": [ + { + "end_address": "Melbourne VIC, Australia", + "start_address": "Sydney NSW, Australia" + } + ], + "overview_polyline": { + "points": "f`vmE{`|y[gIqq@daAZxtEtYr~AwLztA~jCxAb{Ey_@djFpZxsQjh@ztJqv@tpJvnAryCzmA~_@ruCraC|fErxDfnC|RvlAd|B~`DvuD|oEroApsEvfChfCvUnzGpzHthFpeDz_GnzExaDriG~{E~`FhfF~vCbhBh_B~xAl}Dpx@tuElpAz|HxxEpcMbbD|a@jmBviBp`GfkFlpD~iAt|BjuCfyAbbC`JphGxzApuE|~@hdFh|CptDzhEp_HrnCleJna@fnFzYznIkw@ffDnOnvIhnEfiC`W|kLhhAlbKrqBbgCbMxeDie@b|MzpApyHbj@ryHoLriHaqAlgGczBv`IlHdtEiO`aGtwAvuFjhCxeHdFvqCbrAluJakAnoJgyB|uDrAh`G{tBd_E_rA~dEa\\\\noGth@trGnvA|sEvmDrmCbPnwB_hApxClRbhEqu@xsGmNh_GxyA~sLLbcT|~AnoD~pCtwBd_CliExaAjxEbuD|lIbnEgQl_E~{D`m@`mClvDbr@thDtCnwA|xArgDhPjlC|uDbiA~`FfpCfbDhOvzHzm@`pE|rDziHzHj{GnqDj`AteBh|CrjDtnD`}Fb}GtxCprJ|nAvr@xlDgmA~dBoq@nqBzi@x|DreCnuAj|BloDldChoEtpEdsCr`AhzBl_B~iEbXveBj}AxkIt~L`uC|}CjXd}Cp~C|rJlc@|zBdtAnvAdfCoDrmCfgB`xC~}A~oAn_ElhCpVnuB|gFv`CjoAnrDd}AfzC~qDpEbsGb~@zdBzAb`Nj{FhFvzEtqAhiFprBnnGzfHfoBdqB`u@xuAcVlyCqi@ziHgqBvaE{NzbHhvBx~IfiA|_AflCtoDjjBnyAxaA~eDrYl_IxiA|_MhbDz~CjkJlhGt`H~sFpuDkKflD~F`hCvfChlBtvI~q@b}Dn{Ff}D`{AxsCt_@zpFjsChqJvkAptGl}AxcAbcCrxA`mDvuIn~@zdFii@ngDjxCfdJpA`sGx~Hn~WlpGxkGf|EjyHbtBpxBv~AbHd|@pzCmTrxFxpB~dEvxEhxKv_DjaPtzB~lHngF~}G|jEf_ExgBlpAznC`dE`UfpErSzqAbmBha@|`DtsA~sIoLpvBljApwCfLjsHttBjrFd`@lbDx_AffJhEpdBnb@~cAxjBrtD~p@`sD}S~fDtxC~rEd`Cd`GlwAxhFztBfiJf[tkBytAf_@qcCbdBouAvkC`DzpDdw@vmByM~WjvJmCnuBpfAdzA~eDxAbHoeEbgBmjAxiEq_@jtCqzB~l@aS" + }, + "summary": "M31 and National Highway M31" + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/FindPlaceFromTextMuseumOfContemporaryArt.json b/src/test/resources/com/google/maps/FindPlaceFromTextMuseumOfContemporaryArt.json new file mode 100644 index 000000000..ea7524d53 --- /dev/null +++ b/src/test/resources/com/google/maps/FindPlaceFromTextMuseumOfContemporaryArt.json @@ -0,0 +1,41 @@ + +{ + "candidates" : [ + { + "formatted_address" : "140 George St, The Rocks NSW 2000, Australia", + "geometry" : { + "location" : { + "lat" : -33.8599358, + "lng" : 151.2090295 + }, + "viewport" : { + "northeast" : { + "lat" : -33.85824767010727, + "lng" : 151.2102470798928 + }, + "southwest" : { + "lat" : -33.86094732989272, + "lng" : 151.2075474201073 + } + } + }, + "name" : "Museum of Contemporary Art Australia", + "opening_hours" : { + "open_now" : true, + "weekday_text" : [] + }, + "photos" : [ + { + "height" : 2268, + "html_attributions" : [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113202928073475129698/photos\"\u003eEmily Zimny\u003c/a\u003e" + ], + "photo_reference" : "CmRaAAAAXBZe3QrziBst5oTCPUzL4LSgSuWYMctBNRu8bOP4TfwD0aU80YemnnbhjWdFfMX-kkh5h9NhFJky6fW5Ivk_G9fc11GekI0HOCDASZH3qRJmUBsdw0MWoCDZmwQAg-dVEhBb0aLoJXzoZ8cXWEceB9omGhRrX24jI3VnSEQUmInfYoAwSX4OPw", + "width" : 4032 + } + ], + "rating" : 4.4 + } + ], + "status" : "OK" +} diff --git a/src/test/resources/com/google/maps/GeocodeLibraryType.json b/src/test/resources/com/google/maps/GeocodeLibraryType.json new file mode 100644 index 000000000..8f5979e2d --- /dev/null +++ b/src/test/resources/com/google/maps/GeocodeLibraryType.json @@ -0,0 +1,75 @@ + +{ + "results" : [ + { + "address_components" : [ + { + "long_name" : "3548", + "short_name" : "3548", + "types" : [ "subpremise" ] + }, + { + "long_name" : "1849", + "short_name" : "1849", + "types" : [ "street_number" ] + }, + { + "long_name" : "C Street Northwest", + "short_name" : "C St NW", + "types" : [ "route" ] + }, + { + "long_name" : "Northwest Washington", + "short_name" : "Northwest Washington", + "types" : [ "neighborhood", "political" ] + }, + { + "long_name" : "Washington", + "short_name" : "Washington", + "types" : [ "locality", "political" ] + }, + { + "long_name" : "District of Columbia", + "short_name" : "DC", + "types" : [ "administrative_area_level_1", "political" ] + }, + { + "long_name" : "United States", + "short_name" : "US", + "types" : [ "country", "political" ] + }, + { + "long_name" : "20240", + "short_name" : "20240", + "types" : [ "postal_code" ] + }, + { + "long_name" : "0001", + "short_name" : "0001", + "types" : [ "postal_code_suffix" ] + } + ], + "formatted_address" : "1849 C St NW #3548, Washington, DC 20240, USA", + "geometry" : { + "location" : { + "lat" : 38.8944358, + "lng" : -77.0426044 + }, + "location_type" : "ROOFTOP", + "viewport" : { + "northeast" : { + "lat" : 38.8957847802915, + "lng" : -77.04125541970849 + }, + "southwest" : { + "lat" : 38.8930868197085, + "lng" : -77.0439533802915 + } + } + }, + "place_id" : "ChIJi9derqW3t4kREUwRQi51e24", + "types" : [ "establishment", "library", "point_of_interest" ] + } + ], + "status" : "OK" +} diff --git a/src/test/resources/com/google/maps/GeolocationAlternatePayloadBuilder.json b/src/test/resources/com/google/maps/GeolocationAlternatePayloadBuilder.json new file mode 100644 index 000000000..1c12d5a7f --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationAlternatePayloadBuilder.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.42659, + "lng": -122.07266190000001 + }, + "accuracy": 658.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GeolocationBasicResponse.json b/src/test/resources/com/google/maps/GeolocationBasicResponse.json new file mode 100644 index 000000000..49f614670 --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationBasicResponse.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.3989885, + "lng": -122.0585196 + }, + "accuracy": 150.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GeolocationDocSampleResponse.json b/src/test/resources/com/google/maps/GeolocationDocSampleResponse.json new file mode 100644 index 000000000..b1dddaaf9 --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationDocSampleResponse.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.4248297, + "lng": -122.07346549999998 + }, + "accuracy": 1145.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GeolocationMaximumCellTower.json b/src/test/resources/com/google/maps/GeolocationMaximumCellTower.json new file mode 100644 index 000000000..b1dddaaf9 --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationMaximumCellTower.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.4248297, + "lng": -122.07346549999998 + }, + "accuracy": 1145.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GeolocationMaximumWifiResponse.json b/src/test/resources/com/google/maps/GeolocationMaximumWifiResponse.json new file mode 100644 index 000000000..01e790802 --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationMaximumWifiResponse.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.3990122, + "lng": -122.0583656 + }, + "accuracy": 25.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GeolocationMinimumCellTowerResponse.json b/src/test/resources/com/google/maps/GeolocationMinimumCellTowerResponse.json new file mode 100644 index 000000000..1c12d5a7f --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationMinimumCellTowerResponse.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.42659, + "lng": -122.07266190000001 + }, + "accuracy": 658.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GeolocationMinimumWifiResponse.json b/src/test/resources/com/google/maps/GeolocationMinimumWifiResponse.json new file mode 100644 index 000000000..49f614670 --- /dev/null +++ b/src/test/resources/com/google/maps/GeolocationMinimumWifiResponse.json @@ -0,0 +1,7 @@ +{ + "location": { + "lat": 37.3989885, + "lng": -122.0585196 + }, + "accuracy": 150.0 +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GetDirectionsResponse.json b/src/test/resources/com/google/maps/GetDirectionsResponse.json new file mode 100644 index 000000000..0428c0d40 --- /dev/null +++ b/src/test/resources/com/google/maps/GetDirectionsResponse.json @@ -0,0 +1,37 @@ +{ + "geocoded_waypoints": [ + { + "geocoder_status": "OK", + "place_id": "ChIJP3Sa8ziYEmsRUKgyFmh9AQM", + "types": [ + "colloquial_area", + "locality", + "political" + ] + }, + { + "geocoder_status": "OK", + "place_id": "ChIJ90260rVG1moRkM2MIXVWBAQ", + "types": [ + "colloquial_area", + "locality", + "political" + ] + } + ], + "routes": [ + { + "legs": [ + { + "end_address": "Melbourne VIC, Australia", + "start_address": "Sydney NSW, Australia" + } + ], + "overview_polyline": { + "points": "f`vmE{`|y[gIqq@daAZxtEtYr~AwLztA~jCxAb{Ey_@djFpZxsQjh@ztJqv@tpJvnAryCzmA~_@ruCraC|fErxDfnC|RvlAd|B~`DvuD|oEroApsEvfChfCvUnzGpzHthFpeDz_GnzExaDriG~{E~`FhfF~vCbhBh_B~xAl}Dpx@tuElpAz|HxxEpcMbbD|a@jmBviBp`GfkFlpD~iAt|BjuCfyAbbC`JphGxzApuE|~@hdFh|CptDzhEp_HrnCleJna@fnFzYznIkw@ffDnOnvIhnEfiC`W|kLhhAlbKrqBbgCbMxeDie@b|MzpApyHbj@ryHoLriHaqAlgGczBv`IlHdtEiO`aGtwAvuFjhCxeHdFvqCbrAluJakAnoJgyB|uDrAh`G{tBd_E_rA~dEa\\noGth@trGnvA|sEvmDrmCbPnwB_hApxClRbhEqu@xsGmNh_GxyA~sLLbcT|~AnoD~pCtwBd_CliExaAjxEbuD|lIbnEgQl_E~{D`m@`mClvDbr@thDtCnwA|xArgDhPjlC|uDbiA~`FfpCfbDhOvzHzm@`pE|rDziHzHj{GnqDj`AteBh|CrjDtnD`}Fb}GtxCprJ|nAvr@xlDgmA~dBoq@nqBzi@x|DreCnuAj|BloDldChoEtpEdsCr`AhzBl_B~iEbXveBj}AxkIt~L`uC|}CjXd}Cp~C|rJlc@|zBdtAnvAdfCoDrmCfgB`xC~}A~oAn_ElhCpVnuB|gFv`CjoAnrDd}AfzC~qDpEbsGb~@zdBzAb`Nj{FhFvzEtqAhiFprBnnGzfHfoBdqB`u@xuAcVlyCqi@ziHgqBvaE{NzbHhvBx~IfiA|_AflCtoDjjBnyAxaA~eDrYl_IxiA|_MhbDz~CjkJlhGt`H~sFpuDkKflD~F`hCvfChlBtvI~q@b}Dn{Ff}D`{AxsCt_@zpFjsChqJvkAptGl}AxcAbcCrxA`mDvuIn~@zdFii@ngDjxCfdJpA`sGx~Hn~WlpGxkGf|EjyHbtBpxBv~AbHd|@pzCmTrxFxpB~dEvxEhxKv_DjaPtzB~lHngF~}G|jEf_ExgBlpAznC`dE`UfpErSzqAbmBha@|`DtsA~sIoLpvBljApwCfLjsHttBjrFd`@lbDx_AffJhEpdBnb@~cAxjBrtD~p@`sD}S~fDtxC~rEd`Cd`GlwAxhFztBfiJf[tkBytAf_@qcCbdBouAvkC`DzpDdw@vmByM~WjvJmCnuBpfAdzA~eDxAbHoeEbgBmjAxiEq_@jtCqzB~l@aS" + }, + "summary": "M31 and National Highway M31" + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/GetDistanceMatrixWithBasicStringParams.json b/src/test/resources/com/google/maps/GetDistanceMatrixWithBasicStringParams.json new file mode 100644 index 000000000..0a8398a51 --- /dev/null +++ b/src/test/resources/com/google/maps/GetDistanceMatrixWithBasicStringParams.json @@ -0,0 +1,494 @@ +{ + "destination_addresses": [ + "Uluru, Petermann NT 0872, Australia", + "Kakadu NT 0822, Australia", + "Blue Mountains, New South Wales, Australia", + "Purnululu National Park, Western Australia 6770, Australia", + "Pinnacles Drive, Cervantes WA 6511, Australia" + ], + "origin_addresses": [ + "Perth WA, Australia", + "Sydney NSW, Australia", + "Melbourne VIC, Australia", + "Adelaide SA, Australia", + "Brisbane QLD, Australia", + "Darwin NT, Australia", + "Hobart TAS 7000, Australia", + "Canberra ACT 2601, Australia" + ], + "rows": [ + { + "elements": [ + { + "distance": { + "text": "3,670 km", + "value": 3669839 + }, + "duration": { + "text": "1 day 14 hours", + "value": 137846 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,173 km", + "value": 4172519 + }, + "duration": { + "text": "1 day 20 hours", + "value": 157552 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,820 km", + "value": 3819685 + }, + "duration": { + "text": "1 day 16 hours", + "value": 144484 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,112 km", + "value": 3111879 + }, + "duration": { + "text": "1 day 8 hours", + "value": 116918 + }, + "status": "OK" + }, + { + "distance": { + "text": "194 km", + "value": 193530 + }, + "duration": { + "text": "2 hours 20 mins", + "value": 8428 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "2,835 km", + "value": 2835495 + }, + "duration": { + "text": "1 day 6 hours", + "value": 106882 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,996 km", + "value": 3995751 + }, + "duration": { + "text": "1 day 20 hours", + "value": 158372 + }, + "status": "OK" + }, + { + "distance": { + "text": "129 km", + "value": 129162 + }, + "duration": { + "text": "1 hour 55 mins", + "value": 6915 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,458 km", + "value": 4458286 + }, + "duration": { + "text": "2 days 3 hours", + "value": 182989 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,082 km", + "value": 4081644 + }, + "duration": { + "text": "1 day 18 hours", + "value": 152261 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "2,320 km", + "value": 2319610 + }, + "duration": { + "text": "1 day 1 hour", + "value": 89337 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,776 km", + "value": 3776081 + }, + "duration": { + "text": "1 day 17 hours", + "value": 146992 + }, + "status": "OK" + }, + { + "distance": { + "text": "857 km", + "value": 856860 + }, + "duration": { + "text": "9 hours 12 mins", + "value": 33138 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,239 km", + "value": 4238615 + }, + "duration": { + "text": "2 days 0 hours", + "value": 171609 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,566 km", + "value": 3565759 + }, + "duration": { + "text": "1 day 13 hours", + "value": 134716 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "1,595 km", + "value": 1595141 + }, + "duration": { + "text": "17 hours 2 mins", + "value": 61329 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,052 km", + "value": 3051611 + }, + "duration": { + "text": "1 day 9 hours", + "value": 118984 + }, + "status": "OK" + }, + { + "distance": { + "text": "1,252 km", + "value": 1251530 + }, + "duration": { + "text": "13 hours 36 mins", + "value": 48937 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,514 km", + "value": 3514145 + }, + "duration": { + "text": "1 day 16 hours", + "value": 143602 + }, + "status": "OK" + }, + { + "distance": { + "text": "2,841 km", + "value": 2841289 + }, + "duration": { + "text": "1 day 6 hours", + "value": 106708 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "3,237 km", + "value": 3236842 + }, + "duration": { + "text": "1 day 11 hours", + "value": 124400 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,448 km", + "value": 3448098 + }, + "duration": { + "text": "1 day 14 hours", + "value": 136447 + }, + "status": "OK" + }, + { + "distance": { + "text": "1,009 km", + "value": 1008759 + }, + "duration": { + "text": "11 hours 28 mins", + "value": 41292 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,911 km", + "value": 3910632 + }, + "duration": { + "text": "1 day 21 hours", + "value": 161064 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,483 km", + "value": 4482990 + }, + "duration": { + "text": "1 day 23 hours", + "value": 169779 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "1,958 km", + "value": 1958480 + }, + "duration": { + "text": "21 hours 52 mins", + "value": 78694 + }, + "status": "OK" + }, + { + "distance": { + "text": "210 km", + "value": 210387 + }, + "duration": { + "text": "2 hours 16 mins", + "value": 8142 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,850 km", + "value": 3849813 + }, + "duration": { + "text": "1 day 17 hours", + "value": 149004 + }, + "status": "OK" + }, + { + "distance": { + "text": "1,118 km", + "value": 1118071 + }, + "duration": { + "text": "13 hours 44 mins", + "value": 49447 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,970 km", + "value": 3969954 + }, + "duration": { + "text": "1 day 17 hours", + "value": 147229 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "3,031 km", + "value": 3030901 + }, + "duration": { + "text": "1 day 16 hours", + "value": 144120 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,487 km", + "value": 4487372 + }, + "duration": { + "text": "2 days 8 hours", + "value": 201775 + }, + "status": "OK" + }, + { + "distance": { + "text": "1,575 km", + "value": 1575309 + }, + "duration": { + "text": "1 day 1 hour", + "value": 88234 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,950 km", + "value": 4949906 + }, + "duration": { + "text": "2 days 15 hours", + "value": 226392 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,277 km", + "value": 4277050 + }, + "duration": { + "text": "2 days 5 hours", + "value": 189499 + }, + "status": "OK" + } + ] + }, + { + "elements": [ + { + "distance": { + "text": "2,620 km", + "value": 2619695 + }, + "duration": { + "text": "1 day 4 hours", + "value": 99764 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,962 km", + "value": 3962495 + }, + "duration": { + "text": "1 day 19 hours", + "value": 154830 + }, + "status": "OK" + }, + { + "distance": { + "text": "300 km", + "value": 299573 + }, + "duration": { + "text": "3 hours 47 mins", + "value": 13623 + }, + "status": "OK" + }, + { + "distance": { + "text": "4,425 km", + "value": 4425029 + }, + "duration": { + "text": "2 days 2 hours", + "value": 179447 + }, + "status": "OK" + }, + { + "distance": { + "text": "3,866 km", + "value": 3865843 + }, + "duration": { + "text": "1 day 16 hours", + "value": 145143 + }, + "status": "OK" + } + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/OverQueryLimitResponse.json b/src/test/resources/com/google/maps/OverQueryLimitResponse.json new file mode 100644 index 000000000..72a190090 --- /dev/null +++ b/src/test/resources/com/google/maps/OverQueryLimitResponse.json @@ -0,0 +1,4 @@ +{ + "error_message": "Please wait.", + "status": "OVER_QUERY_LIMIT" +} diff --git a/src/test/resources/com/google/maps/PlaceDetailsLookupGoogleSydneyResponse.json b/src/test/resources/com/google/maps/PlaceDetailsLookupGoogleSydneyResponse.json new file mode 100644 index 000000000..b5090a9ef --- /dev/null +++ b/src/test/resources/com/google/maps/PlaceDetailsLookupGoogleSydneyResponse.json @@ -0,0 +1,323 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { + "long_name": "5", + "short_name": "5", + "types": [ + "floor" + ] + }, + { + "long_name": "48", + "short_name": "48", + "types": [ + "street_number" + ] + }, + { + "long_name": "Pirrama Road", + "short_name": "Pirrama Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Pyrmont", + "short_name": "Pyrmont", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2009", + "short_name": "2009", + "types": [ + "postal_code" + ] + } + ], + "adr_address": "5, \u003cspan class=\"street-address\"\u003e48 Pirrama Rd\u003c/span\u003e, \u003cspan class=\"locality\"\u003ePyrmont\u003c/span\u003e \u003cspan class=\"region\"\u003eNSW\u003c/span\u003e \u003cspan class=\"postal-code\"\u003e2009\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eAustralia\u003c/span\u003e", + "chain_name": "Google", + "feature_id": { + "cell_id": 7715420665913760567, + "fprint": 10281119596374313554 + }, + "formatted_address": "5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", + "formatted_phone_number": "(02) 9374 4000", + "geometry": { + "location": { + "lat": -33.866651, + "lng": 151.195827 + }, + "viewport": { + "northeast": { + "lat": -33.8653881697085, + "lng": 151.1969739802915 + }, + "southwest": { + "lat": -33.86808613029149, + "lng": 151.1942760197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "4f89212bf76dde31f092cfc14d7506555d85b5c7", + "international_phone_number": "+61 2 9374 4000", + "name": "Google", + "opening_hours": { + "minutes_until_closed": 226, + "minutes_until_open": 1186, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "1000" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 10:00 AM – 6:00 PM", + "Tuesday: 10:00 AM – 6:00 PM", + "Wednesday: 10:00 AM – 6:00 PM", + "Thursday: 10:00 AM – 6:00 PM", + "Friday: 10:00 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105932078588305868215/photos\"\u003eMaksym Kozlenko\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAMERdVY-5FvgQa3026fAR96qtbMkWn4l2lycjczAiC2E3Ztz52lWvCS-hvHt_QSa-EGXf4aR1vZlRpJK-W3UjMlxpD1ccRi-ifklli0YRQEZxbP48BSaGcdI2jArx4y7fEhAdtpr00KDIufn-JzHeAo5dGhQPs4LxICOZxsd4ihdKOasfIXqQ2g", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipP1U4aCd84U_h3g8MEpgv8pq9jhCZwabhBoaSrJ!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 2048 + }, + { + "height": 900, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114853289796780923190/photos\"\u003eShir Yehoshua\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA8Vq3EfN4DMUTuLmER9chPkUKeUUybkdEG7_d2-yo-bbqCt9q7ziE0seNHcl2_oeQLEqbKzySfghe_pM9Qr2qzLxS0C5Dq5LjwBF8W7NpD5YCXInV60WNlOyIzlIOpXCEEhALc6A0kuFceHUa9bPY_JRiGhS4caN6H5vysy_Ijv6s8SA6h0aViA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNYOZratGeoLGjr7bfmrm0afYBB2trW1tSCGVgG!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 1600 + }, + { + "height": 3264, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102493344958625549078/photos\"\u003eThomas Li\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAX14l98e9K58S5U5iq5PbjE-7fFotJL2dMcHZQxY9f-46WLZKp7DGWsOMt2yNwzEX6D2WTKDkRZbkBIfjpdA_wGDP1y5y43szxtusAGyAIVOI85p6f_TQFGm6DY_5f-VAEhDqZz23ih2rumOsfm7gQ0HFGhRxL96mVj43VNAfOyYsZuqT9BSUpg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPhKnMNvHw5E96Ff3gqslvNgUV-QvXVpCGTs0qO!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 4912 + }, + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115886271727815775491/photos\"\u003eAnthony Huynh\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAcje-JEdObf7uoESSBJuNjsB4Nu6LQYr1ONv2rsOKIWtvkIKm7BwEGoblRELk1rDH_a8sOSXRgIjgcg1D_w5XV9VsWDTtEs4f-_K65Iq76reKNQgqDgYcE5YO4SBKjatmEhDQSaj0zYHtXeDQ5fOSyRa-GhTRkDNeGm3HVz095OFMM3Ta9c4_yw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipN5zFwjADr1H9M_3vuK_VtZiSjFrEG5ExUJirlV!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 4032 + }, + { + "height": 1184, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106645265231048995466/photos\"\u003eMalik Ahamed\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAASwcpC4UUZov3VW0JOe1xZ7RnNlUMRfNmQNYWxGEd-0Y4kUBmkZcIzCLUHjUM6INxd7YILV3ey6aaL2MFK0bg9x9DbaKySTTyVBYt_hagw98TrCUKqIrTZS_j_KYjWmGCEhBd49Qy5Mc6gc7i5eXLULA6GhT3_z-q2pSP9nZnvT2GA_sI2PY3XQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOW7tRMp43SJ7GgDoGJpdN2i3sYafkmjWWkU_PR!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 1776 + }, + { + "height": 5582, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/110754641211532656340/photos\"\u003eRobert Koch\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAQJ4YsqM15BssaHh1Rw_oA7hibUWLhPBolewEzq2AD1Mf_otwPxt5Vh8LPK77FW3YffBgU4RBZ0JHIul-sPENfZ_jCs9QhFR6DkCFTgMbHC-pXFdFCkQ5Cpl89jiOvEvWEhDQ61wNT3GZEBtWVUXma_9CGhRs_eVFJ6a6QZayq6BDH9j2JdIfkA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOPF6xDOc1EpVoeMD-daLpZibzgJ5BtIbp5Zg-U!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 2866 + }, + { + "height": 4032, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102558609090086310801/photos\"\u003eHuy Tran\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAtEBuRAXVD7e2gAsaoSLAxR2k5uqfWg3tYaXRh4M-ENCz16Gz022S9kMIv1ZuLBXO96psR2yV467igTYn5vLf1qzCVOtsb_-698mkhiXXN-iW8u5M_nFJPw414dO_E2j3EhC_q7C9r_cJ9a_qizKLZkaYGhShiU-_T1BOwiZeUgm-TPDD2qDHxg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMZ6g-KaP5kr4ip-oie2lSd406O0y2cPvx_71XS!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 3024 + }, + { + "height": 1944, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115237891004485589752/photos\"\u003eKatherine Howell\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAutcpEG49pnwbS_pVaZCaV0mWup7B6nMSOkK-ACKY-48_V81HHGrrjPVovrepfyyebnWwVZIh_6vrR-4of7HlOJ0ucU6qrFv-klFhiNJYck6Jr12nNNruuX8S9nVWohqjEhATpr-QCcHpnq3VY50E8GORGhSXbbB_-fQqXirrKT7iPSTTutnIhQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMnQDnLhIY5lZgX0AzRUE-51iSi0orgBoaQ_hSE!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 1944 + }, + { + "height": 2448, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116976377324210679577/photos\"\u003eWH CHEN\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAflv2aOQSq3fZ45oS-BnUV8SKq6XoLLVuIyNOHjKuix_yGHBu-acvCSFu65ygnpmqt_935gN1_CFJixTwiqg2bTtImIVzGlbt45nPKVzwbVgs_hcczG_qxBrzwtW1-V_jEhDBF0erjllEgdTH9ZidUwPWGhR0c8WYvVUH8rm1dXpVjs3b1DNdPg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMyLqETfc7XPSejc4UF-J2BqJNhe4zy-18o2yn2!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 3264 + }, + { + "height": 2988, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/109246940950895122662/photos\"\u003eBen Tubridy\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAlESNIhS8ssqV6QulEQR0Zag2aAz-dcY9ejbleWQqA3bmTCkCwtic2X61-fEFtD03nYTjJuDrEMvNPwr7hgvYiIf6hTq87sA5bIhn07bAKrVKm1XnNFxwLDtscsi8HQ7HEhCH3mClbaYDJPgc2jIa9fyEGhRY37P_PokcMsJf2bAR3Q_OWEGtBg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipM8t9hb2nre80sRFHylMuf5XKfrscDNDgxjKge2!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 5312 + } + ], + "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", + "plus_code": { + "compound_code": "45MW+98 Pyrmont, New South Wales, Australia", + "global_code": "4RRH45MW+98" + }, + "rating": 4.4, + "reference": "CmRSAAAAGQJ6PTZr7ufM6fQmQKWECuhtbE5MxVWAVx1Potb6nllk7YrtrHYdMRttPrQfxHdAkFOaiZikxMustzx-R29lup1t8EZ5QhipfhEi9hJaM5AdNTBvo1Nrv7v03MtsEVj6EhDGokaqFIy6yIRgB6b_iKplGhQL3qLuxxusScGvkWpJCSJFafCbog", + "reviews": [ + { + "author_name": "Mark Sales", + "author_url": "https://www.google.com/maps/contrib/100341567599258416785/reviews", + "language": "en", + "profile_photo_url": "https://lh6.googleusercontent.com/-e2bgb-ognDY/AAAAAAAAAAI/AAAAAAAAAAA/AAyYBF5K8QcyGb-B5_yoiWjlWNTXqBcLHA/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "a week ago", + "text": "You have to FIX your Google MY BUSINESS page business.\nWhy the business owner CANNOT delete photos uploaded by anyone.\nI can delete the photos I uploaded but why is it so strange that anyone can upload and the owner of that page cannot delete the photos. I called the call centre and I have to explain IN DETAIL why i want to delete a photo!!!\nThats a total JOKE!!! its my page so i can do whatever I want!!!! So if the photo that a customer is legit and it was 10 years ago, i CANNOT delete that??? USELESS and TOTAL WASTE...", + "time": 1496880715 + }, + { + "author_name": "Starland Painting Pty Ltd", + "author_url": "https://www.google.com/maps/contrib/106844006614491278928/reviews", + "language": "en", + "profile_photo_url": "https://lh4.googleusercontent.com/-zHEV7zQnWfM/AAAAAAAAAAI/AAAAAAAAAAk/aZukMkHPI_o/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "a week ago", + "text": "No customer support, indeed! \nI went through the most of the reviews provided by people whom somehow do business with Google, and I have found that I am not the only disappointed one!!\nMost, almost all, + ve comments are about Google's geo-location and their food..., presumably written by their employees... . \n\nGoogle is a massive cooperation that has the ability/power to do better, indeed, but why they don't, remains unknown, at least to me.\n\nWithout the presence of small or large scale businesses, Google is absolutely nothing but a Website! The foreseeable future may not be as good as it is now for Google, as some fresh competitors may make Google a history, considering the fact that Nothing Is Impossible!", + "time": 1496982169 + }, + { + "author_name": "Paul Sutherland", + "author_url": "https://www.google.com/maps/contrib/104671394445218170123/reviews", + "language": "en", + "profile_photo_url": "https://lh6.googleusercontent.com/-ZRFv8AHxqEQ/AAAAAAAAAAI/AAAAAAABsfg/HluyrsFH2bk/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "a month ago", + "text": "Very disappointed. I have been a supporter of google and some of its innovations, I particular love android and adwords helps my business. Now I have a review on my site that is in breach of defamation laws, however Google don't seem to care. This is a fake review by someone who doesn't identify themselves. I get that reviews are there to assist businesses better themselves and help consumers make a decision, but when its defamatory and hurtful the line needs to be better managed by google. Having read most of your one star reviews, most of them have the same issue. Time to listen to your customers Google.", + "time": 1493618915 + }, + { + "author_name": "Ranjit Nair", + "author_url": "https://www.google.com/maps/contrib/102808647017735332248/reviews", + "language": "en", + "profile_photo_url": "https://lh4.googleusercontent.com/-A-UJAO1hMtk/AAAAAAAAAAI/AAAAAAAAAAA/AAyYBF5ojcf5EM9vU3KsroX_DEyKMn76MA/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "4 months ago", + "text": "Absolutely ZERO support. There is no thought behind how the whole Google Business Review process works and the only answer you get from support staff is that they are only trained to direct people to the Google support page. Why do you need people at the end of a phone line to tell you that?\n\nAs it stands, there is nothing stopping me from standing in front of a store and giving a negative review to a business based on whether it was raining that day or not. There is no process here to verify if the claim of the user is right or not. I thought the whole process of reviews were meant to be a fair representation of the service the business provides. Where is the fairness here?\n\nGoogle encourages businesses to respond to people's reviews. Is Google responding to the reviews that are posted about their business? Why the double standards Google?", + "time": 1485900942 + }, + { + "author_name": "Ben Cohen", + "author_url": "https://www.google.com/maps/contrib/110685259345133164515/reviews", + "language": "en", + "profile_photo_url": "https://lh4.googleusercontent.com/-NwQ8wRwmjlQ/AAAAAAAAAAI/AAAAAAAAAXU/V8YVXItAB-w/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "in the last week", + "text": "Disgusting Service. No communication on a housing location number error that has caused multiple issues including internet access and will be reported to communications ombudsmen. Complete lack of empathy for customer.", + "time": 1497829502 + } + ], + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=10281119596374313554", + "user_ratings_total": 458, + "utc_offset": 600, + "vicinity": "5, 48 Pirrama Road, Pyrmont", + "website": "https://www.google.com.au/about/careers/locations/sydney/" + }, + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlaceDetailsQuay.json b/src/test/resources/com/google/maps/PlaceDetailsQuay.json new file mode 100644 index 000000000..9e06c7939 --- /dev/null +++ b/src/test/resources/com/google/maps/PlaceDetailsQuay.json @@ -0,0 +1,305 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { + "long_name": "3", + "short_name": "3", + "types": [] + }, + { + "long_name": "Overseas Passenger Terminal", + "short_name": "Overseas Passenger Terminal", + "types": [ + "premise" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "The Rocks", + "short_name": "The Rocks", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "adr_address": "3, Overseas Passenger Terminal, \u003cspan class=\"street-address\"\u003eGeorge St & Argyle Street\u003c/span\u003e, \u003cspan class=\"locality\"\u003eThe Rocks\u003c/span\u003e \u003cspan class=\"region\"\u003eNSW\u003c/span\u003e \u003cspan class=\"postal-code\"\u003e2000\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eAustralia\u003c/span\u003e", + "formatted_address": "3, Overseas Passenger Terminal, George St & Argyle Street, The Rocks NSW 2000, Australia", + "formatted_phone_number": "(02) 9251 5600", + "geometry": { + "location": { + "lat": -33.858018, + "lng": 151.210091 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "f181b872b9bc680c8966df3e5770ae9839115440", + "international_phone_number": "+61 2 9251 5600", + "name": "Quay", + "opening_hours": { + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1700" + }, + "open": { + "day": 1, + "time": "1330" + } + }, + { + "close": { + "day": 2, + "time": "1700" + }, + "open": { + "day": 2, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "1700" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "1700" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "1700" + }, + "open": { + "day": 5, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 1:30 – 5:00 pm", + "Tuesday: 10:00 am – 5:00 pm", + "Wednesday: 10:00 am – 5:00 pm", + "Thursday: 10:00 am – 5:00 pm", + "Friday: 10:00 am – 5:00 pm", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 1944, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/101719343658521132777\"\u003eJames Prendergast\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAATDVdhv0RdMEZlvO2jNE_EXXZZnCWvenfvLmWCsYqVtCFxZiasbcv1X0CNDTkpaCtrurGzVxTVt8Fqc7egdA7VyFeq1VFaq1GiFatWrFAUm_H0CN9u2wbfjb1Zf0NL9QiEhCj6I5O2h6eFH_2sa5hyVaEGhTdn8b7RWD-2W64OrT3mFGjzzLWlQ", + "width": 2592 + }, + { + "height": 612, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107255044321733286691\"\u003eTamagotchi Kuchipatchi\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAYL-TTOxtVXgJkTTmDSTVi77OjkPGlLae4Md9wskVNxIT4Qn_HN0k76P4ex7ALWEnrvAGTGEV2Vyiv4yKtpm_7TQ11wCLeg8OXXOEOJgTB1KuOPAazrewXuGb5sLUzB8_EhBuRRFI2aNKyNM9zLm79im1GhTPKsAzFfexw4uJs9eZVN9Nv2uEZg", + "width": 816 + }, + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111085852685867296039\"\u003eAndrea Longinotti\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAWQXc9XOHKUsNufwqCp-3ljOdW8Lu2NdC3J0Xrr1rjbzs6cgVnWjLEealmsMebiLndB4yxv652QEPDxIKYXjo2_umeEsj0UGdiZHlgpyjMXP7CzNOsAqtk3nnY9v6d8BbEhBb97FZ0cUlqOKB7XuVxV_qGhQaXiWkNuhHtqy3qg_mW0we97oW9w", + "width": 1781 + }, + { + "height": 600, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/109274197593239971461\"\u003eBengou Chen\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAD9sxwXUxAhPkya0dq7Hbero3ow4jlBc7E0mTLEG9TdpCnqxxbBP84OuXr8eXBDGbhlD4hL9J7juhCr9oh3V0lBe6u8WDs9JxuysbVRyr2-7Kkkn0542kLyePteuQ_8mrEhClLFqGOaf4jEMDhUh2aqhrGhQIl6Frbi9BDjO86mWoT-8kzkdIsQ", + "width": 800 + }, + { + "height": 2349, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107084328053484542717\"\u003eKutay Kesim\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAFi4pQAzAPqa-Vara3o8OuV88BGAPO9U3s-_6z3551fwBtWcVgIOxiimYtfN5-PfxZBfpU_rcI9mA1jtzOSFo9DtpLHoYlmNZpZhL8Irmvo_OHd-MmpkRSD5hCXyvZeL0EhBeAELfaE146PMqKcwCPSrnGhSJgaWBSEZKctbA9nF6oeagLlE12Q", + "width": 5839 + }, + { + "height": 1951, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107084328053484542717\"\u003eKutay Kesim\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAyv0fpZdGbThZVlIKEtUoFLtF23K9guNDUKGXJC3M-YB-ANIN5jCwCgBgN5tvv6gntt2mh5gakik-CQRLNBQwTxrzG4QNJcRIumu3cENzM9-5p_LqOicxazWKaj6X3sP5EhBUgctZZaxLhp44GnyB-pPpGhQs-9-ZolZGkNASdFrA-iXwgD28pw", + "width": 4949 + }, + { + "height": 852, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/112520482541499149050\"\u003eTeri Leong\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAaR5bk2W2GJzjwC85XLi8WMNkRIyoGtzK_z_PMSrHRqir9wdi2Yrn364xrMFbElcUm_CDacyf13NyDTxt8aBBnIKVbB76MRiiSuf3SnNH1po6kgXnLb8ded01iO5hhYPzEhDXPrKtEaQlKrx_xfXPkwnOGhR7dI1r514aACRfdjfen50funKrsw", + "width": 1280 + }, + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111085852685867296039\"\u003eAndrea Longinotti\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAnSPU8qbxRLCZh1S1-NkrUmXgedfL5a_AwL3jNAZCcHT6JAmz6jelUhMjp10wwzKVDUsPDyQOu9KF8qXdnY_rwjS-1CtReL8-9yxoY72gmD5BYD8P87-1pUB7e8Z5esYOEhCmcVHzRXFtCc4jq6jEVy6XGhQqhw-mH8o7VqPav6kExnxaTxTqDQ", + "width": 1543 + }, + { + "height": 2240, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107084328053484542717\"\u003eKutay Kesim\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAWzryTzB02KsXQgzabGtZq3MGAS19cbzHwFXrLV5hdiFiJTOU6KHZjutERpG6NX5qZ5F2N-EP5j66YyCiXfuc0E8mQBFzhHPH2HVTnFyxvss6SzUmRdqV6bWSyqpSGBfWEhDQryNS3PzHfYtw4Cxysz5EGhTYazVgva930AmIaOiHcUKl2ikVCA", + "width": 4471 + }, + { + "height": 1265, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111658638200198915174\"\u003eWei Guan\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAsY5NCuLSSzkvKCtl5Tr1bKFFKKF0Sx0ZKvOQpCAkxRuIwCxAOXDN6xivlNEFOEA5OZuEeOqD4IHMl1FYDA0o5koJS65HYd3k45byMjpBB2LMR5TWPd6RxtWTo6sJCZJHEhAZl4o1HLQ1Mx72PZOik6NtGhT71-QPp883LgsCGkm9Zgb6jlGVyg", + "width": 949 + } + ], + "place_id": "ChIJ02qnq0KuEmsRHUJF4zo1x4I", + "price_level": 4, + "rating": 4.1, + "reference": "CmRYAAAAPJXQPY-Urzx36za5sPPu5xr2ou57kjH_Owv_EzHeEdkp4dVk7mzoMKaVAThh9mcoZ1GjopmpMglDnBFpsHCIaW_zTTnXKfa9Mi9dBo0cb7v74K464h-oC_aSDR5G89xXEhC6LSFCOIAPJTxe8tf2PIKRGhTfiIkPViAIbWWY8Gg3VY5R0Tv9bw", + "reviews": [ + { + "aspects": [ + { + "rating": 1, + "type": "overall" + } + ], + "author_name": "Rachel Lewis", + "author_url": "https://plus.google.com/114299517944848975298", + "language": "en", + "rating": 3, + "text": "Overall disappointing. This is the second time i've been there and my experience was... Nothing to nibble on for 45 mins and then the bread came. My first entree was the marron which I thought was tasteless - perhaps others would say delicate? but there you go. The XO sea was fantastic. I chose the vegetarian main dish which was all about the texture which was great but nothing at all outstanding about the dish. My husband and daughter chose the duck for their main course it was the smallest main course i've ever seen - their faces were priceless when it arrived!. Snow egg was beautiful but the granita on the bottom had some solid chunks of hard ice. The service was quite good...", + "time": 1441848853 + }, + { + "aspects": [ + { + "rating": 3, + "type": "overall" + } + ], + "author_name": "Cassandra Lee", + "author_url": "https://plus.google.com/104420598240526976175", + "language": "en", + "rating": 5, + "text": "Went here for my first fine dining experience and it was awesome! The food is amazingly presented and cooked to perfection. Service is impeccable and the view is a bonus. Would definitely recommend to anyone looking for somewhere special!", + "time": 1439358403 + }, + { + "aspects": [ + { + "rating": 0, + "type": "overall" + } + ], + "author_name": "Nicole Green", + "author_url": "https://plus.google.com/100274172303441331993", + "language": "en", + "rating": 2, + "text": "This is the first time I have ever decided to review a restaurant on a public forum. Given the amount of money spent I feel compelled to stop other people making the same mistake we did. Group booking for 7 people @ 1pm. Decor is dated with purple carpet, red chairs and mirrored roof. Patrons dressed in tee shirts and jeans - dress code should be in place. Everyone opted for the tasting menu with matching wines. Slow cooked Duck was horrible and tough, along with a tasteless Wagyu. Head Sommelier was condescending with rude commentary. Staff started to vacuum around us while we were still having last course, setting up for the next sitting. If you can't fit a tasting menu in for a 1pm booking do not take bookings at that time. Forgot to bring out the petit fours and only served 1 pot of tea for 2 people. Burnt Piccolo was served and we had to ask for it to be remade. We asked for another round of drinks and was told the bar was closed at 5:30pm. Staff were unkempt with uniforms that badly need an update. Overall presentation was dated and unpleasant. Terrible dining experience for a 3 hat restaurant. Spend half your money and eat at Guillaume for better food and nicer people. We are all very disappointed and won't be recommending this restaurant to anyone any time soon. Unfortunately your reputation is keeping you afloat but this will not last long. ", + "time": 1437960610 + }, + { + "aspects": [ + { + "rating": 0, + "type": "overall" + } + ], + "author_name": "Steakhouse Resume", + "author_url": "https://plus.google.com/114017213059089752059", + "language": "en", + "rating": 1, + "text": "Disappointed is an understatement! My husband and I've had our fair share of fine dining experiences around the world and for a restaurant that requires a booking at least three months in advance we were SHOCKED at the quality of food AND service presented tonight. From rubber-like \"quail\" to a snow-egg that presents and tastes like vanilla ice cream on crystallised sugar on crushed ice was honestly the best option we experienced from our four poor courses. Each meal lacked a wow factor in taste and presentation. $500 later I'd never return and would inform mates to spend their evenings elsewhere. Sorry Quay but best you throw in your hats mates! \n\nNOT WORTH IT!", + "time": 1437777466 + }, + { + "aspects": [ + { + "rating": 0, + "type": "overall" + } + ], + "author_name": "Ben P", + "author_url": "https://plus.google.com/107541698370492845136", + "language": "en", + "rating": 2, + "text": "Having been to a fair number of fine dining restaurants around the world, Quay is one of those restaurants that provides you with good food but ridiculously small servings.\n\nThe amount you pay for a three course meal is significantly higher than the average price charged by other upscale restaurants so you'd think reasonable-sized servings would be provided - unfortunately, this was not the case.\n\nI appreciate that in general, the serving size at these restaurants will not be large, but I certainly do not expect to be hungry 30 minutes after leaving the restaurant. There is an expectation that patrons should leave feeling full and satisfied rather than yearning for more food and finding the closest fast food outlet outside the restaurant - especially after paying those prices.\n\nFor the most recent upscale restaurants my partner and I have been to, serving sizes have been very reasonable and I don't think Quay have cottoned on to the fact that the servings they provide are simply unacceptable. A distinct lack of adaptability to the ever-changing food industry.\n\nWill not return.", + "time": 1436179623 + } + ], + "scope": "GOOGLE", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://plus.google.com/105746337161979416551/about?hl=en-US", + "user_ratings_total": 273, + "utc_offset": 600, + "vicinity": "3 Overseas Passenger Terminal, George Street, The Rocks", + "website": "http://www.quay.com.au/" + }, + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlaceDetailsResponse.json b/src/test/resources/com/google/maps/PlaceDetailsResponse.json new file mode 100644 index 000000000..08effbce4 --- /dev/null +++ b/src/test/resources/com/google/maps/PlaceDetailsResponse.json @@ -0,0 +1,311 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { + "long_name": "5", + "short_name": "5", + "types": [] + }, + { + "long_name": "48", + "short_name": "48", + "types": [ + "street_number" + ] + }, + { + "long_name": "Pirrama Road", + "short_name": "Pirrama Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Pyrmont", + "short_name": "Pyrmont", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2009", + "short_name": "2009", + "types": [ + "postal_code" + ] + } + ], + "adr_address": "5, \u003cspan class=\"street-address\"\u003e48 Pirrama Rd\u003c/span\u003e, \u003cspan class=\"locality\"\u003ePyrmont\u003c/span\u003e \u003cspan class=\"region\"\u003eNSW\u003c/span\u003e \u003cspan class=\"postal-code\"\u003e2009\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eAustralia\u003c/span\u003e", + "formatted_address": "5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", + "formatted_phone_number": "(02) 9374 4000", + "geometry": { + "location": { + "lat": -33.866611, + "lng": 151.195832 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "4f89212bf76dde31f092cfc14d7506555d85b5c7", + "international_phone_number": "+61 2 9374 4000", + "name": "Google", + "opening_hours": { + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1730" + }, + "open": { + "day": 1, + "time": "0830" + } + }, + { + "close": { + "day": 2, + "time": "1730" + }, + "open": { + "day": 2, + "time": "0830" + } + }, + { + "close": { + "day": 3, + "time": "1730" + }, + "open": { + "day": 3, + "time": "0830" + } + }, + { + "close": { + "day": 4, + "time": "1730" + }, + "open": { + "day": 4, + "time": "0830" + } + }, + { + "close": { + "day": 5, + "time": "1700" + }, + "open": { + "day": 5, + "time": "0830" + } + } + ], + "weekday_text": [ + "Monday: 8:30 am – 5:30 pm", + "Tuesday: 8:30 am – 5:30 pm", + "Wednesday: 8:30 am – 5:30 pm", + "Thursday: 8:30 am – 5:30 pm", + "Friday: 8:30 am – 5:00 pm", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 2322, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107252953636064841537\"\u003eWilliam Stewart\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAaLQHaytXWS_wVeJ6tKADOZi_JB4tWwA3bD6Noul1-XP1s4KYQoCYy4FN6JP50KKR4yoLcR5U2cKJt-irDQSAOVo_vxBbDG8WiZqUPoSmntNm8_lYxKqioY9japBQSy6dEhB771sWRb2oX1aUQjT30GjkGhRiX9K4InWUgP95i8jzmsyIGDRJFw", + "width": 4128 + }, + { + "height": 960, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/100919424873665842845\"\u003eDonnie Piercey\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAKODF2c2AdVRvPGHc6NjfEtyUxECGWq6uANHQanU0PuUM2Bz0p7JtsreypLZy-YmPW7uDv59z3tBo4gN5AXtOVuUue-6xC---QS3u10Z1xcxw5sDW3Ob5WJ5Lc6W3uK87EhC-X2fqEJq7652OP9-cwuwnGhRcu2V0kGOofU7zRwXHPRI4vMlDbA", + "width": 1280 + }, + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105932078588305868215\"\u003eMaksym Kozlenko\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAiGyfiUTp-GbdrVEMK0pmxWgOhZCwlU8mL38Y5NHgflI6gbWyIXy-GSQ5VLZMVYDY2Li50HRo9FjKebwRitLAARD0B5huRrJGrfpIkC9nVEnp-gaGi7nqKAHmwiJtewX2EhCDYRdOA6WtZtoLOo6rCCykGhRt4U9FoeebHpXumqb7fk1F9vXA1Q", + "width": 2048 + }, + { + "height": 1131, + "html_attributions": [ + "From a Google User" + ], + "photo_reference": "CmRdAAAA791CwbNORdHpIHCX5T_yr7tylsplV3RkNisNFrD02UX4leEbR0wHio1RgmeNWY8VltcCV5TLhCqGLvIUFqaSXFlL0BMQKiTD7jqTBQhPFtAbkXBKojTXQPp1rK_eTmvXEhB0HNtGkd8Ya2kxJr8OAyQLGhQS9TKpuVK_Fu5uk6DawmgS5kx9nw", + "width": 1600 + }, + { + "height": 2368, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/108508601154030859314\"\u003eLeo Angelo George\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAODFWDa1zoBJY_rDXSjrb2UI-hUVh0mxscKL834w0Bg4mp4qRiHNHz4RlLMpFf0axQuBBXjEe3uRcGIUo3YnkVz1MIQTzlH0A_lIbB2G5jMDCfUndx7kakh8aMYFEwY8MEhB5VqyZGvJDYr2ZhnLuiKyKGhQJ-uaY5Zkrf4IhVmWWVl2V7g5SuQ", + "width": 3200 + }, + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104177669626132953795\"\u003eJustine OBRIEN\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAy9KJDNcz6vaFf1m5D3lGLaBtDi7dRZATao-hkBRUfCRdlxlH5jbvnXy9Zz-WyOa8W-OWOt7Yv131PBhfDQD7S8MdkCaKVCofeh-Pd44jo9dh_PaXtWdQ7SURqYMaIFEJEhCEPyy1CKyEOUQa1cw2CjL8GhS1egmOLFYRyf0Q2QLBzsqAh065QA", + "width": 1536 + }, + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104177669626132953795\"\u003eJustine OBRIEN\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAFroavSdllyzoDwigykpSCjN8NS0srG9PksT1DQohG_VvCNEKgjaiyWbj_jB9hL96UYK96mvLFkC9FuGbWkr6tTmHlEhzgNECF_U4vW3dXZGkh6-JdQyM8XDWwPYVbrj_EhDECjEJK_-xBBE06f_3LByXGhQSTsjTJr3g3BIPsjkvG-jlLrumtQ", + "width": 1536 + }, + { + "height": 608, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116750797999944764767\"\u003eJessica Pfund\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAzmdaTGE050a8jsuKzJUeRk0NohCz5xeXT4cnII_l8b0e-CZ6ohuLqYtP0Jnlbz5t1giBs0L8TSzxMlQVOlwHugwpcnHT_bY2DhRJjX1sV9qxMnPgLjXc-q-vcQLt1lwsEhBNZBGiCgowk1w0ZRoVeGOTGhThrqCB2ucZGPQpUuL2ZVfX0Q7UkQ", + "width": 1080 + }, + { + "height": 612, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114701241123617315548\"\u003eMargaret L\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAxwN0Si3pLjqfWM4UJTL5zFxR2gXTexxhB6QQM80oP9oX7LI5hgqJu1kgBD4YESRFQA2vw-sodpmydKL11ebZKV-8Ui2WWZFu1LBIe5saL2s18bJHr5cfJWBD1ipZdN66EhBkjcF9SonMuDUySKFQYMtZGhT3qKKoEK5zBiM3shUnc0JbUpoOEQ", + "width": 816 + }, + { + "height": 1536, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104177669626132953795\"\u003eJustine OBRIEN\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAxRCr9CdOhKRXQ5PhyPcA5MoxlEHHQoKrkUPGLl4NNPBg786-_gijQOmrypR-BHK9qDwb6YlIzVrwaZoXKbeHyP68cyXFNXI5XlMHBb_rIO1vu5O30EygjDVKlOw2eeRGEhBUjxBpvon78Om8JQBCtyuNGhRQzk7EDZV4GoZZNiF7wXnz_7BaoA", + "width": 2048 + } + ], + "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", + "rating": 4.4, + "reference": "CmRaAAAAZdHX6BGZ2MXDkAjVaAG47181Oht-IQedOYARYLzismzjxKQ-pgzWIjQduTqOfzpGhVdPZFVxd_HE1KQuWKIHDE-eK_AKSyrPYOB_QVBC3XhXNPJF4v1iaGzLndZuVPaHEhDvznqDvW_SA4by5Ar1sICBGhTTXbucgN3OxPDY32AaXPHZT7NeZw", + "reviews": [ + { + "aspects": [ + { + "rating": 3, + "type": "overall" + } + ], + "author_name": "Danielle Lonnon", + "author_url": "https://plus.google.com/118257578392162991040", + "language": "en", + "profile_photo_url": "https://lh5.googleusercontent.com/photo.jpg", + "rating": 5, + "relative_time_description": "a month ago", + "text": "As someone who works in the theatre, I don't find the Google offices nerdy, I find it magical and theatrical. Themed rooms with useful props and big sets with unique and charismatic characters. You sure this isn't a theatre company? Oh no wait Google has money, while the performing art does not.", + "time": 1425790392 + }, + { + "aspects": [ + { + "rating": 3, + "type": "overall" + } + ], + "author_name": "Lachlan Martin", + "author_url": "https://plus.google.com/101767769287488554641", + "language": "en", + "profile_photo_url": "https://lh5.googleusercontent.com/-GnfIbf_lBA4/AAAAAAAAAAI/AAAAAAAAeVM/gkqipH58HoM/s128-c0x00010000-cc-rp-mo-ba2/photo.jpg", + "rating": 5, + "relative_time_description": "a day ago", + "text": "The cool-aid here tastes amazing!!! ", + "time": 1439790358 + }, + { + "aspects": [ + { + "rating": 3, + "type": "overall" + } + ], + "author_name": "Rob Mulally", + "author_url": "https://plus.google.com/100839435712919930388", + "language": "en", + "profile_photo_url": "https://lh5.googleusercontent.com/-GnfIbf_lBA4/AAAAAAAAAAI/AAAAAAAAeVM/gkqipH58HoM/s128-c0x00030000-cc-rp-mo-ba2/photo.jpg", + "rating": 5, + "relative_time_description": "a month ago", + "text": "What can I say, what a great building, cool offices and friendly staff!\nonly had a quick tour but there isn't much missing from this world class modern office.\n\nIf your staff who work here I hope you take advantage of all that it offers , because as a visitor it was a very impressive setup. \n\nThe thing that stood out besides the collaborative area's and beds for resting, was the food availability.\n\nImpressed. 5 Stars.\n", + "time": 1408284830 + }, + { + "aspects": [ + { + "rating": 3, + "type": "overall" + } + ], + "author_name": "Michael Yeung", + "author_url": "https://plus.google.com/104161906493535874402", + "language": "en", + "profile_photo_url": "https://lh5.googleusercontent.com/-GnfIbf_lBA4/AAAAAAAAAAI/AAAAAAAAeVM/gkqipH58HoM/s128-c0x00040000-cc-rp-mo-ba2/photo.jpg", + "rating": 5, + "relative_time_description": "two month ago", + "text": "Best company in the world. The view from the cafeteria is unreal, you take in the entire Darling harbour view like nowhere else :)", + "time": 1435313350 + }, + { + "aspects": [ + { + "rating": 3, + "type": "overall" + } + ], + "author_name": "Marco Palmero", + "author_url": "https://plus.google.com/103363668747424636403", + "language": "en", + "profile_photo_url": "https://lh5.googleusercontent.com/-GnfIbf_lBA4/AAAAAAAAAAI/AAAAAAAAeVM/gkqipH58HoM/s128-c0x00050000-cc-rp-mo-ba2/photo.jpg", + "rating": 5, + "relative_time_description": "a day ago", + "text": "I've been fortunate enough to have visited the Google offices on multiple occasions through the years and I've found this place to be quite awesome. This particular office is the original campus for Google Sydney and they've expanded to the Fairfax building where they've built an even more exciting office!\n\nTotally jealous of their cafeteria and the city views from their office!", + "time": 1413529682 + } + ], + "scope": "GOOGLE", + "types": [ + "establishment" + ], + "url": "https://plus.google.com/111337342022929067349/about?hl=en-US", + "user_ratings_total": 98, + "utc_offset": 600, + "vicinity": "5 48 Pirrama Road, Pyrmont", + "website": "https://www.google.com.au/about/careers/locations/sydney/" + }, + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlaceDetailsResponseForPermanentlyClosedPlace.json b/src/test/resources/com/google/maps/PlaceDetailsResponseForPermanentlyClosedPlace.json new file mode 100644 index 000000000..a95a015c6 --- /dev/null +++ b/src/test/resources/com/google/maps/PlaceDetailsResponseForPermanentlyClosedPlace.json @@ -0,0 +1,7 @@ +{ + "html_attributions": [], + "result": { + "permanently_closed": true + }, + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlaceDetailsResponseWithBusinessStatus.json b/src/test/resources/com/google/maps/PlaceDetailsResponseWithBusinessStatus.json new file mode 100644 index 000000000..b2ee8ea22 --- /dev/null +++ b/src/test/resources/com/google/maps/PlaceDetailsResponseWithBusinessStatus.json @@ -0,0 +1,7 @@ +{ + "html_attributions": [], + "result": { + "business_status": "OPERATIONAL" + }, + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlaceGeocodeResponse.json b/src/test/resources/com/google/maps/PlaceGeocodeResponse.json new file mode 100644 index 000000000..9550a59be --- /dev/null +++ b/src/test/resources/com/google/maps/PlaceGeocodeResponse.json @@ -0,0 +1,68 @@ +{ + "results": [ + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "colloquial_area", + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney NSW, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.5781409, + "lng": 151.3430209 + }, + "southwest": { + "lat": -34.118347, + "lng": 150.5209286 + } + }, + "location": { + "lat": -33.8688197, + "lng": 151.2092955 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.5782519, + "lng": 151.3429976 + }, + "southwest": { + "lat": -34.118328, + "lng": 150.5209286 + } + } + }, + "place_id": "ChIJP3Sa8ziYEmsRUKgyFmh9AQM", + "types": [ + "colloquial_area", + "locality", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/PlacesApiDetailsInFrenchResponse.json b/src/test/resources/com/google/maps/PlacesApiDetailsInFrenchResponse.json new file mode 100644 index 000000000..47f2166fb --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiDetailsInFrenchResponse.json @@ -0,0 +1,342 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { + "long_name": "35", + "short_name": "35", + "types": [ + "street_number" + ] + }, + { + "long_name": "Rue du Chevalier de la Barre", + "short_name": "Rue du Chevalier de la Barre", + "types": [ + "route" + ] + }, + { + "long_name": "18ème Arrondissement", + "short_name": "18ème Arrondissement", + "types": [ + "sublocality_level_1", + "sublocality", + "political" + ] + }, + { + "long_name": "Paris", + "short_name": "Paris", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Paris", + "short_name": "Paris", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "Île-de-France", + "short_name": "Île-de-France", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "75018", + "short_name": "75018", + "types": [ + "postal_code" + ] + } + ], + "adr_address": "\u003cspan class=\"street-address\"\u003e35 Rue du Chevalier de la Barre\u003c/span\u003e, \u003cspan class=\"postal-code\"\u003e75018\u003c/span\u003e \u003cspan class=\"locality\"\u003eParis\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eFrance\u003c/span\u003e", + "feature_id": { + "cell_id": 5180949656259431907, + "fprint": 14970958066519606553 + }, + "formatted_address": "35 Rue du Chevalier de la Barre, 75018 Paris, France", + "formatted_phone_number": "01 53 41 89 00", + "geometry": { + "location": { + "lat": 48.88670459999999, + "lng": 2.3431043 + }, + "viewport": { + "northeast": { + "lat": 48.88778688029149, + "lng": 2.344196130291502 + }, + "southwest": { + "lat": 48.88508891970849, + "lng": 2.341498169708498 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/worship_general-71.png", + "id": "161c19d38e8d0e9529ada27078e8014b253df723", + "international_phone_number": "+33 1 53 41 89 00", + "name": "Sacré-Cœur", + "opening_hours": { + "minutes_until_closed": 956, + "minutes_until_open": 1406, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2230" + }, + "open": { + "day": 0, + "time": "0600" + } + }, + { + "close": { + "day": 1, + "time": "2230" + }, + "open": { + "day": 1, + "time": "0600" + } + }, + { + "close": { + "day": 2, + "time": "2230" + }, + "open": { + "day": 2, + "time": "0600" + } + }, + { + "close": { + "day": 3, + "time": "2230" + }, + "open": { + "day": 3, + "time": "0600" + } + }, + { + "close": { + "day": 4, + "time": "2230" + }, + "open": { + "day": 4, + "time": "0600" + } + }, + { + "close": { + "day": 5, + "time": "2230" + }, + "open": { + "day": 5, + "time": "0600" + } + }, + { + "close": { + "day": 6, + "time": "2230" + }, + "open": { + "day": 6, + "time": "0600" + } + } + ], + "weekday_text": [ + "lundi: 06:00 – 22:30", + "mardi: 06:00 – 22:30", + "mercredi: 06:00 – 22:30", + "jeudi: 06:00 – 22:30", + "vendredi: 06:00 – 22:30", + "samedi: 06:00 – 22:30", + "dimanche: 06:00 – 22:30" + ] + }, + "photos": [ + { + "height": 480, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116540196018193369880/photos\"\u003eR Toni\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAIta7ynT-B2h0EvUXjx0IbbKUb_BvcP1fjd6oL1EtWLlbnfn8u3VOaH5UidQukk5maH74S1YZZgi-4JZkQzpreiDndQX7dyT9HlO100xI1EgtAnq9as178JL0dO8x0QWAEhBR1O9n90N7qP3NG8M15ugZGhTd3uWyC6qF67o6O5p5pTSphxoNUw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOCqWFtYH_ISFOPX4xS7CLnTUz07YyBUZkPShOz!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 640 + }, + { + "height": 1536, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/101964589691538472142/photos\"\u003eGuillermo Cebrián\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAmRGTgHHeFWJw1dA1Tq0l6cVvwH639i5WKd7nlVsmoCuuZStZk4siw5RKsjlUgBr7I2W3JsyVDv94VgdfRDPoH-3XmHOeKGOsvsSBKjFBj1575jKVhyvvrS6OTjENM2poEhCk5zhq92_fvG6nTlTwaiQlGhTpwhdRtI6PTbK-cu1gjAyRNc10tw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNfKy7jBb0XwrDMZPl7dxKXL5PNDDtf5G-jrs72!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 2048 + }, + { + "height": 2160, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113779831186355687683/photos\"\u003ePaola Andrea Quiňones Villamizar\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAqSOziaNKLv1OPI3xYwiBXv0SYNCKiADR3eyQ4_rcDu2y03exUf4C_0QL2ublN_9PhHjNmlv0W1kTWVlxZUv1O8tVnGrMpY6_I0H8b2tvXc57AEmRj2LXCqsAKv63Shx0EhAhXX04simXwO2nHc028YagGhTiISFypjy6GJZzZorZX8VpH9PW0g", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipN1AIKWiptXJ5ywgBE3bBtPqEFMImCAarDvsSyj!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 3840 + }, + { + "height": 1200, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105662551754488862978/photos\"\u003eCristina Batu\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAvkNy-E2Vw5uwhO5RLj5eKXccBVUuZ_0lbJwkPNuCIIB2yvcjex2TQk6RDcy7nLuV_dNAHxvt8iAxo74DLRw7ifALsI72SBRu5P1dWeS02Vt6n2ow0XDo0uPWgHUYZzbnEhAhk1kfLRCGvaVDDlcODPqEGhTu9JoQIcBSPhw4VY8dkq3vlinWCw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMxl6AD72DCDrFXSmb_qLX_LzqxgfZVk8hhABpJ!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 1600 + }, + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111900581174981788536/photos\"\u003ePablo Ugalde\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAADxj1s53bhA-jzDmzqQGImu9KFSVf5urFY_ed-1fEgG-z8tq7xpqQqfBS4YEB3eoIsN5F6NLQdm50qa_tsv4rE2Ei6SHw8e8NlVE_3tNYFBJJeo7g8H8Wf5UGQcRQtCOYEhBGNMpKtqHT6K23lo3yDe2MGhT3PhwG24aX9RAZ0qiuhVcEm6zJzQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMAXucxHWttT-Hpq8QiJopDb9BgPnCbVy6nRzyr!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 4032 + }, + { + "height": 2160, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114970995858040494681/photos\"\u003ekawtar lamsyah\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA_ZQdl_-9vM8UoYwVIzA2Ha_NyTyopZ0y9CfqxDr3Pppq7zu_u_sWc0U7EXWnCT0j3u3C18xZrZIJFtLKBHZ7wtfcU2bIybkkxV2c-QOMGN7ZwyM9KrXigfr2dDjdnJaWEhBO6ETMOldoFs_91W9JEr56GhQP7yY4obAUIOSVtmqzU2pe1KZ0-w", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMh--zctZzEgPkyXKh24eEQZpTr8Bb--PlfZLvn!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 3840 + }, + { + "height": 1456, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106606470883453186473/photos\"\u003eLenny Tim\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA5LbeK-S7Tx9Kt6GldyIiM0TMMGLXu7DiGz1Lmj4Vc7fHmK-xVcXmBetHJmHnhwbCXwkNP13Nf-yPzIsEfwfbHcwv2g4hUmIrTFFCQMlOoex9lOEdW4C00-a-1CTgkReDEhCnaqzwejyvbd4hZmJlK0PmGhS3nIL2fiIx8Epbp_bk7uUJjGoZCg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMM4vme_FB1fPEJpCNudwqXCCS_hGA3y4MxEwqK!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 2592 + }, + { + "height": 3480, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105502507501877136988/photos\"\u003eOussama Jebali\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAtOjCQ30sJDccBr3o-ZoclsnFa93JDKiEASWqi62zuRG9FuLob40jdGL3uXrdLcsgHx20Z_OhD0PjNzPmBbnnJ0LfX68ehhlb0MlPJB29a2J0WBieWg_LdsBfK3dB8-oDEhC2CDNTU6iYXsrgIdIebRhrGhTo3EO463-UdDeZQ78ptwZfSzmSAw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMfBSx4KhhhkF0UeWMCdP0UazRs_7E24vtKzNbY!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 4640 + }, + { + "height": 1232, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113656523680941643053/photos\"\u003eRüdiger Glaser\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA2ysZUbxW5dQlfbb59pYPpOF1l4igT3TQTT7IVN6n-NQ4lukemOM5utqsyR5Nfz8S8uaRRYW8Kx83tT7IEBFpuVTMbhI2Ey3xdWKIHKD9AD5UsZhO4CyJmuXqXwGtHugHEhC9b7KvFCwhHdnzdTeKbH5AGhT8fc5DbtnTzz71POW6nCKbQ9iCVg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNHNUYoAWyszUrGbpq3CPXXiyZPvUFoU85LsLGX!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 2048 + }, + { + "height": 567, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/108984356518392116661/photos\"\u003e孫如春\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAKwB2hUsW2roZJAyZE8aTupmOytpJ1ZmcgGYla2SZu7UVXJUiUMwff0NtkipmGb4bduhQlyTy6s0Wujz167Hzbs3oqpCJtnQkl7YlbPd9tTad-fdd43xYIe1emweWw-67EhD6aI9XyDbjV1SkgLlf9KteGhR5ySV2Y9Fn7ObYRM_YelqXoWGEog", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOOXVS-E2SQ0Eqqtu-ERGRdm7U8hS1R1Tvl-pTa!2e10!4m2!3m1!1s0x47e66e4334868de3:0xcfc3870abe2b8519", + "width": 567 + } + ], + "place_id": "ChIJ442GNENu5kcRGYUrvgqHw88", + "rating": 4.5, + "reference": "CmRSAAAAGqRt61G_amnMCTSG4Jbgm4wu5RdWaMBauyi2nPVVsKbaeDhPQQVCZ_ugee9os5VFYuHiPJxo19M-IovOcrYcagOzX-7yTWEjx9xQKGrAsFJOqLBGNla3x1W58t_OJ5ylEhD6xIGSgMglq5c6woEACAhmGhTjfNHzA8IP2UAO8e72BA0b1nyjLA", + "reviews": [ + { + "author_name": "VIHO GBEASSOR", + "author_url": "https://www.google.com/maps/contrib/106415011320657515581/reviews", + "language": "fr", + "profile_photo_url": "https://lh4.googleusercontent.com/-2dB9WW8DEUQ/AAAAAAAAAAI/AAAAAAAAAnM/0ZasLlN3QNE/s128-c0x00000000-cc-rp-mo-ba1/photo.jpg", + "rating": 5, + "relative_time_description": "au cours de la dernière semaine", + "text": "Je me suis sentie à la maison de mon Père ÉTERNEL. Magnifique lieu où il fait bon de se recueillir et poser sa tête sur la poitrine de mon doux Jésus tellement je me suis sentie accueilli par DIEU. J'Y AI RETROUVÉ LA PAIX, LA JOIE, L'AMOUR ET LA SÉRÉNITÉ DE DIEU LE PÈRE, LE FILS ET LE SAINT ESPRIT. ", + "time": 1497657494 + }, + { + "author_name": "Old School Master", + "author_url": "https://www.google.com/maps/contrib/116043538068600396909/reviews", + "language": "fr", + "profile_photo_url": "https://lh5.googleusercontent.com/-tuKQZ6SH7uI/AAAAAAAAAAI/AAAAAAAAAPw/b3tWKtZ5m34/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 3, + "relative_time_description": "au cours de la dernière semaine", + "text": "Endroit magnifique. La vue est superbe. Mais le tout est gâché par les vendeurs à la sauvette et surtout les pickpockets. Attention à vos affaires. ", + "time": 1497466049 + }, + { + "author_name": "Busity - Guide de Paris mobile", + "author_url": "https://www.google.com/maps/contrib/103980295702337146289/reviews", + "language": "fr", + "profile_photo_url": "https://lh6.googleusercontent.com/-KtxR9dUfbtk/AAAAAAAAAAI/AAAAAAAAAB4/5XyGib5sX9Q/s128-c0x00000000-cc-rp-mo-ba1/photo.jpg", + "rating": 4, + "relative_time_description": "il y a un mois", + "text": "Deux catholiques concluent que la guerre, la famine et le froid polaire de 1870 sont une punition divine et font vœu de construire un lieu de pèlerinage pour se faire pardonner. Ainsi débute la construction critiquée de la Basilique dans ce pays de plus en plus laïque.\n\nOn y pratique depuis 130 ans l'adoration perpétuelle : les inscrits se relaient toute la nuit pour prier, une crèche pour les enfants est même à disposition ! On peut entendre sonner la plus grosse cloche du monde 15 minutes avant les messes de vendredi et dimanche.", + "time": 1494848384 + }, + { + "author_name": "chicago chicago", + "author_url": "https://www.google.com/maps/contrib/100399158584839482616/reviews", + "language": "fr", + "profile_photo_url": "https://lh3.googleusercontent.com/-wsPhHm57W5E/AAAAAAAAAAI/AAAAAAAAAAA/AAyYBF7iaLhu1PV2yFCrXDhDZ70eGnfVzA/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 5, + "relative_time_description": "au cours de la dernière semaine", + "text": "Un lieu extraordinaire pour allumer sa flamme spirituel. \nLe mystère de la foie est très grand et puissant. \nC'est comme l'amour. \nGod bless ", + "time": 1497783169 + }, + { + "author_name": "Carl Carl", + "author_url": "https://www.google.com/maps/contrib/116524517425025586949/reviews", + "language": "fr", + "profile_photo_url": "https://lh4.googleusercontent.com/-7qEez3azS8M/AAAAAAAAAAI/AAAAAAAAAFA/1E43EGNFCXg/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "il y a 2 mois", + "text": "Un endroit pittoresque et magnifique... Un panorama sublime sur Paris, les jours de beaux temps. \nMAIS les vendeurs de gadgets à la sauvette & les pickpockets gâchent le plaisir d'être là... Une vitrine misérable de notre beau pays.", + "time": 1492510266 + } + ], + "scope": "GOOGLE", + "types": [ + "church", + "place_of_worship", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=14970958066519606553", + "user_ratings_total": 3688, + "utc_offset": 120, + "vicinity": "35 Rue du Chevalier de la Barre, Paris", + "website": "http://www.sacre-coeur-montmartre.com/" + }, + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByKeywordResponse.json b/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByKeywordResponse.json new file mode 100644 index 000000000..c5c83ac7d --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByKeywordResponse.json @@ -0,0 +1,939 @@ +{ + "html_attributions": [], + "next_page_token": "CpQCAgEAAFqjzUb17r8FAj_cVCctF0Gm6-CSfN9UCg0RU9O7rBeBATiDF5aXdv8mpLC5zN2YUapM2cCHu3jXjuSanqhoMVQ3bgKNSZAEH8a5LqVY8ugJ9dt7aMHe9fJHF2uGOENBQvLwyhkHs4nt5EIW_-4l_BPdpUOa4hEjCyFyqBEhA9XNDaiyw5dbNmNoaQ_l3DCqtxxnXjeavT2pBn_sYyhZ8QcALgmqHkVynp6tH2K8SaIKD83nu95mjkc3hAdKNztLis0gki8msXN6Pg-0ZPDBHfW-zstUDB8FN9iGYfqgsPkj6HCsqXC9um1wUO7LuBHBkPqjV_LcnRyj7JTw-Bm-8Yo-ukShYqGr4hxREQvf-aa6EhC7kN_hiwzomNcUqh3ljq3UGhRca1jlnRimf7QTQq3LEaBvtn76Xw", + "results": [ + { + "geometry": { + "location": { + "lat": -33.8688161, + "lng": 151.2054207 + }, + "viewport": { + "northeast": { + "lat": -33.86741351970849, + "lng": 151.2067014802915 + }, + "southwest": { + "lat": -33.87011148029149, + "lng": 151.2040035197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "055fc0529233283ca70983a947ccb45cc19952be", + "name": "P.J.O'Brien's", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3000, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117806466331261678520/photos\"\u003eRaymond Hurink\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAqV8ih8gUBsJNAqo-lFtpGYMLOm0_cubgUViqVGi5PVKFfzV-jA6s4mkp-CUYhZ_xiMmBwer1Sz79Mg_XiOz2j5H2FAUj-hTOdCBU5-RlN2t28OrxvX9fFWrFXAxdGqhuEhDsy122aysbgTTu26DbdhqjGhQ9oXKjKA8ZOIo0JvPjxM-HzJJv2Q", + "width": 4000 + } + ], + "place_id": "ChIJi6C1MxquEmsRP3l2oXb357Y", + "price_level": 2, + "rating": 3.8, + "reference": "CmRSAAAAXwvozspYD5iuU2I4ODI-nXWfShHSNxnn6-mb4TZfYZOpnJzxgojdXvpaB9ZMlwAkP-GhTolflYTaSMvJWgna5LJSqDWDW9GRCLAvhZzkbwblKCji3VXgiBbxdocTTwF8EhBi93s-ZyMYF7eyKuwckX77GhTSsyeBXQhzYdzvN2mkMK9L016_IA", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "57 King Street, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.887606, + "lng": 151.194751 + }, + "viewport": { + "northeast": { + "lat": -33.88622746970849, + "lng": 151.1959946802915 + }, + "southwest": { + "lat": -33.88892543029149, + "lng": 151.1932967197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "45cc9dd39d670303368ed99176403f4d77c7b0d3", + "name": "The Duck Inn Pub & Kitchen", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 1023, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113953815050801817786/photos\"\u003eThe Duck Inn Pub & Kitchen\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAlLt4w7t4RfBFFRx1IhRELbYgP5kVe1LhpouLJopzwimTS6csjCLPxzKa9LuRH8hPFwup0B2_TzlpS7eVM_OSJMQkr15ctA1OJWmZle5lGWxPuKzsOCp_tbtN5nKmvtRgEhAvKaUO-y_s-fy_bXLLL3zxGhSNfqvsc15u7sufQFzvDZ-FBzSIzA", + "width": 1024 + } + ], + "place_id": "ChIJ6zKDW9axEmsRATN9DcxvROM", + "price_level": 2, + "rating": 4.2, + "reference": "CmRSAAAA7aKqAQ05LCrykHwqkUPU0yjgU36zIEtOvuf_LMk-7EqIzaAoJiIalyd6OUGYQwJdKfXzaSQQ2wx77yphFjNgxspM7nsXJK92tFcDzyPpWKdVvfe-LSRCtcLG_ZVAe3mnEhCUt0JpxluAPb02CsIhFfO-GhSgGGXnR0txRlSsKiQN6yhioi00bA", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "74 Rose Street, Chippendale" + }, + { + "geometry": { + "location": { + "lat": -33.8807144, + "lng": 151.213533 + }, + "viewport": { + "northeast": { + "lat": -33.8793775197085, + "lng": 151.2149608802915 + }, + "southwest": { + "lat": -33.8820754802915, + "lng": 151.2122629197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "eb09cc4717c706056683e2a521e33d997deabe4e", + "name": "The Porterhouse Irish Pub", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 4160, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/103932258838338736798/photos\"\u003eAaron Rose\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA47rRUKjYaSnzmL1c74LmMo3Hh1qIacyUPv5BVpSREpCT65K1T-WZu1BtjihZe9ZeDxFr2qas7a-LrsXw0f81avIx6nwiUg20_1djkUVk-QlQYPCnNd35F26o4GjfCWmlEhA61BBuARWMP4oMaslH0bEmGhT5DN5m_7xtpnnw0aSogpfOwK0VDA", + "width": 3088 + } + ], + "place_id": "ChIJP2P8zRmuEmsRQVp4dtZkfCA", + "rating": 4, + "reference": "CmRRAAAAF78Px6ybqab26JVnGf6KzEnYlN88EdoEwZOJMCOkQMXK7sg08Wmb6kX-HeOgmsRE9xOKrhZ9gbzu6_ImAQsFodS04xCEx64Buj0ALRfNk3U6xjP69b5BX3gUsc1JkgNbEhBegWETB6NjlrzZFgjbNssfGhQIfMEHSfxCqfieDZmRJYoD22Zh2A", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "233 Riley Street, Surry Hills" + }, + { + "geometry": { + "location": { + "lat": -33.8706234, + "lng": 151.2049546 + }, + "viewport": { + "northeast": { + "lat": -33.86943736970849, + "lng": 151.2063536302915 + }, + "southwest": { + "lat": -33.87213533029149, + "lng": 151.2036556697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "302a5deea4b64f1bbb2ad583acd56ae853825f8f", + "name": "Sydney Pub Crawl", + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105214247616599294112/photos\"\u003eJose Ramon Ayala\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA_3SHRr0kycOCrUi7tq1RbeC4_p0ESok_2yeaPT7t9RwoxufYY_scIwdLgaXNIIFDu7UdctTVFiCKnFLmo2N-6QgaKgx0Q_Cqluob5d2M9-GChH39YleWhwMTrDeCZN_7EhDD0NEyKGdrSOhzm9t1HpE1GhRmmcx2hZzEUsJir4IFExXhIGyU_g", + "width": 2048 + } + ], + "place_id": "ChIJR8Dt0D6uEmsRedzJHCMq0ms", + "rating": 3.4, + "reference": "CmRRAAAAOzOOQvDuX8vC4i_peTWAxFt3-c7Nyv3ztTae_0UhQvkYxxCsSqh4kgZmMWa2bEH7VY70N0FKKqzGIPZAsp9-9yUklRI6P9WhJsfcgC3GWqBXPJgBdmWKvs3Y827b32AnEhDRN_m-qKpRy9jQ2KW4DE7oGhTU4nNaJchMw7K-pZ9yh2xHzL6Acw", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "14, 22 Market Street, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.86845810000001, + "lng": 151.2061085 + }, + "viewport": { + "northeast": { + "lat": -33.8672087697085, + "lng": 151.2074610302915 + }, + "southwest": { + "lat": -33.8699067302915, + "lng": 151.2047630697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "cb213345ba5ce4673daad05555cdc1a587c17f4a", + "name": "Le Pub", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 333, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113737079268584279567/photos\"\u003eLe Pub\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAL16b6pY_zAir-bHDUcR_skHbkQ_POWxH_aFL90cjp6gtJfnZbC-7Y6lYNsIJcBHvqoI8F0F-iwFZz5Y35o6qwZxQfggTcJEueqOITIBOs8EikdCRRklvZMJwzGFE-ayQEhBeWmePL_A1qermtQStS1yrGhSoqwHUwtUes8yJhzsMNWmq3TE9Tw", + "width": 333 + } + ], + "place_id": "ChIJRT6jZT-uEmsRWT9zOPjPrP4", + "price_level": 1, + "rating": 3.9, + "reference": "CmRSAAAA2HvL5LkpWoBKPg4QQrOhIe04wtsK3YewE6Mzm6CNAquOVh3PNFF5KPHGi24wdtHpcS6mHTXKPdoJ9p3LjT6akt-uxI9wxDMw0GZkOA11SGcrPhzfhjYeH-In1skK0QUVEhB2SDL2vIU0xq-yMOQJti2UGhQrlz5NkFqEYZWsZ4H04_gWqIwLdg", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "66 King Street, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.8880093, + "lng": 151.1577672 + }, + "viewport": { + "northeast": { + "lat": -33.8866683697085, + "lng": 151.1590475802915 + }, + "southwest": { + "lat": -33.88936633029149, + "lng": 151.1563496197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "d45983a799fbfe26fd0d25882927c7da06351dc0", + "name": "Norton's Irish Pub- Leichhardt (Formally PJ Gallagher's)", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106670451090160433951/photos\"\u003eEmma Scott\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAkwufIKgAMr7q3PKKP4PteQ6MU6T0-_5KsLn0Kv5jnfe8xfgejPS6PwTumonJViCW8XiBGVYrN_mWjwlKG4yOcDLZxLQ_n4KxEDE9zy8z-HJnZb6dQV3FDCZUeAy_5rRREhA78OpjPTSFr7F0BSBnkNGSGhR_H7jiKsD5lHJzH6H4mxMvRwkM0w", + "width": 3024 + } + ], + "place_id": "ChIJI0nSzBCwEmsRYdQS64yO1us", + "rating": 4.1, + "reference": "CmRSAAAAtmANLCrcg9JrZZWJ1mwbuKEKeznOwVVg4hNi7RL88ujx3mmq9WY4FdHPb9UKCcHAGh31VA_5Os90nQvopuhoKHr5_lHdTrOGSq66EQQM6pFAyulYhOlHG-YDcOgEhieYEhDtLRCr9AmFSWxo-jx6goCZGhTzN20BOd7KIRDPt_P0cUpVQEtQdA", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "1 Norton Street, Leichhardt" + }, + { + "geometry": { + "location": { + "lat": -33.9263286, + "lng": 151.1592735 + }, + "viewport": { + "northeast": { + "lat": -33.9249576697085, + "lng": 151.1605780302915 + }, + "southwest": { + "lat": -33.9276556302915, + "lng": 151.1578800697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "69056af3bfe4ea436f92e6ac1f06cbe4ec4c84d9", + "name": "Riverview Pinup Pub Tempe", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 250, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106848792601934498800/photos\"\u003eRiverview Pinup Pub Tempe\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAApIXD3RNOWfw2OWdMZzQvtAfuXEbR4IFDSKpkEXg3Y59K4SW0ftDJbhjP-nsNZwDW2kPu28GnzqUnua429AL8ZAR7c-EVFnknQA7RcWcm4Vy5KabMB2cLtcHF1gzzuzifEhDgipQhUbYW7iK8vvHE-YRHGhRnIjSq3DqmdboLi9z9aj80THHoMg", + "width": 250 + } + ], + "place_id": "ChIJOwv_v42wEmsRK87j7bW-qUw", + "rating": 3.3, + "reference": "CmRRAAAAO8NhApcRGo4GwthBwDkSRlgOg57H9esFV01IV4M7kt0SK_0W2RumJ-3KG4rVw-oWv46EpKLyb-dil1X7xR7MFO8kyDWOoDJao3gwce15iufcOYCWJvwkbXJL6DTAvjrCEhCVhkH-7gxtuQiBt7kREHweGhRgPNErDEO6tN1dz_l0tnEToh0hnA", + "scope": "GOOGLE", + "types": [ + "bar", + "night_club", + "point_of_interest", + "establishment" + ], + "vicinity": "900 Princes Highway, Tempe" + }, + { + "geometry": { + "location": { + "lat": -33.8608245, + "lng": 151.1716101 + }, + "viewport": { + "northeast": { + "lat": -33.85949146970849, + "lng": 151.1730336302915 + }, + "southwest": { + "lat": -33.86218943029149, + "lng": 151.1703356697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "2c76dd8b7fe41c3e36fdbadfc7da680091d297a6", + "name": "Sackville Pub & Grill", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 1022, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/101875913229322147504/photos\"\u003eSackville Pub & Grill\u003c/a\u003e" + ], + "photo_reference": "CmRZAAAAx9tR6Hkj98WUVbKhx_4InHoTunmNpWsptSh3itK-k6qg6s03rrf7FEsTgfwumjQHYMZ73kuVbHWazFUcs5jfdU_Xh8XBMwD8_tbj1AIiQQpo686VtR-R6tLk7DBh75gSEhD2U0SkJc2xqun2tY3JhXtpGhTD7i11DNXS2fLRUayzw30DuzAHqg", + "width": 1022 + } + ], + "place_id": "ChIJ44wJDsevEmsR7CCw-4O7zVQ", + "rating": 3.9, + "reference": "CmRRAAAAJ67qAMB-y_LaNNMpNzDRLw-5c5yxtCDqOAP_4URmpESdfFRt3NZKgvijXsQJjyhq3tce1OC7sc50Ii071MFoydkn4SPw5ArNjHaN7dsMkFNGU-MEwV5AiMuBCdhtz6DgEhCDDIda9DpzzYmwY0SxeYfAGhS8_p0wlwuUF1NF0Ac3vWpSud0yMA", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "599 Darling Street, Rozelle" + }, + { + "geometry": { + "location": { + "lat": -33.8838547, + "lng": 151.2098683 + }, + "viewport": { + "northeast": { + "lat": -33.88254421970849, + "lng": 151.2111736302915 + }, + "southwest": { + "lat": -33.88524218029149, + "lng": 151.2084756697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "7551d957e77a2683a78f64caef50229949978b5f", + "name": "Keg & Brew", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115845200882116784017/photos\"\u003eKeg & Brew\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAoUNyUUKD4q4EwNiE_DoaiI4Rk5vnUasHWp3HXQakw5_8nWa_Ld3RBfgJ_aiGiZDxQQKXxf57GlzQpb04Kylp0UyOUMnPcOvtEg9SFM6eKl-oyHU9zybf27d2MjiSLGj3EhCOWELpWbCo2TSZRB9Svm0cGhTft1maH-2WFycRZ3iDYREIy5Vgfw", + "width": 1365 + } + ], + "place_id": "ChIJyVf-AyKuEmsRf5Ez1dNyUD0", + "rating": 4.3, + "reference": "CmRRAAAAEhiGWrvYZoimtASDG8w9ijOzf5F1OjNhvjqQaVEmXqfqLEol4P1pbN7izycB42dZCEWkkc9kvYblXaFmyEfDwDw4BWP_z_n7iQlw3rqAyoCMDBOImL7DoRf2majza-vLEhD4BsqrXq2FqTivWtTYyqRFGhTrz6mW9dIqKp0RJFcKgyXRYYzbXA", + "scope": "GOOGLE", + "types": [ + "bar", + "liquor_store", + "lodging", + "restaurant", + "food", + "store", + "point_of_interest", + "establishment" + ], + "vicinity": "26 Foveaux Street, Surry Hills" + }, + { + "geometry": { + "location": { + "lat": -33.8814392, + "lng": 151.1925898 + }, + "viewport": { + "northeast": { + "lat": -33.8800732697085, + "lng": 151.1938764802915 + }, + "southwest": { + "lat": -33.8827712302915, + "lng": 151.1911785197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "670019f3e91c3da6442f5f3b7443da7e775fd2be", + "name": "Friend in Hand Hotel", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 2160, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102106827556597389332/photos\"\u003eGreg Edwards\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAADk_UOPG9X_3grgol9HL_pkXBTan64fIDJq25ITAgnUcMnwPsMs1mtQlkMeEpKLys545GlWYb7bP6jA_YLczO8ZfDOyi7m2vvLCGEjv6YfdNX9acafBzuYh474LIOGa1REhBiQd6gHcVumyMakraHckC1GhT5_-S55b350Fgfi-AtKJ5HtvX9LA", + "width": 3840 + } + ], + "place_id": "ChIJ410y2SuuEmsRO1xNFpqB2Rs", + "price_level": 2, + "rating": 4.3, + "reference": "CmRRAAAAnKkExm_1XzXFz-No3eyv0DbmP5hNaIYw5n3lXaPgSkSro47D6lsslSmY9WzG2p3FGn_bQMlQSGaMP4hycnN8MkM-6mVXWuGVdVZps6CymOkt2QY0Pxuu8-zBfx1fBF6IEhCXGx5xFFFzmVfmD-rUW9gmGhTyLB4bXDkWAkHJ48dou5y9iqig-Q", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "58 Cowper Street, Glebe" + }, + { + "geometry": { + "location": { + "lat": -33.9051572, + "lng": 151.1613261 + }, + "viewport": { + "northeast": { + "lat": -33.9037589697085, + "lng": 151.1626525802915 + }, + "southwest": { + "lat": -33.9064569302915, + "lng": 151.1599546197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "7a3fb9c6b5b3bee22b72e8155d373a53b796fbef", + "name": "The Henson", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 2992, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102947155740486290254/photos\"\u003eSuan Yeo\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAHfqO5Smh0uOCDUWAO6MWbqNwWz9P8V7Xby2Jb3M-HW1bxWKEXiaAQvFIi_SJoU7IPHXCHP2I-hVLMGu2FkCNDas4x-wXINhpXQxVNDh2JXVWUFIUs7QLUHvkBFDl4whREhDpEghQ6hCIQgQpDtEs6iebGhRQhZP2ZrAmQpIcMQ1SQF4Hd85cfQ", + "width": 4000 + } + ], + "place_id": "ChIJzZIVrWmwEmsRcYBztYxViJI", + "rating": 4.4, + "reference": "CmRSAAAApY_H0W51JmJWy6CxWAFPUrq-JPHBBVvgqJAM1epLo18IK3s939gNhe9vkUahxAcKXShaOStk27BKMER-Kay3HrpcMdkfGNfOnpAtTV-2faRlC0uXtybPlPNygptc-BUvEhD4Km_4XPfbEHEu5Lfyr1rkGhRDVCwr__jRon-3RpOCuOc5qLoLrg", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "91 Illawarra Road, Marrickville" + }, + { + "geometry": { + "location": { + "lat": -33.8603379, + "lng": 151.2084268 + }, + "viewport": { + "northeast": { + "lat": -33.8590047697085, + "lng": 151.2098651802915 + }, + "southwest": { + "lat": -33.8617027302915, + "lng": 151.2071672197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "1dd3d981147a8fb67c7f969ce50bfeed910f5b4f", + "name": "Fortune of War", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 535, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111045667209259834013/photos\"\u003eFortune of War\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA6Ejj8nhSM8U-e7iDiliDXRK1yXFu6wTwqISazGH5xgDQA5pn4QePt9je6cW7GK-YdIW5vx5CTRifJZPVPc3GTxFgUjSkvDN5BAOdUsUonV_bGi2FkRsiLSErxJ5LQn4CEhCT6P0RYYoeqPDx4FTptqSzGhRfY6og5c34vp--hXqmKUPNMZ4xtQ", + "width": 800 + } + ], + "place_id": "ChIJNbpd8kKuEmsRBZKmQcttLg0", + "price_level": 1, + "rating": 4, + "reference": "CmRRAAAA0TkGs7_FmYoSmij1pPdPhugraP0NgYmOzdP0QGZerGOWZP3WcBvoRY_ZZFBRZtFvbm3q0lXlQnUuyyiiBkJ_6-C5ESobSPYy1UtRNH64FKLL31UWrbE_zuqdi_1E0eg6EhAasqMHTZKyjdngHeIQ1GrkGhS3lKynlzKDFKaUskgqMzpw_QbT0Q", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "137 George Street, The Rocks" + }, + { + "geometry": { + "location": { + "lat": -33.873378, + "lng": 151.208518 + }, + "viewport": { + "northeast": { + "lat": -33.8720188197085, + "lng": 151.2096561302915 + }, + "southwest": { + "lat": -33.8747167802915, + "lng": 151.2069581697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "e6181904cf8a3bbef6ce391ca2dfd6447858635f", + "name": "P.J. Gallagher's", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3036, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114145759290528331704/photos\"\u003eSlava Kramarov\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAhBNa6yOnuj7NIE3AUkeXajesgflm8834Lmk_6bmv5T7_W5ybg4ZycqcYDI_QnQNCZntOVW6pibhxzbPttc7070FoAFruDPCivOb--BsLWkKoTQjXF5LYd_fAhcViAnM0EhBFkmA3DZgj3j2Mj6BnjsfYGhQhE7vpV-oLC-Qc6Sbr66ENa_Cy-g", + "width": 4048 + } + ], + "place_id": "ChIJ-9uKcz6uEmsRao9rR9Ke7AI", + "rating": 4.3, + "reference": "CmRRAAAA7do9YlAY1u1SEBGe6qq17bXRcLeZ_Aj9hp6fSBO7KmnawG2aQJZYj4c25nPu4T7Oer8YbYN5qbpPqIz_Lvyo9daJTe1MY6-680svCFYzeHHdzVCV2Jh6lxFARgf7XKgOEhDgCRGchvIe0pH4fNKQhIA3GhR8WFTy-cRg2lYqVZREpkH3GtAQ7g", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "260 Pitt Street, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.8871705, + "lng": 151.2101415 + }, + "viewport": { + "northeast": { + "lat": -33.8858549697085, + "lng": 151.2114271802915 + }, + "southwest": { + "lat": -33.8885529302915, + "lng": 151.2087292197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "b2f60421c1872ea923109a82b6844fda35881e01", + "name": "Dove & Olive", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 2412, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106090133994529641347/photos\"\u003eRonny Ilaya\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAdSYG2Eyvb1oBFZsvHRaRChO8zw7nxtZ3Iyow8Y2lOI-JOrmldshci9tLU81U5s1SmpdmGaLLWvXhkfYldlTlgxwwjhcbtn_BoBUNS9qSzfulue21usnI66qKaNxtAJ3HEhAPsS45e0zEAOK3W7BjtnF0GhSF6bOb3RvRvVu_w0tLeL9IlOw9Yg", + "width": 3618 + } + ], + "place_id": "ChIJh7-HKSCuEmsRYE1hFIW0rIc", + "price_level": 1, + "rating": 4, + "reference": "CmRSAAAAAEKiS7OatdI1YkEmgaePnux-0t_sUG1n0whke9vlHuiU5kRJ2yGefRkWS5oqUrgvziTkVCtcOoVos5YYya7xTEAPW33TVm-oNj42LGWDy1MBDox7yKHihH_POaIgufLmEhDPHlHS-C_wO4-f_tadQ0F3GhSk1a1nISYjFVzjL2cI-QetgrHmbA", + "scope": "GOOGLE", + "types": [ + "bar", + "liquor_store", + "restaurant", + "food", + "store", + "point_of_interest", + "establishment" + ], + "vicinity": "156 Devonshire Street, Surry Hills" + }, + { + "geometry": { + "location": { + "lat": -33.8960457, + "lng": 151.1782535 + }, + "viewport": { + "northeast": { + "lat": -33.8946351197085, + "lng": 151.1795323302915 + }, + "southwest": { + "lat": -33.8973330802915, + "lng": 151.1768343697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "4727c560df10217ee34ecc7cc594c8cbe4b375a0", + "name": "Courthouse Hotel", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 2400, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/110501364689876552445/photos\"\u003eAndreas Schnieders\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA85WPTcPHkaZvtaQuEKPvnF_q1jWGW74GqoudqtBxJ9Ei__xkziK7uZ3npZpQ-vGIafku_pHT9rGfLEoHPhZ3qBnmSS0bFarg23eN84AdedtZ_0UqqOfPfSBXduKXAEMqEhBOtiZu_kO1tinzYOn2WRDzGhSvIdPDqU4TO-9G1nRoHP19aSF3fA", + "width": 3200 + } + ], + "place_id": "ChIJlS8KKDGwEmsRWTL3GDRyiTI", + "price_level": 2, + "rating": 4.3, + "reference": "CmRRAAAACXjhrMMCnnOlPJVtOhFJm2GojrXYoejv7UDtFJeQU29phjSxDtVV5sJKNkuEt-P_QxB43Z_LWVgnRl_fqTos-YMlmSCnefaO2AqJbE7qkvsagcAGeOm5Wfx2REpZSwGfEhDdJFX7Wkh9T80yiSukUWaBGhTwPD0tSGae8l3EPx08aOiBsSj3gg", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "202 Australia Street, Newtown" + }, + { + "geometry": { + "location": { + "lat": -33.8731172, + "lng": 151.2206205 + }, + "viewport": { + "northeast": { + "lat": -33.8717808697085, + "lng": 151.2220503302915 + }, + "southwest": { + "lat": -33.8744788302915, + "lng": 151.2193523697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "9f89b0ae436ff3f2aa5fe4748fbf94249978b4d3", + "name": "The Old Fitzroy Hotel", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3456, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105742998263001574374/photos\"\u003eAdam Pasfield\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAvoqvIoHNjnpeKz9H6896f-11NSWPOILlO7yGhyUfPAkXVI7APEIC8-HsZRDPzVKebb3MHWSGORJt9_QSUnMzXuc7nQLUSGrXmst3AEYSRVtaHWpKOkHP5kT_9s3OpuHbEhA1woZrie8HIMkzIlkNKCA4GhS7lQa2WNhAso0L3sqqEPpXA4M2yw", + "width": 5184 + } + ], + "place_id": "ChIJlbQ0lBKuEmsRdaTilXX7vyM", + "price_level": 1, + "rating": 4.4, + "reference": "CmRRAAAAjuCQB0uoA2IJ01bfQP5DB93p57Y9gWiGSxvgOFoJktgWIPlnxl9qZ9U18qZ0KY70ahELg6a5ux5Ernv6ugzUec0Ekx0Vq7VEr5UH3GQpA1x0pmsomglRNGKOn_TU0L_GEhCY1p3WGqq19k84Kk9zCWK4GhRiGymP8R5QmVjQ5MOZ3gSb1ClCHA", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "129 Dowling Street, Woolloomooloo" + }, + { + "geometry": { + "location": { + "lat": -33.8637793, + "lng": 151.2075705 + }, + "viewport": { + "northeast": { + "lat": -33.8623803697085, + "lng": 151.2089168302915 + }, + "southwest": { + "lat": -33.8650783302915, + "lng": 151.2062188697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "92ae26be53927384884828944c93d71c2bb7e912", + "name": "Metropolitan Hotel Sydney", + "photos": [ + { + "height": 1152, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114461641288544581080/photos\"\u003ePeter Phillips\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA2xYLebxkYhlnKsr1vm3selA49bRL2ezZBQW18PPWI27NEpKZeLyGDKzZ0C8ORc--N-lRSlsCFl9DrpYsPeDzEHmTh9TOHYn4LLAM2Y3-EoePVpxXCCVo59bLr58w3C31EhBNyM1P25xZiBWMVGplGN81GhRYlVSJv7qv4Ai3hdfJmh4DHpVZJA", + "width": 2048 + } + ], + "place_id": "ChIJs19DfVtTDWsRiD0NU0ubRS0", + "price_level": 2, + "rating": 3.9, + "reference": "CmRRAAAAQrShEBot8zeUhegGApdZx5fqpOrB_6mhtbkomZmrgw6AZ6ZblouKGXtO4ShR5qCLQP0JXp8vU_Bb8lM6IOpXTfnsCpmyzuv1qMqXypRkbTTJKH6TijRRP4e6_MB8zm7xEhDCJVpnQdcOoB6M4VGL1AwXGhRjCSjfHH2ji1QgpGgwMUh3i1xC_w", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "vicinity": "1 Bridge Street, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.8779535, + "lng": 151.205992 + }, + "viewport": { + "northeast": { + "lat": -33.8765367197085, + "lng": 151.2074472302915 + }, + "southwest": { + "lat": -33.8792346802915, + "lng": 151.2047492697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "a64fe77522be040983b9e6876c9772da29eb36ad", + "name": "Scruffy Murphy's", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 1080, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104717438832872238815/photos\"\u003e中野佳小里\u003c/a\u003e" + ], + "photo_reference": "CmRZAAAAhQqJ3BRK-BSG6m1urBb-8FnuUJJxvaNbgybpvXE2Jd_MesHAr7V_4G2t5Tz1jGkpf4kVcrHwH14T_GjLhlrWR4sgCYOrNpFMFkz8AzegAGvCV_xbuNHHDTnoMKwuOKowEhB0R5PK11mRjWsrR4llNZz0GhSLIHYD30HYTQzAHW6Tmly_CW0keQ", + "width": 1920 + } + ], + "place_id": "ChIJeWxuyzyuEmsRtTLSgVCaXE0", + "price_level": 1, + "rating": 3.5, + "reference": "CmRRAAAAZIwqbQEem4SR7E2jjYGiV8sXKKnmHC86BgU-dGPxp0BIQvMEJ12SHC7GJVCsxArunIKO2klTe1N-d62JHL1h_WX1o10C-ev-SnF7aTALD6DQbuVOYgGZihqh4-q3zkuZEhCHZmfwErFSDpRD9dWqyYTwGhS2OsRmfTfYowkK1JbCXV--AeIKjA", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "43-49 Goulburn St, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.874618, + "lng": 151.208182 + }, + "viewport": { + "northeast": { + "lat": -33.8732758197085, + "lng": 151.2094358802915 + }, + "southwest": { + "lat": -33.8759737802915, + "lng": 151.2067379197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "b400c388bb9aa9054706aa90bf31d2a2930815ee", + "name": "The Edinburgh Castle", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 1080, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113826858084592854155/photos\"\u003eThe Edinburgh Castle\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA8ACWsxByqRLoye73VQYHqgM2LHMWlAvQz5LUHDT1rviknuE2UuNtZb9E9vI0LhD2gcS5pJTUoBelTen19DHJ9XdhXHpK3xPYIDQbn_a_W0G30658unudCokItW1vw9CGEhDuBKYwgPEIOMhKho-5lSetGhRe4Er2tNefPMXaCT4diQd1Mw9oTQ", + "width": 1080 + } + ], + "place_id": "ChIJtTsYxj2uEmsRAfgvGJspid8", + "rating": 3.8, + "reference": "CmRSAAAALEAHXnKmBWI8GvwfZ5ZqIybDjPguBb8kF6iQ6bqXEvWtcOf-OZBn-Z_VzyiX39UE1MRMUU_C-laYVkexTV-Fyo9PXF-SPUJVhlLT6DHNK0epEsdv8I4U1DV9xuiPiyzkEhAk5a6V6XCmbroOUJDR-pWyGhROBMP22GZ4RAMeCyMrzZsnYX9wVQ", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "294 Pitt Street, Sydney" + }, + { + "geometry": { + "location": { + "lat": -33.865556, + "lng": 151.208428 + }, + "viewport": { + "northeast": { + "lat": -33.86415041970849, + "lng": 151.2096571302915 + }, + "southwest": { + "lat": -33.86684838029149, + "lng": 151.2069591697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "e0d362bf984e8322d3a245cfec08190995281ddf", + "name": "30 Knots", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3840, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/103391205190268256461/photos\"\u003eAnton Kalsbeek\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAGDULI-aGM49ntavUh7fVgR4pxYOmH2EwQQzt69rHvLxX9zVRhpSyVtjVy0hLsHSIQ0OZ7lGfEb396H6SW32vjw5T8MiXXy91_ShFSMs_3Faj6THhinNCShG65TtfReDUEhAf3QRJZxXy2vCHJ6ub1hVzGhTlC6rlaAR_y1P4ZqaGIJiDq9MwQA", + "width": 2160 + } + ], + "place_id": "ChIJQz7TtkGuEmsRWkkYLaQCGHE", + "rating": 4.1, + "reference": "CmRRAAAAJTWkSTd1KwabgT8L6rTIIAcXTZzpGBnEOJSYZBJbYykdDfbk8Vp0B0h1JtRx9nADJKjxOcEv_Z_UdEXsrFfhPWcP1s2GHthKGcSZ2q29FhOF365F45p4kwPXbE0Q0fS_EhCcFr43dmyUUIMMezEOhBVUGhSKYwOqVYXiCcAcipMKWNcjzSGfxg", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "vicinity": "30 Hunter Street, Level 1, Grand Hotel, Sydney" + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByNameResponse.json b/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByNameResponse.json new file mode 100644 index 000000000..4dcad0995 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByNameResponse.json @@ -0,0 +1,3004 @@ +{ + "html_attributions": [], + "next_page_token": "CvQCZQEAALscLoZARP84ZZc_1kri5-OvIviHUrsZ5FwUNhm4BltCfXjZRniweBMtc5WC9dgLtaQjne_wZ2M70TlJogRtaNhmZpP-Xc1cjfiBxWw9a9hMgRKdzd-AzpTeZ0UVjaIBU5n4VFDac7RT124SGCl41PgK3GCuReGTp6-MgtnUTHkgZ5CzIygfkLC_S53BXyVfj0m7yaj7dgcbTqhyfJ0p9PL2XoBYrmat0180ZVZqEdUh9khBL9M9ICZyrw0RMnUh3Jt4nnlzw6GYtjUjDs1JsGuenKXTWWOLUDkcrLO_uvqosfJpuLpbIhDOEb--yjciEFL_ONW_enA5l11n5o7Fshgd9Zw3lN7m7V9lRsNcEjx665CAtt8iCL6vhdUkGsB9k_HVB77WsZtN2v2DHcJXO2MFP6HqSNvyw4ziIFuRZhFFiADWRwZRt2hzhd4pl3fAXuFLek_EmdtwCx5mKdi9yXz9TbYSUj6cZQMtjgNeJ5v1EhDg0uhWN3BQItelBzPj5LKtGhRh4iD9RO6QFKQksNKADRH-cGYGBA", + "results": [ + { + "address_components": [ + { + "long_name": "483", + "short_name": "483", + "types": [ + "street_number" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420685021490309, + "fprint": 12782971787189354742 + }, + "formatted_address": "483 George St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9265 9333", + "geometry": { + "location": { + "lat": -33.8731575, + "lng": 151.2061157 + }, + "viewport": { + "northeast": { + "lat": -33.8716321197085, + "lng": 151.2078775302915 + }, + "southwest": { + "lat": -33.8743300802915, + "lng": 151.2051795697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/civic_building-71.png", + "id": "017049cb4e82412aaf0efbde890e82b7f2987c16", + "international_phone_number": "+61 2 9265 9333", + "name": "Sydney Town Hall", + "opening_hours": { + "minutes_until_closed": 198, + "minutes_until_open": 1038, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "0800" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "0800" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "0800" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "0800" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "0800" + } + } + ], + "weekday_text": [ + "Monday: 8:00 AM – 6:00 PM", + "Tuesday: 8:00 AM – 6:00 PM", + "Wednesday: 8:00 AM – 6:00 PM", + "Thursday: 8:00 AM – 6:00 PM", + "Friday: 8:00 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 3264, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106517052581150732239/photos\"\u003eL. Wong\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAlDwWVW3lczfIKI82AzoSy7drdU-fcLLY3NDbjigumvWNvUJVW1t7Xpkl-Ef60VlAyKG8SFJ_0mHuYVpgwhXcZxOj15IfsDdyGOiS-uYM1RYG4MFx3he7x0FKKEDfNyKuEhA2yB0qRFnF-CQyJKSFuty5GhRoFquSMKtslFZEOXO6vCRRDCYYKA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNAupVBTjLPjGvOxLRWk-N_b6cuYwsuzodeCMMT!2e10!4m2!3m1!1s0x6b12ae3c27682c85:0xb1663d1d054300f6", + "width": 4928 + } + ], + "place_id": "ChIJhSxoJzyuEmsR9gBDBR09ZrE", + "rating": 4.2, + "reference": "CmRSAAAA8BnE-UztXyrTnWyaBipDQyv_kLOaRaQqAoqWxtE2XcNGMoQJksJZ-EthNOsP59l7qWvkr7RRJdwGt8V1j4vAVSZ61IIcr_3jOawOKnHJQPLdN0u1mzR2US6KDJgSXGYeEhAftELzcoILlu1_fdUQW57FGhQEMeyNZwTqUnxYtDVor9-2-zLaFA", + "scope": "GOOGLE", + "types": [ + "city_hall", + "local_government_office", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=12782971787189354742", + "user_ratings_total": 176, + "vicinity": "483 George Street, Sydney", + "website": "http://www.sydneytownhall.com.au/" + }, + { + "address_components": [ + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "feature_id": { + "cell_id": 7715420685054891845, + "fprint": 441310607586089100 + }, + "formatted_address": "Australia", + "geometry": { + "location": { + "lat": -33.8736864, + "lng": 151.2069466 + }, + "viewport": { + "northeast": { + "lat": -33.8723063197085, + "lng": 151.2082932302915 + }, + "southwest": { + "lat": -33.8750042802915, + "lng": 151.2055952697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/train-71.png", + "id": "90f95efa6e988cfd6ec4b065b15586128ebdd94a", + "name": "Town Hall", + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113352520827955932935/photos\"\u003eVikrant Singh\u003c/a\u003e" + ], + "photo_reference": "CmRZAAAAgsVA1HCF0y2MB1m7b_FPEpT-qszCUWp0E17ojbGlC6xxNgWoUzq6EKQMXDFlLndZtL1c4TCw4BRgJ0zxYGEzPsIXMSelIcMb0mwoW2JzFNqDv1jixueJTBznqmcx_K9zEhD7u0AS19-fNchuqfSowTsjGhQbOtQ0sQIQYp2WatcsoqFd--ZJnA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMDRiAUTt8kLtsmw7b-RhRzGMODPJvfDVVjkN0!2e10!4m2!3m1!1s0x6b12ae3c2965d745:0x61fd9a8d0c7d88c", + "width": 4032 + } + ], + "place_id": "ChIJRddlKTyuEmsRjNjH0KjZHwY", + "rating": 3.7, + "reference": "CmRRAAAAZlRIEE5TCaYpO9MOHHLywwRQv2Nk0PnwVOc5vFNgey5x-uka0rGPDMsHk_w3CS7BRTdXtJ78Fl5wjhEo85FAJdSiFC5D3wWLrlGk-nTzsr0CuGevmypTXhkUs264uq1dEhDojbw4B8EtsPyuxDfmRMIiGhTFNc0HRyMo8nDpbQS2krlC7YFkyQ", + "scope": "GOOGLE", + "types": [ + "train_station", + "transit_station", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=441310607586089100", + "user_ratings_total": 104, + "vicinity": "Australia" + }, + { + "address_components": [ + { + "long_name": "51", + "short_name": "51", + "types": [ + "street_number" + ] + }, + { + "long_name": "Druitt Street", + "short_name": "Druitt St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420684422656819, + "fprint": 12363350464392566286 + }, + "formatted_address": "51 Druitt St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9261 2626", + "geometry": { + "location": { + "lat": -33.873179, + "lng": 151.204726 + }, + "viewport": { + "northeast": { + "lat": -33.87165621970849, + "lng": 151.2060561802915 + }, + "southwest": { + "lat": -33.87435418029149, + "lng": 151.2033582197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "8528fc58d885664150f88ae12247e591d59331e8", + "international_phone_number": "+61 2 9261 2626", + "name": "Grand Sydney Thai Massage (Town Hall)", + "opening_hours": { + "minutes_until_closed": 408, + "minutes_until_open": 1128, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2130" + }, + "open": { + "day": 0, + "time": "0930" + } + }, + { + "close": { + "day": 1, + "time": "2130" + }, + "open": { + "day": 1, + "time": "0930" + } + }, + { + "close": { + "day": 2, + "time": "2130" + }, + "open": { + "day": 2, + "time": "0930" + } + }, + { + "close": { + "day": 3, + "time": "2130" + }, + "open": { + "day": 3, + "time": "0930" + } + }, + { + "close": { + "day": 4, + "time": "2130" + }, + "open": { + "day": 4, + "time": "0930" + } + }, + { + "close": { + "day": 5, + "time": "2130" + }, + "open": { + "day": 5, + "time": "0930" + } + }, + { + "close": { + "day": 6, + "time": "2130" + }, + "open": { + "day": 6, + "time": "0930" + } + } + ], + "weekday_text": [ + "Monday: 9:30 AM – 9:30 PM", + "Tuesday: 9:30 AM – 9:30 PM", + "Wednesday: 9:30 AM – 9:30 PM", + "Thursday: 9:30 AM – 9:30 PM", + "Friday: 9:30 AM – 9:30 PM", + "Saturday: 9:30 AM – 9:30 PM", + "Sunday: 9:30 AM – 9:30 PM" + ] + }, + "photos": [ + { + "height": 1080, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104283564857309021270/photos\"\u003eNyea Pritchard\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAXu3a82XeBkjZ0pQdURZbRc8UYXFJrSI3bBn0D4WhlMPsfWWJMIJE3CLciWhD2YPa5vHn_mSbyWj-N3Ru38RGSDJoBQBuhCPRPhm2y0v_Ny8s6u5fQvdqNJxMwL_qYQfXEhBhowgEZcLe5q_qPJteRl1iGhQR7mGaXStKexgyyzSjroM-nc5-1w", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPrE8VaDEknLqgSa-FgiyMtsUVJLDpI2ydgmIDf!2e10!4m2!3m1!1s0x6b12ae3c03b6b333:0xab9371be57c7960e", + "width": 1080 + } + ], + "place_id": "ChIJM7O2AzyuEmsRDpbHV75xk6s", + "rating": 4.5, + "reference": "CmRSAAAAcb4JMm6XltVoAP3OtevsNh6gpF6mTaT8ZNZlIV5Tand0voxPEw_coYW-1QOixUpRRmjI66EANUn8fkyWkaQUQ-OQILx4LFBIFV5HvXFUbmGEZZ9aLPYzGNLGMVW_sYJeEhCvD1kXzQBuyZdvHHmE1PhnGhTCBrfvvxnlr6cV4pxqzlmUVAkObg", + "scope": "GOOGLE", + "types": [ + "spa", + "health", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=12363350464392566286", + "user_ratings_total": 28, + "vicinity": "51 Druitt Street, Sydney", + "website": "http://www.grandsydneythaimassage.com/" + }, + { + "address_components": [ + { + "long_name": "40", + "short_name": "40", + "types": [ + "subpremise" + ] + }, + { + "long_name": "464", + "short_name": "464", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420721124100719, + "fprint": 9121994451764812853 + }, + "formatted_address": "40/464 Kent St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9264 7965", + "geometry": { + "location": { + "lat": -33.873868, + "lng": 151.205318 + }, + "viewport": { + "northeast": { + "lat": -33.8725255697085, + "lng": 151.2066097302915 + }, + "southwest": { + "lat": -33.8752235302915, + "lng": 151.2039117697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", + "id": "6a52076c388b1bd2cf28627276719de2c9623fb9", + "international_phone_number": "+61 2 9264 7965", + "name": "Town Hall Square Newsagency", + "opening_hours": { + "minutes_until_closed": 198, + "minutes_until_open": 918, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "0600" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "0600" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "0600" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "0600" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "0600" + } + } + ], + "weekday_text": [ + "Monday: 6:00 AM – 6:00 PM", + "Tuesday: 6:00 AM – 6:00 PM", + "Wednesday: 6:00 AM – 6:00 PM", + "Thursday: 6:00 AM – 6:00 PM", + "Friday: 6:00 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "place_id": "ChIJb0pKj0SuEmsRNZyGnJ_Vl34", + "rating": 4, + "reference": "CmRRAAAAuxT80T0RRzZa5bd5GeZ08hd65nNrMXT0QFBJ4D8KaN1ptGntK7j0kEE_umRiZKdXtWkQTJnW-g3aJRsFUDYBw0cJWL4kbdU-z3xsYDgW0nMJsgUxjRf47Dq2cREtY0T8EhCa6upxV46F6sK-Q5pHWtIyGhRiBgeqv0UldOm7ID7sgpH87p60kA", + "scope": "GOOGLE", + "types": [ + "book_store", + "store", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=9121994451764812853", + "user_ratings_total": 1, + "vicinity": "40/464 Kent Street, Sydney" + }, + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420695635276725, + "fprint": 18411438857916994143 + }, + "formatted_address": "Town Hall House Level 2, 456 Kent Street, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9265 9333", + "geometry": { + "location": { + "lat": -33.873012, + "lng": 151.205815 + }, + "viewport": { + "northeast": { + "lat": -33.87166301970849, + "lng": 151.2071639802915 + }, + "southwest": { + "lat": -33.87436098029149, + "lng": 151.2044660197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/civic_building-71.png", + "id": "89c9d7494a42901b01c992bc8ed2b78d2c047c8e", + "international_phone_number": "+61 2 9265 9333", + "name": "CBD (One Stop Shop) - Town Hall House", + "place_id": "ChIJtbMJoD6uEmsRX1L34ReSgv8", + "reference": "CmRSAAAAGoQEb7UEZeoaT5gBh7auSrlRZCq8kB-_BiVl-6axncnNR2J7r0FmlrR_mNwmCep_uHqgsgGxyeTT5FpAeWxVcbEW5_Jc0ODhI-_wVRvQ1nX0VdVI0ZhgNNmxIkQLnvWKEhBLmS2lE-NT6ibXBj5TkesTGhSf0KSiweC8dWhEpKipm7nn0FOSBg", + "scope": "GOOGLE", + "types": [ + "city_hall", + "local_government_office", + "store", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=18411438857916994143", + "vicinity": "Town Hall House Level 2, 456 Kent Street, Sydney", + "website": "http://www.cityofsydney.nsw.gov.au/council/contact-us/neighbourhood-service-centres/cbd" + }, + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "F45 Training", + "feature_id": { + "cell_id": 7715420693118981999, + "fprint": 230440586011015439 + }, + "formatted_address": "Basement, 199 Castlereagh St, Sydney NSW 2000, Australia", + "formatted_phone_number": "0410 668 650", + "geometry": { + "location": { + "lat": -33.873751, + "lng": 151.209086 + }, + "viewport": { + "northeast": { + "lat": -33.8724020197085, + "lng": 151.2104353302915 + }, + "southwest": { + "lat": -33.87509998029149, + "lng": 151.2077373697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "049f7fcadbe0e7475f9aa294a7180228fccbe41b", + "international_phone_number": "+61 410 668 650", + "name": "F45 Training Sydney Town Hall", + "opening_hours": { + "minutes_until_closed": 258, + "minutes_until_open": 948, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1900" + }, + "open": { + "day": 1, + "time": "0630" + } + }, + { + "close": { + "day": 2, + "time": "1900" + }, + "open": { + "day": 2, + "time": "0630" + } + }, + { + "close": { + "day": 3, + "time": "1900" + }, + "open": { + "day": 3, + "time": "0630" + } + }, + { + "close": { + "day": 4, + "time": "1900" + }, + "open": { + "day": 4, + "time": "0630" + } + }, + { + "close": { + "day": 5, + "time": "1900" + }, + "open": { + "day": 5, + "time": "0630" + } + } + ], + "weekday_text": [ + "Monday: 6:30 AM – 7:00 PM", + "Tuesday: 6:30 AM – 7:00 PM", + "Wednesday: 6:30 AM – 7:00 PM", + "Thursday: 6:30 AM – 7:00 PM", + "Friday: 6:30 AM – 7:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 501, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114519847099110720473/photos\"\u003eF45 Training Sydney Town Hall\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAKba2gEfl8Sf2oKV52tI_Eym2x5IpP2Xz9jD2oBelFnhP7RpQu9HVse2BQ0G4hTsNf1BPA8u21_7sfhHS271BaZXnULc1j7Zzw1Z2BybSGwV-AWqFMfv4y0IytDWya7CgEhDUWfGxGO6z4Q_SQNoKRFnfGhSiAfWDVUHAan6QCusiTiYIVEQsmA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMovv0fWHLTinxHJr29T0mWvcU2-eVylBjZcjqs!2e10!4m2!3m1!1s0x6b12ae3e0a0e176f:0x332b07df704f90f", + "width": 848 + } + ], + "place_id": "ChIJbxcOCj6uEmsRD_kE932wMgM", + "rating": 4.7, + "reference": "CmRRAAAANuwDBR4jSer2nqttxGBVE5mArlQy6fj0TdwAJKvgPsxXm1g9qmW7pGpUi4W98UqzvQLmC7EullOU8AkI1OjpWOA_aupr9uyKruqeEhp8EaygKqjmDKo02jA2hSw6zM-_EhCm_g1VsF4dhjwZVgmyFMjCGhTVl2l7JcogkbWt0ty6TC8IDzPwSA", + "scope": "GOOGLE", + "types": [ + "gym", + "health", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=230440586011015439", + "user_ratings_total": 7, + "vicinity": "Basement, 199 Castlereagh St, Sydney", + "website": "http://www.f45training.com.au/townhall" + }, + { + "address_components": [ + { + "long_name": "9", + "short_name": "9", + "types": [ + "subpremise" + ] + }, + { + "long_name": "Lower Ground", + "short_name": "Lower Ground", + "types": [ + "floor" + ] + }, + { + "long_name": "The Galleries", + "short_name": "The Galleries", + "types": [ + "premise" + ] + }, + { + "long_name": "500", + "short_name": "500", + "types": [ + "street_number" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Sushi Hub", + "feature_id": { + "cell_id": 7715420695249965521, + "fprint": 15389960943525358982 + }, + "formatted_address": "The Galleries, 9/500 George St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9266 0068", + "geometry": { + "location": { + "lat": -33.872582, + "lng": 151.2074943 + }, + "viewport": { + "northeast": { + "lat": -33.8714215197085, + "lng": 151.2086294802915 + }, + "southwest": { + "lat": -33.8741194802915, + "lng": 151.2059315197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "0a44c3ea69b42bba6cdc0e9b248cebefbf890f0d", + "international_phone_number": "+61 2 9266 0068", + "name": "Sushi Hub Sydney - Townhall", + "opening_hours": { + "minutes_until_closed": 348, + "minutes_until_open": 1038, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "1900" + }, + "open": { + "day": 0, + "time": "0800" + } + }, + { + "close": { + "day": 1, + "time": "2030" + }, + "open": { + "day": 1, + "time": "0800" + } + }, + { + "close": { + "day": 2, + "time": "2030" + }, + "open": { + "day": 2, + "time": "0800" + } + }, + { + "close": { + "day": 3, + "time": "2030" + }, + "open": { + "day": 3, + "time": "0800" + } + }, + { + "close": { + "day": 4, + "time": "2030" + }, + "open": { + "day": 4, + "time": "0800" + } + }, + { + "close": { + "day": 5, + "time": "2030" + }, + "open": { + "day": 5, + "time": "0800" + } + }, + { + "close": { + "day": 6, + "time": "1900" + }, + "open": { + "day": 6, + "time": "0800" + } + } + ], + "weekday_text": [ + "Monday: 8:00 AM – 8:30 PM", + "Tuesday: 8:00 AM – 8:30 PM", + "Wednesday: 8:00 AM – 8:30 PM", + "Thursday: 8:00 AM – 8:30 PM", + "Friday: 8:00 AM – 8:30 PM", + "Saturday: 8:00 AM – 7:00 PM", + "Sunday: 8:00 AM – 7:00 PM" + ] + }, + "organizationally_part_of": { + "organization_name": "The Galeries.", + "relation_type": "INDEPENDENT_ESTABLISHMENT_IN" + }, + "photos": [ + { + "height": 414, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/103481682816108776373/photos\"\u003eSushi Hub Sydney - Townhall\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAaVK-hKD51xB1xgUDYdWDExsuwoKIEq4Dk6E3wi8RO6xqawDZPfue12_w2EVlu2WyalT51X-KafZmXJqZ1Ek8Fe8qYH277gvLkjrzeM0a_X3owcEeZA1RN4VyM0wExbICEhC-2deslyGYlkpdiShXPiUuGhSB2CEvgNQrjLLazrFkPcEqI2Tvow", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPZhG6cYhfZesROb9qxfnoGp1zlldGhHZnQYRxv!2e10!4m2!3m1!1s0x6b12ae3e891251d1:0xd5941ff049f6ed86", + "width": 414 + } + ], + "place_id": "ChIJ0VESiT6uEmsRhu32SfAflNU", + "rating": 4.4, + "reference": "CmRSAAAAPP-wn09bE-vmdcnk_I_ULRGR7GLQ-72pyx_QtCjtb60-3I5M5M27ew8HcvtyBXiYgD0_h5V7lskvK0OY3fCEAvnUGu0Bnqchf5OFABqiLyG7ylC-t9p_eZaN1CBEAemEEhCXDJ1-UYUAZlantWXUBMhuGhQ4yo-Op1zp95NTy1bYUr-IV65mEw", + "scope": "GOOGLE", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=15389960943525358982", + "user_ratings_total": 13, + "vicinity": "The Galleries, 9/500 George Street, Sydney", + "website": "http://sushihub.com.au/" + }, + { + "address_components": [ + { + "long_name": "464-480", + "short_name": "464-480", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "ANZ~ATM", + "feature_id": { + "cell_id": 7715420684689348943, + "fprint": 8866272188903742731 + }, + "formatted_address": "464-480 Kent St, Sydney NSW 2000, Australia", + "formatted_phone_number": "13 13 14", + "geometry": { + "location": { + "lat": -33.8741394, + "lng": 151.2053581 + }, + "viewport": { + "northeast": { + "lat": -33.87279666970849, + "lng": 151.2066522302915 + }, + "southwest": { + "lat": -33.87549463029149, + "lng": 151.2039542697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/atm-71.png", + "id": "158f29c8711a232784f356e22ff73acd97b69df7", + "international_phone_number": "+61 13 13 14", + "name": "ANZ ATM Sydney Town Hall Square", + "opening_hours": { + "open_now": true, + "periods": [ + { + "open": { + "day": 0, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: Open 24 hours", + "Tuesday: Open 24 hours", + "Wednesday: Open 24 hours", + "Thursday: Open 24 hours", + "Friday: Open 24 hours", + "Saturday: Open 24 hours", + "Sunday: Open 24 hours" + ] + }, + "photos": [ + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/110301895885789569512/photos\"\u003eMona Bhattacharya\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAewOLYcKKhdFwUDc4hEvk37XDxf124ltZje6zF9fRRZos2qrFvkbtikDbBAqC_xnSWb59SM7x_uGXKOu3YsIR_xvZXMhgapYjWwfAUdU3-04XBZvbjO07UYGNNusmf9J2EhAgSPFqgz5QcWVjai1u9xcfGhSYx6zrmTAVxSQuYl9Q7to0u5EhaA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipN8dqysNym8Fgx3htajWqCG-QTmCb5KtRnVgGo1!2e10!4m2!3m1!1s0x6b12ae3c139c194f:0x7b0b53948d885d0b", + "width": 1152 + } + ], + "place_id": "ChIJTxmcEzyuEmsRC12IjZRTC3s", + "rating": 4, + "reference": "CmRRAAAAToOYupHtbVZpGzuO8fufnxJtyL8WPCx2cSx3Dsq2n3usv3Sa-wd9wEIbZctIvtrRdM9KWQybStElLLgTuLHBLoS0OwvBsD-ZJ-QPVDU60BsvGCoFtvbYNBuO3GN9awxxEhDqVTv5cWEiCZN5k9rFmILTGhQElJpbVfZtKxgNfbkF28gQc8Q6VQ", + "scope": "GOOGLE", + "types": [ + "atm", + "finance", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=8866272188903742731", + "user_ratings_total": 1, + "vicinity": "464-480 Kent Street, Sydney", + "website": "http://www.locate.anz.com/anz/australia/" + }, + { + "address_components": [ + { + "long_name": "249", + "short_name": "249", + "types": [ + "street_number" + ] + }, + { + "long_name": "Oxford Street", + "short_name": "Oxford St", + "types": [ + "route" + ] + }, + { + "long_name": "Paddington", + "short_name": "Paddington", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2021", + "short_name": "2021", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420451260494513, + "fprint": 16535121132281541185 + }, + "formatted_address": "249 Oxford St, Paddington NSW 2021, Australia", + "formatted_phone_number": "(02) 9265 9189", + "geometry": { + "location": { + "lat": -33.8852, + "lng": 151.225949 + }, + "viewport": { + "northeast": { + "lat": -33.8837431697085, + "lng": 151.2273196302915 + }, + "southwest": { + "lat": -33.8864411302915, + "lng": 151.2246216697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/civic_building-71.png", + "id": "7aac1a6169d3756297fc06733ea4a4d7f82a2556", + "international_phone_number": "+61 2 9265 9189", + "name": "Paddington Town Hall", + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102651773255372730837/photos\"\u003eAndre Braun\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA6PSTtcWS-ABxZoxdAV8SQhuf3IeAMOuGwODxuqBFeedq83E8NYPQAUekuItZdJlqIoAQvcgbfxRmvbixD6Wy-vxRnF98EyJyVO6cBimqNt4CIUp1St2rJPLbs7x2IFPaEhB-dJQA_g1deKMMFneJcFfxGhQyerg06gwxuLOK0IfQqnHvhwU6gg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPDF6JvF8C735bWKPpU7BEMbgNKK2-J0-2bujHx!2e10!4m2!3m1!1s0x6b12ae05ba2a72b1:0xe5788d101fb23241", + "width": 4032 + } + ], + "place_id": "ChIJsXIqugWuEmsRQTKyHxCNeOU", + "rating": 3, + "reference": "CmRSAAAA4MuZuCNkOK4GDa-hrpbDUs8BpQ9v2Ibngv8Yf7ailoYFR22cFRXwzECm8Pch8pULrgUgi4eXJZ-KLOfX83raqSs0PwG0r49ZJl2yABFMYg5FxXRvaIFJo0ifJHV0OF8hEhAJ1IE0VSHgLY_nz35T7_4oGhR9kFG4Z4Tg_UeBEHzd-YWeCspTRA", + "scope": "GOOGLE", + "types": [ + "city_hall", + "local_government_office", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=16535121132281541185", + "user_ratings_total": 5, + "vicinity": "249 Oxford Street, Paddington", + "website": "http://www.cityofsydney.nsw.gov.au/business/venuesforhire/paddingtontownhall.asp" + }, + { + "address_components": [ + { + "long_name": "483", + "short_name": "483", + "types": [ + "street_number" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Australian Red Cross Blood Service", + "feature_id": { + "cell_id": 7715420698713797351, + "fprint": 9553799015525062778 + }, + "formatted_address": "483 George St, Sydney NSW 2000, Australia", + "formatted_phone_number": "13 14 95", + "geometry": { + "location": { + "lat": -33.8735289, + "lng": 151.2056379 + }, + "viewport": { + "northeast": { + "lat": -33.8722417197085, + "lng": 151.2076527802915 + }, + "southwest": { + "lat": -33.8749396802915, + "lng": 151.2049548197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "e0d28321b61cd57613788bbdbcee5e9ff5e016cf", + "international_phone_number": "+61 13 14 95", + "name": "Australian Red Cross Blood Service Town Hall Donor Centre", + "opening_hours": { + "minutes_until_closed": 258, + "minutes_until_open": 1008, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1900" + }, + "open": { + "day": 1, + "time": "1130" + } + }, + { + "close": { + "day": 2, + "time": "1900" + }, + "open": { + "day": 2, + "time": "0730" + } + }, + { + "close": { + "day": 3, + "time": "1900" + }, + "open": { + "day": 3, + "time": "0730" + } + }, + { + "close": { + "day": 4, + "time": "1900" + }, + "open": { + "day": 4, + "time": "0730" + } + }, + { + "close": { + "day": 5, + "time": "1630" + }, + "open": { + "day": 5, + "time": "0730" + } + }, + { + "close": { + "day": 6, + "time": "1630" + }, + "open": { + "day": 6, + "time": "0730" + } + } + ], + "weekday_text": [ + "Monday: 11:30 AM – 7:00 PM", + "Tuesday: 7:30 AM – 7:00 PM", + "Wednesday: 7:30 AM – 7:00 PM", + "Thursday: 7:30 AM – 7:00 PM", + "Friday: 7:30 AM – 4:30 PM", + "Saturday: 7:30 AM – 4:30 PM", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 2448, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117318091324923315627/photos\"\u003eGOHAR HABIB\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA8QxYBlPgJQl3mkhug61r32nR9SsrptnVhWiwMyYkmDbb-m3DKVPz9siXSWvX4-yyaFLu-rMLy5fSKoEQbQjtNT_v83Mpeq74NVhdX_6qwMp9t3O8qKln6u8l3_IvMS3QEhCIK72XaprBQhdomhogn1WUGhSxqA77rKM6DDmQ7DD8zCdPVhZlDQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNCYJaT4d-o_fI5cffGD-3_WYd5XtRgTjjALC82!2e10!4m2!3m1!1s0x6b12ae3f578832e7:0x8495e9961fd9187a", + "width": 3264 + } + ], + "place_id": "ChIJ5zKIVz-uEmsRehjZH5bplYQ", + "rating": 4.9, + "reference": "CmRSAAAAXPwTn4T_tcwObmXzpFZxSXO4YXfnQ4htEY8RderD5_dJwG3L4c5Bxx3E7OLmX2Y3BB76CDEVgQhubZ8rnzyyFKIoLWyvidsrLZSNlX1gfPxH1UnnSTFMzYeYYQ9qsXaKEhDdPo-24w2IiC3PV_h-UqE9GhR4nWds5tDcyuw88jOtXfhdrmdMwA", + "scope": "GOOGLE", + "types": [ + "health", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=9553799015525062778", + "user_ratings_total": 31, + "vicinity": "483 George Street, Sydney", + "website": "http://www.donateblood.com.au/ready-to-donate/donor-centre/Sydney-Town-Hall-Donor-Centre" + }, + { + "address_components": [ + { + "long_name": "222", + "short_name": "222", + "types": [ + "street_number" + ] + }, + { + "long_name": "Clarence Street", + "short_name": "Clarence St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Good Games", + "feature_id": { + "cell_id": 7715420696129181493, + "fprint": 14837134797731634317 + }, + "formatted_address": "222 Clarence St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9264 8185", + "geometry": { + "location": { + "lat": -33.8721801, + "lng": 151.2058957 + }, + "viewport": { + "northeast": { + "lat": -33.87083776970851, + "lng": 151.2071259302915 + }, + "southwest": { + "lat": -33.87353573029151, + "lng": 151.2044279697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", + "id": "6e33fd7f4f3996b867411490a9a935d4ac88833f", + "international_phone_number": "+61 2 9264 8185", + "name": "Good Games Town Hall", + "opening_hours": { + "minutes_until_closed": 498, + "minutes_until_open": 1218, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "1900" + }, + "open": { + "day": 0, + "time": "1100" + } + }, + { + "close": { + "day": 1, + "time": "2300" + }, + "open": { + "day": 1, + "time": "1100" + } + }, + { + "close": { + "day": 2, + "time": "2300" + }, + "open": { + "day": 2, + "time": "1100" + } + }, + { + "close": { + "day": 3, + "time": "2300" + }, + "open": { + "day": 3, + "time": "1100" + } + }, + { + "close": { + "day": 4, + "time": "2300" + }, + "open": { + "day": 4, + "time": "1100" + } + }, + { + "close": { + "day": 5, + "time": "2300" + }, + "open": { + "day": 5, + "time": "1100" + } + }, + { + "close": { + "day": 6, + "time": "1900" + }, + "open": { + "day": 6, + "time": "1100" + } + } + ], + "weekday_text": [ + "Monday: 11:00 AM – 11:00 PM", + "Tuesday: 11:00 AM – 11:00 PM", + "Wednesday: 11:00 AM – 11:00 PM", + "Thursday: 11:00 AM – 11:00 PM", + "Friday: 11:00 AM – 11:00 PM", + "Saturday: 11:00 AM – 7:00 PM", + "Sunday: 11:00 AM – 7:00 PM" + ] + }, + "photos": [ + { + "height": 720, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105235291537762659297/photos\"\u003eGood Games Town Hall\u003c/a\u003e" + ], + "photo_reference": "CmRZAAAA3AoDyGR-ZA6MGuhzYo_P9KHUQHHmEDzG4G4igwZD69_5z8LFvwj7I24Xe_aMJhkPxKn8EC3GAzHisWcYeh9WSwXqHsJ1teoKvJcliYP0fhFm3ukhDWGuF8NXYZA610VZEhDQYHKj9ZkaZY_Rul_SXJLkGhRqKl6M-MyUynVnJVtVbMP8SW5fmA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMrjeXBdqgsyRPQgOrYQcPQhPTS9F4wmjWe5KE!2e10!4m2!3m1!1s0x6b12ae3ebd7a1735:0xcde8177cef619c8d", + "width": 960 + } + ], + "place_id": "ChIJNRd6vT6uEmsRjZxh73wX6M0", + "rating": 4.3, + "reference": "CmRSAAAAV3-6gHjht5erVgoXoz_7B9LS6bPYQhXhBTZEBt9ZIGb2xwixQU0NwuwQktyrtphAmZ9Nv8XokKmkZGTz9nmW1KN6mZcQxH8k820ahRM12HzXf4AT9jUf8r9S2Uny_-yOEhBVsZvwdcvD_BH402mxzGENGhTUkTvvUWN1QSxAAqYpSsEMc2DEIQ", + "scope": "GOOGLE", + "types": [ + "store", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=14837134797731634317", + "user_ratings_total": 64, + "vicinity": "222 Clarence Street, Sydney", + "website": "http://www.goodgames.com.au/stores/nsw/sydney-town-hall.html" + }, + { + "address_components": [ + { + "long_name": "LG7", + "short_name": "LG7", + "types": [ + "subpremise" + ] + }, + { + "long_name": "The Pavilion Plaza", + "short_name": "The Pavilion Plaza", + "types": [ + "premise" + ] + }, + { + "long_name": "580", + "short_name": "580", + "types": [ + "street_number" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Flight Centre", + "feature_id": { + "cell_id": 7715420685581077157, + "fprint": 2325243829511166431 + }, + "formatted_address": "The Pavilion Plaza, LG7/580 George St, Sydney NSW 2000, Australia", + "formatted_phone_number": "1300 561 602", + "geometry": { + "location": { + "lat": -33.8750444, + "lng": 151.2069705 + }, + "viewport": { + "northeast": { + "lat": -33.8736595697085, + "lng": 151.2081565302915 + }, + "southwest": { + "lat": -33.8763575302915, + "lng": 151.2054585697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "d2fe028dd50ce88ac7036e0d127c4509a0cf10d5", + "international_phone_number": "+61 1300 561 602", + "name": "Flight Centre Townhall", + "opening_hours": { + "minutes_until_closed": 198, + "minutes_until_open": 1128, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "0930" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "0930" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "0930" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "0930" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "0930" + } + } + ], + "weekday_text": [ + "Monday: 9:30 AM – 6:00 PM", + "Tuesday: 9:30 AM – 6:00 PM", + "Wednesday: 9:30 AM – 6:00 PM", + "Thursday: 9:30 AM – 6:00 PM", + "Friday: 9:30 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "place_id": "ChIJpcrCSDyuEmsR35kuevHsRCA", + "reference": "CmRRAAAAqxlBsceErgvzQ4VQABBcLGYhchdSSuhhvc4vrf3LXZ9GSr64kL1tYMp024_opk71pXS52d4YA8jUB7cJ2psn32TGlNUfX_oCX_s4-kvIkXBqnKcrLH2fuBnLjcJgCLd1EhD6Z-c3ZP5ICb91r66_SOP_GhTtrR1LEtjW8KSdJaSgniuyxuWyIQ", + "scope": "GOOGLE", + "types": [ + "travel_agency", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=2325243829511166431", + "vicinity": "The Pavilion Plaza, LG7/580 George Street, Sydney", + "website": "http://www.flightcentre.com.au/stores/sydney/fc-townhall" + }, + { + "address_components": [ + { + "long_name": "Park Street", + "short_name": "Park St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "CALTEX WOOLWORTHS", + "feature_id": { + "cell_id": 7715420695119316643, + "fprint": 1787461144199566946 + }, + "formatted_address": "Park St & George St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 8565 9275", + "geometry": { + "location": { + "lat": -33.87329729999999, + "lng": 151.2072751 + }, + "viewport": { + "northeast": { + "lat": -33.8718275197085, + "lng": 151.2086069802915 + }, + "southwest": { + "lat": -33.8745254802915, + "lng": 151.2059090197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", + "id": "a5cad25469badfed6becad9c013d1d5b3669ec37", + "international_phone_number": "+61 2 8565 9275", + "name": "Woolworths Town Hall", + "opening_hours": { + "minutes_until_closed": 558, + "minutes_until_open": 918, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "0000" + }, + "open": { + "day": 0, + "time": "0800" + } + }, + { + "close": { + "day": 2, + "time": "0000" + }, + "open": { + "day": 1, + "time": "0600" + } + }, + { + "close": { + "day": 3, + "time": "0000" + }, + "open": { + "day": 2, + "time": "0600" + } + }, + { + "close": { + "day": 4, + "time": "0000" + }, + "open": { + "day": 3, + "time": "0600" + } + }, + { + "close": { + "day": 5, + "time": "0000" + }, + "open": { + "day": 4, + "time": "0600" + } + }, + { + "close": { + "day": 6, + "time": "0000" + }, + "open": { + "day": 5, + "time": "0600" + } + }, + { + "close": { + "day": 0, + "time": "0000" + }, + "open": { + "day": 6, + "time": "0700" + } + } + ], + "weekday_text": [ + "Monday: 6:00 AM – 12:00 AM", + "Tuesday: 6:00 AM – 12:00 AM", + "Wednesday: 6:00 AM – 12:00 AM", + "Thursday: 6:00 AM – 12:00 AM", + "Friday: 6:00 AM – 12:00 AM", + "Saturday: 7:00 AM – 12:00 AM", + "Sunday: 8:00 AM – 12:00 AM" + ] + }, + "photos": [ + { + "height": 2448, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104612929758217906468/photos\"\u003eMary Ang\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAowEWlBy1q8t_o39d2sT1sl4WVfFGK85_HFZ6HQbzfQ3pIvgnc9dlO_M05OCtKEeMOTDZ1DxyTimGMdObd2HH67ylRlRg4b0JUYkfVxWEhR2Jd4N2CrMkfmNe3mPiHRkOEhD8STUQxJJZ6eGtV88vmDuiGhRhD0eGLxPEDIimhTBNfk835cTCLw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMeW5Gg1pIwbq2blIGX8ptxnaZHPRKsiVnT38Ne!2e10!4m2!3m1!1s0x6b12ae3e8148c6a3:0x18ce5670b5d41662", + "width": 3264 + } + ], + "place_id": "ChIJo8ZIgT6uEmsRYhbUtXBWzhg", + "price_level": 1, + "rating": 3.9, + "reference": "CmRRAAAAffzIxGGgSpjohPfU9L_duSNe5ALmaP1tLw08JsLpOamSH-kmtW8VF5HPzKpi6V2xkaxOhF_FX-LQVR18mU0RInCGtMTlDwXyw7ZmF3SJisSqmWdInLDXS50Nm-bHjFJBEhAuU-DG1kkM--2x2CEnTxg0GhSppmLNhhWQRePHSd8Dr7t3eCXbfA", + "scope": "GOOGLE", + "types": [ + "grocery_or_supermarket", + "food", + "store", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=1787461144199566946", + "user_ratings_total": 1051, + "vicinity": "Park Street, Sydney", + "website": "https://www.woolworths.com.au/Shop/ShopLocator/NSW-Sydney-1248?utm_source=GooglePlaces&utm_medium=organic&utm_campaign=GooglePlaces&utm_content=NSW-Sydney-1248" + }, + { + "address_components": [ + { + "long_name": "2", + "short_name": "2", + "types": [ + "floor" + ] + }, + { + "long_name": "456", + "short_name": "456", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420684840179533, + "fprint": 17676980651909706785 + }, + "formatted_address": "2, 456 Kent St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9265 9333", + "geometry": { + "location": { + "lat": -33.87305730000001, + "lng": 151.2053195 + }, + "viewport": { + "northeast": { + "lat": -33.87171116970851, + "lng": 151.2065628802915 + }, + "southwest": { + "lat": -33.87440913029151, + "lng": 151.2038649197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/civic_building-71.png", + "id": "9e4a0becbf5311e5cc13be1ab39c0382db3e0a24", + "international_phone_number": "+61 2 9265 9333", + "name": "City of Sydney Council", + "opening_hours": { + "minutes_until_closed": 198, + "minutes_until_open": 1038, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "0800" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "0800" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "0800" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "0800" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "0800" + } + } + ], + "weekday_text": [ + "Monday: 8:00 AM – 6:00 PM", + "Tuesday: 8:00 AM – 6:00 PM", + "Wednesday: 8:00 AM – 6:00 PM", + "Thursday: 8:00 AM – 6:00 PM", + "Friday: 8:00 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 2988, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117812015317728063182/photos\"\u003eDuy Le\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAlwTMT5trVWIi0yccL6jNLbDxudrsaXxFpBpsYi62hrTFd7tdmMGB9GXm6_qjLBHQonU4k1WvgCUPobAdfld5UBCSEeaSvEwe8Xn3cu-Xt-NhgszD__y_feCYjQghNrbAEhDVWsO-YgkRJc8qAEgsl7KxGhRsWvy16BG9NPGtegbqxrAqFwX2xg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNRaU7lu5qkPoMCKuIbZMEOYta_ntvZSbeGZGy9!2e10!4m2!3m1!1s0x6b12ae3c1c99974d:0xf551403f0a87f421", + "width": 5312 + } + ], + "place_id": "ChIJTZeZHDyuEmsRIfSHCj9AUfU", + "rating": 4, + "reference": "CmRSAAAAt9nI0y0nRJXdsVs1rhx3MilOgEQC-1-6N3gS543I64pRLgcNsLg-I5KzSnya0tCscdZXbXmnQTlfH6-RjV_HhOYxJ8tkOFU9SrCHhDjPwEk-ofNwKh2NSsAPCSEXSm0qEhDsbl6KZ4Z5n3SYYEz-_-eDGhQRRm9Nh7n_eeMnJO9Eqwc-kCEjug", + "scope": "GOOGLE", + "types": [ + "city_hall", + "local_government_office", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=17676980651909706785", + "user_ratings_total": 18, + "vicinity": "2, 456 Kent Street, Sydney", + "website": "http://www.cityofsydney.nsw.gov.au/council/contact-us" + }, + { + "address_components": [ + { + "long_name": "54", + "short_name": "54", + "types": [ + "subpremise" + ] + }, + { + "long_name": "Ground", + "short_name": "Ground", + "types": [ + "floor" + ] + }, + { + "long_name": "2", + "short_name": "2", + "types": [ + "street_number" + ] + }, + { + "long_name": "Park Street", + "short_name": "Park St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Priceline Pharmacy", + "feature_id": { + "cell_id": 7715420695230974319, + "fprint": 12716525901934746596 + }, + "formatted_address": "54/2 Park St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9264 4449", + "geometry": { + "location": { + "lat": -33.8726206, + "lng": 151.2071537 + }, + "viewport": { + "northeast": { + "lat": -33.87134536970849, + "lng": 151.2084322802915 + }, + "southwest": { + "lat": -33.87404333029149, + "lng": 151.2057343197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", + "id": "4e0b84fe1bb8822cb3bb085c1f505c40a9db59f6", + "international_phone_number": "+61 2 9264 4449", + "name": "Priceline Pharmacy Town Hall", + "opening_hours": { + "minutes_until_closed": 498, + "minutes_until_open": 978, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2200" + }, + "open": { + "day": 0, + "time": "0900" + } + }, + { + "close": { + "day": 1, + "time": "2300" + }, + "open": { + "day": 1, + "time": "0700" + } + }, + { + "close": { + "day": 2, + "time": "2300" + }, + "open": { + "day": 2, + "time": "0700" + } + }, + { + "close": { + "day": 3, + "time": "2300" + }, + "open": { + "day": 3, + "time": "0700" + } + }, + { + "close": { + "day": 4, + "time": "2300" + }, + "open": { + "day": 4, + "time": "0700" + } + }, + { + "close": { + "day": 5, + "time": "2300" + }, + "open": { + "day": 5, + "time": "0700" + } + }, + { + "close": { + "day": 6, + "time": "2200" + }, + "open": { + "day": 6, + "time": "0900" + } + } + ], + "weekday_text": [ + "Monday: 7:00 AM – 11:00 PM", + "Tuesday: 7:00 AM – 11:00 PM", + "Wednesday: 7:00 AM – 11:00 PM", + "Thursday: 7:00 AM – 11:00 PM", + "Friday: 7:00 AM – 11:00 PM", + "Saturday: 9:00 AM – 10:00 PM", + "Sunday: 9:00 AM – 10:00 PM" + ] + }, + "photos": [ + { + "height": 3000, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102175818355207625041/photos\"\u003eKeNn-E Su\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAZlwSdisO_5itnG7brHLmWHJ0cb0Xu40mAkFHkqkrkRxfZfoz49DTf7awjEiFwKR8JI22krjeSmhPys9QgoZZs4NusFBZg57PXiDYahIl-SGbL5mXu1GzFVruLtouVMnmEhCze0EF-eO8j4KPC5ievVgvGhQL4Jl_o8jzBkYnDYri2_3FghEN4Q", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPlwGyGZZA6tJfGkei1g79qPqXeI-MXLi9VAnaW!2e10!4m2!3m1!1s0x6b12ae3e87f0896f:0xb07a2ceec9dfdbe4", + "width": 4000 + } + ], + "place_id": "ChIJb4nwhz6uEmsR5Nvfye4serA", + "rating": 4.2, + "reference": "CmRSAAAAlcl_pRczmQpVkCWhs6_8nzXc1APMRB29acMlLivEghariwLC_2PNb6jKdIup1hYl5LPpB6td87_SgqfgpHSi1r_pvxNannd8eSICWza5ghDJfuJ3PrkcfkVuJwk4eILOEhBQxSzugBmVodNer5t8qRBZGhRimK7FFtpblzbA_Lyi6_78ycaT0Q", + "scope": "GOOGLE", + "types": [ + "pharmacy", + "health", + "store", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=12716525901934746596", + "user_ratings_total": 20, + "vicinity": "54/2 Park Street, Sydney", + "website": "https://www.priceline.com.au/" + }, + { + "address_components": [ + { + "long_name": "464-484", + "short_name": "464-484", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420686127899977, + "fprint": 11745464685120671652 + }, + "formatted_address": "464-484 Kent St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9265 1649", + "geometry": { + "location": { + "lat": -33.8738198, + "lng": 151.2053651 + }, + "viewport": { + "northeast": { + "lat": -33.8724782697085, + "lng": 151.2066247802915 + }, + "southwest": { + "lat": -33.8751762302915, + "lng": 151.2039268197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", + "id": "ccf875ae6c5d6f4f3e070736322cfa57e8067d1f", + "international_phone_number": "+61 2 9265 1649", + "name": "Town Hall Square", + "opening_hours": { + "minutes_until_closed": 318, + "minutes_until_open": 888, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "1700" + }, + "open": { + "day": 0, + "time": "0800" + } + }, + { + "close": { + "day": 1, + "time": "2000" + }, + "open": { + "day": 1, + "time": "0530" + } + }, + { + "close": { + "day": 2, + "time": "2000" + }, + "open": { + "day": 2, + "time": "0530" + } + }, + { + "close": { + "day": 3, + "time": "2000" + }, + "open": { + "day": 3, + "time": "0530" + } + }, + { + "close": { + "day": 4, + "time": "2100" + }, + "open": { + "day": 4, + "time": "0530" + } + }, + { + "close": { + "day": 5, + "time": "2000" + }, + "open": { + "day": 5, + "time": "0530" + } + }, + { + "close": { + "day": 6, + "time": "1800" + }, + "open": { + "day": 6, + "time": "0530" + } + } + ], + "weekday_text": [ + "Monday: 5:30 AM – 8:00 PM", + "Tuesday: 5:30 AM – 8:00 PM", + "Wednesday: 5:30 AM – 8:00 PM", + "Thursday: 5:30 AM – 9:00 PM", + "Friday: 5:30 AM – 8:00 PM", + "Saturday: 5:30 AM – 6:00 PM", + "Sunday: 8:00 AM – 5:00 PM" + ] + }, + "photos": [ + { + "height": 2448, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/118410684014024830328/photos\"\u003eMichael Mak\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAsmqPdibizAb5VeMw14WSLMfadFK1P58rvJ8aWhigAAQGeIP7Rnc9kR10i2gysFCXO69XNEzJxMDu_j1NKM3QU9TKo252QiCzr1JSDITOKSIuvtaRyL1hmDQg_H0FZ3vfEhD_4SW6fg-uen0R6nmNpEF4GhTttTFWSuRgWcfB9mVbHcZkmH_Kpg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPfknViket9D_elosdTzXhFZv8ivlxlBd9qgrMv!2e10!4m2!3m1!1s0x6b12ae3c695aa549:0xa30045e6a6834fa4", + "width": 3264 + } + ], + "place_id": "ChIJSaVaaTyuEmsRpE-DpuZFAKM", + "rating": 3.8, + "reference": "CmRSAAAAAntlQMQXdtMswOqGZnNTjPcUZOC6URfoRnB8EPsqaoaFsf65mf47bdBuRpSsnPVh856CpguBES51bInPnD4yjLA-xTOOBhC226jcBIoZN31cdNewVUJz7hgFYWChwfqMEhCgjMt7-gdj1rcW2GPsZ05GGhQMt5bpUEmMnlFqGBgcQvbdIMXCWQ", + "scope": "GOOGLE", + "types": [ + "shopping_mall", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=11745464685120671652", + "user_ratings_total": 69, + "vicinity": "464-484 Kent Street, Sydney", + "website": "http://townhallsquare.com.au/" + }, + { + "address_components": [ + { + "long_name": "511", + "short_name": "511", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420686275623337, + "fprint": 18339099179663411492 + }, + "formatted_address": "511 Kent St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9274 0000", + "geometry": { + "location": { + "lat": -33.8744027, + "lng": 151.2049375 + }, + "viewport": { + "northeast": { + "lat": -33.87295281970849, + "lng": 151.2064486802915 + }, + "southwest": { + "lat": -33.87565078029149, + "lng": 151.2037507197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "6f096869d159216392237631d486f7744fd08d58", + "international_phone_number": "+61 2 9274 0000", + "name": "Adina Apartment Hotel Sydney Town Hall", + "opening_hours": { + "open_now": true, + "periods": [ + { + "open": { + "day": 0, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: Open 24 hours", + "Tuesday: Open 24 hours", + "Wednesday: Open 24 hours", + "Thursday: Open 24 hours", + "Friday: Open 24 hours", + "Saturday: Open 24 hours", + "Sunday: Open 24 hours" + ] + }, + "photos": [ + { + "height": 2988, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105678700795820702645/photos\"\u003eDerek McDonald\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAZKFS5N1Ul0hzf-NXo3RoPumDpcE4aT3Bd1yHt42Qc_z3iPrUFvt-Sc-2YBwuZG8S0LkZGUArWyA_uG03oAAY7J24pbj7ObA4ITcYmio4EK-B40WJV2kNiOOFsOZC-TAIEhCTgUAXrTsOqnVIRrogn_NgGhTnKLMuAbgfrjJLlThQ7wBWkI0MvQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMzUwmg68_pFwOCNxR5tv5zQFzmYch7NCVar1VG!2e10!4m2!3m1!1s0x6b12ae3c7228b9a9:0xfe81918a087ec924", + "width": 5312 + } + ], + "place_id": "ChIJqbkocjyuEmsRJMl-CIqRgf4", + "rating": 4, + "reference": "CmRSAAAAYSZ9xhTKELqe8w3FYNNw5sKGZIaWdxmImRqhHDTW-kiOk0I4XWkHpweGeREz-VLINJ_mY9PCk51lCMaxrTFo9EobB7N26YTmuh6rx4Hed5nL9NrKwBATBzQ5O_P1RacZEhA_QzEG_VuiqOXP9ShwfDQ_GhTjl6WtyeIjur5dLsxy-9t_pnpIlQ", + "scope": "GOOGLE", + "types": [ + "lodging", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=18339099179663411492", + "user_ratings_total": 127, + "vicinity": "511 Kent Street, Sydney", + "website": "https://www.adinahotels.com/hotel/sydney-town-hall/?utm_source=googleplaces&utm_medium=organic&utm_campaign=googleplaces" + }, + { + "address_components": [ + { + "long_name": "500", + "short_name": "500", + "types": [ + "street_number" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "GNC", + "feature_id": { + "cell_id": 7715420694558424135, + "fprint": 68920311763565993 + }, + "formatted_address": "500 George St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9264 4835", + "geometry": { + "location": { + "lat": -33.8724694, + "lng": 151.2071085 + }, + "viewport": { + "northeast": { + "lat": -33.8711217697085, + "lng": 151.2084047802915 + }, + "southwest": { + "lat": -33.87381973029149, + "lng": 151.2057068197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/shopping-71.png", + "id": "7802f6e3326e516c59eb309d48f13fb2e14540cb", + "international_phone_number": "+61 2 9264 4835", + "name": "GNC Town Hall", + "opening_hours": { + "minutes_until_closed": 258, + "minutes_until_open": 1068, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "1800" + }, + "open": { + "day": 0, + "time": "1000" + } + }, + { + "close": { + "day": 1, + "time": "1900" + }, + "open": { + "day": 1, + "time": "0830" + } + }, + { + "close": { + "day": 2, + "time": "1900" + }, + "open": { + "day": 2, + "time": "0830" + } + }, + { + "close": { + "day": 3, + "time": "1900" + }, + "open": { + "day": 3, + "time": "0830" + } + }, + { + "close": { + "day": 4, + "time": "2100" + }, + "open": { + "day": 4, + "time": "0830" + } + }, + { + "close": { + "day": 5, + "time": "1900" + }, + "open": { + "day": 5, + "time": "0830" + } + }, + { + "close": { + "day": 6, + "time": "1800" + }, + "open": { + "day": 6, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 8:30 AM – 7:00 PM", + "Tuesday: 8:30 AM – 7:00 PM", + "Wednesday: 8:30 AM – 7:00 PM", + "Thursday: 8:30 AM – 9:00 PM", + "Friday: 8:30 AM – 7:00 PM", + "Saturday: 10:00 AM – 6:00 PM", + "Sunday: 10:00 AM – 6:00 PM" + ] + }, + "photos": [ + { + "height": 2988, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113209558221010196493/photos\"\u003e林佑軒\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAARFYHvirzUrS_w7VDh31lHWlTvx5Md8scY3IoYhhS6m2e40Uff2bxWKFqc_qpu0SiDMgjk8HLJzBaXAkxGnhUDv1d-PR-B2FdQ6-KoXwEdTR9UODDKLfhYpi2d0YO6V8wEhCH6sz2uEYWe5op-sNJJIl4GhTyiEFt3r0Duz0kFyUETMRagtUshQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOob75n6K5-pIbyZ_hvMTnttuyrW7qKG6Nsg224!2e10!4m2!3m1!1s0x6b12ae3e5fda3c47:0xf4daa88c79fda9", + "width": 5312 + } + ], + "place_id": "ChIJRzzaXz6uEmsRqf15jKja9AA", + "rating": 2.8, + "reference": "ClRQAAAAy2jMlQpMkEa7yDLC9KTP4SNSqb43CXyepwozc4lXAaYVsrxXUUdLoi19cdZoSDIENU2tTTFMS519DIa0UxVhfY8rnkain3BdpO6z9gWxE34SEKqDQBa7m9tYP7tQl8xFZV8aFNQYW1HSs2SuQgtWE3ZRgynK0l5w", + "scope": "GOOGLE", + "types": [ + "health", + "food", + "store", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=68920311763565993", + "user_ratings_total": 11, + "vicinity": "500 George Street, Sydney", + "website": "http://www.gnclivewell.com.au/" + }, + { + "address_components": [ + { + "long_name": "2", + "short_name": "2", + "types": [ + "floor" + ] + }, + { + "long_name": "456", + "short_name": "456", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420684834279907, + "fprint": 2093538203311503502 + }, + "formatted_address": "2, 456 Kent St, Sydney NSW 2000, Australia", + "geometry": { + "location": { + "lat": -33.8731576, + "lng": 151.2054177 + }, + "viewport": { + "northeast": { + "lat": -33.8718086197085, + "lng": 151.2067666802915 + }, + "southwest": { + "lat": -33.8745065802915, + "lng": 151.2040687197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/civic_building-71.png", + "id": "5d57ad63464c839653aa3987b980c8ac938f54e0", + "name": "Sydney Town Hall", + "place_id": "ChIJ45E_HDyuEmsRjngXZOe9DR0", + "reference": "CmRRAAAAzcIUYSDzRDGtgq9PC_8S7X_hvs8hrimNFVF4FV5mnLVS--nR_ELBpVO8IJRkPlP1VWWSvRO9R6FQWqUmIta6-QlIwO1NAdG0we3yClDHhmw6NdvQDRH55iyPEQril6OSEhDN5PUqNZoMJ2-P78BpRaZ0GhSd5TgKxcKAsO89oKiWBwyv_BFn0Q", + "scope": "GOOGLE", + "types": [ + "city_hall", + "local_government_office", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=2093538203311503502", + "vicinity": "2, 456 Kent Street, Sydney", + "website": "http://www.cityofsydney.nsw.gov.au/" + }, + { + "address_components": [ + { + "long_name": "74-80", + "short_name": "74-80", + "types": [ + "street_number" + ] + }, + { + "long_name": "Druitt Street", + "short_name": "Druitt St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420695573543373, + "fprint": 1444377670171227455 + }, + "formatted_address": "74-80 Druitt St, Sydney NSW 2000, Australia", + "geometry": { + "location": { + "lat": -33.8729527, + "lng": 151.2063902 + }, + "viewport": { + "northeast": { + "lat": -33.8715179697085, + "lng": 151.2077514802915 + }, + "southwest": { + "lat": -33.8742159302915, + "lng": 151.2050535197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "6991628fc598ad4e2bc3691b2e74c92f6dce8943", + "name": "Sydney Film Festival Hub, Lower Town Hall", + "photos": [ + { + "height": 2988, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107607828318729496500/photos\"\u003eJulia Jeong\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA9sSdyvChqCFwCbl4vfwkjC4l1_O1vTzl0hVQ-X0d30_wNyidy6Bs9rtjU9ofatHeRcGMsMPmn7yHaIO4bX8qFOBc8EkkZ9W1U6z3HtXhDF-Xn21_riCMOSUy75QxV7wNEhCeLhRT6BhgNh9zjseSdzWUGhTnj1afEIKWw6KJwsXGoRccWO2sdw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPmL1TvPzR495L_2_8NtjyOAgO6Rc-ZB5dOo3Au!2e10!4m2!3m1!1s0x6b12ae3e9c5bb9cd:0x140b75d6a00ff53f", + "width": 5312 + } + ], + "place_id": "ChIJzblbnD6uEmsRP_UPoNZ1CxQ", + "reference": "CmRRAAAAAa4fwKpND_NcqW6mUOtgjZt0QgLVzKV1VwRxJIcGHukRwfomxrZwa0-S4TJk1GvHykJ7cgq4q7G8ewaQq3znPwp8uSffzjzh1pbg1bt8S2DhBMKcm8rGDQPs73K5F130EhDmY8K1ngTGE7yNcSdG2Nd4GhRpzzQv1DfMyAbbuHwYkhu_46ErOw", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=1444377670171227455", + "vicinity": "74-80 Druitt Street, Sydney" + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByTypeResponse.json b/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByTypeResponse.json new file mode 100644 index 000000000..a6a559be2 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiNearbySearchRequestByTypeResponse.json @@ -0,0 +1,3569 @@ +{ + "html_attributions": [], + "next_page_token": "CuQCWAEAAA56gE71BU8GXc0u_DzFlv0Zg56II5Pw52VVsDWAYmqbDB3veboWKzv51x1tMAkbi6pJ27czIjdJig96WTiTcbWydHlALEdlq6e6bbf-RdswGfF3DeAmWMaoq1VZC-2ADQg9k-DM9xUVC0_wPxICGklNgKGH1cbzsMXE5Q-lHJyUccSss-0IRU892-UqdzTcfhi52bZI-rHNrbtNZcVOWw-p8AesBIOWcMFKdPxNuBpyzQ-e_3CYWDVNl2wfOdyRLojof2uC82dgtIgSQmzVlVcYChx_CZeL-bJUTeyt-vnQSHgoJ7MP3qWFE7eKbyVlokYG_KSBXbaD7cn1oRlbA3g21DC5RhLjYHW_Vv4hQxdXLn9yiT5SxPlbKvmG5lyTocTUJ_WqaX9W0dtwKdZviPNMqf_0uHELY6kO2_alQJyjMQ8dx5DVHY5Dl2-18RbyQmTyqJSDvc2DjsLWVjIu934SEEXRsnhOQewV2jaUFLESf4caFFv_UpvPxnMsgiMYnANp556zu1aO", + "results": [ + { + "address_components": [ + { + "long_name": "30", + "short_name": "30", + "types": [ + "street_number" + ] + }, + { + "long_name": "Pitt Street", + "short_name": "Pitt St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Marriott Hotel", + "feature_id": { + "cell_id": 7715420710454617867, + "fprint": 5129266607156806397 + }, + "formatted_address": "30 Pitt St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9259 7000", + "geometry": { + "location": { + "lat": -33.8627261, + "lng": 151.2092998 + }, + "viewport": { + "northeast": { + "lat": -33.86134276970849, + "lng": 151.2106342302915 + }, + "southwest": { + "lat": -33.86404073029149, + "lng": 151.2079362697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "cdb60594ed0359f5e9c97a5b71a6e9f2fe02aef7", + "international_phone_number": "+61 2 9259 7000", + "name": "Sydney Harbour Marriott Hotel at Circular Quay", + "opening_hours": { + "open_now": true, + "periods": [ + { + "open": { + "day": 0, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: Open 24 hours", + "Tuesday: Open 24 hours", + "Wednesday: Open 24 hours", + "Thursday: Open 24 hours", + "Friday: Open 24 hours", + "Saturday: Open 24 hours", + "Sunday: Open 24 hours" + ] + }, + "photos": [ + { + "height": 423, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/109721033836136604114/photos\"\u003eSydney Harbour Marriott Hotel at Circular Quay\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAIcFD-TLcVdc7B7VjRM5woRZcBlYG3gQy66jI1QsNERHhiBt3LegAQxWOinBKbPtTFcauRz7gzBoHHH_kUCfjFOfF_chu3AaT1zobRBwHHKCNaovJ99at3iUAS0PJWnXzEhD7wUOgQQohnOi2D9MKnNArGhQd99F_XQocBGczEvEgaiKIY1MWzQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOjZI7gl85wQJfXLm_sXOPpm9QcwkeOfNJ-nvCR!2e10!4m2!3m1!1s0x6b12ae421356e70b:0x472ed0cd9ddac2fd", + "width": 610 + } + ], + "place_id": "ChIJC-dWE0KuEmsR_cLanc3QLkc", + "rating": 4.2, + "reference": "CmRRAAAAoeiDfxGrdimTh1tQeQTKT7kpYgiKNlQ-8_4QUs32bUmcg5BTV12meUPYFXvyl8Ei48LqoLcYqfpbWd0SV2JKERk9RU0KUXtdZdwtuREqOzGA3Jeqy_U84ePkQw_DQKxeEhBwJBjEvtVC-hBRnxW5A5VuGhSG3_d7Z9h8ofOWTbLUGv3i-V6h9w", + "scope": "GOOGLE", + "types": [ + "bar", + "lodging", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=5129266607156806397", + "user_ratings_total": 563, + "vicinity": "30 Pitt Street, Sydney", + "website": "http://www.marriott.com/hotels/travel/sydmc-sydney-harbour-marriott-hotel-at-circular-quay/?scid=bb1a189a-fec3-4d19-a255-54ba596febe2" + }, + { + "address_components": [ + { + "long_name": "477", + "short_name": "477", + "types": [ + "street_number" + ] + }, + { + "long_name": "Kent Street", + "short_name": "Kent St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Base Backpackers", + "feature_id": { + "cell_id": 7715420684444172843, + "fprint": 8735310580697617209 + }, + "formatted_address": "477 Kent St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9267 7718", + "geometry": { + "location": { + "lat": -33.8732478, + "lng": 151.2050152 + }, + "viewport": { + "northeast": { + "lat": -33.8718877197085, + "lng": 151.2064145802915 + }, + "southwest": { + "lat": -33.87458568029149, + "lng": 151.2037166197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "fde7d75c4c57c442520011b2cf930039d47f244e", + "international_phone_number": "+61 2 9267 7718", + "name": "Base Backpackers Sydney", + "opening_hours": { + "minutes_until_closed": 524, + "minutes_until_open": 554, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2330" + }, + "open": { + "day": 0, + "time": "0000" + } + }, + { + "close": { + "day": 1, + "time": "2330" + }, + "open": { + "day": 1, + "time": "0000" + } + }, + { + "close": { + "day": 2, + "time": "2330" + }, + "open": { + "day": 2, + "time": "0000" + } + }, + { + "close": { + "day": 3, + "time": "2330" + }, + "open": { + "day": 3, + "time": "0000" + } + }, + { + "close": { + "day": 4, + "time": "2330" + }, + "open": { + "day": 4, + "time": "0000" + } + }, + { + "close": { + "day": 5, + "time": "2330" + }, + "open": { + "day": 5, + "time": "0000" + } + }, + { + "close": { + "day": 6, + "time": "2330" + }, + "open": { + "day": 6, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: 12:00 AM – 11:30 PM", + "Tuesday: 12:00 AM – 11:30 PM", + "Wednesday: 12:00 AM – 11:30 PM", + "Thursday: 12:00 AM – 11:30 PM", + "Friday: 12:00 AM – 11:30 PM", + "Saturday: 12:00 AM – 11:30 PM", + "Sunday: 12:00 AM – 11:30 PM" + ] + }, + "photos": [ + { + "height": 1536, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104225749932694313958/photos\"\u003eBase Backpackers Sydney\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAAgUDI53LxUfFnepk13C_PDq-u37PtsnXFT_RnFulkMgl7O2noJrEs9u_d7YQKHN2EqtOIF021-rZj0gXG9oh6Tlc_syNLTodNseGwC9lb4ANrlV1lvGwS-_4keLfyQynEhBfHQHuP1Wb5ymb38jnVlYEGhSs1aFMyOYD5jtNrS9vQwaAWAZF_w", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMtlVRxYMY3cheEfy2i6Yz22NQ8FOp5dXQMm887!2e10!4m2!3m1!1s0x6b12ae3c04ff022b:0x793a0eb105315339", + "width": 2048 + } + ], + "place_id": "ChIJKwL_BDyuEmsROVMxBbEOOnk", + "rating": 3.3, + "reference": "CmRRAAAAPKgmTFE3gLqkTNmAGEUCVTWKHMzbIqRxNpDCktnzgCXpaLCfCd3OIm1PQOeU95aXmDWMecHXK7a0dLqaw6BmlssWuQEO9jSICF6UaBh0j1AhZXPleCB948J6nyR6KXD1EhBWXN4-TeARmKh7_zoWRonZGhTshZus2FORLVkDJqv_xP0Vpw8c_Q", + "scope": "GOOGLE", + "types": [ + "travel_agency", + "bar", + "lodging", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=8735310580697617209", + "user_ratings_total": 153, + "vicinity": "477 Kent Street, Sydney", + "website": "http://www.stayatbase.com/hostels/australia-hostels/base-backpackers-sydney" + }, + { + "address_components": [ + { + "long_name": "6", + "short_name": "6", + "types": [ + "street_number" + ] + }, + { + "long_name": "Cowper Wharf Roadway", + "short_name": "Cowper Wharf Roadway", + "types": [ + "route" + ] + }, + { + "long_name": "Woolloomooloo", + "short_name": "Woolloomooloo", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2011", + "short_name": "2011", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420897503358535, + "fprint": 2143790120205999645 + }, + "formatted_address": "6 Cowper Wharf Roadway, Woolloomooloo NSW 2011, Australia", + "formatted_phone_number": "(02) 9331 9000", + "geometry": { + "location": { + "lat": -33.8689554, + "lng": 151.2201926 + }, + "viewport": { + "northeast": { + "lat": -33.8677380697085, + "lng": 151.2215171302915 + }, + "southwest": { + "lat": -33.8704360302915, + "lng": 151.2188191697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "0ff14c42a0455315ad1f73e5371086b059a6543d", + "international_phone_number": "+61 2 9331 9000", + "name": "Ovolo Woolloomooloo", + "photos": [ + { + "height": 1359, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105085817118594198788/photos\"\u003eOvolo Woolloomooloo\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAANRCtv1wEkpZx3-KYZbbux7mjFlQ3pNj-wT17jTsrFQrAbfBG7Bp3Hai4hLqvtDDGVwDJM7GJuj_pqx9m0SLFmxqC8GZ8zQSEoiS5asW8nOI4LzEN7TdoCzr7hyDrN_gmEhBSRZrGBSz-WyWwcUM883TTGhQRT0v8qtvhpstoZs9fQy0XhxS6xw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMGYZofZsrmscIXeDRnK2-1-dbbor_IdyFqsJDj!2e10!4m2!3m1!1s0x6b12ae6da0502e47:0x1dc045c18bdede1d", + "width": 1920 + } + ], + "place_id": "ChIJRy5QoG2uEmsRHd7ei8FFwB0", + "price_level": 2, + "rating": 4.6, + "reference": "CmRRAAAABgmCKbjuBM8eg4ydAgTk-CmHUT9moxjjQiS4IW5--sWuOTrEInGVEjjehBh6x8GtyuFOGwJ5arnFqVyq4PEtgvbYity3gdc2Ltv1zO8kBivMwurnlDmH65Upa2WYkXnQEhCPqsGya3dn6NDIIUAmMo4UGhSLij3V6y2FBsNgjTXPxrTshykDNw", + "scope": "GOOGLE", + "types": [ + "bar", + "lodging", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=2143790120205999645", + "user_ratings_total": 185, + "vicinity": "6 Cowper Wharf Roadway, Woolloomooloo", + "website": "http://www.ovolohotels.com.au/ovolowoolloomooloo/" + }, + { + "address_components": [ + { + "long_name": "509", + "short_name": "509", + "types": [ + "street_number" + ] + }, + { + "long_name": "Pitt Street", + "short_name": "Pitt St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420581270191831, + "fprint": 10980358689643393875 + }, + "formatted_address": "509 Pitt St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9288 7888", + "geometry": { + "location": { + "lat": -33.88239, + "lng": 151.2047169 + }, + "viewport": { + "northeast": { + "lat": -33.8810728697085, + "lng": 151.2059822802915 + }, + "southwest": { + "lat": -33.8837708302915, + "lng": 151.2032843197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "63a162ae0b1e9cbba3b74c2038eb356d139fb62a", + "international_phone_number": "+61 2 9288 7888", + "name": "Wake Up! Sydney", + "opening_hours": { + "open_now": true, + "periods": [ + { + "open": { + "day": 0, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: Open 24 hours", + "Tuesday: Open 24 hours", + "Wednesday: Open 24 hours", + "Thursday: Open 24 hours", + "Friday: Open 24 hours", + "Saturday: Open 24 hours", + "Sunday: Open 24 hours" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113381452924465106507/photos\"\u003eWake Up! Sydney\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA2RdJ5PbLvCOuvL5Pf0gHtQenXLNSPlrgOBoRQ5oYo9NlVSmLwj9lQtyDyKUrNuhROygz_2qjkHpixWOCRt1M7gTgNlsgnJrh_zjgcpzuzWl-UUfzq3QgM-NNJ2mMwRabEhDdK8E4VgPzskuYhV6eZdZCGhQUQWM6upuNecfheNVTEQFkamIadQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOnQR5c3-mNe9QxuRHGDrRwMcFP1a20ycTP-JJK!2e10!4m2!3m1!1s0x6b12ae23ff58fed7:0x9862120d42917353", + "width": 2048 + } + ], + "place_id": "ChIJ1_5Y_yOuEmsRU3ORQg0SYpg", + "rating": 4.2, + "reference": "CmRSAAAAE6jUaknNFKZaSbgQ-A4va6fQ3LdU1Okoe6BtfxA0yYxAP6f-UsOEnuSzOKM0Esy4s86ujFPzs3ArWzsBWlWcmQSIbr5SQSyEyi85Uzwqgf6no5kzTz8KZsc8aEbrxissEhB-aM68vsiWNy1GO-I10jH0GhROO8Y6Qq7V39WicH7zAEEDwy0a9A", + "scope": "GOOGLE", + "types": [ + "cafe", + "travel_agency", + "bar", + "night_club", + "lodging", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=10980358689643393875", + "user_ratings_total": 252, + "vicinity": "509 Pitt Street, Sydney", + "website": "http://www.wakeup.com.au/" + }, + { + "address_components": [ + { + "long_name": "86-88", + "short_name": "86-88", + "types": [ + "street_number" + ] + }, + { + "long_name": "Chalmers Street", + "short_name": "Chalmers St", + "types": [ + "route" + ] + }, + { + "long_name": "Surry Hills", + "short_name": "Surry Hills", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2010", + "short_name": "2010", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420568432158565, + "fprint": 16902949189742531958 + }, + "formatted_address": "86-88 Chalmers St, Surry Hills NSW 2010, Australia", + "formatted_phone_number": "(02) 9698 2607", + "geometry": { + "location": { + "lat": -33.8859288, + "lng": 151.2070255 + }, + "viewport": { + "northeast": { + "lat": -33.8845700197085, + "lng": 151.2083120302915 + }, + "southwest": { + "lat": -33.8872679802915, + "lng": 151.2056140697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "b01d0ed59fc7430ad54ec2591f56eb626157384b", + "international_phone_number": "+61 2 9698 2607", + "name": "Royal Exhibition Hotel", + "opening_hours": { + "minutes_until_closed": 794, + "minutes_until_open": 1154, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2200" + }, + "open": { + "day": 0, + "time": "1000" + } + }, + { + "close": { + "day": 2, + "time": "0400" + }, + "open": { + "day": 1, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "0400" + }, + "open": { + "day": 2, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "0400" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "0400" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 6, + "time": "0600" + }, + "open": { + "day": 5, + "time": "1000" + } + }, + { + "close": { + "day": 6, + "time": "1700" + }, + "open": { + "day": 6, + "time": "0900" + } + } + ], + "weekday_text": [ + "Monday: 10:00 AM – 4:00 AM", + "Tuesday: 10:00 AM – 4:00 AM", + "Wednesday: 10:00 AM – 4:00 AM", + "Thursday: 10:00 AM – 4:00 AM", + "Friday: 10:00 AM – 6:00 AM", + "Saturday: 9:00 AM – 5:00 PM", + "Sunday: 10:00 AM – 10:00 PM" + ] + }, + "photos": [ + { + "height": 480, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113719639442868633401/photos\"\u003eAshley Hughes\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA8d2sZFbd6iZMsdhRmvpB6wKMhXmC7pIVR5QqzYPAKI7tLRpqDaKtiQxpc8HIQOjxtPFYjArZZ3D5FaiGJA3OEQwk-RcQ7cPfq25BGfaodwlj1XjiHa3fvvKL7hU7Rxw6EhDBRP9Q_zJgsen98tKFZER9GhS28_MosoOWyr0p0rDALtOWeUwZQw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOQ9n07pT1XvXi5jpkieZSEXGyDXD_bmrbN3otV!2e10!4m2!3m1!1s0x6b12ae2102242765:0xea9356bb7f149576", + "width": 640 + } + ], + "place_id": "ChIJZSckAiGuEmsRdpUUf7tWk-o", + "rating": 3.7, + "reference": "CmRSAAAAM3ZSjf-uAGXeV17y3D7SrGljEEhU6Ie3g-wesRdZy0sMQOL_0YkdyQggBUvxI0aPnT5NT5xufLVF163MnzAS8pv2G7D2TeLnfvIm7qPPvEvFe0kYvIW8tAK212d0RMCKEhDUJpwJvt5aUGnHCTxADyuJGhQsz2u6Jq_JSoRw9HUB6aT2TkZfpg", + "scope": "GOOGLE", + "types": [ + "bar", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=16902949189742531958", + "user_ratings_total": 89, + "vicinity": "86-88 Chalmers Street, Surry Hills", + "website": "http://www.royalexhibition.com.au/" + }, + { + "address_components": [ + { + "long_name": "1", + "short_name": "1", + "types": [ + "street_number" + ] + }, + { + "long_name": "Military Road", + "short_name": "Military Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Watsons Bay", + "short_name": "Watsons Bay", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Woollahra Municipal Council", + "short_name": "Woollahra", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2030", + "short_name": "2030", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715418821581644419, + "fprint": 3842461470516305267 + }, + "formatted_address": "1 Military Rd, Watsons Bay NSW 2030, Australia", + "formatted_phone_number": "(02) 9337 5444", + "geometry": { + "location": { + "lat": -33.8431192, + "lng": 151.2820875 + }, + "viewport": { + "northeast": { + "lat": -33.8417136697085, + "lng": 151.2837461302915 + }, + "southwest": { + "lat": -33.8444116302915, + "lng": 151.2810481697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "34af093bb04059be0ace459025df59a37e508eba", + "international_phone_number": "+61 2 9337 5444", + "name": "Watsons Bay Boutique Hotel", + "opening_hours": { + "minutes_until_closed": 554, + "minutes_until_open": 974, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2200" + }, + "open": { + "day": 0, + "time": "0700" + } + }, + { + "close": { + "day": 2, + "time": "0000" + }, + "open": { + "day": 1, + "time": "0700" + } + }, + { + "close": { + "day": 3, + "time": "0000" + }, + "open": { + "day": 2, + "time": "0700" + } + }, + { + "close": { + "day": 4, + "time": "0000" + }, + "open": { + "day": 3, + "time": "0700" + } + }, + { + "close": { + "day": 5, + "time": "0000" + }, + "open": { + "day": 4, + "time": "0700" + } + }, + { + "close": { + "day": 6, + "time": "0000" + }, + "open": { + "day": 5, + "time": "0700" + } + }, + { + "close": { + "day": 0, + "time": "0000" + }, + "open": { + "day": 6, + "time": "0700" + } + } + ], + "weekday_text": [ + "Monday: 7:00 AM – 12:00 AM", + "Tuesday: 7:00 AM – 12:00 AM", + "Wednesday: 7:00 AM – 12:00 AM", + "Thursday: 7:00 AM – 12:00 AM", + "Friday: 7:00 AM – 12:00 AM", + "Saturday: 7:00 AM – 12:00 AM", + "Sunday: 7:00 AM – 10:00 PM" + ] + }, + "photos": [ + { + "height": 3840, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/103456854558716153529/photos\"\u003eWatsons Bay Boutique Hotel\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAGi1s-Xc6UFKSRdcKoPxw9VOD7qfMia4Dqkai7-sXl3LHzY2CVb8Oijwdok6NqvUi-RXmYDuzEJ9KTcu2s2_lx7_dQ79EdxEBvxSNuS9trnx1mgRjLVKidy5rKMA8QG8ZEhC_SzUH7YWuPMt2_WAIbJDoGhQQD2jNjeMoBDMdj0PZ_Mv2AlUIZA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMiCPP8VEvGfYxa6ismkmN8i6j-algVhrKJizSk!2e10!4m2!3m1!1s0x6b12ac8a49bca283:0x35532a59653fdd73", + "width": 5760 + } + ], + "place_id": "ChIJg6K8SYqsEmsRc90_ZVkqUzU", + "rating": 4.1, + "reference": "CmRRAAAAY2L4T7GDZLcKnSJWJ1s5TWL8b1dCxdAwaL6Jg9xg73A72Fdt0D_0W8weTzmmOj_P7v5be5TgwXMFtzygUWLwFS44H5wo0FOUDVmQK47qGHd_hUmjpsalMHDSJgHgQIEhEhCYEr9Q6pHZqb2jyr97YQUnGhRWLpr0zOHQq2TknV4yjGVv0ah17A", + "scope": "GOOGLE", + "types": [ + "bar", + "lodging", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=3842461470516305267", + "user_ratings_total": 333, + "vicinity": "1 Military Road, Watsons Bay", + "website": "http://www.watsonsbayhotel.com.au/" + }, + { + "address_components": [ + { + "long_name": "11", + "short_name": "11", + "types": [ + "street_number" + ] + }, + { + "long_name": "Jamison Street", + "short_name": "Jamison St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Amora Hotel", + "feature_id": { + "cell_id": 7715420706124963179, + "fprint": 8305136006628996129 + }, + "formatted_address": "11 Jamison St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9696 2500", + "geometry": { + "location": { + "lat": -33.864556, + "lng": 151.206286 + }, + "viewport": { + "northeast": { + "lat": -33.8630818197085, + "lng": 151.2075603302915 + }, + "southwest": { + "lat": -33.8657797802915, + "lng": 151.2048623697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "43d9e52705e06ef0e6ce0894c23747af85d24b4b", + "international_phone_number": "+61 2 9696 2500", + "name": "Amora Hotel Jamison Sydney", + "opening_hours": { + "open_now": true, + "periods": [ + { + "open": { + "day": 0, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: Open 24 hours", + "Tuesday: Open 24 hours", + "Wednesday: Open 24 hours", + "Thursday: Open 24 hours", + "Friday: Open 24 hours", + "Saturday: Open 24 hours", + "Sunday: Open 24 hours" + ] + }, + "photos": [ + { + "height": 3264, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/112581335421444049471/photos\"\u003eAmora Hotel Jamison Sydney\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAd6qqMJ_N0ASy2GYm7x68LAbSKNHO-WsG2UiLYy4WBfWzAmzHv43ADSFFiMXUkNNTC9I1ZXirADY4KrO8uguvYHD8tTtDtgebG7gqusWQO2wazE984V3SI-7CQeA-oEOeEhD5c8VUrXqBFmbc3w20sJU2GhTSTukkvhrx7SjHHbE89YhxPfo5lw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNnxGdayjqBn5ey4hEu8ycJWRtgQh95eT8DDD5u!2e10!4m2!3m1!1s0x6b12ae4111459d6b:0x7341c5320e6d7421", + "width": 2448 + } + ], + "place_id": "ChIJa51FEUGuEmsRIXRtDjLFQXM", + "price_level": 3, + "rating": 4.3, + "reference": "CmRRAAAA2vKSFrH0bnlxdtX_pEJfEGDNo6xL3cjyMjVvLpiB99CnSShMW2g7CLQwIvJz-sjutjMP0yaxBS3LQGmVacze71GGIcFP2E12A1dmBrv8J2I8OXT-uKsQNxN1rII47K7qEhDSFg8u-DchClavG8bU3LntGhTmX_76RHr3qUrT1-3zZWY40CUKwg", + "scope": "GOOGLE", + "types": [ + "bar", + "lodging", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=8305136006628996129", + "user_ratings_total": 370, + "vicinity": "11 Jamison Street, Sydney", + "website": "http://sydney.amorahotels.com/" + }, + { + "address_components": [ + { + "long_name": "1", + "short_name": "1", + "types": [ + "subpremise" + ] + }, + { + "long_name": "162", + "short_name": "162", + "types": [ + "street_number" + ] + }, + { + "long_name": "Flinders Street", + "short_name": "Flinders St", + "types": [ + "route" + ] + }, + { + "long_name": "Paddington", + "short_name": "Paddington", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2021", + "short_name": "2021", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420544758719643, + "fprint": 12015149928226689917 + }, + "formatted_address": "1/162 Flinders St, Paddington NSW 2021, Australia", + "formatted_phone_number": "(02) 9331 4533", + "geometry": { + "location": { + "lat": -33.88640109999999, + "lng": 151.2191259 + }, + "viewport": { + "northeast": { + "lat": -33.8850796697085, + "lng": 151.2203470302915 + }, + "southwest": { + "lat": -33.8877776302915, + "lng": 151.2176490697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "01694dc5307b782e2634f7e9c52dfeb50296a744", + "international_phone_number": "+61 2 9331 4533", + "name": "Captain Cook Hotel", + "opening_hours": { + "minutes_until_closed": 614, + "minutes_until_open": 1154, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "0100" + }, + "open": { + "day": 0, + "time": "1000" + } + }, + { + "close": { + "day": 2, + "time": "0100" + }, + "open": { + "day": 1, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "0100" + }, + "open": { + "day": 2, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "0100" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "0100" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 6, + "time": "0100" + }, + "open": { + "day": 5, + "time": "1000" + } + }, + { + "close": { + "day": 0, + "time": "0100" + }, + "open": { + "day": 6, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 10:00 AM – 1:00 AM", + "Tuesday: 10:00 AM – 1:00 AM", + "Wednesday: 10:00 AM – 1:00 AM", + "Thursday: 10:00 AM – 1:00 AM", + "Friday: 10:00 AM – 1:00 AM", + "Saturday: 10:00 AM – 1:00 AM", + "Sunday: 10:00 AM – 1:00 AM" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114726838822693345464/photos\"\u003eCaptain Cook Hotel\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAATGgWmlTLN7RlI3MdZqZPcIim5SK_HzUwiu2AtFaBU8y775pmDlT7-GXsiYSqkWK-dxa-ZzmC-By33rbfqefXdqO-eqiuEvg8Fj5woqc4eYTNxFUjZVdd_7pM3CZj3-7rEhCERmAosytUgpKZmxM12-K0GhRKTkEiHnvqS3ITSd5aiXY__Az2Ag", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOpOf-8jTxJoSXumdsjmmOvpZU9bQfDdEjeZoab!2e10!4m2!3m1!1s0x6b12ae1b7f18249b:0xa6be63336303137d", + "width": 1369 + } + ], + "place_id": "ChIJmyQYfxuuEmsRfRMDYzNjvqY", + "price_level": 1, + "rating": 3.7, + "reference": "CmRSAAAAPKZdLTjv7yRz4NfkN-PUNqlU9Q5LgwxNRSRIzMicwuhJLqFPJ8mzQkPZUbQh9Q5Y7yd-0JezlBQvb_fvsljMN6SfscHtFIeJIkeMtXVZr4FydcLDKp4yDxQN6FP5pUkSEhB4lQhJYbi_vujhgnt9WtIUGhSAcebO6aEP-c0-MauLT91lVm4fJA", + "scope": "GOOGLE", + "types": [ + "restaurant", + "bar", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=12015149928226689917", + "user_ratings_total": 77, + "vicinity": "1/162 Flinders Street, Paddington", + "website": "http://thecaptaincookhotel.com.au/" + }, + { + "address_components": [ + { + "long_name": "169", + "short_name": "169", + "types": [ + "street_number" + ] + }, + { + "long_name": "Castlereagh Street", + "short_name": "Castlereagh St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420693989188007, + "fprint": 13230702910794383794 + }, + "formatted_address": "169 Castlereagh St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9284 1000", + "geometry": { + "location": { + "lat": -33.87256199999999, + "lng": 151.2089733 + }, + "viewport": { + "northeast": { + "lat": -33.8712262697085, + "lng": 151.2104687302915 + }, + "southwest": { + "lat": -33.87392423029149, + "lng": 151.2077707697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "35d410e7239c53450a4cb16077446af9d8f5177b", + "international_phone_number": "+61 2 9284 1000", + "name": "Castlereagh Boutique Hotel", + "opening_hours": { + "open_now": true, + "periods": [ + { + "open": { + "day": 0, + "time": "0000" + } + } + ], + "weekday_text": [ + "Monday: Open 24 hours", + "Tuesday: Open 24 hours", + "Wednesday: Open 24 hours", + "Thursday: Open 24 hours", + "Friday: Open 24 hours", + "Saturday: Open 24 hours", + "Sunday: Open 24 hours" + ] + }, + "photos": [ + { + "height": 1335, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106251108978025242661/photos\"\u003eCastlereagh Boutique Hotel\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAQoAsx-EqQUw9yn9r7N1diGVaF3vfT8xSOxGi0xO7fn34aKDjR3oLsIj7Jcn8Gqovy6Xw6vkKs5q6bdDIvQsEK_nmVNIhFetC_Ohd4ax6XGgd1R6y7VwA2--o6sc7dKqlEhB0vZiw-FXE88Rp_tOWfamXGhRFkCbYJRJ6Lm6lrcNbyFxow0X65w", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMnWqbeEBu1C8WQh-1biFG9itlWytRWnBWRL7R-!2e10!4m2!3m1!1s0x6b12ae3e3dec61a7:0xb79ce632b6a041b2", + "width": 2000 + } + ], + "place_id": "ChIJp2HsPT6uEmsRskGgtjLmnLc", + "rating": 3.9, + "reference": "CmRSAAAA2bpsG3Cdp6Vz35llIcnV7035aCouxbRWwwi_rH6zsS5uOlpMl_fkkuzsP5Kje54rWcEPjKq2_wfV9NlWpRQZFV5nutI1mtR-4ZmcQf-aiGrzo41lMS0jsjrYty8-UxmNEhBb7ZxTa5S-pgN4UqQJ9pwfGhTJF95wXrtXXCCVBFDr7GBj-o2yDQ", + "scope": "GOOGLE", + "types": [ + "bar", + "lodging", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=13230702910794383794", + "user_ratings_total": 83, + "vicinity": "169 Castlereagh Street, Sydney", + "website": "http://www.thecastlereagh.com.au/" + }, + { + "address_components": [ + { + "long_name": "2", + "short_name": "2", + "types": [ + "subpremise" + ] + }, + { + "long_name": "53", + "short_name": "53", + "types": [ + "street_number" + ] + }, + { + "long_name": "Cross Street", + "short_name": "Cross St", + "types": [ + "route" + ] + }, + { + "long_name": "Double Bay", + "short_name": "Double Bay", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Woollahra Municipal Council", + "short_name": "Woollahra", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2028", + "short_name": "2028", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420276514792237, + "fprint": 3454786279734905869 + }, + "formatted_address": "2/53 Cross St, Double Bay NSW 2028, Australia", + "formatted_phone_number": "(02) 9328 1664", + "geometry": { + "location": { + "lat": -33.875774, + "lng": 151.242306 + }, + "viewport": { + "northeast": { + "lat": -33.8745083197085, + "lng": 151.2436018302915 + }, + "southwest": { + "lat": -33.8772062802915, + "lng": 151.2409038697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "240d1d75239b75ea9de6231ae1c1944c6b7b5d9a", + "international_phone_number": "+61 2 9328 1664", + "name": "Pink Salt", + "opening_hours": { + "minutes_until_closed": 464, + "minutes_until_open": 1274, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2200" + }, + "open": { + "day": 0, + "time": "1200" + } + }, + { + "close": { + "day": 2, + "time": "2230" + }, + "open": { + "day": 2, + "time": "1200" + } + }, + { + "close": { + "day": 3, + "time": "2230" + }, + "open": { + "day": 3, + "time": "1200" + } + }, + { + "close": { + "day": 4, + "time": "2230" + }, + "open": { + "day": 4, + "time": "1200" + } + }, + { + "close": { + "day": 6, + "time": "0000" + }, + "open": { + "day": 5, + "time": "1200" + } + }, + { + "close": { + "day": 0, + "time": "0000" + }, + "open": { + "day": 6, + "time": "1200" + } + } + ], + "weekday_text": [ + "Monday: Closed", + "Tuesday: 12:00 – 10:30 PM", + "Wednesday: 12:00 – 10:30 PM", + "Thursday: 12:00 – 10:30 PM", + "Friday: 12:00 PM – 12:00 AM", + "Saturday: 12:00 PM – 12:00 AM", + "Sunday: 12:00 – 10:00 PM" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/108434394238454403484/photos\"\u003ePink Salt\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAuQzZrH2zuNLCww9ZU3Xo9QsW97O3R99cWLQLI7FDR4XDZgu5AZUvm4XO23SwYEld88M7rjRcz6M7mcJ34GKrCzwvuKTu2sLqH3Dvo9w7CVlxoHbo77HHvglcLjC5nwUNEhDPQF94WXzAcz-Ip3w2mUPzGhTnB8mDxniejzgAFVMzCTg5QAZVvQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipP7I8eghSXo9kcq7fKKychmYObEw_r_G5VV82Q1!2e10!4m2!3m1!1s0x6b12addd0a82a32d:0x2ff1ddd1327e0c0d", + "width": 2048 + } + ], + "place_id": "ChIJLaOCCt2tEmsRDQx-MtHd8S8", + "price_level": 1, + "rating": 4.1, + "reference": "CmRRAAAAXAM_qq1yaS0YWlW6Fti3inJTVDOwAvkWeVHuNLnYZn4hPAFJ7zL-3p56BKz4a1YGtoDNCenJvWHzOyGjeiZYunIiKKEMcWZLr2ijDUCrj_i4gsK0zfLgvHnaluiWkvRHEhBikEhnuYzujImCklWWTHEyGhT-QeIon5LdlvkmmDwb5eQOlF1AJQ", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=3454786279734905869", + "user_ratings_total": 49, + "vicinity": "2/53 Cross Street, Double Bay", + "website": "http://www.pinksalt.com.au/" + }, + { + "address_components": [ + { + "long_name": "123", + "short_name": "123", + "types": [ + "street_number" + ] + }, + { + "long_name": "Ferry Road", + "short_name": "Ferry Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Glebe", + "short_name": "Glebe", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2037", + "short_name": "2037", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420644209198229, + "fprint": 1842343863745421534 + }, + "formatted_address": "123 Ferry Rd, Glebe NSW 2037, Australia", + "formatted_phone_number": "(02) 9518 9011", + "geometry": { + "location": { + "lat": -33.873479, + "lng": 151.188172 + }, + "viewport": { + "northeast": { + "lat": -33.8723302197085, + "lng": 151.1893505802915 + }, + "southwest": { + "lat": -33.8750281802915, + "lng": 151.1866526197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "88377a14b921c012985534f4ac6e4d900b5ad49a", + "international_phone_number": "+61 2 9518 9011", + "name": "The Boathouse on Blackwattle Bay", + "opening_hours": { + "minutes_until_closed": 494, + "minutes_until_open": 194, + "open_now": false, + "periods": [ + { + "close": { + "day": 0, + "time": "1500" + }, + "open": { + "day": 0, + "time": "1200" + } + }, + { + "close": { + "day": 0, + "time": "2300" + }, + "open": { + "day": 0, + "time": "1800" + } + }, + { + "close": { + "day": 2, + "time": "2300" + }, + "open": { + "day": 2, + "time": "1800" + } + }, + { + "close": { + "day": 3, + "time": "2300" + }, + "open": { + "day": 3, + "time": "1800" + } + }, + { + "close": { + "day": 4, + "time": "2300" + }, + "open": { + "day": 4, + "time": "1800" + } + }, + { + "close": { + "day": 5, + "time": "1500" + }, + "open": { + "day": 5, + "time": "1200" + } + }, + { + "close": { + "day": 5, + "time": "2300" + }, + "open": { + "day": 5, + "time": "1800" + } + }, + { + "close": { + "day": 6, + "time": "1500" + }, + "open": { + "day": 6, + "time": "1200" + } + }, + { + "close": { + "day": 6, + "time": "2300" + }, + "open": { + "day": 6, + "time": "1800" + } + } + ], + "weekday_text": [ + "Monday: Closed", + "Tuesday: 6:00 – 11:00 PM", + "Wednesday: 6:00 – 11:00 PM", + "Thursday: 6:00 – 11:00 PM", + "Friday: 12:00 – 3:00 PM, 6:00 – 11:00 PM", + "Saturday: 12:00 – 3:00 PM, 6:00 – 11:00 PM", + "Sunday: 12:00 – 3:00 PM, 6:00 – 11:00 PM" + ] + }, + "photos": [ + { + "height": 886, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/110972572752157621191/photos\"\u003eThe Boathouse on Blackwattle Bay\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAfh97qeu0J9xGu3bw-WY39iHtoT3bmMifHGD5mVoU3hoSRCL5O2c7QTylDuXfbFuu8Rg8GpAokrdpwy_x6kInPdUURQVMpXQ3weYk6b-Kvc0E9raDUwobjl9RwwB0Z2M5EhDIHdrxexs5TFfm2GHigA2cGhQ0KFwi5wjsZY3wkR30O49JBMdzJw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPtJ2UJTDdTA1tm9y0CK--C1L4f4_R0BrjkuWCa!2e10!4m2!3m1!1s0x6b12ae32a6ce0495:0x199151fba0aaa0de", + "width": 889 + } + ], + "place_id": "ChIJlQTOpjKuEmsR3qCqoPtRkRk", + "price_level": 4, + "rating": 4.4, + "reference": "CmRRAAAApSZktvWTqaKEN8gAbQ-eySdK7Mji2dLAS7GSyn2tZdAiYRzEjw8pDmMBVPqylojplKGNAofV05vnmQib9nx1I0yIHHx31MaIN6N6WXhD8_DXObdMtxYG86VuoA2dSQ-eEhBiDvhTsnIsgwwvgf_S1_urGhSG_d0KsKUP5giWB94XAkL4CAe7VQ", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=1842343863745421534", + "user_ratings_total": 120, + "vicinity": "123 Ferry Road, Glebe", + "website": "http://www.boathouse.net.au/" + }, + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420669487875263, + "fprint": 14727475779082137602 + }, + "formatted_address": "The Promenade/Cockle Bay Wharf, Sydney NSW 2000, Australia", + "formatted_phone_number": "1300 989 989", + "geometry": { + "location": { + "lat": -33.872193, + "lng": 151.202237 + }, + "viewport": { + "northeast": { + "lat": -33.8707618697085, + "lng": 151.2034219802915 + }, + "southwest": { + "lat": -33.8734598302915, + "lng": 151.2007240197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "949950665166808eb8a91abac22cd267e94c64d4", + "international_phone_number": "+61 1300 989 989", + "name": "Nick's Seafood Restaurant", + "opening_hours": { + "minutes_until_closed": 14, + "minutes_until_open": 164, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2200" + }, + "open": { + "day": 0, + "time": "1130" + } + }, + { + "close": { + "day": 1, + "time": "1500" + }, + "open": { + "day": 1, + "time": "1130" + } + }, + { + "close": { + "day": 1, + "time": "2200" + }, + "open": { + "day": 1, + "time": "1730" + } + }, + { + "close": { + "day": 2, + "time": "1500" + }, + "open": { + "day": 2, + "time": "1130" + } + }, + { + "close": { + "day": 2, + "time": "2200" + }, + "open": { + "day": 2, + "time": "1730" + } + }, + { + "close": { + "day": 3, + "time": "1500" + }, + "open": { + "day": 3, + "time": "1130" + } + }, + { + "close": { + "day": 3, + "time": "2200" + }, + "open": { + "day": 3, + "time": "1730" + } + }, + { + "close": { + "day": 4, + "time": "1500" + }, + "open": { + "day": 4, + "time": "1130" + } + }, + { + "close": { + "day": 4, + "time": "2200" + }, + "open": { + "day": 4, + "time": "1730" + } + }, + { + "close": { + "day": 5, + "time": "1500" + }, + "open": { + "day": 5, + "time": "1130" + } + }, + { + "close": { + "day": 5, + "time": "2300" + }, + "open": { + "day": 5, + "time": "1730" + } + }, + { + "close": { + "day": 6, + "time": "1500" + }, + "open": { + "day": 6, + "time": "1130" + } + }, + { + "close": { + "day": 6, + "time": "2300" + }, + "open": { + "day": 6, + "time": "1730" + } + } + ], + "weekday_text": [ + "Monday: 11:30 AM – 3:00 PM, 5:30 – 10:00 PM", + "Tuesday: 11:30 AM – 3:00 PM, 5:30 – 10:00 PM", + "Wednesday: 11:30 AM – 3:00 PM, 5:30 – 10:00 PM", + "Thursday: 11:30 AM – 3:00 PM, 5:30 – 10:00 PM", + "Friday: 11:30 AM – 3:00 PM, 5:30 – 11:00 PM", + "Saturday: 11:30 AM – 3:00 PM, 5:30 – 11:00 PM", + "Sunday: 11:30 AM – 10:00 PM" + ] + }, + "photos": [ + { + "height": 529, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117751572855245799193/photos\"\u003eNick's Seafood Restaurant\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAeTp2TnAS8nGXGNL6eJ6mhSGFM7QPBWzVsnZ1MsT0XAe7uYtMMiwpG7X_pLxww0G85iGO3VGd3m7XreXii1cokd8L7eBN43--gGxg0TFXAWmYjv4O4AH8NZtkN3307wmMEhCHxQ3wTtn8KwMRkgM9UiI3GhQOyF47poMP9GLdZIu9FCY5stGqwg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNdB0GlNJF8QKvBZpkXbCVQUCHnnzWy_WxydOpL!2e10!4m2!3m1!1s0x6b12ae38898804bf:0xcc6281310a5c3002", + "width": 529 + } + ], + "place_id": "ChIJvwSIiTiuEmsRAjBcCjGBYsw", + "price_level": 3, + "rating": 3.4, + "reference": "CmRSAAAAKUaA3U3C2d_ZHBRSywz75MMOZ7LeXq4vmXbXWcylUM2TSyq6k13J7zdsFOUEA8xkri_A6rCIlnfTHOOeHQF4_6yM05x5DsxMDz_8WOdLa2wicao9JC6b2ZLydpBJo2RhEhAGShiuNAi8F0GZD-eUXJOJGhTSukmi0Scidp7JmoIsAHWGcm65VQ", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=14727475779082137602", + "user_ratings_total": 286, + "vicinity": "The Promenade/Cockle Bay Wharf, Sydney", + "website": "http://www.nicks-seafood.com.au/" + }, + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420703668516373, + "fprint": 4275509081758565537 + }, + "formatted_address": "1 LGF, Lower Ground Floor, Sydney GPO Building, No., 1 Martin Place, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9229 7777", + "geometry": { + "location": { + "lat": -33.86772, + "lng": 151.208175 + }, + "viewport": { + "northeast": { + "lat": -33.8663710197085, + "lng": 151.2095239802915 + }, + "southwest": { + "lat": -33.86906898029149, + "lng": 151.2068260197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "670f64fa781098196ac56391684d89134d0b54e9", + "international_phone_number": "+61 2 9229 7777", + "name": "Prime Steak Restaurant, Sydney", + "opening_hours": { + "minutes_until_closed": 14, + "minutes_until_open": 194, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1500" + }, + "open": { + "day": 1, + "time": "1200" + } + }, + { + "close": { + "day": 1, + "time": "2200" + }, + "open": { + "day": 1, + "time": "1800" + } + }, + { + "close": { + "day": 2, + "time": "1500" + }, + "open": { + "day": 2, + "time": "1200" + } + }, + { + "close": { + "day": 2, + "time": "2200" + }, + "open": { + "day": 2, + "time": "1800" + } + }, + { + "close": { + "day": 3, + "time": "1500" + }, + "open": { + "day": 3, + "time": "1200" + } + }, + { + "close": { + "day": 3, + "time": "2200" + }, + "open": { + "day": 3, + "time": "1800" + } + }, + { + "close": { + "day": 4, + "time": "1500" + }, + "open": { + "day": 4, + "time": "1200" + } + }, + { + "close": { + "day": 4, + "time": "2200" + }, + "open": { + "day": 4, + "time": "1800" + } + }, + { + "close": { + "day": 5, + "time": "1500" + }, + "open": { + "day": 5, + "time": "1200" + } + }, + { + "close": { + "day": 5, + "time": "2200" + }, + "open": { + "day": 5, + "time": "1800" + } + }, + { + "close": { + "day": 6, + "time": "2200" + }, + "open": { + "day": 6, + "time": "1800" + } + } + ], + "weekday_text": [ + "Monday: 12:00 – 3:00 PM, 6:00 – 10:00 PM", + "Tuesday: 12:00 – 3:00 PM, 6:00 – 10:00 PM", + "Wednesday: 12:00 – 3:00 PM, 6:00 – 10:00 PM", + "Thursday: 12:00 – 3:00 PM, 6:00 – 10:00 PM", + "Friday: 12:00 – 3:00 PM, 6:00 – 10:00 PM", + "Saturday: 6:00 – 10:00 PM", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 1031, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104390305802517551227/photos\"\u003ePrime Steak Restaurant, Sydney\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAgagFHy_lyotoG_AKXHqFx-XFhdNZTQKXqP7m6hg2LebeOabEEeReHN2nw69J42Bc-G4UB1rxTGH4FI8qec2B_o_um2Dez8ae68IdIgDvSybd-tNb0LIISuaipeYAtOG1EhBNquFhnmk73o5mwJhzP84YGhSTDx3PqXEU48RGSVLNhWpJtikXOw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOS_Dyq0wftvm5-9U-qglxeqlxEFkgfm4Vol9Uh!2e10!4m2!3m1!1s0x6b12ae407edb3615:0x3b55a8db742d04a1", + "width": 1031 + } + ], + "place_id": "ChIJFTbbfkCuEmsRoQQtdNuoVTs", + "price_level": 3, + "rating": 4.3, + "reference": "CmRRAAAAiEzUvRWajSo5vRfPBHuwwjRohG0F8KTzn7LsKF5YIp9uKKcISSGQ5tidg0bhFUdq8b6Q7OkRdaPEgSMhjeCUWg_lX7NwqgXM1n8MGmO9KcYDwkDFyTp0ir5IZQdkKkFNEhAX7Ze5GKjEkkloE9SQYIQSGhSlspnwmf9oD5c7jefAfHooVRFffg", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=4275509081758565537", + "user_ratings_total": 82, + "vicinity": "1 LGF, Lower Ground Floor, Sydney GPO Building, No., 1 Martin Place, Sydney", + "website": "http://gpogrand.com/prime-steak-restaurant-sydney" + }, + { + "address_components": [ + { + "long_name": "139", + "short_name": "139", + "types": [ + "street_number" + ] + }, + { + "long_name": "Murray Street", + "short_name": "Murray St", + "types": [ + "route" + ] + }, + { + "long_name": "Pyrmont", + "short_name": "Pyrmont", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2009", + "short_name": "2009", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420635191608589, + "fprint": 12071652699896554453 + }, + "formatted_address": "139 Murray St, Pyrmont NSW 2009, Australia", + "formatted_phone_number": "(02) 8586 1888", + "geometry": { + "location": { + "lat": -33.8726709, + "lng": 151.1970516 + }, + "viewport": { + "northeast": { + "lat": -33.8713257197085, + "lng": 151.1985260302915 + }, + "southwest": { + "lat": -33.87402368029149, + "lng": 151.1958280697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/lodging-71.png", + "id": "89c3033e588757f208f171dace0244dd91156ab4", + "international_phone_number": "+61 2 8586 1888", + "name": "Ovolo 1888 Darling Harbour", + "photos": [ + { + "height": 1920, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113586024562415860709/photos\"\u003eOvolo 1888 Darling Harbour\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAlvb5012dn5PBrKSM8Y8tNpU3bTGi5vzDkgUTMY8ZyP8T9F6QTtH17xXirrLeZ3fpyNQknFTIUyqtH6ezBomkKBq4fExsaRHfzXZ3twDw2UKjTthn9RS6CFF9g8P6g1S4EhAU7UIbZtpdyw6IMBhZ35BDGhTW-UdabIn4AChwFlJJzB8I9rejvQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOlAFpJvUZDHt6XUcqzld5sK4KMtCL6kQVv6YYC!2e10!4m2!3m1!1s0x6b12ae308d50850d:0xa787202c153823d5", + "width": 1286 + } + ], + "place_id": "ChIJDYVQjTCuEmsR1SM4FSwgh6c", + "rating": 4.7, + "reference": "CmRSAAAAjCviUM2q4UPVsxXpZK6-eq5xAkHAbP8_YFvKEu1CN3OwssNoGmPGYnbwEdu9AXVp1trop3Vx0GL726DfGQwVM4G0hoNu1PZCAZO-R3rKeu_7ePQGXdswQGcNt3fzq82uEhAMukPREKqKDAtUFD1BQ-cbGhR8l0ACBlGaz9OI_1NKlXssALjNGg", + "scope": "GOOGLE", + "types": [ + "bar", + "lodging", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=12071652699896554453", + "user_ratings_total": 158, + "vicinity": "139 Murray Street, Pyrmont", + "website": "http://www.ovolohotels.com.au/ovolo1888darlingharbour/" + }, + { + "address_components": [ + { + "long_name": "100", + "short_name": "100", + "types": [ + "street_number" + ] + }, + { + "long_name": "Market Street", + "short_name": "Market St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420700330460203, + "fprint": 2915230526462623971 + }, + "formatted_address": "100 Market St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 8223 3883", + "geometry": { + "location": { + "lat": -33.87046329999999, + "lng": 151.2088857 + }, + "viewport": { + "northeast": { + "lat": -33.86911431970849, + "lng": 151.2102346802915 + }, + "southwest": { + "lat": -33.87181228029149, + "lng": 151.2075367197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "e77c544388286c735eec8b960c444e82d6dd81e3", + "international_phone_number": "+61 2 8223 3883", + "name": "360 Bar & Dining", + "opening_hours": { + "minutes_until_closed": 404, + "minutes_until_open": 1274, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2130" + }, + "open": { + "day": 0, + "time": "1200" + } + }, + { + "close": { + "day": 1, + "time": "2130" + }, + "open": { + "day": 1, + "time": "1200" + } + }, + { + "close": { + "day": 2, + "time": "2130" + }, + "open": { + "day": 2, + "time": "1200" + } + }, + { + "close": { + "day": 3, + "time": "2130" + }, + "open": { + "day": 3, + "time": "1200" + } + }, + { + "close": { + "day": 4, + "time": "2130" + }, + "open": { + "day": 4, + "time": "1200" + } + }, + { + "close": { + "day": 5, + "time": "2130" + }, + "open": { + "day": 5, + "time": "1200" + } + }, + { + "close": { + "day": 6, + "time": "2130" + }, + "open": { + "day": 6, + "time": "1200" + } + } + ], + "weekday_text": [ + "Monday: 12:00 – 9:30 PM", + "Tuesday: 12:00 – 9:30 PM", + "Wednesday: 12:00 – 9:30 PM", + "Thursday: 12:00 – 9:30 PM", + "Friday: 12:00 – 9:30 PM", + "Saturday: 12:00 – 9:30 PM", + "Sunday: 12:00 – 9:30 PM" + ] + }, + "organizationally_part_of": { + "organization_name": "Westfield Sydney", + "relation_type": "INDEPENDENT_ESTABLISHMENT_IN" + }, + "photos": [ + { + "height": 466, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117404122580903345419/photos\"\u003e360 Bar & Dining\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAjiHBUXNj9F3jpccciBcze4E2fh6cxy-ghtfMPhraYyhwabgMrlQlpFONTDAcD22qmB2fhmvLc2QFtx27TMYYWPPpy6OVRp9dWXqkqVErTzZWKfvc7nFBHLhiVbuf16C_EhDsrMwdu7f8N0C3OQRIZLW_GhTqnDrFN-CIt_jy7Pp_ocQ-YcyIog", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMsRIl3D0ZGvND-v486VUjg9uDYYkVce7fWN-Lt!2e10!4m2!3m1!1s0x6b12ae3fb7e4842b:0x2874fab7e02e00e3", + "width": 700 + } + ], + "place_id": "ChIJK4Tktz-uEmsR4wAu4Lf6dCg", + "price_level": 3, + "rating": 3.8, + "reference": "CmRRAAAAa7OvSQ2_6rtHGa0k04HQCfBHqLFI7mpPP3Uu9xybzQ9kGN95ssGlnz-4-_7iZxJKBbAnKm3TICmrFa3maByhQUc7pFPLX6UEvaga2qD6--4ylS3xfmcu52hHYn9ChNj7EhAHg5w21ojXXa9O7ckKnwu_GhQ34Y_qQ2wle-Ydf0Fd6A8cvydRIg", + "scope": "GOOGLE", + "types": [ + "restaurant", + "bar", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=2915230526462623971", + "user_ratings_total": 133, + "vicinity": "100 Market Street, Sydney", + "website": "http://www.360dining.com.au/" + }, + { + "address_components": [ + { + "long_name": "201", + "short_name": "201", + "types": [ + "street_number" + ] + }, + { + "long_name": "Clarence Street", + "short_name": "Clarence St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420697863071267, + "fprint": 6188025247222981526 + }, + "formatted_address": "201 Clarence St, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9262 3303", + "geometry": { + "location": { + "lat": -33.869768, + "lng": 151.205249 + }, + "viewport": { + "northeast": { + "lat": -33.8683838697085, + "lng": 151.2066637302915 + }, + "southwest": { + "lat": -33.87108183029149, + "lng": 151.2039657697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "24ddc0267ee8e6eccb6f162976b5e027216e7739", + "international_phone_number": "+61 2 9262 3303", + "name": "Redoak Boutique Beer Cafe", + "photos": [ + { + "height": 1820, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105243651264983087191/photos\"\u003eRedoak Boutique Beer Cafe\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA4wcl5IyB9iVAG0B__MLVDryoYezwVQMrmQfLdabxQlplGXIVQu9fpY8bingG5_omSiXQp6RduDBQ9eQ5RvR-pirZlFQm81CnFnlL2tR2Iu7HosF2FdIBEXRpnq7CCoOnEhCpqXn7jlpuMP5_rZkhxqxWGhRu_GhyzRicwfJfhGpnFvINk5zL2A", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPo7wZztHyEiZJcYn0mbS_ShMNXpkTGxJvCFo2s!2e10!4m2!3m1!1s0x6b12ae3f24d32623:0x55e0482d41dfe396", + "width": 1820 + } + ], + "place_id": "ChIJIybTJD-uEmsRluPfQS1I4FU", + "price_level": 2, + "rating": 4.2, + "reference": "CmRRAAAAQiEbCNPsDIzzNFgJQnfdf9z6WDmPNtgnNl9fXhEuzbI89Pr8lHMttXgLZ51ZCPxi4lThWgcZp0mxjJFQWQhKL8VFzXaPugzNYTJLojxFBur76k4BZtNWOBuzfIz0Vc_oEhBs2svY7_XLgx8CHqbNpR2DGhQQOzJ_Gzb8y2-DxF4yJVFkGsS6zw", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=6188025247222981526", + "user_ratings_total": 162, + "vicinity": "201 Clarence Street, Sydney", + "website": "http://www.redoak.com.au/" + }, + { + "address_components": [ + { + "long_name": "103", + "short_name": "103", + "types": [ + "street_number" + ] + }, + { + "long_name": "Woolwich Road", + "short_name": "Woolwich Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Woolwich", + "short_name": "Woolwich", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "The Council of the Municipality of Hunters Hill", + "short_name": "Hunters Hill", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2110", + "short_name": "2110", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715422244013033723, + "fprint": 4673498300918521924 + }, + "formatted_address": "103 Woolwich Rd, Woolwich NSW 2110, Australia", + "formatted_phone_number": "(02) 9817 2125", + "geometry": { + "location": { + "lat": -33.84049950000001, + "lng": 151.1708651 + }, + "viewport": { + "northeast": { + "lat": -33.8390595197085, + "lng": 151.1721942302915 + }, + "southwest": { + "lat": -33.8417574802915, + "lng": 151.1694962697085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "a9fb9884a180586b575e0dd26de875431775af8b", + "international_phone_number": "+61 2 9817 2125", + "name": "Cucinetta", + "opening_hours": { + "minutes_until_closed": 434, + "minutes_until_open": 164, + "open_now": false, + "periods": [ + { + "close": { + "day": 0, + "time": "1500" + }, + "open": { + "day": 0, + "time": "1130" + } + }, + { + "close": { + "day": 2, + "time": "2200" + }, + "open": { + "day": 2, + "time": "1730" + } + }, + { + "close": { + "day": 3, + "time": "2200" + }, + "open": { + "day": 3, + "time": "1730" + } + }, + { + "close": { + "day": 4, + "time": "2200" + }, + "open": { + "day": 4, + "time": "1730" + } + }, + { + "close": { + "day": 5, + "time": "1500" + }, + "open": { + "day": 5, + "time": "1130" + } + }, + { + "close": { + "day": 5, + "time": "2200" + }, + "open": { + "day": 5, + "time": "1730" + } + }, + { + "close": { + "day": 6, + "time": "1500" + }, + "open": { + "day": 6, + "time": "1130" + } + }, + { + "close": { + "day": 6, + "time": "2200" + }, + "open": { + "day": 6, + "time": "1730" + } + } + ], + "weekday_text": [ + "Monday: Closed", + "Tuesday: 5:30 – 10:00 PM", + "Wednesday: 5:30 – 10:00 PM", + "Thursday: 5:30 – 10:00 PM", + "Friday: 11:30 AM – 3:00 PM, 5:30 – 10:00 PM", + "Saturday: 11:30 AM – 3:00 PM, 5:30 – 10:00 PM", + "Sunday: 11:30 AM – 3:00 PM" + ] + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115958863463466365383/photos\"\u003evincenzo mazzotta\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAvy0CmFzhJc3DoY4giiUn7UCxeBKPO0_PCGQczWTz7OKuwSFq1oETnjSEvkkoDqcTOt6YIaU2d9R3j7txF0l5HJgy1A3NzmlF6R6fQQIshmnFpZZ5W2vTxtHDYSlKAWDeEhBNfaBCdhwBGgErNAgdPbqDGhT-IC1s85lxU7RWWrZFizgwlwwc2w", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOeWXt30FoIxTfZ_DZcgXkcZhBO6j4lVfojNExZ!2e10!4m2!3m1!1s0x6b12afa7228b48fb:0x40db99f184a3e044", + "width": 4032 + } + ], + "place_id": "ChIJ-0iLIqevEmsRROCjhPGZ20A", + "price_level": 3, + "rating": 4.6, + "reference": "CmRRAAAAV1_8ZTRujDYgABqaBX1VsAtSULgBp2iOwrnbcOfo4HWv6iIZ9n-Vu4RYRL_0EVY4bBYgTfnSJM4EQ41wrfAknFzEKgiS6HIHH0EtpnWF0OoNk4zxRnHkBzMo1grNAxdJEhAui-b_M2jw8WQT4lmN0ZyoGhQK5G9EfQFMItIONw0OI7sevH3knA", + "scope": "GOOGLE", + "types": [ + "restaurant", + "bar", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=4673498300918521924", + "user_ratings_total": 47, + "vicinity": "103 Woolwich Road, Woolwich", + "website": "http://www.cucinetta.com.au/" + }, + { + "address_components": [ + { + "long_name": "5A", + "short_name": "5A", + "types": [ + "street_number" + ] + }, + { + "long_name": "The Promenade", + "short_name": "The Promenade", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420733725088857, + "fprint": 16082036443635887000 + }, + "formatted_address": "5A The Promenade, Sydney NSW 2000, Australia", + "formatted_phone_number": "(02) 9295 5066", + "geometry": { + "location": { + "lat": -33.86656629999999, + "lng": 151.2014534 + }, + "viewport": { + "northeast": { + "lat": -33.86540606970849, + "lng": 151.2028246802915 + }, + "southwest": { + "lat": -33.86810403029149, + "lng": 151.2001267197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "2e4cdb757d19cb518db0a551f9aca381d7599c93", + "international_phone_number": "+61 2 9295 5066", + "name": "Georges Mediterranean Bar & Grill", + "opening_hours": { + "minutes_until_closed": 134, + "minutes_until_open": 164, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "1700" + }, + "open": { + "day": 0, + "time": "1200" + } + }, + { + "close": { + "day": 0, + "time": "2230" + }, + "open": { + "day": 0, + "time": "1730" + } + }, + { + "close": { + "day": 1, + "time": "1700" + }, + "open": { + "day": 1, + "time": "1200" + } + }, + { + "close": { + "day": 1, + "time": "2230" + }, + "open": { + "day": 1, + "time": "1730" + } + }, + { + "close": { + "day": 2, + "time": "1700" + }, + "open": { + "day": 2, + "time": "1200" + } + }, + { + "close": { + "day": 2, + "time": "2230" + }, + "open": { + "day": 2, + "time": "1730" + } + }, + { + "close": { + "day": 3, + "time": "1700" + }, + "open": { + "day": 3, + "time": "1200" + } + }, + { + "close": { + "day": 3, + "time": "2230" + }, + "open": { + "day": 3, + "time": "1730" + } + }, + { + "close": { + "day": 4, + "time": "1700" + }, + "open": { + "day": 4, + "time": "1200" + } + }, + { + "close": { + "day": 4, + "time": "2230" + }, + "open": { + "day": 4, + "time": "1730" + } + }, + { + "close": { + "day": 5, + "time": "1700" + }, + "open": { + "day": 5, + "time": "1200" + } + }, + { + "close": { + "day": 5, + "time": "2230" + }, + "open": { + "day": 5, + "time": "1730" + } + }, + { + "close": { + "day": 6, + "time": "1700" + }, + "open": { + "day": 6, + "time": "1200" + } + }, + { + "close": { + "day": 6, + "time": "2230" + }, + "open": { + "day": 6, + "time": "1730" + } + } + ], + "weekday_text": [ + "Monday: 12:00 – 5:00 PM, 5:30 – 10:30 PM", + "Tuesday: 12:00 – 5:00 PM, 5:30 – 10:30 PM", + "Wednesday: 12:00 – 5:00 PM, 5:30 – 10:30 PM", + "Thursday: 12:00 – 5:00 PM, 5:30 – 10:30 PM", + "Friday: 12:00 – 5:00 PM, 5:30 – 10:30 PM", + "Saturday: 12:00 – 5:00 PM, 5:30 – 10:30 PM", + "Sunday: 12:00 – 5:00 PM, 5:30 – 10:30 PM" + ] + }, + "photos": [ + { + "height": 407, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116255917991140608112/photos\"\u003eGeorges Mediterranean Bar & Grill\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA-gvh70hBo_83mr016NnkKv_8m8akGoF3gkKAw5djBT0vwNH0-0szzznFDYP95mp8NHXFETKfZ_bQdUmlHrHmAw25aUZKLnE4rAcuidJkT4cn53A0CLped7G6U4aCUi3bEhCHKbqHuNf6EGJ68SsP90YaGhQL2osyCAuIFSK4qaQGAOGf8Cm7uQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOtleGQzOUmxaU4jTRW4Xu0fraHK0PuEkK1OfZV!2e10!4m2!3m1!1s0x6b12ae477e5e1c59:0xdf2edef06fcdab98", + "width": 405 + } + ], + "place_id": "ChIJWRxefkeuEmsRmKvNb_DeLt8", + "price_level": 1, + "rating": 3.5, + "reference": "CmRSAAAAuXHbhKzXSQ1-6OHY_CR02LjsxzMeO-2nXOFVyLBz3zpZmfd8OE2MMO65MZ-Jyl8SxI9AsR8e51-WzXYBMVMrxKspmAlAtZKPIo8kfxtCxwIyBCQcl_PRRtOVcIt7gjgGEhDTi5GV-1pB2NoiMivvuethGhQHB6UquZLW42kaOHxi1Z8V2vP5kA", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=16082036443635887000", + "user_ratings_total": 80, + "vicinity": "5A The Promenade, Sydney", + "website": "http://www.georgesrestaurant.com.au/" + }, + { + "address_components": [ + { + "long_name": "6", + "short_name": "6", + "types": [ + "subpremise" + ] + }, + { + "long_name": "7-41", + "short_name": "7-41", + "types": [ + "street_number" + ] + }, + { + "long_name": "Cowper Wharf Road", + "short_name": "Cowper Wharf Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Woolloomooloo", + "short_name": "Woolloomooloo", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2011", + "short_name": "2011", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420895065389737, + "fprint": 10588138134163852750 + }, + "formatted_address": "6/7-41 Cowper Wharf Rd, Woolloomooloo NSW 2011, Australia", + "formatted_phone_number": "(02) 9358 6299", + "geometry": { + "location": { + "lat": -33.869365, + "lng": 151.220224 + }, + "viewport": { + "northeast": { + "lat": -33.8679553697085, + "lng": 151.2215916802915 + }, + "southwest": { + "lat": -33.8706533302915, + "lng": 151.2188937197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "25ac143989a29e8eb6408447be7f05a15538f176", + "international_phone_number": "+61 2 9358 6299", + "name": "Sienna Marina", + "opening_hours": { + "minutes_until_closed": 554, + "minutes_until_open": 974, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "0000" + }, + "open": { + "day": 0, + "time": "0700" + } + }, + { + "close": { + "day": 2, + "time": "0000" + }, + "open": { + "day": 1, + "time": "0700" + } + }, + { + "close": { + "day": 3, + "time": "0000" + }, + "open": { + "day": 2, + "time": "0700" + } + }, + { + "close": { + "day": 4, + "time": "0000" + }, + "open": { + "day": 3, + "time": "0700" + } + }, + { + "close": { + "day": 5, + "time": "0000" + }, + "open": { + "day": 4, + "time": "0700" + } + }, + { + "close": { + "day": 6, + "time": "0000" + }, + "open": { + "day": 5, + "time": "0700" + } + }, + { + "close": { + "day": 0, + "time": "0000" + }, + "open": { + "day": 6, + "time": "0700" + } + } + ], + "weekday_text": [ + "Monday: 7:00 AM – 12:00 AM", + "Tuesday: 7:00 AM – 12:00 AM", + "Wednesday: 7:00 AM – 12:00 AM", + "Thursday: 7:00 AM – 12:00 AM", + "Friday: 7:00 AM – 12:00 AM", + "Saturday: 7:00 AM – 12:00 AM", + "Sunday: 7:00 AM – 12:00 AM" + ] + }, + "photos": [ + { + "height": 1440, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/100824454590057241322/photos\"\u003eKritsana Potsatian\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAduXePdkcH7Iucsh8G0eCVH7gTnTxjTySXgoBBD2cYVeEvuaYutKpDpyUpePvPeqSWdjeK1gqTqpbK0Nt8CwITypPaTY_fhe0cY9nP09pBR2qxp05ucan4alD9CtZtcVlEhDgAfcSK5-C8glhf1asDSXVGhQSP6XKxG-hmHKuU9Ub_du2_LfmEQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNhZGSGzvAxoKNYUPQ3P9Cq1o9gB5Aowcw-o4bh!2e10!4m2!3m1!1s0x6b12ae6d0effbaa9:0x92f09f88df9a8dce", + "width": 2560 + } + ], + "place_id": "ChIJqbr_Dm2uEmsRzo2a34if8JI", + "price_level": 1, + "rating": 3.8, + "reference": "CmRSAAAAznTP5jSpwM-s-J1ejlpFy4NJXDboOl7G_vb-A8X35FP9NqbSVQoFtwTkZZw46_FZ3gD4dp8aqQhMNdhh7sc7fukQ2FXlcKgGt4PeYzbhCaj4-u_xXT9RXCPhS3SZ6RCfEhBc1iOfhKVW81L31sWgz4GjGhT6fBU5MW8uAvChNA7oY6BvHf7tJQ", + "scope": "GOOGLE", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=10588138134163852750", + "user_ratings_total": 41, + "vicinity": "6/7-41 Cowper Wharf Road, Woolloomooloo", + "website": "http://siennamarina.com.au/" + }, + { + "address_components": [ + { + "long_name": "Darling Harbour", + "short_name": "Darling Harbour", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "feature_id": { + "cell_id": 7715420672044176881, + "fprint": 1568256268922944206 + }, + "formatted_address": "Cockle Bay Wharf, 108 The Prom, Darling Harbour NSW 2000, Australia", + "formatted_phone_number": "1300 989 989", + "geometry": { + "location": { + "lat": -33.871818, + "lng": 151.2021585 + }, + "viewport": { + "northeast": { + "lat": -33.8704690197085, + "lng": 151.2035074802915 + }, + "southwest": { + "lat": -33.8731669802915, + "lng": 151.2008095197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "b8e6d4a50eb086b0ba6d3cad397154c909b807a3", + "international_phone_number": "+61 1300 989 989", + "name": "Adria Bar Restaurant", + "opening_hours": { + "minutes_until_closed": 494, + "minutes_until_open": 1034, + "open_now": true, + "periods": [ + { + "close": { + "day": 0, + "time": "2300" + }, + "open": { + "day": 0, + "time": "0800" + } + }, + { + "close": { + "day": 1, + "time": "2300" + }, + "open": { + "day": 1, + "time": "0800" + } + }, + { + "close": { + "day": 2, + "time": "2300" + }, + "open": { + "day": 2, + "time": "0800" + } + }, + { + "close": { + "day": 3, + "time": "2300" + }, + "open": { + "day": 3, + "time": "0800" + } + }, + { + "close": { + "day": 4, + "time": "2300" + }, + "open": { + "day": 4, + "time": "0800" + } + }, + { + "close": { + "day": 6, + "time": "0000" + }, + "open": { + "day": 5, + "time": "0800" + } + }, + { + "close": { + "day": 0, + "time": "0000" + }, + "open": { + "day": 6, + "time": "0800" + } + } + ], + "weekday_text": [ + "Monday: 8:00 AM – 11:00 PM", + "Tuesday: 8:00 AM – 11:00 PM", + "Wednesday: 8:00 AM – 11:00 PM", + "Thursday: 8:00 AM – 11:00 PM", + "Friday: 8:00 AM – 12:00 AM", + "Saturday: 8:00 AM – 12:00 AM", + "Sunday: 8:00 AM – 11:00 PM" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117432890088249941161/photos\"\u003eAdria Bar Restaurant\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAARRh6jzb4JkNN2kyQIfCXiReQyii4ebrvN3CzbBFafSWSQnDuIzw9CQc4fNP2RXI5IehW95Ks6HqlB9K7EW8Lsm8YNSYs4GxKXbJwl0ufpQrk1G6piPWvGkXK-dlMefB1EhB1RKlLq1YlWodfrXbUQvwZGhRGoFm5NQtY6w9a5INh7ud747VScw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOxYelKsFznY0iLYqQ6tc3o3GHkz-f_FWFxMod5!2e10!4m2!3m1!1s0x6b12ae3921e615f1:0x15c390c481ee7ace", + "width": 1367 + } + ], + "place_id": "ChIJ8RXmITmuEmsRznrugcSQwxU", + "price_level": 2, + "rating": 2.8, + "reference": "CmRRAAAA5hANZLSW2YCcG42PgyBnIzLgF6B_ZhKs6A93NXtMFz8jsqjeQVf0daBCaO10ADFvDT6TBK81J-nigpDHkSWB67MjU9WIpbHSKXhfGvOkGuqYCajFStme5b97SgIhp_27EhD0SjitKRAnyK3y_xN-ujrHGhRlBH2Y0OWfp2ET5sAngCq3MzWwQg", + "scope": "GOOGLE", + "types": [ + "restaurant", + "bar", + "food", + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=1568256268922944206", + "user_ratings_total": 129, + "vicinity": "Cockle Bay Wharf, 108 The Prom, Darling Harbour", + "website": "http://www.adriabarrestaurant.com.au/" + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/PlacesApiPhotoResponse.json b/src/test/resources/com/google/maps/PlacesApiPhotoResponse.json new file mode 100644 index 000000000..1d2a11fb6 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiPhotoResponse.json @@ -0,0 +1,319 @@ +{ + "html_attributions": [], + "result": { + "address_components": [ + { + "long_name": "5", + "short_name": "5", + "types": [ + "floor" + ] + }, + { + "long_name": "48", + "short_name": "48", + "types": [ + "street_number" + ] + }, + { + "long_name": "Pirrama Road", + "short_name": "Pirrama Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Pyrmont", + "short_name": "Pyrmont", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2009", + "short_name": "2009", + "types": [ + "postal_code" + ] + } + ], + "adr_address": "5, \u003cspan class=\"street-address\"\u003e48 Pirrama Rd\u003c/span\u003e, \u003cspan class=\"locality\"\u003ePyrmont\u003c/span\u003e \u003cspan class=\"region\"\u003eNSW\u003c/span\u003e \u003cspan class=\"postal-code\"\u003e2009\u003c/span\u003e, \u003cspan class=\"country-name\"\u003eAustralia\u003c/span\u003e", + "chain_name": "Google", + "feature_id": { + "cell_id": 7715420665913760567, + "fprint": 10281119596374313554 + }, + "formatted_address": "5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", + "formatted_phone_number": "(02) 9374 4000", + "geometry": { + "location": { + "lat": -33.866651, + "lng": 151.195827 + }, + "viewport": { + "northeast": { + "lat": -33.8653881697085, + "lng": 151.1969739802915 + }, + "southwest": { + "lat": -33.86808613029149, + "lng": 151.1942760197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "4f89212bf76dde31f092cfc14d7506555d85b5c7", + "international_phone_number": "+61 2 9374 4000", + "name": "Google", + "opening_hours": { + "minutes_until_closed": 219, + "minutes_until_open": 1179, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "1000" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 10:00 AM – 6:00 PM", + "Tuesday: 10:00 AM – 6:00 PM", + "Wednesday: 10:00 AM – 6:00 PM", + "Thursday: 10:00 AM – 6:00 PM", + "Friday: 10:00 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105932078588305868215/photos\"\u003eMaksym Kozlenko\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA-N3w5YTMXWautuDW7IZgX9knz_2fNyyUpCWpvYdVEVb8RurBiisMKvr7AFxMW8dsu2yakYoqjW-IYSFk2cylXVM_c50cCxfm7MlgjPErFxumlcW1bLNOe--SwLYmWlvkEhDxjz75xRqim-CkVlwFyp7sGhTs1fE02MZ6GQcc-TugrepSaeWapA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipP1U4aCd84U_h3g8MEpgv8pq9jhCZwabhBoaSrJ!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 2048 + }, + { + "height": 900, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/114853289796780923190/photos\"\u003eShir Yehoshua\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAHZJAFx5FCkmcAFprG-WEPO2b-5t5NhJ--SJJSYMZJZ0GomqC4UHWRX9m2jCzP9Ol-6946ZFOMCMB-U-kYh_yCY2j4Dvz8j1a-7shJEUA0Th7Z3XEEhq-sZXNkPIsLIgCEhC5kmZBmsYosfoUT84Nj1zUGhSHvBNk48TCYevBV-90Pq05zrPWXQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipNYOZratGeoLGjr7bfmrm0afYBB2trW1tSCGVgG!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 1600 + }, + { + "height": 3264, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102493344958625549078/photos\"\u003eThomas Li\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA26qHb4pLYNjOZTXpll1D3HmnAjXZ07rr2Zz-ch0Wn_4Mdmv5LxisX8BfppG2ZaOiuEcjVhH3HvmZl_6v7O82lzQ642O_FKNDpmP4qBZlyl5ZyckX-CUrirqasabZOBD3EhBQBCFJ6Pa7fk-DCy96jk_DGhS698LDUMFeoGL8bEoR6m5kmkvHmA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipPhKnMNvHw5E96Ff3gqslvNgUV-QvXVpCGTs0qO!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 4912 + }, + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115886271727815775491/photos\"\u003eAnthony Huynh\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAipzY_qh7VJTETqwToabIYnVUfjFrTrW5WBj0ETYeSc-V-3xDCBbXOET8XiqRo7WJlGbVsKXJGlxNLPcLl0RsJ3VAGUc2bWrcgqpS7RV7iXVVthcQ6HTFkI2rO3cgatt0EhADKJiVAovktGiRq_ZuZdLDGhT4sagrHbVt0-C1_7Tr_tujmi-cmg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipN5zFwjADr1H9M_3vuK_VtZiSjFrEG5ExUJirlV!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 4032 + }, + { + "height": 1184, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106645265231048995466/photos\"\u003eMalik Ahamed\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAarVCNrBaKmBWUXHOP4U7WZd8rmaauKtnM6fCnO2-koLnWbO6sp-2OMJNPMDdN8cs-WUtKB0xGjGK12kK2M_FLgFVUNz2csVZAphTC_ifymWKvjhOwn8-OdDyWVat_-B6EhD_rkah80gHJQ4lC8dkP9A8GhQiN6CIxXDoY-r9D6sulWiC6yPoOg", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOW7tRMp43SJ7GgDoGJpdN2i3sYafkmjWWkU_PR!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 1776 + }, + { + "height": 5582, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/110754641211532656340/photos\"\u003eRobert Koch\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA3R3aWiCuGkTGwq6vQgBbKof6kWWTeuDtsRU22kol7PJgZA0WPRWCu6AiWmnYXZrlATPmOfVZ8dKDddK0j5rvubvOEKwlGbDfw0JWcuvRvSQSAHbfduZ6YB2dnH-GNEvlEhCcXkWYUcJbafIcvGX6MWrMGhT_2WLsk0iCza4xsfF4elmOKgULnQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipOPF6xDOc1EpVoeMD-daLpZibzgJ5BtIbp5Zg-U!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 2866 + }, + { + "height": 4032, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102558609090086310801/photos\"\u003eHuy Tran\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAlOghADZjTAuEpKh6MSi-ibnbPx2JRxNSNMIlh3i9doD4-KQVEQhkKFdCatvEjCg1G7cYLRk2ieb3XGklJQwgZC4o4XlaxSegRgWR-iaoOOrwvuMse8ILKq9ptvLS-qIqEhCgXtSKRON-sF4tlcABo9C1GhRfTdcNZdw_CYx7EwsHY7y7WWt5Yw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMZ6g-KaP5kr4ip-oie2lSd406O0y2cPvx_71XS!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 3024 + }, + { + "height": 1944, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115237891004485589752/photos\"\u003eKatherine Howell\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAVrhb7L7zIBM0r8PIIHkMmUTi1fg4nizerhc6Zv57KBzCL16XTE6MM4DIs_hBCvjklllqLYjhqJi8jSGyh2nrmL1AH2FtNRrcXh7_WmzRzaLxkHiyrJaYhqrpQqVhyoEkEhARcz4HCHCBE3t90nmM40BkGhQRI0pxjUWp_wXJNme20J7mwOWrZA", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMnQDnLhIY5lZgX0AzRUE-51iSi0orgBoaQ_hSE!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 1944 + }, + { + "height": 2448, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116976377324210679577/photos\"\u003eWH CHEN\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAUo6RZHtqdQdTVlgEtugDZu_n02zsk4peb2SZhR_ZYyB0id8FQzcl1OGgMcS3THc3_E32fXv57My-3j_wgTyqXS3MgslluHojeenKfk5_xOIpiFhKtq1uBabZ6KG2hmPLEhBUD962VMILc5HtUmSKOkP2GhQb1aihC_0v3bifP-Pmyr8_lq0UEQ", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipMyLqETfc7XPSejc4UF-J2BqJNhe4zy-18o2yn2!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 3264 + }, + { + "height": 2988, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/109246940950895122662/photos\"\u003eBen Tubridy\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA5SOH6HAgmK2HG-NIFXa2NL0Lim17k6jDKDXa94S4ecTzsEq2Ef6AicJRZHudZekTYO2v5Yzu880DeXm9wnthNf7Uzbuxpwtv8v_ARVLr5uMvQPMbzpQW_yWC6i7GcNe-EhBzrHO09Aldk-cWX3mGcorOGhSvESymcqawdM8p2TwWIFUPesM5Yw", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipM8t9hb2nre80sRFHylMuf5XKfrscDNDgxjKge2!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 5312 + } + ], + "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", + "rating": 4.4, + "reference": "CmRSAAAAI0ZgjiCXFAS6flEUkLNebbolGycomEszy9JzoW6rbZek4rsRQjxdponn8YueSuo4DH-ezn_nnRR35hvMmYGLDeUnloCrji00D3s4iZzjPW5490yHXi7YyeTr5cfiAQ64EhAWAJkjwR28RnNv_xq0rtRXGhTxZeieTZlyCZSwdwImjG2Pr12caQ", + "reviews": [ + { + "author_name": "Mark Sales", + "author_url": "https://www.google.com/maps/contrib/100341567599258416785/reviews", + "language": "en", + "profile_photo_url": "https://lh6.googleusercontent.com/-e2bgb-ognDY/AAAAAAAAAAI/AAAAAAAAAAA/AAyYBF5K8QcyGb-B5_yoiWjlWNTXqBcLHA/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "a week ago", + "text": "You have to FIX your Google MY BUSINESS page business.\nWhy the business owner CANNOT delete photos uploaded by anyone.\nI can delete the photos I uploaded but why is it so strange that anyone can upload and the owner of that page cannot delete the photos. I called the call centre and I have to explain IN DETAIL why i want to delete a photo!!!\nThats a total JOKE!!! its my page so i can do whatever I want!!!! So if the photo that a customer is legit and it was 10 years ago, i CANNOT delete that??? USELESS and TOTAL WASTE...", + "time": 1496880715 + }, + { + "author_name": "Starland Painting Pty Ltd", + "author_url": "https://www.google.com/maps/contrib/106844006614491278928/reviews", + "language": "en", + "profile_photo_url": "https://lh4.googleusercontent.com/-zHEV7zQnWfM/AAAAAAAAAAI/AAAAAAAAAAk/aZukMkHPI_o/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "a week ago", + "text": "No customer support, indeed! \nI went through the most of the reviews provided by people whom somehow do business with Google, and I have found that I am not the only disappointed one!!\nMost, almost all, + ve comments are about Google's geo-location and their food..., presumably written by their employees... . \n\nGoogle is a massive cooperation that has the ability/power to do better, indeed, but why they don't, remains unknown, at least to me.\n\nWithout the presence of small or large scale businesses, Google is absolutely nothing but a Website! The foreseeable future may not be as good as it is now for Google, as some fresh competitors may make Google a history, considering the fact that Nothing Is Impossible!", + "time": 1496982169 + }, + { + "author_name": "Paul Sutherland", + "author_url": "https://www.google.com/maps/contrib/104671394445218170123/reviews", + "language": "en", + "profile_photo_url": "https://lh6.googleusercontent.com/-ZRFv8AHxqEQ/AAAAAAAAAAI/AAAAAAABsfg/HluyrsFH2bk/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "a month ago", + "text": "Very disappointed. I have been a supporter of google and some of its innovations, I particular love android and adwords helps my business. Now I have a review on my site that is in breach of defamation laws, however Google don't seem to care. This is a fake review by someone who doesn't identify themselves. I get that reviews are there to assist businesses better themselves and help consumers make a decision, but when its defamatory and hurtful the line needs to be better managed by google. Having read most of your one star reviews, most of them have the same issue. Time to listen to your customers Google.", + "time": 1493618915 + }, + { + "author_name": "Ranjit Nair", + "author_url": "https://www.google.com/maps/contrib/102808647017735332248/reviews", + "language": "en", + "profile_photo_url": "https://lh4.googleusercontent.com/-A-UJAO1hMtk/AAAAAAAAAAI/AAAAAAAAAAA/AAyYBF5ojcf5EM9vU3KsroX_DEyKMn76MA/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "4 months ago", + "text": "Absolutely ZERO support. There is no thought behind how the whole Google Business Review process works and the only answer you get from support staff is that they are only trained to direct people to the Google support page. Why do you need people at the end of a phone line to tell you that?\n\nAs it stands, there is nothing stopping me from standing in front of a store and giving a negative review to a business based on whether it was raining that day or not. There is no process here to verify if the claim of the user is right or not. I thought the whole process of reviews were meant to be a fair representation of the service the business provides. Where is the fairness here?\n\nGoogle encourages businesses to respond to people's reviews. Is Google responding to the reviews that are posted about their business? Why the double standards Google?", + "time": 1485900942 + }, + { + "author_name": "Ben Cohen", + "author_url": "https://www.google.com/maps/contrib/110685259345133164515/reviews", + "language": "en", + "profile_photo_url": "https://lh4.googleusercontent.com/-NwQ8wRwmjlQ/AAAAAAAAAAI/AAAAAAAAAXU/V8YVXItAB-w/s128-c0x00000000-cc-rp-mo/photo.jpg", + "rating": 1, + "relative_time_description": "in the last week", + "text": "Disgusting Service. No communication on a housing location number error that has caused multiple issues including internet access and will be reported to communications ombudsmen. Complete lack of empathy for customer.", + "time": 1497829502 + } + ], + "scope": "GOOGLE", + "types": [ + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=10281119596374313554", + "user_ratings_total": 458, + "utc_offset": 600, + "vicinity": "5, 48 Pirrama Road, Pyrmont", + "website": "https://www.google.com.au/about/careers/locations/sydney/" + }, + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlacesApiPizzaInNewYorkResponse.json b/src/test/resources/com/google/maps/PlacesApiPizzaInNewYorkResponse.json new file mode 100644 index 000000000..a9f7da485 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiPizzaInNewYorkResponse.json @@ -0,0 +1,942 @@ +{ + "html_attributions": [], + "next_page_token": "CvQB6AAAAPQLwX6KjvGbOw81Y7aYVhXRlHR8M60aCRXFDM9eyflac4BjE5MaNxTj_1T429x3H2kzBd-ztTFXCSu1CPh3kY44Gu0gmL-xfnArnPE9-BgfqXTpgzGPZNeCltB7m341y4LnU-NE2omFPoDWIrOPIyHnyi05Qol9eP2wKW7XPUhMlHvyl9MeVgZ8COBZKvCdENHbhBD1MN1lWlada6A9GPFj06cCp1aqRGW6v98-IHcIcM9RcfMcS4dLAFm6TsgLq4tpeU6E1kSzhrvDiLMBXdJYFlI0qJmytd2wS3vD0t3zKgU6Im_mY-IJL7AwAqhugBIQ8k0X_n6TnacL9BExELBaixoUo8nPOwWm0Nx02haufF2dY0VL-tg", + "results": [ + { + "formatted_address": "7 Carmine St, New York, NY 10014, United States", + "geometry": { + "location": { + "lat": 40.7305876, + "lng": -74.00214099999999 + }, + "viewport": { + "northeast": { + "lat": 40.7318833302915, + "lng": -74.00074326970849 + }, + "southwest": { + "lat": 40.7291853697085, + "lng": -74.0034412302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "0ed4fa342e8c59111d07d80b81f5c08cd6b84934", + "name": "Joe's Pizza", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 2448, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/100036634431043786774/photos\"\u003eMonica Varona\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAFVwHvmdXmVLu9X5a8BurRHXw0UJtCJpYN2r6nwsitA8YWO5cDi6bSIfyMp86Fz7sM5vNzt-6AKiZLnXAkIUoITJEd2upKBzhYCKfXk5TG6DV6y7PJRBIOSSLbqyDHK7KEhCsdukld67xDiEUhCc88RVJGhRi5MxQiazMISVyrRqYcOXdxgL7qw", + "width": 3264 + } + ], + "place_id": "ChIJ8Q2WSpJZwokRQz-bYYgEskM", + "price_level": 1, + "rating": 4.4, + "reference": "CmRRAAAAYIhqgr3lfaJ7P5OZ-Ke19aDGoao616vTuW1V9rANXqwAZjSVGmbQ6B5lRCYo3WHSMW9uPy-tG2idOUJSoBz21SWnOMM4FFIDyTb-_QH7U-9MCSmet2gJEe1U63pNcvYSEhDjkjjWOARnMCXMxVkvyhxRGhTeTU1KTxA5DISFzsVzoyxuTS9ZtA", + "types": [ + "meal_delivery", + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "60 Greenpoint Ave, Brooklyn, NY 11222, United States", + "geometry": { + "location": { + "lat": 40.7296543, + "lng": -73.95859829999999 + }, + "viewport": { + "northeast": { + "lat": 40.7310615302915, + "lng": -73.95725891970849 + }, + "southwest": { + "lat": 40.7283635697085, + "lng": -73.9599568802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "d30990c93215d02648e78b3b0bdb00e373539903", + "name": "Paulie Gee's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 427, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107146711858841264424/photos\"\u003ePaulie Gee's\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAZZcAbgFWXdKXx31eTZVcDHWoFu58FhdwsUtRuDAhtE_7P1TsX30g6VNkGseA0aCunGzBOLCrX0QNE_qXq-eTGn5N0ST-st7-e8NHh6S2MPv8Y3RzEWobmY3QLsOA29hHEhDdoAL5Ma3q6xkfbgmiZ8c5GhT7G5nz0IWjyZqOUpr9LbZ7FdG1Rw", + "width": 640 + } + ], + "place_id": "ChIJuc8AM0BZwokRtpm2S66ltsE", + "price_level": 2, + "rating": 4.5, + "reference": "CmRSAAAA_a-ewscVFb7KoMh8MZs6L_n6C1sPRjanOqEpNM0GAd4ZcnGoez1txSCNLQEMEA1KtinW-DtzoCU-7m3GUoa7Nop9NtIh5yy8qSq0lporUqOSZvAGlJSHhEuhKDIAVRInEhD20ymW2l34EL5EFmJYJcYjGhRQvWcGQv8D8bxiM9ydb6tN2lp4qw", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "575 Henry St, Brooklyn, NY 11231, United States", + "geometry": { + "location": { + "lat": 40.6818053, + "lng": -74.00029259999999 + }, + "viewport": { + "northeast": { + "lat": 40.6831822802915, + "lng": -73.99904686970851 + }, + "southwest": { + "lat": 40.6804843197085, + "lng": -74.00174483029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "bfa17bb2a401684b663bffce96754b388c8b0a38", + "name": "Lucali", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234/photos\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAYppym7d1r_trS7IKMKUFWzjlbB3YVgOn1JGbMMxnzDihPjRG3_UprLQYaUAzYqTq10qIhofOoURW_4tKDHIegiRge6SwuV2BjBT4GqM-ysUmIc8qrD5ul1K7BBsd0VhyEhB5ocBHhWi6FUhSOpKaGmSYGhQ_U8ME8VOywO7HlOLP5XOLzz3tCQ", + "width": 2048 + } + ], + "place_id": "ChIJ395CMVlawokRt2oLH_8zmvI", + "price_level": 2, + "rating": 4.6, + "reference": "CmRSAAAAElJbjjMU9aXlpNhXBtRIr8HOZtjSOCNrBwbQi1vrN42NdZaoiyYJ-rJB3dkY8rhVIcTkMLhtsqFcMNlfi9Rz0tkVugAmC1inGStCD7Gbzh0XWpokngczj2VkCjgjzuf0EhColfEtv4U-9cC7VYf9fZiTGhRie0XLLFCY2Gt0OPQOLL947i5iJQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "298 Atlantic Ave, Brooklyn, NY 11201, United States", + "geometry": { + "location": { + "lat": 40.6883065, + "lng": -73.9889778 + }, + "viewport": { + "northeast": { + "lat": 40.68974968029148, + "lng": -73.9875655197085 + }, + "southwest": { + "lat": 40.68705171970849, + "lng": -73.9902634802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "5da5ce5a2ee9bae83c5cd1c49ab0e917b0f0e8ab", + "name": "Sottocasa Pizzeria - Brooklyn", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2322, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107207602668522843168/photos\"\u003eKris Gamache\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAArYdDroIHcNAdulIlOVi7fz-WSH2UYzBevk4NurBm2726MpN04QFXVH6bfJESMtf9HKrojiuFxdXAF4_flaFiZpYDIK5pFh2RH7M1Yxfc31iHI0gwzVMX1kNiKzvChwY7EhC4Fkf3GtB5tbxXxCfOZaGfGhRyG_Ky9VbiLxVNbJUsZUYPnwvaAg", + "width": 4128 + } + ], + "place_id": "ChIJjy3QdU5awokRPUY7-kf_A_0", + "price_level": 2, + "rating": 4.5, + "reference": "CmRSAAAASyM_fqeaJvHQiLQrHz4BmPI9O6wILy-TrQWhJnXr3pwTmjSC3gX-YBwy-WWTQQBVa6DADOGL3ij-0TmrPRMwTS_ULFlQQefUwK7BfqcKi_9Gfko7XnZAO-XA65Ky5S2REhBxIlwjD5AD4oZADxdxo3xPGhTMMCiYG25LuOI1hF9DKUH7CJhRlg", + "types": [ + "meal_delivery", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "33 Havemeyer St, Brooklyn, NY 11211, United States", + "geometry": { + "location": { + "lat": 40.7156134, + "lng": -73.9534061 + }, + "viewport": { + "northeast": { + "lat": 40.7170117302915, + "lng": -73.95213716970849 + }, + "southwest": { + "lat": 40.7143137697085, + "lng": -73.9548351302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "c1cad2fa8702d02e533fd9ddd73fdce8c464e248", + "name": "Best Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 375, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/108936034325857950491/photos\"\u003eBest Pizza\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAxVs6dTuBmFJ_3hA_MqVlwz1-glMWFCkevugVU4KeNfAN6yK-YuR4G9CeRYz2VmVXRomhkl5CJ7bRdfpyZZJwreuz7jtN4HXqU45SsW0hXOgwJ3xcz2bC5SNtlMfHOb9jEhCM78kHcML8hlx7GXTcsn-NGhS8zKpxp0gG3NfMtsq6K0vAV-Gdrw", + "width": 376 + } + ], + "place_id": "ChIJzWhpTVlZwokRRyrw-O4FIxI", + "price_level": 1, + "rating": 4.3, + "reference": "CmRRAAAAJjpg0ufqSoy8psHMRIb2mO_Le0tyStw2s8gSKDd62wdVUJbS48np8gOpZyhhYd4ZTA-DZsuEBI9sKP78f8hgoiNeexHh5MINWVkCspPxKYEDRmIv_trZp6kWLp4yLjH_EhCzzaTHkDZEsqgoJo7gmY18GhSInelWOJSQ5Mzr4iSeHVEmOBZHFA", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "261 Moore St, Brooklyn, NY 11206, United States", + "geometry": { + "location": { + "lat": 40.7050766, + "lng": -73.9335923 + }, + "viewport": { + "northeast": { + "lat": 40.7063404302915, + "lng": -73.9322154697085 + }, + "southwest": { + "lat": 40.7036424697085, + "lng": -73.93491343029152 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "3346c75baf04f2affe179999522b012c9a4f96b5", + "name": "Roberta's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1944, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111529711728905843636/photos\"\u003eKathy Ho\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA5uYQfwnr_3CMIJbKB-pSKJeewX0RjmUgIzw3wmgNvJ7uwLMaNSGBsqwZWWVFV7bqrD35itqfMPbG8WZbsnAvSL3Ub7OhaoRn1_BtuTI4fk8rwWd4BaUPsJGptcgxFUYXEhCXZNKXNFbLHarGfhvfYr32GhSUtUjgbBWMYIaoj2hSbwpxFdQeJg", + "width": 2592 + } + ], + "place_id": "ChIJ87Mc5wBcwokRAj4JNcwppaE", + "price_level": 2, + "rating": 4.4, + "reference": "CmRSAAAAGRagGzT_V32h2VD58yUSMB1PVxRRK7YN83EkWu2HLUSUbCe9EcRRnJzSI8huWMXzsoNZC5nJlhpgmYEZtemtK72SC6IOKERzPXFG2JUryLmKWUDFq9uCEAcHSUTbvE0pEhBd_KFP0ly8rXN-QRNV66ySGhQqf1ZPa-iBij_2gKSbkrF0p87qTQ", + "types": [ + "bakery", + "restaurant", + "food", + "store", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "1424 Avenue J, Brooklyn, NY 11230, United States", + "geometry": { + "location": { + "lat": 40.6250156, + "lng": -73.9615451 + }, + "viewport": { + "northeast": { + "lat": 40.6264367802915, + "lng": -73.96013816970849 + }, + "southwest": { + "lat": 40.6237388197085, + "lng": -73.96283613029149 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "3aa44fa0defb16c1da0b12f4a78aab526d9eb6c9", + "name": "Di Fara Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 640, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/108433417989960518643/photos\"\u003eFeiona Chen\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAeA8qh3IKfDWyBKLuoqeT5YqZuJXUeRYb7kpwlP_kBwQH8c6xXj4H3awbQ8JYG8AniomIJMLCVia7Y7yS77sWVwT9GRroM7Nzs-xPMG8fmiAoGbb1iW4iovcQwPAknnzQEhD1o2xweQuPclU-ZEt4N1GsGhRCOiD51M5tSL0CnUk3TcgHFIMJEQ", + "width": 852 + } + ], + "place_id": "ChIJM2mGRMhEwokRv1Fy6oFJ570", + "price_level": 2, + "rating": 4.2, + "reference": "CmRSAAAA5TJhAjYX1RprwCj4DSBOe8ijKOyw1wqOxKBQU4HdaRGomJbZuP-Kff8ufEBe9F2a3y2KVzbY_gU_ZiEeXPKqiAR2o1GWs7n56SAnNDgZMBcHbp-378Evwe5j5fB29o-IEhD2hl1alGI27KcFnkh9U4ULGhT7gB9SJ714F2eJL1EPkC7H-6ycwA", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "413 8th Ave, New York, NY 10001, United States", + "geometry": { + "location": { + "lat": 40.75018499999999, + "lng": -73.99527999999999 + }, + "viewport": { + "northeast": { + "lat": 40.7514759802915, + "lng": -73.99379466970849 + }, + "southwest": { + "lat": 40.7487780197085, + "lng": -73.9964926302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "5ed03945fe2d1238dba9bb50ac5a2d19f5062f4f", + "name": "NY Pizza Suprema", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 562, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/100709142566631664321/photos\"\u003eNY Pizza Suprema\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAArxilUn4-bD39-_Siu6KfPsrBwoZbMssy1l9EYEnaea5opbPd--y4Wd2Vi10DywLC7PGpIsC48fT5TauhkSQBqXKhRktOVKBUl9BvYamVTcOsNqcGzresA9Fl6PVEmvrWEhDnJAYXEq-FCPaMhg6CTDpkGhSFbGA2XZOY8qRxD9I3eHS_xa0MaQ", + "width": 1000 + } + ], + "place_id": "ChIJsS_-2rFZwokRWYvlpNNbjmg", + "price_level": 2, + "rating": 4.4, + "reference": "CmRRAAAAotpdUZSkN2CMcDAYSZ_CJl_XrdzolvsKqMFYRmnRDDZ6jQS7XDdIIeIEjHt3WqvyO4xCdiSPOWAKPxO6fHLQbTwVucpUIolQ0RT2ayrWOuMmhAHvwQjnvueXHNoSZpjmEhAmk53ZlK7c5cuU40ttrIB6GhTFlJxP9zBC0d0Pdd2XldbLh632dQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "27 Prince St A, New York, NY 10012, United States", + "geometry": { + "location": { + "lat": 40.723084, + "lng": -73.994528 + }, + "viewport": { + "northeast": { + "lat": 40.7243418802915, + "lng": -73.99323316970849 + }, + "southwest": { + "lat": 40.7216439197085, + "lng": -73.99593113029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "d7333da0ecaf45403c9b41008d7565e61a756946", + "name": "Prince Street Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 928, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/101075367388701077847/photos\"\u003eKata Crea\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAW3CyLF9Z0O-N0yfifyr_YkLDaqDX2d_BIRY0P92XGAZen3ESZQ6i0fgYAvbHDm2eWQeBJOz940IEw9geSiDi7s6qHmtToix5SoQmNuAoKsYBrdb9k4PYlUZ5UrC6oo2FEhAWn6e65s4CXc6Ri3YuCVs6GhQZXkWnF1UzqKKU8HsbKBy2F2Ei2w", + "width": 1264 + } + ], + "place_id": "ChIJ6xvs94VZwokRnT1D2lX2OTw", + "rating": 4.6, + "reference": "CmRRAAAAscqOdiOWNFFsqFNPyGrvFJZCUzySDD1MJ2UKEfv-jl9V_7mWPluGoPfNTo71QGWSeCNGx7tse1C8oVZoLGYYS1RygE5XqxXbGVt5R5cHGtDCv8ZPbvOEtAlTNlL0STI3EhDMtaFZAzi9PDkBn9zE2imOGhSbuDRT5VcoOZ03ysfcdCrgKMTtKQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "69 7th Ave S, New York, NY 10014, United States", + "geometry": { + "location": { + "lat": 40.73218879999999, + "lng": -74.0033937 + }, + "viewport": { + "northeast": { + "lat": 40.73357833029149, + "lng": -74.0021448197085 + }, + "southwest": { + "lat": 40.73088036970849, + "lng": -74.00484278029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "7e3f519a691dea53fde5703d169ee2fa6abeda4a", + "name": "Bleecker Street Pizza", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107861206092985225681/photos\"\u003eChris Gonz\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAKTSw0fAvLNrXl2g9ZlXFvznF8qAVNPxY6PAxW0APDmCJnevSLvW1gwF72p_XPbx3v7iT4B3CgoGD-NzrRuLHqURqyP40F2Jg9khLkGdhOezgPEzRJ97MpkpzxlC7uHkDEhD2nZKP_JazShnqaHBsZQ1sGhSuZou1byXn3uoV11mZ-X0JV9oFTw", + "width": 4032 + } + ], + "place_id": "ChIJrXXKn5NZwokR78g0ipCnY60", + "price_level": 1, + "rating": 4.2, + "reference": "CmRSAAAAp2SFj3sL13HTi11nSTFr410PzoCg4JPxxOxxPIPSb8sBBQXBHHZM5LTfMn6cFd3nDKRE8Q4F9GE7L03YTUMBgf3XyIWbwREhgfAivGj3sLGypwG4QRm48vIgsBSODkMZEhDotPwRmoDlSmAvbYa977T-GhRbOzan6tU9JPvKIZFjGhiqA31ubg", + "types": [ + "meal_delivery", + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "278 Bleecker St, New York, NY 10014, United States", + "geometry": { + "location": { + "lat": 40.7316187, + "lng": -74.00344679999999 + }, + "viewport": { + "northeast": { + "lat": 40.73299928029149, + "lng": -74.0020021697085 + }, + "southwest": { + "lat": 40.7303013197085, + "lng": -74.00470013029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "4e321140541ea1ec775cddbaa104f977e955e858", + "name": "John's of Bleecker St.", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 960, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/109748343324805422333/photos\"\u003eJohn's of Bleecker St.\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAwhbvA3jytZocQC4340f4E3W0ALFLYbtEH4AdWnGyXj_3C-g_DQ2OU6ahEeapUBWuQoO7r09W3pqW41iAG01X0OqgWSavXTsWPYfL9A9gW2G7ad3puvmj3SxDVRWzrl_8EhDZz38-mGj2uAnC51mpZ8Q2GhRRDndjxbESN5EXCWARik5-2Ve6KQ", + "width": 1280 + } + ], + "place_id": "ChIJuW43oZNZwokRdE5tLzpuykE", + "price_level": 2, + "rating": 4.3, + "reference": "CmRRAAAAM-aMf1jvMLwS67HkzKV09iCmQKP0Bu74kckSzc3OXCtN-sFPOVa-xNqRzYU5JO0BxmQChqTjCmHDZI0PWXLd2vZqcVJU5Qr3MIUPCW0QlracdozhyfZMJrydjlawnjl_EhCSdXFUcCr0Uz6DW1pCygYOGhRGxGgvdtLZwEUHMEVbCV0MyEVGDQ", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "376 Classon Ave, Brooklyn, NY 11238, United States", + "geometry": { + "location": { + "lat": 40.6875171, + "lng": -73.9600013 + }, + "viewport": { + "northeast": { + "lat": 40.6888816802915, + "lng": -73.9585224697085 + }, + "southwest": { + "lat": 40.6861837197085, + "lng": -73.96122043029152 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "253dc3bab17a12464af83615d7f93df6b11761ca", + "name": "Speedy Romeo", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 360, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/100259178945970644281/photos\"\u003eSpeedy Romeo\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA45jcuA5XlClnR3nP7_woc-imwVqcP6XGAcF3XdVqVIQLyUgGyhyDmARVsKSDxDXXbDDGpE8EbtVuW_oP91sXKGD0STrBHAZL2lZSSgmnu4h0dcfBsyB897cCom4Q17wUEhCdIPL2CwY2zkNcz5-5UrQ3GhSpWR3lNzsjCLWg8tcsJd_9k6OAwA", + "width": 360 + } + ], + "place_id": "ChIJOTob_r1bwokRtUoNNbcmw_g", + "price_level": 2, + "rating": 4.4, + "reference": "CmRSAAAAMGAXc8cjUn6764SijlfN4Dp1D_iDVwHTQXpwpulS-tk_o4lAREAOH5y4ekv_nAoofZ6nVb3fcvCnJ6mKvxbZiBANuzzsfA31TLdHRUTGi9bCXbWL8aFrTPKTtJr8u4gbEhCAJlZosVslwdwVrR6QacLlGhQzNzdmIhp0lQy6ycXit2oV0nz2kw", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "231 W 46th St, New York, NY 10036, United States", + "geometry": { + "location": { + "lat": 40.75946800000001, + "lng": -73.98677099999999 + }, + "viewport": { + "northeast": { + "lat": 40.7607614302915, + "lng": -73.9854879197085 + }, + "southwest": { + "lat": 40.7580634697085, + "lng": -73.98818588029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "f846f7d4dfd546c188f700c627e955ce1c633714", + "name": "Patzeria Perfect Pizza Pasta & Grill", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2268, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105498547919624383340/photos\"\u003eDinh N.\u003c/a\u003e" + ], + "photo_reference": "CmRZAAAAYZ-bxYOM9qQYtGfBw8dbzvIbKmBAiSv8SmAZDZDldOpbQ1WNTbyPk-yQklVdXbO9_zVNYt0lx1tZEuIu-JzAIRG86t3WH12Y4Z7IprbHT8O5iqtpkyk9gc_A27Y5arLVEhCszI1Mv2AlhiRU5_NECuxDGhRwYXN9cXYu3NHen9vV0yUwcx-0Eg", + "width": 4032 + } + ], + "place_id": "ChIJIcJCN1RYwokRHQzLh_iGQwI", + "price_level": 2, + "rating": 4.2, + "reference": "CmRRAAAAXQOnmfSP-smtVuoI8ha_MpIUh7Y_xQ4vO87suhLMSHafrmKDGke_JHPa9s8H-mBdWLHwgjU1aVHUC5NBOOJv0WY4ffde-q8KCGIeWFNEBflnIKc4swtoAsNhe_Js_QkSEhBpGodm7r-X-xNYtKcv-CNhGhQZpNIB2t-BzVlNCajJrq5nccWxQw", + "types": [ + "meal_delivery", + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "309 W 50th St, New York, NY 10019, United States", + "geometry": { + "location": { + "lat": 40.7627178, + "lng": -73.98670760000002 + }, + "viewport": { + "northeast": { + "lat": 40.7639866802915, + "lng": -73.9853812197085 + }, + "southwest": { + "lat": 40.7612887197085, + "lng": -73.98807918029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "29b4e15386fffcea6d01bc0ad6dd6fd475e4bef7", + "name": "Don Antonio", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 3200, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234/photos\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAz3ou1VcNwHR1MmXGqdHUimgIqX9_uvRPPvOpOeW5YWCUJrJubVPg0yi0HM2HDxMmsvMltAgGzB9KBCzLM4ecOQc_K4d6oIaE79Dm6lspBys0CsgCBbpI1IhH6w76k9hnEhDg5dm8xS8yBv6P5wDaipchGhRmcYXI24Nip1mMuVzrn0pIWPxIUQ", + "width": 4800 + } + ], + "place_id": "ChIJN_hR41ZYwokRB8wicROs-eY", + "price_level": 2, + "rating": 4.4, + "reference": "CmRSAAAAtL2brKuUbJbdOTnhS96Ksnqd4i3dYMuL8UCVm0wilDIJg8GaEpceg-CSM-JvDEDoWD-8gA2-NPmfRPdofwg2Yei71-Qvhy6rxrkgtBgUohC_VYiNeiQDGSLdNEGv40ZpEhCIp-s_MYf1x-YPMh3jfBm7GhQRhK8vb6F3h1Dof5LjQT_1x8Ppxg", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "348 Flatbush Ave, Brooklyn, NY 11217, United States", + "geometry": { + "location": { + "lat": 40.6760157, + "lng": -73.97180779999999 + }, + "viewport": { + "northeast": { + "lat": 40.6773959802915, + "lng": -73.97038446970848 + }, + "southwest": { + "lat": 40.6746980197085, + "lng": -73.9730824302915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "4db7233be20cdcdf9c167533f9fc1d377e3054c9", + "name": "Franny's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234/photos\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAbYamEQbQ_Y1eOvDkuwffy8CHeAeY23YZ9zki5uU7QPsgAmxL56Hoy0N2GukW97IZ3_2VIs9c7JpBBDgsybtEqvMq53bvSLaSsV7rTFasGm7y3yG-gYOZXb-3dZUlQbEdEhD56KQQQkiJost8T--cUVDcGhQIv45ddG_wcN_lfChytkAvVBZM9w", + "width": 2048 + } + ], + "place_id": "ChIJFfWdcahbwokRn3GElHMDJog", + "price_level": 2, + "rating": 4.2, + "reference": "CmRSAAAAtrT2Ome-gB0wH9cc7pKr-N75w7-syKweXv7ueSUB5n6_knUiqjb0YKQu4kM11ndmGB6yaiHNCOGkVfNZIChe5v5DOqLlDViaSdPHq_oqOYkbJZCXRz_s6NVaYJFnelNKEhDuk5ubRqYx0Fqxl-X4NgtMGhTUVkOH7MTG4BYqUE48Gc05rgIsyQ", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "1524 Neptune Ave, Brooklyn, NY 11224, United States", + "geometry": { + "location": { + "lat": 40.57891729999999, + "lng": -73.9838232 + }, + "viewport": { + "northeast": { + "lat": 40.5803495802915, + "lng": -73.98248746970849 + }, + "southwest": { + "lat": 40.5776516197085, + "lng": -73.98518543029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "8f216ade74f50c055d6c5325bb92ce572ff13cce", + "name": "Totonno Pizzeria Napolitano", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2610, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107459595472833821290/photos\"\u003eKurayami Tenshi\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAA19DR-60US4oW_I1eyWMieK-1ORuHlLHw2wE1TPLdfTAemPo8mWiEo3lo2sFWvwkB6ikUQc2t9QNNvggtGlReyep_t77N3M7wCX26TPr3hyrp08DXRQ8NxZUhdoslbgafEhD5jB4wQxzUTP7ChYcDuq8ZGhTGNNxy_GP3DVDQ6JYcUrM-ZvdhrA", + "width": 4640 + } + ], + "place_id": "ChIJDb9hOrVFwokRkpRqzb71OdM", + "price_level": 2, + "rating": 4, + "reference": "CmRSAAAAffDD35N6OaCcbq6cAbcCE9NQjqFIDwMxn7fQIYnIx0j0DIjVJoflUrbvVFds6ZHy4FY5PgYhyLxu4TfATs_QYOSNwMv8aUut3Xp51UWtaRl8tUN4naFjqMcnTdfHUltuEhAL7YGfPlWiRVshe9kg4JV6GhTSxvxNrLJH1GYeW-cEiaxjWd0KEw", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "19 Old Fulton St, Brooklyn, NY 11201, United States", + "geometry": { + "location": { + "lat": 40.7027325, + "lng": -73.9934349 + }, + "viewport": { + "northeast": { + "lat": 40.70400428029149, + "lng": -73.99215921970848 + }, + "southwest": { + "lat": 40.70130631970849, + "lng": -73.99485718029149 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "6d3c95038625442207d3f34fe5dce925fb06b934", + "name": "Juliana's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 867, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/115599262880008137921/photos\"\u003eJuliana's\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAeeP-EUsinJGsgqzWjJme5GX6v4oTdy6AeFSUEPXHCDyd59-8JT_DfcgJeN3_pFEyKGih8paGMDthYGn1zV1Bf5VIN3upQ0o8t8IYPFgwiTEJ2WUqWpMQuAmj2kENRNkbEhAluit1H8qddAsKClZdJTtdGhS1l1_G78RqUKrLBoAJqervfHXn4g", + "width": 859 + } + ], + "place_id": "ChIJAQr5tjBawokRBBESUv8Tw5I", + "price_level": 2, + "rating": 4.6, + "reference": "CmRSAAAAJ3P1qCky_cgKJKCFmYoKcO6BXfuf04dhwgCt_jEhfc5wFZcEnQ7TAogn7XZ4IJC3D2dDV-__Y26pN5YRgnRcq4T8fRXC92z-T4ACdRZ6m-Fl22rgKIWS5HrKU3RMZtLtEhAc-tdE3XbqWLdMmS4TnGHgGhToUSF9iygO_iiNNC2Q7uhj8i-bLg", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "32 Spring St, New York, NY 10012, United States", + "geometry": { + "location": { + "lat": 40.72157, + "lng": -73.9956368 + }, + "viewport": { + "northeast": { + "lat": 40.7229268802915, + "lng": -73.99421571970849 + }, + "southwest": { + "lat": 40.7202289197085, + "lng": -73.9969136802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "73973bd1fac905f102ee1afe536594dc42bca5ff", + "name": "Lombardi's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2160, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107603344490074518603/photos\"\u003eManuel Vilanova\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAT-o9bC0KU4X8DVPdfGpeNnSaNM21vvuNC3VqXS9jvJo6Ze8w-e25X1zBUySbj4hHmNDrz9qTeVTEvQwpr8pcmBPUPUcujPDdhm1kzWKuiQYk4s9UWN_YD4ENVGb27G28EhCeQrNekiTQF0gO9OTM5T39GhTPV-zqhNN1Ohx3Msfp5jqJRnvGGg", + "width": 3840 + } + ], + "place_id": "ChIJp-cWE4pZwokRmUI8_BIF8dg", + "price_level": 2, + "rating": 4, + "reference": "CmRSAAAAXPVFBIK2bB8urFYN17bcfWGFMTleh5JcQcUW2f2Mwc9P25OGoblq8kWgcNFdPpSVO0mDT1ECvetsNaf70feTtlMd4izJb1oL4hrHfGxq0rtnx4pHEnu3BJUIuceZIXYwEhAkNFjJSM0vzIS9I07wXNXVGhTtSDre2-AiemL9cISxtu1m1hr2kw", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "349 E 12th St # A, New York, NY 10003, United States", + "geometry": { + "location": { + "lat": 40.7303588, + "lng": -73.98382769999999 + }, + "viewport": { + "northeast": { + "lat": 40.7316292802915, + "lng": -73.98256491970849 + }, + "southwest": { + "lat": 40.7289313197085, + "lng": -73.9852628802915 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "f1b210347c98e6c19a7c60c81688685724d12159", + "name": "Motorino", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113572404967976805132/photos\"\u003eFelipe Barbosa\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAe_Mcl46kMx0EvwcV8ari2El_f5GCwGa0Etpsk98e_1EnseFPqKqLxc551Ff_RbCQuJyp8HQdvcuBMt80mZ2bwXhhXbstm3j-09CJU-Gc6aV_1EtGr39e2wzd6ATIjs1qEhA7uA0DftG9tmU5uLyZ2492GhSU6hn5H8zzNhLIxJIBUn5pTD0JiA", + "width": 3398 + } + ], + "place_id": "ChIJT_Db4p1ZwokRr5qgbjIqrs4", + "price_level": 2, + "rating": 4.3, + "reference": "CmRSAAAAAmfgfCwbweiWZIC09ZUd_TCy-x2vDJinCcpdYZs8Z0JRa3I-FNo-TvezdVFVxehsEgpG9rGwgAShpQrWGe7lDSJvw3L50ZNlTBBxVLlNgAxU62xc8iyhkw92WAJp4C_wEhAE58w6K-_w5eAFSMjiZKaaGhR5auEONCOO_q-bHl-Z9NMD1RQl1w", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "943 1st Avenue, New York, NY 10022, United States", + "geometry": { + "location": { + "lat": 40.7551327, + "lng": -73.9655185 + }, + "viewport": { + "northeast": { + "lat": 40.7564371302915, + "lng": -73.9640611697085 + }, + "southwest": { + "lat": 40.7537391697085, + "lng": -73.96675913029151 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "7329c2ba42f553fb6a838793d8ea5e7beeb9c3c0", + "name": "Domino's Pizza", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 3024, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116692611687391915231/photos\"\u003eDomino's Pizza\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAwlOISx663qoK6cDLZNZxDmLeynjspSBrfXramujJHfCEN2aY0bWI8OF5RQd8n1_LY7-Fz8Jn577NrjHsI3WG9A4xertK8DRnp4Z0wsj7FzmNYQgRAS2uV5jd6HcPZ-CFEhADnx-NmZ0TmpoTiAXUkfejGhSzBgS_oXbrQHugrAF3lAs2yae54g", + "width": 3024 + } + ], + "place_id": "ChIJKwxI1eNYwokRm3xaHqGXSLc", + "price_level": 1, + "rating": 3.7, + "reference": "CmRSAAAAL2NztWtoZbo2J8W-Po1KLZAdMVv-1RrPa1fRObJ8nnSeAcAt2H14JKNrBvdsax0VFKI-EaZdTF5N0uroC_yXNK74txOKFSjbntarZ5JJBM0QD80qMMwO4mzPX2rLTGi5EhBAKMS5ZyyFboX8jpzDdpWeGhT7lG1AKa0-3D1lARbwi9AYuIy6Qg", + "types": [ + "meal_delivery", + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/PlacesApiPlaceAutocompleteResponse.json b/src/test/resources/com/google/maps/PlacesApiPlaceAutocompleteResponse.json new file mode 100644 index 000000000..b948f1c20 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiPlaceAutocompleteResponse.json @@ -0,0 +1,293 @@ +{ + "predictions": [ + { + "description": "Town Hall, Sydney, New South Wales, Australia", + "id": "90f95efa6e988cfd6ec4b065b15586128ebdd94a", + "maps_api.places.PredictionInternal.extension": { + "score": 1.044831037521362 + }, + "matched_substrings": [ + { + "length": 7, + "offset": 0 + }, + { + "length": 6, + "offset": 11 + } + ], + "place_id": "ChIJRddlKTyuEmsRjNjH0KjZHwY", + "reference": "CkQ7AAAAn8G90QPO-HBvGWdOkPcy8pxYjWo-g_QxAimuBEtrFPh-SCjcx3ta-hXDI-b6d6uMZPdYMti8AGIqhkM7OlITQRIQCHnWAFde9BIcT7UtOCHpPhoUPuIJRAG96IEZbfsA4ctXs4j--Bc", + "structured_formatting": { + "main_text": "Town Hall", + "main_text_matched_substrings": [ + { + "length": 7, + "offset": 0 + } + ], + "secondary_text": "Sydney, New South Wales, Australia", + "secondary_text_matched_substrings": [ + { + "length": 6, + "offset": 0 + } + ] + }, + "terms": [ + { + "offset": 0, + "value": "Town Hall" + }, + { + "offset": 11, + "value": "Sydney" + }, + { + "offset": 19, + "value": "New South Wales" + }, + { + "offset": 36, + "value": "Australia" + } + ], + "types": [ + "transit_station", + "point_of_interest", + "establishment", + "geocode" + ] + }, + { + "description": "Town Hall Train Station, Sydney, New South Wales, Australia", + "id": "920e9f9cebfc003338b7a94546ba5a2626a334db", + "maps_api.places.PredictionInternal.extension": { + "score": 1.030175447463989 + }, + "matched_substrings": [ + { + "length": 7, + "offset": 0 + }, + { + "length": 6, + "offset": 25 + } + ], + "place_id": "ChIJEX9pKTyuEmsRymiRKpjAeh0", + "reference": "ClRJAAAAUtX6dueIy4BuBKlyH7eUu07k4nvHbZn9x036SyKlemPiddMgf3dcMw_6zvcyyZcKcqmYylEj-DG-BdvgoE6wwpKzU2vALskPGtWhu4lOS_YSEFcUbSrWnj48a6b4nEIezIwaFOYJBeZJmnBVrp_2vOfdghM96CBd", + "structured_formatting": { + "main_text": "Town Hall Train Station", + "main_text_matched_substrings": [ + { + "length": 7, + "offset": 0 + } + ], + "secondary_text": "Sydney, New South Wales, Australia", + "secondary_text_matched_substrings": [ + { + "length": 6, + "offset": 0 + } + ] + }, + "terms": [ + { + "offset": 0, + "value": "Town Hall Train Station" + }, + { + "offset": 25, + "value": "Sydney" + }, + { + "offset": 33, + "value": "New South Wales" + }, + { + "offset": 50, + "value": "Australia" + } + ], + "types": [ + "transit_station", + "point_of_interest", + "establishment", + "geocode" + ] + }, + { + "description": "Sydney Town Hall, George Street, Sydney, New South Wales, Australia", + "id": "abc84117fb8dea61fca47c87132b691c0f54864c", + "maps_api.places.PredictionInternal.extension": { + "score": 1.02807354927063 + }, + "matched_substrings": [ + { + "length": 14, + "offset": 0 + } + ], + "place_id": "ChIJQ0GXOzyuEmsRWt_tBiCGFlA", + "reference": "CmRbAAAAulwd3wWyXJnOEYUNSeRV8fv7Bw4edeVBGIdndAnWLvwTi1ogPkelSRE6HVVWlUlPyCH6-fiSbU9rlHJFj2gWdYBX_MxrG8BR40yZGDF936ldq4Ozvs76IdWinxWJArn5EhCQwT5k49qLBlJrl-bQxdb9GhRYI0AZFNjlegf9gGSgrvD4mCPSXg", + "structured_formatting": { + "main_text": "Sydney Town Hall", + "main_text_matched_substrings": [ + { + "length": 14, + "offset": 0 + } + ], + "secondary_text": "George Street, Sydney, New South Wales, Australia" + }, + "terms": [ + { + "offset": 0, + "value": "Sydney Town Hall" + }, + { + "offset": 18, + "value": "George Street" + }, + { + "offset": 33, + "value": "Sydney" + }, + { + "offset": 41, + "value": "New South Wales" + }, + { + "offset": 58, + "value": "Australia" + } + ], + "types": [ + "premise", + "geocode" + ] + }, + { + "description": "Town Hall Station, Park St, Stand H, Sydney, New South Wales, Australia", + "id": "dac45e752670006bded4094c63725d6318cd24cd", + "maps_api.places.PredictionInternal.extension": { + "score": 0.9677119255065918 + }, + "matched_substrings": [ + { + "length": 7, + "offset": 0 + }, + { + "length": 6, + "offset": 37 + } + ], + "place_id": "ChIJywEdez6uEmsRqYG0exSHxEQ", + "reference": "CmRVAAAApQQ7k3h6nbOzAOzqX8Ws7fYGhOpRh6FJl2DPjFB35_4NBnCniMxaBnhtgaTrZha7TiN_FVNxCmkpdJgtICVcWl1Se6ITnnsjuRl6LVIsGk5hVC3guakMoLKAPdQiKi3REhDbxHPHwt29pHsOsI5Bpv_RGhTgZYcasQ6qD9IN3RiBISjlnokcEg", + "structured_formatting": { + "main_text": "Town Hall Station, Park St, Stand H", + "main_text_matched_substrings": [ + { + "length": 7, + "offset": 0 + } + ], + "secondary_text": "Sydney, New South Wales, Australia", + "secondary_text_matched_substrings": [ + { + "length": 6, + "offset": 0 + } + ] + }, + "terms": [ + { + "offset": 0, + "value": "Town Hall Station, Park St, Stand H" + }, + { + "offset": 37, + "value": "Sydney" + }, + { + "offset": 45, + "value": "New South Wales" + }, + { + "offset": 62, + "value": "Australia" + } + ], + "types": [ + "transit_station", + "point_of_interest", + "establishment", + "geocode" + ] + }, + { + "description": "Town Hall Square, Kent Street, Sydney, New South Wales, Australia", + "id": "ccf875ae6c5d6f4f3e070736322cfa57e8067d1f", + "maps_api.places.PredictionInternal.extension": { + "score": 0.95737624168396 + }, + "matched_substrings": [ + { + "length": 7, + "offset": 0 + }, + { + "length": 6, + "offset": 31 + } + ], + "place_id": "ChIJSaVaaTyuEmsRpE-DpuZFAKM", + "reference": "ClRQAAAA5X8KYNvBnwWWgNDhKrkYmQcBhDkUYTuwXbG795GlvDG4P7VSfCuLn1V_jBS4Za0oR3YVQfePNDDc7l4hls6OL8fw7HaQ7v7HH36B6l1Bh28SEITUri9rStLKj80BDF24kPwaFBQ4bslJz8bhrSb7IpdGotyw2VZf", + "structured_formatting": { + "main_text": "Town Hall Square", + "main_text_matched_substrings": [ + { + "length": 7, + "offset": 0 + } + ], + "secondary_text": "Kent Street, Sydney, New South Wales, Australia", + "secondary_text_matched_substrings": [ + { + "length": 6, + "offset": 13 + } + ] + }, + "terms": [ + { + "offset": 0, + "value": "Town Hall Square" + }, + { + "offset": 18, + "value": "Kent Street" + }, + { + "offset": 31, + "value": "Sydney" + }, + { + "offset": 39, + "value": "New South Wales" + }, + { + "offset": 56, + "value": "Australia" + } + ], + "types": [ + "establishment" + ] + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/PlacesApiPlaceAutocompleteWithTypeResponse.json b/src/test/resources/com/google/maps/PlacesApiPlaceAutocompleteWithTypeResponse.json new file mode 100644 index 000000000..768efeaa0 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiPlaceAutocompleteWithTypeResponse.json @@ -0,0 +1,227 @@ +{ + "predictions": [ + { + "description": "Porirua, Wellington, New Zealand", + "id": "1a7c8cd36ed90e5a1324499d3174723473b27320", + "maps_api.places.PredictionInternal.extension": { + "score": 1.082201957702637 + }, + "matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "place_id": "ChIJjzUPKJxWP20RFJmiQ2HvAAU", + "reference": "CkQ4AAAACGwZ9m7SlpMGolWWYVlyngDZZu-5BDLN1a6-kqEvr8NYwQEuQMWbAlIL9vBau9BibODXQwRZMRZ6VTAyVRSHExIQOx9vy77Fb_tdmoYB48rLKxoU69-wQlCenJG6Wzdezl2F6GeNOdg", + "structured_formatting": { + "main_text": "Porirua", + "main_text_matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "secondary_text": "Wellington, New Zealand" + }, + "terms": [ + { + "offset": 0, + "value": "Porirua" + }, + { + "offset": 9, + "value": "Wellington" + }, + { + "offset": 21, + "value": "New Zealand" + } + ], + "types": [ + "locality", + "political", + "geocode" + ] + }, + { + "description": "Ponsonby, Auckland, New Zealand", + "id": "eae007b4ec42dc79e88f4ed0beb1eb3083bb4157", + "maps_api.places.PredictionInternal.extension": { + "score": 1.082027912139893 + }, + "matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "place_id": "ChIJ3abyGptHDW0REPmiQ2HvAAU", + "reference": "CkQ3AAAALW2Bg1iU4XVvz1E1qmibSUXYXFDuZye85hAHSnksMcZ2qK7n5eN6fcsR6wBl_tzb0GpbQ7GFzgWUOuhHuvpchhIQPSsuJ4wHUUrMtUyXr6hZoRoUnelZpuTUmDo1QXw2xTnc8p9uDOI", + "structured_formatting": { + "main_text": "Ponsonby", + "main_text_matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "secondary_text": "Auckland, New Zealand" + }, + "terms": [ + { + "offset": 0, + "value": "Ponsonby" + }, + { + "offset": 10, + "value": "Auckland" + }, + { + "offset": 20, + "value": "New Zealand" + } + ], + "types": [ + "sublocality_level_1", + "sublocality", + "political", + "geocode" + ] + }, + { + "description": "Point Chevalier, Auckland, New Zealand", + "id": "4467d6c4268e1a1379972d4e6eb4966b75a56261", + "maps_api.places.PredictionInternal.extension": { + "score": 1.059834480285645 + }, + "matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "place_id": "ChIJjc78cz9HDW0RgPiiQ2HvAAU", + "reference": "CkQ-AAAAEQTkKwXdwqiWXhNeUYeJzkBwp8-Mqf9mSEEjZFUFtqZ5wjDvAgW5BKp-2DT3R7WjFWST8jtbw58v_wq5UNe-EBIQDY_RKKyfMbDkCwp9JIDscRoUopRGy656chf-zORGnShCG0TW4Po", + "structured_formatting": { + "main_text": "Point Chevalier", + "main_text_matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "secondary_text": "Auckland, New Zealand" + }, + "terms": [ + { + "offset": 0, + "value": "Point Chevalier" + }, + { + "offset": 17, + "value": "Auckland" + }, + { + "offset": 27, + "value": "New Zealand" + } + ], + "types": [ + "sublocality_level_1", + "sublocality", + "political", + "geocode" + ] + }, + { + "description": "Pokeno, Auckland, New Zealand", + "id": "735c290e5c206baf0c8d57d2e706d109ba36a3fd", + "maps_api.places.PredictionInternal.extension": { + "score": 1.043946743011475 + }, + "matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "place_id": "ChIJJXKXwf9UbW0R0PiiQ2HvAAU", + "reference": "CkQ1AAAADxdYCFs4YhlsaFGjEqBnKhDOcNuATbpqHe3hURtoAV2IO2RpxGU61-45ix_9tJqIgLKmojr47YUZcJbC_FpIMRIQAbWg9de_RRf0BLsJpQg_ohoUWO3zYEoQZvCq-oXD2GyqmYI46Ws", + "structured_formatting": { + "main_text": "Pokeno", + "main_text_matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "secondary_text": "Auckland, New Zealand" + }, + "terms": [ + { + "offset": 0, + "value": "Pokeno" + }, + { + "offset": 8, + "value": "Auckland" + }, + { + "offset": 18, + "value": "New Zealand" + } + ], + "types": [ + "locality", + "political", + "geocode" + ] + }, + { + "description": "Port Waikato, Auckland, New Zealand", + "id": "13630c4fef638458c730927bed376d3b1ec8c0b1", + "maps_api.places.PredictionInternal.extension": { + "score": 1.033524036407471 + }, + "matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "place_id": "ChIJC_rFNWOwEm0RAPqiQ2HvAAU", + "reference": "CkQ7AAAAsPlhv7cc13JyjekZSe62NysqqWUkA5vQVfGlLZhgyeq3vR474fD9lzHDH_VE-GgR3pwtAutUNJqt4Fu6WCrlJhIQgyBj3ua5oJIl3QTfluWbzRoUjYbPkjfkaW_HTT5aJwEolOAdo4E", + "structured_formatting": { + "main_text": "Port Waikato", + "main_text_matched_substrings": [ + { + "length": 2, + "offset": 0 + } + ], + "secondary_text": "Auckland, New Zealand" + }, + "terms": [ + { + "offset": 0, + "value": "Port Waikato" + }, + { + "offset": 14, + "value": "Auckland" + }, + { + "offset": 24, + "value": "New Zealand" + } + ], + "types": [ + "locality", + "political", + "geocode" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/PlacesApiTextSearchResponse.json b/src/test/resources/com/google/maps/PlacesApiTextSearchResponse.json new file mode 100644 index 000000000..3d61981e4 --- /dev/null +++ b/src/test/resources/com/google/maps/PlacesApiTextSearchResponse.json @@ -0,0 +1,178 @@ +{ + "html_attributions": [], + "results": [ + { + "address_components": [ + { + "long_name": "5", + "short_name": "5", + "types": [ + "floor" + ] + }, + { + "long_name": "48", + "short_name": "48", + "types": [ + "street_number" + ] + }, + { + "long_name": "Pirrama Road", + "short_name": "Pirrama Rd", + "types": [ + "route" + ] + }, + { + "long_name": "Pyrmont", + "short_name": "Pyrmont", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2009", + "short_name": "2009", + "types": [ + "postal_code" + ] + } + ], + "chain_name": "Google", + "feature_id": { + "cell_id": 7715420665913760567, + "fprint": 10281119596374313554 + }, + "formatted_address": "5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", + "formatted_phone_number": "(02) 9374 4000", + "geometry": { + "location": { + "lat": -33.866651, + "lng": 151.195827 + }, + "viewport": { + "northeast": { + "lat": -33.8653881697085, + "lng": 151.1969739802915 + }, + "southwest": { + "lat": -33.86808613029149, + "lng": 151.1942760197085 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "4f89212bf76dde31f092cfc14d7506555d85b5c7", + "international_phone_number": "+61 2 9374 4000", + "name": "Google", + "business_status": "OPERATIONAL", + "opening_hours": { + "minutes_until_closed": 224, + "minutes_until_open": 1184, + "open_now": true, + "periods": [ + { + "close": { + "day": 1, + "time": "1800" + }, + "open": { + "day": 1, + "time": "1000" + } + }, + { + "close": { + "day": 2, + "time": "1800" + }, + "open": { + "day": 2, + "time": "1000" + } + }, + { + "close": { + "day": 3, + "time": "1800" + }, + "open": { + "day": 3, + "time": "1000" + } + }, + { + "close": { + "day": 4, + "time": "1800" + }, + "open": { + "day": 4, + "time": "1000" + } + }, + { + "close": { + "day": 5, + "time": "1800" + }, + "open": { + "day": 5, + "time": "1000" + } + } + ], + "weekday_text": [ + "Monday: 10:00 AM – 6:00 PM", + "Tuesday: 10:00 AM – 6:00 PM", + "Wednesday: 10:00 AM – 6:00 PM", + "Thursday: 10:00 AM – 6:00 PM", + "Friday: 10:00 AM – 6:00 PM", + "Saturday: Closed", + "Sunday: Closed" + ] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105932078588305868215/photos\"\u003eMaksym Kozlenko\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAAVhDev3bZzED0zPM3AqQYbvF3-DmRKHKUTtggxKX2qbjnTe1nAWCXqs0yuKxPHwK3ATHq1azZH3XkSV-rvbO5g_3jOoTVEhTB02qCrc2_Zp5Yj5h7BRwlYbYFlrRRW2BqEhBMtjEJlxR1NoJwe8V2qzh2GhR21oKlYLhjoAlVbftM7MpiG_Ei4Q", + "view_in_maps_url": "https://www.google.com/maps/place//data=!3m4!1e2!3m2!1sAF1QipP1U4aCd84U_h3g8MEpgv8pq9jhCZwabhBoaSrJ!2e10!4m2!3m1!1s0x6b12ae37b47f5b37:0x8eaddfcd1b32ca52", + "width": 2048 + } + ], + "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", + "rating": 4.4, + "reference": "CmRSAAAAgav01cd5HQpE48Sj7mw3F3KCMA7pgz8edJr8DqMCeWshYTweSCVNKzG_6luuFgRn3-4NdujKjd0qIWiaxBHtOZFSyfSORyedfrC8A3l0EFR83ym6pDkmt5uhRpxWlmdtEhDAz7WEjybb7jTZo5iA_jQzGhQW4L7Z5U-VE5HmMXqy71j9itz3Cg", + "types": [ + "point_of_interest", + "establishment" + ], + "url": "https://maps.google.com/?cid=10281119596374313554", + "user_ratings_total": 458, + "vicinity": "5, 48 Pirrama Road, Pyrmont", + "website": "https://www.google.com.au/about/careers/locations/sydney/" + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/QueryAutocompleteResponse.json b/src/test/resources/com/google/maps/QueryAutocompleteResponse.json new file mode 100644 index 000000000..966847d09 --- /dev/null +++ b/src/test/resources/com/google/maps/QueryAutocompleteResponse.json @@ -0,0 +1,184 @@ +{ + "predictions": [ + { + "description": "pizza near Paris, France", + "matched_substrings": [ + { + "length": 5, + "offset": 0 + }, + { + "length": 4, + "offset": 6 + }, + { + "length": 3, + "offset": 11 + } + ], + "terms": [ + { + "offset": 0, + "value": "pizza" + }, + { + "offset": 6, + "value": "near" + }, + { + "offset": 11, + "value": "Paris" + }, + { + "offset": 18, + "value": "France" + } + ] + }, + { + "description": "pizza near Parana, Brazil", + "matched_substrings": [ + { + "length": 5, + "offset": 0 + }, + { + "length": 4, + "offset": 6 + }, + { + "length": 3, + "offset": 11 + } + ], + "terms": [ + { + "offset": 0, + "value": "pizza" + }, + { + "offset": 6, + "value": "near" + }, + { + "offset": 11, + "value": "Parana" + }, + { + "offset": 19, + "value": "Brazil" + } + ] + }, + { + "description": "pizza near Para, Brazil", + "matched_substrings": [ + { + "length": 5, + "offset": 0 + }, + { + "length": 4, + "offset": 6 + }, + { + "length": 3, + "offset": 11 + } + ], + "terms": [ + { + "offset": 0, + "value": "pizza" + }, + { + "offset": 6, + "value": "near" + }, + { + "offset": 11, + "value": "Para" + }, + { + "offset": 17, + "value": "Brazil" + } + ] + }, + { + "description": "pizza near Paraiba, Brazil", + "matched_substrings": [ + { + "length": 5, + "offset": 0 + }, + { + "length": 4, + "offset": 6 + }, + { + "length": 3, + "offset": 11 + } + ], + "terms": [ + { + "offset": 0, + "value": "pizza" + }, + { + "offset": 6, + "value": "near" + }, + { + "offset": 11, + "value": "Paraiba" + }, + { + "offset": 20, + "value": "Brazil" + } + ] + }, + { + "description": "pizza near Park Avenue, NY, United States", + "matched_substrings": [ + { + "length": 5, + "offset": 0 + }, + { + "length": 4, + "offset": 6 + }, + { + "length": 3, + "offset": 11 + } + ], + "terms": [ + { + "offset": 0, + "value": "pizza" + }, + { + "offset": 6, + "value": "near" + }, + { + "offset": 11, + "value": "Park Avenue" + }, + { + "offset": 24, + "value": "NY" + }, + { + "offset": 28, + "value": "United States" + } + ] + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/QueryAutocompleteResponseWithPlaceID.json b/src/test/resources/com/google/maps/QueryAutocompleteResponseWithPlaceID.json new file mode 100644 index 000000000..348a12346 --- /dev/null +++ b/src/test/resources/com/google/maps/QueryAutocompleteResponseWithPlaceID.json @@ -0,0 +1,46 @@ +{ + "predictions": [ + { + "description": "Bondi Pizza, Campbell Parade, Sydney, New South Wales, Australia", + "id": "c478ed4e7cb075b307fdce4ad4f6c9d15cab01d7", + "matched_substrings": [ + { + "length": 5, + "offset": 6 + }, + { + "length": 5, + "offset": 30 + } + ], + "place_id": "ChIJv0wpwp6tEmsR0Glcf5tugrk", + "reference": "ClRPAAAAYozD2iM3dQvDMrvrLDIALGoHO7v6pWhxn5vIm18pOyLLqToyikFov34qJoe4NnpoaLtGIWd5LWm5hOpWU1BT-SEI2jGZ8WXuDvYiFtQtjGMSEIR4thVlMws1tnNuE3hE2k0aFCqP_yHWRNSLqaP_vQFzazO-D7Hl", + "terms": [ + { + "offset": 0, + "value": "Bondi Pizza" + }, + { + "offset": 13, + "value": "Campbell Parade" + }, + { + "offset": 30, + "value": "Sydney" + }, + { + "offset": 38, + "value": "New South Wales" + }, + { + "offset": 55, + "value": "Australia" + } + ], + "types": [ + "establishment" + ] + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/ResponseTimesArePopulatedCorrectly.json b/src/test/resources/com/google/maps/ResponseTimesArePopulatedCorrectly.json new file mode 100644 index 000000000..f04a66609 --- /dev/null +++ b/src/test/resources/com/google/maps/ResponseTimesArePopulatedCorrectly.json @@ -0,0 +1,34 @@ +{ + "routes": [ + { + "legs": [ + { + "arrival_time": { + "text": "1:54pm", + "time_zone": "Australia/Sydney", + "value": 1497930863 + }, + "departure_time": { + "text": "1:21pm", + "time_zone": "Australia/Sydney", + "value": 1497928860 + }, + "distance": { + "text": "24.8 km", + "value": 24785 + }, + "duration": { + "text": "33 mins", + "value": 2003 + }, + "end_address": "182 Church St, Parramatta NSW 2150, Australia", + "start_address": "483 George St, Sydney NSW 2000, Australia" + } + ], + "overview_polyline": { + "points": "b}vmEir{y[APbAHpAJjDh@Z?^Cp@Mj@Uf@]fAeAv@wAz@cAt@i@x@c@`Bk@t@SnCUpAPb@@zB\\\\v@LvDh@`BZtBf@nC`AbCtAd@VM^EJ`@V~AlAbDnD~EbHr@zAl@x@r@jApAdBx@|@p@n@|C~BnBzA|FdFdAjAjBdCjAnB~A|CZt@lAxChBnFv@zCZdBf@jDd@hCXnAfBtHZtAn@pCz@fDhBhGb@bB`@tB\\\\rBjAnFh@zD\\\\tDHvF@hJMrDE|DSbDWbBy@`DqAnDoBrDi@dA_@z@s@|Bc@bByBjIeA~DShAUlBM`BEbDDtCVxE`@bFf@pFD|CO`DW~BaAfFa@fB[|BY`DCpBD~BRrCj@pGNtBFjBE~CU|Da@nEc@xDe@zBoAnFcAzEsAzFg@lBm@rCiA~E}AdGgAhD}@bDg@dCUjBa@bDMpDM`GChACvABz@ClACdAItBQjCYfCStA]`BaB|GqBbIUt@s@`Dw@nDg@lBqAfFu@lDu@bDiBnH]jAy@bCs@fB{AhDgAdCc@hAuBrFm@hA}DxF}B~Dy@tA_BzBwBrCcBxBkAbBy@pAq@pAk@jAs@lB[bA_@dB{@jEc@bCS|AcA`Ji@hD_AxISnA_@~Ae@zAi@tA_ApBu@pAgAvAcAfAaB|Ae@f@eBvAo@n@e@j@}@~@y@xAo@tA}@jBi@x@}@fA}A~As@~@{@rAkC|EaJvPe@|@_@z@u@pBuAxDu@`C}@`Eo@nCoAhGsB`Ka@|BQxAOfBIzAEhB?lBDfBFzAPpBf@`GThFJpD?bBKpDSjEGlAYhCgAjFYzBw@lHOvBO`DIpDExD?nCFxIRnEf@xDv@pFLjBDl@DlEEjGKvLO|OBhACdBEvBMdCO|AO~@Kn@c@pB_@pAs@pBe@rAc@~@i@z@_AvAoA~AwAxAgA|@oBpAeC|Ao@XaAj@gAd@}@j@gC~AoHrEkGzD}JjG}JlGsEbD{@n@oB`BaC`CeBhBeAdAqEtE_BfBmEzEyGnHqAvAuCvCqBrBsBlB}DbEuBhCmClD{B`Du@fAkAvBcArBgB~Cc@~@[l@o@`AsC~EqBxD_GvKeBzCsAjCiDtGgA`CaCrEg@x@S^w@v@iBtAgAr@w@^sClAaAXqAb@{Bn@sATo@LQB}ALaDHoBEC??@a@AqBIgAMwE{@sASm@C{CI_A@eCVw@PsAb@m@`@_Aj@mBrAsBlB]^kA`Bg@bAUj@OK\\\\dAYl@[p@Un@Gb@EZI@a@BG?KEEGSNi@Vy@NW?S@" + } + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/ReverseGeocodeResponse.json b/src/test/resources/com/google/maps/ReverseGeocodeResponse.json new file mode 100644 index 000000000..6ffa8f839 --- /dev/null +++ b/src/test/resources/com/google/maps/ReverseGeocodeResponse.json @@ -0,0 +1,675 @@ +{ + "results": [ + { + "address_components": [ + { + "long_name": "343", + "short_name": "343", + "types": [ + "street_number" + ] + }, + { + "long_name": "George Street", + "short_name": "George St", + "types": [ + "route" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "343 George St, Sydney NSW 2000, Australia", + "geometry": { + "location": { + "lat": -33.8675084, + "lng": 151.2066756 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": -33.8661594197085, + "lng": 151.2080245802915 + }, + "southwest": { + "lat": -33.86885738029149, + "lng": 151.2053266197085 + } + } + }, + "place_id": "ChIJg5VYhUCuEmsRbw9hp4iPf-w", + "types": [ + "street_address" + ] + }, + { + "address_components": [ + { + "long_name": "York St Near Barrack St", + "short_name": "York St Near Barrack St", + "types": [ + "bus_station", + "establishment", + "point_of_interest", + "transit_station" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2017", + "short_name": "2017", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "York St Near Barrack St, Sydney NSW 2017, Australia", + "geometry": { + "location": { + "lat": -33.8679199, + "lng": 151.2060489 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": -33.86657091970849, + "lng": 151.2073978802915 + }, + "southwest": { + "lat": -33.86926888029149, + "lng": 151.2046999197085 + } + } + }, + "place_id": "ChIJQ2jNn0CuEmsRjff6m9bEJEM", + "types": [ + "bus_station", + "establishment", + "point_of_interest", + "transit_station" + ] + }, + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "Sydney NSW 2000, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.8561088, + "lng": 151.222951 + }, + "southwest": { + "lat": -33.8797035, + "lng": 151.1970329 + } + }, + "location": { + "lat": -33.8688197, + "lng": 151.2092955 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.8561088, + "lng": 151.222951 + }, + "southwest": { + "lat": -33.8797035, + "lng": 151.1970329 + } + } + }, + "place_id": "ChIJP5iLHkCuEmsRwMwyFmh9AQU", + "types": [ + "locality", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "colloquial_area", + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney NSW, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.5781409, + "lng": 151.3430209 + }, + "southwest": { + "lat": -34.118347, + "lng": 150.5209286 + } + }, + "location": { + "lat": -33.8688197, + "lng": 151.2092955 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.5782519, + "lng": 151.3429976 + }, + "southwest": { + "lat": -34.118328, + "lng": 150.5209286 + } + } + }, + "place_id": "ChIJP3Sa8ziYEmsRUKgyFmh9AQM", + "types": [ + "colloquial_area", + "locality", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "2017", + "short_name": "2017", + "types": [ + "postal_code" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney NSW 2017, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.8664717, + "lng": 151.2117013 + }, + "southwest": { + "lat": -33.8792609, + "lng": 151.204416 + } + }, + "location": { + "lat": -33.8673275, + "lng": 151.2114041 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.8664717, + "lng": 151.2117013 + }, + "southwest": { + "lat": -33.8792609, + "lng": 151.204416 + } + } + }, + "place_id": "ChIJOejzimquEmsRxmGtuAZySvE", + "types": [ + "postal_code" + ] + }, + { + "address_components": [ + { + "long_name": "2000", + "short_name": "2000", + "types": [ + "postal_code" + ] + }, + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney NSW 2000, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.8535504, + "lng": 151.222951 + }, + "southwest": { + "lat": -33.8858133, + "lng": 151.1970329 + } + }, + "location": { + "lat": -33.8708464, + "lng": 151.20733 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.8535504, + "lng": 151.222951 + }, + "southwest": { + "lat": -33.8858133, + "lng": 151.1970329 + } + } + }, + "place_id": "ChIJP-njCjuuEmsRcIe6P2t9ARw", + "postcode_localities": [ + "Barangaroo", + "Dawes Point", + "Haymarket", + "Millers Point", + "Sydney", + "The Rocks" + ], + "types": [ + "postal_code" + ] + }, + { + "address_components": [ + { + "long_name": "Sydney CBD", + "short_name": "Sydney CBD", + "types": [ + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney CBD, NSW, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.8535504, + "lng": 151.222951 + }, + "southwest": { + "lat": -33.8858133, + "lng": 151.186625 + } + }, + "location": { + "lat": -33.8708464, + "lng": 151.20733 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.8535504, + "lng": 151.222951 + }, + "southwest": { + "lat": -33.8858133, + "lng": 151.186625 + } + } + }, + "place_id": "ChIJKaeYMj-uEmsRAgZ4clX6UO8", + "types": [ + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Council of the City of Sydney", + "short_name": "Sydney", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney, NSW, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.85364920000001, + "lng": 151.2331075 + }, + "southwest": { + "lat": -33.9243909, + "lng": 151.1749538 + } + }, + "location": { + "lat": -33.8967541, + "lng": 151.1985879 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.85364920000001, + "lng": 151.2331075 + }, + "southwest": { + "lat": -33.9243909, + "lng": 151.1749538 + } + } + }, + "place_id": "ChIJl9aAttixEmsR8d2wSrqVi5k", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Sydney Metropolitan Area", + "short_name": "Sydney Metropolitan Area", + "types": [ + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney Metropolitan Area, NSW, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.3640261, + "lng": 151.3439 + }, + "southwest": { + "lat": -34.1896128, + "lng": 150.5883888 + } + }, + "location": { + "lat": -33.8817547, + "lng": 150.8609358 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.3640261, + "lng": 151.3438984 + }, + "southwest": { + "lat": -34.1896128, + "lng": 150.5883888 + } + } + }, + "place_id": "ChIJI1bpHkCuEmsRcTz72E_A69A", + "types": [ + "political" + ] + }, + { + "address_components": [ + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "New South Wales, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -28.15702, + "lng": 159.1054441 + }, + "southwest": { + "lat": -37.5052801, + "lng": 140.9992793 + } + }, + "location": { + "lat": -31.2532183, + "lng": 146.921099 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -28.1570718, + "lng": 153.6385162 + }, + "southwest": { + "lat": -37.5050181, + "lng": 140.9992793 + } + } + }, + "place_id": "ChIJDUte93TLDWsRLZ_EIhGvgBc", + "types": [ + "administrative_area_level_1", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/ReverseGeocodeWithKitaWardResponse.json b/src/test/resources/com/google/maps/ReverseGeocodeWithKitaWardResponse.json new file mode 100644 index 000000000..2c73777ad --- /dev/null +++ b/src/test/resources/com/google/maps/ReverseGeocodeWithKitaWardResponse.json @@ -0,0 +1,649 @@ +{ + "results": [ + { + "address_components": [ + { + "long_name": "北山鹿苑寺金閣寺", + "short_name": "北山鹿苑寺金閣寺", + "types": [ + "premise" + ] + }, + { + "long_name": "1", + "short_name": "1", + "types": [ + "political", + "sublocality", + "sublocality_level_4" + ] + }, + { + "long_name": "Kinkakujichō", + "short_name": "Kinkakujichō", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "Kita-ku", + "short_name": "Kita-ku", + "types": [ + "locality", + "political", + "ward" + ] + }, + { + "long_name": "Kyōto-shi", + "short_name": "Kyōto-shi", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kyōto-fu", + "short_name": "Kyōto-fu", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "603-8361", + "short_name": "603-8361", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "Japan, 〒603-8361 Kyōto-fu, Kyōto-shi, Kita-ku, Kinkakujichō, 1 北山鹿苑寺金閣寺", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.0396014, + "lng": 135.7295118 + }, + "southwest": { + "lat": 35.0391291, + "lng": 135.7289492 + } + }, + "location": { + "lat": 35.0393986, + "lng": 135.7293744 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": 35.0407142302915, + "lng": 135.7305794802915 + }, + "southwest": { + "lat": 35.0380162697085, + "lng": 135.7278815197085 + } + } + }, + "place_id": "ChIJLxi4xCCoAWAR0nKK_sUaOtM", + "types": [ + "premise" + ] + }, + { + "address_components": [ + { + "long_name": "1", + "short_name": "1", + "types": [ + "political", + "sublocality", + "sublocality_level_4" + ] + }, + { + "long_name": "Kinkakujichō", + "short_name": "Kinkakujichō", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "Kita-ku", + "short_name": "Kita-ku", + "types": [ + "locality", + "political", + "ward" + ] + }, + { + "long_name": "Kyōto-shi", + "short_name": "Kyōto-shi", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kyōto-fu", + "short_name": "Kyōto-fu", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "603-8361", + "short_name": "603-8361", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "Japan, 〒603-8361 Kyōto-fu, Kyōto-shi, Kita-ku, Kinkakujichō, 1", + "geometry": { + "location": { + "lat": 35.0393553, + "lng": 135.7293265 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": 35.04070428029149, + "lng": 135.7306754802915 + }, + "southwest": { + "lat": 35.0380063197085, + "lng": 135.7279775197085 + } + } + }, + "place_id": "ChIJnT1kwyCoAWAR-d2HQrYxlTs", + "types": [ + "political", + "sublocality", + "sublocality_level_4" + ] + }, + { + "address_components": [ + { + "long_name": "Kinkakujicho", + "short_name": "Kinkakujicho", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "Kita Ward", + "short_name": "Kita Ward", + "types": [ + "locality", + "political", + "ward" + ] + }, + { + "long_name": "Kyoto", + "short_name": "Kyoto", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kyoto Prefecture", + "short_name": "Kyoto Prefecture", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "603-8361", + "short_name": "603-8361", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "Kinkakujicho, Kita Ward, Kyoto, Kyoto Prefecture 603-8361, Japan", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.0409162, + "lng": 135.7318809 + }, + "southwest": { + "lat": 35.0381293, + "lng": 135.7271086 + } + }, + "location": { + "lat": 35.0393553, + "lng": 135.7293265 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 35.0409162, + "lng": 135.7318809 + }, + "southwest": { + "lat": 35.0381293, + "lng": 135.7271086 + } + } + }, + "place_id": "ChIJe9XMwiCoAWARVrQpOsYqdBE", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "address_components": [ + { + "long_name": "Kita Ward", + "short_name": "Kita Ward", + "types": [ + "locality", + "political", + "ward" + ] + }, + { + "long_name": "Kyoto", + "short_name": "Kyoto", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kyoto Prefecture", + "short_name": "Kyoto Prefecture", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Kita Ward, Kyoto, Kyoto Prefecture, Japan", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.1714945, + "lng": 135.7728535 + }, + "southwest": { + "lat": 35.0222614, + "lng": 135.6471605 + } + }, + "location": { + "lat": 35.041053, + "lng": 135.7539826 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 35.1714945, + "lng": 135.7728535 + }, + "southwest": { + "lat": 35.0222614, + "lng": 135.6471605 + } + } + }, + "place_id": "ChIJHSR_jiupAWARcQjngz-_Cxk", + "types": [ + "locality", + "political", + "ward" + ] + }, + { + "address_components": [ + { + "long_name": "Kyoto", + "short_name": "Kyoto", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kyoto Prefecture", + "short_name": "Kyoto Prefecture", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Kyoto, Kyoto Prefecture, Japan", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.32119230000001, + "lng": 135.878779 + }, + "southwest": { + "lat": 34.8748598, + "lng": 135.5589845 + } + }, + "location": { + "lat": 35.0116363, + "lng": 135.7680294 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 35.0542, + "lng": 135.8236 + }, + "southwest": { + "lat": 34.958, + "lng": 135.6983 + } + } + }, + "place_id": "ChIJ8cM8zdaoAWARPR27azYdlsA", + "types": [ + "locality", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "603-8361", + "short_name": "603-8361", + "types": [ + "postal_code" + ] + }, + { + "long_name": "Kinkakujicho", + "short_name": "Kinkakujicho", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "Kita Ward", + "short_name": "Kita Ward", + "types": [ + "locality", + "political", + "ward" + ] + }, + { + "long_name": "Kyoto", + "short_name": "Kyoto", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kyoto Prefecture", + "short_name": "Kyoto Prefecture", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "603-8361, Japan", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.0409162, + "lng": 135.7318809 + }, + "southwest": { + "lat": 35.0381293, + "lng": 135.7271086 + } + }, + "location": { + "lat": 35.0392985, + "lng": 135.7290044 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 35.0409162, + "lng": 135.7318809 + }, + "southwest": { + "lat": 35.0381293, + "lng": 135.7271086 + } + } + }, + "place_id": "ChIJnT1kwyCoAWARkK61Za4dRY4", + "types": [ + "postal_code" + ] + }, + { + "address_components": [ + { + "long_name": "Osaka Metropolitan Area", + "short_name": "Osaka Metropolitan Area", + "types": [ + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Japan, Osaka Metropolitan Area", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.1393087, + "lng": 136.0102211 + }, + "southwest": { + "lat": 34.3113767, + "lng": 134.4371108 + } + }, + "location": { + "lat": 34.7307812, + "lng": 135.5251982 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 35.1393087, + "lng": 136.0102211 + }, + "southwest": { + "lat": 34.3113767, + "lng": 134.4371108 + } + } + }, + "place_id": "ChIJN7QiqzCwAGAR3arYsOjiWEY", + "types": [ + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Kyoto Prefecture", + "short_name": "Kyoto Prefecture", + "types": [ + "administrative_area_level_1", + "establishment", + "point_of_interest", + "political" + ] + }, + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Kyoto Prefecture, Japan", + "geometry": { + "bounds": { + "northeast": { + "lat": 35.7793193, + "lng": 136.0540829 + }, + "southwest": { + "lat": 34.7059884, + "lng": 134.8536955 + } + }, + "location": { + "lat": 35.0212466, + "lng": 135.7555968 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 35.7793193, + "lng": 136.0540829 + }, + "southwest": { + "lat": 34.7059885, + "lng": 134.8536957 + } + } + }, + "place_id": "ChIJYRsf-SB0_18ROJWxOMJ7Clk", + "types": [ + "administrative_area_level_1", + "establishment", + "point_of_interest", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Japan", + "short_name": "JP", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Japan", + "geometry": { + "bounds": { + "northeast": { + "lat": 45.6412626, + "lng": 154.0031455 + }, + "southwest": { + "lat": 20.3585295, + "lng": 122.8554688 + } + }, + "location": { + "lat": 36.204824, + "lng": 138.252924 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 45.52177229999999, + "lng": 145.8162778 + }, + "southwest": { + "lat": 24.0459244, + "lng": 122.9338302 + } + } + }, + "place_id": "ChIJLxl_1w9OZzQRRFJmfNR1QvU", + "types": [ + "country", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/RoadsApiNearestRoadsResponse.json b/src/test/resources/com/google/maps/RoadsApiNearestRoadsResponse.json new file mode 100644 index 000000000..61cfb81b5 --- /dev/null +++ b/src/test/resources/com/google/maps/RoadsApiNearestRoadsResponse.json @@ -0,0 +1,108 @@ +{ + "snappedPoints": [ + { + "location": { + "latitude": -33.865436156120467, + "longitude": 151.1930101572747 + }, + "originalIndex": 0, + "placeId": "ChIJ0XXACjauEmsRUduC5Wd9ARM" + }, + { + "location": { + "latitude": -33.86586065047829, + "longitude": 151.19318696959755 + }, + "originalIndex": 1, + "placeId": "ChIJB7-UDDauEmsRYIxz5md9ARM" + }, + { + "location": { + "latitude": -33.86586065047829, + "longitude": 151.19318696959755 + }, + "originalIndex": 1, + "placeId": "ChIJB7-UDDauEmsRYYxz5md9ARM" + }, + { + "location": { + "latitude": -33.866812311173426, + "longitude": 151.19351259392386 + }, + "originalIndex": 2, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.866812311173426, + "longitude": 151.19351259392386 + }, + "originalIndex": 2, + "placeId": "ChIJuSUoZTauEmsRCVX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867227668776557, + "longitude": 151.19349907121253 + }, + "originalIndex": 3, + "placeId": "ChIJMXAlhzauEmsRcAyD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867227668776557, + "longitude": 151.19349907121253 + }, + "originalIndex": 3, + "placeId": "ChIJMXAlhzauEmsRcQyD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867486546249687, + "longitude": 151.19382957649137 + }, + "originalIndex": 4, + "placeId": "ChIJa5zkiTauEmsRoBGD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867486546249687, + "longitude": 151.19382957649137 + }, + "originalIndex": 4, + "placeId": "ChIJa5zkiTauEmsRoRGD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867873778425583, + "longitude": 151.19405751045474 + }, + "originalIndex": 5, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + }, + { + "location": { + "latitude": -33.867873778425583, + "longitude": 151.19405751045474 + }, + "originalIndex": 5, + "placeId": "ChIJQ2SK7DauEmsR79e81zZ5J4U" + }, + { + "location": { + "latitude": -33.868174058295573, + "longitude": 151.19423711172229 + }, + "originalIndex": 6, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + }, + { + "location": { + "latitude": -33.868174058295573, + "longitude": 151.19423711172229 + }, + "originalIndex": 6, + "placeId": "ChIJQ2SK7DauEmsR79e81zZ5J4U" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/RoadsApiSnapToRoadResponse.json b/src/test/resources/com/google/maps/RoadsApiSnapToRoadResponse.json new file mode 100644 index 000000000..4a88d3fae --- /dev/null +++ b/src/test/resources/com/google/maps/RoadsApiSnapToRoadResponse.json @@ -0,0 +1,60 @@ +{ + "snappedPoints": [ + { + "location": { + "latitude": -33.865233402568428, + "longitude": 151.19288612197704 + }, + "originalIndex": 0, + "placeId": "ChIJjXkMCDauEmsRp5xab4Ske6k" + }, + { + "location": { + "latitude": -33.86586065047829, + "longitude": 151.19318696959752 + }, + "originalIndex": 1, + "placeId": "ChIJB7-UDDauEmsRYIxz5md9ARM" + }, + { + "location": { + "latitude": -33.866812311173426, + "longitude": 151.19351259392386 + }, + "originalIndex": 2, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867069733804293, + "longitude": 151.19362819320284 + }, + "originalIndex": 3, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867486546249687, + "longitude": 151.19382957649137 + }, + "originalIndex": 4, + "placeId": "ChIJa5zkiTauEmsRoBGD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867873778425576, + "longitude": 151.19405751045474 + }, + "originalIndex": 5, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + }, + { + "location": { + "latitude": -33.868174058295573, + "longitude": 151.19423711172229 + }, + "originalIndex": 6, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/RoadsApiSnappedSpeedLimitResponse.json b/src/test/resources/com/google/maps/RoadsApiSnappedSpeedLimitResponse.json new file mode 100644 index 000000000..452d9030b --- /dev/null +++ b/src/test/resources/com/google/maps/RoadsApiSnappedSpeedLimitResponse.json @@ -0,0 +1,97 @@ +{ + "speedLimits": [ + { + "placeId": "ChIJjXkMCDauEmsRp5xab4Ske6k", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJB7-UDDauEmsRYIxz5md9ARM", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJa5zkiTauEmsRoBGD5Wd9ARM", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U", + "speedLimit": 40, + "units": "KPH" + } + ], + "snappedPoints": [ + { + "location": { + "latitude": -33.865233402568428, + "longitude": 151.19288612197704 + }, + "originalIndex": 0, + "placeId": "ChIJjXkMCDauEmsRp5xab4Ske6k" + }, + { + "location": { + "latitude": -33.86586065047829, + "longitude": 151.19318696959752 + }, + "originalIndex": 1, + "placeId": "ChIJB7-UDDauEmsRYIxz5md9ARM" + }, + { + "location": { + "latitude": -33.866812311173426, + "longitude": 151.19351259392386 + }, + "originalIndex": 2, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867069733804293, + "longitude": 151.19362819320284 + }, + "originalIndex": 3, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867486546249687, + "longitude": 151.19382957649137 + }, + "originalIndex": 4, + "placeId": "ChIJa5zkiTauEmsRoBGD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867873778425576, + "longitude": 151.19405751045474 + }, + "originalIndex": 5, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + }, + { + "location": { + "latitude": -33.868174058295573, + "longitude": 151.19423711172229 + }, + "originalIndex": 6, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/RoadsApiSpeedLimitsResponse.json b/src/test/resources/com/google/maps/RoadsApiSpeedLimitsResponse.json new file mode 100644 index 000000000..452d9030b --- /dev/null +++ b/src/test/resources/com/google/maps/RoadsApiSpeedLimitsResponse.json @@ -0,0 +1,97 @@ +{ + "speedLimits": [ + { + "placeId": "ChIJjXkMCDauEmsRp5xab4Ske6k", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJB7-UDDauEmsRYIxz5md9ARM", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJa5zkiTauEmsRoBGD5Wd9ARM", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U", + "speedLimit": 40, + "units": "KPH" + }, + { + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U", + "speedLimit": 40, + "units": "KPH" + } + ], + "snappedPoints": [ + { + "location": { + "latitude": -33.865233402568428, + "longitude": 151.19288612197704 + }, + "originalIndex": 0, + "placeId": "ChIJjXkMCDauEmsRp5xab4Ske6k" + }, + { + "location": { + "latitude": -33.86586065047829, + "longitude": 151.19318696959752 + }, + "originalIndex": 1, + "placeId": "ChIJB7-UDDauEmsRYIxz5md9ARM" + }, + { + "location": { + "latitude": -33.866812311173426, + "longitude": 151.19351259392386 + }, + "originalIndex": 2, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867069733804293, + "longitude": 151.19362819320284 + }, + "originalIndex": 3, + "placeId": "ChIJuSUoZTauEmsRCFX-gNpgzoc" + }, + { + "location": { + "latitude": -33.867486546249687, + "longitude": 151.19382957649137 + }, + "originalIndex": 4, + "placeId": "ChIJa5zkiTauEmsRoBGD5Wd9ARM" + }, + { + "location": { + "latitude": -33.867873778425576, + "longitude": 151.19405751045474 + }, + "originalIndex": 5, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + }, + { + "location": { + "latitude": -33.868174058295573, + "longitude": 151.19423711172229 + }, + "originalIndex": 6, + "placeId": "ChIJQ2SK7DauEmsR7te81zZ5J4U" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/RoadsApiSpeedLimitsUSAResponse.json b/src/test/resources/com/google/maps/RoadsApiSpeedLimitsUSAResponse.json new file mode 100644 index 000000000..7e2df485c --- /dev/null +++ b/src/test/resources/com/google/maps/RoadsApiSpeedLimitsUSAResponse.json @@ -0,0 +1,97 @@ +{ + "speedLimits": [ + { + "placeId": "ChIJrfDjZYoE9YgRLpb3bOhcPno", + "speedLimit": 16.09344, + "units": "KPH" + }, + { + "placeId": "ChIJyU-E2mEE9YgRftyNXxcfQYw", + "speedLimit": 40.2336, + "units": "KPH" + }, + { + "placeId": "ChIJc0BrC2EE9YgR71DvaFzNgrA", + "speedLimit": 40.2336, + "units": "KPH" + }, + { + "placeId": "ChIJi1UvwWYE9YgRo0VhXEwKnUI", + "speedLimit": 40.2336, + "units": "KPH" + }, + { + "placeId": "ChIJ_z5MhGYE9YgRop22JVq5LY4", + "speedLimit": 64.37376, + "units": "KPH" + }, + { + "placeId": "ChIJw3nRTmQE9YgRM9FxvNGW934", + "speedLimit": 64.37376, + "units": "KPH" + }, + { + "placeId": "ChIJYc59-WQE9YgRZJzNCzh8g0Q", + "speedLimit": 64.37376, + "units": "KPH" + } + ], + "snappedPoints": [ + { + "location": { + "latitude": 33.77777408879637, + "longitude": -84.397794661550478 + }, + "originalIndex": 0, + "placeId": "ChIJrfDjZYoE9YgRLpb3bOhcPno" + }, + { + "location": { + "latitude": 33.777549052027929, + "longitude": -84.395701962737832 + }, + "originalIndex": 1, + "placeId": "ChIJyU-E2mEE9YgRftyNXxcfQYw" + }, + { + "location": { + "latitude": 33.776903799999992, + "longitude": -84.393114099999991 + }, + "originalIndex": 2, + "placeId": "ChIJc0BrC2EE9YgR71DvaFzNgrA" + }, + { + "location": { + "latitude": 33.7768556093838, + "longitude": -84.389550115646216 + }, + "originalIndex": 3, + "placeId": "ChIJi1UvwWYE9YgRo0VhXEwKnUI" + }, + { + "location": { + "latitude": 33.7755636, + "longitude": -84.3888147 + }, + "originalIndex": 4, + "placeId": "ChIJ_z5MhGYE9YgRop22JVq5LY4" + }, + { + "location": { + "latitude": 33.773249974129641, + "longitude": -84.388833371339487 + }, + "originalIndex": 5, + "placeId": "ChIJw3nRTmQE9YgRM9FxvNGW934" + }, + { + "location": { + "latitude": 33.771990999273889, + "longitude": -84.388842300365127 + }, + "originalIndex": 6, + "placeId": "ChIJYc59-WQE9YgRZJzNCzh8g0Q" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/RoadsApiSpeedLimitsWithPlaceIds.json b/src/test/resources/com/google/maps/RoadsApiSpeedLimitsWithPlaceIds.json new file mode 100644 index 000000000..bb8ac1329 --- /dev/null +++ b/src/test/resources/com/google/maps/RoadsApiSpeedLimitsWithPlaceIds.json @@ -0,0 +1,19 @@ +{ + "speedLimits": [ + { + "placeId": "ChIJrfDjZYoE9YgRLpb3bOhcPno", + "speedLimit": 16.09344, + "units": "KPH" + }, + { + "placeId": "ChIJyU-E2mEE9YgRftyNXxcfQYw", + "speedLimit": 40.2336, + "units": "KPH" + }, + { + "placeId": "ChIJc0BrC2EE9YgR71DvaFzNgrA", + "speedLimit": 40.2336, + "units": "KPH" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/SimpleGeocodeResponse.json b/src/test/resources/com/google/maps/SimpleGeocodeResponse.json new file mode 100644 index 000000000..9550a59be --- /dev/null +++ b/src/test/resources/com/google/maps/SimpleGeocodeResponse.json @@ -0,0 +1,68 @@ +{ + "results": [ + { + "address_components": [ + { + "long_name": "Sydney", + "short_name": "Sydney", + "types": [ + "colloquial_area", + "locality", + "political" + ] + }, + { + "long_name": "New South Wales", + "short_name": "NSW", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "Australia", + "short_name": "AU", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Sydney NSW, Australia", + "geometry": { + "bounds": { + "northeast": { + "lat": -33.5781409, + "lng": 151.3430209 + }, + "southwest": { + "lat": -34.118347, + "lng": 150.5209286 + } + }, + "location": { + "lat": -33.8688197, + "lng": 151.2092955 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": -33.5782519, + "lng": 151.3429976 + }, + "southwest": { + "lat": -34.118328, + "lng": 150.5209286 + } + } + }, + "place_id": "ChIJP3Sa8ziYEmsRUKgyFmh9AQM", + "types": [ + "colloquial_area", + "locality", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/SimpleReverseGeocodeResponse.json b/src/test/resources/com/google/maps/SimpleReverseGeocodeResponse.json new file mode 100644 index 000000000..cecf1082f --- /dev/null +++ b/src/test/resources/com/google/maps/SimpleReverseGeocodeResponse.json @@ -0,0 +1,702 @@ +{ + "results": [ + { + "address_components": [ + { + "long_name": "277", + "short_name": "277", + "types": [ + "street_number" + ] + }, + { + "long_name": "Bedford Avenue", + "short_name": "Bedford Ave", + "types": [ + "route" + ] + }, + { + "long_name": "Williamsburg", + "short_name": "Williamsburg", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "Brooklyn", + "short_name": "Brooklyn", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "Kings County", + "short_name": "Kings County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "11211", + "short_name": "11211", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "277 Bedford Ave, Brooklyn, NY 11211, USA", + "geometry": { + "location": { + "lat": 40.7142205, + "lng": -73.9612903 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": 40.71556948029149, + "lng": -73.95994131970849 + }, + "southwest": { + "lat": 40.7128715197085, + "lng": -73.9626392802915 + } + } + }, + "place_id": "ChIJd8BlQ2BZwokRAFUEcm_qrcA", + "types": [ + "street_address" + ] + }, + { + "address_components": [ + { + "long_name": "Grand St/Bedford Av", + "short_name": "Grand St/Bedford Av", + "types": [ + "bus_station", + "establishment", + "point_of_interest", + "transit_station" + ] + }, + { + "long_name": "Williamsburg", + "short_name": "Williamsburg", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "Brooklyn", + "short_name": "Brooklyn", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "Kings County", + "short_name": "Kings County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "11211", + "short_name": "11211", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "Grand St/Bedford Av, Brooklyn, NY 11211, USA", + "geometry": { + "location": { + "lat": 40.714321, + "lng": -73.961151 + }, + "location_type": "GEOMETRIC_CENTER", + "viewport": { + "northeast": { + "lat": 40.71566998029149, + "lng": -73.95980201970849 + }, + "southwest": { + "lat": 40.7129720197085, + "lng": -73.96249998029151 + } + } + }, + "place_id": "ChIJi27VXGBZwokRM8ErPyB91yk", + "types": [ + "bus_station", + "establishment", + "point_of_interest", + "transit_station" + ] + }, + { + "address_components": [ + { + "long_name": "Williamsburg", + "short_name": "Williamsburg", + "types": [ + "neighborhood", + "political" + ] + }, + { + "long_name": "Brooklyn", + "short_name": "Brooklyn", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "New York", + "short_name": "New York", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kings County", + "short_name": "Kings County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Williamsburg, Brooklyn, NY, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 40.7251773, + "lng": -73.936498 + }, + "southwest": { + "lat": 40.6979329, + "lng": -73.96984499999999 + } + }, + "location": { + "lat": 40.7081156, + "lng": -73.9570696 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 40.7251773, + "lng": -73.936498 + }, + "southwest": { + "lat": 40.6979329, + "lng": -73.96984499999999 + } + } + }, + "place_id": "ChIJQSrBBv1bwokRbNfFHCnyeYI", + "types": [ + "neighborhood", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Brooklyn", + "short_name": "Brooklyn", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "New York", + "short_name": "New York", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Kings County", + "short_name": "Kings County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Brooklyn, NY, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 40.739446, + "lng": -73.83336509999999 + }, + "southwest": { + "lat": 40.551042, + "lng": -74.05663 + } + }, + "location": { + "lat": 40.6781784, + "lng": -73.94415789999999 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 40.739446, + "lng": -73.83336509999999 + }, + "southwest": { + "lat": 40.551042, + "lng": -74.05663 + } + } + }, + "place_id": "ChIJCSF8lBZEwokRhngABHRcdoI", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "address_components": [ + { + "long_name": "New York", + "short_name": "New York", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "New York, NY, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 40.9175771, + "lng": -73.70027209999999 + }, + "southwest": { + "lat": 40.4773991, + "lng": -74.25908989999999 + } + }, + "location": { + "lat": 40.7127837, + "lng": -74.0059413 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 40.9152555, + "lng": -73.70027209999999 + }, + "southwest": { + "lat": 40.4960439, + "lng": -74.25573489999999 + } + } + }, + "place_id": "ChIJOwg_06VPwokRYv534QaPC8g", + "types": [ + "locality", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "11211", + "short_name": "11211", + "types": [ + "postal_code" + ] + }, + { + "long_name": "Brooklyn", + "short_name": "Brooklyn", + "types": [ + "political", + "sublocality", + "sublocality_level_1" + ] + }, + { + "long_name": "New York", + "short_name": "New York", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Brooklyn, NY 11211, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 40.7280089, + "lng": -73.9207299 + }, + "southwest": { + "lat": 40.7008331, + "lng": -73.9644697 + } + }, + "location": { + "lat": 40.7093358, + "lng": -73.9565551 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 40.7280089, + "lng": -73.9207299 + }, + "southwest": { + "lat": 40.7008331, + "lng": -73.9644697 + } + } + }, + "place_id": "ChIJvbEjlVdZwokR4KapM3WCFRw", + "types": [ + "postal_code" + ] + }, + { + "address_components": [ + { + "long_name": "Kings County", + "short_name": "Kings County", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Kings County, NY, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 40.739446, + "lng": -73.83336509999999 + }, + "southwest": { + "lat": 40.551042, + "lng": -74.05663 + } + }, + "location": { + "lat": 40.6528762, + "lng": -73.95949399999999 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 40.7391407, + "lng": -73.83363179999999 + }, + "southwest": { + "lat": 40.5703742, + "lng": -74.04195919999999 + } + } + }, + "place_id": "ChIJOwE7_GTtwokRs75rhW4_I6M", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "New York-Northern New Jersey-Long Island, NY-NJ-PA", + "short_name": "New York-Northern New Jersey-Long Island, NY-NJ-PA", + "types": [ + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "New York-Northern New Jersey-Long Island, NY-NJ-PA, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 41.6018065, + "lng": -71.85621399999999 + }, + "southwest": { + "lat": 39.49853299999999, + "lng": -75.3585939 + } + }, + "location": { + "lat": 40.9590293, + "lng": -74.0300122 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 41.6018065, + "lng": -71.85621399999999 + }, + "southwest": { + "lat": 39.49853299999999, + "lng": -75.3585939 + } + } + }, + "place_id": "ChIJ3YJV4PRWwokRFFI21ZrHXtQ", + "types": [ + "political" + ] + }, + { + "address_components": [ + { + "long_name": "New York Metropolitan Area", + "short_name": "New York Metropolitan Area", + "types": [ + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "New York Metropolitan Area, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 42.0809059, + "lng": -71.777491 + }, + "southwest": { + "lat": 39.475198, + "lng": -75.3587649 + } + }, + "location": { + "lat": 40.7127761, + "lng": -74.00595439999999 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 42.0809059, + "lng": -71.777491 + }, + "southwest": { + "lat": 39.475198, + "lng": -75.3587649 + } + } + }, + "place_id": "ChIJ-5Z24NaGwokRiMh4Rj8FNMo", + "types": [ + "political" + ] + }, + { + "address_components": [ + { + "long_name": "New York", + "short_name": "NY", + "types": [ + "administrative_area_level_1", + "establishment", + "point_of_interest", + "political" + ] + }, + { + "long_name": "United States", + "short_name": "US", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "New York, USA", + "geometry": { + "bounds": { + "northeast": { + "lat": 45.015865, + "lng": -71.777491 + }, + "southwest": { + "lat": 40.4773991, + "lng": -79.7625901 + } + }, + "location": { + "lat": 43.2994285, + "lng": -74.21793260000001 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 45.0125923, + "lng": -71.8562029 + }, + "southwest": { + "lat": 40.4961036, + "lng": -79.761996 + } + } + }, + "place_id": "ChIJqaUj8fBLzEwRZ5UY3sHGz90", + "types": [ + "administrative_area_level_1", + "establishment", + "point_of_interest", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/TextSearchPizzaInNYC.json b/src/test/resources/com/google/maps/TextSearchPizzaInNYC.json new file mode 100644 index 000000000..10abec897 --- /dev/null +++ b/src/test/resources/com/google/maps/TextSearchPizzaInNYC.json @@ -0,0 +1,741 @@ +{ + "html_attributions": [], + "next_page_token": "CuQB1wAAANI17eHXt1HpqbLjkj7T5Ti69DEAClo02Qampg7Q6W_O_krFbge7hnTtDR7oVF3asexHcGnUtR1ZKjroYd4BTCXxSGPi9LEkjJ0P_zVE7byjEBcHvkdxB6nCHKHAgVNGqe0ZHuwSYKlr3C1-kuellMYwMlg3WSe69bJr1Ck35uToNZkUGvo4yjoYxNFRn1lABEnjPskbMdyHAjUDwvBDxzgGxpd8t0EzA9UOM8Y1jqWnZGJM7u8gacNFcI4prr0Doh9etjY1yHrgGYI4F7lKPbfLQKiks_wYzoHbcAcdbBjkEhAxDHC0XXQ16thDAlwVbEYaGhSaGDw5sHbaZkG9LZIqbcas0IJU8w", + "results": [ + { + "formatted_address": "60 Greenpoint Ave, Brooklyn, NY 11222, United States", + "geometry": { + "location": { + "lat": 40.729606, + "lng": -73.95857599999999 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "d30990c93215d02648e78b3b0bdb00e373539903", + "name": "Paulie Gee's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 427, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107146711858841264424\"\u003ePaulie Gee's\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAume6Q8oFq9AcGSZOQnqGHfgYHyCsQHO4JK-JbxeZ0rn1s-QeSMmLbFDV3NvWiSX3SOCJBLQnpnmpxCwiviSGdJbb6Ja2aqCKi5usrlMw6_wI_JM4eUe9_wsGhNT5MmPwEhDcY98HKcLeAkBLEvYHMja1GhQpQTCXtzKF8dLeyOhkm2XJmWJ2iA", + "width": 640 + } + ], + "place_id": "ChIJuc8AM0BZwokRtpm2S66ltsE", + "price_level": 2, + "rating": 4.4, + "reference": "CmRgAAAA7zeHsKL-tAcJzhm42W2crwr3D6l4W4lC_lg1qRmOtwtHd7ypT2nki7nxiYPJQrv2yuB0vGWjJky3ysMWrHi7rmG2uTg6ZaWN5_uQaJiBpXf7RoCodPM6Iq_92glrcAp2EhBmfw1vMTTcYc8Kv-Kw_8XSGhTpWN9TVVKFkuvmEYvDrea70GPycg", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "271 Bleecker St, New York, NY 10014, United States", + "geometry": { + "location": { + "lat": 40.731528, + "lng": -74.00299699999999 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "57cc6b1f5ab6a10dcfff0b71b0d80b209b5f5251", + "name": "Kesté Pizza & Vino", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1360, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/110548558285915713747\"\u003eKesté Pizza & Vino\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAANLvRz5UweZ1O8JSf6FFL-QoImTS0T7LEDwS56_wqMnKu81IcJNM9qlX3pmGjJQvNdOQN3BGOA66xfQ4nzgatG-l8ZKgoBPsjR0wWlNmgMhKgecHt9EB-iYycCGjrBeWYEhDQfx3HLtHgMR_-MGtEBOSoGhRuBAT0ID18aZAPAzbsDqzEtSdK1g", + "width": 2048 + } + ], + "place_id": "ChIJ6ffdpJNZwokRmcafdROM5q0", + "price_level": 2, + "rating": 4.2, + "reference": "CnRnAAAAOhnvzAfXqiA8oDl_9-swN1qWQVCxgak0XJV9Bbsau_iXfVr7mXtd0NAFg0IDbzZVCeSGQ7OjjUZ1W_ZibhJNCWa-MQ9Zp-vzYZ5sITszRb7V5a8R2hzNJcAFGt-OIBw5qQbbOI-84z7NFaCkHzyH5hIQkK1EzQVPuAOKp5tlhiKZJxoUCT7RENorf4roeL7AgMXnKrYZsmc", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "33 Havemeyer St, Brooklyn, NY 11211, United States", + "geometry": { + "location": { + "lat": 40.71558, + "lng": -73.953412 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "c1cad2fa8702d02e533fd9ddd73fdce8c464e248", + "name": "Best Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAfUAK3vSpm1KEh0MjdvxpsdtRS7tF3VqgkjZ7EWhCvm9Kw0aIJsoUuXPYnYFEo5CMZC2MAVbY2APNINned7m4TST5XUobpZb2CPcdn8ZN6_ydCuOAx6-tLJAA-sYpm3jAEhBcGctO2uvkAIAwOSz6Xxz-GhSox4tRrSHcPXeuLHQlrHFANlDnzA", + "width": 2048 + } + ], + "place_id": "ChIJzWhpTVlZwokRRyrw-O4FIxI", + "price_level": 1, + "rating": 4.3, + "reference": "CmRdAAAANivt1D_cZdEvxLiVKvZz6lUjoYznD8VylSJn3TVt0r-23YGdOZfVi5mCkpFiL5_a5_B0096lSAxcTex3xc1Msa7MgC-nFYXmkZUXPUilNrKr2bZ93VINatMCZEaHmd0dEhBEqMnukHw8824ZSgHTLvVoGhSC4YPm8AOvWu-xiCEXbMP2A44uZQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "1424 Avenue J, Brooklyn, NY 11230, United States", + "geometry": { + "location": { + "lat": 40.62506, + "lng": -73.96155299999999 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "3aa44fa0defb16c1da0b12f4a78aab526d9eb6c9", + "name": "Di Fara Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 540, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAy1sMO5tiAHmSZ9NF3Y8W951g7oXpGebFTqmB1NMGp3ddcuLJIG11b046uN7Ot7q8BrNiZi7Js-K7_8sRAMLWMrzN5MjALM7fwpXYVzHiXEeOSGSByUxVvAEohl7P-POsEhB1GTYz0aBMMpIfi_zOlHhJGhRbpW7ctZ-fArIiYqHiSiv5Roflaw", + "width": 540 + } + ], + "place_id": "ChIJM2mGRMhEwokRv1Fy6oFJ570", + "price_level": 2, + "rating": 4.2, + "reference": "CnRhAAAAveqEAX_ytoDHmM46TleSjQhakVWWKCNWVGujdXSjzj9Sx6Jcf9HZLeoLIuRsGmt8pyq-oivNZErTiXjavg-g437rIobGGQdeC3jFhukRk2UoAKB8Vx2WTO2qjdk6BsVACl12Hb5EaF4aUSNWb17XsxIQs7kGqv4PRLJwyfAuvjLJAhoU4Exbga4j_FHN-j2m0Ntu_qMpgy4", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "7 Carmine Street, @ Bleecker Street, New York, NY 10014, United States", + "geometry": { + "location": { + "lat": 40.730559, + "lng": -74.002168 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "0ed4fa342e8c59111d07d80b81f5c08cd6b84934", + "name": "Joe's Pizza", + "opening_hours": { + "open_now": true, + "weekday_text": [] + }, + "photos": [ + { + "height": 1200, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116142415137360972763\"\u003eJoe's Pizza\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAoaa3WqkeWx7-oXcRFv1M9iI0Do_bqABQDcXJREe2OuUTDCaDG443OiCEm7i1YnRgRRRxaBj34BpLyBQevWneInN3hOCUu6y521aROxV-xt4qrsRCtA7bVo8Gcm4Jz-ahEhCw2ThAHyCadg47IuastVF7GhRfwZlnGRM9ciV5-tg0iwFgEP-NGQ", + "width": 1600 + } + ], + "place_id": "ChIJ8Q2WSpJZwokRQz-bYYgEskM", + "price_level": 1, + "rating": 4.2, + "reference": "CmReAAAAHjQLlsyRDCXmNjNG5UbNveTlf2HxTsEXgrEJvEqvbQQQ8Ph6uHhyQgrKAQD2SagO92HriQSpsu3XOsxOyA-Ckv9xVl3QZuVCwQfa6RW-_0naRWlCTZKgYJcW1ZDgQVIvEhAfzOmoYSHYhqA5Knl05J0sGhSmOwqmsJzL5ElaLPEmE5lbksfODg", + "types": [ + "meal_takeaway", + "restaurant", + "meal_delivery", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "235 Mulberry St, New York, NY 10012, United States", + "geometry": { + "location": { + "lat": 40.722768, + "lng": -73.996138 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "7b4e6a32ca3fce8e2325c0a4334c049e04b9bdb8", + "name": "Rubirosa Ristorante", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 476, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/102837379600698720786\"\u003eRubirosa Ristorante\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAji7wG2FHMAxrqwJ4Sl2Oa4XRPb5FrVjYbZs_FftANpCMgJP88kF3C0Qh9p01BBteB-vjkcwvJocJ3flQ26JziHms2Z1PErSezpqX8-ihQyMjF_p3npQESEdVoN1etSABEhCv0KCafPROSzqKAhkJu57tGhQJoVA-Nhq9Gh9dOSbFq3FSBQS2bg", + "width": 650 + } + ], + "place_id": "ChIJs8MdNo9ZwokRTPUHiArLC-o", + "price_level": 2, + "rating": 4.5, + "reference": "CnRnAAAAKokP3tfwzdvd9LLDVv8i_DwxsATv4cb1wVtVKMk9LMjEMuYmNs2SK6W9XGvkUrfQ8VPWk_iVu0LrKDeH_hqxM-h47qVA8js04qpGb-jTu6xgmWQkn8IW4T-sx2BhzoX3cViE9-xcnTtI0gqeXGWHbBIQ1Cxp0I8jKjVffSTp70HHZRoUFynxnJ06VWS66sLlMeG6hcT0KQ0", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "213 1st Avenue, New York, NY 10003, United States", + "geometry": { + "location": { + "lat": 40.730574, + "lng": -73.983465 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "f3f758d1276297b58f5c62e8822a2234f8fc7b82", + "name": "Luzzo's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/111431098218367577125\"\u003eLuzzo's\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAZbPpBwCrras1NNObL-hsIeC5GwQgJDmBKYgh__PMPlNwIGESh8T8wEl9lPiBMV270wR6VbWMUpuwNp225GGzsWexxge59bTSrtE3_Z8R2XSiJXH_3uIYpYMpF631WO43EhCYP_KDSOBBozjeYfXMH8kLGhQK3WrY86vxvZskjMlzJEap1SEOXw", + "width": 1536 + } + ], + "place_id": "ChIJaf7QAJ5ZwokR0I29INQQLXU", + "price_level": 2, + "rating": 4.3, + "reference": "CmRaAAAAuHi4c5p5-IHMUfNGMundimxEl9y17NE6uLjFlj6Z2Yb17vek151db0hoxRMrbbhBMWcZ8uu02MpeHWuv_QE-E71Ki1kW68sWLvzHXRG3F-FP8DxEy60o0QDyN1l0RaDLEhCaLx2NZ1aLwxs2jxo55njDGhQFuYLyN8zBUJXZQH9Kf3G2fgfqFQ", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "187 Bedford Ave, Brooklyn, NY 11211, United States", + "geometry": { + "location": { + "lat": 40.717628, + "lng": -73.957756 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "c847d5753e75e1f7e6959f7f5fd7e491138b1380", + "name": "Fornino", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 480, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/101431831624077907171\"\u003eBen Schwartz\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAA3c6NcmdCjrOfcXgU-IUoCVPUmTAuh0yD5pOmQgBUKNWWZ4qQ39zoHi0dZ2DhFHa_ZjKkE_UjERFmzdha40vS2_dIrXyww8QWynQo32XMx_XwV3dn2fyFQNdQBQ-mvzf5EhBlwiaKfnhDN7C5Ew00WVKGGhS2aJMkjkc9FQoolrhSe0x1s1RnDw", + "width": 640 + } + ], + "place_id": "ChIJQdsYw11ZwokRGIm5hSnvOb8", + "price_level": 2, + "rating": 4.2, + "reference": "CmRbAAAA3fUmaNm1V5ub8EmqKiLXoUyTim2oaMd5irH-bxPLIpPebyfX9amCcfakuT-EEFEigRLCqpqoDr-twa3zFlckXT5OKTHrfvkeDfgDKcKmWLMCT4k3dfgKkOXgDVZwkT7PEhBd283nx27svzQgyNO4K-MLGhRQPrKnvVffjpNdDsmy818nKqQDAQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "204-23 Hillside Avenue, Jamaica, NY 11423, United States", + "geometry": { + "location": { + "lat": 40.721077, + "lng": -73.760598 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "e9a38b965bf24881fd3b8da1f1440a59fdc5cf4c", + "name": "Gaby's Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 612, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/109881175819738762038\"\u003eDerek Penn\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAArFw-CRzch8rQ_h9pil1hJ72RmK-2b4P1_s42h-hYsA0Lt56w4Tnk7A7eEbBwpB8UFRPBx6muK5dhFcEJYD1OIiKbTCup7Gj8YfSErjsYsgJ3NXYwvWT3ljk6FGgLCWf9EhDFn_kBE5o7nINBJcplrLPqGhR94fidZ4e6WAS0LyBUOr-6_jzpEg", + "width": 816 + } + ], + "place_id": "ChIJ1U4Yc3dhwokRARHDgoiFe8g", + "price_level": 1, + "rating": 4.4, + "reference": "CmRgAAAACyOAgdXDBCTqmCw1-wgcatv7Uo4zHtf2nc0cCxsgyDtCiJnsF72lFyO8a7R0fPcOAkuw461wJh9pJFLclrxIti9r0M8YyidScitfTLYMBm0azwxfmcU5-xJg-M1iNtrmEhAAteNd71udVT559x6hAvj7GhSx6XPfXANtYFIDAWd0z6yk0ycaDQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "2 Gold St, New York, NY 10038, United States", + "geometry": { + "location": { + "lat": 40.707449, + "lng": -74.006871 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "123ea3336b5907b41acfd32bbc297fd13235799d", + "name": "Harry's Italian", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 593, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107671997219038570274\"\u003eHarry's Italian\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAdJ_y427jw_YKxLmjjjL7KKWTXxVCraPs_1ADHt6BwSwG0Wwl2Dt7-tKpTNHaNKmlU0HK8d_ScfAdXixmBX0TbaPmM0u-fOuk0ZOtustqApYIpKxSYFzJW_iPFVmvYEhCEhAPd_5SKGCLtm5VRixXc-bcGhTGAJMPAxW4z-6Scf2eiRJve4TdRg", + "width": 1056 + } + ], + "place_id": "ChIJtfyA3hdawokRQ6bgjbSJN3o", + "price_level": 2, + "rating": 4.2, + "reference": "CnRiAAAAcuCzae3HrNCoeOOfOtJAgzwf0pVPP-BFOB3M2SZcOJnTgjvY177u6TInWHTMfFOKHyFTJpRzbNxah4jIrX0c7DEBDM1UbMov_Pl-Zugf4XMdYn3IITvJT92wRw1U3fjhI7MAS6xmtOOe86ts8ZK7FBIQT3tUiw4JBhhgPLOA1F5tYBoUSrWqL5tmx1RAOXp2Wp0HCK0IBcc", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "32 Spring St, New York, NY 10012, United States", + "geometry": { + "location": { + "lat": 40.721534, + "lng": -73.99562400000001 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "73973bd1fac905f102ee1afe536594dc42bca5ff", + "name": "Lombardi's Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 612, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/106821247590778969964\"\u003eBen Jimenez\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAoWZRTcgV4Fbhn1g8Pp2HjMR8euQGmy0lscjMw07uffk9vj9ZbeSbI8zaj_LTZYhe1wlr4lO8WQklBmErJE7r_0BrItgVxLtCY4uB2ny1RCcZnfnAroka07D1dijEpJdaEhDsH_lXXzWsFDquYnwlYSSzGhRuWkz44Hgh8xnfuUubRz9dO73KQg", + "width": 816 + } + ], + "place_id": "ChIJp-cWE4pZwokRmUI8_BIF8dg", + "price_level": 2, + "rating": 4, + "reference": "CnRkAAAAIftys6z98sbUjspBpK-cmiylYgBhDb-E26j3yQWnOD0VXaTdDwl7gQUI8pR0Wzj8wPmSAybEKrvNHDntqkKwVvLsex_V_9vXrnESdYULonlAu_Sz3lT-yd4zyAdH5yFiRCO3zZxSMi0OyPAwAAEfAhIQAAUymNWrl_VVodO9i7-jXhoU8POj-oV3gnOLBVYtyAKJO6kmhe4", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "278 Bleecker St, New York, NY 10014, United States", + "geometry": { + "location": { + "lat": 40.73164, + "lng": -74.003387 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "4e321140541ea1ec775cddbaa104f977e955e858", + "name": "John’s of Bleecker Street", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2048, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/113107750524422671028\"\u003ePablo De La Noche\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAA4pwncdSg3OgyjubX1xWy9fQmoQ6rZgPGKDrXYrBDooUKR47aIOOcp7kJsO9Hcoug-eED6KmRLJwe-jz5tc4So-SDFGjujyRbCL76RHdvurr4myO6JMdsYx-ufaLdYxlyEhC4rSehObuicJgfwdexfZ5LGhSk8i5cf4QJsknKUx91mbvaGWY6Tg", + "width": 1536 + } + ], + "place_id": "ChIJuW43oZNZwokRdE5tLzpuykE", + "price_level": 2, + "rating": 4.1, + "reference": "CnRuAAAAuCxl_Ovn7MiZIwO9sFfN63oQrldXqbYPjaRJkZGQiLXpFGNZpOzNPMlwD2dHr8XgqJRrEJ6NgDu8Y03ZyC_S1SCHMbiN2hwO2oNUf6YeHaN3Zb_KuBeqe15UmGHjkzJ4Xxe3gZVT5V0pKbbr9FoNEhIQxADD1qC72TE1MZHw1t4CzRoUBGSXExrmGIoTheOosVNIP_pIu24", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "261 Moore St, Brooklyn, NY 11206, United States", + "geometry": { + "location": { + "lat": 40.705077, + "lng": -73.933592 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "3346c75baf04f2affe179999522b012c9a4f96b5", + "name": "Roberta's", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAVxae92P32vQXHrQZuxmS0E71Jfofa1akdxtcQbU3hTAGJNAgzocJqr5G5lhl7WhXMQ6m4OUYhPLTObU5FBhXo42ViCe_jT9IDqU4IKsy-kMt7I4Fo_wN_B9sWUGchTvcEhBmME8Jp8lPIBiMSKGmod4rGhSvLB4QAKxOy4MPb3BrDWW7XaGiNQ", + "width": 2048 + } + ], + "place_id": "ChIJ87Mc5wBcwokRAj4JNcwppaE", + "price_level": 2, + "rating": 4.4, + "reference": "CmRdAAAAFH_hhZFb2xp-uCyH2EDdzM7-Uxm5kd6FdugnrpSrkN7cLofd8t0P0FsQino0ZwV7PguAI6zmI9wkgFDf_UxeCnpamR64H6Uj4f9WoMwQbA9_EyEdRiVqZvifZPH9eacNEhDfdM7gWVwUotRejlNjvibbGhStd0RCQ_whWIl2ge_635ssD34HcQ", + "types": [ + "bakery", + "store", + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "413 8th Ave, New York, NY 10001, United States", + "geometry": { + "location": { + "lat": 40.750182, + "lng": -73.995272 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "5ed03945fe2d1238dba9bb50ac5a2d19f5062f4f", + "name": "New York Pizza Suprema", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 612, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/105768913879991733999\"\u003eMarco Vermeulen\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAUuAa1IDQfJ4AHBcgn9pgJ2K4tCmvfupH7SMvQRd66Np-lQZzg74LBnXaRe_Xso6A32kJuRr7fYNApexNhlHOlhQ6kuZ5UTcAl_4puGV1aqmV02RjGqUTdu6fj6U8399OEhCYWULeyv_Vos_ExlkEDGO_GhRyh43BNQL1bw9Y5TL11kW1tLVLZg", + "width": 816 + } + ], + "place_id": "ChIJsS_-2rFZwokRWYvlpNNbjmg", + "price_level": 2, + "rating": 4.3, + "reference": "CnRpAAAAFjt0jZQINEvq61LSG-CvnUz6O0-ZAs_Sj_snGzn0T431qKZJ0wAd6WWGMyODdztHVR5TLjW1kHJw5OkThFX6KRuPdVO4J2zqHz0ro-q4lmg2yyRUeq-h4-mRHV_372FIqS0onZNJPARv2agyIheIDBIQKhn37-87-w5Nj-ukLm4ppxoU1axD1EylKFGJHfQyuoDTNe2lOns", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "575 Henry St, New York, NY 11231, United States", + "geometry": { + "location": { + "lat": 40.681808, + "lng": -74.00030700000001 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "bfa17bb2a401684b663bffce96754b388c8b0a38", + "name": "Lucali", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1365, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104111246635874032234\"\u003eZAGAT\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAh40hasPObliHkJfHpCPRy0gR4paYOkRffBAlMiSyhz2ix9dcKTKkbnwaFZVRvMUWEzQ9k6t6KJ85sRXl9gp1JmqX7k9bI2bhQOZXBw9fS0I39SbdULppJ1clFG2IsMKkEhARA2qNKgGbxXzioThIfgSUGhRj_TySeu9re8pcUUz970AOBZfUhA", + "width": 2048 + } + ], + "place_id": "ChIJ395CMVlawokRt2oLH_8zmvI", + "price_level": 2, + "rating": 4.5, + "reference": "CmRaAAAAci6kZPBRUevRsz6lMc8bxwb8xSYYl0COAGweeLatfhOX9PsYCPbbaheyIfiqzyPuNmtmnNQx1VcoM_sYSC14FNBjCuyjrZkuEupPqbJouomAFbGKbGwsGVwoYSQj7erjEhA32gPe5EO5A5D_p5pi1b0SGhQ0knAov_JPNzjiZvpywkbgLC21hQ", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "230 Ninth Ave, New York, NY 10001, United States", + "geometry": { + "location": { + "lat": 40.747143, + "lng": -74.000524 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/bar-71.png", + "id": "ee01c82cb5b6b95f2eceb80ff45e56343aa468e3", + "name": "Co.", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 648, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/104819208193648646391\"\u003eGregor J. Rothfuss\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAA-TH_yUgtHaNuxOu0atojwIOLO9c8gvAZ9rXawlhAlSCapnY045Txxhwj0mZURSQQJqQMOdF6VRSa_rRj7d_SswC1IQJoY0w55sGu_uMMX0RLZI9F6zecXdM_ZUo2SVUJEhA0o1Z8gzFx-fQXuBtofRmaGhSC7I6A3zIGjNwhL39Wqqxhm4l3mw", + "width": 486 + } + ], + "place_id": "ChIJ84bLVbpZwokRMdylFSXlstU", + "price_level": 2, + "rating": 4.1, + "reference": "CmRXAAAAi7Bt0whmDxxN8DpWB04SNI4hdExBg5wcFstcMYPSKrN4Llpymibx3152nGy9h3bEJCzHw2fTIPB7esY0QC8Xudij6sCjOB_GsYQWllb37MI0jsekboJpdyFTyGKnzusBEhCmf0uh0Iu5-ygZTnm6mipfGhQWojYW1GP80r7si-Mnc2p9Rc8eJg", + "types": [ + "bar", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "1718 Hylan Blvd, Staten Island, NY 10305, United States", + "geometry": { + "location": { + "lat": 40.586795, + "lng": -74.09149600000001 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "8b4170e5eb8eec81a58a57820d62db72ced51a0a", + "name": "The Original Goodfella's Brick Oven Pizza", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 1119, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116128137754443508332\"\u003eThe Original Goodfella's Brick Oven Pizza\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAaBPG0FgBZ3v4YrZkhdPfYtoEXlRg9AWU3pJkXKj1TIOY1t1aekQetZLgEGAFEUZdaCzUGnmomoqwENde32rkmPNndmv3odRqd8UejPk8m5UsIqpcSqdDGPUTqgsO7AaGEhAHzRE6dk3U_qdSnjGqv_uCGhSZJzfxKfG3FZcnTqqj6XeegTuFUw", + "width": 1246 + } + ], + "place_id": "ChIJDWI8hsROwokRwFYtU_yYA5I", + "price_level": 2, + "rating": 4.4, + "reference": "CoQBfQAAAAo2tPhiyHuV9Oa5OcuLDSkKEjf8a74rPsCqxoLjo1KUvjpKARD79Cl_3diTmdTGmkmEfAEAwrePKh6HXxlnGDjehIf123mDd_qzLhWo38dMJU08V9wLrmEsmyrgyPYPGPAvic9VgUDM6t5iSvOioDOnJzhhg0DJLxC6I6h8faFOEhAeaqHuye0j-IwSxGzfjq3VGhTD676yuOXVvToTuE8cQx5av2nf0Q", + "types": [ + "meal_takeaway", + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "121 Dyckman St, New York, NY 10040, United States", + "geometry": { + "location": { + "lat": 40.862453, + "lng": -73.92507000000001 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "de41d73b2c2d60b077abe485d267687d113aec18", + "name": "Pizza Palace", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 453, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/103992368486725463147\"\u003ePizza Palace\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAADheccWvhVPqHrmIyfkSrOVB4gS4lOJUF_Xgp02aPT9ILPYyYwwD5-F21tcdu5kH2D-WVVpTQb07KNNLxUXRdOHmtcdcwF4Sh8zm9Pl06Z0JT2iMUvIph032bqIJjRWvuEhAwQW4Dk6tIQi1QMVJaLMk3GhRswpjYbe_6tnADWyTZavsAxoOI0A", + "width": 604 + } + ], + "place_id": "ChIJkXfgMAb0wokR6dl8IQPbmhI", + "price_level": 2, + "rating": 4.6, + "reference": "CmRfAAAAANhvxd-0O6607Vhulh6uo-mqinGQL8P0ZmLxjTAtoQfF2x4lHcc7BBehFGkTAC4J5yVu311FgcMPoqkuSQcyA2iDl_nr1Q9Kx_muVs6E0YWDxeIRol1fFOLUZMX9X2moEhCz86aYB6ztkPUACmdMtqX-GhQ94eTOfAukO3kA0y1gWji-18fsfw", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "298 Atlantic Ave, Brooklyn, NY 11201, United States", + "geometry": { + "location": { + "lat": 40.688307, + "lng": -73.98899 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "5da5ce5a2ee9bae83c5cd1c49ab0e917b0f0e8ab", + "name": "Sottocasa Pizzeria", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2322, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/107207602668522843168\"\u003eKris Gamache\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAA6fU4oO2iTKvmBuDTEK8MV8dKygCwufqOmelk1vZPP3GPw8FjWhjVxkEuZNjLIBqP7LVmINedtRlZE483nb5CTzh_6V1agyozA9W9zD0DU8zUBDCTi3jidH8tbTbNl3bNEhB8589LADFDsohGTNOZ4MGmGhSRbNIEwPJLt52iuYPAA-ACfW-gRg", + "width": 4128 + } + ], + "place_id": "ChIJjy3QdU5awokRPUY7-kf_A_0", + "price_level": 2, + "rating": 4.6, + "reference": "CnRmAAAAX5U0kA8rnaf3g_oSCOImuOdmuESZM77TeKNGCBYs9UKvF1DKQE5Vj1-DJJMZxIo7A7kwXZT9NGuwjKxsu_-G1QH4CcMNpGBR83w-pMwzEfByl81vTyvx8sr2hB3d54k0xESbOc71RdaFVGXgDS330hIQHssWRYy6XvY5JHwlSwRfoRoUbXHvx0kv1RnP-zLX-LkDLrUn0t0", + "types": [ + "restaurant", + "meal_delivery", + "food", + "point_of_interest", + "establishment" + ] + }, + { + "formatted_address": "98 Avenue B, New York, NY 10009, United States", + "geometry": { + "location": { + "lat": 40.724725, + "lng": -73.981616 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/restaurant-71.png", + "id": "7fff9117847fb9d358244fa120d8e9ca02a4da7a", + "name": "Gruppo", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 533, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/116453330796515691972\"\u003eAlvaro Lopez\u003c/a\u003e" + ], + "photo_reference": "CmRdAAAAmp9tJCW3TZyRyEy-VouRlGrnTUpaH_Nmi3ThwXSaQQXU7cs1PyUAutIN1-tAE4U2rKNCkfiLYCPkGyvFjBlDEIUDpVRS8gSEXv8bcjzvSP2-jQiTYZzJewE7ccBAjd4FEhD4P17AF24pQemUZN3JQI05GhQ2QPCIrNx4b6nefIp2EDWPu0cOlw", + "width": 800 + } + ], + "place_id": "ChIJOYIs-3ZZwokRH5msQ2RZCQ8", + "price_level": 2, + "rating": 4.3, + "reference": "CmRZAAAAsTzVKUitxDZsLIQprdw4NqDT8z3rYth6c8_zNr5EpkP9dIdeHthUNAQbzwLjtAbIW5jPM6XwnxgwwIW-xrNQUUlVEPAAesCL-HWp6DIef1wra0AlsUv-dsC67o8hFvjwEhC4OBUtZ_1C8-Jw6eJ7zUUJGhQB0k8t8vs_RIYjbv4hSkDG4xos4w", + "types": [ + "restaurant", + "food", + "point_of_interest", + "establishment" + ] + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/TextSearchResponse.json b/src/test/resources/com/google/maps/TextSearchResponse.json new file mode 100644 index 000000000..c74ebce71 --- /dev/null +++ b/src/test/resources/com/google/maps/TextSearchResponse.json @@ -0,0 +1,38 @@ +{ + "html_attributions": [], + "results": [ + { + "formatted_address": "5, 48 Pirrama Rd, Pyrmont NSW 2009, Australia", + "geometry": { + "location": { + "lat": -33.866611, + "lng": 151.195832 + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/generic_business-71.png", + "id": "4f89212bf76dde31f092cfc14d7506555d85b5c7", + "name": "Google", + "opening_hours": { + "open_now": false, + "weekday_text": [] + }, + "photos": [ + { + "height": 2322, + "html_attributions": [ + "William Stewart" + ], + "photo_reference": "CmRdAAAAa43ZeiQvF4n-Yv5UnEGcIe0KjdTzzTH4g-g1GuKgWas0g8W7793eFDGxkrG4Z5i_Jua0Z-Ib88IuYe2iVAZ0W3Q7wUrp4A2mux4BjZmakLFkTkPj_OZ7ek3vSGnrzqExEhBqB3AIn82lmf38RnVSFH1CGhSWrvzN30A_ABGNScuiYEU70wau3w", + "width": 4128 + } + ], + "place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", + "rating": 4.4, + "reference": "CmRaAAAA3EN_NWHqY6zWa9vRX-tfJNji1FqeIxf0_V3xZQmBezAPO3SRwOrjRJzKxjnxTvhB_Lz4dRvLp1HTfoAThW4aFwVmqmE_V-3saLDSF77rXfclqxA9ncQkHXhLFg0J4AqXEhDh8umU5GO0JU1aeJVJeGFgGhR-xp1AIR1GlQOE53OqTADfFQwy8Q", + "types": [ + "establishment" + ] + } + ], + "status": "OK" +} diff --git a/src/test/resources/com/google/maps/UtfResultGeocodeResponse.json b/src/test/resources/com/google/maps/UtfResultGeocodeResponse.json new file mode 100644 index 000000000..c7f8cd755 --- /dev/null +++ b/src/test/resources/com/google/maps/UtfResultGeocodeResponse.json @@ -0,0 +1,394 @@ +{ + "results": [ + { + "address_components": [ + { + "long_name": "1", + "short_name": "1", + "types": [ + "street_number" + ] + }, + { + "long_name": "Rue Fernand Raynaud", + "short_name": "Rue Fernand Raynaud", + "types": [ + "route" + ] + }, + { + "long_name": "Châteauroux", + "short_name": "Châteauroux", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Indre", + "short_name": "Indre", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "Centre-Val de Loire", + "short_name": "Centre-Val de Loire", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "36000", + "short_name": "36000", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "1 Rue Fernand Raynaud, 36000 Châteauroux, France", + "geometry": { + "location": { + "lat": 46.8024498, + "lng": 1.6551494 + }, + "location_type": "ROOFTOP", + "viewport": { + "northeast": { + "lat": 46.8037987802915, + "lng": 1.656498380291502 + }, + "southwest": { + "lat": 46.8011008197085, + "lng": 1.653800419708498 + } + } + }, + "place_id": "ChIJaTxVlWSg-0cR0flO9_azzKY", + "types": [ + "street_address" + ] + }, + { + "address_components": [ + { + "long_name": "Châteauroux", + "short_name": "Châteauroux", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Indre", + "short_name": "Indre", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "Centre-Val de Loire", + "short_name": "Centre-Val de Loire", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + }, + { + "long_name": "36000", + "short_name": "36000", + "types": [ + "postal_code" + ] + } + ], + "formatted_address": "36000 Châteauroux, France", + "geometry": { + "bounds": { + "northeast": { + "lat": 46.8297319, + "lng": 1.742437 + }, + "southwest": { + "lat": 46.774839, + "lng": 1.6381469 + } + }, + "location": { + "lat": 46.811434, + "lng": 1.686779 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 46.8297319, + "lng": 1.742437 + }, + "southwest": { + "lat": 46.774839, + "lng": 1.6381469 + } + } + }, + "place_id": "ChIJzXfzo6ug-0cR99Yrx4WBLhk", + "types": [ + "locality", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "36000", + "short_name": "36000", + "types": [ + "postal_code" + ] + }, + { + "long_name": "Châteauroux", + "short_name": "Châteauroux", + "types": [ + "locality", + "political" + ] + }, + { + "long_name": "Indre", + "short_name": "Indre", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "Centre-Val de Loire", + "short_name": "Centre-Val de Loire", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "36000 Châteauroux, France", + "geometry": { + "bounds": { + "northeast": { + "lat": 46.8298083, + "lng": 1.7424276 + }, + "southwest": { + "lat": 46.7748461, + "lng": 1.6382288 + } + }, + "location": { + "lat": 46.8031198, + "lng": 1.6926546 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 46.8298083, + "lng": 1.7424276 + }, + "southwest": { + "lat": 46.7748461, + "lng": 1.6382288 + } + } + }, + "place_id": "ChIJMcZg2bqg-0cRUPwJiNrIDRw", + "types": [ + "postal_code" + ] + }, + { + "address_components": [ + { + "long_name": "Indre", + "short_name": "Indre", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "long_name": "Centre-Val de Loire", + "short_name": "Centre-Val de Loire", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Indre, France", + "geometry": { + "bounds": { + "northeast": { + "lat": 47.277465, + "lng": 2.204572 + }, + "southwest": { + "lat": 46.3469059, + "lng": 0.8674139 + } + }, + "location": { + "lat": 46.6613966, + "lng": 1.4482662 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 47.277465, + "lng": 2.204572 + }, + "southwest": { + "lat": 46.3469059, + "lng": 0.8674139 + } + } + }, + "place_id": "ChIJUfVaUf6d-0cRwCczBdfIDQM", + "types": [ + "administrative_area_level_2", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "Centre-Val de Loire", + "short_name": "Centre-Val de Loire", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "Centre-Val de Loire, France", + "geometry": { + "bounds": { + "northeast": { + "lat": 48.941029, + "lng": 3.1284099 + }, + "southwest": { + "lat": 46.3469059, + "lng": 0.0527369 + } + }, + "location": { + "lat": 47.7515686, + "lng": 1.6750631 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 48.941029, + "lng": 3.1284099 + }, + "southwest": { + "lat": 46.3469059, + "lng": 0.0527369 + } + } + }, + "place_id": "ChIJiV0INnu55EcRMCUzBdfIDQE", + "types": [ + "administrative_area_level_1", + "political" + ] + }, + { + "address_components": [ + { + "long_name": "France", + "short_name": "FR", + "types": [ + "country", + "political" + ] + } + ], + "formatted_address": "France", + "geometry": { + "bounds": { + "northeast": { + "lat": 51.1241999, + "lng": 9.6624999 + }, + "southwest": { + "lat": 41.3253001, + "lng": -5.5591 + } + }, + "location": { + "lat": 46.227638, + "lng": 2.213749 + }, + "location_type": "APPROXIMATE", + "viewport": { + "northeast": { + "lat": 51.0891628, + "lng": 9.5597934 + }, + "southwest": { + "lat": 41.342778, + "lng": -5.1422579 + } + } + }, + "place_id": "ChIJMVd4MymgVA0R99lHx5Y__Ws", + "types": [ + "country", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/src/test/resources/com/google/maps/placesApiKitaWardResponse.json b/src/test/resources/com/google/maps/placesApiKitaWardResponse.json new file mode 100644 index 000000000..aa614cf6e --- /dev/null +++ b/src/test/resources/com/google/maps/placesApiKitaWardResponse.json @@ -0,0 +1,45 @@ +{ + "html_attributions": [], + "results": [ + { + "formatted_address": "Kita Ward, Kyoto, Kyoto Prefecture, Japan", + "geometry": { + "location": { + "lat": 35.041053, + "lng": 135.7539826 + }, + "viewport": { + "northeast": { + "lat": 35.1714945, + "lng": 135.7728535 + }, + "southwest": { + "lat": 35.0222614, + "lng": 135.6471605 + } + } + }, + "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/geocode-71.png", + "id": "4e06ba9fcbf74a0da71957b8ca78b0cd25ddc09d", + "name": "Kita Ward", + "photos": [ + { + "height": 1151, + "html_attributions": [ + "\u003ca href=\"https://maps.google.com/maps/contrib/117368444055293763041/photos\"\u003e李宗仁\u003c/a\u003e" + ], + "photo_reference": "CmRaAAAACE2XYCQBTPkWNbK47AeXSaNWfHKkwZOF3HCiXo7PEcRkfC5MVBxhF625JzsR5g6EHKWO2dUY69Onaw8FwE3oLZGl3K5Mxkp8YV7sTyi-JcdAtnIIZNmK2TDFVjsKPHO-EhBdXuDlIrLdlttc4Mq2FmRFGhRdYz6pANpp-UGfzqve7pVW0zwS6g", + "width": 2048 + } + ], + "place_id": "ChIJHSR_jiupAWARcQjngz-_Cxk", + "reference": "CmRbAAAAbjP4m3huGPaypYNsSBIvcovO89q8VMXCKgwhridIYah5ykN0kn-9vzuEYu2N-kYKdiQnHSMrOAEcATQ9iJ66kQdrp1ax5CVPwYMb4pFhzh1ggP8hN3uCVn1xtJd1cDZqEhDhPn7oZxPH4u5JTBC5MelRGhQgll-Wp595aQRT7M9fggvV9RINhQ", + "types": [ + "ward", + "locality", + "political" + ] + } + ], + "status": "OK" +} \ No newline at end of file