diff --git docs/STYLE_PLUGINS.txt docs/STYLE_PLUGINS.txt new file mode 100644 index 0000000..f9704f5 --- /dev/null +++ docs/STYLE_PLUGINS.txt @@ -0,0 +1,181 @@ +$Id$ + +Current for 6.x-2.x + +Style plugins are code snippets providing style context +callbacks to OpenLayers styles. + +Such callbacks (written in JavaScript) are needed to provide +styling which is dependent on characteristics which are not +known at time of PHP rendering. + +A common example is changing pointRadius style property based +on the number of features taking place in a cluster, whereas +clustering is performed by JavaScript and so cluster composition +changes w/out PHP being invoked. + +# Creating a new OpenLayers Style Plugin from Scratch + +First, you'll need to create a module. Of course, skip through this step if +there's already a module that exists to which this style plugin will be added. +But if not, create a file called `modulename.info` with the contents + + core = "6.x" + dependencies[] = "openlayers" + name = "modulename" + package = "OpenLayers" + project = "modulename" + +Then you'll want to register your style plugins. + +## Registering plugins directory + +This is done implementing `hook_ctools_plugin_directory' +and `hook_ctools_plugin_api'. + +Since your module is called `modulename', their implementation would be: + + function modulename_ctools_plugin_api($module, $api) { + // Define plugins for OpenLayers plugins api + if ($module == "openlayers") { + switch ($api) { + case 'openlayers_styles': + return array('version' => 1); + } + } + } + + function modulename_ctools_plugin_directory($module, $plugin) { + if ($module == 'openlayers' && $plugin == 'style_plugin') { + return 'plugins/style_plugin'; + } + } + +At that point you can put actual plugins, one file per plugin, +in the `plugins/style_plugin' directory, or whatever you returned +from the `hook_ctools_plugin_directory' implementation. + +## Writing the minimal PHP side of plugin + +A plugin file starts with a plugin definition, like the +following: + + // Define plugin for CTools + $plugin = array( + 'title' => t('My style plugin title'), + 'description' => t('What this plugin does'), + 'style_plugin' => array( + 'class' => 'modulename_pluginname', // <-- NOTE! + 'parent' => 'openlayers_style_plugin', + ), + ); + +The above definition says that the plugin is implemented by +the `modulename_pluginname' class (see NOTE above), which is +a subclass of `openlayers_style_plugin`. We implement that next: + + + class modulename_pluginname extends openlayers_style_plugin { + + // Advertise which style properties we expose support for, + // and which callback to use for each one + function get_context_properties() { + return array( + 'label' => 'getLabel' // <-- NOTE ! + ); + } + + // Send the JS file containing the JavaScript side + // implementation of the plugin. + function render() { + drupal_add_js(drupal_get_path('module', 'modulename') . + '/plugins/style_plugin/modulename_pluginname.js'); + } + + } + +The above definition provides NO parameters to the actual context +callback function. We'll see how to add configurability later. +For now let's move on to the JavaScript side. + +## Writing the JavaScript side of plugin + +The OpenLayers module renderer will expect to find a +Drupal.openlayers.style_plugins. +Javascript class, as specified by the 'class' attribute of +the 'style_plugin' array in the PHP plugin definition. + +The JavaScript side of style plugin will be put in the file +referenced by the render() function of the PHP part, and will need +to provide this class. As with the following example: + + // Define the class constructor + Drupal.openlayers.style_plugin.modulename_pluginname = function (params) { + this.params = params; + }; + +Note that the constructor can take parameters, which the PHP side +of the style plugin can export to let user configure the style plugin. +We'll see this later. + +In addition to the constructor, the Drupal OpenLayers module (`DOL', +from now on) will also expect to find the callbacks advertised +by the `get_context_properties` function of the PHP side of the style +plugin. We'll need to provide those as well. They can be defined +in the class prototype: + + // Style plugin class prototype + Drupal.openlayers.style_plugin.modulename_pluginname.prototype = { + + // Style context function + 'getLabel' = function(feature) { + return feature.attributes.length; + } + + } + +## Providing configurability of style plugin + +The PHP side of code can provide a configuration form for the plugin. +As you saw above, the JavaScript class constructor will be passed +configuration parameters. These parameters you can define from +the PHP side, defining an options_form() and optionally an +options_init() method. Example: + + function options_init() { + return array( + 'feature_weight' => 2, + 'point_radius_min' => 6, + 'point_radius_max' => 12, + ); + } + + function options_form($defaults = array()) { + $form = array(); + + $form['point_radius_min'] = array( + '#type' => 'textfield', + '#title' => t('Min radius'), + '#description' => t('Minimum value for the point radius.'), + '#default_value' => isset($defaults['point_radius_min']) ? + $defaults['point_radius_min'] : 6, + ); + $form['point_radius_max'] = array( + '#type' => 'textfield', + '#title' => t('Max radius'), + '#description' => t('Maximum value for the point radius.'), + '#default_value' => isset($defaults['point_radius_max']) ? + $defaults['point_radius_max'] : 12, + ); + $form['feature_weight'] = array( + '#type' => 'textfield', + '#title' => t('Feature weight'), + '#description' => t('Weight of each additional feature for the final point + radius.'), + '#default_value' => isset($defaults['feature_weight']) ? + $defaults['feature_weight'] : 2, + ); + + return $form; + } + diff --git docs/openlayers.api.php docs/openlayers.api.php index 7951b81..dcb75f3 100644 --- docs/openlayers.api.php +++ docs/openlayers.api.php @@ -282,3 +282,33 @@ function hook_openlayers_presets() { ); return array('default' => $default); } + +/** + * CTools Registration Hook (Style Plugins) + * + * IMPORTANT: + * + * In order to support style plugins, the first step is to + * tell CTools where to find the plugin. + * + * This function is just an example implementation of + * hook_ctools_plugin_directory() and should be alter according to + * your module's name. + * + * For an example, please see the openlayers_test.module + * + * @param $module + * Name of a module that supports CTools exportables. + * @param $plugin + * Name of the kind of plugin supported. + * @return + * If $module is 'openlayers', and $api is a type of exportable that + * your module provides, and you are using Openlayers 2.x, then + * return the directory relative to a module to look for this + * particular plugin. + */ +function openlayers_ctools_plugin_directory($module, $plugin) { + if ($module == 'openlayers' && $plugin == 'style_plugin') { + return 'plugins/style_plugin'; + } +} \ No newline at end of file diff --git includes/openlayers.render.inc includes/openlayers.render.inc index 5e018cd..418d94f 100644 --- includes/openlayers.render.inc +++ includes/openlayers.render.inc @@ -72,10 +72,41 @@ function _openlayers_behaviors_render($behaviors = array(), &$map = array()) { */ function _openlayers_styles_process($styles = array(), $layer_styles = array(), &$map = array()) { + ctools_include('plugins'); // Get styles info array $styles_info = openlayers_styles(); + // Process with handler if available. + $used_plugins = array(); + foreach ($styles_info as $i => $style) { + // Check for property plugins. + foreach ($style->data as $prop => $propval) { + if ( is_array($propval) ) { + $plugname = $propval['plugin']; + if ( ! empty($plugname) ) { + // the above should never happen, except + // for cases in which the old style plugin + // system was used (for prop == 'plugins') + $used_plugins[$plugname] = TRUE; + } + } + } + } + + if ( ! empty($used_plugins) ) { + $style_plugins = openlayers_style_plugins(); + foreach (array_keys($used_plugins) as $plugname) { + $plug = $style_plugins[$plugname]; + $plugin_class = ctools_plugin_get_class($plug, 'style_plugin'); + if (isset($plugin_class)) { + $plugin = new $plugin_class; + $plugin->render(); + } + } + } + + // Go through styles $processed = array(); foreach ($styles as $k => $style) { diff --git js/openlayers.js js/openlayers.js index e8d1589..cb0b4f2 100644 --- js/openlayers.js +++ js/openlayers.js @@ -281,18 +281,67 @@ Drupal.openlayers = { layer.addFeatures(newFeatures); } }, + /** + * Build an OpenLayers style from a drupal style object + * + * @param map Drupal settings object for the map (const) + * @param style_in Drupal settings object for the style (const) + */ + 'buildStyle': function(map, style_in) { + // Build context object and callback values (if needed) + var style_out = {} + var newContext = {} + for (var propname in style_in) { + if (typeof style_in[propname] == 'object') { + var plugin_spec = style_in[propname]; + var plugin_name = plugin_spec['plugin']; + var plugin_options = plugin_spec['conf']; + var plugin_class = Drupal.openlayers.style_plugin[plugin_name]; + // Check for existance of plugin_context_class here + if ( typeof plugin_class === 'function' ) { + var plugin_context = new plugin_class(plugin_options); + + // Add plugin context functions to global context + for (var key in plugin_context) { + var newkey = plugin_name + '_' + propname + '_' + key; + var val = plugin_context[key]; + if ( typeof val === 'function' ) { + newContext[newkey] = OpenLayers.Function.bind(val, + plugin_context); + } + } + } + style_out[propname] = '${' + newkey + '}'; + } else { + style_out[propname] = style_in[propname]; + } + } + + // Instantiate an OL style object. + var olStyle = new OpenLayers.Style(style_out, { context: newContext } ); + return olStyle; + }, 'getStyleMap': function(map, layername) { if (map.styles) { + var stylesAdded = {}; + // Grab and map base styles. - for (var style in map.styles) { - stylesAdded[style] = new OpenLayers.Style(map.styles[style]); + for (var style_name in map.styles) { + var style = map.styles[style_name]; // TODO: skip if undefined ? + stylesAdded[style_name] = this.buildStyle(map, style); } + // Implement layer-specific styles. if (map.layer_styles !== undefined && map.layer_styles[layername]) { - var style = map.layer_styles[layername]; - stylesAdded['default'] = new OpenLayers.Style(map.styles[style]); + + var style_name = map.layer_styles[layername]; + var style = map.styles[style_name]; // TODO: skip if undefined ? + + stylesAdded['default'] = stylesAdded['select'] = this.buildStyle(map, style); + } + return new OpenLayers.StyleMap(stylesAdded); } // Default styles @@ -323,3 +372,4 @@ Drupal.openlayers = { }; Drupal.openlayers.layer = {}; +Drupal.openlayers.style_plugin = {}; diff --git modules/openlayers_ui/includes/openlayers_ui.styles.inc modules/openlayers_ui/includes/openlayers_ui.styles.inc index 54f149b..0a7ad89 100644 --- modules/openlayers_ui/includes/openlayers_ui.styles.inc +++ modules/openlayers_ui/includes/openlayers_ui.styles.inc @@ -12,9 +12,7 @@ /** * Styles add/edit form. */ -function openlayers_ui_styles_form(&$form_state, $style = NULL, $edit = FALSE) { - $form = array(); - +function openlayers_ui_styles_get_properties() { // Available styling properies. Defaults and descriptions are taken // from OpenLayers. // @see http://docs.openlayers.org/library/feature_styling.html @@ -205,17 +203,34 @@ function openlayers_ui_styles_form(&$form_state, $style = NULL, $edit = FALSE) { 'desc' => t('Label font weight.'), ), ); + + return $properties; +} + +/** + * Styles add/edit form. + */ +function openlayers_ui_styles_form(&$form_state, $style = NULL, $edit = FALSE) { + $form = array(); + + $properties = openlayers_ui_styles_get_properties(); // Pass style data along $form['style_data'] = array( '#type' => 'value', - '#value' => $properties, + '#value' => array( + 'definitions' => $properties, + 'defaults' => $style->data + ) ); // Style object basics $form['info'] = array( '#type' => 'fieldset', '#tree' => FALSE, + '#title' => t('Basic Information'), + '#description' => t('The basic information for the style, used to refer to and describe the style.'), + '#collapsible' => TRUE, ); $form['info']['name'] = array( '#title' => t('Name'), @@ -236,21 +251,89 @@ function openlayers_ui_styles_form(&$form_state, $style = NULL, $edit = FALSE) { ); // OpenLayers style properties - $form['data'] = array('#type' => 'fieldset', '#tree' => TRUE); - + $form['data'] = array( + '#type' => 'fieldset', + '#tree' => TRUE, + '#title' => t('Style Properties and Plugins'), + '#description' => t('Style properties are properties as + defined by the OpenLayers library. Plugins are dynamically + process the layer at render time; plugins may override the + values that you have set for style properies.'), + '#collapsible' => TRUE, + ); foreach ($properties as $key => $prop) { + $form['data'][$key] = array( - '#type' => !isset($prop['options']) ? 'textfield' : 'select', + '#type' => 'fieldset', + '#tree' => TRUE, '#title' => $key, '#description' => $prop['desc'], - '#default_value' => isset($style->data[$key]) ? - $style->data[$key] : $prop['default'], + '#collapsible' => TRUE, + '#collapsed' => TRUE, ); - - // Add options if needed - if (isset($prop['options']) && is_array($prop['options'])) { - $form['data'][$key]['#options'] = $prop['options']; + + $def_value = $prop['default']; + $def_use_plugin = ''; + + if ( isset($style->data[$key]) ) { + if ( is_array($style->data[$key]) ) { + $def_use_plugin = $style->data[$key]['plugin']; + } else { + $def_value = $style->data[$key]; + } + } + + // Add plugin options, if any + $handling_plugins = openlayers_ui_get_style_plugins_for_property($key); + + if ( ! empty($handling_plugins) ) { + $plugin_options = array('' => t('none')); + foreach ($handling_plugins as $plugname => $plug) { + $plugin_options[$plugname] = $plug['title']; + } + + $form['data'][$key]['uses_plugin'] = array( + '#title' => t('Use plugin'), + '#type' => 'select', + '#options' => $plugin_options, + '#default_value' => $def_use_plugin, + '#ahah' => array( + 'path' => 'openlayers/ahah/style_plugin/' . $key, + 'wrapper' => $key . '-style-plugin', + 'method' => 'replace', + 'effect' => 'fade', // 'fade', 'none', 'slide' + ) + ); + } + + // Hackish... but is that new for HTML ? ... + $form['data'][$key]['plugin_conf_start'] = array( + '#value' => '
' + ); + + if ( is_array($style->data[$key]) ) { + $defaults = $style->data[$key]['conf']; + $plugname = $style->data[$key]['plugin']; + $form['data'][$key]['plugin'] = + openlayers_ui_get_style_plugin_form($def_use_plugin, $defaults); + } else { + $form['data'][$key]['value'] = array( + '#type' => !isset($prop['options']) ? 'textfield' : 'select', + '#default_value' => $def_value + ); + + // Add options if needed + if (isset($prop['options']) && is_array($prop['options'])) { + $form['data'][$key]['value']['#options'] = $prop['options']; + } } + + + // Hackish... but is that new for HTML ? ... + $form['data'][$key]['plugin_conf_end'] = array( + '#value' => '
' + ); + } $form['submit'] = array( @@ -264,32 +347,48 @@ function openlayers_ui_styles_form(&$form_state, $style = NULL, $edit = FALSE) { * Submit handler for layers. */ function openlayers_ui_styles_form_submit(&$form, &$form_state) { - $style_data = $form_state['values']['style_data']; + + $prop_defn = $form_state['values']['style_data']['definitions']; + //$data = $form_state['values']['data']; + $data = $form_state['clicked_button']['#post']['data']; + // Cast and unset values so JS can handle them better, - // Unless vluase is in form ${attribute} - foreach ($form_state['values']['data'] as $key => $value) { - if ($form_state['values']['data'][$key] === '') { - unset($form_state['values']['data'][$key]); - } - elseif (isset($style_data[$key]['type']) && - strpos($form_state['values']['data'][$key], '${') !== 0) { - if ($style_data[$key]['type'] == 'integer') { - $form_state['values']['data'][$key] = - (int) $form_state['values']['data'][$key]; + // Unless value is in form ${attribute} + foreach ($data as $key => $value) { + if ( $data[$key]['uses_plugin'] == '' ) { + $data[$key] = $data[$key]['value']; + if ($data[$key] === '') { + unset($data[$key]); + } + elseif (isset($prop_defn[$key]['type']) && + strpos($data[$key], '${') !== 0) { + if ($prop_defn[$key]['type'] == 'integer') { + $data[$key] = (int) $data[$key]; + } + elseif ($prop_defn[$key]['type'] == 'float') { + $data[$key] = (float) $data[$key]; + } } - elseif ($style_data[$key]['type'] == 'float') { - $form_state['values']['data'][$key] = - (float) $form_state['values']['data'][$key]; + } else { + $spec = array( + 'plugin' => $data[$key]['uses_plugin'], + ); + if ( isset($data[$key]['plugin']['conf']) ) { + $spec['conf'] = $data[$key]['plugin']['conf']; } + $data[$key] = $spec; } } - + + $form_state['values']['data'] = $data; + $form_state['clicked_button']['#post']['data'] = $data; + $style = new stdClass(); $style->name = $form_state['values']['name']; $style->title = $form_state['values']['title']; $style->description = $form_state['values']['description']; - $style->data = $form_state['values']['data']; + $style->data = $data; $success = openlayers_style_save($style); @@ -303,3 +402,140 @@ function openlayers_ui_styles_form_submit(&$form, &$form_state) { form_set_error('openlayers', t('Error trying to save style.')); } } + +/** + * Get a list of style plugins providing handling of a given property + * + * @param $propname Name of the property we're interested in + * + */ +function openlayers_ui_get_style_plugins_for_property($propname) { + $handling = array(); + $available = openlayers_style_plugins(); + foreach ($available as $plugname => $plugin) { + $plugin_class = ctools_plugin_get_class($plugin, 'style_plugin'); + if (empty($plugin_class)) continue; // should we watchdog here ? + $plugin_instance = new $plugin_class; + if ( $plugin_instance->can_handle_property($propname) ) { + $handling[$plugname] = $plugin; + } + } + return $handling; +} + +/** + * Get options of a style plugin by plugin name + */ +function openlayers_ui_get_style_plugin_form($plugname, $defaults) { + + $form = array(); + + $available = openlayers_style_plugins(); + if ( ! $available[$plugname] ) { + watchdog('openlayers_ui', 'Style plugin !name unknown', + array('!name' => $plugname), WATCHDOG_ERROR); + return $form; + } + + $plugin = $available[$plugname]; + $plugin_class = ctools_plugin_get_class($plugin, 'style_plugin'); + if (empty($plugin_class)) { + watchdog('openlayers_ui', 'Style plugin !name does not have a class?!', + array('!name' => $plugname), WATCHDOG_ERROR); + return $form; + } + + // Create object and ask it for options + $style_plugin = new $plugin_class; + + $form = array ( + '#type' => 'fieldset', + '#tree' => TRUE, + //'#title' => $plugin['title'], + '#description' => $plugin['description'], + 'conf' => $style_plugin->options_form($defaults) + ); + + return $form; +} + +function openlayers_ui_style_plugin_ahah($propname) { + + $posted = $_POST["data"][$propname]; + $plugname = $posted['uses_plugin']; + + // Get cached form + $form_state = array('storage' => NULL, 'submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form_cached = form_get_cache($form_build_id, $form_state); + + // Get style_data from cached form, and defaults + $style_data = $form_cached['style_data']['#value']; + $defaults = $style_data['defaults']; + + + + if ( ! $plugname ) { + + // Find default value + $properties = $style_data['definitions']; + $prop = $properties[$propname]; + $def_value = $prop['default']; + if ( isset($defaults[$propname]) && ! is_array($defaults[$propname]) ) { + $def_value = $defaults[$propname]; + } + + $form = array( + '#type' => !isset($prop['options']) ? 'textfield' : 'select', + '#default_value' => $def_value + ); + + // Add options if needed + if (isset($prop['options']) && is_array($prop['options'])) { + $form['value']['#options'] = $prop['options']; + } + + $form['#parents'] = array('data', $propname, 'value'); + + } else { + + // Find default value + $defs = array(); + if ( isset($defaults[$propname]) && is_array($defaults[$propname]) + && $defaults[$propname]['plugin'] == $plugname ) { + $defs = $defaults[$propname]['conf']; + } + +/* + ob_start(); + echo "
";
+  var_dump($defs);
+  $output = ob_get_clean();
+  drupal_json(array('status' => TRUE, 'data' => $output));
+  return;
+*/
+
+    $form = openlayers_ui_get_style_plugin_form($plugname, $defs);
+    $form['#parents'] = array('data', $propname, 'plugin');
+
+  }
+
+
+  $form_state = array(); // TODO: what to do with this ?
+  $plugform_built = form_builder('style_plugin_form', $form, $form_state);
+
+  $output = drupal_render($plugform_built);
+  
+/*
+  ob_start();
+  echo "
";
+  var_dump($plugform_built);
+  $output = ob_get_clean();
+  drupal_json(array('status' => TRUE, 'data' => $output));
+  return;
+*/
+
+  // Final rendering callback.
+  drupal_json(array('status' => TRUE, 'data' => $output));
+
+}
diff --git modules/openlayers_ui/openlayers_ui.module modules/openlayers_ui/openlayers_ui.module
index fe77d5d..f3c3f20 100644
--- modules/openlayers_ui/openlayers_ui.module
+++ modules/openlayers_ui/openlayers_ui.module
@@ -324,6 +324,14 @@ function openlayers_ui_menu() {
     'file' => 'includes/openlayers_ui.presets.inc',
     'type' => MENU_CALLBACK,
   );
