<?php

/**
 * @defgroup openlayers OpenLayers provides an API and
 * Modules to interface with OpenLayers
 */

/**
 * @file
 * Main OpenLayers API File
 *
 * This file holds the main Drupal hook functions,
 * and the openlayers API functions for the openlayers module.
 *
 * @ingroup openlayers
 */

/**
 * OpenLayers hosted default library.
 */
define('OPENLAYERS_DEFAULT_LIBRARY', 'http://openlayers.org/api/2.12/OpenLayers.js');

/**
 * OpenLayers library compatible suggestion.
 */
define('OPENLAYERS_SUGGESTED_LIBRARY', 2.12);

/**
 * OpenLayers hosted API version.  What version is used when going to
 * http://openlayers.org/api/OpenLayers.js
 */
define('OPENLAYERS_HOSTED_API_LIBRARY', 2.12);

/**
 * Implements hook_help().
 */
function openlayers_help($path, $arg) {
  switch ($path) {
    case 'admin/help#openlayers':
      return '<p>' . t('The OpenLayers module is the base module for the
        OpenLayers suite of modules, and provides the main API.') . '</p>';
  }
  return '';
}

/**
 * Implements hook_theme().
 */
function openlayers_theme($existing, $type, $theme, $path) {
  return array(
    'openlayers_map' => array(
      'arguments' => array(
        'map' => array(),
      ),
      'file' => 'includes/openlayers.theme.inc',
      'template' => '/templates/openlayers-map'
    ),
    'openlayers_styles' => array(
      'arguments' => array(
        'styles' => array(),
        'map' => array(),
      ),
      'file' => 'includes/openlayers.theme.inc',
    ),
  );
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function openlayers_ctools_plugin_directory($module, $plugin) {
  return 'plugins/' . $plugin;
}

/**
 * Implements hook_ctools_plugin_type().
 */
function openlayers_ctools_plugin_type() {
  // For backwards compatibility, we allow for the use
  // of hooks to define these plugins.
  //
  // This should be removed in 7.x-3.x
  return array(
    'behaviors' => array(
      'use hooks' => TRUE,
      'classes' => array('behavior'),
    ),
    'layer_types' => array(
      'use hooks' => TRUE,
      'classes' => array('layer_types'),
    )
  );
}

/**
 * Include necessary CSS and JS for rendering maps
 *
 * @ingroup openlayers_api
 */
function openlayers_include() {

  if ('internal' == variable_get('openlayers_source_type', 'external')) {
    $variant = variable_get('openlayers_source_internal_variant', NULL);
    if ($variant == 'original') $variant = NULL;
    libraries_load('openlayers', $variant);
  }
  else {
    // Use a static variable to prevent running URL check code repeatedly.
    static $once;
    if (!isset($once)) {
      $once = TRUE;

      $path = check_plain(variable_get('openlayers_source_external', OPENLAYERS_DEFAULT_LIBRARY));

      // Correctly handle URLs beginning with a double backslash, see RFC 1808 Section 4
      if (substr($path, 0, 2) == '//') {
        $http_protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
        $path = $http_protocol . ':' . $path;
      }

      // Check for full URL and include it manually if external.
      if (valid_url($path, TRUE)) {
        drupal_add_js($path, 'external');
      }
      else {
        drupal_add_js($path);
      }

      drupal_add_css(drupal_get_path('module', 'openlayers') .
        '/css/openlayers.css', 'file');
      drupal_add_js(drupal_get_path('module', 'openlayers') .
        '/js/openlayers.js', 'file');
    }
  }

}

/**
 * Prepare a map for rendering.
 *
 * Takes a map array and builds up the data given the
 * reference to objects like styles, layers, and behaviors.
 *
 * @ingroup openlayers_api
 *
 * @param $map
 *   Array of map settings
 * @return
 *   Filled in map array.
 */
function openlayers_build_map($map = array()) {
  // Get the necessary parts
  openlayers_include();
  module_load_include('inc', 'openlayers', 'includes/openlayers.render');

  // If no map is specified, use the default map.
  if (empty($map)) {
    if ($loaded_map = openlayers_map_load(
      variable_get('openlayers_default_map', 'default'))) {
      $map = $loaded_map->data;
    }
  }

  // Create ID for map as this will help with alters.
  $map['id'] = !isset($map['id']) ?
    _openlayers_create_map_id() : $map['id'];

  // Hook to alter map before main processing.  Styles, behaviors,
  // layers may all be added here.
  // hook_openlayers_map_preprocess_alter($map)
  drupal_alter('openlayers_map_preprocess', $map);

  // Styles and layer styles are not required parameters
  $map['styles'] = isset($map['styles']) ? $map['styles'] : array();
  $map['layer_styles'] = isset($map['layer_styles']) ? $map['layer_styles'] : array();
  $map['layer_styles_select'] = isset($map['layer_styles_select']) ? $map['layer_styles_select'] : array();
  $map['layer_styles_temporary'] = isset($map['layer_styles_temporary']) ? $map['layer_styles_temporary'] : array();

  // Process map parts.
  $map['layers'] = _openlayers_layers_process($map['layers'], $map);
  $map['behaviors'] = _openlayers_behaviors_render($map['behaviors'], $map);
  $map['styles'] = _openlayers_styles_process($map['styles'], $map['layer_styles'], $map['layer_styles_select'], $map['layer_styles_temporary'], $map);

  // Restrict map to its projection extent (data outwith cannot be represented).
  // Layer can additionally specfiy their maxExtent in case they use
  // non-default grids.
  $projection = openlayers_get_projection_by_identifier($map['projection']);
  $map['maxExtent'] = $projection->getProjectedExtent();

  // In case the layer offers the same projection as the map, use this and do not provide
  // projection definition to client. Otherwise rely on the client to reproject on the fly.
  foreach ($map['layers'] as $layer_name => $layer) {
    if(in_array($map['projection'], $layer['projection'])){
      $map['layers'][$layer_name]['projection'] = $map['projection'];
    } else {
      // Client is able to reproject any possible projection because their definitions need to be
      // known to be able to set up a layer with a certain projection. Thus choice does not matter.
      $layerProjectionIdentifier = reset($layer['projection']);
      if($layerProjectionIdentifier===FALSE){
        throw new Exception(
          t('Layer !title lacks its projection. Please edit it to select a projection.', array(
              '!title' => $layer['title']
          ))
        );
      }
      $map['layers'][$layer_name]['projection'] = $layerProjectionIdentifier;
    }

    // Ensure JavaScript gets proper type.
    $map['layers'][$layer_name]['isBaseLayer'] = (boolean)($layer['isBaseLayer']);
  }

  // Hook to alter map one last time.  Final modification to existing
  // styles, behaviors, layers can happen here, but adding new styles,
  // behaviors will not get rendered.
  // hook_openlayers_map_alter($map)
  drupal_alter('openlayers_map', $map);

  // Check map for errors
  $map['errors'] = openlayers_error_check_map($map);
  return $map;
}

/**
 * Render map array
 *
 * Given a map array, render into HTML to display
 * a map.
 *
 * @ingroup openlayers_api
 *
 * @param $map
 *   Associative array of map paramters.
 * @return
 *   Map HTML.
 */
function openlayers_render_map_data($map = array()) {
  // Run map through build process
  $map = openlayers_build_map($map);

  $output = '';

  // Given hide_empty_map flag, check if the map has any features
  // defined. If not, assume it is an empty map and shouldn't be displayed.
  if (isset($map['hide_empty_map']) && $map['hide_empty_map'] == TRUE) {
    $empty = TRUE;
    foreach ($map['layers'] as $layer) {
      if (isset($layer['features']) && count($layer['features'])) {
        $empty = FALSE;
      }
    }
    if ($empty) {
      // Abort early because there are no features to display on the map anyway
      return '';
    }
  }

  // Currently the restricted extent of maps is always given in EPSG:3857 so
  // this projection needs to be available in the client for all restricted
  // maps. Using EPSG:4326 instead would likely be better.
  if(array_key_exists('restrict', $map['center']) && (boolean)$map['center']['restrict']['restrictextent']){
    openlayers_add_js_projection_definition(
      openlayers_get_projection_by_identifier('EPSG:3857')
    );
  }

  // Return themed map if no errors found
  if (empty($map['errors'])) {

    // In case the layer offers the same projection as the map, use this and do not provide
    // projection definition to client. Otherwise rely on the client to reproject on the fly.
    foreach ($map['layers'] as $layer_name => $layer) {
        // Provide client with projection definition so that it can reproject
        openlayers_add_js_projection_definition(
          openlayers_get_projection_by_identifier($map['layers'][$layer_name]['projection'])
        );
    }

    // Ensure projections in use are known to the client (loads Proj4js if required)
    openlayers_add_js_projection_definition(openlayers_get_projection_by_identifier($map['projection']));
    openlayers_add_js_projection_definition(openlayers_get_projection_by_identifier($map['displayProjection']));

    $js = array('openlayers' => array('maps' => array($map['id'] => $map)));
    drupal_add_js($js, 'setting');

    // Push map through theme function and return
    $output = theme('openlayers_map', array(
      'map' => $map,
    ));
  }
  return $output;
}

/**
 * Load projection transformations in case OpenLayers does not support projections in use natively
 * @param openlayers_projection $projection
 */
function openlayers_add_js_projection_definition(openlayers_projection $projection) {
  $openlayers_natively_supported = array('EPSG:4326', 'EPSG:900913');
  // Only load Proj4js if projection not supported by OpenLayers anyway
  if (!in_array($projection->identifier, $openlayers_natively_supported)) {
    proj4js_load_definition($projection->identifier, $projection->getDefinition());
  }
}

/**
 * Render a map by name
 *
 * Given a map name render it into a full map object.
 *
 * @ingroup openlayers_api
 *
 * @param $map
 *   Name of the map
 * @return
 *   Map HTML.
 */
function openlayers_render_map($map = '') {

  // If it's an array, then we have been passed the map data array
  if (is_array($map)) {
    return openlayers_render_map_data($map);
  }

  // If it's a string, then we are passing a map name instead of the whole map object
  // so we need to load the object
  if (!$map || is_string($map)) {
    $map_name = $map;
    if (!$map_name) {
      $map_name = variable_get('openlayers_default_map', 'default');
    }
    $map = openlayers_map_load($map_name);
    if (!is_object($map)) {
      throw new Exception("Failed to load map called " . $map_name);
    }
  }

  return openlayers_render_map_data($map->data);
}

/**
 * Get layer object
 *
 * @ingroup openlayers_api
 * @return openlayers_layer_type|FALSE
 *   array of layer info
 */
function openlayers_get_layer_object($layer, $map = array()) {
  ctools_include('plugins');

  // Static cache because this function will possibly be called in big loops
  static $layer_types;
  if (!isset($layer_types)) {
    $layer_types = openlayers_layer_types();
  }

  $layer->title = t($layer->title);
  $layer->description = t($layer->description);

  // Attempt to get ctool class
  if (isset($layer_types[$layer->data['layer_type']]) &&
    $class = ctools_plugin_get_class(
      $layer_types[$layer->data['layer_type']],
      'layer_type')
  ) {
    $layer_object = new $class($layer, $map);
    return $layer_object;
  }
  else {
    watchdog('openlayers', 'Layer !layer_name is unavailable because its
      layer type or the module that provides its layer type is missing',
      array('!layer_name' => $layer->title),
      WATCHDOG_ERROR);
    return FALSE;
  }
}

/**
 * Menu loader for layers. (%openlayers_layer)
 * @ingroup openlayers_api
 *
 * @param $name
 *   Layer name
 * @param $reset
 *   Boolean whether to reset cache or not
 * @return openlayers_layer_type|FALSE
 *   Layer export
 */
function openlayers_layer_load($name, $reset = FALSE) {
  ctools_include('export');
  if ($reset) ctools_export_load_object_reset('openlayers_layers');
  $layer = ctools_export_load_object('openlayers_layers', 'names', array($name));

  if (is_array($layer) && isset($layer[$name])) {
    $layer_object = openlayers_get_layer_object($layer[$name]);
    if (openlayers_layer_sanity_check($layer_object)) {
      return $layer_object;
    }
  }
  else {
    return FALSE;
  }
}

/**
 * Get all openlayers layers as objects.
 * @ingroup openlayers_api
 *
 * @param $reset
 *   Boolean whether to reset cache or not
 * @return array
 *   array of layer info
 */
function openlayers_layers_load($reset = FALSE, $include_disabled = FALSE) {
  ctools_include('export');
  $layer_objects = array();
  if ($reset) ctools_export_load_object_reset('openlayers_layers');
  $layers = ctools_export_load_object('openlayers_layers', 'all', array());
  foreach ($layers as $layer) {
    if (!$include_disabled && isset($layer->disabled) && $layer->disabled) {
      continue;
    }

    $layer_objects[$layer->name] = openlayers_get_layer_object($layer);
  }

  return array_filter($layer_objects, 'openlayers_layer_sanity_check');
}

/**
 * Check the plugin definition of a layer.
 * Some field *MUST* be there to work correctly with OL.
 *
 * @ingroup openlayers_api
 * @param $definition
 * @return bool
 */
function openlayers_layer_definition_check($definition) {
  $mandatory_fields = array(
    array('title'),
    array('description'),
    array('name'),
    array('path'),
    array('layer_type', 'file'),
    array('layer_type', 'class'),
    array('layer_type', 'parent'),
  );

  foreach ($mandatory_fields as $field) {
    $missing = drupal_array_nested_key_exists($definition, $field);
    if (!$missing) {
      drupal_set_message(t("Key !key is missing in in the plugin definition of the layer type <em>!type</em>. The layer will be disabled.", array(
        '!key' => htmlspecialchars(implode(', ', $field)),
        '!type' => htmlspecialchars($definition['name']),
      )), 'warning');
      watchdog('openlayers', 'Layer !layer is unavailable because its
                                plugin definition is incomplete.',
        array('!layer' => $definition['name']),
        WATCHDOG_ERROR);
      return FALSE;
    }
  }

  return TRUE;
}

/**
 * Check the plugin definition of a behavior.
 * Some field *MUST* be there to work correctly with OL.
 *
 * @ingroup openlayers_api
 * @param $definition
 * @return bool
 */
function openlayers_behavior_definition_check($definition) {
  $mandatory_fields = array(
    array('title'),
    array('description'),
    array('name'),
    array('path'),
    array('type'),
    array('behavior', 'file'),
    array('behavior', 'class'),
    array('behavior', 'parent'),
  );

  foreach ($mandatory_fields as $field) {
    $missing = drupal_array_nested_key_exists($definition, $field);
    if (!$missing) {
      drupal_set_message(t("Key !key is missing in the definition of the behavior <em>!behavior</em>. The behavior will be disabled.", array(
        '!key' => htmlspecialchars(implode(', ', $field)),
        '!behavior' => htmlspecialchars($definition['name']),
      )), 'warning');
      watchdog('openlayers', 'Behavior !behavior is unavailable because its
                                plugin definition is incomplete.',
        array('!behavior' => $definition['name']),
        WATCHDOG_ERROR);
      return FALSE;
    }
  }

  return TRUE;
}


/**
 * Check layer to determine whether it has all the
 * necessary attributes to be rendered. This is necessary
 * because of API changes, and is a consolidation from other
 * layer-error-checking in this module
 *
 * @param $layer
 *  Layer object
 * @param $projection
 *  Projection number (EPSG) to check compatibility with
 * @param $strict
 *  reject invalid layers
 * @return boolean
 *  layer validity if strict is set, otherwise always true
 */
function openlayers_layer_sanity_check($layer, $projection = FALSE, $strict = FALSE) {
  // Handle layers after they've been rendered for a map
  $layer = (is_array($layer)) ? (object) $layer : $layer;

  if (!isset($layer->name)) {
    return !$strict;
  }

  if (!isset($layer->data['projection']) || !is_array($layer->data['projection'])) {
    watchdog('openlayers', 'Layer %name does not have a projection set.',
      array('%name' => $layer->name));
    drupal_set_message(
      t('OpenLayers layers failed the sanity check. See the
      <a href="@drupallog">Drupal log</a> for details',
      array('@drupallog' => url('admin/reports/dblog')))
    );
    return !$strict;
  }

  if (!isset($layer->data['layer_type'])) {
    watchdog('openlayers', 'Layer %name does not have its layer_type set.',
      array('%name' => $layer->name));
    drupal_set_message(
      t('OpenLayers layers failed the sanity check. See the
      <a href="@drupallog">Drupal log</a> for details',
      array('@drupallog' => url('admin/reports/dblog')))
    );
    return !$strict;
  }

  if ($projection && empty($layer->data['vector']) &&
    (!in_array($projection, $layer->data['projection']))) {
    watchdog('openlayers',
      'The layer %layer_name cannot be reprojected to the map projection: EPSG: %map_proj',
      array(
        '%layer_name' => $layer->name,
        // TODO: $map is not defined.
        '%map_proj' => $map['projection'],
      )
    );
    return !$strict;
  }

  return TRUE;
}

/**
 * Delete a layer object from the database.
 *
 * @ingroup openlayers_api
 *
 * @param $layer
 *   String identifier of a layer or layer object with name.
 * @return
 *   The results of DB delete.
 */
function openlayers_layer_delete($layer) {
  return openlayers_object_delete($layer, 'layer');
}

/**
 * Get all layer types.
 *
 * @ingroup openlayers_api
 *
 * @param $reset
 *   Boolean whether to reset cache or not.
 * @return
 *   Array of layer type info.
 */
function openlayers_layer_types($reset = FALSE, $include_disabled = FALSE) {
  ctools_include('plugins');
  $layers = ctools_get_plugins('openlayers', 'layer_types');

  if (!$include_disabled) {
    $layers = array_filter($layers, function($layer) {
      return (!isset($layer->disabled) || !$layer->disabled);
    });
  }

  return array_filter($layers, 'openlayers_layer_definition_check');
}

/**
 * Menu loader for layer types.
 *
 * @ingroup openlayers_api
 *
 * @param $name
 *   String identifier of layer type.
 * @param $reset
 *   Boolean whether to reset cache or not.
 * @return openlayers_layer_type
 *   An instantiated layer type object or FALSE if not found.
 */
function openlayers_layer_type_load($name, $reset = FALSE) {
  ctools_include('plugins');

  if ($layer_type_class = ctools_plugin_load_class(
    'openlayers',
    'layer_types',
    $name,
    'layer_type')) {
    $layer_type = new $layer_type_class();
    return $layer_type;
  }
  return FALSE;
}

/**
 * Get all behaviors.
 *
 * @ingroup openlayers_api
 *
 * @param $reset
 *   Boolean whether to reset cache or not.
 * @return
 *   Array of behavior info.
 */
function openlayers_behaviors($reset = FALSE, $include_disabled = FALSE) {
  ctools_include('plugins');
  $behaviors = ctools_get_plugins('openlayers', 'behaviors');

  if (!$include_disabled) {
    $behaviors = array_filter($behaviors, function($behavior) {
      return (!isset($behavior->disabled) || !$behavior->disabled);
    });
  }

  return array_filter($behaviors, 'openlayers_behavior_definition_check');

}

/**
 * Get all openlayers styles.
 *
 * @ingroup openlayers_api
 *
 * @param $reset
 *   Boolean whether to reset cache or not.
 * @return
 *   Array of all available styles.
 */
function openlayers_styles($reset = FALSE, $include_disabled = FALSE) {
  ctools_include('export');
  if ($reset) {
    ctools_export_load_object_reset('openlayers_styles');
  }

  $styles = ctools_export_load_object('openlayers_styles', 'all', array());

  if (!$include_disabled) {
    $styles = array_filter($styles, function($style) {
      return !(isset($style->disabled) && $style->disabled);
    });
  }

  return $styles;
}

/**
 * Load a style object by name.
 *
 * This function can also be used as a
 * menu loader for a style.
 *
 * @ingroup openlayers_api
 *
 * @param $name
 *   The string identifier of the style.
 * @param $reset
 *   Boolean whether to reset the cache or not.
 * @return
 *   A style object or FALSE if not found.
 */
function openlayers_style_load($name, $reset = FALSE) {
  $styles = openlayers_styles($reset);
  return !empty($styles[$name]) ? $styles[$name] : FALSE;
}

/**
 * Save style.
 *
 * @ingroup openlayers_api
 *
 * @param $style
 *   The style object to save.
 * @return
 *   The results of DB write or FALSE if no name.
 */
function openlayers_style_save($style) {
  if (!empty($style->name)) {
    return (db_select('openlayers_styles')
          ->fields('openlayers_styles', array('name'))
          ->condition('name', $style->name)
          ->execute()
          ->fetchCol()) ?
      drupal_write_record('openlayers_styles', $style, 'name') :
      drupal_write_record('openlayers_styles', $style);
  }
  return FALSE;
}

/**
 * Delete a style object from the database.
 *
 * @ingroup openlayers_api
 *
 * @param $style
 *   String identifier of a style or style object with name.
 * @return
 *   The results of DB delete.
 */
function openlayers_style_delete($style) {
  return openlayers_object_delete($style, 'style');
}

/**
 * Get maps from DB or code, via cache.
 *
 * @ingroup openlayers_api
 *
 * @param $reset
 *   Boolean whether to reset or not.
 * @return
 *   Return array of maps.
 */
function openlayers_maps($reset = FALSE, $include_disabled = FALSE) {
  ctools_include('export');
  if ($reset) {
    ctools_export_load_object_reset('openlayers_maps');
  }

  $maps = ctools_export_load_object('openlayers_maps', 'all', array());

  if (!$include_disabled) {
    $maps = array_filter($maps, function($map) {
      return !(isset($map->disabled) && $map->disabled);
    });
  }

  return $maps;
}

/**
 * Given a map name, get full map object.
 *
 * This function can also be used as a
 * menu loader for a style.
 *
 * @ingroup openlayers_api
 *
 * @param $name
 *   String identifier of the map.
 * @param $reset
 *   Boolean whether to reset cache.
 * @return
 *   map object or FALSE if not found.
 */
function openlayers_map_load($name = '', $reset = FALSE) {
  ctools_include('export');
  if ($reset) {
    ctools_export_load_object_reset('openlayers_maps');
  }

  $maps = ctools_export_load_object('openlayers_maps', 'names', array($name));

  if (empty($maps[$name])) {
    return FALSE;
  }
  else {
    $map = $maps[$name];
    $map->data['map_name'] = $name;
    return clone $map;
  }
}

/**
 * Save a map object to the database.
 *
 * @ingroup openlayers_api
 *
 * @param $map
 *   map object.
 * @return
 *   The results of DB write or FALSE if no name.
 */
function openlayers_map_save($map) {
  if (!empty($map->name)) {
     return (db_select('openlayers_maps')
          ->fields('openlayers_maps', array('name'))
          ->condition('name', $map->name)
          ->execute()
          ->fetchCol()) ?
      drupal_write_record('openlayers_maps', $map, 'name') :
      drupal_write_record('openlayers_maps', $map);
  }
  return FALSE;
}

/**
 * Delete a map object from the database.
 *
 * @ingroup openlayers_api
 *
 * @param $map
 *   String identifier of a map or map object with name.
 * @return
 *   The results of DB delete.
 */
function openlayers_map_delete($map) {
  return openlayers_object_delete($map, 'map');
}

/**
 * Get map options in an array suitable for a FormAPI element.
 *
 * @ingroup openlayers_api
 *
 * @param $reset
 *   Boolean whether to reset or not.
 * @return
 *   Return array of formatted data.
 */
function openlayers_map_options($reset = FALSE) {
  $maps = openlayers_maps($reset);
  $options = array();
  foreach ($maps as $map) {
    $options[$map->name] = $map->title;
  }
  return $options;
}

/**
 * Delete an object from the database.
 *
 * @ingroup openlayers_api
 *
 * @param $ol_object
 *   String identifier of an object or the object with name.
 * @param $type
 *   Type of object to delete.  The options are the following:
 *   - 'layer'
 *   - 'style'
 *   = 'map'
 * @return
 *   The results of the DB delete.
 */
function openlayers_object_delete($ol_object, $type) {
  // Check for object or name
  $tables = array(
      'style' => 'openlayers_styles',
      'layer' => 'openlayers_layers',
      'map' => 'openlayers_maps');
  if (is_object($ol_object) && isset($ol_object->name) && isset($tables[$type])) {
    $ol_object = $ol_object->name;
    return db_delete($tables[$type])->condition('name', $ol_object)->execute();
  }
}

/**
 * Checks map array for incompatibilities or errors.
 *
 * @ingroup openlayers_api
 *
 * @param $map
 *   Map array
 * @param $log_errors
 *   Boolean whether to log errors.
 * @return
 *   FALSE if passed. Array of descriptive errors if fail.
 */
function openlayers_error_check_map($map, $log_errors = TRUE) {
  $errors = array();

  // Check for layers
  if (!is_array($map['layers'])) {
    $errors[] = t('Map contains no renderable layers.');
  }
  else {
    // Check layer projections
    foreach ($map['layers'] as $layer) {
      openlayers_layer_sanity_check(
        array('data' => $layer),
        $map['projection'],
        TRUE);
    }
  }

  // Check if any errors found to log
  if (count($errors) > 0 && $log_errors) {
    // Log the Error(s)
    watchdog('openlayers', implode(', ', $errors), array(), WATCHDOG_ERROR);
  }

  // Check if errors and return
  return (count($errors) > 0) ? $errors : FALSE;
}

/**
 * Models a projection, a description of a coordinate system.
 */
class openlayers_projection {
  /**
   * @var String Opaque primary key (should not be exposed but ctools API doesn't allow for a more rigid structure)
   */
  public $identifier;

  /**
   * @var String proj4 definition for on-the-fly reprojections of vector data.
   */
  private $definition;

  /**
   * @var number Leftmost boundary where coordinate system is valid
   */
  private $projectedextentleft;
  /**
   * @var number Bottommost boundary where coordinate system is valid
   */
  private $projectedextentbottom;
  /**
   * @var number Rightmost boundary where coordinate system is valid
   */
  private $projectedextentright;
  /**
   * @var number Topmost boundary where coordinate system is valid
   */
  private $projectedextenttop;

  public function __construct($identifier, $definition, $projectedextentleft, $projectedextentbottom, $projectedextentright, $projectedextenttop) {
    $this->identifier = $identifier;
    $this->definition = $definition;
    $this->projectedextentleft = $projectedextentleft;
    $this->projectedextentbottom = $projectedextentbottom;
    $this->projectedextentright = $projectedextentright;
    $this->projectedextenttop = $projectedextenttop;
  }

  /**
   * @return string Textual representation for the user.
   */
  public function getLocalizedMessage() {
    return $this->identifier;
  }

  /**
   * @return array Boundaries of projection in projected coordinates
   */
  public function getProjectedExtent() {
    return array_map('floatval', array($this->projectedextentleft, $this->projectedextentbottom, $this->projectedextentright, $this->projectedextenttop));
  }

  /**
   * @return String Proj4 style definition
   */
  public function getDefinition() {
    return $this->definition;
  }
}

/**
 * @param stdClass $record Object as returned by ctools_export_crud_load for type openlayers_projections
 * @return openlayers_projection
 */
function openlayers_projection_from_record($record) {
  return new openlayers_projection($record->identifier, $record->definition,
    $record->projectedextentleft, $record->projectedextentbottom, $record->projectedextentright, $record->projectedextenttop);
}

/**
 * @param String $authority Organization who defined the code
 * @param String $code Projection identifier
 * @return openlayers_projection
 */
function openlayers_get_projection($authority, $code) {
  static $projections;
  if (!isset($projections)) {
    $projections = array();
  }
  $identifier = $authority . ':' . $code;
  if (!array_key_exists($identifier, $projections)) {
    $projections[$identifier] = openlayers_get_projection_by_identifier($identifier);
  }
  return $projections[$identifier];
}

/**
 * @param String $identifier Identifier, such as “EPSG:4326”
 * @return openlayers_projection
 */
function openlayers_get_projection_by_identifier($identifier) {
  ctools_include('export');

  $records = ctools_export_load_object('openlayers_projections', 'names', array($identifier));

  if (empty($records)) {
    if(mb_strpos($identifier, ':')===FALSE){
      throw new Exception(t("Projection !projection lacks an authority code. Read http://drupal.org/node/1944074 for hints.", array(
        '!projection' => $identifier
      )));
    }
    throw new Exception(t("Projection !projection requested but not supported.", array(
      '!projection' => $identifier
    )));
  }
  return openlayers_projection_from_record($records[key($records)]);
}

/**
 * @return array<openlayers_projection>
 */
function openlayers_get_all_projections($reset = FALSE, $include_disabled = FALSE) {
  ctools_include('export');
  $projections = array();

  foreach (ctools_export_crud_load_all('openlayers_projections', $reset) as $record) {
    if (!$include_disabled && isset($record->disabled) && $record->disabled) {
      continue;
    }

    $projections[] = openlayers_projection_from_record($record);
  }

  return $projections;
}

/**
 * Rerenders form part “Layers & Styles” when map projections changes
 * @param array $form
 * @param array $form_state
 * @return array Form part to rerender
 */
function openlayers_map_layerlist(&$form, &$form_state) {
  module_load_include('inc', 'openlayers_ui', '/include/openlayers_ui.theme');

  // Remove group property as Drupal otherwise returns NULL instead of the rendered form part
  unset($form['layerstyles']['#group']);
  // Force a diffent, fixed identifier so that the form part can reliably replaced
  $form['layerstyles']['#id']= 'edit-layerstyles';
  // Rerender the form part
  return $form['layerstyles'];
}

/**
 * Get extent given projection
 *
 * Returns standard world-max-extents for common projections.
 * Layers implementing other projections and subsets of the
 * world should define their maxExtent in the layer array.
 *
 * @ingroup openlayers_api
 *
 * @param $authority
 *   String Organization code, such as EPSG.
 * @param $projection
 *   String of the projection value, such as 4326.
 * @return
 *   Array of maxExtent in OpenLayers toArray() format.
 */
function openlayers_get_extent($authority, $projection) {
  // Use projected extend from openlayers_projection instead but keep this function for now as it's marked as API
  return openlayers_get_projection($authority, $projection)->getProjectedExtent();
}

/**
 * Get resolution given projection
 *
 * Returns a default set of resolutions for standard
 * TMS, WMS, etc servers serving up common projections.
 * Layers supporting less common projections and resolutions
 * can easily define custom resolutions arrays.
 *
 * @ingroup openlayers_api
 *
 * @param $projection
 *   String specifying which projection this should take, like EPSG:900913.
 * @param $zoom_start
 *   Integer of first zoom level, default 0.
 * @param $zoom_end
 *   Integer of last zoom level, default FALSE.
 * @return
 *   Array of resolutions.
 */
function openlayers_get_resolutions($projection, $zoom_start = 0, $zoom_end = FALSE) {
  // TODO This is conceptually flawed and should not exist. Resolutions depend on the server in use in combination with the tile size.
  switch ($projection) {
    case 'EPSG:900913':
      // 16 zoom levels, taken from
      // openlayers.org TMS map
      $res = array(
        156543.0339,
        78271.51695,
        39135.758475,
        19567.8792375,
        9783.93961875,
        4891.969809375,
        2445.9849046875,
        1222.99245234375,
        611.496226171875,
        305.7481130859375,
        152.87405654296876,
        76.43702827148438,
        38.21851413574219,
        19.109257067871095,
        9.554628533935547,
        4.777314266967774,
        2.388657133483887,
        1.1943285667419434,
        0.5971642833709717,
        0.29858214169740677,
        0.14929107084870338,
        0.07464553542435169
      );
      break;
    case 'EPSG:4326':
      // 16 zoom levels, taken from
      // openlayers.org default WMS map
      $res = array(
        0.703125,
        0.3515625,
        0.17578125,
        0.087890625,
        0.0439453125,
        0.02197265625,
        0.010986328125,
        0.0054931640625,
        0.00274658203125,
        0.001373291015625,
        0.0006866455078125,
        0.00034332275390625,
        0.000171661376953125,
        0.0000858306884765625,
        0.00004291534423828125,
        0.000021457672119140625,
        0.000010728836059570312,
      );
      break;
    default:
      $res = array();
      break;
  }
  $length = ($zoom_end == FALSE) ? NULL : $zoom_end - $zoom_start;
  // By default this will not actually clip the array
  $resolutions = array_slice($res, $zoom_start, $length);

  return $resolutions;
}

/**
 * We define base classes in the core module.
 * All other parent classes can be autoloaded through ctools.
 */
class openlayers_behavior {
  var $options, $map;

  function __construct($options = array(), $map = array()) {
    $this->options = $options + $this->options_init();
    $this->map = $map;
  }

  /*
   * @return array of JavaScript functions required to be defined
   * in order for this function to work
   */
  function js_dependency() {
    return array();
  }

  function options_init() {
    return array();
  }

  /*
   * @param $defaults default values for the form as an array
   * @return a FormAPI form
   */
  function options_form($defaults = array()) {
    return array();
  }

  function render(&$map) {}
}

/**
 * We define base classes in the core module.
 * All other parent classes can be autoloaded through ctools.
 */
class openlayers_layer_type {

  /**
   * Stores the options for this layer.
   * @var array
   */
  public $data = array();

  /**
   * Stores the current map.
   * @var array
   */
  public $map;

  /**
   * Set configuration and store map.
   *
   * @param stdClass $layer
   *   Configuration object with the options for the layer.
   * @param $map
   *   Array with the current map.
   */
  function __construct($layer = array(), $map = array()) {
    foreach (array('name', 'title', 'description', 'data', 'export_type') as $k) {
      if (isset($layer->{$k})) {
        $this->{$k} = $layer->{$k};
      }
    }

    // Extend options with the defaults.
    $this->data += $this->options_init();

    $this->map = $map;
  }

  /**
   * @return array<openlayers_projection>
   *   List of all projections that are supported by the layer.
   */
  public function getProjections() {
    $projections = array();
    // TODO Ignore incomplete data until cause is fixed (projection for every layer set during migration)
    if (isset($this->data['projection'])) {
      foreach ($this->data['projection'] as $projection) {
        $projections[] = openlayers_get_projection_by_identifier($projection);
      }
    }
    return $projections;
  }

  /**
   * Provides the default options for the layer.
   *
   * @return
   *   An associative array with the default options.
   */
  function options_init() {
    return array(
      'layer_type' => get_class($this),
      'isBaseLayer' => TRUE,
      // TODO: Remove hard-coded resolutions
      'projection' => array('EPSG:900913'),
      'serverResolutions' => openlayers_get_resolutions('EPSG:900913'),
      'resolutions' => openlayers_get_resolutions('EPSG:900913'),
      'base_url' => NULL,
      'transitionEffect' => 'resize',
      'weight' => 0
    );
  }

  /**
   * Options form to configure layer instance options.
   *
   * @return
   *   Array with form elements.
   */
  function options_form($default = array()) {
    $allProjectionOptions = array();
    foreach (openlayers_get_all_projections() as $projection) {
      $allProjectionOptions[$projection->identifier] = $projection->getLocalizedMessage();
    }
    return array(
      'projection' => array(
        '#type' => 'select',
        '#title' => t('Projection'),
        '#multiple' => TRUE,
        '#options' => $allProjectionOptions,
        '#default_value' => isset($default->data['projection']) ?
          $default->data['projection'] :
          openlayers_get_projection('EPSG', '3857')->identifier
      ),
      'isBaseLayer' => array(
        '#type' => 'checkbox',
        '#title' => t('Base Layer'),
        '#description' => t('Uncheck to make this map an overlay'),
        '#default_value' => !empty($default->data['isBaseLayer']) ?
          $default->data['isBaseLayer'] : FALSE
      ),
    );
  }

  /**
   * Validate the options_form().
   *
   * @param array $default
   */
  function options_form_validate($form, &$form_state) {

  }

  /**
   * Submit the options_form().
   *
   * @param array $form
   * @param array $form_state
   */
  function options_form_submit($form, &$form_state) {
    $form_state['values']['data']['projection'] = array_keys($form_state['values']['data']['projection']);
    // TODO Resolutions should be processed only in layers
    // that provide a form field for it.
    $form_state['values']['data']['resolutions'] = array_map('floatval', array_values($form_state['values']['data']['resolutions']));
  }

  /**
   * Options form to configure layer-type-wide options.
   *
   * @return
   *   Array with form elements.
   */
  function settings_form() {
    return array();
  }

  /**
   * Render the layer and return the layer options.
   *
   * Has no return value.
   *
   * @param $map
   */
  function render(&$map) {}

}

/**
 * Implements hook_ctools_plugin_api().
 */
function openlayers_ctools_plugin_api($module, $api) {
  if ($module == "openlayers") {
    switch ($api) {
      case 'openlayers_maps':
        return array('version' => 1);

      case 'openlayers_layers':
        return array('version' => 1);

      case 'openlayers_projections':
        return array('version' => 1);

      case 'openlayers_styles':
        return array('version' => 1);

      case 'openlayers_layer_types':
        return array('version' => 1);

      case 'openlayers_behaviors':
        return array('version' => 1);

    }
  }
  elseif ($module == 'boxes' && $api == 'plugins') {
    return array('version' => 1);
  }
}

/**
 * Implements hook_openlayers_behaviors().
 */
function openlayers_openlayers_behaviors() {
  module_load_include('inc', 'openlayers', 'includes/openlayers.behaviors');
  return _openlayers_openlayers_behaviors();
}

/**
 * Implements hook_openlayers_layer_types().
 */
function openlayers_openlayers_layer_types() {
  module_load_include('inc', 'openlayers', 'includes/openlayers.layer_types');
  return _openlayers_openlayers_layer_types();
}

/**
 * Implements hook_openlayers_layers().
 */
function openlayers_openlayers_layers() {
  module_load_include('inc', 'openlayers', 'includes/openlayers.layers');
  return _openlayers_openlayers_layers();
}

/**
 * Implements hook_openlayers_projections().
 */
function openlayers_openlayers_projections() {
  module_load_include('inc', 'openlayers', 'includes/openlayers.projections');
  return _openlayers_openlayers_projections();
}

/**
 * Implements hook_openlayers_styles().
 */
function openlayers_openlayers_styles() {
  module_load_include('inc', 'openlayers', 'includes/openlayers.styles');
  return _openlayers_openlayers_styles();
}

/**
 * Implements hook_openlayers_maps().
 */
function openlayers_openlayers_maps() {
  module_load_include('inc', 'openlayers', 'includes/openlayers.maps');
  return _openlayers_openlayers_maps();
}

/**
 * Implements hook_boxes_plugins().
 */
function openlayers_boxes_plugins() {
  return array(
    'openlayers_simple' => array(
      'title' => 'OpenLayers',
      'handler' => array(
        'parent' => 'boxes_box',
        'class' => 'openlayers_simple',
        'file' => 'openlayers_simple.inc',
        'path' => drupal_get_path('module', 'openlayers') . '/includes/boxes'
      )
    )
  );
}

/**
 * Implements hook_libraries_info().
 */
function openlayers_libraries_info() {
  $libraries['openlayers'] = array(
    'name' => 'OpenLayers',
    'vendor url' => 'http://openlayers.org/',
    'download url' => 'http://openlayers.org/download/OpenLayers-2.12.tar.gz',
    'version arguments' => array(
      'file' => 'lib/OpenLayers.js',
      'pattern' => '/OpenLayers.VERSION_NUMBER="Release (.*?)"/',
      'lines' => 1000,
    ),
    'files' => array(
      'js' => array('OpenLayers.js'),
    ),
    'variants' => array(
      'original debug' => array(
        'files' => array(
          'js' => array(
            'OpenLayers.debug.js'
          ),
        ),
      ),
      'light' => array(
        'files' => array(
          'js' => array(
            'OpenLayers.light.js'
          ),
        ),
      ),
      'light debug' => array(
        'files' => array(
          'js' => array(
            'OpenLayers.light.debug.js'
          ),
        ),
      ),
      'mobile' => array(
        'files' => array(
          'js' => array(
            'OpenLayers.mobile.js'
          )
        ),
      ),
      'mobile debug' => array(
        'files' => array(
          'js' => array(
            'OpenLayers.mobile.debug.js'
          )
        ),
      ),
    ),
    'integration files' => array(
      'openlayers' => array(
        'js' => array('js/openlayers.js'),
        'css' => array('css/openlayers.css')
      ),
    ),
  );

  return $libraries;
}

/**
 * Alias Functions
 *
 * These functions temporarily map the alias the renamed 'map' functions to their
 * previous 'preset' functions to allow time for contrib modules to catch up.
 * These will eventually be removed
 *
 * These should be removed in 7.x-3.x
 */
function openlayers_build_preset($map = array())                        { return openlayers_build_map($map); }
function openlayers_preset_load($name = '', $reset = FALSE)             { return openlayers_map_load($name, $reset); }
function openlayers_render_preset_data($map = array(), $map_name = '')  { return openlayers_render_map_data($map, $map_name); }
function openlayers_presets($reset = FALSE)                             { return openlayers_maps($reset); }
function openlayers_preset_save($map)                                   { return openlayers_map_save($map); }
function openlayers_preset_delete($map)                                 { return openlayers_map_delete($map); }
function openlayers_preset_options($reset = FALSE)                      { return openlayers_map_options($reset); }
function openlayers_error_check_preset($map, $log_errors = TRUE)        { return openlayers_error_check_map($map, $log_errors); }
function openlayers_render_preset($map = '', $map_name = '') {
  if (is_array($map)) {
    return openlayers_render_preset_data($map, $map_name);
  }
  else {
    return openlayers_render_map($map);
  }
}
