UNPKG

geofirestore

Version:

Location-based querying and filtering using Firebase's Firestore

2 lines (1 loc) 16.5 kB
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).window=t.window||{})}(this,(function(t){"use strict";const e="0123456789bcdefghjkmnpqrstuvwxyz";function n(t){return t*Math.PI/180}function i(t,n=!1){let i;if("string"!=typeof t)i="geohash must be a string";else if(0===t.length)i="geohash cannot be the empty string";else for(const n of t)-1===e.indexOf(n)&&(i="geohash cannot contain '"+n+"'");if(void 0===i||n)return!i;throw new Error("Invalid geohash '"+t+"': "+i)}function r(t,e=!1){const n=[];if(t)if(void 0===t.lat)n.push("Latitude must exist on coordinates");else if(void 0===t.lng)n.push("Longitude must exist on coordinates");else{const e=t.lat,i=t.lng;"number"!=typeof e||isNaN(e)?n.push("Latitude must be a number."):e<-90||e>90?n.push("Latitude must be within the range [-90, 90]."):"number"!=typeof i||isNaN(i)?n.push("Longitude must be a number"):(i<-180||i>180)&&n.push("Longitude must be within the range [-180, 180]")}else n.push("Coordinates must exist.");if(n.length>0&&!e)throw new Error("Invalid coordinates: "+n.join(" "));return!n}function o(t,n=10){if(r(t),"number"!=typeof n||isNaN(n))throw new Error("Precision must be a number");if(n<=0)throw new Error("Precision must be greater than 0");if(n>22)throw new Error("Precision cannot be greater than 22");if(Math.round(n)!==n)throw new Error("Precision must be an integer");const i=[-90,90],o=[-180,180];let s="",c=0,a=0,u=1;for(;s.length<n;){const n=u?t.lng:t.lat,r=u?o:i,h=(r[0]+r[1])/2;n>h?(c=1+(c<<1),r[0]=h):(c=0+(c<<1),r[1]=h),u=!u,a<4?a++:(a=0,s+=e.charAt(c),c=0)}return s}function s(t,e=!1){let n;if("[object Object]"!==Object.prototype.toString.call(t)?n="no document found":"g"in t?(n=i(t.g.geohash,!0)?n:"invalid geohash on object",n=a(t.g.geopoint,!0)?n:"invalid location on object"):n="no `g` field found in object",void 0===n||e)return!n;throw new Error("Invalid GeoFirestore object: "+n)}function c(t,e=!1){let n;if("number"!=typeof t||isNaN(t)?n="limit must be a number":t<0&&(n="limit must be greater than or equal to 0"),void 0===n||e)return!n;throw new Error(n)}function a(t,e=!1){let n;if(t)if(void 0===t.latitude)n="latitude must exist on GeoPoint";else if(void 0===t.longitude)n="longitude must exist on GeoPoint";else{const e=t.latitude,i=t.longitude;"number"!=typeof e||isNaN(e)?n="latitude must be a number":e<-90||e>90?n="latitude must be within the range [-90, 90]":"number"!=typeof i||isNaN(i)?n="longitude must be a number":(i<-180||i>180)&&(n="longitude must be within the range [-180, 180]")}else n="GeoPoint must exist";if(void 0===n||e)return!n;throw new Error("Invalid location: "+n)}function u(t,e=!1){if("object"!=typeof t)throw new Error("QueryCriteria must be an object");if(void 0===t.center&&void 0===t.radius)throw new Error("radius and/or center must be specified");if(e&&(void 0===t.center||void 0===t.radius))throw new Error("QueryCriteria for a new query must contain both a center and a radius");const n=Object.keys(t);for(const t of n)if(!["center","radius","limit"].includes(t))throw new Error("Unexpected attribute '"+t+"' found in query criteria");if(void 0!==t.center&&a(t.center),void 0!==t.radius){if("number"!=typeof t.radius||isNaN(t.radius))throw new Error("radius must be a number");if(t.radius<0)throw new Error("radius must be greater than or equal to 0")}void 0!==t.limit&&c(t.limit)}const h="0123456789bcdefghjkmnpqrstuvwxyz",d=110574;function l(t,e){const n=e/d,i=Math.min(90,t.latitude+n),r=Math.max(-90,t.latitude-n),o=2*Math.floor((s=e,Math.min(p(20003930/s),110)));var s;const c=2*Math.floor(b(e,i))-1,a=2*Math.floor(b(e,r))-1;return Math.min(o,c,a,110)}function f(t,e){return a(t),a(e),function(t,e,i="km"){r(t),r(e);const o="miles"===i.toLowerCase()?3963:6371,s=n(e.lat-t.lat),c=n(e.lng-t.lng),a=n(t.lat),u=n(e.lat),h=Math.sin(s/2)*Math.sin(s/2)+Math.sin(c/2)*Math.sin(c/2)*Math.cos(a)*Math.cos(u);return o*(2*Math.atan2(Math.sqrt(h),Math.sqrt(1-h)))}({lat:t.latitude,lng:t.longitude},{lat:e.latitude,lng:e.longitude})}function _(t,e,n=!1){let i,r;if((e=e||"coordinates")in t)r=t[e];else{const n=e.split(".");r=t;for(const e of n){if(!(e in r)){r=t.coordinates;break}r=r[e]}}if(r||(i="could not find GeoPoint"),r&&!a(r,!0)&&(i="invalid GeoPoint"),i&&!n)throw new Error("Invalid GeoFirestore document: "+i);return r}function m(t,e){const n=function(t,e){if(s(t,!0))return{data:()=>t,distance:e?f(t.g.geopoint,e):null};return{data:()=>t,distance:null}}(t.data(),e);return Object.assign({exists:t.exists,id:t.id},n)}function g(t,e){let n=function(t,e){a(t);const n=Math.max(1,l(t,e)),r=Math.ceil(n/5),s=function(t,e){const n=e/d,i=Math.min(90,t.latitude+n),r=Math.max(-90,t.latitude-n),o=y(e,i),s=y(e,r),c=Math.max(o,s);return[v(t.latitude,t.longitude),v(t.latitude,j(t.longitude-c)),v(t.latitude,j(t.longitude+c)),v(i,t.longitude),v(i,j(t.longitude-c)),v(i,j(t.longitude+c)),v(r,t.longitude),v(r,j(t.longitude-c)),v(r,j(t.longitude+c))]}(t,e).map((t=>function(t,e){i(t);const n=Math.ceil(e/5);if(t.length<n)return[t,t+"~"];const r=t.substring(0,n),o=r.substring(0,r.length-1),s=h.indexOf(r.charAt(r.length-1)),c=e-5*o.length,a=5-c,u=s>>a<<a,d=u+(1<<a);return d>31?[o+h[u],o+"~"]:[o+h[u],o+h[d]]}(o({lat:t.latitude,lng:t.longitude},r),n)));return s.filter(((t,e)=>!s.some(((n,i)=>e>i&&t[0]===n[0]&&t[1]===n[1]))))}(e.center,1e3*e.radius).map(w);return n=n.filter(((t,e)=>n.indexOf(t)===e)),n.map((e=>{const n=function(t){const e=t.split(":");if(2!==e.length)throw new Error("Invalid internal state! Not a valid geohash query: "+t);return e}(e);return t.orderBy("g.geohash").startAt(n[0]).endAt(n[1])}))}function p(t){return Math.log(t)/Math.log(2)}function b(t,e){const n=y(t,e);return Math.abs(n)>1e-6?Math.max(1,p(360/n)):1}function y(t,e){const n=function(t){if("number"!=typeof t||isNaN(t))throw new Error("Error: degrees must be a number");return t*Math.PI/180}(e),i=6378137*Math.cos(n)*Math.PI/180*(1/Math.sqrt(1-.00669447819799*Math.sin(n)*Math.sin(n)));return i<1e-12?t>0?360:0:Math.min(360,t/i)}function w(t){if(2!==t.length)throw new Error("Not a valid geohash query: "+t);return t[0]+":"+t[1]}function v(t,e){const n={latitude:t,longitude:e};return a(n),n}function j(t){if(t<=180&&t>=-180)return t;const e=t+180;return e>0?e%360-180:180- -e%360}function q(t,e){if("[object Object]"!==Object.prototype.toString.call(t))throw new Error("document must be an object");const n=_(t,e&&e.customKey,e&&(e.merge||!!e.mergeFields));return n?x(n,t):t}function E(t,e){if("[object Object]"!==Object.prototype.toString.call(t))throw new Error("document must be an object");const n=_(t,e,!0);return n?x(n,t):t}function x(t,e){a(t);const n=o({lat:t.latitude,lng:t.longitude});return Object.assign(Object.assign({},e),{g:{geopoint:t,geohash:n}})}class M{constructor(t,e){this._querySnapshot=t,this._center=e,e&&a(e),this._docs=t.docs.map((t=>m(t,e)))}get native(){return this._querySnapshot}get docs(){return this._docs}get size(){return this._docs.length}get empty(){return!this._docs.length}docChanges(){return(Array.isArray(this._querySnapshot.docChanges)?this._querySnapshot.docChanges:this._querySnapshot.docChanges()).map((t=>({doc:m(t.doc,this._center),newIndex:t.newIndex,oldIndex:t.oldIndex,type:t.type})))}forEach(t,e){this.docs.forEach(t,e)}}class C{constructor(t,e){if(this._queryCriteria=e,this._docs=new Map,u(e),t.forEach((t=>{t.docs.forEach((t=>{const e=f(this._queryCriteria.center,t.data().g.geopoint);this._queryCriteria.radius>=e&&this._docs.set(t.id,t)}))})),this._queryCriteria.limit&&this._docs.size>this._queryCriteria.limit){const t=Array.from(this._docs.values()).map((t=>({distance:f(this._queryCriteria.center,t.data().g.geopoint),id:t.id}))).sort(((t,e)=>t.distance-e.distance));for(let e=this._queryCriteria.limit;e<t.length;e++)this._docs.delete(t[e].id)}}getGeoQuerySnapshot(){const t=Array.from(this._docs.values());return new M({docs:t,docChanges:()=>t.map(((t,e)=>({doc:t,newIndex:e,oldIndex:-1,type:"added"})))},this._queryCriteria.center)}}class O{constructor(t,e,n,i=(()=>{})){this._queries=t,this._queryCriteria=e,this._onNext=n,this._onError=i,this._docs=new Map,this._firstRoundResolved=!1,this._firstEmitted=!1,this._newValues=!1,this._subscriptions=[],this._queriesResolved=[],u(e),this._queriesResolved=new Array(t.length).fill(0),t.forEach(((t,e)=>{const n=t.onSnapshot((t=>this._processSnapshot(t,e)),(t=>this._error=t));this._subscriptions.push(n)})),this._interval=setInterval((()=>this._emit()),100)}unsubscribe(){return()=>{clearInterval(this._interval),this._subscriptions.forEach((t=>t()))}}_next(){if(this._queryCriteria.limit&&this._docs.size>this._queryCriteria.limit){const t=Array.from(this._docs.values()).sort(((t,e)=>t.distance-e.distance));for(let e=this._queryCriteria.limit;e<t.length;e++)if(t[e].emitted){const n={change:Object.assign({},t[e].change),distance:t[e].distance,emitted:t[e].emitted};n.change.type="removed",this._docs.set(n.change.doc.id,n)}else this._docs.delete(t[e].change.doc.id)}let t=0;const e=Array.from(this._docs.values()).map(((e,n)=>{const i={type:e.change.type,doc:e.change.doc,oldIndex:e.emitted?e.change.newIndex:-1,newIndex:"removed"!==e.change.type?n-t:-1};return"removed"===i.type?(t--,this._docs.delete(i.doc.id)):this._docs.set(i.doc.id,{change:i,distance:e.distance,emitted:!0}),i})),n=e.reduce(((t,e)=>(e.newIndex>=0?t.push(e.doc):this._docs.delete(e.doc.id),t)),[]);this._firstEmitted=!0,this._onNext(new M({docs:n,docChanges:()=>e.reduce(((t,e)=>(-1!==e.oldIndex&&"added"===e.type||t.push(e),t)),[])},this._queryCriteria.center))}_emit(){this._error?(this._onError(this._error),this.unsubscribe()()):this._newValues&&this._firstRoundResolved?(this._newValues=!1,this._next()):this._firstRoundResolved||(this._firstRoundResolved=this._queriesResolved.reduce(((t,e)=>t+e),0)===this._queries.length)}_processSnapshot(t,e){const n=Array.isArray(t.docChanges)?t.docChanges:t.docChanges();this._firstRoundResolved||(this._queriesResolved[e]=1),n.length?n.forEach((t=>{const e=t.doc.data(),n=s(e,!0)?e.g.geopoint:null,i=n?f(this._queryCriteria.center,n):null,r=t.doc.id,o=this._docs.get(r),c={change:{doc:t.doc,oldIndex:o&&this._firstEmitted?o.change.oldIndex:-1,newIndex:o&&this._firstEmitted?o.change.newIndex:-1,type:o&&this._firstEmitted?t.type:"added"},distance:i,emitted:!!this._firstEmitted&&!!o};if(this._queryCriteria.radius>=i){if(!o&&"removed"===c.change.type)return;o||"modified"!==c.change.type||(c.change.type="added"),this._newValues=!0,this._docs.set(r,c)}else o?(c.change.type="removed",this._newValues=!0,this._docs.set(r,c)):o||this._firstRoundResolved||(this._newValues=!0)})):this._firstRoundResolved||(this._newValues=!0)}}class S{constructor(t,e){if(this._query=t,"[object Object]"!==Object.prototype.toString.call(t))throw new Error("Query must be an instance of a Firestore Query");this._isWeb="[object Function]"===Object.prototype.toString.call(t.firestore.enablePersistence),e&&("number"==typeof e.limit&&(this._limit=e.limit),e.center&&"number"==typeof e.radius&&(u(e),this._center=e.center,this._radius=e.radius))}get native(){return this._query}get firestore(){return new R(this._query.firestore)}get onSnapshot(){return t=this._query,e=this._queryCriteria,(n,i=(()=>{}))=>e.center&&"number"==typeof e.radius?new O(g(t,e),e,n,i).unsubscribe():(t=e.limit?t.limit(e.limit):t).onSnapshot((t=>n(new M(t))),i);var t,e}get(t={source:"default"}){return function(t,e,n={source:"default"}){const i="[object Function]"===Object.prototype.toString.call(t.firestore.enablePersistence);if(e.center&&"number"==typeof e.radius){const r=g(t,e).map((t=>i?t.get(n):t.get()));return Promise.all(r).then((t=>new C(t,e).getGeoQuerySnapshot()))}return t=e.limit?t.limit(e.limit):t,(i?t.get(n):t.get()).then((t=>new M(t)))}(this._query,this._queryCriteria,t)}limit(t){return c(t),this._limit=t,new S(this._query,this._queryCriteria)}near(t){return u(t,!0),this._center=t.center,this._radius=t.radius,new S(this._query,this._queryCriteria)}where(t,e,n){return new S(this._query.where(t,e,n),this._queryCriteria)}get _queryCriteria(){return{center:this._center,limit:this._limit,radius:this._radius}}}class I{constructor(t,e){if(this._writeBatch=t,this._customKey=e,"[object Object]"!==Object.prototype.toString.call(t))throw new Error("WriteBatch must be an instance of a Firestore WriteBatch")}get native(){return this._writeBatch}set(t,e,n={}){const i=t instanceof P?t._document:t;return n.customKey=n.customKey||this._customKey,this._writeBatch.set(i,q(e,n),N(n)),this}update(t,e,n=this._customKey){const i=t instanceof P?t._document:t;return this._writeBatch.update(i,E(e,n)),this}delete(t){const e=t instanceof P?t._document:t;return this._writeBatch.delete(e),this}commit(){return this._writeBatch.commit()}}class R{constructor(t){if(this._firestore=t,"[object Object]"!==Object.prototype.toString.call(t))throw new Error("Firestore must be an instance of Firestore")}get native(){return this._firestore}batch(t){return new I(this._firestore.batch(),t)}collection(t,e){return new F(this._firestore.collection(t),e)}collectionGroup(t){return new S(this._firestore.collectionGroup(t))}doc(t){return new P(this._firestore.doc(t))}runTransaction(t){return this._firestore.runTransaction(t)}}function N(t){const e=Object.assign({},t);return delete e.customKey,e}class G{constructor(t){if(this._snapshot=t,"[object Object]"!==Object.prototype.toString.call(t))throw new Error("DocumentSnapshot must be an instance of a Firestore DocumentSnapshot");this._isWeb="[object Function]"===Object.prototype.toString.call(t.ref.firestore.enablePersistence)}get native(){return this._snapshot}get exists(){return this._snapshot.exists}get id(){return this._snapshot.id}get ref(){return new P(this._snapshot.ref)}data(t){return this._isWeb&&t?this._snapshot.data(t):this._snapshot.data()}get(t,e){return this._isWeb&&e?this._snapshot.get(t,e):this._snapshot.get(t)}isEqual(t){const e=t instanceof G?t._snapshot:t;return this._snapshot.isEqual(e)}}class P{constructor(t){if(this._document=t,"[object Object]"!==Object.prototype.toString.call(t))throw new Error("DocumentReference must be an instance of a Firestore DocumentReference");this._isWeb="[object Function]"===Object.prototype.toString.call(t.firestore.enablePersistence)}get native(){return this._document}get id(){return this._document.id}get firestore(){return new R(this._document.firestore)}get onSnapshot(){return(t,e=(()=>{}))=>this._document.onSnapshot((e=>t(new G(e))),(t=>e(t)))}get parent(){return new F(this._document.parent)}get path(){return this._document.path}collection(t){return new F(this._document.collection(t))}delete(){return this._document.delete().then((()=>null))}get(t={source:"default"}){return(this._isWeb?this._document.get(t):this._document.get()).then((t=>new G(t)))}isEqual(t){const e=t instanceof P?t._document:t;return this._document.isEqual(e)}set(t,e){return this._document.set(q(t,e),N(e)).then((()=>null))}update(t,e){return this._document.update(E(t,e)).then((()=>null))}}class F extends S{constructor(t,e){super(t),this._collection=t,this._customKey=e}get native(){return this._collection}get id(){return this._collection.id}get parent(){return this._collection.parent?new P(this._collection.parent):null}get path(){return this._collection.path}add(t,e=this._customKey){return this._collection.add(function(t,e){if("[object Object]"!==Object.prototype.toString.call(t))throw new Error("document must be an object");return x(_(t,e),t)}(t,e)).then((t=>new P(t)))}doc(t){return new P(t?this._collection.doc(t):this._collection.doc())}}t.GeoCollectionReference=F,t.GeoDocumentReference=P,t.GeoDocumentSnapshot=G,t.GeoFirestore=R,t.GeoQuery=S,t.GeoTransaction=class{constructor(t,e){if(this._transaction=t,this._customKey=e,"[object Object]"!==Object.prototype.toString.call(t))throw new Error("Transaction must be an instance of a Firestore Transaction")}get native(){return this._transaction}delete(t){const e=t instanceof P?t._document:t;return this._transaction.delete(e),this}get(t){const e=t instanceof P?t._document:t;return this._transaction.get(e).then((t=>new G(t)))}set(t,e,n={}){const i=t instanceof P?t._document:t;return n.customKey=n.customKey||this._customKey,this._transaction.set(i,q(e,n),N(n)),this}update(t,e,n=this._customKey){const i=t instanceof P?t._document:t;return this._transaction.update(i,E(e,n)),this}},t.GeoWriteBatch=I,t.initializeApp=function(t){return new R(t)},Object.defineProperty(t,"__esModule",{value:!0})}));