-
Notifications
You must be signed in to change notification settings - Fork 58
/
GeoQuery.ts
199 lines (185 loc) · 8.16 KB
/
GeoQuery.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import { GeoFirestoreTypes } from './GeoFirestoreTypes';
import { GeoFirestore } from './GeoFirestore';
import { GeoJoinerGet } from './GeoJoinerGet';
import { GeoJoinerOnSnapshot } from './GeoJoinerOnSnapshot';
import { GeoQuerySnapshot } from './GeoQuerySnapshot';
import { validateQueryCriteria, geohashQueries } from './utils';
/**
* A `GeoQuery` refers to a Query which you can read or listen to. You can also construct refined `GeoQuery` objects by adding filters and
* ordering.
*/
export class GeoQuery {
private _center: GeoFirestoreTypes.cloud.GeoPoint | GeoFirestoreTypes.web.GeoPoint;
private _limit: number;
private _radius: number;
private _isWeb: boolean;
/**
* @param _query The `Query` instance.
* @param queryCriteria The query criteria of geo based queries, includes field such as center, radius, and limit.
*/
constructor(
private _query: GeoFirestoreTypes.cloud.Query | GeoFirestoreTypes.web.Query,
queryCriteria?: GeoFirestoreTypes.QueryCriteria
) {
if (Object.prototype.toString.call(_query) !== '[object Object]') {
throw new Error('Query must be an instance of a Firestore Query');
}
this._isWeb = Object.prototype.toString
.call((_query as GeoFirestoreTypes.web.CollectionReference).firestore.enablePersistence) === '[object Function]';
if (queryCriteria) {
if (queryCriteria.limit) {
this._limit = queryCriteria.limit;
}
if (queryCriteria.center && queryCriteria.radius) {
// Validate and save the query criteria
validateQueryCriteria(queryCriteria);
this._center = queryCriteria.center;
this._radius = queryCriteria.radius;
}
}
}
/**
* The `Firestore` for the Firestore database (useful for performing transactions, etc.).
*/
get firestore(): GeoFirestore {
return new GeoFirestore(this._query.firestore);
}
/**
* Attaches a listener for `GeoQuerySnapshot` events.
*
* @param onNext A callback to be called every time a new `GeoQuerySnapshot` is available.
* @param onError A callback to be called if the listen fails or is cancelled. Since multuple queries occur only the failed query will
* cease.
* @return An unsubscribe function that can be called to cancel the snapshot listener.
*/
get onSnapshot(): ((onNext: (snapshot: GeoQuerySnapshot) => void, onError?: (error: Error) => void) => () => void) {
return (onNext: (snapshot: GeoQuerySnapshot) => void, onError?: (error: Error) => void): (() => void) => {
if (this._center && this._radius) {
return new GeoJoinerOnSnapshot(this._generateQuery(), this._queryCriteria, onNext, onError).unsubscribe();
} else {
return (this._query as GeoFirestoreTypes.web.Query).onSnapshot((snapshot) => onNext(new GeoQuerySnapshot(snapshot)), onError);
}
};
}
/**
* Executes the query and returns the results as a GeoQuerySnapshot.
*
* WEB CLIENT ONLY
* Note: By default, get() attempts to provide up-to-date data when possible by waiting for data from the server, but it may return
* cached data or fail if you are offline and the server cannot be reached. This behavior can be altered via the `GetOptions` parameter.
*
* @param options An object to configure the get behavior.
* @return A Promise that will be resolved with the results of the GeoQuery.
*/
public get(options: GeoFirestoreTypes.web.GetOptions = { source: 'default' }): Promise<GeoQuerySnapshot> {
if (this._center && this._radius) {
const queries = this._generateQuery().map((query) => this._isWeb ? query.get(options) : query.get());
return Promise.all(queries).then(value => new GeoJoinerGet(value, this._queryCriteria).getGeoQuerySnapshot());
} else {
const promise = this._isWeb ?
(this._query as GeoFirestoreTypes.web.Query).get(options) : (this._query as GeoFirestoreTypes.web.Query).get();
return promise.then((snapshot) => new GeoQuerySnapshot(snapshot));
}
}
/**
* Creates and returns a new GeoQuery that's additionally limited to only return up to the specified number of documents.
*
* This function returns a new (immutable) instance of the GeoQuery (rather than modify the existing instance) to impose the limit.
*
* Note: Geoqueries require an aggregation of queries, the library applies a paritial limit on the server,
* but primarily runs its limit on the client. This may mean you are loading to the client more documents then you intended.
* Use with this performance limitation in mind.
*
* @param limit The maximum number of items to return.
* @return The created GeoQuery.
*/
public limit(limit: number): GeoQuery {
this._limit = limit || this._limit;
return new GeoQuery(this._query.limit(limit), this._queryCriteria);
}
/**
* Creates and returns a new GeoQuery with the geoquery filter where `get` and `onSnapshot` will query around.
*
* This function returns a new (immutable) instance of the GeoQuery (rather than modify the existing instance) to impose the filter.
*
* @param newQueryCriteria The criteria which specifies the query's center and radius.
* @return The created GeoQuery.
*/
public near(newGeoQueryCriteria: GeoFirestoreTypes.QueryCriteria): GeoQuery {
// Validate and save the new query criteria
validateQueryCriteria(newGeoQueryCriteria);
this._center = newGeoQueryCriteria.center || this._center;
this._radius = newGeoQueryCriteria.radius || this._radius;
return new GeoQuery(this._query, this._queryCriteria);
}
/**
* Creates and returns a new GeoQuery with the additional filter that documents must contain the specified field and that its value
* should satisfy the relation constraint provided.
*
* This function returns a new (immutable) instance of the GeoQuery (rather than modify the existing instance) to impose the filter.
*
* @param fieldPath The path to compare
* @param opStr The operation string (e.g "<", "<=", "==", ">", ">=").
* @param value The value for comparison
* @return The created GeoQuery.
*/
public where(
fieldPath: string | GeoFirestoreTypes.cloud.FieldPath | GeoFirestoreTypes.web.FieldPath,
opStr: GeoFirestoreTypes.WhereFilterOp,
value: any
): GeoQuery {
return new GeoQuery(this._query.where((fieldPath ? ('d.' + fieldPath) : fieldPath), opStr, value), this._queryCriteria);
}
/**
* Creates an array of `Query` objects that query the appropriate geohashes based on the radius and center GeoPoint of the query criteria.
*
* @return Array of Queries to search against.
*/
private _generateQuery(): GeoFirestoreTypes.web.Query[] {
// Get the list of geohashes to query
let geohashesToQuery: string[] = geohashQueries(this._center, this._radius * 1000).map(this._queryToString);
// Filter out duplicate geohashes
geohashesToQuery = geohashesToQuery.filter((geohash: string, i: number) => geohashesToQuery.indexOf(geohash) === i);
return geohashesToQuery.map((toQueryStr: string) => {
// decode the geohash query string
const query: string[] = this._stringToQuery(toQueryStr);
// Create the Firebase query
return this._query.where('g', '>=', query[0]).where('g', '<=', query[1]) as GeoFirestoreTypes.web.Query;
});
}
/**
* Returns the center and radius of geo based queries as a QueryCriteria object.
*/
private get _queryCriteria(): GeoFirestoreTypes.QueryCriteria {
return {
center: this._center,
limit: this._limit,
radius: this._radius
};
}
/**
* Decodes a query string to a query
*
* @param str The encoded query.
* @return The decoded query as a [start, end] pair.
*/
private _stringToQuery(str: string): string[] {
const decoded: string[] = str.split(':');
if (decoded.length !== 2) {
throw new Error('Invalid internal state! Not a valid geohash query: ' + str);
}
return decoded;
}
/**
* Encodes a query as a string for easier indexing and equality.
*
* @param query The query to encode.
* @return The encoded query as string.
*/
private _queryToString(query: string[]): string {
if (query.length !== 2) {
throw new Error('Not a valid geohash query: ' + query);
}
return query[0] + ':' + query[1];
}
}