You are here

kml.module in KML 6

Same filename and directory in other branches
  1. 8 kml.module
  2. 5 kml.module
  3. 6.2 kml.module
  4. 7 kml.module

KML Module

Creates Google Earth KML feeds from location-enabled nodes in Drupal. @author Dan Karran (geodaniel) <dan at karran dot net>

File

kml.module
View source
<?php

/**
 * KML Module
 *
 * @file
 * Creates Google Earth KML feeds from location-enabled nodes in Drupal.
 * @author Dan Karran (geodaniel) <dan at karran dot net>
 */

/**
 * Implementation of hook_help().
 */
function kml_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/help#kml':
      $output = t('<p>The KML module allows you to create KML feeds from Drupal for use in Google Earth. It requires the use of the location module to assign geographic locations to nodes.</p>');
      $output .= t('<p>You can</p>
<ul>
<li>administer KML feed options at !admin_link.</li>
</ul>', array(
        '!admin_link' => l('admin &raquo; settings &raquo; kml', 'admin/settings/kml', array(
          'html' => TRUE,
        )),
      ));
      return $output;
    case 'admin/modules#description':
      return t('Module to feed KML from Drupal to Google Earth');
  }
}

/**
 * Implementation of hook_perm().
 */
function kml_perm() {
  return array(
    'access kml',
    'administer kml',
  );
}

/**
 * Implementation of hook_menu().
 */
function kml_menu() {
  $items['kml'] = array(
    'title' => 'KML feeds',
    'page callback' => 'kml_interface',
    'access arguments' => array(
      'access kml',
    ),
    'type' => MENU_SUGGESTED_ITEM,
  );
  $items['admin/settings/kml'] = array(
    'title' => 'KML',
    'description' => 'Settings for the KML module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'kml_admin_settings',
    ),
    'access arguments' => array(
      'administer kml',
    ),
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}

/**
* Implementation of hook_simpletest().
*/
function kml_simpletest() {
  return array_keys(file_scan_directory(drupal_get_path('module', 'kml') . '/tests', '\\.test$'));
}

/**
 * Default callback
 */
