From 70c0e222964bea697fb8475a6071f2a997abf85c Mon Sep 17 00:00:00 2001 From: Jan Henckens Date: Sat, 4 Jan 2025 20:41:40 +0100 Subject: [PATCH 1/7] Google geocoding for Adress element Signed-off-by: Jan Henckens --- src/EasyAddressField.php | 29 ++++++-- src/models/EasyAddressFieldSettingsModel.php | 3 + src/services/GeoLocationService.php | 72 ++++++++++++++++++-- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/EasyAddressField.php b/src/EasyAddressField.php index 87abbd6..0e962ec 100644 --- a/src/EasyAddressField.php +++ b/src/EasyAddressField.php @@ -6,10 +6,14 @@ namespace studioespresso\easyaddressfield; use Craft; +use craft\base\Element; use craft\base\Model; use craft\base\Plugin; +use craft\elements\Address; +use craft\events\ModelEvent; use craft\events\RegisterComponentTypesEvent; use craft\feedme\events\RegisterFeedMeFieldsEvent; +use craft\helpers\ElementHelper; use craft\helpers\UrlHelper; use craft\services\Fields; use craft\web\twig\variables\CraftVariable; @@ -58,18 +62,35 @@ public function init() ]); // Register our fields - Event::on(Fields::className(), Fields::EVENT_REGISTER_FIELD_TYPES, function(RegisterComponentTypesEvent $event) { + Event::on(Fields::className(), Fields::EVENT_REGISTER_FIELD_TYPES, function (RegisterComponentTypesEvent $event) { $event->types[] = EasyAddressFieldField::class; }); + Event::on(Address::class, Element::EVENT_BEFORE_SAVE, function (ModelEvent $event) { + /* @var Address $element */ + $element = $event->sender; + if (ElementHelper::isDraftOrRevision($element)) { + return; + } + if ($this->getSettings()->enableGeoCodingForCraftElements) { + $coordinates = $this->geoLocation()->locateElement($element); + if ($coordinates) { + $element->setAttributes([ + 'longitude' => $coordinates['longitude'], + 'latitude' => $coordinates['latitude'], + ]); + } + } + }); + // Register our twig functions - Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function(Event $event) { + Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function (Event $event) { $variable = $event->sender; $variable->set('address', AddressVariable::class); }); /** @phpstan-ignore-next-line */ - Event::on(EasyAddressFieldField::class, 'craftQlGetFieldSchema', function(GetFieldSchema $event) { + Event::on(EasyAddressFieldField::class, 'craftQlGetFieldSchema', function (GetFieldSchema $event) { $event->handled = true; $field = $event->sender; $object = $event->schema->createObjectType(ucfirst($field->handle) . 'EasyAddressField'); @@ -89,7 +110,7 @@ public function init() // If craftcms/feed-me is installed & activacted, hook here to register the field for import if (Craft::$app->getPlugins()->isPluginEnabled('feed-me')) { /** @phpstan-ignore-next-line */ - Event::on(\craft\feedme\services\Fields::class, \craft\feedme\services\Fields::EVENT_REGISTER_FEED_ME_FIELDS, function(RegisterFeedMeFieldsEvent $e) { + Event::on(\craft\feedme\services\Fields::class, \craft\feedme\services\Fields::EVENT_REGISTER_FEED_ME_FIELDS, function (RegisterFeedMeFieldsEvent $e) { $e->fields[] = EasyAddressFieldFeedMe::class; }); } diff --git a/src/models/EasyAddressFieldSettingsModel.php b/src/models/EasyAddressFieldSettingsModel.php index 6727546..605831f 100644 --- a/src/models/EasyAddressFieldSettingsModel.php +++ b/src/models/EasyAddressFieldSettingsModel.php @@ -12,5 +12,8 @@ class EasyAddressFieldSettingsModel extends Model { public string $geoCodingService = 'nomanatim'; + public ?string $googleApiKey = null; + + public bool $enableGeoCodingForCraftElements = true; } diff --git a/src/services/GeoLocationService.php b/src/services/GeoLocationService.php index 9191d2e..94032c9 100644 --- a/src/services/GeoLocationService.php +++ b/src/services/GeoLocationService.php @@ -4,6 +4,7 @@ use Craft; use craft\base\Component; +use craft\elements\Address; use craft\helpers\Json; use GuzzleHttp\Client; use maxh\Nominatim\Nominatim; @@ -31,9 +32,9 @@ public function locate(EasyAddressFieldModel $model) try { if (!$model->latitude && !$model->longitude and strlen($model->toString()) >= 2) { if ($this->settings->geoCodingService === 'google') { - $model = $this->geocodeGoogle($model); + $model = $this->geocodeModelGoogle($model); } else { - $model = $this->geocodeOSM($model); + $model = $this->geocodeModelOSM($model); } } return $model; @@ -43,7 +44,70 @@ public function locate(EasyAddressFieldModel $model) } } - private function geocodeGoogle(EasyAddressFieldModel $model) + public function locateElement(Address $element) + { + try { + if (!$element->latitude && !$element->longitude && $element->countryCode) { + if ($this->settings->geoCodingService === 'google') { + $coordinates = $this->geocodeElementGoogle($element); + return $coordinates; + } else { + $coordinates = $this->geocodeElementOSM(); + } + return []; + } + } catch (\Throwable $exception) { + \Craft::error($exception->getMessage(), 'easy-address-field'); + return $element; + } + } + + + private function geocodeElementGoogle(Address $element) + { + if (!$this->settings->googleApiKey) { + return $element; + } + + if (!$element->latitude && !$element->longitude && $element->countryCode) { + $client = new Client(['base_uri' => 'https://maps.googleapis.com']); + $fields = [ + $element->addressLine1, + $element->addressLine2, + $element->addressLine3, + $element->postalCode, + $element->locality, + $element->countryCode + ]; + $fields = array_filter($fields); + $data = implode('+', $fields); + $request = $client->request('GET', + 'maps/api/geocode/json?address=' . urlencode($data) . '&key=' . Craft::parseEnv($this->settings->googleApiKey) . '', + ['allow_redirects' => false] + ); + $json = Json::decodeIfJson($request->getBody()->getContents()); + + if ($json['status'] !== 'OK' && $json['error_message']) { + if (Craft::$app->getConfig()->general->devMode) { + throw new InvalidConfigException('Google API error: ' . $json['error_message']); + } + Craft::error($json['error_message'], 'easy-address-field'); + } + + if ($json['status'] === 'OK') { + if ($json['results'][0]['geometry']['location']) { + return [ + 'latitude' => $json['results'][0]['geometry']['location']['lat'], + 'longitude' => $json['results'][0]['geometry']['location']['lng'], + ]; + } + } + } + + return []; + } + + private function geocodeModelGoogle(EasyAddressFieldModel $model) { if (!$this->settings->googleApiKey) { return $model; @@ -75,7 +139,7 @@ private function geocodeGoogle(EasyAddressFieldModel $model) return $model; } - private function geocodeOSM(EasyAddressFieldModel $model) + private function geocodeModelOSM(EasyAddressFieldModel $model) { // url encode the address $url = "http://nominatim.openstreetmap.org/"; From 17935c53c917c525cecbc4d0b5fd087381080931 Mon Sep 17 00:00:00 2001 From: Jan Henckens Date: Sat, 4 Jan 2025 20:44:41 +0100 Subject: [PATCH 2/7] Nomanatim geocoding for Address elements Signed-off-by: Jan Henckens --- src/services/GeoLocationService.php | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/services/GeoLocationService.php b/src/services/GeoLocationService.php index 94032c9..1c5ee46 100644 --- a/src/services/GeoLocationService.php +++ b/src/services/GeoLocationService.php @@ -139,6 +139,57 @@ private function geocodeModelGoogle(EasyAddressFieldModel $model) return $model; } + + private function geocodeElementOSM(Address $address): array + { + // url encode the address + $url = "http://nominatim.openstreetmap.org/"; + $nominatim = new Nominatim($url); + $search = $nominatim->newSearch() + ->countryCode($address->countryCode) + ->state($address->administrativeArea ?? '') + ->city($address->locality ?? '') + ->postalCode($address->postalCode ?? '') + ->street($address->addressLine1 . ' ' . $address->addressLine2 . ' ' . $address->addressLine3) + ->limit(1) + ->polygon('geojson') + ->addressDetails(); + + $result = $nominatim->find($search); + if (empty($result)) { + return []; + } + + if (isset($result[0]['lat']) && isset($result[0]['lon'])) { + return [ + 'latitude' => $result[0]['lat'], + 'longitude' => $result[0]['lon'], + ]; + + } + + if (is_array($result[0]['geojson']['coordinates'][0]) && is_array($result[0]['geojson']['coordinates'][0][0])) { + return [ + 'latitude' => $result[0]['geojson']['coordinates'][0][0][1], + 'longitude' => $result[0]['geojson']['coordinates'][0][0][0], + ]; + + } + + if (is_array($result[0]['geojson']['coordinates'][0])) { + return [ + 'latitude' => $result[0]['geojson']['coordinates'][0][1], + 'longitude' => $result[0]['geojson']['coordinates'][0][0], + ]; + } + + return [ + 'latitude' => $result[0]['geojson']['coordinates'][1], + 'longitude' => $result[0]['geojson']['coordinates'][0], + ]; + + } + private function geocodeModelOSM(EasyAddressFieldModel $model) { // url encode the address From ab92f362ae24758fd58d52609dd44bebb77f877f Mon Sep 17 00:00:00 2001 From: Jan Henckens Date: Sun, 5 Jan 2025 19:09:48 +0100 Subject: [PATCH 3/7] Register geocoder through event Signed-off-by: Jan Henckens --- src/EasyAddressField.php | 30 ++++-- src/events/RegisterGeocoderEvent.php | 10 ++ src/services/GeoLocationService.php | 95 +++-------------- src/services/geocoders/BaseGeoCoder.php | 36 +++++++ src/services/geocoders/GoogleGeoCoder.php | 104 +++++++++++++++++++ src/services/geocoders/NomanatimGeoCoder.php | 47 +++++++++ src/templates/_settings.twig | 4 +- 7 files changed, 233 insertions(+), 93 deletions(-) create mode 100644 src/events/RegisterGeocoderEvent.php create mode 100644 src/services/geocoders/BaseGeoCoder.php create mode 100644 src/services/geocoders/GoogleGeoCoder.php create mode 100644 src/services/geocoders/NomanatimGeoCoder.php diff --git a/src/EasyAddressField.php b/src/EasyAddressField.php index 0e962ec..1210a6c 100644 --- a/src/EasyAddressField.php +++ b/src/EasyAddressField.php @@ -18,11 +18,14 @@ use craft\services\Fields; use craft\web\twig\variables\CraftVariable; use markhuot\CraftQL\Events\GetFieldSchema; +use studioespresso\easyaddressfield\events\RegisterGeocoderEvent; use studioespresso\easyaddressfield\fields\EasyAddressFieldFeedMe; use studioespresso\easyaddressfield\fields\EasyAddressFieldField; use studioespresso\easyaddressfield\models\EasyAddressFieldSettingsModel; use studioespresso\easyaddressfield\services\CountriesService; use studioespresso\easyaddressfield\services\FieldService; +use studioespresso\easyaddressfield\services\geocoders\GoogleGeoCoder; +use studioespresso\easyaddressfield\services\geocoders\NomanatimGeoCoder; use studioespresso\easyaddressfield\services\GeoLocationService; use studioespresso\easyaddressfield\web\twig\variables\AddressVariable; use yii\base\Event; @@ -73,16 +76,12 @@ public function init() return; } if ($this->getSettings()->enableGeoCodingForCraftElements) { - $coordinates = $this->geoLocation()->locateElement($element); - if ($coordinates) { - $element->setAttributes([ - 'longitude' => $coordinates['longitude'], - 'latitude' => $coordinates['latitude'], - ]); - } + $event->sender = $this->geoLocation()->locateElement($element); } }); + + // Register our twig functions Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function (Event $event) { $variable = $event->sender; @@ -114,6 +113,12 @@ public function init() $e->fields[] = EasyAddressFieldFeedMe::class; }); } + + Event::on(GeoLocationService::class, GeoLocationService::EVENT_REGISTER_GEOCODERS, function (RegisterGeocoderEvent $event) { + $event->geoCoders['nomanatim'] = NomanatimGeoCoder::class; + $event->geoCoders['google'] = GoogleGeoCoder::class; + }); + } // Components @@ -137,13 +142,15 @@ protected function createSettingsModel(): Model */ protected function settingsHtml(): string { + $geoCoders = EasyAddressField::getInstance()->geoLocation->geoCoders; + $geoCoders = $geoCoders->map(function($item) { + return $item->name; + }); + return Craft::$app->getView()->renderTemplate( 'easy-address-field/_settings', [ - 'services' => [ - 'nominatim' => 'Nominatim', - 'google' => 'Google Maps', - ], + 'geoCoders' => $geoCoders->toArray(), 'settings' => $this->getSettings(), ] ); @@ -169,4 +176,5 @@ public function geoLocation(): GeoLocationService { return $this->geoLocation; } + } diff --git a/src/events/RegisterGeocoderEvent.php b/src/events/RegisterGeocoderEvent.php new file mode 100644 index 0000000..4434bc9 --- /dev/null +++ b/src/events/RegisterGeocoderEvent.php @@ -0,0 +1,10 @@ +settings = EasyAddressField::getInstance()->getSettings(); + + $event = new RegisterGeocoderEvent(); + Event::trigger(self::class, self::EVENT_REGISTER_GEOCODERS, $event); + + $this->geoCoders = collect(array_merge($this->geoCoders, $event->geoCoders))->map(function ($geoCoder) { + return \Craft::createObject($geoCoder); + }); + parent::init(); // TODO: Change the autogenerated stub } @@ -63,83 +73,6 @@ public function locateElement(Address $element) } - private function geocodeElementGoogle(Address $element) - { - if (!$this->settings->googleApiKey) { - return $element; - } - - if (!$element->latitude && !$element->longitude && $element->countryCode) { - $client = new Client(['base_uri' => 'https://maps.googleapis.com']); - $fields = [ - $element->addressLine1, - $element->addressLine2, - $element->addressLine3, - $element->postalCode, - $element->locality, - $element->countryCode - ]; - $fields = array_filter($fields); - $data = implode('+', $fields); - $request = $client->request('GET', - 'maps/api/geocode/json?address=' . urlencode($data) . '&key=' . Craft::parseEnv($this->settings->googleApiKey) . '', - ['allow_redirects' => false] - ); - $json = Json::decodeIfJson($request->getBody()->getContents()); - - if ($json['status'] !== 'OK' && $json['error_message']) { - if (Craft::$app->getConfig()->general->devMode) { - throw new InvalidConfigException('Google API error: ' . $json['error_message']); - } - Craft::error($json['error_message'], 'easy-address-field'); - } - - if ($json['status'] === 'OK') { - if ($json['results'][0]['geometry']['location']) { - return [ - 'latitude' => $json['results'][0]['geometry']['location']['lat'], - 'longitude' => $json['results'][0]['geometry']['location']['lng'], - ]; - } - } - } - - return []; - } - - private function geocodeModelGoogle(EasyAddressFieldModel $model) - { - if (!$this->settings->googleApiKey) { - return $model; - } - - if (!$model->latitude && !$model->longitude and strlen($model->toString()) >= 2) { - $client = new Client(['base_uri' => 'https://maps.googleapis.com']); - $request = $client->request('GET', - 'maps/api/geocode/json?address=' . urlencode($model->toString()) . '&key=' . Craft::parseEnv($this->settings->googleApiKey) . '', - ['allow_redirects' => false] - ); - $json = Json::decodeIfJson($request->getBody()->getContents()); - - if ($json['status'] !== 'OK' && $json['error_message']) { - if (Craft::$app->getConfig()->general->devMode) { - throw new InvalidConfigException('Google API error: ' . $json['error_message']); - } - Craft::error($json['error_message'], 'easy-address-field'); - } - - if ($json['status'] === 'OK') { - if ($json['results'][0]['geometry']['location']) { - $model->latitude = $json['results'][0]['geometry']['location']['lat']; - $model->longitude = $json['results'][0]['geometry']['location']['lng']; - } - } - } - - return $model; - } - - private function geocodeElementOSM(Address $address): array { // url encode the address diff --git a/src/services/geocoders/BaseGeoCoder.php b/src/services/geocoders/BaseGeoCoder.php new file mode 100644 index 0000000..e7646ce --- /dev/null +++ b/src/services/geocoders/BaseGeoCoder.php @@ -0,0 +1,36 @@ +settings = EasyAddressField::getInstance()->getSettings(); + parent::init(); + } + + /** + * This function is used to geocode the model from the EasyAddressField + * @param EasyAddressFieldModel $model + * @return mixed + */ + abstract public function geocodeModel(EasyAddressFieldModel $model): EasyAddressFieldModel; + + /** + * This function is used to geocode a Craft Address element + * @param Address $element + * @return mixed + */ + abstract public function geocodeElement(Address $element): Address; +} \ No newline at end of file diff --git a/src/services/geocoders/GoogleGeoCoder.php b/src/services/geocoders/GoogleGeoCoder.php new file mode 100644 index 0000000..61b60e9 --- /dev/null +++ b/src/services/geocoders/GoogleGeoCoder.php @@ -0,0 +1,104 @@ +settings->googleApiKey) { + return $model; + } + + if (!$model->latitude && !$model->longitude and strlen($model->toString()) >= 2) { + $result = $this->makeApiCall($model->toString()); + if ($result === false) { + return $model; + } + + if ($result) { + $model->latitude = $result['latitude']; + $model->longitude = $result['longitude']; + return $model; + } + } + + return $model; + } + + /** + * This function is used to geocode a Craft Address element + * @param Address $element + * @return mixed + */ + public function geocodeElement(Address $element): Address + { + $fields = [ + $element->addressLine1, + $element->addressLine2, + $element->addressLine3, + $element->postalCode, + $element->locality, + $element->countryCode + ]; + $fields = array_filter($fields); + $data = implode('+', $fields); + $result = $this->makeApiCall($data); + if ($result) { + $element->setAttributes([ + 'longitude' => $result['longitude'], + 'latitude' => $result['latitude'], + ]); + } + return $element; + } + + private function makeApiCall($data): array|false + { + $client = new Client(['base_uri' => 'https://maps.googleapis.com']); + $request = $client->request('GET', + 'maps/api/geocode/json?address=' . urlencode($data) . '&key=' . Craft::parseEnv($this->settings->googleApiKey) . '', + ['allow_redirects' => false] + ); + $result = Json::decodeIfJson($request->getBody()->getContents()); + + if ($result['status'] !== 'OK' && $result['error_message']) { + if (Craft::$app->getConfig()->general->devMode) { + throw new InvalidConfigException('Google API error: ' . $result['error_message']); + } + Craft::error($result['error_message'], 'easy-address-field'); + } + if ($result['status'] === 'OK') { + if ($result['status'] === 'OK') { + if ($result['results'][0]['geometry']['location']) { + $data = [ + 'latitude' => $result['results'][0]['geometry']['location']['lat'], + 'longitude' => $result['results'][0]['geometry']['location']['lng'], + ]; + return $data; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/src/services/geocoders/NomanatimGeoCoder.php b/src/services/geocoders/NomanatimGeoCoder.php new file mode 100644 index 0000000..5c5a473 --- /dev/null +++ b/src/services/geocoders/NomanatimGeoCoder.php @@ -0,0 +1,47 @@ + Date: Sun, 5 Jan 2025 19:19:33 +0100 Subject: [PATCH 4/7] Nomanatim geocoder Signed-off-by: Jan Henckens --- src/services/GeoLocationService.php | 112 ++----------------- src/services/geocoders/NomanatimGeoCoder.php | 87 +++++++++++++- 2 files changed, 92 insertions(+), 107 deletions(-) diff --git a/src/services/GeoLocationService.php b/src/services/GeoLocationService.php index d5d840b..b69886b 100644 --- a/src/services/GeoLocationService.php +++ b/src/services/GeoLocationService.php @@ -29,7 +29,7 @@ public function init(): void return \Craft::createObject($geoCoder); }); - parent::init(); // TODO: Change the autogenerated stub + parent::init(); } /** @@ -37,15 +37,11 @@ public function init(): void * * @return EasyAddressFieldModel */ - public function locate(EasyAddressFieldModel $model) + public function locate(EasyAddressFieldModel $model): EasyAddressFieldModel { try { if (!$model->latitude && !$model->longitude and strlen($model->toString()) >= 2) { - if ($this->settings->geoCodingService === 'google') { - $model = $this->geocodeModelGoogle($model); - } else { - $model = $this->geocodeModelOSM($model); - } + return $this->geoCoders[$this->settings->geoCodingService]->geocodeModel($model); } return $model; } catch (\Throwable $exception) { @@ -54,108 +50,20 @@ public function locate(EasyAddressFieldModel $model) } } - public function locateElement(Address $element) + /** + * @param Address $element + * @return Address + */ + public function locateElement(Address $element): Address { try { if (!$element->latitude && !$element->longitude && $element->countryCode) { - if ($this->settings->geoCodingService === 'google') { - $coordinates = $this->geocodeElementGoogle($element); - return $coordinates; - } else { - $coordinates = $this->geocodeElementOSM(); - } - return []; + return $this->geoCoders[$this->settings->geoCodingService]->geocodeElement($element); } + return $element; } catch (\Throwable $exception) { \Craft::error($exception->getMessage(), 'easy-address-field'); return $element; } } - - - private function geocodeElementOSM(Address $address): array - { - // url encode the address - $url = "http://nominatim.openstreetmap.org/"; - $nominatim = new Nominatim($url); - $search = $nominatim->newSearch() - ->countryCode($address->countryCode) - ->state($address->administrativeArea ?? '') - ->city($address->locality ?? '') - ->postalCode($address->postalCode ?? '') - ->street($address->addressLine1 . ' ' . $address->addressLine2 . ' ' . $address->addressLine3) - ->limit(1) - ->polygon('geojson') - ->addressDetails(); - - $result = $nominatim->find($search); - if (empty($result)) { - return []; - } - - if (isset($result[0]['lat']) && isset($result[0]['lon'])) { - return [ - 'latitude' => $result[0]['lat'], - 'longitude' => $result[0]['lon'], - ]; - - } - - if (is_array($result[0]['geojson']['coordinates'][0]) && is_array($result[0]['geojson']['coordinates'][0][0])) { - return [ - 'latitude' => $result[0]['geojson']['coordinates'][0][0][1], - 'longitude' => $result[0]['geojson']['coordinates'][0][0][0], - ]; - - } - - if (is_array($result[0]['geojson']['coordinates'][0])) { - return [ - 'latitude' => $result[0]['geojson']['coordinates'][0][1], - 'longitude' => $result[0]['geojson']['coordinates'][0][0], - ]; - } - - return [ - 'latitude' => $result[0]['geojson']['coordinates'][1], - 'longitude' => $result[0]['geojson']['coordinates'][0], - ]; - - } - - private function geocodeModelOSM(EasyAddressFieldModel $model) - { - // url encode the address - $url = "http://nominatim.openstreetmap.org/"; - $nominatim = new Nominatim($url); - $search = $nominatim->newSearch() - ->countryCode($model->country) - ->state($model->state ?? '') - ->city($model->city ?? '') - ->postalCode($model->postalCode ?? '') - ->street($model->street . ' ' . $model->street2) - ->limit(1) - ->polygon('geojson') - ->addressDetails(); - - $result = $nominatim->find($search); - if (empty($result)) { - return $model; - } - - if (isset($result[0]['lat']) && isset($result[0]['lon'])) { - $model->longitude = $result[0]['lon']; - $model->latitude = $result[0]['lat']; - } elseif (is_array($result[0]['geojson']['coordinates'][0]) && is_array($result[0]['geojson']['coordinates'][0][0])) { - $model->longitude = $result[0]['geojson']['coordinates'][0][0][0]; - $model->latitude = $result[0]['geojson']['coordinates'][0][0][1]; - } elseif (is_array($result[0]['geojson']['coordinates'][0])) { - $model->longitude = $result[0]['geojson']['coordinates'][0][0]; - $model->latitude = $result[0]['geojson']['coordinates'][0][1]; - } else { - $model->longitude = $result[0]['geojson']['coordinates'][0]; - $model->latitude = $result[0]['geojson']['coordinates'][1]; - } - return $model; - } } diff --git a/src/services/geocoders/NomanatimGeoCoder.php b/src/services/geocoders/NomanatimGeoCoder.php index 5c5a473..c411452 100644 --- a/src/services/geocoders/NomanatimGeoCoder.php +++ b/src/services/geocoders/NomanatimGeoCoder.php @@ -2,13 +2,9 @@ namespace studioespresso\easyaddressfield\services\geocoders; -use Craft; -use craft\base\Component; use craft\elements\Address; -use craft\helpers\Json; -use GuzzleHttp\Client; +use maxh\Nominatim\Nominatim; use studioespresso\easyaddressfield\models\EasyAddressFieldModel; -use yii\base\InvalidConfigException; class NomanatimGeoCoder extends BaseGeoCoder { @@ -26,6 +22,23 @@ class NomanatimGeoCoder extends BaseGeoCoder */ public function geocodeModel(EasyAddressFieldModel $model): EasyAddressFieldModel { + $data = [ + 'country' => $model->countryCode, + 'state' => $model->state, + 'city' => $model->city, + 'postalCode' => $model->postalCode, + 'street' => $model->street, + 'street2' => $model->street2, + ]; + + $result = $this->makeApiCall($data); + if (empty($result)) { + return $model; + } + + $model->latitude = $result['latitude']; + $model->longitude = $result['longitude']; + return $model; } @@ -36,12 +49,76 @@ public function geocodeModel(EasyAddressFieldModel $model): EasyAddressFieldMode */ public function geocodeElement(Address $element): Address { + $data = [ + 'country' => $element->countryCode, + 'state' => $element->state, + 'city' => '', + 'postalCode' => $element->postalCode, + 'street' => $element->addressLine1, + 'street2' => $element->addressLine2 . ' ' . $element->addressLine3, + ]; + + $result = $this->makeApiCall($data); + if (empty($result)) { + return $element; + } + + if ($result) { + $element->setAttributes([ + 'longitude' => $result['longitude'], + 'latitude' => $result['latitude'], + ]); + } return $element; } private function makeApiCall($data): array|false { + // url encode the address + $url = "http://nominatim.openstreetmap.org/"; + $nominatim = new Nominatim($url); + $search = $nominatim->newSearch() + ->countryCode($data['country']) + ->state($data['state'] ?? '') + ->city($data['city'] ?? '') + ->postalCode($data['postalCode'] ?? '') + ->street($data['street'] . ' ' . $data['street2']) + ->limit(1) + ->polygon('geojson') + ->addressDetails(); + + $result = $nominatim->find($search); + if (empty($result)) { + return []; + } + + if (isset($result[0]['lat']) && isset($result[0]['lon'])) { + return [ + 'latitude' => $result[0]['lat'], + 'longitude' => $result[0]['lon'], + ]; + + } + + if (is_array($result[0]['geojson']['coordinates'][0]) && is_array($result[0]['geojson']['coordinates'][0][0])) { + return [ + 'latitude' => $result[0]['geojson']['coordinates'][0][0][1], + 'longitude' => $result[0]['geojson']['coordinates'][0][0][0], + ]; + + } + + if (is_array($result[0]['geojson']['coordinates'][0])) { + return [ + 'latitude' => $result[0]['geojson']['coordinates'][0][1], + 'longitude' => $result[0]['geojson']['coordinates'][0][0], + ]; + } + return [ + 'latitude' => $result[0]['geojson']['coordinates'][1], + 'longitude' => $result[0]['geojson']['coordinates'][0], + ]; } } \ No newline at end of file From fbe365d221faf473430b93e6dbfee975b7794ca6 Mon Sep 17 00:00:00 2001 From: Jan Henckens Date: Sun, 5 Jan 2025 19:45:35 +0100 Subject: [PATCH 5/7] Setting field for enableGeoCodingForCraftElements Signed-off-by: Jan Henckens --- src/models/EasyAddressFieldSettingsModel.php | 2 +- src/templates/_settings.twig | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/models/EasyAddressFieldSettingsModel.php b/src/models/EasyAddressFieldSettingsModel.php index 605831f..dd9c841 100644 --- a/src/models/EasyAddressFieldSettingsModel.php +++ b/src/models/EasyAddressFieldSettingsModel.php @@ -15,5 +15,5 @@ class EasyAddressFieldSettingsModel extends Model public ?string $googleApiKey = null; - public bool $enableGeoCodingForCraftElements = true; + public bool $enableGeoCodingForCraftElements = false; } diff --git a/src/templates/_settings.twig b/src/templates/_settings.twig index 742f8de..2ea6903 100644 --- a/src/templates/_settings.twig +++ b/src/templates/_settings.twig @@ -1,6 +1,5 @@ {% import "_includes/forms" as forms %} -{% set services = [] %} {{ forms.selectField({ label: 'Geocoding service'|t, @@ -12,8 +11,6 @@ errors: settings.getErrors('geoCodingService'), }) }} - - {{ forms.autosuggestField({ label: 'Google API Key for geocoding requests'|t, instructions: 'Only needed if you are using the geocoding functions in your templates'|t, @@ -25,3 +22,11 @@ suggestAliases: true, }) }} +
+ +{{ forms.lightswitchField({ + label: 'Enable geocoding for Craft Address fields/elements'|t('easy-address-field'), + name: 'enableGeoCodingForCraftElements', + on: settings['enableGeoCodingForCraftElements'] +}) }} + From d71a11b682b698f8601ee48a6953192737f0606f Mon Sep 17 00:00:00 2001 From: Jan Henckens Date: Sun, 5 Jan 2025 19:49:19 +0100 Subject: [PATCH 6/7] Readme to register geocoders Signed-off-by: Jan Henckens --- README.MD | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.MD b/README.MD index ec2e066..af34490 100644 --- a/README.MD +++ b/README.MD @@ -43,6 +43,12 @@ Google's Geocoding API does not support HTTP referrer restrictions. Make sure th #### API Errors When `devMode` is enabled, any errors returned by Google's API will show an exception so you can clearly see what's going wrong. With `devMode` disabled, any errors will be logged to Craft's `web.log`. + +### GeoCoding for Craft Address elements +Since Craft 5, Craft has a built-in element for Addresses, but no way to get coordinates for addresses out of the box. +When you install Easy Address Field, you can enable GeoCoding for Craft Address elements. This will add a `latitude` and `longitude` values to each Address element. +You can enable this feature in the plugin settings. + ## Template variables ### Printing address values @@ -60,6 +66,25 @@ field.longitude field.getDirectionsUrl() // get a directions link to the given address ```` + +## Custom GeoCoding services +Out of the box, the plugin comes with support for geocoding with OpenStreetMap's Nominatim service and Google. +If you'd like to use a different service, you can create a custom service by creating a new class that implements the `studioespresso\easyaddressfield\services\geocoders\BaseGeoCoder` interface. + +Once you created your geocoder, register it with the following event: + +````php +use studioespresso\easyaddressfield\events\RegisterGeocoderEvent; +use studioespresso\easyaddressfield\services\GeoLocationService; + +Event::on( + GeoLocationService::class, + GeoLocationService::EVENT_REGISTER_GEOCODERS, + function (RegisterGeocoderEvent $event) { + $event->geoCoders['your-service'] = YourGeoCoder::class; +}); +```` + ## Upgrading from Craft 4 to Craft 5 ### getDirectionsUrl() If you're using the ``getDirectionsUrl()`` function on `craft.address`, you'll now need to call the function on the field itself instead of the on the plugin's Twig variable. From c461b0fbe86fe801c4cabe3d9a7323152e028ef7 Mon Sep 17 00:00:00 2001 From: Jan Henckens Date: Sun, 5 Jan 2025 19:59:50 +0100 Subject: [PATCH 7/7] ECS fixes Signed-off-by: Jan Henckens --- src/EasyAddressField.php | 14 ++++++-------- src/events/RegisterGeocoderEvent.php | 2 +- src/services/GeoLocationService.php | 4 +--- src/services/geocoders/BaseGeoCoder.php | 2 +- src/services/geocoders/GoogleGeoCoder.php | 6 ++---- src/services/geocoders/NomanatimGeoCoder.php | 5 +---- 6 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/EasyAddressField.php b/src/EasyAddressField.php index 1210a6c..8f13ccc 100644 --- a/src/EasyAddressField.php +++ b/src/EasyAddressField.php @@ -65,11 +65,11 @@ public function init() ]); // Register our fields - Event::on(Fields::className(), Fields::EVENT_REGISTER_FIELD_TYPES, function (RegisterComponentTypesEvent $event) { + Event::on(Fields::className(), Fields::EVENT_REGISTER_FIELD_TYPES, function(RegisterComponentTypesEvent $event) { $event->types[] = EasyAddressFieldField::class; }); - Event::on(Address::class, Element::EVENT_BEFORE_SAVE, function (ModelEvent $event) { + Event::on(Address::class, Element::EVENT_BEFORE_SAVE, function(ModelEvent $event) { /* @var Address $element */ $element = $event->sender; if (ElementHelper::isDraftOrRevision($element)) { @@ -83,13 +83,13 @@ public function init() // Register our twig functions - Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function (Event $event) { + Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function(Event $event) { $variable = $event->sender; $variable->set('address', AddressVariable::class); }); /** @phpstan-ignore-next-line */ - Event::on(EasyAddressFieldField::class, 'craftQlGetFieldSchema', function (GetFieldSchema $event) { + Event::on(EasyAddressFieldField::class, 'craftQlGetFieldSchema', function(GetFieldSchema $event) { $event->handled = true; $field = $event->sender; $object = $event->schema->createObjectType(ucfirst($field->handle) . 'EasyAddressField'); @@ -109,16 +109,15 @@ public function init() // If craftcms/feed-me is installed & activacted, hook here to register the field for import if (Craft::$app->getPlugins()->isPluginEnabled('feed-me')) { /** @phpstan-ignore-next-line */ - Event::on(\craft\feedme\services\Fields::class, \craft\feedme\services\Fields::EVENT_REGISTER_FEED_ME_FIELDS, function (RegisterFeedMeFieldsEvent $e) { + Event::on(\craft\feedme\services\Fields::class, \craft\feedme\services\Fields::EVENT_REGISTER_FEED_ME_FIELDS, function(RegisterFeedMeFieldsEvent $e) { $e->fields[] = EasyAddressFieldFeedMe::class; }); } - Event::on(GeoLocationService::class, GeoLocationService::EVENT_REGISTER_GEOCODERS, function (RegisterGeocoderEvent $event) { + Event::on(GeoLocationService::class, GeoLocationService::EVENT_REGISTER_GEOCODERS, function(RegisterGeocoderEvent $event) { $event->geoCoders['nomanatim'] = NomanatimGeoCoder::class; $event->geoCoders['google'] = GoogleGeoCoder::class; }); - } // Components @@ -176,5 +175,4 @@ public function geoLocation(): GeoLocationService { return $this->geoLocation; } - } diff --git a/src/events/RegisterGeocoderEvent.php b/src/events/RegisterGeocoderEvent.php index 4434bc9..7b5bd6e 100644 --- a/src/events/RegisterGeocoderEvent.php +++ b/src/events/RegisterGeocoderEvent.php @@ -7,4 +7,4 @@ class RegisterGeocoderEvent extends Event { public $geoCoders = []; -} \ No newline at end of file +} diff --git a/src/services/GeoLocationService.php b/src/services/GeoLocationService.php index b69886b..15582ab 100644 --- a/src/services/GeoLocationService.php +++ b/src/services/GeoLocationService.php @@ -5,11 +5,9 @@ use craft\base\Component; use craft\base\Event; use craft\elements\Address; -use maxh\Nominatim\Nominatim; use studioespresso\easyaddressfield\EasyAddressField; use studioespresso\easyaddressfield\events\RegisterGeocoderEvent; use studioespresso\easyaddressfield\models\EasyAddressFieldModel; -use studioespresso\easyaddressfield\services\geocoders\GoogleGeoCoder; class GeoLocationService extends Component { @@ -25,7 +23,7 @@ public function init(): void $event = new RegisterGeocoderEvent(); Event::trigger(self::class, self::EVENT_REGISTER_GEOCODERS, $event); - $this->geoCoders = collect(array_merge($this->geoCoders, $event->geoCoders))->map(function ($geoCoder) { + $this->geoCoders = collect(array_merge($this->geoCoders, $event->geoCoders))->map(function($geoCoder) { return \Craft::createObject($geoCoder); }); diff --git a/src/services/geocoders/BaseGeoCoder.php b/src/services/geocoders/BaseGeoCoder.php index e7646ce..a508c58 100644 --- a/src/services/geocoders/BaseGeoCoder.php +++ b/src/services/geocoders/BaseGeoCoder.php @@ -33,4 +33,4 @@ abstract public function geocodeModel(EasyAddressFieldModel $model): EasyAddress * @return mixed */ abstract public function geocodeElement(Address $element): Address; -} \ No newline at end of file +} diff --git a/src/services/geocoders/GoogleGeoCoder.php b/src/services/geocoders/GoogleGeoCoder.php index 61b60e9..2d53c40 100644 --- a/src/services/geocoders/GoogleGeoCoder.php +++ b/src/services/geocoders/GoogleGeoCoder.php @@ -3,7 +3,6 @@ namespace studioespresso\easyaddressfield\services\geocoders; use Craft; -use craft\base\Component; use craft\elements\Address; use craft\helpers\Json; use GuzzleHttp\Client; @@ -12,7 +11,6 @@ class GoogleGeoCoder extends BaseGeoCoder { - /** * Label for the geocoder, displayed in the plugin's settings * @var string|null @@ -59,7 +57,7 @@ public function geocodeElement(Address $element): Address $element->addressLine3, $element->postalCode, $element->locality, - $element->countryCode + $element->countryCode, ]; $fields = array_filter($fields); $data = implode('+', $fields); @@ -101,4 +99,4 @@ private function makeApiCall($data): array|false } return false; } -} \ No newline at end of file +} diff --git a/src/services/geocoders/NomanatimGeoCoder.php b/src/services/geocoders/NomanatimGeoCoder.php index c411452..ab37ac8 100644 --- a/src/services/geocoders/NomanatimGeoCoder.php +++ b/src/services/geocoders/NomanatimGeoCoder.php @@ -8,7 +8,6 @@ class NomanatimGeoCoder extends BaseGeoCoder { - /** * Label for the geocoder, displayed in the plugin's settings * @var string|null @@ -98,7 +97,6 @@ private function makeApiCall($data): array|false 'latitude' => $result[0]['lat'], 'longitude' => $result[0]['lon'], ]; - } if (is_array($result[0]['geojson']['coordinates'][0]) && is_array($result[0]['geojson']['coordinates'][0][0])) { @@ -106,7 +104,6 @@ private function makeApiCall($data): array|false 'latitude' => $result[0]['geojson']['coordinates'][0][0][1], 'longitude' => $result[0]['geojson']['coordinates'][0][0][0], ]; - } if (is_array($result[0]['geojson']['coordinates'][0])) { @@ -121,4 +118,4 @@ private function makeApiCall($data): array|false 'longitude' => $result[0]['geojson']['coordinates'][0], ]; } -} \ No newline at end of file +}