xref: /f-stack/app/redis-5.0.5/src/geo.c (revision 572c4311)
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