diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 22:52:54 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-01-21 22:52:54 +0100 |
| commit | dcacc00e3750300617ba6e16eb346713f91a783a (patch) | |
| tree | 38e2d4fb5ed9d119711d4295c6eda4b014af73fd /examples/redis-unstable/src/geo.c | |
| parent | 58dac10aeb8f5a041c46bddbeaf4c7966a99b998 (diff) | |
| download | crep-dcacc00e3750300617ba6e16eb346713f91a783a.tar.gz | |
Remove testing data
Diffstat (limited to 'examples/redis-unstable/src/geo.c')
| -rw-r--r-- | examples/redis-unstable/src/geo.c | 1006 |
1 files changed, 0 insertions, 1006 deletions
diff --git a/examples/redis-unstable/src/geo.c b/examples/redis-unstable/src/geo.c deleted file mode 100644 index ce890f7..0000000 --- a/examples/redis-unstable/src/geo.c +++ /dev/null @@ -1,1006 +0,0 @@ -/* - * Copyright (c) 2014, Matt Stancliff <matt@genges.com>. - * Copyright (c) 2015-current, Redis Ltd. - * 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" -#include "pqsort.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 and populate with data a new entry to the geoArray. */ -geoPoint *geoArrayAppend(geoArray *ga, double *xy, double dist, - double score, char *member) -{ - 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; - gp->longitude = xy[0]; - gp->latitude = xy[1]; - gp->dist = dist; - gp->member = member; - gp->score = score; - 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) { - addReplyErrorFormat(c, - "-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 (!strcasecmp(u, "m")) { - return 1; - } else if (!strcasecmp(u, "km")) { - return 1000; - } else if (!strcasecmp(u, "ft")) { - return 0.3048; - } else if (!strcasecmp(u, "mi")) { - return 1609.34; - } else { - addReplyError(c, - "unsupported unit provided. please use M, KM, FT, MI"); - return -1; - } -} - -/* Input Argument Helper. - * Extract the distance from the specified two arguments starting at 'argv' - * that should be in the form: <number> <unit>, and return C_OK or C_ERR means success or failure - * *conversions is populated with the coefficient to use in order to convert meters to the unit.*/ -int extractDistanceOrReply(client *c, robj **argv, - double *conversion, double *radius) { - double distance; - if (getDoubleFromObjectOrReply(c, argv[0], &distance, - "need numeric radius") != C_OK) { - return C_ERR; - } - - if (distance < 0) { - addReplyError(c,"radius cannot be negative"); - return C_ERR; - } - if (radius) *radius = distance; - - double to_meters = extractUnitOrReply(c,argv[1]); - if (to_meters < 0) { - return C_ERR; - } - - if (conversion) *conversion = to_meters; - return C_OK; -} - -/* Input Argument Helper. - * Extract height and width from the specified three arguments starting at 'argv' - * that should be in the form: <number> <number> <unit>, and return C_OK or C_ERR means success or failure - * *conversions is populated with the coefficient to use in order to convert meters to the unit.*/ -int extractBoxOrReply(client *c, robj **argv, double *conversion, - double *width, double *height) { - double h, w; - if ((getDoubleFromObjectOrReply(c, argv[0], &w, "need numeric width") != C_OK) || - (getDoubleFromObjectOrReply(c, argv[1], &h, "need numeric height") != C_OK)) { - return C_ERR; - } - - if (h < 0 || w < 0) { - addReplyError(c, "height or width cannot be negative"); - return C_ERR; - } - if (height) *height = h; - if (width) *width = w; - - double to_meters = extractUnitOrReply(c,argv[2]); - if (to_meters < 0) { - return C_ERR; - } - - if (conversion) *conversion = to_meters; - return C_OK; -} - -/* 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]; - const int dlen = fixedpoint_d2string(dbuf, sizeof(dbuf), d, 4); - addReplyBulkCBuffer(c, dbuf, dlen); -} - -/* Helper function for geoGetPointsInRange(): given a sorted set score - * representing a point, and a GeoShape, checks if the point is within the search area. - * - * shape: the rectangle - * score: the encoded version of lat,long - * xy: output variable, the decoded lat,long - * distance: output variable, the distance between the center of the shape and the point - * - * Return values: - * - * The return value is C_OK if the point is within search area, or C_ERR if it is outside. - * "*xy" is populated with the decoded lat,long. - * "*distance" is populated with the distance between the center of the shape and the point. - */ -int geoWithinShape(GeoShape *shape, double score, double *xy, double *distance) { - if (!decodeGeohash(score,xy)) return C_ERR; /* Can't decode. */ - /* Note that geohashGetDistanceIfInRadiusWGS84() takes arguments in - * reverse order: longitude first, latitude later. */ - if (shape->type == CIRCULAR_TYPE) { - if (!geohashGetDistanceIfInRadiusWGS84(shape->xy[0], shape->xy[1], xy[0], xy[1], - shape->t.radius*shape->conversion, distance)) - return C_ERR; - } else if (shape->type == RECTANGLE_TYPE) { - if (!geohashGetDistanceIfInRectangle(shape->t.r.width * shape->conversion, - shape->t.r.height * shape->conversion, - shape->xy[0], shape->xy[1], xy[0], xy[1], distance)) - return C_ERR; - } - 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 'geoArray'. - * The command returns the number of elements added to the array. - * - * Elements which are farther 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, GeoShape *shape, geoArray *ga, unsigned long limit) { - /* 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; - if (zobj->encoding == OBJ_ENCODING_LISTPACK) { - 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 = lpNext(zl, eptr); - while (eptr) { - double xy[2]; - double distance = 0; - score = zzlGetScore(sptr); - - /* If we fell out of range, break. */ - if (!zslValueLteMax(score, &range)) - break; - - vstr = lpGetValue(eptr, &vlen, &vlong); - if (geoWithinShape(shape, score, xy, &distance) == C_OK) { - /* Append the new element. */ - char *member = (vstr == NULL) ? sdsfromlonglong(vlong) : sdsnewlen(vstr, vlen); - geoArrayAppend(ga, xy, distance, score, member); - } - if (ga->used && limit && ga->used >= limit) break; - zzlNext(zl, &eptr, &sptr); - } - } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) { - zset *zs = zobj->ptr; - zskiplist *zsl = zs->zsl; - zskiplistNode *ln; - - if ((ln = zslNthInRange(zsl, &range, 0, NULL)) == NULL) { - /* Nothing exists starting at our min. No results. */ - return 0; - } - - while (ln) { - double xy[2]; - double distance = 0; - /* Abort when the node is no longer in range. */ - if (!zslValueLteMax(ln->score, &range)) - break; - if (geoWithinShape(shape, ln->score, xy, &distance) == C_OK) { - /* Append the new element. */ - sds ele = zslGetNodeElement(ln); - geoArrayAppend(ga, xy, distance, ln->score, sdsdup(ele)); - } - if (ga->used && limit && ga->used >= limit) break; - 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 prefix (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, GeoShape *shape, unsigned long limit) { - GeoHashFix52Bits min, max; - - scoresOfGeoHashBox(hash,&min,&max); - return geoGetPointsInRange(zobj, min, max, shape, ga, limit); -} - -/* Search all eight neighbors + self geohash box */ -int membersOfAllNeighbors(robj *zobj, const GeoHashRadius *n, GeoShape *shape, geoArray *ga, unsigned long limit) { - 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; - } - if (ga->used && limit && ga->used >= limit) break; - count += membersOfGeoHashBox(zobj, neighbors[i], ga, shape, limit); - 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 [CH] [NX|XX] long lat name [long2 lat2 name2 ... longN latN nameN] */ -void geoaddCommand(client *c) { - int xx = 0, nx = 0, longidx = 2; - int i; - - /* Parse options. At the end 'longidx' is set to the argument position - * of the longitude of the first element. */ - while (longidx < c->argc) { - char *opt = c->argv[longidx]->ptr; - if (!strcasecmp(opt,"nx")) nx = 1; - else if (!strcasecmp(opt,"xx")) xx = 1; - else if (!strcasecmp(opt,"ch")) { /* Handle in zaddCommand. */ } - else break; - longidx++; - } - - if ((c->argc - longidx) % 3 || (xx && nx)) { - /* Need an odd number of arguments if we got this far... */ - addReplyErrorObject(c,shared.syntaxerr); - return; - } - - /* Set up the vector for calling ZADD. */ - int elements = (c->argc - longidx) / 3; - int argc = longidx+elements*2; /* ZADD key [CH] [NX|XX] score ele ... */ - robj **argv = zcalloc(argc*sizeof(robj*)); - argv[0] = createRawStringObject("zadd",4); - for (i = 1; i < longidx; i++) { - argv[i] = c->argv[i]; - incrRefCount(argv[i]); - } - - /* 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. */ - for (i = 0; i < elements; i++) { - double xy[2]; - - if (extractLongLatOrReply(c, (c->argv+longidx)+(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 = createStringObjectFromLongLongWithSds(bits); - robj *val = c->argv[longidx + i * 3 + 2]; - argv[longidx+i*2] = score; - argv[longidx+1+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<<0) /* Search around coordinates. */ -#define RADIUS_MEMBER (1<<1) /* Search around member. */ -#define RADIUS_NOSTORE (1<<2) /* Do not accept STORE/STOREDIST option. */ -#define GEOSEARCH (1<<3) /* GEOSEARCH command variant (different arguments supported) */ -#define GEOSEARCHSTORE (1<<4) /* GEOSEARCHSTORE just accept STOREDIST option */ - -/* GEORADIUS key x y radius unit [WITHDIST] [WITHHASH] [WITHCOORD] [ASC|DESC] - * [COUNT count [ANY]] [STORE key|STOREDIST key] - * GEORADIUSBYMEMBER key member radius unit ... options ... - * GEOSEARCH key [FROMMEMBER member] [FROMLONLAT long lat] [BYRADIUS radius unit] - * [BYBOX width height unit] [WITHCOORD] [WITHDIST] [WITHASH] [COUNT count [ANY]] [ASC|DESC] - * GEOSEARCHSTORE dest_key src_key [FROMMEMBER member] [FROMLONLAT long lat] [BYRADIUS radius unit] - * [BYBOX width height unit] [COUNT count [ANY]] [ASC|DESC] [STOREDIST] - * */ -void georadiusGeneric(client *c, int srcKeyIndex, int flags) { - robj *storekey = NULL; - int storedist = 0; /* 0 for STORE, 1 for STOREDIST. */ - - /* Look up the requested zset */ - kvobj *zobj = lookupKeyRead(c->db, c->argv[srcKeyIndex]); - if (checkType(c, zobj, OBJ_ZSET)) return; - - /* Find long/lat to use for radius or box search based on inquiry type */ - int base_args; - GeoShape shape = {0}; - if (flags & RADIUS_COORDS) { - /* GEORADIUS or GEORADIUS_RO */ - base_args = 6; - shape.type = CIRCULAR_TYPE; - if (extractLongLatOrReply(c, c->argv + 2, shape.xy) == C_ERR) return; - if (extractDistanceOrReply(c, c->argv+base_args-2, &shape.conversion, &shape.t.radius) != C_OK) return; - } else if ((flags & RADIUS_MEMBER) && !zobj) { - /* We don't have a source key, but we need to proceed with argument - * parsing, so we know which reply to use depending on the STORE flag. */ - base_args = 5; - } else if (flags & RADIUS_MEMBER) { - /* GEORADIUSBYMEMBER or GEORADIUSBYMEMBER_RO */ - base_args = 5; - shape.type = CIRCULAR_TYPE; - robj *member = c->argv[2]; - if (longLatFromMember(zobj, member, shape.xy) == C_ERR) { - addReplyError(c, "could not decode requested zset member"); - return; - } - if (extractDistanceOrReply(c, c->argv+base_args-2, &shape.conversion, &shape.t.radius) != C_OK) return; - } else if (flags & GEOSEARCH) { - /* GEOSEARCH or GEOSEARCHSTORE */ - base_args = 2; - if (flags & GEOSEARCHSTORE) { - base_args = 3; - storekey = c->argv[1]; - } - } else { - addReplyError(c, "Unknown georadius search type"); - return; - } - - /* Discover and populate all optional parameters. */ - int withdist = 0, withhash = 0, withcoords = 0; - int frommember = 0, fromloc = 0, byradius = 0, bybox = 0; - int sort = SORT_NONE; - int any = 0; /* any=1 means a limited search, stop as soon as enough results were found. */ - long long count = 0; /* Max number of results to return. 0 means unlimited. */ - 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, "any")) { - any = 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 && - !(flags & RADIUS_NOSTORE) && - !(flags & GEOSEARCH)) - { - storekey = c->argv[base_args+i+1]; - storedist = 0; - i++; - } else if (!strcasecmp(arg, "storedist") && - (i+1) < remaining && - !(flags & RADIUS_NOSTORE) && - !(flags & GEOSEARCH)) - { - storekey = c->argv[base_args+i+1]; - storedist = 1; - i++; - } else if (!strcasecmp(arg, "storedist") && - (flags & GEOSEARCH) && - (flags & GEOSEARCHSTORE)) - { - storedist = 1; - } else if (!strcasecmp(arg, "frommember") && - (i+1) < remaining && - flags & GEOSEARCH && - !fromloc) - { - /* No source key, proceed with argument parsing and return an error when done. */ - if (zobj == NULL) { - frommember = 1; - i++; - continue; - } - - if (longLatFromMember(zobj, c->argv[base_args+i+1], shape.xy) == C_ERR) { - addReplyError(c, "could not decode requested zset member"); - return; - } - frommember = 1; - i++; - } else if (!strcasecmp(arg, "fromlonlat") && - (i+2) < remaining && - flags & GEOSEARCH && - !frommember) - { - if (extractLongLatOrReply(c, c->argv+base_args+i+1, shape.xy) == C_ERR) return; - fromloc = 1; - i += 2; - } else if (!strcasecmp(arg, "byradius") && - (i+2) < remaining && - flags & GEOSEARCH && - !bybox) - { - if (extractDistanceOrReply(c, c->argv+base_args+i+1, &shape.conversion, &shape.t.radius) != C_OK) - return; - shape.type = CIRCULAR_TYPE; - byradius = 1; - i += 2; - } else if (!strcasecmp(arg, "bybox") && - (i+3) < remaining && - flags & GEOSEARCH && - !byradius) - { - if (extractBoxOrReply(c, c->argv+base_args+i+1, &shape.conversion, &shape.t.r.width, - &shape.t.r.height) != C_OK) return; - shape.type = RECTANGLE_TYPE; - bybox = 1; - i += 3; - } else { - addReplyErrorObject(c,shared.syntaxerr); - return; - } - } - } - - /* Trap options not compatible with STORE and STOREDIST. */ - if (storekey && (withdist || withhash || withcoords)) { - addReplyErrorFormat(c, - "%s is not compatible with WITHDIST, WITHHASH and WITHCOORD options", - flags & GEOSEARCHSTORE? "GEOSEARCHSTORE": "STORE option in GEORADIUS"); - return; - } - - if ((flags & GEOSEARCH) && !(frommember || fromloc)) { - addReplyErrorFormat(c, - "exactly one of FROMMEMBER or FROMLONLAT can be specified for %s", - (char *)c->argv[0]->ptr); - return; - } - - if ((flags & GEOSEARCH) && !(byradius || bybox)) { - addReplyErrorFormat(c, - "exactly one of BYRADIUS and BYBOX can be specified for %s", - (char *)c->argv[0]->ptr); - return; - } - - if (any && !count) { - addReplyError(c, "the ANY argument requires COUNT argument"); - return; - } - - /* Return ASAP when src key does not exist. */ - if (zobj == NULL) { - if (storekey) { - /* store key is not NULL, try to delete it and return 0. */ - if (dbDelete(c->db, storekey)) { - keyModified(c, c->db, storekey, NULL, 1); - notifyKeyspaceEvent(NOTIFY_GENERIC, "del", storekey, c->db->id); - server.dirty++; - } - addReply(c, shared.czero); - } else { - /* Otherwise we return an empty array. */ - addReply(c, shared.emptyarray); - } - return; - } - - /* COUNT without ordering does not make much sense (we need to - * sort in order to return the closest N entries), - * force ASC ordering if COUNT was specified but no sorting was - * requested. Note that this is not needed for ANY option. */ - if (count != 0 && sort == SORT_NONE && !any) sort = SORT_ASC; - - /* Get all neighbor geohash boxes for our radius search */ - GeoHashRadius georadius = geohashCalculateAreasByShapeWGS84(&shape); - - /* Search the zset for all matching points */ - geoArray *ga = geoArrayCreate(); - membersOfAllNeighbors(zobj, &georadius, &shape, ga, any ? count : 0); - - /* If no matching results, the user gets an empty reply. */ - if (ga->used == 0 && storekey == NULL) { - addReply(c,shared.emptyarray); - 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_NONE) { - int (*sort_gp_callback)(const void *a, const void *b) = NULL; - if (sort == SORT_ASC) { - sort_gp_callback = sort_gp_asc; - } else if (sort == SORT_DESC) { - sort_gp_callback = sort_gp_desc; - } - - if (returned_items == result_length) { - qsort(ga->array, result_length, sizeof(geoPoint), sort_gp_callback); - } else { - pqsort(ga->array, result_length, sizeof(geoPoint), sort_gp_callback, - 0, (returned_items - 1)); - } - } - - 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 array 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. */ - addReplyArrayLen(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 /= shape.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) - addReplyArrayLen(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) { - addReplyArrayLen(c, 2); - addReplyDouble(c,gp->longitude); - addReplyDouble(c,gp->latitude); - } - } - } else { - /* Target key, create a sorted set with the results. */ - robj *zobj; - zset *zs; - int i; - size_t maxelelen = 0, totelelen = 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 /= shape.conversion; /* Fix according to unit. */ - double score = storedist ? gp->dist : gp->score; - size_t elelen = sdslen(gp->member); - - if (maxelelen < elelen) maxelelen = elelen; - totelelen += elelen; - znode = zslInsert(zs->zsl,score,gp->member); - serverAssert(dictAdd(zs->dict, znode, NULL) == DICT_OK); - sdsfree(gp->member); /* zslInsert copies the sds, so free the original */ - gp->member = NULL; - } - - if (returned_items) { - zsetConvertToListpackIfNeeded(zobj,maxelelen,totelelen); - setKey(c,c->db,storekey,&zobj,0); - notifyKeyspaceEvent(NOTIFY_ZSET,flags & GEOSEARCH ? "geosearchstore" : "georadiusstore",storekey, - c->db->id); - server.dirty += returned_items; - } else if (dbDelete(c->db,storekey)) { - keyModified(c,c->db,storekey,NULL,1); - 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, 1, RADIUS_COORDS); -} - -/* GEORADIUSBYMEMBER wrapper function. */ -void georadiusbymemberCommand(client *c) { - georadiusGeneric(c, 1, RADIUS_MEMBER); -} - -/* GEORADIUS_RO wrapper function. */ -void georadiusroCommand(client *c) { - georadiusGeneric(c, 1, RADIUS_COORDS|RADIUS_NOSTORE); -} - -/* GEORADIUSBYMEMBER_RO wrapper function. */ -void georadiusbymemberroCommand(client *c) { - georadiusGeneric(c, 1, RADIUS_MEMBER|RADIUS_NOSTORE); -} - -void geosearchCommand(client *c) { - georadiusGeneric(c, 1, GEOSEARCH); -} - -void geosearchstoreCommand(client *c) { - georadiusGeneric(c, 2, GEOSEARCH|GEOSEARCHSTORE); -} - -/* 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 */ - kvobj *zobj = lookupKeyRead(c->db, c->argv[1]); - if (checkType(c, zobj, OBJ_ZSET)) return; - - /* Geohash elements one after the other, using a null bulk reply for - * missing elements. */ - addReplyArrayLen(c,c->argc-2); - for (j = 2; j < c->argc; j++) { - double score; - if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) { - addReplyNull(c); - } 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)) { - addReplyNull(c); - 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; - if (i == 10) { - /* We have just 52 bits, but the API used to output - * an 11 bytes geohash. For compatibility we assume - * zero. */ - idx = 0; - } else { - 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 (checkType(c, zobj, OBJ_ZSET)) return; - - /* Report elements one after the other, using a null bulk reply for - * missing elements. */ - addReplyArrayLen(c,c->argc-2); - for (j = 2; j < c->argc; j++) { - double score; - if (!zobj || zsetScore(zobj, c->argv[j]->ptr, &score) == C_ERR) { - addReplyNullArray(c); - } else { - /* Decode... */ - double xy[2]; - if (!decodeGeohash(score,xy)) { - addReplyNullArray(c); - continue; - } - addReplyArrayLen(c,2); - addReplyDouble(c,xy[0]); - addReplyDouble(c,xy[1]); - } - } -} - -/* GEODIST key ele1 ele2 [unit] - * - * Return the distance, in meters by default, otherwise according 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) { - addReplyErrorObject(c,shared.syntaxerr); - return; - } - - /* Look up the requested zset */ - kvobj *zobj = NULL; - if ((zobj = lookupKeyReadOrReply(c, c->argv[1], shared.null[c->resp])) - == 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) - { - addReplyNull(c); - return; - } - - /* Decode & compute the distance. */ - if (!decodeGeohash(score1,xyxy) || !decodeGeohash(score2,xyxy+2)) - addReplyNull(c); - else - addReplyDoubleDistance(c, - geohashGetDistance(xyxy[0],xyxy[1],xyxy[2],xyxy[3]) / to_meter); -} |
