Add GEOSEARCH / GEOSEARCHSTORE commands (#8094)
Add commands to query geospatial data with bounding box. Two new commands that replace the existing 4 GEORADIUS* commands. GEOSEARCH key [FROMMEMBER member] [FROMLOC long lat] [BYRADIUS radius unit] [BYBOX width height unit] [WITHCORD] [WITHDIST] [WITHASH] [COUNT count] [ASC|DESC] GEOSEARCHSTORE dest_key src_key [FROMMEMBER member] [FROMLOC long lat] [BYRADIUS radius unit] [BYBOX width height unit] [WITHCORD] [WITHDIST] [WITHASH] [COUNT count] [ASC|DESC] [STOREDIST] - Add two types of CIRCULAR_TYPE and RECTANGLE_TYPE to achieve different searches - Judge whether the point is within the rectangle, refer to: geohashGetDistanceIfInRectangle
This commit is contained in:
parent
8c291b97b9
commit
4d06d99bf8
211
src/geo.c
211
src/geo.c
@ -144,31 +144,57 @@ double extractUnitOrReply(client *c, robj *unit) {
|
||||
|
||||
/* Input Argument Helper.
|
||||
* Extract the distance from the specified two arguments starting at 'argv'
|
||||
* that should be in the form: <number> <unit>, and return the distance in the
|
||||
* specified unit on success. *conversions 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) {
|
||||
* 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 -1;
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
if (distance < 0) {
|
||||
addReplyError(c,"radius cannot be negative");
|
||||
return -1;
|
||||
return C_ERR;
|
||||
}
|
||||
if (radius) *radius = distance;
|
||||
|
||||
double to_meters = extractUnitOrReply(c,argv[1]);
|
||||
if (to_meters < 0) {
|
||||
return -1;
|
||||
return C_ERR;
|
||||
}
|
||||
|
||||
if (conversion) *conversion = to_meters;
|
||||
return distance * 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 *height, double *width) {
|
||||
double h, w;
|
||||
if ((getDoubleFromObjectOrReply(c, argv[0], &h, "need numeric height") != C_OK) ||
|
||||
(getDoubleFromObjectOrReply(c, argv[1], &w, "need numeric width") != 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
|
||||
@ -183,21 +209,22 @@ void addReplyDoubleDistance(client *c, double d) {
|
||||
}
|
||||
|
||||
/* 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.
|
||||
* representing a point, and a GeoShape, 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];
|
||||
int geoAppendIfWithinShape(geoArray *ga, GeoShape *shape, double score, sds member) {
|
||||
double distance = 0, 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;
|
||||
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->bounds, shape->xy[0], shape->xy[1],
|
||||
xy[0], xy[1], &distance)) return C_ERR;
|
||||
}
|
||||
|
||||
/* Append the new element. */
|
||||
@ -222,7 +249,7 @@ int geoAppendIfWithinRadius(geoArray *ga, double lon, double lat, double radius,
|
||||
* 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) {
|
||||
int geoGetPointsInRange(robj *zobj, double min, double max, GeoShape *shape, 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 };
|
||||
@ -254,7 +281,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, double lon, double l
|
||||
ziplistGet(eptr, &vstr, &vlen, &vlong);
|
||||
member = (vstr == NULL) ? sdsfromlonglong(vlong) :
|
||||
sdsnewlen(vstr,vlen);
|
||||
if (geoAppendIfWithinRadius(ga,lon,lat,radius,score,member)
|
||||
if (geoAppendIfWithinShape(ga,shape,score,member)
|
||||
== C_ERR) sdsfree(member);
|
||||
zzlNext(zl, &eptr, &sptr);
|
||||
}
|
||||
@ -275,7 +302,7 @@ int geoGetPointsInRange(robj *zobj, double min, double max, double lon, double l
|
||||
break;
|
||||
|
||||
ele = sdsdup(ele);
|
||||
if (geoAppendIfWithinRadius(ga,lon,lat,radius,ln->score,ele)
|
||||
if (geoAppendIfWithinShape(ga,shape,ln->score,ele)
|
||||
== C_ERR) sdsfree(ele);
|
||||
ln = ln->level[0].forward;
|
||||
}
|
||||
@ -315,15 +342,15 @@ void scoresOfGeoHashBox(GeoHashBits hash, GeoHashFix52Bits *min, GeoHashFix52Bit
|
||||
/* 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) {
|
||||
int membersOfGeoHashBox(robj *zobj, GeoHashBits hash, geoArray *ga, GeoShape *shape) {
|
||||
GeoHashFix52Bits min, max;
|
||||
|
||||
scoresOfGeoHashBox(hash,&min,&max);
|
||||
return geoGetPointsInRange(zobj, min, max, lon, lat, radius, ga);
|
||||
return geoGetPointsInRange(zobj, min, max, shape, ga);
|
||||
}
|
||||
|
||||
/* Search all eight neighbors + self geohash box */
|
||||
int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, double radius, geoArray *ga) {
|
||||
int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, GeoShape *shape, geoArray *ga) {
|
||||
GeoHashBits neighbors[9];
|
||||
unsigned int i, count = 0, last_processed = 0;
|
||||
int debugmsg = 0;
|
||||
@ -374,7 +401,7 @@ int membersOfAllNeighbors(robj *zobj, GeoHashRadius n, double lon, double lat, d
|
||||
D("Skipping processing of %d, same as previous\n",i);
|
||||
continue;
|
||||
}
|
||||
count += membersOfGeoHashBox(zobj, neighbors[i], ga, lon, lat, radius);
|
||||
count += membersOfGeoHashBox(zobj, neighbors[i], ga, shape);
|
||||
last_processed = i;
|
||||
}
|
||||
return count;
|
||||
@ -454,51 +481,60 @@ void geoaddCommand(client *c) {
|
||||
|
||||
#define RADIUS_COORDS (1<<0) /* Search around coordinates. */
|
||||
#define RADIUS_MEMBER (1<<1) /* Search around member. */
|
||||
#define RADIUS_NOSTORE (1<<2) /* Do not acceot STORE/STOREDIST option. */
|
||||
#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] [STORE key] [STOREDIST key]
|
||||
* GEORADIUSBYMEMBER key member radius unit ... options ... */
|
||||
void georadiusGeneric(client *c, int flags) {
|
||||
robj *key = c->argv[1];
|
||||
* GEORADIUSBYMEMBER key member radius unit ... options ...
|
||||
* GEOSEARCH key [FROMMEMBER member] [FORMLOG long lat] [BYRADIUS radius unit]
|
||||
* [BYBOX width height unit] [WITHCORD] [WITHDIST] [WITHASH] [COUNT count] [ASC|DESC]
|
||||
* GEOSEARCHSTORE dest_key src_key [FROMMEMBER member] [FORMLOG long lat] [BYRADIUS radius unit]
|
||||
* [BYBOX width height unit] [WITHCORD] [WITHDIST] [WITHASH] [COUNT count] [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 */
|
||||
robj *zobj = NULL;
|
||||
if ((zobj = lookupKeyReadOrReply(c, key, shared.emptyarray)) == NULL ||
|
||||
if ((zobj = lookupKeyReadOrReply(c, c->argv[srcKeyIndex], shared.emptyarray)) == NULL ||
|
||||
checkType(c, zobj, OBJ_ZSET)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Find long/lat to use for radius search based on inquiry type */
|
||||
/* Find long/lat to use for radius or box search based on inquiry type */
|
||||
int base_args;
|
||||
double xy[2] = { 0 };
|
||||
GeoShape shape = {0};
|
||||
if (flags & RADIUS_COORDS) {
|
||||
base_args = 6;
|
||||
if (extractLongLatOrReply(c, c->argv + 2, xy) == C_ERR)
|
||||
return;
|
||||
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) {
|
||||
base_args = 5;
|
||||
shape.type = CIRCULAR_TYPE;
|
||||
robj *member = c->argv[2];
|
||||
if (longLatFromMember(zobj, member, xy) == C_ERR) {
|
||||
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) {
|
||||
base_args = 2;
|
||||
if (flags & GEOSEARCHSTORE) {
|
||||
base_args = 3;
|
||||
storekey = c->argv[1];
|
||||
}
|
||||
} 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 frommember = 0, fromloc = 0, byradius = 0, bybox = 0;
|
||||
int sort = SORT_NONE;
|
||||
long long count = 0;
|
||||
if (c->argc > base_args) {
|
||||
@ -525,18 +561,64 @@ void georadiusGeneric(client *c, int flags) {
|
||||
i++;
|
||||
} else if (!strcasecmp(arg, "store") &&
|
||||
(i+1) < remaining &&
|
||||
!(flags & RADIUS_NOSTORE))
|
||||
!(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 & 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)
|
||||
{
|
||||
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, "fromloc") &&
|
||||
(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.height,
|
||||
&shape.t.r.width) != C_OK) return;
|
||||
shape.type = RECTANGLE_TYPE;
|
||||
bybox = 1;
|
||||
i += 3;
|
||||
} else {
|
||||
addReply(c, shared.syntaxerr);
|
||||
return;
|
||||
@ -552,17 +634,26 @@ void georadiusGeneric(client *c, int flags) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((flags & GEOSEARCH) && !(frommember || fromloc)) {
|
||||
addReplyErrorFormat(c, "exactly one of FROMMEMBER or FROMLOC 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;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
GeoHashRadius georadius = geohashCalculateAreasByShapeWGS84(&shape);
|
||||
|
||||
/* Search the zset for all matching points */
|
||||
geoArray *ga = geoArrayCreate();
|
||||
membersOfAllNeighbors(zobj, georadius, xy[0], xy[1], radius_meters, ga);
|
||||
membersOfAllNeighbors(zobj, georadius, &shape, ga);
|
||||
|
||||
/* If no matching results, the user gets an empty reply. */
|
||||
if (ga->used == 0 && storekey == NULL) {
|
||||
@ -607,7 +698,7 @@ void georadiusGeneric(client *c, int flags) {
|
||||
int i;
|
||||
for (i = 0; i < returned_items; i++) {
|
||||
geoPoint *gp = ga->array+i;
|
||||
gp->dist /= conversion; /* Fix according to unit. */
|
||||
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
|
||||
@ -645,7 +736,7 @@ void georadiusGeneric(client *c, int flags) {
|
||||
for (i = 0; i < returned_items; i++) {
|
||||
zskiplistNode *znode;
|
||||
geoPoint *gp = ga->array+i;
|
||||
gp->dist /= conversion; /* Fix according to unit. */
|
||||
gp->dist /= shape.conversion; /* Fix according to unit. */
|
||||
double score = storedist ? gp->dist : gp->score;
|
||||
size_t elelen = sdslen(gp->member);
|
||||
|
||||
@ -659,7 +750,7 @@ void georadiusGeneric(client *c, int flags) {
|
||||
zsetConvertToZiplistIfNeeded(zobj,maxelelen);
|
||||
setKey(c,c->db,storekey,zobj);
|
||||
decrRefCount(zobj);
|
||||
notifyKeyspaceEvent(NOTIFY_ZSET,"georadiusstore",storekey,
|
||||
notifyKeyspaceEvent(NOTIFY_ZSET,flags & GEOSEARCH ? "geosearchstore" : "georadiusstore",storekey,
|
||||
c->db->id);
|
||||
server.dirty += returned_items;
|
||||
} else if (dbDelete(c->db,storekey)) {
|
||||
@ -674,22 +765,30 @@ void georadiusGeneric(client *c, int flags) {
|
||||
|
||||
/* GEORADIUS wrapper function. */
|
||||
void georadiusCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_COORDS);
|
||||
georadiusGeneric(c, 1, RADIUS_COORDS);
|
||||
}
|
||||
|
||||
/* GEORADIUSBYMEMBER wrapper function. */
|
||||
void georadiusbymemberCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_MEMBER);
|
||||
georadiusGeneric(c, 1, RADIUS_MEMBER);
|
||||
}
|
||||
|
||||
/* GEORADIUS_RO wrapper function. */
|
||||
void georadiusroCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_COORDS|RADIUS_NOSTORE);
|
||||
georadiusGeneric(c, 1, RADIUS_COORDS|RADIUS_NOSTORE);
|
||||
}
|
||||
|
||||
/* GEORADIUSBYMEMBER_RO wrapper function. */
|
||||
void georadiusbymemberroCommand(client *c) {
|
||||
georadiusGeneric(c, RADIUS_MEMBER|RADIUS_NOSTORE);
|
||||
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
|
||||
|
@ -90,6 +90,25 @@ typedef struct {
|
||||
GeoHashBits south_west;
|
||||
} GeoHashNeighbors;
|
||||
|
||||
#define CIRCULAR_TYPE 1
|
||||
#define RECTANGLE_TYPE 2
|
||||
typedef struct {
|
||||
int type; /* search type */
|
||||
double xy[2]; /* search center point, xy[0]: lon, xy[1]: lat */
|
||||
double conversion; /* km: 1000 */
|
||||
double bounds[4]; /* bounds[0]: min_lon, bounds[1]: min_lat
|
||||
* bounds[2]: max_lon, bounds[3]: max_lat */
|
||||
union {
|
||||
/* CIRCULAR_TYPE */
|
||||
double radius;
|
||||
/* RECTANGLE_TYPE */
|
||||
struct {
|
||||
double height;
|
||||
double width;
|
||||
} r;
|
||||
} t;
|
||||
} GeoShape;
|
||||
|
||||
/*
|
||||
* 0:success
|
||||
* -1:failed
|
||||
|
@ -82,10 +82,9 @@ uint8_t geohashEstimateStepsByRadius(double range_meters, double lat) {
|
||||
return step;
|
||||
}
|
||||
|
||||
/* Return the bounding box of the search area centered at latitude,longitude
|
||||
* having a radius of radius_meter. bounds[0] - bounds[2] is the minimum
|
||||
* and maximum longitude, while bounds[1] - bounds[3] is the minimum and
|
||||
* maximum latitude.
|
||||
/* Return the bounding box of the search area by shape (see geohash.h GeoShape)
|
||||
* bounds[0] - bounds[2] is the minimum and maximum longitude
|
||||
* while bounds[1] - bounds[3] is the minimum and maximum latitude.
|
||||
*
|
||||
* This function does not behave correctly with very large radius values, for
|
||||
* instance for the coordinates 81.634948934258375 30.561509253718668 and a
|
||||
@ -100,34 +99,51 @@ uint8_t geohashEstimateStepsByRadius(double range_meters, double lat) {
|
||||
* Since this function is currently only used as an optimization, the
|
||||
* optimization is not used for very big radiuses, however the function
|
||||
* should be fixed. */
|
||||
int geohashBoundingBox(double longitude, double latitude, double radius_meters,
|
||||
double *bounds) {
|
||||
int geohashBoundingBox(GeoShape *shape, double *bounds) {
|
||||
if (!bounds) return 0;
|
||||
double longitude = shape->xy[0];
|
||||
double latitude = shape->xy[1];
|
||||
double height = shape->conversion * (shape->type == CIRCULAR_TYPE ? shape->t.radius : shape->t.r.height/2);
|
||||
double width = shape->conversion * (shape->type == CIRCULAR_TYPE ? shape->t.radius : shape->t.r.width/2);
|
||||
|
||||
bounds[0] = longitude - rad_deg(radius_meters/EARTH_RADIUS_IN_METERS/cos(deg_rad(latitude)));
|
||||
bounds[2] = longitude + rad_deg(radius_meters/EARTH_RADIUS_IN_METERS/cos(deg_rad(latitude)));
|
||||
bounds[1] = latitude - rad_deg(radius_meters/EARTH_RADIUS_IN_METERS);
|
||||
bounds[3] = latitude + rad_deg(radius_meters/EARTH_RADIUS_IN_METERS);
|
||||
const double long_delta = rad_deg(width/EARTH_RADIUS_IN_METERS/cos(deg_rad(latitude)));
|
||||
const double lat_delta = rad_deg(height/EARTH_RADIUS_IN_METERS);
|
||||
bounds[0] = longitude - long_delta;
|
||||
bounds[2] = longitude + long_delta;
|
||||
bounds[1] = latitude - lat_delta;
|
||||
bounds[3] = latitude + lat_delta;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Return a set of areas (center + 8) that are able to cover a range query
|
||||
* for the specified position and radius. */
|
||||
GeoHashRadius geohashGetAreasByRadius(double longitude, double latitude, double radius_meters) {
|
||||
/* Calculate a set of areas (center + 8) that are able to cover a range query
|
||||
* for the specified position and shape (see geohash.h GeoShape).
|
||||
* the bounding box saved in shaple.bounds */
|
||||
GeoHashRadius geohashCalculateAreasByShapeWGS84(GeoShape *shape) {
|
||||
GeoHashRange long_range, lat_range;
|
||||
GeoHashRadius radius;
|
||||
GeoHashBits hash;
|
||||
GeoHashNeighbors neighbors;
|
||||
GeoHashArea area;
|
||||
double min_lon, max_lon, min_lat, max_lat;
|
||||
double bounds[4];
|
||||
int steps;
|
||||
|
||||
geohashBoundingBox(longitude, latitude, radius_meters, bounds);
|
||||
min_lon = bounds[0];
|
||||
min_lat = bounds[1];
|
||||
max_lon = bounds[2];
|
||||
max_lat = bounds[3];
|
||||
geohashBoundingBox(shape, shape->bounds);
|
||||
min_lon = shape->bounds[0];
|
||||
min_lat = shape->bounds[1];
|
||||
max_lon = shape->bounds[2];
|
||||
max_lat = shape->bounds[3];
|
||||
|
||||
double longitude = shape->xy[0];
|
||||
double latitude = shape->xy[1];
|
||||
/* radius_meters is calculated differently in different search types:
|
||||
* 1) CIRCULAR_TYPE, just use radius.
|
||||
* 2) RECTANGLE_TYPE, in order to calculate accurately, we should use
|
||||
* sqrt((width/2)^2 + (height/2)^2), so that the box is bound by a circle,
|
||||
* But the current code a simpler approach resulting in a smaller circle,
|
||||
* which is safe because we search the 8 nearby boxes anyway. */
|
||||
double radius_meters = shape->type == CIRCULAR_TYPE ? shape->t.radius :
|
||||
shape->t.r.width > shape->t.r.height ? shape->t.r.width/2 : shape->t.r.height/2;
|
||||
radius_meters *= shape->conversion;
|
||||
|
||||
steps = geohashEstimateStepsByRadius(radius_meters,latitude);
|
||||
|
||||
@ -196,11 +212,6 @@ GeoHashRadius geohashGetAreasByRadius(double longitude, double latitude, double
|
||||
return radius;
|
||||
}
|
||||
|
||||
GeoHashRadius geohashGetAreasByRadiusWGS84(double longitude, double latitude,
|
||||
double radius_meters) {
|
||||
return geohashGetAreasByRadius(longitude, latitude, radius_meters);
|
||||
}
|
||||
|
||||
GeoHashFix52Bits geohashAlign52Bits(const GeoHashBits hash) {
|
||||
uint64_t bits = hash.bits;
|
||||
bits <<= (52 - hash.step * 2);
|
||||
@ -233,3 +244,15 @@ int geohashGetDistanceIfInRadiusWGS84(double x1, double y1, double x2,
|
||||
double *distance) {
|
||||
return geohashGetDistanceIfInRadius(x1, y1, x2, y2, radius, distance);
|
||||
}
|
||||
|
||||
/* Judge whether a point is in the axis-aligned rectangle.
|
||||
* bounds : see geohash.h GeoShape::bounds
|
||||
* x1, y1 : the center of the box
|
||||
* x2, y2 : the point to be searched
|
||||
*/
|
||||
int geohashGetDistanceIfInRectangle(double *bounds, double x1, double y1,
|
||||
double x2, double y2, double *distance) {
|
||||
if (x2 < bounds[0] || x2 > bounds[2] || y2 < bounds[1] || y2 > bounds[3]) return 0;
|
||||
*distance = geohashGetDistance(x1, y1, x2, y2);
|
||||
return 1;
|
||||
}
|
||||
|
@ -49,14 +49,8 @@ typedef struct {
|
||||
|
||||
int GeoHashBitsComparator(const GeoHashBits *a, const GeoHashBits *b);
|
||||
uint8_t geohashEstimateStepsByRadius(double range_meters, double lat);
|
||||
int geohashBoundingBox(double longitude, double latitude, double radius_meters,
|
||||
double *bounds);
|
||||
GeoHashRadius geohashGetAreasByRadius(double longitude,
|
||||
double latitude, double radius_meters);
|
||||
GeoHashRadius geohashGetAreasByRadiusWGS84(double longitude, double latitude,
|
||||
double radius_meters);
|
||||
GeoHashRadius geohashGetAreasByRadiusMercator(double longitude, double latitude,
|
||||
double radius_meters);
|
||||
int geohashBoundingBox(GeoShape *shape, double *bounds);
|
||||
GeoHashRadius geohashCalculateAreasByShapeWGS84(GeoShape *shape);
|
||||
GeoHashFix52Bits geohashAlign52Bits(const GeoHashBits hash);
|
||||
double geohashGetDistance(double lon1d, double lat1d,
|
||||
double lon2d, double lat2d);
|
||||
@ -66,5 +60,7 @@ int geohashGetDistanceIfInRadius(double x1, double y1,
|
||||
int geohashGetDistanceIfInRadiusWGS84(double x1, double y1, double x2,
|
||||
double y2, double radius,
|
||||
double *distance);
|
||||
int geohashGetDistanceIfInRectangle(double *bounds, double x1, double y1,
|
||||
double x2, double y2, double *distance);
|
||||
|
||||
#endif /* GEOHASH_HELPER_HPP_ */
|
||||
|
12
src/server.c
12
src/server.c
@ -919,7 +919,7 @@ struct redisCommand redisCommandTable[] = {
|
||||
|
||||
/* GEORADIUS has store options that may write. */
|
||||
{"georadius",georadiusCommand,-6,
|
||||
"write @geo",
|
||||
"write use-memory @geo",
|
||||
0,georadiusGetKeys,1,1,1,0,0,0},
|
||||
|
||||
{"georadius_ro",georadiusroCommand,-6,
|
||||
@ -927,7 +927,7 @@ struct redisCommand redisCommandTable[] = {
|
||||
0,georadiusGetKeys,1,1,1,0,0,0},
|
||||
|
||||
{"georadiusbymember",georadiusbymemberCommand,-5,
|
||||
"write @geo",
|
||||
"write use-memory @geo",
|
||||
0,georadiusGetKeys,1,1,1,0,0,0},
|
||||
|
||||
{"georadiusbymember_ro",georadiusbymemberroCommand,-5,
|
||||
@ -946,6 +946,14 @@ struct redisCommand redisCommandTable[] = {
|
||||
"read-only @geo",
|
||||
0,NULL,1,1,1,0,0,0},
|
||||
|
||||
{"geosearch",geosearchCommand,-7,
|
||||
"read-only @geo",
|
||||
0,NULL,1,1,1,0,0,0},
|
||||
|
||||
{"geosearchstore",geosearchstoreCommand,-8,
|
||||
"write use-memory @geo",
|
||||
0,NULL,1,2,1,0,0,0},
|
||||
|
||||
{"pfselftest",pfselftestCommand,1,
|
||||
"admin @hyperloglog",
|
||||
0,NULL,0,0,0,0,0,0},
|
||||
|
@ -2546,6 +2546,8 @@ void geoaddCommand(client *c);
|
||||
void geohashCommand(client *c);
|
||||
void geoposCommand(client *c);
|
||||
void geodistCommand(client *c);
|
||||
void geosearchCommand(client *c);
|
||||
void geosearchstoreCommand(client *c);
|
||||
void pfselftestCommand(client *c);
|
||||
void pfaddCommand(client *c);
|
||||
void pfcountCommand(client *c);
|
||||
|
@ -94,10 +94,43 @@ start_server {tags {"geo"}} {
|
||||
r georadius nyc -73.9798091 40.7598464 3 km asc
|
||||
} {{central park n/q/r} 4545 {union square}}
|
||||
|
||||
test {GEOSEARCH simple (sorted)} {
|
||||
r geosearch nyc fromloc -73.9798091 40.7598464 bybox 6 6 km asc
|
||||
} {{central park n/q/r} 4545 {union square} {lic market}}
|
||||
|
||||
test {GEOSEARCH FROMLOC and FROMMEMBER cannot exist at the same time} {
|
||||
catch {r geosearch nyc fromloc -73.9798091 40.7598464 frommember xxx bybox 6 6 km asc} e
|
||||
set e
|
||||
} {ERR*syntax*}
|
||||
|
||||
test {GEOSEARCH FROMLOC and FROMMEMBER one must exist} {
|
||||
catch {r geosearch nyc bybox 3 3 km asc desc withhash withdist withcoord} e
|
||||
set e
|
||||
} {ERR*exactly one of FROMMEMBER or FROMLOC*}
|
||||
|
||||
test {GEOSEARCH BYRADIUS and BYBOX cannot exist at the same time} {
|
||||
catch {r geosearch nyc fromloc -73.9798091 40.7598464 byradius 3 km bybox 3 3 km asc} e
|
||||
set e
|
||||
} {ERR*syntax*}
|
||||
|
||||
test {GEOSEARCH BYRADIUS and BYBOX one must exist} {
|
||||
catch {r geosearch nyc fromloc -73.9798091 40.7598464 asc desc withhash withdist withcoord} e
|
||||
set e
|
||||
} {ERR*exactly one of BYRADIUS and BYBOX*}
|
||||
|
||||
test {GEOSEARCH with STOREDIST option} {
|
||||
catch {r geosearch nyc fromloc -73.9798091 40.7598464 bybox 6 6 km asc storedist} e
|
||||
set e
|
||||
} {ERR*syntax*}
|
||||
|
||||
test {GEORADIUS withdist (sorted)} {
|
||||
r georadius nyc -73.9798091 40.7598464 3 km withdist asc
|
||||
} {{{central park n/q/r} 0.7750} {4545 2.3651} {{union square} 2.7697}}
|
||||
|
||||
test {GEOSEARCH withdist (sorted)} {
|
||||
r geosearch nyc fromloc -73.9798091 40.7598464 bybox 6 6 km withdist asc
|
||||
} {{{central park n/q/r} 0.7750} {4545 2.3651} {{union square} 2.7697} {{lic market} 3.1991}}
|
||||
|
||||
test {GEORADIUS with COUNT} {
|
||||
r georadius nyc -73.9798091 40.7598464 10 km COUNT 3
|
||||
} {{central park n/q/r} 4545 {union square}}
|
||||
@ -120,6 +153,35 @@ start_server {tags {"geo"}} {
|
||||
r georadiusbymember nyc "wtc one" 7 km
|
||||
} {{wtc one} {union square} {central park n/q/r} 4545 {lic market}}
|
||||
|
||||
test {GEOSEARCH FROMMEMBER simple (sorted)} {
|
||||
r geosearch nyc frommember "wtc one" bybox 14 14 km
|
||||
} {{wtc one} {union square} {central park n/q/r} 4545 {lic market} q4}
|
||||
|
||||
test {GEOSEARCH vs GEORADIUS} {
|
||||
r del Sicily
|
||||
r geoadd Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
|
||||
r geoadd Sicily 12.758489 38.788135 "edge1" 17.241510 38.788135 "eage2"
|
||||
set ret1 [r georadius Sicily 15 37 200 km asc]
|
||||
assert_equal $ret1 {Catania Palermo}
|
||||
set ret2 [r geosearch Sicily fromloc 15 37 bybox 400 400 km asc]
|
||||
assert_equal $ret2 {Catania Palermo eage2 edge1}
|
||||
}
|
||||
|
||||
test {GEOSEARCH non square, long and narrow} {
|
||||
r del Sicily
|
||||
r geoadd Sicily 12.75 37.00 "test1"
|
||||
r geoadd Sicily 12.75 36.50 "test2"
|
||||
r geoadd Sicily 13.00 36.50 "test3"
|
||||
# box height=2km width=400km
|
||||
set ret1 [r geosearch Sicily fromloc 15 37 bybox 2 400 km]
|
||||
assert_equal $ret1 {test1}
|
||||
|
||||
# Add a western Hemisphere point
|
||||
r geoadd Sicily -1 37.00 "test3"
|
||||
set ret2 [r geosearch Sicily fromloc 15 37 bybox 2 3000 km asc]
|
||||
assert_equal $ret2 {test1 test3}
|
||||
}
|
||||
|
||||
test {GEORADIUSBYMEMBER withdist (sorted)} {
|
||||
r georadiusbymember nyc "wtc one" 7 km withdist
|
||||
} {{{wtc one} 0.0000} {{union square} 3.2544} {{central park n/q/r} 6.7000} {4545 6.1975} {{lic market} 6.8969}}
|
||||
@ -178,6 +240,11 @@ start_server {tags {"geo"}} {
|
||||
set e
|
||||
} {*ERR*syntax*}
|
||||
|
||||
test {GEOSEARCHSTORE STORE option: syntax error} {
|
||||
catch {r geosearchstore abc points fromloc 13.361389 38.115556 byradius 50 km store abc} e
|
||||
set e
|
||||
} {*ERR*syntax*}
|
||||
|
||||
test {GEORANGE STORE option: incompatible options} {
|
||||
r del points
|
||||
r geoadd points 13.361389 38.115556 "Palermo" \
|
||||
@ -198,6 +265,11 @@ start_server {tags {"geo"}} {
|
||||
assert_equal [r zrange points 0 -1] [r zrange points2 0 -1]
|
||||
}
|
||||
|
||||
test {GEOSEARCHSTORE STORE option: plain usage} {
|
||||
r geosearchstore points2 points fromloc 13.361389 38.115556 byradius 500 km
|
||||
assert_equal [r zrange points 0 -1] [r zrange points2 0 -1]
|
||||
}
|
||||
|
||||
test {GEORANGE STOREDIST option: plain usage} {
|
||||
r del points
|
||||
r geoadd points 13.361389 38.115556 "Palermo" \
|
||||
@ -209,6 +281,14 @@ start_server {tags {"geo"}} {
|
||||
assert {[lindex $res 3] < 167}
|
||||
}
|
||||
|
||||
test {GEOSEARCHSTORE STOREDIST option: plain usage} {
|
||||
r geosearchstore points2 points fromloc 13.361389 38.115556 byradius 500 km storedist
|
||||
set res [r zrange points2 0 -1 withscores]
|
||||
assert {[lindex $res 1] < 1}
|
||||
assert {[lindex $res 3] > 166}
|
||||
assert {[lindex $res 3] < 167}
|
||||
}
|
||||
|
||||
test {GEORANGE STOREDIST option: COUNT ASC and DESC} {
|
||||
r del points
|
||||
r geoadd points 13.361389 38.115556 "Palermo" \
|
||||
|
Loading…
x
Reference in New Issue
Block a user