diff --git a/.bazeliskrc b/.bazeliskrc new file mode 100644 index 00000000..5af90737 --- /dev/null +++ b/.bazeliskrc @@ -0,0 +1,5 @@ +# Set options for Bazelisk, a wrapper for Bazel. + +# Set the version of Bazel to use. +# TODO: #642 -- Update once the JS bazelbuild/rules_closure supports bazel modules. +USE_BAZEL_VERSION=7.4.1 diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 00000000..c5923401 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,9 @@ +# Reduce OOM errors on TravisCI +startup --host_jvm_args=-Xms500m +startup --host_jvm_args=-Xmx500m +startup --host_jvm_args=-XX:-UseParallelGC +# build --local_resources=400,2,1.0 +build --worker_max_instances=auto + +# Configure tests - increase timeout, print everything and timeout warnings +test --verbose_failures --test_output=all --test_verbose_timeout_warnings diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..46f15666 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,176 @@ +name: CI +on: [push, pull_request, workflow_dispatch] +jobs: + # C implementation. Lives in c/, tested with bazel. + test-c: + runs-on: ubuntu-latest + env: + OLC_PATH: c + steps: + - uses: actions/checkout@v4 + - uses: bazelbuild/setup-bazelisk@v3 + - name: test + run: bazel test --test_output=all ${OLC_PATH}:all + - name: check formatting + run: cd ${OLC_PATH} && bash clang_check.sh + + # C implementation. Lives in c/, tested with bazel. + # Use Mac with Apple Silicon for this test because of different floating point math + # which can affect test results https://github.com/google/open-location-code/issues/652 + test-c-macos-latest: + runs-on: macos-latest + env: + OLC_PATH: c + steps: + - uses: actions/checkout@v4 + - uses: bazelbuild/setup-bazelisk@v3 + - name: test + run: bazel test --test_output=all ${OLC_PATH}:all + + # C++ implementation. Lives in cpp/, tested with bazel. + test-cpp: + runs-on: ubuntu-latest + env: + OLC_PATH: cpp + steps: + - uses: actions/checkout@v4 + - uses: bazelbuild/setup-bazelisk@v3 + - name: test + run: bazel test --test_output=all ${OLC_PATH}:all + - name: check formatting + run: cd ${OLC_PATH} && bash clang_check.sh + + # Dart implementation. Lives in dart/. + test-dart: + runs-on: ubuntu-latest + env: + OLC_PATH: dart + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + - name: test + run: | + cd ${OLC_PATH} + dart pub get && dart test + bash checks.sh + + # Go implementation. Lives in go/. Tests fail if files have not been formatted with gofmt. + test-go: + runs-on: ubuntu-latest + env: + OLC_PATH: go + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.23' + - name: test + run: | + cd ${OLC_PATH} + diff -u <(echo -n) <(gofmt -d -s ./) + go test -bench=. ./ -v + - name: test-gridserver + run: | + cd tile_server/gridserver + go test ./ -v + + # Java implementation. Lives in java/, tested with bazel and maven. + test-java: + runs-on: ubuntu-latest + env: + OLC_PATH: java + strategy: + matrix: + java: [ '17', '21' ] + name: test-java-${{ matrix.java }} + steps: + - uses: actions/checkout@v4 + - uses: bazelbuild/setup-bazelisk@v3 + - name: Setup java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ matrix.java }} + - name: test + run: bazel test --test_output=all ${OLC_PATH}:all && cd ${OLC_PATH} && mvn package -Dsurefire.useFile=false -DdisableXmlReport=true + + # Javascript Closure library implementation. Lives in js/closure, tested with bazel. + test-js-closure: + runs-on: ubuntu-latest + env: + OLC_PATH: js/closure + steps: + - uses: actions/checkout@v4 + - uses: bazelbuild/setup-bazelisk@v3 + - name: test + run: | + bazel test ${OLC_PATH}:all + cd js && npm install && ./node_modules/.bin/eslint closure/*js + + # Javascript implementation. Lives in js/. + test-js: + runs-on: ubuntu-latest + env: + OLC_PATH: js + steps: + - uses: actions/checkout@v4 + - name: test + run: | + cd ${OLC_PATH} + bash checks.sh + + # Python implementation. Lives in python/, tested with bazel. + test-python: + runs-on: ubuntu-latest + env: + OLC_PATH: python + strategy: + matrix: + python: [ '3.11', '3.12', '3.13' ] + name: test-python-${{ matrix.python }} + steps: + - uses: actions/checkout@v4 + - uses: bazelbuild/setup-bazelisk@v3 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: test + run: bazel test --test_output=all ${OLC_PATH}:all + - name: check formatting + run: | + cd ${OLC_PATH} + pip install yapf + python3 -m yapf --diff *py + exit 0 + + # Ruby implementation. Lives in ruby/. + test-ruby: + runs-on: ubuntu-latest + env: + OLC_PATH: ruby + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3.6' + - name: test + run: | + gem install rubocop + gem install test-unit + cd ${OLC_PATH} && ruby test/plus_codes_test.rb + rubocop --config rubocop.yml + + # Rust implementation. Lives in rust/. + test-rust: + runs-on: ubuntu-latest + env: + OLC_PATH: rust + steps: + - uses: actions/checkout@v4 + - name: test + run: | + cd ${OLC_PATH} + cargo fmt --all -- --check + cargo build + cargo test -- --nocapture diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e7c3a2d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Ignore backup and swap files. +*~ +.*.swp +# Bazel module lock file. +MODULE.bazel.lock +# Ignore all bazel-* links. +/bazel-* +# Ignore outputs generated during Bazel bootstrapping. +/output/ +# Ignore compiled files +**/target/ +# Ignore intelliJ auto generated dir +**/.idea/ +# Ignore iml extensions files +**/*.iml +# Ignore visual studio auto generated dir +**/.vscode/ +# Ignore JS NPM modules +/js/node_modules/ +/js/package-lock.json +# Ignore dynamically generated test JSON files. +/js/test/*json +# Ignore Rust lockfile. +/rust/Cargo.lock diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5643bc2a..00000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -sudo: false -# Travis only supports one language per project, so we need to put something -# here. node_js is as good as any other I suppose. -language: node_js -node_js: - - "0.10" - - -# Install package dependencies. See -# http://docs.travis-ci.com/user/installing-dependencies/ -# gulp: required for JS testing -before_install: - - npm install -g gulp - -# Define the list of directories to execute tests in. -env: - - TEST_DIR=js - - TEST_DIR=go - - TEST_DIR=ruby - -# Test script to run. This is called once for each TEST_DIR value above. -script: ./run_tests.sh diff --git a/API.txt b/API.txt deleted file mode 100644 index f4150ee8..00000000 --- a/API.txt +++ /dev/null @@ -1,97 +0,0 @@ -Open Location Code API Reference - -The following public methods should be provided by any Open Location Code -implementation, subject to minor changes caused by language conventions. - -Note that any method that returns an Open Location Code should return -upper case characters, with the prefix, and full codes should include -a separator if they have five or more characters. - -Methods that accept Open Location Codes as parameters should be case -insensitive, prefixes optional, and for full codes the separator character -is optional (although if present, it is permissable to throw an error if -there are more than one or if it is in the wrong position). - -isValid: - The isValid method takes a single parameter, a string, and returns a - boolean indicating whether the string is a valid Open Location Code - sequence or not. - - To be valid, all characters must be from the Open Location Code character - set with at most one separator. If the separator character is present, it - must be after four characters. If the prefix character is present, it must - be the first character. - -isShort: - The isShort method takes a single parameter, a string, and returns a - boolean indicating whether the string is a valid short Open Location Code - or not. - - A short Open Location Code is a sequence created by removing the first - four or six characters from a full Open Location Code. - - A code must be a possible sub-string of a generated Open Location Code, at - least four and at most seven characters long and not include a separator - character. If the prefix character is present, it must be the first - character. - -isFull: - Determines if a code is a valid full Open Location Code. - - Not all possible combinations of Open Location Code characters decode to - valid latitude and longitude values. This checks that a code is valid and - also that the latitude and longitude values are legal. If the prefix - character is present, it must be the first character. If the separator - character is present, it must be after four characters. - -encode: - Encode a location into an Open Location Code. This takes a latitude and - longitude and an optional length. If the length is not specified, a code - with 10 characters (excluding the prefix and separator) will be generated. - -decode: - Decodes an Open Location Code into the location coordinates. This method - takes a string. If the string is a valid full Open Location Code, it - returns an object with the lower and upper latitude and longitude pairs, - the center latitude and longitude, and the length of the original code. - -shortenBy4: - Passed a valid full 10 or 11 character Open Location Code and a latitude - and longitude, this tries to remove the first four characters. This will - only succeed if both the latitude and longitude are less than 0.25 - degrees from the code center. (Up to 0.5 degrees difference would work, - the requirement for 0.25 degrees represents a safety margin for cases - where the coordinates have come from a geocoding system and where the - center of large entities, such as cities, is subject to debate.) - - If not possible, the original code is returned. If trimming is possible, - the first four characters are removed and the subsequent characters are - returned with the prefix added. - -shortenBy6: - Passed a valid full 10 or 11 character Open Location Code and a latitude - and longitude, this tries to remove the first six characters. This will - only succeed if both the latitude and longitude are less than 0.0125 - degrees from the code center. (Up to 0.025 degrees difference would - work, the requirement for 0.0125 degrees represents a safety margin for - cases where the coordinates have come from a geocoding system and where - the center of large entities, such as cities, is subject to debate.) - - If not possible, the original code is returned. If trimming is possible, - the first six characters are removed and the subsequent characters are - returned with the prefix added. - -recoverNearest: - This method is passed a valid short Open Location Code (of four to seven - characters) and a latitude and longitude, and returns the nearest - matching full Open Location Code to the specified location. - - The number of characters that will be prepended to the short code, where S - is the supplied short code and R are the computed characters, are: - SSSS -> RRRR.RRSSSS - SSSSS -> RRRR.RRSSSSS - SSSSSS -> RRRR.SSSSSS - SSSSSSS -> RRRR.SSSSSSS - Note that short codes with an odd number of characters will have their - last character decoded using the grid refinement algorithm. - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1ba85392..695ad5fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,81 @@ -Want to contribute? Great! First, read this page (including the small print at the end). - -### Before you contribute -Before we can use your code, you must sign the -[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1) -(CLA), which you can do online. The CLA is necessary mainly because you own the -copyright to your changes, even after your contribution becomes part of our -codebase, so we need your permission to use and distribute your code. We also -need to be sure of various other things—for instance that you'll tell us if you -know that your code infringes on other people's patents. You don't have to sign -the CLA until after you've submitted your code for review and a member has -approved it, but you must do it before we can put your code into our codebase. -Before you start working on a larger contribution, you should get in touch with -us first through the issue tracker with your idea so that we can help out and -possibly guide you. Coordinating up front makes it much easier to avoid -frustration later on. - -### Code reviews -All submissions, including submissions by project members, require review. We -use Github pull requests for this purpose. - -### The small print -Contributions made by corporations are covered by a different agreement than -the one above, the Software Grant and Corporate Contributor License Agreement. +# Contributing to Open Location Code + +The Open Location Code project strongly encourages technical contributions. + +We hope you'll become an ongoing participant in our open source community, but we also welcome one-off contributions for the issues you're particularly passionate about. + +- [Contributing to Open Location Code](#contributing-to-open-location-code) + - [Filing issues](#filing-issues) + - [Bugs](#bugs) + - [Suggestions](#suggestions) + - [Contributing code](#contributing-code) + - [Contributing a new implementation](#contributing-a-new-implementation) + - [Contributor License Agreement](#contributor-license-agreement) + - [Ongoing participation](#ongoing-participation) + - [Discussion channels](#discussion-channels) + +## Filing issues + +### Bugs + +If you find a bug in an Open Location Code library, please [file an issue](https://github.com/google/open-location-code/issues/new). +Members of the community are regularly monitoring issues and will try to fix open bugs quickly. + +The best bug reports provide a detailed description of the issue, step-by-step instructions for predictably reproducing the issue, and possibly even a working example that demonstrates the issue. + +Please note that questions about how to use Open Location Code or other general questions should be asked on the [Open Location Code Google Group](https://groups.google.com/forum/#!forum/open-location-code) +instead of filing an issue here. + +### Suggestions + +The Open Location Code project is meant to evolve with feedback. +The project and its users appreciate your thoughts on ways to improve the design or features or creative ways to use the codes. + +To make a suggestion [file an issue](https://github.com/google/open-location-code/issues/new). + +If you are intending to implement, please see the [Contributing code](#contributing-code) section below for next steps. + +If you are adding the Open Location Code library to your project, please contact the [Open Location Code Google Group](https://groups.google.com/forum/#!forum/open-location-code) so we can suggest how you can make the most of the codes. + +## Contributing code + +The Open Location Code project accepts and greatly appreciates code contributions! + +A few things to note: + +* The Open Location Code project follows the [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) model for accepting contributions. +* We follow [Google's JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html). + More generally make sure to follow the same comment and coding style as the rest of the project. +* Do not try to address multiple issues in a single pull request. + In some cases, you might even resolve a single issue with multiple PRs (e.g. if you are changing multiple implementations). +* Include [tests](TESTING.md) when contributing code. + There are tests that you can use as examples. + +## Contributing a new implementation + +If you have an implementation in your own repository, that's great! +Unfortunately we can't accept implementations in languages we're not familiar with as we won't be able to maintain or test them. +You can add a link to it in our [list of external implementations](Documentation/External_Implementations.md). + +* Look at the existing implementations, to get an idea of the usage and how much work is involved. +* If you copy the code structure and algorithms from an existing implementation, you'll have a much shorter review cycle. +* [Create a new GitHub issue](https://github.com/google/open-location-code/issues/new) to start discussion of the new feature. +* Follow the guidelines for [Contributing code](#contributing-code) described above. +* Don't forget to add tests! + +## Contributor License Agreement + +The Open Location Code project hosted at GitHub requires all contributors to sign a Contributor License Agreement ([individual](https://developers.google.com/open-source/cla/individual) or [corporation](https://developers.google.com/open-source/cla/corporate)) in order to protect contributors, users and Google in issues of intellectual property. + +When you create a Pull Request, a check will be run to ensure that you have signed the CLA. +Make sure that you sign the CLA with the same email address you associate with your commits (likely via the `user.email` Git config as described on GitHub's [Set up Git](https://help.github.com/articles/set-up-git/) page). + +## Ongoing participation + +We actively encourage ongoing participation by community members. + +### Discussion channels + +Technical issues, designs, etc. are discussed on [GitHub issues](https://github.com/google/open-location-code/issues) and [pull requests](https://github.com/google/open-location-code/pulls), +or the [Open Location Code Google Group](https://groups.google.com/forum/#!forum/open-location-code). + diff --git a/Documentation/External_Implementations.md b/Documentation/External_Implementations.md new file mode 100644 index 00000000..6a5968bf --- /dev/null +++ b/Documentation/External_Implementations.md @@ -0,0 +1,18 @@ +# External Implementations + +This page lists implementations of the Open Location Code library by third parties. +These include implementations in languages other than the those in this repository. + +These implementations have not been tested or reviewed, and are included here as a convenience. + +| Language | Link | Notes | +| ------------------ | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| Ada | https://github.com/malaise/ada/ | | +| C# (.NET standard) | https://github.com/JonMcPherson/open-location-code | | +| Common Lisp | https://github.com/ralph-schleicher/open-location-code | | +| EMACS | https://gitlab.liu.se/davby02/olc | | +| R | https://github.com/Ironholds/olctools | | +| Solidity | https://etherscan.io/address/0xf85c6320cc60dec45af1f7ce82b13dd24d539690#code#F3#L28 | URL is a link to a contract, not a repo | +| Swift 3.x, 4.x | https://github.com/curbmap/OpenLocationCode-swift | Not fully implemented | +| Swift 5.0 | https://github.com/google/open-location-code-swift | This library supports Objective-C for iOS, macOS, tvOS, and watchOS. | +| Typescript | https://github.com/tspoke/typescript-open-location-code | | diff --git a/Documentation/FAQ.md b/Documentation/FAQ.md new file mode 100644 index 00000000..d24f5efb --- /dev/null +++ b/Documentation/FAQ.md @@ -0,0 +1,99 @@ +# Open Location Code Frequently Asked Questions + +## Table Of Contents +- [Open Location Code Frequently Asked Questions](#open-location-code-frequently-asked-questions) + - [Table Of Contents](#table-of-contents) + - [Background](#background) + - ["Plus Codes" or "Open Location Code"?](#plus-codes-or-open-location-code) + - [What are they for?](#what-are-they-for) + - [Why not use street addresses?](#why-not-use-street-addresses) + - [Why not use latitude and longitude?](#why-not-use-latitude-and-longitude) + - [Why is Open Location Code based on latin characters?](#why-is-open-location-code-based-on-latin-characters) + - [Plus Code digital addresses](#plus-code-digital-addresses) + - [Reference location dataset](#reference-location-dataset) + - [Plus Code addresses in Google Maps](#plus-code-addresses-in-google-maps) + - [Plus Code addresses of high-rise buildings](#plus-code-addresses-of-high-rise-buildings) + - [Plus Code precision](#plus-code-precision) + + + +## Background + +### "Plus Codes" or "Open Location Code"? + +The software library (and this GitHub project) is called "Open Location Code", because it's a location code that is open source. +The codes it generates are called "Plus Codes" because they have a plus sign in them. + +### What are they for? + +Plus Codes provide a short reference to any location. +We created them to provide a way to refer to any location, regardless of whether there are named roads, unnamed roads, or no roads at all. + +### Why not use street addresses? + +A lot of rural areas can be far away from roads, and people still want to be able to refer to specific locations. +Also, at lot of the roads in the world don't have names, and so locations along those roads don't have addresses. +There is an estimate by the World Bank that the majority of urban roads don't have names. + +Street-based addressing projects are expensive and slow, and haven't made much of a dent in this problem. +Plus Codes can be assigned rapidly and because they can be used immediately can solve the addressing problem quickly and cheaply. + +### Why not use latitude and longitude? + +One answer is that if latitude and longitude were a practical solution, people would already be using them. +The problem with latitude and longitude is that they are two numbers, possibly signed, with a lot of digits, and the order is important. + +But latitude and longitude, and many other systems such as MGRS, geocodes, etc, also have the problem that they do not look like addresses. +We all know what an address looks like - a collection of references from less detailed to more detailed, typically: country, state, city, street, and house. +This hierarchy is important since it makes it easy to determine if something is near or far without having to understand the whole thing. +You can tell if it's in a different city without having to know the street name. + +### Why is Open Location Code based on latin characters? + +We are aware that many of the countries where Plus Codes will be most useful use non-Latin character sets, such as Arabic, Chinese, Cyrillic, Thai, Vietnamese, etc. +We selected Latin characters as the most common second-choice character set in these locations. +We considered defining alternative Open Location Code alphabets in each character set, but this would result in codes that would be unusable to visitors to that region, or internationally. + +## Plus Code digital addresses + +Plus Code digital addresses use known address information, like country, state, city, and then use the Plus Code to provide the final information. +Typically converting a Plus Code to a Plus Code address removes the first four digits from the code to shorten it to just six digits. + +Any city or place name within approximately 30-50 km can be used to recover the original location. + +### Reference location dataset + +The open source libraries support conversion to/from addresses using the latlng of the reference location. +Callers will need to convert place names to/from latlng using a geocoding system. + +Providing a global dataset isn't within scope of this project. +For a potential free alternative, see [Open Street Map](https://wiki.openstreetmap.org/) and derived geocoding service [Nominatim](https://nominatim.org/). + +### Plus Code addresses in Google Maps + +Google Maps displays Plus Code addresses on all entries. +It does this by using the location of the business for the Plus Code, and then using the place name to shorten the Plus Code to a more convenient form. + +If the listing is managed by the business owner, it will try to use a place name from the address, otherwise it will use Google's best guess for the place name. (Google tries to pick names for cities rather than suburbs or neighbourhoods.) + +If you think a different place name would be better, you can use that, and as long as Google knows about that place name the Plus Code address should work. + +### Plus Code addresses of high-rise buildings + +Plus Codes don't include the floor or apartment in high-rise buildings. +If you live in a multi-storey building located at "9G8F+6W, Zürich, Switzerland", think of the Plus Code as like the street name and number, and put your floor or apartment number in front: "Fourth floor, 9G8F+6W, Zürich, Switzerland" + +The reason for this is that Plus Codes need to be created without knowing specifically what is there. +The other reason is that addresses in high-rise buildings are assigned differently in different parts of the world, and we don't need to change that. + +### Plus Code precision + +The precision of a Plus Code is indicated by the number of digits after the "+" sign. + +* Two digits after the plus sign is an area roughly 13.7 by 13.7 meters; +* Three digits after the plus sign is an area roughly 2.7 by 3.5 meters; +* Four digits after the plus sign is an area roughly 0.5 by 0.8 meters. + +Apps can choose the level of precision they display, but should bear in mind the likely precision of GPS devices like smartphones, and the increased difficulty of remembering longer codes. + +One reason to use three or four digits after the plus sign might be when addressing areas that contain small dwellings, to avoid having multiple dwellings with the same Plus Code address. diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100644 index 00000000..21227223 --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1,29 @@ +# Documentation + +The wiki is where you can find out information about using the software, the codes, or the API. + +### Common pages + +* [External Implementations](External_Implementations.md) - need an Ada or Typescript library? +* [Frequently Asked Questions (FAQ)](FAQ.md) +* [Who is using Plus Codes?](Reference/Plus_Code_Users.md) + +### Specification and technical implementation + +* [Open Location Code Overview](Specification/olc_definition.adoc) +* [Open Location Code Specification](Specification/specification.md) +* [Guidance for Shortening Codes](Specification/Short_Code_Guidance.md) +* [Open Location Code Reference API](Specification/API.md) +* [Plus Codes and Open Location Code Naming Guidelines](Specification/Naming_Guidelines.md) + +### Technical + +* [An Evaluation of Location Encoding Systems](Reference/comparison.adoc) +* [Supporting Plus Codes in GIS software](Reference/GIS_Software.md) +* [Supporting Plus Codes in an app](Reference/App_Developers.md) + +### Tools + +* [Field Data Collection Practices](Reference/Field_Collection_Data_Practices.md) +* [https://plus.codes API Reference](Reference/plus.codes_Website_API.md) +* [Using Plus Codes in Spreadsheets](Reference/Using_Spreadsheets.md) diff --git a/Documentation/Reference/App_Developers.md b/Documentation/Reference/App_Developers.md new file mode 100644 index 00000000..4bf24c73 --- /dev/null +++ b/Documentation/Reference/App_Developers.md @@ -0,0 +1,94 @@ +# Supporting Plus Codes technology in apps and sites + +This page gives guidelines for how to support Plus Codes in a website or mapping application. +These guidelines should make it clear that adding support for OLC is not onerous, but actually quite easy. + +> Note that with the availability of the [https://plus.codes website API](plus.codes_Website_API.md), these instructions really only apply to apps that require offline support. +If your app or site can rely on a network connection, integrating with the API will give a better solution. + +# Supporting Plus Codes for search + +To support Plus Codes for searching, there are three different cases: + +* global codes, such as "796RWF8Q+WF" +* local codes, such as "WF8Q+WF" +* local codes with a locality, such as "WF8Q+WF Praia, Cabo Verde" + +The assumption is that this is being done by a mapping application, that allows people to enter queries and then highlights that location on a map or uses it for directions. + +## Supporting global codes + +Global codes can be recognised and extracted from a query using a regular expression: + +``` +/(^|\s)([23456789C][23456789CFGHJMPQRV][23456789CFGHJMPQRVWX]{6}\+[23456789CFGHJMPQRVWX]{2,7})(\s|$)/?i +``` + +This will extract (in capturing group **2**) a global code at the start or end of a string, or enclosed with spaces. +It will not match a global code embedded in a string such as "777796RWF8Q+WFFFFFFF". + +If a location query includes a global code, the rest of the query can be ignored, since the global code gives the latitude and longitude. + +To support a global code, once you have the user query, match it against the above regex, and if you have a match use the `decode()` method to get the coordinates, and use the center latitude and longitude. + +## Supporting local codes + +A variant of the global code regex can be used to check whether a location query includes a local code: + +``` +/(^|\s)([23456789CFGHJMPQRVWX]{4,6}\+[23456789CFGHJMPQRVWX]{2,3})(\s|$)/?i +``` + +If the query matches, *and the user has not entered any other text*, then another location must be used to recover the original code. +If you are displaying a map to the user, then use the current map center, pass it to the `recoverNearest()` method to get a global code, and then decode it as above. + +If there is no map, you can use the device location. +If you have no map and cannot determine the device location, a local code is not sufficient and you should display a message back to the user asking them to provide a town or city name or the full global code. + +## Supporting local codes with localities + +If the user input includes a local code with some other text, then extract the local code and send the remaining text to your geocoding service (Nominatim, Google, etc). +Use the location returned by your geocoding service as the reference location in the `recoverNearest()` method to get a global code, decode that and you have the location. + +## Displaying the result + +If the user specified a Plus Code in their query, the result should match. +That is, it is easier to understand if they enter a Plus Code to get a Plus Code displayed as the result. +Searching for a Plus Code and displaying the result back to the user as "14°55'02.3"N 23°30'40.7"W" is confusing, unhelpful and should be avoided. + +# Computing Plus Codes for places + +Superficially computing Plus Codes for places is trivial. +All that is needed is to call the `encode()` method on the coordinates, and then to display the code. + +The problem is that this only displays the global code, not the more convenient and easy to remember local code. +But to display the local code, you need to do two things: + +* Compute the locality name +* Ensure that the locality is located near enough + +## Computing a locality name + +To display a local code (e.g., WF8Q+WF), you need a reference location that is within half a degree latitude and half a degree longitude. + +Make a call to a reverse geocoding backend, preferably one that returns structured information, and extract the town or city name. + +Some geocoding backends are more suitable than others, so you might need to perform some tests. + +## Ensuring the locality is near enough + +After reverse geocoding the location and extracting the locality name, you should make a call to a geocoding service to get the location of the locality. +This is likely to be its center, not the position of the Plus Code, and could be some distance away. + +You want it to be as close as possible, because other geocoding services are likely to position it slightly differently. +If it is very close to half a degree away, another geocoding service could result in the Plus Code being decoded to a different location. + +Typically you should aim for a locality within a quarter of a degree - this is approximately 25km away (at the equator) so still quite a large range. + +If the locality is near enough, you should display the local code and locality together. +The `shorten()` method in the OLC library may remove 2, 4, 6 or even 8 characters, depending on how close the reference location is. +Although all of these are valid, we recommend only removing the first 4 characters, so that Plus Codes have a consistent appearance. + +# Summary + +Supporting Plus Codes in search use cases should not be a complex exercise. \ No newline at end of file diff --git a/Documentation/Reference/Field_Collection_Data_Practices.md b/Documentation/Reference/Field_Collection_Data_Practices.md new file mode 100644 index 00000000..5a3902c1 --- /dev/null +++ b/Documentation/Reference/Field_Collection_Data_Practices.md @@ -0,0 +1,106 @@ +# Field Collection of Plus Code Locations +[](https://play.google.com/store/apps/details?id=org.odk.collect.android) + +## Summary + +Collecting locations of equipment, buildings, homes etc from the field, and obtaining the Plus Codes, is a common problem. + +[Open Data Kit](https://opendatakit.org) is a suite of free and open source software to support collecting, managing and using data. [Open Data Kit Collect](https://play.google.com/store/apps/details?id=org.odk.collect.android) (ODK Collect) is a free, open source app available in the Google Play Store for customizable data collection in an offline environment. + +This document explains how to get started with ODK to collect location data and convert it to Plus Codes. + +**Note:** This process will collect latitude and longitude and convert them to global Plus Codes, e.g. 8FVC9G8F+6W. +Converting these to Plus Code addresses (9G8F+6W Zurich, Switzerland) is out of scope of this data collection. (One way it could be done is using the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/intro).) + +## Overview + +First we will define a [form](https://docs.opendatakit.org/form-design-intro/) that specifies what data we want, and then use [ODK Collect](https://docs.opendatakit.org/collect-intro/), an Android app, to collect filled in forms. + +ODK Collect saves location information as latitude and longitude, so the final step will be to convert those to Plus Codes using the [Plus Code add-on for Google Sheets](https://gsuite.google.com/marketplace). + +## Requirements + +* ODK Collect runs on Android devices +* The field workers will need Google accounts (we're going to use Google Drive and Sheets). + +## Alternatives + +Other options for collecting this data might be to use Google Maps - manually long pressing on the map displays an address card, and expanding that shows the Plus Code. + +Alternatively, you could write an HTML5 web app or develop another mobile app. +These could do the conversion from GPS coordinates to Plus Codes directly. +However, we think that using Open Data Kit provides the fastest route to general functionality. + +## Using Open Data Kit + +Here is a [description of using ODK with Google Drive and Sheets](https://www.google.com/earth/outreach/learn/odk-collect-and-google-drive-integration-to-store-and-manage-your-data). + +This procedure can be followed exactly, or a slightly easier method to define the form is described below. + +## Online Form Editor + +That document uses a Google Sheet to define the form. +This can be complicated to test and debug. +A simpler way is to use the [online form editor](https://build.opendatakit.org/). + +This provides a drag and drop method to sequence the form questions and set the field names, list of options etc. + +You can build a basic flow with location collection, and include additional metadata such as the time of collection, the phone number etc. + +You will need to create a blank Google Sheet. +Name one of the tabs "Form Submissions" or similar, copy the URL of that tab and set it as the `submission URL` in the form (using Edit -> Form Properties). + +The, save the form and export it (with File -> Export to XML), and then transfer that XML file to your Google Drive account. (Putting it in a folder together with the spreadsheet will make sharing those files to your field workers easy.) + +### Location Notes + +You can select whether to use Google Maps or OpenStreetMap in the general settings. +You can also select whether to display the street map, or aerial imagery. + +ODK Collect will only use GPS locations when it can see a minimum number of satellites. +If your field workers will be using it indoors, then the GPS location may not be available. +Instead, you can set the field to not use GPS but allow a [user entered location](https://docs.opendatakit.org/form-question-types/#geopoint-with-user-selected-location) - but that will not collect accuracy or altitude, and may also be inaccurate. + +A better solution is to use the manual location as a fallback to GPS. +You can have one question that uses the GPS location (with or without a map), and a second question that gets the location manually, and only show that question if the GPS is not available, or the location accuracy was poor. + +If using the online editor, enter the following in the **Relevance** field for the manual location field: +``` +not(boolean(/data/gps_location)) or number(/data/gps_location)>15 +``` + +(This assumes the data name of the GPS location field is `gps_location`.) + +If building your form in a spreadsheet, put the following in the **survey** tab: + +| type | name | label | relevant | appearance | +|------|------|-------|----------|------------| +| geopoint | gps_location | GPS location | | maps +| geopoint | manual_location | Manual location | `not(boolean(${gps_location})) or number(${gps_location})>15` | placement-map + +# Configuring ODK Collect + +Install and configure ODK Collect as described in the [document](https://www.google.com/earth/outreach/learn/odk-collect-and-google-drive-integration-to-store-and-manage-your-data). + +The document also describes how to use it and upload completed forms to the Google Sheet. + +# Converting Locations To Plus Codes + +ODK uploads locations to the sheet using three fields: +* location (decimal degrees) +* altitude (set to zero for manual locations) +* accuracy (set to zero for manual locations) + +To convert these to Plus Codes, install the Google Sheets Plus Code add-on from the [G Suite Marketplace](https://gsuite.google.com/marketplace). +You can convert a column of locations into their corresponding Plus Codes using the formula: +``` +=PLUSCODE(B:B) +``` +This will use the default precision code length, 10 digits. +If you need a different precision, specify the code length in the formula: +``` +=PLUSCODE(B:B, 11) +``` +Installing and using the Google Sheets Plus Codes add-on is covered in a series of videos: + +[![Google Sheets Plus Codes add-on video playlist](https://i.ytimg.com/vi/min-u1w4SOQ/hqdefault.jpg)](https://www.youtube.com/watch?v=n9kJC5qVeS0&list=PLaBfOq9xgeeBgOLyKnw8kvpFpZ_9v_sHa) diff --git a/Documentation/Reference/GIS_Software.md b/Documentation/Reference/GIS_Software.md new file mode 100644 index 00000000..a610babf --- /dev/null +++ b/Documentation/Reference/GIS_Software.md @@ -0,0 +1,56 @@ +# Plus Codes in GIS software + +This page provides information about using Plus Codes in GIS software. + +## Tile Service + +If you want to visualise the Plus Codes grid, you can use the [grid service](https://grid.plus.codes) to fetch the grid tiles. + +This is a shared service, and it may rate limit you. +If you need to use the grid heavily, you can start your own [tile_server](https://github.com/google/open-location-code/blob/main/tile_server). + +The tile service provides GeoJSON objects, one per Plus Codes square, or PNG images that can be added as an overlay. + +## Software + +### QGIS + +| precision level | intervals in degrees | +|-----|-----| +| 10 | 0.000125 | +| 8 | 0.0025 | +| 6 | 0.05 | +| 4 | 1 | +| 2 | 20 | + +We can generate the grid lines in QGIS. + +Just make sure your starting lat-long values are an exact multiple of the interval values for your chosen precision level you want. + +Example : Creating a grid with precision 6 : starting latitude cannot be 16.4563. +Change it to 16.45 or 16.50 so that when you divide it by 0.05 it gives you an integer answer. + +In QGIS, you can generate a grid by clicking in the top Menu: Vector > Research tools > Vector Grid + +* Grid extent (xmin,xmax,ymin,ymax): 78.1,79,16.9,18.2 (in my example, a city in India) +* Set both lat and lon interval as per the precision level you want. + So for precision level 6, enter 0.05 for both. +* You can set output as lines or polygons, your choice. + Lines make simpler and smaller shapefiles. +* And that should generate the grid for you. + You can save that layer as a shapefile in any format. + +Note that this will not put any information about the Plus Codes in your grid's metadata. +They're just lines/boxes. + +But if you make polygons, then I can think of a roundabout way of adding Plus Code values to those polygons (I have not done this myself yet): + +* Generate a centroid layer (Vector > Geometry Tools > Polygon Centroid) from the grid-polygons layer. + This will place points inside each grid box. (in a new points layer.) +* Install "Lat Lon Tools" plugin. +* That plugin can generate Plus Codes from points. + So run it on the centroid layer you made. +* (And this I can't quite figure out yet) Figure out a way to move the meta field from the centroid layer to the grid polygons layer. + +There is a plugin for QGIS, [Lat Lon tools](https://github.com/NationalSecurityAgency/qgis-latlontools-plugin). + diff --git a/Documentation/Reference/Plus_Code_Users.md b/Documentation/Reference/Plus_Code_Users.md new file mode 100644 index 00000000..99f0b89a --- /dev/null +++ b/Documentation/Reference/Plus_Code_Users.md @@ -0,0 +1,21 @@ +# Where Are Plus Codes Being Used? + +This page lists the sites, apps and organisations that support Plus Codes / Open Location Code. + +# Organisations + +* [Correios de Cabo Verde](correios.cv) (August 2016). + Support Plus Codes for postal delivery. + +# Apps and sites + +* Google (Search, Maps) + * Search support for global and local codes (early 2016). + * Display of global codes in [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.maps) and [iOS](https://itunes.apple.com/app/id585027354) maps (September 2016). + * [Assistant integration](https://assistant.google.com/services/a/uid/000000706b4e2cf1?hl=en) (early 2018) +* [mapy.cz](mapy.cz) (mid 2016) Search support for global codes. +* www.waze.com (early 2018?) Search support for Plus Code addresses (global and local) +* www.locusmap.eu (early 2018) Supports using global codes to set the map location +* [OSMAnd](https://osmand.net/) OpenStreetMap based offline mobile maps and navigation - Supports displaying OLC as the "coordinates" of any point on earth. +* [QGIS](https://qgis.org/) via [Lat Lon Tools](https://plugins.qgis.org/plugins/latlontools/) plug in - good support, points to olc, olc to points +* [nicesi.com](https://nicesi.com/) (early 2019) Nicesi Location API support for global codes in [reverse geocoding service](https://nicesi.com/doc/#reverse-geocoding) \ No newline at end of file diff --git a/Documentation/Reference/Using_Spreadsheets.md b/Documentation/Reference/Using_Spreadsheets.md new file mode 100644 index 00000000..2509eb31 --- /dev/null +++ b/Documentation/Reference/Using_Spreadsheets.md @@ -0,0 +1,19 @@ +# Using Plus Codes in Spreadsheets + +Being able to work with Plus Codes in spreadsheets is, for most people, probably the easiest method to work with them in bulk. + +This page explains how you can access the Open Location Code functions from Excel, LibreOffice or Google Spreadsheets. + +## Google Sheets + +There is an [add-on for Google Sheets](https://gsuite.google.com/marketplace/app/plus_codes/604254879289) that allows you to create Plus Codes from latitude and longitude coordinates, and to decode them as well. + +The [Google Maps YouTube channel](https://www.youtube.com/@googlemaps) has some [videos showing how to install and use the add-on](https://www.youtube.com/playlist?list=PLcRbp4LqBpwE5ofG2MN08D_4DJAk9gZBI). + +## Excel/LibreOffice + +VBA script and instructions are checked into github [here](https://github.com/google/open-location-code/blob/main/visualbasic). + +LibreOffice does sometimes have problems. +This may be due to slightly unreliable VBA support in some versions. + diff --git a/docs/comparison.adoc b/Documentation/Reference/comparison.adoc similarity index 85% rename from docs/comparison.adoc rename to Documentation/Reference/comparison.adoc index 0f821955..99235684 100644 --- a/docs/comparison.adoc +++ b/Documentation/Reference/comparison.adoc @@ -4,8 +4,6 @@ An Evaluation of Location Encoding Systems :toc-placement: preamble :icons: -Doug Rinckes, Google - == Abstract Many parts of the world and more than half the world's urban population @@ -18,17 +16,13 @@ explains the attributes that we feel are important to such an encoding system, and evaluates a number of existing encoding systems against the requirements. -NOTE: This document is based on a comparison of the systems at early 2014, -to provide background to the design of Open Location Codes. Subsequent changes -to the included encoding systems will not necessarily be included. - == Desired Attributes We spent some time debating what attributes a location code should have. The list we came up with was, in no particular order: * Codes should be short enough that they can be memorised - * A code should be sufficient on it's own, not requiring additional + * A code should be sufficient on its own, not requiring additional information such as country name * To prevent confusion, a place should have only one code * Codes should not include easily confused characters (e.g., 0 and O, 8 and @@ -40,7 +34,7 @@ list we came up with was, in no particular order: * Codes should represent an area, not a point, where the size of the area is variable * Shortening a code should represent a larger area that contains the - original location. A side affect of this is that nearby codes will have a + original location. A side effect of this is that nearby codes will have a common prefix; * The code for a place should be deterministically generated, not requiring any setup or application @@ -96,11 +90,6 @@ south poles. Longitude 180 and the poles are acceptable due to the low populations but the equator and longitude 0 have multiple population centers along them . -A single location can have more than one code, depending on the input -values, and multiple different codes can decode to the same value. For -example, "c216ne4" and "c216new" (and others) all decode to the -same coordinates (45.37 -121.7). - == Geohash-36 Geohash-36 codes are designed for URLs and electronic storage and @@ -115,15 +104,15 @@ character. This causes codes over a boundary to be dissimilar even though they may be neighbours: .9x, g2, g7 and G2 code locations compared -image::images/geohash36_grid.png[width=412,height=407,align="center"] +image::../images/geohash36_grid.png[width=412,height=407,align="center"] With just two levels, we can see that the cell "g2" (red, upper left of the cell marked g) is next to the cell 9X, but further from g7 (which is next to G2). Using real Geohash-36 codes, "bdg345476Q" is next to "bdbtTVTXWB" but several kilometers from "bdg3Hhg4Xd". -Geohash-36 codes may be one character shorter than full Open Location Codes -for similar accuracies. +Geohash-36 codes may be one character shorter than full Plus Code for similar +accuracies. The Geohash-36 definition includes an optional altitude specification, and an optional checksum, neither of which are provided by Open Location Code. @@ -131,9 +120,9 @@ an optional checksum, neither of which are provided by Open Location Code. == MapCode MapCodes can be defined globally or within a containing territory -<>. The global codes are a similar length to Open Location Codes, -but codes defined within a territory are shorter than full Open Location -Codes, and a similar length to short Open Location Codes. +<>. The global codes are a similar length to Plus Codes, but codes +defined within a territory are shorter than full Plus Codes, and a similar +length to short Plus Codes. To decode the identifiers, a data file needs to be maintained and distributed. The identifiers are mostly ISO-3166 codes for the territory @@ -160,11 +149,11 @@ character set. == Open Post Code Open Post Codes <> can be defined globally or within a containing country -<>. The global codes are a similar length to Open Location -Codes, but codes defined within a country are shorter than full Open -Location Codes, and a similar length to short Open Location Codes. +<>. The global codes are a similar length to Plus Codes, but +codes defined within a country are shorter than full Plus Code, and a similar +length to short Plus Codes. -Four countries are defined: Ireland, Hong Kong, Yemen and India. +Four countries are defined: Ireland, Hong Kong, Yemen and India. Every location on the planet has a global code. Locations within the countries where Open Post Code has been defined also have a local code. @@ -182,11 +171,11 @@ Post Codes can be truncated a single character at a time. Open Post Codes use a 5x5 grid, meaning that two different codes may be closer together than two highly similar codes: -.8x, H2, H7 and J2 code locations compared -image::images/geohash36_grid.png[width=404,height=399,align="center"] +.8x, H2, H6 and J2 code locations compared +image::../images/openpostcode_grid.png[width=404,height=399,align="center"] With just two levels , we can see that the cell "H2" (red, upper left of the -cell marked "H") is next to the cell "8X", but comparatively far from "H7" +cell marked "H") is next to the cell "8X", but comparatively far from "H6" (which is next to "J2"). Using Open Post Codes for Ireland, "KFLLLRFT" is the house next to @@ -219,9 +208,7 @@ Natural Area Codes have a discontinuity at longitude 180 and at the poles. == Maidenhead Locator System (MLS) Maidenhead Locator System codes explicitly represent areas, and can be -truncated in a similar way to Open Location Codes. The accuracy and length -of the codes is similar, but Maidenhead Locator System codes include vowels -and so the generated codes include words <>. +truncated in a similar way to Plus Codes. The accuracy and length of the codes is similar. MLS cannot generate words, but it can generate sequences that may appear to be words (e.g. when reading "1" as "l") <>. Maidenhead Locator System codes are based on an interleaving of latitude and longitude, and so are truncatable, and nearby locations have similar codes. @@ -262,23 +249,23 @@ information such as contact details, photos etc in addition to the location. We felt that the attributes of the above systems didn't sufficiently meet our requirements. As a result, we defined a new coding system and termed it -Open Location Code. +Open Location Code; codes created using this system are referred to as 'Plus Codes' (see the link:../Specification/Naming_Guidelines.md[Naming Guidelines]). -Open Location Codes are 10 to 11 characters long. They can also be used in a +Plus Codes are 10 to 11 characters long. They can also be used in a short form of four to seven characters, similar to telephone numbers and postcodes, within approximately 50km of the original location. Within approximately 2.5km of the original location they can be shortened further, to just four to five characters. To aid recognition and memorisation, we include a separator to break the code -into two parts, and to distinguise codes from postal codes. +into two parts, and to distinguish codes from postal codes. -In their short form, Open Location Codes have from four to seven characters. +In their short form, Plus Codes have from four to seven characters. These can be used on their own within 50km of the place, or globally by -providing a city or locality within that distance. Full Open Location Codes +providing a city or locality within that distance. Full Plus Code require no other information to locate them. -There is only one Open Location Code for a given location and area size. +There is only one Plus Code for a given location and area size. Different codes can be generated with different areas, but they will share the leading characters. @@ -292,15 +279,15 @@ billion possibilities, using a word list of 10,000 words from 30 languages. All possible sets were scored on whether they could spell the test words, and the most promising sets evaluated by hand. -The character set used to form Open Location Codes is not contiguous. This +The character set used to form Plus Codes is not contiguous. This is a result of removing easily confused characters, vowels and some other characters. This does make manually comparing codes difficult, as one has to remember whether there are characters between 9 and C in order to tell if 8FV9 is next to 8FVC. However, we think that this is justified by the improved usability. -Nearby places have similar Open Location Codes. There are three -discontinuities, at longitude 180 and the north and south poles, where +Nearby places have similar Plus Codes. There are three +discontinuities, at longitude 180 and at the north and south poles, where nearby locations can have very different codes, but due to the low populations in these areas we feel this is an acceptable limitation. @@ -310,19 +297,19 @@ latitudes are clipped to be greater than or equal to -90 and less than 90 degrees, making representing the exact location of the North Pole impossible although it can be very closely approximated. -Open Location Codes represent areas, and the size of the area depends on the +Plus Codes represent areas, and the size of the area depends on the code length. The longer the code, the smaller and more accurate the area. -Truncating an Open Location Code increases the area and contains the +Truncating a Plus Code increases the area and contains the original location. The codes are based on a simple encoding of latitude and longitude. The code for a place can be looked up by anyone and does not require any setup or configuration. -Open Location Codes can be encoded and decoded offline. +Plus Codes can be encoded and decoded offline. -Open Location Codes do not depend on any infrastructure, and so are not +Plus Codes do not depend on any infrastructure, and so are not dependent on any organisation or company for their continued existence or usage. @@ -358,4 +345,4 @@ receivers, the compact eTrex series, was introduced in 2000". In Wikipedia. - [[nac-site]] http://nacgeo.com Retrieved October 15 2014. -- [[[mls]]] In Wikipedia. Retrieved October 15 2014 from http://en.wikipedia.org/wiki/Maidenhead_Locator_System +- [[[mls]]] In Wikipedia. Retrieved October 15 2014 from http://en.wikipedia.org/wiki/Maidenhead_Locator_System \ No newline at end of file diff --git a/Documentation/Reference/plus.codes_Website_API.md b/Documentation/Reference/plus.codes_Website_API.md new file mode 100644 index 00000000..7cce443b --- /dev/null +++ b/Documentation/Reference/plus.codes_Website_API.md @@ -0,0 +1,290 @@ +# API Developer's Guide + +### Table of Contents +- [https://plus.codes API Developer's Guide](#httpspluscodes-api-developers-guide) + - [Table of Contents](#table-of-contents) + - [History](#history) + - [Functionality](#functionality) + - [API Request Format](#api-request-format) + - [Example Requests (no Google API key)](#example-requests-no-google-api-key) + - [Example Requests (with Google API key)](#example-requests-with-google-api-key) + - [JSON Response Format](#json-response-format) + - [Locality Requests](#locality-requests) + - [API Keys](#api-keys) + - [Obtaining A Google API Key](#obtaining-a-google-api-key) + - [Securing Your API Key](#securing-your-api-key) + - [Securing Your API Key With An HTTP Referrer](#securing-your-api-key-with-an-http-referrer) + - [Allowing Multiple Referrers](#allowing-multiple-referrers) + +> This API is *experimental*. +As of December 2016, we are soliciting feedback on the functionality, in order to inform proposals to geocoding API providers such as Google. +You can discuss the API in the [public mailing list](https://groups.google.com/forum/#!forum/open-location-code) or create an issue in the [issue tracker](https://github.com/google/open-location-code/issues/new?labels=api&assignee=drinckes). + +> If the API needs to be turned off, or there are other important messages, they will be returned in the JSON result in the field `error_message`, described on this page, or sent to the [mailing list](https://groups.google.com/forum/#!forum/open-location-code). + +> Feb/March 2017: Google API keys are required for the generation of short codes and searching by address. +See the [API Keys](#api-keys) section and the `key` parameter. + +> October 2018: v2 of the API is launched that returns the same localities that are displayed in Google Maps. +A side effect of this update is that the API should be slightly faster, and if API keys are used, the cost should be reduced. + +## History + +* December 2016. v1 of API launched +* October 2018. v2 of API launched and made default. + +## Functionality + +The API provides the following functions: + +* Conversion of a latitude and longitude to a Plus Code (including the bounding box and the center); +* Conversion of a Plus Code to the bounding box and center. + +Additionally, it can use the [Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/intro) to: + +* Include short codes and localities in the returned Plus Code (such as "WF8Q+WF Praia, Cape Verde" for "796RWF8Q+WF"); +* Handle converting from a street address or business name to the Plus Code; +* Handle converting a short code and locality to the global code and coordinates. + +The results are provided in JSON format. +The API is loosely modeled on the [Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/intro). + +## API Request Format + +A Plus Codes API request takes the following form: + +`https://plus.codes/api?parameters` + +**Note**: URLs must be [properly encoded](https://developers.google.com/maps/web-services/overview#BuildingURLs) (specifically, `+` characters must be encoded to `%2B`). + +**Required parameter:** + +* `address` — The address to encode. + This can be any of the following (if the `ekey` parameter is also provided): + * A latitude/longitude + * A street address + * A global code + * A local code and locality + * ~~A local code and latitude/longitude~~ (Deprecated in v2 of the API) + +**Recommended parameters:** + +* `key` — An Google API key. + See [API Keys](#api-keys). + If this parameter is omitted, only latitude/longitude and global codes can be used in the `address` parameter, and locality information will not be returned. (This can also be specified using `ekey`.) +* `email` — Provide an email address that can be used to contact you. +* `language` — The language in which to return results. + This won't affect the global code, but it will affect the names of any features generated, as well as the address used for the local code. + * If `language` is not supplied, results will be provided in English. + +## Example Requests (no Google API key) + +The following two requests show how to convert a latitude and longitude to a code, or how to get the geometry of a global code: + +* https://plus.codes/api?address=14.917313,-23.511313&email=YOUR_EMAIL_HERE +* https://plus.codes/api?address=796RWF8Q%2BWF&email=YOUR_EMAIL_HERE + +Both of these requests will return the global code, it's geometry, and the center: + +```javascript +{ + "plus_code": { + "global_code": "796RWF8Q+WF", + "geometry": { + "bounds": { + "northeast": { + "lat": 14.917375000000007, + "lng": -23.511250000000018 + }, + "southwest": { + "lat": 14.91725000000001, + "lng": -23.511375000000015 + } + }, + "location": { + "lat": 14.917312500000008, + "lng": -23.511312500000017 + } + }, + }, + "status": "OK" +} +``` + +## Example Requests (with Google API key) + +Here are some example requests. +You must include your Google API key in the request for these to work fully: + +* https://plus.codes/api?address=14.917313,-23.511313&ekey=YOUR_ENCRYPTED_KEY&email=YOUR_EMAIL_HERE +* https://plus.codes/api?address=796RWF8Q%2BWF&ekey=YOUR_ENCRYPTED_KEY&email=YOUR_EMAIL_HERE +* https://plus.codes/api?address=WF8Q%2BWF%20Praia%20Cape%20Verde&ekey=YOUR_ENCRYPTED_KEY&email=YOUR_EMAIL_HERE + +These requests would all return: + +```javascript + + "plus_code": { + "global_code": "796RWF8Q+WF", + "geometry": { + "bounds": { + "northeast": { + "lat": 14.917375000000007, + "lng": -23.511250000000018 + }, + "southwest": { + "lat": 14.91725000000001, + "lng": -23.511375000000015 + } + }, + "location": { + "lat": 14.917312500000008, + "lng": -23.511312500000017 + } + }, + "local_code": "WF8Q+WF", + "locality": { + "local_address": "Praia, Cape Verde" + }, + }, + "status": "OK" +} +``` + +## JSON Response Format + +The JSON response contains two root elements: + +* `"status"` contains metadata on the request. + Other status values are documented [here](https://developers.google.com/maps/documentation/geocoding/intro#StatusCodes). +* `"plus_code"` contains the Plus Code information for the location specified in `address`. + +> There may be an additional `error_message` field within the response object. +> This may contain additional background information for the status code. + +The `plus_code` structure has the following fields: +* `global_code` gives the global code for the latitude/longitude +* `bounds` provides the bounding box of the code, with the north east and south west coordinates +* `location` provides the centre of the bounding box. + +If a locality feature near enough and large enough to be used to shorten the code was found, the following fields will also be returned: +* `local_code` gives the local code relative to the locality +* `locality` provides the name of the locality using `local_address`. + +If the `ekey` encrypted key is not provided the following fields will not be returned: +* `local_code` +* `locality` +* `local_address` + +### Locality Requests + +The `address` parameter may match a large feature, such as: + +``` +https://plus.codes/api?address=Paris&ekey=YOUR_ENCRYPTED_KEY&email=YOUR_EMAIL_HERE +``` + +In this case, the returned code may not have full precision. +This is because for large features, the returned code will be the **largest** code that fits entirely **within** the bounding box of the feature: + +```javascript +{ + "plus_code": { + "global_code": "8FW4V900+", + "geometry": { + "bounds": { + "northeast": { + "lat": 48.900000000000006, + "lng": 2.4000000000000057 + }, + "southwest": { + "lat": 48.849999999999994, + "lng": 2.3499999999999943 + } + }, + "location": { + "lat": 48.875, + "lng": 2.375 + } + } + }, + "status": "OK" +} +``` + +## API Keys + +### Obtaining A Google API Key +To search for addresses, return addresses, and return short codes and localities, the Plus Codes API uses the [Google Maps Geocoding API](https://developers.google.com/maps/documentation/geocoding/intro). +If you want to be able to: +* search by address, +* search by short codes with localities, +* or have short codes and localities included in responses + +you *must* provide a Google API key in your request. +To obtain a Google API key refer [here](https://developers.google.com/maps/documentation/geocoding/start#get-a-key). + +Important: +* The API key *must* have the Google Maps Geocoding API enabled +* The API key *must* not have any restrictions (referrer, IP address). (This is because it is used to call the web service API, which does not allow restrictions on the key.) + +Once you have your API key, you can specify it in requests using the `key` parameter, but you should read the next two sections on securing your key. + +### Securing Your API Key + +Google API keys have a free daily quota allowance. +If other people obtain your key, they can make requests to any API that is enabled for that key, consuming your free quota, and if you have billing enabled, can incur charges. + +The normal way of securing API keys is setting restrictions on the host that can use it to call Google APIs. +These methods won't work here, because the calls to the Google API are being done from the Plus Codes server. + +Instead, you can encrypt your Google API key, and use the encrypted value in the requests to the Plus Codes API. +The Plus Codes server will decrypt the key and use the decrypted value to make the calls to Google. + +If anyone obtains your encrypted API key, they cannot use it to make direct requests to any Google API. (They can still use it to make requests to the Plus Codes API, see the next section for a solution.) + +For example, to protect the Google API key `my_google_api_key`, encrypt it like this: + +``` +https://plus.codes/api?encryptkey=my_google_api_key +``` + +The Plus Codes API will respond with: + +```javascript +{ + "encryption_message": "Provide the key in the key= parameter in your requests", + "key": "8Kr54rKYBj8l8DcTxRj7NkvG%2Fe%2FlwvEU%2F4M41bPX3Zmm%2FZX7XoZlsg%3D%3D", + "status": "OK" +} +``` + +### Securing Your API Key With An HTTP Referrer + +For extra security, you can encrypt a hostname with the key. +When the Plus Codes server decrypts the key, it checks that the HTTP referrer matches the encrypted hostname. +This prevents the encrypted key from being used from another host. + +For example, to protect the Google API key `my_google_api_key`, and require the HTTP referrer host to be `openlocationcode.com`, encrypt it like this: + +``` +https://plus.codes/api?referer=openlocationcode.com&encryptkey=my_google_api_key +``` + +The Plus Codes API will respond with: + +```javascript +{ + "encryption_message": "Provide the key in the key= parameter in your requests", + "key": "Nn%2BzIy2LOz7sptIe4tkONei3xfO7MUPSyYdoNanqv%2F1wgDaGvUryUDt8EPXRS4xzP%2F0b04b3J6%2BzFeeu", + "status": "OK" +} +``` + +#### Allowing Multiple Referrers + +If you need to use the same encrypted key from multiple different hosts, say `example1.com` and `example2.com`, include the hosts in the referer like this: + +``` +https://plus.codes/key?referer=example1.com|example2.com&key=my_google_api_key +``` diff --git a/Documentation/Specification/Naming_Guidelines.md b/Documentation/Specification/Naming_Guidelines.md new file mode 100644 index 00000000..7b0f5150 --- /dev/null +++ b/Documentation/Specification/Naming_Guidelines.md @@ -0,0 +1,37 @@ +# Plus Codes and Open Location Code Naming Guidelines + +## Why have guidelines? + +These guidelines are offered so that people see a consistent reference to the codes and technology. +This will make it easier for them to understand what they are being shown or asked for. + +We have chosen two names - one for the technology and one for the actual codes. +The name for the codes reflects and reinforces the importance of the plus symbol, which is how the codes can be recognised. + +## Plus Codes + +When referring to Plus Codes in English, the **only** term that should be used is "Plus Code", in Title Case. + +Some examples of usage are: +* "My Plus Code is CX37+M9." +* "I gave your Plus Code to the cab driver and he found the way without any problems." +* "Will the postcard arrive if I put your Plus Code as the address?" +* "Enter your Plus Code or street address here." + +### Global and local codes + +Codes that can be decoded to a lat/lng on their own, e.g. 796RWF8Q+WF are referred to as global codes. + +The shortened codes, e.g., WF8Q+WF are referred to as local codes, because they work within a local area. + +## Open Location Code + +When discussing with organisations or developers, refer to the library, algorithm and technology as "Open Location Code". +This can be abbreviated to OLC, is capitalised, and shouldn't be translated. + +It shouldn't be used to refer to the actual codes - don't say "Open Location Codes" for example. + +## Summary + +Having consistent names and presentation will make it easier for people to recognise what is meant, and make it easier for them to use and benefit from the project. + diff --git a/docs/olc_definition.adoc b/Documentation/Specification/olc_definition.adoc similarity index 66% rename from docs/olc_definition.adoc rename to Documentation/Specification/olc_definition.adoc index c889117e..b91ccf29 100644 --- a/docs/olc_definition.adoc +++ b/Documentation/Specification/olc_definition.adoc @@ -30,14 +30,14 @@ specific local knowledge. This is due to a variety of factors, including: -- Streets that are unnamed or have been renamed a few times -- Unofficial roadways or settlements (e.g., slums such as Kibera in Kenya +* Streets that are unnamed or have been renamed a few times +* Unofficial roadways or settlements (e.g., slums such as Kibera in Kenya and Dharavi in India) -- Newly constructed streets whose names are not widely known -- Areas that have the same or similarly named streets in close proximity -- Locally used names of streets that differ from the official names. These +* Newly constructed streets whose names are not widely known +* Areas that have the same or similarly named streets in close proximity +* Locally used names of streets that differ from the official names. These could be completely different names or abbreviations of the official names -- Unusual orderings of building numbers - non-consecutive or not aligned +* Unusual orderings of building numbers - non-consecutive or not aligned with the street. Often, the most precise and easily accessible street maps are online. @@ -79,9 +79,9 @@ Three approaches are usually used to provide a location in these circumstances. The most common solution is to provide simplified directions instead of an address. This results in addresses such as: -- 11th km of Old Road from Heraklion to Re <> -- in front of old civil engineering lab, Oke Ado Road, Ogbomosho <> -- Up the winding road, 600 m from the bridge, Past Lazy Dog, past English +* 11th km of Old Road from Heraklion to Re <> +* in front of old civil engineering lab, Oke Ado Road, Ogbomosho <> +* Up the winding road, 600 m from the bridge, Past Lazy Dog, past English Bakery on the left at Hotel Mountain Dew, Manali, Himachal Pradesh <> These directions rely on detailed local knowledge and are difficult or @@ -109,14 +109,14 @@ phone, in print and handwritten. We believe that these codes need to meet the following requirements for widespread adoption. -- Easy to use -- Complete -- Flexible precision -- Indicate proximity -- Cultural independence -- Functions offline -- Easy to implement -- Free +* Easy to use +* Complete +* Flexible precision +* Indicate proximity +* Cultural independence +* Functions offline +* Easy to implement +* Free *Easy to use*: The codes must be short enough to be remembered and used. This means that they need to be shorter than latitude and longitude and about the @@ -165,15 +165,15 @@ Open Location Code is a new way to express location that meets these requirements. It is shorter than latitude and longitude because it uses a higher number base. It uses a number base of 20 because: -- In base 20, 10 characters can represent a 14x14 meter area suitable for +* In base 20, 10 characters can represent a 14x14 meter area suitable for many buildings -- Using a number base of 20 makes some calculations easier -- We could identify a 20 character subset from 0-9A-Z that doesn't spell words. +* Using a number base of 20 makes some calculations easier +* We could identify a 20 character subset from 0-9A-Z that doesn't spell words. -The characters that are used in Open Location Codes were chosen by computing +The characters that are used by Open Location Code were chosen by computing all possible 20 character combinations from 0-9A-Z and scoring them on how well they spell 10,000 words from over 30 languages. This was to avoid, as -far as possible, Open Location Codes being generated that included +far as possible, Plus Codes being generated that included recognisable words. The selected 20 character set is made up of "23456789CFGHJMPQRVWX". @@ -181,10 +181,10 @@ Note on terminology: The characters from 0-9A-Z that make up the significant part of an Open Location Code are referred to as "digits". Additional symbols used for formatting are referred to as "characters". -Open Location Codes are encodings of WGS84 latitude and longitude +Open Location Code uses encodings of WGS84 latitude and longitude coordinates in degrees. Decoding a code returns an area, not a point. The area of a code depends on the length (longer codes are more precise with -smaller areas). A two-digit code has height and width<> of 20 +smaller areas). A two-digit code has height and width <> of 20 degrees, and with each pair of digits added to the code, both height and width are divided by 20. @@ -198,8 +198,8 @@ direction from one code to another to be determined visually, and for codes to be truncated, resulting in a larger area. [[fig_olc_area]] -.Comparing areas of four and six digit Open Location Codes -image::images/code_areas.png[width=400,height=350,align="center"] +.Comparing areas of four and six digit Plus Codes +image::../images/code_areas.png[width=400,height=350,align="center"] The large rectangle in <> is the Open Location Code 8FVC (1 degree height and width). The smaller rectangle is the code 8FVC22 (1/20 @@ -209,13 +209,13 @@ A 10 digit code represents a 1/8000° by 1/8000° area. (At the equator, this is approximately 13.9 meters x 13.9 meters.) .10 digit Open Location Code (1/8000 degree resolution), 10.5m x 13.9m -image::images/olc_10_character.png[width=406,height=272,align="center"] +image::../images/olc_10_character.png[width=406,height=272,align="center"] A 10 digit code will be precise enough for many locations. However, in areas where building density is high (such as informal settlements, semi-detached houses or apartment blocks), such an area could extend over several dwellings. A 12 digit code would be less than 1 square meter. An -11 digit code would be preferable because it it shorter, and a slightly +11 digit code would be preferable because it is shorter, and a slightly lower precision area could be acceptable. From 11 digits on, a different algorithm is used. The areas are slightly @@ -226,10 +226,10 @@ cell is identified by a single digit. The digit for the cell containing the desired location is added to the code. Using a single grid refinement step, we have an 11 digit code that -represents a 1/32000° by 1/40000° area (roughly 3.4 by 2.7 meters). +represents a 1/32000° by 1/40000° area (roughly 3.4 by 2.7 meters at the equator). -.A 10 digit code divided into its grid. Each small square is approximately 2.6m x 2.8m -image::images/olc_11_grid.png[width=406,height=272,align="center"] +.A 10 digit code divided into its grid. In this location, each small square is approximately 2.6m x 2.8m +image::../images/olc_11_grid.png[width=406,height=272,align="center"] The first approach (where a pair of digits is added for each step) provides codes that can be visually compared, or alphabetically ordered to @@ -241,7 +241,7 @@ not be reliably compared visually. 10 and 11 digit codes provide the necessary resolution to represent building locations. Other lengths are also valid. -== Shortening Open Location Codes +== Shortening Plus Codes We are accustomed to providing different levels of detail in a street address depending on who we give it to. People far away usually require the @@ -276,14 +276,14 @@ have to remember from four to seven digits of their code. A "+" symbol is inserted into the code after the eighth digit. This performs two key functions: -- It allows us to recognise shortened code fragments such as MQPX+9G. Because +* It allows us to recognise shortened code fragments such as MQPX+9G. Because we know that the "+" is after the eighth digit, we know that there are four digits to be recovered for this code. -- It allows us to distinguise four or six digit codes from postal codes. +* It allows us to distinguish four or six digit codes from postal codes. But this means that we have a problem if we want to represent the 1x1 degree area 6GCR. The solution here is to use zero, as a padding symbol, giving us -6GCR0000+. Zeros in Open Location Codes must not be followed by any other +6GCR0000+. Zeros in Plus Codes must not be followed by any other digits. == Imperfections @@ -291,26 +291,26 @@ digits. Open Location Code has some imperfections, driven by usability compromises or the encoding methodology. The key ones are listed here. -- To prevent the codes including words, some letters are not used. For +* To prevent the codes including words, some letters are not used. For example, A and B are not used in the codes. The codes W9 and WC are next to each other, but this isn't immediately obvious -- The character set is defined in Latin characters. We have considered +* The character set is defined in Latin characters. We have considered defining different character sets for different languages, but there can be problems identifying the language if visually similar characters are used. For example, it is difficult to distinguish the latin "H" from the cyrillic "Н". Although latin characters may not be the first choice in many areas, it is probably the most common second choice throughout the world -- Code areas distort at high latitudes due to longitude convergence. The +* Code areas distort at high latitudes due to longitude convergence. The practical impact of these disadvantages are not significant due to the low populations at the north or south poles, and the ability to use codes representing small areas to approximate point locations -- Code discontinuities at the poles and longitude 180. Codes on either side +* Code discontinuities at the poles and longitude 180. Codes on either side of the 180th meridian, although they are close, will differ significantly. Similarly, locations at the poles, although physically close, can also have significantly different encodings. The fact that there are no significant population centers affected means that this is an imperfection we are willing to accept -- Open Location Codes cannot exactly represent coordinates at latitude 90. +* Plus Codes cannot exactly represent coordinates at latitude 90. The codes for latitude 90 would normally have an area whose lower latitude is at 90 degrees and an upper latitude of 90 + the height of the code area, but this would result in meaningless coordinates. Instead, when encoding @@ -321,148 +321,12 @@ shortcoming since there is no permanent settlement at the North Pole. == Open Location Code Specification -. The valid digits used in Open Location Codes and their values are -shown below: -+ -[options="header,autowidth"] -|======================= -|Decimal|OLC -|0|2 -|1|3 -|2|4 -|3|5 -|4|6 -|5|7 -|6|8 -|7|9 -|8|C -|9|F -|10|G -|11|H -|12|J -|13|M -|14|P -|15|Q -|16|R -|17|V -|18|W -|19|X -|======================= -+ -. In addition to the above characters, a full Open Location Code must include a -single "+" as a separator after the eighth digit. - -. Open Location Codes with less than eight digits can be suffixed with zeros -with a "+" used as the final character. Zeros may not be followed by any -other digit. - -. Processing of Open Location Codes must be case insensitive. - -. Open Location Code implementations must return upper case codes. They must -include the "+" character and zero padding when returning codes. - -. Latitude and longitude coordinates must be provided in decimal degrees, -based on WGS84. Latitude coordinates will be clipped, longitude coordinates -will be normalised. - -. Encoding a latitude and longitude to an Open Location Code of up to 10 -characters is done by: - - Clip the latitude to the range -90 to 90 - - Normalise the longitude to the range -180 to 180 - - If the latitude is 90, compute the height of the area based on the - requested code length and subtract the height from the latitude. (This - ensures the area represented does not exceed 90 degrees latitude.) - - Adding 90 to the latitude - - Adding 180 to the longitude - - Encoding up to five latitude and five longitude characters (10 in total) by -converting each value into base 20 (starting with a positional value of 20) -and using the Open Location Code digits - - Interleave the latitude and longitude digits, starting with latitude. - -. To extend Open Location Codes with 10 digits, divide the area of the -code into a 4x5 grid and append the letter identifying the grid cell as -shown. Repeat as many times as necessary. -+ -[options="autowidth"] -|======================= -|R|V|W|X -|J|M|P|Q -|C|F|G|H -|6|7|8|9 -|2|3|4|5 -|======================= -+ - -. Open Location Codes with even numbered lengths of 10 digits or less -have the same height and width in degrees: - - A two digit code must have a height and width of 20° - - A four digit code must have a height and width of 1° - - A six digit code must have a height and width of 0.05° - - An eight digit code must have a height and width of 0.0025° - - A ten digit code must have a height and width of 0.000125°. - -. Open Location Codes with lengths of more than 10 digits have different -heights and widths: - - An 11 digit code has a height of 0.000025°, and a width of 0.00003125° - - Subsequent lengths divide the height by five and the width by four. - -. Decoding an Open Location Code provides the coordinates of the south west -corner. The north east coordinates are obtained by adding the height and -width to the south west corner. - -. The area of an Open Location Code is defined as including the south west -coordinates but excluding the north east coordinates. - -. The two standard code lengths are 10 digits (1/8000° x 1/8000°), and 11 -digits (1/40000° x 1/32000°). Other code lengths are considered -non-standard for household addressing, although they may be used for other -purposes. - -. Using a reference location, the first four digits of a -code with at least eight digits may be removed if both the latitude and -longitude of a reference location are within +/- 0.25° of the latitude and -longitude of the Open Location Code center. - -. Using a reference location, the first six digits of a -code with at least eight digits may be removed if both the latitude and -longitude of the reference location are within +/- 0.0125° of the latitude -and longitude of the Open Location Code center. - -. Only Open Location Codes with at least eight digits may be shortened by -omitting leading digits. Short codes must always include the "+" -character. - -. When recovering a full Open Location Code from a short Open Location Code -the number of digits to recover can be computed from the position of the "+" -character. - -. When recovering a full Open Location Code from a short Open Location Code -using a reference location, the method must return the nearest matching code -to the reference location, taking note that this will not necessarily have -the same leading digits as the code produced by encoding the reference -location. - -. Open Location Code implementations must provide the following methods: - - a method to convert a latitude and longitude into a 10 digit Open - Location Code - - a method to convert a latitude and longitude into an arbitrary length - Open Location Code - - a method to decode an Open Location Code into, at a minimum, the - latitude and longitude of the south-west corner and the areas height and - width - - a method to determine if a string is a valid sequence of Open Location - Code characters - - a method to determine if a string is a valid full Open Location Code - - a method to determine if a string is a valid short Open Location Code - - a method to remove four or six digits from the front of an Open Location - Code given a reference location - - a method to recover a full Open Location Code from a short code and a - reference location. +Refer to link:specification.md[Open Location Code Specification]. [bibliography] == Notes -- [[[poste_restante]]]Post restante (French: lit. post remaining or general +- [[[poste_restante]]] Post restante (French: lit. post remaining or general delivery) is a service where a delivery is made to a post office that holds the package until the recipient calls for it. - [[[height_width]]] "Height" and "width" are used as a shorthand for north/south diff --git a/Documentation/Specification/specification.md b/Documentation/Specification/specification.md new file mode 100644 index 00000000..565ac12d --- /dev/null +++ b/Documentation/Specification/specification.md @@ -0,0 +1,212 @@ +# Open Location Code Specification + +## Input values + +Open Location Code encodes two numbers, latitude and longitude, in degrees, into a single, short string. + +The latitude and longitude should be WGS84 values. If other datums are used it must be stated and made clear that these will produce different locations if plotted. + +## Character Set + +The following defines the valid characters in a Plus Code. Sequences that contain other characters are by definition not valid Open Location Code. + +### Digits + +Open Location Code symbols have been selected to reduce writing errors and prevent accidentally spelling words. +Here are the digits, shown with their numerical values: + +|Symbol|2|3|4|5|6|7|8|9|C|F|G|H|J|M|P|Q|R|V|W|X| +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +|Value|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19| + +### Format Separator + +The "+" character (U+002B) is used as a non-significant character to aid formatting. + +### Padding Character + +The "0" character (U+0030) is used as a padding character before the format separator. + +## Encoding + +Code digits and ordering do not change in right-to-left (RTL) languages. + +The latitude number must be clipped to be in the range -90 to 90. + +The longitude number must be normalised to be in the range -180 to 180. + +### Most significant 10 digits + +Summary: +Add 90 to latitude and 180 to longitude to force them into positive ranges. +Encode both latitude and longitude into base 20, using the symbols above, for five digits each i.e. to a place value of 0.000125. +Starting with latitude, interleave the digits. + +The following provides an algorithm to encode the values from least significant digit to most significant digit: +1. Add 90 to the latitude and add 180 to the longitude, multiply both by 8000 and take the integer parts as latitude and longitude respectively +1. Prefix the existing code with the symbol that has the integer part of longitude modulus 20 +1. Prefix the existing code with the symbol that has the integer part of latitude modulus 20 +1. Divide both longitude and latitude by 20 +1. Repeat from step 2 four more times. + +### Least significant five digits + +This differs from the above method in that each step produces a single character. +This encodes latitude into base five and longitude into base four, and then combines the digits for each position together. + +The following provides an algorithm to encode the values from least significant digit to most significant digit: +1. Add 90 to the latitude, multiply the fractional part by 2.5e7 and take the integer part as latitude. +1. Add 180 to the longitude, multiply the fractional part by 8.192e6 and take the integer part as longitude. +1. Take the integer part of latitude modulus 5. Multiply that by 4, and add the integer part of the longitude modulus 4. +1. Prefix the existing code with the symbol with the above value. +1. Divide longitude by four and latitude by five. +1. Repeat from step 2 four more times. + +### Code length + +The minimum valid length of a Plus Code is two digits. +The maximum length of a Plus Code is 15 digits. + +Below 10 digits, only even numbers are valid lengths. + +The default length for most purposes is 10 digits. + +### Formatting + +The format separator must be inserted after eight digits. +If the requested code length is fewer than eight digits, the remaining digits before the format separator must consist of the padding character. + +### Code precision + +The following table gives the precision of the valid code lengths in degrees and in meters. Where the precisions differ between latitude and longitude both are shown: + +| Code length | Precision in degrees | Precision | +| :---------: | :------------------: | :--------------: | +| 2 | 20 | 2226 km | +| 4 | 1 | 111.321 km | +| 6 | 1/20 | 5566 meters | +| 8 | 1/400 | 278 meters | +| 10 | 1/8000 | 13.9 meters | +| 11 | 1/40000 x 1/32000 | 2.8 x 3.5 meters | +| 12 | 1/200000 x 1/128000 | 56 x 87 cm | +| 13 | 1/1e6 x 1/512000 | 11 x 22 cm | +| 14 | 1/5e6 x 1/2.048e6 | 2 x 5 cm | +| 15 | 1/2.5e7 x 1/8.192e6 | 4 x 14 mm | + +NB: This table assumes one degree is 111321 meters, and that all distances are calculated at the equator. + +## Decoding + +The coordinates obtained when decoding are the south-west corner. +(The north-east corner and center coordinates can be obtained by adding the precision values.) + +This implies that the north-east coordinates are not included in the area of the code, with the exception of codes whose northern latitude is 90 degrees. + +## Short Codes + +Short codes are used relative to a reference location. +They allow the code part to be shorter, easier to use and easier to remember. + +Short codes have at least two and a maximum of six digits removed from the beginning of the code. +The resulting code must include the "+" character (the format separator). + +Codes that include padding characters must not be shortened. + +Digits can be removed from the code, while the precision of the position is more than twice the maximum of the latitude or longitude offset between the code center and the reference location. +Recovery of the original code must meet the same criteria. + +For example, 8FVC9G8F+6W has the center 47.365562,8.524813. The following table shows what it can be shortened to, relative to various locations: + +| Reference Location | Latitude offset | Longitude offset | Twice max offset | Code can be shortened to | +| ------------------ | --------------: | ---------------: | ---------------: | -----------------------: | +| 47.373313,8.537562 | 0.008 | 0.013 | 0.025 | 8F+6W | +| 47.339563,8.556687 | 0.026 | 0.032 | 0.064 | 9G8F+6W | +| 47.985187,8.440688 | 0.620 | 0.084 | 1.239 | VC9G8F+6W | +| 38.800562,-9.064937| 0.620 | 8.565 | 17.590 | 8FVC9G8F+6W | + +Note: A code that has been shortened will not necessarily have the same initial four digits as the reference location. + +### Generating Short Codes + +Being able to say _WF8Q+WF, Praia_ is significantly easier than remembering and using _796RWF8Q+WF_. +With that in mind, how do you choose the locality to use as a reference? + +Ideally, you need to use both the bounding box of the locality, as well as its center point. + +Given a global code, _796RWF8Q+WF_, you can eliminate the first **four** digits of the code if: + * The center point of the feature is within **0.4** degrees latitude and **0.4** degrees longitude + * The bounding box of the feature is less than **0.8** degrees high and wide. + +(These values are chosen because a four digit Plus Code is 1x1 degrees.) + +If there is no suitable locality close enough or small enough, you can eliminate the first **two** digits of the code if: + * The center point of the feature is within **8** degrees latitude and **8** degrees longitude + * The bounding box of the feature is less than **16** degrees high and wide. + +(These values are chosen because a two digit Plus Code is 20x20 degrees.) + +The values above are slightly smaller than the maximums to allow for different geocoder backends placing localities in slightly different positions. +Although they could be increased there will be a risk that a shortened code will recover to a different location than the original, and people misdirected. + +Note: Usually your feature will be a town or city, but you could also use geographical features such as lakes or mountains, if they are the best local reference. +If a settlement (such as neighbourhood, town or city) is to be used, you should choose the most prominent feature that meets the requirements, to avoid using obscure features that may not be widely known. +(Basically, prefer city or town over neighbourhood.) + +## API Requirements + +The following public methods should be provided by any Open Location Code implementation, subject to minor changes caused by language conventions. + +Note that any method that returns a Plus Code should return upper case characters. + +Methods that accept Plus Codes as parameters should be case insensitive. + +Capitalisation should follow the language convention, for example the method `isValid` in golang would be `IsValid`. + +Errors should be returned following the language convention. For example exceptions in Python and Java, `error` objects in golang. + +### `isValid` + +The `isValid` method takes a single parameter, a string, and returns a boolean indicating whether the string is a valid Plus Code. + +### `isShort` + +The `isShort` method takes a single parameter, a string, and returns a boolean indicating whether the string is a valid short Plus Code. + + See [Short Codes](#short-codes) above. + +### `isFull` + +Determines if a code is a valid full (i.e. not shortened) Plus Code. + +Not all possible combinations of Open Location Code characters decode to valid latitude and longitude values. +This checks that a code is valid and that the resulting latitude and longitude values are legal. +Full codes must include the format separator character and it must be after eight characters. + +### `encode` + +Encode a location into a Plus Code. +This takes a latitude and longitude and an optional length. +If the length is not specified, a code with 10 digits (and the format separator character) will be returned. + +### `decode` + +Decodes a Plus Code into the location coordinates. +This method takes a string. +If the string is a valid full Plus Code, it returns: +- the latitude and longitude of the SW corner of the bounding box; +- the latitude and longitude of the NE corner of the bounding box; +- the latitude and longitude of the center of the bounding box; +- the number of digits in the original code. + +### `shorten` + +Passed a valid full Plus Code and a latitude and longitude this removes as many digits as possible (up to a maximum of six) such that the resulting code is the closest matching code to the passed location. +A safety factor may be included. + +If the code cannot be shortened, the original full code should be returned. + +Since the only really useful shortenings are removing the first four or six characters, methods such as `shortenBy4` or `shortenBy6` could be provided instead. + +### `recoverNearest` + +This method is passed a valid short Plus Code and a latitude and longitude, and returns the nearest matching full Plus Code to the specified location. diff --git a/docs/images/code_areas.png b/Documentation/images/code_areas.png similarity index 100% rename from docs/images/code_areas.png rename to Documentation/images/code_areas.png diff --git a/docs/images/geohash36_grid.png b/Documentation/images/geohash36_grid.png similarity index 100% rename from docs/images/geohash36_grid.png rename to Documentation/images/geohash36_grid.png diff --git a/docs/images/olc_10_character.png b/Documentation/images/olc_10_character.png similarity index 100% rename from docs/images/olc_10_character.png rename to Documentation/images/olc_10_character.png diff --git a/docs/images/olc_11_grid.png b/Documentation/images/olc_11_grid.png similarity index 100% rename from docs/images/olc_11_grid.png rename to Documentation/images/olc_11_grid.png diff --git a/docs/images/openpostcode_grid.png b/Documentation/images/openpostcode_grid.png similarity index 100% rename from docs/images/openpostcode_grid.png rename to Documentation/images/openpostcode_grid.png diff --git a/FAQ.txt b/FAQ.txt index 103ac280..ddcaa9d8 100644 --- a/FAQ.txt +++ b/FAQ.txt @@ -12,7 +12,7 @@ codes should work offline, should not include words and should not require setting up. It should be possible to tell if two codes are close to each other by looking at them. -Q: Where do we expect Open Location Codes to be useful? +Q: Where do we expect Open Location Code to be useful? A: More than half the world's urban dwellers live on streets that don't have formal names. We expect these codes will be mostly used by people in areas lacking street addresses, but could also be used in areas that are mapped @@ -28,7 +28,7 @@ approximately $5USD per addressed building. The advantage of Open Location Codes is that they are available now to anyone with access to a computer or smartphone. -Q: Are there uses for Open Location Codes in well-mapped countries? +Q: Are there uses for Open Location Code in well-mapped countries? A: Yes, for example Switzerland has villages where multiple streets have the same name. The UK has some homes that are identified by names, rather than by street numbers. Venice and Japan both have block-based addresses, rather @@ -50,7 +50,38 @@ devices for at least 15 years, and yet latitude and longitude coordinates are still not widely used by people to specify destinations. We think that this shows latitude and longitude are just too complicated for normal use. -Q: Why don't Open Location Codes include altitude? +Q: How do short codes differ from full codes? +A: A full code encodes a globally unique location and can be decoded offline +and without any additional data. Short codes are generated from full codes +by dropping characters from the start, which means that they can only be +decoded relative to a reference location. This reference location must be +communicated with the short code, if it isn't shared knowledge of all parties +involved. + +Q: How can a reference location be communicated with a short code? +A: Often, a reference location can be assumed to be shared knowledge and +does not need to be explicitly given. For example, when talking to someone +in the same city and agreeing to meet them at "G98H+F2", there will typically +be just one location with that short code in any given city. + +If a reference location needs to be communicated, it is often sensible to +just append the name of the city. For example, if postal or rescue services +in an area make use of Open Location Code, using "G98H+F2 Berlin" should allow +mail to be delivered or an ambulance to navigate to its intended destination. +If short codes need to be encoded and decoded algorithmically, for example by +using a lookup table or by making use of a (reverse) geocoding API, the same +data or API should be used for both encoding and decoding. + +Q: Can a global lookup table for short codes be added to this repository? +Providing this data is not within the scope of this repository, and issues +opened to request such data will be closed. If online geocoding APIs can not +be used, it is often useful to first think about the actual requirements of +the Open Location Code application, as regional instead of global data will +often suffice. This data might already exist for a specific application, or +can potentially be extracted from open GIS or map data available online. +The community mailing list can be used to ask for help with such data. + +Q: Why doesn't Open Location Code include altitude? A: We didn't want to append it as a suffix or bury it in the code because we want to be able to truncate the codes reliably. We also didn't want to unnecessarily extend the length of codes for what we expect to be a minority @@ -60,7 +91,7 @@ different ways of numbering building floors depending on local custom. In summary, we couldn't think of a way that was better than specifying the code and allowing people to just say "3rd floor". -Q: Why do Open Location Codes use two algorithms? +Q: Why does Open Location Code use two algorithms? A: The first algorithm provides codes that can be visually compared and sorted. This is used until the code is 10 characters long, with a resolution of 1/8000th of a degree, approximately 14 meters. This will often be @@ -77,7 +108,7 @@ If we had based the entire code on the second algorithm, we would have codes that would not be reliably visually comparable or sortable by proximity. Q: Why is Open Location Code based on latin characters? -A: We are aware that many of the countries where Open Location Codes will be +A: We are aware that many of the countries where Plus Codes will be most useful use non-Latin character sets, such as Arabic, Chinese, Cyrillic, Thai, Vietnamese, etc. We selected Latin characters as the most common second-choice character set in these locations. @@ -96,7 +127,7 @@ Hyderabad!") because users will realise that a code cannot possibly be correct. It's analogous to someone using the wrong suburb name today - it happens, people are able to deal with it. -Q: Why do Open Location Codes look like something fell on my keyboard? +Q: Why do Plus Codes look like something fell on my keyboard? A: We wanted something that wasn't linked to a single culture, so word-based codes were out. That meant that the codes would be essentially a number, but we used letters as well as digits to raise the number base and shorten the @@ -107,14 +138,14 @@ noncontiguous, although it is difficult to see how we could change that without violating any of the aims. Q: What coordinate system does Open Location Code use? -A: Open Location Codes should be based on WGS84, since this is the datum +A: Open Location Code should be based on WGS84, since this is the datum used by GPS and is how coordinates on smartphone devices are made available. There is nothing to prevent coordinates using other datums being used, but when decoded by someone who expects them to be WGS84, it may result in a different location. Q: Why do Open Location Code areas distort at high latitudes? -A: Open Location Codes are a function of latitude and longitude. As +A: Plus Codes are a function of latitude and longitude. As longitude lines converge on the north and south poles the areas become narrower and narrower. At the equator codes are square, but at about 60 degrees latitude, the codes are only half as wide. @@ -125,12 +156,12 @@ different, even though they may be very close together. Apart from some islands in the Fiji group, there are almost no affected inhabited areas, and we feel this is acceptable. The other discontinuities are at the poles, but as these do not have large permanent populations we don't expect significant -use of Open Location Codes here. +use of Open Location Code here. Q: What about continental drift? A: Most tectonic plates are moving at rates of 1-5cm per year. With the 10 -character Open Location Codes representing 14x14 meter boxes, codes should -be valid for many years. Even the more accurate 11 character codes should -not require updating for 30-50 years. But even if they do, the worst result -is that someone using a code will find themselves at the home or building +character Plus Code representing 14x14 meter boxes, codes should be valid for +many years. Even the more accurate 11 character codes should not require +updating for 30-50 years. But even if they do, the worst result is that +someone using a code will find themselves at the home or building next door. diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 00000000..a639d0b0 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,13 @@ +module( + name = "openlocationcode", + version = "1.0", +) + +# bazel-skylib required by #@io_bazel_rules_closure. +# https://github.com/bazelbuild/bazel-skylib +bazel_dep(name = "bazel_skylib", version = "1.7.1") + +# googletest required by c/BUILD. +# https://github.com/google/googletest +bazel_dep(name = "googletest", version = "1.15.2") + diff --git a/README.md b/README.md index 865c3bec..a9ff8fb4 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,40 @@ Open Location Code ================== -Open Location Codes are a way of encoding location into a form that is -easier to use than latitude and longitude. +[![Build Status](https://github.com/google/open-location-code/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/google/open-location-code/actions/workflows/main.yml?query=branch%3Amain) +[![CDNJS](https://img.shields.io/cdnjs/v/openlocationcode.svg)](https://cdnjs.com/libraries/openlocationcode) -They are designed to be used as a replacement for street addresses, especially +Open Location Code is a technology that gives a way of encoding location into a form that is +easier to use than latitude and longitude. The codes generated are called Plus Codes, as their +distinguishing attribute is that they include a "+" character. + +The technology is designed to produce codes that can be used as a replacement for street addresses, especially in places where buildings aren't numbered or streets aren't named. -Open Location Codes represent an area, not a point. As digits are added -to a code, the area shrinks, so a long code is more accurate than a short +Plus Codes represent an area, not a point. As digits are added +to a code, the area shrinks, so a long code is more precise than a short code. Codes that are similar are located closer together than codes that are different. -A location can be converted into a code, and a code can be converted back -to a location completely offline. +A location can be converted into a code, and this (full) code can be converted back to a location completely offline, without any data tables to lookup or online services required. + +Codes can be shortened for easier communication, in which case they can be used regionally or in combination with a reference location that all users of this short code need to be aware of. If the reference location is given in form of a location name, use of a geocoding service might be necessary to recover the original location. -There are no data tables to lookup or online services required. The -algorithm is publicly available and can be used without restriction. +Algorithms to +* encode and decode full codes, +* shorten them relative to a reference location, and +* recover a location from a short code and a reference location given as latitude/longitude pair + +are publicly available and can be used without restriction. Geocoding services are not a part of the Open Location Code technology. Links ----- * [Demonstration site](http://plus.codes/) * [Mailing list](https://groups.google.com/forum/#!forum/open-location-code) - * [Comparison of existing location encoding systems](https://github.com/google/open-location-code/blob/master/docs/comparison.adoc) - * [Open Location Code definition](https://github.com/google/open-location-code/blob/master/docs/olc_definition.adoc) + * [Comparison of existing location encoding systems](Documentation/Reference/comparison.adoc) + * [Open Location Code definition](Documentation/Specification/olc_definition.adoc) Description ----------- @@ -39,14 +48,14 @@ previous area. And so on - each pair of digits reduces the area to 1/400th of the previous area. As an example, the Parliament Buildings in Nairobi, Kenya are located at -6GCRPR6C+24. 6GCR is the area from 2S 36E to 1S 37E. PR6C+24 is a 14 meter -wide by 14 meter high area within 6GCR. +6GCRPR6C+24. 6GCR is the area from 2°S 36°E to 1°S 37°E. PR6C+24 is a 14 +by 14 meter wide area within 6GCR. A "+" character is used after eight digits, to break the code up into two parts -and to distinguish shortened Open Location Codes from postal codes. +and to distinguish codes from postal codes. -There will be locations where a 10 digit code is not sufficiently accurate, but -refining it by a factor of 20 is unnecessarily precise and requires extending +There will be locations where a 10-digit code is not sufficiently precise, but +refining it by a factor of 20 is i) unnecessarily precise and ii) requires extending the code by two digits. Instead, after 10 digits, the area is divided into a 4x5 grid and a single digit used to identify the grid square. A single grid refinement step reduces the area to approximately 3.5x2.8 meters. @@ -65,6 +74,8 @@ Rather than a large city size feature to generate the reference location, it is better to use smaller, neighbourhood features, that will not have as much variation in their geocode results. +Guidelines for shortening codes are in the [wiki](Documentation/Specification/Short_Code_Guidance.md). + Recovering shortened codes works by providing the short code and a reference location. This does not need to be the same as the location used to shorten the code, but it does need to be nearby. Shortened codes always include the "+" @@ -81,7 +92,7 @@ languages. Each implementation provides the following functions: * Test a code to see if it is a valid sequence * Test a code to see if it is a valid full code - Not all valid sequences are valid full codes + (not all valid sequences are valid full codes) * Encode a latitude and longitude to a standard accuracy (14 meter by 14 meter) code * Encode a latitude and longitude to a code of any length diff --git a/TESTING.md b/TESTING.md index 218e089d..877a2848 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1,61 +1,84 @@ -# Automated Integration Testing -Changes are sent to [Travis CI](https://travis-ci.org) -for integration testing after pushes, and you can see the current test status -[here](https://travis-ci.org/google/open-location-code). +# Testing +The preferred mechanism for testing is using the [Bazel](https://bazel.build/) build system. +This uses files called `BUILD` ([example](https://github.com/google/open-location-code/blob/main/python/BUILD) to provide rules to build code and run tests). +Rather than installing Bazel directly, install [Bazelisk](https://github.com/bazelbuild/bazelisk), a launcher for Bazel that allows configuration of the specific version to use. -The testing configuration is controlled by two files: -[`travis.yml`](.travis.yml) and [`run_tests.sh`](run_tests.sh). +Create a `BUILD` file in your code directory with a [test rule](https://bazel.build/versions/master/docs/test-encyclopedia.html). +You can then test your code by running: -## [.travis.yml](.travis.yml) -This file defines the prerequisites required for testing, and the list of -directories to be tested. (The directories listed are tested in parallel.) +```sh +bazelisk test : +``` + +All tests can be run with: + +```sh +bazelisk test ...:all +``` -The same script ([run_tests.sh](run_tests.sh)) is executed for all directories. +## Automated Integration Testing +On pushes and pull requests changes are tested via GitHub Actions. +You can see the current test status in the [Actions tab](https://github.com/google/open-location-code/actions/workflows/main.yml?query=branch%3Amain). -## [run_tests.sh](run_tests.sh) -This file is run once for _each_ directory defined in -`.travis.yml`. The directory name being tested is passed in the environment -variable `TEST_DIR`.) +The testing configuration is controlled by the [`.github/workflows/main.yml`](.github/workflows/main.yml) file. -[`run_tests.sh`](run_tests.sh) checks the value of `TEST_DIR`, and then runs -commands to test the relevant implementation. The commands that do the testing -**must** return zero on success and non-zero value on failure. _Tests that -return zero, even if they output error messages, will be considered by the -testing framework as a success_. +### [.github/workflows/main.yml](.github/workflows/main.yml) +This file defines each language configuration to be tested. + +Some languages can be tested natively, others are built and tested using Bazel BUILD files. + +An example of a language being tested natively is go: -## Adding Your Tests -Add your directory to the [`.travis.yml`](.travis.yml) file: ``` -# Define the list of directories to execute tests in. -env: - - TEST_DIR=js - - TEST_DIR=go - - TEST_DIR=your directory goes here + # Go implementation. Lives in go/ + test-go: + runs-on: ubuntu-latest + env: + OLC_PATH: go + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.17 + - name: test + run: go test ./${OLC_PATH} ``` -Then add the necessary code to [`run_tests.sh`](run_tests.sh): +This defines the language, uses the `1.17` version, sets an environment variable with the path and then runs the testing command `go test ./go`. + +An example of a language using Bazel is Python: + ``` -# Your language goes here -if [ "$TEST_DIR" == "your directory goes here" ]; then - cd directory && run something && run another thing - exit # Exit immediately, returning the last command status -fi + # Python implementation. Lives in python/, tested with Bazel. + test-python: + runs-on: ubuntu-latest + env: + OLC_PATH: python + strategy: + matrix: + python: [ '2.7', '3.6', '3.7', '3.8' ] + name: test-python-${{ matrix.python }} + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: test + run: bazelisk test --test_output=all ${OLC_PATH}:all ``` -Note the use of `&&` to combine the test commands. This ensures that if any -command in the sequence fails, the script will stop and return a test failure. - -The test commands **must be** followed by an `exit` statement. This ensures that -the script will return the same status as the tests. If this status is zero, -the test will be marked successful. If not, the test will be marked as a -failure. - -## Testing Multiple Languages -[Travis CI](https://travis-ci.org) assumes that each github project has only -a single language. That language is specified in the [.travis.yml](.travis.yml) -file (`language: node_js`). - -This shouldn't be a problem, since prerequisites can still be loaded in the -`before_script` section, and then commands executed in -[`run_tests.sh`](run_tests.sh). However in the event that you can't resolve a -problem, leave a comment in the issue or push request and we'll see if someone -can figure out a solution. + +Bazel is pre-installed on GitHub-hosted runners which are used to run CI, so there's no need to install it. +This example also shows how to test with multiple versions of a language. + +### Adding Your Tests + +Simply add a new section to the `.github/workflows/main.yml` file with the appropriate language, and either the native test command or call `bazelisk test` like the other examples. +More information about GitHub actions can be found in the [documentation](https://docs.github.com/en/actions/quickstart). + +## Bazel version + +Currently (2004), Bazel version 8 or later cannot be used for testing. (See issue google/open-location-code#662.) +The `js/closure` tests require using https://github.com/bazelbuild/rules_closure, which is not yet available as a Bazel module. +That dependency must be specified using a Bazel `WORKSPACE` file, and the version of Bazel to use is specified in the `.bazeliskrc` file. + diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 00000000..047e6abe --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,19 @@ +# Workspace configuration for Bazel build tools. + +# TODO: #642 -- Remove once io_bazel_rules_closure supports Bazel module configuration. +workspace(name = "openlocationcode") + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "io_bazel_rules_closure", + integrity = "sha256-EvEWnr54L4Yx/LjagaoSuhkviVKHW0oejyDEn8bhAiM=", + strip_prefix = "rules_closure-0.14.0", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/rules_closure/archive/0.14.0.tar.gz", + "https://github.com/bazelbuild/rules_closure/archive/0.14.0.tar.gz", + ], +) +load("@io_bazel_rules_closure//closure:repositories.bzl", "rules_closure_dependencies", "rules_closure_toolchains") +rules_closure_dependencies() +rules_closure_toolchains() diff --git a/android_demo/.gitignore b/android_demo/.gitignore new file mode 100644 index 00000000..5c6903a5 --- /dev/null +++ b/android_demo/.gitignore @@ -0,0 +1,22 @@ +# Built application files +*/build/ + +# Local configuration file (sdk path, etc) +local.properties + +# IntelliJ configuration file +android/android.iml + +# Gradle generated files +.gradle/ + +# Signing files +.signing/ + +# User-specific configurations +.idea/* + +# OS-specific files +.DS_Store +._* +.Trashes diff --git a/android_demo/README.md b/android_demo/README.md new file mode 100644 index 00000000..8808437b --- /dev/null +++ b/android_demo/README.md @@ -0,0 +1,20 @@ +Android Open Location Code Demonstrator +======================================= + +This is the source code for an Android app that uses +[Open Location Code](https://maps.google.com/pluscodes/) and displays them on a +map. + +It displays the current location, computes the code for that location, and uses +a bundled set of place names to shorten the code to a more convenient form. +Currently all the place names exist within Cape Verde. Other place names can be +added to the source. + +Using a bundled set of places means that determining the current address, and +locating an entered address will work offline. + +This project has been structured to be used with +[Android Studio](https://developer.android.com/studio/index.html). + +An Open Location Code JAR file is in the directory `android/libs`. If the core library +is updated, you will need to update this JAR file. diff --git a/android_demo/android/build.gradle b/android_demo/android/build.gradle new file mode 100644 index 00000000..f98467ea --- /dev/null +++ b/android_demo/android/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' +apply plugin: 'com.neenbedankt.android-apt' + +android { + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + applicationId "com.openlocationcode.android" + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion" + compile "com.android.support:design:$rootProject.supportLibraryVersion" + + // Dagger dependencies + apt "com.google.dagger:dagger-compiler:$rootProject.daggerVersion" + provided 'org.glassfish:javax.annotation:10.0-b28' + compile "com.google.dagger:dagger:$rootProject.daggerVersion" + + compile "com.android.volley:volley:$rootProject.volleyVersion" + + compile "com.google.auto.factory:auto-factory:$rootProject.autoFactoryVersion" + + + compile "com.google.android.gms:play-services-location:$rootProject.gmsVersion" + compile "com.google.android.gms:play-services-base:$rootProject.gmsVersion" +} diff --git a/android_demo/android/libs/README.md b/android_demo/android/libs/README.md new file mode 100644 index 00000000..2efb02e2 --- /dev/null +++ b/android_demo/android/libs/README.md @@ -0,0 +1,32 @@ +Building the Open Location Code JAR file +== + +Using the source in the +[Java](https://github.com/google/open-location-code/blob/master/java/com/google/openlocationcode/OpenLocationCode.java) +implementation, build a JAR file and put it in this location. + +-- + +Assuming you've downloaded this repository locally: + +``` +cd open-location-code-master/java +javac com/google/openlocationcode/OpenLocationCode.java +jar -cfM ./openlocationcode.jar com/google/openlocationcode/OpenLocationCode\$CodeArea.class com/google/openlocationcode/OpenLocationCode.class +``` + +The `.jar` file is in the `open-location-code-master/java` directory + +If working with Android Studio, add `openlocationcode.jar` to `/{PROJECT_NAME}/{APP}/libs` *(you may need to create the `/libs` folder)* + +Why don't we include a JAR file here? +-- + +Basically, we want to make sure that we don't fix a bug in the Java implementation and forget to +update this JAR file. + +Why don't we have a Maven repository? +-- + +So far, we've only had one request. If you would like to be able to pull the library via Maven, +file an issue and we'll consider it. diff --git a/android_demo/android/proguard-rules.pro b/android_demo/android/proguard-rules.pro new file mode 100644 index 00000000..9d6093f9 --- /dev/null +++ b/android_demo/android/proguard-rules.pro @@ -0,0 +1,15 @@ +# Add project specific ProGuard rules here. +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/android_demo/android/src/main/AndroidManifest.xml b/android_demo/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..09f45b26 --- /dev/null +++ b/android_demo/android/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/code/CodeContract.java b/android_demo/android/src/main/java/com/openlocationcode/android/code/CodeContract.java new file mode 100644 index 00000000..bd8781e9 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/code/CodeContract.java @@ -0,0 +1,55 @@ +/* + * 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.openlocationcode.android.code; + +import com.openlocationcode.android.direction.Direction; + +import com.google.openlocationcode.OpenLocationCode; + +/** + * The contract for the functionality displaying the code, in {@link + * com.openlocationcode.android.main.MainActivity}. + */ +public interface CodeContract { + + interface View { + + /** + * Implements displaying the {@code code}. + */ + void displayCode(OpenLocationCode code); + } + + interface ActionsListener { + + /** + * Call this when the code is requested for a new location (eg when the map is dragged). + * + * @param latitude + * @param longitude + * @param isCurrent + * @return + */ + Direction codeLocationUpdated(double latitude, double longitude, boolean isCurrent); + + /** + * @return the code currently displayed to the user. + */ + OpenLocationCode getCurrentOpenLocationCode(); + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/code/CodePresenter.java b/android_demo/android/src/main/java/com/openlocationcode/android/code/CodePresenter.java new file mode 100644 index 00000000..213112f5 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/code/CodePresenter.java @@ -0,0 +1,163 @@ +/* + * 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.openlocationcode.android.code; + +import android.content.Intent; +import android.location.Location; +import android.net.Uri; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Intents.Insert; +import android.support.v7.widget.PopupMenu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; + +import com.openlocationcode.android.R; +import com.openlocationcode.android.direction.Direction; +import com.openlocationcode.android.direction.DirectionUtil; +import com.openlocationcode.android.main.MainActivity; + +import com.google.openlocationcode.OpenLocationCode; +import com.google.openlocationcode.OpenLocationCode.CodeArea; + +import java.util.Locale; + +/** + * Presents the code information, direction and handles the share menu. + */ +public class CodePresenter + implements CodeContract.ActionsListener, PopupMenu.OnMenuItemClickListener { + + private static final float MAX_WALKING_MODE_DISTANCE = 3000f; // 3 km + + private final CodeView mView; + + public CodePresenter(CodeView view) { + mView = view; + + mView.getShareButton() + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + openShareMenu(); + } + }); + mView.getNavigateButton() + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + navigate(); + } + }); + } + + @Override + public Direction codeLocationUpdated(double latitude, double longitude, boolean isCurrent) { + OpenLocationCode code = OpenLocationCodeUtil.createOpenLocationCode(latitude, longitude); + mView.displayCode(code); + Location currentLocation = MainActivity.getMainPresenter().getCurrentLocation(); + + Direction direction; + if (isCurrent) { + direction = new Direction(code, null, 0, 0); + } else if (currentLocation == null) { + direction = new Direction(null, code, 0, 0); + } else { + direction = DirectionUtil.getDirection(currentLocation, code); + } + + return direction; + } + + @Override + public OpenLocationCode getCurrentOpenLocationCode() { + return mView.getLastFullCode(); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + int itemId = item.getItemId(); + String code = mView.getCodeAndLocality(); + if (itemId == R.id.share_code) { + openShareChooser(code); + return true; + } else if (itemId == R.id.save_to_contact) { + saveCodeAsContact(code); + return true; + } + return false; + } + + private void openShareMenu() { + PopupMenu popup = new PopupMenu(mView.getContext(), mView.getShareButton()); + popup.setOnMenuItemClickListener(this); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.share_menu, popup.getMenu()); + popup.show(); + } + + private void openShareChooser(String code) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, code); + intent.setType("text/plain"); + mView.getContext() + .startActivity( + Intent.createChooser( + intent, + mView.getContext().getResources().getText(R.string.share_chooser_title))); + } + + private void saveCodeAsContact(String code) { + Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + intent.putExtra(Insert.POSTAL, code); + mView.getContext().startActivity(intent); + } + + private void navigate() { + OpenLocationCode code = mView.getLastFullCode(); + CodeArea codeArea = code.decode(); + Location currentLocation = MainActivity.getMainPresenter().getCurrentLocation(); + float[] results = new float[3]; + Location.distanceBetween( + currentLocation.getLatitude(), + currentLocation.getLongitude(), + codeArea.getCenterLatitude(), + codeArea.getCenterLongitude(), + results); + float distance = results[0]; + char navigationMode; + if (distance <= MAX_WALKING_MODE_DISTANCE) { + navigationMode = 'w'; + } else { + navigationMode = 'd'; + } + + Uri gmmIntentUri = Uri.parse( + String.format( + Locale.US, + "google.navigation:q=%f,%f&mode=%s", + codeArea.getCenterLatitude(), + codeArea.getCenterLongitude(), + navigationMode)); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + mView.getContext().startActivity(mapIntent); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/code/CodeView.java b/android_demo/android/src/main/java/com/openlocationcode/android/code/CodeView.java new file mode 100644 index 00000000..ff7e297a --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/code/CodeView.java @@ -0,0 +1,95 @@ +/* + * 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.openlocationcode.android.code; + +import com.openlocationcode.android.R; +import com.openlocationcode.android.localities.Locality; +import com.openlocationcode.android.search.SearchContract; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.google.openlocationcode.OpenLocationCode; + +public class CodeView extends LinearLayout implements CodeContract.View, SearchContract.TargetView { + + private final TextView mCodeTV; + private final TextView mLocalityTV; + private final ImageButton mShareButton; + private final ImageButton mNavigateButton; + private final Resources resources; + private OpenLocationCode lastFullCode; + private boolean localityValid = false; + + public CodeView(Context context, AttributeSet attrs) { + super(context, attrs); + LayoutInflater inflater; + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.code, this, true); + resources = context.getResources(); + + mCodeTV = (TextView) findViewById(R.id.code); + mLocalityTV = (TextView) findViewById(R.id.locality); + mShareButton = (ImageButton) findViewById(R.id.shareCodeButton); + mNavigateButton = (ImageButton) findViewById(R.id.navigateButton); + } + + public String getCodeAndLocality() { + if (!localityValid) { + return lastFullCode.getCode(); + } + + return mCodeTV.getText().toString() + " " + mLocalityTV.getText().toString(); + } + + public OpenLocationCode getLastFullCode() { + return lastFullCode; + } + + public ImageButton getShareButton() { + return mShareButton; + } + + public ImageButton getNavigateButton() { + return mNavigateButton; + } + + @Override + public void displayCode(OpenLocationCode code) { + lastFullCode = code; + // Display the code but remove the first four digits. + mCodeTV.setText(code.getCode().substring(4)); + // Try to append a locality. If we don't have one, get the unknown string. + try { + mLocalityTV.setText(Locality.getNearestLocality(code)); + localityValid = true; + } catch (Locality.NoLocalityException e) { + mLocalityTV.setText(resources.getString(R.string.no_locality)); + localityValid = false; + } + } + + @Override + public void showSearchCode(OpenLocationCode code) { + displayCode(code); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/code/OpenLocationCodeUtil.java b/android_demo/android/src/main/java/com/openlocationcode/android/code/OpenLocationCodeUtil.java new file mode 100644 index 00000000..601cebf6 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/code/OpenLocationCodeUtil.java @@ -0,0 +1,103 @@ +/* + * 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.openlocationcode.android.code; + +import android.support.annotation.Nullable; +import android.util.Log; + +import com.openlocationcode.android.localities.Locality; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.openlocationcode.OpenLocationCode; + +/** + * Util functions related to OpenLocationCode + */ +public class OpenLocationCodeUtil { + private static final String TAG = OpenLocationCodeUtil.class.getSimpleName(); + + private static final int CODE_LENGTH_TO_GENERATE = 11; + + // Pattern to split the search string into the OLC code and the locality name. + private static final Pattern SPLITTER_PATTERN = Pattern.compile("^(\\S+)\\s+(\\S.+)"); + + public static OpenLocationCode createOpenLocationCode(double latitude, double longitude) { + return new OpenLocationCode(latitude, longitude, CODE_LENGTH_TO_GENERATE); + } + + /** + * @param searchStr An OLC code (full or short) followed by an optional locality. + * @param latitude A latitude to use to complete the code if it was short and no locality was + * provided. + * @param longitude A longitude to use to complete the code if it was short and no locality was + * provided. + * Only localities we can look up can be used. + */ + @Nullable + public static OpenLocationCode getCodeForSearchString( + String searchStr, double latitude, double longitude) { + try { + // Split the search string into a code and optional locality. + String code = null; + String locality = null; + + if (!searchStr.matches(".*\\s+.*")) { + // No whitespace, treat it all as a code. + code = searchStr; + } else { + Matcher matcher = SPLITTER_PATTERN.matcher(searchStr); + if (matcher.find()) { + code = matcher.group(1); + locality = matcher.group(2); + } + } + if (code == null) { + return null; + } + OpenLocationCode searchCode = new OpenLocationCode(code); + // If the code is full, we're done. + if (searchCode.isFull()) { + Log.i(TAG, "Code is full, we're done"); + return searchCode; + } + // If we have a valid locality, use that to complete the code. + if (locality != null && !Locality.getLocalityCode(locality).isEmpty()) { + OpenLocationCode localityCode = + new OpenLocationCode(Locality.getLocalityCode(locality)); + Log.i( + TAG, + String.format( + "Got locality %s: locality code: %s", + locality, + localityCode.getCode())); + OpenLocationCode.CodeArea localityArea = localityCode.decode(); + return searchCode.recover( + localityArea.getCenterLatitude(), localityArea.getCenterLongitude()); + + } + // Use the passed latitude and longitude to complete the code. + Log.i(TAG, "Using passed location to complete code"); + return searchCode.recover(latitude, longitude); + } catch (IllegalArgumentException e) { + // Invalid code + return null; + } + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/current/GoogleApiModule.java b/android_demo/android/src/main/java/com/openlocationcode/android/current/GoogleApiModule.java new file mode 100644 index 00000000..79e3c93e --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/current/GoogleApiModule.java @@ -0,0 +1,81 @@ +/* + * 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.openlocationcode.android.current; + +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.LocationServices; + +import android.content.Context; +import android.hardware.SensorManager; +import android.location.LocationManager; +import android.view.Display; +import android.view.WindowManager; + +import dagger.Module; +import dagger.Provides; + +import javax.inject.Singleton; + +@Module +public class GoogleApiModule { + + private final Context mContext; + + public GoogleApiModule(Context context) { + this.mContext = context; + } + + @Provides + @Singleton + public GoogleApiClient provideGoogleApiClient() { + return new GoogleApiClient.Builder(mContext).addApi(LocationServices.API).build(); + } + + @Provides + @Singleton + public GoogleApiAvailability provideGoogleApiAvailability() { + return GoogleApiAvailability.getInstance(); + } + + @SuppressWarnings("SameReturnValue") + @Provides + @Singleton + public FusedLocationProviderApi provideFusedLocationProviderApi() { + return LocationServices.FusedLocationApi; + } + + @Provides + @Singleton + public LocationManager provideLocationManager() { + return (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE); + } + + @Provides + @Singleton + public SensorManager provideSensorManager() { + return (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + } + + @Provides + @Singleton + public Display provideDisplayManager() { + return ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay(); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationProvider.java b/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationProvider.java new file mode 100644 index 00000000..6aa94ec8 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationProvider.java @@ -0,0 +1,350 @@ +/* + * 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.openlocationcode.android.current; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.IntentSender; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.Display; +import android.view.Surface; + +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.location.FusedLocationProviderApi; +import com.google.android.gms.location.LocationAvailability; +import com.google.android.gms.location.LocationRequest; +import com.google.auto.factory.AutoFactory; +import com.google.auto.factory.Provided; + +public class LocationProvider + implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, + com.google.android.gms.location.LocationListener, SensorEventListener { + + public interface LocationCallback { + + /** + * Will never return a null location + **/ + void handleNewLocation(Location location); + + void handleNewBearing(float bearing); + + void handleLocationNotAvailable(); + } + + private static final String TAG = LocationProvider.class.getSimpleName(); + + private static final int CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000; + + private static final int INTERVAL_IN_MS = 10 * 1000; + + private static final int FASTEST_INTERVAL_IN_MS = 1000; + + private static final float MIN_BEARING_DIFF = 2.0f; + + private final GoogleApiAvailability mGoogleApiAvailability; + + private final GoogleApiClient mGoogleApiClient; + + private final FusedLocationProviderApi mFusedLocationProviderApi; + + private final LocationManager mLocationManager; + + private final Context mContext; + + private final LocationCallback mLocationCallback; + + private final LocationRequest mLocationRequest; + + private final LocationListener mNetworkLocationListener; + + private final LocationListener mGpsLocationListener; + + private Location mCurrentBestLocation; + + private boolean mUsingGms = false; + + private final SensorManager mSensorManager; + + private final Display mDisplay; + + private int mAxisX, mAxisY; + + private Float mBearing; + + @AutoFactory + public LocationProvider( + @Provided GoogleApiAvailability googleApiAvailability, + @Provided GoogleApiClient googleApiClient, + @Provided FusedLocationProviderApi fusedLocationProviderApi, + @Provided LocationManager locationManager, + @Provided SensorManager sensorManager, + @Provided Display displayManager, + Context context, + LocationCallback locationCallback) { + this.mGoogleApiAvailability = googleApiAvailability; + this.mGoogleApiClient = googleApiClient; + this.mFusedLocationProviderApi = fusedLocationProviderApi; + this.mLocationManager = locationManager; + this.mContext = context; + this.mLocationCallback = locationCallback; + this.mSensorManager = sensorManager; + this.mDisplay = displayManager; + mLocationRequest = + LocationRequest.create() + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) + .setInterval(INTERVAL_IN_MS) + .setFastestInterval(FASTEST_INTERVAL_IN_MS); + mNetworkLocationListener = createLocationListener(); + mGpsLocationListener = createLocationListener(); + + determineIfUsingGms(); + if (isUsingGms()) { + mGoogleApiClient.registerConnectionCallbacks(this); + mGoogleApiClient.registerConnectionFailedListener(this); + } + } + + private boolean isUsingGms() { + return mUsingGms; + } + + public void connect() { + if (isUsingGms()) { + if (mGoogleApiClient.isConnected()) { + onConnected(new Bundle()); + } else { + mGoogleApiClient.connect(); + } + } else { + connectUsingOldApi(); + } + + Sensor mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); + mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_NORMAL * 5); + } + + public void disconnect() { + if (isUsingGms() && mGoogleApiClient.isConnected()) { + mFusedLocationProviderApi.removeLocationUpdates(mGoogleApiClient, this); + mGoogleApiClient.disconnect(); + } else if (!isUsingGms()) { + disconnectUsingOldApi(); + } + mSensorManager.unregisterListener(this); + } + + @Override + public void onConnected(Bundle bundle) { + Log.i(TAG, "Connected to location services"); + + LocationAvailability locationAvailability = + mFusedLocationProviderApi.getLocationAvailability(mGoogleApiClient); + if (!locationAvailability.isLocationAvailable()) { + mLocationCallback.handleLocationNotAvailable(); + return; + } + + Location lastKnownLocation = mFusedLocationProviderApi.getLastLocation(mGoogleApiClient); + mFusedLocationProviderApi.requestLocationUpdates(mGoogleApiClient, mLocationRequest, this); + if (lastKnownLocation != null) { + Log.i(TAG, "Received last known location: " + lastKnownLocation); + mCurrentBestLocation = lastKnownLocation; + if (mBearing != null) { + mCurrentBestLocation.setBearing(mBearing); + } + mLocationCallback.handleNewLocation(mCurrentBestLocation); + } + } + + @Override + public void onConnectionSuspended(int i) { + } + + @Override + public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { + if (connectionResult.hasResolution() && mContext instanceof Activity) { + try { + Activity activity = (Activity) mContext; + connectionResult.startResolutionForResult( + activity, CONNECTION_FAILURE_RESOLUTION_REQUEST); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, e.getMessage()); + } + } else { + Log.i( + TAG, + "Location services connection failed with code: " + + connectionResult.getErrorCode()); + connectUsingOldApi(); + } + } + + @Override + public void onLocationChanged(Location location) { + if (location == null) { + return; + } + mCurrentBestLocation = location; + if (mBearing != null) { + mCurrentBestLocation.setBearing(mBearing); + } + mLocationCallback.handleNewLocation(mCurrentBestLocation); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + if (sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { + Log.i(TAG, "Rotation sensor accuracy changed to: " + accuracy); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + float rotationMatrix[] = new float[16]; + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values); + float[] orientationValues = new float[3]; + readDisplayRotation(); + SensorManager.remapCoordinateSystem(rotationMatrix, mAxisX, mAxisY, rotationMatrix); + SensorManager.getOrientation(rotationMatrix, orientationValues); + double azimuth = Math.toDegrees(orientationValues[0]); + // Azimuth values are now -180-180 (N=0), but once added to the location object + // they become 0-360 (N=0). + @SuppressLint("UseValueOf") Float newBearing = new Float(azimuth); + if (mBearing == null || Math.abs(mBearing - newBearing) > MIN_BEARING_DIFF) { + mBearing = newBearing; + if (mCurrentBestLocation != null) { + mCurrentBestLocation.setBearing(mBearing); + } + mLocationCallback.handleNewBearing(mBearing); + } + } + + private void determineIfUsingGms() { + // Possible returned status codes can be found at + // https://developers.google.com/android/reference/com/google/android/gms/common/GoogleApiAvailability + int statusCode = mGoogleApiAvailability.isGooglePlayServicesAvailable(mContext); + if (statusCode == ConnectionResult.SUCCESS + || statusCode == ConnectionResult.SERVICE_UPDATING) { + mUsingGms = true; + } + } + + private void connectUsingOldApi() { + Location lastKnownGpsLocation = + mLocationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + Location lastKnownNetworkLocation = + mLocationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + Location bestLastKnownLocation = mCurrentBestLocation; + if (lastKnownGpsLocation != null + && LocationUtil.isBetterLocation(lastKnownGpsLocation, bestLastKnownLocation)) { + bestLastKnownLocation = lastKnownGpsLocation; + } + if (lastKnownNetworkLocation != null + && LocationUtil.isBetterLocation( + lastKnownNetworkLocation, bestLastKnownLocation)) { + bestLastKnownLocation = lastKnownNetworkLocation; + } + mCurrentBestLocation = bestLastKnownLocation; + + if (mLocationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER) + && mLocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + mLocationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, FASTEST_INTERVAL_IN_MS, 0.0f, + mGpsLocationListener); + } + if (mLocationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER) + && mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + mLocationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, FASTEST_INTERVAL_IN_MS, 0.0f, + mNetworkLocationListener); + } + if (bestLastKnownLocation != null) { + Log.i(TAG, "Received last known location via old API: " + bestLastKnownLocation); + if (mBearing != null) { + bestLastKnownLocation.setBearing(mBearing); + } + mLocationCallback.handleNewLocation(bestLastKnownLocation); + } + } + + private void disconnectUsingOldApi() { + mLocationManager.removeUpdates(mGpsLocationListener); + mLocationManager.removeUpdates(mNetworkLocationListener); + } + + private LocationListener createLocationListener() { + return new LocationListener() { + public void onLocationChanged(Location location) { + if (LocationUtil.isBetterLocation(location, mCurrentBestLocation)) { + mCurrentBestLocation = location; + if (mBearing != null) { + mCurrentBestLocation.setBearing(mBearing); + } + mLocationCallback.handleNewLocation(location); + } + } + + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + public void onProviderEnabled(String provider) { + } + + public void onProviderDisabled(String provider) { + } + }; + } + + /** + * Read the screen rotation so we can correct the device heading. + */ + @SuppressWarnings("SuspiciousNameCombination") + private void readDisplayRotation() { + mAxisX = SensorManager.AXIS_X; + mAxisY = SensorManager.AXIS_Y; + switch (mDisplay.getRotation()) { + case Surface.ROTATION_0: + break; + case Surface.ROTATION_90: + mAxisX = SensorManager.AXIS_Y; + mAxisY = SensorManager.AXIS_MINUS_X; + break; + case Surface.ROTATION_180: + mAxisY = SensorManager.AXIS_MINUS_Y; + break; + case Surface.ROTATION_270: + mAxisX = SensorManager.AXIS_MINUS_Y; + mAxisY = SensorManager.AXIS_X; + break; + default: + break; + } + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationProviderFactoryComponent.java b/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationProviderFactoryComponent.java new file mode 100644 index 00000000..681fb30c --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationProviderFactoryComponent.java @@ -0,0 +1,27 @@ +/* + * 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.openlocationcode.android.current; + +import dagger.Component; + +import javax.inject.Singleton; + +@Component(modules = {GoogleApiModule.class}) +@Singleton +public interface LocationProviderFactoryComponent { + LocationProviderFactory locationProviderFactory(); +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationUtil.java b/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationUtil.java new file mode 100644 index 00000000..211a76ba --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/current/LocationUtil.java @@ -0,0 +1,85 @@ +/* + * 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.openlocationcode.android.current; + +import android.location.Location; + +/** + * Contains util functions related to determining location. + */ +class LocationUtil { + private static final int TWO_MINUTES = 1000 * 60 * 2; + + /** + * Determines whether one Location reading is better than the current Location fix. + * Copied from http://developer.android.com/guide/topics/location/strategies.html + * + * @param location The new Location that you want to evaluate + * @param currentBestLocation The current Location fix, to which you want to compare the new one + */ + public static boolean isBetterLocation(Location location, Location currentBestLocation) { + if (currentBestLocation == null) { + // A new location is always better than no location + return true; + } + + // Check whether the new location fix is newer or older + long timeDelta = location.getTime() - currentBestLocation.getTime(); + boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; + boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; + boolean isNewer = timeDelta > 0; + + // If it's been more than two minutes since the current location, use the new location + // because the user has likely moved + if (isSignificantlyNewer) { + return true; + // If the new location is more than two minutes older, it must be worse + } else if (isSignificantlyOlder) { + return false; + } + + // Check whether the new location fix is more or less accurate + int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); + boolean isLessAccurate = accuracyDelta > 0; + boolean isMoreAccurate = accuracyDelta < 0; + boolean isSignificantlyLessAccurate = accuracyDelta > 200; + + // Check if the old and new location are from the same provider + boolean isFromSameProvider = + isSameProvider(location.getProvider(), currentBestLocation.getProvider()); + + // Determine location quality using a combination of timeliness and accuracy + if (isMoreAccurate) { + return true; + } else if (isNewer && !isLessAccurate) { + return true; + } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { + return true; + } + return false; + } + + /** + * Checks whether two providers are the same + */ + private static boolean isSameProvider(String provider1, String provider2) { + if (provider1 == null) { + return provider2 == null; + } + return provider1.equals(provider2); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/direction/Direction.java b/android_demo/android/src/main/java/com/openlocationcode/android/direction/Direction.java new file mode 100644 index 00000000..a2e04d49 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/direction/Direction.java @@ -0,0 +1,87 @@ +/* + * 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.openlocationcode.android.direction; + +import com.google.openlocationcode.OpenLocationCode; + +import java.util.Locale; + +/** + * Immutable model representing the direction between two given locations. It contains the distance, + * the initial and the final bearing. To understand the meaning of initial and final bearing, refer + * to http://www.onlineconversion.com/map_greatcircle_bearings.htm. + */ +public class Direction { + + private final int mDistanceInMeter; + + private final float mInitialBearing; + + private final OpenLocationCode mFromCode; + + private final OpenLocationCode mToCode; + + public Direction( + OpenLocationCode fromCode, + OpenLocationCode toCode, + float distanceInMeter, + float initialBearing) { + mDistanceInMeter = (int) distanceInMeter; + mInitialBearing = initialBearing; + mFromCode = fromCode; + mToCode = toCode; + } + + /** + * @return Bearing in degrees East of true North. + */ + public float getInitialBearing() { + return mInitialBearing; + } + + /** + * @return Distance in meter. + */ + public int getDistance() { + return mDistanceInMeter; + } + + /** + * @return The code representing the origin location. + */ + public OpenLocationCode getFromCode() { + return mFromCode; + } + + /** + * @return The code representing the destination location. + */ + public OpenLocationCode getToCode() { + return mToCode; + } + + @Override + public String toString() { + return String.format( + Locale.US, + "Direction from code %s to %s, distance %d, initial bearing %f", + mFromCode, + mToCode, + mDistanceInMeter, + mInitialBearing); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionContract.java b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionContract.java new file mode 100644 index 00000000..9f2d9bbd --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionContract.java @@ -0,0 +1,41 @@ +/* + * 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.openlocationcode.android.direction; + +/** + * The contract for the direction feature. The direction shows the direction (bearing) and the + * distance of a code, compared to the user current location. + */ +public interface DirectionContract { + + interface View { + + void showDirection(float degreesFromNorth); + + void showDistance(int distanceInMeters); + } + + interface ActionsListener { + + /** + * Call this when the user current location or the code currently shown to the user is + * updated. + */ + void directionUpdated(Direction direction); + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionPresenter.java b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionPresenter.java new file mode 100644 index 00000000..28962e14 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionPresenter.java @@ -0,0 +1,32 @@ +/* + * 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.openlocationcode.android.direction; + +public class DirectionPresenter implements DirectionContract.ActionsListener { + + private final DirectionView mView; + + public DirectionPresenter(DirectionView view) { + mView = view; + } + + @Override + public void directionUpdated(Direction direction) { + mView.showDirection(direction.getInitialBearing()); + mView.showDistance(direction.getDistance()); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionUtil.java b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionUtil.java new file mode 100644 index 00000000..82a6855e --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionUtil.java @@ -0,0 +1,74 @@ +/* + * 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.openlocationcode.android.direction; + +import android.location.Location; + +import com.google.openlocationcode.OpenLocationCode; +import com.google.openlocationcode.OpenLocationCode.CodeArea; + +import com.openlocationcode.android.code.OpenLocationCodeUtil; + +/** + * Util functions related to direction. + */ +public class DirectionUtil { + + /** + * This computes a direction between {@code fromLocation} and a + * {@code destinationCode}. The computation is done using + * {@link Location#distanceBetween(double, double, double, double, float[])}. + * + * @param fromLocation The user position. + * @param destinationCode The code to compute the direction to. + * @return the {@link Direction} + */ + public static Direction getDirection(Location fromLocation, OpenLocationCode destinationCode) { + CodeArea destinationArea = destinationCode.decode(); + double toLatitude = destinationArea.getCenterLatitude(); + double toLongitude = destinationArea.getCenterLongitude(); + float[] results = new float[3]; + Location.distanceBetween( + fromLocation.getLatitude(), + fromLocation.getLongitude(), + toLatitude, + toLongitude, + results); + + // The device bearing in the location object is 0-360, the value returned from + // distanceBetween is -180 to 180. Adjust the device bearing to be in the same range. + float deviceBearing = fromLocation.getBearing(); + if (deviceBearing > 180) { + deviceBearing = deviceBearing - 360; + } + + // Compensate the initial bearing for the device bearing. + results[1] = results[1] - deviceBearing; + if (results[1] > 180) { + results[1] = -360 + results[1]; + } else if (results[1] < -180) { + results[1] = 360 + results[1]; + } + return new Direction( + OpenLocationCodeUtil.createOpenLocationCode( + fromLocation.getLatitude(), fromLocation.getLongitude()), + destinationCode, + results[0], + results[1]); + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionView.java b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionView.java new file mode 100644 index 00000000..1a0729b8 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/direction/DirectionView.java @@ -0,0 +1,82 @@ +/* + * 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.openlocationcode.android.direction; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.openlocationcode.android.R; + +public class DirectionView extends LinearLayout implements DirectionContract.View { + + private static final long ANIMATION_DURATION_MS = 500; + + // Gives the current heading of the direction indicator. + // This is continuous with 0 as north and 180/-180 as south. + // This avoids the animation "spinning" when crossing a boundary. + private float mDirectionCurrentRotation = 0f; + + public DirectionView(Context context, AttributeSet attrs) { + super(context, attrs); + LayoutInflater inflater; + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.direction, this, true); + } + + @Override + public void showDirection(float degreesFromNorth) { + ImageView iv = (ImageView) findViewById(R.id.direction_indicator); + float correctedCurrent = ((180 + mDirectionCurrentRotation) % 360) - 180; + if (correctedCurrent < -180) { + correctedCurrent = 360 + correctedCurrent; + } else if (correctedCurrent > 180) { + correctedCurrent = correctedCurrent - 360; + } + + float relativeRotation = degreesFromNorth - correctedCurrent; + if (relativeRotation < -180) { + relativeRotation = 360 + relativeRotation; + } else if (relativeRotation > 180) { + relativeRotation = -360 + relativeRotation; + } + mDirectionCurrentRotation = mDirectionCurrentRotation + relativeRotation; + iv.animate().rotation(mDirectionCurrentRotation).setDuration(ANIMATION_DURATION_MS).start(); + } + + @Override + public void showDistance(int distanceInMeters) { + TextView tv = (TextView) findViewById(R.id.distance); + + if (distanceInMeters < 1000) { + tv.setText(String.format(getResources().getString(R.string.distance_meters), + distanceInMeters)); + } else if (distanceInMeters < 3000) { + double distanceInKm = distanceInMeters / 1000.0; + tv.setText(String.format(getResources().getString(R.string.distance_few_kilometers), + distanceInKm)); + } else { + double distanceInKm = distanceInMeters / 1000.0; + tv.setText(String.format(getResources().getString(R.string.distance_many_kilometers), + distanceInKm)); + } + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/localities/Locality.java b/android_demo/android/src/main/java/com/openlocationcode/android/localities/Locality.java new file mode 100644 index 00000000..f05ee223 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/localities/Locality.java @@ -0,0 +1,372 @@ +/* + * 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.openlocationcode.android.localities; + +import android.support.annotation.NonNull; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.google.openlocationcode.OpenLocationCode; +import com.google.openlocationcode.OpenLocationCode.CodeArea; + +/** + * Return the nearest locality within 0.4 degrees latitude and longitude. + */ +public class Locality { + // Localities more than this distance in either latitude or longitude are too far away. + private static final double MAX_DISTANCE_DEGREES = 0.4; + + private static final String TAG = "Locality"; + + /** + * Thrown when no locality nearby exists. + */ + public static final class NoLocalityException extends Exception { + } + + /** + * Returns the locality name nearest to the passed location or throws + * a NoLocalityException if none are near enough to be used. + */ + public static String getNearestLocality(OpenLocationCode location) throws NoLocalityException { + String nearestLocality = null; + double distanceDegrees = Float.MAX_VALUE; + CodeArea locationArea = location.decode(); + // Scan through the localities to find the nearest. + for (int i = 0; i < LOCALITIES.size(); i++) { + String localityEntry = LOCALITIES.get(i); + String[] parts = localityEntry.split(":"); + OpenLocationCode localityCode = new OpenLocationCode(parts[0]); + CodeArea localityArea = localityCode.decode(); + double localityDistanceDegrees = Math.max( + Math.abs(localityArea.getCenterLatitude() - locationArea.getCenterLatitude()), + Math.abs(localityArea.getCenterLongitude() - locationArea.getCenterLongitude()) + ); + if (localityDistanceDegrees >= MAX_DISTANCE_DEGREES) { + continue; + } + if (localityDistanceDegrees < distanceDegrees) { + nearestLocality = parts[1]; + distanceDegrees = localityDistanceDegrees; + } + } + if (nearestLocality == null) { + throw new NoLocalityException(); + } + return nearestLocality; + } + + /** + * @param localitySearch The locality name to search for. + * @return the locality full code, or an empty string if not found. + */ + public static String getLocalityCode(@NonNull String localitySearch) { + for (String locality : LOCALITIES) { + if (locality.subSequence(10, locality.length()).equals(localitySearch)) { + return locality.subSequence(0, 9).toString(); + } + } + return ""; + } + + /** + * Note: The search for currentLocation and mapLocation locality is done locally only. + * + * @param currentLocation If not null, the locality of the {@code currentLocation} will appear + * first in the list + * @param mapLocation If not null, the locality of the {@code mapLocation} will appear next + * in the list + * @return the list of localities to show in the search suggestions + */ + public static List getAllLocalitiesForSearchDisplay( + OpenLocationCode currentLocation, OpenLocationCode mapLocation) { + List allLocalities = new ArrayList<>(LOCALITIES_WITHOUT_CODE_ALPHABETICAL); + if (mapLocation != null) { + String mapLocationLocality; + try { + mapLocationLocality = getNearestLocality(mapLocation); + int index = allLocalities.indexOf(mapLocationLocality); + allLocalities.remove(index); + allLocalities.add(0, mapLocationLocality); + } catch (NoLocalityException e) { + Log.d(TAG, "map not centered on CV"); + } + } + if (currentLocation != null) { + String currentLocationLocality; + try { + currentLocationLocality = getNearestLocality(currentLocation); + int index = allLocalities.indexOf(currentLocationLocality); + allLocalities.remove(index); + allLocalities.add(0, currentLocationLocality); + } catch (NoLocalityException e) { + Log.d(TAG, "current location not in CV"); + } + } + return allLocalities; + } + + // List of OLC codes and locality names. This must be sorted by OLC code. + private static final List LOCALITIES = Arrays.asList( + "796QR7P7+:Lomba Tantum", + "796QR7R7+:Palhal", + "796QR7XH+:Campo Baixo", + "796QR8P4+:Cachaço", + "796QRJXV+:Fonte Aleixo", + "796QV72G+:Escovinha", + "796QV74J+:Nossa Senhora do Monte", + "796QV77Q+:Cova de Joana", + "796QV7C9+:Fajã de Água", + "796QV7CW+:Cova Rodela", + "796QV884+:Mato Grande", + "796QV8C4+:Nova Sintra", + "796QV8F7+:Santa Bárbara", + "796QV8G2+:Lem", + "796QV8Q9+:Furna", + "796QVGRX+:Forno", + "796QVGW4+:São Filipe", + "796QVH7J+:Salto", + "796QVHFG+:Patim", + "796QVHH6+:Penteada", + "796QVJ45+:Achada Poio", + "796QVJC8+:Monte Largo", + "796QVJFR+:Achada Furna", + "796QVM4F+:Dacabalaio", + "796QVMCR+:Figueira Pavão", + "796QVMRP+:Mae Joana", + "796QVMWH+:Estancia Rogue", + "796QVPQ4+:Cova Figueira", + "796QWGCX+:Lagariça", + "796QWGGF+:Tongon", + "796QWGJQ+:Cerrado", + "796QWGQJ+:Logar Novo", + "796QWHV5+:Nhuco", + "796QWPH3+:Tinteira", + "796QX896+:Ilhéu Grande", + "796QXG8V+:Italiano", + "796QXHH4+:Mira-Mira", + "796QXMQW+:Achada Grande", + "796QXPF3+:Relva", + "796RW8VP+:Porto Gouveia", + "796RW98W+:Cidade Velha", + "796RWCCH+:Costa D' Achada", + "796RWCXQ+:Dias", + "796RWFMP+:Praia", + "796RX828+:Porto Mosquito", + "796RX9Q6+:Santa Ana", + "796RXC5Q+:Trindade", + "796RXC69+:João Varela", + "796RXCGM+:Ribeirinha", + "796RXF8G+:Sao Filipe", + "796RXFM2+:Cambujana", + "796RXFM6+:Veneza", + "796RXFW6+:Achada Venteiro", + "796RXG3F+:Sao Tome", + "796RXGH5+:São Francisco", + "797Q2H22+:Galinheiros", + "797Q2H7F+:São Jorge", + "797Q2H8R+:Campanas de Baixo", + "797Q2JG6+:Atalaia", + "797Q2JQF+:Ribeira Ilhéu", + "797Q2M2W+:Corvo", + "797Q2MCP+:Fonsaco", + "797Q2MJF+:Mosteiros", + "797Q2MV5+:Fajãzinha", + "797R28RX+:Pico Leao", + "797R2CHP+:São Domingos", + "797R2CP2+:Rui Vaz", + "797R2F5C+:Curral Grande", + "797R2FHJ+:Milho Branco", + "797R2GJ7+:Portal", + "797R2GM4+:Capela", + "797R2GQC+:Cancelo", + "797R2GX2+:Mato Jorge", + "797R2HC2+:Moia Moia", + "797R366M+:Ponta Rincão", + "797R37PV+:Achada Gregorio", + "797R37VV+:Chao de Tanque", + "797R38G2+:Palha Carga", + "797R38XH+:Assomada", + "797R393X+:São Jorge dos Órgãos", + "797R39J7+:Picos", + "797R3CHP+:Ribeira Seca", + "797R3CP2+:Galeao", + "797R3CQG+:Liberao", + "797R3F77+:Cruz do Gato", + "797R3FH9+:Salas", + "797R3FHC+:Joao Teves", + "797R3FPC+:Renque Purga", + "797R3FQ2+:Poilao", + "797R3G8C+:Praia Baixo", + "797R46PV+:Ribeira da Barca", + "797R476G+:Tomba Toiro", + "797R476R+:Ribao Manuel", + "797R48GH+:Boa Entrada", + "797R4988+:Jalalo", + "797R499J+:Tribuna", + "797R49FR+:Rebelo", + "797R49W5+:Saltos de Cima", + "797R49XP+:Saltos de Baixo", + "797R4C4C+:Cudelho", + "797R4CG2+:Serelbo", + "797R4CM3+:Toril", + "797R4CMR+:Santa Cruz", + "797R4F9J+:Achada Fazenda", + "797R4FP8+:Pedra Badejo", + "797R4QQV+:Vila do Maio", + "797R4RPQ+:Barreiro", + "797R4VVH+:Ribeira Dom João", + "797R57M9+:Figueira da Naus", + "797R57R7+:Figueira das Naus", + "797R584P+:Joao Dias", + "797R5872+:Fundura", + "797R5967+:Flamengos", + "797R59J4+:Ribeira de Sao Miquel", + "797R59W8+:Pilao Cao", + "797R5C2Q+:Cancelo", + "797R5CP5+:Calheta de São Miguel", + "797R5QHG+:Morro", + "797R5R6X+:Figueira da Horta", + "797R67H4+:Ribeira da Prata", + "797R67QC+:Milho Branco", + "797R683H+:Ribeira Principal", + "797R6896+:Lagoa", + "797R68R7+:Massa Pe", + "797R68VX+:Achada Tenda", + "797R69J4+:Mangue de Setes Ribeiras", + "797R6QHP+:Calheta", + "797R6V5W+:Pilão Cão", + "797R6V8W+:Alcatraz", + "797R6VXM+:Pedro Vaz", + "797R7765+:Chão Bom", + "797R77G2+:Tarrafal", + "797R77VP+:Trás os Montes", + "797R7845+:Biscainhos", + "797R7Q8W+:Morrinho", + "797R7RCG+:Cascabulho", + "797R7V8P+:Santo António", + "797VX6P6+:Curral Velho", + "798PRWJQ+:São Pedro", + "798PVXGH+:Lazareto", + "798PXM5R+:Tarrafal de Monte Trigo", + "798PXQ97+:João Daninha", + "798PXQ9P+:Água das Fortes", + "798PXQWJ+:Lombo de Torre", + "798PXRW2+:Baboso", + "798PXV93+:Pedrinha", + "798QHJ7W+:Tarrafal de São Nicolau", + "798QHJGQ+:Escada", + "798QHP79+:Preguiça", + "798QHPWV+:Cruz de Baixo", + "798QHWQ2+:Urzuleiros", + "798QHWX5+:Jalunga", + "798QHXM5+:Castilhiano", + "798QJC87+:Ilhéu Raso", + "798QJH2Q+:Barril", + "798QJJV5+:Praia Branca", + "798QJM2V+:Cabeçalinho", + "798QJM3X+:Calejão", + "798QJMC7+:Cachaço", + "798QJMR7+:Fajã de Cima", + "798QJMVM+:Queimada", + "798QJMXG+:Fajã de Baixo", + "798QJMXW+:Carvoeiro", + "798QJP83+:Ribeira Brava", + "798QJQJC+:Belém", + "798QJRJ4+:Morro Brás", + "798QJV79+:Juncalinho", + "798QM84H+:Ilhéu Branco", + "798QMJ5P+:Ribeira da Prata", + "798QMM9H+:Estância de Brás", + "798QMMF7+:Ribeira Funda", + "798QQ734+:Ponta da Cruz", + "798QQ774+:Ilha de Santa Luzia", + "798QR2VM+:Ermida", + "798QR3J9+:Madeiral", + "798QV2G9+:Mindelo", + "798QV43P+:Ribeira de Calhau", + "798QW333+:Salamansa", + "798QW33R+:Baía das Gatas", + "798V23PM+:Povoação Velha", + "798V27G3+:João Barrosa", + "798V358M+:São Jorge", + "798V35WR+:Ilha da Boa Vista", + "798V44J7+:Rabil", + "798V4588+:Amador", + "798V47FG+:Cabeça dos Tarrafes", + "798V47QF+:Fundo das Figueiras", + "798V53MP+:Sal Rei", + "798V54JX+:Ponta Adiante", + "798V55PG+:Bofareira", + "798V5724+:João Galego", + "798V57WH+:Gata", + "798V6652+:Espingueira", + "798V66F3+:Derrubado", + "798VJ32Q+:Santa Maria", + "798VM3H8+:Murdeira", + "798VQ25C+:Palmeira", + "798VQ32X+:Feijoal", + "798VQ346+:Espargos", + "798VQ474+:Pedra Lume", + "799P2MHH+:Monte Trigo", + "799P2QRV+:Cha de Morte", + "799P2QVR+:Curral das Vacas", + "799P2QXH+:Cirio", + "799P2R42+:Leiro", + "799P2WCM+:Porto Novo", + "799P3MPP+:Bonita", + "799P3PHR+:Lascado", + "799P3PM6+:Ribeira Vermellia", + "799P3QMG+:Miguel Pires", + "799P3QR3+:Martiene", + "799P3R6V+:Ribeira Fria", + "799P3RJQ+:Tabuadinha", + "799P3WXM+:Lombo de Figueira", + "799P4Q54+:Ribeira da Cruz", + "799P4QVQ+:Figueiras de Baixo", + "799P4RCQ+:Esgamaleiro", + "799P4VJG+:Caibros de Ribeira de Jorge", + "799P4VXX+:Figueiral de Cima", + "799P4W5J+:Faja de Cima", + "799P4WQ7+:Espongeiro", + "799P4XXM+:Paul", + "799P5R4P+:Chã de Igreja", + "799P5R9M+:Cruzinha", + "799P5V3X+:Dagoio", + "799P5V6J+:Boca de Coruja", + "799P5V7W+:Boca de Curral", + "799P5WJM+:Ribeira Grande", + "799P5XGC+:Sinagoga", + "799P6W24+:Ponta do Sol", + "799Q4282+:Janela"); + + private static final List LOCALITIES_WITHOUT_CODE_ALPHABETICAL = + createLocalitiesWithoutCode(); + + private static List createLocalitiesWithoutCode() { + List allLocalities = new ArrayList<>(); + for (String localityWithCode : LOCALITIES) { + allLocalities.add(localityWithCode.substring(10)); + } + Collections.sort(allLocalities); + return allLocalities; + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/main/MainActivity.java b/android_demo/android/src/main/java/com/openlocationcode/android/main/MainActivity.java new file mode 100644 index 00000000..dd6dbc96 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/main/MainActivity.java @@ -0,0 +1,225 @@ +/* + * 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.openlocationcode.android.main; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.FragmentActivity; +import android.util.Log; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import com.google.android.gms.maps.MapView; +import com.google.android.gms.maps.model.CameraPosition; +import com.openlocationcode.android.R; +import com.openlocationcode.android.code.CodeView; +import com.openlocationcode.android.direction.DirectionView; +import com.openlocationcode.android.map.MyMapView; +import com.openlocationcode.android.search.SearchView; + +/** + * The home {@link android.app.Activity}. All features are implemented in this Activity. The + * features with a UI are code, direction, map, and search. Additionally, the app also obtains the + * current location of the user. + *

