Friday, May 31, 2013

How to add a driving directions link to a marker popup in a Drupal Map

I was tasked at work today with finding a good, lightweight way to display multiple locations on a map, and somehow provide directions to each location. If it looked really cool, well, bonus! The solution that I came up with was just one of many many possible solutions.  For an excellent overview of Drupal Mapping in general, see https://drupal.org/node/1704948. That article was my starting point.  I had several important constraints, however.  Although it would have certainly been possible to replace the geolocation storage that we were already using, I wanted to first see if I could make it work with what we had.  Our initial setup was the following:

Content Type with the following fields:

Address - Provided by the addressfield module.
Map - Provided by the geofield module, requires latitude and longitude.*

* I am tempted to add the geocode module, as it will populate a geofield field based on an addressfield field, but for simplicity's sake, I left it out for now.

My second constraint was that I wanted it to be as simple as possible for the end user, i.e., they add a location with an address and lat/long and everything else is generated automatically. The solution that I came up with uses the leaflet module (see list for link)

Modules:

Addressfield - https://drupal.org/project/addressfield
Geofield - https://drupal.org/project/geofield (version 7x-1.1)
Leaflet - https://drupal.org/project/leaflet
Leaflet More Maps - https://drupal.org/project/leaflet_more_maps
Get Directions - https://drupal.org/project/getdirections
Views - https://drupal.org/project/views*

* Be sure you enable the views_ui module, or you won't be able to create your map view!

Setup is quite simple.  Download and enable all of the modules.  The only one that requires separate configuration is the getdirections module (admin/config/services/getdirections), and the defaults are fine to start with. That said, it is probably best to go into those settings and select 'Use Googlemaps version 3', as that will remove the need for a google api key. Create a content type, call it whatever you want, we'll go with Zingzang, for the purposes of this tutorial, and add an address field(address) and a geolocation field(map).  Now add several test Zingzang nodes, and be sure you fill in the lat/long, that is important.

Now for the view: create a new view, with content of type: Zingzang, and create a page.  Give it a path of /map, or location, or whatever you like. Set the pager to: Display all items. Format the view as Leaflet Map, with these settings:
Data Source = Content:Map
Title Field = Content:Title
Description Content (This one is important!) = Content:Nid

If you installed Leaflet more maps, you will have about 20 styles to choose from in the Map dropdown, I personally like Stamen Toner, fwiw.  Pick a style, and then click on Apply.

Now for the fields. Add these fields:
Content: Map
   Settings: Don't touch the settings, the view formatter takes care of this.
Content: Title
   Settings: Exclude from display, *and* Rewrite the output of this field as: Get Directions to [title]
Content: Nid
   Settings: Rewrite the output of this field as: [title]  *and* Output this field as a link, Link Path is: getdirections/location/to/[nid]

Save your view.  Now you can navigate to your sweet map!  It should look something like this:


The Get Directions to Another Location link will open another page, that should look something like this:



And that's it!  A (relatively) lightweight, super cool looking way to display locations on a map with a directions popup.

As always, please feel free to leave a comment if you have any questions.  I can't promise that I'll be able to answer them, but it's worth a shot... ;)

Wednesday, May 22, 2013

How to rewrite a views field based on the value of a separate field.

Recently at work I was configuring a view of staff members.  Staff could be a Team Leader,  a Team Leader *and* a Board Member, or neither.  An integer list field, field_staff_leader, gave these options the value of 1, 2, and 0, respectively.  The view itself displayed team leaders, so obviously I added a simple filter that only displayed staff members that had a value of 1 or 2 in field_staff_leader.  Simple enough.  The tricky part was the value of another (text) field, field_role.  This field needed to be displayed normally if the value of field_staff_leader was 1 (is a Team Leader), but if the value was 2 (Team Leader and Board Member), field_role needed to be overwritten with the text "Board Member".

There's a hook for that! hook_views_pre_render does exactly what I needed.  Here's the code
/**

 * Check for Board Member status and overwrite field_role if TRUE .

 */

