Skip to content
location.module 57 KiB
Newer Older
Brandon Bergren's avatar
Brandon Bergren committed
/**
 * @file
 * Location module main routines.
 * An implementation of a universal API for location manipulation.  Provides functions for
 * postal_code proximity searching, deep-linking into online mapping services.  Currently,
 * some options are configured through an interface provided by location.module.
 */
Brandon Bergren's avatar
Brandon Bergren committed
define('LOCATION_PATH',                   drupal_get_path('module', 'location'));

define('LOCATION_LATLON_UNDEFINED',       0);
define('LOCATION_LATLON_USER_SUBMITTED',  1);
define('LOCATION_LATLON_GEOCODED_APPROX', 2);
Brandon Bergren's avatar
Brandon Bergren committed
define('LOCATION_LATLON_GEOCODED_EXACT',  3);
define('LOCATION_LATLON_JIT_GEOCODING',   4); // Force regeocoding immediately.
Brandon Bergren's avatar
Brandon Bergren committed
define('LOCATION_USER_DONT_COLLECT',      0);
define('LOCATION_USER_COLLECT',           1);
Brandon Bergren's avatar
Brandon Bergren committed
include_once LOCATION_PATH .'/location.inc';

Brandon Bergren's avatar
Brandon Bergren committed
 * Implementation of hook_menu().

  $items['location/autocomplete'] = array(
    'access arguments' => array('access content'),
    'page callback' => '_location_autocomplete',
    'type' => MENU_CALLBACK,
  );

  $items['admin/settings/location'] = array(
    'title' => 'Location',
    'description' => 'Settings for Location module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('location_admin_settings'),
    'file' => 'location.admin.inc',
    'access arguments' => array('administer site configuration'),
  );
  $items['admin/settings/location/main'] = array(
    'title' => 'Main settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/settings/location/maplinking'] = array(
    'title' => 'Map links',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('location_map_link_options_form'),
    'access arguments' => array('administer site configuration'),
    'file' => 'location.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
  $items['admin/settings/location/geocoding'] = array(
    'title' => 'Geocoding options',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('location_geocoding_options_form'),
    'access arguments' => array('administer site configuration'),
    'file' => 'location.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
  );
  $items['admin/settings/location/geocoding/%/%'] = array(
    'page callback' => 'location_geocoding_parameters_page',
    'page arguments' => array(4, 5),
    'access arguments' => array('administer site configuration'),
    'type' => MENU_CALLBACK,
  );
  $items['admin/settings/location/util'] = array(
    'title' => 'Location utilities',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('location_util_form'),
    'access arguments' => array('administer site configuration'),
    'file' => 'location.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 3,
  );
/**
 *
 */
function location_api_variant() {
Brandon Bergren's avatar
Brandon Bergren committed
/**
 * Implementation of hook_perm().
 */
function location_perm() {
  return array(
    'submit latitude/longitude',
  );
}

Bruno Massa's avatar
Bruno Massa committed
/**
Brandon Bergren's avatar
Brandon Bergren committed
 * Implementation of hook_help().
Bruno Massa's avatar
Bruno Massa committed
 *
Brandon Bergren's avatar
Brandon Bergren committed
 * @TODO: check/fix this: admin/content/configure/types (still use %? still same url?)
Bruno Massa's avatar
Bruno Massa committed
 */
function location_help($path, $arg) {
  switch ($path) {
    case 'admin/help#location':
      $output = '<p>'. t('The location module allows you to associate a geographic location with content and users. Users can do proximity searches by postal code.  This is useful for organizing communities that have a geographic presence.') .'</p>';
      $output .= '<p>'. t('To administer locative information for content, use the content type administration page.  To support most location enabled features, you will need to install the country specific include file.  To support postal code proximity searches for a particular country, you will need a database dump of postal code data for that country.  As of June 2007 only U.S. and German postal codes are supported.') .'</p>';
      $output .= t('<p>You can</p>
<ul>
<li>administer locative information at <a href="@admin-node-configure-types">Administer &gt;&gt; Content management &gt;&gt; Content types</a> to configure a type and see the locative information.</li>
<li>administer location at <a href="@admin-settings-location">Administer &gt;&gt; Site configuration &gt;&gt; Location</a>.</li>
<li>use a database dump for a U.S. and/or German postal codes table that can be found at <a href="@external-http-cvs-drupal-org">zipcode database</a>.</li>
', array('@admin-node-configure-types' => url('admin/content/types'), '@admin-settings-location' => url('admin/settings/location'), '@external-http-cvs-drupal-org' => 'http://cvs.drupal.org/viewcvs/drupal/contributions/modules/location/database/')) .'</ul>';
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@location">Location page</a>.', array('@location' => 'http://www.drupal.org/handbook/modules/location/')) .'</p>';
      return $output;
Bruno Massa's avatar
Bruno Massa committed
  }
}
Brandon Bergren's avatar
Brandon Bergren committed
/**
 * Implementation of hook_elements().
 */
function location_elements() {
  return array(
    'location_element' => array(
      '#input' => TRUE,
      '#process' => array('_location_expand_location'),
Brandon Bergren's avatar
Brandon Bergren committed
      '#tree' => TRUE,
      '#location_settings' => array(),
      '#attributes' => array('class' => 'location'),
      '#element_validate' => array('location_element_validate'),
Brandon Bergren's avatar
Brandon Bergren committed
    ),
    'location_settings' => array(
      '#input' => TRUE,
      '#process' => array('_location_expand_location_settings'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#tree' => TRUE,
    ),
/**
 * Theme function to fixup location elements.
Brandon Bergren's avatar
Brandon Bergren committed
 * @ingroup themable
 */
function theme_location_element($element) {
  // Prevent spurious "Array" from appearing.
  unset($element['#value']);
  return theme('fieldset', $element);
}

/**
 * Implementation of hook_theme().
 */
function location_theme() {
  return array(
    'location_settings' => array('arguments' => array('element')),
    'locations' => array(
      'template' => 'locations',
      'arguments' => array(
        'locations' => NULL,
        'hide' => array(),
      ),
    ),
    'location' => array(
      'template' => 'location',
      'arguments' => array(
        'location' => NULL,
        'hide' => array(),
      ),
    ),
    'location_latitude_dms' => array('arguments' => array('latitude')),
    'location_longitude_dms' => array('arguments' => array('longitude')),
    'location_map_link_options' => array('arguments' => array('form')),
    'location_geocoding_options' => array('arguments' => array('form')),
    'location_element' => array('arguments' => array('element')),
    'location_distance' => array('template' => 'location_distance', 'arguments' => array('distance' => 0, 'units' => 'km')),
/**
 * Implementation of hook_views_api().
 */
function location_views_api() {
  return array(
    'api' => 2,
    //'path' => drupal_get_path('module', 'location') .'/includes',
  );
}

Brandon Bergren's avatar
Brandon Bergren committed
/**
 * Process a location element.
 */
function _location_expand_location($element) {
  drupal_add_css(drupal_get_path('module', 'location') .'/location.css');
Brandon Bergren's avatar
Brandon Bergren committed
  $element['#tree'] = TRUE;
Brandon Bergren's avatar
Brandon Bergren committed
  if (!isset($element['#title'])) {
    $element['#title'] = t('Location');
  }
  if (empty($element['#location_settings'])) {
    $element['#location_settings'] = array();
Brandon Bergren's avatar
Brandon Bergren committed
  }
  if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
Brandon Bergren's avatar
Brandon Bergren committed
  }
  $element['location_settings'] = array(
    '#type' => 'value',
    '#value' => $element['#location_settings'],
  );
  // Ensure this isn't accidentally used later.
  unset($element['#location_settings']);
  // Make a reference to the settings.
  $settings =& $element['location_settings']['#value'];

  if (isset($element['#default_value']['lid']) && $element['#default_value']['lid']) {
    // Keep track of the old LID.
    $element['lid'] = array(
      '#type' => 'value',
      '#value' => $element['#default_value']['lid'],
    );
  }
  // Fill in missing defaults, etc.
  location_normalize_settings($settings, $element['#required']);
  $defaults = location_empty_location($settings);
  if (isset($element['lid']['#value']) && $element['lid']['#value']) {
    $defaults = location_load_location($element['lid']['#value']);
  $fsettings =& $settings['form']['fields'];

  // $settings -> $settings['form']['fields']

  // $defaults is not necessarily what we want.
  // If #default_value was already specified, we want to use that, because
  // otherwise we will lose our values on preview!
  $fdefaults = $defaults;
  foreach ($element['#default_value'] as $k => $v) {
    $fdefaults[$k] = $v;
  }

  foreach ($fields as $field => $title) {
    if (!isset($element[$field])) {
      // @@@ Permission check hook?
      if ($fsettings[$field]['collect'] != 0) {
        $element[$field] = location_invoke_locationapi($fdefaults[$field], 'field_expand', $field, $fsettings[$field]['collect'], $fdefaults);
        $element[$field]['#weight'] = (int)$fsettings[$field]['weight'];
      // Only include 'Street Additional' if 'Street' is 'allowed' or 'required'
      if ($field == 'street' && $fsettings[$field]['collect']) {
        $element['additional'] = location_invoke_locationapi($defaults['additional'], 'field_expand', 'additional', 1, $defaults);
        $element['additional']['#weight'] = (int)$fsettings['additional']['weight'];
  // @@@ Split into submit and view permissions?
  if (user_access('submit latitude/longitude') && $fsettings['locpick']['collect']) {
    $element['locpick'] = array('#weight' => $fsettings['locpick']['weight']);
    if (location_has_coordinates($defaults, FALSE)) {
      $element['locpick']['current'] = array(
        '#type' => 'fieldset',
        '#title' => t('Current coordinates'),
      );
      $element['locpick']['current']['current_latitude'] = array(
        '#type' => 'item',
        '#title' => t('Latitude'),
        '#value' => $defaults['latitude'],
      );
      $element['locpick']['current']['current_longitude'] = array(
        '#type' => 'item',
        '#title' => t('Longitude'),
        '#value' => $defaults['longitude'],
      switch($defaults['source']) {
        case LOCATION_LATLON_USER_SUBMITTED:
          $source = t('User-submitted');
          break;
        case LOCATION_LATLON_GEOCODED_APPROX:
          $source = t('Geocoded (Postal code level)');
          break;
        case LOCATION_LATLON_GEOCODED_EXACT:
          $source = t('Geocoded (Exact)');
      }
      $element['locpick']['current']['current_source'] = array(
        '#type' => 'item',
        '#title' => t('Source'),
        '#value' => $source,
      );
    }

    $element['locpick']['user_latitude'] = array(
      '#type' => 'textfield',
      '#title' => t('Latitude'),
      '#default_value' => isset($element['#default_value']['locpick']['user_latitude']) ? $element['#default_value']['locpick']['user_latitude'] : '',
      '#size' => 16,
      '#attributes' => array('class' => 'container-inline'),
      '#maxlength' => 20,
    );
    $element['locpick']['user_longitude'] = array(
      '#type' => 'textfield',
      '#title' => t('Longitude'),
      '#default_value' => isset($element['#default_value']['locpick']['user_longitude']) ? $element['#default_value']['locpick']['user_longitude'] : '',
      '#size' => 16,
      '#maxlength' => 20,
    );
Brandon Bergren's avatar
Brandon Bergren committed

    $element['locpick']['instructions'] = array(
      '#type' => 'markup',
      '#weight' => 1,
      '#prefix' => '<div class=\'description\'>',
      '#value' => '<br /><br />' . t('If you wish to supply your own latitude and longitude, you may enter them above.  If you leave these fields blank, the system will attempt to determine a latitude and longitude for you from the entered address.  To have the system recalculate your location from the address, for example if you change the address, delete the values for these fields.'),
    if (function_exists('gmap_get_auto_mapid') && variable_get('location_usegmap', FALSE)) {
      $mapid = gmap_get_auto_mapid();
      $map = gmap_parse_macro(variable_get('location_locpick_macro', '[gmap]'));
      $map['id'] = $mapid;
      $map['points'] = array();
      $map['pointsOverlays'] = array();
      $map['lines'] = array();

      $map['behavior']['locpick'] = TRUE;
      $map['behavior']['collapsehack'] = TRUE;
      // Use previous coordinates to center the map.
      if (location_has_coordinates($defaults, FALSE)) {
        $map['latitude'] = (float)$defaults['latitude'];
        $map['longitude'] = (float)$defaults['longitude'];
          'latitude' => $defaults['latitude'],
          'longitude' => $defaults['longitude'],
          'markername' => 'small gray', // @@@ Settable?
          'offset' => 0,
          'opts' => array(
            'clickable' => FALSE,
          ),
        );
      }
      $element['locpick']['user_latitude']['#map'] = $mapid;
      gmap_widget_setup($element['locpick']['user_latitude'], 'locpick_latitude');
      $element['locpick']['user_longitude']['#map'] = $mapid;
      gmap_widget_setup($element['locpick']['user_longitude'], 'locpick_longitude');

      $element['locpick']['map'] = array(
        '#type' => 'gmap',
        '#map' => $mapid,
        '#settings' => $map,
      $element['locpick']['map_instructions'] = array(
        '#type' => 'markup',
        '#weight' => 2,
        '#prefix' => '<div class=\'description\'>',
Brandon Bergren's avatar
Brandon Bergren committed
        '#value' => t('You may set the location by clicking on the map, or dragging the location marker.  To clear the location and cause it to be recalculated, click on the marker.'),
  if (isset($defaults['lid']) && !empty($defaults['lid'])) {
    $element['delete_location'] = array(
      '#type' => 'checkbox',
      '#title' => t('Delete'),
      '#default_value' => isset($fdefaults['delete_location']) ? $fdefaults['delete_location'] : FALSE,
      '#description' => t('Check this box to delete this location.'),
    );
Brandon Bergren's avatar
Brandon Bergren committed
  $element += _element_info('fieldset');
function _location_expand_location_settings(&$element) {
Brandon Bergren's avatar
Brandon Bergren committed
  // Set a value for the fieldset that doesn't interfere with rendering and doesn't generate a warning.
  $element['#tree'] = TRUE;
  $element['#theme'] = 'location_settings';
  if (!isset($element['#title'])) {
    $element['#title'] = t('Location Fields');
  }
  if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
    $element['#default_value'] = array();
  }
  // Force #tree on.
  $element['#tree'] = TRUE;
  $defaults = $element['#default_value'];
  if (!isset($defaults) || !is_array($defaults)) {
    $defaults = array();
  }
  $temp = location_invoke_locationapi($element, 'defaults');
  foreach ($temp as $k => $v) {
    if (!isset($defaults[$k])) {
      $defaults[$k] = array();
    }
    $defaults[$k] = array_merge($v, $defaults[$k]);
  }

  $fields = location_field_names();
  // Options for fields.
  $options = array(
    0 => t('Do not collect'),
    1 => t('Allow'),
    2 => t('Require'),
    4 => t('Force Default'), // Need to consider the new "defaults" when saving.
  foreach ($fields as $field => $title) {
    $element[$field] = array(
      '#type' => 'fieldset',
      '#tree' => TRUE,
    );
    $element[$field]['name'] = array(
      '#type' => 'item',
      '#value' => $title,
    );
    $element[$field]['collect'] = array(
      '#type' => 'select',
      '#default_value' => $defaults[$field]['collect'],
      '#options' => $options,
    );

    $temp = $defaults[$field]['default'];
    $element[$field]['default'] = location_invoke_locationapi($temp, 'field_expand', $field, 1, $defaults);
    $defaults[$field]['default'] = $temp;

    $element[$field]['weight'] = array(
      '#type' => 'weight',
      '#delta' => 100,
      '#default_value' => $defaults[$field]['weight'],
  // 'Street Additional' field should depend on 'Street' setting.
  // It should never be required and should only display when the street field is 'allowed' or 'required'
//  unset($element['additional']);

  // @@@ Alter here?

function theme_location_settings($element) {
  $rows = array();
  $header = array(
    array(
      'data' => t('Name'),
      'colspan' => 2,
    ),
    t('Collect'), t('Default'), t('Weight'));

  // Force country required.
  $element['country']['default']['#required'] = TRUE;
  unset($element['country']['collect']['#options'][0]);

  foreach (element_children($element) as $key) {
    $element[$key]['weight']['#attributes']['class'] = 'location-settings-weight';
    unset($element[$key]['default']['#title']);
    $row = array();
    $row[] = array('data' => '', 'class' => 'location-settings-drag');
    $row[] = drupal_render($element[$key]['name']);
    $row[] = drupal_render($element[$key]['collect']);
    $row[] = drupal_render($element[$key]['default']);
    $row[] = array('data' => drupal_render($element[$key]['weight']), 'class' => 'delta-order');

    $rows[] = array(
      '#weight' => (int)$element[$key]['weight']['#value'],
  uasort($rows, 'element_sort');
  foreach ($rows as $k => $v) {
    unset($rows[$k]['#weight']);
  }

  drupal_add_tabledrag('location-settings-table', 'order', 'sibling', 'location-settings-weight');

  $output = theme('table', $header, $rows, array('id' => 'location-settings-table'));
  //return theme('form_element', $element, $element['#children']);
  return $output;
function location_field_names($all = FALSE) {
  static $allfields;
  if ($all) {
    if (empty($allfields)) {
      $dummy = array();
      $allfields = location_invoke_locationapi($dummy, 'fields');
      $virtual = location_invoke_locationapi($dummy, 'virtual fields');
      $allfields += $virtual;
      $fields = location_invoke_locationapi($dummy, 'fields');
    }
    return $fields;
/**
 * Implementation of hook_locationapi().
 */
Brandon Bergren's avatar
Brandon Bergren committed
function location_locationapi(&$obj, $op, $a3 = NULL, $a4 = NULL, $a5 = NULL) {
  switch ($op) {
    case 'fields':
      return array('name' => t('Location name'), 'street' => t('Street location'), 'additional' => t('Additional'), 'city' => t('City'), 'province' => t('State/Province'), 'postal_code' => t('Postal code'), 'country' => t('Country'), 'locpick' => t('Coordinate Chooser'));
      return array('province_name' => t('Province name'), 'country_name' => t('Country name'), 'map_link' => t('Map link'), 'coords' => t('Coordinates'));
Brandon Bergren's avatar
Brandon Bergren committed
      return array(
        'lid'         => array('default' => FALSE),
        'name'        => array('default' => '', 'collect' => 1, 'weight' => 2),
        'street'      => array('default' => '', 'collect' => 1, 'weight' => 4),
        'additional'  => array('default' => '', 'collect' => 1, 'weight' => 6),
        'city'        => array('default' => '', 'collect' => 0, 'weight' => 8),
        'province'    => array('default' => '', 'collect' => 0, 'weight' => 10),
        'postal_code' => array('default' => '', 'collect' => 0, 'weight' => 12),
        'country'     => array('default' => variable_get('location_default_country', 'us'), 'collect' => 1, 'weight' => 14), // @@@ Fix weight?
        'locpick'     => array('default' => FALSE, 'collect' => 1, 'weight' => 20, 'nodiff' => TRUE),
        'latitude'    => array('default' => 0),
        'longitude'   => array('default' => 0),
        'source'      => array('default' => LOCATION_LATLON_UNDEFINED),
        'is_primary'  => array('default' => 0), // @@@
        'delete_location' => array('default' => FALSE, 'nodiff' => TRUE),
    case 'validate':
      if (!empty($obj['country'])) {
        if (!empty($obj['province'])) {
          $provinces = location_get_provinces($obj['country']);
          $found = FALSE;
          $p = strtoupper($obj['province']);
          foreach ($provinces as $k => $v) {
            if ($p == strtoupper($k) || $p == strtoupper($v)) {
              $found = TRUE;
              break;
            }
          }
          if (!$found) {
            form_error($a3['province'], t('The specified province was not found in the specified country.'));
          }
        }
      }
      if (!empty($obj['locpick']) && is_array($obj['locpick'])) {
        // Can't specify just latitude or just longitude.
        if (_location_floats_are_equal($obj['locpick']['user_latitude'], 0) xor _location_floats_are_equal($obj['locpick']['user_longitude'], 0)) {
          $ref = &$a3['locpick']['user_latitude'];
          if (_location_floats_are_equal($obj['locpick']['user_longitude'], 0)) {
            $ref = &$a3['locpick']['user_longitude'];
          }
          form_error($ref, t('You must fill out both latitude and longitude or you must leave them both blank.'));
    case 'field_expand':
      switch ($a3) {
        case 'name':
          return array(
            '#type'           => 'textfield',
            '#title'          => t('Location name'),
            '#default_value'  => $obj,
            '#size'           => 64,
            '#maxlength'      => 64,
            '#description'    => t('e.g. a place of business, venue, meeting point'),
            '#attributes'     => NULL,
            '#required'       => ($a4 == 2),
          );
        case 'street':
          return array(
            '#type'           => 'textfield',
            '#title'          => t('Street'),
            '#default_value'  => $obj,
            '#size'           => 64,
            '#maxlength'      => 64,
            '#required'       => ($a4 == 2),
          );
        // Additional is linked to street.
        case 'additional':
          return array(
            '#type'           => 'textfield',
            '#title'          => t('Additional'),
            '#default_value'  => $obj,
            '#size'           => 64,
            '#maxlength'      => 64,
            // Required is forced OFF because this is technically part of street.
        case 'city':
          return array(
            '#type'           => 'textfield',
            '#title'          => t('City'),
            '#default_value'  => $obj,
            '#size'           => 64,
            '#maxlength'      => 64,
            '#description'    => NULL,
            '#attributes'     => NULL,
            '#required'       => ($a4 == 2),
          );
          drupal_add_js(drupal_get_path('module', 'location') .'/location_autocomplete.js');
          $country = $a5['country'] ? $a5['country'] : variable_get('location_default_country', 'us');
          return array(
            '#type' => 'textfield',
            '#title' => t('State/Province'),
            '#autocomplete_path' => 'location/autocomplete/'. $country,
            '#default_value' => $obj,
            '#size' => 64,
            '#maxlength' => 64,
            '#description' => NULL,
            // Used by province autocompletion js.
            '#attributes' => array('class' => 'location_auto_province'),
            '#required' => ($a4 == 2),
          // Force default.
            return array(
              '#type' => 'value',
            $options = array_merge(array('' => t('Please select'), 'xx' => t('NOT LISTED')), location_get_iso3166_list());
            return array(
              '#type'           => 'select',
              '#title'          => t('Country'),
              '#default_value'  => $obj,
              '#options'        => $options,
              '#description'    => NULL,
              '#required'       => ($a4 == 2),
              // Used by province autocompletion js.
              '#attributes'     => array('class' => 'location_auto_country'),
            );
          }
          break;
        case 'postal_code':
          return array(
            '#type'           => 'textfield',
            '#title'          => t('Postal code'),
            '#default_value'  => $obj,
            '#size'           => 16,
            '#maxlength'      => 16,
            '#required'       => ($a4 == 2),
          );
    case 'isunchanged':
      switch ($a3) {
        case 'lid':
          // Consider 0, NULL, and FALSE to be equivilent.
          if (empty($obj[$a3]) && empty($a4)) {
            return TRUE;
          }
          break;
        case 'latitude':
        case 'longitude':
          if (_location_floats_are_equal($obj[$a3], $a4)) {
            return TRUE;
          }
          break;

        case 'country':
          // Consider '  ' and '' to be equivilent, due to us storing country
          // as char(2) in the database.
          if (trim($obj[$a3]) == trim($a4)) {
            return TRUE;
          }
          break;

        case 'province_name':
        case 'country_name':
        case 'map_link':
        case 'coords':
          // Always considered unchanged.
function location_geocoding_parameters_page($country_iso, $service) {
  drupal_set_title(t('Configure parameters for %service geocoding', array('%service' => $service)));
  $breadcrumbs = drupal_get_breadcrumb();
  $breadcrumbs[] = l('location', 'admin/settings/location');
  $breadcrumbs[] = l('geocoding', 'admin/settings/location/geocoding');
  $countries = location_get_iso3166_list();
  $breadcrumbs[] = l($countries[$country_iso], 'admin/settings/location/geocoding', array('fragment' => $country_iso));
  drupal_set_breadcrumb($breadcrumbs);
  return drupal_get_form('location_geocoding_parameters_form', $country_iso, $service);
}
Bruno Massa's avatar
Bruno Massa committed

function location_geocoding_parameters_form(&$form_state, $country_iso, $service) {
  location_load_country($country_iso);
  $geocode_settings_form_function_specific = 'location_geocode_'. $country_iso .'_'. $service .'_settings';
  $geocode_settings_form_function_general = $service .'_geocode_settings';
  if (function_exists($geocode_settings_form_function_specific)) {
    return system_settings_form($geocode_settings_form_function_specific());
  }
  location_load_geocoder($service);
  if (function_exists($geocode_settings_form_function_general)) {
    return system_settings_form($geocode_settings_form_function_general());
  }
  else {
    return system_settings_form(array(
      '#type' => 'markup',
      '#value' => t('No configuration parameters are necessary, or a form to take such paramters has not been implemented.')
    ));
Bruno Massa's avatar
Bruno Massa committed
  }
}

/**
 * Load associated locations.
 *
 * @param $id The identifier to match. (An integer.)
 * @param $key The search key for {location_instance} (usually vid or uid.)
 * @return An array of loaded locations.
 */
function location_load_locations($id, $key = 'vid') {
  if (empty($id)) {
    // If the id is 0 or '' (or false), force returning early.
    // Otherwise, this could accidentally load a huge amount of data
    // by accident. 0 and '' are reserved for "not applicable."
    return array();
  }
  if ($key == 'genid') {
    $result = db_query('SELECT lid FROM {location_instance} WHERE '. db_escape_table($key) ." = '%s'", $id);
  }
  else {
    $result = db_query('SELECT lid FROM {location_instance} WHERE '. db_escape_table($key) .' = %d', $id);
  }
  $locations = array();
  while ($lid = db_fetch_object($result)) {
    $locations[] = location_load_location($lid->lid);
  return $locations;
/**
 * Save associated locations.
 *
 * @param $locations The associated locations.
 *   You can pass an empty array to remove all location references associated
 *   with the given criteria. This is useful if you are about to delete an object,
 *   and need Location to clean up any locations that are no longer referenced.
 *
 * @param $criteria An array of instance criteria to save as.
 *   Example: array('genid' => 'my_custom_1111')
 */
function location_save_locations(&$locations, $criteria) {
  if (isset($locations) && is_array($locations) && !empty($criteria) && is_array($criteria)) {
    foreach (array_keys($locations) as $key) {
      location_save($locations[$key], TRUE, $criteria);
    }
    $columns = array();
    $placeholders = array();
    $qfrags = array();
    $args = array();
    foreach (array('nid' => '%d', 'vid' => '%d', 'uid' => '%d', 'genid' => "'%s'") as $key => $placeholder) {
      if (isset($criteria[$key])) {
        $columns[] = $key;
        $placeholders[] = $placeholder;
        $args[] = $criteria[$key];
        $qfrags[] = "$key = $placeholder";
      }
    }
    $querybase = 'FROM {location_instance} WHERE '. implode(' AND ', $qfrags);

    $oldlids = array();
    $newlids = array();
    // Find affected lids.
    $query = "SELECT lid $querybase";
    $result = db_query($query, $args);
    while ($t = db_fetch_object($result)) {
      $oldlids[] = $t->lid;
    }

    $query = "DELETE $querybase";
    db_query($query, $args);
    // Tack on the lid.
    $columns[] = 'lid';
    $placeholders[] = '%d';

    foreach ($locations as $location) {
      // Don't save "empty" locations.
      // location_save() explicitly returns FALSE for empty locations,
      // so it should be ok to rely on the data type.
      if ($location['lid'] !== FALSE) {
        $args[] = $location['lid'];
        $newlids[] = $location['lid'];
        db_query('INSERT INTO {location_instance} ('. implode(', ', $columns) .') VALUES ('. implode(', ', $placeholders) .')', $args);
        array_pop($args);
      }

    // Check anything that dropped a reference during this operation.
    foreach (array_diff($oldlids, $newlids) as $check) {
      // An instance may have been deleted. Check reference count.
      $count = db_result(db_query('SELECT COUNT(*) FROM {location_instance} WHERE lid = %d', $check));
      if ($count !== FALSE && $count == 0) {
        watchdog('location', t('Deleting unreferenced location with LID %lid.', array('%lid' => $check)));
        $location = array('lid' => $check);
        location_invoke_locationapi($location, 'delete');
        db_query('DELETE FROM {location} WHERE lid = %d', $location['lid']);
      }
    }
Brandon Bergren's avatar
Brandon Bergren committed
/**
 * Load a single location by lid.
 *
 * @param $lid Location ID to load.
 * @return A location array.
Brandon Bergren's avatar
Brandon Bergren committed
 */
function location_load_location($lid) {
  $location = db_fetch_array(db_query('SELECT * FROM {location} WHERE lid = %d', $lid));
  // @@@ Just thought of this, but I am not certain it is a good idea...
  if (empty($location)) {
    $location = array('lid' => $lid);
Bruno Massa's avatar
Bruno Massa committed
  }
  if (isset($location['source']) && $location['source'] == LOCATION_LATLON_USER_SUBMITTED) {
    // Set up location chooser or lat/lon fields from the stored location.
    $location['locpick'] = array(
      'user_latitude' => $location['latitude'],
      'user_longitude' => $location['longitude'],
    );
  }

  // JIT Geocoding
  // Geocodes during load, useful with bulk imports.
  if (isset($location['source']) && $location['source'] == LOCATION_LATLON_JIT_GEOCODING) {
    if (variable_get('location_jit_geocoding', FALSE)) {
      _location_geo_logic($location, array('street' => 1), array());
      db_query("UPDATE {location} SET latitude = '%f', longitude = '%f', source = %d WHERE lid = %d",
      $location['latitude'],
      $location['longitude'],
      $location['source'],
      $location['lid']);
    }
  }

  $location['province_name'] = '';
  $location['country_name'] = '';

  if (!empty($location['country'])) {
    $location['country_name'] = location_country_name($location['country']);

    if (!empty($location['province'])) {
      $location['province_name'] = location_province_name($location['country'], $location['province']);
    }
  }

  $location = array_merge($location, location_invoke_locationapi($location, 'load', $lid));
Brandon Bergren's avatar
Brandon Bergren committed
/**
 * Create a list of states from a given country.
 *
 * @param $country
 *   String. The country code
 * @param $string
 *   String (optional). The state name typed by user
 * @return
 *   Javascript array. List of states
 */
function _location_autocomplete($country, $string = '') {
  $counter = 0;
Brandon Bergren's avatar
Brandon Bergren committed
  $string   = strtolower($string);
  $string   = '/^'. $string .'/';
  $matches  = array();

  if (strpos($country, ',') !== FALSE) {
    // Multiple countries specified.
    $provinces = array();
    $country = explode(',', $country);
    foreach($country as $c) {
      $provinces = $provinces + location_get_provinces($c);
    }
  }
  else {
    $provinces = location_get_provinces($country);
  }
Brandon Bergren's avatar
Brandon Bergren committed

  if (!empty($provinces)) {
    while (list($code, $name) = each($provinces)) {
Brandon Bergren's avatar
Brandon Bergren committed
      if ($counter < 5) {
        if (preg_match($string, strtolower($name))) {
Brandon Bergren's avatar
Brandon Bergren committed
          ++$counter;
        }
      }
    }
  }
  echo drupal_to_js($matches);
Bruno Massa's avatar
Bruno Massa committed

Brandon Bergren's avatar
Brandon Bergren committed
 * Epsilon test.
 * Helper function for seeing if two floats are equal.  We could use other functions, but all
 * of them belong to libraries that do not come standard with PHP out of the box.
 */
function _location_floats_are_equal($x, $y) {
  $x = floatval($x);
  $y = floatval($y);
  return (abs(max($x, $y) - min($x, $y)) < pow(10, -6));
}
Bruno Massa's avatar
Bruno Massa committed

/**
 * Check whether a location has coordinates or not.
 *
 * @param $location The location to check.
 * @param $canonical Is this a location that is fully saved?
 *   If set to TRUE, only the source will be checked.
 */
function location_has_coordinates($location, $canonical = FALSE) {
  // Locations that have been fully saved have an up to date source.
  if ($canonical) {
    return ($location['source'] != LOCATION_LATLON_UNDEFINED);
  }

  // Otherwise, we need to do the full checks.

  // If latitude or longitude are empty / missing
  if (empty($location['latitude']) || empty($location['longitude'])) {
    return FALSE;
  }

  // If the latitude or longitude are zeroed (Although it could be a good idea to relax this slightly sometimes)
  if (_location_floats_are_equal($location['latitude'], 0.0) || _location_floats_are_equal($location['longitude'], 0.0)) {
    return FALSE;
  }

  return TRUE;
}

/**
 * Invoke a hook_locationapi() operation on all modules.
 *
 * @param &$location A location object.
 * @param $op A string containing the name of the locationapi operation.
Brandon Bergren's avatar
Brandon Bergren committed
 * @param $a3, $a4, $a5 Arguments to pass on to the hook.
 * @return The returned value of the invoked hooks.
 */
Brandon Bergren's avatar
Brandon Bergren committed
function location_invoke_locationapi(&$location, $op, $a3 = NULL, $a4 = NULL, $a5 = NULL) {
  $return = array();
  foreach (module_implements('locationapi') as $name) {
    $function = $name .'_locationapi';
Brandon Bergren's avatar
Brandon Bergren committed
    $result = $function($location, $op, $a3, $a4, $a5);
    if (isset($result) && is_array($result)) {
      $return = array_merge($return, $result);
    }
    else if (isset($result)) {
      $return[] = $result;
    }
Bruno Massa's avatar
Bruno Massa committed
}