├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.sh ├── changelog.txt ├── common ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── firebase │ └── geofire │ ├── EventListenerBridge.java │ ├── EventRaiser.java │ ├── GeoFire.java │ ├── GeoLocation.java │ ├── GeoQuery.java │ ├── GeoQueryDataEventListener.java │ ├── GeoQueryEventListener.java │ ├── LocationCallback.java │ ├── ThreadEventRaiser.java │ ├── core │ ├── GeoHash.java │ └── GeoHashQuery.java │ └── util │ ├── Base32Utils.java │ ├── Constants.java │ └── GeoUtils.java ├── create-docs.sh ├── firebase.json ├── java ├── pom.xml ├── service-account.json.enc └── src │ └── test │ └── java │ └── com │ └── firebase │ └── geofire │ ├── GeoFireIT.java │ ├── GeoHashQueryTest.java │ ├── GeoHashQueryUtilsTest.java │ ├── GeoHashTest.java │ ├── GeoLocationTest.java │ ├── GeoQueryIT.java │ └── GeoUtilsTest.java ├── pom.xml ├── release.sh ├── settings.xml ├── site └── index.html └── testing ├── pom.xml └── src └── main └── java └── com └── firebase └── geofire └── testing ├── GeoFireTestingRule.java ├── GeoQueryDataEventTestListener.java ├── GeoQueryEventTestListener.java ├── SimpleFuture.java ├── TestCallback.java └── TestListener.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS-X 2 | .DS_STORE 3 | 4 | # idea 5 | .idea 6 | *.iml 7 | *.ipr 8 | *.iws 9 | 10 | # Maven 11 | target 12 | 13 | # Other 14 | /site/docs 15 | 16 | # Credentials / Releasing 17 | service-account.json 18 | release.properties 19 | *.xml.releaseBackup 20 | *.xml.tag 21 | *.xml.next 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk8 3 | sudo: required 4 | before_cache: 5 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 6 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 7 | cache: 8 | directories: 9 | - "$HOME/.gradle/caches/" 10 | - "$HOME/.gradle/wrapper/" 11 | script: "./build.sh" 12 | java/service-account: 13 | json: 14 | secure: MWpRST/xWJ/t+CCs5u7VGgb5qm7iDMovV+Bv/M5taQFnMq3BnEjyf76Z/or6pHB2hB09HDWws12yC/leAgC3tlUiz3Vp79TXPhHqNVrVOMCsoX7zsGM39cRYVwUfmR4dMUnTc12se3juaS8qy55GnS7fqz8CBPg6YuR1GsDfHcZgvFIRCR38Gj4XaJuvMLfRU23KmpWGWjjwTX3oCg+p07R6GeIMpfcPuOapgYjTm/llDxVCzQ/dpH6uP0tl1S607l4HOpnbnIEwANqD1PyH1YuB1oCvBmjcCazrfVmYm/F9OgR2ElnNKGNIoplAOIvSeK7viNLn9YUTg8PReDgVvfIMkpKeXjAkcGaON7VMjjhIp6S+Tznp4SS1CTSoizN9UWxnSeerwDadB7iqjO5HcjIqvtGH2q+Yyb66N6cRHU1iMQa6OQjk2Vu658k5kv7YVfizNl2huzVdwk5j1gzG2qGJ3sVASaxjhojCP4SNxifvYQW3biggu4ZhK7ABMcS+KrWLc8wD/3SAfVNmzlyNVdD0ye5k5PoQkR2v+J6WJE3MkS/gzldvqnpdpi5VS//dcBgm9u3a3tj9+mGyuPkWP2ka4gueBA9e7h8V+sLFr6/f2ttyzMUJ+vrfHnrr8gUGbswkavzDLv1SNtovyXlbaOfp/oOfhU6qbHubC/H1QRQ= 15 | env: 16 | global: 17 | secure: GsBHt/Zbqe+h/ACtg7CKl9rA143Z4EOp1bOsEyUgQ9Ch7kwVdXQkKB5LGdK+I+TbZhvOXvrvQ1JBAwMrj1yHSmPHLfiTFUdNqwOFLfqc8EvWYQ8mjmk3wg5lYohO7Nh1gShYrzevxfiR/cAAixUlRpZEETHF/SGcUKGqyoqKnUg= 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love for you to contribute to our source code and to make it even better than it is today! Here are the guidelines we'd like you to follow: 4 | 5 | - [Code of Conduct](#coc) 6 | - [Question or Problem?](#question) 7 | - [Issues and Bugs](#issue) 8 | - [Feature Requests](#feature) 9 | - [Submission Guidelines](#submit) 10 | - [Coding Rules](#rules) 11 | - [Signing the CLA](#cla) 12 | 13 | ## Code of Conduct 14 | 15 | As contributors and maintainers of the project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. 16 | 17 | Communication through any of Firebase's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 18 | 19 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same. 20 | 21 | If any member of the community violates this code of conduct, the maintainers of the project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate. 22 | 23 | If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at nivco@google.com. 24 | 25 | ## Got a Question or Problem? 26 | 27 | If you have questions about how to use the project, please direct these to [StackOverflow][stackoverflow] and use the `firebase` tag. We are also available on GitHub issues. 28 | 29 | If you feel that we're missing an important bit of documentation, feel free to 30 | file an issue so we can help. Here's an example to get you started: 31 | 32 | ``` 33 | What are you trying to do or find out more about? 34 | 35 | Where have you looked? 36 | 37 | Where did you expect to find this information? 38 | ``` 39 | 40 | ## Found an Issue? 41 | 42 | If you find a bug in the source code or a mistake in the documentation, you can help us by 43 | submitting an issue on this repository. Even better you can submit a Pull Request 44 | with a fix. 45 | 46 | See [below](#submit) for some guidelines. 47 | 48 | ## Submission Guidelines 49 | 50 | ### Submitting an Issue 51 | 52 | Before you submit your issue search the archive, maybe your question was already answered. 53 | 54 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 55 | Help us to maximize the effort we can spend fixing issues and adding new 56 | features, by not reporting duplicate issues. Providing the following information will increase the 57 | chances of your issue being dealt with quickly: 58 | 59 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 60 | * **Motivation for or Use Case** - explain why this is a bug for you 61 | * **Browsers and Operating System** - is this a problem with all browsers or only IE9? 62 | * **Reproduce the Error** - provide a live example or an unambiguous set of steps. 63 | * **Related Issues** - has a similar issue been reported before? 64 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 65 | causing the problem (line of code or commit) 66 | 67 | **If you get help, help others. Good karma rulez!** 68 | 69 | Here's a template to get you started: 70 | 71 | ``` 72 | System information (OS, Device, etc): 73 | 74 | What steps will reproduce the problem: 75 | 1. 76 | 2. 77 | 3. 78 | 79 | What is the expected result? 80 | 81 | What happens instead of that? 82 | 83 | Code, logs, or screenshot that illustrate the problem: 84 | ``` 85 | 86 | ### Submitting a Pull Request 87 | Before you submit your pull request consider the following guidelines: 88 | 89 | * Search for an open or closed Pull Request 90 | that relates to your submission. You don't want to duplicate effort. 91 | * Please sign our [Contributor License Agreement (CLA)](#cla) before 92 | sending pull requests. We cannot accept code without this. 93 | * Make your changes in a new git branch: 94 | 95 | ```shell 96 | git checkout -b my-fix-branch master 97 | ``` 98 | 99 | * Create your patch, **including appropriate test cases**. 100 | * Follow our [Coding Rules](#rules). 101 | * Commit your changes using a descriptive commit message. 102 | 103 | ```shell 104 | git commit -a 105 | ``` 106 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 107 | 108 | * Push your branch to GitHub: 109 | 110 | ```shell 111 | git push origin my-fix-branch 112 | ``` 113 | 114 | * In GitHub, send a pull request to `master`. 115 | * If we suggest changes then: 116 | * Make the required updates. 117 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 118 | 119 | ```shell 120 | git rebase master -i 121 | git push origin my-fix-branch -f 122 | ``` 123 | 124 | That's it! Thank you for your contribution! 125 | 126 | #### After your pull request is merged 127 | 128 | After your pull request is merged, you can safely delete your branch and pull the changes 129 | from the main (upstream) repository: 130 | 131 | * Delete the remote branch on GitHub either through the GitHub UI or your local shell as follows: 132 | 133 | ```shell 134 | git push origin --delete my-fix-branch 135 | ``` 136 | 137 | * Check out the master branch: 138 | 139 | ```shell 140 | git checkout master -f 141 | ``` 142 | 143 | * Delete the local branch: 144 | 145 | ```shell 146 | git branch -D my-fix-branch 147 | ``` 148 | 149 | * Update your master with the latest upstream version: 150 | 151 | ```shell 152 | git pull --ff upstream master 153 | ``` 154 | 155 | ## Coding Rules 156 | 157 | We generally follow [Google's style guides][style-guide]. 158 | 159 | ## Signing the CLA 160 | 161 | Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code 162 | changes to be accepted, the CLA must be signed. It's a quick process, we promise! 163 | 164 | *This guide was inspired by the [AngularJS contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).* 165 | 166 | [google-cla]: https://cla.developers.google.com 167 | [style-guide]: http://google.github.io/styleguide/ 168 | [stackoverflow]: http://stackoverflow.com/questions/tagged/firebase 169 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Firebase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeoFire for Java — Realtime location queries with Firebase 2 | 3 | [![Build Status](https://travis-ci.org/firebase/geofire-java.svg?branch=master)](https://travis-ci.org/firebase/geofire-java?branch=master) 4 | 5 | **Note** This library is only for _server side_ Java development. If you want to use GeoFire 6 | in your Android application, see [`geofire-android`](https://github.com/firebase/geofire-android). 7 | 8 | GeoFire is an open-source library for Java that allows you to store and query a 9 | set of keys based on their geographic location. 10 | 11 | At its heart, GeoFire simply stores locations with string keys. Its main 12 | benefit however, is the possibility of querying keys within a given geographic 13 | area - all in realtime. 14 | 15 | GeoFire uses the [Firebase Realtime Database](https://firebase.google.com/products/realtime-database/) database for 16 | data storage, allowing query results to be updated in realtime as they change. 17 | GeoFire *selectively loads only the data near certain locations, keeping your 18 | applications light and responsive*, even with extremely large datasets. 19 | 20 | GeoFire clients are also available for other languages: 21 | 22 | * [Android](https://github.com/firebase/geofire-android) 23 | * [Objective-C (iOS)](https://github.com/firebase/geofire-objc) 24 | * [JavaScript (Web)](https://github.com/firebase/geofire-js) 25 | 26 | ### Integrating GeoFire with your data 27 | 28 | GeoFire is designed as a lightweight add-on to the Firebase Realtime Database. However, to keep things 29 | simple, GeoFire stores data in its own format and its own location within 30 | your Firebase database. This allows your existing data format and security rules to 31 | remain unchanged and for you to add GeoFire as an easy solution for geo queries 32 | without modifying your existing data. 33 | 34 | ### Example Usage 35 | 36 | Assume you are building an app to rate bars and you store all information for a 37 | bar, e.g. name, business hours and price range, at `/bars/`. Later, you 38 | want to add the possibility for users to search for bars in their vicinity. This 39 | is where GeoFire comes in. You can store the location for each bar using 40 | GeoFire, using the bar IDs as GeoFire keys. GeoFire then allows you to easily 41 | query which bar IDs (the keys) are nearby. To display any additional information 42 | about the bars, you can load the information for each bar returned by the query 43 | at `/bars/`. 44 | 45 | 46 | ## Upgrading GeoFire 47 | 48 | ### Upgrading from GeoFire 1.x to 2.x 49 | 50 | GeoFire 2.x is based on the new 3.x release of [Firebase](https://firebase.google.com). 51 | 52 | ### Upgrading from GeoFire 1.0.x to 1.1.x 53 | 54 | With the release of GeoFire for Android/Java 1.1.0, this library now uses [the new query 55 | functionality found in Firebase 2.0.0](https://www.firebase.com/blog/2014-11-04-firebase-realtime-queries.html). 56 | As a result, you will need to upgrade to Firebase 2.x.x and add a new `.indexOn` rule to your 57 | Security and Firebase Rules to get the best performance. You can view [the updated rules 58 | here](https://github.com/firebase/geofire-js/blob/master/examples/securityRules/rules.json) 59 | and [read our docs for more information about indexing your data](https://www.firebase.com/docs/security/guide/indexing-data.html). 60 | 61 | 62 | ## Including GeoFire in your Java project 63 | 64 | In order to use GeoFire in your project, you need to [add the Firebase Admin 65 | SDK](https://firebase.google.com/docs/admin/setup). After that you can include GeoFire with one of the choices below. 66 | 67 | ### Gradle 68 | 69 | Add a dependency for GeoFire to your `build.gradle` file: 70 | 71 | ```groovy 72 | dependencies { 73 | implementation 'com.firebase:geofire-java:3.0.0' 74 | } 75 | 76 | ``` 77 | 78 | ### Maven 79 | 80 | Add a dependency for GeoFire to your `pom.xml` file: 81 | 82 | ```xml 83 | 84 | com.firebase 85 | geofire-java 86 | 3.0.0 87 | 88 | ``` 89 | 90 | ## Usage 91 | 92 | ### GeoFire 93 | 94 | A `GeoFire` object is used to read and write geo location data to your Firebase 95 | database and to create queries. To create a new `GeoFire` instance you need to attach it to a Firebase database 96 | reference. 97 | 98 | ```java 99 | DatabaseReference ref = FirebaseDatabase.getInstance().getReference("path/to/geofire"); 100 | GeoFire geoFire = new GeoFire(ref); 101 | ``` 102 | 103 | Note that you can point your reference to anywhere in your Firebase database, but don't 104 | forget to [setup security rules for 105 | GeoFire](https://github.com/firebase/geofire-js/blob/master/examples/securityRules). 106 | 107 | #### Setting location data 108 | 109 | In GeoFire you can set and query locations by string keys. To set a location for 110 | a key simply call the `setLocation` method. The method is passed a key 111 | as a string and the location as a `GeoLocation` object containing the location's latitude and longitude: 112 | 113 | ```java 114 | geoFire.setLocation("firebase-hq", new GeoLocation(37.7853889, -122.4056973)); 115 | ``` 116 | 117 | To check if a write was successfully saved on the server, you can add a 118 | `GeoFire.CompletionListener` to the `setLocation` call: 119 | 120 | ```java 121 | geoFire.setLocation("firebase-hq", new GeoLocation(37.7853889, -122.4056973), new GeoFire.CompletionListener() { 122 | @Override 123 | public void onComplete(String key, FirebaseError error) { 124 | if (error != null) { 125 | System.err.println("There was an error saving the location to GeoFire: " + error); 126 | } else { 127 | System.out.println("Location saved on server successfully!"); 128 | } 129 | } 130 | }); 131 | ``` 132 | 133 | To remove a location and delete it from the database simply pass the location's key to `removeLocation`: 134 | 135 | ```java 136 | geoFire.removeLocation("firebase-hq"); 137 | ``` 138 | 139 | #### Retrieving a location 140 | 141 | Retrieving a location for a single key in GeoFire happens with callbacks: 142 | 143 | ```java 144 | geoFire.getLocation("firebase-hq", new LocationCallback() { 145 | @Override 146 | public void onLocationResult(String key, GeoLocation location) { 147 | if (location != null) { 148 | System.out.println(String.format("The location for key %s is [%f,%f]", key, location.latitude, location.longitude)); 149 | } else { 150 | System.out.println(String.format("There is no location for key %s in GeoFire", key)); 151 | } 152 | } 153 | 154 | @Override 155 | public void onCancelled(DatabaseError databaseError) { 156 | System.err.println("There was an error getting the GeoFire location: " + databaseError); 157 | } 158 | }); 159 | ``` 160 | 161 | ### Geo Queries 162 | 163 | GeoFire allows you to query all keys within a geographic area using `GeoQuery` 164 | objects. As the locations for keys change, the query is updated in realtime and fires events 165 | letting you know if any relevant keys have moved. `GeoQuery` parameters can be updated 166 | later to change the size and center of the queried area. 167 | 168 | ```java 169 | // creates a new query around [37.7832, -122.4056] with a radius of 0.6 kilometers 170 | GeoQuery geoQuery = geoFire.queryAtLocation(new GeoLocation(37.7832, -122.4056), 0.6); 171 | ``` 172 | 173 | #### Receiving events for geo queries 174 | 175 | ##### Key Events 176 | 177 | There are five kinds of "key" events that can occur with a geo query: 178 | 179 | 1. **Key Entered**: The location of a key now matches the query criteria. 180 | 2. **Key Exited**: The location of a key no longer matches the query criteria. 181 | 3. **Key Moved**: The location of a key changed but the location still matches the query criteria. 182 | 4. **Query Ready**: All current data has been loaded from the server and all 183 | initial events have been fired. 184 | 5. **Query Error**: There was an error while performing this query, e.g. a 185 | violation of security rules. 186 | 187 | Key entered events will be fired for all keys initially matching the query as well as any time 188 | afterwards that a key enters the query. Key moved and key exited events are guaranteed to be 189 | preceded by a key entered event. 190 | 191 | Sometimes you want to know when the data for all the initial keys has been 192 | loaded from the server and the corresponding events for those keys have been 193 | fired. For example, you may want to hide a loading animation after your data has 194 | fully loaded. This is what the "ready" event is used for. 195 | 196 | Note that locations might change while initially loading the data and key moved and key 197 | exited events might therefore still occur before the ready event is fired. 198 | 199 | When the query criteria is updated, the existing locations are re-queried and the 200 | ready event is fired again once all events for the updated query have been 201 | fired. This includes key exited events for keys that no longer match the query. 202 | 203 | To listen for events you must add a `GeoQueryEventListener` to the `GeoQuery`: 204 | 205 | ```java 206 | geoQuery.addGeoQueryEventListener(new GeoQueryEventListener() { 207 | @Override 208 | public void onKeyEntered(String key, GeoLocation location) { 209 | System.out.println(String.format("Key %s entered the search area at [%f,%f]", key, location.latitude, location.longitude)); 210 | } 211 | 212 | @Override 213 | public void onKeyExited(String key) { 214 | System.out.println(String.format("Key %s is no longer in the search area", key)); 215 | } 216 | 217 | @Override 218 | public void onKeyMoved(String key, GeoLocation location) { 219 | System.out.println(String.format("Key %s moved within the search area to [%f,%f]", key, location.latitude, location.longitude)); 220 | } 221 | 222 | @Override 223 | public void onGeoQueryReady() { 224 | System.out.println("All initial data has been loaded and events have been fired!"); 225 | } 226 | 227 | @Override 228 | public void onGeoQueryError(DatabaseError error) { 229 | System.err.println("There was an error with this query: " + error); 230 | } 231 | }); 232 | ``` 233 | 234 | You can call either `removeGeoQueryEventListener` to remove a 235 | single event listener or `removeAllListeners` to remove all event listeners 236 | for a `GeoQuery`. 237 | 238 | ##### Data Events 239 | 240 | If you are storing model data and geo data in the same database location, you may 241 | want access to the `DataSnapshot` as part of geo events. In this case, use a 242 | `GeoQueryDataEventListener` rather than a key listener. 243 | 244 | These "data event" listeners have all of the same events as the key listeners with 245 | one additional event type: 246 | 247 | 6. **Data Changed**: the underlying `DataSnapshot` has changed. Every "data moved" 248 | event is followed by a data changed event but you can also get change events without 249 | a move if the data changed does not affect the location. 250 | 251 | Adding a data event listener is similar to adding a key event listener: 252 | 253 | ```java 254 | geoQuery.addGeoQueryDataEventListener(new GeoQueryDataEventListener() { 255 | 256 | @Override 257 | public void onDataEntered(DataSnapshot dataSnapshot, GeoLocation location) { 258 | // ... 259 | } 260 | 261 | @Override 262 | public void onDataExited(DataSnapshot dataSnapshot) { 263 | // ... 264 | } 265 | 266 | @Override 267 | public void onDataMoved(DataSnapshot dataSnapshot, GeoLocation location) { 268 | // ... 269 | } 270 | 271 | @Override 272 | public void onDataChanged(DataSnapshot dataSnapshot, GeoLocation location) { 273 | // ... 274 | } 275 | 276 | @Override 277 | public void onGeoQueryReady() { 278 | // ... 279 | } 280 | 281 | @Override 282 | public void onGeoQueryError(DatabaseError error) { 283 | // ... 284 | } 285 | 286 | }); 287 | 288 | ``` 289 | 290 | #### Updating the query criteria 291 | 292 | The `GeoQuery` search area can be changed with `setCenter` and `setRadius`. Key 293 | exited and key entered events will be fired for keys moving in and out of 294 | the old and new search area, respectively. No key moved events will be 295 | fired; however, key moved events might occur independently. 296 | 297 | Updating the search area can be helpful in cases such as when you need to update 298 | the query to the new visible map area after a user scrolls. 299 | 300 | 301 | ## Deployment 302 | 303 | - In your local environment set `$BINTRAY_USER` and `$BINTRAY_KEY` to your 304 | Bintray.com username and API key. 305 | - Checkout and update the master branch. 306 | - Run `./release.sh` to build and deploy. 307 | - On bintray.com, publish the draft artifacts. 308 | ``` 309 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit on error 4 | set -e 5 | 6 | # Set travis environment variables even when run locally 7 | 8 | # 1) Pull request 9 | if [ -z "$TRAVIS_PULL_REQUEST" ]; then 10 | echo "TRAVIS_PULL_REQUEST: unset, setting to false" 11 | TRAVIS_PULL_REQUEST=false 12 | else 13 | echo "TRAVIS_PULL_REQUEST: $TRAVIS_PULL_REQUEST" 14 | fi 15 | 16 | # 2) Secure env variables 17 | if [ -z "$TRAVIS_SECURE_ENV_VARS" ]; then 18 | echo "TRAVIS_SECURE_ENV_VARS: unset, setting to false" 19 | TRAVIS_SECURE_ENV_VARS=false 20 | else 21 | echo "TRAVIS_SECURE_ENV_VARS: $TRAVIS_SECURE_ENV_VARS" 22 | fi 23 | 24 | # Build 25 | mvn -e clean compile test-compile 26 | 27 | # Run unit tests 28 | mvn -e test 29 | 30 | # Only run test suite when we can decode the service acct 31 | if [ "$TRAVIS_SECURE_ENV_VARS" = false ]; then 32 | echo "Could not find secure environment variables, skipping integration tests." 33 | else 34 | # Decrypt service account 35 | openssl aes-256-cbc -K $encrypted_d7b8d9290299_key -iv $encrypted_d7b8d9290299_iv \ 36 | -in java/service-account.json.enc -out java/service-account.json -d 37 | 38 | # Run test suite 39 | mvn -Dfailsafe.rerunFailingTestsCount=2 verify 40 | fi 41 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | 7 | com.firebase 8 | geofire 9 | 3.0.1-SNAPSHOT 10 | ../ 11 | 12 | 13 | geofire-common 14 | jar 15 | 16 | geofire-common 17 | GeoFire is an open-source library for Android/Java that allows you to store and query a set of keys based on their geographic location. 18 | 19 | Firebase 20 | https://www.firebase.com/ 21 | 22 | https://github.com/firebase/geofire-java 23 | 24 | scm:git:git@github.com:firebase/geofire-java.git 25 | scm:git:git@github.com:firebase/geofire-java.git 26 | https://github.com/firebase/geofire-java 27 | HEAD 28 | 29 | 30 | 31 | MIT 32 | http://firebase.mit-license.org 33 | 34 | 35 | 36 | UTF-8 37 | 38 | 39 | 40 | 41 | geofire 42 | https://api.bintray.com/maven/firebase/geofire/geofire-common 43 | 44 | 45 | 46 | 47 | src/main/java 48 | 49 | 50 | 51 | 52 | 53 | com.google.firebase 54 | firebase-admin 55 | [6.0.0,) 56 | provided 57 | 58 | 59 | 60 | 61 | junit 62 | junit 63 | 4.12 64 | test 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/EventListenerBridge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | import com.google.firebase.database.DataSnapshot; 32 | import com.google.firebase.database.DatabaseError; 33 | import java.util.Objects; 34 | 35 | /** 36 | * GeoQuery notifies listeners with this interface about dataSnapshots that entered, exited, or moved within the query. 37 | */ 38 | final class EventListenerBridge implements GeoQueryDataEventListener { 39 | private final GeoQueryEventListener listener; 40 | 41 | public EventListenerBridge(final GeoQueryEventListener listener) { 42 | this.listener = listener; 43 | } 44 | 45 | @Override 46 | public void onDataEntered(final DataSnapshot dataSnapshot, final GeoLocation location) { 47 | listener.onKeyEntered(dataSnapshot.getKey(), location); 48 | } 49 | 50 | @Override 51 | public void onDataExited(final DataSnapshot dataSnapshot) { 52 | listener.onKeyExited(dataSnapshot.getKey()); 53 | } 54 | 55 | @Override 56 | public void onDataMoved(final DataSnapshot dataSnapshot, final GeoLocation location) { 57 | listener.onKeyMoved(dataSnapshot.getKey(), location); 58 | } 59 | 60 | @Override 61 | public void onDataChanged(final DataSnapshot dataSnapshot, final GeoLocation location) { 62 | // No-op. 63 | } 64 | 65 | @Override 66 | public void onGeoQueryReady() { 67 | listener.onGeoQueryReady(); 68 | } 69 | 70 | @Override 71 | public void onGeoQueryError(final DatabaseError error) { 72 | listener.onGeoQueryError(error); 73 | } 74 | 75 | @Override 76 | public boolean equals(final Object o) { 77 | if (this == o) { 78 | return true; 79 | } 80 | if (o == null || getClass() != o.getClass()) { 81 | return false; 82 | } 83 | final EventListenerBridge that = (EventListenerBridge) o; 84 | return listener.equals(that.listener); 85 | } 86 | 87 | @Override 88 | public int hashCode() { 89 | return listener.hashCode(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/EventRaiser.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | interface EventRaiser { 4 | void raiseEvent(Runnable r); 5 | } 6 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/GeoFire.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | import static com.firebase.geofire.util.GeoUtils.capRadius; 32 | 33 | import com.firebase.geofire.core.GeoHash; 34 | import com.google.firebase.database.DataSnapshot; 35 | import com.google.firebase.database.DatabaseError; 36 | import com.google.firebase.database.DatabaseReference; 37 | import com.google.firebase.database.GenericTypeIndicator; 38 | import com.google.firebase.database.ValueEventListener; 39 | import java.util.Arrays; 40 | import java.util.HashMap; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.logging.Logger; 44 | 45 | /** 46 | * A GeoFire instance is used to store geo location data in Firebase. 47 | */ 48 | public class GeoFire { 49 | public static Logger LOGGER = Logger.getLogger("GeoFire"); 50 | 51 | /** 52 | * A listener that can be used to be notified about a successful write or an error on writing. 53 | */ 54 | public interface CompletionListener { 55 | /** 56 | * Called once a location was successfully saved on the server or an error occurred. On success, the parameter 57 | * error will be null; in case of an error, the error will be passed to this method. 58 | * 59 | * @param key The key whose location was saved 60 | * @param error The error or null if no error occurred 61 | */ 62 | void onComplete(String key, DatabaseError error); 63 | } 64 | 65 | /** 66 | * A small wrapper class to forward any events to the LocationEventListener. 67 | */ 68 | private static class LocationValueEventListener implements ValueEventListener { 69 | 70 | private final LocationCallback callback; 71 | 72 | LocationValueEventListener(LocationCallback callback) { 73 | this.callback = callback; 74 | } 75 | 76 | @Override 77 | public void onDataChange(DataSnapshot dataSnapshot) { 78 | if (dataSnapshot.getValue() == null) { 79 | this.callback.onLocationResult(dataSnapshot.getKey(), null); 80 | } else { 81 | GeoLocation location = GeoFire.getLocationValue(dataSnapshot); 82 | if (location != null) { 83 | this.callback.onLocationResult(dataSnapshot.getKey(), location); 84 | } else { 85 | String message = "GeoFire data has invalid format: " + dataSnapshot.getValue(); 86 | this.callback.onCancelled(DatabaseError.fromException(new Throwable(message))); 87 | } 88 | } 89 | } 90 | 91 | @Override 92 | public void onCancelled(DatabaseError databaseError) { 93 | this.callback.onCancelled(databaseError); 94 | } 95 | } 96 | 97 | public static GeoLocation getLocationValue(DataSnapshot dataSnapshot) { 98 | try { 99 | GenericTypeIndicator> typeIndicator = new GenericTypeIndicator>() {}; 100 | Map data = dataSnapshot.getValue(typeIndicator); 101 | List location = (List) data.get("l"); 102 | Number latitudeObj = (Number) location.get(0); 103 | Number longitudeObj = (Number) location.get(1); 104 | double latitude = latitudeObj.doubleValue(); 105 | double longitude = longitudeObj.doubleValue(); 106 | if (location.size() == 2 && GeoLocation.coordinatesValid(latitude, longitude)) { 107 | return new GeoLocation(latitude, longitude); 108 | } else { 109 | return null; 110 | } 111 | } catch (NullPointerException e) { 112 | return null; 113 | } catch (ClassCastException e) { 114 | return null; 115 | } 116 | } 117 | 118 | private final DatabaseReference databaseReference; 119 | private final EventRaiser eventRaiser; 120 | 121 | /** 122 | * Creates a new GeoFire instance at the given Firebase reference. 123 | * 124 | * @param databaseReference The Firebase reference this GeoFire instance uses 125 | */ 126 | public GeoFire(DatabaseReference databaseReference) { 127 | this.databaseReference = databaseReference; 128 | this.eventRaiser = new ThreadEventRaiser(); 129 | } 130 | 131 | /** 132 | * @return The Firebase reference this GeoFire instance uses 133 | */ 134 | public DatabaseReference getDatabaseReference() { 135 | return this.databaseReference; 136 | } 137 | 138 | DatabaseReference getDatabaseRefForKey(String key) { 139 | return this.databaseReference.child(key); 140 | } 141 | 142 | /** 143 | * Sets the location for a given key. 144 | * 145 | * @param key The key to save the location for 146 | * @param location The location of this key 147 | */ 148 | public void setLocation(String key, GeoLocation location) { 149 | this.setLocation(key, location, null); 150 | } 151 | 152 | /** 153 | * Sets the location for a given key. 154 | * 155 | * @param key The key to save the location for 156 | * @param location The location of this key 157 | * @param completionListener A listener that is called once the location was successfully saved on the server or an 158 | * error occurred 159 | */ 160 | public void setLocation(final String key, final GeoLocation location, final CompletionListener completionListener) { 161 | if (key == null) { 162 | throw new NullPointerException(); 163 | } 164 | DatabaseReference keyRef = this.getDatabaseRefForKey(key); 165 | GeoHash geoHash = new GeoHash(location); 166 | Map updates = new HashMap<>(); 167 | updates.put("g", geoHash.getGeoHashString()); 168 | updates.put("l", Arrays.asList(location.latitude, location.longitude)); 169 | if (completionListener != null) { 170 | keyRef.setValue(updates, geoHash.getGeoHashString(), new DatabaseReference.CompletionListener() { 171 | @Override 172 | public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { 173 | completionListener.onComplete(key, databaseError); 174 | } 175 | }); 176 | } else { 177 | Object priority = geoHash.getGeoHashString(); 178 | keyRef.setValueAsync(updates, priority); 179 | } 180 | } 181 | 182 | /** 183 | * Removes the location for a key from this GeoFire. 184 | * 185 | * @param key The key to remove from this GeoFire 186 | */ 187 | public void removeLocation(String key) { 188 | this.removeLocation(key, null); 189 | } 190 | 191 | /** 192 | * Removes the location for a key from this GeoFire. 193 | * 194 | * @param key The key to remove from this GeoFire 195 | * @param completionListener A completion listener that is called once the location is successfully removed 196 | * from the server or an error occurred 197 | */ 198 | public void removeLocation(final String key, final CompletionListener completionListener) { 199 | if (key == null) { 200 | throw new NullPointerException(); 201 | } 202 | DatabaseReference keyRef = this.getDatabaseRefForKey(key); 203 | if (completionListener != null) { 204 | keyRef.setValue(null, new DatabaseReference.CompletionListener() { 205 | @Override 206 | public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { 207 | completionListener.onComplete(key, databaseError); 208 | } 209 | }); 210 | } else { 211 | keyRef.removeValueAsync(); 212 | } 213 | } 214 | 215 | /** 216 | * Gets the current location for a key and calls the callback with the current value. 217 | * 218 | * @param key The key whose location to get 219 | * @param callback The callback that is called once the location is retrieved 220 | */ 221 | public void getLocation(String key, LocationCallback callback) { 222 | DatabaseReference keyRef = this.getDatabaseRefForKey(key); 223 | LocationValueEventListener valueListener = new LocationValueEventListener(callback); 224 | keyRef.addListenerForSingleValueEvent(valueListener); 225 | } 226 | 227 | /** 228 | * Returns a new Query object centered at the given location and with the given radius. 229 | * 230 | * @param center The center of the query 231 | * @param radius The radius of the query, in kilometers. The maximum radius that is 232 | * supported is about 8587km. If a radius bigger than this is passed we'll cap it. 233 | * @return The new GeoQuery object 234 | */ 235 | public GeoQuery queryAtLocation(GeoLocation center, double radius) { 236 | return new GeoQuery(this, center, capRadius(radius)); 237 | } 238 | 239 | public void raiseEvent(Runnable r) { 240 | this.eventRaiser.raiseEvent(r); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/GeoLocation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | /** 32 | * A wrapper class for location coordinates. 33 | */ 34 | public final class GeoLocation { 35 | 36 | /** The latitude of this location in the range of [-90, 90] */ 37 | public final double latitude; 38 | 39 | /** The longitude of this location in the range of [-180, 180] */ 40 | public final double longitude; 41 | 42 | /** 43 | * Creates a new GeoLocation with the given latitude and longitude. 44 | * 45 | * @throws IllegalArgumentException If the coordinates are not valid geo coordinates 46 | * @param latitude The latitude in the range of [-90, 90] 47 | * @param longitude The longitude in the range of [-180, 180] 48 | */ 49 | public GeoLocation(double latitude, double longitude) { 50 | if (!GeoLocation.coordinatesValid(latitude, longitude)) { 51 | throw new IllegalArgumentException("Not a valid geo location: " + latitude + ", " + longitude); 52 | } 53 | this.latitude = latitude; 54 | this.longitude = longitude; 55 | } 56 | 57 | /** 58 | * Checks if these coordinates are valid geo coordinates. 59 | * @param latitude The latitude must be in the range [-90, 90] 60 | * @param longitude The longitude must be in the range [-180, 180] 61 | * @return True if these are valid geo coordinates 62 | */ 63 | public static boolean coordinatesValid(double latitude, double longitude) { 64 | return latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; 65 | } 66 | 67 | @Override 68 | public boolean equals(Object o) { 69 | if (this == o) return true; 70 | if (o == null || getClass() != o.getClass()) return false; 71 | 72 | GeoLocation that = (GeoLocation) o; 73 | 74 | if (Double.compare(that.latitude, latitude) != 0) return false; 75 | if (Double.compare(that.longitude, longitude) != 0) return false; 76 | 77 | return true; 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | int result; 83 | long temp; 84 | temp = Double.doubleToLongBits(latitude); 85 | result = (int) (temp ^ (temp >>> 32)); 86 | temp = Double.doubleToLongBits(longitude); 87 | result = 31 * result + (int) (temp ^ (temp >>> 32)); 88 | return result; 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | return "GeoLocation(" + latitude + ", " + longitude + ")"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/GeoQuery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | import com.firebase.geofire.core.GeoHash; 32 | import com.firebase.geofire.core.GeoHashQuery; 33 | import com.firebase.geofire.util.GeoUtils; 34 | import java.util.HashMap; 35 | import java.util.HashSet; 36 | import java.util.Iterator; 37 | import java.util.Map; 38 | import java.util.Set; 39 | import com.google.firebase.database.ChildEventListener; 40 | import com.google.firebase.database.DataSnapshot; 41 | import com.google.firebase.database.DatabaseError; 42 | import com.google.firebase.database.DatabaseReference; 43 | import com.google.firebase.database.Query; 44 | import com.google.firebase.database.ValueEventListener; 45 | 46 | import static com.firebase.geofire.util.GeoUtils.capRadius; 47 | 48 | /** 49 | * A GeoQuery object can be used for geo queries in a given circle. The GeoQuery class is thread safe. 50 | */ 51 | public class GeoQuery { 52 | private static final int KILOMETER_TO_METER = 1000; 53 | 54 | private static class LocationInfo { 55 | final GeoLocation location; 56 | final boolean inGeoQuery; 57 | final GeoHash geoHash; 58 | final DataSnapshot dataSnapshot; 59 | 60 | public LocationInfo(GeoLocation location, boolean inGeoQuery, DataSnapshot dataSnapshot) { 61 | this.location = location; 62 | this.inGeoQuery = inGeoQuery; 63 | this.geoHash = new GeoHash(location); 64 | this.dataSnapshot = dataSnapshot; 65 | } 66 | } 67 | 68 | private final ChildEventListener childEventLister = new ChildEventListener() { 69 | @Override 70 | public void onChildAdded(DataSnapshot dataSnapshot, String s) { 71 | synchronized (GeoQuery.this) { 72 | GeoQuery.this.childAdded(dataSnapshot); 73 | } 74 | } 75 | 76 | @Override 77 | public void onChildChanged(DataSnapshot dataSnapshot, String s) { 78 | synchronized (GeoQuery.this) { 79 | GeoQuery.this.childChanged(dataSnapshot); 80 | } 81 | } 82 | 83 | @Override 84 | public void onChildRemoved(DataSnapshot dataSnapshot) { 85 | synchronized (GeoQuery.this) { 86 | GeoQuery.this.childRemoved(dataSnapshot); 87 | } 88 | } 89 | 90 | @Override 91 | public synchronized void onChildMoved(DataSnapshot dataSnapshot, String s) { 92 | // ignore, this should be handled by onChildChanged 93 | } 94 | 95 | @Override 96 | public synchronized void onCancelled(DatabaseError databaseError) { 97 | // ignore, our API does not support onCancelled 98 | } 99 | }; 100 | 101 | private final GeoFire geoFire; 102 | private final Set eventListeners = new HashSet<>(); 103 | private final Map firebaseQueries = new HashMap<>(); 104 | private final Set outstandingQueries = new HashSet<>(); 105 | private final Map locationInfos = new HashMap<>(); 106 | private GeoLocation center; 107 | private double radius; 108 | private Set queries; 109 | 110 | /** 111 | * Creates a new GeoQuery object centered at the given location and with the given radius. 112 | * @param geoFire The GeoFire object this GeoQuery uses 113 | * @param center The center of this query 114 | * @param radius The radius of the query, in kilometers. The maximum radius that is 115 | * supported is about 8587km. If a radius bigger than this is passed we'll cap it. 116 | */ 117 | GeoQuery(GeoFire geoFire, GeoLocation center, double radius) { 118 | this.geoFire = geoFire; 119 | this.center = center; 120 | this.radius = radius * KILOMETER_TO_METER; // Convert from kilometers to meters. 121 | } 122 | 123 | private boolean locationIsInQuery(GeoLocation location) { 124 | return GeoUtils.distance(location, center) <= this.radius; 125 | } 126 | 127 | private void updateLocationInfo(final DataSnapshot dataSnapshot, final GeoLocation location) { 128 | String key = dataSnapshot.getKey(); 129 | LocationInfo oldInfo = this.locationInfos.get(key); 130 | boolean isNew = oldInfo == null; 131 | final boolean changedLocation = oldInfo != null && !oldInfo.location.equals(location); 132 | boolean wasInQuery = oldInfo != null && oldInfo.inGeoQuery; 133 | 134 | boolean isInQuery = this.locationIsInQuery(location); 135 | if ((isNew || !wasInQuery) && isInQuery) { 136 | for (final GeoQueryDataEventListener listener: this.eventListeners) { 137 | this.geoFire.raiseEvent(new Runnable() { 138 | @Override 139 | public void run() { 140 | listener.onDataEntered(dataSnapshot, location); 141 | } 142 | }); 143 | } 144 | } else if (!isNew && isInQuery) { 145 | for (final GeoQueryDataEventListener listener: this.eventListeners) { 146 | this.geoFire.raiseEvent(new Runnable() { 147 | @Override 148 | public void run() { 149 | if (changedLocation) { 150 | listener.onDataMoved(dataSnapshot, location); 151 | } 152 | 153 | listener.onDataChanged(dataSnapshot, location); 154 | } 155 | }); 156 | } 157 | } else if (wasInQuery && !isInQuery) { 158 | for (final GeoQueryDataEventListener listener: this.eventListeners) { 159 | this.geoFire.raiseEvent(new Runnable() { 160 | @Override 161 | public void run() { 162 | listener.onDataExited(dataSnapshot); 163 | } 164 | }); 165 | } 166 | } 167 | LocationInfo newInfo = new LocationInfo(location, this.locationIsInQuery(location), dataSnapshot); 168 | this.locationInfos.put(key, newInfo); 169 | } 170 | 171 | private boolean geoHashQueriesContainGeoHash(GeoHash geoHash) { 172 | if (this.queries == null) { 173 | return false; 174 | } 175 | for (GeoHashQuery query: this.queries) { 176 | if (query.containsGeoHash(geoHash)) { 177 | return true; 178 | } 179 | } 180 | return false; 181 | } 182 | 183 | private void reset() { 184 | for(Map.Entry entry: this.firebaseQueries.entrySet()) { 185 | entry.getValue().removeEventListener(this.childEventLister); 186 | } 187 | this.outstandingQueries.clear(); 188 | this.firebaseQueries.clear(); 189 | this.queries = null; 190 | this.locationInfos.clear(); 191 | } 192 | 193 | private boolean hasListeners() { 194 | return !this.eventListeners.isEmpty(); 195 | } 196 | 197 | private boolean canFireReady() { 198 | return this.outstandingQueries.isEmpty(); 199 | } 200 | 201 | private void checkAndFireReady() { 202 | if (canFireReady()) { 203 | for (final GeoQueryDataEventListener listener: this.eventListeners) { 204 | this.geoFire.raiseEvent(new Runnable() { 205 | @Override 206 | public void run() { 207 | listener.onGeoQueryReady(); 208 | } 209 | }); 210 | } 211 | } 212 | } 213 | 214 | private void addValueToReadyListener(final Query firebase, final GeoHashQuery query) { 215 | firebase.addListenerForSingleValueEvent(new ValueEventListener() { 216 | @Override 217 | public void onDataChange(DataSnapshot dataSnapshot) { 218 | synchronized (GeoQuery.this) { 219 | GeoQuery.this.outstandingQueries.remove(query); 220 | GeoQuery.this.checkAndFireReady(); 221 | } 222 | } 223 | 224 | @Override 225 | public void onCancelled(final DatabaseError databaseError) { 226 | synchronized (GeoQuery.this) { 227 | for (final GeoQueryDataEventListener listener : GeoQuery.this.eventListeners) { 228 | GeoQuery.this.geoFire.raiseEvent(new Runnable() { 229 | @Override 230 | public void run() { 231 | listener.onGeoQueryError(databaseError); 232 | } 233 | }); 234 | } 235 | } 236 | } 237 | }); 238 | } 239 | 240 | private void setupQueries() { 241 | Set oldQueries = (this.queries == null) ? new HashSet() : this.queries; 242 | Set newQueries = GeoHashQuery.queriesAtLocation(center, radius); 243 | this.queries = newQueries; 244 | for (GeoHashQuery query: oldQueries) { 245 | if (!newQueries.contains(query)) { 246 | firebaseQueries.get(query).removeEventListener(this.childEventLister); 247 | firebaseQueries.remove(query); 248 | outstandingQueries.remove(query); 249 | } 250 | } 251 | for (final GeoHashQuery query: newQueries) { 252 | if (!oldQueries.contains(query)) { 253 | outstandingQueries.add(query); 254 | DatabaseReference databaseReference = this.geoFire.getDatabaseReference(); 255 | Query firebaseQuery = databaseReference.orderByChild("g").startAt(query.getStartValue()).endAt(query.getEndValue()); 256 | firebaseQuery.addChildEventListener(this.childEventLister); 257 | addValueToReadyListener(firebaseQuery, query); 258 | firebaseQueries.put(query, firebaseQuery); 259 | } 260 | } 261 | for (Map.Entry info: this.locationInfos.entrySet()) { 262 | LocationInfo oldLocationInfo = info.getValue(); 263 | 264 | if (oldLocationInfo != null) { 265 | updateLocationInfo(oldLocationInfo.dataSnapshot, oldLocationInfo.location); 266 | } 267 | } 268 | // remove locations that are not part of the geo query anymore 269 | Iterator> it = this.locationInfos.entrySet().iterator(); 270 | while (it.hasNext()) { 271 | Map.Entry entry = it.next(); 272 | if (!this.geoHashQueriesContainGeoHash(entry.getValue().geoHash)) { 273 | it.remove(); 274 | } 275 | } 276 | 277 | checkAndFireReady(); 278 | } 279 | 280 | private void childAdded(DataSnapshot dataSnapshot) { 281 | GeoLocation location = GeoFire.getLocationValue(dataSnapshot); 282 | if (location != null) { 283 | this.updateLocationInfo(dataSnapshot, location); 284 | } else { 285 | throw new AssertionError("Got Datasnapshot without location with key " + dataSnapshot.getKey()); 286 | } 287 | } 288 | 289 | private void childChanged(DataSnapshot dataSnapshot) { 290 | GeoLocation location = GeoFire.getLocationValue(dataSnapshot); 291 | if (location != null) { 292 | this.updateLocationInfo(dataSnapshot, location); 293 | } else { 294 | throw new AssertionError("Got Datasnapshot without location with key " + dataSnapshot.getKey()); 295 | } 296 | } 297 | 298 | private void childRemoved(DataSnapshot dataSnapshot) { 299 | final String key = dataSnapshot.getKey(); 300 | final LocationInfo info = this.locationInfos.get(key); 301 | if (info != null) { 302 | this.geoFire.getDatabaseRefForKey(key).addListenerForSingleValueEvent(new ValueEventListener() { 303 | @Override 304 | public void onDataChange(final DataSnapshot dataSnapshot) { 305 | synchronized(GeoQuery.this) { 306 | GeoLocation location = GeoFire.getLocationValue(dataSnapshot); 307 | GeoHash hash = (location != null) ? new GeoHash(location) : null; 308 | if (hash == null || !GeoQuery.this.geoHashQueriesContainGeoHash(hash)) { 309 | final LocationInfo info = locationInfos.remove(key); 310 | 311 | if (info != null && info.inGeoQuery) { 312 | for (final GeoQueryDataEventListener listener: GeoQuery.this.eventListeners) { 313 | GeoQuery.this.geoFire.raiseEvent(new Runnable() { 314 | @Override 315 | public void run() { 316 | listener.onDataExited(info.dataSnapshot); 317 | } 318 | }); 319 | } 320 | } 321 | } 322 | } 323 | } 324 | 325 | @Override 326 | public void onCancelled(DatabaseError databaseError) { 327 | // tough luck 328 | } 329 | }); 330 | } 331 | } 332 | 333 | /** 334 | * Adds a new GeoQueryEventListener to this GeoQuery. 335 | * 336 | * @throws IllegalArgumentException If this listener was already added 337 | * 338 | * @param listener The listener to add 339 | */ 340 | public synchronized void addGeoQueryEventListener(final GeoQueryEventListener listener) { 341 | addGeoQueryDataEventListener(new EventListenerBridge(listener)); 342 | } 343 | 344 | /** 345 | * Adds a new GeoQueryEventListener to this GeoQuery. 346 | * 347 | * @throws IllegalArgumentException If this listener was already added 348 | * 349 | * @param listener The listener to add 350 | */ 351 | public synchronized void addGeoQueryDataEventListener(final GeoQueryDataEventListener listener) { 352 | if (eventListeners.contains(listener)) { 353 | throw new IllegalArgumentException("Added the same listener twice to a GeoQuery!"); 354 | } 355 | eventListeners.add(listener); 356 | if (this.queries == null) { 357 | this.setupQueries(); 358 | } else { 359 | for (final Map.Entry entry: this.locationInfos.entrySet()) { 360 | final String key = entry.getKey(); 361 | final LocationInfo info = entry.getValue(); 362 | 363 | if (info.inGeoQuery) { 364 | this.geoFire.raiseEvent(new Runnable() { 365 | @Override 366 | public void run() { 367 | listener.onDataEntered(info.dataSnapshot, info.location); 368 | } 369 | }); 370 | } 371 | } 372 | if (this.canFireReady()) { 373 | this.geoFire.raiseEvent(new Runnable() { 374 | @Override 375 | public void run() { 376 | listener.onGeoQueryReady(); 377 | } 378 | }); 379 | } 380 | } 381 | } 382 | 383 | /** 384 | * Removes an event listener. 385 | * 386 | * @throws IllegalArgumentException If the listener was removed already or never added 387 | * 388 | * @param listener The listener to remove 389 | */ 390 | public synchronized void removeGeoQueryEventListener(GeoQueryEventListener listener) { 391 | removeGeoQueryEventListener(new EventListenerBridge(listener)); 392 | } 393 | 394 | /** 395 | * Removes an event listener. 396 | * 397 | * @throws IllegalArgumentException If the listener was removed already or never added 398 | * 399 | * @param listener The listener to remove 400 | */ 401 | public synchronized void removeGeoQueryEventListener(final GeoQueryDataEventListener listener) { 402 | if (!eventListeners.contains(listener)) { 403 | throw new IllegalArgumentException("Trying to remove listener that was removed or not added!"); 404 | } 405 | eventListeners.remove(listener); 406 | if (!this.hasListeners()) { 407 | reset(); 408 | } 409 | } 410 | 411 | /** 412 | * Removes all event listeners from this GeoQuery. 413 | */ 414 | public synchronized void removeAllListeners() { 415 | eventListeners.clear(); 416 | reset(); 417 | } 418 | 419 | /** 420 | * Returns the current center of this query. 421 | * @return The current center 422 | */ 423 | public synchronized GeoLocation getCenter() { 424 | return center; 425 | } 426 | 427 | /** 428 | * Sets the new center of this query and triggers new events if necessary. 429 | * @param center The new center 430 | */ 431 | public synchronized void setCenter(GeoLocation center) { 432 | this.center = center; 433 | if (this.hasListeners()) { 434 | this.setupQueries(); 435 | } 436 | } 437 | 438 | /** 439 | * Returns the radius of the query, in kilometers. 440 | * @return The radius of this query, in kilometers 441 | */ 442 | public synchronized double getRadius() { 443 | // convert from meters 444 | return radius / KILOMETER_TO_METER; 445 | } 446 | 447 | /** 448 | * Sets the radius of this query, in kilometers, and triggers new events if necessary. 449 | * @param radius The radius of the query, in kilometers. The maximum radius that is 450 | * supported is about 8587km. If a radius bigger than this is passed we'll cap it. 451 | */ 452 | public synchronized void setRadius(double radius) { 453 | // convert to meters 454 | this.radius = capRadius(radius) * KILOMETER_TO_METER; 455 | if (this.hasListeners()) { 456 | this.setupQueries(); 457 | } 458 | } 459 | 460 | /** 461 | * Sets the center and radius (in kilometers) of this query, and triggers new events if necessary. 462 | * @param center The new center 463 | * @param radius The radius of the query, in kilometers. The maximum radius that is 464 | * supported is about 8587km. If a radius bigger than this is passed we'll cap it. 465 | */ 466 | public synchronized void setLocation(GeoLocation center, double radius) { 467 | this.center = center; 468 | // convert radius to meters 469 | this.radius = capRadius(radius) * KILOMETER_TO_METER; 470 | if (this.hasListeners()) { 471 | this.setupQueries(); 472 | } 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/GeoQueryDataEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | import com.google.firebase.database.DataSnapshot; 32 | import com.google.firebase.database.DatabaseError; 33 | 34 | /** 35 | * GeoQuery notifies listeners with this interface about dataSnapshots that entered, exited, or moved within the query. 36 | */ 37 | public interface GeoQueryDataEventListener { 38 | 39 | /** 40 | * Called if a dataSnapshot entered the search area of the GeoQuery. This method is called for every dataSnapshot currently in the 41 | * search area at the time of adding the listener. 42 | * 43 | * This method is once per datasnapshot, and is only called again if onDataExited was called in the meantime. 44 | * 45 | * @param dataSnapshot The associated dataSnapshot that entered the search area 46 | * @param location The location for this dataSnapshot as a GeoLocation object 47 | */ 48 | void onDataEntered(DataSnapshot dataSnapshot, GeoLocation location); 49 | 50 | /** 51 | * Called if a datasnapshot exited the search area of the GeoQuery. This is method is only called if onDataEntered was called 52 | * for the datasnapshot. 53 | * 54 | * @param dataSnapshot The associated dataSnapshot that exited the search area 55 | */ 56 | void onDataExited(DataSnapshot dataSnapshot); 57 | 58 | /** 59 | * Called if a dataSnapshot moved within the search area. 60 | * 61 | * This method can be called multiple times. 62 | * 63 | * @param dataSnapshot The associated dataSnapshot that moved within the search area 64 | * @param location The location for this dataSnapshot as a GeoLocation object 65 | */ 66 | void onDataMoved(DataSnapshot dataSnapshot, GeoLocation location); 67 | 68 | /** 69 | * Called if a dataSnapshot changed within the search area. 70 | * 71 | * An onDataMoved() is always followed by onDataChanged() but it is be possible to see 72 | * onDataChanged() without an preceding onDataMoved(). 73 | * 74 | * This method can be called multiple times for a single location change, due to the way 75 | * the Realtime Database handles floating point numbers. 76 | * 77 | * Note: this method is not related to ValueEventListener#onDataChange(DataSnapshot). 78 | * 79 | * @param dataSnapshot The associated dataSnapshot that moved within the search area 80 | * @param location The location for this dataSnapshot as a GeoLocation object 81 | */ 82 | void onDataChanged(DataSnapshot dataSnapshot, GeoLocation location); 83 | 84 | /** 85 | * Called once all initial GeoFire data has been loaded and the relevant events have been fired for this query. 86 | * Every time the query criteria is updated, this observer will be called after the updated query has fired the 87 | * appropriate dataSnapshot entered or dataSnapshot exited events. 88 | */ 89 | void onGeoQueryReady(); 90 | 91 | /** 92 | * Called in case an error occurred while retrieving locations for a query, e.g. violating security rules. 93 | * @param error The error that occurred while retrieving the query 94 | */ 95 | void onGeoQueryError(DatabaseError error); 96 | 97 | } 98 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/GeoQueryEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | import com.google.firebase.database.DatabaseError; 32 | 33 | /** 34 | * GeoQuery notifies listeners with this interface about keys that entered, exited, or moved within the query. 35 | */ 36 | public interface GeoQueryEventListener { 37 | 38 | /** 39 | * Called if a key entered the search area of the GeoQuery. This method is called for every key currently in the 40 | * search area at the time of adding the listener. 41 | * 42 | * This method is once per key, and is only called again if onKeyExited was called in the meantime. 43 | * 44 | * @param key The key that entered the search area 45 | * @param location The location for this key as a GeoLocation object 46 | */ 47 | void onKeyEntered(String key, GeoLocation location); 48 | 49 | /** 50 | * Called if a key exited the search area of the GeoQuery. This is method is only called if onKeyEntered was called 51 | * for the key. 52 | * 53 | * @param key The key that exited the search area 54 | */ 55 | void onKeyExited(String key); 56 | 57 | /** 58 | * Called if a key moved within the search area. 59 | * 60 | * This method can be called multiple times. 61 | * 62 | * @param key The key that moved within the search area 63 | * @param location The location for this key as a GeoLocation object 64 | */ 65 | void onKeyMoved(String key, GeoLocation location); 66 | 67 | /** 68 | * Called once all initial GeoFire data has been loaded and the relevant events have been fired for this query. 69 | * Every time the query criteria is updated, this observer will be called after the updated query has fired the 70 | * appropriate key entered or key exited events. 71 | */ 72 | void onGeoQueryReady(); 73 | 74 | /** 75 | * Called in case an error occurred while retrieving locations for a query, e.g. violating security rules. 76 | * @param error The error that occurred while retrieving the query 77 | */ 78 | void onGeoQueryError(DatabaseError error); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/LocationCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Firebase GeoFire Java Library 3 | * 4 | * Copyright © 2014 Firebase - All Rights Reserved 5 | * https://www.firebase.com 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binaryform must reproduce the above copyright notice, 14 | * this list of conditions and the following disclaimer in the documentation 15 | * and/or other materials provided with the distribution. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY FIREBASE AS IS AND ANY EXPRESS OR 18 | * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | * EVENT SHALL FIREBASE BE LIABLE FOR ANY DIRECT, 21 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 25 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 26 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package com.firebase.geofire; 30 | 31 | import com.google.firebase.database.DatabaseError; 32 | 33 | /** 34 | * Classes implementing this interface can be used to receive the locations stored in GeoFire. 35 | */ 36 | public interface LocationCallback { 37 | 38 | /** 39 | * This method is called with the current location of the key. location will be null if there is no location 40 | * stored in GeoFire for the key. 41 | * @param key The key whose location we are getting 42 | * @param location The location of the key 43 | */ 44 | void onLocationResult(String key, GeoLocation location); 45 | 46 | /** 47 | * Called if the callback could not be added due to failure on the server or security rules. 48 | * @param databaseError The error that occurred 49 | */ 50 | void onCancelled(DatabaseError databaseError); 51 | 52 | } 53 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/ThreadEventRaiser.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | 6 | class ThreadEventRaiser implements EventRaiser { 7 | 8 | private final ExecutorService executorService; 9 | 10 | public ThreadEventRaiser() { 11 | this.executorService = Executors.newSingleThreadExecutor(); 12 | } 13 | 14 | @Override 15 | public void raiseEvent(Runnable r) { 16 | this.executorService.submit(r); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/core/GeoHash.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.core; 2 | 3 | import com.firebase.geofire.GeoLocation; 4 | import com.firebase.geofire.util.Base32Utils; 5 | 6 | import static java.util.Locale.US; 7 | 8 | public class GeoHash { 9 | private final String geoHash; 10 | 11 | // The default precision of a geohash 12 | private static final int DEFAULT_PRECISION = 10; 13 | 14 | // The maximal precision of a geohash 15 | public static final int MAX_PRECISION = 22; 16 | 17 | // The maximal number of bits precision for a geohash 18 | public static final int MAX_PRECISION_BITS = MAX_PRECISION * Base32Utils.BITS_PER_BASE32_CHAR; 19 | 20 | public GeoHash(double latitude, double longitude) { 21 | this(latitude, longitude, DEFAULT_PRECISION); 22 | } 23 | 24 | public GeoHash(GeoLocation location) { 25 | this(location.latitude, location.longitude, DEFAULT_PRECISION); 26 | } 27 | 28 | public GeoHash(double latitude, double longitude, int precision) { 29 | if (precision < 1) { 30 | throw new IllegalArgumentException("Precision of GeoHash must be larger than zero!"); 31 | } 32 | if (precision > MAX_PRECISION) { 33 | throw new IllegalArgumentException("Precision of a GeoHash must be less than " + (MAX_PRECISION + 1) + "!"); 34 | } 35 | if (!GeoLocation.coordinatesValid(latitude, longitude)) { 36 | throw new IllegalArgumentException(String.format(US, "Not valid location coordinates: [%f, %f]", latitude, longitude)); 37 | } 38 | double[] longitudeRange = { -180, 180 }; 39 | double[] latitudeRange = { -90, 90 }; 40 | 41 | char[] buffer = new char[precision]; 42 | 43 | for (int i = 0; i < precision; i++) { 44 | int hashValue = 0; 45 | for (int j = 0; j < Base32Utils.BITS_PER_BASE32_CHAR; j++) { 46 | boolean even = (((i*Base32Utils.BITS_PER_BASE32_CHAR) + j) % 2) == 0; 47 | double val = even ? longitude : latitude; 48 | double[] range = even ? longitudeRange : latitudeRange; 49 | double mid = (range[0] + range[1])/2; 50 | if (val > mid) { 51 | hashValue = (hashValue << 1) + 1; 52 | range[0] = mid; 53 | } else { 54 | hashValue = hashValue << 1; 55 | range[1] = mid; 56 | } 57 | } 58 | buffer[i] = Base32Utils.valueToBase32Char(hashValue); 59 | } 60 | this.geoHash = new String(buffer); 61 | } 62 | 63 | public GeoHash(String hash) { 64 | if (hash.length() == 0 || !Base32Utils.isValidBase32String(hash)) { 65 | throw new IllegalArgumentException("Not a valid geoHash: " + hash); 66 | } 67 | this.geoHash = hash; 68 | } 69 | 70 | public String getGeoHashString() { 71 | return this.geoHash; 72 | } 73 | 74 | @Override 75 | public boolean equals(Object o) { 76 | if (this == o) return true; 77 | if (o == null || getClass() != o.getClass()) return false; 78 | 79 | GeoHash other = (GeoHash) o; 80 | 81 | return this.geoHash.equals(other.geoHash); 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return "GeoHash{" + 87 | "geoHash='" + geoHash + '\'' + 88 | '}'; 89 | } 90 | 91 | @Override 92 | public int hashCode() { 93 | return this.geoHash.hashCode(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/core/GeoHashQuery.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.core; 2 | 3 | import com.firebase.geofire.GeoLocation; 4 | import com.firebase.geofire.util.Base32Utils; 5 | import com.firebase.geofire.util.Constants; 6 | import com.firebase.geofire.util.GeoUtils; 7 | 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | public class GeoHashQuery { 12 | 13 | public static final class Utils { 14 | 15 | private Utils() { 16 | throw new AssertionError("No instances."); 17 | } 18 | 19 | public static double bitsLatitude(double resolution) { 20 | return Math.min(Math.log(Constants.EARTH_MERIDIONAL_CIRCUMFERENCE/2/resolution)/Math.log(2), 21 | GeoHash.MAX_PRECISION_BITS); 22 | } 23 | 24 | public static double bitsLongitude(double resolution, double latitude) { 25 | double degrees = GeoUtils.distanceToLongitudeDegrees(resolution, latitude); 26 | return (Math.abs(degrees) > 0) ? Math.max(1, Math.log(360/degrees)/Math.log(2)) : 1; 27 | } 28 | 29 | public static int bitsForBoundingBox(GeoLocation location, double size) { 30 | double latitudeDegreesDelta = GeoUtils.distanceToLatitudeDegrees(size); 31 | double latitudeNorth = Math.min(90, location.latitude + latitudeDegreesDelta); 32 | double latitudeSouth = Math.max(-90, location.latitude - latitudeDegreesDelta); 33 | int bitsLatitude = (int)Math.floor(Utils.bitsLatitude(size)) *2; 34 | int bitsLongitudeNorth = (int)Math.floor(Utils.bitsLongitude(size, latitudeNorth)) *2 - 1; 35 | int bitsLongitudeSouth = (int)Math.floor(Utils.bitsLongitude(size, latitudeSouth)) *2 - 1; 36 | return Math.min(bitsLatitude, Math.min(bitsLongitudeNorth, bitsLongitudeSouth)); 37 | } 38 | } 39 | 40 | private final String startValue; 41 | private final String endValue; 42 | 43 | public GeoHashQuery(String startValue, String endValue) { 44 | this.startValue = startValue; 45 | this.endValue = endValue; 46 | } 47 | 48 | public static GeoHashQuery queryForGeoHash(GeoHash geohash, int bits) { 49 | String hash = geohash.getGeoHashString(); 50 | int precision = (int)Math.ceil((double)bits/Base32Utils.BITS_PER_BASE32_CHAR); 51 | if (hash.length() < precision) { 52 | return new GeoHashQuery(hash, hash+"~"); 53 | } 54 | hash = hash.substring(0, precision); 55 | String base = hash.substring(0, hash.length() - 1); 56 | int lastValue = Base32Utils.base32CharToValue(hash.charAt(hash.length() - 1)); 57 | int significantBits = bits - (base.length() * Base32Utils.BITS_PER_BASE32_CHAR); 58 | int unusedBits = Base32Utils.BITS_PER_BASE32_CHAR - significantBits; 59 | // delete unused bits 60 | int startValue = (lastValue >> unusedBits) << unusedBits; 61 | int endValue = startValue + (1 << unusedBits); 62 | String startHash = base + Base32Utils.valueToBase32Char(startValue); 63 | String endHash; 64 | if (endValue > 31) { 65 | endHash = base + "~"; 66 | } else { 67 | endHash = base + Base32Utils.valueToBase32Char(endValue); 68 | } 69 | return new GeoHashQuery(startHash, endHash); 70 | } 71 | 72 | public static Set queriesAtLocation(GeoLocation location, double radius) { 73 | int queryBits = Math.max(1, Utils.bitsForBoundingBox(location, radius)); 74 | int geoHashPrecision = (int) Math.ceil((float)queryBits /Base32Utils.BITS_PER_BASE32_CHAR); 75 | 76 | double latitude = location.latitude; 77 | double longitude = location.longitude; 78 | double latitudeDegrees = radius/Constants.METERS_PER_DEGREE_LATITUDE; 79 | double latitudeNorth = Math.min(90, latitude + latitudeDegrees); 80 | double latitudeSouth = Math.max(-90, latitude - latitudeDegrees); 81 | double longitudeDeltaNorth = GeoUtils.distanceToLongitudeDegrees(radius, latitudeNorth); 82 | double longitudeDeltaSouth = GeoUtils.distanceToLongitudeDegrees(radius, latitudeSouth); 83 | double longitudeDelta = Math.max(longitudeDeltaNorth, longitudeDeltaSouth); 84 | 85 | Set queries = new HashSet<>(); 86 | 87 | GeoHash geoHash = new GeoHash(latitude, longitude, geoHashPrecision); 88 | GeoHash geoHashW = new GeoHash(latitude, GeoUtils.wrapLongitude(longitude - longitudeDelta), geoHashPrecision); 89 | GeoHash geoHashE = new GeoHash(latitude, GeoUtils.wrapLongitude(longitude + longitudeDelta), geoHashPrecision); 90 | 91 | GeoHash geoHashN = new GeoHash(latitudeNorth, longitude, geoHashPrecision); 92 | GeoHash geoHashNW = new GeoHash(latitudeNorth, GeoUtils.wrapLongitude(longitude - longitudeDelta), geoHashPrecision); 93 | GeoHash geoHashNE = new GeoHash(latitudeNorth, GeoUtils.wrapLongitude(longitude + longitudeDelta), geoHashPrecision); 94 | 95 | GeoHash geoHashS = new GeoHash(latitudeSouth, longitude, geoHashPrecision); 96 | GeoHash geoHashSW = new GeoHash(latitudeSouth, GeoUtils.wrapLongitude(longitude - longitudeDelta), geoHashPrecision); 97 | GeoHash geoHashSE = new GeoHash(latitudeSouth, GeoUtils.wrapLongitude(longitude + longitudeDelta), geoHashPrecision); 98 | 99 | queries.add(queryForGeoHash(geoHash, queryBits)); 100 | queries.add(queryForGeoHash(geoHashE, queryBits)); 101 | queries.add(queryForGeoHash(geoHashW, queryBits)); 102 | queries.add(queryForGeoHash(geoHashN, queryBits)); 103 | queries.add(queryForGeoHash(geoHashNE, queryBits)); 104 | queries.add(queryForGeoHash(geoHashNW, queryBits)); 105 | queries.add(queryForGeoHash(geoHashS, queryBits)); 106 | queries.add(queryForGeoHash(geoHashSE, queryBits)); 107 | queries.add(queryForGeoHash(geoHashSW, queryBits)); 108 | 109 | // Join queries 110 | boolean didJoin; 111 | do { 112 | GeoHashQuery query1 = null; 113 | GeoHashQuery query2 = null; 114 | for (GeoHashQuery query: queries) { 115 | for (GeoHashQuery other: queries) { 116 | if (query != other && query.canJoinWith(other)) { 117 | query1 = query; 118 | query2 = other; 119 | break; 120 | } 121 | } 122 | } 123 | if (query1 != null && query2 != null) { 124 | queries.remove(query1); 125 | queries.remove(query2); 126 | queries.add(query1.joinWith(query2)); 127 | didJoin = true; 128 | } else { 129 | didJoin = false; 130 | } 131 | } while (didJoin); 132 | 133 | return queries; 134 | } 135 | 136 | private boolean isPrefix(GeoHashQuery other) { 137 | return (other.endValue.compareTo(this.startValue) >= 0) && 138 | (other.startValue.compareTo(this.startValue) < 0) && 139 | (other.endValue.compareTo(this.endValue) < 0); 140 | } 141 | 142 | private boolean isSuperQuery(GeoHashQuery other) { 143 | int startCompare = other.startValue.compareTo(this.startValue); 144 | return startCompare <= 0 && other.endValue.compareTo(this.endValue) >= 0; 145 | } 146 | 147 | public boolean canJoinWith(GeoHashQuery other) { 148 | return this.isPrefix(other) || other.isPrefix(this) || this.isSuperQuery(other) || other.isSuperQuery(this); 149 | } 150 | 151 | public GeoHashQuery joinWith(GeoHashQuery other) { 152 | if (other.isPrefix(this)) { 153 | return new GeoHashQuery(this.startValue, other.endValue); 154 | } else if (this.isPrefix(other)) { 155 | return new GeoHashQuery(other.startValue, this.endValue); 156 | } else if (this.isSuperQuery(other)) { 157 | return other; 158 | } else if (other.isSuperQuery(this)) { 159 | return this; 160 | } else { 161 | throw new IllegalArgumentException("Can't join these 2 queries: " + this + ", " + other); 162 | } 163 | } 164 | 165 | public boolean containsGeoHash(GeoHash hash) { 166 | String hashStr = hash.getGeoHashString(); 167 | return this.startValue.compareTo(hashStr) <= 0 && this.endValue.compareTo(hashStr) > 0; 168 | } 169 | 170 | public String getStartValue() { 171 | return this.startValue; 172 | } 173 | 174 | public String getEndValue() { 175 | return this.endValue; 176 | } 177 | 178 | @Override 179 | public boolean equals(Object o) { 180 | if (this == o) return true; 181 | if (o == null || getClass() != o.getClass()) return false; 182 | 183 | GeoHashQuery that = (GeoHashQuery) o; 184 | 185 | if (!endValue.equals(that.endValue)) return false; 186 | if (!startValue.equals(that.startValue)) return false; 187 | 188 | return true; 189 | } 190 | 191 | @Override 192 | public int hashCode() { 193 | int result = startValue.hashCode(); 194 | result = 31 * result + endValue.hashCode(); 195 | return result; 196 | } 197 | 198 | @Override 199 | public String toString() { 200 | return "GeoHashQuery{" + 201 | "startValue='" + startValue + '\'' + 202 | ", endValue='" + endValue + '\'' + 203 | '}'; 204 | } 205 | 206 | } 207 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/util/Base32Utils.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.util; 2 | 3 | public final class Base32Utils { 4 | 5 | /* number of bits per base 32 character */ 6 | public static final int BITS_PER_BASE32_CHAR = 5; 7 | 8 | private static final String BASE32_CHARS = "0123456789bcdefghjkmnpqrstuvwxyz"; 9 | 10 | private Base32Utils() { 11 | throw new AssertionError("No instances."); 12 | } 13 | 14 | public static char valueToBase32Char(int value) { 15 | if (value < 0 || value >= BASE32_CHARS.length()) { 16 | throw new IllegalArgumentException("Not a valid base32 value: " + value); 17 | } 18 | return BASE32_CHARS.charAt(value); 19 | } 20 | 21 | public static int base32CharToValue(char base32Char) { 22 | int value = BASE32_CHARS.indexOf(base32Char); 23 | if (value == -1) { 24 | throw new IllegalArgumentException("Not a valid base32 char: " + base32Char); 25 | } else { 26 | return value; 27 | } 28 | } 29 | 30 | public static boolean isValidBase32String(String string) { 31 | return string.matches("^[" + BASE32_CHARS + "]*$"); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/util/Constants.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.util; 2 | 3 | public final class Constants { 4 | 5 | // Length of a degree latitude at the equator 6 | public static final double METERS_PER_DEGREE_LATITUDE = 110574; 7 | 8 | // The equatorial circumference of the earth in meters 9 | public static final double EARTH_MERIDIONAL_CIRCUMFERENCE = 40007860; 10 | 11 | // The equatorial radius of the earth in meters 12 | public static final double EARTH_EQ_RADIUS = 6378137; 13 | 14 | // The meridional radius of the earth in meters 15 | public static final double EARTH_POLAR_RADIUS = 6357852.3; 16 | 17 | /* The following value assumes a polar radius of 18 | * r_p = 6356752.3 19 | * and an equatorial radius of 20 | * r_e = 6378137 21 | * The value is calculated as e2 == (r_e^2 - r_p^2)/(r_e^2) 22 | * Use exact value to avoid rounding errors 23 | */ 24 | public static final double EARTH_E2 = 0.00669447819799; 25 | 26 | // Cutoff for floating point calculations 27 | public static final double EPSILON = 1e-12; 28 | 29 | private Constants() { 30 | throw new AssertionError("No instances."); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common/src/main/java/com/firebase/geofire/util/GeoUtils.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.util; 2 | 3 | import com.firebase.geofire.GeoLocation; 4 | 5 | import static com.firebase.geofire.GeoFire.LOGGER; 6 | 7 | public final class GeoUtils { 8 | private static final double MAX_SUPPORTED_RADIUS = 8587; 9 | 10 | private GeoUtils() { 11 | throw new AssertionError("No instances."); 12 | } 13 | 14 | public static double distance(GeoLocation location1, GeoLocation location2) { 15 | return distance(location1.latitude, location1.longitude, location2.latitude, location2.longitude); 16 | } 17 | 18 | public static double distance(double lat1, double long1, double lat2, double long2) { 19 | // Earth's mean radius in meters 20 | final double radius = (Constants.EARTH_EQ_RADIUS + Constants.EARTH_POLAR_RADIUS)/2; 21 | double latDelta = Math.toRadians(lat1 - lat2); 22 | double lonDelta = Math.toRadians(long1 - long2); 23 | 24 | double a = (Math.sin(latDelta/2)*Math.sin(latDelta/2)) + 25 | (Math.cos(Math.toRadians(lat1))*Math.cos(Math.toRadians(lat2)) * 26 | Math.sin(lonDelta/2) * Math.sin(lonDelta/2)); 27 | return radius * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 28 | } 29 | 30 | public static double distanceToLatitudeDegrees(double distance) { 31 | return distance/Constants.METERS_PER_DEGREE_LATITUDE; 32 | } 33 | 34 | public static double distanceToLongitudeDegrees(double distance, double latitude) { 35 | double radians = Math.toRadians(latitude); 36 | double numerator = Math.cos(radians) * Constants.EARTH_EQ_RADIUS * Math.PI / 180; 37 | double denominator = 1/Math.sqrt(1 - Constants.EARTH_E2*Math.sin(radians)*Math.sin(radians)); 38 | double deltaDegrees = numerator*denominator; 39 | if (deltaDegrees < Constants.EPSILON) { 40 | return distance > 0 ? 360 : distance; 41 | } else { 42 | return Math.min(360, distance/deltaDegrees); 43 | } 44 | } 45 | 46 | public static double wrapLongitude(double longitude) { 47 | if (longitude >= -180 && longitude <= 180) { 48 | return longitude; 49 | } 50 | double adjusted = longitude + 180; 51 | if (adjusted > 0) { 52 | return (adjusted % 360.0) - 180; 53 | } else { 54 | return 180 - (-adjusted % 360); 55 | } 56 | } 57 | 58 | public static double capRadius(double radius) { 59 | if (radius > MAX_SUPPORTED_RADIUS) { 60 | LOGGER.warning("The radius is bigger than " + MAX_SUPPORTED_RADIUS + " and hence we'll use that value"); 61 | return MAX_SUPPORTED_RADIUS; 62 | } 63 | 64 | return radius; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /create-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | DIR=$(dirname "${BASH_SOURCE[0]}") 5 | pushd $DIR 6 | 7 | echo "Generating javadocs..." 8 | mvn javadoc:javadoc 9 | 10 | echo "Renaming output folder" 11 | rm -rf site/docs 12 | mkdir -p target/site/apidocs 13 | mv target/site/apidocs site/docs 14 | popd 15 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_id": "geofire-java", 3 | "firebase": "geofire-java", 4 | "public": "site", 5 | "ignore": [ 6 | "firebase.json", 7 | "**/.*", 8 | "**/node_modules/**" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | 7 | com.firebase 8 | geofire 9 | 3.0.1-SNAPSHOT 10 | ../ 11 | 12 | 13 | geofire-java 14 | jar 15 | 16 | geofire-java 17 | GeoFire is an open-source library for Android/Java that allows you to store and query a set of keys based on their geographic location. 18 | 19 | Firebase 20 | https://www.firebase.com/ 21 | 22 | https://github.com/firebase/geofire-java 23 | 24 | scm:git:git@github.com:firebase/geofire-java.git 25 | scm:git:git@github.com:firebase/geofire-java.git 26 | https://github.com/firebase/geofire-java 27 | HEAD 28 | 29 | 30 | 31 | MIT 32 | http://firebase.mit-license.org 33 | 34 | 35 | 36 | UTF-8 37 | 38 | 39 | 40 | 41 | geofire 42 | https://api.bintray.com/maven/firebase/geofire/geofire-java 43 | 44 | 45 | 46 | 47 | src/test/java 48 | 49 | 50 | 51 | 52 | com.firebase 53 | geofire-common 54 | ${project.version} 55 | 56 | 57 | 58 | 59 | com.google.firebase 60 | firebase-admin 61 | [6.0.0,) 62 | provided 63 | 64 | 65 | 66 | 67 | junit 68 | junit 69 | 4.12 70 | test 71 | 72 | 73 | com.firebase 74 | geofire-testing 75 | ${project.version} 76 | test 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /java/service-account.json.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebase/geofire-java/3975ac456afcf5c60d03bb80fdcc41075f8b19b7/java/service-account.json.enc -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoFireIT.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import com.firebase.geofire.testing.GeoFireTestingRule; 4 | import com.firebase.geofire.testing.SimpleFuture; 5 | import com.firebase.geofire.testing.TestCallback; 6 | import com.google.firebase.database.DataSnapshot; 7 | import com.google.firebase.database.DatabaseError; 8 | import com.google.firebase.database.DatabaseReference; 9 | import com.google.firebase.database.ValueEventListener; 10 | import org.junit.Assert; 11 | import org.junit.Test; 12 | import org.junit.Rule; 13 | import org.junit.runner.RunWith; 14 | import org.junit.runners.JUnit4; 15 | 16 | import java.util.Arrays; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | import java.util.concurrent.*; 20 | 21 | @RunWith(JUnit4.class) 22 | public class GeoFireIT { 23 | static final String DATABASE_URL = "https://geofiretest-8d811.firebaseio.com/"; 24 | 25 | @Rule public final GeoFireTestingRule geoFireTestingRule = new GeoFireTestingRule(DATABASE_URL); 26 | 27 | @Test 28 | public void geoFireSetsLocations() throws InterruptedException, ExecutionException, TimeoutException { 29 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 30 | geoFireTestingRule.setLocation(geoFire, "loc1", 0.1, 0.1); 31 | geoFireTestingRule.setLocation(geoFire, "loc2", 50.1, 50.1); 32 | geoFireTestingRule.setLocation(geoFire, "loc3", -89.1, -89.1, true); 33 | 34 | final SimpleFuture future = new SimpleFuture<>(); 35 | geoFire.getDatabaseReference().addListenerForSingleValueEvent(new ValueEventListener() { 36 | @Override 37 | public void onDataChange(DataSnapshot dataSnapshot) { 38 | future.put(dataSnapshot); 39 | } 40 | 41 | @Override 42 | public void onCancelled(DatabaseError databaseError) { 43 | future.put(databaseError); 44 | } 45 | }); 46 | 47 | Map expected = new HashMap<>(); 48 | expected.put("loc1", new HashMap() {{ 49 | put("l", Arrays.asList(0.1, 0.1)); 50 | put("g", "s000d60yd1"); 51 | }}); 52 | expected.put("loc2", new HashMap() {{ 53 | put("l", Arrays.asList(50.1, 50.1)); 54 | put("g", "v0gth03tws"); 55 | }}); 56 | expected.put("loc3", new HashMap() {{ 57 | put("l", Arrays.asList(-89.1, -89.1)); 58 | put("g", "400th7z6gs"); 59 | }}); 60 | Object result = future.get(geoFireTestingRule.timeout, TimeUnit.SECONDS); 61 | Assert.assertEquals(expected, ((DataSnapshot)result).getValue()); 62 | } 63 | 64 | @Test 65 | public void getLocationReturnsCorrectLocation() throws InterruptedException, ExecutionException, TimeoutException { 66 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 67 | 68 | TestCallback testCallback1 = new TestCallback(); 69 | geoFire.getLocation("loc1", testCallback1); 70 | Assert.assertEquals(TestCallback.noLocation("loc1"), testCallback1.getCallbackValue()); 71 | 72 | TestCallback testCallback2 = new TestCallback(); 73 | geoFireTestingRule.setLocation(geoFire, "loc1", 0, 0, true); 74 | geoFire.getLocation("loc1", testCallback2); 75 | Assert.assertEquals(TestCallback.location("loc1", 0, 0), testCallback2.getCallbackValue()); 76 | 77 | TestCallback testCallback3 = new TestCallback(); 78 | geoFireTestingRule.setLocation(geoFire, "loc2", 1, 1, true); 79 | geoFire.getLocation("loc2", testCallback3); 80 | Assert.assertEquals(TestCallback.location("loc2", 1, 1), testCallback3.getCallbackValue()); 81 | 82 | TestCallback testCallback4 = new TestCallback(); 83 | geoFireTestingRule.setLocation(geoFire, "loc1", 5, 5, true); 84 | geoFire.getLocation("loc1", testCallback4); 85 | Assert.assertEquals(TestCallback.location("loc1", 5, 5), testCallback4.getCallbackValue()); 86 | 87 | TestCallback testCallback5 = new TestCallback(); 88 | geoFireTestingRule.removeLocation(geoFire, "loc1"); 89 | geoFire.getLocation("loc1", testCallback5); 90 | Assert.assertEquals(TestCallback.noLocation("loc1"), testCallback5.getCallbackValue()); 91 | } 92 | 93 | @Test 94 | public void getLocationOnWrongDataReturnsError() throws InterruptedException { 95 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 96 | geoFireTestingRule.setValueAndWait(geoFire.getDatabaseRefForKey("loc1"), "NaN"); 97 | 98 | final Semaphore semaphore = new Semaphore(0); 99 | geoFire.getLocation("loc1", new LocationCallback() { 100 | @Override 101 | public void onLocationResult(String key, GeoLocation location) { 102 | Assert.fail("This should not be a valid location!"); 103 | } 104 | 105 | @Override 106 | public void onCancelled(DatabaseError databaseError) { 107 | semaphore.release(); 108 | } 109 | }); 110 | semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS); 111 | 112 | geoFireTestingRule.setValueAndWait(geoFire.getDatabaseRefForKey("loc2"), new HashMap() {{ 113 | put("l", 10); 114 | put("g", "abc"); 115 | }}); 116 | 117 | geoFire.getLocation("loc2", new LocationCallback() { 118 | @Override 119 | public void onLocationResult(String key, GeoLocation location) { 120 | Assert.fail("This should not be a valid location!"); 121 | } 122 | 123 | @Override 124 | public void onCancelled(DatabaseError databaseError) { 125 | semaphore.release(); 126 | } 127 | }); 128 | semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS); 129 | } 130 | 131 | @Test 132 | public void invalidCoordinatesThrowException() { 133 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 134 | try { 135 | geoFire.setLocation("test", new GeoLocation(-91, 90)); 136 | Assert.fail("Did not throw illegal argument exception!"); 137 | } catch (IllegalArgumentException expected) { 138 | } 139 | 140 | try { 141 | geoFire.setLocation("test", new GeoLocation(0, -180.1)); 142 | Assert.fail("Did not throw illegal argument exception!"); 143 | } catch (IllegalArgumentException expected) { 144 | } 145 | 146 | try { 147 | geoFire.setLocation("test", new GeoLocation(0, 181.1)); 148 | Assert.fail("Did not throw illegal argument exception!"); 149 | } catch (IllegalArgumentException expected) { 150 | } 151 | } 152 | 153 | @Test 154 | public void locationWorksWithLongs() throws InterruptedException, ExecutionException, TimeoutException { 155 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 156 | DatabaseReference databaseReference = geoFire.getDatabaseRefForKey("loc"); 157 | 158 | final Semaphore semaphore = new Semaphore(0); 159 | databaseReference.setValue(new HashMap() {{ 160 | put("l", Arrays.asList(1L, 2L)); 161 | put("g", "7zzzzzzzzz"); // this is wrong but we don't care in this test 162 | }}, "7zzzzzzzzz", new DatabaseReference.CompletionListener() { 163 | @Override 164 | public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { 165 | semaphore.release(); 166 | } 167 | }); 168 | semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS); 169 | 170 | TestCallback testCallback = new TestCallback(); 171 | geoFire.getLocation("loc", testCallback); 172 | Assert.assertEquals(TestCallback.location("loc", 1, 2), testCallback.getCallbackValue()); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoHashQueryTest.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import com.firebase.geofire.core.GeoHash; 4 | import com.firebase.geofire.core.GeoHashQuery; 5 | import com.firebase.geofire.util.GeoUtils; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.junit.runners.JUnit4; 10 | 11 | import java.util.Set; 12 | 13 | @RunWith(JUnit4.class) 14 | public class GeoHashQueryTest { 15 | @Test 16 | public void queryForGeoHash() { 17 | Assert.assertEquals(new GeoHashQuery("60", "6h"), GeoHashQuery.queryForGeoHash(new GeoHash("64m9yn96mx"), 6)); 18 | Assert.assertEquals(new GeoHashQuery("0", "h"), GeoHashQuery.queryForGeoHash(new GeoHash("64m9yn96mx"), 1)); 19 | Assert.assertEquals(new GeoHashQuery("64", "65"), GeoHashQuery.queryForGeoHash(new GeoHash("64m9yn96mx"), 10)); 20 | Assert.assertEquals(new GeoHashQuery("640", "64h"), GeoHashQuery.queryForGeoHash(new GeoHash("6409yn96mx"), 11)); 21 | Assert.assertEquals(new GeoHashQuery("64h", "64~"), GeoHashQuery.queryForGeoHash(new GeoHash("64m9yn96mx"), 11)); 22 | Assert.assertEquals(new GeoHashQuery("6", "6~"), GeoHashQuery.queryForGeoHash(new GeoHash("6"), 10)); 23 | Assert.assertEquals(new GeoHashQuery("64s", "64~"), GeoHashQuery.queryForGeoHash(new GeoHash("64z178"), 12)); 24 | Assert.assertEquals(new GeoHashQuery("64z", "64~"), GeoHashQuery.queryForGeoHash(new GeoHash("64z178"), 15)); 25 | } 26 | 27 | @Test 28 | public void pointsInGeoHash() { 29 | for (int i = 0; i < 1000; i++) { 30 | double centerLat = Math.random()*160 - 80; 31 | double centerLong = Math.random()*360 - 180; 32 | double radius = Math.random()*100000; 33 | double radiusDegrees = GeoUtils.distanceToLatitudeDegrees(radius); 34 | Set queries = GeoHashQuery.queriesAtLocation(new GeoLocation(centerLat, centerLong), radius); 35 | for (int j = 0; j < 1000; j++) { 36 | double pointLat = Math.max(-89.9, Math.min(89.9, centerLat + Math.random()*radiusDegrees)); 37 | double pointLong = GeoUtils.wrapLongitude(centerLong + Math.random()*radiusDegrees); 38 | if (GeoUtils.distance(centerLat, centerLong, pointLat, pointLong) < radius) { 39 | GeoHash geoHash = new GeoHash(pointLat, pointLong); 40 | boolean inQuery = false; 41 | for (GeoHashQuery query: queries) { 42 | if (query.containsGeoHash(geoHash)) { 43 | inQuery = true; 44 | } 45 | } 46 | Assert.assertTrue(inQuery); 47 | } 48 | } 49 | } 50 | } 51 | 52 | @Test 53 | public void canJoinWith() { 54 | Assert.assertTrue(new GeoHashQuery("abcd", "abce").canJoinWith(new GeoHashQuery("abce", "abcf"))); 55 | Assert.assertTrue(new GeoHashQuery("abce", "abcf").canJoinWith(new GeoHashQuery("abcd", "abce"))); 56 | Assert.assertTrue(new GeoHashQuery("abcd", "abcf").canJoinWith(new GeoHashQuery("abcd", "abce"))); 57 | Assert.assertTrue(new GeoHashQuery("abcd", "abcf").canJoinWith(new GeoHashQuery("abce", "abcf"))); 58 | Assert.assertTrue(new GeoHashQuery("abc", "abd").canJoinWith(new GeoHashQuery("abce", "abcf"))); 59 | Assert.assertTrue(new GeoHashQuery("abce", "abcf").canJoinWith(new GeoHashQuery("abc", "abd"))); 60 | Assert.assertTrue(new GeoHashQuery("abcd", "abce~").canJoinWith(new GeoHashQuery("abc", "abd"))); 61 | Assert.assertTrue(new GeoHashQuery("abcd", "abce~").canJoinWith(new GeoHashQuery("abce", "abcf"))); 62 | Assert.assertTrue(new GeoHashQuery("abcd", "abcf").canJoinWith(new GeoHashQuery("abce", "abcg"))); 63 | 64 | Assert.assertFalse(new GeoHashQuery("abcd", "abce").canJoinWith(new GeoHashQuery("abcg", "abch"))); 65 | Assert.assertFalse(new GeoHashQuery("abcd", "abce").canJoinWith(new GeoHashQuery("dce", "dcf"))); 66 | Assert.assertFalse(new GeoHashQuery("abc", "abd").canJoinWith(new GeoHashQuery("dce", "dcf"))); 67 | } 68 | 69 | @Test 70 | public void joinWith() { 71 | Assert.assertEquals(new GeoHashQuery("abcd", "abcf"), new GeoHashQuery("abcd", "abce").joinWith(new GeoHashQuery("abce", "abcf"))); 72 | Assert.assertEquals(new GeoHashQuery("abcd", "abcf"), new GeoHashQuery("abce", "abcf").joinWith(new GeoHashQuery("abcd", "abce"))); 73 | Assert.assertEquals(new GeoHashQuery("abcd", "abcf"), new GeoHashQuery("abcd", "abcf").joinWith(new GeoHashQuery("abcd", "abce"))); 74 | Assert.assertEquals(new GeoHashQuery("abcd", "abcf"), new GeoHashQuery("abcd", "abcf").joinWith(new GeoHashQuery("abce", "abcf"))); 75 | Assert.assertEquals(new GeoHashQuery("abc", "abd"), new GeoHashQuery("abc", "abd").joinWith(new GeoHashQuery("abce", "abcf"))); 76 | Assert.assertEquals(new GeoHashQuery("abc", "abd"), new GeoHashQuery("abce", "abcf").joinWith(new GeoHashQuery("abc", "abd"))); 77 | Assert.assertEquals(new GeoHashQuery("abc", "abd"), new GeoHashQuery("abcd", "abce~").joinWith(new GeoHashQuery("abc", "abd"))); 78 | Assert.assertEquals(new GeoHashQuery("abcd", "abcf"), new GeoHashQuery("abcd", "abce~").joinWith(new GeoHashQuery("abce", "abcf"))); 79 | Assert.assertEquals(new GeoHashQuery("abcd", "abcg"), new GeoHashQuery("abcd", "abcf").joinWith(new GeoHashQuery("abce", "abcg"))); 80 | 81 | try { 82 | new GeoHashQuery("abcd", "abce").joinWith(new GeoHashQuery("abcg", "abch")); 83 | Assert.fail("Exception was not thrown!"); 84 | } catch(IllegalArgumentException expected) { 85 | } 86 | 87 | try { 88 | new GeoHashQuery("abcd", "abce").joinWith(new GeoHashQuery("dce", "dcf")); 89 | Assert.fail("Exception was not thrown!"); 90 | } catch(IllegalArgumentException expected) { 91 | } 92 | 93 | try { 94 | new GeoHashQuery("abc", "abd").joinWith(new GeoHashQuery("dce", "dcf")); 95 | Assert.fail("Exception was not thrown!"); 96 | } catch(IllegalArgumentException expected) { 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoHashQueryUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import com.firebase.geofire.core.GeoHashQuery; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.JUnit4; 8 | 9 | @RunWith(JUnit4.class) 10 | public class GeoHashQueryUtilsTest { 11 | 12 | @Test 13 | public void boundingBoxBits() { 14 | Assert.assertEquals(28, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(35, 0), 1000)); 15 | Assert.assertEquals(27, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(35.645, 0), 1000)); 16 | Assert.assertEquals(27, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(36, 0), 1000)); 17 | Assert.assertEquals(28, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(0, 0), 1000)); 18 | Assert.assertEquals(28, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(0, -180), 1000)); 19 | Assert.assertEquals(28, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(0, 180), 1000)); 20 | Assert.assertEquals(22, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(0, 0), 8000)); 21 | Assert.assertEquals(27, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(45, 0), 1000)); 22 | Assert.assertEquals(25, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(75, 0), 1000)); 23 | Assert.assertEquals(23, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(75, 0), 2000)); 24 | Assert.assertEquals(1, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(90, 0), 1000)); 25 | Assert.assertEquals(1, GeoHashQuery.Utils.bitsForBoundingBox(new GeoLocation(90, 0), 2000)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoHashTest.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import com.firebase.geofire.core.GeoHash; 4 | import org.junit.Assert; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.rules.ExpectedException; 8 | import org.junit.runner.RunWith; 9 | import org.junit.runners.JUnit4; 10 | 11 | @RunWith(JUnit4.class) 12 | public class GeoHashTest { 13 | @Rule 14 | public org.junit.rules.ExpectedException exception = ExpectedException.none(); 15 | 16 | @Test 17 | public void hashValues() { 18 | Assert.assertEquals(new GeoHash("7zzzzzzzzz"), new GeoHash(0, 0)); 19 | Assert.assertEquals(new GeoHash("2pbpbpbpbp"), new GeoHash(0, -180)); 20 | Assert.assertEquals(new GeoHash("rzzzzzzzzz"), new GeoHash(0, 180)); 21 | Assert.assertEquals(new GeoHash("5bpbpbpbpb"), new GeoHash(-90, 0)); 22 | Assert.assertEquals(new GeoHash("0000000000"), new GeoHash(-90, -180)); 23 | Assert.assertEquals(new GeoHash("pbpbpbpbpb"), new GeoHash(-90, 180)); 24 | Assert.assertEquals(new GeoHash("gzzzzzzzzz"), new GeoHash(90, 0)); 25 | Assert.assertEquals(new GeoHash("bpbpbpbpbp"), new GeoHash(90, -180)); 26 | Assert.assertEquals(new GeoHash("zzzzzzzzzz"), new GeoHash(90, 180)); 27 | 28 | Assert.assertEquals(new GeoHash("9q8yywe56g"), new GeoHash(37.7853074, -122.4054274)); 29 | Assert.assertEquals(new GeoHash("dqcjf17sy6"), new GeoHash(38.98719, -77.250783)); 30 | Assert.assertEquals(new GeoHash("tj4p5gerfz"), new GeoHash(29.3760648, 47.9818853)); 31 | Assert.assertEquals(new GeoHash("umghcygjj7"), new GeoHash(78.216667, 15.55)); 32 | Assert.assertEquals(new GeoHash("4qpzmren1k"), new GeoHash(-54.933333, -67.616667)); 33 | Assert.assertEquals(new GeoHash("4w2kg3s54y"), new GeoHash(-54, -67)); 34 | } 35 | 36 | @Test 37 | public void customPrecision() { 38 | Assert.assertEquals(new GeoHash("000000"), new GeoHash(-90, -180, 6)); 39 | Assert.assertEquals(new GeoHash("zzzzzzzzzzzzzzzzzzzz"), new GeoHash(90, 180, 20)); 40 | Assert.assertEquals(new GeoHash("p"), new GeoHash(-90, 180, 1)); 41 | Assert.assertEquals(new GeoHash("bpbpb"), new GeoHash(90, -180, 5)); 42 | Assert.assertEquals(new GeoHash("9q8yywe5"), new GeoHash(37.7853074, -122.4054274, 8)); 43 | Assert.assertEquals(new GeoHash("dqcjf17sy6cppp8vfn"), new GeoHash(38.98719, -77.250783, 18)); 44 | Assert.assertEquals(new GeoHash("tj4p5gerfzqu"), new GeoHash(29.3760648, 47.9818853, 12)); 45 | Assert.assertEquals(new GeoHash("u"), new GeoHash(78.216667, 15.55, 1)); 46 | Assert.assertEquals(new GeoHash("4qpzmre"), new GeoHash(-54.933333, -67.616667, 7)); 47 | Assert.assertEquals(new GeoHash("4w2kg3s54"), new GeoHash(-54, -67, 9)); 48 | } 49 | 50 | @Test 51 | public void zeroPrecisionException() { 52 | exception.expect(IllegalArgumentException.class); 53 | new GeoHash(1,2,0); 54 | } 55 | 56 | @Test 57 | public void largePrecisionException() { 58 | exception.expect(IllegalArgumentException.class); 59 | new GeoHash(1,2,23); 60 | } 61 | 62 | @Test 63 | public void invalidGeoHashException() { 64 | exception.expect(IllegalArgumentException.class); 65 | new GeoHash("abc"); 66 | new GeoHash(""); 67 | new GeoHash("~"); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoLocationTest.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.fail; 5 | 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.JUnit4; 9 | 10 | @RunWith(JUnit4.class) 11 | public class GeoLocationTest { 12 | 13 | private static final double EPSILON = 0.0000001; 14 | 15 | @Test 16 | public void geoLocationHasCorrectValues() { 17 | assertEquals(new GeoLocation(1, 2).latitude, 1.0, EPSILON); 18 | assertEquals(new GeoLocation(1, 2).longitude, 2.0, EPSILON); 19 | assertEquals(new GeoLocation(0.000001, 2).latitude, 0.000001, EPSILON); 20 | assertEquals(new GeoLocation(0, 0.000001).longitude, 0.000001, EPSILON); 21 | } 22 | 23 | @Test 24 | public void invalidCoordinatesThrowException() { 25 | try { 26 | new GeoLocation(-90.1, 90); 27 | fail("Did not throw illegal argument exception!"); 28 | } catch (IllegalArgumentException expected) { 29 | } 30 | 31 | try { 32 | new GeoLocation(0, -180.1); 33 | fail("Did not throw illegal argument exception!"); 34 | } catch (IllegalArgumentException expected) { 35 | } 36 | 37 | try { 38 | new GeoLocation(0, 180.1); 39 | fail("Did not throw illegal argument exception!"); 40 | } catch (IllegalArgumentException expected) { 41 | } 42 | 43 | try { 44 | new GeoLocation(Double.NaN, 0); 45 | fail("Did not throw illegal argument exception!"); 46 | } catch (IllegalArgumentException expected) { 47 | } 48 | 49 | try { 50 | new GeoLocation(0, Double.NaN); 51 | fail("Did not throw illegal argument exception!"); 52 | } catch (IllegalArgumentException expected) { 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoQueryIT.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import static com.firebase.geofire.GeoFireIT.DATABASE_URL; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertTrue; 6 | import static org.junit.Assert.fail; 7 | 8 | import com.firebase.geofire.testing.GeoFireTestingRule; 9 | import com.firebase.geofire.testing.GeoQueryDataEventTestListener; 10 | import com.firebase.geofire.testing.GeoQueryEventTestListener; 11 | import com.google.firebase.database.DatabaseError; 12 | import com.google.firebase.database.DatabaseReference; 13 | import java.util.Collections; 14 | import java.util.HashSet; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.concurrent.Semaphore; 19 | import java.util.concurrent.TimeUnit; 20 | import org.junit.Rule; 21 | import org.junit.Test; 22 | import org.junit.runner.RunWith; 23 | import org.junit.runners.JUnit4; 24 | 25 | @RunWith(JUnit4.class) 26 | public class GeoQueryIT { 27 | @Rule public final GeoFireTestingRule geoFireTestingRule = new GeoFireTestingRule(DATABASE_URL); 28 | 29 | @Test 30 | public void keyEntered() throws InterruptedException { 31 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 32 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 33 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 34 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 35 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 36 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 37 | 38 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37, -122), 0.5); 39 | 40 | GeoQueryEventTestListener testListener = new GeoQueryEventTestListener(); 41 | query.addGeoQueryEventListener(testListener); 42 | 43 | geoFireTestingRule.waitForGeoFireReady(geoFire); 44 | 45 | Set events = new HashSet<>(); 46 | events.add(GeoQueryEventTestListener.keyEntered("1", 37, -122)); 47 | events.add(GeoQueryEventTestListener.keyEntered("2", 37.0001, -122.0001)); 48 | events.add(GeoQueryEventTestListener.keyEntered("4", 37.0002, -121.9998)); 49 | 50 | testListener.expectEvents(events); 51 | 52 | query.removeAllListeners(); 53 | } 54 | 55 | @Test 56 | public void keyExited() throws InterruptedException { 57 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 58 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 59 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 60 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 61 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 62 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 63 | 64 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37, -122), 0.5); 65 | GeoQueryEventTestListener testListener = new GeoQueryEventTestListener(false, false, true); 66 | query.addGeoQueryEventListener(testListener); 67 | 68 | geoFireTestingRule.waitForGeoFireReady(geoFire); 69 | 70 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); // not in query 71 | geoFireTestingRule.setLocation(geoFire, "1", 0, 0); // exited 72 | geoFireTestingRule.setLocation(geoFire, "2", 0, 0); // exited 73 | geoFireTestingRule.setLocation(geoFire, "3", 2, 0, true); // not in query 74 | geoFireTestingRule.setLocation(geoFire, "0", 3, 0); // not in query 75 | geoFireTestingRule.setLocation(geoFire, "1", 4, 0); // not in query 76 | geoFireTestingRule.setLocation(geoFire, "2", 5, 0, true); // not in query 77 | 78 | List events = new LinkedList<>(); 79 | events.add(GeoQueryEventTestListener.keyExited("1")); 80 | events.add(GeoQueryEventTestListener.keyExited("2")); 81 | 82 | testListener.expectEvents(events); 83 | } 84 | 85 | @Test 86 | public void keyMoved() throws InterruptedException { 87 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 88 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 89 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 90 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 91 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 92 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 93 | 94 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37, -122), 0.5); 95 | 96 | GeoQueryEventTestListener testListener = new GeoQueryEventTestListener(false, true, false); 97 | query.addGeoQueryEventListener(testListener); 98 | 99 | GeoQueryEventTestListener exitListener = new GeoQueryEventTestListener(false, false, true); 100 | query.addGeoQueryEventListener(exitListener); 101 | 102 | geoFireTestingRule.waitForGeoFireReady(geoFire); 103 | 104 | geoFireTestingRule.setLocation(geoFire, "0", 1, 1); // outside of query 105 | geoFireTestingRule.setLocation(geoFire, "1", 37.0001, -122.0000); // moved 106 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); // location stayed the same 107 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -122.0000); // moved 108 | geoFireTestingRule.setLocation(geoFire, "3", 37.0000, -122.0000, true); // entered 109 | geoFireTestingRule.setLocation(geoFire, "3", 37.0003, -122.0003, true); // moved: 110 | geoFireTestingRule.setLocation(geoFire, "2", 0, 0, true); // exited 111 | // wait for location to exit 112 | exitListener.expectEvents(Collections.singletonList(GeoQueryEventTestListener.keyExited("2"))); 113 | geoFireTestingRule.setLocation(geoFire, "2", 37.0000, -122.0000, true); // entered 114 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001, true); // moved 115 | 116 | List events = new LinkedList<>(); 117 | events.add(GeoQueryEventTestListener.keyMoved("1", 37.0001, -122.0000)); 118 | events.add(GeoQueryEventTestListener.keyMoved("4", 37.0002, -122.0000)); 119 | events.add(GeoQueryEventTestListener.keyMoved("3", 37.0003, -122.0003)); 120 | events.add(GeoQueryEventTestListener.keyMoved("2", 37.0001, -122.0001)); 121 | 122 | testListener.expectEvents(events); 123 | } 124 | 125 | @Test 126 | public void dataChanged() throws InterruptedException { 127 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 128 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 129 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0001); 130 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 131 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 132 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 133 | 134 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37, -122), 0.5); 135 | 136 | GeoQueryDataEventTestListener testListener = new GeoQueryDataEventTestListener( 137 | false, true, true, false); 138 | query.addGeoQueryDataEventListener(testListener); 139 | 140 | geoFireTestingRule.waitForGeoFireReady(geoFire); 141 | 142 | geoFireTestingRule.setLocation(geoFire, "0", 1, 1, true); // outside of query 143 | geoFireTestingRule.setLocation(geoFire, "1", 37.0001, -122.0001, true); // moved 144 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001, true); // location stayed the same 145 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9999, true); // moved 146 | 147 | DatabaseReference childRef = geoFire.getDatabaseRefForKey("2").child("some_child"); 148 | geoFireTestingRule.setValueAndWait(childRef, "some_value"); // data changed 149 | 150 | List events = new LinkedList<>(); 151 | events.add(GeoQueryDataEventTestListener.dataMoved("1", 37.0001, -122.0001)); 152 | events.add(GeoQueryDataEventTestListener.dataChanged("1", 37.0001, -122.0001)); 153 | 154 | events.add(GeoQueryDataEventTestListener.dataMoved("4", 37.0002, -121.9999)); 155 | events.add(GeoQueryDataEventTestListener.dataChanged("4", 37.0002, -121.9999)); 156 | 157 | events.add(GeoQueryDataEventTestListener.dataChanged("2", 37.0001, -122.0001)); 158 | 159 | testListener.expectEvents(events); 160 | } 161 | 162 | @Test 163 | public void subQueryTriggersKeyMoved() throws InterruptedException { 164 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 165 | geoFireTestingRule.setLocation(geoFire, "0", 1, 1, true); 166 | geoFireTestingRule.setLocation(geoFire, "1", -1, -1, true); 167 | 168 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(0, 0), 1000); 169 | GeoQueryEventTestListener testListener = new GeoQueryEventTestListener(false, true, true); 170 | query.addGeoQueryEventListener(testListener); 171 | 172 | geoFireTestingRule.waitForGeoFireReady(geoFire); 173 | 174 | geoFireTestingRule.setLocation(geoFire, "0", -1, -1); 175 | geoFireTestingRule.setLocation(geoFire, "1", 1, 1); 176 | 177 | Set events = new HashSet<>(); 178 | events.add(GeoQueryEventTestListener.keyMoved("0", -1, -1)); 179 | events.add(GeoQueryEventTestListener.keyMoved("1", 1, 1)); 180 | 181 | testListener.expectEvents(events); 182 | } 183 | 184 | @Test 185 | public void removeSingleObserver() throws InterruptedException { 186 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 187 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 188 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 189 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 190 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 191 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 192 | 193 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37.0, -122), 1); 194 | 195 | GeoQueryEventTestListener testListenerRemoved = new GeoQueryEventTestListener(true, true, true); 196 | query.addGeoQueryEventListener(testListenerRemoved); 197 | 198 | GeoQueryEventTestListener testListenerRemained = new GeoQueryEventTestListener(true, true, true); 199 | query.addGeoQueryEventListener(testListenerRemained); 200 | 201 | Set addedEvents = new HashSet<>(); 202 | addedEvents.add(GeoQueryEventTestListener.keyEntered("1", 37, -122)); 203 | addedEvents.add(GeoQueryEventTestListener.keyEntered("2", 37.0001, -122.0001)); 204 | addedEvents.add(GeoQueryEventTestListener.keyEntered("4", 37.0002, -121.9998)); 205 | 206 | testListenerRemained.expectEvents(addedEvents); 207 | testListenerRemained.expectEvents(addedEvents); 208 | 209 | query.removeGeoQueryEventListener(testListenerRemoved); 210 | 211 | geoFireTestingRule.setLocation(geoFire, "0", 37, -122); // entered 212 | geoFireTestingRule.setLocation(geoFire, "1", 0, 0); // exited 213 | geoFireTestingRule.setLocation(geoFire, "2", 37, -122.0001); // moved 214 | 215 | Set furtherEvents = new HashSet<>(addedEvents); 216 | furtherEvents.add(GeoQueryEventTestListener.keyEntered("0", 37, -122)); // entered 217 | furtherEvents.add(GeoQueryEventTestListener.keyExited("1")); // exited 218 | furtherEvents.add(GeoQueryEventTestListener.keyMoved("2", 37.0000, -122.0001)); // moved 219 | 220 | testListenerRemained.expectEvents(furtherEvents); 221 | testListenerRemoved.expectEvents(addedEvents); 222 | } 223 | 224 | @Test 225 | public void removeAllObservers() throws InterruptedException { 226 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 227 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 228 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 229 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 230 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 231 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 232 | 233 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37.0, -122), 1); 234 | 235 | GeoQueryEventTestListener testListenerRemoved = new GeoQueryEventTestListener(true, true, true); 236 | query.addGeoQueryEventListener(testListenerRemoved); 237 | 238 | GeoQueryEventTestListener testListenerRemained = new GeoQueryEventTestListener(true, true, true); 239 | query.addGeoQueryEventListener(testListenerRemained); 240 | 241 | Set addedEvents = new HashSet<>(); 242 | addedEvents.add(GeoQueryEventTestListener.keyEntered("1", 37, -122)); 243 | addedEvents.add(GeoQueryEventTestListener.keyEntered("2", 37.0001, -122.0001)); 244 | addedEvents.add(GeoQueryEventTestListener.keyEntered("4", 37.0002, -121.9998)); 245 | 246 | testListenerRemained.expectEvents(addedEvents); 247 | testListenerRemained.expectEvents(addedEvents); 248 | 249 | query.removeGeoQueryEventListener(testListenerRemoved); 250 | query.removeAllListeners(); 251 | 252 | geoFireTestingRule.setLocation(geoFire, "0", 37, -122); // entered 253 | geoFireTestingRule.setLocation(geoFire, "1", 0, 0); // exited 254 | geoFireTestingRule.setLocation(geoFire, "2", 37, -122.0001, true); // moved 255 | 256 | testListenerRemained.expectEvents(addedEvents); 257 | testListenerRemoved.expectEvents(addedEvents); 258 | } 259 | 260 | @Test 261 | public void readyListener() throws InterruptedException { 262 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 263 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 264 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 265 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 266 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 267 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 268 | 269 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37.0, -122), 1); 270 | final boolean[] done = new boolean[1]; 271 | final boolean[] failed = new boolean[1]; 272 | final Semaphore semaphore = new Semaphore(0); 273 | query.addGeoQueryEventListener(new GeoQueryEventListener() { 274 | @Override 275 | public void onKeyEntered(String key, GeoLocation location) { 276 | if (done[0]) { 277 | failed[0] = true; 278 | } 279 | } 280 | 281 | @Override 282 | public void onKeyExited(String key) { 283 | } 284 | 285 | @Override 286 | public void onKeyMoved(String key, GeoLocation location) { 287 | } 288 | 289 | @Override 290 | public void onGeoQueryReady() { 291 | done[0] = true; 292 | semaphore.release(); 293 | } 294 | 295 | @Override 296 | public void onGeoQueryError(DatabaseError error) { 297 | fail("onGeoQueryError: " + error.toString()); 298 | } 299 | }); 300 | 301 | assertTrue(semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS)); 302 | assertTrue("GeoQuery not ready, test timed out.", done[0]); 303 | // wait for any further events to fire 304 | Thread.sleep(250); 305 | assertFalse("Key entered after ready event occurred!", failed[0]); 306 | } 307 | 308 | @Test 309 | public void readyListenerAfterReady() throws InterruptedException { 310 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 311 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 312 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 313 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 314 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 315 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 316 | 317 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37.0, -122), 1); 318 | 319 | final Semaphore semaphore = new Semaphore(0); 320 | query.addGeoQueryEventListener(new GeoQueryEventListener() { 321 | @Override 322 | public void onKeyEntered(String key, GeoLocation location) { 323 | } 324 | 325 | @Override 326 | public void onKeyExited(String key) { 327 | } 328 | 329 | @Override 330 | public void onKeyMoved(String key, GeoLocation location) { 331 | } 332 | 333 | @Override 334 | public void onGeoQueryReady() { 335 | semaphore.release(); 336 | } 337 | 338 | @Override 339 | public void onGeoQueryError(DatabaseError error) { 340 | } 341 | }); 342 | 343 | assertTrue(semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS)); 344 | 345 | query.addGeoQueryEventListener(new GeoQueryEventListener() { 346 | @Override 347 | public void onKeyEntered(String key, GeoLocation location) { 348 | } 349 | 350 | @Override 351 | public void onKeyExited(String key) { 352 | } 353 | 354 | @Override 355 | public void onKeyMoved(String key, GeoLocation location) { 356 | } 357 | 358 | @Override 359 | public void onGeoQueryReady() { 360 | semaphore.release(); 361 | } 362 | 363 | @Override 364 | public void onGeoQueryError(DatabaseError error) { 365 | } 366 | }); 367 | assertTrue(semaphore.tryAcquire(10, TimeUnit.MILLISECONDS)); 368 | } 369 | 370 | @Test 371 | public void readyAfterUpdateCriteria() throws InterruptedException { 372 | GeoFire geoFire = geoFireTestingRule.newTestGeoFire(); 373 | geoFireTestingRule.setLocation(geoFire, "0", 0, 0); 374 | geoFireTestingRule.setLocation(geoFire, "1", 37.0000, -122.0000); 375 | geoFireTestingRule.setLocation(geoFire, "2", 37.0001, -122.0001); 376 | geoFireTestingRule.setLocation(geoFire, "3", 37.1000, -122.0000); 377 | geoFireTestingRule.setLocation(geoFire, "4", 37.0002, -121.9998, true); 378 | 379 | GeoQuery query = geoFire.queryAtLocation(new GeoLocation(37.0, -122), 1); 380 | final boolean[] done = new boolean[1]; 381 | final Semaphore semaphore = new Semaphore(0); 382 | final int[] readyCount = new int[1]; 383 | query.addGeoQueryEventListener(new GeoQueryEventListener() { 384 | @Override 385 | public void onKeyEntered(String key, GeoLocation location) { 386 | if (key.equals("0")) { 387 | done[0] = true; 388 | } 389 | } 390 | 391 | @Override 392 | public void onKeyExited(String key) { 393 | } 394 | 395 | @Override 396 | public void onKeyMoved(String key, GeoLocation location) { 397 | } 398 | 399 | @Override 400 | public void onGeoQueryReady() { 401 | semaphore.release(); 402 | readyCount[0]++; 403 | } 404 | 405 | @Override 406 | public void onGeoQueryError(DatabaseError error) { 407 | 408 | } 409 | }); 410 | 411 | assertTrue(semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS)); 412 | query.setCenter(new GeoLocation(0,0)); 413 | assertTrue(semaphore.tryAcquire(geoFireTestingRule.timeout, TimeUnit.SECONDS)); 414 | assertTrue(done[0]); 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /java/src/test/java/com/firebase/geofire/GeoUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire; 2 | 3 | import com.firebase.geofire.util.GeoUtils; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.junit.runners.JUnit4; 8 | 9 | @RunWith(JUnit4.class) 10 | public class GeoUtilsTest { 11 | 12 | @Test 13 | public void wrapLongitude() { 14 | Assert.assertEquals(1, GeoUtils.wrapLongitude(1), 1e-6); 15 | Assert.assertEquals(0, GeoUtils.wrapLongitude(0), 1e-6); 16 | Assert.assertEquals(180, GeoUtils.wrapLongitude(180), 1e-6); 17 | Assert.assertEquals(-180, GeoUtils.wrapLongitude(-180), 1e-6); 18 | Assert.assertEquals(-178, GeoUtils.wrapLongitude(182), 1e-6); 19 | Assert.assertEquals(-90, GeoUtils.wrapLongitude(270), 1e-6); 20 | Assert.assertEquals(0, GeoUtils.wrapLongitude(360), 1e-6); 21 | Assert.assertEquals(-180, GeoUtils.wrapLongitude(540), 1e-6); 22 | Assert.assertEquals(-90, GeoUtils.wrapLongitude(630), 1e-6); 23 | Assert.assertEquals(0, GeoUtils.wrapLongitude(720), 1e-6); 24 | Assert.assertEquals(90, GeoUtils.wrapLongitude(810), 1e-6); 25 | Assert.assertEquals(0, GeoUtils.wrapLongitude(-360), 1e-6); 26 | Assert.assertEquals(178, GeoUtils.wrapLongitude(-182), 1e-6); 27 | Assert.assertEquals(90, GeoUtils.wrapLongitude(-270), 1e-6); 28 | Assert.assertEquals(0, GeoUtils.wrapLongitude(-360), 1e-6); 29 | Assert.assertEquals(-90, GeoUtils.wrapLongitude(-450), 1e-6); 30 | Assert.assertEquals(180, GeoUtils.wrapLongitude(-540), 1e-6); 31 | Assert.assertEquals(90, GeoUtils.wrapLongitude(-630), 1e-6); 32 | Assert.assertEquals(0, GeoUtils.wrapLongitude(1080), 1e-6); 33 | Assert.assertEquals(0, GeoUtils.wrapLongitude(-1080), 1e-6); 34 | } 35 | 36 | @Test 37 | public void distanceToLongitudeDegrees() { 38 | Assert.assertEquals(0.008983, GeoUtils.distanceToLongitudeDegrees(1000, 0), 1e-5); 39 | Assert.assertEquals(1, GeoUtils.distanceToLongitudeDegrees(111320, 0), 1e-5); 40 | Assert.assertEquals(1, GeoUtils.distanceToLongitudeDegrees(107550, 15), 1e-5); 41 | Assert.assertEquals(1, GeoUtils.distanceToLongitudeDegrees(96486, 30), 1e-5); 42 | Assert.assertEquals(1, GeoUtils.distanceToLongitudeDegrees(78847, 45), 1e-5); 43 | Assert.assertEquals(1, GeoUtils.distanceToLongitudeDegrees(55800, 60), 1e-5); 44 | Assert.assertEquals(1, GeoUtils.distanceToLongitudeDegrees(28902, 75), 1e-5); 45 | Assert.assertEquals(0, GeoUtils.distanceToLongitudeDegrees(0, 90), 1e-5); 46 | Assert.assertEquals(360, GeoUtils.distanceToLongitudeDegrees(1000, 90), 1e-5); 47 | Assert.assertEquals(360, GeoUtils.distanceToLongitudeDegrees(1000, 89.9999), 1e-5); 48 | Assert.assertEquals(102.594208, GeoUtils.distanceToLongitudeDegrees(1000, 89.995), 1e-5); 49 | } 50 | 51 | @Test 52 | public void capRadius() { 53 | Assert.assertEquals(1.0d, GeoUtils.capRadius(1.0d), 0.1d); 54 | Assert.assertEquals(100.0d, GeoUtils.capRadius(100.0d), 0.1d); 55 | Assert.assertEquals(813.0d, GeoUtils.capRadius(813.0d), 0.1d); 56 | Assert.assertEquals(8586.0d, GeoUtils.capRadius(8586.0d), 0.1d); 57 | Assert.assertEquals(8587.0d, GeoUtils.capRadius(8587.0d), 0.1d); 58 | 59 | Assert.assertEquals(8587.0d, GeoUtils.capRadius(8588.0d), 0.1d); 60 | Assert.assertEquals(8587.0d, GeoUtils.capRadius(10000.0d), 0.1d); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.firebase 6 | geofire 7 | 3.0.1-SNAPSHOT 8 | pom 9 | 10 | geofire 11 | GeoFire is an open-source library for Android/Java that allows you to store and query a set of keys based on their geographic location. 12 | 13 | Firebase 14 | https://www.firebase.com/ 15 | 16 | https://github.com/firebase/geofire-java 17 | 18 | scm:git:git@github.com:firebase/geofire-java.git 19 | scm:git:git@github.com:firebase/geofire-java.git 20 | https://github.com/firebase/geofire-java 21 | HEAD 22 | 23 | 24 | 25 | 26 | samtstern 27 | Sam Stern 28 | samstern@google.com 29 | Firebase 30 | https://firebase.google.com 31 | 32 | Project-Administrator 33 | Developer 34 | 35 | -8 36 | 37 | 38 | 39 | 40 | 41 | MIT 42 | http://firebase.mit-license.org 43 | 44 | 45 | 46 | UTF-8 47 | 48 | 49 | 50 | common 51 | java 52 | testing 53 | 54 | 55 | 56 | 57 | geofire 58 | https://api.bintray.com/maven/firebase/geofire/geofire 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | co.leantechniques 69 | maven-buildtime-extension 70 | 2.0.3 71 | 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-compiler-plugin 78 | 3.1 79 | 80 | 1.7 81 | 1.7 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-javadoc-plugin 88 | 2.9.1 89 | 90 | 91 | attach-javadocs 92 | 93 | jar 94 | 95 | 96 | 97 | 98 | com.firebase.geofire.* 99 | public 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-source-plugin 106 | 2.3 107 | 108 | 109 | add-sources 110 | 111 | jar 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-release-plugin 122 | 2.5.3 123 | 124 | 125 | 126 | 127 | maven-surefire-plugin 128 | 2.19.1 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-failsafe-plugin 135 | 2.20.1 136 | 137 | 138 | 139 | integration-test 140 | verify 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd $(dirname $0) 5 | 6 | ########################### 7 | # VALIDATE GEOFIRE REPO # 8 | ########################### 9 | # Ensure the checked out geofire branch is master 10 | CHECKED_OUT_BRANCH="$(git branch | grep "*" | awk -F ' ' '{print $2}')" 11 | if [[ $CHECKED_OUT_BRANCH != "master" ]]; then 12 | echo "Error: Your geofire repo is not on the master branch." 13 | exit 1 14 | fi 15 | 16 | # Make sure the geofire branch does not have existing changes 17 | if ! git --git-dir=".git" diff --quiet; then 18 | echo "Error: Your geofire repo has existing changes on the master branch. Make sure you commit and push the new version before running this release script." 19 | exit 1 20 | fi 21 | 22 | ############################## 23 | # VALIDATE CLIENT VERSIONS # 24 | ############################## 25 | 26 | VERSION=$(grep version pom.xml |head -2|tail -1|awk -F '>' '{print $2}'|awk -F '<' '{print $1}'|awk -F '-' '{print $1}') 27 | read -p "We are releasing $VERSION, is this correct? (press enter to continue) " DERP 28 | if [[ ! -z $DERP ]]; then 29 | echo "Cancelling release, please update pom.xml to desired version" 30 | fi 31 | 32 | # Ensure there is not an existing git tag for the new version 33 | # XXX this is wrong; needs to be semver sorted as my other scripts are 34 | LAST_GIT_TAG="$(git tag --list | tail -1 | awk -F 'v' '{print $2}')" 35 | if [[ $VERSION == $LAST_GIT_TAG ]]; then 36 | echo "Error: git tag v${VERSION} already exists. Make sure you are not releasing an already-released version." 37 | exit 1 38 | fi 39 | 40 | # Create docs 41 | ./create-docs.sh 42 | if [[ $? -ne 0 ]]; then 43 | echo "error: There was an error creating the docs." 44 | exit 1 45 | fi 46 | 47 | ################### 48 | # DEPLOY TO MAVEN # 49 | ################### 50 | read -p "Next, make sure this repo is clean and up to date. We will be kicking off a deploy to maven." DERP 51 | mvn clean 52 | mvn -s settings.xml release:clean release:prepare release:perform -Darguments="-DskipTests" -Dtag=v$VERSION 53 | 54 | if [[ $? -ne 0 ]]; then 55 | echo "error: Error building and releasing to maven." 56 | exit 1 57 | fi 58 | 59 | ############## 60 | # UPDATE GIT # 61 | ############## 62 | 63 | # Push the new git tag created by Maven 64 | git push --tags 65 | if [[ $? -ne 0 ]]; then 66 | echo "Error: Failed to do 'git push --tags' from geofire repo." 67 | exit 1 68 | fi 69 | 70 | ################ 71 | # MANUAL STEPS # 72 | ################ 73 | 74 | echo "Manual steps:" 75 | echo " 1) Release all draft artifacts on Bintray at https://bintray.com/firebase/geofire" 76 | echo " 2) On bintray, initiate Maven Central sync for latest version" 77 | echo " 3) Deply new docs: $> firebase deploy" 78 | echo " 4) Update the release notes for GeoFire version ${VERSION} on GitHub and add jars for downloading" 79 | echo " 5) Update firebase-versions.json in the firebase-clients repo with the changelog information" 80 | echo " 6) Tweet @FirebaseRelease: 'v${VERSION} of @Firebase GeoFire for Java is available https://github.com/firebase/geofire-java" 81 | echo --- 82 | echo "Done! Woo!" 83 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | geofire 10 | ${env.BINTRAY_USER} 11 | ${env.BINTRAY_KEY} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | false 21 | 22 | central 23 | bintray 24 | http://jcenter.bintray.com 25 | 26 | 27 | 28 | 29 | 30 | false 31 | 32 | central 33 | bintray-plugins 34 | http://jcenter.bintray.com 35 | 36 | 37 | bintray 38 | 39 | 40 | 41 | 42 | bintray 43 | 44 | 45 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GeoFire for Java 5 | 6 | 7 | 8 |

