@thetsf/geofirex
Version:
Realtime Firestore GeoQueries with RxJS
130 lines (129 loc) • 5.73 kB
JavaScript
;
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();
}