Geekout: Video on Maps for Cable Access TV

I recently did some Drupal development work for Cambridge Community Television. As part of the really amazing work they are doing combining new media with traditional Cable Access Television, CCTV has been mapping videos their members produce. They call this project the Mediamap.

I was really excited to work on the Mediamap with CCTV because of my long involvement with Cable Access Television, most notably the now-defunct DigitalBicycle Project and the community maintained directory of Cable Access Stations I built and administer: MappingAccess.com.

Despite CCTV running their website on Drupal, their first proof-of-concept version of the Mediamap was created manually, using the very capable Mapbuilder.net service and copy-and-pasted embedded flash video. While simple from a technological standpoint, they were running to problems optimizing the workflow of updating the map; changes had to be made via the Mapbuilder.net interface, with a single username and password, then manually parsed to remove some coding irregularities, and finally copy and pasted whole into a page on their website.

I was asked to improve the workflow and ultimately take fuller advantage of Drupal’s built-in user management and content management features. For instance, taking advantage of CCTV’s current member submitted video capabilities and flowing them into the map as an integrated report, not a separate and parallel system.

In my discussions with them, a couple of issues came up. Foremost was that CCTV was running an older version of Drupal: 4.7. While still quite powerful, many newer features and contributed modules were not available for this earlier release. The current version of Drupal, 5.1, has many rich, well-developed utilities for creating reports and mapping them: Content Construction Kit (CCK) + Views + Gmap + Location. As it was though, with the older version, I would have to develop the additional functionality manually.

The following is a description, with code examples, of the functionality I created for the Mediamap. Additionally, following this initial development, CCTV upgraded their Drupal installation to 5.1, giving me the opportunity to demonstrate the ease and power of Drupal’s most recent release—rendering blissfully obsolete most of the custom coding I had done.

Location and Gmap was used in both versions for storing geographic data and hooking into the Google Map API. One of Drupal’s great strengths is the both the diversity of contributed modules, and the flexibility with which a developer can use them.

Adding additional content fields

CCTV already has a process in which member’s can submit content nodes. In 4.7, the easiest way to add additional data fields to these was with a custom NodeAPI module. CCTV was interested in using embedded flash video, primarily from Blip.tv, but also Google Video or YouTube if the flexibility was needed. To simplify the process, we decided on just adding the cut-and-paste embed code to a custom content field in existing nodes.

To do this, I created a new module that invoked hook_nodeapi:

/\*\*\ * Implementation of hook_nodeapi\ * /

