/* * 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; }