You are here

IpGeoLocPluginStyleLeaflet.php in IP Geolocation Views & Maps 8

File

src/Plugin/views/style/IpGeoLocPluginStyleLeaflet.php
View source
<?php

namespace Drupal\ip_geoloc\Plugin\views\style;

use Drupal\core\form\FormStateInterface;
use Drupal\views\Plugin\views\style\StylePluginBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\ip_geoloc\Services\IpGeoLocAPI;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\ip_geoloc\Services\IpGeoLocGlobal;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\ip_geoloc\Services\IpGeoLocSession;
use Drupal\Component\Utility\Xss;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\Unicode;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\ip_geoloc\Services\IpGeoLocViewsPluginStyle;
use Drupal\Core\Config\ConfigFactory;

/*require_once 'ip_geoloc_plugin_style.inc';*/
define('LEAFLET_MARKERCLUSTER_EXCLUDE_FROM_CLUSTER', 1);
define('LEAFLET_SYNC_CONTENT_TO_MARKER', 1 << 1);
define('LEAFLET_SYNC_MARKER_TO_CONTENT', 1 << 2);
define('LEAFLET_SYNC_MARKER_TO_CONTENT_WITH_POPUP', 1 << 3);
define('LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT', 1 << 4);

/**
 * Views Style plugin extension for Leaflet (if enabled).
 *
 * @ingroup views_style_plugins
 *
 * @ViewsStyle(
 *   id = "ip_geoloc_plugin_style_leaflet",
 *   title = @Translation("Map (Leaflet API, via IPGV&M) - requires Leaflet"),
 *   help = @Translation("Views Style plugin extension for Leaflet (if enabled)."),
 *   theme = "views_view_ip_geoloc_leaflet",
 *   display_types = { "normal" }
 * )
 */
class IpGeoLocPluginStyleLeaflet extends StylePluginBase implements ContainerFactoryPluginInterface {

  /**
   * {@inheritdoc}
   */
  protected $usesGrouping = TRUE;

  /**
   * {@inheritdoc}
   */
  protected $usesFields = TRUE;
  public $moduleHandler;
  public $ipGeolocGlobal;
  public $api;
  public $stringTranslation;
  public $ipGeolocSession;
  public $viewPluginStyle;
  public $config;

  /**
   * Plugin base constructor for dependency injection.
   *
   * @param array $configuration
   *   Plugin configuration.
   * @param string $plugin_id
   *   Plugin identifier.
   * @param string $plugin_definition
   *   Plugin definition.
   * @param \Drupal\ip_geoloc\Services\IpGeoLocAPI $api
   *   API helper class.
   * @param \Drupal\Core\Extension\ModuleHandler $moduleHandler
   *   Drupal Module Handler.
   * @param \Drupal\ip_geoloc\Services\IpGeoLocGlobal $ipGeolocGlobal
   *   Global servicio helper.
   * @param \Drupal\Core\StringTranslation\TranslationInterface $stringTranslation
   *   Translation class.
   * @param \Drupal\ip_geoloc\Services\IpGeoLocSession $ipGeolocSession
   *   Session helper class.
   * @param \Drupal\ip_geoloc\Services\IpGeoLocViewsPluginStyle $viewPluginStyle
   *   Global form parts definition class.
   * @param \Drupal\Core\Config\ConfigFactory $config_factory
   *   Drupal configuration factory .
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, IpGeoLocAPI $api, ModuleHandler $moduleHandler, IpGeoLocGlobal $ipGeolocGlobal, TranslationInterface $stringTranslation, IpGeoLocSession $ipGeolocSession, IpGeoLocViewsPluginStyle $viewPluginStyle, ConfigFactory $config_factory) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->api = $api;
    $this->moduleHandler = $moduleHandler;
    $this->ipGeolocGlobal = $ipGeolocGlobal;
    $this->stringTranslation = $stringTranslation;
    $this->session = $ipGeolocSession;
    $this->viewPluginStyle = $viewPluginStyle;
    $this->config = $config_factory
      ->get('ip_geoloc.settings');
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static($configuration, $plugin_id, $plugin_definition, $container
      ->get('ip_geoloc.api'), $container
      ->get('module_handler'), $container
      ->get('ip_geoloc.global'), $container
      ->get('string_translation'), $container
      ->get('ip_geoloc.session'), $container
      ->get('ip_geoloc.views_plugin_style'), $container
      ->get('config.factory'));
  }

  /**
   * Set default Leaflet options.
   */
  protected function defineOptions() {
    $options = parent::defineOptions();

    // The leaflet.module default.
    $options['map'] = [
      'default' => 'OSM Mapnik',
    ];
    $options['map_height'] = [
      'default' => 300,
    ];
    $latitude = $this->moduleHandler
      ->moduleExists('location') ? 'location_latitude' : 'ip_geoloc_latitude';
    $longitude = $this->moduleHandler
      ->moduleExists('location') ? 'location_longitude' : ($latitude == 'ip_geoloc_latitude' ? 'ip_geoloc_longitude' : $latitude);
    $options['ip_geoloc_views_plugin_latitude'] = [
      'default' => $latitude,
    ];
    $options['ip_geoloc_views_plugin_longitude'] = [
      'default' => $longitude,
    ];
    $options['default_marker'] = [
      'contains' => [
        'default_marker_color' => [
          'default' => '',
        ],
        'default_marker_special_char' => [
          'default' => '',
        ],
        'default_marker_special_char_class' => [
          'default' => '',
        ],
      ],
    ];
    $options['visitor_marker_leaflet'] = [
      'contains' => [
        'visitor_marker_color' => [
          'default' => '',
        ],
        'visitor_marker_special_char' => [
          'default' => '',
        ],
        'visitor_marker_special_char_class' => [
          'default' => '',
        ],
        'visitor_marker_accuracy_circle' => [
          'default' => FALSE,
        ],
      ],
    ];
    $options['differentiator'] = [
      'contains' => [
        'differentiator_field' => [
          'default' => '',
        ],
      ],
    ];
    $options['center_option'] = [
      'default' => 0,
    ];
    $options['tags'] = [
      'contains' => [
        'marker_tag' => [
          'default' => '',
        ],
        'tag_css_class' => [
          'default' => 'tag-inside-marker',
        ],
      ],
    ];
    $options['tooltips'] = [
      'contains' => [
        'marker_tooltip' => [
          'default' => '',
        ],
      ],
    ];
    $options['sync'] = [
      'contains' => [
        LEAFLET_SYNC_CONTENT_TO_MARKER => [
          'default' => FALSE,
        ],
        LEAFLET_SYNC_MARKER_TO_CONTENT => [
          'default' => FALSE,
        ],
        LEAFLET_SYNC_MARKER_TO_CONTENT_WITH_POPUP => [
          'default' => TRUE,
        ],
        LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT => [
          'default' => TRUE,
        ],
      ],
    ];
    $options['full_screen'] = [
      'default' => FALSE,
    ];
    $options['scale_metric'] = [
      'default' => FALSE,
    ];
    $options['scale_imperial'] = [
      'default' => FALSE,
    ];
    $options['zoom_indicator'] = [
      'default' => FALSE,
    ];
    $options['open_balloons_on_click'] = [
      'default' => TRUE,
    ];
    $options['open_balloons_on_hover'] = [
      'default' => FALSE,
    ];
    $options['goto_content_on_click'] = [
      'default' => FALSE,
    ];
    $options['map_reset'] = [
      'default' => FALSE,
    ];
    $options['map_reset_css_class'] = [
      'default' => 'R',
    ];
    $options['map_cluster_toggle'] = [
      'default' => FALSE,
    ];
    $options['mini_map'] = [
      'contains' => [
        'on' => [
          'default' => FALSE,
        ],
        'height' => [
          'default' => 100,
        ],
        'width' => [
          'default' => 150,
        ],
        'toggle' => [
          'default' => TRUE,
        ],
        'scope_color' => [
          'default' => 'red',
        ],
        'zoom_delta' => [
          'default' => -5,
        ],
      ],
    ];
    $options['cluster_radius'] = [
      'default' => $this->moduleHandler
        ->moduleExists('leaflet_markercluster') ? 80 : '',
    ];
    $options['disable_clustering_at_zoom'] = [
      'default' => '',
    ];
    $options['cluster_differentiator'] = [
      'contains' => [
        'cluster_differentiator_fields' => [
          'default' => '',
        ],
        'zoom_ranges' => [
          'default' => [],
        ],
        'cluster_tooltips' => [
          'default' => TRUE,
        ],
        'cluster_outline' => [
          'default' => 0,
        ],
        'cluster_touch_mode' => [
          'default' => 1,
        ],
      ],
    ];
    $options['cluster_aggregation'] = [
      'contains' => [
        'aggregation_field' => [
          'default' => '',
        ],
        'aggregation_function' => [
          'default' => '',
        ],
        'ranges' => [
          'contains' => [],
        ],
        'precision' => [
          'default' => '',
        ],
      ],
    ];
    $range = 10;
    foreach ([
      'small',
      'medium',
      'large',
    ] as $size) {
      $options['cluster_aggregation']['contains']['ranges']['contains'][$size] = [
        'default' => $range,
      ];
      $range *= 10;
    }
    $options['disable_clustering_at_zoom'] = [
      'default' => '',
    ];
    $options['empty_map_center'] = [
      'default' => '',
    ];
    $options['map_options_leaflet'] = [
      'contains' => [
        'maxzoom' => [
          'default' => 18,
        ],
        'zoom' => [
          'default' => 2,
        ],
        'zoom_on_click' => [
          'default' => '',
        ],
        'center_lat' => [
          'default' => '',
        ],
        'center_lon' => [
          'default' => '',
        ],
        'scrollwheelzoom' => [
          'default' => TRUE,
        ],
        'dragging' => [
          'default' => TRUE,
        ],
        'separator' => [
          'default' => '<br/>',
        ],
      ],
    ];
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    parent::buildOptionsForm($form, $form_state);
    $path = drupal_get_path('module', 'ip_geoloc');
    if (strpos($this->ipGeolocGlobal
      ->markerDirectory(), 'amarkers')) {
      $form['#attached']['library'][] = 'ip_geoloc/admin_css_amarker';
    }
    else {
      $form['#attached']['library'][] = 'ip_geoloc/admin_css_marker';
    }
    $form_state
      ->set('renderer', 'leaflet');
    $weight = 1;
    $this
      ->addMapAndHeight($form, $weight);
    $this->viewPluginStyle
      ->pluginStyleBulkOfForm($this, $form, $form_state);
    $form['center_option']['#options'][0] = $this
      ->t('Auto-box to fit all markers (include visitor marker if color <strong>not</strong> set to &lt;none&gt;)');
    $lib_markercluster = $this->moduleHandler
      ->moduleExists('leaflet_markercluster') ? 'leaflet_markercluster/leaflet-markercluster' : FALSE;
    $fields = ip_geoloc_get_display_fields($this->displayHandler, FALSE, TRUE);
    $this
      ->addDefaultMarker($form, $weight);
    $this
      ->addVisitorMarker($form, $weight);
    $this
      ->addCheckBoxes($form, $lib_markercluster, $weight);
    $this
      ->addMiniMapInset($form, $weight);
    $this
      ->addMarkerTags($form, $fields, $weight);
    $this
      ->addMarkerTooltips($form, $fields, $weight);
    $this
      ->addSync($form, $weight);
    $this
      ->addMarkerCluster($form, $lib_markercluster, $weight);
    $this
      ->addClusterDifferentiator($form, $form_state, $lib_markercluster, $weight);
    $this
      ->addMoreMapOptions($form, $weight);
  }

