1 /*
2 * Copyright (c) 2014, Matt Stancliff <[email protected]>.
3 * Copyright (c) 2015-2016, Salvatore Sanfilippo <[email protected]>.
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * * Redistributions of source code must retain the above copyright notice,
10 * this list of conditions and the following disclaimer.
11 * * Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * * Neither the name of Redis nor the names of its contributors may be used
15 * to endorse or promote products derived from this software without
16 * specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include "geo.h"
32 #include "geohash_helper.h"
33 #include "debugmacro.h"
34
35 /* Things exported from t_zset.c only for geo.c, since it is the only other
36 * part of Redis that requires close zset introspection. */
37 unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range);
38 int zslValueLteMax(double value, zrangespec *spec);
39
40 /* ====================================================================
41 * This file implements the following commands:
42 *
43 * - geoadd - add coordinates for value to geoset
44 * - georadius - search radius by coordinates in geoset
45 * - georadiusbymember - search radius based on geoset member position
46 * ==================================================================== */
47
48 /* ====================================================================
49 * geoArray implementation
50 * ==================================================================== */
51
52 /* Create a new array of geoPoints. */
geoArrayCreate(void)53 geoArray *geoArrayCreate(void) {
54 geoArray *ga = zmalloc(sizeof(*ga));
55 /* It gets allocated on first geoArrayAppend() call. */
56 ga->array = NULL;
57 ga->buckets = 0;
58 ga->used = 0;
59 return ga;
60 }
61
62 /* Add a new entry and return its pointer so that the caller can populate
63 * it with data. */
geoArrayAppend(geoArray * ga)64 geoPoint *geoArrayAppend(geoArray *ga) {
65 if (ga->used == ga->buckets) {
66 ga->buckets = (ga->buckets == 0) ? 8 : ga->buckets*2;
67 ga->array = zrealloc(ga->array,sizeof(geoPoint)*ga->buckets);
68 }
69 geoPoint *gp = ga->array+ga->used;
70 ga->used++;
71 return gp;
72 }
73
74 /* Destroy a geoArray created with geoArrayCreate(). */
geoArrayFree(geoArray * ga)75 void geoArrayFree(geoArray *ga) {
76 size_t i;
77 for (i = 0; i < ga->used; i++) sdsfree(ga->array[i].member);
78 zfree(ga->array);
79 zfree(ga);
80 }
81
82 /* ====================================================================
83 * Helpers
84 * ==================================================================== */
decodeGeohash(double bits,double * xy)85 int decodeGeohash(double bits, double *xy) {
86 GeoHashBits hash = { .bits = (uint64_t)bits, .step = GEO_STEP_MAX };
87 return geohashDecodeToLongLatWGS84(hash, xy);
88 }
89
90 /* Input Argument Helper */
91 /* Take a pointer to the latitude arg then use the next arg for longitude.
92 * On parse error C_ERR is returned, otherwise C_OK. */
extractLongLatOrReply(client * c,robj ** argv,double * xy)93 int extractLongLatOrReply(client *c, robj **argv, double *xy) {
94 int i;
95 for (i = 0; i < 2; i++) {
96 if (getDoubleFromObjectOrReply(c, argv[i], xy + i, NULL) !=
97 C_OK) {
98 return C_ERR;
99 }
100 }
101 if (xy[0] < GEO_LONG_MIN || xy[0] > GEO_LONG_MAX ||
102 xy[1] < GEO_LAT_MIN || xy[1] > GEO_LAT_MAX) {
103 addReplySds(c, sdscatprintf(sdsempty(),
104 "-ERR invalid longitude,latitude pair %f,%f\r\n",xy[0],xy[1]));
105 return C_ERR;
106 }
107 return C_OK;
108 }
109
110 /* Input Argument Helper */
111 /* Decode lat/long from a zset member's score.
112 * Returns C_OK on successful decoding, otherwise C_ERR is returned. */
longLatFromMember(robj * zobj,robj * member,double * xy)113 int longLatFromMember(robj *zobj, robj *member, double *xy) {
114 double score = 0;
115
116 if (zsetScore(zobj, member->ptr, &score) == C_ERR) return C_ERR;
117 if (!decodeGeohash(score, xy)) return C_ERR;
118 return C_OK;
119 }
120
121 /* Check that the unit argument matches one of the known units, and returns
122 * the conversion factor to meters (you need to divide meters by the conversion
123 * factor to convert to the right unit).
124 *
125 * If the unit is not valid, an error is reported to the client, and a value
126 * less than zero is returned. */
extractUnitOrReply(client * c,robj * unit)127 double extractUnitOrReply(client *c, robj *unit) {
128 char *u = unit->ptr;
129
130 if (!strcmp(u, "m")) {
131 return 1;
132 } else if (!strcmp(u, "km")) {
133 return 1000;
134 } else if (!strcmp(u, "ft")) {
135 return 0.3048;
136 } else if (!strcmp(u, "mi")) {
137 return 1609.34;
138 } else {
139 addReplyError(c,
140 "unsupported unit provided. please use m, km, ft, mi");
141 return -1;
142 }
143 }
144
145 /* Input Argument Helper.
146 * Extract the dinstance from the specified two arguments starting at 'argv'
147 * that shouldbe in the form: <number> <unit> and return the dinstance in the
148 * specified unit on success. *conversions is populated with the coefficient
149 * to use in order to convert meters to the unit.
150 *
151 * On error a value less than zero is returned. */
extractDistanceOrReply(client * c,robj ** argv,double * conversion)152 double extractDistanceOrReply(client *c, robj **argv,
153 double *conversion) {
154 double distance;
155 if (getDoubleFromObjectOrReply(c, argv[0], &distance,
156 "need numeric radius") != C_OK) {
157 return -1;
158 }
159
160 if (distance < 0) {
161 addReplyError(c,"radius cannot be negative");
162 return -1;
163 }
164
165 double to_meters = extractUnitOrReply(c,argv[1]);
166 if (to_meters < 0) {
167 return -1;
168 }
169
170 if (conversion) *conversion = to_meters;
171 return distance * to_meters;
172 }
173
174 /* The default addReplyDouble has too much accuracy. We use this
175 * for returning location distances. "5.2145 meters away" is nicer
176 * than "5.2144992818115 meters away." We provide 4 digits after the dot
177 * so that the returned value is decently accurate even when the unit is
178 * the kilometer. */
addReplyDoubleDistance(client * c,double d)179 void addReplyDoubleDistance(client *c, double d) {
180 char dbuf[128];
181 int dlen = snprintf(dbuf, sizeof(dbuf), "%.4f", d);
182 addReplyBulkCBuffer(c, dbuf, dlen);
183 }
184
185 /* Helper function for geoGetPointsInRange(): given a sorted set score
186 * representing a point, and another point (the center of our search) and
187 * a radius, appends this entry as a geoPoint into the specified geoArray
188 * only if the point is within the search area.
189 *
190 * returns C_OK if the point is included, or REIDS_ERR if it is outside. */
geoAppendIfWithinRadius(geoArray * ga,double lon,double lat,double radius,double score,sds member)191 int geoAppendIfWithinRadius(geoArray *ga, double lon, double lat, double radius, double score, sds member) {
192 double distance, xy[2];
193
194 if (!decodeGeohash(score,xy)) return C_ERR; /* Can't decode. */
195 /* Note that geohashGetDistanceIfInRadiusWGS84() takes arguments in
196 * reverse order: longitude first, latitude later. */
197 if (!geohashGetDistanceIfInRadiusWGS84(lon,lat, xy[0], xy[1],
198 radius, &distance))
199 {
200 return C_ERR;
201 }
202
203 /* Append the new element. */
204 geoPoint *gp = geoArrayAppend(ga);
205 gp->longitude = xy[0];
206 gp->latitude = xy[1];
207 gp->dist = distance;
208 gp->member = member;
209 gp->score = score;
210 return C_OK;
211 }
212
213 /* Query a Redis sorted set to extract all the elements between 'min' and
214 * 'max', appending them into the array of geoPoint structures 'gparray'.
215 * The command returns the number of elements added to the array.
216 *
217 * Elements which are farest than 'radius' from the specified 'x' and 'y'
218 * coordinates are not included.
219 *
220 * The ability of this function to append to an existing set of points is
221 * important for good performances because querying by radius is performed
222 * using multiple queries to the sorted set, that we later need to sort
223 * via qsort. Similarly we need to be able to reject points outside the search
224 * radius area ASAP in order to allocate and process more points than needed. */
geoGetPointsInRange(robj * zobj,double min,double max,double lon,double lat,double radius,geoArray * ga)225 int geoGetPointsInRange(robj *zobj, double min, double max, double lon, double lat, double radius, geoArray *ga) {
226 /* minex 0 = include min in range; maxex 1 = exclude max in range */
227 /* That's: min <= val < max */
228 zrangespec range = { .min = min, .max = max, .minex = 0, .maxex = 1 };
229 size_t origincount = ga->used;
230 sds member;
231
232 if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
233 unsigned char *zl = zobj->ptr;
234 unsigned char *eptr, *sptr;
235 unsigned char *vstr = NULL;
236 unsigned int vlen = 0;
237 long long vlong = 0;
238 double score = 0;
239
240 if ((eptr = zzlFirstInRange(zl, &range)) == NULL) {
241 /* Nothing exists starting at our min. No results. */
242 return 0;
243 }
244
245 sptr = ziplistNext(zl, eptr);
246 while (eptr) {
247 score = zzlGetScore(sptr);
248
249 /* If we fell out of range, break. */
250 if (!zslValueLteMax(score, &range))
251 break;
252
253 /* We know the element exists. ziplistGet should always succeed */
254 ziplistGet(eptr, &vstr, &vlen, &vlong);
255 member = (vstr == NULL) ? sdsfromlonglong(vlong) :
256 sdsnewlen(vstr,vlen);
257 if (geoAppendIfWithinRadius(ga,lon,lat,radius,score,member)
258 == C_ERR) sdsfree(member);
259 zzlNext(zl, &eptr, &sptr);
260 }
261 } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
262 zset *zs = zobj->ptr;
263 zskiplist *zsl = zs->zsl;
264 zskiplistNode *ln;
265
266 if ((ln = zslFirstInRange(zsl, &range)) == NULL) {
267 /* Nothing exists starting at our min. No results. */
268 return 0;
269 }
270
271 while (ln) {
272 sds ele = ln->ele;
273 /* Abort when the node is no longer in range. */
274 if (!zslValueLteMax(ln->score, &range))
275 break;
276
277 ele = sdsdup(ele);
278 if (geoAppendIfWithinRadius(ga,lon,lat,radius,ln->score,ele)
279 == C_ERR) sdsfree(ele);
280 ln = ln->level[0].forward;
281 }
282 }
283 return ga->used - origincount;
284 }
285
286 /* Compute the sorted set scores min (inclusive), max (exclusive) we should
287 * query in order to retrieve all the elements inside the specified area
288 * 'hash'. The two scores are returned by reference in *min and *max. */
scoresOfGeoHashBox(GeoHashBits hash,GeoHashFix52Bits * min,GeoHashFix52Bits * max)289 void scoresOfGeoHashBox(GeoHashBits hash, GeoHashFix52Bits *min, GeoHashFix52Bits *max) {
290 /* We want to compute the sorted set scores that will include all the
291 * elements inside the specified Geohash 'hash', which has as many
292 * bits as specified by hash.step * 2.
293 *
294 * So if step is, for example, 3, and the hash value in binary
295 * is 101010, since our score is 52 bits we want every element which
296 * is in binary: 101010?????????????????????????????????????????????
297 * Where ? can be 0 or 1.
298 *
299 * To get the min score we just use the initial hash value left
300 * shifted enough to get the 52 bit value. Later we increment the
301 * 6 bit prefis (see the hash.bits++ statement), and get the new
302 * prefix: 101011, which we align again to 52 bits to get the maximum
303 * value (which is excluded from the search). So we get everything
304 * between the two following scores (represented in binary):
305 *
306 * 1010100000000000000000000000000000000000000000000000 (included)
307 * and
308 * 1010110000000000000000000000000000000000000000000000 (excluded).
309 */
310 *min = geohashAlign52Bits(hash);
311 hash.bits++;
312 *max = geohashAlign52Bits(hash);
313 }
314
315 /* Obtain all members between the min/max of this geohash bounding box.
316 * Populate a geoArray of GeoPoints by calling geoGetPointsInRange().
317 * Return the number of points added to the array. */
membersOfGeoHashBox(robj * zobj,GeoHashBits hash,geoArray * ga,double lon,double lat,double radius)318 int membersOfGeoHashBox(robj *zobj, GeoHashBits hash, geoArray *ga, double lon, double lat, double radius) {
319 GeoHashFix52Bits min, max;
320
321 scoresOfGeoHashBox(hash,&min,&max);
322 return geoGetPointsInRange(zobj, min, max, lon, lat, radius, ga);
323 }
324
325 /* Search all eight neighbors + self geohash box */
membersOfAllNeighbors(robj * zobj,GeoHashRadius n,double lon,double lat,double radius,geoArray * ga)326 int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) {
327 GeoHashBits neighbors[9];
328 unsigned int i, count = 0, last_processed = 0;
329 int debugmsg = 0;
330
331 neighbors[0] = n.hash;
332 neighbors[1] = n.neighbors.north;
333 neighbors[2] = n.neighbors.south;
334 neighbors[3] = n.neighbors.east;
335 neighbors[4] = n.neighbors.west;
336 neighbors[5] = n.neighbors.north_east;
337 neighbors[6] = n.neighbors.north_west;
338 neighbors[7] = n.neighbors.south_east;
339 neighbors[8] = n.neighbors.south_west;
340
341 /* For each neighbor (*and* our own hashbox), get all the matching
342 * members and add them to the potential result list. */
343 for (i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) {
344 if (HASHISZERO(neighbors[i])) {
345 if (debugmsg) D("neighbors[%d] is zero",i);
346 continue;
347 }
348
349 /* Debugging info. */
350 if (debugmsg) {
351 GeoHashRange long_range, lat_range;
352 geohashGetCoordRange(&long_range,&lat_range);
353 GeoHashArea myarea = {{0}};
354 geohashDecode(long_range, lat_range, neighbors[i], &myarea);
355
356 /* Dump center square. */
357 D("neighbors[%d]:\n",i);
358 D("area.longitude.min: %f\n", myarea.longitude.min);
359 D("area.longitude.max: %f\n", myarea.longitude.max);
360 D("area.latitude.min: %f\n", myarea.latitude.min);
361 D("area.latitude.max: %f\n", myarea.latitude.max);
362 D("\n");
363 }
364
365 /* When a huge Radius (in the 5000 km range or more) is used,
366 * adjacent neighbors can be the same, leading to duplicated
367 * elements. Skip every range which is the same as the one
368 * processed previously. */
369 if (last_processed &&
370 neighbors[i].bits == neighbors[last_processed].bits &&
371 neighbors[i].step == neighbors[last_processed].step)
372 {
373 if (debugmsg)
374 D("Skipping processing of %d, same as previous\n",i);
375 continue;
376 }
377 count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius);
378 last_processed = i;
379 }
380 return count;
381 }
382
383 /* Sort comparators for qsort() */
sort_gp_asc(const void * a,const void * b)384 static int sort_gp_asc(const void *a, const void *b) {
385 const struct geoPoint *gpa = a, *gpb = b;
386 /* We can't do adist - bdist because they are doubles and
387 * the comparator returns an int. */
388 if (gpa->dist > gpb->dist)
389 return 1;
390 else if (gpa->dist == gpb->dist)
391 return 0;
392 else
393 return -1;
394 }
395
sort_gp_desc(const void * a,const void * b)396 static int sort_gp_desc(const void *a, const void *b) {
397 return -sort_gp_asc(a, b);
398 }
399
400 /* ====================================================================
401 * Commands
402 * ==================================================================== */
403
404 /* GEOADD key long lat name [long2 lat2 name2 ... longN latN nameN] */
geoaddCommand(client * c)405 void geoaddCommand(client *c) {
406 /* Check arguments number for sanity. */
407 if ((c->argc - 2) % 3 != 0) {
408 /* Need an odd number of arguments if we got this far... */
409 addReplyError(c, "syntax error. Try GEOADD key [x1] [y1] [name1] "
410 "[x2] [y2] [name2] ... ");
411 return;
412 }
413
414 int elements = (c->argc - 2) / 3;
415 int argc = 2+elements*2; /* ZADD key score ele ... */
416 robj **argv = zcalloc(argc*sizeof(robj*));
417 argv[0] = createRawStringObject("zadd",4);
418 argv[1] = c->argv[1]; /* key */
419 incrRefCount(argv[1]);
420
421 /* Create the argument vector to call ZADD in order to add all
422 * the score,value pairs to the requested zset, where score is actually
423 * an encoded version of lat,long. */
424 int i;
425 for (i = 0; i < elements; i++) {
426 double xy[2];
427
428 if (extractLongLatOrReply(c, (c->argv+2)+(i*3),xy) == C_ERR) {
429 for (i = 0; i < argc; i++)
430 if (argv[i]) decrRefCount(argv[i]);
431 zfree(argv);
432 return;
433 }
434
435 /* Turn the coordinates into the score of the element. */
436 GeoHashBits hash;
437 geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);
438 GeoHashFix52Bits bits = geohashAlign52Bits(hash);
439 robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));
440 robj *val = c->argv[2 + i * 3 + 2];
441 argv[2+i*2] = score;
442 argv[3+i*2] = val;
443 incrRefCount(val);
444 }
445
446 /* Finally call ZADD that will do the work for us. */
447 replaceClientCommandVector(c,argc,argv);
448 zaddCommand(c);
449 }
450
451 #define SORT_NONE 0
452 #define SORT_ASC 1
453 #define SORT_DESC 2
454
455 #define RADIUS_COORDS (1<<0) /* Search around coordinates. */
456 #define RADIUS_MEMBER (1<<1) /* Search around member. */
457 #define RADIUS_NOSTORE (1<<2) /* Do not acceot STORE/STOREDIST option. */
458
459 /* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
460 * [COUNT count] [STORE key] [STOREDIST key]
461 * GEORADIUSBYMEMBER key member radius unit ... options ... */
georadiusGeneric(client * c,int flags)462 void georadiusGeneric(client *c, int flags) {
463 robj *key = c->argv[1];
464 robj *storekey = NULL;
465 int storedist = 0; /* 0 for STORE, 1 for STOREDIST. */
466
467 /* Look up the requested zset */
468 robj *zobj = NULL;
469 if ((zobj = lookupKeyReadOrReply(c, key, shared.emptymultibulk)) == NULL ||
470 checkType(c, zobj, OBJ_ZSET)) {
471 return;
472 }
473
474 /* Find long/lat to use for radius search based on inquiry type */
475 int base_args;
476 double xy[2] = { 0 };
477 if (flags & RADIUS_COORDS) {
478 base_args = 6;
479 if (extractLongLatOrReply(c, c->argv + 2, xy) == C_ERR)
480 return;
481 } else if (flags & RADIUS_MEMBER) {
482 base_args = 5;
483 robj *member = c->argv[2];
484 if (longLatFromMember(zobj, member, xy) == C_ERR) {
485 addReplyError(c, "could not decode requested zset member");
486 return;
487 }
488 } else {
489 addReplyError(c, "Unknown georadius search type");
490 return;
491 }
492
493 /* Extract radius and units from arguments */
494 double radius_meters = 0, conversion = 1;
495 if ((radius_meters = extractDistanceOrReply(c, c->argv + base_args - 2,
496 &conversion)) < 0) {
497 return;
498 }
499
500 /* Discover and populate all optional parameters. */
501 int withdist = 0, withhash = 0, withcoords = 0;
502 int sort = SORT_NONE;
503 long long count = 0;
504 if (c->argc > base_args) {
505 int remaining = c->argc - base_args;
506 for (int i = 0; i < remaining; i++) {
507 char *arg = c->argv[base_args + i]->ptr;
508 if (!strcasecmp(arg, "withdist")) {
509 withdist = 1;
510 } else if (!strcasecmp(arg, "withhash")) {
511 withhash = 1;
512 } else if (!strcasecmp(arg, "withcoord")) {
513 withcoords = 1;
514 } else if (!strcasecmp(arg, "asc")) {
515 sort = SORT_ASC;
516 } else if (!strcasecmp(arg, "desc")) {
517 sort = SORT_DESC;
518 } else if (!strcasecmp(arg, "count") && (i+1) < remaining) {
519 if (getLongLongFromObjectOrReply(c, c->argv[base_args+i+1],
520 &count, NULL) != C_OK) return;
521 if (count <= 0) {
522 addReplyError(c,"COUNT must be > 0");
523 return;
524 }
525 i++;
526 } else if (!strcasecmp(arg, "store") &&
527 (i+1) < remaining &&
528 !(flags & RADIUS_NOSTORE))
529 {
530 storekey = c->argv[base_args+i+1];
531 storedist = 0;
532 i++;
533 } else if (!strcasecmp(arg, "storedist") &&
534 (i+1) < remaining &&
535 !(flags & RADIUS_NOSTORE))
536 {
537 storekey = c->argv[base_args+i+1];
538 storedist = 1;
539 i++;
540 } else {
541 addReply(c, shared.syntaxerr);
542 return;
543 }
544 }
545 }
546
547 /* Trap options not compatible with STORE and STOREDIST. */
548 if (storekey && (withdist || withhash || withcoords)) {
549 addReplyError(c,
550 "STORE option in GEORADIUS is not compatible with "
551 "WITHDIST, WITHHASH and WITHCOORDS options");
552 return;
553 }
554
555 /* COUNT without ordering does not make much sense, force ASC
556 * ordering if COUNT was specified but no sorting was requested. */
557 if (count != 0 && sort == SORT_NONE) sort = SORT_ASC;
558
559 /* Get all neighbor geohash boxes for our radius search */
560 GeoHashRadius georadius =
561 geohashGetAreasByRadiusWGS84(xy[0], xy[1], radius_meters);
562
563 /* Search the zset for all matching points */
564 geoArray *ga = geoArrayCreate();
565 membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga);
566
567 /* If no matching results, the user gets an empty reply. */
568 if (ga->used == 0 && storekey == NULL) {
569 addReply(c, shared.emptymultibulk);
570 geoArrayFree(ga);
571 return;
572 }
573
574 long result_length = ga->used;
575 long returned_items = (count == 0 || result_length < count) ?
576 result_length : count;
577 long option_length = 0;
578
579 /* Process [optional] requested sorting */
580 if (sort == SORT_ASC) {
581 qsort(ga->array, result_length, sizeof(geoPoint), sort_gp_asc);
582 } else if (sort == SORT_DESC) {
583 qsort(ga->array, result_length, sizeof(geoPoint), sort_gp_desc);
584 }
585
586 if (storekey == NULL) {
587 /* No target key, return results to user. */
588
589 /* Our options are self-contained nested multibulk replies, so we
590 * only need to track how many of those nested replies we return. */
591 if (withdist)
592 option_length++;
593
594 if (withcoords)
595 option_length++;
596
597 if (withhash)
598 option_length++;
599
600 /* The multibulk len we send is exactly result_length. The result is
601 * either all strings of just zset members *or* a nested multi-bulk
602 * reply containing the zset member string _and_ all the additional
603 * options the user enabled for this request. */
604 addReplyMultiBulkLen(c, returned_items);
605
606 /* Finally send results back to the caller */
607 int i;
608 for (i = 0; i < returned_items; i++) {
609 geoPoint *gp = ga->array+i;
610 gp->dist /= conversion; /* Fix according to unit. */
611
612 /* If we have options in option_length, return each sub-result
613 * as a nested multi-bulk. Add 1 to account for result value
614 * itself. */
615 if (option_length)
616 addReplyMultiBulkLen(c, option_length + 1);
617
618 addReplyBulkSds(c,gp->member);
619 gp->member = NULL;
620
621 if (withdist)
622 addReplyDoubleDistance(c, gp->dist);
623
624 if (withhash)
625 addReplyLongLong(c, gp->score);
626
627 if (withcoords) {
628 addReplyMultiBulkLen(c, 2);
629 addReplyHumanLongDouble(c, gp->longitude);
630 addReplyHumanLongDouble(c, gp->latitude);
631 }
632 }
633 } else {
634 /* Target key, create a sorted set with the results. */
635 robj *zobj;
636 zset *zs;
637 int i;
638 size_t maxelelen = 0;
639
640 if (returned_items) {
641 zobj = createZsetObject();
642 zs = zobj->ptr;
643 }
644
645 for (i = 0; i < returned_items; i++) {
646 zskiplistNode *znode;
647 geoPoint *gp = ga->array+i;
648 gp->dist /= conversion; /* Fix according to unit. */
649 double score = storedist ? gp->dist : gp->score;
650 size_t elelen = sdslen(gp->member);
651
652 if (maxelelen < elelen) maxelelen = elelen;
653 znode = zslInsert(zs->zsl,score,gp->member);
654 serverAssert(dictAdd(zs->dict,gp->member,&znode->score) == DICT_OK);
655 gp->member = NULL;
656 }
657
658 if (returned_items) {
659 zsetConvertToZiplistIfNeeded(zobj,maxelelen);
660 setKey(c->db,storekey,zobj);
661 decrRefCount(zobj);
662 notifyKeyspaceEvent(NOTIFY_ZSET,"georadiusstore",storekey,
663 c->db->id);
664 server.dirty += returned_items;
665 } else if (dbDelete(c->db,storekey)) {
666 signalModifiedKey(c->db,storekey);
667 notifyKeyspaceEvent(NOTIFY_GENERIC,"del",storekey,c->db->id);
668 server.dirty++;
669 }
670 addReplyLongLong(c, returned_items);
671 }
672 geoArrayFree(ga);
673 }
674
675 /* GEORADIUS wrapper function. */
georadiusCommand(client * c)676 void georadiusCommand(client *c) {
677 georadiusGeneric(c, RADIUS_COORDS);
678 }
679
680 /* GEORADIUSBYMEMBER wrapper function. */
georadiusbymemberCommand(client * c)681 void georadiusbymemberCommand(client *c) {
682 georadiusGeneric(c, RADIUS_MEMBER);
683 }
684
685 /* GEORADIUS_RO wrapper function. */
georadiusroCommand(client * c)686 void georadiusroCommand(client *c) {
687 georadiusGeneric(c, RADIUS_COORDS|RADIUS_NOSTORE);
688 }
689
690 /* GEORADIUSBYMEMBER_RO wrapper function. */
georadiusbymemberroCommand(client * c)691 void georadiusbymemberroCommand(client *c) {
692 georadiusGeneric(c, RADIUS_MEMBER|RADIUS_NOSTORE);
693 }
694
695 /* GEOHASH key ele1 ele2 ... eleN
696 *
697 * Returns an array with an 11 characters geohash representation of the
698 * position of the specified elements. */
geohashCommand(client * c)699 void geohashCommand(client *c) {
700 char *geoalphabet= "0123456789bcdefghjkmnpqrstuvwxyz";
701 int j;
702
703 /* Look up the requested zset */
704 robj *zobj = lookupKeyRead(c->db, c->argv[1]);
705 if (zobj && checkType(c, zobj, OBJ_ZSET)) return;
706
707 /* Geohash elements one after the other, using a null bulk reply for
708 * missing elements. */
709 addReplyMultiBulkLen(c,c->argc-2);
710 for (j = 2; j < c->argc; j++) {
711 double score;
712 if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
713 addReply(c,shared.nullbulk);
714 } else {
715 /* The internal format we use for geocoding is a bit different
716 * than the standard, since we use as initial latitude range
717 * -85,85, while the normal geohashing algorithm uses -90,90.
718 * So we have to decode our position and re-encode using the
719 * standard ranges in order to output a valid geohash string. */
720
721 /* Decode... */
722 double xy[2];
723 if (!decodeGeohash(score,xy)) {
724 addReply(c,shared.nullbulk);
725 continue;
726 }
727
728 /* Re-encode */
729 GeoHashRange r[2];
730 GeoHashBits hash;
731 r[0].min = -180;
732 r[0].max = 180;
733 r[1].min = -90;
734 r[1].max = 90;
735 geohashEncode(&r[0],&r[1],xy[0],xy[1],26,&hash);
736
737 char buf[12];
738 int i;
739 for (i = 0; i < 11; i++) {
740 int idx = (hash.bits >> (52-((i+1)*5))) & 0x1f;
741 buf[i] = geoalphabet[idx];
742 }
743 buf[11] = '\0';
744 addReplyBulkCBuffer(c,buf,11);
745 }
746 }
747 }
748
749 /* GEOPOS key ele1 ele2 ... eleN
750 *
751 * Returns an array of two-items arrays representing the x,y position of each
752 * element specified in the arguments. For missing elements NULL is returned. */
geoposCommand(client * c)753 void geoposCommand(client *c) {
754 int j;
755
756 /* Look up the requested zset */
757 robj *zobj = lookupKeyRead(c->db, c->argv[1]);
758 if (zobj && checkType(c, zobj, OBJ_ZSET)) return;
759
760 /* Report elements one after the other, using a null bulk reply for
761 * missing elements. */
762 addReplyMultiBulkLen(c,c->argc-2);
763 for (j = 2; j < c->argc; j++) {
764 double score;
765 if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
766 addReply(c,shared.nullmultibulk);
767 } else {
768 /* Decode... */
769 double xy[2];
770 if (!decodeGeohash(score,xy)) {
771 addReply(c,shared.nullmultibulk);
772 continue;
773 }
774 addReplyMultiBulkLen(c,2);
775 addReplyHumanLongDouble(c,xy[0]);
776 addReplyHumanLongDouble(c,xy[1]);
777 }
778 }
779 }
780
781 /* GEODIST key ele1 ele2 [unit]
782 *
783 * Return the distance, in meters by default, otherwise accordig to "unit",
784 * between points ele1 and ele2. If one or more elements are missing NULL
785 * is returned. */
geodistCommand(client * c)786 void geodistCommand(client *c) {
787 double to_meter = 1;
788
789 /* Check if there is the unit to extract, otherwise assume meters. */
790 if (c->argc == 5) {
791 to_meter = extractUnitOrReply(c,c->argv[4]);
792 if (to_meter < 0) return;
793 } else if (c->argc > 5) {
794 addReply(c,shared.syntaxerr);
795 return;
796 }
797
798 /* Look up the requested zset */
799 robj *zobj = NULL;
800 if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.nullbulk))
801 == NULL || checkType(c, zobj, OBJ_ZSET)) return;
802
803 /* Get the scores. We need both otherwise NULL is returned. */
804 double score1, score2, xyxy[4];
805 if (zsetScore(zobj, c->argv[2]->ptr, &score1) == C_ERR ||
806 zsetScore(zobj, c->argv[3]->ptr, &score2) == C_ERR)
807 {
808 addReply(c,shared.nullbulk);
809 return;
810 }
811
812 /* Decode & compute the distance. */
813 if (!decodeGeohash(score1,xyxy) || !decodeGeohash(score2,xyxy+2))
814 addReply(c,shared.nullbulk);
815 else
816 addReplyDoubleDistance(c,
817 geohashGetDistance(xyxy[0],xyxy[1],xyxy[2],xyxy[3]) / to_meter);
818 }
819