GeoFire for iOS

9 | 10 |

API Reference

11 |

12 | A full API reference for GeoFire for Java is available here. 13 |

14 | 15 |

Quick Start

16 |

17 | Check out GitHub for a quick start guide and examples. 18 |

19 | 20 |

Source Code

21 |

22 | GeoFire for Java is open source and available on GitHub 23 |

24 | 25 |

Other version

26 |

27 | Other versions of GeoFire are available fore JavaScript and iOS. 29 |

30 | 31 | 32 | -------------------------------------------------------------------------------- /testing/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | 7 | com.firebase 8 | geofire 9 | 3.0.1-SNAPSHOT 10 | ../ 11 | 12 | 13 | geofire-testing 14 | jar 15 | 16 | geofire-testing 17 | GeoFire is an open-source library for Android/Java that allows you to store and query a set of keys based on their geographic location. 18 | 19 | Firebase 20 | https://www.firebase.com/ 21 | 22 | https://github.com/firebase/geofire-java 23 | 24 | scm:git:git@github.com:firebase/geofire-java.git 25 | scm:git:git@github.com:firebase/geofire-java.git 26 | https://github.com/firebase/geofire-java 27 | HEAD 28 | 29 | 30 | 31 | MIT 32 | http://firebase.mit-license.org 33 | 34 | 35 | 36 | UTF-8 37 | 38 | 39 | 40 | 41 | geofire 42 | https://api.bintray.com/maven/firebase/geofire/geofire-testing 43 | 44 | 45 | 46 | 47 | src/main/java 48 | 49 | 50 | 51 | 52 | com.firebase 53 | geofire-common 54 | ${project.version} 55 | 56 | 57 | junit 58 | junit 59 | 4.12 60 | 61 | 62 | org.slf4j 63 | slf4j-simple 64 | 1.7.25 65 | 66 | 67 | 68 | 69 | com.google.firebase 70 | firebase-admin 71 | [6.0.0,) 72 | provided 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /testing/src/main/java/com/firebase/geofire/testing/GeoFireTestingRule.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.testing; 2 | 3 | import static org.junit.Assert.assertNull; 4 | import static org.junit.Assert.assertTrue; 5 | import static org.junit.Assert.fail; 6 | 7 | import com.firebase.geofire.GeoFire; 8 | import com.firebase.geofire.GeoLocation; 9 | import com.google.auth.oauth2.GoogleCredentials; 10 | import com.google.firebase.FirebaseApp; 11 | import com.google.firebase.FirebaseOptions; 12 | import com.google.firebase.database.DataSnapshot; 13 | import com.google.firebase.database.DatabaseError; 14 | import com.google.firebase.database.DatabaseReference; 15 | import com.google.firebase.database.FirebaseDatabase; 16 | import com.google.firebase.database.ValueEventListener; 17 | import java.io.FileInputStream; 18 | import java.io.IOException; 19 | import java.util.Random; 20 | import java.util.concurrent.Semaphore; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.TimeoutException; 23 | import org.junit.rules.TestWatcher; 24 | import org.junit.runner.Description; 25 | import org.slf4j.impl.SimpleLogger; 26 | 27 | /** 28 | * This is a JUnit rule that can be used for hooking up Geofire with a real database instance. 29 | */ 30 | public final class GeoFireTestingRule extends TestWatcher { 31 | 32 | static final long DEFAULT_TIMEOUT_SECONDS = 5; 33 | 34 | private static final String ALPHA_NUM_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"; 35 | 36 | private static final String SERVICE_ACCOUNT_CREDENTIALS = "service-account.json"; 37 | 38 | private DatabaseReference databaseReference; 39 | 40 | public final String databaseUrl; 41 | 42 | /** Timeout in seconds. */ 43 | public final long timeout; 44 | 45 | public GeoFireTestingRule(final String databaseUrl) { 46 | this(databaseUrl, DEFAULT_TIMEOUT_SECONDS); 47 | } 48 | 49 | public GeoFireTestingRule(final String databaseUrl, final long timeout) { 50 | this.databaseUrl = databaseUrl; 51 | this.timeout = timeout; 52 | } 53 | 54 | @Override 55 | public void starting(Description description) { 56 | if (FirebaseApp.getApps().isEmpty()) { 57 | final GoogleCredentials credentials; 58 | 59 | try { 60 | credentials = GoogleCredentials.fromStream(new FileInputStream(SERVICE_ACCOUNT_CREDENTIALS)); 61 | } catch (IOException e) { 62 | throw new RuntimeException(e); 63 | } 64 | 65 | FirebaseOptions firebaseOptions = new FirebaseOptions.Builder() 66 | .setDatabaseUrl(databaseUrl) 67 | .setCredentials(credentials) 68 | .build(); 69 | FirebaseApp.initializeApp(firebaseOptions); 70 | 71 | System.setProperty(SimpleLogger.DEFAULT_LOG_LEVEL_KEY, "DEBUG"); 72 | } 73 | this.databaseReference = FirebaseDatabase.getInstance().getReferenceFromUrl(databaseUrl); 74 | } 75 | 76 | /** This will return you a new Geofire instance that can be used for testing. */ 77 | public GeoFire newTestGeoFire() { 78 | return new GeoFire(databaseReference.child(randomAlphaNumericString(16))); 79 | } 80 | 81 | /** 82 | * Sets a given location key from the latitude and longitude on the provided Geofire instance. 83 | * This operation will run asychronously. 84 | */ 85 | public void setLocation(GeoFire geoFire, String key, double latitude, double longitude) { 86 | setLocation(geoFire, key, latitude, longitude, false); 87 | } 88 | 89 | /** 90 | * Removes a location on the provided Geofire instance. 91 | * This operation will run asychronously. 92 | */ 93 | public void removeLocation(GeoFire geoFire, String key) { 94 | removeLocation(geoFire, key, false); 95 | } 96 | 97 | /** Sets the value on the given databaseReference and waits until the operation has successfully finished. */ 98 | public void setValueAndWait(DatabaseReference databaseReference, Object value) { 99 | final SimpleFuture futureError = new SimpleFuture(); 100 | databaseReference.setValue(value, new DatabaseReference.CompletionListener() { 101 | @Override 102 | public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { 103 | futureError.put(databaseError); 104 | } 105 | }); 106 | try { 107 | assertNull(futureError.get(timeout, TimeUnit.SECONDS)); 108 | } catch (InterruptedException e) { 109 | throw new RuntimeException(e); 110 | } catch (TimeoutException e) { 111 | fail("Timeout occured!"); 112 | } 113 | } 114 | 115 | /** 116 | * Sets a given location key from the latitude and longitude on the provided Geofire instance. 117 | * This operation will run asychronously or synchronously depending on the wait boolean. 118 | */ 119 | public void setLocation(GeoFire geoFire, String key, double latitude, double longitude, boolean wait) { 120 | final SimpleFuture futureError = new SimpleFuture(); 121 | geoFire.setLocation(key, new GeoLocation(latitude, longitude), new GeoFire.CompletionListener() { 122 | @Override 123 | public void onComplete(String key, DatabaseError error) { 124 | futureError.put(error); 125 | } 126 | }); 127 | if (wait) { 128 | try { 129 | assertNull(futureError.get(timeout, TimeUnit.SECONDS)); 130 | } catch (InterruptedException e) { 131 | throw new RuntimeException(e); 132 | } catch (TimeoutException e) { 133 | fail("Timeout occured!"); 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Removes a location on the provided Geofire instance. 140 | * This operation will run asychronously or synchronously depending on the wait boolean. 141 | */ 142 | public void removeLocation(GeoFire geoFire, String key, boolean wait) { 143 | final SimpleFuture futureError = new SimpleFuture(); 144 | geoFire.removeLocation(key, new GeoFire.CompletionListener() { 145 | @Override 146 | public void onComplete(String key, DatabaseError error) { 147 | futureError.put(error); 148 | } 149 | }); 150 | if (wait) { 151 | try { 152 | assertNull(futureError.get(timeout, TimeUnit.SECONDS)); 153 | } catch (InterruptedException e) { 154 | throw new RuntimeException(e); 155 | } catch (TimeoutException e) { 156 | fail("Timeout occured!"); 157 | } 158 | } 159 | } 160 | 161 | /** This lets you blockingly wait until the onGeoFireReady was fired on the provided Geofire instance. */ 162 | public void waitForGeoFireReady(GeoFire geoFire) throws InterruptedException { 163 | final Semaphore semaphore = new Semaphore(0); 164 | geoFire.getDatabaseReference().addListenerForSingleValueEvent(new ValueEventListener() { 165 | @Override 166 | public void onDataChange(DataSnapshot dataSnapshot) { 167 | semaphore.release(); 168 | } 169 | 170 | @Override 171 | public void onCancelled(DatabaseError databaseError) { 172 | fail("Firebase error: " + databaseError); 173 | } 174 | }); 175 | 176 | assertTrue("Timeout occured!", semaphore.tryAcquire(timeout, TimeUnit.SECONDS)); 177 | } 178 | 179 | @Override 180 | public void finished(Description description) { 181 | this.databaseReference.setValueAsync(null); 182 | this.databaseReference = null; 183 | } 184 | 185 | private static String randomAlphaNumericString(int length) { 186 | StringBuilder sb = new StringBuilder(); 187 | Random random = new Random(); 188 | for (int i = 0; i < length; i++ ) { 189 | sb.append(ALPHA_NUM_CHARS.charAt(random.nextInt(ALPHA_NUM_CHARS.length()))); 190 | } 191 | return sb.toString(); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /testing/src/main/java/com/firebase/geofire/testing/GeoQueryDataEventTestListener.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.testing; 2 | 3 | import static java.util.Locale.US; 4 | 5 | import com.firebase.geofire.GeoLocation; 6 | import com.firebase.geofire.GeoQueryDataEventListener; 7 | import com.google.firebase.database.DatabaseError; 8 | import com.google.firebase.database.DataSnapshot; 9 | 10 | /** 11 | * This listener can be used for testing your Geofire instance and asserting that certain events were sent. 12 | */ 13 | public final class GeoQueryDataEventTestListener extends TestListener implements 14 | GeoQueryDataEventListener { 15 | public static String dataEntered(String key, double latitude, double longitude) { 16 | return String.format(US, "DATA_ENTERED(%s,%f,%f)", key, latitude, longitude); 17 | } 18 | 19 | public static String dataExited(String key) { 20 | return String.format("DATA_EXITED(%s)", key); 21 | } 22 | 23 | public static String dataMoved(String key, double latitude, double longitude) { 24 | return String.format(US, "DATA_MOVED(%s,%f,%f)", key, latitude, longitude); 25 | } 26 | 27 | public static String dataChanged(String key, double latitude, double longitude) { 28 | return String.format(US, "DATA_CHANGED(%s,%f,%f)", key, latitude, longitude); 29 | } 30 | 31 | private final boolean recordEntered; 32 | private final boolean recordMoved; 33 | private final boolean recordChanged; 34 | private final boolean recordExited; 35 | 36 | /** This will by default record all of the events. */ 37 | public GeoQueryDataEventTestListener() { 38 | this(true, true, true, true); 39 | } 40 | 41 | /** Allows you to specify exactly which of the events you want to record. */ 42 | public GeoQueryDataEventTestListener(boolean recordEntered, boolean recordMoved, 43 | boolean recordChanged, boolean recordExited) { 44 | this.recordEntered = recordEntered; 45 | this.recordMoved = recordMoved; 46 | this.recordChanged = recordChanged; 47 | this.recordExited = recordExited; 48 | } 49 | 50 | @Override 51 | public void onDataEntered(DataSnapshot dataSnapshot, GeoLocation location) { 52 | if (recordEntered) { 53 | addEvent(dataEntered(dataSnapshot.getKey(), location.latitude, location.longitude)); 54 | } 55 | } 56 | 57 | @Override 58 | public void onDataExited(DataSnapshot dataSnapshot) { 59 | if (recordExited) { 60 | addEvent(dataExited(dataSnapshot.getKey())); 61 | } 62 | } 63 | 64 | @Override 65 | public void onDataMoved(DataSnapshot dataSnapshot, GeoLocation location) { 66 | if (recordMoved) { 67 | addEvent(dataMoved(dataSnapshot.getKey(), location.latitude, location.longitude)); 68 | } 69 | } 70 | 71 | @Override 72 | public void onDataChanged(DataSnapshot dataSnapshot, GeoLocation location) { 73 | if (recordChanged) { 74 | addEvent(dataChanged(dataSnapshot.getKey(), location.latitude, location.longitude)); 75 | } 76 | } 77 | 78 | @Override 79 | public void onGeoQueryReady() { 80 | // No-op. 81 | } 82 | 83 | @Override 84 | public void onGeoQueryError(DatabaseError error) { 85 | throw error.toException(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /testing/src/main/java/com/firebase/geofire/testing/GeoQueryEventTestListener.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.testing; 2 | 3 | import com.firebase.geofire.GeoLocation; 4 | import com.firebase.geofire.GeoQueryEventListener; 5 | import com.google.firebase.database.DatabaseError; 6 | 7 | import static java.util.Locale.US; 8 | 9 | /** 10 | * This listener can be used for testing your Geofire instance and asserting that certain events were sent. 11 | */ 12 | public final class GeoQueryEventTestListener extends TestListener implements GeoQueryEventListener { 13 | public static String keyEntered(String key, double latitude, double longitude) { 14 | return String.format(US, "KEY_ENTERED(%s,%f,%f)", key, latitude, longitude); 15 | } 16 | 17 | public static String keyMoved(String key, double latitude, double longitude) { 18 | return String.format(US, "KEY_MOVED(%s,%f,%f)", key, latitude, longitude); 19 | } 20 | 21 | public static String keyExited(String key) { 22 | return String.format("KEY_EXITED(%s)", key); 23 | } 24 | 25 | private final boolean recordEntered; 26 | private final boolean recordMoved; 27 | private final boolean recordExited; 28 | 29 | /** This will by default record all of the events. */ 30 | public GeoQueryEventTestListener() { 31 | this(true, true, true); 32 | } 33 | 34 | /** Allows you to specify exactly which of the events you want to record. */ 35 | public GeoQueryEventTestListener(boolean recordEntered, boolean recordMoved, boolean recordExited) { 36 | this.recordEntered = recordEntered; 37 | this.recordMoved = recordMoved; 38 | this.recordExited = recordExited; 39 | } 40 | 41 | @Override 42 | public void onKeyEntered(String key, GeoLocation location) { 43 | if (recordEntered) { 44 | addEvent(keyEntered(key, location.latitude, location.longitude)); 45 | } 46 | } 47 | 48 | @Override 49 | public void onKeyExited(String key) { 50 | if (recordExited) { 51 | addEvent(keyExited(key)); 52 | } 53 | } 54 | 55 | @Override 56 | public void onKeyMoved(String key, GeoLocation location) { 57 | if (recordMoved) { 58 | addEvent(keyMoved(key, location.latitude, location.longitude)); 59 | } 60 | } 61 | 62 | @Override 63 | public void onGeoQueryReady() { 64 | // No-op. 65 | } 66 | 67 | @Override 68 | public void onGeoQueryError(DatabaseError error) { 69 | throw error.toException(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /testing/src/main/java/com/firebase/geofire/testing/SimpleFuture.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.testing; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import java.util.concurrent.TimeoutException; 5 | import java.util.concurrent.locks.Condition; 6 | import java.util.concurrent.locks.Lock; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | 9 | /** 10 | * This is a simple version of a future that does not implement the interface. 11 | * It allows you to put a certain value in it and then blockingly getting it. 12 | */ 13 | public final class SimpleFuture { 14 | private V value; 15 | private boolean isSet; 16 | 17 | private final Lock lock = new ReentrantLock(); 18 | private final Condition condition = lock.newCondition(); 19 | 20 | /** Puts the value into the future. */ 21 | public synchronized void put(final V valueToPut) { 22 | lock.lock(); 23 | 24 | try { 25 | value = valueToPut; 26 | isSet = true; 27 | condition.signalAll(); 28 | } finally { 29 | lock.unlock(); 30 | } 31 | } 32 | 33 | /** Allows you to get the value that might be set here in a blocking way. */ 34 | public V get(final long timeout, final TimeUnit unit) throws InterruptedException, TimeoutException { 35 | lock.lock(); 36 | 37 | try { 38 | while (!isSet) { 39 | if (!condition.await(timeout, unit)) { 40 | throw new TimeoutException(); 41 | } 42 | } 43 | 44 | return value; 45 | } finally { 46 | lock.unlock(); 47 | } 48 | } 49 | 50 | /** If the value is set this method returns true and the get method will return it then. */ 51 | public boolean isDone() { 52 | return isSet; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /testing/src/main/java/com/firebase/geofire/testing/TestCallback.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.testing; 2 | 3 | import static java.util.Locale.US; 4 | 5 | import com.firebase.geofire.GeoLocation; 6 | import com.firebase.geofire.LocationCallback; 7 | import com.firebase.geofire.testing.GeoFireTestingRule; 8 | import com.google.firebase.database.DatabaseError; 9 | import java.util.concurrent.ExecutionException; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.TimeoutException; 12 | import org.junit.Assert; 13 | 14 | /** 15 | * This is a test callback for the LocationCallback interface. 16 | * It allows you to verify that a certain value was set on the given location. 17 | */ 18 | public final class TestCallback implements LocationCallback { 19 | public static String location(String key, double latitude, double longitude) { 20 | return String.format(US, "LOCATION(%s,%f,%f)", key, latitude, longitude); 21 | } 22 | 23 | public static String noLocation(String key) { 24 | return String.format("NO_LOCATION(%s)", key); 25 | } 26 | 27 | private final SimpleFuture future = new SimpleFuture<>(); 28 | 29 | /** Timeout in seconds. */ 30 | public final long timeout; 31 | 32 | public TestCallback() { 33 | this(GeoFireTestingRule.DEFAULT_TIMEOUT_SECONDS); 34 | } 35 | 36 | public TestCallback(final long timeout) { 37 | this.timeout = timeout; 38 | } 39 | 40 | /** 41 | * Returns the callback value as a string in a blocking fashion. 42 | * It's one of the values that are returned by the static factory methods above. 43 | */ 44 | public String getCallbackValue() throws InterruptedException, ExecutionException, TimeoutException { 45 | return future.get(timeout, TimeUnit.SECONDS); 46 | } 47 | 48 | @Override 49 | public void onLocationResult(String key, GeoLocation location) { 50 | if (future.isDone()) { 51 | throw new IllegalStateException("Already received callback"); 52 | } 53 | 54 | if (location != null) { 55 | future.put(location(key, location.latitude, location.longitude)); 56 | } else { 57 | future.put(noLocation(key)); 58 | } 59 | } 60 | 61 | @Override 62 | public void onCancelled(DatabaseError firebaseError) { 63 | Assert.fail("Firebase synchronization failed: " + firebaseError); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /testing/src/main/java/com/firebase/geofire/testing/TestListener.java: -------------------------------------------------------------------------------- 1 | package com.firebase.geofire.testing; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.fail; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.LinkedHashSet; 9 | import java.util.List; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.locks.Condition; 12 | import java.util.concurrent.locks.ReentrantLock; 13 | 14 | abstract class TestListener { 15 | private final List events = new ArrayList<>(); 16 | private final ReentrantLock lock = new ReentrantLock(); 17 | private final Condition condition = lock.newCondition(); 18 | 19 | void addEvent(String event) { 20 | lock.lock(); 21 | 22 | try { 23 | events.add(event); 24 | condition.signal(); 25 | } finally { 26 | lock.unlock(); 27 | } 28 | } 29 | 30 | /** 31 | * This allows you to assert that certain events were retrieved. 32 | * You can use the static factory methods on {@link GeoQueryDataEventTestListener} or 33 | * {@link GeoQueryEventTestListener} to retrieve the strings. 34 | */ 35 | public void expectEvents(Collection events) throws InterruptedException { 36 | boolean stillWaiting = true; 37 | lock.lock(); 38 | 39 | try { 40 | while (!contentsEqual(this.events, events)) { 41 | if (!stillWaiting) { 42 | assertEquals(events, new LinkedHashSet<>(this.events)); 43 | fail("Timeout occured"); 44 | return; 45 | } 46 | stillWaiting = condition.await(10, TimeUnit.SECONDS); 47 | } 48 | } finally { 49 | lock.unlock(); 50 | } 51 | } 52 | 53 | private boolean contentsEqual(Collection c1, Collection c2) { 54 | return (new LinkedHashSet<>(c1).equals(new LinkedHashSet<>(c2))); 55 | } 56 | } 57 | --------------------------------------------------------------------------------