diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a278efe..9f17ef9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,11 @@ jobs: run: | sudo service mysql start mysql -h 127.0.0.1 -u root -proot -e 'CREATE DATABASE cakephp;' + - name: Setup Geography for PostgreSQL + if: matrix.db-type == 'pgsql' + run: | + sudo apt-get install postgis + - name: Setup PHP uses: shivammathur/setup-php@v2 with: diff --git a/docs/Behavior/Geocoder.md b/docs/Behavior/Geocoder.md index e8e3b01..c568fad 100644 --- a/docs/Behavior/Geocoder.md +++ b/docs/Behavior/Geocoder.md @@ -166,3 +166,18 @@ You can test the geocoding and also remove cache data where needed. ## Providers Full list of existing providers [here](https://github.com/geocoder-php/Geocoder#providers). + +## Spatial +You can also use the "spatial" finder using coordinates as POINT instead of lat/lng. +```php +$query = $this->Addresses->find('spatial', [ + 'lat' => 13.3, + 'lng' => 19.2, + 'distance' => 100, +]); +``` +Note: This only works with PostGIS and MySQL 5.7+ (and MariaDB 10.4+) databases, as they support spatial data types. + +There can be a performance improvement when using spatial indexes, so you might want to consider using this for larger datasets. +In reality I didnt get the index to work, though. Only managed to limit down the looked up entries from full table scan to range, which +sped it up, too. diff --git a/docs/Model/GeocodedAddresses.md b/docs/Model/GeocodedAddresses.md index 57016f7..2f4e004 100644 --- a/docs/Model/GeocodedAddresses.md +++ b/docs/Model/GeocodedAddresses.md @@ -25,7 +25,7 @@ if ($address && $address->lat && $address->lng) { } ``` -Don't forget to add the Type mapping of `Geo\Database\Type\ObjectType` in your bootstrap.php. +Remember to add the Type mapping of `Geo\Database\Type\ObjectType` in your bootstrap.php. ```php TypeFactory::map('object', 'Geo\Database\Type\ObjectType'); ``` diff --git a/src/Controller/Admin/GeocodedAddressesController.php b/src/Controller/Admin/GeocodedAddressesController.php index d5f3bee..fdc4b83 100644 --- a/src/Controller/Admin/GeocodedAddressesController.php +++ b/src/Controller/Admin/GeocodedAddressesController.php @@ -9,7 +9,7 @@ * * @property \Geo\Model\Table\GeocodedAddressesTable $GeocodedAddresses * - * @method \Cake\Datasource\ResultSetInterface<\Geo\Model\Entity\GeocodedAddress> paginate($object = null, array $settings = []) + * @method \Cake\Datasource\ResultSetInterface<\Geo\Model\Entity\GeocodedAddress> paginate(\Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface|string|null $object = null, array $settings = []) */ class GeocodedAddressesController extends AppController { diff --git a/src/Model/Behavior/GeocoderBehavior.php b/src/Model/Behavior/GeocoderBehavior.php index 5c45d03..0ee98ff 100644 --- a/src/Model/Behavior/GeocoderBehavior.php +++ b/src/Model/Behavior/GeocoderBehavior.php @@ -76,6 +76,7 @@ class GeocoderBehavior extends Behavior { 'unit' => Calculator::UNIT_KM, 'implementedFinders' => [ 'distance' => 'findDistance', + 'spatial' => 'findSpatial', ], 'validationError' => null, 'cache' => false, // Enable only if you got a GeocodedAddresses table running @@ -401,6 +402,60 @@ public function findDistance(SelectQuery $query, ?float $lat = null, ?float $lng return $query; } + /** + * @param \Cake\ORM\Query\SelectQuery $query + * @param float|null $lat + * @param float|null $lng + * @param \Geocoder\Model\Coordinates|null $coordinates + * @param int|null $distance + * @param string|null $tableName + * @param bool $sort + * @return \Cake\ORM\Query\SelectQuery + */ + public function findSpatial(SelectQuery $query, ?float $lat = null, ?float $lng = null, ?Coordinates $coordinates = null, ?int $distance = null, ?string $tableName = null, bool $sort = true): SelectQuery { + $options = [ + 'tableName' => $tableName, + 'sort' => $sort, + 'lat' => $lat, + 'lng' => $lng, + 'distance' => $distance, + 'coordinates' => $coordinates, + ]; + $options = $this->assertCoordinates($options); + + if ($query->isAutoFieldsEnabled() === null) { + $query->enableAutoFields(true); + } + + $lat = $options[static::OPTION_LAT]; + $lng = $options[static::OPTION_LNG]; + + // Add distance calculation as a virtual field + $query->select([ + 'distance' => new QueryExpression( + "ST_Distance_Sphere(coordinates, ST_GeomFromText('POINT($lng $lat)')) / 1000", + ), + ]); + + // Filter by max distance if limit is provided + if (isset($options['distance'])) { + $distance = (float)$options['distance']; + $query->where(function (QueryExpression $exp) use ($lat, $lng, $distance) { + return $exp->lte( + new QueryExpression("ST_Distance_Sphere(coordinates, ST_GeomFromText('POINT($lng $lat)')) / 1000"), + $distance, + ); + }); + } + + if ($options['sort']) { + $sort = $options['sort'] === true ? 'ASC' : $options['sort']; + $query->orderBy(['distance' => $sort]); + } + + return $query; + } + /** * Forms a sql snippet for distance calculation on db level using two lat/lng points. * @@ -653,6 +708,10 @@ protected function assertCoordinates(array $options): array { throw new InvalidArgumentException($error); } + if ($options[static::OPTION_LAT] < -90 || $options[static::OPTION_LAT] > 90 || $options[static::OPTION_LNG] < -180 || $options[static::OPTION_LNG] > 180) { + throw new InvalidArgumentException('Invalid latitude or longitude in (' . $options[static::OPTION_LAT] . '/' . $options[static::OPTION_LNG] . ').'); + } + return $options; } diff --git a/templates/Admin/GeocodedAddresses/index.php b/templates/Admin/GeocodedAddresses/index.php index 61e38bd..23cdfbc 100644 --- a/templates/Admin/GeocodedAddresses/index.php +++ b/templates/Admin/GeocodedAddresses/index.php @@ -1,10 +1,13 @@ $geocodedAddresses */ +use Cake\Core\Plugin; + +?> -use Cake\Core\Plugin; ?>