+ * The UI features all live in their own package, with a Contract interface defining the methods on + * their view(s) and their action listeners. The action listener interface is implemented via a + * feature Presenter, and the view interface via a {@link android.view.View}. + *
+ * Note that some features have a source and target view, the source view being the UI that the + * user interacts with for that feature, and the target view being a view that needs to update its + * data based on an action in that feature (eg Search result). + *

+ * The MainActivity also has a presenter, the {@link MainPresenter}, which implements the user + * location feature. As some features need to know the user location and the app consists of one + * Activity only, the {@link MainPresenter} is made accessible to the other features via a static + * reference. + */ +public class MainActivity extends FragmentActivity { + + private static final String TAG = MainActivity.class.getSimpleName(); + + private static final String MAP_CAMERA_POSITION_LATITUDE = "map_camera_position_latitude"; + + private static final String MAP_CAMERA_POSITION_LONGITUDE = "map_camera_position_longitude"; + + private static final String MAP_CAMERA_POSITION_ZOOM = "map_camera_position_zoom"; + + private static final String URI_QUERY_SEPARATOR = "q="; + + private static final String URI_ZOOM_SEPARATOR = "&"; + + /** + * As all features are implemented in this activity, a static {@link MainPresenter} allows all + * features to access its data without passing a reference to it to each feature presenter. + */ + private static MainPresenter mMainPresenter; + + // We need to store this because we need to call this at different point in the lifecycle + private MapView mMapView; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_act); + MyMapView myMapView = (MyMapView) findViewById(R.id.myMapView); + mMapView = myMapView.getMapView(); + mMainPresenter = + new MainPresenter( + this, + (SearchView) findViewById(R.id.searchView), + (DirectionView) findViewById(R.id.directionView), + (CodeView) findViewById(R.id.codeView), + myMapView); + + mMapView.onCreate(savedInstanceState); + + // Adjust the map controls and search box top margins to account for the translucent status + // bar. + int statusBarHeight = getStatusBarHeight(this); + LinearLayout mapControl = (LinearLayout) findViewById(R.id.mapControls); + FrameLayout.LayoutParams mapParams = + (FrameLayout.LayoutParams) mapControl.getLayoutParams(); + mapParams.topMargin = mapParams.topMargin + statusBarHeight; + mapControl.setLayoutParams(mapParams); + + RelativeLayout searchBox = (RelativeLayout) findViewById(R.id.searchBox); + FrameLayout.LayoutParams searchParams = + (FrameLayout.LayoutParams) searchBox.getLayoutParams(); + searchParams.topMargin = searchParams.topMargin + statusBarHeight; + searchBox.setLayoutParams(searchParams); + + if (getIntent() != null && Intent.ACTION_VIEW.equals(getIntent().getAction())) { + handleGeoIntent(getIntent()); + } + } + + @Override + public void onResume() { + super.onResume(); + mMapView.onResume(); + + mMainPresenter.loadCurrentLocation(); + } + + @Override + public void onPause() { + super.onPause(); + + mMapView.onPause(); + + mMainPresenter.stopListeningForLocation(); + } + + @Override + protected void onSaveInstanceState(Bundle savedInstanceState) { + CameraPosition currentMapCameraPosition = mMainPresenter.getMapCameraPosition(); + Log.i(TAG, "Saving state"); + if (currentMapCameraPosition != null) { + savedInstanceState.putDouble( + MAP_CAMERA_POSITION_LATITUDE, currentMapCameraPosition.target.latitude); + savedInstanceState.putDouble( + MAP_CAMERA_POSITION_LONGITUDE, currentMapCameraPosition.target.longitude); + savedInstanceState.putFloat(MAP_CAMERA_POSITION_ZOOM, currentMapCameraPosition.zoom); + } + + super.onSaveInstanceState(savedInstanceState); + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + Log.i(TAG, "Restoring state"); + if (savedInstanceState != null) { + double mapCameraPositionLatitude = + savedInstanceState.getDouble(MAP_CAMERA_POSITION_LATITUDE); + double mapCameraPositionLongitude = + savedInstanceState.getDouble(MAP_CAMERA_POSITION_LONGITUDE); + float mapCameraPositionZoom = savedInstanceState.getFloat(MAP_CAMERA_POSITION_ZOOM); + mMainPresenter.setMapCameraPosition( + mapCameraPositionLatitude, mapCameraPositionLongitude, mapCameraPositionZoom); + } + } + + /** + * Handles intent URIs, extracts the query part and sends it to the search function. + *

+ * URIs may be of the form: + *

    + *
  • {@code geo:37.802,-122.41962} + *
  • {@code geo:37.802,-122.41962?q=7C66CM4X%2BC34&z=20} + *
  • {@code geo:0,0?q=WF59%2BX67%20Praia} + *
+ *

+ * Only the query string is used. Coordinates and zoom level are ignored. If the query string + * is not recognised by the search function (say, it's a street address), it will fail. + */ + private void handleGeoIntent(Intent intent) { + Uri uri = intent != null ? intent.getData() : null; + if (uri == null) { + return; + } + String schemeSpecificPart = uri.getEncodedSchemeSpecificPart(); + if (schemeSpecificPart == null || schemeSpecificPart.isEmpty()) { + return; + } + // Get everything after q= + int queryIndex = schemeSpecificPart.indexOf(URI_QUERY_SEPARATOR); + if (queryIndex == -1) { + return; + } + String searchQuery = schemeSpecificPart.substring(queryIndex + 2); + if (searchQuery.contains(URI_ZOOM_SEPARATOR)) { + searchQuery = searchQuery.substring(0, searchQuery.indexOf(URI_ZOOM_SEPARATOR)); + } + final String searchString = Uri.decode(searchQuery); + Log.i(TAG, "Search string is " + searchString); + + // Give the map some time to get ready. + Handler h = new Handler(); + Runnable r = new Runnable() { + @Override + public void run() { + if (mMainPresenter.getSearchActionsListener().searchCode(searchString)) { + mMainPresenter.getSearchActionsListener().setSearchText(searchString); + } + } + }; + h.postDelayed(r, 2000); + } + + public static MainPresenter getMainPresenter() { + return mMainPresenter; + } + + // A method to find height of the status bar + public static int getStatusBarHeight(Context context) { + int result = 0; + int resourceId = context.getResources().getIdentifier( + "status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = context.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/main/MainFragment.java b/android_demo/android/src/main/java/com/openlocationcode/android/main/MainFragment.java new file mode 100644 index 00000000..ed9b0d2c --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/main/MainFragment.java @@ -0,0 +1,41 @@ +/* + * 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.openlocationcode.android.main; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.openlocationcode.android.R; + +/** + * A placeholder fragment containing a simple view. + */ +public class MainFragment extends Fragment { + + public MainFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + return inflater.inflate(R.layout.main_frag, container, false); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/main/MainPresenter.java b/android_demo/android/src/main/java/com/openlocationcode/android/main/MainPresenter.java new file mode 100644 index 00000000..ca740447 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/main/MainPresenter.java @@ -0,0 +1,172 @@ +/* + * 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.openlocationcode.android.main; + +import android.content.Context; +import android.location.Location; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.gms.maps.model.CameraPosition; +import com.openlocationcode.android.R; +import com.openlocationcode.android.code.CodeContract; +import com.openlocationcode.android.code.CodePresenter; +import com.openlocationcode.android.code.CodeView; +import com.openlocationcode.android.current.DaggerLocationProviderFactoryComponent; +import com.openlocationcode.android.current.GoogleApiModule; +import com.openlocationcode.android.current.LocationProvider; +import com.openlocationcode.android.current.LocationProvider.LocationCallback; +import com.openlocationcode.android.current.LocationProviderFactoryComponent; +import com.openlocationcode.android.direction.Direction; +import com.openlocationcode.android.direction.DirectionContract; +import com.openlocationcode.android.direction.DirectionPresenter; +import com.openlocationcode.android.direction.DirectionUtil; +import com.openlocationcode.android.direction.DirectionView; +import com.openlocationcode.android.map.MapContract; +import com.openlocationcode.android.map.MapPresenter; +import com.openlocationcode.android.map.MyMapView; +import com.openlocationcode.android.search.SearchContract; +import com.openlocationcode.android.search.SearchContract.TargetView; +import com.openlocationcode.android.search.SearchPresenter; +import com.openlocationcode.android.search.SearchView; + +import java.util.ArrayList; +import java.util.List; + +import com.google.openlocationcode.OpenLocationCode; + +/** + * This Presenter takes care of obtaining the user current location, as well as synchronising data + * between all the features: search, code, direction, and map. + */ +public class MainPresenter implements LocationCallback { + + private static final String TAG = MainPresenter.class.getSimpleName(); + + private final SearchContract.ActionsListener mSearchActionsListener; + + private final CodeContract.ActionsListener mCodeActionsListener; + + private final DirectionContract.ActionsListener mDirectionActionsListener; + + private final MapContract.ActionsListener mMapActionsListener; + + private final Context mContext; + + private final LocationProvider mLocationProvider; + + private Location mCurrentLocation; + + public MainPresenter( + Context context, + SearchView searchView, + DirectionView directionView, + CodeView codeView, + MyMapView mapView) { + List searchTargetViews = new ArrayList<>(); + searchTargetViews.add(codeView); + searchTargetViews.add(mapView); + mContext = context; + mSearchActionsListener = new SearchPresenter(searchView, searchTargetViews); + searchView.setActionsListener(mSearchActionsListener); + mDirectionActionsListener = new DirectionPresenter(directionView); + mCodeActionsListener = new CodePresenter(codeView); + mMapActionsListener = + new MapPresenter(mapView, mCodeActionsListener, mDirectionActionsListener); + mapView.setListener(mMapActionsListener); + + LocationProviderFactoryComponent locationProviderFactoryComponent = + DaggerLocationProviderFactoryComponent.builder() + .googleApiModule(new GoogleApiModule(context)) + .build(); + mLocationProvider = + locationProviderFactoryComponent.locationProviderFactory().create(context, this); + } + + public void loadCurrentLocation() { + Toast.makeText( + mContext, mContext.getResources().getString(R.string.current_location_loading), + Toast.LENGTH_LONG).show(); + mLocationProvider.connect(); + } + + public void currentLocationUpdated(Location location) { + if (location.hasBearing() && getCurrentOpenLocationCode() != null) { + Direction direction = + DirectionUtil.getDirection(location, getCurrentOpenLocationCode()); + mDirectionActionsListener.directionUpdated(direction); + } + if (mCurrentLocation == null) { + // This is the first location received, so we can move the map to this position. + mMapActionsListener.setMapCameraPosition( + location.getLatitude(), location.getLongitude(), MyMapView.INITIAL_MAP_ZOOM); + } + mCurrentLocation = location; + } + + public void stopListeningForLocation() { + mLocationProvider.disconnect(); + } + + @Override + public void handleNewLocation(Location location) { + Log.i(TAG, "Received new location from LocationProvider: " + location); + currentLocationUpdated(location); + } + + @Override + public void handleNewBearing(float bearing) { + Log.i(TAG, "Received new bearing from LocationProvider: " + bearing); + if (mCurrentLocation != null) { + mCurrentLocation.setBearing(bearing); + currentLocationUpdated(mCurrentLocation); + } + } + + @Override + public void handleLocationNotAvailable() { + Toast.makeText( + mContext, + mContext.getResources().getString(R.string.current_location_error), + Toast.LENGTH_LONG) + .show(); + } + + public Location getCurrentLocation() { + return mCurrentLocation; + } + + private OpenLocationCode getCurrentOpenLocationCode() { + return mCodeActionsListener.getCurrentOpenLocationCode(); + } + + public CameraPosition getMapCameraPosition() { + return mMapActionsListener.getMapCameraPosition(); + } + + public SearchContract.ActionsListener getSearchActionsListener() { + return mSearchActionsListener; + } + + public void setMapCameraPosition(double latitude, double longitude, float zoom) { + mMapActionsListener.setMapCameraPosition(latitude, longitude, zoom); + } + + public MapContract.ActionsListener getMapActionsListener() { + return mMapActionsListener; + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/main/WelcomeActivity.java b/android_demo/android/src/main/java/com/openlocationcode/android/main/WelcomeActivity.java new file mode 100644 index 00000000..bfdc871a --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/main/WelcomeActivity.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.openlocationcode.android.main; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.FragmentActivity; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout.LayoutParams; + +import com.openlocationcode.android.R; + + +/** + * Displays a welcome screen if it has not yet been displayed for the current version code. + */ +public class WelcomeActivity extends FragmentActivity { + private static final String TAG = WelcomeActivity.class.getSimpleName(); + private static final String WELCOME_VERSION_CODE_SHOWN_PREF = "welcome_screen_version_code"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + // Second argument is the default to use if the preference can't be found + int savedVersionCode = prefs.getInt(WELCOME_VERSION_CODE_SHOWN_PREF, 0); + int appVersionCode = 0; + try { + appVersionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode; + } catch (NameNotFoundException nnfe) { + Log.w(TAG, "Exception getting appVersionCode : " + nnfe); + } + + final Intent intent = new Intent(this, MainActivity.class); + final Activity activity = this; + + if (appVersionCode == savedVersionCode) { + activity.startActivity(intent); + activity.finish(); + } else { + Log.i(TAG, "Starting welcome page"); + setContentView(R.layout.welcome); + + // Increase the margin on the image to account for the translucent status bar. + ImageView welcomeImage = (ImageView) findViewById(R.id.welcome_image); + LayoutParams layoutParams = (LayoutParams) welcomeImage.getLayoutParams(); + layoutParams.topMargin = layoutParams.topMargin + MainActivity.getStatusBarHeight(this); + welcomeImage.setLayoutParams(layoutParams); + + Button button = (Button) findViewById(R.id.welcome_button); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + activity.startActivity(intent); + activity.finish(); + } + }); + + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(WELCOME_VERSION_CODE_SHOWN_PREF, appVersionCode); + editor.apply(); + } + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/map/MapContract.java b/android_demo/android/src/main/java/com/openlocationcode/android/map/MapContract.java new file mode 100644 index 00000000..7023f14a --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/map/MapContract.java @@ -0,0 +1,79 @@ +/* + * 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.openlocationcode.android.map; + +import android.location.Location; + +import com.google.openlocationcode.OpenLocationCode; +import com.google.android.gms.maps.model.CameraPosition; + + +/** + * The contract for the map feature. + */ +public interface MapContract { + + interface View { + + void moveMapToLocation(OpenLocationCode code); + + void drawCodeArea(OpenLocationCode code); + + void showSatelliteView(); + + void showRoadView(); + + void setListener(ActionsListener listener); + + CameraPosition getCameraPosition(); + + void setCameraPosition(double latitude, double longitude, float zoom); + + void stopUpdateCodeOnDrag(); + + void startUpdateCodeOnDrag(); + } + + interface ActionsListener { + + void mapChanged(double latitude, double longitude); + + void requestSatelliteView(); + + void requestRoadView(); + + void moveMapToLocation(Location location); + + CameraPosition getMapCameraPosition(); + + void setMapCameraPosition(double latitude, double longitude, float zoom); + + /** + * Call this to stop updating the code feature on dragging the map. This is used by the + * search feature, ie make sure the search result is shown and not cancelled by dragging + * the map. + */ + void stopUpdateCodeOnDrag(); + + /** + * Call this to start updating the code feature on dragging the map, eg when search is + * cancelled. + */ + void startUpdateCodeOnDrag(); + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/map/MapPresenter.java b/android_demo/android/src/main/java/com/openlocationcode/android/map/MapPresenter.java new file mode 100644 index 00000000..50621c0b --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/map/MapPresenter.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.openlocationcode.android.map; + +import android.location.Location; +import android.view.View; +import android.view.View.OnClickListener; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.CameraPosition; + +import com.openlocationcode.android.code.CodeContract; +import com.openlocationcode.android.direction.Direction; +import com.openlocationcode.android.direction.DirectionContract; +import com.openlocationcode.android.main.MainActivity; + +public class MapPresenter implements MapContract.ActionsListener { + + private final MyMapView mView; + + private final CodeContract.ActionsListener mCodeActionsListener; + + private final DirectionContract.ActionsListener mDirectionActionsListener; + + public MapPresenter( + MyMapView view, + CodeContract.ActionsListener codeListener, + DirectionContract.ActionsListener directionActionsListener) { + mView = view; + mCodeActionsListener = codeListener; + mDirectionActionsListener = directionActionsListener; + + mView + .getSatelliteButton() + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + int mapType = mView.getMapType(); + if (mapType == -1) { + // Map is not initialized + return; + } + if (mapType != GoogleMap.MAP_TYPE_HYBRID) { + requestSatelliteView(); + } else { + requestRoadView(); + } + } + }); + + mView + .getMyLocationButton() + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Location currentLocation = + MainActivity.getMainPresenter().getCurrentLocation(); + if (currentLocation == null) { + MainActivity.getMainPresenter().loadCurrentLocation(); + } else { + moveMapToLocation(currentLocation); + } + } + }); + } + + @Override + public void mapChanged(double latitude, double longitude) { + Direction direction = mCodeActionsListener.codeLocationUpdated(latitude, longitude, false); + + if (direction.getToCode() != null) { + mView.drawCodeArea(direction.getToCode()); + } + + mDirectionActionsListener.directionUpdated(direction); + } + + @Override + public void requestSatelliteView() { + mView.showSatelliteView(); + } + + @Override + public void requestRoadView() { + mView.showRoadView(); + } + + @Override + public void moveMapToLocation(Location location) { + if (location != null) { + Direction direction = + mCodeActionsListener.codeLocationUpdated( + location.getLatitude(), location.getLongitude(), true); + mView.moveMapToLocation(direction.getFromCode()); + } + } + + @Override + public CameraPosition getMapCameraPosition() { + return mView.getCameraPosition(); + } + + @Override + public void setMapCameraPosition(double latitude, double longitude, float zoom) { + mView.setCameraPosition(latitude, longitude, zoom); + } + + @Override + public void startUpdateCodeOnDrag() { + mView.startUpdateCodeOnDrag(); + } + + @Override + public void stopUpdateCodeOnDrag() { + mView.stopUpdateCodeOnDrag(); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/map/MyMapView.java b/android_demo/android/src/main/java/com/openlocationcode/android/map/MyMapView.java new file mode 100644 index 00000000..430b1dc9 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/map/MyMapView.java @@ -0,0 +1,240 @@ +/* + * 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.openlocationcode.android.map; + +import com.openlocationcode.android.R; +import com.openlocationcode.android.search.SearchContract; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener; +import com.google.android.gms.maps.MapView; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; + +import android.content.Context; +import android.os.Handler; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.widget.ImageButton; +import android.widget.LinearLayout; + +import com.google.openlocationcode.OpenLocationCode; +import com.google.openlocationcode.OpenLocationCode.CodeArea; + +public class MyMapView extends LinearLayout + implements MapContract.View, OnMapReadyCallback, SearchContract.TargetView { + + private static final String TAG = MyMapView.class.getSimpleName(); + // The zoom level needs to be close enough in to make the red box visible. + public static final float INITIAL_MAP_ZOOM = 19.0f; + // Initial map position is Cape Verde's National Assembly. This will be the center of the map + // view if no known last location is available. + public static final double INITIAL_MAP_LATITUDE = 14.905818; + public static final double INITIAL_MAP_LONGITUDE = -23.514907; + + private static final float CODE_AREA_STROKE_WIDTH = 5.0f; + + private final MapView mMapView; + + private GoogleMap mMap; + + private final ImageButton mSatelliteButton; + + private final ImageButton mMyLocationButton; + + private Polygon mCodePolygon; + + private boolean mRetryShowingCurrentLocationOnMap; + + private MapContract.ActionsListener mListener; + + private OpenLocationCode mLastDisplayedCode; + + public MyMapView(Context context, AttributeSet attrs) { + super(context, attrs); + LayoutInflater inflater; + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.my_map, this, true); + + mMapView = (MapView) findViewById(R.id.map); + + mSatelliteButton = (ImageButton) findViewById(R.id.satelliteButton); + + mMyLocationButton = (ImageButton) findViewById(R.id.myLocationButton); + + mRetryShowingCurrentLocationOnMap = true; + + mMapView.getMapAsync(this); + } + + public MapView getMapView() { + return mMapView; + } + + public ImageButton getSatelliteButton() { + return mSatelliteButton; + } + + public ImageButton getMyLocationButton() { + return mMyLocationButton; + } + + public int getMapType() { + if (mMap == null) { + return -1; + } + return mMap.getMapType(); + } + + @Override + public void onMapReady(GoogleMap map) { + mMap = map; + setCameraPosition(INITIAL_MAP_LATITUDE, INITIAL_MAP_LONGITUDE, INITIAL_MAP_ZOOM); + // Enable the location indicator, but disable the button (so we can implement it where we + // want). + mMap.setMyLocationEnabled(true); + mMap.getUiSettings().setMyLocationButtonEnabled(false); + // Disable tilt and rotate gestures. Can move the button with setPadding() but that also + // moves the center of the map, and we can't get the dimensions of searchView yet. + mMap.getUiSettings().setTiltGesturesEnabled(false); + mMap.getUiSettings().setRotateGesturesEnabled(false); + + startUpdateCodeOnDrag(); + } + + @Override + public void startUpdateCodeOnDrag() { + Log.i(TAG, "Starting camera drag listener"); + mMap.setOnCameraChangeListener(new OnCameraChangeListener() { + @Override + public void onCameraChange(CameraPosition cameraPosition) { + mListener.mapChanged( + cameraPosition.target.latitude, cameraPosition.target.longitude); + } + }); + // And poke the map. + CameraPosition cameraPosition = getCameraPosition(); + mListener.mapChanged(cameraPosition.target.latitude, cameraPosition.target.longitude); + } + + @Override + public void stopUpdateCodeOnDrag() { + Log.i(TAG, "Stopping camera drag listener"); + mMap.setOnCameraChangeListener(null); + } + + @Override + public void drawCodeArea(final OpenLocationCode code) { + if (code.equals(mLastDisplayedCode)) { + // Skip if we're already displaying this location. + return; + } + mLastDisplayedCode = code; + if (mMap != null) { + CodeArea area = code.decode(); + LatLng southWest = new LatLng(area.getSouthLatitude(), area.getWestLongitude()); + LatLng northWest = new LatLng(area.getNorthLatitude(), area.getWestLongitude()); + LatLng southEast = new LatLng(area.getSouthLatitude(), area.getEastLongitude()); + LatLng northEast = new LatLng(area.getNorthLatitude(), area.getEastLongitude()); + + if (mCodePolygon != null) { + mCodePolygon.remove(); + } + + mCodePolygon = mMap.addPolygon( + new PolygonOptions().add(southWest, northWest,northEast, southEast) + .strokeColor(ContextCompat.getColor(this.getContext(), R.color.code_box_stroke)) + .strokeWidth(CODE_AREA_STROKE_WIDTH) + .fillColor(ContextCompat.getColor(this.getContext(), R.color.code_box_fill))); + } + } + + @Override + public void moveMapToLocation(final OpenLocationCode code) { + if (mMap != null) { + CodeArea area = code.decode(); + + mMap.animateCamera(CameraUpdateFactory.newLatLngZoom( + new LatLng(area.getCenterLatitude(), area.getCenterLongitude()), + INITIAL_MAP_ZOOM)); + + drawCodeArea(code); + } else { + // In case the map isn't ready yet + Handler h = new Handler(); + Runnable r = new Runnable() { + @Override + public void run() { + moveMapToLocation(code); + } + }; + if (mRetryShowingCurrentLocationOnMap) { + h.postDelayed(r, 2000); + mRetryShowingCurrentLocationOnMap = false; + } + } + } + + @Override + public void showSatelliteView() { + if (mMap != null) { + mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); + mSatelliteButton.setSelected(true); + } + } + + @Override + public void showRoadView() { + if (mMap != null) { + mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); + mSatelliteButton.setSelected(false); + } + } + + @Override + public void setListener(MapContract.ActionsListener listener) { + mListener = listener; + } + + @Override + public void showSearchCode(final OpenLocationCode code) { + moveMapToLocation(code); + } + + @Override + public CameraPosition getCameraPosition() { + if (mMap == null) { + return null; + } + return mMap.getCameraPosition(); + } + + @Override + public void setCameraPosition(double latitude, double longitude, float zoom) { + if (mMap == null) { + Log.w(TAG, "Couldn't set camera position because the map is null"); + } else { + mMap.moveCamera(CameraUpdateFactory.zoomTo(zoom)); + mMap.moveCamera(CameraUpdateFactory.newLatLng(new LatLng(latitude, longitude))); + } + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/search/AutoCompleteEditor.java b/android_demo/android/src/main/java/com/openlocationcode/android/search/AutoCompleteEditor.java new file mode 100644 index 00000000..f693abd1 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/search/AutoCompleteEditor.java @@ -0,0 +1,55 @@ +/* + * 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.openlocationcode.android.search; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.AutoCompleteTextView; + +/** + * Super class of AutoCompleteTextView that allows detection of the soft keyboard dismissal. + */ +public class AutoCompleteEditor extends AutoCompleteTextView { + + private com.openlocationcode.android.search.SearchContract.SourceView imeBackListener; + + public AutoCompleteEditor(Context context) { + super(context); + } + + public AutoCompleteEditor(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setImeBackListener(SearchContract.SourceView imeBackListener) { + this.imeBackListener = imeBackListener; + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK + && event.getAction() == KeyEvent.ACTION_UP) { + if (imeBackListener != null) { + imeBackListener.imeBackHandler(); + } + } + + return super.onKeyPreIme(keyCode, event); + } +} + diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchContract.java b/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchContract.java new file mode 100644 index 00000000..7e214d2c --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchContract.java @@ -0,0 +1,66 @@ +/* + * 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.openlocationcode.android.search; + +import com.google.openlocationcode.OpenLocationCode; + +import java.util.List; + +/** + * The contract for the search functionality. + */ +public interface SearchContract { + + /** + * The contract for a view allowing the user to enter search criteria. + */ + interface SourceView { + + void showInvalidCode(); + + void showEmptyCode(); + + void setActionsListener(ActionsListener listener); + + void showSuggestions(List suggestions); + + void imeBackHandler(); + + void setText(String text); + + } + + /** + * The contract for a view displaying the result of the search. + */ + interface TargetView { + + void showSearchCode(OpenLocationCode code); + + } + + interface ActionsListener { + + boolean searchCode(String code); + + void getSuggestions(String code); + + void setSearchText(String text); + + } + +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchPresenter.java b/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchPresenter.java new file mode 100644 index 00000000..3eaa807f --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchPresenter.java @@ -0,0 +1,117 @@ +/* + * 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.openlocationcode.android.search; + +import com.openlocationcode.android.code.OpenLocationCodeUtil; +import com.openlocationcode.android.localities.Locality; +import com.openlocationcode.android.main.MainActivity; +import com.google.android.gms.maps.model.CameraPosition; +import com.openlocationcode.android.map.MyMapView; + +import android.location.Location; + +import java.util.ArrayList; +import java.util.List; + +import com.google.openlocationcode.OpenLocationCode; + +public class SearchPresenter implements SearchContract.ActionsListener { + + private final SearchContract.SourceView mSourceView; + + private final List mTargetViews; + + public SearchPresenter( + SearchContract.SourceView sourceView, List targetViews) { + mSourceView = sourceView; + mTargetViews = targetViews; + } + + @Override + public boolean searchCode(String codeStr) { + if (codeStr.trim().isEmpty()) { + mSourceView.showEmptyCode(); + } else { + // We need to convert the search string into an OLC code. To do this, we might need + // a location. We prefer the center of the map, or the current location if the map + // cannot be loaded. + OpenLocationCode code; + CameraPosition position = MainActivity.getMainPresenter().getMapCameraPosition(); + Location location = MainActivity.getMainPresenter().getCurrentLocation(); + if (position != null) { + code = + OpenLocationCodeUtil.getCodeForSearchString( + codeStr.trim(), + position.target.latitude, + position.target.longitude); + + } else if (location != null) { + code = + OpenLocationCodeUtil.getCodeForSearchString( + codeStr.trim(), location.getLatitude(), location.getLongitude()); + } else { + // We don't have a map or a location for the user. Use the map default location. + code = + OpenLocationCodeUtil.getCodeForSearchString( + codeStr.trim(), + MyMapView.INITIAL_MAP_LATITUDE, + MyMapView.INITIAL_MAP_LONGITUDE); + } + + if (code != null) { + for (SearchContract.TargetView view : mTargetViews) { + view.showSearchCode(code); + } + // If we are showing a result, stop updating the code etc when the map is dragged. + MainActivity.getMainPresenter().getMapActionsListener().stopUpdateCodeOnDrag(); + return true; + } + } + mSourceView.showInvalidCode(); + return false; + } + + @Override + public void getSuggestions(String code) { + OpenLocationCode currentLocationCode = null; + if (MainActivity.getMainPresenter().getCurrentLocation() != null) { + currentLocationCode = + OpenLocationCodeUtil.createOpenLocationCode( + MainActivity.getMainPresenter().getCurrentLocation().getLatitude(), + MainActivity.getMainPresenter().getCurrentLocation().getLongitude()); + } + OpenLocationCode mapLocationCode = null; + if (MainActivity.getMainPresenter().getMapCameraPosition() != null) { + CameraPosition position = MainActivity.getMainPresenter().getMapCameraPosition(); + mapLocationCode = + OpenLocationCodeUtil.createOpenLocationCode( + position.target.latitude, position.target.longitude); + } + List localities = + Locality.getAllLocalitiesForSearchDisplay(currentLocationCode, mapLocationCode); + List suggestions = new ArrayList<>(); + for (String locality : localities) { + suggestions.add(code + " " + locality); + } + mSourceView.showSuggestions(suggestions); + } + + @Override + public void setSearchText(String text) { + mSourceView.setText(text); + } +} diff --git a/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchView.java b/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchView.java new file mode 100644 index 00000000..87055b29 --- /dev/null +++ b/android_demo/android/src/main/java/com/openlocationcode/android/search/SearchView.java @@ -0,0 +1,236 @@ +/* + * 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.openlocationcode.android.search; + +import com.openlocationcode.android.main.MainActivity; +import com.openlocationcode.android.R; + + +import android.content.Context; +import android.os.Build.VERSION; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView.OnDismissListener; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; + +import com.google.openlocationcode.OpenLocationCode; + +public class SearchView extends FrameLayout implements SearchContract.SourceView, TextWatcher { + + private SearchContract.ActionsListener mListener; + + private FrameLayout mFocusLayerView; + + private AutoCompleteEditor mSearchET; + + private ImageView mCancelButton; + + private ArrayAdapter mSuggestionsAdapter; + + private boolean mEditFieldFocused; + + public SearchView(Context context, AttributeSet attrs) { + super(context, attrs); + LayoutInflater inflater; + inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.search, this, true); + + initUI(); + } + + private void initUI() { + mFocusLayerView = (FrameLayout) findViewById(R.id.focusLayer); + mSearchET = (AutoCompleteEditor) findViewById(R.id.searchET); + mSearchET.addTextChangedListener(this); + mSuggestionsAdapter = + new ArrayAdapter<>( + getContext(), + android.R.layout.simple_dropdown_item_1line, + new ArrayList()); + mSearchET.setAdapter(mSuggestionsAdapter); + mSearchET.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mFocusLayerView.getWindowToken(), 0); + mSearchET.clearFocus(); + // searchCode stops listening to map drags if the search string was valid. + mListener.searchCode(getSearchString()); + } + }); + + mCancelButton = (ImageView) findViewById(R.id.cancel); + mCancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mSearchET.setText(""); + // When the cancel button is pressed, start listening to map drags again. + MainActivity.getMainPresenter().getMapActionsListener().startUpdateCodeOnDrag(); + } + }); + + mSearchET.setImeBackListener(this); + + mSearchET.setOnFocusChangeListener(new OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + mEditFieldFocused = hasFocus; + } + }); + + mSearchET.setOnEditorActionListener( + new OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + InputMethodManager imm = + (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mFocusLayerView.getWindowToken(), 0); + mSearchET.clearFocus(); + mListener.searchCode(getSearchString()); + return true; + } + return false; + } + }); + + if (VERSION.SDK_INT >= 17) { + mSearchET.setOnDismissListener( + new OnDismissListener() { + @Override + public void onDismiss() { + mFocusLayerView.setAlpha(0f); + } + }); + } + + mFocusLayerView.setOnTouchListener( + new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (mEditFieldFocused) { + InputMethodManager imm = + (InputMethodManager) getContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mFocusLayerView.getWindowToken(), 0); + mSearchET.clearFocus(); + } + return false; + } + }); + + } + + private String getSearchString() { + return mSearchET.getText().toString().trim(); + } + + @Override + public void imeBackHandler() { + mSearchET.setText(""); + mSearchET.clearFocus(); + } + + @Override + public void setActionsListener(SearchContract.ActionsListener listener) { + mListener = listener; + } + + @Override + public void showSuggestions(List suggestions) { + mSuggestionsAdapter.clear(); + mSuggestionsAdapter.addAll(suggestions); + mSuggestionsAdapter.notifyDataSetChanged(); + } + + @Override + public void showInvalidCode() { + Toast.makeText( + getContext(), + getResources().getString(R.string.search_invalid), + Toast.LENGTH_LONG) + .show(); + } + + @Override + public void showEmptyCode() { + Toast.makeText( + getContext(), + getResources().getString(R.string.search_empty), + Toast.LENGTH_LONG) + .show(); + } + + @Override + public void setText(String searchText) { + if (searchText != null && searchText.length() > 0) { + mSearchET.setText(searchText); + mCancelButton.setVisibility(View.VISIBLE); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Not required + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Show/hide cancel search button + if (s.length() > 0) { + mCancelButton.setVisibility(View.VISIBLE); + } else { + mCancelButton.setVisibility(View.GONE); + // When the cancel button is removed, start listening to map drags again. + MainActivity.getMainPresenter().getMapActionsListener().startUpdateCodeOnDrag(); + } + + // Show autocomplete if: + // - code of format XXXX+XXX is entered + // - a space is typed after at least 7 characters (eg after a XXXX+XX code) + if ((s.length() >= 8 && OpenLocationCode.isValidCode(s.toString())) + || (s.length() > 7 && s.charAt(s.length() - 1) == ' ')) { + mListener.getSuggestions(s.subSequence(0, 8).toString()); + mFocusLayerView.setAlpha(0.15f); + } + } + + @Override + public void afterTextChanged(Editable s) { + // Not required + } +} diff --git a/android_demo/android/src/main/res/drawable-hdpi/ic_action_maps_navigation.png b/android_demo/android/src/main/res/drawable-hdpi/ic_action_maps_navigation.png new file mode 100644 index 00000000..f16cd32c Binary files /dev/null and b/android_demo/android/src/main/res/drawable-hdpi/ic_action_maps_navigation.png differ diff --git a/android_demo/android/src/main/res/drawable-hdpi/ic_launcher.png b/android_demo/android/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 00000000..61fc0717 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/android_demo/android/src/main/res/drawable-hdpi/map_center.png b/android_demo/android/src/main/res/drawable-hdpi/map_center.png new file mode 100644 index 00000000..49ed85c5 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-hdpi/map_center.png differ diff --git a/android_demo/android/src/main/res/drawable-hdpi/welcome_image.png b/android_demo/android/src/main/res/drawable-hdpi/welcome_image.png new file mode 100644 index 00000000..2205acf1 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-hdpi/welcome_image.png differ diff --git a/android_demo/android/src/main/res/drawable-ldpi/map_center.png b/android_demo/android/src/main/res/drawable-ldpi/map_center.png new file mode 100644 index 00000000..7850ba15 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-ldpi/map_center.png differ diff --git a/android_demo/android/src/main/res/drawable-mdpi/ic_action_maps_navigation.png b/android_demo/android/src/main/res/drawable-mdpi/ic_action_maps_navigation.png new file mode 100644 index 00000000..bd752f49 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-mdpi/ic_action_maps_navigation.png differ diff --git a/android_demo/android/src/main/res/drawable-mdpi/ic_launcher.png b/android_demo/android/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 00000000..051a55dd Binary files /dev/null and b/android_demo/android/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/android_demo/android/src/main/res/drawable-mdpi/map_center.png b/android_demo/android/src/main/res/drawable-mdpi/map_center.png new file mode 100644 index 00000000..52d14ace Binary files /dev/null and b/android_demo/android/src/main/res/drawable-mdpi/map_center.png differ diff --git a/android_demo/android/src/main/res/drawable-xhdpi/ic_action_maps_navigation.png b/android_demo/android/src/main/res/drawable-xhdpi/ic_action_maps_navigation.png new file mode 100644 index 00000000..f8084b92 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xhdpi/ic_action_maps_navigation.png differ diff --git a/android_demo/android/src/main/res/drawable-xhdpi/ic_launcher.png b/android_demo/android/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 00000000..2f90e551 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/android_demo/android/src/main/res/drawable-xhdpi/map_center.png b/android_demo/android/src/main/res/drawable-xhdpi/map_center.png new file mode 100644 index 00000000..bc765e27 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xhdpi/map_center.png differ diff --git a/android_demo/android/src/main/res/drawable-xhdpi/welcome_image.png b/android_demo/android/src/main/res/drawable-xhdpi/welcome_image.png new file mode 100644 index 00000000..10bb25f2 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xhdpi/welcome_image.png differ diff --git a/android_demo/android/src/main/res/drawable-xxhdpi/ic_action_maps_navigation.png b/android_demo/android/src/main/res/drawable-xxhdpi/ic_action_maps_navigation.png new file mode 100644 index 00000000..82e817f7 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxhdpi/ic_action_maps_navigation.png differ diff --git a/android_demo/android/src/main/res/drawable-xxhdpi/ic_launcher.png b/android_demo/android/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..0d258804 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/android_demo/android/src/main/res/drawable-xxhdpi/map_center.png b/android_demo/android/src/main/res/drawable-xxhdpi/map_center.png new file mode 100644 index 00000000..3a9effc8 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxhdpi/map_center.png differ diff --git a/android_demo/android/src/main/res/drawable-xxhdpi/welcome_image.png b/android_demo/android/src/main/res/drawable-xxhdpi/welcome_image.png new file mode 100644 index 00000000..70804c60 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxhdpi/welcome_image.png differ diff --git a/android_demo/android/src/main/res/drawable-xxxhdpi/ic_action_maps_navigation.png b/android_demo/android/src/main/res/drawable-xxxhdpi/ic_action_maps_navigation.png new file mode 100644 index 00000000..d6e0d2ac Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxxhdpi/ic_action_maps_navigation.png differ diff --git a/android_demo/android/src/main/res/drawable-xxxhdpi/ic_launcher.png b/android_demo/android/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4664831b Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/android_demo/android/src/main/res/drawable-xxxhdpi/map_center.png b/android_demo/android/src/main/res/drawable-xxxhdpi/map_center.png new file mode 100644 index 00000000..7ef61690 Binary files /dev/null and b/android_demo/android/src/main/res/drawable-xxxhdpi/map_center.png differ diff --git a/android_demo/android/src/main/res/drawable/map_satellite_button.xml b/android_demo/android/src/main/res/drawable/map_satellite_button.xml new file mode 100644 index 00000000..7be6641e --- /dev/null +++ b/android_demo/android/src/main/res/drawable/map_satellite_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android_demo/android/src/main/res/drawable/rounded_layout.xml b/android_demo/android/src/main/res/drawable/rounded_layout.xml new file mode 100644 index 00000000..4323853e --- /dev/null +++ b/android_demo/android/src/main/res/drawable/rounded_layout.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android_demo/android/src/main/res/drawable/search_button.xml b/android_demo/android/src/main/res/drawable/search_button.xml new file mode 100644 index 00000000..1f679a77 --- /dev/null +++ b/android_demo/android/src/main/res/drawable/search_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android_demo/android/src/main/res/layout/code.xml b/android_demo/android/src/main/res/layout/code.xml new file mode 100644 index 00000000..f753b707 --- /dev/null +++ b/android_demo/android/src/main/res/layout/code.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/android_demo/android/src/main/res/layout/direction.xml b/android_demo/android/src/main/res/layout/direction.xml new file mode 100644 index 00000000..eee2778e --- /dev/null +++ b/android_demo/android/src/main/res/layout/direction.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/android_demo/android/src/main/res/layout/main_act.xml b/android_demo/android/src/main/res/layout/main_act.xml new file mode 100644 index 00000000..5eae60a0 --- /dev/null +++ b/android_demo/android/src/main/res/layout/main_act.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/android_demo/android/src/main/res/layout/main_frag.xml b/android_demo/android/src/main/res/layout/main_frag.xml new file mode 100644 index 00000000..2f6d67bf --- /dev/null +++ b/android_demo/android/src/main/res/layout/main_frag.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + diff --git a/android_demo/android/src/main/res/layout/my_map.xml b/android_demo/android/src/main/res/layout/my_map.xml new file mode 100644 index 00000000..f06916ca --- /dev/null +++ b/android_demo/android/src/main/res/layout/my_map.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + diff --git a/android_demo/android/src/main/res/layout/search.xml b/android_demo/android/src/main/res/layout/search.xml new file mode 100644 index 00000000..0e85f457 --- /dev/null +++ b/android_demo/android/src/main/res/layout/search.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + diff --git a/android_demo/android/src/main/res/layout/welcome.xml b/android_demo/android/src/main/res/layout/welcome.xml new file mode 100644 index 00000000..2119bd24 --- /dev/null +++ b/android_demo/android/src/main/res/layout/welcome.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + +