Skip to content

Commit 6db92f7

Browse files
committed
update: improve docs & specs
1 parent 3379be7 commit 6db92f7

File tree

9 files changed

+95
-32
lines changed

9 files changed

+95
-32
lines changed

README.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
Realtime Geolocation with Firestore & RxJS
1212

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

1516
## :checkered_flag: QuickStart
1617

@@ -105,7 +106,7 @@ Write data just like you would in Firestore
105106
Or use one of the client's conveniece methods
106107

107108
- `collection.setDoc(id, data)` - Set a document in the collection with an ID.
108-
- `collection.setPoint(lat, lng)` - Non-destructive update with a GeoFirePoint
109+
- `collection.setPoint(id, field, lat, lng)`- Add a geohash to an existing doc
109110

110111
#### Read Data
111112

@@ -144,7 +145,7 @@ A custom RxJS operator that transforms a collection into a [GeoJSON FeatureColle
144145
```ts
145146
const query = geo.collection('cars').within(...)
146147

147-
query.pipe( getGeoJSON() )
148+
query.pipe( toGeoJSON() )
148149

149150
// Emits a single object typed as a FeatureCollection<Geometry>
150151
{
@@ -172,11 +173,13 @@ async function getCars {
172173

173174
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.
174175

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

177180
```ts
178181
const users = geo.collection('users', ref =>
179-
ref.where('status', '==', 'online').limit(1000)
182+
ref.where('status', '==', 'online')
180183
);
181184

182185
const nearbyOnlineUsers = users.within(center, radius, field);

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

+2-6
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,8 @@ export class BasicGeoqueryComponent implements OnInit {
2222
const radius = 0.5;
2323
const field = 'pos';
2424

25-
// this.geo.collection('bearings').within(center, radius, field);
26-
27-
this.points = this.radius.pipe(
28-
switchMap(r => {
29-
return this.geo.collection('bearings').within(center, r, field);
30-
})
25+
const collection = this.geo.collection('users', ref =>
26+
ref.where('status', '==', 'single').where('online', '==', true)
3127
);
3228
}
3329

spec/main.spec.ts

+40-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as firebase from 'firebase/app';
22
import 'firebase/firestore';
33

4-
import { config } from './util';
4+
import { config, mockResponse } from './util';
55

66
import * as _ from 'lodash';
77
import 'jest';
88

9-
import { GeoFirePoint } from '../src/geohash';
9+
import { GeoFirePoint } from '../src/point';
1010
import { GeoFireCollectionRef, toGeoJSON, get } from '../src/collection';
11-
import { Observable, BehaviorSubject } from 'rxjs';
11+
import { Observable, BehaviorSubject, of } from 'rxjs';
1212
import { first, take, switchMap } from 'rxjs/operators';
1313

1414
import { GeoFireClient } from '../src/client';
@@ -153,14 +153,26 @@ describe('RxGeofire', () => {
153153
});
154154
});
155155

