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