Realtime Geolocation with Firestore & RxJS. Query geographic points within a radius on the web or Node.js.
- ๐ Live Demo
- ๐บ Video Tutorial
npm install geofirex rxjs firebase
The library is a lightweight extension for the Firebase Web and Admin JavaScript SDKs to provide tools for wrangling geolocation data in Firestore.
Web:
// Init Firebase
import firebase from 'firebase/app';
firebase.initializeApp(yourConfig);
// Init GeoFireX
import geofirex from 'geofirex';
const geo = geofirex.init(firebase);
Node.js with the Firebase Admin SDK:
const admin = require('firebase-admin');
admin.initializeApp();
const geo = require('geofirex').init(admin);
With Typescript:
import firebase from 'firebase/compat/app';
import * as geofirex from 'geofirex';
const geo = geofirex.init(firebase);
Next, add some geolocation data in your database using the main Firebase SDK. You can add multiple points to a single doc. Calling geo.point(lat, lng)
creates an object with a geohash string and a Firestore GeoPoint. Data must be saved in this format to be queried.
const cities = firestore().collection('cities');
const position = geo.point(40, -119);
cities.add({ name: 'Phoenix', position });
Query Firestore for cities.position within 100km radius of a centerpoint.
const center = geo.point(40.1, -119.1);
const radius = 100;
const field = 'position';
const query = geo.query(cities).within(center, radius, field);
Each hit returns a realtime Observable of the document data, plus some useful hitMetadata
like distance and bearing from the query centerpoint.
query.subscribe(console.log);
// [{ ...documentData, hitMetadata: { distance: 1.23232, bearing: 230.23 } }]
You now have a realtime stream of data to visualize on a map.
Creates reference to a Firestore collection or query that can be used to make geo-queries.
Example:
const geoRef = geo.query('cities');
// OR make a geoquery on top of a firestore query
const firestoreRef = firestore().collection('cities').where('name', '==', 'Phoenix');
const geoRef = geo.query(firestoreRef);
const query = geoRef.within(center: FirePoint, radius: number, field: string)
query.subscribe(hits => console.log(hits))
// OR fetch as a promise
import { get } from 'geofirex';
const hits = await get(query);
Query the parent Firestore collection by geographic distance. It will return documents that exist within X kilometers of the centerpoint.
Each doc also contains returns distance and bearing calculated on the query on the hitMetadata
property.
Returns an object with the required geohash format to save to Firestore.
Example: const point = geo.point(38, -119)
A point is a plain JS object with two properties.
point.geohash
Returns a geohash string at precision 9point.geopoint
Returns a Firestore GeoPoint
The goal of this package is to facilitate rapid feature development with tools like MapBox, Google Maps, and D3.js. If you have an idea for a useful feature, open an issue.
Each query runs on a set of geohash squares, so you may read more documents than actually exist inside the radius. Use the log
option to examine the total query size and latency.
query.within(center, radius, field, { log: true })
Convenience methods for calculating distance and bearing.
geo.distance(geo.point(38, -118), geo.point(40, -115))
Haversine distancegeo.bearing(to, from)
Haversine bearing
A custom RxJS operator that transforms a collection into a GeoJSON FeatureCollection. Very useful for tools like MapBox that can use GeoJSON to update a realtime data source.
import { toGeoJSON } from 'geofirex';
const query = geo.query('cars').within(...)
query.pipe( toGeoJSON() )
// Emits a single object typed as a FeatureCollection<Geometry>
{
"type": "FeatureCollection",
"features": [...]
}
Don't need a realtime stream? Convert any query observable to a promise by wrapping it with get
.
import { get } from 'geofirex';
async function getCars {
const query = geo.query('cars').within(...)
const cars = await get(query)
}
The only well-supported type of compound query is where
. A geoquery combines multiple smaller queries into a unified radius, so limit
and pagination operators will not provide predictable results - a better approach is to search a smaller radius and do your sorting client-side.
Example:
// Make a query like you normally would
const users = firestore().collection('users').where('status', '==', 'online');
const nearbyOnlineUsers = geo.query(users).within(center, radius, field);
Note: This query requires a composite index, which you will be prompted to create with an error from Firestore on the first request.
This package requires RxJS 6.2, but you can still use it with older versions without blowing up your app by installing rxjs-compat.
Example:
npm i rxjs@latest rxjs-compat
const radius = new BehaviorSubject(1);
const cities = geo.query('cities');
const points = this.radius.pipe(
switchMap(rad => {
return cities.within(center, rad, 'point');
})
);
// Now update your query
radius.next(23);
The GeoJSON spec formats coords as [Longitude, Latitude]
to represent an X/Y plane. However, the Firebase GeoPoint uses [Latitude, Longitude]
. For consistency, this library always requires to use the latter Firebase-style format.