function kml_interface($a = NULL, $b = NULL, $c = NULL, $d = NULL, $e = NULL) {
  $sortmode = variable_get('kml_sortmode', 'n.created');
  $sortorder = variable_get('kml_sortorder', 'asc');
  if ($a == 'node') {
    if (is_numeric($b)) {

      /* Single node */
      if ($c == 'networklink') {
        $attributes['kml_feed'] = url('kml/node/' . $b, array(
          'absolute' => TRUE,
        ));
        kml_networklink($attributes);
      }
      else {
        $cache_name = 'kml:node:' . $b;
        $nodes = db_query("SELECT n.nid, n.created FROM {node} n INNER JOIN {location_instance} l ON n.vid = l.vid WHERE n.status = 1 AND n.nid = %d", $b);
        _kml_feed_check_access($nodes, $attributes, $cache_name);
      }
    }
    else {
      if ($b == 'networklink') {

        /* Network link for all location-enabled nodes */
        $attributes['kml_feed'] = url('kml/node/', array(
          'absolute' => TRUE,
        ));
        kml_networklink($attributes);
      }
      else {

        /* All location-enabled nodes */
        $cache_name = 'kml:node';
        $nodes = db_query("SELECT n.nid, n.created FROM {node} n INNER JOIN {location_instance} l ON n.vid = l.vid WHERE n.status = 1 ORDER BY %s %s", $sortmode, $sortorder);
        _kml_feed_check_access($nodes, $attributes, $cache_name);
      }
    }
  }
  else {
    if ($a == 'term' && is_numeric($b)) {
      if (module_exists('taxonomy')) {
        if ($term = taxonomy_get_term($b)) {
          $tag = $term->name;
          if ($c == 'networklink') {

            /* Network link for all location-enabled nodes tagged with a certain term */
            $attributes['kml_feed'] = url('kml/term/' . $b . '/', array(
              'absolute' => TRUE,
            ));
            $attributes['title'] = t('Tag: %tag', array(
              '%tag' => $tag,
            ));
            $attributes['description'] = t('Nodes tagged with %tag', array(
              '%tag' => $tag,
            ));
            kml_networklink($attributes);
          }
          else {

            /* All location-enabled nodes tagged with a certain term */
            $cache_name = 'kml:term:' . $b;
            $nodes = db_query("SELECT n.nid, n.created FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid INNER JOIN {location_instance} l ON n.vid = l.vid WHERE n.status = 1 AND tn.tid = %d ORDER BY %s %s", $b, $sortmode, $sortorder);
            $attributes['title'] = t('Tag: %tag', array(
              '%tag' => $tag,
            ));
            $attributes['description'] = t('Nodes tagged with %tag', array(
              '%tag' => $tag,
            ));
            _kml_feed_check_access($nodes, $attributes, $cache_name);
          }
        }
        else {
          drupal_not_found();
        }
      }
    }
    else {
      if ($a == 'group' && is_numeric($b)) {
        if (module_exists('og')) {
          $groupnode = node_load($b);
          if (og_is_group_type($groupnode->type)) {

            // TODO: check also to see if user is part of group
            $group_name = $groupnode->title;
            if ($c == 'networklink') {

              /* Network link for all location-enabled nodes in a certain group */
              $attributes['kml_feed'] = url('kml/group/' . $b . '/', array(
                'absolute' => TRUE,
              ));
              $attributes['title'] = t('Group: %group', array(
                '%group' => $group_name,
              ));
              $attributes['description'] = t('Nodes in %group', array(
                '%group' => $group_name,
              ));
              kml_networklink($attributes);
            }
            else {

              /* All location-enabled nodes in a certain group */
              $cache_name = 'kml:group:' . $b;
              $nodes = db_query("SELECT n.nid, n.created FROM {node} n INNER JOIN {node_access} na ON n.nid = na.nid INNER JOIN {location_instance} l ON n.vid = l.vid WHERE n.status = 1 AND na.gid = %d ORDER BY %s %s", $b, $sortmode, $sortorder);
              $attributes['title'] = t('Group: %group', array(
                '%group' => $group_name,
              ));
              $attributes['description'] = t('Nodes in %group', array(
                '%group' => $group_name,
              ));
              _kml_feed_check_access($nodes, $attributes, $cache_name);
            }
          }
          else {
            drupal_not_found();
          }
        }
        else {
          drupal_not_found();
        }
      }
      else {
        if ($a == 'view' && $b) {
          if (module_exists('views')) {
            $b = check_plain($b);
            $view = module_invoke('views', 'get_view', $b);
            if ($view->url) {
              if ($c == 'networklink') {

                /* Network link for nodes in a view */
                if ($view->page_type == 'kml') {

                  // a view intended to be used as KML feed
                  $attributes['kml_feed'] = url($view->url . '/', array(
                    'absolute' => TRUE,
                  ));
                }
                else {

                  // any other view
                  $attributes['kml_feed'] = url('kml/view/' . $view->name . '/', array(
                    'absolute' => TRUE,
                  ));
                }
                $attributes['title'] = t('%view_title', array(
                  '%view_title' => $view->page_title,
                ));
                $attributes['description'] = t('%view_description', array(
                  '%view_description' => $view->description,
                ));
                kml_networklink($attributes);
              }
              else {

                /* Invoke views module to get the page defined by the view */
                if ($view->page_type == 'kml') {
                  print module_invoke('views', 'build_view', 'page', $view, $args);
                }
                else {
                  $nodes = module_invoke('views', 'build_view', 'items', $view, $args);
                  theme_kml_feed($view, $nodes['items'], 'page');
                }
              }
            }
            else {
              drupal_not_found();
            }
          }
          else {
            drupal_not_found();
          }
        }
        else {
          if ($a == 'search' && $b == 'node' && $c) {
            if (module_exists('search')) {
              $type = check_plain($b);
              $keys = check_plain($c);
              if ($d == 'networklink') {

                /* Network link for all nodes returned by a certain search */
                $attributes['kml_feed'] = url('kml/search/' . $type . '/' . $keys . '/', array(
                  'absolute' => TRUE,
                ));
                $attributes['title'] = t('Search: %terms', array(
                  '%terms' => $keys,
                ));
                $attributes['description'] = t('Nodes matching %terms', array(
                  '%terms' => $keys,
                ));
                kml_networklink($attributes);
              }
              else {

                /* All nodes returned by a certain search */
                $nodes = module_invoke($type, 'search', 'search', $keys);
                $attributes['title'] = t('Search: %terms', array(
                  '%terms' => $keys,
                ));
                $attributes['description'] = t('Nodes matching %terms', array(
                  '%terms' => $keys,
                ));
                _kml_feed($nodes, $attributes);
              }
            }
            else {
              drupal_not_found();
            }
          }
          else {
            if ($a == 'user') {

              // TODO: user locations
            }
            else {
              kml_page();
            }
          }
        }
      }
    }
  }
}

/**
 * Form for settings page
 */
