How to save GeoFire coordinates along with other items in Firebase database?
Asked Answered
G

4

19

I'm developing an app and saving some strings like postedAtTime, postedBy, postedOnDate in Firebase database. I want to save the GeoFire coordinates in the same node in which all the above string are saved, so that later I can do query, easily.

Here's the path to which I'm saving all the strings:

databaseReferenceHRequests = firebaseDatabase.getReferenceFromUrl("https://appName-e1a35.firebaseio.com/requests/");

This is how I'm saving it:

// in onButtonClicked method:
postNewRequest(null, imageUID, MainActivity.userName.getText().toString(), time, date, utcFormatDateTime, MainActivity.userEmail.getText().toString(), geoFire);

// the method:
public void postNewRequest(Bitmap bitmap, String imageUIDh, String postedBy, String postedAtTime, String postedOnDate, String utcFormatDateTime, String userEmail, GeoFire geoFire) {
    HRequest hRequest = new HelpRequest(null, imageUIDh, postedBy, postedAtTime, postedOnDate, utcFormatDateTime, userEmail, geoFire);
    databaseReferenceHRequests.push().setValue(hRequest);
}

Here's how it is getting saved in the database:

database structure screenshot

What I want is to save the GeoFire coordinates in the same node, which is -KLIoLUsI0SpQZGpV1h4 here. This is just a push ID and it gets generated randomly.

I tried it by giving this reference:

geoFire = new GeoFire(firebaseDatabase.getReferenceFromUrl("https://appName-e1a35.firebaseio.com/requests/"));

And then pushing it with other items as shown above. But, this saved only GeoFire coordinates and not the other items under the node requests.

So, what should be my GeoFire reference so that it gets saved along with all the data in the same node?

What is going wrong here? Please let me know.

Grassofparnassus answered 13/7, 2016 at 0:43 Comment(0)
S
39

Frank's answer is correct, but I want to give an example. Your database structure should be like this.

{
"items" : {
    <itemId> : {
        "someData" : "someData",
        ...
    }
  },
"items_location" : {
    <itemId> : {
        <geofireData> ...
    }
  }
}

To get the data, first you need to do GeoQuery at items_location node and then get the data on the onKeyEntered method. The parameter key is itemId from my example.

geoFire = new GeoFire(FirebaseDatabase.getInstance().getReference().child("items_location");
geoQuery = geoFire.queryAtLocation(geoLocation), radius);
geoQuery.addGeoQueryEventListener(new GeoQueryEventListener() {
    @Override
    public void onKeyEntered(String key, GeoLocation location) {
         //retrieve data
    }
};    

Hope this helps.

EDIT How to push the item and set the geofire data.

String itemId = ref.child("items").push().getKey();

ref.child("items").child(itemId).setValue(item);

geoFire = new GeoFire(ref.child("items_location"));
geoFire.setLocation(itemId, new GeoLocation(lattitude, longitude));

EDIT Save the item data and geofire data in one API call

GeoHash geoHash = new GeoHash(new GeoLocation(latitude, longitude));
Map<String, Object> updates = new HashMap<>();
updates.put("items/" + itemId, item);
updates.put("items_location/" + itemId + "/g", geoHash.getGeoHashString());
updates.put("items_location/" + itemId + "/l", Arrays.asList(latitude, longitude));
ref.updateChildren(updates);
Schoolroom answered 13/7, 2016 at 6:53 Comment(13)
Thanks for your answer. The problem is that my <itemId> is a push id. How can I keep track of it? Please let me know.Grassofparnassus
is it necessary to first get the itemId and then push data... can't I push data first and then fetch the itemId (pushId)?Grassofparnassus
looks like there's no other efficient way than this. if you push the data first by calling ref.push().setValue(item), you can't get the push id so it's not possible to do geoFire.setLocation().Schoolroom
Sorry to bring up an old post, but how can I handle the atomicity of both ops ? I mean, I could use the updateChildren, but geofire handles the insertion internally.Zimmerman
@Zimmerman yes, you can. read here GeoFire.java at setLocation(String, GeoLocation, CompletionListener) method. You should get the idea already. :)Schoolroom
Thanks for the quick reply ! Actually that's not 100% true, because using the callback I can still get some error when inserting the new data, leaving the GeoLocation orphan.Zimmerman
Thanks, that's what I had in mind. I opened an issue on GeoFire to see if it makes sense to implement something like that out-of-the-box.Zimmerman
Why do we need to add "/g" or "/l" to that "items_location/" ?Hackman
Does this mean if I get 50 keys within a certain location query, I will have to do 50 extra separate http calls to get the objects themselves? Seems hugely inefficient...Obed
@Obed sadly yes :)Schoolroom
If I need to save altitude data, it seems I should set it as a property under items. Am I correct?Selfdiscipline
@Selfdiscipline GeoLocation doesn't store altitude, so yes you have to save it under items :)Schoolroom
Follow-up question "Does GeoFire enforce identical keys between item and item_location?": https://mcmap.net/q/665359/-does-firebase-quot-geofire-quot-enforce-identical-keys-between-nodes-with-geo-and-non-geo-data/2327328Undistinguished
L
13

When you use Geofire, you have two lists of data:

  1. a list of items with their regular properties
  2. a list of geohash indexes and their associated keys, which you query through Geofire

You use the keys to get from the Geoquery results to the regular items. That's why the events for Geofire are called "Key Entered", "Key Exited", etc.

Trying to store them in one node is a bad idea, since you're mixing mostly static data (the properties of your items) with highly volatile data (the geo-location information). Separating the two out leads to better performance, which is why Geofire enforces it.

While there may be use-cases where the properties and geo-data are equally dynamic/static, GeoFire does not support keeping the geo-data and other properties in a single location.

Logistics answered 13/7, 2016 at 1:9 Comment(3)
Thanks for answering, but I am not sure I got the answer to my question. I mean how would I be able to retrieve only those posts from database which is within 0.5 km radius of the user? I'm also saving the latitude and longitude from where the posts were posted. Please make it more clear, sir.Grassofparnassus
Does Geofire return just the keys from the node and then you have to query the regular properties node. Or does Geofire also return the regular properties data?Afterlife
@Frank can you please explains how to store geohash indexes and what will be its structurreMarentic
D
1

You can use Firebase functions to enter it for you on every new entry

let functions = require('firebase-functions');

let GeoFire = require('geofire');

exports.testLocation = functions.database.ref('/items/{item}').onWrite(event => {
let data = event.data.val();
   console.log(data);
   console.log(event.params.item);

   if (data.location && data.location.coords) {

       console.log('Update GeoFire');

       let ref = event.data.adminRef.parent.parent.child('/items_locations'));

       let key = event.params.test;
       let location = [data.location.coords.latitude, data.location.coords.longitude]);
       let geoFire = new GeoFire(ref);

       geoFire.set(key, location).then(() => {
          console.log('Update succesfull');
       }).catch(error => {
          console.log(error);
       });
    }
}
Diaphragm answered 15/5, 2017 at 17:51 Comment(0)
K
1

For those more recently coming to this post with the same question, this is possible with the Geofirestore library, which supports Geofire for apps built on top of the Firebase Firestore database.

Karole answered 10/7, 2018 at 21:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.