  /**
   * Sets rendered field definition.
   */
  public function setRenderedFields($rendered_fields = []) {
    $this->rendered_fields = $rendered_fields;
  }

  /**
   * Gets rendered field definition.
   */
  public function getRenderedFields() {
    return $this->rendered_fields;
  }

  /**
   * Form part definition.
   */
  private function addMapAndHeight(&$form, &$weight) {
    $maps = [];
    foreach ($this
      ->pluginStyleLeafletMapGetInfo() as $key => $map) {
      $maps[$key] = $map['label'];
    }
    $form['map'] = [
      '#title' => $this
        ->t('Map'),
      '#type' => 'select',
      '#options' => $maps,
      '#default_value' => $this->options['map'],
      '#required' => TRUE,
      '#weight' => $weight++,
    ];
    $desc1 = $this
      ->t('Examples: <em>250</em> or <em>50em</em> or <em>40vh</em> (percentage of viewport height).');
    $desc2 = $this
      ->t('If left blank, the height defaults to 300 pixels. The width of the map will extend to its bounding container.');
    $desc3 = $this
      ->t('You may enter <em>&lt;none></em>. If you do, then the height attribute must be set through Javascript or CSS elsewhere or the map will not display. CSS example: <em>.ip-geoloc-map .leaflet-container { height: 150px; }</em>');
    $form['map_height'] = [
      '#title' => $this
        ->t('Map height'),
      '#type' => 'textfield',
      '#size' => 7,
      '#default_value' => $this->options['map_height'],
      '#description' => $desc1 . '<br/>' . $desc2 . '<br/>' . $desc3,
      '#weight' => $weight++,
    ];
  }

  /**
   * Form part definition.
   */
  private function addDefaultMarker(&$form, &$weight) {
    $path = drupal_get_path('module', 'ip_geoloc');
    $desc1 = $this
      ->t('In addition to selecting a color, you may superimpose a special icon on top of each marker. <br><a target="fsymbols" href="!url_fsymbols">fsymbols</a> characters can be copied and pasted straight into the <strong>Font icon character</strong> field. Other libraries like <a target="fontawesome" href="!url_fontawesome">Font Awesome</a> and <a target="flaticon" href="!url_flaticon">flaticon</a> use names that you type in the <strong>Font icon class</strong> field.', [
      // '!url_fsymbols' => url('http://fsymbols.com'),
      // '!url_fontawesome' => url('http://fortawesome.github.io/Font-Awesome/cheatsheet'),
      // '!url_flaticon' => url('http://flaticon.com'),
      '!url_fsymbols' => 'http://fsymbols.com',
      '!url_fontawesome' => 'http://fortawesome.github.io/Font-Awesome/cheatsheet',
      '!url_flaticon' => 'http://flaticon.com',
    ]);
    $desc2 = $this
      ->t('<em>fsymbols</em> require no further installation. For other libraries see the <a target="readme" href="!url_readme">README</a>.', [
      // '!url_readme' => url("$path/README.txt"),.
      '!url_readme' => "{$path}/README.txt",
    ]);
    $desc3 = $this
      ->t('All this works best with the markers from the <em>/amarkers</em> directory, configurable <a target="config" href="!url_config">here</a>.', [
      // '!url_config' => url('admin/config/system/ip_geoloc'),.
      '!url_config' => 'admin/config/system/ip_geoloc',
    ]);
    $form['default_marker'] = [
      '#type' => 'fieldset',
      '#title' => $this
        ->t('Default marker style'),
      '#description' => $desc1 . '<br/>' . $desc2 . '<br/>' . $desc3,
      '#weight' => $weight++,
    ];
    $form['default_marker']['default_marker_color'] = [
      '#title' => $this
        ->t('Style/color'),
      '#type' => 'select',
      '#default_value' => $this->options['default_marker']['default_marker_color'],
      '#options' => $this->ipGeolocGlobal
        ->markerColors(),
      '#description' => $this
        ->t('Select an image to use for all location markers whose images are not overridden by the <strong>Location differentiator</strong> below.'),
      '#attributes' => [
        'class' => [
          'marker-color',
        ],
      ],
    ];
    $form['default_marker']['default_marker_special_char'] = [
      '#title' => $this
        ->t('Font icon character'),
      '#type' => 'textfield',
      '#size' => 8,
      '#default_value' => $this->options['default_marker']['default_marker_special_char'],
      '#description' => $this
        ->t('Paste directly from <a target="fsymbols" href="!url_fsymbols">fsymbols</a>. If the character displays in color or as a square then it may not save or display correctly.', [
        '!url_fsymbols' => 'http://text-symbols.com',
      ]),
    ];
    $desc4 = $this
      ->t('Use the class name from the font icon library you are using. Append <strong>light</strong>, <strong>dark</strong> or <strong>red</strong> to change the color. Examples:<br/>Font Awesome: <strong>fa fa-beer light</strong><br/>flaticon: <strong>flaticon-bicycle12 red</strong>');
    $form['default_marker']['default_marker_special_char_class'] = [
      '#title' => $this
        ->t('Font icon class'),
      '#type' => 'textfield',
      '#size' => 25,
      '#default_value' => $this->options['default_marker']['default_marker_special_char_class'],
      '#description' => $desc4,
    ];
  }

