openlayers.module

<?php
// $Id: openlayers.module,v 1.56 2009/05/28 06:55:49 binarybill Exp $

/**
 * @defgroup openlayers OpenLayers: Provides an API and Modules to interface with OpenLayers
 *
 * Provides an API and Modules to interface with OpenLayers.  Needs work...
 */
 
/**
 * @defgroup openlayers_api OpenLayers API: Specific functions that are part of the OpenLayers API
 *
 * Provides an API and Modules to interface with OpenLayers.  Needs work...
 */

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

/**
 * Map ID Prefix
 */
define('OPENLAYERS_MAP_ID_PREFIX', 'openlayers-map-auto-id');

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

/**
 * Implementation of hook_perm
 */
function openlayers_perm() {
  return array('administer openlayers');
}

/**
 * Implementation of hook_theme().
 */
function openlayers_theme() {
  return array(
    'openlayers_map' => array(
      'arguments' => array(
        'map' => array(),
      ),
    ),
  );
}

/**
 * Implementation of hook_menu
 */
function openlayers_menu() {
  $items = array();

  $items['admin/settings/openlayers'] = array(
    'title' => 'OpenLayers',
    'description' => t('OpenLayers administrative settings.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('openlayers_admin_settings'),
    'access arguments' => array('administer openlayers'),
    'file' => 'includes/openlayers.admin.inc',
  );
  $items['admin/settings/openlayers/settings'] = array(
    'title' => 'Settings',
    'description' => t('Main settings for OpenLayers.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('openlayers_admin_settings'),
    'access arguments' => array('administer openlayers'),
    'file' => 'includes/openlayers.admin.inc',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -20,
  );
  $items['admin/settings/openlayers/defaults'] = array(
    'title' => 'Defaults',
    'description' => t('Configure defaults for OpenLayers maps.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('openlayers_admin_default_settings'),
    'access arguments' => array('administer openlayers'),
    'file' => 'includes/openlayers.defaults.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => -10,
  );

  return $items;
}

/**
 * Theme function for openlayers_map
 */
function theme_openlayers_map($map = array()) {
  $output = '
    <div id="'. $map['id'] .'" class="openlayers-map"></div>
  ';
  return $output;
}

/**
 * Intialize OpenLayers
 *
 * Make sure that we have everything we need for OpenLayers
 *
 * @ingroup openlayers_api
 * @param $params
 *   Associative array of pamaters
 *   (none at the moment)
 * @return
 *   Boolean if intialization was succesful
 */
function openlayers_intialize() {
  $success = TRUE;
  
  // Include the OpenLayers JS
  // We need to check if it a local path or URL, but really only need to do it once.
  static $included = FALSE;
  if ($included == FALSE) {
    $path = check_plain(variable_get('openlayers_source', 'http://openlayers.org/dev/OpenLayers.js'));
    // Check for full URL
    if (valid_url($path, TRUE)) {
      // If URL, we have to manually include it in Drupal
      drupal_set_html_head('<script src="'. check_url($path) .'" type="text/javascript"></script>');
    }
    else {
      drupal_add_js($path);
    }
    
    // Add CSS
    drupal_add_css(drupal_get_path('module', 'openlayers') .'/openlayers.css', 'module');
    // Add base JS file
    drupal_add_js(drupal_get_path('module', 'openlayers') .'/js/openlayers.js', 'module');
    $included = TRUE;
  }
  
  return $success;
}

/**
 * Render Map
 *
 * Given perimeters, render an OpenLayers map
 *
 * @ingroup openlayers_api
 * @param $map
 *   Associative array of map paramters
 * @param $render
 *   Boolean whether to fully render (include theme and JS)
 *  
 * @return
 *   Boolean if successful
 */
function openlayers_render_map($map = array(), $render = TRUE) {
  // Check array
  if (!is_array($map)) {
    return FALSE;
  }
  
  // Intialize
  if (openlayers_intialize() == FALSE) {
    return FALSE;
  }
  
  // Check ID
  if (!$map['id']) {
    $map['id'] = _openlayers_create_map_id();
  }
  
  // If the map does not specify that it does not want to merge
  // in the default layers, then merge in defaults
  if (!$map['only_these_layers']) {
    $saved_defaults = variable_get('openlayers_defaults', array());
    $map = openlayers_merge_maps($saved_defaults, $map);
  }
  
  // Merge with module/system defaults
  $system_defaults = _openlayers_get_map_defaults();
  $map = openlayers_merge_maps($system_defaults, $map);
  
  // Process layers
  $map['layers'] = _openlayers_layers_process($map['layers'], $map);
  // Process behaviors
  $map['behaviors'] = _openlayers_behaviors_process($map['behaviors'], $map);
  
  // Hook for one last alter
  // hook_openlayers_map_alter(&$map = array())
  drupal_alter('openlayers_map', &$map);
  
  // Check our map for errors
  $map['errors'] = openlayers_error_check_map($map);
  // Add JS and theme if no errors found
  if (!$map['errors'] || $render == FALSE) {
    // Add map container to drupal JS settings
    $openlayers = array(
      'openlayers' => array(
        'maps' => array(
          $map['id'] => $map,
        ),
      ),
    );
    drupal_add_js($openlayers, 'setting');
    
    // Add themed HTML (no need for it to go to JS)
    $map['themed'] = theme('openlayers_map', $map);
  }
  
  // Return map with or without errors
  return $map;
}

/**
 * Get Layer Info
 *
 * Wrapper around layer info hook
 *
 * @ingroup openlayers_api
 * @param $reset
 *   Boolean whether to reset cache or not
 * @return
 *   array of layer info
 */
function openlayers_layers_get_info($reset = FALSE) {
  static $info = array();
  
  // If empty or reset, get info
  if (count($info) == 0 || $reset == TRUE) {
    // Hook layer info ($map is not necessary)
    // hook_openlayers_layers_info($map = array())
    $info = module_invoke_all('openlayers_layers_info');
  }

  return $info;
}

/**
 * Get Behavior Info
 *
 * Wrapper around behavior info hook
 *
 * @ingroup openlayers_api
 * @param $reset
 *   Boolean whether to reset cache or not
 * @return
 *   array of behavior info
 */
function openlayers_behaviors_get_info($reset = FALSE) {
  static $info = array();
  
  // If empty or reset, get info
  if (count($info) == 0 || $reset == TRUE) {
    // Hook behaviors info ($map is not necessary)
    // hook_openlayers_behaviors_info($map = array())
    $info = module_invoke_all('openlayers_behaviors_info');
  }

  return $info;
}

/**
 * OpenLayers Form Wrapper
 *
 * Get the form array to customize OpenLayers Maps.  This is wrapper
 * for a function in another file.  This keeps the form from
 * being parsed unnecesarily
 *
 * @ingroup openlayers_api
 * @param $defaults
 *   Array of defaults
 * @return
 *   Array of form items
 */
function openlayers_map_form($defaults = array()) {
  module_load_include('inc', 'openlayers', '/includes/openlayers.form');
  return _openlayers_map_form($defaults);
}

/**
 * OpenLayers Form Validate Wrapper
 *
 * Validate the form array to customize OpenLayers Maps.  This is wrapper
 * for a function in another file.  This keeps the form from
 * being parsed unnecesarily
 *
 * @ingroup openlayers_api
 * @param $form
 *   Array of of map form
 */
function openlayers_map_form_validate($map_form = array()) {
  module_load_include('inc', 'openlayers', '/includes/openlayers.form');
  return _openlayers_map_form_validate($map_form);
}

/**
 * OpenLayers Form to Map
 *
 * Converts Form submission to map array.  This is wrapper
 * for a function in another file.  This keeps the form from
 * being parsed unnecesarily
 *
 * @ingroup openlayers_api
 * @param $values
 *   Array of values to convert
 * @return
 *   Array of map
 */
function openlayers_convert_form_to_map($values = array()) {
  module_load_include('inc', 'openlayers', '/includes/openlayers.form');
  return _openlayers_convert_form_to_map($values);
}

/**
 * OpenLayers Convert Map to Form
 *
 * Converst map array to form defualts.  This is wrapper
 * for a function in another file.  This keeps the form from
 * being parsed unnecesarily
 *
 * @ingroup openlayers_api
 * @param $map
 *   Array of map values to convert
 * @return
 *   Array of form items
 */
function openlayers_convert_map_to_form($map = array()) {
  module_load_include('inc', 'openlayers', '/includes/openlayers.form');
  return _openlayers_convert_map_to_form($map);
}

/**
 * Merge Maps
 *
 * Recursive function to merge maps.  PHP's array_merge_recursive
 * creates unnecesary arrays to values if keys are the same.  This
 * function simply overwrites a value, even if the key is numeric.
 *
 * @ingroup openlayers_api
 * @param $map1
 *   Map array that holds values that are not preferred
 * @param $map2
 *   Map array that holds values that are preferred
 * @return
 *   map array
 */
function openlayers_merge_maps($map1 = array(), $map2 = array()) {
  // Check maps
  if (is_array($map1) && is_array($map2)) {
    foreach ($map2 as $k => $v) {
      if (isset($map1[$k]) && is_array($v) && is_array($map1[$k])) {
        $map1[$k] = openlayers_merge_maps($map1[$k], $v);
      }
      else {
        $map1[$k] = $v;
      }
    }
  }
  
  return $map1;
}

/**
 * Check Map Errors
 *
 * Checks map array for incompatibilities or errors.
 *
 * @ingroup openlayers_api
 * @param $map
 *   Map array
 * @param $log_errors
 *   Boolean whether to log erros
 * @return
 *   FALSE if passed. Array of descriptive errors if fail
 */
function openlayers_error_check_map($map, $log_errors = TRUE) {
  // @TODO: Instead of manually specifying projections, we should do a lookup on the projections in a big table to get variables that it should be checked against.
  // @TODO: For next release, make hook
  $errors = array();
  
  // Check layer projections
  foreach ($map['layers'] as $layer) {
    if ($layer['projection']) {
      if (!in_array($map['projection'], $layer['projection'])) {
        $errors[] = t('The layer %layer_name cannot work with the map projection: EPSG: %map_proj', array(
          '%layer_name' => $layer['name'],
          '%map_proj' => $map['projection'],
        ));
      }
    }
  }
  
  // If we are using a web spherical mercador projection and maxResolution 
  // and maxExtent are not set the map will not function. 
  if ($map['projection'] == '900913' || $map['projection'] == '3785') {
    if (!$map['options']['maxExtent'] || !$map['options']['maxResolution']) {
      $errors[] = t("You are using a web spherical mercador projection.  However maxExtent or maxResolution are not set. ");
    }
  }

  // If we are using a degree based projection, then check to make sure 
  // our bounds are not over 180/90 degrees
  if ($map['projection'] == '4326' || $map['projection'] == '4269') {
    if (  
      ($map['options']['maxExtent']['top']     && $map['options']['maxExtent']['top']    > 90)   ||
      ($map['options']['maxExtent']['bottom']  && $map['options']['maxExtent']['bottom'] < -90)  ||
      ($map['options']['maxExtent']['left']    && $map['options']['maxExtent']['left']   < -180) ||
      ($map['options']['maxExtent']['right']   && $map['options']['maxExtent']['right']  > 180)  ||
      ($map['options']['maxResoluton']         && $map['options']['maxResoluton']        > 180)
    ) {
      $errors[] = t("Your Maximum Extents are set greater than 180/90 degrees. Try Maximum Extent of: -180,180,-90,90 and a Maximum Resolution of 1.40625");
    }
  }
  
  // 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;
}

/**
 * Implemnetation of hook_openlayers_layers_handler_info
 */
function openlayers_openlayers_layers_handler_info($map = array()) {
  return array(
    'WMS' => array(
      'layer_handler' => 'WMS',
      'js_file' => drupal_get_path('module', 'openlayers') .'/js/openlayers.layers.js',
    ),
    'Vector' => array(
      'layer_handler' => 'Vector',
      'js_file' => drupal_get_path('module', 'openlayers') .'/js/openlayers.layers.js',
    ),
  );
}

/**
 * Implementation of hook_openlayers_layers_info
 */
function openlayers_openlayers_layers_info() {
  // Define info array
  $info['openlayers_default_wms'] = array(
    'name' => t('Default OpenLayers WMS'),
    'description' => t('A simple basemap to get you started'),
    'file' => drupal_get_path('module', 'openlayers') .'/includes/openlayers.layers.inc',
    'callback' => 'openlayers_process_layers',
    'projection' => array('4326', '900913', '4269'),
    'baselayer' => TRUE,
  );
  
  return $info;
}

/**
 * Process Layers
 *
 * Get full data for any layers and add handlers
 *
 * @param $layers
 *   Array of layers to process
 * @param $map
 *   Map array
 * @return
 *   Array of processed layers
 */
function _openlayers_layers_process($layers = array(), $map = array()) {
  if (!$layers){
    $layers = array();
  }
  
  $processed = array();
  
  // Get layer info array
  $layer_info = openlayers_layers_get_info();
  
  // Check to make sure our default layer is present, if it isn't then add it.
  if (!array_key_exists($map['default_layer'], $layers) && ($map['default_layer'])) {
    $layers[$map['default_layer']] = $map['default_layer'];
  }
  
  // Go through layers
  foreach ($layers as $k => $layer) {
    // Check if array, if array, just pass on
    if (is_array($layer)) {
      $processed[$k] = $layer;
    }
    else {
      // If not array, we want to include the file and call the function
      if (($info = $layer_info[$layer]) && is_array($layer_info[$layer])) {
        // Check if file exists
        if (is_file('./'. $info['file'])) {
          require_once './'. $info['file'];
          // Check for function
          if (function_exists($info['callback'])) {
            // Call function and give it the layer name
            $result = call_user_func_array($info['callback'], $layer);
            // Check for result
            if (isset($result) && is_array($result)) {
              $processed[$layer] = $result;
            }
          }
        }
      }
    }
  }

  // Add Handlers
  $handlers = module_invoke_all('openlayers_layers_handler_info', $map);
  // Go through processed
  foreach ($processed as $k => $l) {
    // Check for handler
    if (is_string($handlers[$l['type']]['layer_handler'])) {
      $processed[$k]['layer_handler'] = $handlers[$l['type']]['layer_handler'];
      // Include JS file if there is one
      if (is_string($handlers[$l['type']]['js_file'])) {
        drupal_add_js($handlers[$l['type']]['js_file'], 'module');
      }
    }
  }
  
  // Return processed
  return $processed;
}

/**
 * Process Behaviors
 *
 * Get full data for any behaviors and add handlers
 *
 * @param $layers
 *   Array of layers to process
 * @param $map
 *   Map array
 * @return
 *   Array of processed behaviors
 */
function _openlayers_behaviors_process($behaviors = array(), &$map) {
  // Check input
  if (!is_array($behaviors)) {
    $behaviors = array();
  }
  if (!is_array($map)) {
    $map = array();
  }
  
  // Initialized variables
  $processed = array();
  // Get behavior info array
  $behavior_info = openlayers_behaviors_get_info();
  
  // Go through behaviors
  foreach ($behaviors as $k => $behavior) {
    // Check if array, if array, just pass on
    if (is_array($behaviors)) {
      $processed[$behavior['id']] = $behavior;
    }
    else {
      $processed[$behavior] = array(
        'id' => $behavior,
        'type' => $behavior,
      );
    }
  }
  
  // Go through processed
  foreach ($processed as $bkey => $behavior) {
    $info = $behavior_info[$behavior['type']];
    
    if (is_file($info['file'])) {
      require_once $info['file'];
      // Check for function
      if (function_exists($info['callback'])) {
        // Call function and give it the behavior array
        $result = call_user_func_array($info['callback'], array($behavior, &$map));
        // Check for result
        if (isset($result) && is_array($result)) {
          $result['js_callback'] = $info['js_callback'];
          $processed[$bkey] = $result;
        }
      }
      // Include JavaScript
      if ($info['js_file']){
        drupal_add_js($info['js_file'], 'module');
      }
    }
  }
    
  // Return processed
  return $processed;
}

/**
 * Create Map ID
 *
 * Create a unique ID for any maps that are not assigned an ID
 *
 * @note
 *   Technically someone can assign a map ID identical
 *   to the one that is created
 * @return
 *   New map id
 */
function _openlayers_create_map_id() {
  // Set up variables
  $map_id = '';
  static $map_count = 0;
  // Put together ID
  $map_id = OPENLAYERS_MAP_ID_PREFIX .'-'. $map_count;
  // Add another
  $map_count += 1;
  // Return ID
  return $map_id;
}

/**
 * Get Map Defaults
 *
 * This is the "system defaults" that come with the module.
 * The goal of these defaults is so that a map can be rendered
 * simply by installing the OL API module.
 *
 * @return
 *   map array
 */
function _openlayers_get_map_defaults() {
  $map_default = array(
    'id' => _openlayers_create_map_id(),
    'projection' => '4326',
    'width' => 'auto',
    'default_layer' => 'openlayers_default_wms',
    'only_these_layers' => FALSE,
    'height' => '300px',
    'center' => array(
      'lat' => '0',
      'lon' => '0',
      'zoom' => '2',
    ),
    'options' => array(
      'displayProjection' => '4326',
    ),
    'controls' => array(
      'LayerSwitcher' => TRUE,
      'Navigation' => TRUE,
      'PanZoomBar' => TRUE,
      'MousePosition' => TRUE,
    ),
  );
  return $map_default;
}