summaryrefslogtreecommitdiff
path: root/src/geo.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/geo.c')
-rw-r--r--src/geo.c801
1 files changed, 801 insertions, 0 deletions
diff --git a/src/geo.c b/src/geo.c
new file mode 100644
index 0000000..8423931
--- /dev/null
+++ b/src/geo.c
@@ -0,0 +1,801 @@
+/*
+ * Copyright (c) 2014, Matt Stancliff <[email protected]>.
+ * Copyright (c) 2015-2016, Salvatore Sanfilippo <[email protected]>.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of Redis nor the names of its contributors may be used
+ * to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "geo.h"
+#include "geohash_helper.h"
+#include "debugmacro.h"
+
+/* Things exported from t_zset.c only for geo.c, since it is the only other
+ * part of Redis that requires close zset introspection. */
+unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range);
+int zslValueLteMax(double value, zrangespec *spec);
+
+/* ====================================================================
+ * This file implements the following commands:
+ *
+ * - geoadd - add coordinates for value to geoset
+ * - georadius - search radius by coordinates in geoset
+ * - georadiusbymember - search radius based on geoset member position
+ * ==================================================================== */
+
+/* ====================================================================
+ * geoArray implementation
+ * ==================================================================== */
+
+/* Create a new array of geoPoints. */
+geoArray *geoArrayCreate(void) {
+ geoArray *ga = zmalloc(sizeof(*ga));
+ /* It gets allocated on first geoArrayAppend() call. */
+ ga->array = NULL;
+ ga->buckets = 0;
+ ga->used = 0;
+ return ga;
+}
+
+/* Add a new entry and return its pointer so that the caller can populate
+ * it with data. */
+geoPoint *geoArrayAppend(geoArray *ga) {
+ if (ga->used == ga->buckets) {
+ ga->buckets = (ga->buckets == 0) ? 8 : ga->buckets*2;
+ ga->array = zrealloc(ga->array,sizeof(geoPoint)*ga->buckets);
+ }
+ geoPoint *gp = ga->array+ga->used;
+ ga->used++;
+ return gp;
+}
+
+/* Destroy a geoArray created with geoArrayCreate(). */
+void geoArrayFree(geoArray *ga) {
+ size_t i;
+ for (i = 0; i < ga->used; i++) sdsfree(ga->array[i].member);
+ zfree(ga->array);
+ zfree(ga);
+}
+
+/* ====================================================================
+ * Helpers
+ * ==================================================================== */
+int decodeGeohash(double bits, double *xy) {
+ GeoHashBits hash = { .bits = (uint64_t)bits, .step = GEO_STEP_MAX };
+ return geohashDecodeToLongLatWGS84(hash, xy);
+}
+
+/* Input Argument Helper */
+/* Take a pointer to the latitude arg then use the next arg for longitude.
+ * On parse error C_ERR is returned, otherwise C_OK. */
+int extractLongLatOrReply(client *c, robj **argv, double *xy) {
+ int i;
+ for (i = 0; i < 2; i++) {
+ if (getDoubleFromObjectOrReply(c, argv[i], xy + i, NULL) !=
+ C_OK) {
+ return C_ERR;
+ }
+ }
+ if (xy[0] < GEO_LONG_MIN || xy[0] > GEO_LONG_MAX ||
+ xy[1] < GEO_LAT_MIN || xy[1] > GEO_LAT_MAX) {
+ addReplySds(c, sdscatprintf(sdsempty(),
+ "-ERR invalid longitude,latitude pair %f,%f\r\n",xy[0],xy[1]));
+ return C_ERR;
+ }
+ return C_OK;
+}
+
+/* Input Argument Helper */
+/* Decode lat/long from a zset member's score.
+ * Returns C_OK on successful decoding, otherwise C_ERR is returned. */
+int longLatFromMember(robj *zobj, robj *member, double *xy) {
+ double score = 0;
+
+ if (zsetScore(zobj, member->ptr, &score) == C_ERR) return C_ERR;
+ if (!decodeGeohash(score, xy)) return C_ERR;
+ return C_OK;
+}
+
+/* Check that the unit argument matches one of the known units, and returns
+ * the conversion factor to meters (you need to divide meters by the conversion
+ * factor to convert to the right unit).
+ *
+ * If the unit is not valid, an error is reported to the client, and a value
+ * less than zero is returned. */
+double extractUnitOrReply(client *c, robj *unit) {
+ char *u = unit->ptr;
+
+ if (!strcmp(u, "m")) {
+ return 1;
+ } else if (!strcmp(u, "km")) {
+ return 1000;
+ } else if (!strcmp(u, "ft")) {
+ return 0.3048;
+ } else if (!strcmp(u, "mi")) {
+ return 1609.34;
+ } else {
+ addReplyError(c,
+ "unsupported unit provided. please use m, km, ft, mi");
+ return -1;
+ }
+}
+
+/* Input Argument Helper.
+ * Extract the dinstance from the specified two arguments starting at 'argv'
+ * that shouldbe in the form: <number> <unit> and return the dinstance in the
+ * specified unit on success. *conversino is populated with the coefficient
+ * to use in order to convert meters to the unit.
+ *
+ * On error a value less than zero is returned. */
+double extractDistanceOrReply(client *c, robj **argv,
+ double *conversion) {
+ double distance;
+ if (getDoubleFromObjectOrReply(c, argv[0], &distance,
+ "need numeric radius") != C_OK) {
+ return -1;
+ }
+
+ if (distance < 0) {
+ addReplyError(c,"radius cannot be negative");
+ return -1;
+ }
+
+ double to_meters = extractUnitOrReply(c,argv[1]);
+ if (to_meters < 0) {
+ return -1;
+ }
+
+ if (conversion) *conversion = to_meters;
+ return distance * to_meters;
+}
+
+/* The default addReplyDouble has too much accuracy. We use this
+ * for returning location distances. "5.2145 meters away" is nicer
+ * than "5.2144992818115 meters away." We provide 4 digits after the dot
+ * so that the returned value is decently accurate even when the unit is
+ * the kilometer. */
+void addReplyDoubleDistance(client *c, double d) {
+ char dbuf[128];
+ int dlen = snprintf(dbuf, sizeof(dbuf), "%.4f", d);
+ addReplyBulkCBuffer(c, dbuf, dlen);
+}
+
+/* Helper function for geoGetPointsInRange(): given a sorted set score
+ * representing a point, and another point (the center of our search) and
+ * a radius, appends this entry as a geoPoint into the specified geoArray
+ * only if the point is within the search area.
+ *
+ * returns C_OK if the point is included, or REIDS_ERR if it is outside. */
+int geoAppendIfWithinRadius(geoArray *ga, double lon, double lat, double radius, double score, sds member) {
+ double distance, xy[2];
+
+ if (!decodeGeohash(score,xy)) return C_ERR; /* Can't decode. */
+ /* Note that geohashGetDistanceIfInRadiusWGS84() takes arguments in
+ * reverse order: longitude first, latitude later. */
+ if (!geohashGetDistanceIfInRadiusWGS84(lon,lat, xy[0], xy[1],
+ radius, &distance))
+ {
+ return C_ERR;
+ }
+
+ /* Append the new element. */
+ geoPoint *gp = geoArrayAppend(ga);
+ gp->longitude = xy[0];
+ gp->latitude = xy[1];
+ gp->dist = distance;
+ gp->member = member;
+ gp->score = score;
+ return C_OK;
+}
+
+/* Query a Redis sorted set to extract all the elements between 'min' and
+ * 'max', appending them into the array of geoPoint structures 'gparray'.
+ * The command returns the number of elements added to the array.
+ *
+ * Elements which are farest than 'radius' from the specified 'x' and 'y'
+ * coordinates are not included.
+ *
+ * The ability of this function to append to an existing set of points is
+ * important for good performances because querying by radius is performed
+ * using multiple queries to the sorted set, that we later need to sort
+ * via qsort. Similarly we need to be able to reject points outside the search
+ * radius area ASAP in order to allocate and process more points than needed. */
+int geoGetPointsInRange(robj *zobj, double min, double max, double lon, double lat, double radius, geoArray *ga) {
+ /* minex 0 = include min in range; maxex 1 = exclude max in range */
+ /* That's: min <= val < max */
+ zrangespec range = { .min = min, .max = max, .minex = 0, .maxex = 1 };
+ size_t origincount = ga->used;
+ sds member;
+
+ if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
+ unsigned char *zl = zobj->ptr;
+ unsigned char *eptr, *sptr;
+ unsigned char *vstr = NULL;
+ unsigned int vlen = 0;
+ long long vlong = 0;
+ double score = 0;
+
+ if ((eptr = zzlFirstInRange(zl, &range)) == NULL) {
+ /* Nothing exists starting at our min. No results. */
+ return 0;
+ }
+
+ sptr = ziplistNext(zl, eptr);
+ while (eptr) {
+ score = zzlGetScore(sptr);
+
+ /* If we fell out of range, break. */
+ if (!zslValueLteMax(score, &range))
+ break;
+
+ /* We know the element exists. ziplistGet should always succeed */
+ ziplistGet(eptr, &vstr, &vlen, &vlong);
+ member = (vstr == NULL) ? sdsfromlonglong(vlong) :
+ sdsnewlen(vstr,vlen);
+ if (geoAppendIfWithinRadius(ga,lon,lat,radius,score,member)
+ == C_ERR) sdsfree(member);
+ zzlNext(zl, &eptr, &sptr);
+ }
+ } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
+ zset *zs = zobj->ptr;
+ zskiplist *zsl = zs->zsl;
+ zskiplistNode *ln;
+
+ if ((ln = zslFirstInRange(zsl, &range)) == NULL) {
+ /* Nothing exists starting at our min. No results. */
+ return 0;
+ }
+
+ while (ln) {
+ sds ele = ln->ele;
+ /* Abort when the node is no longer in range. */
+ if (!zslValueLteMax(ln->score, &range))
+ break;
+
+ ele = sdsdup(ele);
+ if (geoAppendIfWithinRadius(ga,lon,lat,radius,ln->score,ele)
+ == C_ERR) sdsfree(ele);
+ ln = ln->level[0].forward;
+ }
+ }
+ return ga->used - origincount;
+}
+
+/* Compute the sorted set scores min (inclusive), max (exclusive) we should
+ * query in order to retrieve all the elements inside the specified area
+ * 'hash'. The two scores are returned by reference in *min and *max. */
+void scoresOfGeoHashBox(GeoHashBits hash, GeoHashFix52Bits *min, GeoHashFix52Bits *max) {
+ /* We want to compute the sorted set scores that will include all the
+ * elements inside the specified Geohash 'hash', which has as many
+ * bits as specified by hash.step * 2.
+ *
+ * So if step is, for example, 3, and the hash value in binary
+ * is 101010, since our score is 52 bits we want every element which
+ * is in binary: 101010?????????????????????????????????????????????
+ * Where ? can be 0 or 1.
+ *
+ * To get the min score we just use the initial hash value left
+ * shifted enough to get the 52 bit value. Later we increment the
+ * 6 bit prefis (see the hash.bits++ statement), and get the new
+ * prefix: 101011, which we align again to 52 bits to get the maximum
+ * value (which is excluded from the search). So we get everything
+ * between the two following scores (represented in binary):
+ *
+ * 1010100000000000000000000000000000000000000000000000 (included)
+ * and
+ * 1010110000000000000000000000000000000000000000000000 (excluded).
+ */
+ *min = geohashAlign52Bits(hash);
+ hash.bits++;
+ *max = geohashAlign52Bits(hash);
+}
+
+/* Obtain all members between the min/max of this geohash bounding box.
+ * Populate a geoArray of GeoPoints by calling geoGetPointsInRange().
+ * Return the number of points added to the array. */
+int membersOfGeoHashBox(robj *zobj, GeoHashBits hash, geoArray *ga, double lon, double lat, double radius) {
+ GeoHashFix52Bits min, max;
+
+ scoresOfGeoHashBox(hash,&min,&max);
+ return geoGetPointsInRange(zobj, min, max, lon, lat, radius, ga);
+}
+
+/* Search all eight neighbors + self geohash box */
+int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) {
+ GeoHashBits neighbors[9];
+ unsigned int i, count = 0, last_processed = 0;
+ int debugmsg = 0;
+
+ neighbors[0] = n.hash;
+ neighbors[1] = n.neighbors.north;
+ neighbors[2] = n.neighbors.south;
+ neighbors[3] = n.neighbors.east;
+ neighbors[4] = n.neighbors.west;
+ neighbors[5] = n.neighbors.north_east;
+ neighbors[6] = n.neighbors.north_west;
+ neighbors[7] = n.neighbors.south_east;
+ neighbors[8] = n.neighbors.south_west;
+
+ /* For each neighbor (*and* our own hashbox), get all the matching
+ * members and add them to the potential result list. */
+ for (i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) {
+ if (HASHISZERO(neighbors[i])) {
+ if (debugmsg) D("neighbors[%d] is zero",i);
+ continue;
+ }
+
+ /* Debugging info. */
+ if (debugmsg) {
+ GeoHashRange long_range, lat_range;
+ geohashGetCoordRange(&long_range,&lat_range);
+ GeoHashArea myarea = {{0}};
+ geohashDecode(long_range, lat_range, neighbors[i], &myarea);
+
+ /* Dump center square. */
+ D("neighbors[%d]:\n",i);
+ D("area.longitude.min: %f\n", myarea.longitude.min);
+ D("area.longitude.max: %f\n", myarea.longitude.max);
+ D("area.latitude.min: %f\n", myarea.latitude.min);
+ D("area.latitude.max: %f\n", myarea.latitude.max);
+ D("\n");
+ }
+
+ /* When a huge Radius (in the 5000 km range or more) is used,
+ * adjacent neighbors can be the same, leading to duplicated
+ * elements. Skip every range which is the same as the one
+ * processed previously. */
+ if (last_processed &&
+ neighbors[i].bits == neighbors[last_processed].bits &&
+ neighbors[i].step == neighbors[last_processed].step)
+ {
+ if (debugmsg)
+ D("Skipping processing of %d, same as previous\n",i);
+ continue;
+ }
+ count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius);
+ last_processed = i;
+ }
+ return count;
+}
+
+/* Sort comparators for qsort() */
+static int sort_gp_asc(const void *a, const void *b) {
+ const struct geoPoint *gpa = a, *gpb = b;
+ /* We can't do adist - bdist because they are doubles and
+ * the comparator returns an int. */
+ if (gpa->dist > gpb->dist)
+ return 1;
+ else if (gpa->dist == gpb->dist)
+ return 0;
+ else
+ return -1;
+}
+
+static int sort_gp_desc(const void *a, const void *b) {
+ return -sort_gp_asc(a, b);
+}
+
+/* ====================================================================
+ * Commands
+ * ==================================================================== */
+
+/* GEOADD key long lat name [long2 lat2 name2 ... longN latN nameN] */
+void geoaddCommand(client *c) {
+ /* Check arguments number for sanity. */
+ if ((c->argc - 2) % 3 != 0) {
+ /* Need an odd number of arguments if we got this far... */
+ addReplyError(c, "syntax error. Try GEOADD key [x1] [y1] [name1] "
+ "[x2] [y2] [name2] ... ");
+ return;
+ }
+
+ int elements = (c->argc - 2) / 3;
+ int argc = 2+elements*2; /* ZADD key score ele ... */
+ robj **argv = zcalloc(argc*sizeof(robj*));
+ argv[0] = createRawStringObject("zadd",4);
+ argv[1] = c->argv[1]; /* key */
+ incrRefCount(argv[1]);
+
+ /* Create the argument vector to call ZADD in order to add all
+ * the score,value pairs to the requested zset, where score is actually
+ * an encoded version of lat,long. */
+ int i;
+ for (i = 0; i < elements; i++) {
+ double xy[2];
+
+ if (extractLongLatOrReply(c, (c->argv+2)+(i*3),xy) == C_ERR) {
+ for (i = 0; i < argc; i++)
+ if (argv[i]) decrRefCount(argv[i]);
+ zfree(argv);
+ return;
+ }
+
+ /* Turn the coordinates into the score of the element. */
+ GeoHashBits hash;
+ geohashEncodeWGS84(xy[0], xy[1], GEO_STEP_MAX, &hash);
+ GeoHashFix52Bits bits = geohashAlign52Bits(hash);
+ robj *score = createObject(OBJ_STRING, sdsfromlonglong(bits));
+ robj *val = c->argv[2 + i * 3 + 2];
+ argv[2+i*2] = score;
+ argv[3+i*2] = val;
+ incrRefCount(val);
+ }
+
+ /* Finally call ZADD that will do the work for us. */
+ replaceClientCommandVector(c,argc,argv);
+ zaddCommand(c);
+}
+
+#define SORT_NONE 0
+#define SORT_ASC 1
+#define SORT_DESC 2
+
+#define RADIUS_COORDS 1
+#define RADIUS_MEMBER 2
+
+/* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC]
+ * [COUNT count] [STORE key] [STOREDIST key]
+ * GEORADIUSBYMEMBER key member radius unit ... options ... */
+void georadiusGeneric(client *c, int type) {
+ robj *key = c->argv[1];
+ robj *storekey = NULL;
+ int storedist = 0; /* 0 for STORE, 1 for STOREDIST. */
+
+ /* Look up the requested zset */
+ robj *zobj = NULL;
+ if ((zobj = lookupKeyReadOrReply(c, key, shared.emptymultibulk)) == NULL ||
+ checkType(c, zobj, OBJ_ZSET)) {
+ return;
+ }
+
+ /* Find long/lat to use for radius search based on inquiry type */
+ int base_args;
+ double xy[2] = { 0 };
+ if (type == RADIUS_COORDS) {
+ base_args = 6;
+ if (extractLongLatOrReply(c, c->argv + 2, xy) == C_ERR)
+ return;
+ } else if (type == RADIUS_MEMBER) {
+ base_args = 5;
+ robj *member = c->argv[2];
+ if (longLatFromMember(zobj, member, xy) == C_ERR) {
+ addReplyError(c, "could not decode requested zset member");
+ return;
+ }
+ } else {
+ addReplyError(c, "unknown georadius search type");
+ return;
+ }
+
+ /* Extract radius and units from arguments */
+ double radius_meters = 0, conversion = 1;
+ if ((radius_meters = extractDistanceOrReply(c, c->argv + base_args - 2,
+ &conversion)) < 0) {
+ return;
+ }
+
+ /* Discover and populate all optional parameters. */
+ int withdist = 0, withhash = 0, withcoords = 0;
+ int sort = SORT_NONE;
+ long long count = 0;
+ if (c->argc > base_args) {
+ int remaining = c->argc - base_args;
+ for (int i = 0; i < remaining; i++) {
+ char *arg = c->argv[base_args + i]->ptr;
+ if (!strcasecmp(arg, "withdist")) {
+ withdist = 1;
+ } else if (!strcasecmp(arg, "withhash")) {
+ withhash = 1;
+ } else if (!strcasecmp(arg, "withcoord")) {
+ withcoords = 1;
+ } else if (!strcasecmp(arg, "asc")) {
+ sort = SORT_ASC;
+ } else if (!strcasecmp(arg, "desc")) {
+ sort = SORT_DESC;
+ } else if (!strcasecmp(arg, "count") && (i+1) < remaining) {
+ if (getLongLongFromObjectOrReply(c, c->argv[base_args+i+1],
+ &count, NULL) != C_OK) return;
+ if (count <= 0) {
+ addReplyError(c,"COUNT must be > 0");
+ return;
+ }
+ i++;
+ } else if (!strcasecmp(arg, "store") && (i+1) < remaining) {
+ storekey = c->argv[base_args+i+1];
+ storedist = 0;
+ i++;
+ } else if (!strcasecmp(arg, "storedist") && (i+1) < remaining) {
+ storekey = c->argv[base_args+i+1];
+ storedist = 1;
+ i++;
+ } else {
+ addReply(c, shared.syntaxerr);
+ return;
+ }
+ }
+ }
+
+ /* Trap options not compatible with STORE and STOREDIST. */
+ if (storekey && (withdist || withhash || withcoords)) {
+ addReplyError(c,
+ "STORE option in GEORADIUS is not compatible with "
+ "WITHDIST, WITHHASH and WITHCOORDS options");
+ return;
+ }
+
+ /* COUNT without ordering does not make much sense, force ASC
+ * ordering if COUNT was specified but no sorting was requested. */
+ if (count != 0 && sort == SORT_NONE) sort = SORT_ASC;
+
+ /* Get all neighbor geohash boxes for our radius search */
+ GeoHashRadius georadius =
+ geohashGetAreasByRadiusWGS84(xy[0], xy[1], radius_meters);
+
+ /* Search the zset for all matching points */
+ geoArray *ga = geoArrayCreate();
+ membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga);
+
+ /* If no matching results, the user gets an empty reply. */
+ if (ga->used == 0 && storekey == NULL) {
+ addReply(c, shared.emptymultibulk);
+ geoArrayFree(ga);
+ return;
+ }
+
+ long result_length = ga->used;
+ long returned_items = (count == 0 || result_length < count) ?
+ result_length : count;
+ long option_length = 0;
+
+ /* Process [optional] requested sorting */
+ if (sort == SORT_ASC) {
+ qsort(ga->array, result_length, sizeof(geoPoint), sort_gp_asc);
+ } else if (sort == SORT_DESC) {
+ qsort(ga->array, result_length, sizeof(geoPoint), sort_gp_desc);
+ }
+
+ if (storekey == NULL) {
+ /* No target key, return results to user. */
+
+ /* Our options are self-contained nested multibulk replies, so we
+ * only need to track how many of those nested replies we return. */
+ if (withdist)
+ option_length++;
+
+ if (withcoords)
+ option_length++;
+
+ if (withhash)
+ option_length++;
+
+ /* The multibulk len we send is exactly result_length. The result is
+ * either all strings of just zset members *or* a nested multi-bulk
+ * reply containing the zset member string _and_ all the additional
+ * options the user enabled for this request. */
+ addReplyMultiBulkLen(c, returned_items);
+
+ /* Finally send results back to the caller */
+ int i;
+ for (i = 0; i < returned_items; i++) {
+ geoPoint *gp = ga->array+i;
+ gp->dist /= conversion; /* Fix according to unit. */
+
+ /* If we have options in option_length, return each sub-result
+ * as a nested multi-bulk. Add 1 to account for result value
+ * itself. */
+ if (option_length)
+ addReplyMultiBulkLen(c, option_length + 1);
+
+ addReplyBulkSds(c,gp->member);
+ gp->member = NULL;
+
+ if (withdist)
+ addReplyDoubleDistance(c, gp->dist);
+
+ if (withhash)
+ addReplyLongLong(c, gp->score);
+
+ if (withcoords) {
+ addReplyMultiBulkLen(c, 2);
+ addReplyHumanLongDouble(c, gp->longitude);
+ addReplyHumanLongDouble(c, gp->latitude);
+ }
+ }
+ } else {
+ /* Target key, create a sorted set with the results. */
+ robj *zobj;
+ zset *zs;
+ int i;
+ size_t maxelelen = 0;
+
+ if (returned_items) {
+ zobj = createZsetObject();
+ zs = zobj->ptr;
+ }
+
+ for (i = 0; i < returned_items; i++) {
+ zskiplistNode *znode;
+ geoPoint *gp = ga->array+i;
+ gp->dist /= conversion; /* Fix according to unit. */
+ double score = storedist ? gp->dist : gp->score;
+ size_t elelen = sdslen(gp->member);
+
+ if (maxelelen < elelen) maxelelen = elelen;
+ znode = zslInsert(zs->zsl,score,gp->member);
+ serverAssert(dictAdd(zs->dict,gp->member,&znode->score) == DICT_OK);
+ gp->member = NULL;
+ }
+
+ if (returned_items) {
+ zsetConvertToZiplistIfNeeded(zobj,maxelelen);
+ setKey(c->db,storekey,zobj);
+ decrRefCount(zobj);
+ notifyKeyspaceEvent(NOTIFY_LIST,"georadiusstore",storekey,
+ c->db->id);
+ server.dirty += returned_items;
+ } else if (dbDelete(c->db,storekey)) {
+ signalModifiedKey(c->db,storekey);
+ notifyKeyspaceEvent(NOTIFY_GENERIC,"del",storekey,c->db->id);
+ server.dirty++;
+ }
+ addReplyLongLong(c, returned_items);
+ }
+ geoArrayFree(ga);
+}
+
+/* GEORADIUS wrapper function. */
+void georadiusCommand(client *c) {
+ georadiusGeneric(c, RADIUS_COORDS);
+}
+
+/* GEORADIUSBYMEMBER wrapper function. */
+void georadiusByMemberCommand(client *c) {
+ georadiusGeneric(c, RADIUS_MEMBER);
+}
+
+/* GEOHASH key ele1 ele2 ... eleN
+ *
+ * Returns an array with an 11 characters geohash representation of the
+ * position of the specified elements. */
+void geohashCommand(client *c) {
+ char *geoalphabet= "0123456789bcdefghjkmnpqrstuvwxyz";
+ int j;
+
+ /* Look up the requested zset */
+ robj *zobj = lookupKeyRead(c->db, c->argv[1]);
+ if (zobj && checkType(c, zobj, OBJ_ZSET)) return;
+
+ /* Geohash elements one after the other, using a null bulk reply for
+ * missing elements. */
+ addReplyMultiBulkLen(c,c->argc-2);
+ for (j = 2; j < c->argc; j++) {
+ double score;
+ if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
+ addReply(c,shared.nullbulk);
+ } else {
+ /* The internal format we use for geocoding is a bit different
+ * than the standard, since we use as initial latitude range
+ * -85,85, while the normal geohashing algorithm uses -90,90.
+ * So we have to decode our position and re-encode using the
+ * standard ranges in order to output a valid geohash string. */
+
+ /* Decode... */
+ double xy[2];
+ if (!decodeGeohash(score,xy)) {
+ addReply(c,shared.nullbulk);
+ continue;
+ }
+
+ /* Re-encode */
+ GeoHashRange r[2];
+ GeoHashBits hash;
+ r[0].min = -180;
+ r[0].max = 180;
+ r[1].min = -90;
+ r[1].max = 90;
+ geohashEncode(&r[0],&r[1],xy[0],xy[1],26,&hash);
+
+ char buf[12];
+ int i;
+ for (i = 0; i < 11; i++) {
+ int idx = (hash.bits >> (52-((i+1)*5))) & 0x1f;
+ buf[i] = geoalphabet[idx];
+ }
+ buf[11] = '\0';
+ addReplyBulkCBuffer(c,buf,11);
+ }
+ }
+}
+
+/* GEOPOS key ele1 ele2 ... eleN
+ *
+ * Returns an array of two-items arrays representing the x,y position of each
+ * element specified in the arguments. For missing elements NULL is returned. */
+void geoposCommand(client *c) {
+ int j;
+
+ /* Look up the requested zset */
+ robj *zobj = lookupKeyRead(c->db, c->argv[1]);
+ if (zobj && checkType(c, zobj, OBJ_ZSET)) return;
+
+ /* Report elements one after the other, using a null bulk reply for
+ * missing elements. */
+ addReplyMultiBulkLen(c,c->argc-2);
+ for (j = 2; j < c->argc; j++) {
+ double score;
+ if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) {
+ addReply(c,shared.nullmultibulk);
+ } else {
+ /* Decode... */
+ double xy[2];
+ if (!decodeGeohash(score,xy)) {
+ addReply(c,shared.nullmultibulk);
+ continue;
+ }
+ addReplyMultiBulkLen(c,2);
+ addReplyHumanLongDouble(c,xy[0]);
+ addReplyHumanLongDouble(c,xy[1]);
+ }
+ }
+}
+
+/* GEODIST key ele1 ele2 [unit]
+ *
+ * Return the distance, in meters by default, otherwise accordig to "unit",
+ * between points ele1 and ele2. If one or more elements are missing NULL
+ * is returned. */
+void geodistCommand(client *c) {
+ double to_meter = 1;
+
+ /* Check if there is the unit to extract, otherwise assume meters. */
+ if (c->argc == 5) {
+ to_meter = extractUnitOrReply(c,c->argv[4]);
+ if (to_meter < 0) return;
+ } else if (c->argc > 5) {
+ addReply(c,shared.syntaxerr);
+ return;
+ }
+
+ /* Look up the requested zset */
+ robj *zobj = NULL;
+ if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.nullbulk))
+ == NULL || checkType(c, zobj, OBJ_ZSET)) return;
+
+ /* Get the scores. We need both otherwise NULL is returned. */
+ double score1, score2, xyxy[4];
+ if (zsetScore(zobj, c->argv[2]->ptr, &score1) == C_ERR ||
+ zsetScore(zobj, c->argv[3]->ptr, &score2) == C_ERR)
+ {
+ addReply(c,shared.nullbulk);
+ return;
+ }
+
+ /* Decode & compute the distance. */
+ if (!decodeGeohash(score1,xyxy) || !decodeGeohash(score2,xyxy+2))
+ addReply(c,shared.nullbulk);
+ else
+ addReplyDoubleDistance(c,
+ geohashGetDistance(xyxy[0],xyxy[1],xyxy[2],xyxy[3]) / to_meter);
+}