function kml_admin_settings() {
  $form['individual_nodes'] = array(
    '#type' => 'fieldset',
    '#title' => t('Node display'),
    '#description' => t('You can add a KML link to the bottom of location-enabled nodes using these options.'),
  );
  $form['individual_nodes']['kml_nodelink'] = array(
    '#type' => 'checkbox',
    '#title' => t('Add KML link to footer of each full node that has coordinates'),
    '#default_value' => variable_get('kml_nodelink', 1),
  );
  $form['individual_nodes']['kml_nodelink_teaser'] = array(
    '#type' => 'checkbox',
    '#title' => t('Add KML link to footer of each node teaser that has coordinates'),
    '#default_value' => variable_get('kml_nodelink_teaser', 0),
  );
  $form['multiple_nodes'] = array(
    '#type' => 'fieldset',
    '#title' => t('Node ordering'),
    '#description' => t('Choose the order in which multiple nodes are sent in KML feeds. Google Earth should display them in this order, allowing a user to fly through them.'),
  );
  $sort_modes = array(
    'n.created' => 'Time created',
    'n.changed' => 'Time changed',
    'u.name' => 'Author',
    'n.title' => 'Title',
  );
  $sort_orders = array(
    'asc' => 'Ascending',
    'desc' => 'Descending',
  );
  $form['multiple_nodes']['kml_sortmode'] = array(
    '#type' => 'radios',
    '#title' => t('Sort mode'),
    '#default_value' => variable_get('kml_sortmode', 'n.created'),
    '#options' => $sort_modes,
    '#description' => t('Note that this will not affect KML feeds defined through views module.'),
  );
  $form['multiple_nodes']['kml_sortorder'] = array(
    '#type' => 'radios',
    '#title' => t('Sort order'),
    '#default_value' => variable_get('kml_sortorder', 'asc'),
    '#options' => $sort_orders,
  );
  $form['time_information'] = array(
    '#type' => 'fieldset',
    '#title' => t('Time information'),
    '#description' => t('Choose the type of time information to include in the KML feeds. This will allow users to filter the information by time in Google Earth.'),
  );
  $time_types = array(
    'created' => 'Time created',
    'changed' => 'Time changed',
    'none' => 'None',
  );
  $form['time_information']['kml_timetype'] = array(
    '#type' => 'radios',
    '#title' => t('Timestamp'),
    '#default_value' => variable_get('kml_timetype', 'created'),
    '#options' => $time_types,
  );
  $form['style'] = array(
    '#type' => 'fieldset',
    '#title' => t('Display style'),
    '#description' => t('Customise how nodes will be displayed in Google Earth.'),
  );
  $form['style']['kml_sitelogo_url'] = array(
    '#type' => 'textfield',
    '#title' => t('Site logo'),
    '#default_value' => variable_get('kml_sitelogo_url', ''),
    '#description' => t("Add the URL to an image to include a logo in the top left of the Google Earth screen."),
    '#length' => 40,
  );
  $form['style']['kml_altitude'] = array(
    '#type' => 'textfield',
    '#title' => t('Node altitude (meters)'),
    '#default_value' => variable_get('kml_altitude', 0),
    '#description' => t("This will extrude a node from the earth by the specified distance. A line will join the icon to the position on earth. It is useful if you don't want to obscure an area with its own icon."),
    '#size' => '2',
  );
  $form['style']['kml_extrude'] = array(
    '#type' => 'checkbox',
    '#title' => t('Extrude nodes'),
    '#default_value' => variable_get('kml_extrude', 0),
    '#description' => t("This will draw a line joining the icon to the position on earth if an altitude is set above."),
  );
  $content_types = node_get_types('names');
  foreach ($content_types as $name => $title) {
    $form['style']['placemarks'][$name] = array(
      '#type' => 'fieldset',
      '#title' => t('Google Earth Highlight placemark url for ') . $title,
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form['style']['placemarks'][$name]['kml_highlightplacemark_url_' . $name] = array(
      '#type' => 'textfield',
      '#title' => t('Google Earth Highlight placemark url'),
      '#default_value' => variable_get('kml_highlightplacemark_url_' . $name, 'http://maps.google.com/mapfiles/kml/paddle/red-stars.png'),
      '#description' => t("Add the URL to an image to use as Google Earth highlight placemark."),
      '#length' => 40,
    );
    $form['style']['placemarks'][$name]['kml_normalplacemark_url_' . $name] = array(
      '#type' => 'textfield',
      '#title' => t('Google Earth normal placemark url'),
      '#default_value' => variable_get('kml_normalplacemark_url_' . $name, 'http://maps.google.com/mapfiles/kml/paddle/wht-blank.png'),
      '#description' => t("Add the URL to an image to use as Google Earth normal placemark."),
      '#length' => 40,
    );
  }
  $form['network_links'] = array(
    '#type' => 'fieldset',
    '#title' => t('Network links'),
    '#description' => t('Network links are simply a pointer to the current version of the KML feed. A user need only download a Network link once, and have the data within it automatically refresh to the latest data from this Drupal site.'),
  );
  $use_networklinks = array(
    1 => 'Yes',
    0 => 'No',
  );
  $form['network_links']['kml_usenetworklinks'] = array(
    '#type' => 'radios',
    '#title' => t('Use network links?'),
    '#default_value' => variable_get('kml_usenetworklinks', 1),
    '#description' => t('Plain feeds are good if the user does not need the information to stay up-to-date. Note that you may have problems when using network links on a site which requires users to log in, and you will require a module such as securesite to provide a standard HTTP Auth login option for any KML feeds.'),
    '#options' => $use_networklinks,
  );
  $refresh_modes = array(
    'never' => 'Never',
    'onStop' => 'onStop',
    'onRequest' => 'onRequest',
  );
  $form['network_links']['kml_refreshmode'] = array(
    '#type' => 'radios',
    '#title' => t('Refresh mode'),
    '#default_value' => variable_get('kml_refreshmode', 'onStop'),
    '#description' => t('When information should be requested from the server.'),
    '#options' => $refresh_modes,
  );
  $form['network_links']['kml_refreshtime'] = array(
    '#type' => 'textfield',
    '#title' => t('Refresh delay'),
    '#default_value' => variable_get('kml_refreshtime', 60),
    '#description' => t("The number of seconds to wait before refreshing the Network Link after the view in Google Earth has stopped moving. Requires 'onStop' to be selected above."),
    '#maxlength' => '3',
    '#size' => '2',
  );
  $form['file_format'] = array(
    '#type' => 'fieldset',
    '#title' => t('File format'),
    '#description' => t('Specify the settings to be used for the KML files produced.'),
  );

  //$kmz_disabled = 0; // TODO: add KMZ file support - http://drupal.org/node/289832
  $kmz_disabled = 1;
  $kmz_note = t('<em><a href="http://drupal.org/node/289832">KMZ support</a> is not yet available.</em>');
  $form['file_format']['kml_usekmz'] = array(
    '#type' => 'checkbox',
    '#title' => t('Compress KML into KMZ?'),
    '#default_value' => variable_get('kml_usekmz', 0),
    '#description' => t('By letting the module compress your KML files into KMZ format you will save a lot on bandwidth and your users will be able to download your data quicker.') . ' ' . $kmz_note,
    '#disabled' => $kmz_disabled,
  );

  // TODO: allow for token module support in here so sites can create dynamic filenames
  $form['file_format']['kml_filename'] = array(
    '#type' => 'textfield',
    '#title' => t('KML filename'),
    '#default_value' => variable_get('kml_filename', 'nodes'),
    '#description' => t('Filename to use for KML feeds. Note that .kml or .kmz will be appended to the above string, so do not include that yourself.'),
  );
  $form['file_format']['kml_networklinkfilename'] = array(
    '#type' => 'textfield',
    '#title' => t('Network Link filename'),
    '#default_value' => variable_get('kml_networklinkfilename', 'networklink'),
    '#description' => t('Filename to use for KML network links. Note that .kml or .kmz will be appended to the above string, so do not include that yourself.'),
  );
  return system_settings_form($form);
}

/**
 * Displays directory of KML feeds available
 */
function kml_page() {

  // TODO: actually list all terms, groups, etc with links to their feeds
  $content = t('<p>You can view all of the location-enabled content from this site using Google Earth. Simply click the icon and if prompted, tell your browser to open the file with Google Earth.</p>') . "\n";
  $url = 'kml/node';
  if (variable_get('kml_usenetworklinks', 1)) {
    $url .= '/networklink';
  }
  $feeds[] = theme('kml_link', url($url)) . ' ' . l('All content from this site', $url);
  $content .= theme('item_list', $feeds);
  print theme('page', $content);
}

/**
 * Implementation of hook_block()
 */
function kml_block($op = 'list', $delta = 0, $edit = array()) {
  switch ($op) {
    case 'list':
      $blocks[0]['info'] = t('KML links for node groupings');
      return $blocks;
    case 'configure':
      $form = array();
      if ($delta == 0) {
      }
      return $form;
    case 'save':
      if ($delta == 0) {
      }
      return;
    case 'view':
    default:
      switch ($delta) {
        case 0:
          $block['subject'] = t('View this content in Google Earth');
          $block['content'] = kml_block_content($delta);
          $block['weight'] = -6;
          $block['enabled'] = 1;
          $block['region'] = 'right';
          break;
      }
      return $block;
  }
}

/**
 * Function to generate block content
 */
function kml_block_content($block) {
  if ($block == 0) {

    // TODO: allow admin to choose if network links are used, or normal links
    if (arg(0) == 'taxonomy' && arg(1) == 'term') {
      $path = 'kml/term/' . arg(2);
      if (variable_get('kml_usenetworklinks', 1)) {
        $path .= '/networklink';
      }
      return theme('kml_link', url($path));
    }
    else {
      if (module_exists('og') && ($groupnode = og_get_group_context())) {
        if ($groupnode->nid == arg(1)) {

          // show block only on group homepage
          $path = 'kml/group/' . $groupnode->nid;
          if (variable_get('kml_usenetworklinks', 1)) {
            $path .= '/networklink';
          }
          return theme('kml_link', url($path));
        }
      }
      else {
        if (arg(0) == 'search' && arg(1) == 'node' && arg(2)) {
          $path = 'kml/search/' . arg(1) . '/' . arg(2);
          if (variable_get('kml_usenetworklinks', 1)) {
            $path .= '/networklink';
          }
          return theme('kml_link', url($path));
        }
      }
    }
  }
}

/**
 * Implementation of hook_link(). Adds KML links to individual nodes
 */
function kml_link($type, $node = 0, $main = 0) {
  $links = array();

  // if node type is location enabled and has location info
  if (variable_get('location_maxnum_' . $node->type, 0) && $node->location) {
    if (variable_get('kml_nodelink', 1) && $main == 0 || variable_get('kml_nodelink_teaser', 0) && $main == 1) {
      $url = 'kml/node/' . $node->nid;
      if (variable_get('kml_usenetworklinks', 1)) {
        $url .= '/networklink';
      }
      $links['kml_link_node'] = array(
        'title' => t('KML'),
        'href' => $url,
        'attributes' => array(
          'title' => t('View location in Google Earth'),
        ),
      );
    }
  }
  return $links;
}
function kml_theme() {
  return array(
    'kml_link' => array(
      'arguments' => array(
        'link' => NULL,
      ),
    ),
    'kml_icon' => array(
      'arguments' => array(
        'url' => NULL,
        'type' => NULL,
      ),
    ),
    'kml_placemark_description' => array(
      'arguments' => array(
        'item' => NULL,
        'link' => NULL,
      ),
    ),
    'kml_feed' => array(
      'arguments' => array(
        'view' => NULL,
        'nodes' => NULL,
        'type' => NULL,
      ),
    ),
  );
}

/**
 * Standardised KML link
 */
function theme_kml_link($link) {
  return theme_kml_icon($link, 'kml');
}

/**
 * Display the KML icon
 */
function theme_kml_icon($url, $type) {
  $icon = drupal_get_path('module', 'kml') . '/images/kml.gif';
  $text = 'View in Google Earth';
  if ($image = theme('image', $icon, $text, $text)) {
    return '<a href="' . check_url($url) . '" class="feed-icon">' . $image . '</a>';
  }
}

/**
 * Send KML feed.
 */
function _kml_send_feed($output) {
  $extension = variable_get('kml_usekmz', 0) == 1 ? '.kmz' : '.kml';
  $filename = variable_get('kml_filename', 'nodes') . $extension;
  drupal_set_header('Content-Type: application/vnd.google-earth.kml+xml');
  drupal_set_header('Content-Disposition: attachment; filename="' . $filename . '"');
  print $output;
  module_invoke_all('exit');
  exit;
}

/*
 * Look in the cache to see if there is something.
 * Check that the nodes we've been asked to build are not newer.
 * Send the cahe if OK, or rebuild and save.
 */
function _kml_feed_check_access($nodes, $attributes, $cache_name) {

  /* Get the node list and take note of latest change */
  $max_changed = 0;
  $hash_list = array();
  $nodes_array = array();
  while ($node = db_fetch_object($nodes)) {
    $node = node_load($node->nid);
    if (node_access('view', $node)) {
      $nodes_array[] = $node;
      $hash_list[] = $node->nid;
      if ($node->changed > $max_changed) {
        $max_changed = $node->changed;
      }
    }
  }
  sort($hash_list);
  $cid = $cache_name . ':' . md5(join('+', $hash_list));
  _kml_feed_check_cache($nodes_array, $attributes, $cid, $max_changed);
}
function _kml_feed_check_cache($nodes, $attributes, $cid, $changed) {
  $cache = cache_get($cid);
  if ($cache && $cache->created > $changed) {

    /* Cache is good, return it */
    _kml_send_feed($cache->data['output']);
  }

  /* Not in the cache, or it's out of date - rebuild. */
  _kml_feed($nodes, $attributes, $cid);
}

/**
 * Displays a KML feed containing all location-enabled nodes.
 */
function _kml_feed($nodes, $attributes = array(), $cid = FALSE) {
  $title = $attributes['title'] ? $attributes['title'] : variable_get('site_name', 'drupal');
  $link = $attributes['link'] ? $attributes['link'] : url('', array(
    'absolute' => TRUE,
  ));
  $description = $attributes['description'] ? $attributes['description'] : variable_get('site_mission', '');
  $channel['title'] = $title;
  $channel['link'] = $link;
  $channel['description'] = $description;
  $output = _kml_format_feed($nodes, $channel);
  if ($cid) {
    $cache['output'] = $output;
    cache_set($cid, $cache, 'cache', time() + variable_get('kml_cache_lifetime', 3600));
  }
  _kml_send_feed($output);
}

/**
 * Provide views plugins for creating KML feeds.
 */
function kml_views_style_plugins() {
  return array(
    'kml' => array(
      'name' => t('KML feed'),
      'theme' => 'kml_feed',
      'needs_table_header' => TRUE,
      'needs_fields' => TRUE,
    ),
  );
}
function kml_views_feed_argument($op, &$view, $arg) {
  if ($op == 'argument' && $arg == 'kml') {
    $view->page_type = 'kml';
  }
  else {
    if ($op == 'post_view') {
      $path = views_post_view_make_url($view, $arg, 'kml');
      if (variable_get('kml_usenetworklinks', 1)) {

        // TODO: need a better way of dealing with network links using args.
        $path = 'kml/view/' . $view->name . '/networklink';
      }
      $url = url($path, array(
        'absolute' => TRUE,
      ));
      drupal_add_link(array(
        'rel' => 'alternate',
        'type' => 'application/vnd.google-earth.kml+xml',
        'title' => t('kml'),
        'href' => $url,
      ));
      return theme('kml_icon', $url, 'kml');
    }
  }
}

/**
 * Views plugin that displays the KML feed for views
 */
function theme_kml_feed($view, $nodes, $type) {
  if ($type == 'block') {
    return;
  }
  $attributes['title'] = t('%view_title', array(
    '%view_title' => $view->page_title,
  ));
  $attributes['description'] = t('%view_description', array(
    '%view_description' => $view->description,
  ));

  /* Get changed date for cache. */
  $max_changed = 0;
  $hash_list = array();
  foreach ($nodes as $node) {
    $hash_list[] = $node->nid;

    /* Get the changed for this node for cache */
    $changed = db_result(db_query("SELECT n.changed FROM {node} n WHERE n.nid = %d", $node->nid));
    if ($changed > $max_changed) {
      $max_changed = $changed;
    }
  }
  sort($hash_list);
  $cache_name = 'kml:view:' . $view->vid;
  $cid = $cache_name . ':' . md5(join('+', $hash_list));

  /* There's no need to check access because views does that for us */
  _kml_feed_check_cache($nodes, $attributes, $cid, $max_changed);
}

/**
 * A generic function for generating KML (Keyhole Markup Language for Google Earth) feeds from a set of nodes.
 *
 * @param $nodes
 *   An object as returned by db_query() which contains the nid field.
 * @param $channel
 *   An associative array containing title, link, description and other keys.
 *   The link should be an absolute URL.
 */
function _kml_format_feed($nodes_array = array(), $channel = array()) {
  $items = '';

  // get altitude to display placemarks at, and whether to extrude
  $altitude = variable_get('kml_altitude', 0);
  $kml_extra['altitudeMode'] = 'relativeToGround';

  // TODO: make configurable?
  if ($kml_extrude = variable_get('kml_extrude', 0)) {
    $kml_extra['extrude'] = variable_get('kml_extrude', 0);
  }
  if ($nodes_array) {

    /*
     * If we're going to load nodes, might as well remember then for
     * later use.
     */
    foreach ($nodes_array as &$node) {

      // Load the specified node:
      if ($node = node_load($node->nid)) {

        // TODO: using db_rewrite_sql above may make node_load redundant
        // TODO: check to make sure the node has geo properties
        $link = url('node/' . $node->nid, array(
          'absolute' => TRUE,
        ));

        // Filter and prepare node teaser
        if (node_hook($node, 'view')) {
          node_invoke($node, 'view', TRUE, FALSE);
        }
        else {
          $node = node_prepare($node, TRUE);
        }

        // Allow modules to change $node->teaser before viewing.
        node_invoke_nodeapi($node, 'view', TRUE, FALSE);

        // Allow modules to add additional item fields
        $extra = node_invoke_nodeapi($node, 'kml item');
        $extra = array_merge($extra, $kml_extra);

        // TODO: if node has more than one location, add a folder containing the locations as placemarks
        $items .= kml_format_placemark($node, $link, $altitude, $extra);
      }
    }
  }
  $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" . "<kml xmlns=\"http://earth.google.com/kml/2.1\">\n" . " <Document>\n";

  // TODO: only include styles for node types included in the feed
  $content_types = node_get_types('names');
  foreach ($content_types as $name => $title) {
    $output .= ' <Style id="highlightPlacemark_' . $name . '">' . "\n";
    $output .= '  <IconStyle>' . "\n";
    $output .= '   <Icon>' . "\n";
    $output .= '    <href>' . variable_get('kml_highlightplacemark_url_' . $name, 'http://maps.google.com/mapfiles/kml/paddle/red-stars.png') . '</href>' . "\n";
    $output .= '   </Icon>' . "\n";
    $output .= '  </IconStyle>' . "\n";
    $output .= ' </Style>' . "\n";
    $output .= ' <Style id="normalPlacemark_' . $name . '">' . "\n";
    $output .= '  <IconStyle>' . "\n";
    $output .= '   <Icon>' . "\n";
    $output .= '    <href>' . variable_get('kml_normalplacemark_url_' . $name, 'http://maps.google.com/mapfiles/kml/paddle/wht-blank.png') . '</href>' . "\n";
    $output .= '   </Icon>' . "\n";
    $output .= '  </IconStyle>' . "\n";
    $output .= ' </Style>' . "\n";
    $output .= ' <StyleMap id="myStyleMap_' . $name . '">' . "\n";
    $output .= '  <Pair>' . "\n";
    $output .= '   <key>normal</key>' . "\n";
    $output .= '   <styleUrl>#normalPlacemark_' . $name . '</styleUrl>' . "\n";
    $output .= '  </Pair>' . "\n";
    $output .= '  <Pair>' . "\n";
    $output .= '   <key>highlight</key>' . "\n";
    $output .= '   <styleUrl>#highlightPlacemark_' . $name . '</styleUrl>' . "\n";
    $output .= '  </Pair>' . "\n";
    $output .= ' </StyleMap>' . "\n";
  }

  // See if any modules want to add anything to the file.
  // Pass in the list of nodes we'll present for their reference.
  $feed_extras = module_invoke_all('kml_feed_extras', $nodes_array);
  foreach ($feed_extras as $feed_extra) {
    $output .= $feed_extra;
  }

  // Add site logo if one is specified
  if ($sitelogo = variable_get('kml_sitelogo_url', '')) {
    $output .= "  <ScreenOverlay>\n" . "   <name><![CDATA[" . t('%site logo', array(
      '%site' => $channel['title'],
    )) . "]]></name>\n" . "   <Icon>\n" . "    <href>" . $sitelogo . "</href>\n" . "   </Icon>\n" . "   <overlayXY x=\"0\" y=\"1\" xunits=\"fraction\" yunits=\"fraction\"/>\n" . "   <screenXY x=\"0\" y=\"1\" xunits=\"fraction\" yunits=\"fraction\"/>\n" . "   <size x=\"0\" y=\"0\" xunits=\"fraction\" yunits=\"fraction\"/>\n" . "  </ScreenOverlay>\n";
  }
  $output .= kml_format_folder($channel['title'], $channel['description'], $items, $extras) . " </Document>\n" . "</kml>\n";
  return $output;
}

/**
 * Formats a KML Folder (based on format_rss_channel()).
 *
 * Arbitrary elements may be added using the $args associative array.
 */
function kml_format_folder($title, $description, $items, $args = array()) {

  // arbitrary elements may be added using the $args associative array
  $output = "  <Folder>\n" . '   <name>' . check_plain($title) . "</name>\n" . '   <description>' . check_plain($description) . "</description>\n";
  if ($args) {

    // TODO: needs better way of structuring this data so we can embed elements and their attributes
    foreach ($args as $key => $value) {
      if (is_array($value)) {
        if ($value['key']) {
          $output .= '    <' . $value['key'];
          if (is_array($value['attributes'])) {
            $output .= drupal_attributes($value['attributes']);
          }
          if ($value['value']) {
            $output .= '>' . $value['value'] . '</' . $value['key'] . ">\n";
          }
          else {
            $output .= " />\n";
          }
        }
      }
      else {
        $output .= '    <' . $key . '>' . check_plain($value) . "</{$key}>\n";
      }
    }
  }
  $output .= $items . "  </Folder>\n";
  return $output;
}

/**
 * Format a single KML Placemark (based on format_rss_item()).
 *
 * Arbitrary elements may be added using the $args associative array.
 */
function kml_format_placemark($item, $link, $alt = 0, $args = array()) {
  $title = $item->title;
  $output = "   <Placemark>\n" . '    <name>' . check_plain($title) . "</name>\n" . "    <description><![CDATA[\n" . theme('kml_placemark_description', $item, $link) . "\n]]></description>\n" . '    <styleUrl>#myStyleMap_' . $item->type . '</styleUrl>' . "\n";
  $time_type = variable_get('kml_timetype', 'created');
  if ($time_type != 'none') {
    if ($time_type == 'created') {
      $timestamp = date("Y-m-d\\TH:i:s\\Z", $item->created);
    }
    else {
      if ($time_type == 'changed') {
        $timestamp = date("Y-m-d\\TH:i:s\\Z", $item->changed);
      }
    }
    if ($timestamp) {
      $output .= "    <TimeStamp><when>" . $timestamp . "</when></TimeStamp>\n";
    }
  }

  // Strip any geometry information from args
  $geometrys = array();
  $force_multi = FALSE;

  // Handle Placemark on it's own.
  if ($args['Placemark']) {
    $extra_places = $args['Placemark'];
    unset($args['Placemark']);
  }

  // Handle MultiGeometry on it's own.
  if ($args['MultiGeometry']) {
    $geometrys[] = $args['MultiGeometry'] . "\n";
    unset($args['MultiGeometry']);
    $force_multi = TRUE;
  }
  $geo_types = array(
    'Point',
    'LineString',
    'LinearRing',
    'Polygon',
    'Model',
  );
  foreach ($geo_types as $geo_type) {
    if ($args[$geo_type]) {
      $geometrys[] = "<{$geo_type}>" . $args[$geo_type] . "</{$geo_type}>\n";
      unset($args[$geo_type]);
    }
  }

  // Coordinate information
  $lat = $item->location['latitude'];
  $long = $item->location['longitude'];
  if ($lat && $long) {
    $point = "    <Point>\n";
    $pointelements = array(
      'altitudeMode',
      'extrude',
    );

    // elements that extend Point
    foreach ($pointelements as $pointelement) {
      if ($args[$pointelement]) {
        $point .= "     <{$pointelement}>" . $args[$pointelement] . "</{$pointelement}>\n";
        unset($args[$pointelement]);
      }
    }
    $point .= '     <coordinates>' . $long . ',' . $lat . ',' . $alt . "</coordinates>\n";
    $point .= "    </Point>\n";
    $geometrys[] = $point;
  }
  if ($force_multi or count($geometrys) > 1) {
    $output .= "    <MultiGeometry>\n";
    while ($geometrys) {
      $output .= '      ' . array_shift($geometrys);
    }
    $output .= "    </MultiGeometry>\n";
  }
  else {
    if (count($geometrys)) {
      $output .= array_shift($geometrys);
    }
  }

  // Address information. Needs city and country at a minimum.
  // Includes <address> element as well as <AddressDetails> based on xAL format
  // see http://www.oasis-open.org/committees/ciq/Downloads/xNAL/xAL/Versions/xALv2_0/
  if ($item->location['city'] && $item->location['country']) {
    if ($item->location['name']) {
      $address['name'] = check_plain($item->location['name']);
    }
    if ($item->location['street']) {
      $address['street'] = check_plain($item->location['street']);
    }
    if ($item->location['additional']) {
      $address['additional'] = check_plain($item->location['additional']);
    }
    if ($item->location['city']) {
      $address['city'] = check_plain($item->location['city']);
    }
    if ($item->location['province']) {
      $address['province'] = check_plain($item->location['province']);
    }
    if ($item->location['postal_code']) {
      $address['postal_code'] = check_plain($item->location['postal_code']);
    }
    if ($item->location['country']) {
      if (module_exists('location')) {

        // look up country name if location module is enabled
        $countries = location_get_iso3166_list();
        $address['country'] = $countries[$item->location['country']];
      }
      else {
        $address['country'] = check_plain($item->location['country']);
      }
    }

    // Single line address
    $output .= '    <address>' . implode(', ', $address) . "</address>\n";
    $output .= "    <AddressDetails>\n";
    $output .= "     <Country>\n";
    $output .= "      <AddressLine>" . $address['country'] . "</AddressLine>\n";
    if ($address['province']) {
      $output .= "      <AdministrativeArea>\n";
      $output .= "       <AddressLine>" . $address['province'] . "</AddressLine>\n";
    }
    $output .= "       <Locality>\n";
    $output .= "        <AddressLine>" . $address['city'] . "</AddressLine>\n";
    if ($address['street']) {
      $output .= "        <Thoroughfare>\n";
      $output .= "         <AddressLine>" . $address['street'] . "</AddressLine>\n";
      $output .= "        </Thoroughfare>\n";
    }
    if ($address['postal_code']) {
      $output .= "        <PostalCode>\n";
      $output .= "         <AddressLine>" . $address['postal_code'] . "</AddressLine>\n";
      $output .= "        </PostalCode>\n";
    }
    $output .= "       </Locality>\n";
    if ($address['province']) {
      $output .= "      </AdministrativeArea>\n";
    }
    $output .= "     </Country>\n";
    $output .= "    </AddressDetails>\n";
  }
  if ($args) {

    // TODO: needs better way of structuring this data so we can embed elements and their attributes
    foreach ($args as $key => $value) {
      if (is_array($value)) {
        if ($value['key']) {
          $output .= '    <' . $value['key'];
          if (is_array($value['attributes'])) {
            $output .= drupal_attributes($value['attributes']);
          }
          if ($value['value']) {
            $output .= '>' . $value['value'] . '</' . $value['key'] . ">\n";
          }
          else {
            $output .= " />\n";
          }
        }
      }
      else {
        $output .= '    <' . $key . '>' . check_plain($value) . "</{$key}>\n";
      }
    }
  }
  $output .= "   </Placemark>\n";

  // Add in extra_places if set
  if (isset($extra_places)) {
    if (is_array($extra_places)) {
      foreach ($extra_places as $placemark) {
        $output .= "   <Placemark>\n{$placemark}\n</Placemark>\n";
      }
    }
    else {
      $output .= "   <Placemark>\n{$extra_places}\n</Placemark>\n";
    }
  }
  return $output;
}

/** 
 * Theme the contents of the placemark
 */
function theme_kml_placemark_description($item, $link) {
  $output = check_markup($item->teaser);
  $output .= '<br/><a href="' . check_url($link) . '">' . t('View page') . '</a>';
  return $output;
}

/**
 * A function for creating a KML Network Link for a specific KML feed.
 *
 */
function kml_networklink($attributes = array()) {
  $title = $attributes['title'] ? $attributes['title'] : variable_get('site_name', 'drupal');

  // set link to KML feed to send out through Network Link
  $kml_feed = $attributes['kml_feed'] ? $attributes['kml_feed'] : url('kml/node', array(
    'absolute' => TRUE,
  ));

  // TODO: allow these to be customised in a settings page
  $url['name'] = $title;
  $url['href'] = $kml_feed;
  $url['viewRefreshMode'] = variable_get('kml_refreshmode', 'onStop');
  $url['viewRefreshTime'] = variable_get('kml_refreshtime', 2);
  $output = kml_format_networklink($title, $kml_feed, $url);
  $extension = variable_get('kml_usekmz', 0) == 1 ? '.kmz' : '.kml';
  $filename = variable_get('kml_networklinkfilename', 'networklink') . $extension;
  drupal_set_header('Content-Type: application/vnd.google-earth.kml+xml');
  drupal_set_header('Content-Disposition: attachment; filename="' . $filename . '"');
  print $output;
}

/**
 * A generic function for generating a KML (Keyhole Markup Language for Google Earth) Network Link.
 *
 * Arbitrary elements may be added using the $args associative array.
 */
function kml_format_networklink($title, $kml_feed, $url_items = array(), $args = array()) {
  $output = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
  $output .= "<kml xmlns=\"http://earth.google.com/kml/2.0\">\n";
  $output .= " <Document>\n";
  $output .= "  <NetworkLink>\n";
  $output .= "   <Url>\n";
  foreach ($url_items as $url_item => $value) {
    $output .= '    <' . $url_item . '>' . check_plain($value) . '</' . $url_item . ">\n";
  }
  $output .= "   </Url>\n";
  $output .= "  </NetworkLink>\n";
  $output .= " </Document>\n";
  $output .= "</kml>\n";
  return $output;
}

Functions

Namesort descending Description
kml_admin_settings Form for settings page
kml_block Implementation of hook_block()
kml_block_content Function to generate block content
kml_format_folder Formats a KML Folder (based on format_rss_channel()).
kml_format_networklink A generic function for generating a KML (Keyhole Markup Language for Google Earth) Network Link.
kml_format_placemark Format a single KML Placemark (based on format_rss_item()).
kml_help Implementation of hook_help().
kml_interface Default callback
kml_link Implementation of hook_link(). Adds KML links to individual nodes
kml_menu Implementation of hook_menu().
kml_networklink A function for creating a KML Network Link for a specific KML feed.
kml_page Displays directory of KML feeds available
kml_perm Implementation of hook_perm().
kml_simpletest Implementation of hook_simpletest().
kml_theme
kml_views_feed_argument
kml_views_style_plugins Provide views plugins for creating KML feeds.
theme_kml_feed Views plugin that displays the KML feed for views
theme_kml_icon Display the KML icon
theme_kml_link Standardised KML link
theme_kml_placemark_description Theme the contents of the placemark
_kml_feed Displays a KML feed containing all location-enabled nodes.
_kml_feed_check_access
_kml_feed_check_cache
_kml_format_feed A generic function for generating KML (Keyhole Markup Language for Google Earth) feeds from a set of nodes.
_kml_send_feed Send KML feed.