  /**
   * Form part definition.
   */
  private function addVisitorMarker(&$form, &$weight) {
    $form['visitor_marker_leaflet'] = [
      '#type' => 'fieldset',
      '#title' => $this
        ->t('Visitor marker style'),
      '#description' => $this
        ->t('For the visitor marker to show, enable the <em>Set my location</em> block and/or tick the option to periodically reverse-geocode via Google, under the <a href="@config_page">Data collection options</a>.', [
        // '@config_page' => url('admin/config/system/ip_geoloc'),.
        '@config_page' => 'admin/config/system/ip_geoloc',
      ]),
      '#weight' => $weight++,
    ];
    $visitor_marker_colors = [
      'none' => '<' . $this
        ->t('none') . '>',
    ] + $this->ipGeolocGlobal
      ->markerColors();
    unset($visitor_marker_colors['0']);
    $form['visitor_marker_leaflet']['visitor_marker_color'] = [
      '#title' => $this
        ->t('Style/color'),
      '#type' => 'select',
      '#multiple' => FALSE,
      '#default_value' => $this->options['visitor_marker_leaflet']['visitor_marker_color'],
      '#options' => $visitor_marker_colors,
      '#attributes' => [
        'class' => [
          'marker-color',
        ],
      ],
    ];
    $form['visitor_marker_leaflet']['visitor_marker_special_char'] = [
      '#title' => $this
        ->t('Font icon character'),
      '#type' => 'textfield',
      '#size' => 8,
      '#default_value' => $this->options['visitor_marker_leaflet']['visitor_marker_special_char'],
      '#description' => $this
        ->t('As above'),
    ];
    $form['visitor_marker_leaflet']['visitor_marker_special_char_class'] = [
      '#title' => $this
        ->t('Font icon class'),
      '#type' => 'textfield',
      '#size' => 25,
      '#default_value' => $this->options['visitor_marker_leaflet']['visitor_marker_special_char_class'],
      '#description' => $this
        ->t('As above.'),
    ];
    $form['visitor_marker_leaflet']['visitor_marker_accuracy_circle'] = [
      '#title' => $this
        ->t('Accuracy circle'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['visitor_marker_leaflet']['visitor_marker_accuracy_circle'],
      '#description' => $this
        ->t("Display a circle depicting where the visitor's real location is most likely to be."),
    ];
  }

  /**
   * Form part definition.
   */
  private function addMarkerTags(&$form, $fields, &$weight) {
    $form['tags'] = [
      '#title' => $this
        ->t('Marker tags'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => empty($this->options['tags']['marker_tag']),
      '#description' => $this
        ->t('Each marker may have a tag. A tag is a number or short text shown permanently above, below or inside the marker.'),
      '#weight' => $weight++,
    ];
    $form['tags']['marker_tag'] = [
      '#title' => $this
        ->t('Views field to populate tags'),
      '#type' => 'select',
      '#default_value' => $this->options['tags']['marker_tag'],
      '#options' => $fields,
      '#description' => $this
        ->t('Example: "Content: Title". Use "Global: View result counter" if you want to number your locations.'),
    ];
    $form['tags']['tag_css_class'] = [
      '#title' => $this
        ->t('Tag position and style'),
      '#type' => 'textfield',
      '#default_value' => $this->options['tags']['tag_css_class'],
      '#description' => $this
        ->t('The CSS class or classes applied to each tag. Tagged marker CSS classes coming with this module are <strong>tag-above-marker</strong>, <strong>tag-below-marker</strong> and <strong>tag-inside-marker</strong>. If you opted to have <em>no markers</em>, i.e. tags only, you may use <strong>tag-rounded-corners</strong> or <strong>tag-pointy-circle</strong>, which is recommended for numbers. You may also create your own CSS classes and use them here.'),
    ];
  }

  /**
   * Form part definition.
   */
  private function addMarkerTooltips(&$form, $fields, &$weight) {
    $form['tooltips'] = [
      '#title' => $this
        ->t('Marker tooltips'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => empty($this->options['tooltips']['marker_tooltip']),
      '#description' => $this
        ->t('In addition to balloons, which pop up when markers are <em>clicked</em>, you can have tooltips. A tooltip is a short text that appears when you <em>hover</em> over a marker.'),
      '#weight' => $weight++,
    ];
    $note_polygons = $this
      ->t('Applies to markers. If you want tooltips for lines and polygons too, please use this selector in combination with <a href="@url_leaflet_label">Leaflet Label</a>.', [
      '@url_leaflet_label' => 'http://drupal.org/project/leaflet_label',
    ]);
    $form['tooltips']['marker_tooltip'] = [
      '#title' => $this
        ->t('Views field to populate tooltips'),
      '#type' => 'select',
      // '#multiple' => TRUE,
      // '#size' => 6,.
      '#default_value' => $this->options['tooltips']['marker_tooltip'],
      '#options' => $fields,
      '#description' => $this
        ->t('Example: "Content: Title"') . '<br/>' . $note_polygons,
    ];
  }

  /**
   * Form part definition.
   */
  private function addCheckBoxes(&$form, $lib_markercluster, &$weight) {
    $form['full_screen'] = [
      '#title' => $this
        ->t('Add a full-screen toggle to the map'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['full_screen'],
      '#weight' => $weight++,
    ];

    // @TODO check how to migrate this library

    /*$lib_fullscreen = libraries_get_path('leaflet-fullscreen');

        if ($lib_fullscreen) {
        $file_fullscreen = $lib_fullscreen . '/dist/Leaflet.fullscreen.min.js';
        if (!file_exists($file_fullscreen)) {
        $form['full_screen']['#description'] = $this->t('Error: <em>leaflet-fullscreen</em> library found, but %js_file is missing.', array('%js_file' => $file_fullscreen));
        }
        }
        else {
        $form['full_screen']['#description'] = $this->t('Requires this <a target="_js" href="@js_lib">JS library</a> to be downloaded to <em>/sites/all/libraries</em>. Change the directory name to <em>leaflet-fullscreen</em>.', array(
        '@js_lib' => 'https://github.com/Leaflet/Leaflet.fullscreen'));
        }*/
    $form['scale_imperial'] = [
      '#title' => $this
        ->t('Add an imperial (miles) scale'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['scale_imperial'],
      '#weight' => $weight++,
    ];
    $form['scale_metric'] = [
      '#title' => $this
        ->t('Add a metric (km) scale'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['scale_metric'],
      '#weight' => $weight++,
    ];
    $form['zoom_indicator'] = [
      '#title' => $this
        ->t('Add an indicator showing the active zoom level'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['zoom_indicator'],
      '#weight' => $weight++,
    ];
    $form['map_reset'] = [
      '#title' => $this
        ->t('Add a reset button'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['map_reset'],
      '#description' => $this
        ->t('This button allows the visitor to reset the map to its initial bounds (center and zoom level).'),
      '#weight' => $weight++,
    ];
    $form['map_reset_css_class'] = [
      '#title' => $this
        ->t('CSS class to apply to reset button'),
      '#type' => 'textfield',
      '#size' => 40,
      '#default_value' => $this->options['map_reset_css_class'],
      '#description' => $this
        ->t('You can use this to superimpose a font-icon on the button. For instance, if you have the Font Awesome library loaded for your markers, try <strong>fa fa-repeat</strong>. If you enter only one or two characters, for example <strong>R</strong>, these will be used verbatim as the label instead.'),
      '#weight' => $weight++,
      '#states' => [
        'visible' => [
          'input[name="style_options[map_reset]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];
    $form['map_cluster_toggle'] = [
      '#title' => $this
        ->t('Add cluster toggle button'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['map_cluster_toggle'],
      '#description' => $this
        ->t('This button allows the visitor to toggle marker clustering on/off at any time and at any zoom level.') . '<br/>' . $this
        ->t('A cluster radius must be specified below.'),
      '#weight' => $weight++,
    ];
    $form['open_balloons_on_click'] = [
      '#title' => $this
        ->t('Display non-excluded fields in a pop-up balloon above the marker, when <em>clicked</em>.'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['open_balloons_on_click'],
      '#weight' => $weight++,
    ];
    $form['open_balloons_on_hover'] = [
      '#title' => $this
        ->t('Display non-excluded fields in a pop-up balloon above the marker, when <em>hovered</em>.'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['open_balloons_on_hover'],
      '#weight' => $weight++,
    ];
    $form['goto_content_on_click'] = [
      '#title' => $this
        ->t('Go to the associated content page, when a marker is <em>clicked</em>.'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['goto_content_on_click'],
      '#weight' => $weight++,
    ];
    if (!$lib_markercluster) {
      $form['map_cluster_toggle']['#description'] .= '<br/>' . $this
        ->t('<a href="!url_project">Leaflet MarkerCluster</a> must be enabled.', [
        '!url_project' => 'http://drupal.org/project/leaflet_markercluster',
      ]);
    }
  }

  /**
   * Form part definition.
   */
  private function addSync(&$form, &$weight) {
    $form['sync'] = [
      '#title' => $this
        ->t('Cross-highlighting between map markers and page content outside the map'),
      '#description' => '<br/>' . $this
        ->t('For the cross-highlighting to work, content outside the map must have the CSS class <em>.sync-id-[nid]</em>, where <em>[nid]</em> represents the content ID.') . ' ' . $this
        ->t('For Views content, you can do this by adding a <strong>Row class</strong> to the Grid, Table, HTML or Unformatted list formats of your Views Attachment or Block displays.'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => empty($this->options['sync'][LEAFLET_SYNC_CONTENT_TO_MARKER]),
      '#weight' => $weight++,
    ];
    $note = $this
      ->t('You can redefine this class to change the default look.');
    $form['sync'][LEAFLET_SYNC_CONTENT_TO_MARKER] = [
      '#title' => $this
        ->t('When hovering markers on the map, highlight associated content on the page'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['sync'][LEAFLET_SYNC_CONTENT_TO_MARKER] && $this->options['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT],
      '#description' => $this
        ->t('Content is highlighted dynamically through the automatic addition of the CSS class <em>.synced-marker-hover</em>.') . ' ' . $note,
      '#weight' => $weight++,
    ];
    $caveat = $this
      ->t('For this feature to work in combination with any <em>sorting</em> on the hovered content, the associated Views display must have <em>Use Ajax: No</em>.');
    $form['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT] = [
      '#title' => $this
        ->t('When hovering content, highlight associated markers on the map'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT],
      '#description' => $this
        ->t('Markers are highlighted dynamically through the automatic addition of the CSS class <em>.synced-content-hover</em>.') . ' ' . $note . '<br/>' . $caveat,
      '#weight' => $weight++,
    ];
    $form['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT_WITH_POPUP] = [
      '#title' => $this
        ->t('As above, but also pop up marker balloons'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT_WITH_POPUP],
      '#states' => [
        'visible' => [
          ':input[name="style_options[sync][4]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
      '#weight' => $weight++,
    ];
    $form['sync'][LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT] = [
      '#title' => $this
        ->t('Unhighlight marker and close its balloon when hovering off the map'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['sync'][LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT],
      '#states' => [
        'visible' => [
          ':input[name="style_options[sync][4]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
      '#weight' => $weight++,
    ];
  }

  /**
   * Form part definition.
   */
  private function addMiniMapInset(&$form, &$weight) {
    $form['mini_map'] = [
      '#title' => $this
        ->t('Mini-map inset'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => !empty($this->options['mini_map']['on']),
      '#description' => $this
        ->t('A zoomed-out version of the main map as a mini-map inset in the bottom corner.'),
      '#weight' => $weight++,
    ];
    $form['mini_map']['on'] = [
      '#title' => $this
        ->t('Enable mini-map inset'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->options['mini_map']['on']),
    ];
    $form['mini_map']['height'] = [
      '#title' => $this
        ->t('Height of inset'),
      '#type' => 'textfield',
      '#size' => 4,
      '#field_suffix' => $this
        ->t('px'),
      '#default_value' => $this->options['mini_map']['height'],
      '#states' => [
        'visible' => [
          'input[name="style_options[mini_map][on]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];
    $form['mini_map']['width'] = [
      '#title' => $this
        ->t('Width of inset'),
      '#type' => 'textfield',
      '#size' => 4,
      '#field_suffix' => $this
        ->t('px'),
      '#default_value' => $this->options['mini_map']['width'],
      '#states' => [
        'visible' => [
          'input[name="style_options[mini_map][on]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];
    $form['mini_map']['toggle'] = [
      '#title' => $this
        ->t('Allow visitor to minimise inset'),
      '#type' => 'checkbox',
      '#default_value' => !empty($this->options['mini_map']['toggle']),
      '#states' => [
        'visible' => [
          'input[name="style_options[mini_map][on]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];
    $form['mini_map']['scope_color'] = [
      '#title' => $this
        ->t('Scope rectangle color'),
      '#type' => 'textfield',
      '#size' => 10,
      '#default_value' => $this->options['mini_map']['scope_color'],
      '#description' => $this
        ->t('<em>#rrggbb</em> or <a target="_colors" href="@url">color name</a>.', [
        '@url' => 'http://www.w3schools.com/html/html_colornames.asp',
      ]),
      // '@url' => url('http://www.w3schools.com/html/html_colornames.asp'))),
      '#states' => [
        'visible' => [
          'input[name="style_options[mini_map][on]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];
    $form['mini_map']['zoom_delta'] = [
      '#title' => $this
        ->t('Zoom delta'),
      '#type' => 'textfield',
      '#size' => 3,
      '#default_value' => $this->options['mini_map']['zoom_delta'],
      '#description' => $this
        ->t('The difference between the zoom levels of main map and mini-map.'),
      '#states' => [
        'visible' => [
          'input[name="style_options[mini_map][on]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];

    // @ TODO find correct way to include this library

    /*$lib_minimap = libraries_get_path('leaflet-minimap');
      if ($lib_minimap) {
      $file_minimap = $lib_minimap . '/dist/Control.MiniMap.min.js';
      if (!file_exists($file_minimap)) {
      $form['mini_map']['#description'] .= '<br/>' . $this->t('Error: <em>leaflet-minimap</em> library found, but %js_file is missing.', array('%js_file' => $file_minimap));
      }
      }
      else {
      $form['mini_map']['#description'] .= '<br/>' . $this->t('Requires this <a target="_js" href="@js_lib">JS library</a> to be downloaded to <em>/sites/all/libraries</em>. Change the directory name to <em>leaflet-minimap</em>.', array(
      '@js_lib' => 'https://github.com/Norkart/Leaflet-Minimap'));
      }*/
  }

  /**
   * Form part defintion.
   */
  private function addMarkerCluster(&$form, $lib_markercluster, &$weight) {
    $desc_a = $this
      ->t('A typical cluster radius is 20 to 100 px. When you use a <em>cluster region differentiator</em> (see below), a marker radius of 200 px or more may give superior results. <br/>The visitor marker is excluded from clustering. Enter 0 to disable clustering altogether.');
    $desc_b = $this
      ->t('Requires the <a target="project" href="!url_project">Leaflet MarkerCluster</a> module.', [
      '!url_project' => 'http://drupal.org/project/leaflet_markercluster',
    ]);
    $form['cluster_radius'] = [
      '#title' => $this
        ->t('Marker cluster radius'),
      '#type' => 'textfield',
      '#field_suffix' => $this
        ->t('px'),
      '#size' => 4,
      '#default_value' => $this->options['cluster_radius'],
      '#description' => $lib_markercluster ? $desc_a : $desc_b,
      '#weight' => $weight++,
    ];
    $form['disable_clustering_at_zoom'] = [
      '#title' => $this
        ->t('Disable clustering at zoom'),
      '#type' => 'textfield',
      '#size' => 4,
      '#default_value' => is_numeric($this->options['disable_clustering_at_zoom']) ? $this->options['disable_clustering_at_zoom'] : '',
      '#description' => $this
        ->t('If you specify a zoom level, then there will be no clustering beyond that zoom level, regardless of the radius specified.'),
      '#weight' => $weight++,
    ];
    $form['allow_clusters_of_one'] = [
      '#title' => $this
        ->t('Allow clusters of one'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['allow_clusters_of_one'],
      '#description' => $this
        ->t('Especially recommended when your clusters employ aggregation functions.'),
      '#weight' => $weight++,
    ];
  }

  /**
   * Form part definition.
   */
  private function addClusterDifferentiator(&$form, FormStateInterface &$form_state, $lib_markercluster, &$weight) {
    $path = drupal_get_path('module', 'ip_geoloc');
    $intro = $this
      ->t('Region-aware marker clustering with <a target="regionbound" href="!url_regionbound">RegionBound</a>. See the <a target="readme" href="!url_readme">README</a> for details.', [
      // '!url_regionbound' => url('http://regionbound.com'),
      // '!url_readme' => url("$path/README.txt"),.
      '!url_regionbound' => 'http://regionbound.com',
      '!url_readme' => "{$path}/README.txt",
    ]);
    $form['cluster_differentiator'] = [
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#title' => $this
        ->t('Cluster region differentiator'),
      '#description' => '<em>' . $intro . '</em><br/>',
      // The id in the prefix must match the AJAX submit handlers below.
      '#prefix' => '<div id="cluster-differentiator-wrapper">',
      '#suffix' => '</div>',
      '#weight' => $weight++,
    ];
    $region_field_names = NULL;
    if ($trigger = $form_state
      ->getTriggeringElement()) {

      // Get here when any form element with #ajax was changed/clicked causing
      // an auto-rebuild of the form.
      if (strpos($trigger['#id'], 'cluster-differentiator-field') > 0) {

        // Get here when it was the cluster-differentiator multi-select that was clicked.
        $region_field_names = $trigger['#value'];
      }
    }
    else {
      $region_field_names = $this->options['cluster_differentiator']['cluster_differentiator_fields'];
    }
    if (!$this->moduleHandler
      ->moduleExists('leaflet_markercluster')) {
      $desc = $this
        ->t('Requires the <a target="project" href="!url_project">Leaflet MarkerCluster</a> module', [
        '!url_project' => 'http://drupal.org/project/leaflet_markercluster',
      ]);
      $form['cluster_differentiator']['#description'] .= $desc . ' ' . $this
        ->t('and <a href="!url_regionbound">RegionBound</a> JS plugin.', [
        '!url_regionbound' => 'http://regionbound.com',
      ]);
    }
    elseif ($lib_markercluster && (empty($region_field_names) || !reset($region_field_names))) {
      $note_region = $this
        ->t('Download the file %js from <a target="regionbound" href="!url_regionbound">Regionbound</a> and drop it in %directory, without renaming. Then select your region differentiator below.', [
        '%js' => IP_GEOLOC_LEAFLET_MARKERCLUSTER_REGIONBOUND_JS,
        '%directory' => $lib_markercluster,
        '!url_regionbound' => 'http://regionbound.com',
      ]);
      $form['cluster_differentiator']['#description'] .= "<p>{$note_region}</p>";
    }

    // @TODO check how to migrate this
    // $fields = ip_geoloc_get_display_fields($this->displayHandler, FALSE, FALSE);
    $fields = ip_geoloc_get_display_fields($this->displayHandler, TRUE);
    $form['cluster_differentiator']['cluster_differentiator_fields'] = [
      '#title' => $this
        ->t('Region differentiator'),
      '#type' => 'select',
      '#multiple' => TRUE,
      '#size' => 6,
      '#options' => $fields,
      '#default_value' => $region_field_names,
      '#ajax' => [
        'callback' => [
          self::class,
          'pluginStyleLeafletRefreshClusterFieldsetJs',
        ],
        'wrapper' => 'cluster-differentiator-wrapper',
        'prevent' => 'submit',
      ],
      '#description' => $this
        ->t('Select a field (or sequence of fields) that reflect for each location marker the region hierarchy it belongs to. Examples are an <a target="drupal" href="!url_addressfield">AddressField</a>, a hierarchical taxonomy term based on regions, or individual fields for country, state, city, suburb (<em>in that order</em>). Make sure that the region differentiator you wish to use is included as a field in your view, so it appears in the list above. Note that region differentiators do <em>not</em> need to be associated with latitudes or longitudes. They are just name fields.', [
        '!url_addressfield' => 'http://drupal.org/project/addressfield',
      ]),
    ];
    $level = 0;
    if (!empty($region_field_names)) {
      foreach ($region_field_names as $region_field_name) {
        if (!empty($region_field_name)) {
          $region_field = FieldStorageConfig::loadByName('node', $region_field_name);

          //$region_field = \Drupal::entityManager()->getStorage($region_field_name)->loadMultiple();

          // $region_field = field_info_field($region_field_name);
          $field_type = empty($region_field
            ->get('type')) ? '' : $region_field
            ->get('type');
          $region_depth = $this
            ->getRegionFieldDepth($region_field);
          $zoom_titles = $this
            ->getZoomTitles($field_type, $fields[$region_field_name], $region_depth);
          foreach ($zoom_titles as $title) {
            $level++;
            $default_value = isset($this->options['cluster_differentiator']['zoom_ranges'][$level]) ? $this->options['cluster_differentiator']['zoom_ranges'][$level] : '';
            $form['cluster_differentiator']['zoom_ranges'][$level] = [
              '#type' => 'textfield',
              '#title' => Xss::filterAdmin($title),
              '#size' => 28,
              '#default_value' => $default_value,
              '#element_validate' => [
                'ip_geoloc_range_widget_validate',
              ],
            ];
          }
        }
      }
      if ($level > 0) {
        $desc1 = $level === 1 ? $this
          ->t('Below enter the zoom level range to be associated with the selected differentiator.') : $this
          ->t('Below enter the zoom level ranges to which each of the region hierarchy levels apply.') . '<br/>' . $this
          ->t('Zoom ranges may start and end at any level, but must not overlap.');

        // $this->t('Minimum and maximum zoom levels for this map can be found below under <strong>More map options</strong>.');.
        $desc2 = '';
        $desc3 = $this
          ->t('Or leave all fields blank to use the defaults and refine later.');
        $zoom1 = $this
          ->t('Typical zoom ranges for Europe: country: 3--6, province: 7--9, city: 10--14, postcode: 15--18');
        $zoom2 = $this
          ->t('Typical zoom ranges for US & Canada: country: 1--3, state: 4--8, city: 9--13, zip: 14--18');
        $zoom3 = $this
          ->t('Typical zoom ranges for Australia: country: 1--2, state: 3--9, city: 10--13, suburb/postcode: 14--18');
        $form['cluster_differentiator']['cluster_differentiator_fields']['#description'] = implode('<br/>', [
          "{$desc1} {$desc2}<br/>{$desc3}<br/>",
          $zoom1,
          $zoom2,
          $zoom3,
        ]);
      }
      $form['cluster_differentiator']['cluster_tooltips'] = [
        '#title' => $this
          ->t('Add cluster tooltips'),
        '#type' => 'checkbox',
        '#default_value' => $this->options['cluster_differentiator']['cluster_tooltips'],
        '#description' => $this
          ->t("When hovering a cluster, tooltips reveal the cluster region name and the names of its populated subregions."),
      ];
      $form['cluster_differentiator']['cluster_touch_mode'] = [
        '#title' => $this
          ->t('Cluster action on touch devices (e.g. mobile phones)'),
        '#type' => 'radios',
        '#options' => [
          1 => $this
            ->t('Single tap displays cluster regions, double-tap drills into cluster (default)'),
          0 => $this
            ->t('Single tap drills into cluster. Sub-region names not displayed, but visible on mouse devices.'),
        ],
        '#default_value' => $this->options['cluster_differentiator']['cluster_touch_mode'],
        '#description' => $this
          ->t('Applies only to devices that do not have a mouse, like mobile phones.'),
      ];
      $form['cluster_differentiator']['cluster_outline'] = [
        '#title' => $this
          ->t('Cluster population outline'),
        '#type' => 'select',
        '#options' => [
          0 => $this
            ->t('Traditional (convex hull)'),
          1 => $this
            ->t('Avant-garde (heuristic hull)'),
        ],
        '#default_value' => $this->options['cluster_differentiator']['cluster_outline'],
        '#description' => $this
          ->t('When hovering a cluster, a <a target="regionbound" href="!url_regionbound">coverage outline</a> visualises the envelope or footprint of the underlying marker population. Select your preferred style of doing this.', [
          '!url_regionbound' => 'http://regionbound.com/enhanced-cluster-envelope-using-heuristic-hull',
        ]),
      ];
    }
    $intro = $this
      ->t('Use clusters to report on region aggregates; requires <a target="regionbound" href="!url_regionbound">RegionBound</a>', [
      '!url_regionbound' => 'http://regionbound.com/coffee-prices-across-melbourne',
    ]);
    $desc = $this
      ->t('This feature aggregates values of a selected field across every region and displays the resulting <em>sum/average/min/max</em> on each cluster icon at every zoom level. It also colours each cluster icon based on its aggregated value, rather than its marker count.');
    $form['cluster_aggregation'] = [
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#title' => $this
        ->t('Cluster aggregation'),
      '#description' => "<em>{$intro}</em><p>{$desc}</p>",
      '#weight' => $weight++,
    ];
    $form['cluster_aggregation']['aggregation_field'] = [
      '#title' => $this
        ->t('Field to perform aggregation on'),
      '#type' => 'select',
      '#default_value' => $this->options['cluster_aggregation']['aggregation_field'],
      '#options' => $fields,
      '#description' => $this
        ->t('For aggregation to make sense, the selected field must be numeric or represent a list. If a list, aggregation will be applied to the element <em>count</em>.'),
    ];
    $form['cluster_aggregation']['aggregation_function'] = [
      '#title' => $this
        ->t('Aggregation function'),
      '#type' => 'select',
      '#default_value' => $this->options['cluster_aggregation']['aggregation_function'],
      '#options' => [
        'average' => $this
          ->t('Average'),
        'maximum' => $this
          ->t('Maximum'),
        'minimum' => $this
          ->t('Minimum'),
        'sum' => $this
          ->t('Sum'),
      ],
    ];
    $form['cluster_aggregation']['ranges'] = [
      '#type' => 'item',
      '#description' => $this
        ->t('Edit the above to change the color-coding of cluster icons based on their aggregate values.'),
      '#prefix' => '<div id="cluster-aggregation-aggregate">',
      '#suffix' => '</div>',
    ];
    foreach ([
      'small',
      'medium',
      'large',
    ] as $size) {
      $default_value = isset($this->options['cluster_aggregation']['ranges'][$size]) ? $this->options['cluster_aggregation']['ranges'][$size] : '';
      $form['cluster_aggregation']['ranges'][$size] = [
        '#title' => "{$size} " . $this
          ->t('goes to'),
        '#type' => 'textfield',
        '#size' => 8,
        '#default_value' => $default_value,
      ];
    }
    $form['cluster_aggregation']['precision'] = [
      '#title' => $this
        ->t('Precision'),
      '#type' => 'textfield',
      '#size' => 2,
      '#default_value' => $this->options['cluster_aggregation']['precision'],
      '#description' => $this
        ->t('Number of significant digits used to display the aggregate value on the cluster icon. Leave blank for native formatting.'),
    ];
  }

  /**
   * Form part definition.
   */
  private function addMoreMapOptions(&$form, &$weight) {
    $selected_map = $this
      ->pluginStyleLeafletMapGetInfo($this->options['map']);
    $zoom_top = 21;
    if (isset($selected_map['settings']['maxZoom'])) {
      $zoom_top = $selected_map['settings']['maxZoom'];
    }
    $form['map_options_leaflet'] = [
      '#title' => $this
        ->t('More map options'),
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      // Or: empty($this->options['map_options_leaflet']['zoom']) ?
      '#collapsed' => TRUE,
      '#weight' => $weight++,
    ];
    $form['map_options_leaflet']['maxzoom'] = [
      '#title' => $this
        ->t('Maximum zoom level (0..@zoomtop)', [
        '@zoomtop' => $zoom_top,
      ]),
      '#type' => 'textfield',
      '#size' => 2,
      '#default_value' => $this->options['map_options_leaflet']['maxzoom'],
      '#description' => $this
        ->t('Note that not all maps support all zoom levels.'),
    ];
    $initial_zoom_max = $zoom_top;
    if (is_numeric($this->options['map_options_leaflet']['maxzoom'])) {
      $initial_zoom_max = min($zoom_top, $this->options['map_options_leaflet']['maxzoom']);
    }
    $form['map_options_leaflet']['zoom'] = [
      '#title' => $this
        ->t('Initial zoom level (0..@maxzoom)', [
        '@maxzoom' => $initial_zoom_max,
      ]),
      '#type' => 'textfield',
      '#size' => 2,
      '#default_value' => $this->options['map_options_leaflet']['zoom'],
      '#description' => $this
        ->t('Does not apply to auto-box centering except when only one or no markers are shown.'),
    ];
    $form['map_options_leaflet']['zoom_on_click'] = [
      '#title' => $this
        ->t('Zoom-on-click zoom level (1..@maxzoom)', [
        '@maxzoom' => $zoom_top,
      ]),
      '#type' => 'textfield',
      '#size' => 2,
      '#default_value' => $this->options['map_options_leaflet']['zoom_on_click'],
      '#description' => $this
        ->t('Level to zoom to when a marker is clicked. Leave blank to disable this feature.'),
    ];
    $form['map_options_leaflet']['center_lat'] = [
      '#title' => $this
        ->t('Latitude of initial center of map'),
      '#type' => 'textfield',
      '#size' => 6,
      '#default_value' => $this->options['map_options_leaflet']['center_lat'],
      '#description' => $this
        ->t('If both latitude and longitude are filled out, these override any centering option until the visitor changes their location.'),
    ];
    $form['map_options_leaflet']['center_lon'] = [
      '#title' => $this
        ->t('Longitude of initial center of map'),
      '#type' => 'textfield',
      '#size' => 6,
      '#default_value' => $this->options['map_options_leaflet']['center_lon'],
      '#description' => $this
        ->t('If both latitude and longitude are filled out, these override any centering option until the visitor changes their location.'),
    ];
    $form['map_options_leaflet']['scrollwheelzoom'] = [
      '#title' => $this
        ->t('Enable scroll wheel zoom'),
      '#type' => 'select',
      '#default_value' => $this->options['map_options_leaflet']['scrollwheelzoom'],
      '#options' => [
        TRUE => $this
          ->t('Yes'),
        FALSE => $this
          ->t('No'),
      ],
    ];
    $form['map_options_leaflet']['dragging'] = [
      '#title' => $this
        ->t('Dragging/Panning of the map'),
      '#type' => 'select',
      '#default_value' => $this->options['map_options_leaflet']['dragging'],
      '#options' => [
        TRUE => $this
          ->t('Yes'),
        FALSE => $this
          ->t('No'),
      ],
    ];
    $form['map_options_leaflet']['separator'] = [
      '#title' => $this
        ->t('Separator used in marker balloons'),
      '#type' => 'textfield',
      '#size' => 10,
      '#default_value' => $this->options['map_options_leaflet']['separator'],
      '#description' => $this
        ->t('You may use most HTML tags.'),
    ];
  }

  /**
   * Validate the options form.
   */
  public function validateOptionsForm(&$form, FormStateInterface $form_state) {
    $this->viewPluginStyle
      ->pluginStyleBulkOfFormValidate($form, $form_state);
    $this_options = $form_state
      ->getValue('style_options');
    $map_height = trim($this_options['map_height']);
    if (is_numeric($map_height) && $map_height <= 0) {
      form_error($form['map_height'], $this
        ->t('Map height must be a positive number.'));
    }
    $selected_map = $this
      ->pluginStyleLeafletMapGetInfo($this_options['map']);
    $zoom_top = 18;
    if (isset($selected_map['settings']['maxZoom'])) {
      $zoom_top = $selected_map['settings']['maxZoom'];
    }
    $max_zoom = $this_options['map_options_leaflet']['maxzoom'];
    if ($max_zoom != '' && (!is_numeric($max_zoom) || $max_zoom < 0 || $max_zoom > $zoom_top)) {
      $form_state
        ->setError($form['map_options_leaflet']['maxzoom'], $this
        ->t('"Maximum zoom level" for %map must be in range 0..@zoomtop', [
        '%map' => $selected_map['label'],
        '@zoomtop' => $zoom_top,
      ]));
    }
    $zoom = $this_options['map_options_leaflet']['zoom'];
    if ($zoom != '' && (!is_numeric($zoom) || $zoom < 0 || $zoom > $max_zoom)) {
      $form_state
        ->setError($form['map_options_leaflet']['zoom'], $this
        ->t('"Initial zoom level" must be a non-negative number not greater than "Maximum zoom level".'));
    }
    $disable_zoom = $this_options['disable_clustering_at_zoom'];
    if ($disable_zoom != '' && (!is_numeric($disable_zoom) || $disable_zoom < 0 || $disable_zoom > $max_zoom)) {
      $form_state
        ->setError($form['disable_clustering_at_zoom'], $this
        ->t('"Disable clustering at zoom" level must be a positive number not greater than "Maximum zoom level".'));
    }
    if (isset($this_options['cluster_aggregation'])) {
      $cluster_aggregation = $this_options['cluster_aggregation'];
      if (!empty($cluster_aggregation['aggregation_field'])) {
        $aggregation_field_info = FieldStorageConfig::loadByName($cluster_aggregation['aggregation_field']);
        $valid_types = [
          'number_integer',
          'number_decimal',
          'number_float',
          'entityreference',
        ];
        if (!$aggregation_field_info || !in_array($aggregation_field_info['type'], $valid_types)) {
          $this->messenger
            ->addMessage($this
            ->t('A cluster aggregation field that cannot be interpreted as a number may cause unexpected errors.'));
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    return 0;
  }

  /**
   * Pending doc.
   */
  public function getRegionFieldDepth($region_field) {
    $type = $region_field
      ->get('type');
    if (empty($type)) {

      // Dodgy business. Return 1 and hope for the best.
      return 1;
    }
    if ($type === 'addressfield') {
      return 4;
    }
    $settings = $region_field
      ->getSettings();

    //if (empty($settings['allowed_values'])) {
    if ($settings['target_type'] != 'taxonomy_term') {
      return 1;
    }

    // Possibly a taxonomy or list.
    $depth = 0;

    //@TODO Find out how to migrate this

    // foreach ($region_field['settings']['allowed_values'] as $tree) {
    //   if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
    //     if ($terms = taxonomy_get_tree($vocabulary->vid, $tree['parent'])) {
    //       foreach ($terms as $term) {
    //         $depth = max($term->depth, $depth);
    //       }
    //       break;
    //     }
    //   }
    // }
    return $depth + 1;
  }

  /**
   * Peding doc.
   */
  private function getZoomTitles($field_type, $label, $region_depth) {
    $titles = [];
    if ($field_type === 'addressfield') {
      $titles[] = $this
        ->t('Zoom range for country');
      $titles[] = $this
        ->t('Zoom range for administrative area (state, district, county)');
      $titles[] = $this
        ->t('Zoom range for locality (city, town, village)');
      $titles[] = $this
        ->t('Zoom range for post code (ZIP)');
      return $titles;
    }
    $pos_colon = strrpos($label, ':');
    $label = Unicode::substr($label, $pos_colon > 0 ? $pos_colon + 1 : 0);
    if ($field_type === 'taxonomy_term_reference') {
      for ($level = 1; $level <= $region_depth; $level++) {
        $titles[] = $this
          ->t('Zoom range for %field level @level', [
          '%field' => $label,
          '@level' => $level,
        ]);
      }
    }
    else {
      $titles[] = $this
        ->t('Zoom range for %field', [
        '%field' => $label,
      ]);
    }
    return $titles;
  }

  /**
   * Fills out the region hierarchy belonging to a location object.
   *
   * @param string $field_type
   *   Is a 'taxonomy_term_reference', 'addressfield' or other.
   * @param object $location
   *   The location object whose regions attribute will be fleshed ut.
   * @param string $region
   *   Region or region hierarchy taken from the View result in the form of a
   *   taxonomy term (leaf) or AddressField. Or call the function repeatedly on
   *   the same location object passing in regions as plain fields, one by one,
   *   going from the big region (country) down to the small (suburb)
   * @param int $level
   *   Level of the region in the hierarchy, updated on return.
   */
  private function fillOutLocationRegion($field_type, &$location, $region, &$level) {
    switch ($field_type) {
      case 'taxonomy_term_reference':
        $region_hierarchy = taxonomy_get_parents_all($region);

        // Reverse, to order region hierarchy from large region to small.
        foreach (array_reverse($region_hierarchy) as $region_term) {
          $location->regions[$level++] = trim($region_term->name);
        }
        break;
      case 'addressfield':

        // $region = reset($region);
        if (!empty($region)) {
          $format_callback = 'addressfield_format_address_generate';
          if (function_exists($format_callback) && isset($region['country'])) {
            $format = [];
            $context = [
              'mode' => NULL,
            ];

            // Replace state and country codes by their full names.
            addressfield_format_address_generate($format, $region, $context);
            if (isset($format['country']['#options'][$region['country']])) {
              $region['country'] = $format['country']['#options'][$region['country']];
            }
            if (isset($region['administrative_area']) && isset($format['locality_block']['administrative_area']['#options'][$region['administrative_area']])) {
              $region['administrative_area'] = $format['locality_block']['administrative_area']['#options'][$region['administrative_area']];
            }
          }
          else {

            // drupal_set_message(t('IPGV&M: cannot flesh out countries and states on locations. Format callback %name is not available.', array('%name' => $format_callback)), 'warning', FALSE);.
          }
          $location->regions = [
            1 => isset($region['country']) ? trim($region['country']) : '',
            2 => isset($region['administrative_area']) ? trim($region['administrative_area']) : '',
            3 => isset($region['locality']) ? trim($region['locality']) : '',
            4 => isset($region['postal_code']) ? trim($region['postal_code']) : '',
          ];
          $level = 5;
        }
        break;
      default:

        // Note: $location->regions is meant to be ordered big to small.
        $location->regions[$level++] = trim($region);
    }
  }

  /**
   * Pending doc.
   */
  public function fillOutLocationRegions($locations) {

    // When an AddressField or hierarchical vocabulary is used, this normally
    // returns a single field name (as an array).
    if (empty($this->options['cluster_differentiator']['cluster_differentiator_fields'])) {
      return;
    }
    $region_fields = [];
    foreach ($this->options['cluster_differentiator']['cluster_differentiator_fields'] as $region_fieldname) {

      // @TODO get the $entity type change hard coding
      $region_field = FieldStorageConfig::loadByName('node', $region_fieldname);
      $region_fields[] = empty($region_field) ? $region_fieldname : $region_field;
    }
    if (empty($region_fields) || !reset($region_fields)) {
      return;
    }
    foreach ($this->view->result as $key => $row) {
      if (isset($locations[$key])) {
        $level = 1;
        foreach ($region_fields as $region_field) {
          $region_values = $this->viewPluginStyle
            ->getViewResult($this, $region_field, $key);
          $field_type = $region_field
            ->getType();

          // If the region is multi-valued, use the last value. A particular
          // case is a hierarchical region taxonomy. We want the smallest of the
          // regions in the hierarchy.
          $region = $field_type == 'addressfield' ? $region_values : end($region_values);
          if (empty($region)) {

            // Make sure region is a string, not 0 or FALSE.
            $region = '';
          }
          $this
            ->fillOutLocationRegion($field_type, $locations[$key], $region, $level);
        }
      }
    }
  }

  /**
   * Get the center of a lat/lon pair.
   */
  public function getCenter($location) {
    if (empty($location->type) || $location->type == 'point') {
      $lat = isset($location->lat) ? $location->lat : (isset($location->latitude) ? $location->latitude : 0.0);
      $lon = isset($location->lon) ? $location->lon : (isset($location->longitude) ? $location->longitude : 0.0);
      return [
        'lat' => $lat,
        'lon' => $lon,
      ];
    }
    if (!empty($location->component[0]['points'][0])) {
      return $location->component[0]['points'][0];
    }
  }

  /**
   * Checks if marker color is a good value.
   *
   * @param mixed $marker_color
   *   The color of the marker.
   *
   * @return bool
   *   TRUE if marker color is "0", zero, or FALSE
   *   FALSE if marker color equals '' or NULL
   */
  public function isNoMarker($marker_color) {
    return isset($marker_color) && ($marker_color === '0' || $marker_color === 0 || $marker_color === FALSE);
  }

  /**
   * Wrapper around the only programmatic dependency we have on Leaflet module.
   *
   * Note: this indirectly calls ip_geoloc_leaflet_map_info_alter($map_info).
   */
  public function pluginStyleLeafletMapGetInfo($map_name = NULL) {
    return $this->moduleHandler
      ->moduleExists('leaflet') ? leaflet_map_get_info($map_name) : [];
  }

  /**
   * Callback to compare locations based on weight.
   */
  public function pluginStyleLeafletCompare($location1, $location2) {
    $weight1 = empty($location1->weight) ? 0 : $location1->weight;
    $weight2 = empty($location2->weight) ? 0 : $location2->weight;
    return $weight2 - $weight1;
  }

  /**
   * Ajax callback in response to new rows or the diff. drop-down being changed.
   *
   * At this point the $form has already been rebuilt. All we have to do here is
   * tell AJAX what part of the browser form needs to be updated.
   */
  public function pluginStyleLeafletRefreshClusterFieldsetJs($form, &$form_state) {

    // Return the updated fieldset, so that ajax.inc can issue commands to the
    // browser to update only the targeted sections of the page.
    return $form['options']['style_options']['cluster_differentiator'];
  }

  /**
   * Transform the View result in a list of marker locations and render on map.
   *
   * @todo refactor
   */
  public function render() {
    if (empty($this->options['map']) || !($map = $this
      ->pluginStyleLeafletMapGetInfo($this->options['map']))) {
      return $this
        ->t('No Leaflet map was selected or map configuration was not found.');
    }

    // @TODO check if this is the right property
    if (!empty($this->view->preview)) {
      return $this
        ->t('The preview function is incompatible with Leaflet maps so cannot be used. Please visit the page path or the block to view your map.');
    }
    $render_start = microtime(TRUE);
    $this->viewPluginStyle
      ->pluginStyleRenderFields($this);
    $open_balloons_on_click = !empty($this->options['open_balloons_on_click']);
    $open_balloons_on_hover = !empty($this->options['open_balloons_on_hover']);
    $enable_balloons = $open_balloons_on_click || $open_balloons_on_hover;
    $locations = $this->viewPluginStyle
      ->pluginStyleExtractLocations($this, $enable_balloons);
    $this
      ->fillOutLocationRegions($locations);
    $marker_color = $this->options['default_marker']['default_marker_color'];
    $visitor_marker_color = $this->options['visitor_marker_leaflet']['visitor_marker_color'];
    $center_option = !isset($this->options['center_option']) ? 0 : $this->options['center_option'];
    $sync_flags = 0;
    if (!empty($this->options['sync'][LEAFLET_SYNC_CONTENT_TO_MARKER])) {
      $sync_flags |= LEAFLET_SYNC_CONTENT_TO_MARKER;
    }
    if (!empty($this->options['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT])) {
      $sync_flags |= LEAFLET_SYNC_MARKER_TO_CONTENT;
      if (!empty($this->options['sync'][LEAFLET_SYNC_MARKER_TO_CONTENT_WITH_POPUP])) {
        $sync_flags |= LEAFLET_SYNC_MARKER_TO_CONTENT_WITH_POPUP;
      }
      if (!empty($this->options['sync'][LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT])) {
        $sync_flags |= LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT;
      }
    }
    $has_full_screen = !empty($this->options['full_screen']);
    $has_mini_map = !empty($this->options['mini_map']['on']);
    $zoom_indicator = empty($this->options['zoom_indicator']) ? FALSE : TRUE;

    /*array('position' => 'topleft')*/
    $scale_control = FALSE;
    if (!empty($this->options['scale_metric']) || !empty($this->options['scale_imperial'])) {
      $scale_control = [
        'metric' => !empty($this->options['scale_metric']),
        'imperial' => !empty($this->options['scale_imperial']),
      ];
    }
    $goto_content_on_click = !empty($this->options['goto_content_on_click']);
    $reset_control = FALSE;
    if (!empty($this->options['map_reset'])) {
      $label = Xss::filterAdmin($this->options['map_reset_css_class']);
      $reset_control = [
        'label' => empty($label) ? ' ' : $label,
      ];
    }
    $cluster_control = FALSE;
    if ($this->moduleHandler
      ->moduleExists('leaflet_markercluster') && !empty($this->options['map_cluster_toggle'])) {
      $cluster_control = [
        'label' => 'C',
      ];
    }
    $cluster_radius = (int) $this->session
      ->getSessionValue('markercluster-radius');
    if ($cluster_radius < 2) {
      $cluster_radius = (int) $this->options['cluster_radius'];
    }
    $disable_clustering_at_zoom = $this->options['disable_clustering_at_zoom'];
    $hull_hug_factor = empty($this->options['cluster_differentiator']['cluster_outline']) ? -1 : 'auto';
    $cluster_tooltips = !empty($this->options['cluster_differentiator']['cluster_tooltips']);
    $cluster_touch_mode = empty($this->options['cluster_differentiator']['cluster_touch_mode']) ? 0 : 'auto';
    $cluster_aggregation_field = $this->options['cluster_aggregation']['aggregation_field'];
    $cluster_aggregation_function = $this->options['cluster_aggregation']['aggregation_function'];
    $cluster_aggregate_ranges = $this->options['cluster_aggregation']['ranges'];
    $cluster_aggregate_precision = $this->options['cluster_aggregation']['precision'];
    $allow_clusters_of_one = !empty($this->options['allow_clusters_of_one']);
    $tag_css_classes = $this->options['tags']['tag_css_class'];
    $module_path = drupal_get_path('module', 'ip_geoloc');
    $marker_path = file_create_url($this->ipGeolocGlobal
      ->markerDirectory());
    $max_zoom = (int) $this->options['map_options_leaflet']['maxzoom'];
    $zoom = max(1, (int) $this->options['map_options_leaflet']['zoom']);
    $zoom_on_click = (int) $this->options['map_options_leaflet']['zoom_on_click'];
    $scroll_wheel_zoom = (bool) $this->options['map_options_leaflet']['scrollwheelzoom'];
    $dragging = (bool) $this->options['map_options_leaflet']['dragging'];
    $visitor_location = $this->api
      ->getVisitorLocation();
    if (!isset($visitor_location['latitude'])) {
      $connection = \Drupal::database();
      $ip = \Drupal::request()
        ->getClientIp();

      //$query = $connection->select('ip_geoloc')->condition('ip_address', $ip);
      $result = $connection
        ->query('SELECT * FROM {ip_geoloc} WHERE ip_address = :ip_address', [
        ':ip_address' => $ip,
      ], []);
      foreach ($result as $item) {
        $visitor_location = $item;
      }

      // $visitor_location = $query->execute();
    }
    $use_specified_center = !empty($this->options['map_options_leaflet']['center_lat']) && !empty($this->options['map_options_leaflet']['center_lon']) && empty($visitor_location['is_updated']);
    if ($use_specified_center) {
      $map['center'] = [
        'lat' => $this->options['map_options_leaflet']['center_lat'],
        'lon' => $this->options['map_options_leaflet']['center_lon'],
      ];
    }
    elseif (!empty($locations) && ($center_option == IP_GEOLOC_MAP_CENTER_ON_FIRST_LOCATION || $visitor_marker_color == 'none' && count($locations) == 1)) {
      $map['center'] = $this
        ->getCenter(reset($locations));
    }
    elseif (($center_option == IP_GEOLOC_MAP_CENTER_OF_LOCATIONS || $center_option == IP_GEOLOC_MAP_CENTER_OF_LOCATIONS_WEIGHTED) && !empty($locations)) {
      list($center_lat, $center_lon) = $this->api
        ->centerOfLocations($locations, $center_option == IP_GEOLOC_MAP_CENTER_OF_LOCATIONS_WEIGHTED);
      $map['center'] = [
        'lat' => $center_lat,
        'lon' => $center_lon,
      ];
    }
    if (!$use_specified_center && (empty($locations) || $center_option == IP_GEOLOC_MAP_CENTER_ON_VISITOR) && isset($visitor_location['latitude'])) {
      $map['center'] = [
        'lat' => $visitor_location['latitude'],
        'lon' => $visitor_location['longitude'],
      ];
    }
    if (empty($locations)) {
      $ll = trim($this->options['empty_map_center']);
      if (empty($ll)) {

        // No map whatsoever.
        return;
      }
      if ($ll != $this
        ->t('visitor')) {

        // Empty map centered on coordinates provided.
        list($map['center']['lat'], $map['center']['lon']) = preg_split("/[\\s,]+/", $ll);
      }

      // else: empty map centered on visitor location, as set above.
    }
    else {
      uasort($locations, '_ip_geoloc_plugin_style_leaflet_compare');
    }
    $marker_dimensions = explode('x', $this->ipGeolocGlobal
      ->markerDimensions());
    $marker_width = (int) $marker_dimensions[0];
    $marker_height = (int) $marker_dimensions[1];
    $anchor_position = $this->config
      ->get('ip_geoloc_marker_anchor_pos') ? $this->config
      ->get('ip_geoloc_marker_anchor_pos') : 'bottom';
    switch ($anchor_position) {
      case 'top':
        $marker_anchor = 0;
        break;
      case 'middle':
        $marker_anchor = (int) (($marker_height + 1) / 2);
        break;
      default:
        $marker_anchor = $marker_height;
    }
    $features = [];
    foreach ($locations as $location) {
      $feature = [];
      if (isset($location->latitude) || isset($location->lat)) {
        $feature['type'] = 'point';
        $feature['lat'] = isset($location->latitude) ? $location->latitude : $location->lat;
        $feature['lon'] = isset($location->longitude) ? $location->longitude : $location->lon;
        if (!empty($location->random_displacement)) {
          $this->api
            ->addRandomDisplacement($feature, $location->random_displacement);
          $circle = [
            'type' => 'circle',
            'lat' => $feature['lat'],
            'lon' => $feature['lon'],
            'radius' => $location->random_displacement,
          ];
          $features[] = $circle;
        }
      }
      elseif (isset($location->component)) {

        // Possibly parsed by leaflet_process_geofield()
        // see _ip_geoloc_plugin_style_extract_lat_lng().
        $feature['type'] = $location->type;
        $feature['component'] = $location->component;
      }
      elseif (isset($location->points)) {
        $feature['type'] = $location->type;
        $feature['points'] = $location->points;
      }
      if (isset($location->id)) {

        // Allow marker events to identify the corresponding node.
        $feature['feature_id'] = $location->id;
      }

      // At this point $feature['type'] should be set.
      if (!empty($feature['type']) && $feature['type'] != 'point') {

        // Linestring, polygon ...
        $feature['flags'] = LEAFLET_MARKERCLUSTER_EXCLUDE_FROM_CLUSTER;
      }
      elseif (!isset($feature['lat'])) {

        // Points must have coords.
        continue;
      }
      if (!empty($sync_flags)) {
        $feature['flags'] = isset($feature['flags']) ? $feature['flags'] | $sync_flags : $sync_flags;
      }
      if ($enable_balloons && isset($location->balloon_text)) {
        $feature['popup'] = $location->balloon_text;
      }
      if (!empty($location->marker_special_char) || !empty($location->marker_special_char_class)) {
        $has_special_markers = TRUE;
        if (!empty($location->marker_special_char)) {
          $feature['specialChar'] = Xss::filterAdmin($location->marker_special_char);
        }
        if (!empty($location->marker_special_char_class)) {
          $feature['specialCharClass'] = Xss::filterAdmin($location->marker_special_char_class);
        }
      }
      elseif (!empty($this->options['default_marker']['default_marker_special_char']) || !empty($this->options['default_marker']['default_marker_special_char_class'])) {
        $has_special_markers = TRUE;
        $feature['specialChar'] = $this->options['default_marker']['default_marker_special_char'];
        $feature['specialCharClass'] = $this->options['default_marker']['default_marker_special_char_class'];
      }
      if (!empty($location->marker_tooltip)) {
        if ($this->moduleHandler
          ->moduleExists('leaflet_label')) {
          $feature['label'] = $location->marker_tooltip;
        }
        else {
          $has_special_markers = TRUE;
          $feature['tooltip'] = $location->marker_tooltip;
        }
      }
      if (!empty($location->regions)) {
        $has_special_markers = TRUE;

        // Make sure we start with 0 or regions will come across as Object
        // rather than array.
        $feature['regions'] = [
          0 => '',
        ] + $location->regions;
        if (isset($location->aggregation_value)) {
          $feature['aggregationValue'] = (double) $location->aggregation_value;
        }

        // Note: cannot use <br/>  or HTML in tooltip as separator. Use \n.
        $feature['tooltip'] = empty($feature['tooltip']) ? '' : $feature['tooltip'] . "\n";
        $second_last = count($feature['regions']) - 2;
        if ($second_last > 0) {
          $feature['tooltip'] .= $feature['regions'][$second_last] . ' - ';
        }
        $feature['tooltip'] .= end($feature['regions']);
      }
      if (!empty($location->marker_tag)) {
        $has_special_markers = TRUE;
        $feature['tag'] = $location->marker_tag;
      }
      if (!empty($tag_css_classes)) {
        $feature['cssClass'] = $tag_css_classes;
      }
      if (isset($location->marker_color) && $this
        ->isNoMarker($location->marker_color) || !isset($location->marker_color) && $this
        ->isNoMarker($marker_color)) {

        // "No marker" as opposed to "default" marker.
        $has_special_markers = TRUE;
        $feature['icon'] = FALSE;
      }
      elseif (!empty($location->marker_color) || !empty($marker_color)) {

        // Switch from default icon to specified color.
        $color = empty($location->marker_color) ? $marker_color : $location->marker_color;
        $feature['icon'] = [
          'iconUrl' => $marker_path . "/{$color}.png",
          'iconSize' => [
            'x' => $marker_width,
            'y' => $marker_height,
          ],
          'iconAnchor' => [
            'x' => (int) (($marker_width + 1) / 2),
            'y' => $marker_anchor,
          ],
          // Just above topline, center.
          'popupAnchor' => [
            'x' => 0,
            'y' => -$marker_height - 1,
          ],
        ];
      }
      $features[] = $feature;
    }
    if (isset($visitor_location['latitude'])) {
      if ($visitor_marker_color != 'none') {

        // See leaflet/README.txt for examples of Leaflet "features".
        $visitor_feature = [
          'type' => 'point',
          'lat' => $visitor_location['latitude'],
          'lon' => $visitor_location['longitude'],
          'specialChar' => Xss::filterAdmin($this->options['visitor_marker_leaflet']['visitor_marker_special_char']),
          'specialCharClass' => Xss::filterAdmin($this->options['visitor_marker_leaflet']['visitor_marker_special_char_class']),
          'popup' => !empty($visitor_location['popup']) ? $visitor_location['popup'] : $this
            ->t('Your approximate location'),
          'tooltip' => !empty($visitor_location['tooltip']) ? $visitor_location['tooltip'] : $this
            ->t('Your approximate location'),
          'zIndex' => 9999,
          // See leaflet_markercluster.drupal.js.
          'flags' => LEAFLET_MARKERCLUSTER_EXCLUDE_FROM_CLUSTER,
        ];
        if ($visitor_marker_color != '') {
          if (!empty($visitor_feature['specialChar']) || !empty($visitor_feature['specialCharClass'])) {
            $has_special_markers = TRUE;
          }
          $visitor_feature['icon'] = [
            'iconUrl' => $marker_path . "/{$visitor_marker_color}.png",
            'iconSize' => [
              'x' => $marker_width,
              'y' => $marker_height,
            ],
            'iconAnchor' => [
              'x' => (int) (($marker_width + 1) / 2),
              'y' => $marker_anchor,
            ],
            // Just above topline, center.
            'popupAnchor' => [
              'x' => 0,
              'y' => -$marker_height - 1,
            ],
          ];
        }
        $features[] = $visitor_feature;
      }
      if (!empty($this->options['visitor_marker_leaflet']['visitor_marker_accuracy_circle']) && !empty($visitor_location['accuracy'])) {
        $visitor_accuracy_circle = [
          'type' => 'circle',
          'lat' => $visitor_location['latitude'],
          'lon' => $visitor_location['longitude'],
          'radius' => (double) $visitor_location['accuracy'],
          'popup' => !empty($visitor_location['popup']) ? $visitor_location['popup'] : $this
            ->t("You are within @m meters of the centre of this circle.", [
            '@m' => $visitor_location['accuracy'],
          ]),
          // Requires Leaflet Label.
          'label' => !empty($visitor_location['tooltip']) ? $visitor_location['tooltip'] : $this
            ->t('You are within this circle'),
          'zIndex' => 9998,
          'flags' => LEAFLET_MARKERCLUSTER_EXCLUDE_FROM_CLUSTER,
        ];
        $features[] = $visitor_accuracy_circle;
      }
    }

    // If auto-box is chosen ($center_option==0), zoom only when there are
    // 0 or 1 markers [#1863374].
    if (!$use_specified_center && empty($center_option) && count($features) > 1) {
      unset($map['center']);
    }
    else {
      $map['settings']['zoom'] = $zoom;
      if (!empty($map['center'])) {

        // A leaflet.drupal.js quirk? Have to specify AND force a center...
        $map['center']['force'] = TRUE;
      }
    }
    $map['settings']['maxZoom'] = $max_zoom;
    $map['settings']['scrollWheelZoom'] = $scroll_wheel_zoom;
    $map['settings']['dragging'] = $dragging;
    $map['settings']['revertLastMarkerOnMapOut'] = (bool) ($sync_flags & LEAFLET_SYNC_REVERT_LAST_MARKER_ON_MAP_OUT);
    $map['settings']['maxClusterRadius'] = 0;
    if ($cluster_radius > 0) {
      if ($this->moduleHandler
        ->moduleExists('leaflet_markercluster')) {
        $map['settings']['maxClusterRadius'] = $cluster_radius;
        $map['settings']['disableClusteringAtZoom'] = $disable_clustering_at_zoom;
        $map['settings']['addRegionToolTips'] = $cluster_tooltips;
        $map['settings']['hullHugFactor'] = $hull_hug_factor;
        $map['settings']['touchMode'] = $cluster_touch_mode;
        $map['settings']['animateAddingMarkers'] = TRUE;
        if (!empty($cluster_aggregation_field)) {
          $map['settings']['clusterAggregationFunction'] = $cluster_aggregation_function;
          $map['settings']['clusterAggregateRanges'] = $cluster_aggregate_ranges;
          $map['settings']['clusterAggregatePrecision'] = $cluster_aggregate_precision;

          //@TODO check where is this library
          $output['#attached']['library'][] = 'leaflet/markercluster-aggregation';

          //drupal_add_css(leaflet_markercluster_get_library_path() . '/MarkerCluster.Aggregations.css');
        }
        if ($allow_clusters_of_one) {
          $map['settings']['allowClustersOfOne'] = TRUE;
          $map['settings']['spiderfyDistanceMultiplier'] = 4.0;
        }
      }
      else {
        $display = $this->view
          ->getDisplay();
        $display_name = $display
          ->getOption('title') . ' (' . $display->display['display_title'] . ')';
        $this->messenger
          ->addMessage($this
          ->t('Cannot cluster markers in View %display_name, as the module Leaflet MarkerCluster is not enabled.', [
          '%display_name' => $display_name,
        ]), 'warning');
      }
    }
    $zoom_ranges = array_filter($this->options['cluster_differentiator']['zoom_ranges']);
    if (!empty($zoom_ranges)) {

      // Make sure we start array with 0 and no missing elements. Otherwise this
      // array will arrive as an Object on the JS side.
      $region_levels = array_fill(0, $max_zoom + 1, 0);
      foreach ($zoom_ranges as $level => $zoom_range) {
        for ($zoom = 1; $zoom <= $max_zoom; $zoom++) {
          if ($this->ip_geoloc_global
            ->isInRange($zoom, $zoom_range)) {
            $region_levels[$zoom] = $level;
          }
        }
      }

      // Remove any gaps and zeroes.
      for ($z = 1; $z <= $max_zoom; $z++) {
        if (empty($region_levels[$z])) {
          $region_levels[$z] = $region_levels[$z - 1];
        }
      }
      $map['settings']['regionLevels'] = $region_levels;
    }

    // @TODO Check this code for displayid
    $map_id = 'ip-geoloc-map-of-view-' . $this->view->id . '-' . $this->view
      ->getDisplay()->display['display_id'] . '-' . md5(serialize($features));
    $output['#attached']['library'][] = 'leaflet/leaflet-drupal';

    // Don't load sync JS and CSS when option is not requested.
    if ($sync_flags !== 0) {
      $output['#attached']['library'][] = 'ip_geoloc/sync_content';
    }
    if ($has_full_screen) {

      // Load the 'leaflet-fullscreen' library, containing JS and CSS.
      // @TODO check libraries integration

      //@TODO check where is this library
      $output['#attached']['library'][] = 'ip_geoloc/leaflet-fullscreen';
      $map['settings']['fullscreenControl'] = [
        'position' => 'topright',
      ];
    }
    if ($has_mini_map) {

      // Load the 'leaflet-minimap' library, containing JS and CSS.
      // See https://github.com/Norkart/Leaflet-MiniMap for more settings.
      // @TODO add minimap library
      $output['#attached']['library'][] = 'ip_geoloc/leaflet-minimap';

      /*if (drupal_add_library('ip_geoloc', 'leaflet-minimap')) {*/
      $map['settings']['miniMap'] = [
        'autoToggleDisplay' => TRUE,
        'height' => $this->options['mini_map']['height'],
        'width' => $this->options['mini_map']['width'],
        'position' => 'bottomright',
        // 'bottomright'
        'toggleDisplay' => !empty($this->options['mini_map']['toggle']),
        'zoomAnimation' => FALSE,
        'zoomLevelOffset' => (int) $this->options['mini_map']['zoom_delta'],
        // Superimposed rectangle showing extent of main map on the inset.
        'aimingRectOptions' => [
          'color' => $this->options['mini_map']['scope_color'],
          'weight' => 3,
          'fillOpacity' => 0.1,
        ],
        // The "shadow" rectangle that shows the new map outline.
        'shadowRectOptions' => [
          'color' => '#888',
          'weight' => 1,
        ],
      ];

      /*}*/
    }
    $map['settings']['zoomIndicator'] = $zoom_indicator;
    $map['settings']['zoomOnClick'] = $zoom_on_click;
    $map['settings']['resetControl'] = $reset_control;
    $map['settings']['clusterControl'] = $cluster_control;
    $map['settings']['scaleControl'] = $scale_control;
    if ($has_mini_map || $zoom_indicator || $reset_control || $cluster_control || $scale_control) {
      $output['#attached']['library'][] = 'ip_geoloc/leaflet_controls';

      /*drupal_add_js($module_path . '/js/ip_geoloc_leaflet_controls.js', array('weight' => 1));
        drupal_add_css($module_path . '/css/ip_geoloc_leaflet_controls.css');*/
    }
    $map['settings']['openBalloonsOnHover'] = $open_balloons_on_hover;
    $map['settings']['gotoContentOnClick'] = $goto_content_on_click;
    if ($open_balloons_on_hover || $goto_content_on_click) {
      $output['#attached']['library'][] = 'ip_geoloc/content_on_click';
    }
    $settings = [
      'mapid' => $map_id,
      'map' => $map,
      'features' => $features,
    ];
    $output['#attached']['drupalSettings']['leaflet'] = array(
      $settings,
    );

    // @TODO check how to send variales to the libray
    // drupal_add_js(array('leaflet' => array($settings)), $this->options);
    $output['#attached']['library'][] = 'leaflet/leaflet';

    // @TODO not sure if this is migrated
    // libraries_load('leaflet');
    // Little hacky this, but can't see another way to load libraries for
    // Leaflet More Maps, Leaflet MarkerCluster, Leaflet Hash...
    // drupal_alter('leaflet_map_prebuild', $settings);
    if ($reset_control || $cluster_control || !empty($has_special_markers)) {

      // Load the CSS that comes with the font icon library which in return
      // tells the browser to fetch either the WOFF, TTF or SVG files that
      // define the font faces.
      $output['#attached']['library'][] = 'ip_geoloc/ip_geoloc_font_icon_libs';
      $variables['#attached']['library'][] = 'ip_geoloc/leaflet_markers';
    }
    if ($zoom_on_click) {
      $variables['#attached']['library'][] = 'ip_geoloc/leaflet_zoom_on_click';

      // drupal_add_js($module_path . '/js/ip_geoloc_leaflet_zoom_on_click.js', array('scope' => 'footer'));.
    }

    // drupal_alter('leaflet_build_map', $build); // @todo [#2567391].
    $marker_set = $this->ipGeolocGlobal
      ->markerDirectory();
    $marker_set = Unicode::substr($marker_set, strrpos($marker_set, '/') + 1);
    $height = trim($this->options['map_height']) ? trim($this->options['map_height']) : '300';
    $height = empty($height) ? '300px' : (is_numeric($height) ? $height . 'px' : trim($height));
    $style = Unicode::substr($height, 0, 6) == '<none>' ? '' : ' style="height:' . SafeMarkup::checkPlain($height) . '"';
    $output = $output + [
      '#theme' => 'ip_geoloc_leaflet',
      '#map_id' => $map_id,
      //'#view' => $this->view,
      '#marker_set' => $marker_set,
      '#style' => $style,
    ];
    $this->ipGeolocGlobal
      ->debug($this
      ->t('-- Leaflet map preparation time: %sec s', [
      '%sec' => number_format(microtime(TRUE) - $render_start, 2),
    ]));
    return \Drupal::service('renderer')
      ->render($output);

    //return $output;
  }

}