function cambridge_mediamap_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'validate':
      if (variable_get('cambridge_mediamap_'.$node - > type, TRUE)) {
        if (user_access('modify node data')) {
          if ($node - > cambridge_mediamap['display'] && $node - > cambridge_mediamap['embed'] == '') {
            form_set_error('cambridge_mediamap', t('Media Map: You must enter embed code or disable display of this node on the map'));
          }
        }
      }
      break;
    case 'load':
      $object = db_fetch_object(db_query('SELECT display, embed FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid));
      $embed = $object - > embed;
      $embed_resize = cambridge_mediamap_resize($embed);
      return array('cambridge_mediamap' => array('display' => $object - > display, 'embed' => $embed, 'embed_resize' => $embed_resize, ));
      break;
    case 'insert':
      db_query("INSERT INTO {cambridge_mediamap} (nid, display, embed) VALUES (%d, %d, '%s')", $node - > nid, $node - > cambridge_mediamap['display'], $node - > cambridge_mediamap['embed']);
      break;
    case 'update':
      db_query('DELETE FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid);
      db_query("INSERT INTO {cambridge_mediamap} (nid, display, embed) VALUES (%d, %d, '%s')", $node - > nid, $node - > cambridge_mediamap['display'], $node - > cambridge_mediamap['embed']);
      break;
    case 'delete':
      db_query('DELETE FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid);
      break;  
    case 'view':
      break;
  }
}

As you can see, there is a considerable amount of coding required, from defining the form, validating input and configuring database storage and retrieval calls.

Now that we have the glue for the custom field, we have to configure what node types that custom field appears on. Additionally, we need to set up administrative settings to configure where that custom field will appear, and lastly insert that field into the node edit screen:


\**
 * Implementation of hook_form_alter
 */
function cambridge_mediamap_nodeapi( & $node, $op, $teaser, $page) {
  switch ($op) {
    case 'validate':
      if (variable_get('cambridge_mediamap_'.$node - > type, TRUE)) {
        if (user_access('modify node data')) {
          if ($node - > cambridge_mediamap['display'] && $node - > cambridge_mediamap['embed'] == '') {
            form_set_error('cambridge_mediamap', t('Media Map: You must enter embed code or disable display of this node on the map'));
          }
        }
      }
      break;
    case 'load':
      $object = db_fetch_object(db_query('SELECT display, embed FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid));
      $embed = $object - > embed;
      $embed_resize = cambridge_mediamap_resize($embed);
      return array(
        'cambridge_mediamap' => array(
          'display' => $object - > display,
          'embed' => $embed,
          'embed_resize' => $embed_resize,
        )
      );
      break;
    case 'insert':
      db_query("INSERT INTO {cambridge_mediamap} (nid, display, embed) VALUES (%d, %d, '%s')", $node - > nid, $node - > cambridge_mediamap['display'], $node - > cambridge_mediamap['embed']);
      break;
    case 'update':
      db_query('DELETE FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid);
      db_query("INSERT INTO {cambridge_mediamap} (nid, display, embed) VALUES (%d, %d, '%s')", $node - > nid, $node - > cambridge_mediamap['display'], $node - > cambridge_mediamap['embed']);
      break;
    case 'delete':
      db_query('DELETE FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid);
      break;
    case 'view':
      break;
  }
}

As you can see, that’s a lot of lines of code for what we essentially can do, in Drupal 5.1 with CCK. CCK allows you, graphically through the Drupal web-interface, to create a new content field and add it to a node type; it takes about a minute.

Building the Map

The primary goal of rebuilding the Mediamap using native Drupal was workflow optimization: it was frustrating to submit information both within Drupal and then recreate it within Mapbuilder. In essence, the map should be just another report of Drupal content: you may have a short bulleted list of the top five articles, a paginated history with teasers and author information, or a full-blown map, but most importantly, all of it is flowing dynamically out of the Drupal database.

The Gmap module provides many powerful ways to integrate the Google Map API with Drupal. While Gmap for 4.7 provides a default map of content it would not provide the features or customizability we desired with the Mediamap. Instead, one of the most powerful ways to use Gmap is to hook directly into the module’s own API-like functions:

\**
 * A page callback to draw the map
 */

function cambridge_mediamap_map() {
  $output = '';
  //Collect the nodes to be displayed
  $results = db_query('SELECT embed, nid FROM {cambridge_mediamap} WHERE display = 1');
  //Initialize our marker array
  $markers = array();
  //check to see what modules are enabled
  $location_enabled = module_exist('location');
  $gmap_location_enabled = module_exist('gmap_location');
  //load each node and set it's attributes in the marker array
  while ($item = db_fetch_object($results)) {
    $latitude = 0;
    $longitude = 0;
    //load the node
    $node = node_load(array('nid' => $item - > nid));
    //set the latitude and longitude
    //give location module data preference over gmap module data
    if ($location_enabled) {
      $latitude = $node - > location['latitude'];
      $longitude = $node - > location['longitude'];
    }
    elseif($gmap_location_enabled) {
      $latitude = $node - > gmap_location_latitude;
      $longitude = $node - > gmap_location_longitude;
    }
    if ($latitude && $longitude) {
      $markers[] = array(
        'label' => theme('cambridge_mediamap_marker', $node),
        'latitude' => $latitude,
        'longitude' => $longitude,
        'markername' => variable_get('cambridge_mediamap_default_marker', 'marker'),
      );
    }
  }
  $latlon = explode(',', variable_get('cambridge_mediamap_default_latlong', '42.369452,-71.100426'));
  $map = array(
    'id' => 'cambridge_mediamap',
    'latitude' => trim($latlon[0]),
    'longitude' => trim($latlon[1]),
    'width' => variable_get('cambridge_mediamap_default_width', '100%'),
    'height' => variable_get('cambridge_mediamap_default_height', '500px'),
    'zoom' => variable_get('cambridge_mediamap_default_zoom', 13),
    'control' => variable_get('cambridge_mediamap_default_control', 'Large'),
    'type' => variable_get('cambridge_mediamap_default_type', 'Satellite'),
    'markers' => $markers,
  );
   
  return gmap_draw_map($map);
}

As you can see, this is quite complicated. Drupal 5.1 offers the powerful Views module, which allows one to define custom reports, once again graphically from the Drupal web-interface, in just a couple minutes of configuration. The gmap_views module, which ships with Gmap, allows one to add those custom reports to a Google Map, which is incredibly useful and renders obsolete much of the development work I did.

On displaying video in maps

In my discussions with CCTV, we felt it most pragmatic to use the embedded video code provided by video hosting services such as Blip.tv. While we could have used one of the Drupal video modules, we wanted the ability to host video offsite due to storage constraints. While I was concerned about the danger of code injection via minimally validated inputs, we felt that this would be of small danger because the content would be maintained by CCTV staff and select members.

The markers were themed using the embedded video field pulled from the Drupal database, along with the title and a snippet of the description, all linking back to the full content node.

/**
 * A theme function for our markers
 */
function theme_cambridge_mediamap_marker($node) {
  $output = '';
  $output. = '' . l($node->title, 'node / ' . $node->nid) . '';
  $output. = '' . $node->cambridge_mediamap[' . embed_resize '] . '';
  $output. = '';
  return $output;
}

With Drupal 5.1 and Views, we still had to override the standard marker themes, but this was simple and done through the standard methods.

One of the most helpful pieces was some code developed by Rebecca White, who I previously worked with on Panlexicon. She provided the critical pieces of code that parsed the embedded video code and resized it for display on small marker windows.

/**

\* Returns a resized embed code

\*/
function cambridge_mediamap_resize($embed = '') {
  if (!$embed) {
    return '';
  }
  list($width, $height) = cambridge_mediamap_get_embed_size($embed);
  //width/height ratio
  $width_to_height = $width / $height;
  $max_width = variable_get('cambridge_mediamap_embed_width', '320');
  $max_height = variable_get('cambridge_mediamap_embed_height', '240');
  //shrink down widths while maintaining proportion
  if ($width >= $height) {
    if ($width > $max_width) {
      $width = $max_width;
      $height = (1 / $width_to_height)\ * $width;
    }
    if ($height > $max_height) {
      $height = $max_height;
      $width = ($width_to_height)\ * $height;
    }
  } else {
    if ($height > $max_height) {
      $height = $max_height;
      $width = ($width_to_height)\ * $height;
    }
    if ($width > $max_width) {
      $width = $max_width;
      $height = (1 / $width_to_height)\ * $width;
    }
  }
  return cambridge_mediamap_set_embed_size($embed, intval($width), intval($height));
}
/\*\*\ * find out what size the embedded thing is\ * /

function cambridge_mediamap_get_embed_size($html) {
  preg_match('/]\*width(\s\*=\s\*"|:\s\*)(\d+)/i', $html, $match_width);
  preg_match('/]\*height(\s\*=\s\*"|:\s\*)(\d+)/i', $html, $match_height);
  return array($match_width[2], $match_height[2]);
}
/\*\*\ * set the size of the embeded thing\ * /

function cambridge_mediamap_set_embed_size($html, $width, $height) {
  $html = preg_replace('/(<(embed|object)\s[^>]\*width(\s\*=\s\*"|:\s\*))(\d+)/i', '${1}'.$width, $html);
  $html = preg_replace('/(<(embed|object)\s[^>]\*height(\s\*=\s\*"|:\s\*))(\d+)/i', '${1}'.$height, $html);
  return $html;
}
/\*\*\ * returns the base url of the src attribute.\*youtube = www.youtube.com\ * blip = blip.tv\ * google video = video.google.com\ * /

function cambridge_mediamap_get_embed_source($html) {
  preg_match('/]\*src="http:\/\/([^\/"]+)/i', $html, $match_src);
  return $match_src[1];
}

The Wrap-Up

While it may not seem so from the lines of code above, developing for Drupal is still relatively easy. Drupal provides a rich set of features for developers, well documented features, and strong coding standards—making reading other people’s code and learning from it incredibly productive.

Below is the entirety of the custom module I developed for the 4.7 version of the CCTV Media Map. Because it was custom and intended to be used in-house, many important, release worthy functions were omitted, such as richer administrative options and module/function verifications.

'cambridge_mediamap', 'title' => t('Mediamap'), 'callback' => 'cambridge_mediamap_map', 'access' => user_access('access mediamap'), );
}
return $items;
}
/\*\*\ * Implementation of hook_nodeapi\ * /

function cambridge_mediamap_nodeapi( & $node, $op, $teaser, $page) {
  switch ($op) {
    case 'validate':
      if (variable_get('cambridge_mediamap_'.$node - > type, TRUE)) {
        if (user_access('modify node data')) {
          if ($node - > cambridge_mediamap['display'] && $node - > cambridge_mediamap['embed'] == '') {
            form_set_error('cambridge_mediamap', t('Media Map: You must enter embed code or disable display of this node on the map'));
          }
        }
      }
      break;
    case 'load':
      $object = db_fetch_object(db_query('SELECT display, embed FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid));
      $embed = $object - > embed;
      $embed_resize = cambridge_mediamap_resize($embed);
      return array('cambridge_mediamap' => array('display' => $object - > display, 'embed' => $embed, 'embed_resize' => $embed_resize, ));
      break;
    case 'insert':
      db_query("INSERT INTO {cambridge_mediamap} (nid, display, embed) VALUES (%d, %d, '%s')", $node - > nid, $node - > cambridge_mediamap['display'], $node - > cambridge_mediamap['embed']);
      break;
    case 'update':
      db_query('DELETE FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid);
      db_query("INSERT INTO {cambridge_mediamap} (nid, display, embed) VALUES (%d, %d, '%s')", $node - > nid, $node - > cambridge_mediamap['display'], $node - > cambridge_mediamap['embed']);
      break;
    case 'delete':
      db_query('DELETE FROM {cambridge_mediamap} WHERE nid = %d', $node - > nid);
      break;
    case 'view':
      break;
  }
}
/\*\*\ * Returns a resized embed code\ * /

function cambridge_mediamap_resize($embed = '') {
  if (!$embed) {
    return '';
  }
  list($width, $height) = cambridge_mediamap_get_embed_size($embed);
  //width/height ratio
  $width_to_height = $width / $height;
  $max_width = variable_get('cambridge_mediamap_embed_width', '320');
  $max_height = variable_get('cambridge_mediamap_embed_height', '240');
  //shrink down widths while maintaining proportion
  if ($width >= $height) {
    if ($width > $max_width) {
      $width = $max_width;
      $height = (1 / $width_to_height)\ * $width;
    }
    if ($height > $max_height) {
      $height = $max_height;
      $width = ($width_to_height)\ * $height;
    }
  } else {
    if ($height > $max_height) {
      $height = $max_height;
      $width = ($width_to_height)\ * $height;
    }
    if ($width > $max_width) {
      $width = $max_width;
      $height = (1 / $width_to_height)\ * $width;
    }
  }
  return cambridge_mediamap_set_embed_size($embed, intval($width), intval($height));
}
/\*\*\ * find out what size the embedded thing is\ * /

function cambridge_mediamap_get_embed_size($html) {
  preg_match('/]\*width(\s\*=\s\*"|:\s\*)(\d+)/i', $html, $match_width);
  preg_match('/]\*height(\s\*=\s\*"|:\s\*)(\d+)/i', $html, $match_height);
  return array($match_width[2], $match_height[2]);
}
/\*\*\ * set the size of the embeded thing\ * /

function cambridge_mediamap_set_embed_size($html, $width, $height) {
  $html = preg_replace('/(<(embed|object)\s[^>]\*width(\s\*=\s\*"|:\s\*))(\d+)/i', '${1}'.$width, $html);
  $html = preg_replace('/(<(embed|object)\s[^>]\*height(\s\*=\s\*"|:\s\*))(\d+)/i', '${1}'.$height, $html);
  return $html;
}
/\*\*\ * returns the base url of the src attribute.\*youtube = www.youtube.com\ * blip = blip.tv\ * google video = video.google.com\ * /

function cambridge_mediamap_get_embed_source($html) {
  preg_match('/]\*src="http:\/\/([^\/"]+)/i', $html, $match_src);
  return $match_src[1];
}
/\*\*\ * Implementation of hook_form_alter\ * /

function cambridge_mediamap_form_alter($form_id, & $form) {
  // We're only modifying node forms, if the type field isn't set we don't need
  // to bother.
  if (!isset($form['type'])) {
    return;
  }
  //disable the Gmap module's location map for unauthorized users
  //unfortunately Gmap.module doesn't have this setting
  if (isset($form['coordinates'])) {
    if (!user_access('modify node data')) {
      unset($form['coordinates']);
    }
  }
  // Make a copy of the type to shorten up the code
  $type = $form['type']['#value'];
  // Is the map enabled for this content type?
  $enabled = variable_get('cambridge_mediamap_'.$type, 0);
  switch ($form_id) {
    // We need to have a way for administrators to indicate which content
    // types should have the additional media map information added.
    case $type.
    '_node_settings':
      $form['workflow']['cambridge_mediamap_'.$type] = array('#type' => 'radios', '#title' => t('Cambridge Mediamap setting'), '#default_value' => $enabled, '#options' => array(0 => t('Disabled'), 1 => t('Enabled')), '#description' => t('Allow the attaching of externally hosted imbedded video to be displayed in a map?'), );
      break;
    case $type.
    '_node_form':
      if ($enabled && user_access('modify node data')) {
        //create the fieldset
        $form['cambridge_mediamap'] = array('#type' => 'fieldset', '#title' => t('Media Map'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, );
        //insert the embed code
        $form['cambridge_mediamap']['embed'] = array('#type' => 'textarea', '#title' => t('Video Embed Code'), '#default_value' => $form['#node'] - > cambridge_mediamap['embed'], '#cols' => 60, '#rows' => 5, '#description' => t('Copy and paste the embed code from an external video or media hosting service'), );
        //enable or disable on map
        $form['cambridge_mediamap']['display'] = array('#type' => 'select', '#title' => t('Display this node'), '#default_value' => $form['#node'] - > cambridge_mediamap['display'], '#options' => array('0' => t('Disable display'), '1' => t('Enable display'), ), );
      }
      break;
  }
}
/\*\*\ * A page callback to draw the map\ * /

function cambridge_mediamap_map() {
  $output = '';
  //Collect the nodes to be displayed
  $results = db_query('SELECT embed, nid FROM {cambridge_mediamap} WHERE display = 1');
  //Initialize our marker array
  $markers = array();
  //check to see what modules are enabled
  $location_enabled = module_exist('location');
  $gmap_location_enabled = module_exist('gmap_location');
  //load each node and set it's attributes in the marker array
  while ($item = db_fetch_object($results)) {
    $latitude = 0;
    $longitude = 0;
    //load the node
    $node = node_load(array('nid' => $item - > nid));
    //set the latitude and longitude
    //give location module data preference over gmap module data
    if ($location_enabled) {
      $latitude = $node - > location['latitude'];
      $longitude = $node - > location['longitude'];
    }
    elseif($gmap_location_enabled) {
      $latitude = $node - > gmap_location_latitude;
      $longitude = $node - > gmap_location_longitude;
    }
    if ($latitude && $longitude) {
      $markers[] = array('label' => theme('cambridge_mediamap_marker', $node), 'latitude' => $latitude, 'longitude' => $longitude, 'markername' => variable_get('cambridge_mediamap_default_marker', 'marker'), );
    }
  }
  $latlon = explode(',', variable_get('cambridge_mediamap_default_latlong', '42.369452,-71.100426'));
  $map = array('id' => 'cambridge_mediamap', 'latitude' => trim($latlon[0]), 'longitude' => trim($latlon[1]), 'width' => variable_get('cambridge_mediamap_default_width', '100%'), 'height' => variable_get('cambridge_mediamap_default_height', '500px'), 'zoom' => variable_get('cambridge_mediamap_default_zoom', 13), 'control' => variable_get('cambridge_mediamap_default_control', 'Large'), 'type' => variable_get('cambridge_mediamap_default_type', 'Satellite'), 'markers' => $markers, );
  return gmap_draw_map($map);
}
/\*\*\ * A theme
function
for our markers\ * /

function theme_cambridge_mediamap_marker($node) {
  $output = '
  ';
  $output. = '
  ' . l($node->title, '
  node / ' . $node->nid) . '
  ';
  $output. = '
  ' . $node->cambridge_mediamap['
  embed_resize '] . '
  ';
  $output. = '
  ';
  return $output;
}
/\*\*\ * Settings page\ * /

function cambridge_mediamap_settings() {
  // Cambridge data
  // latitude = 42.369452
  // longitude = -71.100426
  $form['defaults'] = array('#type' => 'fieldset', '#title' => t('Default map settings'), );
  $form['defaults']['cambridge_mediamap_default_width'] = array('#type' => 'textfield', '#title' => t('Default width'), '#default_value' => variable_get('cambridge_mediamap_default_width', '100%'), '#size' => 25, '#maxlength' => 6, '#description' => t('The default width of a Google map. Either px or %'), );
  $form['defaults']['cambridge_mediamap_default_height'] = array('#type' => 'textfield', '#title' => t('Default height'), '#default_value' => variable_get('cambridge_mediamap_default_height', '500px'), '#size' => 25, '#maxlength' => 6, '#description' => t('The default height of Mediamap. In px.'), );
  $form['defaults']['cambridge_mediamap_default_latlong'] = array('#type' => 'textfield', '#title' => t('Default center'), '#default_value' => variable_get('cambridge_mediamap_default_latlong', '42.369452,-71.100426'), '#description' => 'The decimal latitude,longitude of the centre of the map. The "." is used for decimal, and "," is used to separate latitude and longitude.', '#size' => 50, '#maxlength' => 255, '#description' => t('The default longitude, latitude of Mediamap.'), );
  $form['defaults']['cambridge_mediamap_default_zoom'] = array('#type' => 'select', '#title' => t('Default zoom'), '#default_value' => variable_get('cambridge_mediamap_default_zoom', 13), '#options' => drupal_map_assoc(range(0, 17)), '#description' => t('The default zoom level of Mediamap.'), );
  $form['defaults']['cambridge_mediamap_default_control'] = array('#type' => 'select', '#title' => t('Default control type'), '#default_value' => variable_get('cambridge_mediamap_default_control', 'Large'), '#options' => array('None' => t('None'), 'Small' => t('Small'), 'Large' => t('Large')), );
  $form['defaults']['cambridge_mediamap_default_type'] = array('#type' => 'select', '#title' => t('Default map type'), '#default_value' => variable_get('cambridge_mediamap_default_type', 'Satellite'), '#options' => array('Map' => t('Map'), 'Satellite' => t('Satellite'), 'Hybrid' => t('Hybrid')), );
  $markers = gmap_get_markers();
  $form['defaults']['cambridge_mediamap_default_marker'] = array('#type' => 'select', '#title' => t('Marker'), '#default_value' => variable_get('cambridge_mediamap_default_marker', 'marker'), '#options' => $markers, );
  $form['embed'] = array('#type' => 'fieldset', '#title' => t('Default embedded video settings'), );
  $form['embed']['cambridge_mediamap_embed_width'] = array('#type' => 'textfield', '#title' => t('Default width'), '#default_value' => variable_get('cambridge_mediamap_embed_width', '320'), '#size' => 25, '#maxlength' => 6, '#description' => t('The maximum width of embedded video'), );
  $form['embed']['cambridge_mediamap_embed_height'] = array('#type' => 'textfield', '#title' => t('Default height'), '#default_value' => variable_get('cambridge_mediamap_embed_height', '240'), '#size' => 25, '#maxlength' => 6, '#description' => t('The maximum height of embedded video.'), );
  return $form;
}
/\*\*\ * Prints human - readable(html) information about a variable.\*Use: print debug($variable_name);\ * Or assign output to a variable.\*/

function debug($value) {
  return preg_replace("/\s/", " ", preg_replace("/\n/", "", print_r($value, true)));
}

Walkabout Trail, RI

Frog Helen sees something

A few weeks ago I hiked the Walkabout Trail in Northwestern Rhode Island with my friend Helen. The trail was gorgeously green and mossy with many fun frogs, toads and critters. We hiked the eight miles (which cuts for shorter walks) in a little over three hours, which was a great pace leading up to a cooling swim in the adjoining pond afterwards.


“Should I get a nonprofit job?”

I have a lot of friends and acquaintances considering a job in the nonprofit sector. I’ve been employed within small (under $2 million budgets), community nonprofit organizations for three years now, beginning straight out of college, but have also talked to many people with many different experiences and histories in the sector and outside of it about their experiences. The following is my boilerplate advice to people that asks me about working, or finding work, within nonprofits.

Assuming that you are an intelligent, well-educated (or seeking to be), self-motivated and upwardly mobile individual, your interest probably spans a combination of two distinct (or should be in your mind) issues:

  • You want a job, with a modicum of stability, freedom, and disposable income.
  • You want to change the world, or a least do it less harm than otherwise.

My advice for you:

Find a corporate job that you like, or don’t feel too guilty about, and that provides you with plenty of disposable income and time. Find a small, local nonprofit (or church, or social group) that meets your standards for doing good, and invest your disposable income and time with them. Join their governing board, connect them with your professional and personal networks and help them grow in a direction you believe in. You will enact more change from a higher level than you could, in most situations, by being a direct employee of that organization.

Non-categorical rationale:

Nonprofits have jobs, but they don’t have a lot of them and it’s hard to break into one that distinguishes you from your peers: you can find a job answering phones, but it’s difficult to get one with responsibility and authority. Nonprofits are bad (or relatively worse than their commercial peers) at: recognizing ability, enabling it, and rewarding it.

Nonprofits are insulating. Because you are constantly understaffed, under-budgeted and under-resourced (time, training, equipment) it is difficult to find the time to truly reflect. It is difficult to critically look at what you are doing and what you have done; to connect with other practitioners and look at what you are doing as a group; to reach outside the sector to learn from others and see how you fit into that broadest context.

A job is a job, wherever you’re working. This may sound selfish (and it probably is) but you should be concerned that, whatever your job is, you:

  1. are challenged
  2. are encouraged to try and learn new things
  3. are acknowledged (even celebrated on occassion)
  4. can advance to greater responsibility and authority
  5. are provided a separate personal life
  6. are afforded physical and mental health (no 80 hour weeks or screaming matches)
  7. have fun or enjoy your work a majority of the time (no puritan work ethic for me)

By looking after yourself on an individual level, you will ultimately be in a better position to have compassion for those around you and be better positioned to act upon that compassion.


Planning Strategic Planning

At work I have been furiously engaged in strategic planning in advance of some major grant-writing. This process is a continuation from some vine-withered efforts my coworker and I had made last fall, but due to some changing circumstances—a better understanding of the existing processes at play and increased authority to manage the outcomes—this most recent effort is bearing more fruit.

Perhaps the largest set-back to our strategic planning projects has been the planning of our strategic planning. Without having to recurse infinitely backwards, perhaps the most important things I’ve learned are:

  • Strategic planning relies on individuals. Strategic planning usually requires the actions of a single, or a small handful of individuals that not only are motivated, but have the resources and authority (or the full backing of someone with authority) to proceed. This is not to diminish the value of SWOT or SMART, but to reaffirm that strategic planning relies upon someone to begin the process, facilitate it, and ensure that its outcomes are useful.

  • Strategic planning should build on your strengths. There is a tendency to relegate strategic planning activities to weak periods or to use it to shore up areas that are perceived lacking. This often means that you aren’t able to properly recognize what led to successful periods or why certain components succeeded.

  • Define goals by need, not by resources or activities. Perhaps due to the tendency to use strategic planning to shore up weaknesses, goals are often defined narrowly out of current activities. Rather, goals should be widest effect you hope to enact. Just because a program’s goals are narrow does not mean that an organization cannot hope to affect a broader mission; or that a program cannot target only a single need within a much larger issue.

These three pieces come from my own experience with strategic planning, but I do like that they tie in nicely with much broader advice on philanthropy from Peter Drucker (via The World We Want):

  1. Fund extraordinary people, not institutions.

  2. Build on islands of health, not problems to be solved.

  3. Get big or get gone. Scale up to the size of the need, not down to the resources available.

(I’m always looking this quote up for a variety of reasons, so for simplicities sake I figure I should just post it here.)



Spring arrives

CIMG2509.JPG

Spring came a lot earlier to Washington, DC than it did to New England, but the mercury is topping 80 today in Boston. Above, I’m at the Cherry Blossom Festival in Washington, DC last month. Below is the one of the multitudinous dandelions that have sprung up around UMass Boston.

Dandelions at UMass Boston


New England Construction

Wrote this a while back and it’s been floating around my drafts pile. The story is a little light, but mostly I wrote it to remember all the arcane details my landlord’s handyman told me when he came to rehang our doors.

Monday morning; my girl was out of town on business. Her business. I was watching the Price is Right: dolts with nothing better to do with their time either. One knocked at my door.

I wiped the spent food refuse and old files towards the far end of my desk. The desk is a large one and sometimes that’s needed. Today it wasn’t.

I called it, her, in.

I’m no bunny humper but fur coats make little sense to me I don’t need the socio-economic, market differentiation explanation. Here’s an old woman, probably afraid to watch the evening news, yet wears the clothing equivalent of a sausage factory.

I operate my business on a sliding scale. I slid it to the right. I’ve been known to do pro-bono, but I didn’t think she’d need it. She didn’t.

“I’ve lost something.”

I keep my list of services vague. Boston’s a tough town and you never know when a treed cat will pay the heating bill.

“A ring, it’s somewhere in my house.”

“I’m sorry, Miss—”

“Capshaw, Rosemary Capshaw.”

“I don’t do house cleaning. Perhaps you have an assistant?” Or a psychic. But from the looks of her she probably did; on retainer. A phrenologist too. I gave the scale another shove.

“No, it’s in my house. Somewhere in the walls.”

“You need a carpenter.” And a shrink. But she’d already have one of those too; maybe should up her dose.

“I had one but he couldn’t find it. He tore my house apart and still couldn’t find it. Then he gave me your number.”

I have to wonder about the people that recommend me. I probably photographed his wife cheating. Maybe roughed the guy up too. Value-addeds make customers for life.

“I’ll see what I can do.” My calendar, if I had a calendar, was empty; also why I don’t have a calendar.

Rosemary drove. I could have asked to bring along an elephant and it would have fit comfortably in the backseat; might have scuffed up the leather though.

Her house was large, but that was to be expected considering she needed somewhere to park the car. It was an old carriage house with a mansard roof. That sort of thing may have fooled the French, who taxed by the floor, but Yankee assayers are more clever. They call it historical architecture and increase the value. But I don’t think property taxes keep Rosemary up at night.

She had a nice place. Selling it would lead to quite the nice South Florida retirement. Nothing like my own apartment. Atlantic city has nothing on the Boston rental market: the odds are horrible and the house always wins. I rent the second floor of a triple-decker. Unless I experience a “life altering” event, I’m guaranteed to be out on my ass: carting my belongings down the street when the place goes condo, or in an ambulance when the owner firebombs it.

She showed me to the attic. Quite a carpenter she found; the place was ripped to shreds. To be expected if the guy recommended me. The floorboards along an entire wall was torn apart, down to the studs.

While we were driving over she explained the situation: priceless ring, slip of the hand, crack in the floor.

“So you’re sure this is exactly where it slipped.”

“Completely. I was right here.”

“What’s beneath this?”

“My parlor.”

The wood in this place was amazing, and I don’t just mean the floor. Moldings, doorframes, inset cabinetry was all perfect. Most times it’s easier just gut them rather than refinish. Replace priceless mahogany and cherry with spruce or pine. Spruce takes a good coat of paint but not much else. As for pine, when I was a baby I teethed on our furniture. Wouldn’t happen here though. Somebody probably sanded their family a new car working on this place and that car would still be out of my price range.

In the parlor the baseboards were completely torn up too, with obvious belief that the ring would have fallen down through the wall. No insulation: typical. Coldest winters in the country and nobody thinks to use a little fiberglass.

I knocked the wall. Carpenter was a dolt. New England houses are always interesting, and he apparently didn’t know this one’s particular peculiarity:

Hurricane brace; a solid beam running diagonally across the wall. From modern architectural standing such a thing is half over-engineering, half-diminished standards: buildings these days are mostly plastic wrap. That it comes from 3M rather than Stretch-Tite doesn’t make much of a difference.

I measured out the dimensions in my head, matching up where she dropped her ring in the attic above.

“Do you mind?”

Plaster walls: it takes a jackhammer to drive a nail into, but any flat, blunt object will bust them wide open. Like a fist.

There are few parts of my CV I enjoy more than my extensive knowledge of and experience in effectively hitting things. The wall gave way in a burst of gypsum and lead paint. Between the mangled wooden strips of plaster-backing, I spotted a glint of something.

I reached through the hole, gave my sliding scale one last tap, and retrieved her ring. Needless to say, she was satisfied. And cleaning up is never a part of my job description.


Introducing Panlexicon.com

Panlexicon.com

I’m very proud to be officially launching ** Panlexicon.com: a unique thesaurus. Using intuitive **“tag clouds” to represent synonyms, Panlexicon makes discovering the word you want quick, easy and explorational.

Panlexicon’s current functions allow you to:

  • First, perform a lookup on a single word and receive a weighted cloud of synonyms.

  • Second, view synonyms that overlap across multiple words either by entering the words manually, or clicking on words already in the cloud to further refine your search.

For example, performing a search on “ cool” provides a wide variety of synonyms from “chilly”, to “unimpassioned”, to “groovy”. Refining the search using cool and nifty provides more refined synonyms.

By varying the size of the typeface, like tag clouds do, the most relevant terms pop out at you allowing you to quickly scan through large lists of words. Also, because the algorithm is a little fuzzy, you may run across related words that provide better context.

Panlexicon was developed jointly with Rebecca who originally proposed the project and did much of the research on thesauri and helped develop the word relevance algorithms.

The word lists come from the Moby Thesaurus as part of Project Gutenburg’s library of free electronic texts. Drupal is used as a simple framework for core functions such as database abstraction and page callbacks and to simplify future feature developments. Google AdSense is activated on the site, but that is due more to curiosity over the interplay of contextual advertising and the word lists than on any current revenue model.


Nonprofit Communications 2.0

Last week I attended NTEN’s 2007 Nonprofit Technology Conference and sat in on a wonderful session entitled Nonprofit Communications 2.0: Seven Steps to Transform Your Organization. Led by Lauren-Glenn Davitian of the CCTV Center for Media and Democracy, the session provided a strong framework for nonprofits to better communicate in an increasingly networked society.

I am also very lucky to serve with Lauren-Glenn on the editorial board of the Community Media Review.

The video itself is approximately 1 hour, 24 minutes long and worth every second, but I included my notes from the session below.

Community building talent is the single most important resource in the modern world.

Peter Drucker

How to engage and mobilize members

A Communications framework for thinking about how organizational objectives are met through interaction. The correlating Development framework is in parenthesis.

  1. Welcome (Prospect)
  2. Educate (Cultivation)
  3. Ask (Involvement)
  4. Thank (Stewardship)

The Seven Steps

  1. Assessment: Defining your goal (What behavior are you trying to change in undertaking a communications strategy?), audience (an explicit, targeted “who” and their values), evaluating your infrastructure (orthodoxies, structure, time, leadership)
  2. Awareness: Start by searching NTEN, TechSoup, Idealware, etc. (Link Research)
  3. Training: A discipline of doing things. How are stories told, infrastructure built and actions communicated to regular people?
  4. Content Production: “The currency of the new world”
  5. Technical Support: An example: how to know when to build and when to buy
  6. Partnerships: Who is going to stand up for you?
  7. Planning: What are the components that revolve around your goal?

I shot this video with a Casio EX-S600, which shoots full-frame (640 x 480) MPEG-4 video. With a two gigabyte SD Card it can shoot approximately an hour and a half of video at medium quality before its battery dies. The Casio’s AVI wrapper is incompatible with iMovie (or any Quicktime decoder), so I first used VisualHub to repackage the video as an MP4 before importing into iMovie to add titles. I exported from iMovie as DV and then converted that with VisualHub into MPEG-4. Compressed and at quarter-frame (320 x 240) the entire video was 105 MB. This time I uploaded to Google Video since Blip.tv stalled out.


MeetAmeriCorps still a success

The Faces of MeetAmeriCorps.com

I just got back from some extended travel in California where I met with some fellow AmeriCorps*VISTAs and, among other things, discussed how we could get our AmeriCorps social networking website growing even faster. Right now the site has over 300 registered users, which is pretty good for a six month old baby.

Most importantly, we’re working on stepping up our outreach. Unlike what some of the hype may tell you, social networking website don’t build themselves. If you build it, they won’t come, at least not if you don’t tell anyone about.

Outreach is key, and unfortunately it usually means a change of strategy. We’re building our networks online because it’s cheap real estate, but on the internet you can’t shout very far and most people are deaf. If you’re trying to get in front of someone’s face (or next to their ear), putting something on the internet is probably the worst way to go about it.

Physical objects are best, so I’m in the process of designing a postcard to mail out to AmeriCorps host organizations.

Also, because MeetAmeriCorps.com already has so many members across the country, we can have them lend a hand too in outreach activities. That’s always a benefit of working with AmeriCorps: we love to help.