You are here

openlayers_geofield.module in Openlayers 7.3

Openlayers Geofield integration.

File

modules/openlayers_geofield/openlayers_geofield.module
View source
<?php

/**
 * @file
 * Openlayers Geofield integration.
 */

// Always include the widget code of geofield. This re-uses major parts of it.
module_load_include('inc', 'geofield', 'geofield.widgets.openlayers');

/**
 * Implements hook_ctools_plugin_api().
 */
function openlayers_geofield_ctools_plugin_api($module, $api) {
  $path = drupal_get_path('module', 'openlayers_geofield') . '/includes';
  return array(
    'version' => 1,
    'path' => $path,
  );
}

/**
 * Implements hook_field_formatter_info().
 */
function openlayers_geofield_field_formatter_info() {
  $formatters = array();
  $formatters['openlayers_geofield'] = array(
    'label' => t('Openlayers'),
    'field types' => array(
      'geofield',
    ),
    'settings' => array(
      'data' => 'full',
      'map_layer_preset' => 'openlayers_geofield_map_geofield_formatter:openlayers_geofield_layer_formatter',
    ),
  );
  return $formatters;
}

/**
 * Implements hook_field_formatter_info_alter().
 */
function openlayers_geofield_field_formatter_info_alter(&$info) {

  // Overwrite the openlayers integration provided by the geofield module.
  unset($info['geofield_openlayers']);
}

/**
 * Implements hook_field_formatter_settings_form().
 *
 * Heavily borrowed from geofield_field_formatter_settings_form().
 *
 * @see geofield_field_formatter_settings_form()
 */
function openlayers_geofield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $options = array();
  foreach (\Drupal\openlayers\Openlayers::loadAll('map') as $map) {
    foreach ($map
      ->getCollection()
      ->getObjects('layer') as $layer) {
      $key = $map
        ->getMachineName() . ':' . $layer
        ->getMachineName();
      $value = sprintf("%s (%s)", $layer
        ->getName(), $layer
        ->getMachineName());
      $options[$map
        ->getName()][$key] = $value;
    }
  }
  if (empty($options)) {
    form_set_error('openlayers_map', "Error: You have no compatible openlayers maps. Make sure that at least one map has the 'GeoField Formatter' component enabled.");
  }
  $element['map_layer_preset'] = array(
    '#title' => t('Openlayers map & layer to use'),
    '#type' => 'select',
    '#default_value' => $settings['map_layer_preset'] ? $settings['map_layer_preset'] : 'openlayers_geofield_map_geofield_formatter',
    '#required' => TRUE,
    '#options' => $options,
    '#description' => t('Select which Openlayers map and layer you would like to add the Geofield data to. Map features will be added to the source of the layer when the map is built.'),
  );
  $data_options = _geofield_formatter_settings_data_options($display['type']);
  $element['data'] = array(
    '#title' => t('Data options'),
    '#type' => 'select',
    '#default_value' => $settings['data'] ? $settings['data'] : 'full',
    '#required' => TRUE,
    '#options' => $data_options,
  );
  return $element;
}

/**
 * Implements hook_field_formatter_settings_summary().
 *
 * Heavily borrowed from geofield_field_formatter_settings_summary().
 *
 * @see geofield_field_formatter_settings_summary()
 */
function openlayers_geofield_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $summary = array();
  $data_options = _geofield_formatter_settings_data_options($display['type']);
  if ($display['type'] == 'openlayers_geofield' && !empty($settings['map_layer_preset'])) {
    list($map_name, $layer_name) = explode(':', $settings['map_layer_preset']);
    if (($map = \Drupal\openlayers\Openlayers::load('Map', $map_name)) == TRUE) {
      $summary[] = t('Openlayers map: @data', array(
        '@data' => $map
          ->getName(),
      ));
      $summary[] = t('Openlayers layer: @data', array(
        '@data' => $layer_name,
      ));
    }
  }

  // Styles could be lost because of enabled/disabled modules that defines
  // their styles in code.
  if (!empty($data_options[$settings['data']])) {
    $summary[] = t('Data options: @data', array(
      '@data' => $data_options[$settings['data']],
    ));
  }
  else {
    $summary[] = t('No data options set');
  }
  if (!empty($settings['address']) && $settings['address']) {
    $summary[] = t('Including reverse-geocoded address');
  }
  return implode('<br />', $summary);
}

