Skip to content

Commit

Permalink
Merge pull request #78 from codediodeio/node-support
Browse files Browse the repository at this point in the history
Node support
  • Loading branch information
codediodeio authored Nov 7, 2019
2 parents a1d2859 + e7ae646 commit 41a224b
Show file tree
Hide file tree
Showing 74 changed files with 21,301 additions and 20,123 deletions.
159 changes: 82 additions & 77 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,67 @@

# GeoFireX

Realtime Geolocation with Firestore & RxJS
Realtime Geolocation with Firestore & RxJS. Query geographic points within a radius on the web or Node.js.

:point_right: [Live Demo](https://geo-test-c92e4.firebaseapp.com)
:tv: [Video Tutorial](https://angularfirebase.com/lessons/geolocation-query-in-firestore-realtime/)
- :point_right: [Live Demo](https://geo-test-c92e4.firebaseapp.com)
- :tv: [Video Tutorial](https://angularfirebase.com/lessons/geolocation-query-in-firestore-realtime/)

## :checkered_flag: QuickStart
## :zap: QuickStart

```shell
npm install geofirex

# peer dependencies
npm install rxjs firebase
npm install geofirex rxjs firebase
```

### Initialize

The library is a lightweight client for the Firebase Web SDK that provides tools for wrangling geolocation data in Firestore. You need a [Firebase project](https://firebase.google.com/docs/storage/web/start) to get started.
The library is a lightweight extension for the Firebase Web and Admin JavaScript SDKs to provide tools for wrangling geolocation data in Firestore.

```ts
Web:

```js
// Init Firebase
import * as firebase from 'firebase/app';
import firebase from 'firebase/app';
firebase.initializeApp(yourConfig);

// Init GeoFireX
import * as geofirex from 'geofirex';
import geofirex from 'geofirex';
const geo = geofirex.init(firebase);
```

### Write Geo Data
Node.js with the Firebase Admin SDK:

Next, add some geolocation data in your database. A `collection` creates a reference to Firestore (just like the SDK), but with some extra geolocation tools. The `point` method returns a class that helps you create geolocation data.
```js
const admin = require('firebase-admin');
admin.initializeApp();

const geo = require('geofirex').init(admin);
```

With Typescript:

```ts
const cities = geo.collection('cities');
import * as geofirex from 'geofirex';
const geo = geofirex.init(firebase);
```

### Write Geolocation Data

const point = geo.point(40, -119);
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](https://www.movable-type.co.uk/scripts/geohash.html) and a [Firestore GeoPoint](https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/GeoPoint). Data must be saved in this format to be queried.

cities.add({ name: 'Phoenix', position: point.data });
```ts
const cities = firestore().collection('cities');

const position = geo.point(40, -119);

cities.add({ name: 'Phoenix', position });
```

Calling `point.data` returns an object that contains a [geohash string](https://www.movable-type.co.uk/scripts/geohash.html) and a [Firestore GeoPoint](https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/GeoPoint). It should look like this in your database. You can name the object whatever you want and even save multiple points on a single document.

![](https://firebasestorage.googleapis.com/v0/b/geo-test-c92e4.appspot.com/o/point1.png?alt=media&token=0c833700-3dbd-476a-99a9-41c1143dbe97)

### Query Geo Data

Now let's query Firestore for _cities.position within 100km radius of a centerpoint_.
Query Firestore for _cities.position within 100km radius of a centerpoint_.

```ts
const center = geo.point(40.1, -119.1);
Expand All @@ -64,11 +78,11 @@ const field = 'position';
const query = cities.within(center, radius, field);
```

The query returns a realtime Observable of the document data, plus some useful metadata like _distance_ and _bearing_ from the query centerpoint.
Each hit returns a realtime Observable of the document data, plus some useful `hitMetadata` like _distance_ and _bearing_ from the query centerpoint.

```ts
query.subscribe(console.log);
// [{ ...documentData, queryMetadata: { distance: 1.23232, bearing: 230.23 } }]
// [{ ...documentData, hitMetadata: { distance: 1.23232, bearing: 230.23 } }]
```

You now have a realtime stream of data to visualize on a map.
Expand All @@ -77,73 +91,74 @@ You now have a realtime stream of data to visualize on a map.

## :notebook: API

### `collection(path: string, query? QueryFn)`
### `query(ref: CollectionReference | Query | string)`

Creates reference to a Firestore collection that can be used to make geo-queries and perform writes If you pass an optional Firestore query function, all subsequent geo-queries will be limited to this subset of documents
Creates reference to a Firestore collection or query that can be used to make geo-queries.

Example:

```ts
const collection = geo.collection('cities');
const geoQuery = geo.query('cities');

// OR make a geoquery on top of a firestore query

const query = firestore().collection('cities').where('name', '==', 'Phoenix');
const geoQuery = geo.query(query);
```

#### Performing Geo-Queries
#### Perform Geo-Queries

`collection.within(center: GeoFirePoint, radius: number, field: string)`
```js
geoQuery.within(center: FirePoint, radius: number, field: string)
.subscribe((hits) => console.log((hits)))
```

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 `queryMetadata` property.
Each doc also contains returns _distance_ and _bearing_ calculated on the query on the `hitMetadata` property.

**Returns:** `Observable<object[]>`
**Returns:** `Observable<T[]>`

#### Write Data

Write data just like you would in Firestore
### `point(latitude: number, longitude: number): FirePoint`

`collection.add(data)`
Returns an object with the required geohash format to save to Firestore.

Or use one of the client's conveniece methods

- `collection.setDoc(id, data)` - Set a document in the collection with an ID.
- `collection.setPoint(id, field, lat, lng)`- Add a geohash to an existing doc
Example: `const point = geo.point(38, -119)`

#### Read Data
A point is a plain JS object with two properties.

In addition to Geo-Queries, you can also read the collection like you would normally in Firestore, but as an Observable
- `point.geohash` Returns a geohash string at precision 9
- `point.geopoint` Returns a Firestore GeoPoint

- `collection.data()`- Observable of document data
- `collection.snapshot()`- Observable of Firestore QuerySnapshot

### `point(latitude: number, longitude: number)`
## Additional Features

Returns a GeoFirePoint allowing you to create geohashes, format data, and calculate relative distance/bearing.
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.

Example: `const point = geo.point(38, -119)`
### Logging

#### Getters
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.

- `point.hash` Returns a geohash string at precision 9
- `point.geoPoint` Returns a Firestore GeoPoint
- `point.geoJSON` Returns data as a GeoJSON `Feature<Point>`
- `point.coords` Returns coordinates as `[latitude, longitude]`
- `point.data` Returns data object suitable for saving to the Firestore database
```js
ref.within(center, radius, field, { log: true })
```

#### Geo Calculations
![Logging GeoQueries](https://firebasestorage.googleapis.com/v0/b/geo-test-c92e4.appspot.com/o/geofirex-logging.PNG?alt=media&token=9b8b487d-18b2-4e5f-bb04-564fa6f2996d)

- `point.distance(latitude, longitude)` Haversine distance to a point
- `point.bearing(latitude, longitude)` Haversine bearing to a point
### Geo Calculations

## :pizza: Additional Features
Convenience methods for calculating distance and bearing.

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.
- `geo.distance(geo.point(38, -118), geo.point(40, -115))` Haversine distance
- `geo.bearing(to, from)` Haversine bearing

### `toGeoJSON` Operator

A custom RxJS operator that transforms a collection into a [GeoJSON FeatureCollection](https://macwright.org/2015/03/23/geojson-second-bite.html#featurecollection). Very useful for tools like [MapBox](https://blog.mapbox.com/real-time-maps-for-live-events-fad0b334e4e) that can use GeoJSON to update a realtime data source.

```ts
const query = geo.collection('cars').within(...)
const query = geo.query('cars').within(...)

query.pipe( toGeoJSON() )

Expand All @@ -162,29 +177,32 @@ Don't need a realtime stream? Convert any query observable to a promise by wrapp
import { get } from 'geofirex';

async function getCars {
const query = geo.collection('cars').within(...)
const query = geo.query('cars').within(...)
const cars = await get(query)
}
```

## :zap: Tips
## Tips

### Scale to Massive Collections
### Compound Queries

It's possibe to build Firestore collections with billions of documents. One of the main motivations of this project was to make geoqueries possible on a queried subset of data. You can make a regular Firestore query on collection by passing a callback as the second argument, then all geoqueries will scoped these contstraints.
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.

Note: This query requires a composite index, which you will be prompted to create with an error from Firestore on the first request.

Example:

```ts
const users = geo.collection('users', ref =>
ref.where('status', '==', 'online')
);
// Make a query like you normally would
const query = firestore().collection('users').where('status', '==', 'online');

const users = geo.query(query)

const nearbyOnlineUsers = 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.


### Usage with RxJS < 6.2, or Ionic v3

This package requires RxJS 6.2, but you can still use it with older versions without blowing up your app by installing rxjs-compat.
Expand All @@ -195,24 +213,11 @@ Example:
npm i rxjs@latest rxjs-compat
```

### Seeing this error: `DocumentReference.set() called with invalid data`

Firestore writes cannot use custom classes, so make sure to call the `data` getter on the point.

```ts
const point = geo.point(40, -50);
// This is an ERROR
ref.add({ location: point });

// This is GOOD
ref.add({ location: point.data });
```

### Make Dynamic Queries the RxJS Way

```ts
const radius = new BehaviorSubject(1);
const cities = geo.collection('cities');
const cities = geo.query('cities');

const points = this.radius.pipe(
switchMap(rad => {
Expand All @@ -226,4 +231,4 @@ radius.next(23);

### Always Order by `[Latitude, Longitude]`

The GeoJSON spec formats coords as `[Longitude, Latitude]` to represent an X/Y plane. However, the Firebase GeoPoint uses `[Latitude, Longitude]`. For consistency, this libary will always require you to use the latter format.
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.
2 changes: 1 addition & 1 deletion integration/.editorconfig → example/.editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Editor configuration, see http://editorconfig.org
# Editor configuration, see https://editorconfig.org
root = true

[*]
Expand Down
File renamed without changes.
7 changes: 7 additions & 0 deletions integration/.gitignore → example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out

# dependencies
/node_modules

# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json

# IDEs and editors
/.idea
.project
Expand All @@ -23,6 +29,7 @@
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*

# misc
/.sass-cache
Expand Down
4 changes: 2 additions & 2 deletions integration/README.md → example/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Integration
# Example

This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.8.
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.17.

## Development server

Expand Down
Loading

0 comments on commit 41a224b

Please sign in to comment.