Skip to content

Commit 6e66e32

Browse files
committed
refactor: convert point to pojo, rename collection to query
1 parent c408b3f commit 6e66e32

File tree

14 files changed

+324
-433
lines changed

14 files changed

+324
-433
lines changed

README.md

Lines changed: 46 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@ Realtime Geolocation with Firestore & RxJS. Runs on the Web and Node.js.
1313
:point_right: [Live Demo](https://geo-test-c92e4.firebaseapp.com)
1414
:tv: [Video Tutorial](https://angularfirebase.com/lessons/geolocation-query-in-firestore-realtime/)
1515

16-
## :checkered_flag: QuickStart
16+
## :zap: QuickStart
1717

1818
```shell
19-
npm install geofirex
20-
21-
# peer dependencies
22-
npm install rxjs firebase
19+
npm install geofirex rxjs firebase
2320
```
2421

2522
### Initialize
@@ -42,6 +39,8 @@ Node.js with the Firebase Admin SDK:
4239

4340
```js
4441
const admin = require('firebase-admin');
42+
admin.initializeApp();
43+
4544
const geo = require('geofirex').init(admin);
4645
```
4746

@@ -52,25 +51,24 @@ import * as geofirex from 'geofirex';
5251
const geo = geofirex.init(firebase);
5352
```
5453

55-
### Write Geo Data
54+
### Write Geolocation Data
5655

57-
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
56+
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.
5857

5958
```ts
60-
const cities = geo.collection('cities');
59+
const cities = firestore().collection('cities');
6160

62-
const point = geo.point(40, -119);
61+
const position = geo.point(40, -119);
6362

64-
cities.add({ name: 'Phoenix', position: point.data() });
63+
cities.add({ name: 'Phoenix', position });
6564
```
6665

67-
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.
6866

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

7169
### Query Geo Data
7270

73-
Now let's query Firestore for _cities.position within 100km radius of a centerpoint_.
71+
Query Firestore for _cities.position within 100km radius of a centerpoint_.
7472

7573
```ts
7674
const center = geo.point(40.1, -119.1);
@@ -93,14 +91,19 @@ You now have a realtime stream of data to visualize on a map.
9391

9492
## :notebook: API
9593

96-
### `collection(path: string, query? QueryFn)`
94+
### `query(ref: CollectionReference | Query | string)`
9795

98-
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
96+
Creates reference to a Firestore collection or query that can be used to make geo-queries.
9997

10098
Example:
10199

102100
```ts
103-
const collection = geo.collection('cities');
101+
const ref = geo.query('cities');
102+
103+
// OR make a geoquery on top of a firestore query
104+
105+
const query = firestore().collection('cities').where('name', '==', 'Phoenix');
106+
const geoRef = geo.query(query);
104107
```
105108

106109
#### Performing Geo-Queries
@@ -113,60 +116,46 @@ Each doc also contains returns _distance_ and _bearing_ calculated on the query
113116

114117
**Returns:** `Observable<object[]>`
115118

116-
#### Write Data
117-
118-
Write data just like you would in Firestore
119-
120-
`collection.add(data)`
121-
122-
Or use one of the client's conveniece methods
123-
124-
- `collection.setDoc(id, data)` - Set a document in the collection with an ID.
125-
- `collection.setPoint(id, field, lat, lng)`- Add a geohash to an existing doc
126-
127-
#### Read Data
128-
129-
In addition to Geo-Queries, you can also read the collection like you would normally in Firestore, but as an Observable
130-
131-
- `collection.data()`- Observable of document data
132-
- `collection.snapshot()`- Observable of Firestore QuerySnapshot
133119

134120
### `point(latitude: number, longitude: number)`
135121

136122
Returns a GeoFirePoint allowing you to create geohashes, format data, and calculate relative distance/bearing.
137123

138124
Example: `const point = geo.point(38, -119)`
139125

140-
#### Get Data
141-
142-
A point can return data in a variety of formats.
126+
A point is a plain JS object with two properties.
143127

144-
- `point.hash()` Returns a geohash string at precision 9
145-
- `point.geoPoint()` Returns a Firestore GeoPoint
146-
- `point.geoJSON()` Returns data as a GeoJSON `Feature<Point>`
147-
- `point.coords()` Returns coordinates as `[latitude, longitude]`
148-
- `point.data()` Returns data object suitable for saving to the Firestore database
128+
- `point.geohash` Returns a geohash string at precision 9
129+
- `point.geopoint` Returns a Firestore GeoPoint
149130

150-
#### Geo Calculations
151-
152-
- `point.distance(latitude, longitude)` Haversine distance to a point
153-
- `point.bearing(latitude, longitude)` Haversine bearing to a point
154131

155132
## :pizza: Additional Features
156133

157134
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.
158135

159136
### Logging
160137

138+
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.
139+
140+
```js
141+
ref.within(center, radius, field, { log: true })
142+
```
161143

162144
![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)
163145

146+
### Geo Calculations
147+
148+
Convenience methods for calculating distance and bearing.
149+
150+
- `geo.distance(to, from)` Haversine distance
151+
- `geo.bearing(to, from)` Haversine bearing
152+
164153
### `toGeoJSON` Operator
165154

166155
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.
167156

168157
```ts
169-
const query = geo.collection('cars').within(...)
158+
const query = geo.query('cars').within(...)
170159

171160
query.pipe( toGeoJSON() )
172161

@@ -185,29 +174,32 @@ Don't need a realtime stream? Convert any query observable to a promise by wrapp
185174
import { get } from 'geofirex';
186175

187176
async function getCars {
188-
const query = geo.collection('cars').within(...)
177+
const query = geo.query('cars').within(...)
189178
const cars = await get(query)
190179
}
191180
```
192181

193182
## :zap: Tips
194183

195-
### Scale to Massive Collections
184+
### Compound Queries
196185

197-
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.
186+
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.
198187

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

201189
Example:
202190

203191
```ts
204-
const users = geo.collection('users', ref =>
205-
ref.where('status', '==', 'online')
206-
);
192+
// Make a query like you normally would
193+
const query = firestore().collection('users').where('status', '==', 'online');
194+
195+
const users = geo.query(query)
207196

208197
const nearbyOnlineUsers = users.within(center, radius, field);
209198
```
210199

200+
Note: This query requires a composite index, which you will be prompted to create with an error from Firestore on the first request.
201+
202+
211203
### Usage with RxJS < 6.2, or Ionic v3
212204

213205
This package requires RxJS 6.2, but you can still use it with older versions without blowing up your app by installing rxjs-compat.
@@ -218,24 +210,11 @@ Example:
218210
npm i rxjs@latest rxjs-compat
219211
```
220212

221-
### Seeing this error: `DocumentReference.set() called with invalid data`
222-
223-
Firestore writes cannot use custom classes, so make sure to call the `data` getter on the point.
224-
225-
```ts
226-
const point = geo.point(40, -50);
227-
// This is an ERROR
228-
ref.add({ location: point });
229-
230-
// This is GOOD
231-
ref.add({ location: point.data() });
232-
```
233-
234213
### Make Dynamic Queries the RxJS Way
235214

236215
```ts
237216
const radius = new BehaviorSubject(1);
238-
const cities = geo.collection('cities');
217+
const cities = geo.query('cities');
239218

240219
const points = this.radius.pipe(
241220
switchMap(rad => {
@@ -249,4 +228,4 @@ radius.next(23);
249228

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

252-
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.
231+
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.

example/src/app/basic-geoquery/basic-geoquery.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class BasicGeoqueryComponent implements OnInit {
2424
this.points = this.radius.pipe(
2525
switchMap(r => {
2626
console.log('new rad');
27-
return this.geo.collection('bearings').within(center, r, field, { log: true });
27+
return this.geo.query('bearings').within(center, r, field, { log: true });
2828
}),
2929
shareReplay(1)
3030
);

example/src/app/realtime-geoquery/realtime-geoquery.component.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Component, OnInit, OnDestroy } from '@angular/core';
22
import { Observable, BehaviorSubject, interval } from 'rxjs';
33
import { switchMap, tap, map, take, finalize } from 'rxjs/operators';
4-
import * as firebaseApp from 'firebase/app';
4+
import * as firebase from 'firebase/app';
55
import * as geofirex from 'geofirex';
66
import { GeoFireCollectionRef } from 'geofirex';
77
import { Point, Feature } from 'geojson';
@@ -12,27 +12,30 @@ import { Point, Feature } from 'geojson';
1212
styleUrls: ['./realtime-geoquery.component.scss']
1313
})
1414
export class RealtimeGeoqueryComponent implements OnInit, OnDestroy {
15-
geo = geofirex.init(firebaseApp);
16-
points: any;
15+
geo = geofirex.init(firebase);
16+
points: Observable<any>;
1717
testDoc;
1818

19-
collection: GeoFireCollectionRef;
19+
path: 'positions';
20+
collection;
21+
geoCollection;
2022
clicked;
21-
docId;
23+
docId = 'testPoint' + Date.now();
2224

2325
constructor() {
26+
this.collection = firebase.firestore().collection('positions');
2427
window.onbeforeunload = () => {
25-
this.collection.delete(this.docId);
28+
this.collection.doc(this.docId).delete();
2629
};
2730
}
2831

2932
ngOnInit() {
30-
this.collection = this.geo.collection('positions');
33+
this.geoCollection = this.geo.query('positions');
3134
const center = this.geo.point(34, -113);
3235

33-
this.points = this.collection.within(center, 200, 'pos');
36+
this.points = this.geoCollection.within(center, 200, 'pos');
3437
this.testDoc = this.points.pipe(
35-
map(arr => (arr as any[]).find(o => o.id === this.docId))
38+
map(arr => arr.find(o => o.id === this.docId))
3639
);
3740

3841
// this.testDoc.subscribe(x => {
@@ -51,7 +54,7 @@ export class RealtimeGeoqueryComponent implements OnInit, OnDestroy {
5154

5255
const randA = this.rand();
5356
const randB = this.rand();
54-
this.docId = 'testPoint' + Date.now();
57+
5558
interval(700)
5659
.pipe(
5760
take(30),
@@ -60,14 +63,14 @@ export class RealtimeGeoqueryComponent implements OnInit, OnDestroy {
6063
lng += randB * Math.random();
6164

6265
const point = this.geo.point(lat, lng);
63-
const data = { name: 'testPoint', pos: point.data(), allow: true };
64-
this.collection.setDoc(this.docId, data);
66+
const data = { name: 'testPoint', pos: point, allow: true };
67+
this.collection.doc(this.docId).set(data);
6568
console.log(v);
6669
}),
6770
finalize(() => {
6871
this.clicked = false;
6972
// this.collection.setPoint('testPoint', 32.9, -114.2, 'pos');
70-
this.collection.delete(this.docId);
73+
this.collection.doc(this.docId).delete();
7174
})
7275
)
7376
.subscribe();

0 commit comments

Comments
 (0)