diff --git a/deps/Makefile b/deps/Makefile index 71f6d3a2..10ae6e79 100644 --- a/deps/Makefile +++ b/deps/Makefile @@ -36,6 +36,7 @@ distclean: -(cd hiredis && $(MAKE) clean) > /dev/null || true -(cd linenoise && $(MAKE) clean) > /dev/null || true -(cd lua && $(MAKE) clean) > /dev/null || true + -(cd geohash-int && $(MAKE) clean) > /dev/null || true -(cd jemalloc && [ -f Makefile ] && $(MAKE) distclean) > /dev/null || true -(rm -f .make-*) @@ -81,3 +82,9 @@ jemalloc: .make-prerequisites cd jemalloc && $(MAKE) CFLAGS="$(JEMALLOC_CFLAGS)" LDFLAGS="$(JEMALLOC_LDFLAGS)" lib/libjemalloc.a .PHONY: jemalloc + +geohash-int: .make-prerequisites + @printf '%b %b\n' $(MAKECOLOR)MAKE$(ENDCOLOR) $(BINCOLOR)$@$(ENDCOLOR) + cd geohash-int && $(MAKE) + +.PHONY: geohash-int diff --git a/deps/geohash-int/Makefile b/deps/geohash-int/Makefile new file mode 100644 index 00000000..bf9eaebb --- /dev/null +++ b/deps/geohash-int/Makefile @@ -0,0 +1,23 @@ +STD= +WARN= -Wall +OPT= -Ofast + +R_CFLAGS= $(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) +R_LDFLAGS= $(LDFLAGS) +DEBUG= -g + +R_CC=$(CC) $(R_CFLAGS) +R_LD=$(CC) $(R_LDFLAGS) + +all: geohash.o geohash_helper.o + +.PHONY: all + +geohash.o: geohash.h geohash.c +geohash_helper.o: geohash.h geohash_helper.h geohash_helper.c + +.c.o: + $(R_CC) -c $< + +clean: + rm -f *.o diff --git a/deps/geohash-int/geohash.c b/deps/geohash-int/geohash.c new file mode 100644 index 00000000..9212736b --- /dev/null +++ b/deps/geohash-int/geohash.c @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2013-2014, yinqiwen + * Copyright (c) 2014, Matt Stancliff . + * 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 "geohash.h" + +/** + * Hashing works like this: + * Divide the world into 4 buckets. Label each one as such: + * ----------------- + * | | | + * | | | + * | 0,1 | 1,1 | + * ----------------- + * | | | + * | | | + * | 0,0 | 1,0 | + * ----------------- + */ + +bool geohashGetCoordRange(uint8_t coord_type, GeoHashRange *lat_range, + GeoHashRange *long_range) { + switch (coord_type) { + case GEO_WGS84_TYPE: { + /* These are constraints from EPSG:900913 / EPSG:3785 / OSGEO:41001 */ + /* We can't geocode at the north/south pole. */ + lat_range->max = 85.05112878; + lat_range->min = -85.05112878; + long_range->max = 180.0; + long_range->min = -180.0; + break; + } + case GEO_MERCATOR_TYPE: { + lat_range->max = 20037726.37; + lat_range->min = -20037726.37; + long_range->max = 20037726.37; + long_range->min = -20037726.37; + break; + } + default: { return false; } + } + return true; +} + +bool geohashEncode(GeoHashRange *lat_range, GeoHashRange *long_range, + double latitude, double longitude, uint8_t step, + GeoHashBits *hash) { + uint8_t i; + + if (NULL == hash || step > 32 || step == 0 || RANGEPISZERO(lat_range) || + RANGEPISZERO(long_range)) { + return false; + } + + hash->bits = 0; + hash->step = step; + + if (latitude < lat_range->min || latitude > lat_range->max || + longitude < long_range->min || longitude > long_range->max) { + return false; + } + + for (i = 0; i < step; i++) { + uint8_t lat_bit, long_bit; + + if (lat_range->max - latitude >= latitude - lat_range->min) { + lat_bit = 0; + lat_range->max = (lat_range->max + lat_range->min) / 2; + } else { + lat_bit = 1; + lat_range->min = (lat_range->max + lat_range->min) / 2; + } + if (long_range->max - longitude >= longitude - long_range->min) { + long_bit = 0; + long_range->max = (long_range->max + long_range->min) / 2; + } else { + long_bit = 1; + long_range->min = (long_range->max + long_range->min) / 2; + } + + hash->bits <<= 1; + hash->bits += long_bit; + hash->bits <<= 1; + hash->bits += lat_bit; + } + return true; +} + +bool geohashEncodeType(uint8_t coord_type, double latitude, double longitude, + uint8_t step, GeoHashBits *hash) { + GeoHashRange r[2] = { { 0 } }; + geohashGetCoordRange(coord_type, &r[0], &r[1]); + return geohashEncode(&r[0], &r[1], latitude, longitude, step, hash); +} + +bool geohashEncodeWGS84(double latitude, double longitude, uint8_t step, + GeoHashBits *hash) { + return geohashEncodeType(GEO_WGS84_TYPE, latitude, longitude, step, hash); +} + +bool geohashEncodeMercator(double latitude, double longitude, uint8_t step, + GeoHashBits *hash) { + return geohashEncodeType(GEO_MERCATOR_TYPE, latitude, longitude, step, + hash); +} + +static inline uint8_t get_bit(uint64_t bits, uint8_t pos) { + return (bits >> pos) & 0x01; +} + +bool geohashDecode(const GeoHashRange lat_range, const GeoHashRange long_range, + const GeoHashBits hash, GeoHashArea *area) { + uint8_t i; + + if (HASHISZERO(hash) || NULL == area || RANGEISZERO(lat_range) || + RANGEISZERO(long_range)) { + return false; + } + + area->hash = hash; + area->latitude.min = lat_range.min; + area->latitude.max = lat_range.max; + area->longitude.min = long_range.min; + area->longitude.max = long_range.max; + + for (i = 0; i < hash.step; i++) { + uint8_t lat_bit, long_bit; + + long_bit = get_bit(hash.bits, (hash.step - i) * 2 - 1); + lat_bit = get_bit(hash.bits, (hash.step - i) * 2 - 2); + + if (lat_bit == 0) { + area->latitude.max = (area->latitude.max + area->latitude.min) / 2; + } else { + area->latitude.min = (area->latitude.max + area->latitude.min) / 2; + } + + if (long_bit == 0) { + area->longitude.max = + (area->longitude.max + area->longitude.min) / 2; + } else { + area->longitude.min = + (area->longitude.max + area->longitude.min) / 2; + } + } + return true; +} + +bool geohashDecodeType(uint8_t coord_type, const GeoHashBits hash, + GeoHashArea *area) { + GeoHashRange r[2] = { { 0 } }; + geohashGetCoordRange(coord_type, &r[0], &r[1]); + return geohashDecode(r[0], r[1], hash, area); +} + +bool geohashDecodeWGS84(const GeoHashBits hash, GeoHashArea *area) { + return geohashDecodeType(GEO_WGS84_TYPE, hash, area); +} + +bool geohashDecodeMercator(const GeoHashBits hash, GeoHashArea *area) { + return geohashDecodeType(GEO_MERCATOR_TYPE, hash, area); +} + +bool geohashDecodeAreaToLatLong(const GeoHashArea *area, double *latlong) { + double y, x; + + if (!latlong) + return false; + + y = (area->latitude.min + area->latitude.max) / 2; + x = (area->longitude.min + area->longitude.max) / 2; + + latlong[0] = y; + latlong[1] = x; + return true; +} + +bool geohashDecodeToLatLongType(uint8_t coord_type, const GeoHashBits hash, + double *latlong) { + GeoHashArea area = { { 0 } }; + if (!latlong || !geohashDecodeType(coord_type, hash, &area)) + return false; + return geohashDecodeAreaToLatLong(&area, latlong); +} + +bool geohashDecodeToLatLongWGS84(const GeoHashBits hash, double *latlong) { + return geohashDecodeToLatLongType(GEO_WGS84_TYPE, hash, latlong); +} + +bool geohashDecodeToLatLongMercator(const GeoHashBits hash, double *latlong) { + return geohashDecodeToLatLongType(GEO_MERCATOR_TYPE, hash, latlong); +} + +static void geohash_move_x(GeoHashBits *hash, int8_t d) { + if (d == 0) + return; + + uint64_t x = hash->bits & 0xaaaaaaaaaaaaaaaaLL; + uint64_t y = hash->bits & 0x5555555555555555LL; + + uint64_t zz = 0x5555555555555555LL >> (64 - hash->step * 2); + + if (d > 0) { + x = x + (zz + 1); + } else { + x = x | zz; + x = x - (zz + 1); + } + + x &= (0xaaaaaaaaaaaaaaaaLL >> (64 - hash->step * 2)); + hash->bits = (x | y); +} + +static void geohash_move_y(GeoHashBits *hash, int8_t d) { + if (d == 0) + return; + + uint64_t x = hash->bits & 0xaaaaaaaaaaaaaaaaLL; + uint64_t y = hash->bits & 0x5555555555555555LL; + + uint64_t zz = 0xaaaaaaaaaaaaaaaaLL >> (64 - hash->step * 2); + if (d > 0) { + y = y + (zz + 1); + } else { + y = y | zz; + y = y - (zz + 1); + } + y &= (0x5555555555555555LL >> (64 - hash->step * 2)); + hash->bits = (x | y); +} + +void geohashNeighbors(const GeoHashBits *hash, GeoHashNeighbors *neighbors) { + neighbors->east = *hash; + neighbors->west = *hash; + neighbors->north = *hash; + neighbors->south = *hash; + neighbors->south_east = *hash; + neighbors->south_west = *hash; + neighbors->north_east = *hash; + neighbors->north_west = *hash; + + geohash_move_x(&neighbors->east, 1); + geohash_move_y(&neighbors->east, 0); + + geohash_move_x(&neighbors->west, -1); + geohash_move_y(&neighbors->west, 0); + + geohash_move_x(&neighbors->south, 0); + geohash_move_y(&neighbors->south, -1); + + geohash_move_x(&neighbors->north, 0); + geohash_move_y(&neighbors->north, 1); + + geohash_move_x(&neighbors->north_west, -1); + geohash_move_y(&neighbors->north_west, 1); + + geohash_move_x(&neighbors->north_east, 1); + geohash_move_y(&neighbors->north_east, 1); + + geohash_move_x(&neighbors->south_east, 1); + geohash_move_y(&neighbors->south_east, -1); + + geohash_move_x(&neighbors->south_west, -1); + geohash_move_y(&neighbors->south_west, -1); +} diff --git a/deps/geohash-int/geohash.h b/deps/geohash-int/geohash.h new file mode 100644 index 00000000..30fc1714 --- /dev/null +++ b/deps/geohash-int/geohash.h @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2013-2014, yinqiwen + * Copyright (c) 2014, Matt Stancliff . + * 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. + */ + +#ifndef GEOHASH_H_ +#define GEOHASH_H_ + +#include +#include +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +#define HASHISZERO(r) (!(r).bits && !(r).step) +#define RANGEISZERO(r) (!(r).max && !(r).min) +#define RANGEPISZERO(r) (r == NULL || RANGEISZERO(*r)) + +#define GEO_WGS84_TYPE 1 +#define GEO_MERCATOR_TYPE 2 + +#define GEO_STEP_MAX 26 + +typedef enum { + GEOHASH_NORTH = 0, + GEOHASH_EAST, + GEOHASH_WEST, + GEOHASH_SOUTH, + GEOHASH_SOUTH_WEST, + GEOHASH_SOUTH_EAST, + GEOHASH_NORT_WEST, + GEOHASH_NORT_EAST +} GeoDirection; + +typedef struct { + uint64_t bits; + uint8_t step; +} GeoHashBits; + +typedef struct { + double max; + double min; +} GeoHashRange; + +typedef struct { + GeoHashBits hash; + GeoHashRange latitude; + GeoHashRange longitude; +} GeoHashArea; + +typedef struct { + GeoHashBits north; + GeoHashBits east; + GeoHashBits west; + GeoHashBits south; + GeoHashBits north_east; + GeoHashBits south_east; + GeoHashBits north_west; + GeoHashBits south_west; +} GeoHashNeighbors; + +/* + * 0:success + * -1:failed + */ +bool geohashGetCoordRange(uint8_t coord_type, GeoHashRange *lat_range, + GeoHashRange *long_range); +bool geohashEncode(GeoHashRange *lat_range, GeoHashRange *long_range, + double latitude, double longitude, uint8_t step, + GeoHashBits *hash); +bool geohashEncodeType(uint8_t coord_type, double latitude, double longitude, + uint8_t step, GeoHashBits *hash); +bool geohashEncodeMercator(double latitude, double longitude, uint8_t step, + GeoHashBits *hash); +bool geohashEncodeWGS84(double latitude, double longitude, uint8_t step, + GeoHashBits *hash); +bool geohashDecode(const GeoHashRange lat_range, const GeoHashRange long_range, + const GeoHashBits hash, GeoHashArea *area); +bool geohashDecodeType(uint8_t coord_type, const GeoHashBits hash, + GeoHashArea *area); +bool geohashDecodeMercator(const GeoHashBits hash, GeoHashArea *area); +bool geohashDecodeWGS84(const GeoHashBits hash, GeoHashArea *area); +bool geohashDecodeAreaToLatLong(const GeoHashArea *area, double *latlong); +bool geohashDecodeToLatLongType(uint8_t coord_type, const GeoHashBits hash, + double *latlong); +bool geohashDecodeToLatLongWGS84(const GeoHashBits hash, double *latlong); +bool geohashDecodeToLatLongMercator(const GeoHashBits hash, double *latlong); +void geohashNeighbors(const GeoHashBits *hash, GeoHashNeighbors *neighbors); + +#if defined(__cplusplus) +} +#endif +#endif /* GEOHASH_H_ */ diff --git a/deps/geohash-int/geohash_helper.c b/deps/geohash-int/geohash_helper.c new file mode 100644 index 00000000..010ab070 --- /dev/null +++ b/deps/geohash-int/geohash_helper.c @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2013-2014, yinqiwen + * Copyright (c) 2014, Matt Stancliff . + * 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. + */ + +/* This is a C++ to C conversion from the ardb project. + * This file started out as: + * https://github.com/yinqiwen/ardb/blob/d42503/src/geo/geohash_helper.cpp + */ + +#include "geohash_helper.h" + +#define D_R (M_PI / 180.0) +#define R_MAJOR 6378137.0 +#define R_MINOR 6356752.3142 +#define RATIO (R_MINOR / R_MAJOR) +#define ECCENT (sqrt(1.0 - (RATIO *RATIO))) +#define COM (0.5 * ECCENT) + +/// @brief The usual PI/180 constant +const double DEG_TO_RAD = 0.017453292519943295769236907684886; +/// @brief Earth's quatratic mean radius for WGS-84 +const double EARTH_RADIUS_IN_METERS = 6372797.560856; + +const double MERCATOR_MAX = 20037726.37; +const double MERCATOR_MIN = -20037726.37; + +static inline double deg_rad(double ang) { return ang * D_R; } +static inline double rad_deg(double ang) { return ang / D_R; } + +double mercator_y(double lat) { + lat = fmin(89.5, fmax(lat, -89.5)); + double phi = deg_rad(lat); + double sinphi = sin(phi); + double con = ECCENT * sinphi; + con = pow((1.0 - con) / (1.0 + con), COM); + double ts = tan(0.5 * (M_PI * 0.5 - phi)) / con; + return 0 - R_MAJOR * log(ts); +} + +double mercator_x(double lon) { return R_MAJOR * deg_rad(lon); } +double merc_lon(double x) { return rad_deg(x) / R_MAJOR; } + +double merc_lat(double y) { + double ts = exp(-y / R_MAJOR); + double phi = M_PI_2 - 2 * atan(ts); + double dphi = 1.0; + int i; + for (i = 0; fabs(dphi) > 0.000000001 && i < 15; i++) { + double con = ECCENT * sin(phi); + dphi = + M_PI_2 - 2 * atan(ts * pow((1.0 - con) / (1.0 + con), COM)) - phi; + phi += dphi; + } + return rad_deg(phi); +} + +/* You must *ONLY* estimate steps when you are encoding. + * If you are decoding, always decode to GEO_STEP_MAX (26). */ +uint8_t geohashEstimateStepsByRadius(double range_meters) { + uint8_t step = 1; + while (range_meters > 0 && range_meters < MERCATOR_MAX) { + range_meters *= 2; + step++; + } + step--; + if (!step) + step = 26; /* if range = 0, give max resolution */ + return step > 26 ? 26 : step; +} + +double geohashGetXWGS84(double x) { return merc_lon(x); } +double geohashGetYWGS84(double y) { return merc_lat(y); } + +double geohashGetXMercator(double longtitude) { + if (longtitude > 180 || longtitude < -180) { + return longtitude; + } + return mercator_x(longtitude); +} +double geohashGetYMercator(double latitude) { + if (latitude > 90 || latitude < -90) { + return latitude; + } + return mercator_y(latitude); +} + +int geohashBitsComparator(const GeoHashBits *a, const GeoHashBits *b) { + /* If step not equal, compare on step. Else, compare on bits. */ + return a->step != b->step ? a->step - b->step : a->bits - b->bits; +} + +bool geohashBoundingBox(double latitude, double longitude, double radius_meters, + double *bounds) { + if (!bounds) + return false; + + double latr, lonr; + latr = deg_rad(latitude); + lonr = deg_rad(longitude); + + double distance = radius_meters / EARTH_RADIUS_IN_METERS; + double min_latitude = latr - distance; + double max_latitude = latr + distance; + + /* Note: we're being lazy and not accounting for coordinates near poles */ + double min_longitude, max_longitude; + double difference_longitude = asin(sin(distance) / cos(latr)); + min_longitude = lonr - difference_longitude; + max_longitude = lonr + difference_longitude; + + bounds[0] = rad_deg(min_latitude); + bounds[1] = rad_deg(min_longitude); + bounds[2] = rad_deg(max_latitude); + bounds[3] = rad_deg(max_longitude); + + return true; +} + +GeoHashRadius geohashGetAreasByRadius(uint8_t coord_type, double latitude, + double longitude, double radius_meters) { + GeoHashRange lat_range, long_range; + GeoHashRadius radius = { { 0 } }; + GeoHashBits hash = { 0 }; + GeoHashNeighbors neighbors = { { 0 } }; + GeoHashArea area = { { 0 } }; + double delta_longitude, delta_latitude; + double min_lat, max_lat, min_lon, max_lon; + int steps; + + if (coord_type == GEO_WGS84_TYPE) { + double bounds[4]; + geohashBoundingBox(latitude, longitude, radius_meters, bounds); + min_lat = bounds[0]; + min_lon = bounds[1]; + max_lat = bounds[2]; + max_lon = bounds[3]; + } else { + delta_latitude = delta_longitude = radius_meters; + min_lat = latitude - delta_latitude; + max_lat = latitude + delta_latitude; + min_lon = longitude - delta_longitude; + max_lon = longitude + delta_longitude; + } + + steps = geohashEstimateStepsByRadius(radius_meters); + + geohashGetCoordRange(coord_type, &lat_range, &long_range); + geohashEncode(&lat_range, &long_range, latitude, longitude, steps, &hash); + geohashNeighbors(&hash, &neighbors); + geohashDecode(lat_range, long_range, hash, &area); + + if (area.latitude.min < min_lat) { + GZERO(neighbors.south); + GZERO(neighbors.south_west); + GZERO(neighbors.south_east); + } + if (area.latitude.max > max_lat) { + GZERO(neighbors.north); + GZERO(neighbors.north_east); + GZERO(neighbors.north_west); + } + if (area.longitude.min < min_lon) { + GZERO(neighbors.west); + GZERO(neighbors.south_west); + GZERO(neighbors.north_west); + } + if (area.longitude.max > max_lon) { + GZERO(neighbors.east); + GZERO(neighbors.south_east); + GZERO(neighbors.north_east); + } + radius.hash = hash; + radius.neighbors = neighbors; + radius.area = area; + return radius; +} + +GeoHashRadius geohashGetAreasByRadiusWGS84(double latitude, double longitude, + double radius_meters) { + return geohashGetAreasByRadius(GEO_WGS84_TYPE, latitude, longitude, + radius_meters); +} + +GeoHashRadius geohashGetAreasByRadiusMercator(double latitude, double longitude, + double radius_meters) { + return geohashGetAreasByRadius(GEO_MERCATOR_TYPE, latitude, longitude, + radius_meters); +} + +GeoHashFix52Bits geohashAlign52Bits(const GeoHashBits hash) { + uint64_t bits = hash.bits; + bits <<= (52 - hash.step * 2); + return bits; +} + +/* calculate distance using haversin great circle distance formula */ +double distanceEarth(double lat1d, double lon1d, double lat2d, double lon2d) { + double lat1r, lon1r, lat2r, lon2r, u, v; + lat1r = deg_rad(lat1d); + lon1r = deg_rad(lon1d); + lat2r = deg_rad(lat2d); + lon2r = deg_rad(lon2d); + u = sin((lat2r - lat1r) / 2); + v = sin((lon2r - lon1r) / 2); + return 2.0 * EARTH_RADIUS_IN_METERS * + asin(sqrt(u * u + cos(lat1r) * cos(lat2r) * v * v)); +} + +bool geohashGetDistanceIfInRadius(uint8_t coord_type, double x1, double y1, + double x2, double y2, double radius, + double *distance) { + if (coord_type == GEO_WGS84_TYPE) { + *distance = distanceEarth(y1, x1, y2, x2); + if (*distance > radius) { + return false; + } + } else { + double xx = (x1 - x2) * (x1 - x2); + double yy = (y1 - y2) * (y1 - y2); + double dd = xx + yy; + *distance = dd; + if (dd > (radius * radius)) { + return false; + } + } + return true; +} + +bool geohashGetDistanceIfInRadiusWGS84(double x1, double y1, double x2, + double y2, double radius, + double *distance) { + return geohashGetDistanceIfInRadius(GEO_WGS84_TYPE, x1, y1, x2, y2, radius, + distance); +} + +bool geohashGetDistanceSquaredIfInRadiusMercator(double x1, double y1, + double x2, double y2, + double radius, + double *distance) { + return geohashGetDistanceIfInRadius(GEO_MERCATOR_TYPE, x1, y1, x2, y2, + radius, distance); +} + +bool geohashVerifyCoordinates(uint8_t coord_type, double x, double y) { + GeoHashRange lat_range, long_range; + geohashGetCoordRange(coord_type, &lat_range, &long_range); + + if (x < long_range.min || x > long_range.max || y < lat_range.min || + y > lat_range.max) { + return false; + } + return true; +} diff --git a/deps/geohash-int/geohash_helper.h b/deps/geohash-int/geohash_helper.h new file mode 100644 index 00000000..b72e5144 --- /dev/null +++ b/deps/geohash-int/geohash_helper.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2013-2014, yinqiwen + * Copyright (c) 2014, Matt Stancliff . + * 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. + */ + +#ifndef GEOHASH_HELPER_HPP_ +#define GEOHASH_HELPER_HPP_ + +#include +#include +#include "geohash.h" + +#define GZERO(s) s.bits = s.step = 0; +#define GISZERO(s) (!s.bits && !s.step) +#define GISNOTZERO(s) (s.bits || s.step) + +typedef uint64_t GeoHashFix52Bits; +typedef uint64_t GeoHashVarBits; + +typedef struct { + GeoHashBits hash; + GeoHashArea area; + GeoHashNeighbors neighbors; +} GeoHashRadius; + +int GeoHashBitsComparator(const GeoHashBits *a, const GeoHashBits *b); +uint8_t geohashEstimateStepsByRadius(double range_meters); +bool geohashBoundingBox(double latitude, double longitude, double radius_meters, + double *bounds); +GeoHashRadius geohashGetAreasByRadius(uint8_t coord_type, double latitude, + double longitude, double radius_meters); +GeoHashRadius geohashGetAreasByRadiusWGS84(double latitude, double longitude, + double radius_meters); +GeoHashRadius geohashGetAreasByRadiusMercator(double latitude, double longitude, + double radius_meters); +GeoHashFix52Bits geohashAlign52Bits(const GeoHashBits hash); +double geohashGetXMercator(double longtitude); +double geohashGetYMercator(double latitude); +double geohashGetXWGS84(double x); +double geohashGetYWGS84(double y); +bool geohashVerifyCoordinates(uint8_t coord_type, double x, double y); +bool geohashGetDistanceIfInRadius(uint8_t coord_type, double x1, double y1, + double x2, double y2, double radius, + double *distance); +bool geohashGetDistanceIfInRadiusWGS84(double x1, double y1, double x2, + double y2, double radius, + double *distance); +bool geohashGetDistanceSquaredIfInRadiusMercator(double x1, double y1, + double x2, double y2, + double radius, + double *distance); + +#endif /* GEOHASH_HELPER_HPP_ */ diff --git a/src/Makefile b/src/Makefile index 271ab34d..054857ca 100644 --- a/src/Makefile +++ b/src/Makefile @@ -14,8 +14,8 @@ release_hdr := $(shell sh -c './mkreleasehdr.sh') uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') -OPTIMIZATION?=-O2 -DEPENDENCY_TARGETS=hiredis linenoise lua +#OPTIMIZATION?=-O2 +DEPENDENCY_TARGETS=hiredis linenoise lua geohash-int # Default settings STD=-std=c99 -pedantic -DREDIS_STATIC='' @@ -53,7 +53,7 @@ endif # Override default settings if possible -include .make-settings -FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(REDIS_CFLAGS) +FINAL_CFLAGS=$(STD) $(WARN) $(OPT) $(DEBUG) $(CFLAGS) $(REDIS_CFLAGS) -I../deps/geohash-int FINAL_LDFLAGS=$(LDFLAGS) $(REDIS_LDFLAGS) $(DEBUG) FINAL_LIBS=-lm DEBUG=-g -ggdb @@ -117,7 +117,8 @@ endif REDIS_SERVER_NAME=redis-server REDIS_SENTINEL_NAME=redis-sentinel -REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o +REDIS_SERVER_OBJ=adlist.o quicklist.o ae.o anet.o dict.o redis.o sds.o zmalloc.o lzf_c.o lzf_d.o pqsort.o zipmap.o sha1.o ziplist.o release.o networking.o util.o object.o db.o replication.o rdb.o t_string.o t_list.o t_set.o t_zset.o t_hash.o config.o aof.o pubsub.o multi.o debug.o sort.o intset.o syncio.o cluster.o crc16.o endianconv.o slowlog.o scripting.o bio.o rio.o rand.o memtest.o crc64.o bitops.o sentinel.o notify.o setproctitle.o blocked.o hyperloglog.o latency.o sparkline.o redis-check-rdb.o geo.o zset.o geojson.o +REDIS_GEOHASH_OBJ=../deps/geohash-int/geohash.o ../deps/geohash-int/geohash_helper.o REDIS_CLI_NAME=redis-cli REDIS_CLI_OBJ=anet.o sds.o adlist.o redis-cli.o zmalloc.o release.o anet.o ae.o crc64.o REDIS_BENCHMARK_NAME=redis-benchmark @@ -171,7 +172,7 @@ endif # redis-server $(REDIS_SERVER_NAME): $(REDIS_SERVER_OBJ) - $(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a $(FINAL_LIBS) + $(REDIS_LD) -o $@ $^ ../deps/hiredis/libhiredis.a ../deps/lua/src/liblua.a $(REDIS_GEOHASH_OBJ) $(FINAL_LIBS) # redis-sentinel $(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME) diff --git a/src/geo.c b/src/geo.c new file mode 100644 index 00000000..36e87eef --- /dev/null +++ b/src/geo.c @@ -0,0 +1,749 @@ +/* + * Copyright (c) 2014, Matt Stancliff . + * 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 "geojson.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 + * ==================================================================== */ +static inline bool decodeGeohash(double bits, double *latlong) { + 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 */ +static inline bool extractLatLongOrReply(redisClient *c, robj **argv, + double *latlong) { + for (int i = 0; i < 2; i++) { + if (getDoubleFromObjectOrReply(c, argv[i], latlong + i, NULL) != + REDIS_OK) { + return false; + } + } + return true; +} + +/* Input Argument Helper */ +/* Decode lat/long from a zset member's score */ +static bool latLongFromMember(robj *zobj, robj *member, double *latlong) { + double score = 0; + + if (!zsetScore(zobj, member, &score)) + return false; + + if (!decodeGeohash(score, latlong)) + return false; + + return true; +} + +/* 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; +} + +/* Output Reply Helper */ +static void latLongToGeojsonAndReply(redisClient *c, struct geojsonPoint *gp, + char *units) { + sds geojson = geojsonLatLongToPointFeature( + gp->latitude, gp->longitude, gp->set, gp->member, gp->dist, units); + + addReplyBulkCBuffer(c, geojson, sdslen(geojson)); + sdsfree(geojson); +} + +/* Output Reply Helper */ +static void decodeGeohashToGeojsonBoundsAndReply(redisClient *c, + uint64_t hashbits, + struct geojsonPoint *gp) { + GeoHashArea area = { { 0 } }; + GeoHashBits hash = { .bits = hashbits, .step = GEO_STEP_MAX }; + + geohashDecodeWGS84(hash, &area); + + sds geojson = geojsonBoxToPolygonFeature( + area.latitude.min, area.longitude.min, area.latitude.max, + area.longitude.max, gp->set, gp->member); + addReplyBulkCBuffer(c, geojson, sdslen(geojson)); + sdsfree(geojson); +} + +/* The defailt addReplyDouble has too much accuracy. We use this + * for returning location distances. "5.21 meters away" is nicer + * than "5.2144992818115 meters away." */ +static inline void addReplyDoubleNicer(redisClient *c, double d) { + char dbuf[128] = { 0 }; + int dlen = snprintf(dbuf, sizeof(dbuf), "%.2f", d); + addReplyBulkCBuffer(c, dbuf, dlen); +} + +/* Output Reply Helper */ +static void replyGeojsonCollection(redisClient *c, struct geojsonPoint *gp, + long result_length, char *units) { + sds geojson = geojsonFeatureCollection(gp, result_length, units); + addReplyBulkCBuffer(c, geojson, sdslen(geojson)); + sdsfree(geojson); +} + +/* 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]; + + 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 (int i = 0; i < sizeof(neighbors) / sizeof(*neighbors); i++) { + 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); + GeoHashArea area = { { 0 } }; + 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; +} + +/* With no subscribers, each call of this function adds a median latency of 2 + * microseconds. */ +/* We aren't participating in any keyspace/keyevent notifications other than + * what's provided by the underlying zset itself, but it's probably not useful + * for clients to get the 52-bit integer geohash as an "update" value. */ +static int publishLocationUpdate(const sds zset, const sds member, + const double latitude, + const double longitude) { + int published; + + /* event is: " " */ + sds event = sdscatprintf(sdsempty(), "%.7f %.7f", latitude, longitude); + robj *eventobj = createObject(REDIS_STRING, event); + + /* channel is: __geo:: */ + /* If you want all events for this zset then just psubscribe + * to "__geo::*" */ + sds chan = sdsnewlen("__geo:", 6); + chan = sdscatsds(chan, zset); + chan = sdscatlen(chan, ":", 1); + chan = sdscatsds(chan, member); + robj *chanobj = createObject(REDIS_STRING, chan); + + published = pubsubPublishMessage(chanobj, eventobj); + + decrRefCount(chanobj); + decrRefCount(eventobj); + + return published; +} + +/* Sort comparators for qsort() */ +static int sort_gp_asc(const void *a, const void *b) { + const struct geojsonPoint *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 + * ==================================================================== */ +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); + publishLocationUpdate(key->ptr, val->ptr, latitude, longitude); + } + + /* 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; + } + + sds units = c->argv[base_args - 2 + 1]->ptr; + + /* Discover and populate all optional parameters. */ + bool withdist = false, withhash = false, withcoords = false, + withgeojson = false, withgeojsonbounds = false, + withgeojsoncollection = false, noproperties = false; + 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)) + withdist = true; + else if (!strcasecmp(arg, "withhash")) + withhash = true; + else if (!strncasecmp(arg, "withcoord", 9)) + withcoords = true; + else if (!strncasecmp(arg, "withgeojsonbound", 16)) + withgeojsonbounds = true; + else if (!strncasecmp(arg, "withgeojsoncollection", 21)) + withgeojsoncollection = true; + else if (!strncasecmp(arg, "withgeo", 7) || + !strcasecmp(arg, "geojson") || !strcasecmp(arg, "json") || + !strcasecmp(arg, "withjson")) + withgeojson = true; + else if (!strncasecmp(arg, "noprop", 6) || + !strncasecmp(arg, "withoutprop", 11)) + noproperties = true; + 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; + } + } + } + + bool withgeo = withgeojsonbounds || withgeojsoncollection || withgeojson; + + /* 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++; + + if (withgeojson) + option_length++; + + if (withgeojsonbounds) + 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, result_length + withgeojsoncollection); + + /* Iterate over results, populate struct used for sorting and result sending + */ + listIter li; + listRewind(found_matches, &li); + struct geojsonPoint gp[result_length]; + /* 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; + + /* The layout of geojsonPoint allows us to pass the start offset + * 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); + if (withgeo && !noproperties) + gp[i].member = sdscatprintf(sdsempty(), "%llu", zr->val.v); + break; + case ZR_STRING: + addReplyBulkCBuffer(c, zr->val.s, sdslen(zr->val.s)); + if (withgeo && !noproperties) + gp[i].member = sdsdup(zr->val.s); + break; + } + + if (withdist) + addReplyDoubleNicer(c, gp[i].dist); + + if (withhash) + addReplyLongLong(c, zr->score); + + if (withcoords) { + addReplyMultiBulkLen(c, 2); + addReplyDouble(c, gp[i].latitude); + addReplyDouble(c, gp[i].longitude); + } + + if (withgeojson) + latLongToGeojsonAndReply(c, gp + i, units); + + if (withgeojsonbounds) + decodeGeohashToGeojsonBoundsAndReply(c, zr->score, gp + i); + } + + if (withgeojsoncollection) + replyGeojsonCollection(c, gp, result_length, units); + + if (withgeo && !noproperties) + for (int i = 0; i < result_length; i++) + sdsfree(gp[i].member); + + 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) { + /* args 0-1: ["geodecode", geohash]; + * optional: [geojson] */ + + GeoHashBits geohash; + if (getLongLongFromObjectOrReply(c, c->argv[1], (long long *)&geohash.bits, + NULL) != REDIS_OK) + return; + + bool withgeojson = false; + if (c->argc == 3) + withgeojson = true; + + 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 */ + addReplyMultiBulkLen(c, 3 + withgeojson * 2); + + /* 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); + + if (withgeojson) { + struct geojsonPoint gp = { .latitude = y, + .longitude = x, + .member = NULL }; + + /* Return geojson Feature Point */ + latLongToGeojsonAndReply(c, &gp, NULL); + + /* Return geojson Feature Polygon */ + decodeGeohashToGeojsonBoundsAndReply(c, geohash.bits, &gp); + } +} + +void geoEncodeCommand(redisClient *c) { + /* args 0-2: ["geoencode", lat, long]; + * optionals: [radius, units] + * - AND / OR - + * optional: [geojson] */ + + bool withgeojson = false; + for (int i = 3; i < c->argc; i++) { + char *arg = c->argv[i]->ptr; + if (!strncasecmp(arg, "withgeo", 7) || !strcasecmp(arg, "geojson") || + !strcasecmp(arg, "json") || !strcasecmp(arg, "withjson")) { + withgeojson = true; + break; + } + } + + double radius_meters = 0; + if (c->argc >= 5) { + if ((radius_meters = extractDistanceOrReply(c, c->argv + 3, NULL)) < + 0) { + return; + } + } else if (c->argc == 4 && !withgeojson) { + addReplyError(c, "must provide units when asking for radius encode"); + return; + } + + double latlong[2]; + if (!extractLatLongOrReply(c, c->argv + 1, latlong)) + return; + + /* 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; + + /* Return four nested multibulk replies with optional geojson returns */ + addReplyMultiBulkLen(c, 4 + withgeojson * 2); + + /* 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); + + if (withgeojson) { + struct geojsonPoint gp = { .latitude = y, + .longitude = x, + .member = NULL }; + + /* Return geojson Feature Point */ + latLongToGeojsonAndReply(c, &gp, NULL); + + /* Return geojson Feature Polygon (bounding box for this step size) */ + /* We don't use the helper function here because we can't re-calculate + * the area if we have a non-GEO_STEP_MAX step size. */ + sds geojson = geojsonBoxToPolygonFeature( + area.latitude.min, area.longitude.min, area.latitude.max, + area.longitude.max, gp.set, gp.member); + addReplyBulkCBuffer(c, geojson, sdslen(geojson)); + sdsfree(geojson); + } +} diff --git a/src/geo.h b/src/geo.h new file mode 100644 index 00000000..f8207166 --- /dev/null +++ b/src/geo.h @@ -0,0 +1,12 @@ +#ifndef __GEO_H__ +#define __GEO_H__ + +#include "redis.h" + +void geoEncodeCommand(redisClient *c); +void geoDecodeCommand(redisClient *c); +void geoRadiusByMemberCommand(redisClient *c); +void geoRadiusCommand(redisClient *c); +void geoAddCommand(redisClient *c); + +#endif diff --git a/src/geojson.c b/src/geojson.c new file mode 100644 index 00000000..bb0befc9 --- /dev/null +++ b/src/geojson.c @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2014, Matt Stancliff . + * 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 "geojson.h" + +#define L server.lua + +/* ==================================================================== + * The Encoder + * ==================================================================== */ +static sds jsonEncode() { + /* When entering this function, stack is: [1:[geojson table to encode]] */ + lua_getglobal(L, "cjson"); + lua_getfield(L, -1, "encode"); + + /* Stack is now: [1:[geojson table], 2:'cjson', 3:'encode'] */ + + /* Move current top ('encode') to bottom of stack */ + lua_insert(L, 1); + + /* Move current top ('cjson') to bottom of stack so we can 'cjson.encode' */ + lua_insert(L, 1); + + /* Stack is now: [1:'cjson', 2:'encode', 3:[table of geojson to encode]] */ + + /* Call cjson.encode on the element above it on the stack; + * obtain one return value */ + if (lua_pcall(L, 1, 1, 0) != 0) + redisLog(REDIS_WARNING, "Could not encode geojson: %s", + lua_tostring(L, -1)); + + sds geojson = sdsnew(lua_tostring(L, -1)); + + /* We're done. Remove entire stack. Drop mic. Walk away. */ + lua_pop(L, lua_gettop(L)); + + /* Return sds the caller must sdsfree() on their own */ + return geojson; +} + +/* ==================================================================== + * The Lua Helpers + * ==================================================================== */ +static inline void luaCreateFieldFromPrevious(const char *field) { + lua_setfield(L, -2, field); +} + +static inline void luaCreateFieldStr(const char *field, const char *value) { + lua_pushstring(L, value); + luaCreateFieldFromPrevious(field); +} + +/* Creates [Lat, Long] array attached to "coordinates" key */ +static void luaCreateCoordinates(const double x, const double y) { + /* Create array table with two elements */ + lua_createtable(L, 2, 0); + + lua_pushnumber(L, x); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, y); + lua_rawseti(L, -2, 2); +} + +static void luaCreatePropertyNull(void) { + /* Create empty table and give it a name. This is a json {} value. */ + lua_createtable(L, 0, 0); + luaCreateFieldFromPrevious("properties"); +} + +static void _luaCreateProperties(const char *k1, const char *v1, const char *k2, + const char *v2, const int noclose) { + /* we may add additional properties outside of here, so newtable instead of + * fixed-size createtable */ + lua_newtable(L); + + luaCreateFieldStr(k1, v1); + luaCreateFieldStr(k2, v2); + + if (!noclose) + luaCreateFieldFromPrevious("properties"); +} + +static void luaCreateProperties(const char *k1, const char *v1, const char *k2, + const char *v2) { + _luaCreateProperties(k1, v1, k2, v2, 0); +} + +/* ==================================================================== + * The Lua Aggregation Helpers + * ==================================================================== */ +static void attachProperties(const char *set, const char *member) { + if (member) + luaCreateProperties("set", set, "member", member); + else + luaCreatePropertyNull(); +} + +static void attachPropertiesWithDist(const char *set, const char *member, + double dist, const char *units) { + if (member) { + _luaCreateProperties("set", set, "member", member, 1); + if (units) { + /* Add units then distance. After encoding it comes + * out as distance followed by units in the json. */ + lua_pushstring(L, units); + luaCreateFieldFromPrevious("units"); + lua_pushnumber(L, dist); + luaCreateFieldFromPrevious("distance"); + } + + /* We requested to leave the properties table open, but now we + * are done and can close it. */ + luaCreateFieldFromPrevious("properties"); + } else { + luaCreatePropertyNull(); + } +} + +static void createGeometryPoint(const double x, const double y) { + lua_createtable(L, 0, 2); + + /* coordinates = [x, y] */ + luaCreateCoordinates(x, y); + luaCreateFieldFromPrevious("coordinates"); + + /* type = Point */ + luaCreateFieldStr("type", "Point"); + + /* geometry = (coordinates = [x, y]) */ + luaCreateFieldFromPrevious("geometry"); +} + +static void createGeometryBox(const double x1, const double y1, const double x2, + const double y2) { + lua_createtable(L, 0, 2); + + /* Result = [[[x1,y1],[x2,y1],[x2,y2],[x1,y2], [x1,y1]] */ + /* The end coord is the start coord to make a closed polygon */ + lua_createtable(L, 1, 0); + lua_createtable(L, 5, 0); + + /* Bottom left */ + luaCreateCoordinates(x1, y1); + lua_rawseti(L, -2, 1); + + /* Top Left */ + luaCreateCoordinates(x2, y1); + lua_rawseti(L, -2, 2); + + /* Top Right */ + luaCreateCoordinates(x2, y2); + lua_rawseti(L, -2, 3); + + /* Bottom Right */ + luaCreateCoordinates(x1, y2); + lua_rawseti(L, -2, 4); + + /* Bottom Left (Again) */ + luaCreateCoordinates(x1, y1); + lua_rawseti(L, -2, 5); + + /* Set the outer array of our inner array of the inner coords */ + lua_rawseti(L, -2, 1); + + /* Bundle those together in coordinates: [a, b, c, d] */ + luaCreateFieldFromPrevious("coordinates"); + + /* Add type field */ + luaCreateFieldStr("type", "Polygon"); + + luaCreateFieldFromPrevious("geometry"); +} + +static void createFeature() { + /* Features have three fields: type, geometry, and properties */ + lua_createtable(L, 0, 3); + + luaCreateFieldStr("type", "Feature"); + + /* You must call attachProperties on your own */ +} + +static void createCollection(size_t size) { + /* FeatureCollections have two fields: type and features */ + lua_createtable(L, 0, 2); + + luaCreateFieldStr("type", "FeatureCollection"); +} + +static void pointsToCollection(const struct geojsonPoint *pts, const size_t len, + const char *units) { + createCollection(len); + + lua_createtable(L, len, 0); + for (int i = 0; i < len; i++) { + createFeature(); + createGeometryPoint(pts[i].longitude, pts[i].latitude); /* x, y */ + attachPropertiesWithDist(pts[i].set, pts[i].member, pts[i].dist, units); + lua_rawseti(L, -2, i + 1); /* Attach this Feature to "features" array */ + } + luaCreateFieldFromPrevious("features"); +} + +static void latLongToPointFeature(const double latitude, + const double longitude) { + createFeature(); + createGeometryPoint(longitude, latitude); /* geojson is: x,y */ +} + +static void squareToPolygonFeature(const double x1, const double y1, + const double x2, const double y2) { + createFeature(); + createGeometryBox(x1, y1, x2, y2); +} + +/* ==================================================================== + * The Interface Functions + * ==================================================================== */ +sds geojsonFeatureCollection(const struct geojsonPoint *pts, const size_t len, + const char *units) { + pointsToCollection(pts, len, units); + return jsonEncode(); +} + +sds geojsonLatLongToPointFeature(const double latitude, const double longitude, + const char *set, const char *member, + const double dist, const char *units) { + latLongToPointFeature(latitude, longitude); + attachPropertiesWithDist(set, member, dist, units); + return jsonEncode(); +} + +sds geojsonBoxToPolygonFeature(const double y1, const double x1, + const double y2, const double x2, + const char *set, const char *member) { + squareToPolygonFeature(x1, y1, x2, y2); + attachProperties(set, member); + return jsonEncode(); +} diff --git a/src/geojson.h b/src/geojson.h new file mode 100644 index 00000000..55993bea --- /dev/null +++ b/src/geojson.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014, Matt Stancliff . + * 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. + */ + +#ifndef __GEOJSON_H__ +#define __GEOJSON_H__ + +#include "redis.h" +#include "geohash_helper.h" + +struct geojsonPoint { + double latitude; + double longitude; + double dist; + char *set; + char *member; + void *userdata; +}; + +sds geojsonLatLongToPointFeature(const double latitude, const double longitude, + const char *set, const char *member, + const double dist, const char *units); +sds geojsonBoxToPolygonFeature(const double x1, const double y1, + const double x2, const double y2, + const char *set, const char *member); +sds geojsonFeatureCollection(const struct geojsonPoint *pts, const size_t len, + const char *units); + +#endif diff --git a/src/redis.c b/src/redis.c index 09653119..359ccb35 100644 --- a/src/redis.c +++ b/src/redis.c @@ -282,6 +282,11 @@ struct redisCommand redisCommandTable[] = { {"bitpos",bitposCommand,-3,"r",0,NULL,1,1,1,0,0}, {"wait",waitCommand,3,"rs",0,NULL,0,0,0,0,0}, {"command",commandCommand,0,"rlt",0,NULL,0,0,0,0,0}, + {"geoadd",geoAddCommand,-5,"wm",0,NULL,1,1,1,0,0}, + {"georadius",geoRadiusCommand,-6,"r",0,NULL,1,1,1,0,0}, + {"georadiusbymember",geoRadiusByMemberCommand,-5,"r",0,NULL,1,1,1,0,0}, + {"geoencode",geoEncodeCommand,-3,"r",0,NULL,0,0,0,0,0}, + {"geodecode",geoDecodeCommand,-2,"r",0,NULL,0,0,0,0,0}, {"pfselftest",pfselftestCommand,1,"r",0,NULL,0,0,0,0,0}, {"pfadd",pfaddCommand,-2,"wmF",0,NULL,1,1,1,0,0}, {"pfcount",pfcountCommand,-2,"r",0,NULL,1,1,1,0,0}, diff --git a/src/redis.h b/src/redis.h index 53f3967d..2344c9eb 100644 --- a/src/redis.h +++ b/src/redis.h @@ -1556,6 +1556,11 @@ void bitcountCommand(redisClient *c); void bitposCommand(redisClient *c); void replconfCommand(redisClient *c); void waitCommand(redisClient *c); +void geoEncodeCommand(redisClient *c); +void geoDecodeCommand(redisClient *c); +void geoRadiusByMemberCommand(redisClient *c); +void geoRadiusCommand(redisClient *c); +void geoAddCommand(redisClient *c); void pfselftestCommand(redisClient *c); void pfaddCommand(redisClient *c); void pfcountCommand(redisClient *c); @@ -1588,4 +1593,8 @@ void redisLogHexDump(int level, char *descr, void *value, size_t len); #define redisDebugMark() \ printf("-- MARK %s:%d --\n", __FILE__, __LINE__) +/***** TEMPORARY *******/ +#include "zset.h" +/***** END TEMPORARY *******/ + #endif diff --git a/src/t_zset.c b/src/t_zset.c index dbc561a9..b900a9cc 100644 --- a/src/t_zset.c +++ b/src/t_zset.c @@ -213,7 +213,7 @@ static int zslValueGteMin(double value, zrangespec *spec) { return spec->minex ? (value > spec->min) : (value >= spec->min); } -static int zslValueLteMax(double value, zrangespec *spec) { +int zslValueLteMax(double value, zrangespec *spec) { return spec->maxex ? (value < spec->max) : (value <= spec->max); } diff --git a/src/zset.c b/src/zset.c new file mode 100644 index 00000000..2412a1c4 --- /dev/null +++ b/src/zset.c @@ -0,0 +1,185 @@ +#include "zset.h" + +/* t_zset.c prototypes (there's no t_zset.h) */ +unsigned char *zzlFirstInRange(unsigned char *zl, zrangespec *range); +unsigned char *zzlFind(unsigned char *zl, robj *ele, double *score); +int zzlLexValueLteMax(unsigned char *p, zlexrangespec *spec); + +/* Converted from static in t_zset.c: */ +int zslValueLteMax(double value, zrangespec *spec); + +/* ==================================================================== + * Direct Redis DB Interaction + * ==================================================================== */ + +/* zset access is mostly a copy/paste from zscoreCommand() */ +bool zsetScore(robj *zobj, robj *member, double *score) { + if (!zobj || !member) + return false; + + if (zobj->encoding == REDIS_ENCODING_ZIPLIST) { + if (zzlFind(zobj->ptr, member, score) == NULL) + return false; + } else if (zobj->encoding == REDIS_ENCODING_SKIPLIST) { + zset *zs = zobj->ptr; + dictEntry *de; + + member = tryObjectEncoding(member); + de = dictFind(zs->dict, member); + if (de != NULL) { + *score = *(double *)dictGetVal(de); + } else + return false; + } else { + return false; + } + return true; +} + +/* Largely extracted from genericZrangebyscoreCommand() in t_zset.c */ +/* The zrangebyscoreCommand expects to only operate on a live redisClient, + * but we need results returned to us, not sent over an async socket. */ +list *geozrangebyscore(robj *zobj, double min, double max, int 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 }; + list *l = NULL; /* result list */ + + if (zobj->encoding == REDIS_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 NULL; + } + + l = listCreate(); + + sptr = ziplistNext(zl, eptr); + + while (eptr && limit--) { + 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); + if (vstr == NULL) { + listAddNodeTail(l, result_long(score, vlong)); + } else { + listAddNodeTail(l, result_str(score, vstr, vlen)); + } + zzlNext(zl, &eptr, &sptr); + } + } else if (zobj->encoding == REDIS_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 NULL; + } + + l = listCreate(); + + while (ln && limit--) { + robj *o = ln->obj; + /* Abort when the node is no longer in range. */ + if (!zslValueLteMax(ln->score, &range)) + break; + + if (o->encoding == REDIS_ENCODING_INT) { + listAddNodeTail(l, result_long(ln->score, (long)o->ptr)); + } else { + listAddNodeTail(l, + result_str(ln->score, o->ptr, sdslen(o->ptr))); + } + + ln = ln->level[0].forward; + } + } + if (l) { + listSetFreeMethod(l, (void (*)(void *ptr)) & free_zipresult); + } + + return l; +} + +/* ==================================================================== + * Helpers + * ==================================================================== */ + +/* join 'join' to 'join_to' and free 'join' container */ +void listJoin(list *join_to, list *join) { + /* If the current list has zero size, move join to become join_to. + * If not, append the new list to the current list. */ + if (join_to->len == 0) { + join_to->head = join->head; + } else { + join_to->tail->next = join->head; + join->head->prev = join_to->tail; + join_to->tail = join->tail; + } + + /* Update total element count */ + join_to->len += join->len; + + /* Release original list container. Internal nodes were transferred over. */ + zfree(join); +} + +/* A ziplist member may be either a long long or a string. We create the + * contents of our return zipresult based on what the ziplist contained. */ +static struct zipresult *result(double score, long long v, unsigned char *s, + int len) { + struct zipresult *r = zmalloc(sizeof(*r)); + + /* If string and length, become a string. */ + /* Else, if not string or no length, become a long. */ + if (s && len >= 0) + r->type = ZR_STRING; + else if (!s || len < 0) + r->type = ZR_LONG; + + r->score = score; + switch (r->type) { + case(ZR_LONG) : + r->val.v = v; + break; + case(ZR_STRING) : + r->val.s = sdsnewlen(s, len); + break; + } + return r; +} + +struct zipresult *result_long(double score, long long v) { + return result(score, v, NULL, -1); +} + +struct zipresult *result_str(double score, unsigned char *str, int len) { + return result(score, 0, str, len); +} + +void free_zipresult(struct zipresult *r) { + if (!r) + return; + + switch (r->type) { + case(ZR_LONG) : + break; + case(ZR_STRING) : + sdsfree(r->val.s); + break; + } + + zfree(r); +} diff --git a/src/zset.h b/src/zset.h new file mode 100644 index 00000000..6a78d1ed --- /dev/null +++ b/src/zset.h @@ -0,0 +1,31 @@ +#ifndef __ZSET_H__ +#define __ZSET_H__ + +#include "redis.h" +#include + +#define ZR_LONG 1 +#define ZR_STRING 2 +struct zipresult { + double score; + union { + long long v; + sds s; + } val; + double distance; /* distance is in meters */ + char type; /* access type for the union */ +}; + +/* Redis DB Access */ +bool zsetScore(robj *zobj, robj *member, double *score); +list *geozrangebyscore(robj *zobj, double min, double max, int limit); + +/* New list operation: append one list to another */ +void listJoin(list *join_to, list *join); + +/* Helpers for returning zrangebyscore results */ +struct zipresult *result_str(double score, unsigned char *str, int len); +struct zipresult *result_long(double score, long long v); +void free_zipresult(struct zipresult *r); + +#endif diff --git a/tests/unit/geo.tcl b/tests/unit/geo.tcl new file mode 100644 index 00000000..015fcf87 --- /dev/null +++ b/tests/unit/geo.tcl @@ -0,0 +1,53 @@ +start_server {tags {"geo"}} { + test {GEOADD create} { + r geoadd nyc 40.747533 -73.9454966 "lic market" + } {1} + + test {GEOADD update} { + r geoadd nyc 40.747533 -73.9454966 "lic market" + } {0} + + test {GEOADD multi add} { + r geoadd nyc 40.7648057 -73.9733487 "central park n/q/r" 40.7362513 -73.9903085 "union square" 40.7126674 -74.0131604 "wtc one" 40.6428986 -73.7858139 "jfk" 40.7498929 -73.9375699 "q4" 40.7480973 -73.9564142 4545 + } {6} + + test {Check geoset values} { + r zrange nyc 0 -1 withscores + } {{wtc one} 1791873972053020 {union square} 1791875485187452 {central park n/q/r} 1791875761332224 4545 1791875796750882 {lic market} 1791875804419201 q4 1791875830079666 jfk 1791895905559723} + + test {GEORADIUS simple (sorted)} { + r georadius nyc 40.7598464 -73.9798091 3 km ascending + } {{central park n/q/r} 4545 {union square}} + + test {GEORADIUS withdistance (sorted)} { + r georadius nyc 40.7598464 -73.9798091 3 km withdistance ascending + } {{{central park n/q/r} 0.78} {4545 2.37} {{union square} 2.77}} + + test {GEORADIUSBYMEMBER simple (sorted)} { + r georadiusbymember nyc "wtc one" 7 km + } {{wtc one} {union square} {central park n/q/r} 4545 {lic market}} + + test {GEORADIUSBYMEMBER simple (sorted, json)} { + r georadiusbymember nyc "wtc one" 7 km withgeojson + } {{{wtc one} {{"type":"Feature","geometry":{"type":"Point","coordinates":[-74.01316255331,40.712667181451]},"properties":{"distance":0,"member":"wtc one","units":"km","set":"nyc"}}}}\ + {{union square} {{"type":"Feature","geometry":{"type":"Point","coordinates":[-73.990310132504,40.736250227118]},"properties":{"distance":3.2543954573354,"member":"union square","units":"km","set":"nyc"}}}}\ + {{central park n/q/r} {{"type":"Feature","geometry":{"type":"Point","coordinates":[-73.973347842693,40.764806395699]},"properties":{"distance":6.7000029092796,"member":"central park n\/q\/r","units":"km","set":"nyc"}}}}\ + {4545 {{"type":"Feature","geometry":{"type":"Point","coordinates":[-73.956412374973,40.748097513816]},"properties":{"distance":6.1975173818008,"member":"4545","units":"km","set":"nyc"}}}}\ + {{lic market} {{"type":"Feature","geometry":{"type":"Point","coordinates":[-73.945495784283,40.747532270998]},"properties":{"distance":6.8968709532081,"member":"lic market","units":"km","set":"nyc"}}}}} + + test {GEORADIUSBYMEMBER withdistance (sorted)} { + r georadiusbymember nyc "wtc one" 7 km withdist + } {{{wtc one} 0.00} {{union square} 3.25} {{central park n/q/r} 6.70} {4545 6.20} {{lic market} 6.90}} + + test {GEOENCODE simple} { + r geoencode 41.2358883 1.8063239 + } {3471579339700058 {41.235888125243704 1.8063229322433472}\ + {41.235890659964866 1.806328296661377}\ + {41.235889392604285 1.8063256144523621}} + + test {GEODECODE simple} { + r geodecode 3471579339700058 + } {{41.235888125243704 1.8063229322433472}\ + {41.235890659964866 1.806328296661377}\ + {41.235889392604285 1.8063256144523621}} +}