diff --git a/.travis.yml b/.travis.yml index 3a65fe0..4d9dc75 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: before_script: - phpize - EXTRA_LDFLAGS="-precious-files-regex .libs/geospatial.gcno" LDFLAGS="-lgcov" CFLAGS="-Wall -ggdb3 -fno-strict-aliasing -coverage -O0" ./configure --enable-geospatial - - make -j 5 test && if ls tests/*.diff >/dev/null 2>&1; then echo "Tests failed" && exit 1; fi + - make -j 5 test && if ls tests/*.diff >/dev/null 2>&1; then echo "Tests failed" && cat tests/*.diff && exit 1; fi - gcov --object-directory .libs *.c - bash <(curl -s https://codecov.io/bash) diff --git a/README.rst b/README.rst index d3f0732..c82c9d6 100644 --- a/README.rst +++ b/README.rst @@ -170,3 +170,29 @@ you would use:: $point2 = [ 'type' => 'Point', 'coordinates' => [ 15, 10 ] ]; var_dump(fraction_along_gc_line($point1, $point2, 0.25)); + +Geohashing +---------- + +The `geohash_encode` function can be used to convert GeoJSON Point to a geohash of a specific lenth (in this case, 12):: + + echo geohash_encode(array('type' => 'Point', 'coordinates' => [16.4, 48.2]), 12); + +Which outputs:: + + u2edjnw17enr + +Similarly, a hashed geopoint can be decoded using `geohash_decode` function:: + + var_dump(geohash_decode('u2edjnw17enr')); + array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(16.40000006184) + [1]=> + float(48.199999993667) + } + } \ No newline at end of file diff --git a/config.m4 b/config.m4 index 4843d11..3a7189f 100644 --- a/config.m4 +++ b/config.m4 @@ -5,5 +5,5 @@ PHP_ARG_ENABLE(geospatial, whether to enable geospatial support, [ --enable-geospatial Enable geospatial support]) if test "$PHP_GEOSPATIAL" != "no"; then - PHP_NEW_EXTENSION(geospatial, geospatial.c geo_array.c, $ext_shared) + PHP_NEW_EXTENSION(geospatial, geospatial.c geo_array.c geohash.c, $ext_shared) fi diff --git a/geo_array.c b/geo_array.c index 340bdb2..7076658 100644 --- a/geo_array.c +++ b/geo_array.c @@ -19,6 +19,8 @@ +----------------------------------------------------------------------+ */ +#ifndef PHP_GEO_ARRAY_H +#define PHP_GEO_ARRAY_H #include #include "geo_array.h" @@ -59,3 +61,4 @@ void geo_array_dtor(geo_array *points) free(points->y); free(points); } +#endif /* PHP_GEO_ARRAY_H */ diff --git a/geo_lat_long.h b/geo_lat_long.h new file mode 100644 index 0000000..3c48098 --- /dev/null +++ b/geo_lat_long.h @@ -0,0 +1,28 @@ +/* + +----------------------------------------------------------------------+ + | PHP Version 7 | + +----------------------------------------------------------------------+ + | Copyright (c) 2017 The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Emir Beganovic | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_GEO_LAT_LONG_H +#define PHP_GEO_LAT_LONG_H + +typedef struct { + double x; + double y; + double z; +} geo_lat_long; + +#endif diff --git a/geohash.c b/geohash.c new file mode 100644 index 0000000..13fb3c9 --- /dev/null +++ b/geohash.c @@ -0,0 +1,146 @@ +/* + +----------------------------------------------------------------------+ + | PHP Version 7 | + +----------------------------------------------------------------------+ + | Copyright (c) 2017 The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Emir Beganovic | + +----------------------------------------------------------------------+ +*/ + +#include + +#include "php.h" +#include "geo_lat_long.h" +#include "geohash.h" + +#define MAX_LAT 90.0 +#define MIN_LAT -90.0 + +#define MAX_LONG 180.0 +#define MIN_LONG -180.0 + +typedef struct interval_string { + double high; + double low; +} interval_struct; + +static char char_map[32] = "0123456789bcdefghjkmnpqrstuvwxyz"; + +char* php_geo_geohash_encode(double latitude, double longitude, int precision) +{ + char* hash; + int steps; + double coord, mid; + int is_even = 1; + unsigned int hash_char = 0; + int i; + interval_struct lat_interval = { MAX_LAT, MIN_LAT }; + interval_struct lng_interval = { MAX_LONG, MIN_LONG }; + interval_struct* interval; + + hash = (char*)safe_emalloc(precision, sizeof(char), 1); + + hash[precision] = '\0'; + steps = precision * 5.0; + + for (i = 1; i <= steps; i++) { + if (is_even) { + interval = &lng_interval; + coord = longitude; + } else { + interval = &lat_interval; + coord = latitude; + } + + mid = (interval->low + interval->high) / 2.0; + hash_char = hash_char << 1; + + if (coord > mid) { + interval->low = mid; + hash_char |= 0x01; + } else { + interval->high = mid; + } + + if (!(i % 5)) { + hash[(i - 1) / 5] = char_map[hash_char]; + hash_char = 0; + } + + is_even = !is_even; + } + + return hash; +} + +static unsigned int index_for_char(char c, char* string) +{ + unsigned int index = -1; + int string_amount = strlen(string); + int i; + + for (i = 0; i < string_amount; i++) { + if (c == string[i]) { + index = i; + break; + } + } + + return index; +} + +geo_lat_long php_geo_geohash_decode(char* hash) +{ + geo_lat_long coordinate; + int char_amount = strlen(hash); + + if (char_amount) { + + int charmap_index; + double delta; + int i, j; + + interval_struct lat_interval = { MAX_LAT, MIN_LAT }; + interval_struct lng_interval = { MAX_LONG, MIN_LONG }; + interval_struct* interval; + + int is_even = 1; + + for (i = 0; i < char_amount; i++) { + + charmap_index = index_for_char(hash[i], (char*)char_map); + + /* Interpret the last 5 bits of the integer */ + for (j = 0; j < 5; j++) { + interval = is_even ? &lng_interval : &lat_interval; + + delta = (interval->high - interval->low) / 2.0; + + if ((charmap_index << j) & 0x0010) { + interval->low += delta; + } else { + interval->high -= delta; + } + + is_even = !is_even; + } + } + + coordinate.x = lat_interval.high - ((lat_interval.high - lat_interval.low) / 2.0); + coordinate.y = lng_interval.high - ((lng_interval.high - lng_interval.low) / 2.0); + coordinate.z = 0; + } + + return coordinate; +} + +/* }}} */ diff --git a/geohash.h b/geohash.h new file mode 100644 index 0000000..31364d4 --- /dev/null +++ b/geohash.h @@ -0,0 +1,33 @@ +/* + +----------------------------------------------------------------------+ + | PHP Version 5/7 | + +----------------------------------------------------------------------+ + | Copyright (c) 2017 The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Emir Beganovic | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_GEOHASH_H +#define PHP_GEOHASH_H + +char* php_geo_geohash_encode(double lat, double lng, int precision); +geo_lat_long php_geo_geohash_decode(char* hash); +#endif /* PHP_GEOHASH_H */ + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: noet sw=4 ts=4 fdm=marker + * vim<600: noet sw=4 ts=4 + */ diff --git a/geospatial.c b/geospatial.c index 6a45ffe..e00693d 100644 --- a/geospatial.c +++ b/geospatial.c @@ -16,6 +16,7 @@ | Michael Maclean | | Nathaniel McHugh | | Marcus Deglos | + | Emir Beganovic | +----------------------------------------------------------------------+ */ @@ -28,6 +29,8 @@ #include "ext/standard/info.h" #include "php_geospatial.h" #include "geo_array.h" +#include "geo_lat_long.h" +#include "geohash.h" #include "Zend/zend_exceptions.h" #include "ext/spl/spl_exceptions.h" @@ -111,6 +114,16 @@ ZEND_BEGIN_ARG_INFO_EX(interpolate_polygon_args, 0, 0, 2) ZEND_ARG_INFO(0, epsilon) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(geohash_encode_args, 0, 0, 3) + ZEND_ARG_INFO(0, latitude) + ZEND_ARG_INFO(0, longitude) + ZEND_ARG_INFO(0, precision) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(geohash_decode_args, 0, 0, 1) + ZEND_ARG_INFO(0, geohash) +ZEND_END_ARG_INFO() + /* {{{ geospatial_functions[] * * Every user visible function must have an entry in geospatial_functions[]. @@ -129,6 +142,8 @@ const zend_function_entry geospatial_functions[] = { PHP_FE(rdp_simplify, rdp_simplify_args) PHP_FE(interpolate_linestring, interpolate_linestring_args) PHP_FE(interpolate_polygon, interpolate_polygon_args) + PHP_FE(geohash_encode, geohash_encode_args) + PHP_FE(geohash_decode, geohash_decode_args) /* End of functions */ { NULL, NULL, NULL } }; @@ -354,7 +369,7 @@ double php_geo_vincenty(double from_lat, double from_long, double to_lat, double sinLambda = sin(lambda); cosLambda = cos(lambda); sinSigma = sqrt((cosU2*sinLambda) * (cosU2*sinLambda) + - (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) * (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda)); cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; sigma = atan2(sinSigma, cosSigma); sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma; @@ -363,14 +378,14 @@ double php_geo_vincenty(double from_lat, double from_long, double to_lat, double C = eli.f / 16.0 * cos2Alpha * (4.0 + eli.f * (4.0 - 3.0 * cos2Alpha)); lambdaP = lambda; lambda = L + (1.0 - C) * eli.f * sinAlpha * - (sigma + C*sinSigma*(cosof2sigma+C*cosSigma*(-1.0 + 2.0 *cosof2sigma*cosof2sigma))); + (sigma + C*sinSigma*(cosof2sigma+C*cosSigma*(-1.0 + 2.0 *cosof2sigma*cosof2sigma))); --loopLimit; } while (fabs(lambda - lambdaP) > precision && loopLimit > 0); uSq = cos2Alpha * (eli.a * eli.a - eli.b * eli.b) / (eli.b * eli.b); A = 1.0 + uSq / 16384.0 * (4096.0 + uSq * (-768.0 + uSq * (320.0 - 175.0 * uSq))); B = uSq / 1024.0 * ( 256.0 + uSq * (-128.0 + uSq * (74.0 - 47.0 * uSq))); deltaSigma = B * sinSigma * (cosof2sigma+B/4.0 * (cosSigma * (-1.0 + 2.0 *cosof2sigma*cosof2sigma)- - B / 6.0 * cosof2sigma * (-3.0 + 4.0 *sinSigma*sinSigma) * (-3.0 + 4.0 *cosof2sigma*cosof2sigma))); + B / 6.0 * cosof2sigma * (-3.0 + 4.0 *sinSigma*sinSigma) * (-3.0 + 4.0 *cosof2sigma*cosof2sigma))); s = eli.b * A * (sigma - deltaSigma); s = floor(s * 1000) / 1000; return s; @@ -453,9 +468,9 @@ geo_lat_long cartesian_to_polar(double x, double y, double z, geo_ellipsoid eli) lambda = atan2(y ,x); h = p / cos(phi) - nu; - polar.latitude = phi / GEO_DEG_TO_RAD; - polar.longitude = lambda / GEO_DEG_TO_RAD; - polar.height = h; + polar.x = phi / GEO_DEG_TO_RAD; + polar.y = lambda / GEO_DEG_TO_RAD; + polar.z = h; return polar; } @@ -483,7 +498,7 @@ PHP_FUNCTION(dms_to_decimal) sign = strcmp(direction, "S") == 0 || strcmp(direction, "W") == 0 ? -1 : 1; } - decimal = abs(degrees) + minutes / 60 + seconds / 3600; + decimal = fabs(degrees) + minutes / 60 + seconds / 3600; decimal *= sign; RETURN_DOUBLE(decimal); } @@ -582,9 +597,9 @@ PHP_FUNCTION(cartesian_to_polar) geo_ellipsoid eli = get_ellipsoid(reference_ellipsoid); array_init(return_value); polar = cartesian_to_polar(x, y, z, eli); - add_assoc_double(return_value, "lat", polar.latitude); - add_assoc_double(return_value, "long", polar.longitude); - add_assoc_double(return_value, "height", polar.height); + add_assoc_double(return_value, "lat", polar.x); + add_assoc_double(return_value, "long", polar.y); + add_assoc_double(return_value, "height", polar.z); } /* }}} */ @@ -619,7 +634,7 @@ PHP_FUNCTION(transform_datum) add_assoc_double(return_value, "long", polar.longitude); add_assoc_double(return_value, "height", polar.height); */ - retval_point_from_coordinates(return_value, polar.longitude, polar.latitude); + retval_point_from_coordinates(return_value, polar.y, polar.x); } /* }}} */ @@ -714,7 +729,7 @@ double php_initial_bearing(double from_lat, double from_long, double to_lat, dou /* var y = Math.sin(dLon) * Math.cos(lat2); var x = Math.cos(lat1)*Math.sin(lat2) - - Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon); + Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon); var brng = Math.atan2(y, x).toDeg(); */ double x, y; @@ -1067,6 +1082,62 @@ PHP_FUNCTION(interpolate_polygon) } /* }}} */ + +/* {{{ string geohash_encode(GeoJSONPoint $point [, int $precision = 12]) + */ +PHP_FUNCTION(geohash_encode) +{ + double longitude, latitude; + +#if PHP_VERSION_ID >= 70000 + zend_long precision = 12; +#else + long precision = 12; +#endif + zval *geojson; + char* hash; + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "al", &geojson, &precision) == FAILURE) { + return; + } + + if (!geojson_point_to_lon_lat(geojson, &longitude, &latitude TSRMLS_CC)) { + RETURN_FALSE; + } + + + hash = php_geo_geohash_encode(latitude, longitude, precision); +#if PHP_VERSION_ID < 70000 + RETVAL_STRING(hash, 0); +#else + RETVAL_STRING(hash); + efree(hash); +#endif +} + +/* {{{ string geohash_decode(string $geohash) + */ +PHP_FUNCTION(geohash_decode) +{ + char* hash; + +#if PHP_VERSION_ID >= 70000 + size_t hash_len; +#else + int hash_len; +#endif + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &hash, &hash_len) == FAILURE) { + return; + } + + geo_lat_long area = php_geo_geohash_decode(hash); + + retval_point_from_coordinates(return_value, area.y, area.x); +} + +/* }}}*/ + /* * Local variables: * tab-width: 4 diff --git a/php_geospatial.h b/php_geospatial.h index 453355b..965d85b 100644 --- a/php_geospatial.h +++ b/php_geospatial.h @@ -39,12 +39,6 @@ extern zend_module_entry geospatial_module_entry; #include "TSRM.h" #endif -typedef struct { - double latitude; - double longitude; - double height; -} geo_lat_long; - typedef struct { double a; double b; @@ -143,6 +137,8 @@ PHP_FUNCTION(vincenty); PHP_FUNCTION(rdp_simplify); PHP_FUNCTION(interpolate_linestring); PHP_FUNCTION(interpolate_polygon); +PHP_FUNCTION(geohash_encode); +PHP_FUNCTION(geohash_decode); #endif /* PHP_GEOSPATIAL_H */ diff --git a/tests/geohash_decode.phpt b/tests/geohash_decode.phpt new file mode 100644 index 0000000..73ac618 --- /dev/null +++ b/tests/geohash_decode.phpt @@ -0,0 +1,191 @@ +--TEST-- +Test geohash_decode +--SKIPIF-- + +--FILE-- + +--EXPECT-- +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(157.5) + [1]=> + float(67.5) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(174.375) + [1]=> + float(47.8125) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(170.859375) + [1]=> + float(49.921875) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.03515625) + [1]=> + float(49.658203125) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.01318359375) + [1]=> + float(49.68017578125) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.02966308594) + [1]=> + float(49.671936035156) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.02897644043) + [1]=> + float(49.67399597168) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.0284614563) + [1]=> + float(49.674081802368) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.02861166) + [1]=> + float(49.674146175385) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.02859556675) + [1]=> + float(49.674154222012) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.0285962373) + [1]=> + float(49.674153551459) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(171.02859674022) + [1]=> + float(49.674154138193) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(-5.60302734375) + [1]=> + float(42.60498046875) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(157.5) + [1]=> + float(67.5) + } +} +array(2) { + ["type"]=> + string(5) "Point" + ["coordinates"]=> + array(2) { + [0]=> + float(16.40000006184) + [1]=> + float(48.199999993667) + } +} diff --git a/tests/geohash_encode.phpt b/tests/geohash_encode.phpt new file mode 100644 index 0000000..eda95e6 --- /dev/null +++ b/tests/geohash_encode.phpt @@ -0,0 +1,22 @@ +--TEST-- +Test geohash_encode +--SKIPIF-- + +--FILE-- + 'Point', 'coordinates' => [16.4, 48.2]), 12).PHP_EOL; +echo geohash_encode(array('type' => 'Point', 'coordinates' => [90, 90]), 6).PHP_EOL; +echo geohash_encode(array('type' => 'Point', 'coordinates' => [16.363, 48.21]), 32).PHP_EOL; +echo geohash_encode(array('type' => 'Point', 'coordinates' => [95, 95]), 6).PHP_EOL; +echo geohash_encode(array('type' => 'Point', 'coordinates' => [185, 185]), 12).PHP_EOL; +echo geohash_encode(array('type' => 'Point', 'coordinates' => [-90, -185]), 12).PHP_EOL; +echo var_dump(geohash_encode(array('type' => 'Point', 'coordinates' => [30, 30]), 0)); +?> +--EXPECT-- +u2edjnw17enr +vzzzzz +u2edk275te35u5s6504t7yfpbpbpbpbp +ypgxcz +zzzzzzzzzzzz +1bpbpbpbpbpb +string(0) ""