Skip to content

Commit

Permalink
feat(firestore): early implementation of geofirestore
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelSolati committed Mar 9, 2018
1 parent e8e2ff5 commit 51e76bd
Show file tree
Hide file tree
Showing 15 changed files with 787 additions and 74 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
"uglify-js": "^3.3.13"
},
"scripts": {
"browserify": "browserify dist/index.js -o dist/geofire.js",
"build": "tsc && npm run browserify && npm run uglify",
"browserify": "browserify dist/geoFire/index.js -o dist/browser/geofire.js && browserify dist/geoFirestore/index.js -o dist/browser/geofirestore.js",
"build": "rm -rf ./dist && tsc && npm run browserify && npm run uglify",
"test": "mocha --reporter spec -r ts-node/register -r jsdom-global/register --timeout 5000 --exit 'tests/**/*.test.ts'",
"travis": "npm run test && npm run build",
"uglify": "uglifyjs dist/geofire.js -c -m -o dist/geofire.min.js"
"uglify": "uglifyjs dist/browser/geofire.js -c -m -o dist/browser/geofire.min.js && uglifyjs dist/browser/geofirestore.js -c -m -o dist/browser/geofirestore.min.js"
}
}
28 changes: 20 additions & 8 deletions src/firebase/geoFire.ts → src/geoFire/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
/*!
* 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 { GeoQuery } from './geoQuery';
import { decodeGeoFireObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from './geoFireUtils';
import { GeoFireQuery } from './query';
import { decodeGeoFireObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from '../tools/utils';

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

Expand Down Expand Up @@ -89,7 +101,7 @@ export class GeoFire {

const newData = {};

Object.keys(locations).forEach(function (key) {
Object.keys(locations).forEach((key) => {
validateKey(key);

const location: number[] = locations[key];
Expand All @@ -108,13 +120,13 @@ export class GeoFire {
};

/**
* Returns a new GeoQuery instance with the provided queryCriteria.
* Returns a new GeoFireQuery instance with the provided queryCriteria.
*
* @param queryCriteria The criteria which specifies the GeoQuery's center and radius.
* @return A new GeoQuery object.
* @param queryCriteria The criteria which specifies the GeoFireQuery's center and radius.
* @return A new GeoFireQuery object.
*/
public query(queryCriteria: QueryCriteria): GeoQuery {
return new GeoQuery(this._firebaseRef, queryCriteria);
public query(queryCriteria: QueryCriteria): GeoFireQuery {
return new GeoFireQuery(this._firebaseRef, queryCriteria);
};

/********************/
Expand Down
16 changes: 8 additions & 8 deletions src/firebase/geoQuery.ts → src/geoFire/query.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as firebase from 'firebase';

import { GeoFire } from './geoFire';
import { GeoCallbackRegistration } from './geoCallbackRegistration';
import { decodeGeoFireObject, encodeGeohash, getKey, geohashQueries, validateCriteria, validateLocation } from './geoFireUtils';
import { GeoFire } from './';
import { GeoCallbackRegistration } from '../tools/callbackRegistration';
import { decodeGeoFireObject, encodeGeohash, geoFireGetKey, geohashQueries, validateCriteria, validateLocation } from '../tools/utils';

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

/**
* Creates a GeoQuery instance.
* Creates a GeoFireQuery instance.
*/
export class GeoQuery {
export class GeoFireQuery {
// Event callbacks
private _callbacks: any = { ready: [], key_entered: [], key_exited: [], key_moved: [] };
// Variable to track when the query is cancelled
Expand Down Expand Up @@ -235,7 +235,7 @@ export class GeoQuery {
* @param locationDataSnapshot A snapshot of the data stored for this location.
*/
private _childAddedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void {
this._updateLocation(getKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val()));
this._updateLocation(geoFireGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val()));
}

/**
Expand All @@ -244,7 +244,7 @@ export class GeoQuery {
* @param locationDataSnapshot A snapshot of the data stored for this location.
*/
private _childChangedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void {
this._updateLocation(getKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val()));
this._updateLocation(geoFireGetKey(locationDataSnapshot), decodeGeoFireObject(locationDataSnapshot.val()));
}

/**
Expand All @@ -253,7 +253,7 @@ export class GeoQuery {
* @param locationDataSnapshot A snapshot of the data stored for this location.
*/
private _childRemovedCallback(locationDataSnapshot: firebase.database.DataSnapshot): void {
const key: string = getKey(locationDataSnapshot);
const key: string = geoFireGetKey(locationDataSnapshot);
if (this._locationsTracked.hasOwnProperty(key)) {
this._firebaseRef.child(key).once('value', (snapshot: firebase.database.DataSnapshot) => {
const location: number[] = (snapshot.val() === null) ? null : decodeGeoFireObject(snapshot.val());
Expand Down
159 changes: 159 additions & 0 deletions src/geoFirestore/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*!
* 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 { GeoFirestoreQuery } from './query';
import { decodeGeoFireObject, degreesToRadians, encodeGeoFireObject, encodeGeohash, validateLocation, validateKey } from '../tools/utils';

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

/**
* Creates a GeoFirestore instance.
*/
export class GeoFirestore {
/**
* @param _collectionRef A Firestore Collection reference where the GeoFirestore data will be stored.
*/
constructor(private _collectionRef: firebase.firestore.CollectionReference) {
if (Object.prototype.toString.call(this._collectionRef) !== '[object Object]') {
throw new Error('collectionRef must be an instance of a Firestore Collection');
}
}

/********************/
/* PUBLIC METHODS */
/********************/
/**
* Returns a promise fulfilled with the location corresponding to the provided key.
*
* If the provided key does not exist, the returned promise is fulfilled with null.
*
* @param key The key of the location to retrieve.
* @returns A promise that is fulfilled with the location of the given key.
*/
public get(key: string): Promise<number[]> {
validateKey(key);
return this._collectionRef.doc(key).get().then((documentSnapshot: firebase.firestore.DocumentSnapshot) => {
const snapshotVal = <GeoFireObj>documentSnapshot.data();
if (snapshotVal === null) {
return null;
} else {
return decodeGeoFireObject(snapshotVal);
}
});
};

/**
* Returns the Firestore Collection used to create this GeoFirestore instance.
*
* @returns The Firestore Collection used to create this GeoFirestore instance.
*/
public ref(): firebase.firestore.CollectionReference {
return this._collectionRef;
};

/**
* Removes the provided key from this GeoFirestore. Returns an empty promise fulfilled when the key has been removed.
*
* If the provided key 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.
*/
public remove(key: string): Promise<string> {
return this.set(key, null);
};

/**
* 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.
* @returns A promise that is fulfilled when the write is complete.
*/
public set(keyOrLocations: string | any, location?: number[]): Promise<any> {
const batch: firebase.firestore.WriteBatch = this._collectionRef.firestore.batch();
let locations;
if (typeof keyOrLocations === 'string' && keyOrLocations.length !== 0) {
// If this is a set for a single location, convert it into a object
locations = {};
locations[keyOrLocations] = location;
} 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().');
}
locations = keyOrLocations;
} else {
throw new Error('keyOrLocations must be a string or a mapping of key - location pairs.');
}

Object.keys(locations).forEach((key) => {
validateKey(key);

const ref = this._collectionRef.doc(key);
const location: number[] = locations[key];
if (location === null) {
batch.delete(ref);
} else {
validateLocation(location);

const geohash: string = encodeGeohash(location);
batch.set(ref, encodeGeoFireObject(location, geohash), { merge: true });
}
});

return batch.commit();
};

/**
* Returns a new GeoQuery instance with the provided queryCriteria.
*
* @param queryCriteria The criteria which specifies the GeoQuery's center and radius.
* @return A new GeoFirestoreQuery object.
*/
public query(queryCriteria: QueryCriteria): GeoFirestoreQuery {
return new GeoFirestoreQuery(this._collectionRef, queryCriteria);
};

/********************/
/* STATIC METHODS */
/********************/
/**
* Static method which calculates the distance, in kilometers, between two locations,
* 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.
* @returns The distance, in kilometers, between the inputted locations.
*/
static distance(location1: number[], location2: number[]) {
validateLocation(location1);
validateLocation(location2);

var radius = 6371; // Earth's radius in kilometers
var latDelta = degreesToRadians(location2[0] - location1[0]);
var lonDelta = degreesToRadians(location2[1] - location1[1]);

var a = (Math.sin(latDelta / 2) * Math.sin(latDelta / 2)) +
(Math.cos(degreesToRadians(location1[0])) * Math.cos(degreesToRadians(location2[0])) *
Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2));

var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

return radius * c;
};
}
Loading

0 comments on commit 51e76bd

Please sign in to comment.