From 74a5d13a73d3c1c71fed15c2af0000ec90ac6ff4 Mon Sep 17 00:00:00 2001 From: Jayden Smith Date: Mon, 7 Sep 2020 14:50:32 +1000 Subject: [PATCH] Allow filtering of multiple locations. --- src/services/MapService.php | 197 ++++++++++++++++++++---------------- 1 file changed, 112 insertions(+), 85 deletions(-) diff --git a/src/services/MapService.php b/src/services/MapService.php index 902f956..c78ce3e 100644 --- a/src/services/MapService.php +++ b/src/services/MapService.php @@ -212,7 +212,6 @@ public function modifyElementsQuery (ElementQueryInterface $query, $value, MapFi } $oldOrderBy = null; - $search = false; if (!is_array($query->orderBy)) { @@ -220,12 +219,7 @@ public function modifyElementsQuery (ElementQueryInterface $query, $value, MapFi $query->orderBy = []; } - // Coordinate CraftQL support - if (array_key_exists('coordinate', $value)) - $value['location'] = $value['coordinate']; - - if (array_key_exists('location', $value)) - $search = $this->_searchLocation($query, $value, $alias); + $search = $this->_searchLocation($query, $value, $alias); if (array_key_exists('distance', $query->orderBy)) $this->_replaceOrderBy($query, $search); @@ -298,90 +292,118 @@ public function populateMissingData (Map $map, MapField $field) * search string if we can. * * @param ElementQuery $query - * @param mixed $value - * @param string $table + * @param mixed $values + * @param string $table * - * @return bool|string + * @return array|bool * @throws \Exception */ - private function _searchLocation (ElementQuery $query, $value, $table) + private function _searchLocation (ElementQuery $query, $values, $table) { - $location = $value['location']; - $country = $value['country'] ?? null; - $radius = $value['radius'] ?? 50.0; - $unit = $value['unit'] ?? 'km'; - - // Normalize location - $location = GeoService::normalizeLocation($location, $country); - - if ($location === null) - return false; - - $lat = $location['lat']; - $lng = $location['lng']; - - // Normalize radius - if (!is_numeric($radius)) - $radius = (float) $radius; - - if (!is_numeric($radius)) - $radius = 50.0; - - // Normalize unit - $unit = GeoService::normalizeDistance($unit); - - // Base Distance - $distance = $unit === 'km' ? '111.045' : '69.0'; - - // Store for populating search result distance - $this->_location = $location; - $this->_distance = (float) $distance; - - // Search Query - $search = str_replace(["\r", "\n", "\t"], '', "( - $distance * - DEGREES( - ACOS( - COS(RADIANS($lat)) * - COS(RADIANS([[$table.lat]])) * - COS(RADIANS($lng) - RADIANS([[$table.lng]])) + - SIN(RADIANS($lat)) * - SIN(RADIANS([[$table.lat]])) + if (array_key_exists('location', $values)) { + $values = [$values]; + } + + $queryOrderBy = []; + $queryRestrict = ['or']; + $queryWhere = ['or']; + $queryHaving = ['or']; + + foreach ($values as $index => $value) { + // Coordinate CraftQL support + if (array_key_exists('coordinate', $value)) + $value['location'] = $value['coordinate']; + + $location = $value['location']; + $country = $value['country'] ?? null; + $radius = $value['radius'] ?? 50.0; + $unit = $value['unit'] ?? 'km'; + + // Normalize location + $location = GeoService::normalizeLocation($location, $country); + + if ($location === null) + return false; + + $lat = $location['lat']; + $lng = $location['lng']; + + // Normalize radius + if (!is_numeric($radius)) + $radius = (float) $radius; + + if (!is_numeric($radius)) + $radius = 50.0; + + // Normalize unit + $unit = GeoService::normalizeDistance($unit); + + // Base Distance + $distance = $unit === 'km' ? '111.045' : '69.0'; + + // Store for populating search result distance + $this->_location = $location; + $this->_distance = (float) $distance; + + // Search Query + $search = str_replace(["\r", "\n", "\t"], '', "( + $distance * + DEGREES( + ACOS( + COS(RADIANS($lat)) * + COS(RADIANS([[$table.lat]])) * + COS(RADIANS($lng) - RADIANS([[$table.lng]])) + + SIN(RADIANS($lat)) * + SIN(RADIANS([[$table.lat]])) + ) ) - ) - )"); + )"); - // Restrict the results - $restrict = [ - 'and', - [ - 'and', - "[[$table.lat]] >= $lat - ($radius / $distance)", - "[[$table.lat]] <= $lat + ($radius / $distance)", - ], - [ + // Restrict the results + $queryRestrict[] = [ 'and', - "[[$table.lng]] >= $lng - ($radius / ($distance * COS(RADIANS($lat))))", - "[[$table.lng]] <= $lng + ($radius / ($distance * COS(RADIANS($lat))))", - ] - ]; - - // Filter the query - $query - ->subQuery - ->addSelect($search . ' as [[mapsCalculatedDistance]]') - ->andWhere($restrict) - ->andWhere([ - 'not', - ['[[' . $table . '.lat]]' => null], - ]); + [ + 'and', + "[[$table.lat]] >= $lat - ($radius / $distance)", + "[[$table.lat]] <= $lat + ($radius / $distance)", + ], + [ + 'and', + "[[$table.lng]] >= $lng - ($radius / ($distance * COS(RADIANS($lat))))", + "[[$table.lng]] <= $lng + ($radius / ($distance * COS(RADIANS($lat))))", + ] + ]; + + $key = "mapsCalculatedDistance_$index"; + + // Filter the query + $query + ->subQuery + ->addSelect("$search as [[$key]]"); + + $queryWhere[] = "$search <= $radius"; + $queryHaving[] = "[[$key]] <= $radius"; + $queryOrderBy[] = "[[$key]]"; + } - if (Craft::$app->getDb()->driverName === 'pgsql') - $query->subQuery->andWhere($search . ' <= ' . $radius); - else - $query->subQuery->andHaving('[[mapsCalculatedDistance]] <= ' . $radius); + if ($queryOrderBy) { + $query + ->subQuery + ->andWhere($queryRestrict) + ->andWhere([ + 'not', + ['[[' . $table . '.lat]]' => null], + ]); + + if (Craft::$app->getDb()->driverName === 'pgsql') + $query->subQuery->andWhere($queryWhere); + else + $query->subQuery->andHaving($queryHaving); + + return $queryOrderBy; + } - return '[[mapsCalculatedDistance]]'; + return false; } /** @@ -389,7 +411,7 @@ private function _searchLocation (ElementQuery $query, $value, $table) * or otherwise remove it. * * @param ElementQuery $query - * @param bool $search + * @param array|bool $search */ private function _replaceOrderBy (ElementQuery $query, $search = false) { @@ -397,8 +419,13 @@ private function _replaceOrderBy (ElementQuery $query, $search = false) foreach ((array) $query->orderBy as $order => $sort) { - if ($order === 'distance' && $search) $nextOrder[$search] = $sort; - elseif ($order !== 'distance') $nextOrder[$order] = $sort; + if ($order === 'distance' && $search) { + foreach ($search as $searchParam) { + $nextOrder[$searchParam] = $sort; + } + } elseif ($order !== 'distance') { + $nextOrder[$order] = $sort; + } } $query->orderBy($nextOrder);