├── .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 | [](https://travis-ci.org/birgire/geo-query)
5 | [](https://github.com/birgire/geo-query/blob/master/LICENCE)
6 | [](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 |
--------------------------------------------------------------------------------