/**
 * Implements hook_field_formatter_view().
 *
 * Heavily borrowed from geofield_field_formatter_view() and
 * _geofield_openlayers_formatter()
 *
 * @see geofield_field_formatter_view()
 * @see _geofield_openlayers_formatter()
 */
function openlayers_geofield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  // First check to see if we have any value and remove any unset deltas.
  foreach ($items as $delta => $item) {
    if (empty($item['geom'])) {
      unset($items[$delta]);
    }
  }

  // If there are no items, stop here. We won't show anything.
  if (empty($items)) {
    return $element;
  }

  // Ensure geophp is available.
  geophp_load();

  // Transform into centroid or bounding if needed.
  if ($display['settings']['data'] != 'full') {
    if ($display['settings']['data'] == 'centroid') {
      foreach ($items as $delta => $item) {
        $centroid_wkt = 'POINT(' . $item['lon'] . ' ' . $item['lat'] . ')';
        $centroid = geoPHP::load($centroid_wkt);
        $items[$delta] = geofield_get_values_from_geometry($centroid);
      }
    }
    if ($display['settings']['data'] == 'bounding') {
      foreach ($items as $delta => $item) {
        $envelope_wkt = 'POLYGON ((' . $item['left'] . ' ' . $item['top'] . ', ' . $item['right'] . ' ' . $item['top'] . ', ' . $item['right'] . ' ' . $item['bottom'] . ', ' . $item['left'] . ' ' . $item['bottom'] . ', ' . $item['left'] . ' ' . $item['top'] . '))';
        $envelope = geoPHP::load($envelope_wkt);
        $items[$delta] = geofield_get_values_from_geometry($envelope);
      }
    }
  }

  // If we are a lat, lon, or latlon, and we are using degrees-minutes-seconds
  // (instead of decimal degrees), then do a transformation.
  if (isset($display['settings']['format'])) {
    if ($display['settings']['format'] == 'degrees_minutes_seconds') {
      foreach ($items as $delta => $item) {
        $items[$delta]['lat'] = geofield_latlon_DECtoDMS($item['lat'], 'lat');
        $items[$delta]['lon'] = geofield_latlon_DECtoDMS($item['lon'], 'lon');
      }
    }
    if ($display['settings']['format'] == 'ccs') {
      foreach ($items as $delta => $item) {
        $items[$delta]['lat'] = geofield_latlon_DECtoCCS($item['lat'], 'lat');
        $items[$delta]['lon'] = geofield_latlon_DECtoCCS($item['lon'], 'lon');
      }
    }
  }

  // Create array of $features.
  $features = array();
  foreach ($items as $delta) {
    if (array_key_exists('geom', $delta)) {
      $geometry = geoPHP::load($delta['geom']);
    }
    else {
      $geometry = geoPHP::load($delta);
    }
    $features[] = array(
      'wkt' => $geometry
        ->out('wkt'),
      'projection' => 'EPSG:4326',
    );
  }

  // Load map and set features.
  list($map_name, $layer_name) = explode(':', $display['settings']['map_layer_preset'], 2);

  /** @var \Drupal\openlayers\Types\MapInterface $map */
  if (count($items) && ($map = \Drupal\openlayers\Openlayers::load('Map', $map_name)) == TRUE) {

    /** @var \Drupal\openlayers\Types\LayerInterface $layer */
    if ($layer = $map
      ->getCollection()
      ->getObjectById('layer', $layer_name)) {
      if ($layer
        ->getSource() instanceof \Drupal\openlayers\Plugin\Source\Vector\Vector) {
        $layer
          ->getSource()
          ->setOption('features', $features);
      }
    }

    // Build map.
    $element[0] = array(
      '#type' => 'openlayers',
      '#map' => $map,
    );
  }
  return $element;
}

