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