UNPKG

geofirestore-core

Version:

Lightweight location-based querying and encoding of Firebase Firestore Documents for geofirestore.

2 lines (1 loc) 11.7 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 o(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 r(t,n=10){if(o(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],r=[-180,180];let s="",a=0,u=0,d=1;for(;s.length<n;){const n=d?t.lng:t.lat,o=d?r:i,c=(o[0]+o[1])/2;n>c?(a=1+(a<<1),o[0]=c):(a=0+(a<<1),o[1]=c),d=!d,u<4?u++:(u=0,s+=e.charAt(a),a=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=u(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 a(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 u(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 d(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&&u(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&&a(t.limit)}const c="0123456789bcdefghjkmnpqrstuvwxyz",h=110574;function l(t,e){const n=e/h,i=Math.min(90,t.latitude+n),o=Math.max(-90,t.latitude-n),r=2*Math.floor((s=e,Math.min(b(20003930/s),110)));var s;const a=2*Math.floor(_(e,i))-1,u=2*Math.floor(_(e,o))-1;return Math.min(r,a,u,110)}function f(t,e){return u(t),u(e),function(t,e,i="km"){o(t),o(e);const r="miles"===i.toLowerCase()?3963:6371,s=n(e.lat-t.lat),a=n(e.lng-t.lng),u=n(t.lat),d=n(e.lat),c=Math.sin(s/2)*Math.sin(s/2)+Math.sin(a/2)*Math.sin(a/2)*Math.cos(u)*Math.cos(d);return r*(2*Math.atan2(Math.sqrt(c),Math.sqrt(1-c)))}({lat:t.latitude,lng:t.longitude},{lat:e.latitude,lng:e.longitude})}function g(t,e,n=!1){let i,o;if((e=e||"coordinates")in t)o=t[e];else{const n=e.split(".");o=t;for(const e of n){if(!(e in o)){o=t.coordinates;break}o=o[e]}}if(o||(i="could not find GeoPoint"),o&&!u(o,!0)&&(i="invalid GeoPoint"),i&&!n)throw new Error("Invalid GeoFirestore document: "+i);return o}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 p(t,e){let n=function(t,e){u(t);const n=Math.max(1,l(t,e)),o=Math.ceil(n/5),s=function(t,e){const n=e/h,i=Math.min(90,t.latitude+n),o=Math.max(-90,t.latitude-n),r=y(e,i),s=y(e,o),a=Math.max(r,s);return[v(t.latitude,t.longitude),v(t.latitude,x(t.longitude-a)),v(t.latitude,x(t.longitude+a)),v(i,t.longitude),v(i,x(t.longitude-a)),v(i,x(t.longitude+a)),v(o,t.longitude),v(o,x(t.longitude-a)),v(o,x(t.longitude+a))]}(t,e).map((t=>function(t,e){i(t);const n=Math.ceil(e/5);if(t.length<n)return[t,t+"~"];const o=t.substring(0,n),r=o.substring(0,o.length-1),s=c.indexOf(o.charAt(o.length-1)),a=e-5*r.length,u=5-a,d=s>>u<<u,h=d+(1<<u);return h>31?[r+c[d],r+"~"]:[r+c[d],r+c[h]]}(r({lat:t.latitude,lng:t.longitude},o),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 b(t){return Math.log(t)/Math.log(2)}function _(t,e){const n=y(t,e);return Math.abs(n)>1e-6?Math.max(1,b(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 u(n),n}function x(t){if(t<=180&&t>=-180)return t;const e=t+180;return e>0?e%360-180:180- -e%360}function E(t,e){u(t);const n=r({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&&u(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 q{constructor(t,e){if(this._queryCriteria=e,this._docs=new Map,d(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 j{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=[],d(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,o=t.doc.id,r=this._docs.get(o),a={change:{doc:t.doc,oldIndex:r&&this._firstEmitted?r.change.oldIndex:-1,newIndex:r&&this._firstEmitted?r.change.newIndex:-1,type:r&&this._firstEmitted?t.type:"added"},distance:i,emitted:!!this._firstEmitted&&!!r};if(this._queryCriteria.radius>=i){if(!r&&"removed"===a.change.type)return;r||"modified"!==a.change.type||(a.change.type="added"),this._newValues=!0,this._docs.set(o,a)}else r?(a.change.type="removed",this._newValues=!0,this._docs.set(o,a)):r||this._firstRoundResolved||(this._newValues=!0)})):this._firstRoundResolved||(this._newValues=!0)}}t.GeoQuerySnapshot=M,t.encodeDocumentAdd=function(t,e){if("[object Object]"!==Object.prototype.toString.call(t))throw new Error("document must be an object");return E(g(t,e),t)},t.encodeDocumentSet=function(t,e){if("[object Object]"!==Object.prototype.toString.call(t))throw new Error("document must be an object");const n=g(t,e&&e.customKey,e&&(e.merge||!!e.mergeFields));return n?E(n,t):t},t.encodeDocumentUpdate=function(t,e){if("[object Object]"!==Object.prototype.toString.call(t))throw new Error("document must be an object");const n=g(t,e,!0);return n?E(n,t):t},t.encodeGeoDocument=E,t.geoQueryGet=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 o=p(t,e).map((t=>i?t.get(n):t.get()));return Promise.all(o).then((t=>new q(t,e).getGeoQuerySnapshot()))}return t=e.limit?t.limit(e.limit):t,(i?t.get(n):t.get()).then((t=>new M(t)))},t.geoQueryOnSnapshot=function(t,e){return(n,i=(()=>{}))=>e.center&&"number"==typeof e.radius?new j(p(t,e),e,n,i).unsubscribe():(t=e.limit?t.limit(e.limit):t).onSnapshot((t=>n(new M(t))),i)},t.validateGeoDocument=s,t.validateLimit=a,t.validateLocation=u,t.validateQueryCriteria=d,Object.defineProperty(t,"__esModule",{value:!0})}));