/**
 * Implements hook_field_widget_info().
 */
function openlayers_geofield_field_widget_info() {

  // Provide geofield widget.
  $widgets = array();
  $widgets['openlayers_geofield'] = array(
    'label' => t('Openlayers Map'),
    'field types' => array(
      'geofield',
    ),
    'behaviors' => array(
      'multiple values' => FIELD_BEHAVIOR_CUSTOM,
    ),
  );
  return $widgets;
}

/**
 * Implements hook_field_widget_info_alter().
 */
function openlayers_geofield_field_widget_info_alter(&$info) {

  // Overwrite the openlayers integration provided by the geofield module.
  unset($info['geofield_openlayers']);
}

/**
 * Implements hook_field_widget_settings_form().
 *
 * Bases on openlayers_field_widget_settings_form()
 *
 * @see openlayers_field_widget_settings_form()
 */
function openlayers_geofield_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];
  $form = array();

  // Get preset options, filtered to those which have the GeoField component.
  $map_options = array();
  foreach (\Drupal\openlayers\Openlayers::loadAll('Map') as $map) {
    foreach ($map
      ->getObjects('component') as $component) {
      if ($component instanceof \Drupal\openlayers_geofield\Plugin\Component\GeofieldWidget\GeofieldWidget) {
        $map_options[$map
          ->getMachineName()] = $map
          ->getName();
      }
    }
  }
  if (empty($map_options)) {
    form_set_error('openlayers_map', 'Error: You have no compatible openlayers maps. Make sure that at least one map has the "GeoField Widget" component enabled.');
  }
  $form['openlayers_map'] = array(
    '#type' => 'select',
    '#title' => t('Openlayers Map'),
    '#default_value' => isset($settings['openlayers_map']) ? $settings['openlayers_map'] : 'openlayers_geofield_map_geofield',
    '#options' => $map_options,
    '#description' => t('Select which Openlayers map you would like to use. Only maps which have the "GeoField Widget" component may be selected. If your preferred map is not here, add the "GeoField Widget" component to it first. The "Draw Features" behavior is incompatible - presets with this behavior are not shown.'),
  );
  $form['data_storage'] = array(
    '#type' => 'radios',
    '#title' => t('Storage Options'),
    '#description' => t('Should the widget only allow simple features (points, lines, or polygons), or should the widget allow for complex features? Note that changing this setting from complex to simple after data has been entered can lead to data loss.'),
    '#options' => array(
      'single' => 'Store each simple feature as a separate field.',
      'collection' => 'Store as a single collection.',
    ),
    '#default_value' => isset($settings['data_storage']) ? $settings['data_storage'] : 'single',
  );
  $form['feature_types'] = array(
    '#title' => t('Available Features'),
    '#type' => 'checkboxes',
    '#options' => array(
      'point' => t('Point'),
      'path' => t('Path'),
      'polygon' => t('Polygon'),
    ),
    '#description' => t('Select what features are available to draw.'),
    '#default_value' => isset($settings['feature_types']) ? $settings['feature_types'] : array(
      'point' => 'point',
      'path' => 'path',
      'polygon' => 'polygon',
    ),
  );
  $form['allow_edit'] = array(
    '#title' => t('Allow shape modification'),
    '#type' => 'checkbox',
    '#description' => t('Can you edit and delete shapes.'),
    '#default_value' => isset($settings['allow_edit']) ? $settings['allow_edit'] : 1,
  );
  $form['showInputField'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show input field'),
    '#description' => t('Shows the data in a textarea.'),
    '#default_value' => !empty($settings['showInputField']),
  );

  // Add optional Geocoder support.
  $use_geocoder = isset($settings['use_geocoder']) ? $settings['use_geocoder'] : 0;
  $geocoder_form = array(
    '#type' => 'fieldset',
    '#title' => t('Geocoder settings'),
  );
  if (module_exists('geocoder')) {
    $geocoder_form['use_geocoder'] = array(
      '#type' => 'checkbox',
      '#title' => t('Enable geocoding of location data'),
      '#default_value' => $use_geocoder,
      // Can't nest this in a fieldset element without affecting data storage so
      // instead hardcode one.
      '#prefix' => '<fieldset><legend><span="fieldset-legend">' . t('Geocoder settings') . '</span></legend><div class="fieldset-wrapper">',
    );

    // Load the Geocoder widget settings.
    module_load_include('inc', 'geocoder', 'geocoder.widget');
    $new = geocoder_field_widget_settings_form($field, $instance);
    $new['geocoder_field']['#options'][$field['field_name']] = t('Add extra field');

    // If there are no options available search by ourselves to ensure the text
    // field type was taken in account.
    if (empty($new['geocoder_handler']['#options'])) {
      $supported_field_types = geocoder_supported_field_types();
      if (isset($supported_field_types['text'])) {
        $processors = geocoder_handler_info();
        foreach ($supported_field_types['text'] as $handler) {
          $new['geocoder_handler']['#options'][$handler] = $processors[$handler]['title'];
        }
      }
    }

    // Show the geocoder fields only if geocoder is selected.
    openlayers_widget_add_states($new, ':input[name="instance[widget][settings][use_geocoder]"]');

    // Close the fieldset we opened in the #prefix to use_geocoder.
    $element_children = element_children($new);
    $new[end($element_children)]['#suffix'] = '</div></fieldset>';
    $geocoder_form += $new;
  }
  else {
    $geocoder_form['add_geocoder'] = array(
      '#markup' => t('Optionally, install the <a href="http//drupal.org/project/geocoder">Geocoder</a> module and add an <a href="http://drupal.org/project/addressfield">Address field</a> to enable mapping by address.'),
    );
  }
  $form += $geocoder_form;
  return $form;
}