156-
describe('geoqueries', () => {
156+
describe('within(...) queries', () => {
157157
let ref: GeoFireCollectionRef;
158158
let center;
159159
beforeEach(() => {
160160
ref = gfx.collection('bearings');
161161
center = gfx.point(40.5, -80.0);
162162
});
163163

164+
test('work with compound Firestore queries', async done => {
165+
const ref = gfx.collection('compound', ref =>
166+
ref.where('color', '==', 'blue')
167+
);
168+
const point = gfx.point(38, -119);
169+
const query = ref.within(point, 50, 'point');
170+
171+
const val = await resolve(query);
172+
expect(val.length).toBe(1);
173+
done();
174+
});
175+
164176
test('should return 16 positions within 10km radius', async done => {
165177
const query = ref.within(center, 10, 'pos');
166178
expect(query).toBeInstanceOf(Observable);
@@ -214,24 +226,38 @@ describe('RxGeofire', () => {
214226
});
215227

216228
describe('Custom Operators', () => {
229+
test('toGeoJSON should map a collection to GeoJSON', async done => {
230+
const val = (await get(of(mockResponse).pipe(toGeoJSON('point')))) as any;
231+
expect(val.type).toEqual('FeatureCollection');
232+
done();
233+
});
234+
});
235+
236+
describe('Query Shape', () => {
217237
let ref: GeoFireCollectionRef;
218-
let center: GeoFirePoint;
219-
beforeEach(() => {
220-
ref = gfx.collection('bearings', ref => ref.limit(1));
238+
let center;
239+
let data;
240+
beforeAll(async () => {
241+
ref = gfx.collection('bearings');
221242
center = gfx.point(40.5, -80.0);
243+
data = await get(ref.within(center, 5, 'pos'));
222244
});
223-
224-
test.skip('toGeoJSON should map a collection to GeoJSON', async done => {
225-
const query = ref.within(center, 0.5, 'pos').pipe(toGeoJSON('pos'));
226-
const val = await resolve(query);
227-
expect(val.type).toEqual('FeatureCollection');
245+
test('should have query metadata', async done => {
246+
expect(data[0].queryMetadata.bearing).toBeDefined();
247+
expect(data[0].queryMetadata.distance).toBeDefined();
248+
done();
249+
});
250+
test('should be ordered by distance', async done => {
251+
const first = data[0].queryMetadata.distance;
252+
const last = data[data.length - 1].queryMetadata.distance;
253+
expect(first).toBeCloseTo(0.2);
254+
expect(last).toBeCloseTo(5);
255+
expect(first).toBeLessThan(last);
228256
done();
229257
});
230258
});
231259
});
232260

233-
// import { seed } from './seed';
234-
// seed();
235261
function sleep(delay) {
236262
const start = Date.now();
237263
while (Date.now() < start + delay);

spec/seed.ts

+7
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,10 @@
5151
// }
5252
// }
5353
// }
54+
55+
// export async function seedOne(gfx: GeoFireClient) {
56+
// const col = gfx.collection('compound');
57+
// const point = gfx.point(38, -119);
58+
// await col.setDoc(`foo${Date.now()}`, { point: point.data, color: 'blue' });
59+
// console.log('foo');
60+
// }

spec/util.ts

+27
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,30 @@ export const config = {
66
storageBucket: 'geo-test-c92e4.appspot.com',
77
messagingSenderId: '200126650097'
88
};
9+
10+
import * as firebase from 'firebase/app';
11+
12+
export const mockResponse = [
13+
{
14+
title: 'mock1',
15+
point: {
16+
geopoint: new firebase.firestore.GeoPoint(28, -119),
17+
geohash: 'xxxxxxxxx'
18+
},
19+
queryMetadata: {
20+
bearing: 90.2,
21+
distance: 120.2
22+
}
23+
},
24+
{
25+
title: 'mock2',
26+
point: {
27+
geopoint: new firebase.firestore.GeoPoint(38, -90),
28+
geohash: 'xxxxxxxxx'
29+
},
30+
queryMetadata: {
31+
bearing: 40.2,
32+
distance: 20.2
33+
}
34+
}
35+
];

src/client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { firestore } from './interfaces';
22

33
import { GeoFireCollectionRef, QueryFn, get } from './collection';
4-
import { GeoFirePoint } from './geohash';
4+
import { GeoFirePoint } from './point';
55

66
export class GeoFireClient {
77
constructor(private app: firestore.FirebaseApp) {}

src/collection.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { firestore } from './interfaces';
22

33
import { Observable, combineLatest } from 'rxjs';
44
import { shareReplay, map, first } from 'rxjs/operators';
5-
import { GeoFirePoint, Latitude, Longitude } from './geohash';
5+
import { GeoFirePoint, Latitude, Longitude } from './point';
66
import { setPrecsion } from './util';
77
import { FeatureCollection, Geometry } from 'geojson';
88

@@ -80,16 +80,16 @@ export class GeoFireCollectionRef {
8080
/**
8181
* Create or update a document with GeoFirePoint data
8282
* @param {string} id document id
83+
* @param {string} field name of point on the doc
8384
* @param {Latitude} latitude
8485
* @param {Longitude} longitude
85-
* @param {string} field optional name of the document property, defaults to 'point'
8686
* @returns {Promise<void>}
8787
*/
8888
setPoint(
8989
id: string,
90+
field: string,
9091
latitude: Latitude,
91-
longitude: Longitude,
92-
field: string = 'point'
92+
longitude: Longitude
9393
) {
9494
const point = new GeoFirePoint(this.app, latitude, longitude).data;
9595
return this.ref.doc(id).set({ [field]: point }, { merge: true });
@@ -113,7 +113,7 @@ export class GeoFireCollectionRef {
113113
* @param {number} radius the radius to search from the centerpoint
114114
* @param {string} field the document field that contains the GeoFirePoint data
115115
* @param {GeoQueryOptions} opts=defaultOpts
116-
* @returns {Observable<GeoQueryDocument>}
116+
* @returns {Observable<GeoQueryDocument>} sorted by nearest to farthest
117117
*/
118118
within(
119119
center: GeoFirePoint,
@@ -137,7 +137,7 @@ export class GeoFireCollectionRef {
137137
.filter(val => {
138138
const lat = val[field].geopoint.latitude;
139139
const lng = val[field].geopoint.longitude;
140-
return center.distance(lat, lng) <= radius * 1.05; // buffer for edge distances;
140+
return center.distance(lat, lng) <= radius * 1.02; // buffer for edge distances;
141141
})
142142

143143
.map(val => {
@@ -148,14 +148,18 @@ export class GeoFireCollectionRef {
148148
bearing: center.bearing(lat, lng)
149149
};
150150
return { ...val, queryMetadata };
151-
});
151+
})
152+
153+
.sort((a, b) => a.queryMetadata.distance - b.queryMetadata.distance);
152154
}),
153155
shareReplay(1)
154156
);
155157

156158
return combo;
157159
}
158160

161+
first() {}
162+
159163
private queryPoint(geohash: string, field: string) {
160164
const end = geohash + '~';
161165
return this.query

src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export * from './geohash';
1+
export * from './point';
22
export * from './interfaces';
33
export * from './collection';
44
export * from './client';

src/geohash.ts src/point.ts

File renamed without changes.

0 commit comments

Comments
 (0)