diff --git a/config/install/geolocation.settings.yml b/config/install/geolocation.settings.yml index 0c09497..24995cd 100644 --- a/config/install/geolocation.settings.yml +++ b/config/install/geolocation.settings.yml @@ -1 +1,10 @@ google_map_api_key: '' +geocode_options: + nominatim: + geocodingQueryParams: + email: '' + viewbox: '6.832,50.8464,6.937,50.8043' + bounded: 1 + city: 'Brühl' + mapzen: + apiKey: 'search-QdeL4tF' \ No newline at end of file diff --git a/css/geolocation-widget-leaflet.css b/css/geolocation-widget-leaflet.css new file mode 100644 index 0000000..e69de29 diff --git a/geolocation.libraries.yml b/geolocation.libraries.yml index 4ea97ab..df147b7 100644 --- a/geolocation.libraries.yml +++ b/geolocation.libraries.yml @@ -59,3 +59,36 @@ geolocation.commonmap: - core/drupal - core/jquery - geolocation/geolocation.core + +# Leaflet widget library. +geolocation.widgets.leaflet: + version: 1.x + css: + theme: + css/geolocation-widget-leaflet.css: {} + component: + //npmcdn.com/leaflet@1.0.0-rc.3/dist/leaflet.css: {} + /libraries/leaflet-control-geocoder/dist/Control.Geocoder.css : {} + //maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css: {} + //cdn.jsdelivr.net/leaflet.locatecontrol/0.52.0/L.Control.Locate.min.css : {} + js: + //npmcdn.com/leaflet@1.0.0-rc.3/dist/leaflet.js: {} + /libraries/leaflet-control-geocoder/dist/Control.Geocoder.js : {} + //cdn.jsdelivr.net/leaflet.locatecontrol/0.52.0/L.Control.Locate.min.js : {} + js/geolocation-widget-leaflet.js: { scope: footer } + + dependencies: + - geolocation/geolocation.core + + +# Leaflet maps display formatter. +geolocation.formatter.leaflet: + version: 8.x-1.x + js: + //npmcdn.com/leaflet@1.0.0-rc.3/dist/leaflet.js: {} + js/geolocation-formatter-leaflet.js: {} + css: + component: + //npmcdn.com/leaflet@1.0.0-rc.3/dist/leaflet.css: {} + dependencies: + - geolocation/geolocation.core diff --git a/js/geolocation-formatter-leaflet.js b/js/geolocation-formatter-leaflet.js new file mode 100644 index 0000000..344ae43 --- /dev/null +++ b/js/geolocation-formatter-leaflet.js @@ -0,0 +1,100 @@ +/** + * @file + * Javascript for the Google map formatter. + */ +(function ($, Drupal) { + + 'use strict'; + + /** + * @namespace + */ + Drupal.geolocation = Drupal.geolocation || {}; + + /** + * Attach google map formatter functionality. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches google map formatter functionality to relevant elements. + */ + Drupal.behaviors.geolocationLeaflet = { + attach: function (context, settings) { + // Ensure itterables. + settings.geolocation = settings.geolocation || {maps: []}; + + var mapIds = []; + $.each(settings.geolocation.maps, function (index, item) { + mapIds.push('#' + item.id); + }); + + if ($(mapIds.join(', '), context).length < 1) { + // None of the target IDs is present. Stop here. + return; + } + initialize(settings.geolocation.maps, context); + + } + }; + + /** + * Runs after Map Ids are available. + * + * @param {object} maps - The google map object. + * @param {object} context - The html context. + */ + function initialize(maps, context) { + // Loop though all objects and add maps to the page. + $.each(maps, function (delta, map) { + console.log(map.id, map.settings.leaflet_settings); + // Get the map container. + map.container = $('#' + map.id, context).first(); + console.log(map); + console.log(map.container); + if (map.container.length && !map.container.hasClass('geolocation-processed')) { + // Add the map by ID with settings. + var container = '#' + map.id; + $(container).addClass('geolocation-processed'); + + $(container).css({ + height: map.settings.leaflet_settings.height, + width: map.settings.leaflet_settings.width + }); + // Apply the myCustomBehaviour effect to the elements only once. + var mapProcessed = new L.map(map.id, { + fullscreenControl: true, + scrollWheelZoom: false, + maxZoom: 18, + }); + var latLng = [map.lat, map.lng]; + mapProcessed.setView(latLng, 12); + + L.marker(latLng, {title:map.settings.title}).addTo(mapProcessed) + .bindPopup(map.settings.info_text) + .openPopup(); + + // any behavior is now applied once + var tileLayer = L.tileLayer(map.settings.leaflet_settings.tileserver, { + attribution: '© OpenStreetMap contributors, © CartoDB' + }); + mapProcessed.addLayer(tileLayer); + + + // We can not use addMap from geolocation.js as google wont be defined. + // Instead use settings here + // + // Drupal.geolocation.addMap(map); + // Set the already processed flag. + + // Set the container size. + + + // Get the center point. + // var center = new L.LatLng(map.lat, map.lng); + + + } + }); + } +})(jQuery, Drupal); diff --git a/js/geolocation-widget-leaflet.js b/js/geolocation-widget-leaflet.js new file mode 100644 index 0000000..853fd19 --- /dev/null +++ b/js/geolocation-widget-leaflet.js @@ -0,0 +1,164 @@ +/** + * @file + * Javascript for the Google map formatter. + */ +(function ($, Drupal) { + + 'use strict'; + + /** + * @namespace + */ + Drupal.geolocation = Drupal.geolocation || {}; + + /** + * Attach google map formatter functionality. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches google map formatter functionality to relevant elements. + */ + Drupal.behaviors.geolocationLeaflet = { + attach: function (context, settings) { + // Ensure itterables. + settings.geolocation = settings.geolocation || {maps: []}; + + var mapIds = []; + $.each(settings.geolocation.widgetMaps, function (index, item) { + mapIds.push('#' + item.id); + }); + + if ($(mapIds.join(', '), context).length < 1) { + // None of the target IDs is present. Stop here. + // return; + } + initialize(settings.geolocation, context); + + } + }; + + /** + * Runs after settings are available + * + * @param {object} maps - The leaflet map object. + * @param {object} context - The html context. + */ + function initialize(maps, context) { + var allMaps = maps.widgetMaps; + // Loop though all objects and add maps to the page. + $.each(allMaps, function (delta, map) { + + // Get the map container. + map.container = $('#' + map.id, context).first(); + + if (map.container.length && !map.container.hasClass('geolocation-processed')) { + // Add the map by ID with settings. + var $container = '#' + map.id; + $($container).addClass('geolocation-processed'); + + $($container).css({ + height: map.settings.leaflet_settings.height, + width: map.settings.leaflet_settings.width + }); + + var mapProcessed = new L.map(map.id, { + fullscreenControl: true, + scrollWheelZoom: false, + maxZoom: 18, + }); + + var latLng = [map.lat, map.lng]; + mapProcessed.setView(latLng, 12); + + L.marker(latLng, { + }).addTo(mapProcessed); + + var tileLayer = L.tileLayer(map.settings.leaflet_settings.tileserver, { + attribution: '© OpenStreetMap contributors, © CartoDB' + }); + mapProcessed.addLayer(tileLayer); + + + // Set the values of hidden form inputs. + var latDefault = $('.geolocation-hidden-lat').val(); + var lngDefault = $('.geolocation-hidden-lng').val(); + if ( latDefault == '' && lngDefault == '' ) { + latDefault = 0; + lngDefault = 0; + } + mapProcessed.setView(new L.latLng(latDefault, lngDefault),18); + + var options = maps.geocodeOptions; + var geocoderSetting = map.settings.leaflet_settings.geocoder; + console.log(geocoderSetting); + switch (geocoderSetting) { + case 'Mapzen': + var geocoder = L.Control.Geocoder.mapzen(options.mapzen); + break; + case 'Google': + geocoder = L.Control.Geocoder.google(options.google); + break; + default: + geocoder = L.Control.Geocoder.nominatim(options.nominatim); + } + + // Add the leaflet geocoder control to map and wait for input. + var control = L.Control.geocoder({ geocoder, + defaultMarkGeocode: false + }).on('markgeocode', function(e) { + var bbox = e.geocode.bbox; + var poly = L.polygon([ + bbox.getSouthEast(), + bbox.getNorthEast(), + bbox.getNorthWest(), + bbox.getSouthWest() + ]); + mapProcessed.fitBounds(poly.getBounds()); + L.circle(e.geocode.center, 5).addTo(mapProcessed); + + marker = L.marker(e.geocode.center).bindPopup(e.geocode.html).addTo(mapProcessed).openPopup(); + + var lat = e.geocode.center.lat; + var lng = e.geocode.center.lng; + + // Set the values of hidden form inputs. + $('.geolocation-hidden-lat').val(lat); + $('.geolocation-hidden-lng').val(lng); + }) + .addTo(mapProcessed); + // Add Location control to map widget. + L.control.locate().addTo(mapProcessed); + mapProcessed.on('locationfound', onLocationFound); + + // Reverse geocoding on map click. + var marker; + mapProcessed.on('click', function(e) { + geocoder.reverse(e.latlng, mapProcessed.options.crs.scale(mapProcessed.getZoom()), function (results) { + var r = results[0]; + if (r) { + if (marker) { + marker.setLatLng(r.center).setPopupContent(r.html || r.html).openPopup(); + } else { + marker = L.marker(r.center, {draggable: true}).bindPopup(r.html).addTo(mapProcessed).openPopup(); + } + } + }) + }); + } + }); + } + + + function onLocationFound(e) { + var radius = e.accuracy / 2; + + L.marker(e.latlng).addTo(mapProcessed) + .bindPopup("You are within " + radius + " meters from this point").openPopup(); + + L.circle(e.latlng, radius).addTo(mapProcessed); + } +})(jQuery, Drupal); + + + diff --git a/src/LeafletDisplayTrait.php b/src/LeafletDisplayTrait.php new file mode 100644 index 0000000..32ce214 --- /dev/null +++ b/src/LeafletDisplayTrait.php @@ -0,0 +1,233 @@ + 'Nominatim OSM Geocoding', + static::$MAPZEN => "Mapzen Search", + static::$GOOGLE => 'Google Maps Geocoding API', + ]; + + return array_map([$this, 't'], $geocodingApi); + } + + /** + * Provide a populated settings array. + * + * @return array + * The settings array with the default map settings. + */ + public static function getLeafletDefaultSettings() { + return [ + 'leaflet_settings' => [ + 'zoom' => 10, + 'mapTypeControl' => TRUE, + 'zoomControl' => TRUE, + 'scrollwheel' => TRUE, + 'disableDoubleClickZoom' => FALSE, + 'draggable' => TRUE, + 'height' => '400px', + 'width' => '100%', + 'info_auto_display' => TRUE, + 'tileserver' => 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png', + 'geocoder' => static::$NOMINATIM, + ], + ]; + } + + /** + * Provide settings ready to handover to JS to feed to Google Maps. + * + * @param array $settings + * Current settings. Might contain unrelated settings as well. + * + * @return array + * An array only containing keys defined in this trait. + */ + public function getLeafletSettings($settings = []) { + $default_settings = self::getLeafletDefaultSettings(); + $settings = array_merge($default_settings, $settings); + + foreach ($settings['leaflet_settings'] as $key => $setting) { + if (!isset($default_settings['leaflet_settings'][$key])) { + unset($settings['leaflet_settings'][$key]); + } + } + + // Convert JSON string to actual array before handing to Renderer. + if (!empty($settings['leaflet_settings']['style'])) { + $json = json_decode($settings['leaflet_settings']['style']); + if (is_array($json)) { + $settings['leaflet_settings']['style'] = $json; + } + } + + return $settings; + } + + /** + * Provide a summary array to use in field formatters. + * + * @param array $settings + * The current map settings. + * + * @return array + * An array to use as field formatter summary. + */ + public function getLeafletSettingsSummary($settings) { + $summary = []; + $summary[] = $this->t('Map Tileserver: @tileserver', ['@tileserver' => $settings['leaflet_settings']['tileserver']]); + $summary[] = $this->t('Geocoding service: @geocoder', ['@geocoder' => $settings['leaflet_settings']['geocoder']]); + $summary[] = $this->t('Zoom level: @zoom', ['@zoom' => $settings['leaflet_settings']['zoom']]); + $summary[] = $this->t('Height: @height', ['@height' => $settings['leaflet_settings']['height']]); + $summary[] = $this->t('Width: @width', ['@width' => $settings['leaflet_settings']['width']]); + return $summary; + } + + /** + * Provide a generic map settings form array. + * + * @param array $settings + * The current map settings. + * + * @return array + * A form array to be integrated in whatever. + */ + public function getLeafletSettingsForm($settings = []) { + $form = [ + 'leaflet_settings' => [ + '#type' => 'details', + '#title' => t('Leaflet settings'), + '#description' => t('Addtional map settings provided by Leaflet'), + ], + ]; + + $form['leaflet_settings']['tileserver'] = [ + '#type' => 'textfield', + '#title' => $this->t('Default map tile server url'), + '#default_value' => $settings['leaflet_settings']['tileserver'], + ]; + + $form['leaflet_settings']['geocoder'] = [ + '#type' => 'select', + '#title' => $this->t('Geocoder API'), + '#options' => $this->getApi(), + '#default_value' => $settings['leaflet_settings']['geocoder'], + ]; + + $form['leaflet_settings']['zoom'] = [ + '#type' => 'select', + '#title' => $this->t('Zoom level'), + '#options' => range(0, 18), + '#description' => $this->t('The initial resolution at which to display the map, where zoom 0 corresponds to a map of the Earth fully zoomed out, and higher zoom levels zoom in at a higher resolution.'), + '#default_value' => $settings['leaflet_settings']['zoom'], + ]; + $form['leaflet_settings']['zoomControl'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Zoom control'), + '#description' => $this->t('Show zoom controls.'), + '#default_value' => $settings['leaflet_settings']['zoomControl'], + ]; + $form['leaflet_settings']['scrollwheel'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Scrollwheel'), + '#description' => $this->t('Allow the user to zoom the map using the scrollwheel.'), + '#default_value' => $settings['leaflet_settings']['scrollwheel'], + ]; + $form['leaflet_settings']['disableDoubleClickZoom'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Disable double click zoom'), + '#description' => $this->t('Disables the double click zoom functionality.'), + '#default_value' => $settings['leaflet_settings']['disableDoubleClickZoom'], + ]; + $form['leaflet_settings']['draggable'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Draggable'), + '#description' => $this->t('Allow the user to change the field of view.'), + '#default_value' => $settings['leaflet_settings']['draggable'], + ]; + $form['leaflet_settings']['height'] = [ + '#type' => 'textfield', + '#title' => $this->t('Height'), + '#description' => $this->t('Enter the dimensions and the measurement units. E.g. 200px or 100%.'), + '#size' => 4, + '#default_value' => $settings['leaflet_settings']['height'], + ]; + $form['leaflet_settings']['width'] = [ + '#type' => 'textfield', + '#title' => $this->t('Width'), + '#description' => $this->t('Enter the dimensions and the measurement units. E.g. 200px or 100%.'), + '#size' => 4, + '#default_value' => $settings['leaflet_settings']['width'], + ]; + $form['leaflet_settings']['info_auto_display'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Automatically show info text'), + '#default_value' => $settings['leaflet_settings']['info_auto_display'], + ]; + + return $form; + } + + /** + * Validate the form elements defined above. + * + * @param array $form + * Values to validate. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current Formstate. + * @param string|null $prefix + * Form state prefix if needed. + */ + public function validateLeafletSettingsForm($form, FormStateInterface $form_state, $prefix = NULL) { + if ($prefix) { + $values = $form_state->getValues(); + if (!empty($values[$prefix])) { + $values = $values[$prefix]; + $prefix = $prefix . ']['; + } + else { + return; + } + } + else { + $values = $form_state->getValues(); + } + + $json_style = $values['leaflet_settings']['style']; + if (!empty($json_style)) { + if (!is_string($json_style)) { + $form_state->setErrorByName($prefix . 'leaflet_settings][style', $this->t('Please enter a JSON string as style.')); + } + $json_result = json_decode($json_style); + if ($json_result === NULL) { + $form_state->setErrorByName($prefix . 'leaflet_settings][style', $this->t('Decoding style JSON failed. Error: %error.', ['%error' => json_last_error()])); + } + elseif (!is_array($json_result)) { + $form_state->setErrorByName($prefix . 'leaflet_settings][style', $this->t('Decoded style JSON is not an array.')); + } + } + } + +} diff --git a/src/Plugin/Field/FieldFormatter/GeolocationLeafletFormatter.php b/src/Plugin/Field/FieldFormatter/GeolocationLeafletFormatter.php new file mode 100644 index 0000000..30a56c4 --- /dev/null +++ b/src/Plugin/Field/FieldFormatter/GeolocationLeafletFormatter.php @@ -0,0 +1,169 @@ +getSettings(); + + $element['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Hover title'), + '#description' => $this->t('The hover title is a tool tip that will be displayed when the mouse is paused over the map marker.'), + '#default_value' => $settings['title'], + ]; + + $element += $this->getLeafletSettingsForm($settings); + + $element['info_text'] = [ + '#type' => 'textarea', + '#title' => $this->t('Info text'), + '#description' => $this->t('This text will be displayed in an "Info' + . ' window" above the map marker. The "Info window" will be displayed by' + . ' default unless the "Automatically show info text" format setting' + . ' is unchecked. Leave blank if you do not wish to display an "Info' + . ' window". See "REPLACEMENT PATTERNS" below for available replacements.'), + '#default_value' => $settings['info_text'], + ]; + + $element['replacement_patterns'] = [ + '#type' => 'details', + '#title' => 'Replacement patterns', + '#description' => $this->t('The following replacement patterns are available for the "Info text" and the "Hover title" settings.'), + ]; + $element['replacement_patterns']['native'] = [ + '#markup' => $this->t('

