Skip to content

Commit

Permalink
feat(set): update set function to use GeoPoints
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelSolati committed May 28, 2018
1 parent 5181443 commit 5cf04fb
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 123 deletions.
88 changes: 40 additions & 48 deletions src/geofirestore.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
/*!
* GeoFire is an open-source library that allows you to store and query a set
* of keys based on their geographic location. At its heart, GeoFire simply
* stores locations with string keys. Its main benefit, however, is the
* possibility of retrieving only those keys within a given geographic area -
* all in realtime.
*
* GeoFire 0.0.0
* https://github.com/firebase/geofire-js/
* License: MIT
*/

import * as firebase from 'firebase';
import { firestore } from 'firebase/app';

import { GeoFirestoreQuery } from './query';
import { decodeGeoFirestoreObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from './utils';
import { decodeGeoFirestoreObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey, findCoordinatesKey } from './utils';

import { QueryCriteria, GeoFirestoreObj } from './interfaces';

Expand All @@ -24,7 +12,7 @@ export class GeoFirestore {
/**
* @param _collectionRef A Firestore Collection reference where the GeoFirestore data will be stored.
*/
constructor(private _collectionRef: firebase.firestore.CollectionReference) {
constructor(private _collectionRef: firestore.CollectionReference) {
if (Object.prototype.toString.call(this._collectionRef) !== '[object Object]') {
throw new Error('collectionRef must be an instance of a Firestore Collection');
}
Expand All @@ -43,7 +31,7 @@ export class GeoFirestore {
*/
public get($key: string): Promise<number[]> {
validateKey($key);
return this._collectionRef.doc($key).get().then((documentSnapshot: firebase.firestore.DocumentSnapshot) => {
return this._collectionRef.doc($key).get().then((documentSnapshot: firestore.DocumentSnapshot) => {
if (!documentSnapshot.exists) {
return null;
} else {
Expand All @@ -58,62 +46,66 @@ export class GeoFirestore {
*
* @returns The Firestore Collection used to create this GeoFirestore instance.
*/
public ref(): firebase.firestore.CollectionReference {
public ref(): firestore.CollectionReference {
return this._collectionRef;
};

/**
* Removes the provided key from this GeoFirestore. Returns an empty promise fulfilled when the key has been removed.
* Removes the provided key, or keys, from this GeoFirestore. Returns an empty promise fulfilled when the key(s) has been removed.
*
* If the provided key is not in this GeoFirestore, the promise will still successfully resolve.
* If the provided key(s) is not in this GeoFirestore, the promise will still successfully resolve.
*
* @param $key The key of the location to remove.
* @returns A promise that is fulfilled after the inputted key is removed.
* @param keyOrKeys The key representing the document to remove or an array of keys to remove.
* @returns A promise that is fulfilled after the inputted key(s) is removed.
*/
public remove($key: string): Promise<void> {
return this._collectionRef.doc($key).delete();
public remove(keyOrKeys: string | string[]): Promise<void> {
if (Array.isArray(keyOrKeys)) {
const documents = {};
keyOrKeys.forEach(key => { documents[key] = null; });
return this.set(documents);
} else {
return this.set(keyOrKeys);
}
};

/**
* Adds the provided key - location pair(s) to Firestore. Returns an empty promise which is fulfilled when the write is complete.
*
* If any provided key already exists in this GeoFirestore, it will be overwritten with the new location value.
*
* @param keyOrLocations The key representing the location to add or a mapping of key - location pairs which
* represent the locations to add.
* @param location The [latitude, longitude] pair to add.
* @param keyOrDocuments The key representing the document to add or an array of $key/document pairs.
* @param document The document to be added to the GeoFirestore.
* @param customKey The key of the document to use as the location. Otherwise we default to `coordinates`.
* @returns A promise that is fulfilled when the write is complete.
*/
public set(keyOrLocations: string | any, location?: number[]): Promise<void> {
if (typeof keyOrLocations === 'string' && keyOrLocations.length !== 0) {
validateKey(keyOrLocations);
if (location === null) {
public set(keyOrDocuments: string | any, document?: any, customKey?: string): Promise<void> {
if (typeof keyOrDocuments === 'string' && keyOrDocuments.length !== 0) {
validateKey(keyOrDocuments);
if (document === null) {
// Setting location to null is valid since it will remove the key
return this._collectionRef.doc(keyOrLocations).delete();
return this._collectionRef.doc(keyOrDocuments).delete();
} else {
validateLocation(location);
const locationKey = findCoordinatesKey(document, customKey);
const location: firestore.GeoPoint = document[locationKey];
const geohash: string = encodeGeohash(location);
return this._collectionRef.doc(keyOrLocations).set(encodeGeoFireObject(location, geohash));
}
} else if (typeof keyOrLocations === 'object') {
if (typeof location !== 'undefined') {
throw new Error('The location argument should not be used if you pass an object to set().');
return this._collectionRef.doc(keyOrDocuments).set(encodeGeoFireObject(location, geohash, document));
}
} else {
throw new Error('keyOrLocations must be a string or a mapping of key - location pairs.');
} else if (typeof keyOrDocuments !== 'object') {
throw new Error('keyOrLocations must be a string or a mapping of key - document pairs.');
}

const batch: firebase.firestore.WriteBatch = this._collectionRef.firestore.batch();
Object.keys(keyOrLocations).forEach((key) => {
const batch: firestore.WriteBatch = this._collectionRef.firestore.batch();
Object.keys(keyOrDocuments).forEach((key) => {
validateKey(key);
const ref = this._collectionRef.doc(key);
const location: number[] = keyOrLocations[key];
if (location === null) {
const documentToUpdate: any = keyOrDocuments[key];
if (documentToUpdate === null) {
batch.delete(ref);
} else {
validateLocation(location);
const locationKey = findCoordinatesKey(documentToUpdate, customKey);
const location: firestore.GeoPoint = documentToUpdate[locationKey];
const geohash: string = encodeGeohash(location);
batch.set(ref, encodeGeoFireObject(location, geohash), { merge: true });
batch.set(ref, encodeGeoFireObject(location, geohash, documentToUpdate), { merge: true });
}
});
return batch.commit();
Expand All @@ -137,11 +129,11 @@ export class GeoFirestore {
* via the Haversine formula. Note that this is approximate due to the fact that the
* Earth's radius varies between 6356.752 km and 6378.137 km.
*
* @param location1 The [latitude, longitude] pair of the first location.
* @param location2 The [latitude, longitude] pair of the second location.
* @param location1 The GeoPoint of the first location.
* @param location2 The GeoPoint of the second location.
* @returns The distance, in kilometers, between the inputted locations.
*/
static distance(location1: number[], location2: number[]) {
static distance(location1: firestore.GeoPoint, location2: firestore.GeoPoint) {
validateLocation(location1);
validateLocation(location2);

Expand Down
8 changes: 4 additions & 4 deletions src/interfaces/geoFirestoreObj.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as firebase from 'firebase';
import { firestore } from 'firebase/app';

export interface GeoFirestoreObj {
geohash: string;
location: firebase.firestore.GeoPoint;
document: any;
g: string;
l: firestore.GeoPoint;
d: any;
}
4 changes: 3 additions & 1 deletion src/interfaces/queryCriteria.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { firestore } from 'firebase/app';

export interface QueryCriteria {
center?: number[];
center?: firestore.GeoPoint;
radius?: number;
}
46 changes: 23 additions & 23 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as firebase from 'firebase';
import { firestore } from 'firebase/app';

import { GeoFirestore } from './';
import { GeoCallbackRegistration } from './callbackRegistration';
import { decodeGeoFirestoreObject, encodeGeohash, geoFirestoreGetKey, geohashQueries, validateCriteria, validateLocation } from './utils';

import { QueryCriteria, GeoFireObj, GeoFirestoreQueryState } from './interfaces';
import { QueryCriteria, GeoFirestoreObj, GeoFirestoreQueryState } from './interfaces';

/**
* Creates a GeoFirestoreQuery instance.
Expand All @@ -14,7 +14,7 @@ export class GeoFirestoreQuery {
private _callbacks: any = { ready: [], key_entered: [], key_exited: [], key_moved: [] };
// Variable to track when the query is cancelled
private _cancelled: boolean = false;
private _center: number[];
private _center: firestore.GeoPoint;
// A dictionary of geohash queries which currently have an active callbacks
private _currentGeohashesQueried: any = {};
// A dictionary of locations that a currently active in the queries
Expand All @@ -36,7 +36,7 @@ export class GeoFirestoreQuery {
* @param _collectionRef A Firestore Collection reference where the GeoFirestore data will be stored.
* @param _queryCriteria The criteria which specifies the query's center and radius.
*/
constructor(private _collectionRef: firebase.firestore.CollectionReference, private _queryCriteria: QueryCriteria) {
constructor(private _collectionRef: firestore.CollectionReference, private _queryCriteria: QueryCriteria) {
// Firebase reference of the GeoFirestore which created this query
if (Object.prototype.toString.call(this._collectionRef) !== '[object Object]') {
throw new Error('firebaseRef must be an instance of Firestore');
Expand Down Expand Up @@ -89,9 +89,9 @@ export class GeoFirestoreQuery {
/**
* Returns the location signifying the center of this query.
*
* @returns The [latitude, longitude] pair signifying the center of this query.
* @returns The GeoPoint signifying the center of this query.
*/
public center(): number[] {
public center(): firestore.GeoPoint {
return this._center;
};

Expand Down Expand Up @@ -231,8 +231,8 @@ export class GeoFirestoreQuery {
*
* @param locationDataSnapshot A snapshot of the data stored for this location.
*/
private _childAddedCallback(locationDataSnapshot: firebase.firestore.DocumentSnapshot): void {
const data = <GeoFireObj>locationDataSnapshot.data();
private _childAddedCallback(locationDataSnapshot: firestore.DocumentSnapshot): void {
const data = <GeoFirestoreObj>locationDataSnapshot.data();
this._updateLocation(geoFirestoreGetKey(locationDataSnapshot), decodeGeoFirestoreObject(data));
}

Expand All @@ -241,8 +241,8 @@ export class GeoFirestoreQuery {
*
* @param locationDataSnapshot A snapshot of the data stored for this location.
*/
private _childChangedCallback(locationDataSnapshot: firebase.firestore.DocumentSnapshot): void {
const data = <GeoFireObj>locationDataSnapshot.data();
private _childChangedCallback(locationDataSnapshot: firestore.DocumentSnapshot): void {
const data = <GeoFirestoreObj>locationDataSnapshot.data();
this._updateLocation(geoFirestoreGetKey(locationDataSnapshot), decodeGeoFirestoreObject(data));
}

Expand All @@ -251,12 +251,12 @@ export class GeoFirestoreQuery {
*
* @param locationDataSnapshot A snapshot of the data stored for this location.
*/
private _childRemovedCallback(locationDataSnapshot: firebase.firestore.DocumentSnapshot): void {
private _childRemovedCallback(locationDataSnapshot: firestore.DocumentSnapshot): void {
const key: string = geoFirestoreGetKey(locationDataSnapshot);
if (key in this._locationsTracked) {
this._collectionRef.doc(key).get().then((snapshot: firebase.firestore.DocumentSnapshot) => {
const data = (!snapshot.exists) ? null : <GeoFireObj>snapshot.data();
const location: number[] = (!snapshot.exists) ? null : decodeGeoFirestoreObject(data);
this._collectionRef.doc(key).get().then((snapshot: firestore.DocumentSnapshot) => {
const data = (!snapshot.exists) ? null : <GeoFirestoreObj>snapshot.data();
const location: firestore.GeoPoint = (!snapshot.exists && data.l && validateLocation(data.l)) ? null : data.l;
const geohash: string = (location !== null) ? encodeGeohash(location) : null;
// Only notify observers if key is not part of any other geohash query or this actually might not be
// a key exited event, but a key moved or entered event. These events will be triggered by updates
Expand Down Expand Up @@ -309,10 +309,10 @@ export class GeoFirestoreQuery {
*
* @param eventType The event type whose callbacks to fire. One of 'key_entered', 'key_exited', or 'key_moved'.
* @param key The key of the location for which to fire the callbacks.
* @param location The location as [latitude, longitude] pair
* @param location The location as a Firestore GeoPoint.
* @param distanceFromCenter The distance from the center or null.
*/
private _fireCallbacksForKey(eventType: string, key: string, location?: number[], distanceFromCenter?: number): void {
private _fireCallbacksForKey(eventType: string, key: string, location?: firestore.GeoPoint, distanceFromCenter?: number): void {
this._callbacks[eventType].forEach((callback) => {
if (typeof location === 'undefined' || location === null) {
callback(key, null, null);
Expand Down Expand Up @@ -411,11 +411,11 @@ export class GeoFirestoreQuery {
const query: string[] = this._stringToQuery(toQueryStr);

// Create the Firebase query
const firestoreQuery: firebase.firestore.Query = this._collectionRef.orderBy('g').startAt(query[0]).endAt(query[1]);
const firestoreQuery: firestore.Query = this._collectionRef.orderBy('g').startAt(query[0]).endAt(query[1]);

// For every new matching geohash, determine if we should fire the 'key_entered' event
const childCallback = firestoreQuery.onSnapshot((snapshot: firebase.firestore.QuerySnapshot) => {
snapshot.docChanges.forEach((change: firebase.firestore.DocumentChange) => {
const childCallback = firestoreQuery.onSnapshot((snapshot: firestore.QuerySnapshot) => {
snapshot.docChanges.forEach((change: firestore.DocumentChange) => {
if (change.type === 'added') {
this._childAddedCallback(change.doc);
}
Expand Down Expand Up @@ -469,9 +469,9 @@ export class GeoFirestoreQuery {
* Removes the location from the local state and fires any events if necessary.
*
* @param key The key to be removed.
* @param currentLocation The current location as [latitude, longitude] pair or null if removed.
* @param currentLocation The current location as firestore GeoPoint or null if removed.
*/
private _removeLocation(key: string, currentLocation?: number[]): void {
private _removeLocation(key: string, currentLocation?: firestore.GeoPoint): void {
const locationDict = this._locationsTracked[key];
delete this._locationsTracked[key];
if (typeof locationDict !== 'undefined' && locationDict.isInQuery) {
Expand Down Expand Up @@ -502,9 +502,9 @@ export class GeoFirestoreQuery {
* any necessary cleanup.
*
* @param key The key of the GeoFirestore location.
* @param location The location as [latitude, longitude] pair.
* @param location The location as a Firestore GeoPoint.
*/
private _updateLocation(key: string, location?: number[]): void {
private _updateLocation(key: string, location?: firestore.GeoPoint): void {
validateLocation(location);
// Get the key and location
let distanceFromCenter: number, isInQuery;
Expand Down
Loading

0 comments on commit 5cf04fb

Please sign in to comment.