├── .gitignore ├── phpunit.xml.dist ├── tests ├── bootstrap.php └── test-geo-query.php ├── composer.json ├── src ├── GeoQueryInterface.php ├── GeoQueryHaversineOptimized.php ├── GeoQueryAbstract.php ├── GeoQueryContext.php ├── GeoQueryPostCustomTableHaversine.php ├── GeoQueryHaversine.php └── GeoQueryUserHaversine.php ├── LICENCE ├── .travis.yml ├── geo-query.php ├── bin └── install-wp-tests.sh ├── composer.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | =5.3.3" 16 | }, 17 | "autoload": { 18 | "psr-4": {"Birgir\\Geo\\": "src/"} 19 | }, 20 | "extra" : { 21 | "installer-name" : "geo-query" 22 | }, 23 | "config": { 24 | "allow-plugins": { 25 | "composer/installers": true 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GeoQueryInterface.php: -------------------------------------------------------------------------------- 1 | setup( $GLOBALS['wpdb'] )->activate(); 43 | } 44 | 45 | }); 46 | 47 | -------------------------------------------------------------------------------- /src/GeoQueryContext.php: -------------------------------------------------------------------------------- 1 | db = $db; 28 | return $this; 29 | } 30 | 31 | 32 | /** 33 | * Add posts_clauses and pre_user_query filters. 34 | * 35 | * @since 0.0.1 36 | * @since 0.1.0 Adds the pre_user_query filter. 37 | * 38 | * @return void 39 | */ 40 | 41 | public function activate() 42 | { 43 | add_filter( 'posts_clauses', array( $this, 'posts_clauses' ), 99, 2 ); 44 | add_action( 'pre_user_query', array( $this, 'pre_user_query' ), 99, 2 ); 45 | return $this; 46 | } 47 | 48 | 49 | /** 50 | * Modify the SQL for the fields, orderby, groupby, join and where clauses, according to the given context ( algorithm ). 51 | * 52 | * @since 0.0.1 53 | * 54 | * @param array $clauses Array of clauses parts 55 | * @param WP_Query $q An instance of WP_Query 56 | * @return string $clauses Array of clauses parts 57 | */ 58 | 59 | public function posts_clauses( $clauses, \WP_Query $q ) 60 | { 61 | // Get user input: 62 | $geo_query = $q->get( 'geo_query' ); 63 | 64 | if( is_array( $geo_query ) && ! empty( $geo_query ) ) 65 | { 66 | // Run this filter callback only once: 67 | remove_filter( current_filter(), array( $this, __FUNCTION__ ) ); 68 | 69 | // Filter the query clauses: 70 | $clauses = $this->get_query_clauses( $geo_query, $clauses ); 71 | } 72 | return $clauses; 73 | } 74 | 75 | public function pre_user_query( \WP_User_Query $q ) { 76 | // get user input 77 | $geo_query = $q->get('geo_query'); 78 | 79 | if (is_array($geo_query) && ! empty( $geo_query )) { 80 | 81 | // get the existing clauses 82 | $clauses = array( 83 | 'fields' => $q->query_fields, 84 | 'from' => $q->query_from, 85 | 'where' => $q->query_where, 86 | 'orderby' => $q->query_orderby, 87 | ); 88 | 89 | // override default context explictly for a user query 90 | if (empty($geo_query['context'])) { 91 | $geo_query['context'] = __NAMESPACE__ . '\\GeoQueryUserHaversine'; 92 | } 93 | 94 | // get new values for each clause 95 | $clauses = $this->get_query_clauses( $geo_query, $clauses ); 96 | 97 | // replace clauses in the running query 98 | $q->query_fields = $clauses['fields']; 99 | $q->query_from = $clauses['from']; 100 | $q->query_where = $clauses['where']; 101 | $q->query_orderby = $clauses['orderby']; 102 | } 103 | } 104 | 105 | protected function get_query_clauses( array $geo, array $clauses ) { 106 | // Default implementation: 107 | $class = __NAMESPACE__ . '\\GeoQueryHaversine'; 108 | 109 | // Check if the user wants another implementation: 110 | if( ! empty( $geo['context'] ) ) 111 | $class = preg_replace( '/[^a-z0-9\\\]+/i', '', $geo['context'] ); 112 | 113 | // Create an implementation instance, if the class exists and implements the GeoQueryInterface interface: 114 | if( class_exists( $class ) && in_array( __NAMESPACE__ . '\\GeoQueryInterface', class_implements( $class ) ) ) 115 | { 116 | $g = new $class; 117 | $g->setup( $this->db, $geo ); 118 | $clauses = $g->algorithm( $clauses ); 119 | } 120 | 121 | return $clauses; 122 | } 123 | 124 | } // end class 125 | 126 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 29 | WP_TESTS_TAG="branches/$WP_VERSION" 30 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 31 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 32 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 33 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 34 | else 35 | WP_TESTS_TAG="tags/$WP_VERSION" 36 | fi 37 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 38 | WP_TESTS_TAG="trunk" 39 | else 40 | # http serves a single offer, whereas https serves multiple. we only want one 41 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 42 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 43 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 44 | if [[ -z "$LATEST_VERSION" ]]; then 45 | echo "Latest WordPress version could not be found" 46 | exit 1 47 | fi 48 | WP_TESTS_TAG="tags/$LATEST_VERSION" 49 | fi 50 | 51 | set -ex 52 | 53 | install_wp() { 54 | 55 | if [ -d $WP_CORE_DIR ]; then 56 | return; 57 | fi 58 | 59 | mkdir -p $WP_CORE_DIR 60 | 61 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 62 | mkdir -p $TMPDIR/wordpress-nightly 63 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 64 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 65 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 66 | else 67 | if [ $WP_VERSION == 'latest' ]; then 68 | local ARCHIVE_NAME='latest' 69 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 70 | # https serves multiple offers, whereas http serves single. 71 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 72 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 73 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 74 | LATEST_VERSION=${WP_VERSION%??} 75 | else 76 | # otherwise, scan the releases and get the most up to date minor version of the major release 77 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 78 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 79 | fi 80 | if [[ -z "$LATEST_VERSION" ]]; then 81 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 82 | else 83 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 84 | fi 85 | else 86 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 87 | fi 88 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 89 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 90 | fi 91 | 92 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 93 | } 94 | 95 | install_test_suite() { 96 | # portable in-place argument for both GNU sed and Mac OSX sed 97 | if [[ $(uname -s) == 'Darwin' ]]; then 98 | local ioption='-i .bak' 99 | else 100 | local ioption='-i' 101 | fi 102 | 103 | # set up testing suite if it doesn't yet exist 104 | if [ ! -d $WP_TESTS_DIR ]; then 105 | # set up testing suite 106 | mkdir -p $WP_TESTS_DIR 107 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 108 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 109 | fi 110 | 111 | if [ ! -f wp-tests-config.php ]; then 112 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 113 | # remove all forward slashes in the end 114 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 115 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 116 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 117 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 118 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 120 | fi 121 | 122 | } 123 | 124 | install_db() { 125 | 126 | if [ ${SKIP_DB_CREATE} = "true" ]; then 127 | return 0 128 | fi 129 | 130 | # parse DB_HOST for port or socket references 131 | local PARTS=(${DB_HOST//\:/ }) 132 | local DB_HOSTNAME=${PARTS[0]}; 133 | local DB_SOCK_OR_PORT=${PARTS[1]}; 134 | local EXTRA="" 135 | 136 | if ! [ -z $DB_HOSTNAME ] ; then 137 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 138 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 139 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 140 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 141 | elif ! [ -z $DB_HOSTNAME ] ; then 142 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 143 | fi 144 | fi 145 | 146 | # create database 147 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 148 | } 149 | 150 | install_wp 151 | install_test_suite 152 | install_db 153 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "483b28a8d1f52146031d836d3942b1ee", 8 | "packages": [ 9 | { 10 | "name": "composer/installers", 11 | "version": "v2.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/composer/installers.git", 15 | "reference": "c29dc4b93137acb82734f672c37e029dfbd95b35" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/composer/installers/zipball/c29dc4b93137acb82734f672c37e029dfbd95b35", 20 | "reference": "c29dc4b93137acb82734f672c37e029dfbd95b35", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "composer-plugin-api": "^1.0 || ^2.0", 25 | "php": "^7.2 || ^8.0" 26 | }, 27 | "require-dev": { 28 | "composer/composer": "1.6.* || ^2.0", 29 | "composer/semver": "^1 || ^3", 30 | "phpstan/phpstan": "^0.12.55", 31 | "phpstan/phpstan-phpunit": "^0.12.16", 32 | "symfony/phpunit-bridge": "^5.3", 33 | "symfony/process": "^5" 34 | }, 35 | "type": "composer-plugin", 36 | "extra": { 37 | "class": "Composer\\Installers\\Plugin", 38 | "branch-alias": { 39 | "dev-main": "2.x-dev" 40 | }, 41 | "plugin-modifies-install-path": true 42 | }, 43 | "autoload": { 44 | "psr-4": { 45 | "Composer\\Installers\\": "src/Composer/Installers" 46 | } 47 | }, 48 | "notification-url": "https://packagist.org/downloads/", 49 | "license": [ 50 | "MIT" 51 | ], 52 | "authors": [ 53 | { 54 | "name": "Kyle Robinson Young", 55 | "email": "kyle@dontkry.com", 56 | "homepage": "https://github.com/shama" 57 | } 58 | ], 59 | "description": "A multi-framework Composer library installer", 60 | "homepage": "https://composer.github.io/installers/", 61 | "keywords": [ 62 | "Dolibarr", 63 | "Eliasis", 64 | "Hurad", 65 | "ImageCMS", 66 | "Kanboard", 67 | "Lan Management System", 68 | "MODX Evo", 69 | "MantisBT", 70 | "Mautic", 71 | "Maya", 72 | "OXID", 73 | "Plentymarkets", 74 | "Porto", 75 | "RadPHP", 76 | "SMF", 77 | "Starbug", 78 | "Thelia", 79 | "Whmcs", 80 | "WolfCMS", 81 | "agl", 82 | "annotatecms", 83 | "attogram", 84 | "bitrix", 85 | "cakephp", 86 | "chef", 87 | "cockpit", 88 | "codeigniter", 89 | "concrete5", 90 | "croogo", 91 | "dokuwiki", 92 | "drupal", 93 | "eZ Platform", 94 | "elgg", 95 | "expressionengine", 96 | "fuelphp", 97 | "grav", 98 | "installer", 99 | "itop", 100 | "known", 101 | "kohana", 102 | "laravel", 103 | "lavalite", 104 | "lithium", 105 | "magento", 106 | "majima", 107 | "mako", 108 | "matomo", 109 | "mediawiki", 110 | "miaoxing", 111 | "modulework", 112 | "modx", 113 | "moodle", 114 | "osclass", 115 | "pantheon", 116 | "phpbb", 117 | "piwik", 118 | "ppi", 119 | "processwire", 120 | "puppet", 121 | "pxcms", 122 | "reindex", 123 | "roundcube", 124 | "shopware", 125 | "silverstripe", 126 | "sydes", 127 | "sylius", 128 | "tastyigniter", 129 | "wordpress", 130 | "yawik", 131 | "zend", 132 | "zikula" 133 | ], 134 | "support": { 135 | "issues": "https://github.com/composer/installers/issues", 136 | "source": "https://github.com/composer/installers/tree/v2.2.0" 137 | }, 138 | "funding": [ 139 | { 140 | "url": "https://packagist.com", 141 | "type": "custom" 142 | }, 143 | { 144 | "url": "https://github.com/composer", 145 | "type": "github" 146 | }, 147 | { 148 | "url": "https://tidelift.com/funding/github/packagist/composer/composer", 149 | "type": "tidelift" 150 | } 151 | ], 152 | "time": "2022-08-20T06:45:11+00:00" 153 | } 154 | ], 155 | "packages-dev": [], 156 | "aliases": [], 157 | "minimum-stability": "stable", 158 | "stability-flags": [], 159 | "prefer-stable": false, 160 | "prefer-lowest": false, 161 | "platform": { 162 | "php": ">=5.3.3" 163 | }, 164 | "platform-dev": [], 165 | "plugin-api-version": "2.6.0" 166 | } 167 | -------------------------------------------------------------------------------- /src/GeoQueryPostCustomTableHaversine.php: -------------------------------------------------------------------------------- 1 | db = $db; 40 | 41 | // Default user input: 42 | $default = array( 43 | 'table' => 'custom_table', 44 | 'pid_col' => 'pid', 45 | 'lat_col' => 'lat', 46 | 'lng_col' => 'lng', 47 | 'lat' => 0, 48 | 'lng' => 0, 49 | 'radius' => 0, 50 | 'distance_unit' => 111.045, 51 | 'order' => '', 52 | 'context' => '', 53 | ); 54 | $geo_query = wp_parse_args( $geo_query, $default ); 55 | 56 | // Sanitize the user input: 57 | $this->table = sanitize_key( $geo_query['table'] ); 58 | $this->lat_col = sanitize_key( $geo_query['lat_col'] ); 59 | $this->lng_col = sanitize_key( $geo_query['lng_col'] ); 60 | $this->pid_col = sanitize_key( $geo_query['pid_col'] ); 61 | $this->lat = floatval( $geo_query['lat'] ); 62 | $this->lng = floatval( $geo_query['lng'] ); 63 | $this->radius = floatval( $geo_query['radius'] ); 64 | $this->distance_unit = floatval( $geo_query['distance_unit'] ); 65 | $this->order = in_array( strtoupper( $geo_query['order'] ), array( 'ASC', 'DESC' ) ) ? strtoupper( $geo_query['order'] ) : ''; 66 | } 67 | 68 | 69 | /** 70 | * Algorithm - let's implement the interface 71 | * 72 | * @since 0.2.0 73 | * 74 | * @param array $clauses 75 | * @return array $clauses 76 | */ 77 | 78 | public function algorithm( $clauses = array() ) 79 | { 80 | // Modify SQL query parts: 81 | $clauses['fields'] = $this->posts_fields( $clauses['fields'] ); 82 | $clauses['where'] = $this->posts_where( $clauses['where'] ); 83 | $clauses['join'] = $this->posts_join( $clauses['join'] ); 84 | 85 | if( $this->radius > 0 ) 86 | $clauses['groupby'] = $this->posts_groupby( $clauses['groupby'] ); 87 | 88 | if( $this->order ) 89 | $clauses['orderby'] = $this->posts_orderby( $clauses['orderby'] ); 90 | 91 | return $clauses; 92 | } 93 | 94 | 95 | /** 96 | * Modify the SQL for the fields clause 97 | * 98 | * @since 0.2.0 99 | * 100 | * @param string $fields 101 | * @return string $fields 102 | */ 103 | 104 | protected function posts_fields( $fields ) 105 | { 106 | $fields .= ", settings.distance_unit * DEGREES( ACOS( LEAST( 1.0, 107 | COS( RADIANS( settings.latpoint ) ) 108 | * COS( RADIANS( gqct.{$this->lat_col} ) ) 109 | * COS( RADIANS( settings.longpoint ) - RADIANS( gqct.{$this->lng_col} ) ) 110 | + SIN( RADIANS( settings.latpoint ) ) 111 | * SIN( RADIANS( gqct.{$this->lat_col} )) 112 | ))) AS distance_value "; 113 | 114 | return $fields; 115 | } 116 | 117 | 118 | /** 119 | * Modify the SQL for the where clause 120 | * 121 | * @since 0.0.1 122 | * 123 | * @param string $where 124 | * @return string $where 125 | */ 126 | 127 | protected function posts_where( $where ) 128 | { 129 | return $where; 130 | } 131 | 132 | 133 | /** 134 | * Modify the SQL for the groupby clause 135 | * 136 | * @since 0.2.0 137 | * 138 | * @param string $groupby 139 | * @return string $groupby 140 | */ 141 | 142 | protected function posts_groupby( $groupby ) 143 | { 144 | if( empty( $groupby ) ) 145 | $groupby = " {$this->db->posts}.ID "; 146 | 147 | $groupby .= $this->db->prepare( 148 | " HAVING distance_value <= %f ", 149 | $this->radius 150 | ); 151 | 152 | return $groupby; 153 | } 154 | 155 | 156 | /** 157 | * Modify the SQL for the join clause 158 | * 159 | * @since 0.2.0 160 | * 161 | * @param string $join 162 | * @return string $join 163 | */ 164 | 165 | protected function posts_join( $join ) 166 | { 167 | $join .= " INNER JOIN {$this->db->prefix}{$this->table} AS gqct ON ( {$this->db->posts}.ID = gqct.{$this->pid_col} ) "; 168 | 169 | $join .= $this->db->prepare( 170 | " INNER JOIN ( SELECT %f AS latpoint, %f AS longpoint, %f AS distance_unit, %f AS radius ) AS settings ", 171 | $this->lat, 172 | $this->lng, 173 | $this->distance_unit, 174 | $this->radius 175 | ); 176 | 177 | return $join; 178 | } 179 | 180 | 181 | /** 182 | * Modify the SQL for the orderby clause 183 | * 184 | * @since 0.2.0 185 | * 186 | * @param string $orderby 187 | * @return string $orderby 188 | */ 189 | 190 | protected function posts_orderby( $orderby ) 191 | { 192 | return ' distance_value ' . $this->order . ', ' . $orderby; 193 | } 194 | 195 | 196 | } // end class 197 | 198 | -------------------------------------------------------------------------------- /src/GeoQueryHaversine.php: -------------------------------------------------------------------------------- 1 | db = $db; 39 | 40 | // Default user input: 41 | $default = array( 42 | 'lat_meta_key' => 'lat', 43 | 'lng_meta_key' => 'lng', 44 | 'lat' => 0, 45 | 'lng' => 0, 46 | 'radius' => 0, 47 | 'distance_unit' => 111.045, 48 | 'order' => '', 49 | 'context' => '', 50 | ); 51 | $geo_query = wp_parse_args( $geo_query, $default ); 52 | 53 | // Sanitize the user input: 54 | $this->lat_meta_key = sanitize_key( $geo_query['lat_meta_key'] ); 55 | $this->lng_meta_key = sanitize_key( $geo_query['lng_meta_key'] ); 56 | $this->lat = floatval( $geo_query['lat'] ); 57 | $this->lng = floatval( $geo_query['lng'] ); 58 | $this->radius = floatval( $geo_query['radius'] ); 59 | $this->distance_unit = floatval( $geo_query['distance_unit'] ); 60 | $this->order = in_array( strtoupper( $geo_query['order'] ), array( 'ASC', 'DESC' ) ) ? strtoupper( $geo_query['order'] ) : ''; 61 | } 62 | 63 | 64 | /** 65 | * Algorithm - let's implement the interface 66 | * 67 | * @since 0.0.1 68 | * 69 | * @param array $clauses 70 | * @return array $clauses 71 | */ 72 | 73 | public function algorithm( $clauses = array() ) 74 | { 75 | // Modify SQL query parts: 76 | $clauses['fields'] = $this->posts_fields( $clauses['fields'] ); 77 | $clauses['where'] = $this->posts_where( $clauses['where'] ); 78 | $clauses['join'] = $this->posts_join( $clauses['join'] ); 79 | 80 | if( $this->radius > 0 ) 81 | $clauses['groupby'] = $this->posts_groupby( $clauses['groupby'] ); 82 | 83 | if( $this->order ) 84 | $clauses['orderby'] = $this->posts_orderby( $clauses['orderby'] ); 85 | 86 | return $clauses; 87 | } 88 | 89 | 90 | /** 91 | * Modify the SQL for the fields clause 92 | * 93 | * @since 0.0.1 94 | * 95 | * @param string $fields 96 | * @return string $fields 97 | */ 98 | 99 | protected function posts_fields( $fields ) 100 | { 101 | $fields .= ", settings.distance_unit * DEGREES( ACOS( 102 | COS( RADIANS( settings.latpoint ) ) 103 | * COS( RADIANS( mtlat.meta_value ) ) 104 | * COS( RADIANS( settings.longpoint ) - RADIANS( mtlng.meta_value ) ) 105 | + SIN( RADIANS( settings.latpoint ) ) 106 | * SIN( RADIANS( mtlat.meta_value )))) AS distance_value "; 107 | 108 | return $fields; 109 | } 110 | 111 | 112 | /** 113 | * Modify the SQL for the where clause 114 | * 115 | * @since 0.0.1 116 | * 117 | * @param string $where 118 | * @return string $where 119 | */ 120 | 121 | protected function posts_where( $where ) 122 | { 123 | return $where; 124 | } 125 | 126 | 127 | /** 128 | * Modify the SQL for the groupby clause 129 | * 130 | * @since 0.0.1 131 | * 132 | * @param string $groupby 133 | * @return string $groupby 134 | */ 135 | 136 | protected function posts_groupby( $groupby ) 137 | { 138 | if( empty( $groupby ) ) 139 | $groupby = " {$this->db->posts}.ID "; 140 | 141 | $groupby .= $this->db->prepare( 142 | " HAVING distance_value <= %f ", 143 | $this->radius 144 | ); 145 | 146 | return $groupby; 147 | } 148 | 149 | 150 | /** 151 | * Modify the SQL for the join clause 152 | * 153 | * @since 0.0.1 154 | * 155 | * @param string $join 156 | * @return string $join 157 | */ 158 | 159 | protected function posts_join( $join ) 160 | { 161 | $join .= $this->db->prepare( 162 | " INNER JOIN {$this->db->postmeta} AS mtlat ON ( {$this->db->posts}.ID = mtlat.post_id AND mtlat.meta_key = '%s' ) ", 163 | $this->lat_meta_key 164 | ); 165 | 166 | $join .= $this->db->prepare( 167 | " INNER JOIN {$this->db->postmeta} AS mtlng ON ( {$this->db->posts}.ID = mtlng.post_id AND mtlng.meta_key = '%s' ) ", 168 | $this->lng_meta_key 169 | ); 170 | 171 | $join .= $this->db->prepare( 172 | " INNER JOIN ( SELECT %f AS latpoint, %f AS longpoint, %f AS distance_unit, %f AS radius ) AS settings ", 173 | $this->lat, 174 | $this->lng, 175 | $this->distance_unit, 176 | $this->radius 177 | ); 178 | 179 | return $join; 180 | } 181 | 182 | 183 | /** 184 | * Modify the SQL for the orderby clause 185 | * 186 | * @since 0.0.1 187 | * 188 | * @param string $orderby 189 | * @return string $orderby 190 | */ 191 | 192 | protected function posts_orderby( $orderby ) 193 | { 194 | return ' distance_value ' . $this->order . ', ' . $orderby; 195 | } 196 | 197 | 198 | } // end class 199 | 200 | -------------------------------------------------------------------------------- /src/GeoQueryUserHaversine.php: -------------------------------------------------------------------------------- 1 | db = $db; 38 | 39 | // Default user input: 40 | $default = array( 41 | 'lat_meta_key' => 'lat', 42 | 'lng_meta_key' => 'lng', 43 | 'lat' => 0, 44 | 'lng' => 0, 45 | 'radius' => 0, 46 | 'distance_unit' => 111.045, 47 | 'order' => '', 48 | 'context' => '', 49 | ); 50 | $geo_query = wp_parse_args( $geo_query, $default ); 51 | 52 | // Sanitize the user input: 53 | $this->lat_meta_key = sanitize_key( $geo_query['lat_meta_key'] ); 54 | $this->lng_meta_key = sanitize_key( $geo_query['lng_meta_key'] ); 55 | $this->lat = floatval( $geo_query['lat'] ); 56 | $this->lng = floatval( $geo_query['lng'] ); 57 | $this->radius = floatval( $geo_query['radius'] ); 58 | $this->distance_unit = floatval( $geo_query['distance_unit'] ); 59 | $this->order = in_array( strtoupper( $geo_query['order'] ), array( 'ASC', 'DESC' ) ) ? strtoupper( $geo_query['order'] ) : ''; 60 | } 61 | 62 | 63 | /** 64 | * Algorithm - let's implement the interface 65 | * 66 | * @since 0.1.0 67 | * 68 | * @param array $clauses 69 | * @return array $clauses 70 | */ 71 | 72 | public function algorithm( $clauses = array() ) 73 | { 74 | // Modify SQL query parts: 75 | $clauses['fields'] = $this->users_fields( $clauses['fields'] ); 76 | $clauses['where'] = $this->users_where( $clauses['where'] ); 77 | $clauses['from'] = $this->users_from( $clauses['from'] ); 78 | 79 | // honor the orderby param 80 | $orderby = isset( $clauses['orderby'] ) ? $clauses['orderby'] : ''; 81 | if( $this->order ) 82 | $clauses['orderby'] = $this->users_orderby( $orderby ); 83 | 84 | return $clauses; 85 | } 86 | 87 | 88 | /** 89 | * Modify the SQL for the fields clause 90 | * 91 | * @since 0.1.0 92 | * 93 | * @param string $fields 94 | * @return string $fields 95 | */ 96 | 97 | protected function users_fields( $fields ) 98 | { 99 | $fields .= ", settings.distance_unit * DEGREES( ACOS( 100 | COS( RADIANS( settings.latpoint ) ) 101 | * COS( RADIANS( mtlat.meta_value ) ) 102 | * COS( RADIANS( settings.longpoint ) - RADIANS( mtlng.meta_value ) ) 103 | + SIN( RADIANS( settings.latpoint ) ) 104 | * SIN( RADIANS( mtlat.meta_value )))) AS distance_value "; 105 | 106 | return $fields; 107 | } 108 | 109 | 110 | /** 111 | * Modify the SQL for the where clause 112 | * 113 | * @since 0.1.0 114 | * 115 | * @param string $where 116 | * @return string $where 117 | */ 118 | 119 | protected function users_where( $where ) 120 | { 121 | // check for radius 122 | if( $this->radius > 0 ) { 123 | // ensure we don't mess up any existing HAVING clause 124 | if (strpos($where, ' HAVING ') !== false) { 125 | $where .= ' HAVING '; 126 | } 127 | 128 | // add the HAVING clause with the radius value 129 | $where .= $this->db->prepare( 130 | " HAVING distance_value <= %f ", 131 | $this->radius 132 | ); 133 | } 134 | 135 | return $where; 136 | } 137 | 138 | 139 | /** 140 | * Modify the SQL for the HAVING clause 141 | * 142 | * @since 0.1.0 143 | * 144 | * @return string $having 145 | */ 146 | 147 | protected function users_having() 148 | { 149 | return $this->db->prepare( 150 | " HAVING distance_value <= %f ", 151 | $this->radius 152 | ); 153 | } 154 | 155 | 156 | /** 157 | * Modify the SQL for the FROM/JOIN clauses 158 | * 159 | * @since 0.1.0 160 | * 161 | * @param string $from 162 | * @return string $from 163 | */ 164 | 165 | protected function users_from( $from ) 166 | { 167 | $from .= $this->db->prepare( 168 | " INNER JOIN {$this->db->usermeta} AS mtlat ON ( {$this->db->users}.ID = mtlat.user_id AND mtlat.meta_key = '%s' ) ", 169 | $this->lat_meta_key 170 | ); 171 | 172 | $from .= $this->db->prepare( 173 | " INNER JOIN {$this->db->usermeta} AS mtlng ON ( {$this->db->users}.ID = mtlng.user_id AND mtlng.meta_key = '%s' ) ", 174 | $this->lng_meta_key 175 | ); 176 | 177 | $from .= $this->db->prepare( 178 | " INNER JOIN ( SELECT %f AS latpoint, %f AS longpoint, %f AS distance_unit, %f AS radius ) AS settings ", 179 | $this->lat, 180 | $this->lng, 181 | $this->distance_unit, 182 | $this->radius 183 | ); 184 | 185 | return $from; 186 | } 187 | 188 | /** 189 | * Modify the SQL for the orderby clause 190 | * 191 | * @since 0.1.0 192 | * 193 | * @param string $orderby 194 | * @return string $orderby 195 | */ 196 | 197 | protected function users_orderby( $orderby ) 198 | { 199 | return ' ORDER BY distance_value ' . $this->order . ', ' . str_replace( 'ORDER BY ', '', $orderby ); 200 | } 201 | 202 | 203 | } // end class 204 | 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WordPress plugin: Geo Query 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/birgire/geo-query.svg?branch=master)](https://travis-ci.org/birgire/geo-query) 5 | [![GitHub license](https://img.shields.io/github/license/birgire/geo-query.svg)](https://github.com/birgire/geo-query/blob/master/LICENCE) 6 | [![Packagist](https://img.shields.io/packagist/v/birgir/geo-query.svg)](https://packagist.org/packages/birgir/geo-query) 7 | 8 | 9 | ### Description 10 | 11 | This plugin adds a support for the `geo_query` part of the `WP_Query` and `WP_User_Query`. 12 | 13 | Supports geo data stored in post/user meta or in a custom table. 14 | 15 | It uses the Haversine SQL implementation by Ollie Jones (see [here](http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/)). 16 | 17 | The plugin works on PHP 5.3+. 18 | 19 | It supports the GitHub Updater. 20 | 21 | Activate the plugin and you can use the `geo_query` parameter in all your `WP_Query` and `WP_User_Query` queries. 22 | 23 | Few examples are here below, e.g. for the Rest API. 24 | 25 | 26 | ### Installation 27 | 28 | Upload the plugin to the plugin folder and activate it. 29 | 30 | To install dependencies with Composer (not required): 31 | 32 | composer install 33 | 34 | or 35 | 36 | php composer.phar install 37 | 38 | within our folder. See here for more information on how to install Composer. 39 | 40 | Then play with the example below, in your theme or in a plugin. 41 | 42 | Have fun ;-) 43 | 44 | ### Example - Basic `WP_Query` usage: 45 | 46 | Here's an example of the default input parameters of the `geo_query` part: 47 | 48 | $args = [ 49 | 'post_type' => 'post', 50 | 'posts_per_page' => 10, 51 | 'ignore_sticky_posts' => true, 52 | 'orderby' => [ 'title' => 'DESC' ], 53 | 'geo_query' => [ 54 | 'lat' => 64, // Latitude point 55 | 'lng' => -22, // Longitude point 56 | 'lat_meta_key' => 'geo_lat', // Meta-key for the latitude data 57 | 'lng_meta_key' => 'geo_lng', // Meta-key for the longitude data 58 | 'radius' => 150, // Find locations within a given radius (km) 59 | 'order' => 'DESC', // Order by distance 60 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 61 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 62 | ], 63 | ]; 64 | $query = new WP_Query( $args ); 65 | 66 | ### Example - Rest API usage: 67 | 68 | Here's a modified example from @florianweich: 69 | 70 | add_filter( 'rest_query_vars', function ( $valid_vars ) { 71 | return array_merge( $valid_vars, [ 'geo_location' ] ); 72 | } ); 73 | 74 | add_filter( 'rest_post_query', function( $args, $request ) { 75 | $geo = json_decode( $request->get_param( 'geo_location' ) ); 76 | if ( isset( $geo->lat, $geo->lng ) ) { 77 | $args['geo_query'] = [ 78 | 'lat' => (float) $geo->lat, 79 | 'lng' => (float) $geo->lng, 80 | 'lat_meta_key' => 'geo_lat', 81 | 'lng_meta_key' => 'geo_lng', 82 | 'radius' => ($geo->radius) ? (float) $geo->radius : 50, 83 | ]; 84 | } 85 | return $args; 86 | }, 10, 2 ); 87 | 88 | Test it with e.g.: 89 | 90 | https://example.com/wp-json/wp/v2/posts?geo_location={"lat":"64.128288","lng":"-21.827774","radius":"50"} 91 | 92 | One can use `rest_{custom-post-type-slug}_query` filter for a custom post type. 93 | 94 | ### Example - Basic `WP_User_Query` usage: 95 | 96 | Here's an example from @acobster: 97 | 98 | $args = [ 99 | 'role' => 'subscriber', 100 | 'geo_query' => [ 101 | 'lat' => 47.236, 102 | 'lng' => -122.435, 103 | 'lat_meta_key' => 'geo_lat', 104 | 'lng_meta_key' => 'geo_lng', 105 | 'radius' => 1, 106 | 'context' => '\\Birgir\\Geo\\GeoQueryUserHaversine', 107 | ], 108 | ]; 109 | 110 | $query = new WP_User_Query( $args ); 111 | 112 | ### Example - Basic `WP_Query` usage for fetching lat/lng data from a custom table with Haversine formula: 113 | 114 | $args = array( 115 | 'post_type' => 'post', 116 | 'posts_per_page' => 10, 117 | 'ignore_sticky_posts' => true, 118 | 'orderby' => array( 'title' => 'DESC' ), 119 | 'geo_query' => array( 120 | 'table' => 'custom_table', // Table name for the geo custom table. 121 | 'pid_col' => 'pid', // Column name for the post ID data 122 | 'lat_col' => 'lat', // Column name for the latitude data 123 | 'lng_col' => 'lng', // Column name for the longitude data 124 | 'lat' => 64.0, // Latitude point 125 | 'lng' => -22.0, // Longitude point 126 | 'radius' => 1, // Find locations within a given radius (km) 127 | 'order' => 'DESC', // Order by distance 128 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 129 | 'context' => '\\Birgir\\Geo\\GeoQueryPostCustomTableHaversine', // Custom table implementation, you can use your own here instead. 130 | ), 131 | ); 132 | 133 | $query = new WP_Query( $args ); 134 | 135 | Check the unit test method `test_custom_table_in_wp_query()` as a more detailed example. 136 | 137 | ### Notes on the parameters: 138 | 139 | - The plugin assumes we store the latitudes and longitudes as custom fields ( post meta), so we need to tell the query about meta keys with the `'lat_meta_key'` and `'lng_meta_key'` parameters. 140 | 141 | - Skipping the `'radius'` parameter means that no distance filtering will take place. 142 | 143 | - If we use the `'order'` parameter within the `'geo_query'`, then it will be prepended to the native `'orderby'` parameter. 144 | 145 | - The `'distance_unit'` parameter should be `69.0` for distance in statute miles, else `111.045` for distance in kilometers. 146 | 147 | - If we want to use the optimized Haversine version by Ollie Jones, we use: 148 | 149 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversineOptimized' 150 | 151 | Notice that on our current plugin setup (i.e. fetching data from the LONGTEXT post meta fields) this isn't more performant than the default `GeoQueryHaversine` class. 152 | 153 | A future work could be to use a custom table with indexes, for the optimization to work. 154 | 155 | 156 | - If we create our own implementation of the Haversine formula, for example the `GeoQueryCustom` class, we just have to make sure it implements the `GeoQueryInterface` interface: 157 | 158 | 'context' => 'GeoQueryCustom' 159 | 160 | ### Feedback 161 | 162 | Any suggestions are welcomed. 163 | 164 | ### Changelog 165 | 166 | 0.2.3 (2024-10-29) 167 | - Fix: Deprecated creation of dynamic property. Props @lukasbesch 168 | 169 | 0.2.2 (2024-01-05) 170 | - Bump composer/installers to ^2.0.0. Props @wujekbogdan 171 | 172 | 0.2.1 (2023-07-26) 173 | - Fix deprecated notice in PHP 8.1 #24 174 | 175 | 0.2.0 (2020-04-25) 176 | - Support for fetching points from a custom table and doing Haversine formula in WP_Query. 177 | 178 | 0.1.1 (2019-01-23) 179 | - Fixed #14. Fixed the user query ordering. Props @baden03 180 | 181 | 0.1.0 (2018-08-06) 182 | - Added support for user queries. Props @acobster 183 | - Fixed Travis issue. Travis runs successfully for PHP 7.2, 7.1,7,5.6 when installed via Composer and also 5.4 when installed without Composer. Skip the 5.4 Composer check for now. 184 | 185 | 0.0.7 (2018-06-27) 186 | - Fixed #10. Use ^1.0.0 for composer installer. Props @wujekbogdan 187 | 188 | 0.0.6 (2017-11-16) 189 | - Fixed #6. Support floating point radius. Props @wujekbogdan 190 | - Added integration tests. 191 | 192 | 0.0.5 (2017-02-26) 193 | 194 | - Added fallback for those that don't use Composer 195 | - Removed the vendor directory 196 | 197 | 0.0.4 (2017-02-26) 198 | 199 | - Fixed #4. Props @billzhong . 200 | 201 | 0.0.3 (2015-04-29) 202 | 203 | - Fixed #2. Fixed a typo. Props @Ben764 and @con322. 204 | 205 | 0.0.2 (2015-03-10) 206 | 207 | - Added: Support for the GitHub Updater. 208 | - Updated: README.md 209 | - Changed: Use distance_value instead of distance_in_km in SQL, since we can use miles by changing the distance_unit parameter. 210 | 211 | 0.0.1 - Init 212 | -------------------------------------------------------------------------------- /tests/test-geo-query.php: -------------------------------------------------------------------------------- 1 | get_charset_collate(); 20 | require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); 21 | 22 | $table_name = $wpdb->prefix . 'custom_table'; 23 | $sql = "CREATE TABLE $table_name ( 24 | id bigint(20) NOT NULL AUTO_INCREMENT, 25 | pid bigint(20) NOT NULL, 26 | lat decimal(10,8) NOT NULL, 27 | lng decimal(11,8) NOT NULL, 28 | PRIMARY KEY (id) 29 | ) ENGINE=InnoDB $charset_collate;"; 30 | dbDelta( $sql ); 31 | 32 | $points = array( 33 | (object) array( 34 | 'title' => 'Reykjavik #1', 35 | 'lat' => 64, 36 | 'lng'=> -22.001 37 | ), 38 | (object) array( 39 | 'title' => 'Reykjavik #2', 40 | 'lat' => 64.1, 41 | 'lng' => -22.201 42 | ), 43 | (object) array( 44 | 'title' => 'Akureyri', 45 | 'lat' => 65.6839, 46 | 'lng' => -18.1105 47 | ), 48 | ); 49 | 50 | foreach ( $points as $point ){ 51 | 52 | $pid = self::factory()->post->create( array( 53 | 'post_title' => $point->title 54 | ) ); 55 | 56 | $wpdb->insert( 57 | $table_name, 58 | array( 59 | 'pid' => $pid, 60 | 'lat' => $point->lat, 61 | 'lng'=> $point->lng 62 | ), 63 | array( 64 | '%d', 65 | '%f', 66 | '%f' 67 | ) 68 | ); 69 | } 70 | 71 | $args = array( 72 | 'post_type' => 'post', 73 | 'posts_per_page' => 10, 74 | 'ignore_sticky_posts' => true, 75 | 'orderby' => array( 'title' => 'DESC' ), 76 | 'geo_query' => array( 77 | 'table' => 'custom_table', // Table name for the geo custom table. 78 | 'pid_col' => 'pid', // Column name for the post ID data 79 | 'lat_col' => 'lat', // Column name for the latitude data 80 | 'lng_col' => 'lng', // Column name for the longitude data 81 | 'lat' => 64.0, // Latitude point 82 | 'lng' => -22.0, // Longitude point 83 | 'radius' => 1, // Find locations within a given radius (km) 84 | 'order' => 'DESC', // Order by distance 85 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 86 | 'context' => '\\Birgir\\Geo\\GeoQueryPostCustomTableHaversine', // Default implementation, you can use your own here instead. 87 | ), 88 | ); 89 | 90 | $query = new WP_Query( $args ); 91 | 92 | $this->assertSame( 1, count( $query->posts ) ); 93 | $this->assertSame( 'Reykjavik #1', $query->posts[0]->post_title ); 94 | $this->assertGreaterThan( 0, $query->posts[0]->distance_value ); 95 | } 96 | 97 | /** 98 | * Tests fetching points within 1 km radius 99 | * 100 | */ 101 | public function test_1_km_radius() { 102 | 103 | $p1 = self::factory()->post->create( array( 104 | 'post_title' => 'Reykjavik #1', 105 | ) ); 106 | add_post_meta( $p1, 'my_lat', '64' ); 107 | add_post_meta( $p1, 'my_lng', '-22.001' ); 108 | 109 | $p2 = self::factory()->post->create( array( 110 | 'post_title' => 'Reykjavik #2', 111 | ) ); 112 | add_post_meta( $p2, 'my_lat', '64' ); 113 | add_post_meta( $p2, 'my_lng', '-22.101' ); 114 | 115 | $p3 = self::factory()->post->create( array( 116 | 'post_title' => 'Akureyri', 117 | ) ); 118 | add_post_meta( $p3, 'my_lat', '65.6839' ); 119 | add_post_meta( $p3, 'my_lng', '-18.1105' ); 120 | 121 | $args = array( 122 | 'post_type' => 'post', 123 | 'posts_per_page' => 10, 124 | 'ignore_sticky_posts' => true, 125 | 'orderby' => array( 'title' => 'DESC' ), 126 | 'geo_query' => array( 127 | 'lat' => 64, // Latitude point 128 | 'lng' => -22, // Longitude point 129 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 130 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 131 | 'radius' => 1, // Find locations within a given radius (km) 132 | 'order' => 'DESC', // Order by distance 133 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 134 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 135 | ), 136 | ); 137 | 138 | $query = new WP_Query( $args ); 139 | 140 | $this->assertSame( 1, count( $query->posts ) ); 141 | $this->assertSame( 'Reykjavik #1', $query->posts[0]->post_title ); 142 | } 143 | 144 | /** 145 | * Tests fetching points within 1 km radius 146 | * 147 | */ 148 | public function test_distance() { 149 | 150 | $p1 = self::factory()->post->create( array( 151 | 'post_title' => 'Reykjavik #1', 152 | ) ); 153 | add_post_meta( $p1, 'my_lat', '38.897147' ); 154 | add_post_meta( $p1, 'my_lng', '-77.043934' ); 155 | 156 | 157 | $args = array( 158 | 'post_type' => 'post', 159 | 'posts_per_page' => 10, 160 | 'ignore_sticky_posts' => true, 161 | 'orderby' => array( 'title' => 'DESC' ), 162 | 'geo_query' => array( 163 | 'lat' => 38.898556, // Latitude point 164 | 'lng' => -77.037852, // Longitude point 165 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 166 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 167 | 'radius' => 1, // Find locations within a given radius (km) 168 | 'order' => 'DESC', // Order by distance 169 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 170 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 171 | ), 172 | ); 173 | 174 | $query = new WP_Query( $args ); 175 | 176 | $actual_distance_value = 0; 177 | while( $query->have_posts() ) { 178 | $query->the_post(); 179 | $actual_distance_value = get_post_field( 'distance_value' ); 180 | } 181 | 182 | $expected_distance_value = .549; 183 | 184 | $error_ratio = ( abs( $actual_distance_value - $expected_distance_value ) / $expected_distance_value ) * 100; 185 | 186 | $this->assertTrue( .11 > $error_ratio ); 187 | } 188 | 189 | /** 190 | * Tests fetching points within a radius less than 1km 191 | * 192 | * @ticket 6 193 | */ 194 | public function test_radius_less_than_1_km() { 195 | 196 | $p1 = self::factory()->post->create( array( 197 | 'post_title' => 'Reykjavik #1', 198 | ) ); 199 | add_post_meta( $p1, 'my_lat', '64' ); 200 | add_post_meta( $p1, 'my_lng', '-22.001' ); 201 | 202 | $p2 = self::factory()->post->create( array( 203 | 'post_title' => 'Reykjavik #2', 204 | ) ); 205 | add_post_meta( $p2, 'my_lat', '64' ); 206 | add_post_meta( $p2, 'my_lng', '-22.101' ); 207 | 208 | $p3 = self::factory()->post->create( array( 209 | 'post_title' => 'Akureyri', 210 | ) ); 211 | add_post_meta( $p3, 'my_lat', '65.6839' ); 212 | add_post_meta( $p3, 'my_lng', '-18.1105' ); 213 | 214 | $args = array( 215 | 'post_type' => 'post', 216 | 'posts_per_page' => 10, 217 | 'ignore_sticky_posts' => true, 218 | 'orderby' => array( 'title' => 'DESC' ), 219 | 'geo_query' => array( 220 | 'lat' => 64, // Latitude point 221 | 'lng' => -22, // Longitude point 222 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 223 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 224 | 'radius' => 0.54321, // Find locations within a given radius (km) 225 | 'order' => 'DESC', // Order by distance 226 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 227 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 228 | ), 229 | ); 230 | 231 | $query = new WP_Query( $args ); 232 | 233 | $this->assertSame( 1, count( $query->posts ) ); 234 | $this->assertSame( 'Reykjavik #1', $query->posts[0]->post_title ); 235 | } 236 | 237 | /** 238 | * Tests distance from Reykjavik - London 239 | * 240 | * @ticket 6 241 | */ 242 | public function test_distance_from_reykjavik_london() { 243 | 244 | $p1 = self::factory()->post->create( array( 245 | 'post_title' => 'London', 246 | ) ); 247 | add_post_meta( $p1, 'my_lat', '51.509865' ); 248 | add_post_meta( $p1, 'my_lng', '-0.118092' ); 249 | 250 | $args = array( 251 | 'post_type' => 'post', 252 | 'posts_per_page' => 10, 253 | 'ignore_sticky_posts' => true, 254 | 'orderby' => array( 'title' => 'DESC' ), 255 | 'geo_query' => array( 256 | 'lat' => 64.128288, // Latitude point 257 | 'lng' => -21.827774, // Longitude point 258 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 259 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 260 | 'radius' => 5000, // Find locations within a given radius (km) 261 | 'order' => 'DESC', // Order by distance 262 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 263 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 264 | ), 265 | ); 266 | 267 | $query = new WP_Query( $args ); 268 | 269 | $this->assertSame( 1, count( $query->posts ) ); 270 | $this->assertSame( 'London', $query->posts[0]->post_title ); 271 | $this->assertSame( 1881 , (int) $query->posts[0]->distance_value ); 272 | } 273 | 274 | /** 275 | * Tests a search query. 276 | * 277 | * @ticket 9 278 | */ 279 | public function test_search_query() { 280 | // Arrange. 281 | 282 | $p1 = self::factory()->post->create( array( 283 | 'post_title' => 'London', 284 | ) ); 285 | add_post_meta( $p1, 'my_lat', '51.509865' ); 286 | add_post_meta( $p1, 'my_lng', '-0.118092' ); 287 | 288 | $p2 = self::factory()->post->create( array( 289 | 'post_title' => 'Reykjavik', 290 | ) ); 291 | add_post_meta( $p2, 'my_lat', '64' ); 292 | add_post_meta( $p2, 'my_lng', '-22.101' ); 293 | 294 | // Act. 295 | $args = array( 296 | 's' => 'london', // Search. 297 | 'post_type' => 'post', 298 | 'posts_per_page' => 10, 299 | 'ignore_sticky_posts' => true, 300 | 'orderby' => array( 'title' => 'DESC' ), 301 | 'geo_query' => array( 302 | 'lat' => 64.128288, // Latitude point 303 | 'lng' => -21.827774, // Longitude point 304 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 305 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 306 | 'radius' => 5000, // Find locations within a given radius (km) 307 | 'order' => 'DESC', // Order by distance 308 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 309 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 310 | ), 311 | ); 312 | 313 | $query = new WP_Query( $args ); 314 | 315 | // Assert. 316 | $this->assertSame( 1, count( $query->posts ) ); 317 | $this->assertSame( 'London', $query->posts[0]->post_title ); 318 | $this->assertSame( 1881 , (int) $query->posts[0]->distance_value ); 319 | } 320 | 321 | /** 322 | * Tests a search query that should give empty result. 323 | * 324 | * @ticket 9 325 | */ 326 | public function test_search_query_should_empty_result() { 327 | // Arrange. 328 | $p1 = self::factory()->post->create( array( 329 | 'post_title' => 'London', 330 | ) ); 331 | add_post_meta( $p1, 'my_lat', '51.509865' ); 332 | add_post_meta( $p1, 'my_lng', '-0.118092' ); 333 | 334 | $p2 = self::factory()->post->create( array( 335 | 'post_title' => 'Reykjavik', 336 | ) ); 337 | add_post_meta( $p2, 'my_lat', '64' ); 338 | add_post_meta( $p2, 'my_lng', '-22.101' ); 339 | 340 | // Act. 341 | $args = array( 342 | 's' => 'akureyri', // Search. 343 | 'post_type' => 'post', 344 | 'posts_per_page' => 10, 345 | 'ignore_sticky_posts' => true, 346 | 'orderby' => array( 'title' => 'DESC' ), 347 | 'geo_query' => array( 348 | 'lat' => 64.128288, // Latitude point 349 | 'lng' => -21.827774, // Longitude point 350 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 351 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 352 | 'radius' => 5000, // Find locations within a given radius (km) 353 | 'order' => 'DESC', // Order by distance 354 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 355 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 356 | ), 357 | ); 358 | 359 | $query = new WP_Query( $args ); 360 | 361 | // Assert. 362 | $this->assertEmpty( $query->posts ); 363 | } 364 | 365 | /** 366 | * Tests for applying a geo query on the search query with `pre_get_posts hook`. 367 | * 368 | * @ticket 9 369 | */ 370 | public function test_search_query_with_pre_get_posts_hook() { 371 | // Arrange. 372 | $p1 = self::factory()->post->create( array( 373 | 'post_title' => 'London', 374 | ) ); 375 | add_post_meta( $p1, 'my_lat', '51.509865' ); 376 | add_post_meta( $p1, 'my_lng', '-0.118092' ); 377 | 378 | $p2 = self::factory()->post->create( array( 379 | 'post_title' => 'Reykjavik', 380 | ) ); 381 | add_post_meta( $p2, 'my_lat', '64' ); 382 | add_post_meta( $p2, 'my_lng', '-22.101' ); 383 | 384 | // Act. 385 | $args = array( 386 | 's' => 'london', // Search. 387 | 'post_type' => 'post', 388 | 'posts_per_page' => 10, 389 | 'ignore_sticky_posts' => true, 390 | 'orderby' => array( 'title' => 'DESC' ), 391 | ); 392 | 393 | add_action( 'pre_get_posts', array( $this, 'override_search_query' ) ); 394 | $query = new WP_Query( $args ); 395 | remove_action( 'pre_get_posts', array( $this, 'override_search_query' ) ); 396 | 397 | // Assert. 398 | $this->assertCount( 1, $query->posts ); 399 | $this->assertContains( 'RADIANS', $query->request ); 400 | $this->assertSame( 'London', $query->posts[0]->post_title ); 401 | $this->assertSame( 1881 , (int) $query->posts[0]->distance_value ); 402 | } 403 | 404 | /** 405 | * Tests for applying a geo query on users. 406 | */ 407 | public function test_1_km_user_query() { 408 | $u1 = self::factory()->user->create( array( 409 | 'first_name' => 'Site', 410 | 'last_name' => 'Crafting', 411 | )); 412 | add_user_meta( $u1, 'my_lat', 47.236567 ); 413 | add_user_meta( $u1, 'my_lng', -122.4357428 ); 414 | 415 | $u2 = self::factory()->user->create( array( 416 | 'first_name' => 'Point', 417 | 'last_name' => 'Defiance', 418 | )); 419 | add_user_meta( $u2, 'my_lat', 47.3048779 ); 420 | add_user_meta( $u2, 'my_lng', -122.5230098 ); 421 | 422 | $u3 = self::factory()->user->create( array( 423 | 'first_name' => 'Space', 424 | 'last_name' => 'Needle', 425 | )); 426 | add_user_meta( $u3, 'my_lat', 47.6205099 ); 427 | add_user_meta( $u3, 'my_lng', -122.3514661 ); 428 | 429 | $args = array( 430 | 'role' => 'subscriber', 431 | 'geo_query' => array( 432 | 'lat' => 47.236, 433 | 'lng' => -122.435, 434 | 'lat_meta_key' => 'my_lat', 435 | 'lng_meta_key' => 'my_lng', 436 | 'radius' => 1, 437 | 'context' => '\\Birgir\\Geo\\GeoQueryUserHaversine', 438 | ), 439 | ); 440 | 441 | $query = new WP_User_Query( $args ); 442 | 443 | $this->assertCount( 1, $query->results ); 444 | $this->assertSame( 'Site', $query->results[0]->first_name ); 445 | } 446 | 447 | /** 448 | * Tests for query ordering when applying a geo query on users. 449 | * 450 | * @ticket 14 451 | */ 452 | public function test_user_query_ordering() { 453 | $u1 = self::factory()->user->create( array( 454 | 'display_name' => 'B - Site Crafting', 455 | )); 456 | add_user_meta( $u1, 'my_lat', 47.236567 ); 457 | add_user_meta( $u1, 'my_lng', -122.4357428 ); 458 | 459 | $u2 = self::factory()->user->create( array( 460 | 'display_name' => 'A - Site Crafting', 461 | )); 462 | add_user_meta( $u2, 'my_lat', 47.236567 ); 463 | add_user_meta( $u2, 'my_lng', -122.4357428 ); 464 | 465 | $u3 = self::factory()->user->create( array( 466 | 'display_name' => 'A - Point Defiance', 467 | )); 468 | add_user_meta( $u3, 'my_lat', 47.3048779 ); 469 | add_user_meta( $u3, 'my_lng', -122.5230098 ); 470 | 471 | $u4 = self::factory()->user->create( array( 472 | 'display_name' => 'B - Point Defiance', 473 | )); 474 | add_user_meta( $u4, 'my_lat', 47.3048779 ); 475 | add_user_meta( $u4, 'my_lng', -122.5230098 ); 476 | 477 | $u5 = self::factory()->user->create( array( 478 | 'display_name' => 'Space Needle', 479 | )); 480 | add_user_meta( $u5, 'my_lat', 47.6205099 ); 481 | add_user_meta( $u5, 'my_lng', -122.3514661 ); 482 | 483 | $args = array( 484 | 'role' => 'subscriber', 485 | 'geo_query' => array( 486 | 'lat' => 47.236, 487 | 'lng' => -122.435, 488 | 'lat_meta_key' => 'my_lat', 489 | 'lng_meta_key' => 'my_lng', 490 | 'radius' => 40, // Exclude Space Needle. 491 | 'context' => '\\Birgir\\Geo\\GeoQueryUserHaversine', 492 | 'order' => 'DESC', 493 | ), 494 | 'orderby' => 'display_name', 495 | 'order' => 'ASC', 496 | ); 497 | 498 | $query = new WP_User_Query( $args ); 499 | 500 | $this->assertCount( 4, $query->results ); 501 | $this->assertSame( 502 | array( 'A - Point Defiance', 'B - Point Defiance', 'A - Site Crafting', 'B - Site Crafting' ), 503 | array( 504 | $query->results[0]->display_name, 505 | $query->results[1]->display_name, 506 | $query->results[2]->display_name, 507 | $query->results[3]->display_name, 508 | ) 509 | ); 510 | } 511 | 512 | /** 513 | * Callback to apply a geo query. 514 | * 515 | * @param WP_Query $query Instance of WP_Query. 516 | */ 517 | public function override_search_query( \WP_Query $query ) { 518 | if ( $query->is_search() ) { 519 | $query->set( 'geo_query', array( 520 | 'lat' => 64.128288, // Latitude point 521 | 'lng' => -21.827774, // Longitude point 522 | 'lat_meta_key' => 'my_lat', // Meta-key for the latitude data 523 | 'lng_meta_key' => 'my_lng', // Meta-key for the longitude data 524 | 'radius' => 5000, // Find locations within a given radius (km) 525 | 'order' => 'DESC', // Order by distance 526 | 'distance_unit' => 111.045, // Default distance unit (km per degree). Use 69.0 for statute miles per degree. 527 | 'context' => '\\Birgir\\Geo\\GeoQueryHaversine', // Default implementation, you can use your own here instead. 528 | ) ); 529 | } 530 | } 531 | } 532 | --------------------------------------------------------------------------------