Geolocation field data:

'), + ]; + // Add the token UI from the token module if present. + $element['replacement_patterns']['token_help'] = [ + '#theme' => 'token_tree_link', + '#prefix' => $this->t('

Tokens:

'), + '#token_types' => [$this->fieldDefinition->getTargetEntityTypeId()], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $settings = $this->getSettings(); + + $summary = []; + $summary[] = $this->t('Hover Title: @type', ['@type' => $settings['title']]); + $summary = array_merge($summary, $this->getLeafletSettingsSummary($settings)); + $summary[] = $this->t('Info Text: @type', [ + '@type' => current(explode(chr(10), wordwrap($settings['info_text'], 30))), + ]); + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + // Add formatter settings to the drupalSettings array. + $field_settings = $this->getLeafletSettings($this->getSettings()) + $this->getSettings(); + $elements = []; + // This is a list of tokenized settings that should have placeholders + // replaced with contextual values. + $tokenized_settings = [ + 'info_text', + 'title', + ]; + + foreach ($items as $delta => $item) { + // @todo: Add token support to the geolocaiton field exposing sub-fields. + // Get token context. + $token_context = [ + 'field' => $items, + $this->fieldDefinition->getTargetEntityTypeId() => $items->getEntity(), + ]; + + $uniqueue_id = uniqid("map-canvas-"); + + $elements[$delta] = [ + '#type' => 'markup', + '#markup' => '
', + '#attached' => [ + 'library' => ['geolocation/geolocation.formatter.leaflet'], + 'drupalSettings' => [ + 'geolocation' => [ + 'maps' => [ + $uniqueue_id => [ + 'id' => "{$uniqueue_id}", + 'lat' => (float) $item->lat, + 'lng' => (float) $item->lng, + 'settings' => $field_settings, + ], + ], + 'google_map_api_key' => \Drupal::config('geolocation.settings')->get('google_map_api_key'), + ], + ], + ], + ]; + + // Replace placeholders with token values. + $item_settings = &$elements[$delta]['#attached']['drupalSettings']['geolocation']['maps'][$uniqueue_id]['settings']; + array_walk($tokenized_settings, function ($v) use (&$item_settings, $token_context, $item) { + $item_settings[$v] = \Drupal::token()->replace($item_settings[$v], $token_context); + // TODO: Drupal does not like variables handed to t(). + $item_settings[$v] = $this->t($item_settings[$v], [ + ':lat' => (float) $item->lat, + '%lat' => (float) $item->lat, + ':lng' => (float) $item->lng, + '%lng' => (float) $item->lng, + ]); + }); + + } + return $elements; + } + +} diff --git a/src/Plugin/Field/FieldWidget/GeolocationLeafletWidget.php b/src/Plugin/Field/FieldWidget/GeolocationLeafletWidget.php new file mode 100644 index 0000000..23eb9c3 --- /dev/null +++ b/src/Plugin/Field/FieldWidget/GeolocationLeafletWidget.php @@ -0,0 +1,225 @@ + $violation) { + if ($violation->getMessageTemplate() == 'This value should not be null.') { + $form_state->setErrorByName($items->getName(), t('No location has been selected yet for required field %field.', ['%field' => $items->getFieldDefinition()->getLabel()])); + } + } + parent::flagErrors($items, $violations, $form, $form_state); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + $settings = [ + 'populate_address_field' => NULL, + 'target_address_field' => NULL, + ]; + $settings += parent::defaultSettings(); + $settings += self::getLeafletDefaultSettings(); + + return $settings; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $settings = $this->getSettings(); + $element = []; + + $element += $this->getLeafletSettingsForm($settings); + + /** @var \Drupal\Core\Entity\EntityFieldManager $field_manager */ + $field_manager = \Drupal::service('entity_field.manager'); + + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */ + $field_definitions = $field_manager->getFieldDefinitions($this->fieldDefinition->getTargetEntityTypeId(), $this->fieldDefinition->getTargetBundle()); + + $address_fields = []; + foreach ($field_definitions as $field_definition) { + if ($field_definition->getType() == 'address' && $field_definition->getFieldStorageDefinition()->getCardinality() == 1) { + $address_fields[$field_definition->getName()] = $field_definition->getLabel(); + } + } + + if (empty($address_fields)) { + return $element; + } + + $element['populate_address_field'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Store retrieved address data in address field?'), + '#default_value' => $settings['populate_address_field'], + ]; + + $element['target_address_field'] = [ + '#type' => 'select', + '#title' => $this->t('Select target field to append address data.'), + '#description' => $this->t('Only fields of type "address" with a cardinality of 1 are available.'), + '#options' => $address_fields, + '#default_value' => $settings['target_address_field'], + '#states' => [ + // Only show this field when the 'toggle_me' checkbox is enabled. + 'visible' => [ + ':input[name="fields[' . $this->fieldDefinition->getName() . '][settings_edit_form][settings][populate_address_field]"]' => ['checked' => TRUE], + ], + ], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + $settings = $this->getSettings(); + + $summary = array_merge($summary, $this->getLeafletSettingsSummary($settings)); + + if (!empty($settings['populate_address_field'])) { + $summary[] = t('Geocoded address will be stored in @field', array('@field' => $settings['target_address_field'])); + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $settings = $this->getLeafletSettings($this->getSettings()) + $this->getSettings(); + + // Get this field name and parent. + $field_name = $this->fieldDefinition->getName(); + $parents = $form['#parents']; + // Get the field state. + $field_state = static::getWidgetState($parents, $field_name, $form_state); + + // Create a unique canvas id for each map of each geolocation field + // instance. + $field_id = preg_replace('/[^a-zA-Z0-9\-]/', '-', $this->fieldDefinition->getName()); + $canvas_id = !empty($field_state['canvas_ids'][$delta]) + ? $field_state['canvas_ids'][$delta] + : uniqid("map-canvas-{$field_id}-"); + + // Add the canvas id for this field. + $field_state['canvas_ids'] = isset($field_state['canvas_ids']) + ? $field_state['canvas_ids'] + [$delta => $canvas_id] + : [$delta => $canvas_id]; + + // Save the field state for this field. + static::setWidgetState($parents, $field_name, $form_state, $field_state); + + // Get the geolocation value for this element. + $lat = $items[$delta]->lat; + $lng = $items[$delta]->lng; + + // Get the default values for existing field. + $lat_default_value = isset($lat) ? $lat : NULL; + $lng_default_value = isset($lng) ? $lng : NULL; + + // Hidden lat,lng input fields. + $element['lat'] = [ + '#type' => 'hidden', + '#default_value' => $lat_default_value, + '#attributes' => ['class' => ['geolocation-hidden-lat']], + ]; + $element['lng'] = [ + '#type' => 'hidden', + '#default_value' => $lng_default_value, + '#attributes' => ['class' => ['geolocation-hidden-lng']], + ]; + + // Add Google API key to js. + $config = \Drupal::config('geolocation.settings'); + + // Add the map container. + $element['map_canvas'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'id' => $canvas_id, + 'class' => ['geolocation-map-canvas'], + ], + '#attached' => [ + 'library' => ['geolocation/geolocation.widgets.leaflet'], + 'drupalSettings' => [ + 'geolocation' => [ + 'widgetSettings' => [], + 'widgetMaps' => [ + $canvas_id => [ + 'id' => $canvas_id, + 'lat' => (float) $lat_default_value, + 'lng' => (float) $lng_default_value, + 'settings' => $settings, + ], + ], + 'geocodeOptions' => $config->get('geocode_options'), + ], + ], + ], + ]; + if ($settings['populate_address_field']) { + $element['map_canvas']['#attached']['drupalSettings']['geolocation']['widgetSettings']['addressFieldTarget'] = $settings['target_address_field']; + + foreach ([ + 'country_code', + 'administrative_area', + 'locality', + 'dependent_locality', + 'postal_code', + 'address_line1', + ] as $component) { + $element[$component] = [ + '#type' => 'hidden', + '#attributes' => [ + 'class' => ['geolocation-hidden-' . $component], + ], + ]; + } + } + + // Wrap the whole form in a container. + $element += [ + '#type' => 'fieldset', + '#title' => $element['#title'], + '#attributes' => [ + 'class' => ['canvas-' . $canvas_id], + ], + ]; + + return $element; + } + +}