Skip to content

Commit

Permalink
Merge branch 'develop-v5' of github.com:studioespresso/craft-easyaddr…
Browse files Browse the repository at this point in the history
…essfield into develop-v5
  • Loading branch information
janhenckens committed Jan 5, 2025
2 parents 5c82eb7 + 9947f32 commit 58759f8
Show file tree
Hide file tree
Showing 9 changed files with 367 additions and 82 deletions.
25 changes: 25 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
35 changes: 31 additions & 4 deletions src/EasyAddressField.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,26 @@
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;
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;
Expand Down Expand Up @@ -62,6 +69,19 @@ public function init()
$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) {
$event->sender = $this->geoLocation()->locateElement($element);
}
});



// Register our twig functions
Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function(Event $event) {
$variable = $event->sender;
Expand Down Expand Up @@ -93,6 +113,11 @@ 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
Expand All @@ -116,13 +141,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(),
]
);
Expand Down
10 changes: 10 additions & 0 deletions src/events/RegisterGeocoderEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace studioespresso\easyaddressfield\events;

use craft\base\Event;

class RegisterGeocoderEvent extends Event
{
public $geoCoders = [];
}
3 changes: 3 additions & 0 deletions src/models/EasyAddressFieldSettingsModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@
class EasyAddressFieldSettingsModel extends Model
{
public string $geoCodingService = 'nomanatim';

public ?string $googleApiKey = null;

public bool $enableGeoCodingForCraftElements = false;
}
104 changes: 29 additions & 75 deletions src/services/GeoLocationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,44 @@

namespace studioespresso\easyaddressfield\services;

use Craft;
use craft\base\Component;
use craft\helpers\Json;
use GuzzleHttp\Client;
use maxh\Nominatim\Nominatim;
use craft\base\Event;
use craft\elements\Address;
use studioespresso\easyaddressfield\EasyAddressField;
use studioespresso\easyaddressfield\events\RegisterGeocoderEvent;
use studioespresso\easyaddressfield\models\EasyAddressFieldModel;
use yii\base\InvalidConfigException;

class GeoLocationService extends Component
{
public $settings;

public const EVENT_REGISTER_GEOCODERS = 'registerGeoCodersEvent';
public $geoCoders = [];

public function init(): void
{
$this->settings = EasyAddressField::getInstance()->getSettings();
parent::init(); // TODO: Change the autogenerated stub

$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();
}

/**
* @param EasyAddressFieldModel $model
*
* @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->geocodeGoogle($model);
} else {
$model = $this->geocodeOSM($model);
}
return $this->geoCoders[$this->settings->geoCodingService]->geocodeModel($model);
}
return $model;
} catch (\Throwable $exception) {
Expand All @@ -43,71 +48,20 @@ public function locate(EasyAddressFieldModel $model)
}
}

private function geocodeGoogle(EasyAddressFieldModel $model)
/**
* @param Address $element
* @return Address
*/
public function locateElement(Address $element): Address
{
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'];
}
try {
if (!$element->latitude && !$element->longitude && $element->countryCode) {
return $this->geoCoders[$this->settings->geoCodingService]->geocodeElement($element);
}
return $element;
} catch (\Throwable $exception) {
\Craft::error($exception->getMessage(), 'easy-address-field');
return $element;
}

return $model;
}

private function geocodeOSM(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;
}
}
36 changes: 36 additions & 0 deletions src/services/geocoders/BaseGeoCoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace studioespresso\easyaddressfield\services\geocoders;

use craft\base\Component;
use craft\elements\Address;
use studioespresso\easyaddressfield\EasyAddressField;
use studioespresso\easyaddressfield\models\EasyAddressFieldModel;
use studioespresso\easyaddressfield\models\EasyAddressFieldSettingsModel;

abstract class BaseGeoCoder extends Component
{
public EasyAddressFieldSettingsModel $settings;

public ?string $name = null;

public function init(): void
{
$this->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;
}
Loading

0 comments on commit 58759f8

Please sign in to comment.