/**
 * Implements hook_field_widget_form().
 */
function openlayers_geofield_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $widget = $instance['widget'];
  $parents = array_merge($element['#field_parents'], array(
    $element['#field_name'],
    $langcode,
    $delta,
  ));
  $field_name = $field['field_name'];
  $id_prefix = implode('-', array_merge($parents, array(
    $field_name,
    $delta,
  )));
  $wrapper_id = drupal_html_id($id_prefix . '-use-geocoder-wrapper');
  $settings_defaults = array(
    'openlayers_map' => 'openlayers_geofield_map_geofield_widget',
    'data_storage' => 'single',
    'feature_types' => array(
      'point' => 'point',
      'path' => 'path',
      'polygon' => 'polygon',
    ),
    'allow_edit' => 1,
    'showInputField' => 1,
  );
  $settings = array_merge($settings_defaults, $widget['settings']);
  $openlayers_map_id = !empty($instance['widget']['settings']['openlayers_map']) ? $instance['widget']['settings']['openlayers_map'] : 'openlayers_geofield_map_geofield_widget';

  /** @var \Drupal\openlayers\Types\MapInterface $map */
  if (($map = \Drupal\openlayers\Openlayers::load('Map', $openlayers_map_id)) == FALSE) {

    // If the map couldn't be load we can't do a thing.
    return array();
  }
  foreach ($map
    ->getObjects('component') as $component) {

    /** @var \Drupal\openlayers\Types\Component $component */

    // Configure component based on the widget settings.
    if ($component
      ->getFactoryService() === 'openlayers.Component:GeofieldWidget') {

      // Conversion from geofield to openlayers geofield feature types.
      $feature_types = $settings['feature_types'];
      if (!empty($feature_types['path'])) {
        $feature_types['LineString'] = 'LineString';
        unset($feature_types['path']);
      }
      if (!empty($feature_types['polygon'])) {
        $feature_types['Polygon'] = 'Polygon';
        unset($feature_types['polygon']);
      }
      if (!empty($feature_types['point'])) {
        $feature_types['Point'] = 'Point';
        unset($feature_types['point']);
      }
      $action_feature = array(
        'draw' => 'draw',
      );
      if (!empty($settings['allow_edit'])) {
        $action_feature['modify'] = 'modify';
      }

      // Get the initial data from $form_state['input'], if it exists.
      // Otherwise, use the field's $items. This allows the Geocoder button
      // to populate the map, replacing any other shapes.
      $initial_data = $items;
      if (($input_value = drupal_array_get_nested_value($form_state['input'], $element['#field_parents'])) && !empty($input_value[$field_name][$langcode])) {
        $initial_data = $input_value[$field_name][$langcode];
      }
      $component
        ->setOption('dataType', array(
        'WKT' => 'WKT',
      ));
      $component
        ->setOption('dataProjection', 'EPSG:4326');
      $component
        ->setOption('typeOfFeature', $feature_types);
      $component
        ->setOption('actionFeature', $action_feature);
      $component
        ->setOption('initialData', $initial_data);
      $component
        ->setOption('featureLimit', $field['cardinality']);
      $component
        ->setOption('inputFieldName', 'geom');
      $component
        ->setOption('showInputField', !empty($settings['showInputField']));
      $component
        ->setOption('parents', $parents);
    }
  }
  $element += array(
    '#type' => 'openlayers',
    '#map' => $map,
  );
  $element['map']['#map_id'] = $openlayers_map_id;
  $element['geom']['#required'] = isset($instance['required']) ? $instance['required'] : FALSE;
  $element['input_format']['#value'] = GEOFIELD_INPUT_WKT;
  $element['#data_storage'] = !empty($settings['data_storage']) ? $settings['data_storage'] : 'collection';

  // Attach the widget and field settings so they can be accesses by JS and
  // validate functions.
  $element['#widget_settings'] = $settings;
  $element['#widget_settings']['allow_edit'] = (bool) $settings['allow_edit'];
  $element['#widget_settings']['feature_types'] = array();
  foreach ($settings['feature_types'] as $feature => $feature_setting) {
    if ($feature_setting) {
      $element['#widget_settings']['feature_types'][] = $feature;
    }
  }
  $element['#widget_settings']['cardinality'] = $field['cardinality'];

  // Make sure we set the input-format to WKT so geofield knows how to process
  // it.
  $element['input_format'] = array(
    '#type' => 'value',
    '#value' => GEOFIELD_INPUT_WKT,
  );

  // Time to deal with optional geocoder integration
  // Conditionally add geocoder button.
  $is_settings_form = isset($form['#title']) && $form['#title'] == t('Default value');
  if (!$is_settings_form && !empty($settings['use_geocoder']) && !empty($settings['geocoder_field'])) {
    if ($settings['geocoder_field'] == $element['#field_name']) {

      // Extra field.
      $element['geocoder_input'] = array(
        '#type' => 'textfield',
        '#title' => t('Geocode'),
        '#description' => t('Enter the place to geocode.'),
      );
      $label = $element['geocoder_input']['#title'];
    }
    elseif ($field = field_info_instance($instance['entity_type'], $settings['geocoder_field'], $instance['bundle'])) {
      $label = $field['label'];
    }
    else {
      switch ($settings['geocoder_field']) {
        case 'title':
          $label = t('Title');
          break;
        case 'name':
          $label = t('Name');
          break;
        default:
          $label = $settings['geocoder_field'];
      }
    }
    $element['#prefix'] = '<div id="' . $wrapper_id . '">';
    $element['#suffix'] = '</div>';
    $element['use_geocoder'] = array(
      '#type' => 'submit',
      '#name' => strtr($id_prefix, '-', '_') . '_use_geocoder',
      '#value' => t('Find using @field field', array(
        '@field' => $label,
      )),
      '#attributes' => array(
        'class' => array(
          'field-use-geocoder-submit',
        ),
      ),
      // Avoid validation errors for e.g. required fields but do pass the value
      // of the geocoder field.
      '#limit_validation_errors' => array(),
      '#ajax' => array(
        'callback' => 'openlayers_geofield_geocode_ajax_callback',
        'wrapper' => $wrapper_id,
        'effect' => 'fade',
      ),
      '#submit' => array(
        'openlayers_openlayers_use_geocoder_submit',
      ),
    );
  }

  // Add the element to an array, because it's the format that
  // FIELD_BEHAVIOR_CUSTOM expects.
  $full_element = array(
    $element,
  );

  // Override the element_validate as we need to deal with deltas.
  unset($full_element[0]['#element_validate']);
  $full_element['#element_validate'][] = 'openlayers_geofield_widget_element_validate';
  return $full_element;
}

