2014-05-12 14:38:17 -04:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2014, Matt Stancliff <matt@genges.com>.
|
|
|
|
* 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 "zset.h"
|
|
|
|
|
|
|
|
/* ====================================================================
|
|
|
|
* Redis Add-on Module: geo
|
|
|
|
* Provides commands: geoadd, georadius, georadiusbymember,
|
|
|
|
* geoencode, geodecode
|
|
|
|
* Behaviors:
|
|
|
|
* - geoadd - add coordinates for value to geoset
|
|
|
|
* - georadius - search radius by coordinates in geoset
|
|
|
|
* - georadiusbymember - search radius based on geoset member position
|
|
|
|
* - geoencode - encode coordinates to a geohash integer
|
|
|
|
* - geodecode - decode geohash integer to representative coordinates
|
|
|
|
* ==================================================================== */
|
|
|
|
|
|
|
|
/* ====================================================================
|
|
|
|
* Helpers
|
|
|
|
* ==================================================================== */
|
2015-06-22 05:24:58 -04:00
|
|
|
static inline int decodeGeohash(double bits, double *latlong) {
|
2014-05-12 14:38:17 -04:00
|
|
|
GeoHashBits hash = { .bits = (uint64_t)bits, .step = GEO_STEP_MAX };
|
|
|
|
return geohashDecodeToLatLongWGS84(hash, latlong);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Input Argument Helper */
|
|
|
|
/* Take a pointer to the latitude arg then use the next arg for longitude */
|
2015-06-22 05:24:58 -04:00
|
|
|
static inline int extractLatLongOrReply(redisClient *c, robj **argv,
|
2014-05-12 14:38:17 -04:00
|
|
|
double *latlong) {
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
|
|
if (getDoubleFromObjectOrReply(c, argv[i], latlong + i, NULL) !=
|
|
|
|
REDIS_OK) {
|
2015-06-22 05:24:58 -04:00
|
|
|
return 0;
|
2014-05-12 14:38:17 -04:00
|
|
|
}
|
|
|
|
}
|
2015-06-22 05:24:58 -04:00
|
|
|
return 1;
|
2014-05-12 14:38:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Input Argument Helper */
|
|
|
|
/* Decode lat/long from a zset member's score */
|
2015-06-22 05:24:58 -04:00
|
|
|
static int latLongFromMember(robj *zobj, robj *member, double *latlong) {
|
2014-05-12 14:38:17 -04:00
|
|
|
double score = 0;
|
|
|
|
|
|
|
|
if (!zsetScore(zobj, member, &score))
|
2015-06-22 05:24:58 -04:00
|
|
|
return 0;
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
if (!decodeGeohash(score, latlong))
|
2015-06-22 05:24:58 -04:00
|
|
|
return 0;
|
2014-05-12 14:38:17 -04:00
|
|
|
|
2015-06-22 05:24:58 -04:00
|
|
|
return 1;
|
2014-05-12 14:38:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/* Input Argument Helper */
|
|
|
|
static double extractDistanceOrReply(redisClient *c, robj **argv,
|
|
|
|
double *conversion) {
|
|
|
|
double distance;
|
|
|
|
if (getDoubleFromObjectOrReply(c, argv[0], &distance,
|
|
|
|
"need numeric radius") != REDIS_OK) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
double to_meters;
|
|
|
|
sds units = argv[1]->ptr;
|
|
|
|
if (!strcmp(units, "m") || !strncmp(units, "meter", 5)) {
|
|
|
|
to_meters = 1;
|
|
|
|
} else if (!strcmp(units, "ft") || !strncmp(units, "feet", 4)) {
|
|
|
|
to_meters = 0.3048;
|
|
|
|
} else if (!strcmp(units, "mi") || !strncmp(units, "mile", 4)) {
|
|
|
|
to_meters = 1609.34;
|
|
|
|
} else if (!strcmp(units, "km") || !strncmp(units, "kilometer", 9)) {
|
|
|
|
to_meters = 1000;
|
|
|
|
} else {
|
|
|
|
addReplyError(c, "unsupported unit provided. please use meters (m), "
|
|
|
|
"kilometers (km), miles (mi), or feet (ft)");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (conversion)
|
|
|
|
*conversion = to_meters;
|
|
|
|
|
|
|
|
return distance * to_meters;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* The defailt addReplyDouble has too much accuracy. We use this
|
2015-06-22 07:08:46 -04:00
|
|
|
* 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. */
|
|
|
|
static inline void addReplyDoubleDistance(redisClient *c, double d) {
|
|
|
|
char dbuf[128];
|
|
|
|
int dlen = snprintf(dbuf, sizeof(dbuf), "%.4f", d);
|
2014-05-12 14:38:17 -04:00
|
|
|
addReplyBulkCBuffer(c, dbuf, dlen);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* geohash range+zset access helper */
|
|
|
|
/* Obtain all members between the min/max of this geohash bounding box. */
|
|
|
|
/* Returns list of results. List must be listRelease()'d later. */
|
|
|
|
static list *membersOfGeoHashBox(robj *zobj, GeoHashBits hash) {
|
|
|
|
GeoHashFix52Bits min, max;
|
|
|
|
|
|
|
|
min = geohashAlign52Bits(hash);
|
|
|
|
hash.bits++;
|
|
|
|
max = geohashAlign52Bits(hash);
|
|
|
|
|
|
|
|
return geozrangebyscore(zobj, min, max, -1); /* -1 = no limit */
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Search all eight neighbors + self geohash box */
|
|
|
|
static list *membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double x,
|
|
|
|
double y, double radius) {
|
|
|
|
list *l = NULL;
|
|
|
|
GeoHashBits neighbors[9];
|
2015-06-22 05:24:58 -04:00
|
|
|
unsigned int i;
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
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. */
|
2015-06-22 05:24:58 -04:00
|
|
|
for (i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) {
|
2014-05-12 14:38:17 -04:00
|
|
|
list *r;
|
|
|
|
|
|
|
|
if (HASHISZERO(neighbors[i]))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
r = membersOfGeoHashBox(zobj, neighbors[i]);
|
|
|
|
if (!r)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (!l) {
|
|
|
|
l = r;
|
|
|
|
} else {
|
|
|
|
listJoin(l, r);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* if no results across any neighbors (*and* ourself, which is unlikely),
|
|
|
|
* then just give up. */
|
|
|
|
if (!l)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
/* Iterate over all matching results in the combined 9-grid search area */
|
|
|
|
/* Remove any results outside of our search radius. */
|
|
|
|
listIter li;
|
|
|
|
listNode *ln;
|
|
|
|
listRewind(l, &li);
|
|
|
|
while ((ln = listNext(&li))) {
|
|
|
|
struct zipresult *zr = listNodeValue(ln);
|
2015-06-22 05:24:58 -04:00
|
|
|
GeoHashArea area = {{0,0},{0,0},{0,0}};
|
2014-05-12 14:38:17 -04:00
|
|
|
GeoHashBits hash = { .bits = (uint64_t)zr->score,
|
|
|
|
.step = GEO_STEP_MAX };
|
|
|
|
|
|
|
|
if (!geohashDecodeWGS84(hash, &area)) {
|
|
|
|
/* Perhaps we should delete this node if the decode fails? */
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
double neighbor_y = (area.latitude.min + area.latitude.max) / 2;
|
|
|
|
double neighbor_x = (area.longitude.min + area.longitude.max) / 2;
|
|
|
|
|
|
|
|
double distance;
|
|
|
|
if (!geohashGetDistanceIfInRadiusWGS84(x, y, neighbor_x, neighbor_y,
|
|
|
|
radius, &distance)) {
|
|
|
|
/* If result is in the grid, but not in our radius, remove it. */
|
|
|
|
listDelNode(l, ln);
|
|
|
|
#ifdef DEBUG
|
|
|
|
fprintf(stderr, "No match for neighbor (%f, %f) within (%f, %f) at "
|
|
|
|
"distance %f\n",
|
|
|
|
neighbor_y, neighbor_x, y, x, distance);
|
|
|
|
#endif
|
|
|
|
} else {
|
|
|
|
/* Else: bueno. */
|
|
|
|
#ifdef DEBUG
|
|
|
|
fprintf(
|
|
|
|
stderr,
|
|
|
|
"Matched neighbor (%f, %f) within (%f, %f) at distance %f\n",
|
|
|
|
neighbor_y, neighbor_x, y, x, distance);
|
|
|
|
#endif
|
|
|
|
zr->distance = distance;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* We found results, but rejected all of them as out of range. Clean up. */
|
|
|
|
if (!listLength(l)) {
|
|
|
|
listRelease(l);
|
|
|
|
l = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Success! */
|
|
|
|
return l;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Sort comparators for qsort() */
|
|
|
|
static int sort_gp_asc(const void *a, const void *b) {
|
2015-06-22 05:53:14 -04:00
|
|
|
const struct geoPoint *gpa = a, *gpb = b;
|
2014-05-12 14:38:17 -04:00
|
|
|
/* 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
|
|
|
|
* ==================================================================== */
|
|
|
|
void geoAddCommand(redisClient *c) {
|
|
|
|
/* args 0-4: [cmd, key, lat, lng, val]; optional 5-6: [radius, units]
|
|
|
|
* - OR -
|
|
|
|
* args 0-N: [cmd, key, lat, lng, val, lat2, lng2, val2, ...] */
|
|
|
|
robj *cmd = c->argv[0];
|
|
|
|
robj *key = c->argv[1];
|
|
|
|
|
|
|
|
/* Prepare for the three different forms of the add command. */
|
|
|
|
double radius_meters = 0;
|
|
|
|
if (c->argc == 7) {
|
|
|
|
if ((radius_meters = extractDistanceOrReply(c, c->argv + 5, NULL)) <
|
|
|
|
0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else if (c->argc == 6) {
|
|
|
|
addReplyError(c, "must provide units when asking for radius encode");
|
|
|
|
return;
|
|
|
|
} else if ((c->argc - 2) % 3 != 0) {
|
|
|
|
/* Need an odd number of arguments if we got this far... */
|
|
|
|
addReplyError(c, "format is: geoadd [key] [lat1] [long1] [member1] "
|
|
|
|
"[lat2] [long2] [member2] ... ");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
redisClient *client = c;
|
|
|
|
int elements = (c->argc - 2) / 3;
|
|
|
|
/* elements will always be correct size (integer math floors for us if we
|
|
|
|
* have 6 or 7 total arguments) */
|
|
|
|
if (elements > 1) {
|
|
|
|
/* We should probably use a static client and not create/free it
|
|
|
|
* for every multi-add */
|
|
|
|
client = createClient(-1); /* fake client for multi-zadd */
|
|
|
|
|
|
|
|
/* Tell fake client to use the same DB as our calling client. */
|
|
|
|
selectDb(client, c->db->id);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Capture all lat/long components up front so if we encounter an error we
|
|
|
|
* return before making any changes to the database. */
|
|
|
|
double latlong[elements * 2];
|
|
|
|
for (int i = 0; i < elements; i++) {
|
|
|
|
if (!extractLatLongOrReply(c, (c->argv + 2) + (i * 3),
|
|
|
|
latlong + (i * 2)))
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Add all (lat, long, value) triples to the requested zset */
|
|
|
|
for (int i = 0; i < elements; i++) {
|
|
|
|
uint8_t step = geohashEstimateStepsByRadius(radius_meters);
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
printf("Adding with step size: %d\n", step);
|
|
|
|
#endif
|
|
|
|
GeoHashBits hash;
|
|
|
|
int ll_offset = i * 2;
|
|
|
|
double latitude = latlong[ll_offset];
|
|
|
|
double longitude = latlong[ll_offset + 1];
|
|
|
|
geohashEncodeWGS84(latitude, longitude, step, &hash);
|
|
|
|
|
|
|
|
GeoHashFix52Bits bits = geohashAlign52Bits(hash);
|
|
|
|
robj *score = createObject(REDIS_STRING, sdsfromlonglong(bits));
|
|
|
|
robj *val = c->argv[2 + i * 3 + 2];
|
|
|
|
/* (base args) + (offset for this triple) + (offset of value arg) */
|
|
|
|
|
|
|
|
rewriteClientCommandVector(client, 4, cmd, key, score, val);
|
|
|
|
decrRefCount(score);
|
|
|
|
zaddCommand(client);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* If we used a fake client, return a real reply then free fake client. */
|
|
|
|
if (client != c) {
|
|
|
|
addReplyLongLong(c, elements);
|
|
|
|
freeClient(client);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#define SORT_NONE 0
|
|
|
|
#define SORT_ASC 1
|
|
|
|
#define SORT_DESC 2
|
|
|
|
|
|
|
|
#define RADIUS_COORDS 1
|
|
|
|
#define RADIUS_MEMBER 2
|
|
|
|
|
|
|
|
static void geoRadiusGeneric(redisClient *c, int type) {
|
|
|
|
/* type == cords: [cmd, key, lat, long, radius, units, [optionals]]
|
|
|
|
* type == member: [cmd, key, member, radius, units, [optionals]] */
|
|
|
|
robj *key = c->argv[1];
|
|
|
|
|
|
|
|
/* Look up the requested zset */
|
|
|
|
robj *zobj = NULL;
|
|
|
|
if ((zobj = lookupKeyReadOrReply(c, key, shared.emptymultibulk)) == NULL ||
|
|
|
|
checkType(c, zobj, REDIS_ZSET)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Find lat/long to use for radius search based on inquiry type */
|
|
|
|
int base_args;
|
|
|
|
double latlong[2] = { 0 };
|
|
|
|
if (type == RADIUS_COORDS) {
|
|
|
|
base_args = 6;
|
|
|
|
if (!extractLatLongOrReply(c, c->argv + 2, latlong))
|
|
|
|
return;
|
|
|
|
} else if (type == RADIUS_MEMBER) {
|
|
|
|
base_args = 5;
|
|
|
|
robj *member = c->argv[2];
|
|
|
|
if (!latLongFromMember(zobj, member, latlong)) {
|
|
|
|
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. */
|
2015-06-22 05:53:14 -04:00
|
|
|
int withdist = 0, withhash = 0, withcoords = 0, noproperties = 0;
|
2014-05-12 14:38:17 -04:00
|
|
|
int sort = SORT_NONE;
|
|
|
|
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 (!strncasecmp(arg, "withdist", 8))
|
2015-06-22 05:24:58 -04:00
|
|
|
withdist = 1;
|
2014-05-12 14:38:17 -04:00
|
|
|
else if (!strcasecmp(arg, "withhash"))
|
2015-06-22 05:24:58 -04:00
|
|
|
withhash = 1;
|
2014-05-12 14:38:17 -04:00
|
|
|
else if (!strncasecmp(arg, "withcoord", 9))
|
2015-06-22 05:24:58 -04:00
|
|
|
withcoords = 1;
|
2014-05-12 14:38:17 -04:00
|
|
|
else if (!strncasecmp(arg, "noprop", 6) ||
|
|
|
|
!strncasecmp(arg, "withoutprop", 11))
|
2015-06-22 05:24:58 -04:00
|
|
|
noproperties = 1;
|
2014-05-12 14:38:17 -04:00
|
|
|
else if (!strncasecmp(arg, "asc", 3) ||
|
|
|
|
!strncasecmp(arg, "sort", 4))
|
|
|
|
sort = SORT_ASC;
|
|
|
|
else if (!strncasecmp(arg, "desc", 4))
|
|
|
|
sort = SORT_DESC;
|
|
|
|
else {
|
|
|
|
addReply(c, shared.syntaxerr);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Get all neighbor geohash boxes for our radius search */
|
|
|
|
GeoHashRadius georadius =
|
|
|
|
geohashGetAreasByRadiusWGS84(latlong[0], latlong[1], radius_meters);
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
printf("Searching with step size: %d\n", georadius.hash.step);
|
|
|
|
#endif
|
|
|
|
/* {Lat, Long} = {y, x} */
|
|
|
|
double y = latlong[0];
|
|
|
|
double x = latlong[1];
|
|
|
|
|
|
|
|
/* Search the zset for all matching points */
|
|
|
|
list *found_matches =
|
|
|
|
membersOfAllNeighbors(zobj, georadius, x, y, radius_meters);
|
|
|
|
|
|
|
|
/* If no matching results, the user gets an empty reply. */
|
|
|
|
if (!found_matches) {
|
|
|
|
addReply(c, shared.emptymultibulk);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
long result_length = listLength(found_matches);
|
|
|
|
long option_length = 0;
|
|
|
|
|
|
|
|
/* 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. */
|
2015-06-22 05:53:14 -04:00
|
|
|
addReplyMultiBulkLen(c, result_length);
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
/* Iterate over results, populate struct used for sorting and result sending
|
|
|
|
*/
|
|
|
|
listIter li;
|
|
|
|
listRewind(found_matches, &li);
|
2015-06-22 05:53:14 -04:00
|
|
|
struct geoPoint gp[result_length];
|
2014-05-12 14:38:17 -04:00
|
|
|
/* populate gp array from our results */
|
|
|
|
for (int i = 0; i < result_length; i++) {
|
|
|
|
struct zipresult *zr = listNodeValue(listNext(&li));
|
|
|
|
|
|
|
|
gp[i].member = NULL;
|
|
|
|
gp[i].set = key->ptr;
|
|
|
|
gp[i].dist = zr->distance / conversion;
|
|
|
|
gp[i].userdata = zr;
|
|
|
|
|
2015-06-22 05:53:14 -04:00
|
|
|
/* The layout of geoPoint allows us to pass the start offset
|
2014-05-12 14:38:17 -04:00
|
|
|
* of the struct directly to decodeGeohash. */
|
|
|
|
decodeGeohash(zr->score, (double *)(gp + i));
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Process [optional] requested sorting */
|
|
|
|
if (sort == SORT_ASC) {
|
|
|
|
qsort(gp, result_length, sizeof(*gp), sort_gp_asc);
|
|
|
|
} else if (sort == SORT_DESC) {
|
|
|
|
qsort(gp, result_length, sizeof(*gp), sort_gp_desc);
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Finally send results back to the caller */
|
|
|
|
for (int i = 0; i < result_length; i++) {
|
|
|
|
struct zipresult *zr = gp[i].userdata;
|
|
|
|
|
|
|
|
/* 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);
|
|
|
|
|
|
|
|
switch (zr->type) {
|
|
|
|
case ZR_LONG:
|
|
|
|
addReplyBulkLongLong(c, zr->val.v);
|
|
|
|
break;
|
|
|
|
case ZR_STRING:
|
|
|
|
addReplyBulkCBuffer(c, zr->val.s, sdslen(zr->val.s));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (withdist)
|
2015-06-22 07:08:46 -04:00
|
|
|
addReplyDoubleDistance(c, gp[i].dist);
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
if (withhash)
|
|
|
|
addReplyLongLong(c, zr->score);
|
|
|
|
|
|
|
|
if (withcoords) {
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, gp[i].latitude);
|
|
|
|
addReplyDouble(c, gp[i].longitude);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
listRelease(found_matches);
|
|
|
|
}
|
|
|
|
|
|
|
|
void geoRadiusCommand(redisClient *c) {
|
|
|
|
/* args 0-5: ["georadius", key, lat, long, radius, units];
|
|
|
|
* optionals: [withdist, withcoords, asc|desc] */
|
|
|
|
geoRadiusGeneric(c, RADIUS_COORDS);
|
|
|
|
}
|
|
|
|
|
|
|
|
void geoRadiusByMemberCommand(redisClient *c) {
|
|
|
|
/* args 0-4: ["georadius", key, compare-against-member, radius, units];
|
|
|
|
* optionals: [withdist, withcoords, asc|desc] */
|
|
|
|
geoRadiusGeneric(c, RADIUS_MEMBER);
|
|
|
|
}
|
|
|
|
|
|
|
|
void geoDecodeCommand(redisClient *c) {
|
|
|
|
GeoHashBits geohash;
|
|
|
|
if (getLongLongFromObjectOrReply(c, c->argv[1], (long long *)&geohash.bits,
|
|
|
|
NULL) != REDIS_OK)
|
|
|
|
return;
|
|
|
|
|
|
|
|
GeoHashArea area;
|
|
|
|
geohash.step = GEO_STEP_MAX;
|
|
|
|
geohashDecodeWGS84(geohash, &area);
|
|
|
|
|
|
|
|
double y = (area.latitude.min + area.latitude.max) / 2;
|
|
|
|
double x = (area.longitude.min + area.longitude.max) / 2;
|
|
|
|
|
|
|
|
/* Returning three nested replies */
|
2015-06-22 05:53:14 -04:00
|
|
|
addReplyMultiBulkLen(c, 3);
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
/* First, the minimum corner */
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, area.latitude.min);
|
|
|
|
addReplyDouble(c, area.longitude.min);
|
|
|
|
|
|
|
|
/* Next, the maximum corner */
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, area.latitude.max);
|
|
|
|
addReplyDouble(c, area.longitude.max);
|
|
|
|
|
|
|
|
/* Last, the averaged center of this bounding box */
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, y);
|
|
|
|
addReplyDouble(c, x);
|
|
|
|
}
|
|
|
|
|
|
|
|
void geoEncodeCommand(redisClient *c) {
|
|
|
|
/* args 0-2: ["geoencode", lat, long];
|
2015-06-22 05:53:14 -04:00
|
|
|
* optionals: [radius, units] */
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
double radius_meters = 0;
|
|
|
|
if (c->argc >= 5) {
|
2015-06-22 05:53:14 -04:00
|
|
|
if ((radius_meters = extractDistanceOrReply(c, c->argv + 3, NULL)) < 0)
|
2014-05-12 14:38:17 -04:00
|
|
|
return;
|
2015-06-22 05:53:14 -04:00
|
|
|
} else if (c->argc == 4) {
|
2014-05-12 14:38:17 -04:00
|
|
|
addReplyError(c, "must provide units when asking for radius encode");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
double latlong[2];
|
2015-06-22 05:53:14 -04:00
|
|
|
if (!extractLatLongOrReply(c, c->argv + 1, latlong)) return;
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
/* Encode lat/long into our geohash */
|
|
|
|
GeoHashBits geohash;
|
|
|
|
uint8_t step = geohashEstimateStepsByRadius(radius_meters);
|
|
|
|
geohashEncodeWGS84(latlong[0], latlong[1], step, &geohash);
|
|
|
|
|
|
|
|
/* Align the hash to a valid 52-bit integer based on step size */
|
|
|
|
GeoHashFix52Bits bits = geohashAlign52Bits(geohash);
|
|
|
|
|
|
|
|
/* Decode the hash so we can return its bounding box */
|
|
|
|
#ifdef DEBUG
|
|
|
|
printf("Decoding with step size: %d\n", geohash.step);
|
|
|
|
#endif
|
|
|
|
GeoHashArea area;
|
|
|
|
geohashDecodeWGS84(geohash, &area);
|
|
|
|
|
|
|
|
double y = (area.latitude.min + area.latitude.max) / 2;
|
|
|
|
double x = (area.longitude.min + area.longitude.max) / 2;
|
|
|
|
|
2015-06-22 05:53:14 -04:00
|
|
|
/* Return four nested multibulk replies. */
|
|
|
|
addReplyMultiBulkLen(c, 4);
|
2014-05-12 14:38:17 -04:00
|
|
|
|
|
|
|
/* Return the binary geohash we calculated as 52-bit integer */
|
|
|
|
addReplyLongLong(c, bits);
|
|
|
|
|
|
|
|
/* Return the minimum corner */
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, area.latitude.min);
|
|
|
|
addReplyDouble(c, area.longitude.min);
|
|
|
|
|
|
|
|
/* Return the maximum corner */
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, area.latitude.max);
|
|
|
|
addReplyDouble(c, area.longitude.max);
|
|
|
|
|
|
|
|
/* Return the averaged center */
|
|
|
|
addReplyMultiBulkLen(c, 2);
|
|
|
|
addReplyDouble(c, y);
|
|
|
|
addReplyDouble(c, x);
|
|
|
|
}
|