Clear map has a maximum zoom level of 5. Some pins may appear
overlapped even at maximum zoom.'),
+ ],
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[tile_options][map_type]"]' => ['value' => 'clear_map'],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Sets tile options for the form.
+ *
+ * @param array $form
+ * The form.
+ * @param array $geoFieldSources
+ * The view's geofields.
+ * @param array $iso3FieldSources
+ * The view's string fields.
+ */
+ protected function setRenderingOptionsCheckbox(array &$form, array $geoFieldSources, array $iso3FieldSources) {
+ $form['rendering_options']['render_items'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Data visualization'),
+ '#description' => $this->t('Select how to display data on the map.'),
+ '#options' => [
+ 'pin' => $this->t('Pins (needs latitude/longitude data source)'),
+ 'area' => $this->t('Highlight area (needs geofield polygon data source)'),
+ 'country' => $this->t('Highlight countries (needs ISO3 code data source)'),
+ ],
+ '#default_value' => $this->options['rendering_options']['render_items'],
+ ];
+
+ if (!count($geoFieldSources)) {
+ $form['rendering_options']['geofield_warning'] = [
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ '#value' => $this->t('You haven not added any GeoField, to render pins or areas add one to fields and come back here to set it as Data Source.'),
+ '#attributes' => [
+ 'class' => ['warning'],
+ ],
+ ];
+ }
+
+ if (!count($iso3FieldSources)) {
+ $form['rendering_options']['string_warning'] = [
+ '#type' => 'html_tag',
+ '#tag' => 'div',
+ '#value' => $this->t('You haven not added any plain Text fields, to render colored countries add one to fields and come back here to set it as Data Source.'),
+ '#attributes' => [
+ 'class' => ['warning'],
+ ],
+ ];
+ }
+ }
+
+ /**
+ * Sets tile options for the form.
+ *
+ * @param array $form
+ * The form.
+ * @param array $geoFieldSources
+ * The view's geofields.
+ * @param array $iso3FieldSources
+ * The view's string fields.
+ */
+ protected function setRenderingOptions(array &$form, array $geoFieldSources, array $iso3FieldSources) {
+ $form['rendering_options']['pins_source'] = [
+ '#title' => $this->t('Data Source for Pins'),
+ '#type' => 'select',
+ '#description' => $this->t('Which geofield do you want to use to draw pins?'),
+ '#options' => $geoFieldSources,
+ '#default_value' => $this->options['rendering_options']['pins_source'] ?? NULL,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][pin]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="style_options[rendering_options][render_items][pin]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['rendering_options']['area_source'] = [
+ '#title' => $this->t('Data Source for Area'),
+ '#type' => 'select',
+ '#description' => $this->t('Which geofield do you want to use to draw areas?'),
+ '#options' => $geoFieldSources,
+ '#default_value' => $this->options['rendering_options']['area_source'] ?? NULL,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][area]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="style_options[rendering_options][render_items][area]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['rendering_options']['country_source'] = [
+ '#title' => $this->t('Data Source for Country'),
+ '#type' => 'select',
+ '#description' => $this->t('Which string field (ISO3) do you want to use to draw countries?'),
+ '#options' => $iso3FieldSources,
+ '#default_value' => $this->options['rendering_options']['country_source'] ?? NULL,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['rendering_options']['area_color'] = [
+ '#title' => $this->t('Color to highlight areas (HEX)'),
+ '#type' => 'textfield',
+ '#default_value' => $this->options['rendering_options']['area_color'] ?? NULL,
+ '#field_prefix' => '#',
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][area]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="style_options[rendering_options][render_items][area]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['rendering_options']['country_color'] = [
+ '#title' => $this->t('Color to highlight countries (HEX)'),
+ '#type' => 'textfield',
+ '#default_value' => $this->options['rendering_options']['country_color'] ?? NULL,
+ '#field_prefix' => '#',
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ 'required' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['rendering_options']['area_hover_color'] = [
+ '#title' => $this->t('Hover color to highlight areas (HEX)'),
+ '#description' => $this->t('If not set a lighter shade of main color will be used.'),
+ '#type' => 'textfield',
+ '#default_value' => $this->options['rendering_options']['area_hover_color'] ?? NULL,
+ '#field_prefix' => '#',
+ '#required' => FALSE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][area]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['rendering_options']['country_hover_color'] = [
+ '#title' => $this->t('Hover Color to highlight countries (HEX)'),
+ '#description' => $this->t('If not set a lighter shade of main color will be used.'),
+ '#type' => 'textfield',
+ '#default_value' => $this->options['rendering_options']['country_hover_color'] ?? NULL,
+ '#field_prefix' => '#',
+ '#required' => FALSE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Sets tile options for the form.
+ *
+ * @param array $form
+ * The form.
+ * @param array $allFields
+ * View's fields.
+ */
+ protected function setPopupOptions(array &$form, array $allFields) {
+ $form['popup_options']['pin_popup_source'] = [
+ '#title' => $this->t('Data Source for pins Pop-ups'),
+ '#type' => 'select',
+ '#description' => $this->t('Which field you want to be rendered in popup at pin click ?'),
+ '#options' => $allFields,
+ '#default_value' => $this->options['popup_options']['pin_popup_source'] ?? NULL,
+ '#empty_option' => t('- None -'),
+ '#empty_value' => '_none',
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][pin]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['popup_options']['area_popup_source'] = [
+ '#title' => $this->t('Data Source for area Pop-ups'),
+ '#type' => 'select',
+ '#description' => $this->t('Which field you want to be rendered in popup at area click ?'),
+ '#options' => $allFields,
+ '#default_value' => $this->options['popup_options']['area_popup_source'] ?? NULL,
+ '#empty_option' => t('- None -'),
+ '#empty_value' => '_none',
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['popup_options']['country_popup_source'] = [
+ '#title' => $this->t('Data Source for country Pop-ups'),
+ '#type' => 'select',
+ '#description' => $this->t('Which field you want to be rendered in popup when clicking on a country ?'),
+ '#options' => $allFields,
+ '#default_value' => $this->options['popup_options']['country_popup_source'] ?? NULL,
+ '#empty_option' => t('- None -'),
+ '#empty_value' => '_none',
+ ];
+ }
+
+ /**
+ * Sets target link options at click on a country.
+ *
+ * @param array $form
+ * The form.
+ * @param array $fields
+ * The link fields available.
+ */
+ protected function setContryClickOptions(&$form, array $fields) {
+ $form['country_click'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Country target links at click'),
+ '#description' => 'Here you can choose to open a new tab when clicking on a country. This only works well if popups are not displayed or are displayed at hover.',
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][country]"]' => ['checked' => TRUE],
+ ],
+ ],
+ 'country_click_source' => [
+ '#title' => $this->t('Data Source for country redirects'),
+ '#type' => 'select',
+ '#description' => $this->t('Which link field you want to use as target when clicking on a country ?'),
+ '#options' => $fields,
+ '#default_value' => $this->options['country_click']['country_click_source'] ?? NULL,
+ '#empty_option' => t('- None -'),
+ '#empty_value' => '_none',
+ ],
+ ];
+ }
+
+ /**
+ * Sets tile options for the form.
+ *
+ * @param array $form
+ * The form.
+ */
+ protected function setDisplayOptions(&$form) {
+ $form['display_options']['projection'] = [
+ '#title' => $this->t('Projection'),
+ '#description' => $this->t('Sets which projection a map is rendered in.'),
+ '#type' => 'select',
+ '#options' => [
+ 'mercator' => $this->t('Mercator'),
+ 'globe' => $this->t('Globe'),
+ ],
+ '#default_value' => $this->options['display_options']['projection'] ?? 'mercator',
+ '#required' => TRUE,
+ ];
+
+ $form['display_options']['zoom'] = [
+ '#title' => $this->t('Default zoom'),
+ '#type' => 'number',
+ '#min' => 0,
+ '#step' => 0.1,
+ '#default_value' => $this->options['display_options']['zoom'] ?? 1.5,
+ '#required' => TRUE,
+ ];
+
+ $form['display_options']['max_zoom'] = [
+ '#title' => $this->t('Maximum zoom'),
+ '#type' => 'number',
+ '#min' => 0,
+ '#max' => 22,
+ '#step' => 1,
+ '#default_value' => $this->options['display_options']['max_zoom'] ?? 22,
+ '#required' => FALSE,
+ '#description' => $this->t('
Do not change this unless you really need it! It also affects the maximum level of pin clustering, so you can see overlapping pins.
+If Clear Map is use the max zoom level will be
5 unless you select a lower value.'),
+ ];
+
+ $form['display_options']['pitch'] = [
+ '#title' => $this->t('Pitch'),
+ '#description' => $this->t('Angle towards the horizon'),
+ '#type' => 'number',
+ '#min' => 0,
+ '#step' => 0.1,
+ '#default_value' => $this->options['display_options']['pitch'] ?? 1.5,
+ '#required' => TRUE,
+ ];
+
+ $form['display_options']['center'] = [
+ '#type' => 'fieldset',
+ '#collapsible' => FALSE,
+ '#title' => $this->t('Default map center'),
+ '#description' => $this->t('The reference longitude and latitude of the projection between -180-90 and 18090 inclusive.'),
+ 'long' => [
+ '#title' => $this->t('Longitude'),
+ '#type' => 'textfield',
+ '#default_value' => $this->options['display_options']['center']['long'] ?? 0,
+ '#required' => TRUE,
+ ],
+ 'lat' => [
+ '#title' => $this->t('Latitude'),
+ '#type' => 'textfield',
+ '#default_value' => $this->options['display_options']['center']['lat'] ?? 0,
+ '#required' => TRUE,
+ ],
+ ];
+
+ $form['display_options']['disable_scroll_zoom'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Disable scroll zoom'),
+ '#default_value' => $this->options['display_options']['disable_scroll_zoom'] ?? 0,
+ ];
+
+ $form['display_options']['world_copies'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Render world copies'),
+ '#default_value' => $this->options['display_options']['world_copies'] ?? 0,
+ ];
+
+ $form['display_options']['clusters'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enable pin clustering'),
+ '#default_value' => $this->options['display_options']['clusters'] ?? 1,
+ '#states' => [
+ 'visible' => [
+ ':input[name="style_options[rendering_options][render_items][pin]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ $form['display_options']['hover_popups'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Show popups at hover'),
+ '#description' => $this->t('If selected, popups will be shown at hover instead of click'),
+ '#default_value' => $this->options['display_options']['hover_popups'] ?? 0,
+ ];
+ }
+
+}
diff --git a/modules/edw_maps/src/Form/EdwMapsSettingsForm.php b/modules/edw_maps/src/Form/EdwMapsSettingsForm.php
new file mode 100644
index 0000000..33c76d2
--- /dev/null
+++ b/modules/edw_maps/src/Form/EdwMapsSettingsForm.php
@@ -0,0 +1,83 @@
+configFactory->getEditable('edw_maps.settings');
+ $form['token'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Mapbox Token'),
+ '#default_value' => $config->get('token'),
+ '#description' => $this->t("The mapbox account token. If you don't have an account, you can create one
here"),
+ '#required' => TRUE,
+ '#config' => [
+ 'key' => 'edw_maps.settings:token',
+ ],
+ ];
+
+ $form['default_style_url'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Default Mapbox Style URL'),
+ '#default_value' => $config->get('default_style_url'),
+ '#description' => $this->t("If set this will be used for all maps that do not specify a style"),
+ '#required' => FALSE,
+ '#config' => [
+ 'key' => 'edw_maps.settings:default_style_url',
+ ],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $formState) {
+ $token = $formState->getValue('token');
+ if (!isset($token) || trim($token) == '') {
+ $formState->setErrorByName('token', $this->t('Please enter a valid Mapbox Token.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $formState) {
+ $config = $this->configFactory->getEditable('edw_maps.settings');
+ $values = $formState->getValues();
+ $config->set('token', $values['token']);
+ $config->set('default_style_url', $values['default_style_url']);
+ $config->save();
+
+ $this->messenger()
+ ->addMessage($this->t('EDW Mapbox settings have been saved.'));
+ }
+
+}
diff --git a/modules/edw_maps/src/Plugin/views/style/MapboxMapStyle.php b/modules/edw_maps/src/Plugin/views/style/MapboxMapStyle.php
new file mode 100644
index 0000000..c50bb46
--- /dev/null
+++ b/modules/edw_maps/src/Plugin/views/style/MapboxMapStyle.php
@@ -0,0 +1,375 @@
+ ['country_color', 'country_hover_color'],
+ 'area' => ['area_color', 'area_hover_color'],
+ ];
+
+ /**
+ * Plugin constructor.
+ */
+ public function __construct(
+ array $configuration,
+ $plugin_id,
+ $plugin_definition,
+ ConfigFactory $configFactory,
+ EntityFieldManagerInterface $fieldTypeManager,
+ FormBuilder $formBuilder,
+ EdwMapsDataService $edwMapsDataService
+ ) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->configFactory = $configFactory;
+ $this->fieldTypeManager = $fieldTypeManager;
+ $this->formBuilder = $formBuilder;
+ $this->edwMapsDataService = $edwMapsDataService;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->get('config.factory'),
+ $container->get('entity_field.manager'),
+ $container->get('form_builder'),
+ $container->get('edw_maps.utils'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function evenEmpty() {
+ // Render map even if there is no data.
+ return TRUE;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function validateOptionsForm(&$form, FormStateInterface $formState) {
+ parent::validateOptionsForm($form, $formState);
+ $this->validateFields($formState);
+
+ $config = $this->configFactory->get('edw_maps.settings');
+ $values = $formState->getValues()['style_options'];
+ $mapType = $values['tile_options']['map_type'];
+
+ if ($config->get('token') == NULL) {
+ $formState->setErrorByName('', $this->t('No Mapbox TOKEN has been configured. Please visit modules settings and add one.'));
+ return;
+ }
+
+ if ($mapType == 'custom' && $config->get('default_style_url') == NULL &&
+ empty($values['tile_options']['style_url'])) {
+ $formState->setErrorByName('style_options][tile_options][style_url', $this->t('Style URL is required for custom map type.'));
+ }
+
+ if ($mapType == 'carto_tile' && ($values['rendering_options']['render_items']['pin']
+ || $values['rendering_options']['render_items']['area'])) {
+ $formState->setErrorByName('style_options][rendering_options][render_items', $this->t('Carto tile supports only country highlight.'));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $formState) {
+ parent::buildOptionsForm($form, $formState);
+ $defaultMap = 'mapbox://styles/mapbox/streets-v11';
+ $config = $this->configFactory->get('edw_maps.settings');
+ if ($config !== NULL) {
+ $defaultMap = $config->get('default_style_url');
+ }
+
+ $geofieldSources = $this->getAvailableDataSources('geofield');
+ $iso3FieldSources = $this->getAvailableDataSources('string');
+ $linkSources = $this->getAvailableDataSources('link');
+
+ $allFields = $this->getAvailableDataSources('all');
+
+ $this->setFieldsets($form);
+ $this->setTileOptions($form, $defaultMap);
+
+ $this->setRenderingOptionsCheckbox($form, $geofieldSources, $iso3FieldSources);
+ $this->setRenderingOptions($form, $geofieldSources, $iso3FieldSources);
+ $this->setPopupOptions($form, $allFields);
+ $this->setContryClickOptions($form, $linkSources);
+ $this->setDisplayOptions($form);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function render() {
+ $variables = parent::render();
+ if (!isset($variables[0]['#view'])) {
+ return [];
+ }
+ /** @var \Drupal\views\ViewExecutable $view */
+ $view = $variables[0]['#view'];
+ $options = $view->style_plugin->options;
+ $containerId = 'map-container-' . $view->id() . '-' . $view->current_display;
+ $config = $this->configFactory->get('edw_maps.settings');
+ $renderPins = (boolean) $options['rendering_options']['render_items']['pin'];
+ $renderCountries = (boolean) $options['rendering_options']['render_items']['country'];
+ $renderAreas = (boolean) $options['rendering_options']['render_items']['area'];
+ $pinsSourceField = $options['rendering_options']['pins_source'];
+ $countrySourceField = $options['rendering_options']['country_source'];
+ $areaSourceField = $options['rendering_options']['area_source'];
+ $popupPinSource = $options['popup_options']['pin_popup_source'];
+ $popupCountrySource = $options['popup_options']['country_popup_source'];
+ $popupAreaSourceField = $options['popup_options']['area_popup_source'];
+ $countryClickSource = $options['country_click']['country_click_source'];
+ $mapType = $options['tile_options']['map_type'];
+
+ $pinData = [];
+ $countryData = [];
+ $areaData = [];
+
+ if ($renderPins) {
+ $pinData = $this->edwMapsDataService->getPinData($view, $pinsSourceField, $popupPinSource);
+ }
+ if ($renderCountries) {
+ $countryData = $this->edwMapsDataService->getCountryData($view, $countrySourceField, $popupCountrySource, $countryClickSource);
+ }
+ if ($renderAreas) {
+ $areaData = $this->edwMapsDataService->getAreaData($view, $areaSourceField, $popupAreaSourceField);
+ }
+
+ $maxZoom = isset($options['display_options']['max_zoom']) ? (int) $options['display_options']['max_zoom'] : 22;
+ if ($options['tile_options']['map_type'] == 'clear_map' && $maxZoom > 5) {
+ $maxZoom = 5;
+ }
+
+ $settings = [
+ 'containerId' => $containerId,
+ 'mapboxToken' => $config->get('token'),
+ 'mapType' => $mapType,
+ 'mapboxStyleUrl' => empty($options['tile_options']['style_url']) ? $config->get('default_style_url') : $options['tile_options']['style_url'],
+ 'projection' => $options['display_options']['projection'],
+ 'center' => [
+ (float) $options['display_options']['center']['long'],
+ (float) $options['display_options']['center']['lat'],
+ ],
+ 'pitch' => (float) $options['display_options']['pitch'],
+ 'zoom' => (float) $options['display_options']['zoom'],
+ 'maxZoom' => $maxZoom,
+ 'disableScrollZoom' => (boolean) $options['display_options']['disable_scroll_zoom'],
+ 'worldCopies' => (boolean) $options['display_options']['world_copies'],
+ 'renderClusters' => (boolean) $options['display_options']['clusters'],
+ 'hoverPopups' => (boolean) $options['display_options']['hover_popups'],
+ 'renderPins' => $renderPins,
+ 'renderCountries' => $renderCountries,
+ 'renderAreas' => $renderAreas,
+ 'countryColor' => $options['rendering_options']['country_color'],
+ 'countryHoverColor' => $options['rendering_options']['country_hover_color'],
+ 'areaColor' => $options['rendering_options']['area_color'],
+ 'areaHoverColor' => $options['rendering_options']['area_hover_color'],
+ 'pinData' => $pinData,
+ 'countryData' => $countryData,
+ 'areaData' => $areaData,
+ 'clearMapSource' => $mapType == 'clear_map' ? $this->edwMapsDataService->getClearMapSource() : NULL,
+ 'countryLinks' => !empty($countryClickSource) && $countryClickSource != '_none',
+ ];
+
+ return [
+ '#theme' => 'views_view_mapbox_map',
+ '#mapContainerId' => $containerId,
+ '#exposedFilters' => !empty($view->exposed_data),
+ '#attached' => [
+ 'library' => ['edw_maps/edw_map'],
+ 'drupalSettings' => [
+ 'edw_map' => $settings,
+ ],
+ ],
+ '#cache' => [
+ 'tags' => ['config:edw_maps.settings'],
+ ],
+ ];
+ }
+
+ /**
+ * Get a list of fields and a sublist of geo data fields in this view.
+ *
+ * @param string $type
+ * The field type to look for in views fields.
+ *
+ * @return array
+ * Available data sources.
+ */
+ protected function getAvailableDataSources(string $type) {
+ $availableFields = [];
+ if ($type == 'link') {
+ $availableFields['default'] = $this->t('Link to default entity');
+ }
+ /** @var \Drupal\views\Plugin\views\ViewsHandlerInterface $handler */
+ $fieldHandlers = $this->displayHandler->getHandlers('field');
+ foreach ($fieldHandlers as $fieldId => $handler) {
+ $label = $handler->adminLabel() ?: $fieldId;
+ if ($type == 'all') {
+ $availableFields[$fieldId] = $label;
+ continue;
+ }
+
+ $entityType = $handler->getEntityType();
+ $allDefinitions = $this->fieldTypeManager->getFieldStorageDefinitions($entityType);
+ if (isset($allDefinitions[$fieldId])) {
+ $fieldType = $allDefinitions[$fieldId]->getType();
+ if ($type == $fieldType) {
+ $availableFields[$fieldId] = $label;
+ }
+ }
+ }
+
+ return $availableFields;
+ }
+
+ /**
+ * Validates if a string is a correct latitude value.
+ *
+ * @param string $latitude
+ * The string to be verified upon.
+ *
+ * @return bool
+ * If it is valid or not.
+ */
+ protected function isValidLatitude($latitude) {
+ if (!is_numeric($latitude)) {
+ return FALSE;
+ }
+ $latitude = floatval($latitude);
+ return ($latitude >= -90.0 && $latitude <= 90.0);
+ }
+
+ /**
+ * Validates if a string is a correct longitude value.
+ *
+ * @param string $longitude
+ * The string to be verified upon.
+ *
+ * @return bool
+ * If it is valid or not.
+ */
+ protected function isValidLongitude($longitude) {
+ if (!is_numeric($longitude)) {
+ return FALSE;
+ }
+ $longitude = floatval($longitude);
+ return ($longitude >= -90.0 && $longitude <= 90.0);
+ }
+
+ /**
+ * Validates if a string is a correct HEX color value.
+ *
+ * @param string $color
+ * The string to be verified upon.
+ *
+ * @return bool
+ * If it is valid or not.
+ */
+ protected function isValidHexColor($color) {
+ $pattern = '/^[A-Fa-f0-9]{6}$/';
+ return preg_match($pattern, $color) === 1;
+ }
+
+ /**
+ * Validates basic fields for options form.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $formState
+ * The form state.
+ */
+ protected function validateFields(FormStateInterface $formState) {
+ $values = $formState->getValues()['style_options'];
+ if (!$this->isValidLatitude($values['display_options']['center']['lat'])) {
+ $formState->setErrorByName('style_options][display_options][center][lat', $this->t('Latitude value is wrong.'));
+ }
+
+ if (!$this->isValidLongitude($values['display_options']['center']['long'])) {
+ $formState->setErrorByName('style_options][display_options][center][long', $this->t('Longitude value is wrong.'));
+ }
+
+ foreach (self::COLORS as $category => $fields) {
+ foreach ($fields as $field) {
+ $value = $values['rendering_options'][$field];
+ if (!empty($value) && !$this->isValidHexColor($value)) {
+ $formState->setErrorByName("style_options][rendering_options][$field",
+ $this->t('Invalid HEX color for @category.', ['@category' => $category]));
+ }
+ }
+ }
+ }
+
+}
diff --git a/modules/edw_maps/src/Services/EdwMapsDataService.php b/modules/edw_maps/src/Services/EdwMapsDataService.php
new file mode 100644
index 0000000..c925aa1
--- /dev/null
+++ b/modules/edw_maps/src/Services/EdwMapsDataService.php
@@ -0,0 +1,316 @@
+entityTypeManager = $entityTypeManager;
+ $this->renderer = $renderer;
+ $this->moduleHandler = $moduleHandler;
+ }
+
+ /**
+ * Get data objects for rendering markers on mapbox.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view.
+ * @param string $dataSource
+ * The data source field id.
+ * @param string $popupSource
+ * The popup source field id.
+ *
+ * @return array
+ * The formatted pin data.
+ */
+ public function getPinData(ViewExecutable $view, string $dataSource, string $popupSource) {
+ $rows = $view->result;
+ $data = [];
+ foreach ($rows as $row) {
+ $entity = $this->getEntity($row, $dataSource);
+ $coordinates = $entity->get($dataSource)->getValue();
+ $coordinates = reset($coordinates);
+ if (empty($coordinates) || $coordinates['geo_type'] !== 'Point') {
+ continue;
+ }
+ // Mapbox uses lng - lat format.
+ $pinCoordinates = [$coordinates['lon'], $coordinates['lat']];
+ $data[] = [
+ 'coordinates' => $pinCoordinates,
+ 'popup' => $this->getPopupContent($view, $row, $popupSource, 'pin'),
+ ];
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get data objects for highlighting country polygons on mapbox.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view.
+ * @param string $dataSource
+ * The data source field id.
+ * @param string $popupSource
+ * The popup source field id.
+ *
+ * @return array
+ * The formatted country data.
+ */
+ public function getCountryData(ViewExecutable $view, string $dataSource, string $popupSource, string $linkSource) {
+ $rows = $view->result;
+ $data = [];
+ foreach ($rows as $row) {
+ $entity = $this->getEntity($row, $dataSource);
+ $iso3 = $entity->get($dataSource)->value;
+ if (empty($iso3)) {
+ continue;
+ }
+ $data[] = [
+ 'iso3' => $entity->get($dataSource)->value,
+ 'popup' => $this->getPopupContent($view, $row, $popupSource, 'country'),
+ 'link' => $this->getRedirectLink($row, $linkSource),
+ ];
+ }
+
+ return $data;
+ }
+
+ /**
+ * Get data objects for rendering area polygons on mapbox.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view.
+ * @param string $dataSource
+ * The data source field id.
+ * @param string $popupSource
+ * The popup source field id.
+ *
+ * @return array
+ * The formatted area data.
+ *
+ * @SuppressWarnings(PHPMD.StaticAccess)
+ */
+ public function getAreaData(ViewExecutable $view, string $dataSource, string $popupSource) {
+ $rows = $view->result;
+ $data = [
+ 'type' => 'FeatureCollection',
+ 'features' => [],
+ ];
+ foreach ($rows as $row) {
+ $entity = $this->getEntity($row, $dataSource);
+ $coordinates = $entity->get($dataSource)->getValue();
+ $coordinates = reset($coordinates);
+ if (empty($coordinates) || $coordinates['geo_type'] !== 'Polygon') {
+ continue;
+ }
+ $wktPolygon = $entity->get($dataSource)->value;
+ try {
+ $geometry = GeoPHP::load($wktPolygon, 'wkt');
+ // Create a GeoJSON feature.
+ $geoJsonGeometry = json_decode($geometry->out('json'), TRUE);
+
+ // Create the GeoJSON feature structure.
+ $geoJsonFeature = [
+ 'type' => 'Feature',
+ 'geometry' => $geoJsonGeometry,
+ 'properties' => [
+ 'id' => $entity->id(),
+ 'popup' => $this->getPopupContent($view, $row, $popupSource, 'area'),
+ ],
+ ];
+
+ $data['features'][] = $geoJsonFeature;
+ }
+ catch (\exception) {
+ continue;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * Gets url for GeoJson boundaries for clear map.
+ *
+ * @return \Drupal\Core\GeneratedUrl|string
+ * The absolute url for the GeoJson.
+ */
+ public function getClearMapSource() {
+ $path = $this->moduleHandler->getModule('edw_maps')
+ ->getPath() . '/assets/country_boundaries/country_polygon.geojson';
+ if (!file_exists($path)) {
+ $this->unzipGeoJson();
+ }
+ return Url::fromUserInput("/$path", ['absolute' => TRUE])
+ ->toString();
+ }
+
+ /**
+ * Unzips geoJson file with country borders for clear map.
+ */
+ public function unzipGeoJson() {
+ $path = $this->moduleHandler->getModule('edw_maps')
+ ->getPath() . '/assets/country_boundaries';
+ try {
+ $zip = new \ZipArchive();
+ $res = $zip->open("$path/country_polygon.zip");
+ if ($res === TRUE) {
+ $zip->extractTo($path);
+ $zip->close();
+ }
+ } catch (Exception $e) {
+ $this->messenger()->addWarning('Could not extract geoJson file. Error message ' . $e->getMessage());
+
+ }
+ }
+
+ /**
+ * Renders popup content after calling altering hooks.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view.
+ * @param \Drupal\views\ResultRow $row
+ * The current row.
+ * @param string $popupSource
+ * The field id of the popup source.
+ * @param string $renderItem
+ * The name of the render item. One of pin, area or country,.
+ *
+ * @return callable|\Drupal\Component\Render\MarkupInterface|mixed|void|null
+ * The rendered item or null.
+ */
+ protected function getPopupContent(ViewExecutable $view, ResultRow $row, string $popupSource, string $renderItem) {
+ if (empty($popupSource) || $popupSource == '_none') {
+ return NULL;
+ }
+ $renderValue = $view->field[$popupSource]->render($row);
+ if (!is_array($renderValue)) {
+ try {
+ return $view->field[$popupSource]->advancedRender($row);
+ }
+ catch (\LogicException) {
+ return $view->field[$popupSource]->render($row)
+ ->__toString();
+ }
+ }
+
+ $this->moduleHandler->invokeAll('edw_maps_' . $renderItem . '_tooltip_data_alter', [&$renderValue]);
+ return $this->renderer->renderPlain($renderValue);
+ }
+
+ /**
+ * Gets entity with the field id form view or relationships.
+ *
+ * @param \Drupal\views\ResultRow $row
+ * The view's row.
+ * @param string $fieldId
+ * The field id.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * The entity.
+ */
+ protected function getEntity(ResultRow $row, string $fieldId) {
+ $entity = $row->_entity;
+ $relationShipEntities = $row->_relationship_entities;
+ if ($entity->hasField($fieldId)) {
+ return $entity;
+ }
+
+ foreach ($relationShipEntities as $relationShipEntity) {
+ if ($relationShipEntity->hasField($fieldId)) {
+ return $relationShipEntity;
+ }
+ }
+
+ return $entity;
+ }
+
+ /**
+ * Gets link to redirect to when clicking on a territory.
+ *
+ * @param \Drupal\views\ResultRow $row
+ * The current row.
+ * @param string $linkSource
+ * The link's source.
+ *
+ * @return string|null
+ * The url if it exists, otherwise null.
+ *
+ * @throws \Drupal\Core\Entity\EntityMalformedException
+ */
+ protected function getRedirectLink(ResultRow $row, string $linkSource) {
+ if (empty($linkSource) || $linkSource == '_none') {
+ return NULL;
+ }
+
+ if ($linkSource == 'default') {
+ $entity = $row->_entity;
+ if (empty($entity)) {
+ return NULL;
+ }
+ return $entity->toUrl('canonical', ['absolute' => TRUE])->toString();
+ }
+
+ $entity = $this->getEntity($row, $linkSource);
+ $link = $entity->get($linkSource)->getValue();
+ $link = reset($link);
+ if (!empty($link)) {
+ // Convert internal links to absolute links.
+ $url = Url::fromUri($link['uri']);
+ if ($url->isRouted()) {
+ return $url->setAbsolute()->toString();
+ }
+ return $link['uri'];
+ }
+
+ return NULL;
+ }
+
+}
diff --git a/modules/edw_maps/templates/views-view-mapbox-map.html.twig b/modules/edw_maps/templates/views-view-mapbox-map.html.twig
new file mode 100644
index 0000000..7d586eb
--- /dev/null
+++ b/modules/edw_maps/templates/views-view-mapbox-map.html.twig
@@ -0,0 +1,8 @@
+{% if exposedFilters %}
+
+
+{% endif %}
+
+
+