/**
 * Submit handler for the geocoder integration.
 */
function openlayers_openlayers_use_geocoder_submit($form, &$form_state) {
  $button = $form_state['triggering_element'];

  // Go one level up in the form, to the widgets container.
  $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
  $field_name = $element['#field_name'];
  $langcode = $element['#language'];
  $delta = $element['#delta'];
  $parents = $element['#field_parents'];

  // Set the widget value based on geocoding results.
  $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  $geocoder_field = $field_state['instance']['widget']['settings']['geocoder_field'];
  $geocoder_field_parents = array_merge(array_slice($button['#array_parents'], 0, -4), array(
    $geocoder_field,
    $langcode,
  ));

  // Since all validation errors are disabled we need to fetch the values for
  // the geocoding as only validate values are sent to the values array of the
  // form state.
  $values = drupal_array_get_nested_value($form_state['input'], $geocoder_field_parents);

  // Inject the values into the values array to ensure the geofield module is
  // able to handle the data.
  drupal_array_set_nested_value($form_state['values'], $geocoder_field_parents, $values);
  if ($field_value = geocoder_widget_get_field_value($element['#entity_type'], $field_state['instance'], NULL, $values)) {
    geophp_load();
    $geometry = geoPHP::load($field_value[$langcode][$delta]['geom']);

    // Openlayers can only use WKT, so translate.
    $field_value[$langcode][$delta]['geom'] = $geometry
      ->out('wkt');
    $field_value[$langcode][$delta]['input_format'] = 'wkt';

    // Override the field's value in the 'input' array to substitute the new
    // field value for the one that was submitted.
    drupal_array_set_nested_value($form_state, array_merge(array(
      'input',
    ), array_slice($button['#array_parents'], 0, -4), array(
      $field_name,
    )), $field_value);
  }

  // Rebuild the form to ensure that the map is recreated from scratch.
  // See field_add_more_submit() in core field.form.inc for similar usage.
  $form_state['rebuild'] = TRUE;
}

/**
 * Return the altered form element from an AJAX request.
 *
 * @see openlayers_geofield_field_widget_form()
 */
function openlayers_geofield_geocode_ajax_callback($form, $form_state) {
  $button = $form_state['triggering_element'];

  // Go one level up in the form, to the widgets container.
  $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));

  // Return the map, but remove the '_weight' element inserted
  // by the field API.
  unset($element['_weight']);
  return $element;
}

/**
 * Implements hook_geocoder_geocode_values_alter().
 */
function openlayers_geofield_geocoder_geocode_values_alter(&$source_field_values, &$field_info, $handler_settings, $field_instance) {

  // If this is an openlayers_geofield pointing to its extra field adjust the
  // field values and mock a text field.
  if (isset($field_instance['widget']['settings']['geocoder_field']) && $field_instance['widget']['type'] == 'openlayers_geofield' && $field_instance['widget']['settings']['geocoder_field'] == $field_info['field_name']) {
    if (isset($source_field_values[0]['geocoder_input'])) {
      $source_field_values = array(
        array(
          'value' => $source_field_values[0]['geocoder_input'],
        ),
      );
      $field_info = array(
        'type' => 'text',
      );
    }
  }
}