UNPKG

@thetsf/geofirex

Version:

Realtime Firestore GeoQueries with RxJS

130 lines (129 loc) 5.73 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeoFireQuery = void 0; exports.toGeoJSON = toGeoJSON; exports.get = get; const firestore_1 = require("firebase/firestore"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const util_1 = require("./util"); const defaultOpts = { units: 'km', log: false }; class GeoFireQuery { constructor(app, refString) { this.app = app; this.refString = refString; if (typeof refString === 'string') { const db = (0, firestore_1.getFirestore)(app); this.ref = (0, firestore_1.collection)(db, refString); // this.ref = this.app.firestore().collection(ref); } else { this.ref = refString; } } // GEO QUERIES /** * Queries the Firestore collection based on geograpic radius * @param {FirePoint} center the starting point for the query, i.e gfx.point(lat, lng) * @param {number} radius the radius to search from the centerpoint * @param {string} field the document field that contains the FirePoint data * @param {GeoQueryOptions} opts=defaultOpts * @returns {Observable<GeoQueryDocument>} sorted by nearest to farthest */ within(center, radius, field, opts) { opts = Object.assign(Object.assign({}, defaultOpts), opts); const tick = Date.now(); const precision = (0, util_1.setPrecision)(radius); const radiusBuffer = radius * 1.02; // buffer for edge distances const centerHash = center.geohash.substr(0, precision); const area = (0, util_1.neighbors)(centerHash).concat(centerHash); const { latitude: centerLat, longitude: centerLng } = center.geopoint; // Used to cancel the individual geohash subscriptions const complete = new rxjs_1.Subject(); // Map geohash neighbors to individual queries const queries = area.map(hash => { const query = this.queryPoint(hash, field); return createStream(query).pipe(snapToData(), (0, operators_1.takeUntil)(complete)); }); // Combine all queries concurrently const combo = (0, rxjs_1.combineLatest)(...queries).pipe((0, operators_1.map)(arr => { // Combine results into a single array const reduced = arr.reduce((acc, cur) => acc.concat(cur)); // Filter by radius const filtered = reduced.filter(val => { const { latitude, longitude } = val[field].geopoint; return ((0, util_1.distance)([centerLat, centerLng], [latitude, longitude]) <= radiusBuffer); }); // Optional logging if (opts.log) { console.group('GeoFireX Query'); console.log(`🌐 Center ${[centerLat, centerLng]}. Radius ${radius}`); console.log(`📍 Hits: ${reduced.length}`); console.log(`⌚ Elapsed time: ${Date.now() - tick}ms`); console.log(`🟢 Within Radius: ${filtered.length}`); console.groupEnd(); } // Map and sort to final output return filtered .map(val => { const { latitude, longitude } = val[field].geopoint; const hitMetadata = { distance: (0, util_1.distance)([centerLat, centerLng], [latitude, longitude]), bearing: (0, util_1.bearing)([centerLat, centerLng], [latitude, longitude]) }; return Object.assign(Object.assign({}, val), { hitMetadata }); }) .sort((a, b) => a.hitMetadata.distance - b.hitMetadata.distance); }), (0, operators_1.shareReplay)(1), (0, operators_1.finalize)(() => { opts.log && console.log('✋ Query complete'); complete.next(true); })); return combo; } queryPoint(geohash, field) { const end = geohash + '~'; return (0, firestore_1.query)(this.ref, (0, firestore_1.orderBy)(`${field}.geohash`), (0, firestore_1.startAt)(geohash), (0, firestore_1.endAt)(end)); /*return (this.ref as CollectionReference) .orderBy(`${field}.geohash`) .startAt(geohash) .endAt(end);*/ } } exports.GeoFireQuery = GeoFireQuery; function snapToData(id = 'id') { return (0, operators_1.map)((querySnapshot) => querySnapshot.docs.map(v => { return Object.assign(Object.assign({}, (id ? { [id]: v.id } : null)), v.data()); })); } /** internal, do not use. Converts callback to Observable. */ function createStream(input) { return new rxjs_1.Observable(observer => { const unsubscribe = (0, firestore_1.onSnapshot)(input, val => observer.next(val), err => observer.error(err)); return { unsubscribe }; }); } /** * RxJS operator that converts a collection to a GeoJSON FeatureCollection * @param {string} field the document field that contains the FirePoint * @param {boolean=false} includeProps */ function toGeoJSON(field, includeProps = false) { return (0, operators_1.map)((data) => { return { type: 'FeatureCollection', features: data.map(v => (0, util_1.toGeoJSONFeature)([v[field].geopoint.latitude, v[field].geopoint.longitude], includeProps ? Object.assign({}, v) : {})) }; }); } /** * Helper function to convert any query from an RxJS Observable to a Promise * Example usage: await get( collection.within(a, b, c) ) * @param {Observable<any>} observable * @returns {Promise<any>} */ function get(observable) { return observable.pipe((0, operators_1.first)()).toPromise(); }