function mymodule_views_pre_render(&$view) {

  $results = &$view->result;

    foreach ($results as $key => $result) {

      if (($view->name == 'staff') && ($view->current_display == 'panel_pane_1')) {

        $field_board = $result->_field_data['nid']['entity']->field_staff_leader;

        if ($field_board['und'][0]['value'] == '2') {

          $results[$key]->field_field_role[0]['rendered']['#markup'] = 'Board Member';

      }

    }

  }
 
To break it down: First you need to access the view's results, and then of course make sure that you're applying your code to the correct view display.  If you are not sure what the view name or display id is, add dsm($view); to your code (be sure you have the devel module enabled), and your needed values will be in $view->name, and $view->current_display, respectively. 
/**

 * Check for Board Member status and overwrite field_role if TRUE .

 */

function mymodule_views_pre_render(&$view) {

  $results = &$view->result;

    foreach ($results as $key => $result) {

      if (($view->name == 'staff') && ($view->current_display == 'panel_pane_1')) {
Once you have targeted the correct view/display, check the field value:

        $field_board = $result->_field_data['nid']['entity']->field_staff_leader;

        if ($field_board['und'][0]['value'] == '2') {
...and then add your custom markup:
          $results[$key]->field_field_role[0]['rendered']['#markup'] = 'Board Member';
Then you may go enjoy your fully functional Death Star. Er, View.

Thursday, May 2, 2013

How to write a custom ctools access plugin for a Boolean field.

I had the delightful task at work of determining the visibility of a view (in a node variant) based on the value of a boolean field.  But it got better!  Said boolean field was not on the node that was being viewed, it was on a node that referenced the node being viewed.  But wait!  It gets better!  The referenced node was an organic group, and those references work differently then a usual entity reference!  Confused yet?  Yeah, me too.  I got it in the end though, and here it is, on the vague assumption that such an odd use case will ever come up again...

First things first: you have to let ctools know that there is a plugin to use.  In your custom module (or in my case, in the relevant feature) .module, you need something like this:
/**
 * Implements hook_ctools_plugin_directory().
 *
 * It simply tells panels where to look for the .inc file that
 * defines various args, contexts and content_types.
 */
function my_feature_ctools_plugin_directory($module, $plugin) {
 if ($module == 'ctools' && !empty($plugin)) {
   return "plugins/$plugin";
 }
}



Then in your feature (or custom module), create the directory, so you have a my_feature/plugins/access folder structure.

In the access folder, create a new .inc file.  For the purposes of this exercise, the field we are using to determine visibility will be called field_widget, so I would call my file field_widget.inc.

You have to start by defining the plugin, so:
<?php

/**
 * Plugins are described by creating a $plugin array which will
 * be used by the system that includes the file.
 */
$plugin = array(
  'title' => t('Node: Widget'),
  'description' => t('Only displays this pane if the Widget field on the related Home Page for this Organic Group is set to On.'),
  'callback' => 'my_feature_field_widget_ctools_access_check',
  'default' => array('field_widget' => 1),
  'summary' => 'my_feature_field_widget_ctools_access_summary',
  'required context' => new ctools_context_required(t('Node'), 'node'),
);  



Now write the callback:
/**
 * Custom callback defined by 'callback' in the $plugin array.
 *
 * Check for access.
 */
function my_feature_field_widget_ctools_access_check($conf, $context) {

  // If for some unknown reason that $context isn't set, return false.
  if (empty($context) || empty($context->data)) {
    return FALSE;
  }

Because the field is *not* on the node that is currently being viewed, we have to identify the correct home page node.
  // Identify the home page node for the current organic group.
  $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'node')
      ->entityCondition('bundle', 'division_home')
      ->fieldCondition('og_division_home_division_ref', 'target_id', $context->data->nid);
    $result = $query->execute();




Now we have to load the node, to access the field_widget value.
    $home_page_nid = current($result['node'])->nid;
    $home_page_node = node_load($home_page_nid);


Here is where we check the value of field_widget.
  Being a boolean, if the value is 0, that means the boolean is not checked, and we want to hide the pane.
// If the home page widget field is not checked, hide the pane.
  if (!$home_page_node->field_widget['und'][0]['value']) {
  }
Otherwise, show the pane.
  return TRUE;
}



Here is the whole kit and caboodle:
<?php

/**
 * Plugins are described by creating a $plugin array which will
 * be used by the system that includes the file.
 */
$plugin = array(
  'title' => t('Node: Widget'),
 
 'description' => t('Only displays this pane if the Widget field on 
the related Home Page for this Organic Group is set to On.'),
  'callback' => 'my_feature_field_widget_ctools_access_check',
  'default' => array('field_widget' => 1),
  'summary' => 'my_feature_field_widget_ctools_access_summary',
  'required context' => new ctools_context_required(t('Node'), 'node'),
);   

/**
 * Custom callback defined by 'callback' in the $plugin array.
 *
 * Check for access.
 */
function my_feature_field_widget_ctools_access_check($conf, $context) {

  // If for some unknown reason that $context isn't set, return false.
  if (empty($context) || empty($context->data)) {
    return FALSE;
  }



  // Identify the home page node for the current organic group.
  $query = new EntityFieldQuery();
    $query->entityCondition('entity_type', 'node')
      ->entityCondition('bundle', 'division_home')
      ->fieldCondition('og_division_home_division_ref', 'target_id', $context->data->nid);
    $result = $query->execute();

    $home_page_nid = current($result['node'])->nid;
    $home_page_node = node_load($home_page_nid);



// If the home page widget field is not checked, hide the pane.
  if (!$home_page_node->field_widget['und'][0]['value']) {
  }

  return TRUE;
}


And there you have it.  :)