<?php
// $Id$
//////////////////////////////////////////////////////////////////////////////
// Module settings
define('EXHIBIT_DATE_FORMAT', '%Y-%m-%dT%H:%M:%SZ');
define('EXHIBIT_XMLNS_URL', 'http://simile.mit.edu/2006/11/exhibit#'); // TODO: What's this for? This needs to be updated or removed
define('EXHIBIT_JS_URL', variable_get('exhibit_js_url', 'http://api.simile-widgets.org/exhibit/2.2.0/exhibit-api.js'));
define('EXHIBIT_MAP_JS_URL', variable_get('exhibit_map_js_url', 'http://api.simile-widgets.org/exhibit/2.2.0/extensions/map/map-extension.js'));
define('EXHIBIT_BARCHART_JS_URL', variable_get('exhibit_barchart_js_url', 'http://api.simile-widgets.org/exhibit/2.2.0/extensions/chart/chart-extension.js'));
define('EXHIBIT_SCATTERPLOT_JS_URL', variable_get('exhibit_scatterplot_js_url', 'http://api.simile-widgets.org/exhibit/2.2.0/extensions/chart/chart-extension.js'));
define('EXHIBIT_TIMELINE_JS_URL', variable_get('exhibit_timeline_js_url', 'http://api.simile-widgets.org/exhibit/2.2.0/extensions/time/time-extension.js'));
define('EXHIBIT_CALENDAR_JS_URL', variable_get('exhibit_calendar_js_url', 'http://api.simile-widgets.org/exhibit/2.2.0/extensions/calendar/calendar-extension.js'));
define('EXHIBIT_FEED_LIFETIME', (int)variable_get('timeline_feed_lifetime', 0));
define('EXHIBIT_FEED_ETAG', (bool)variable_get('timeline_feed_etag', TRUE));
//////////////////////////////////////////////////////////////////////////////
// Core API hooks
/**
* Implementation of hook_help().
*/
function exhibit_help($path) {
switch ($path) {
case 'admin/help#exhibit':
return '<p>' . t('') . '</p>'; // TODO
case 'admin/settings/exhibit':
//return '<p>' . t('') . '</p>'; // TODO
}
}
/**
* Implementation of hook_perm().
*/
function exhibit_perm() {
return array(
'administer exhibit feeds',
//'use external exhibit feeds', // TODO
'access exhibits',
'create exhibits',
'edit exhibits',
'edit own exhibits',
'delete exhibits',
'delete own exhibits',
);
}
/**
* Implementation of hook_menu().
*/
function exhibit_menu() {
return array(
'node/%node/exhibit-json' => array(
'type' => MENU_CALLBACK,
'access callback' => 'user_access',
'access arguments' => array('access exhibits'),
'page callback' => 'exhibit_output_node',
'page arguments' => array(1),
'file' => 'exhibit.pages.inc',
),
'node/__history__.html' => array(
'type' => MENU_CALLBACK,
'access arguments' => array('access exhibits'),
'page callback' => 'exhibit_output_history',
'file' => 'exhibit.pages.inc',
),
'exhibit/tsv/%' => array(
'type' => MENU_CALLBACK,
'access arguments' => array('access exhibits'),
'page callback' => 'exhibit_output_convert',
'page arguments' => array(2, 'exhibit_parse_tsv', 'text/tab-separated-values'),
'file' => 'exhibit.pages.inc',
),
'exhibit/feeds/%exhibit_feed' => array(
'type' => MENU_CALLBACK,
'access arguments' => array('access exhibits'),
'page callback' => 'exhibit_output_feed',
'page arguments' => array(2),
'file' => 'exhibit.pages.inc',
),
'admin/settings/exhibit' => array(
'title' => 'Exhibits',
'description' => 'Settings for the Exhibit module.',
'access arguments' => array('administer site configuration'),
'page callback' => 'drupal_get_form',
'page arguments' => array('exhibit_admin_settings'),
'file' => 'exhibit.admin.inc',
),
'admin/content/exhibit' => array(
'title' => 'Exhibit feeds',
'access arguments' => array('administer exhibit feeds'),
'page callback' => 'exhibit_admin_feeds',
'file' => 'exhibit.admin.inc',
),
'admin/content/exhibit/list' => array(
'title' => 'List',
'type' => MENU_DEFAULT_LOCAL_TASK,
'access arguments' => array('administer exhibit feeds'),
'weight' => -10,
),
'admin/content/exhibit/add' => array(
'title' => 'Add data feed',
'type' => MENU_LOCAL_TASK,
'access arguments' => array('administer exhibit feeds'),
'page callback' => 'drupal_get_form',
'page arguments' => array('exhibit_admin_feed_form'),
'file' => 'exhibit.admin.inc',
'weight' => 10,
),
'admin/content/exhibit/edit/%exhibit_feed' => array(
'title' => 'Edit data feed',
'type' => MENU_CALLBACK,
'access arguments' => array('administer exhibit feeds'),
'page callback' => 'drupal_get_form',
'page arguments' => array('exhibit_admin_feed_form', 4),
'file' => 'exhibit.admin.inc',
),
'admin/content/exhibit/delete/%exhibit_feed' => array(
'title' => 'Delete data feed',
'type' => MENU_CALLBACK,
'access arguments' => array('administer exhibit feeds'),
'page callback' => 'drupal_get_form',
'page arguments' => array('exhibit_admin_feed_delete', 4),
'file' => 'exhibit.admin.inc',
),
);
}
/**
* Implementation of hook_block().
*/
function exhibit_block($op = 'list', $delta = 0, $edit = array()) {
switch ($op) {
case 'list':
return array('facets' => array('info' => t('Exhibit facets')));
case 'view':
switch ($delta) {
case 'facets':
if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) != 'edit') {
$node = node_load(arg(1));
$block['content'] = $node->exhibit['facet_definition'];
}
break;
}
return $block;
}
}
/**
* Implementation of hook_theme().
*/
function exhibit_theme() {
return array(
'exhibit_definition' => array(
'arguments' => array('node' => NULL),
),
);
}
//////////////////////////////////////////////////////////////////////////////
// Node API hooks
/**
* Implementation of hook_node_info().
*/
function exhibit_node_info() {
return array(
'exhibit' => array(
'name' => t('Exhibit'),
'module' => 'exhibit', //_node',
'description' => t('An <em>exhibit</em> displays structured data in the form of rich visualizations that can be searched, filtered and sorted using faceted browsing.'),
'has_title' => TRUE,
'title_label' => t('Title'),
'has_body' => TRUE,
'body_label' => t('Description'),
),
);
}
/**
* Implementation of hook_access().
*/
function exhibit_access($op, $node) {
switch ($op) {
case 'view':
return user_access('access exhibits');
case 'create':
return user_access('create exhibits');
case 'update':
global $user;
return user_access('edit exhibits') || (user_access('edit own exhibits') && $user->uid == $node->uid);
case 'delete':
global $user;
return user_access('delete exhibits') || (user_access('delete own exhibits') && $user->uid == $node->uid);
}
}
/**
* Implementation of hook_load().
*/
function exhibit_load($node) {
$exhibit = db_fetch_array(db_query('SELECT * FROM {exhibit_nodes} WHERE vid = %d', $node->vid));
$exhibit['feeds'] = explode(',', $exhibit['feeds']);
return (object)array('exhibit' => $exhibit);
}
/**
* Implementation of hook_validate().
*/
function exhibit_validate(&$node) {}
/**
* Implementation of hook_insert().
*/
function exhibit_insert($node) {
$node->exhibit['feeds'] = implode(',', array_filter(array_values($node->exhibit['feeds']), 'trim'));
db_query("INSERT INTO {exhibit_nodes} (nid, vid, feeds, definition, facet_definition) VALUES (%d, %d, '%s', '%s', '%s')", $node->nid, $node->vid, $node->exhibit['feeds'], $node->exhibit['definition'], $node->exhibit['facet_definition']);
}
/**
* Implementation of hook_update().
*/
function exhibit_update($node) {
if ($node->revision) {
return exhibit_insert($node);
}
$node->exhibit['feeds'] = implode(',', array_filter(array_values($node->exhibit['feeds']), 'trim'));
db_query("UPDATE {exhibit_nodes} SET feeds = '%s', definition = '%s', facet_definition = '%s' WHERE vid = %d", $node->exhibit['feeds'], $node->exhibit['definition'], $node->exhibit['facet_definition'], $node->vid);
}
/**
* Implementation of hook_delete().
*/
function exhibit_delete($node) {
db_query('DELETE FROM {exhibit_nodes} WHERE nid = %d', $node->nid);
}
/**
* Implementation of hook_nodeapi().
*/
function exhibit_nodeapi(&$node, $op, $teaser, $page) {
switch ($op) {
case 'delete revision':
db_query('DELETE FROM {exhibit_nodes} WHERE vid = %d', $node->vid);
break;
}
}
/**
* Implementation of hook_form().
*/
function exhibit_form(&$node, &$param) {
drupal_add_js(drupal_get_path('module', 'exhibit') .'/exhibit.js');
drupal_add_js(array('exhibit' => exhibit_get_feeds('url')), 'setting');
$type = node_get_types('type', $node);
if ($type->has_title) {
$form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5);
}
if ($type->has_body) {
// FIXME: this works around a bug in node_submit(), lines 782-793, which
// causes the body field to be emptied when not using node_body_field()
// to generate the body and format fieldset. Need to submit a core patch.
$form['teaser_include'] = array('#type' => 'hidden', '#value' => '1');
$form['body_filter'] = array('#type' => 'fieldset', '#title' => check_plain($type->body_label), '#collapsible' => TRUE, '#collapsed' => TRUE);
$form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Description'), '#default_value' => $node->body, '#required' => ($type->min_word_count > 0), '#rows' => 2);
$form['body_filter']['format'] = filter_form($node->format);
}
$form['exhibit'] = array('#type' => 'fieldset', '#title' => t('Exhibit'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE);
$form['exhibit']['feeds'] = array('#type' => 'checkboxes', '#title' => t('Data feeds'), '#default_value' => !empty($node->exhibit['feeds']) ? $node->exhibit['feeds'] : array(), '#options' => exhibit_get_feeds('title'), '#attributes' => array('class' => 'exhibit-feed-checkbox'), '#prefix' => '<div id="feed-field-selection">', '#suffix' => '</div>');
$form['exhibit']['definition'] = array('#type' => 'textarea', '#title' => t('Definition'), '#default_value' => !empty($node->exhibit['definition']) ? $node->exhibit['definition'] : theme('exhibit_definition'), '#rows' => 10);
$form['exhibit']['facet_definition'] = array('#type' => 'textarea', '#title' => t('Facet definition'), '#default_value' => @$node->exhibit['facet_definition'], '#rows' => 5);
$form['exhibit']['facet-generator-form-wrapper'] = array('#type' => 'fieldset', '#title' => t('Facet Generator'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#tree' => TRUE, '#attributes' => array('id' => 'exhibit-facet-builder', 'style' => 'display:none;'), '#value' => '<div id="facet-generator-form"></div>');
return $form;
}
/**
* Implementation of hook_view().
*/
function exhibit_view($node, $teaser = FALSE, $page = FALSE) {
// Load the SIMILE Exhibit API and any enabled extensions
drupal_set_html_head('<script type="text/javascript" src="'. EXHIBIT_JS_URL .'"></script>'."\n");
// Load any enabled Exhibit extensions
foreach (exhibit_exhibit_views() as $id => $view) {
if (variable_get('exhibit_extensions_'. $id, FALSE)) {
drupal_set_html_head('<script type="text/javascript" src="'. $view['href'] .'"></script>'."\n");
}
}
// Insert <LINK> references to all enabled data feeds
foreach ($node->exhibit['feeds'] as $fid) {
if ($feed = exhibit_feed_load($fid)) {
exhibit_add_feed(exhibit_get_feed_url($feed), check_plain($feed['type']));
}
}
$node = node_prepare($node, $teaser);
$node->content['exhibit'] = array('#value' => $node->exhibit['definition'], '#weight' => 1);
return $node;
}
//////////////////////////////////////////////////////////////////////////////
// Views API hooks
/**
* Implementation of hook_views_api().
*/
function exhibit_views_api() {
return array(
'api' => 2,
'path' => drupal_get_path('module', 'exhibit'),
);
}
//////////////////////////////////////////////////////////////////////////////
// Exhibit API hooks
/**
* Implementation of hook_exhibit_views().
*/
function exhibit_exhibit_views() {
return array(
'tile' => array(
'title' => t('Tile (default)'),
'class' => '',
),
'thumbnail' => array(
'title' => t('Thumbnail'),
'class' => 'Thumbnail',
),
'map' => array(
'title' => t('Map'),
'class' => 'Map',
'href' => variable_get('exhibit_extensions_map_api', EXHIBIT_MAP_JS_URL) . '?gmapkey=' . (module_exists('gmap') ? gmap_get_key() : variable_get('exhibit_gmap_key', '')),
),
'barchart' => array(
'title' => t('Barchart'),
'class' => 'Barchart',
'href' => variable_get('exhibit_extensions_barchart_api', EXHIBIT_BARCHART_JS_URL),
),
'scatterplot' => array(
'title' => t('Scatterplot'),
'class' => 'Scatterplot',
'href' => variable_get('exhibit_extensions_scatterplot_api', EXHIBIT_SCATTERPLOT_JS_URL),
),
'timeline' => array(
'title' => t('Timeline'),
'class' => 'Timeline',
'href' => variable_get('exhibit_extensions_timeline_api', EXHIBIT_TIMELINE_JS_URL),
),
'calendar' => array(
'title' => t('Calendar'),
'class' => 'Calendar',
'href' => variable_get('exhibit_extensions_calendar_api', EXHIBIT_CALENDAR_JS_URL),
),
'tabular' => array(
'title' => t('Tabular'),
'class' => 'Tabular',
),
);
}
//////////////////////////////////////////////////////////////////////////////
// Exhibit API functions
function exhibit_add_link($url) {
drupal_add_link(array('rel' => 'meta', 'type' => 'application/json', 'title' => 'Exhibit JSON', 'href' => exhibit_get_url($url)));
}
function exhibit_get_url($url) {
if (exhibit_is_link_internal($url)) {
$url_parts = parse_url($url);
// Handle views args
if (isset($_GET['args'])) {
$url_parts['path'] = preg_replace('/\%/', $_GET['args'], $url_parts['path']);
}
if (empty($url_parts['query'])) {
return url($url_parts['path']);
}
else {
return url($url_parts['path'], array('query' => $url_parts['query']));
}
}
else {
return url($url, array('external' => TRUE));
}
}
function exhibit_is_link_internal($url) {
return (bool)!preg_match('!^[\w\d\+\-]+://!', $url);
}
function exhibit_add_feed($url, $type = 'application/json') {
switch ($type) {
case 'application/vnd.google-spreadsheet': // special case
drupal_add_link(array('rel' => 'exhibit/data', 'href' => $url, 'type' => 'application/jsonp', 'ex:converter' => 'googleSpreadsheets'));
break;
default:
drupal_add_link(array('rel' => 'exhibit/data', 'href' => $url, 'type' => $type));
break;
}
}
function exhibit_feed_load($fid) {
if (is_numeric($fid)) {
return db_fetch_array(db_query('SELECT f.* FROM {exhibit_feeds} f WHERE f.fid = %d', $fid));
}
else {
if (module_exists('views')) {
$views = views_get_all_views();
foreach ($views as $view) {
// Skip disabled views or broken views
if (!empty($view->disabled) || empty($view->display)) {
continue;
}
// Check each display, add those using the Exhibit JSON style plugin
foreach (array_keys($view->display) as $id) {
$plugin = views_fetch_plugin_data('display', $view->display[$id]->display_plugin);
$display_options = $view->display[$id]->display_options;
if ($plugin['handler'] == 'views_plugin_display_feed' && $display_options['style_plugin'] == 'exhibit_json') {
$view_feed_id = $view->vid . '_' . $id;
if ($view_feed_id == $fid) {
return array('fid' => $view_feed_id,
'module' => 'views',
'title' => $view->name . ': ' . $view->display[$id]->display_title,
'enabled' => TRUE,
'cache' => FALSE,
'type' => 'application/json',
'url' => $display_options['path']);
}
}
}
}
}
}
}
function exhibit_get_feeds($key = NULL) {
$feeds = array();
$result = db_query('SELECT f.* FROM {exhibit_feeds} f ORDER BY f.title ASC');
while ($feed = db_fetch_object($result)) {
$feeds[$feed->fid] = $key ? $feed->$key : $feed;
}
// Check for Exhibit JSON feeds in views and add automaticall add them to the list
if (module_exists('views')) {
$views = views_get_all_views();
foreach ($views as $view) {
// Skip disabled views or broken views
if (!empty($view->disabled) || empty($view->display)) {
continue;
}
// Check each display, add those using the Exhibit JSON style plugin
foreach (array_keys($view->display) as $id) {
$plugin = views_fetch_plugin_data('display', $view->display[$id]->display_plugin);
$display_options = $view->display[$id]->display_options;
if ($plugin['handler'] == 'views_plugin_display_feed' && $display_options['style_plugin'] == 'exhibit_json') {
$view_feed_id = $view->vid . '_' . $id;
if ($key) {
switch ($key) {
case 'fid':
$feeds[$view_feed_id] = $view_feed_id;
break;
case 'module':
$feeds[$view_feed_id] = 'views';
break;
case 'title':
$feeds[$view_feed_id] = l($view->name . ': ' . $view->display[$id]->display_title, 'admin/build/views/edit/' . $view->name);
break;
case 'enabled':
$feeds[$view_feed_id] = TRUE;
break;
case 'cache':
$feeds[$view_feed_id] = FALSE;
break;
case 'type':
$feeds[$view_feed_id] = 'application/json';
break;
case 'url':
$feeds[$view_feed_id] = $display_options['path'];
break;
}
}
else {
$feeds[$view_feed_id] = (object)array('fid' => $view_feed_id,
'module' => 'views',
'title' => l($view->name . ': ' . $view->display[$id]->display_title, 'admin/build/views/edit/' . $view->name),
'enabled' => TRUE,
'cache' => FALSE,
'type' => 'application/json',
'url' => $display_options['path']);
}
}
}
}
}
return $feeds;
}
function exhibit_get_feed_url($feed) {
$feed = (object)$feed;
return exhibit_get_url(!empty($feed->cache) ? 'exhibit/feeds/' . $feed->fid : $feed->url);
}
function exhibit_get_feed_contents($url) {
if (exhibit_is_link_internal($url)) {
// Internal Drupal path
ob_start();
$result = $url ? menu_execute_active_handler($url) : MENU_NOT_FOUND;
$output = ob_get_clean();
if (is_int($result)) {
switch ($result) {
case MENU_NOT_FOUND:
return die(drupal_not_found());
case MENU_ACCESS_DENIED:
return die(drupal_access_denied());
}
}
}
else {
// External URL
$output = file_get_contents($url);
}
return $output;
}
function exhibit_json($items, $types = NULL, $properties = NULL) {
return exhibit_compact_item(array(
'types' => $types,
'properties' => $properties,
'items' => $items,
));
}
function exhibit_compact_item($item) {
foreach ($item as $k => $v) {
if (is_null($v)) {
unset($item[$k]);
}
}
return $item;
}
//////////////////////////////////////////////////////////////////////////////
// Theme callbacks
function theme_exhibit_definition() { // TODO
return '<div ex:role="viewPanel"><div ex:role="view"></div></div>';
}
function theme_exhibit_lens() { // TODO
return '<div ex:content=".label" class="label"></div>';
}