firebase / geofire-js

GeoFire for JavaScript - Realtime location queries with Firebase
MIT License
1.45k stars 346 forks source link

Firebase Cloud Firestore #163

Closed vascoV closed 3 years ago

vascoV commented 7 years ago

Geofire is an amazing library that can be used with the FIrebase Real-time Database, is gonna be updated for the Cloud Firestore?

sonaye commented 6 years ago

You can utilize geohashes.

import g from 'ngeohash';

const store = firestore();

const addLocation = (lat, lng) =>
  store
    .collection('locations')
    .add({
      g10: g.encode_int(lat, lng, 24), // ~10 km radius
      g5: g.encode_int(lat, lng, 26), // ~5 km radius
      g1: g.encode_int(lat, lng, 30) // ~1 km radius
    });

const nearbyLocationsRef = (lat, lng, d = 1) => {
  const bits =  d === 10 ? 24 : d === 5 ? 26 : 30;

  const h = g.encode_int(lat, lng, bits);

  return store
    .collection('locations')
    .where(`g${d}`, '>=', g.neighbor_int(h, [-1, -1], bits))
    .where(`g${d}`, '<=', g.neighbor_int(h, [1, 1], bits));
};

Edit: Added some explanation.

We convert from (lat, lng) to an integer geohash that has a max. resolution of 52 bits in JS.

// Empire State Building, New York
const lat = 40.748676;
const lng = -73.985654;

// geohash with a 10 km radius resolution
g.encode_int(lat, lng, 24); // 6671229

// geohash with a 5 km radius resolution
g.encode_int(lat, lng, 26); // 26684916

// geohash with a 1 km radius resolution
g.encode_int(lat, lng, 30); // 426958662

// geohash at full resolution (most accurate)
g.encode_int(lat, lng, 52); // 1790794426114269

In order to do a proximity search, one could compute the southwest corner (low geohash with low latitude and longitude) and northeast corner (high geohash with high latitude and longitude) of a bounding box and search for geohashes between those two.

g.neighbors_int(26684916, 26); // 5 km

// [ 26684917,
//   26684919,
//   26684918,
//   26684915,
//   26684913,
//   26684891,
//   26684894,
//   26684895 ]

This translates to something like:

All locations in this 3x3 grid will have a geohash prefix that is in the range of [neighborsMin, neighborsMax] geohashes, or to be exact [southWestNeighbor, northEastNeighbor] geohashes. We can now do a simple range look up using where().

This can be done with the realtime database as well.

db
  .ref('locations')
  .orderByChild('g5')
  .startAt(g.neighbor_int(h, [-1, -1], 26))
  .endAt(g.neighbor_int(h, [1, 1], 26));

More info on how it's done + a ref. implementation (but for strings, for Redis) can be found here.

Bits to radius table

screen shot 2018-03-03 at 12 23 48 pm

Source.

For more accurate results (as described here) you will need 9 listeners for each piece of the 3x3 grid that check for strict equality.

const nearbyLocationsRefs = (lat, lng, d = 1) => {
  const bits = d === 10 ? 24 : d === 5 ? 26 : 30;

  const h = g.encode_int(lat, lng, bits);

  const n = [h, ...g.neighbors_int(h, bits)];

  const refs = [];

  n.forEach(
    (hash, i) =>
      (refs[i] = store.collection('locations').where(`g${d}`, '==', hash))
  );

  return refs;
};

Edit: Added a note about possible duplicate results.

In your .onSnapshot(handleSnapshot) handler make sure that you merge unique docs.

let locations = [];

handleSnapshot = snapshot => {
  const docs = [];

  snapshot.forEach(doc => {
    const data = doc.data();
    data.id = doc.id;
    docs.push(data);
  });

  locations = _.uniqBy(locations.concat(docs), 'id'); // lodash
};
MichaelSolati commented 6 years ago

To those who care, I had a PR for Firestore support but after a little conversation it was deemed not to be the appropriate time to bring it into the official library. However for any of you that are interested I have my geofirestore library code available here, and on npm npm i geofirestore. It works very effectively the same as the official geofire library (same class names/functions/etc...)

sonaye commented 6 years ago

Apparently we can execute a very basic text search operation in Firebase with the help of \uf8ff. We can drop the nine listeners.

db
  .collection('locations')
  .add({ g: g.encode(lat, lng) });

// ..

locations = [];

const h = g
  .encode(lat, lng)
  .substring(0, 5);
  // why 5? http://www.elastic.co/guide/en/elasticsearch/guide/current/geohashes.html

db
  .collection('locations')
  .orderBy('g')
  .startAt(h)
  .endAt(`${h}\uf8ff`)
  .onSnapshot(handleSnapshot);

handleSnapshot = snapshot => {
  const docs = [];

  snapshot.forEach(doc => {
    const data = doc.data();
    data.id = doc.id;
    docs.push(data);
  });

  locations = uniqBy(locations.concat(docs), 'id');
};
zoro238 commented 5 years ago

pls for android java

puf commented 3 years ago

We split the GeoFire libraries into two:

  1. A core GeoFireUtils library with just the bits having to do with geohashes and queries
  2. The complete library that implements #1 on top of Realtime Database

For documentation on how to use #1 with Cloud Firestore, see the documentation here: https://firebase.google.com/docs/firestore/solutions/geoqueries