+  $items['openlayers/ahah/style_plugin/%'] = array(
+    'title' => 'OpenLayers Style Plugin AHAH',
+    'page callback' => 'openlayers_ui_style_plugin_ahah',
+    'page arguments' => array(3), // 3rd url item (the '%') is first arg
+    'access callback' => TRUE,
+    'file' => 'includes/openlayers_ui.styles.inc',
+    'type' => MENU_CALLBACK, 
+  );
 
   return $items;
 }
diff --git openlayers.module openlayers.module
index c730349..cc39779 100644
--- openlayers.module
+++ openlayers.module
@@ -447,6 +447,21 @@ function openlayers_behaviors($reset = FALSE) {
 }
 
 /**
+ * Get all style plugins.
+ *
+ * @ingroup openlayers_api
+ *
+ * @param $reset
+ *   Boolean whether to reset cache or not.
+ * @return
+ *   Array of style handler info.
+ */
+function openlayers_style_plugins($reset = FALSE) {
+  ctools_include('plugins');
+  return ctools_get_plugins('openlayers', 'style_plugin');
+}
+
+/**
  * Get all openlayers styles.
  *
  * @ingroup openlayers_api
@@ -904,6 +919,88 @@ class openlayers_layer_type {
 }
 
 /**
+ * Base class for style plugins
+ *
+ * We define base classes in the core module.
+ * All other parent classes can be autoloaded through ctools.
+ */
+class openlayers_style_plugin {
+
+  /**
+   * Return true if this plugin can handle
+   * the given property
+   */
+  function can_handle_property($propname) {
+    return array_key_exists($propname, $this->get_context_properties());
+  }
+
+  /**
+   * Get an array of style property callbacks
+   *
+   * @return
+   *   Array of  => 
+   */
+  function get_context_properties() {
+    return array();
+  }
+
+  /**
+   * Initial default options.
+   *
+   * @return
+   *   Array of default options.
+   */
+  function options_init() {
+    return array();
+  }
+
+  /**
+   * Options form.
+   *
+   * @param $defaults
+   *   Array of default values for the form.
+   * @return
+   *   Array of Drupal form elements.
+   */
+  function options_form($defaults = array()) {
+    return array();
+  }
+
+  /**
+   * Render the style.
+   */
+  function render() {
+    // Render style.
+  }
+}
+
+/**
+ * Implementation of hook_ctools_plugin_directory
+ */
+function openlayers_ctools_plugin_directory($module, $plugin) {
+  // The format of plugin includes should be the
+  // following:
+  //   modulename_plugin_name.inc
+  //
+  // For example:
+  //  openlayers_style_plugin_name.inc
+  
+  // If this module needed to supply style plugins.
+  /*
+  if ($module == 'openlayers' && $plugin == 'style_plugin') {
+    return 'plugins/style_plugin';
+  }
+  */
+  
+  // This should change to the following when converted:
+  /*
+  if ($module == 'openlayers') {
+    return 'plugins/' . $plugin;
+  }
+  */
+}
+
+/**
  * Implementation of hook_ctools_plugin
  */
 function openlayers_ctools_plugin_behaviors() {
diff --git tests/openlayers_test.module tests/openlayers_test.module
index 9e0d826..67362cf 100644
--- tests/openlayers_test.module
+++ tests/openlayers_test.module
@@ -30,6 +30,16 @@ function openlayers_test_menu() {
 }
 
 /**
+ * Implementation of hook_ctools_plugin_directory
+ */
+function openlayers_test_ctools_plugin_directory($module, $plugin) {
+  if ($module == 'openlayers' && $plugin == 'style_plugin') {
+    return 'plugins/style_plugin';
+  }
+}
+  
+
+/**
  * Implementation of hook_ctools_plugin_api().
  */
 function openlayers_test_ctools_plugin_api($module, $api) {
@@ -38,6 +48,9 @@ function openlayers_test_ctools_plugin_api($module, $api) {
     switch ($api) {
       case 'openlayers_presets':
         return array('version' => 1);
+        
+      case 'openlayers_styles':
+        return array('version' => 1);
 
     }
   }
@@ -455,4 +468,4 @@ function openlayers_test_openlayers_map_alter(&$map) {
     drupal_set_message(t('OpenLayers map alter hook fired.'));
     $performed = TRUE;
   }
-}
\ No newline at end of file
+}
diff --git tests/plugins/style_plugin/openlayers_test_rnd_factor.inc tests/plugins/style_plugin/openlayers_test_rnd_factor.inc
new file mode 100644
index 0000000..9112f72
--- /dev/null
+++ tests/plugins/style_plugin/openlayers_test_rnd_factor.inc
@@ -0,0 +1,61 @@
+ t('TEST: random factor'),
+  'description' => t('Example style plugin for context styling. '
+    . 'Provides a random 0..1 factors.'),
+  'style_plugin' => array(
+    'class' => 'openlayers_test_rnd_factor',
+    'parent' => 'openlayers_style_plugin',
+  ),
+);
+
+/**
+ * Style Plugin for testing purposes.
+ */
+class openlayers_test_rnd_factor extends 
+  openlayers_style_plugin {
+  /**
+   * Provide initial values for options.
+   */
+  function options_init() {
+    return array(
+    );
+  }
+  
+  /**
+   * Options form.
+   */
+  function options_form($defaults = array()) {
+    $form = array();
+    return $form;
+  }
+
+  /**
+   * Get an array of style property callbacks
+   */
+  function get_context_properties() {
+    return array(
+      'fillOpacity' => 'getFactor',
+      'strokeOpacity' => 'getFactor',
+      'graphicOpacity' => 'getFactor',
+    );
+  }
+
+
+  /**
+   * Render function
+   */
+  function render() {
+    // Add JS
+    drupal_add_js(drupal_get_path('module', 'openlayers_test') .
+      '/plugins/style_plugin/openlayers_test_rnd_factor.js');
+  }
+}
diff --git tests/plugins/style_plugin/openlayers_test_rnd_factor.js tests/plugins/style_plugin/openlayers_test_rnd_factor.js
new file mode 100644
index 0000000..823de59
--- /dev/null
+++ tests/plugins/style_plugin/openlayers_test_rnd_factor.js
@@ -0,0 +1,26 @@
+// $Id$
+
+/**
+ * @file
+ * File to hold custom context styling
+ */
+
+/**
+ * Style plugin context class
+ */
+Drupal.openlayers.style_plugin.openlayers_test_rnd_factor = function (params) {
+  this.params = params;
+};
+
+/**
+ * Style plugin context class methods
+ */
+Drupal.openlayers.style_plugin.openlayers_test_rnd_factor.prototype = {
+
+  // Fill opacity context.  Sets random fill opacity.
+  'getFactor' : function(feature) {
+    // Random factor
+    return Math.random();
+  }
+
+};
diff --git tests/plugins/style_plugin/openlayers_test_rnd_int.inc tests/plugins/style_plugin/openlayers_test_rnd_int.inc
new file mode 100644
index 0000000..c72fd90
--- /dev/null
+++ tests/plugins/style_plugin/openlayers_test_rnd_int.inc
@@ -0,0 +1,88 @@
+ t('TEST: random integer'),
+  'description' => t('Example style plugin for context styling. '
+    . 'Provides random integers.'),
+  'style_plugin' => array(
+    'class' => 'openlayers_test_rnd_int',
+    'parent' => 'openlayers_style_plugin',
+  ),
+);
+
+/**
+ * Style Plugin for testing purposes.
+ */
+class openlayers_test_rnd_int extends 
+  openlayers_style_plugin {
+  /**
+   * Provide initial values for options.
+   */
+  function options_init() {
+    return array(
+      'low' => 1,
+      'high' => 10,
+    );
+  }
+  
+  /**
+   * Options form.
+   */
+  function options_form($defaults = array()) {
+    $form = array();
+    
+    // Allow use to pick the highest and lowest for random
+    // point radius
+    $form['low'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Lowest value'),
+      '#description' => t('Lowest value for the random integer.'),
+      '#default_value' => isset($defaults['low']) ?
+        $defaults['low'] : 2,
+    );
+    $form['high'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Highest value'),
+      '#description' => t('Highest value for the random integer.'),
+      '#default_value' => isset($defaults['high']) ?
+        $defaults['high'] : 10,
+    );
+    
+    return $form;
+  }
+
+  /**
+   * Get an array of style property callbacks
+   */
+  function get_context_properties() {
+    return array(
+      'pointRadius' => 'getInt',
+      'strokeWidth' => 'getInt',
+      'graphicWidth' => 'getInt',
+      'graphicHeight' => 'getInt',
+      'graphicXOffset' => 'getInt',
+      'graphicYOffset' => 'getInt',
+      'rotation' => 'getInt',
+      'labelXOffset' => 'getInt',
+      'labelYOffset' => 'getInt',
+      'fontSize' => 'getInt',
+    );
+  }
+
+
+  /**
+   * Render function
+   */
+  function render() {
+    // Add JS
+    drupal_add_js(drupal_get_path('module', 'openlayers_test') .
+      '/plugins/style_plugin/openlayers_test_rnd_int.js');
+  }
+}
diff --git tests/plugins/style_plugin/openlayers_test_rnd_int.js tests/plugins/style_plugin/openlayers_test_rnd_int.js
new file mode 100644
index 0000000..b575d70
--- /dev/null
+++ tests/plugins/style_plugin/openlayers_test_rnd_int.js
@@ -0,0 +1,37 @@
+// $Id$
+
+/**
+ * @file
+ * File to hold custom context styling
+ */
+
+/**
+ * Style plugin context class
+ */
+Drupal.openlayers.style_plugin.openlayers_test_rnd_int = function (params) {
+  this.params = params;
+  this.params.high = parseInt(this.params.high);
+  this.params.low = parseInt(this.params.low);
+};
+
+/**
+ * Style plugin context class methods
+ */
+Drupal.openlayers.style_plugin.openlayers_test_rnd_int.prototype = {
+
+  // Private methods (not copied to final style context object)
+  'prv' : {
+    'random' : function(low, high) {
+      return Math.floor(Math.random() * (high-low+1)) + low;
+    }
+  },
+  
+  // Point radius context.  Given paramters, gets a random
+  // pointRadius.
+  'getInt' :  function(feature) {
+    var high = this.params.high;
+    var low = this.params.low;
+    var ret = this.prv.random(low, high);
+    return ret;
+  }
+};