chukwa-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From asrab...@apache.org
Subject svn commit: r793443 [1/2] - in /hadoop/chukwa/trunk: ./ lib/ src/java/org/apache/hadoop/chukwa/analysis/ src/java/org/apache/hadoop/chukwa/analysis/salsa/ src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/ src/java/org/apache/hadoop/chukwa...
Date Mon, 13 Jul 2009 02:31:17 GMT
Author: asrabkin
Date: Mon Jul 13 02:31:17 2009
New Revision: 793443

URL: http://svn.apache.org/viewvc?rev=793443&view=rev
Log:
CHUKWA-342. Static swimlanes visualization widget. Contributed by Jiaqi Tan

Added:
    hadoop/chukwa/trunk/lib/prefuse.jar   (with props)
    hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/
    hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/
    hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/
    hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/Swimlanes.java
    hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/ImageSlicer.java
    hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/OfflineTimeHandler.java
    hadoop/chukwa/trunk/src/web/hicc/descriptors/swimlanes-static.descriptor
    hadoop/chukwa/trunk/src/web/hicc/js/behaviour.js
    hadoop/chukwa/trunk/src/web/hicc/js/gsv.js
    hadoop/chukwa/trunk/src/web/hicc/jsp/image-viewer.jsp
    hadoop/chukwa/trunk/src/web/hicc/jsp/swimlanes-static.jsp
Modified:
    hadoop/chukwa/trunk/CHANGES.txt
    hadoop/chukwa/trunk/build.xml
    hadoop/chukwa/trunk/src/web/hicc/WEB-INF/jetty.xml
    hadoop/chukwa/trunk/src/web/hicc/jsp/permlink.jsp
    hadoop/chukwa/trunk/src/web/hicc/jsp/session.jsp

Modified: hadoop/chukwa/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/CHANGES.txt?rev=793443&r1=793442&r2=793443&view=diff
==============================================================================
--- hadoop/chukwa/trunk/CHANGES.txt (original)
+++ hadoop/chukwa/trunk/CHANGES.txt Mon Jul 13 02:31:17 2009
@@ -4,6 +4,8 @@
 
   NEW FEATURES
 
+    CHUKWA-342. Statics swimlanes visualization widget.   (Jiaqi Tan via asrabkin)
+
     CHUKWA-299. Added HDFS Spatial heatmaps visualization. (Jiaqi Tan via Eric Yang)
 
     CHUKWA-185. Ability to tail a whole directory. (asrabkin)
@@ -216,12 +218,6 @@
                 * Daily will process a daily compaction only when all hourly would have been done.
                 * Demux is now able to send NSCA commands to Nagios. (Jerome Boulon via Eric Yang)
 
-    CHUKWA-26.  * DemuxManager, ArchiveManager and PostProcessorManager are now a single daemon process each.
-                * Each one working independently from others, as soon as something is available.
-                * Start-data-processor is now using those new daemons instead of pocessSink.sh
-                * Daily will process a daily compaction only when all hourly would have been done.
-                * Demux is now able to send NSCA commands to Nagios. (Jerome Boulon via Eric Yang)
-
     CHUKWA-81.  Fix file descriptor leak.  (asrabkin)
 
     CHUKWA-29.  Added TaskTracker and DataNode client trace log parser and database loader.  (Chris Douglas via Eric Yang)

Modified: hadoop/chukwa/trunk/build.xml
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/build.xml?rev=793443&r1=793442&r2=793443&view=diff
==============================================================================
--- hadoop/chukwa/trunk/build.xml (original)
+++ hadoop/chukwa/trunk/build.xml Mon Jul 13 02:31:17 2009
@@ -1122,7 +1122,8 @@
 		<mkdir dir="${build.dir}/${final.name}/var/run" />
 		<mkdir dir="${build.dir}/${final.name}/logs" />
 		<mkdir dir="${build.dir}/${final.name}/webapps" />
-
+		<mkdir dir="${build.dir}/${final.name}/webapps/sandbox" />
+		
 		<copy todir="${build.dir}/${final.name}" includeEmptyDirs="false">
 			<fileset dir="${build.dir}">
 				<include name="*.jar" />

Added: hadoop/chukwa/trunk/lib/prefuse.jar
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/lib/prefuse.jar?rev=793443&view=auto
==============================================================================
Binary file - no diff available.

Propchange: hadoop/chukwa/trunk/lib/prefuse.jar
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/Swimlanes.java
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/Swimlanes.java?rev=793443&view=auto
==============================================================================
--- hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/Swimlanes.java (added)
+++ hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/analysis/salsa/visualization/Swimlanes.java Mon Jul 13 02:31:17 2009
@@ -0,0 +1,923 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ 
+package org.apache.hadoop.chukwa.analysis.salsa.visualization;
+
+import prefuse.data.io.sql.*;
+import prefuse.data.Table;
+import prefuse.data.expression.parser.*;
+import prefuse.data.expression.*;
+import prefuse.data.column.*;
+import prefuse.data.query.*;
+import prefuse.data.*;
+import prefuse.action.*;
+import prefuse.action.layout.*;
+import prefuse.action.assignment.*;
+import prefuse.visual.expression.*;
+import prefuse.visual.*;
+import prefuse.render.*;
+import prefuse.util.collections.*;
+import prefuse.util.*;
+import prefuse.*;
+
+import org.apache.hadoop.chukwa.hicc.OfflineTimeHandler;
+import org.apache.hadoop.chukwa.hicc.TimeHandler;
+import org.apache.hadoop.chukwa.util.DatabaseWriter;
+import org.apache.hadoop.chukwa.database.Macro;
+import org.apache.hadoop.chukwa.util.XssFilter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import javax.servlet.http.*;
+import javax.swing.BorderFactory;
+
+import java.sql.*;
+import java.util.*;
+import java.text.NumberFormat;
+import java.text.DateFormat;
+
+import java.awt.Font;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * Static image generation for Swimlanes visualization for scalable 
+ * rendering on front-end client (web-browser)
+ * Handles database data retrieval, transforming data to form for 
+ * visualization elements, and initializing and calling visualization
+ * elements
+ */
+public class Swimlanes {
+
+  private static Log log = LogFactory.getLog(Swimlanes.class);
+
+  int SIZE_X=1600, SIZE_Y=1600;
+  final int [] BORDER = {50,50,50,50};
+  final int LEGEND_X_OFFSET = 50;
+  final int LEGEND_Y_OFFSET = 25;
+  final int LEGEND_TEXT_OFFSET = 20;
+  final int LEGEND_FONT_SIZE = 18;
+  final int AXIS_NAME_FONT_SIZE = 24;
+
+  protected boolean offline_use = true;
+  protected HttpServletRequest request;
+  
+  protected String abc;
+
+  /**
+   * Modifier for generic Swimlanes plots to plot shuffle, sort, and reducer
+   * states of same reduce on same line 
+   */
+  protected class MapReduceSwimlanes {
+    protected Table plot_tab;
+    protected HashMap<String, ArrayList<Tuple> > reducepart_hash;
+    protected boolean collate_reduces = false;
+    
+    public MapReduceSwimlanes() {
+      this.plot_tab = new Table();
+      this.plot_tab.addColumn("ycoord",float.class);
+      this.plot_tab.addColumn("state_name",String.class);
+      this.plot_tab.addColumn("hostname",String.class);
+      this.plot_tab.addColumn("friendly_id",String.class);
+      this.plot_tab.addColumn(START_FIELD_NAME,double.class);
+      this.plot_tab.addColumn(END_FIELD_NAME,double.class);
+      this.plot_tab.addColumn(PolygonRenderer.POLYGON,float[].class);
+      this.reducepart_hash = new HashMap<String, ArrayList<Tuple> >();
+    }
+    
+    public void populateTable_OneLinePerState(Table orig_tab) {
+      IntIterator rownumiter;
+      int newrownum, origrownum;
+      rownumiter = orig_tab.rows(); // iterate over everything
+      while (rownumiter.hasNext()) {
+        origrownum = ((Integer)rownumiter.next()).intValue();
+        newrownum = this.plot_tab.addRow();
+        this.plot_tab.set(newrownum, "state_name", orig_tab.getString(origrownum, "state_name"));
+        this.plot_tab.set(newrownum, "ycoord", orig_tab.getInt(origrownum, "seqno"));
+        this.plot_tab.set(newrownum,"hostname",orig_tab.getString(origrownum,"hostname"));
+        this.plot_tab.set(newrownum,"friendly_id",orig_tab.getString(origrownum,"friendly_id"));
+        this.plot_tab.set(newrownum,START_FIELD_NAME, orig_tab.getDouble(origrownum,START_FIELD_NAME));
+        this.plot_tab.set(newrownum,END_FIELD_NAME, orig_tab.getDouble(origrownum,END_FIELD_NAME));
+      }      
+    }
+    
+    public void populateTable_CollateReduces(Table orig_tab) {
+      IntIterator rownumiter;
+      int newrownum, origrownum;
+      
+      this.collate_reduces = true;
+      
+      // add maps normally
+      rownumiter = orig_tab.rows(
+        (Predicate) ExpressionParser.parse("[state_name] == 'map' " + 
+          "OR [state_name] == 'shuffle_local' " + 
+          "OR [state_name] == 'shuffle_remote'")
+      );
+      
+      while (rownumiter.hasNext()) {
+        origrownum = ((Integer)rownumiter.next()).intValue();
+        newrownum = this.plot_tab.addRow();
+        this.plot_tab.set(newrownum, "state_name", orig_tab.getString(origrownum, "state_name"));
+        this.plot_tab.set(newrownum, "ycoord", orig_tab.getInt(origrownum, "seqno"));
+        this.plot_tab.set(newrownum,"hostname",orig_tab.getString(origrownum,"hostname"));
+        this.plot_tab.set(newrownum,"friendly_id",orig_tab.getString(origrownum,"friendly_id"));
+        this.plot_tab.set(newrownum,START_FIELD_NAME, orig_tab.getDouble(origrownum,START_FIELD_NAME));
+        this.plot_tab.set(newrownum,END_FIELD_NAME, orig_tab.getDouble(origrownum,END_FIELD_NAME));
+      }
+
+      // special breakdown for reduces
+      IntIterator rownumiter3 = orig_tab.rows(
+        (Predicate) ExpressionParser.parse("[state_name] == 'reduce_reducer' " +
+          "OR [state_name] == 'reduce_shufflewait' " + 
+          "OR [state_name] == 'reduce_sort' " + 
+          "OR [state_name] == 'reduce'")
+      );
+      
+      ArrayList<Tuple> tuple_array;
+      while (rownumiter3.hasNext()) {
+        origrownum = ((Integer)rownumiter3.next()).intValue();
+        if (orig_tab.getString(origrownum,"state_name").equals("reduce")) {
+          continue; // do NOT add reduces
+        }
+        String curr_reduce = orig_tab.getString(origrownum, "friendly_id");
+        newrownum = this.plot_tab.addRow();
+        
+        this.plot_tab.set(newrownum, "state_name", orig_tab.getString(origrownum, "state_name"));
+        this.plot_tab.set(newrownum, "ycoord", orig_tab.getInt(origrownum, "seqno"));
+        this.plot_tab.set(newrownum,"hostname",orig_tab.getString(origrownum,"hostname"));
+        this.plot_tab.set(newrownum,"friendly_id",orig_tab.getString(origrownum,"friendly_id"));        
+        this.plot_tab.set(newrownum,START_FIELD_NAME, orig_tab.getDouble(origrownum,START_FIELD_NAME));
+        this.plot_tab.set(newrownum,END_FIELD_NAME, orig_tab.getDouble(origrownum,END_FIELD_NAME));
+        
+        tuple_array = this.reducepart_hash.get(curr_reduce);
+        if (tuple_array == null) {
+          tuple_array = new ArrayList<Tuple>();
+          tuple_array.add(this.plot_tab.getTuple(newrownum));
+          this.reducepart_hash.put(curr_reduce, tuple_array);
+        } else {
+          tuple_array.add(this.plot_tab.getTuple(newrownum));
+        }
+      }  
+    }
+    
+    public void populateTable_MapsReducesOnly(Table orig_tab) {
+      IntIterator rownumiter;
+      int newrownum, origrownum;
+      rownumiter = orig_tab.rows(
+        (Predicate) ExpressionParser.parse("[state_name] == 'map' OR [state_name] == 'reduce'")
+      );
+      while (rownumiter.hasNext()) {
+        origrownum = ((Integer)rownumiter.next()).intValue();
+        newrownum = this.plot_tab.addRow();
+        this.plot_tab.set(newrownum, "state_name", orig_tab.getString(origrownum, "state_name"));
+        this.plot_tab.set(newrownum, "ycoord", orig_tab.getInt(origrownum, "seqno"));
+        this.plot_tab.set(newrownum,"hostname",orig_tab.getString(origrownum,"hostname"));
+        this.plot_tab.set(newrownum,"friendly_id",orig_tab.getString(origrownum,"friendly_id"));        
+        this.plot_tab.set(newrownum,START_FIELD_NAME, orig_tab.getDouble(origrownum,START_FIELD_NAME));
+        this.plot_tab.set(newrownum,START_FIELD_NAME, orig_tab.getDouble(origrownum,END_FIELD_NAME));
+      }
+    }
+    
+    /**
+     * Reassigns Y coord values to group by state
+     */
+    public void groupByState() {
+      int counter, rownum;
+      int rowcount = this.plot_tab.getRowCount();
+      HashSet<String> states = new HashSet<String>();
+      String curr_state = null;
+      Iterator<String> state_iter;
+      IntIterator rownumiter;
+      
+      for (int i = 0; i < rowcount; i++) {
+        states.add(this.plot_tab.getString(i,"state_name"));
+      }
+     
+      state_iter = states.iterator();
+      counter = 1;
+      while (state_iter.hasNext()) {
+        curr_state = state_iter.next();
+        
+        if (this.collate_reduces) {
+          if (curr_state.equals("reduce_reducer") || curr_state.equals("reduce_sort")) {
+            continue;
+          }
+        }
+        rownumiter = this.plot_tab.rows(
+          (Predicate) ExpressionParser.parse("[state_name] == '"+curr_state+"'")
+        );
+        if (this.collate_reduces && curr_state.equals("reduce_shufflewait")) {
+          while (rownumiter.hasNext()) {
+            rownum = ((Integer)rownumiter.next()).intValue();
+            this.plot_tab.setFloat(rownum,"ycoord",(float)counter);
+            
+            ArrayList<Tuple> alt = this.reducepart_hash.get(this.plot_tab.getString(rownum,"friendly_id"));
+            Object [] tarr = alt.toArray();
+            for (int i = 0; i < tarr.length; i++) ((Tuple)tarr[i]).setFloat("ycoord",(float)counter);
+            counter++;            
+          }
+        } else {
+          while (rownumiter.hasNext()) {
+            rownum = ((Integer)rownumiter.next()).intValue();
+            this.plot_tab.setFloat(rownum,"ycoord",(float)counter);
+            counter++;
+          }          
+        }
+      }
+    }
+    
+    public void groupByStartTime() {
+      int counter, rownum;
+      int rowcount = this.plot_tab.getRowCount();
+      HashSet<String> states = new HashSet<String>();
+      String curr_state = null;
+      Iterator<String> state_iter;
+      IntIterator rownumiter;
+     
+      rownumiter = this.plot_tab.rowsSortedBy(START_FIELD_NAME, true);
+     
+      counter = 1;
+      while (rownumiter.hasNext()) {
+        rownum = ((Integer)rownumiter.next()).intValue();
+        curr_state = this.plot_tab.getString(rownum, "state_name");        
+
+        if (this.collate_reduces && curr_state.equals("reduce_shufflewait")) {
+          this.plot_tab.setFloat(rownum,"ycoord",(float)counter);
+          ArrayList<Tuple> alt = this.reducepart_hash.get(this.plot_tab.getString(rownum,"friendly_id"));
+          Object [] tarr = alt.toArray();
+          for (int i = 0; i < tarr.length; i++) ((Tuple)tarr[i]).setFloat("ycoord",(float)counter);
+          counter++;   
+        } else if (!curr_state.equals("reduce_sort") && !curr_state.equals("reduce_reducer")) {
+          this.plot_tab.setFloat(rownum,"ycoord",(float)counter);          
+          counter++;
+        }
+      }
+    }
+    
+    public void groupByEndTime() {
+      int counter, rownum;
+      int rowcount = this.plot_tab.getRowCount();
+      HashSet<String> states = new HashSet<String>();
+      String curr_state = null;
+      Iterator<String> state_iter;
+      IntIterator rownumiter;
+     
+      rownumiter = this.plot_tab.rowsSortedBy(END_FIELD_NAME, true);
+      counter = 1;
+      while (rownumiter.hasNext()) {
+        rownum = ((Integer)rownumiter.next()).intValue();
+        curr_state = this.plot_tab.getString(rownum, "state_name");        
+
+        if (this.collate_reduces && curr_state.equals("reduce_reducer")) {
+          this.plot_tab.setFloat(rownum,"ycoord",(float)counter);
+          ArrayList<Tuple> alt = this.reducepart_hash.get(this.plot_tab.getString(rownum,"friendly_id"));
+          Object [] tarr = alt.toArray();
+          for (int i = 0; i < tarr.length; i++) ((Tuple)tarr[i]).setFloat("ycoord",(float)counter);
+          counter++;
+        } else if (!curr_state.equals("reduce_sort") && !curr_state.equals("reduce_shufflewait")) {
+          this.plot_tab.setFloat(rownum,"ycoord",(float)counter);          
+          counter++;
+        }
+      }
+    }    
+    
+    public VisualTable addToVisualization(Visualization viz, String groupname) {
+      return viz.addTable(groupname, this.plot_tab);
+    }
+  }
+
+  /**
+   * Provide constant mapping between state names and colours
+   * so that even if particular states are missing, the colours are fixed
+   * for each state
+   */
+  public static class SwimlanesStatePalette {
+    protected final String [] states = {"map","reduce","reduce_shufflewait","reduce_sort","reduce_reducer","shuffle"};
+    HashMap<String,Integer> colourmap; 
+    protected int [] palette;
+    public SwimlanesStatePalette() {
+      palette = ColorLib.getCategoryPalette(states.length);
+      colourmap = new HashMap<String,Integer>();
+      for (int i = 0; i < states.length; i++) {
+        colourmap.put(states[i], new Integer(palette[i]));
+      }
+    }
+    public int getColour(String state_name) {
+      Integer val = colourmap.get(state_name);
+      if (val == null) {
+        return ColorLib.color(java.awt.Color.BLACK);
+      } else {
+        return val.intValue();
+      }
+    }
+    public int getNumStates() {
+      return this.states.length;
+    }
+    public String [] getStates() {
+      return this.states;
+    }
+  }
+
+  /**
+   * Provides convenient rescaling of raw values to be plotted to
+   * actual pixels for plotting on image
+   */
+  public static class CoordScaler {
+    double x_pixel_size, y_pixel_size;
+    double x_max_value, y_max_value, x_min_value, y_min_value;
+    double x_start, y_start;
+    
+    public CoordScaler() {
+      this.x_pixel_size = 0.0;
+      this.y_pixel_size = 0.0;
+      this.x_max_value = 1.0;
+      this.y_max_value = 1.0;
+      this.x_min_value = 0.0;
+      this.y_min_value = 0.0;
+      this.x_start = 0.0;
+      this.y_start = 0.0;
+    }
+    public void set_pixel_start(double x, double y) {
+      this.x_start = x;
+      this.y_start = y;
+    }
+    public void set_pixel_size(double x, double y) {
+      this.x_pixel_size = x;
+      this.y_pixel_size = y;
+    }
+    public void set_value_ranges(double x_min, double y_min, double x_max, double y_max) {
+      this.x_max_value = x_max;
+      this.y_max_value = y_max;
+      this.x_min_value = x_min;
+      this.y_min_value = y_min;
+    }
+    public double get_x_coord(double x_value) {
+      return x_start+(((x_value - x_min_value) / (x_max_value-x_min_value)) * x_pixel_size);
+    }
+    public double get_y_coord(double y_value) {
+      // this does "inverting" to shift the (0,0) point from top-right to bottom-right
+      return y_start+(y_pixel_size - ((((y_value - y_min_value) / (y_max_value-y_min_value)) * y_pixel_size)));
+    }
+  }
+
+  /**
+   * Prefuse action for plotting a line for each state
+   */
+  public static class SwimlanesStateAction extends GroupAction {
+    
+    protected CoordScaler cs;
+    
+    public SwimlanesStateAction() {
+      super();
+    }
+    
+    public SwimlanesStateAction(String group, CoordScaler cs) {
+      super(group);
+      this.cs = cs;
+    }
+    
+    public void run (double frac) {
+      VisualItem item = null;
+      SwimlanesStatePalette pal = new SwimlanesStatePalette();
+      
+      Iterator curr_group_items = this.m_vis.items(this.m_group);
+      
+      int i = 0;
+      
+      while (curr_group_items.hasNext()) {
+        item = (VisualItem) curr_group_items.next();
+        
+        double start_time = item.getDouble(START_FIELD_NAME);
+        double finish_time = item.getDouble(END_FIELD_NAME);        
+        item.setShape(Constants.POLY_TYPE_LINE);
+        item.setX(0.0);
+        item.setY(0.0);        
+        
+        float [] coords = new float[4];
+        coords[0] = (float) cs.get_x_coord(start_time);
+        coords[1] = (float) cs.get_y_coord((double)item.getInt("ycoord"));
+        coords[2] = (float) cs.get_x_coord(finish_time);
+        coords[3] = (float) cs.get_y_coord((double)item.getInt("ycoord"));
+
+        item.set(VisualItem.POLYGON,coords);
+        item.setStrokeColor(pal.getColour(item.getString("state_name")));
+        i++;
+      }
+    }    
+  } // SwimlanesStateAction
+
+  // keys that need to be filled:
+  // period (last1/2/3/6/12/24hr,last7d,last30d), time_type (range/last), start, end
+  protected HashMap<String, String> param_map;
+  
+  protected String cluster;
+  protected String timezone;
+  protected String shuffle_option;
+  protected final String table = new String("mapreduce_fsm");
+  protected boolean plot_legend = true;
+  protected String jobname = null;
+  
+  protected Display dis;
+  protected Visualization viz;
+  
+  protected Rectangle2D dataBound = new Rectangle2D.Double();
+  protected Rectangle2D xlabBound = new Rectangle2D.Double();
+  protected Rectangle2D ylabBound = new Rectangle2D.Double();
+  protected Rectangle2D labelBottomBound = new Rectangle2D.Double();
+  
+  static final String START_FIELD_NAME = "start_time_num";
+  static final String END_FIELD_NAME = "finish_time_num";
+  
+  /* Different group names allow control of what Renderers to use */
+  final String maingroup = "Job";
+  final String othergroup = "Misc";
+  final String labelgroup = "Label";
+  final String legendgroup = "Legend";
+  final String legendshapegroup = "LegendShape";
+  
+  public Swimlanes() {
+    this.cluster = new String("");
+    this.timezone = new String("");
+    this.shuffle_option = new String("");
+    param_map = new HashMap<String, String>();
+  }
+  
+  /**
+   * @brief Constructor for Swimlanes visualization object
+   * @param timezone Timezone string from environment
+   * @param cluster Cluster name from environment
+   * @param event_type Whether to display shuffles or not
+   * @param valmap HashMap of key/value pairs simulating parameters from a HttpRequest
+   */
+  public Swimlanes
+    (String timezone, String cluster, String event_type, 
+     HashMap<String, String> valmap) 
+  {
+    this.cluster = new String(cluster);
+    if (timezone != null) {
+      this.timezone = new String(timezone);
+    } else {
+      this.timezone = null;
+    }
+    this.shuffle_option = new String(event_type);
+    
+    /* This should "simulate" an HttpServletRequest
+     * Need to have "start" and "end" in seconds since Epoch
+     */
+    this.param_map = valmap;
+  }
+  
+  public Swimlanes
+    (String timezone, String cluster, String event_type, 
+     HashMap<String, String> valmap, int width, int height) 
+  {
+    this.cluster = new String(cluster);
+    if (timezone != null) {
+      this.timezone = new String(timezone);
+    } else {
+      this.timezone = null;
+    }
+    this.shuffle_option = new String(event_type);
+    
+    /* This should "simulate" an HttpServletRequest
+     * Need to have "start" and "end" in seconds since Epoch
+     */
+    this.param_map = valmap; 
+    
+    this.SIZE_X = width;
+    this.SIZE_Y = height;
+  }
+  
+  public Swimlanes
+    (String timezone, String cluster, String event_type, 
+     HashMap<String, String> valmap, int width, int height,
+     String legend_opt) 
+  {
+    this.cluster = new String(cluster);
+    if (timezone != null) {
+      this.timezone = new String(timezone);
+    } else {
+      this.timezone = null;
+    }
+    this.shuffle_option = new String(event_type);
+    
+    /* This should "simulate" an HttpServletRequest
+     * Need to have "start" and "end" in seconds since Epoch
+     */
+    this.param_map = valmap;
+    
+    this.SIZE_X = width;
+    this.SIZE_Y = height;
+    
+    if (legend_opt.equals("nolegend")) {
+      this.plot_legend = false;
+    }
+    
+  }
+  
+  public Swimlanes(HttpServletRequest request) {
+    XssFilter xf = new XssFilter(request);
+    this.offline_use = false;
+    this.request = request;
+    HttpSession session = request.getSession();
+    this.cluster = session.getAttribute("cluster").toString();
+      String evt_type = xf.getParameter("event_type");
+    if (evt_type != null) {
+      this.shuffle_option = new String(evt_type);
+    } else {
+      this.shuffle_option = new String("noshuffle");
+    }
+    this.timezone = session.getAttribute("time_zone").toString();
+  }
+  
+  /**
+   * Set job ID to filter results on
+   * Call before calling @see #run
+   */
+  public void setJobName(String s) {
+    this.jobname = new String(s);
+  }
+
+  /**
+   * Set dimensions of image to be generated
+   * Call before calling @see #run
+   */  
+  public void setDimensions(int width, int height) {
+    this.SIZE_X=width;
+    this.SIZE_Y=height;
+  }
+  
+  /**
+   * Specify whether to print legend of states
+   * Advisable to not print legend for excessively small images since
+   * legend has fixed point size
+   * Call before calling @see #run
+   */
+  public void setLegend(boolean legendopt) {
+    if (legendopt) {
+      this.plot_legend = true;
+    } else {
+      this.plot_legend = false;
+    }
+  }
+  
+  /**
+   * Generates image in specified format, and writes image as binary
+   * output to supplied output stream 
+   */
+  public boolean getImage(java.io.OutputStream output, String img_fmt, double scale) {
+    dis = new Display(this.viz);
+    dis.setSize(SIZE_X,SIZE_Y);
+    dis.setHighQuality(true);
+    dis.setFont(new Font(Font.SANS_SERIF,Font.PLAIN,24));
+    return dis.saveImage(output, img_fmt, scale);
+  } 
+  
+  /**
+   * Adds a column to given table by converting timestamp to long with
+   * seconds since epoch, and adding milliseconds from additional column
+   * in original table
+   *
+   * @param origTable Table to add to
+   * @param srcFieldName Name of column containing timestamp
+   * @param srcMillisecondFieldName Name of column containing millisecond value of time 
+   * @param dstFieldName Name of new column to add
+   * 
+   * @return Modified table with added column
+   */
+  protected Table addTimeCol 
+    (Table origTable, String srcFieldName, 
+     String srcMillisecondFieldName, String dstFieldName)
+  {
+    origTable.addColumn(dstFieldName, long.class);
+    
+    int total_rows = origTable.getRowCount();
+    for (int curr_row_num = 0; curr_row_num < total_rows; curr_row_num++) {
+      origTable.setLong(curr_row_num, dstFieldName, 
+        ((Timestamp)origTable.get(curr_row_num, srcFieldName)).getTime() + 
+        origTable.getLong(curr_row_num, srcMillisecondFieldName)
+      );
+    }
+    
+    return origTable;
+  }
+  
+  /**
+   * Adds a column with number of seconds of timestamp elapsed since lowest
+   * start time; allows times to be plotted as a delta of the start time
+   * 
+   * @param origTable Table to add column to
+   * @param srcFieldName Name of column containing timestamp
+   * @param srcMillisecondFieldName Name of column containing millisecond value of time 
+   * @param dstFieldName Name of new column to add
+   *   
+   * @return Modified table with added column
+   */
+  protected Table addTimeOffsetCol
+    (Table origTable, String srcFieldName,
+     String srcMillisecondFieldName, String dstFieldName,
+     long timeOffset) 
+  {
+    Table newtable = addTimeCol(origTable, srcFieldName, 
+      srcMillisecondFieldName, dstFieldName + "_fulltime");
+      
+    ColumnMetadata dstcol = newtable.getMetadata(dstFieldName + "_fulltime");
+    long mintime = newtable.getLong(dstcol.getMinimumRow(), dstFieldName + "_fulltime");
+    
+    if (timeOffset == 0) {
+      newtable.addColumn(dstFieldName, "ROUND((["+dstFieldName+"_fulltime] - " + mintime +"L) / 1000L)");
+    } else {
+      newtable.addColumn(dstFieldName, "ROUND((["+dstFieldName+"_fulltime] - " + timeOffset +"L) / 1000L)");      
+    }
+    
+    return newtable;
+  }
+  
+  protected void setupRenderer() {
+    this.viz.setRendererFactory(new RendererFactory(){
+      AbstractShapeRenderer sr = new ShapeRenderer();
+      ShapeRenderer sr_big = new ShapeRenderer(20);
+      Renderer arY = new AxisRenderer(Constants.LEFT, Constants.TOP);
+      Renderer arX = new AxisRenderer(Constants.CENTER, Constants.BOTTOM);
+      PolygonRenderer pr = new PolygonRenderer(Constants.POLY_TYPE_LINE);
+      LabelRenderer lr = new LabelRenderer("label");
+      LabelRenderer lr_legend = new LabelRenderer("label");
+      
+      public Renderer getRenderer(VisualItem item) {
+        lr.setHorizontalAlignment(Constants.CENTER);
+        lr.setVerticalAlignment(Constants.TOP);
+        lr_legend.setHorizontalAlignment(Constants.LEFT);
+        lr_legend.setVerticalAlignment(Constants.CENTER);
+        
+        if (item.isInGroup("ylab")) {
+          return arY;
+        } else if (item.isInGroup("xlab")) {
+          return arX;
+        } else if (item.isInGroup(maingroup)) {
+          return pr;
+        } else if (item.isInGroup(labelgroup)) {
+          return lr;
+        } else if (item.isInGroup(legendgroup)) {
+          return lr_legend;
+        } else if (item.isInGroup(legendshapegroup)) {
+          return sr_big;
+        } else {
+          return sr;
+        }
+      }
+    });
+  }
+  
+  // setup columns: add additional time fields
+  protected Table setupDataTable() {
+    Table res_tab = this.getData();    
+    if (res_tab == null) {
+        return res_tab;
+    }
+    
+    res_tab.addColumn("seqno","ROW()");
+    res_tab = addTimeOffsetCol(res_tab, "start_time", "start_time_millis", START_FIELD_NAME, 0);    
+    ColumnMetadata dstcol = res_tab.getMetadata(START_FIELD_NAME);
+    long mintime = ((Timestamp)res_tab.get(dstcol.getMinimumRow(), "start_time")).getTime();
+    res_tab = addTimeOffsetCol(res_tab, "finish_time", "finish_time_millis", END_FIELD_NAME, mintime);    
+    res_tab.addColumn(PolygonRenderer.POLYGON,float[].class);
+    
+    log.debug("After adding seqno: #cols: " + res_tab.getColumnCount() + "; #rows: " + res_tab.getRowCount());
+    
+    return res_tab;
+  }
+  
+  protected void addAxisNames() {
+    Table textlabels_table = new Table();
+    textlabels_table.addColumn("label",String.class);
+    textlabels_table.addColumn("type",String.class);
+    textlabels_table.addRow();
+    textlabels_table.setString(0,"label",new String("Time/s"));
+    textlabels_table.setString(0,"type",new String("xaxisname"));
+    
+    VisualTable textlabelsviz = this.viz.addTable(labelgroup, textlabels_table);
+    textlabelsviz.setX(0,SIZE_X/2);
+    textlabelsviz.setY(0,SIZE_Y - BORDER[2] + (BORDER[2]*0.1));
+    textlabelsviz.setTextColor(0,ColorLib.color(java.awt.Color.GRAY));
+    textlabelsviz.setFont(0,new Font(Font.SANS_SERIF,Font.PLAIN,AXIS_NAME_FONT_SIZE));
+  }
+  
+  protected void addLegend() {
+    SwimlanesStatePalette ssp = new SwimlanesStatePalette();
+    
+    Table shapes_table = new Table();
+    shapes_table.addColumn(VisualItem.X,float.class);
+    shapes_table.addColumn(VisualItem.Y,float.class);
+    
+    Table legend_labels_table = new Table();
+    Table legend_squares_table = new Table();
+    legend_labels_table.addColumn("label",String.class);
+    
+    // add labels
+    int num_states = ssp.getNumStates();
+    String [] state_names = ssp.getStates();
+    legend_labels_table.addRows(num_states);
+    shapes_table.addRows(num_states);
+    for (int i = 0; i < num_states; i++) {
+      legend_labels_table.setString(i,"label",state_names[i]);
+    }
+    
+    // add legend shapes, manipulate visualitems to set colours
+    VisualTable shapes_table_viz = viz.addTable(legendshapegroup, shapes_table);
+    float start_x = BORDER[0] + LEGEND_X_OFFSET;
+    float start_y = BORDER[1] + LEGEND_Y_OFFSET;
+    float incr = (float) 30.0;
+    for (int i = 0; i < num_states; i++) {
+      shapes_table_viz.setFillColor(i, ssp.getColour(state_names[i]));
+      shapes_table_viz.setFloat(i, VisualItem.X, start_x);
+      shapes_table_viz.setFloat(i, VisualItem.Y, start_y + (i * incr));
+    }
+    
+    // add legend labels, manipulate visualitems to set font
+    VisualTable legend_labels_table_viz = this.viz.addTable(legendgroup, legend_labels_table);
+    for (int i = 0; i < num_states; i++) {
+      legend_labels_table_viz.setFloat(i, VisualItem.X, start_x + LEGEND_TEXT_OFFSET);
+      legend_labels_table_viz.setFloat(i, VisualItem.Y, start_y + (i * incr));
+      legend_labels_table_viz.setTextColor(i,ColorLib.color(java.awt.Color.BLACK));
+      legend_labels_table_viz.setFont(i,new Font(Font.SANS_SERIF,Font.PLAIN,LEGEND_FONT_SIZE));
+    }
+    
+  }
+  
+  public void run() {
+
+    // setup bounds
+    this.dataBound.setRect(BORDER[0],BORDER[1],SIZE_X-BORDER[2]-BORDER[0],SIZE_Y-BORDER[3]-BORDER[1]);
+    this.xlabBound.setRect(BORDER[0],BORDER[1],SIZE_X-BORDER[2]-BORDER[0],SIZE_Y-BORDER[3]-BORDER[1]);
+    this.ylabBound.setRect(BORDER[0],BORDER[1],SIZE_X-BORDER[2]-BORDER[0],SIZE_Y-BORDER[3]-BORDER[1]);
+    this.labelBottomBound.setRect(BORDER[0],SIZE_X-BORDER[2],SIZE_Y-BORDER[0]-BORDER[1],BORDER[3]);
+    
+    // setup visualization
+    this.viz = new Visualization();
+    this.setupRenderer();
+    
+    // add table to visualization
+    Table raw_data_tab = this.setupDataTable();
+    MapReduceSwimlanes mrs = new MapReduceSwimlanes();
+    mrs.populateTable_CollateReduces(raw_data_tab);
+    mrs.groupByState();
+    VisualTable maindatatable = mrs.addToVisualization(this.viz, maingroup);
+        
+    addAxisNames();
+    if (plot_legend) {
+      addLegend();
+    }
+
+    // plot swimlanes lines: setup axes, call custom action
+    ActionList draw = new ActionList();
+    {
+      // setup axes
+      AxisLayout xaxis = new AxisLayout(maingroup, START_FIELD_NAME, Constants.X_AXIS, VisiblePredicate.TRUE);
+      AxisLayout yaxis = new AxisLayout(maingroup, "ycoord", Constants.Y_AXIS, VisiblePredicate.FALSE);    
+      xaxis.setLayoutBounds(dataBound);
+      yaxis.setLayoutBounds(dataBound);
+    
+      ColumnMetadata starttime_meta = maindatatable.getMetadata(START_FIELD_NAME);    
+      ColumnMetadata finishtime_meta = maindatatable.getMetadata(END_FIELD_NAME);    
+      ColumnMetadata ycoord_meta = maindatatable.getMetadata("ycoord");
+      long x_min = (long) ((Double)maindatatable.get(starttime_meta.getMinimumRow(), START_FIELD_NAME)).doubleValue();
+      long x_max = (long) ((Double)maindatatable.get(finishtime_meta.getMaximumRow(), END_FIELD_NAME)).doubleValue();
+      xaxis.setRangeModel(new NumberRangeModel(x_min,x_max,x_min,x_max));
+      float y_max = maindatatable.getFloat(ycoord_meta.getMaximumRow(),"ycoord");
+      yaxis.setRangeModel(new NumberRangeModel(0,y_max,0,y_max));
+      
+      // call custom action to plot actual swimlanes lines
+      CoordScaler cs = new CoordScaler();
+      cs.set_pixel_size(SIZE_X-BORDER[0]-BORDER[2], SIZE_Y-BORDER[1]-BORDER[3]);
+      cs.set_pixel_start(BORDER[0],BORDER[1]);
+      cs.set_value_ranges(x_min,0,x_max,y_max);
+      //SwimlanesStateAction swimlaneslines = new SwimlanesStateAction(maingroup, cs);
+      SwimlanesStateAction swimlaneslines = new SwimlanesStateAction(maingroup, cs);
+      
+      // add everything to the plot
+      draw.add(xaxis);
+      draw.add(yaxis);
+      draw.add(swimlaneslines);
+      
+      AxisLabelLayout xlabels = new AxisLabelLayout("xlab", xaxis, xlabBound);
+      this.viz.putAction("xlabels",xlabels);
+      AxisLabelLayout ylabels = new AxisLabelLayout("ylab", yaxis, ylabBound);
+      this.viz.putAction("ylabels",ylabels);
+    }
+    
+    // add axes names
+    {
+      SpecifiedLayout sl = new SpecifiedLayout(labelgroup, VisualItem.X, VisualItem.Y);
+      ActionList labeldraw = new ActionList();
+      labeldraw.add(sl);
+      this.viz.putAction(labelgroup, labeldraw);
+    }
+    
+    // add legend
+    if (plot_legend) {
+      ShapeAction legend_sa = new ShapeAction(legendshapegroup);
+      SpecifiedLayout legendlabels_sl = new SpecifiedLayout(legendgroup, VisualItem.X, VisualItem.Y);
+    
+      ActionList legenddraw = new ActionList();
+      legenddraw.add(legend_sa);
+      this.viz.putAction(legendshapegroup, legenddraw);
+      ActionList legendlabelsdraw = new ActionList();
+      legendlabelsdraw.add(legendlabels_sl);
+      this.viz.putAction(legendgroup,legendlabelsdraw);
+    }
+    
+    // draw everything else
+    this.viz.putAction("draw",draw);
+
+    // finally draw
+    this.viz.run("draw");
+    this.viz.run("xlabels");
+    this.viz.run("ylabels");
+
+  }
+  
+  public Table getData() {
+    // preliminary setup
+    OfflineTimeHandler time_offline;
+    TimeHandler time_online;
+    long start, end;
+    
+    if (offline_use) {
+      time_offline = new OfflineTimeHandler(param_map, this.timezone);
+      start = time_offline.getStartTime();
+      end = time_offline.getEndTime();
+    } else {
+      time_online = new TimeHandler(this.request, this.timezone);
+      start = time_online.getStartTime();
+      end = time_online.getEndTime();
+    }
+    
+    DatabaseWriter dbw = new DatabaseWriter(this.cluster);
+    String query;
+    
+    // setup query
+    if (this.shuffle_option != null && this.shuffle_option.equals("shuffles")) {
+      query = "select job_id,friendly_id,start_time,finish_time,start_time_millis,finish_time_millis,status,state_name,hostname from ["+this.table+"] where finish_time between '[start]' and '[end]'";
+    } else {
+      query = "select job_id,friendly_id,start_time,finish_time,start_time_millis,finish_time_millis,status,state_name,hostname from ["+this.table+"] where finish_time between '[start]' and '[end]' and not state_name like 'shuffle_local' and not state_name like 'shuffle_remote'";
+    }
+    if (this.jobname != null) {
+      query = query + " and job_id like '"+ this.jobname +"'";
+    }
+    Macro mp = new Macro(start,end,query);
+    query = mp.toString() + " order by start_time";
+    
+    Table rs_tab = null;    
+    DatabaseDataSource dds; 
+    DefaultSQLDataHandler dh = new DefaultSQLDataHandler();
+
+    log.debug("Query: " + query);
+    // execute query
+    try {
+      dds = ConnectionFactory.getDatabaseConnection(dbw.getConnection());
+      rs_tab = dds.getData(query);
+    } catch (prefuse.data.io.DataIOException e) {
+      System.err.println("prefuse data IO error: " + e);
+      log.warn("prefuse data IO error: " + e);
+      return null;
+    } catch (SQLException e) {
+      System.err.println("Error in SQL: " + e + " in statement: " + query);
+      log.warn("Error in SQL: " + e + " in statement: " + query);
+      return null;
+    }
+    
+    HashMap<String, Integer> state_counts = new HashMap<String, Integer>();
+    HashSet<String> states = new HashSet<String>();
+    for (int i = 0; i < rs_tab.getRowCount(); i++) {
+      String curr_state = rs_tab.getString(i, "state_name");
+      states.add(curr_state);
+      Integer cnt = state_counts.get(curr_state);
+      if (cnt == null) {
+        state_counts.put(curr_state, new Integer(1));
+      } else {
+        state_counts.remove(curr_state);
+        state_counts.put(curr_state, new Integer(cnt.intValue()+1));
+      }
+    }
+    
+    log.info("Search complete: #cols: " + rs_tab.getColumnCount() + "; #rows: " + rs_tab.getRowCount());
+    
+    return rs_tab;
+  }
+  
+}
\ No newline at end of file

Added: hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/ImageSlicer.java
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/ImageSlicer.java?rev=793443&view=auto
==============================================================================
--- hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/ImageSlicer.java (added)
+++ hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/ImageSlicer.java Mon Jul 13 02:31:17 2009
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hadoop.chukwa.hicc;
+
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.awt.image.CropImageFilter;
+import java.awt.image.FilteredImageSource;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.imageio.ImageIO;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.hadoop.chukwa.util.ExceptionUtil;
+import org.apache.hadoop.chukwa.util.XssFilter;
+
+public class ImageSlicer {
+  private BufferedImage src = null;
+  private Log log = LogFactory.getLog(ImageSlicer.class);
+  private String sandbox = System.getenv("CHUKWA_HOME")+File.separator+"webapps"+File.separator+"sandbox"+File.separator;
+  private int maxLevel = 0;
+  
+  public ImageSlicer() {
+  }
+  
+  /*
+   * Prepare a large image for tiling.
+   * 
+   * Load an image from a file. Resize the image so that it is square,
+   * with dimensions that are an even power of two in length (e.g. 512,
+   * 1024, 2048, ...). Then, return it.
+   * 
+   */
+  public BufferedImage prepare(String filename) {
+    try {
+      src = ImageIO.read(new File(filename));
+    } catch (IOException e) {
+      log.error("Image file does not exist:"+filename+", can not render image.");
+    }
+    XYData fullSize = new XYData(1, 1);
+    while(fullSize.getX()<src.getWidth() || fullSize.getY()<src.getHeight()) {
+      fullSize.set(fullSize.getX()*2, fullSize.getY()*2);
+    }
+    float scaleX = (float)fullSize.getX()/src.getWidth();
+    float scaleY = (float)fullSize.getY()/src.getHeight();
+    log.info("Image size: ("+src.getWidth()+","+src.getHeight()+")");
+    log.info("Scale size: ("+scaleX+","+scaleY+")");
+    
+    AffineTransform at = 
+      AffineTransform.getScaleInstance(scaleX,scaleY);
+
+      //       AffineTransform.getScaleInstance((fullSize.getX()-src.getWidth())/2,(fullSize.getY()-src.getHeight())/2);
+    AffineTransformOp op = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
+    BufferedImage dest = op.filter(src, null);
+    return dest;
+  }
+  
+  /*
+   * Extract a single tile from a larger image.
+   * 
+   * Given an image, a zoom level (int), a quadrant (column, row tuple;
+   * ints), and an output size, crop and size a portion of the larger
+   * image. If the given zoom level would result in scaling the image up,
+   * throw an error - no need to create information where none exists.
+   * 
+   */
+  public BufferedImage tile(BufferedImage image, int level, XYData quadrant, XYData size, boolean efficient) throws Exception {
+    double scale = Math.pow(2, level);
+    if(efficient) {
+      /* efficient: crop out the area of interest first, then scale and copy it */
+      XYData inverSize = new XYData((int)(image.getWidth(null)/(size.getX()*scale)), 
+          (int)(image.getHeight(null)/(size.getY()*scale)));
+      XYData topLeft = new XYData(quadrant.getX()*size.getX()*inverSize.getX(), 
+          quadrant.getY()*size.getY()*inverSize.getY());
+      XYData newSize = new XYData((size.getX()*inverSize.getX()), 
+          (size.getY()*inverSize.getY()));
+      if(inverSize.getX()<1.0 || inverSize.getY() < 1.0) {
+        throw new Exception("Requested zoom level ("+level+") is too high.");
+      }
+      image = image.getSubimage(topLeft.getX(), topLeft.getY(), newSize.getX(), newSize.getY());
+      BufferedImage zoomed = new BufferedImage(size.getX(), size.getY(), BufferedImage.TYPE_INT_RGB);
+      zoomed.getGraphics().drawImage(image, 0, 0, size.getX(), size.getY(), null);
+      if(level>maxLevel) {
+        maxLevel = level;
+      }
+      return zoomed;
+    } else {
+      /* inefficient: copy the whole image, scale it and then crop out the area of interest */
+      XYData newSize = new XYData((int)(size.getX()*scale), (int)(size.getY()*scale));
+      XYData topLeft = new XYData(quadrant.getX()*size.getX(), quadrant.getY()*size.getY());
+      if(newSize.getX() > image.getWidth(null) || newSize.getY() > image.getHeight(null)) {
+        throw new Exception("Requested zoom level ("+level+") is too high.");
+      }
+      AffineTransform tx = new AffineTransform();
+      AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR);
+      tx.scale(scale, scale);
+      image = op.filter(image, null);
+      BufferedImage zoomed = image.getSubimage(topLeft.getX(), topLeft.getY(), newSize.getX(), newSize.getY());
+      if(level>maxLevel) {
+        maxLevel = level;
+      }
+      return zoomed;
+    }
+  }
+  
+  /*
+   * Recursively subdivide a large image into small tiles.
+   * 
+   * Given an image, a zoom level (int), a quadrant (column, row tuple;
+   * ints), and an output size, cut the image into even quarters and
+   * recursively subdivide each, then generate a combined tile from the
+   * resulting subdivisions. If further subdivision would result in
+   * scaling the image up, use tile() to turn the image itself into a
+   * tile.
+   */
+  public BufferedImage subdivide(BufferedImage image, int level, XYData quadrant, XYData size, String prefix) {
+    if(image.getWidth()<=size.getX()*Math.pow(2, level)) {
+      try {
+        BufferedImage outputImage = tile(image, level, quadrant, size, true);
+        write(outputImage, level, quadrant, prefix);
+        return outputImage;
+      } catch (Exception e) {
+        log.error(ExceptionUtil.getStackTrace(e));
+      }
+    }
+    
+    BufferedImage zoomed = new BufferedImage(size.getX()*2, size.getY()*2, BufferedImage.TYPE_INT_RGB);
+    Graphics g = zoomed.getGraphics();
+    XYData newQuadrant = new XYData(quadrant.getX() * 2 + 0, quadrant.getY() * 2 + 0);
+    g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), 0, 0, null);
+    newQuadrant = new XYData(quadrant.getX()*2 + 0, quadrant.getY()*2 + 1);
+    g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), 0, size.getY(), null);
+    newQuadrant = new XYData(quadrant.getX()*2 + 1, quadrant.getY()*2 + 0);
+    g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), size.getX(), 0, null);
+    newQuadrant = new XYData(quadrant.getX()*2 + 1, quadrant.getY()*2 + 1);
+    g.drawImage(subdivide(image, level+1, newQuadrant, size, prefix), size.getX(), size.getY(), null);
+    BufferedImage outputImage = new BufferedImage(size.getX(), size.getY(), BufferedImage.TYPE_INT_RGB);
+    outputImage.getGraphics().drawImage(zoomed, 0, 0, size.getX(), size.getY(), null);
+    write(outputImage, level, quadrant, prefix);
+    return outputImage;    
+  }
+  
+  /*
+   * Write image file.
+   */
+  public void write(BufferedImage image, int level, XYData quadrant, String prefix) {
+    StringBuilder outputFile = new StringBuilder();
+    outputFile.append(sandbox);
+    outputFile.append(File.separator);
+    outputFile.append(prefix);
+    outputFile.append("-");
+    outputFile.append(level);
+    outputFile.append("-");
+    outputFile.append(quadrant.getX());
+    outputFile.append("-");
+    outputFile.append(quadrant.getY());
+    outputFile.append(".png");
+    FileOutputStream fos;
+    try {
+      fos = new FileOutputStream(outputFile.toString());
+      ImageIO.write(image, "PNG", fos);
+      fos.close();   
+    } catch (IOException e) {
+      log.error(ExceptionUtil.getStackTrace(e));
+    }
+  }
+  
+  public int process(String filename) {
+    Pattern p = Pattern.compile("(.*)\\.(.*)");
+    Matcher m = p.matcher(filename);
+    if(m.matches()) {
+      String prefix = m.group(1);
+      String fullPath = sandbox + File.separator + filename;
+      subdivide(prepare(fullPath), 0, new XYData(0, 0), new XYData(256, 256), prefix);
+      return maxLevel;
+    }
+    return 0;
+  }
+
+}
+
+class XYData {
+  private int x = 0;
+  private int y = 0;
+  
+  public XYData(int x, int y) {
+    this.x=x;
+    this.y=y;
+  }
+
+  public void set(int x, int y) {
+    this.x=x;
+    this.y=y;
+  }
+  
+  public int getX() {
+    return x;
+  }
+
+  public int getY() {
+    return y;
+  }
+  
+}

Added: hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/OfflineTimeHandler.java
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/OfflineTimeHandler.java?rev=793443&view=auto
==============================================================================
--- hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/OfflineTimeHandler.java (added)
+++ hadoop/chukwa/trunk/src/java/org/apache/hadoop/chukwa/hicc/OfflineTimeHandler.java Mon Jul 13 02:31:17 2009
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.chukwa.hicc;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.text.SimpleDateFormat;
+import java.util.HashMap;
+
+public class OfflineTimeHandler {
+  private TimeZone tz = null;
+  private long start = 0;
+  private long end = 0;
+  private String startDate = null;
+  private String startHour = null;
+  private String startMin = null;
+  private String endDate = null;
+  private String endHour = null;
+  private String endMin = null;
+  private String startS = null;
+  private String endS = null;
+  
+  public OfflineTimeHandler(HashMap<String, String> map) {
+    this.tz = TimeZone.getTimeZone("UTC");
+    init(map);
+  }
+
+  public OfflineTimeHandler(HashMap<String, String> map, String tz) {
+    if (tz != null) {
+      this.tz = TimeZone.getTimeZone(tz);
+    } else {
+      this.tz = TimeZone.getTimeZone("UTC");
+    }
+    init(map);
+  }
+
+  public void init(HashMap<String, String> map) {
+    Calendar now = Calendar.getInstance();
+    String timeType = "last";
+    if (map.get("time_type") == null
+        && map.get("time_type") == null
+        && map.get("period") == null
+        && map.get("period") == null) {
+      timeType = "last";
+      end = now.getTimeInMillis();
+      start = end - 60 * 60 * 1000;
+    } else if (map.get("period") != null
+        && !map.get("period").equals("")) {
+      String period = map.get("period");
+      this.start = now.getTimeInMillis();
+      this.end = now.getTimeInMillis();
+      if (period.equals("last1hr")) {
+        start = end - (60 * 60 * 1000);
+      } else if (period.equals("last2hr")) {
+        start = end - (2 * 60 * 60 * 1000);
+      } else if (period.equals("last3hr")) {
+        start = end - (3 * 60 * 60 * 1000);
+      } else if (period.equals("last6hr")) {
+        start = end - (6 * 60 * 60 * 1000);
+      } else if (period.equals("last12hr")) {
+        start = end - (12 * 60 * 60 * 1000);
+      } else if (period.equals("last24hr")) {
+        start = end - (24 * 60 * 60 * 1000);
+      } else if (period.equals("last7d")) {
+        start = end - (7 * 24 * 60 * 60 * 1000);
+      } else if (period.equals("last30d")) {
+        start = end - (30 * 24 * 60 * 60 * 1000);
+      }
+    } else if (map.get("start") != null
+        && map.get("end") != null) {
+      start = Long.parseLong(map.get("start"));
+      end = Long.parseLong(map.get("end"));
+    } else if (map.get("time_type").equals("range")) {
+      start = Long.parseLong(map.get("start"));
+      end = Long.parseLong(map.get("end"));
+    } else if (map.get("time_type").equals("last")
+        && map.get("period") != null) {
+      String period = map.get("period");
+      this.start = now.getTimeInMillis();
+      this.end = now.getTimeInMillis();
+      if (period.equals("last1hr")) {
+        start = end - (60 * 60 * 1000);
+      } else if (period.equals("last2hr")) {
+        start = end - (2 * 60 * 60 * 1000);
+      } else if (period.equals("last3hr")) {
+        start = end - (3 * 60 * 60 * 1000);
+      } else if (period.equals("last6hr")) {
+        start = end - (6 * 60 * 60 * 1000);
+      } else if (period.equals("last12hr")) {
+        start = end - (12 * 60 * 60 * 1000);
+      } else if (period.equals("last24hr")) {
+        start = end - (24 * 60 * 60 * 1000);
+      } else if (period.equals("last7d")) {
+        start = end - (7 * 24 * 60 * 60 * 1000);
+      } else if (period.equals("last30d")) {
+        start = end - (30L * 24 * 60 * 60 * 1000);
+      }
+    }
+    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+    SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");
+    SimpleDateFormat formatHour = new SimpleDateFormat("HH");
+    SimpleDateFormat formatMin = new SimpleDateFormat("mm");
+
+    formatter.setTimeZone(this.tz);
+    formatDate.setTimeZone(this.tz);
+    formatHour.setTimeZone(this.tz);
+    formatMin.setTimeZone(this.tz);
+
+    startS = formatter.format(start);
+    this.startDate = formatDate.format(start);
+    this.startHour = formatHour.format(start);
+    this.startMin = formatMin.format(start);
+    endS = formatter.format(end);
+    this.endDate = formatDate.format(end);
+    this.endHour = formatHour.format(end);
+    this.endMin = formatMin.format(end);
+  }
+
+  public String getStartDate(String format) {
+    SimpleDateFormat formatter = new SimpleDateFormat(format);
+    formatter.setTimeZone(this.tz);
+    return formatter.format(this.start);
+  }
+
+  public String getStartDate() {
+    return this.startDate;
+  }
+
+  public String getStartHour() {
+    return this.startHour;
+  }
+
+  public String getStartMinute() {
+    return this.startMin;
+  }
+
+  public String getStartTimeText() {
+    return this.startS;
+  }
+
+  public long getStartTime() {
+    return start;
+  }
+
+  public String getEndDate(String format) {
+    SimpleDateFormat formatter = new SimpleDateFormat(format);
+    formatter.setTimeZone(this.tz);
+    return formatter.format(this.end);
+  }
+
+  public String getEndDate() {
+    return this.endDate;
+  }
+
+  public String getEndHour() {
+    return this.endHour;
+  }
+
+  public String getEndMinute() {
+    return this.endMin;
+  }
+
+  public String getEndTimeText() {
+    return this.endS;
+  }
+
+  public long getEndTime() {
+    return end;
+  }
+
+}

Modified: hadoop/chukwa/trunk/src/web/hicc/WEB-INF/jetty.xml
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/web/hicc/WEB-INF/jetty.xml?rev=793443&r1=793442&r2=793443&view=diff
==============================================================================
--- hadoop/chukwa/trunk/src/web/hicc/WEB-INF/jetty.xml (original)
+++ hadoop/chukwa/trunk/src/web/hicc/WEB-INF/jetty.xml Mon Jul 13 02:31:17 2009
@@ -117,7 +117,7 @@
       <Arg>
         <New class="org.mortbay.jetty.deployer.ContextDeployer">
           <Set name="contexts"><Ref id="Contexts"/></Set>
-          <Set name="configurationDir"><SystemProperty name="CHUKWA_HOME" default="."/>/contexts</Set>
+          <Set name="configurationDir"><SystemProperty name="CHUKWA_HOME" default="."/>/webapps/sandbox</Set>
           <Set name="scanInterval">1</Set>
         </New>
       </Arg>

Added: hadoop/chukwa/trunk/src/web/hicc/descriptors/swimlanes-static.descriptor
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/web/hicc/descriptors/swimlanes-static.descriptor?rev=793443&view=auto
==============================================================================
--- hadoop/chukwa/trunk/src/web/hicc/descriptors/swimlanes-static.descriptor (added)
+++ hadoop/chukwa/trunk/src/web/hicc/descriptors/swimlanes-static.descriptor Mon Jul 13 02:31:17 2009
@@ -0,0 +1,25 @@
+{
+"id":"swimlanes_viewer_static",
+"title":"Swimlanes (Static)",
+"version":"0.1",
+"categories":"Hadoop,Status",
+"module":"iframe/jsp/swimlanes-static.jsp",
+"description":"Display Time sorted events",
+"screendump":"\/images\/start.png",
+"refresh":"15",
+"parameters":[
+{"name":"height","type":"select","value":"600","label":"Height","options":[
+{"label":"300","value":"300"},
+{"label":"450","value":"400"},
+{"label":"600","value":"500"},
+{"label":"800","value":"800"},
+{"label":"1000","value":"1000"},
+{"label":"1200","value":"1200"}
+]},
+{"name":"event_type","type":"select","value":"noshuffle","label":"Show/Hide Shuffles","options":[
+{"label":"Do not show shuffles","value":"noshuffle"},
+{"label":"Show shuffles","value":"shuffles"}
+]}
+]
+}
+

Added: hadoop/chukwa/trunk/src/web/hicc/js/behaviour.js
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/web/hicc/js/behaviour.js?rev=793443&view=auto
==============================================================================
--- hadoop/chukwa/trunk/src/web/hicc/js/behaviour.js (added)
+++ hadoop/chukwa/trunk/src/web/hicc/js/behaviour.js Mon Jul 13 02:31:17 2009
@@ -0,0 +1,254 @@
+/*
+   Behaviour v1.0 by Ben Nolan, June 2005. Based largely on the work
+   of Simon Willison (see comments by Simon below).
+
+   Description:
+   	
+   	Uses css selectors to apply javascript behaviours to enable
+   	unobtrusive javascript in html documents.
+   	
+   Usage:   
+   
+	var myrules = {
+		'b.someclass' : function(element){
+			element.onclick = function(){
+				alert(this.innerHTML);
+			}
+		},
+		'#someid u' : function(element){
+			element.onmouseover = function(){
+				this.innerHTML = "BLAH!";
+			}
+		}
+	);
+	
+	Behaviour.register(myrules);
+	
+	// Call Behaviour.apply() to re-apply the rules (if you
+	// update the dom, etc).
+
+   License:
+   
+   	My stuff is BSD licensed. Not sure about Simon's.
+   	
+   More information:
+   	
+   	http://ripcord.co.nz/behaviour/
+   
+*/   
+
+var Behaviour = {
+	list : new Array,
+	
+	register : function(sheet){
+		Behaviour.list.push(sheet);
+	},
+	
+	start : function(){
+		Behaviour.addLoadEvent(function(){
+			Behaviour.apply();
+		});
+	},
+	
+	apply : function(){
+		for (h=0;sheet=Behaviour.list[h];h++){
+			for (selector in sheet){
+				list = document.getElementsBySelector(selector);
+				
+				if (!list){
+					continue;
+				}
+
+				for (i=0;element=list[i];i++){
+					sheet[selector](element);
+				}
+			}
+		}
+	},
+	
+	addLoadEvent : function(func){
+		var oldonload = window.onload;
+		
+		if (typeof window.onload != 'function') {
+			window.onload = func;
+		} else {
+			window.onload = function() {
+				oldonload();
+				func();
+			}
+		}
+	}
+}
+
+Behaviour.start();
+
+/*
+   The following code is Copyright (C) Simon Willison 2004.
+
+   document.getElementsBySelector(selector)
+   - returns an array of element objects from the current document
+     matching the CSS selector. Selectors can contain element names, 
+     class names and ids and can be nested. For example:
+     
+       elements = document.getElementsBySelect('div#main p a.external')
+     
+     Will return an array of all 'a' elements with 'external' in their 
+     class attribute that are contained inside 'p' elements that are 
+     contained inside the 'div' element which has id="main"
+
+   New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
+   See http://www.w3.org/TR/css3-selectors/#attribute-selectors
+
+   Version 0.4 - Simon Willison, March 25th 2003
+   -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
+   -- Opera 7 fails 
+*/
+
+function getAllChildren(e) {
+  // Returns all children of element. Workaround required for IE5/Windows. Ugh.
+  return e.all ? e.all : e.getElementsByTagName('*');
+}
+
+document.getElementsBySelector = function(selector) {
+  // Attempt to fail gracefully in lesser browsers
+  if (!document.getElementsByTagName) {
+    return new Array();
+  }
+  // Split selector in to tokens
+  var tokens = selector.split(' ');
+  var currentContext = new Array(document);
+  for (var i = 0; i < tokens.length; i++) {
+    token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
+    if (token.indexOf('#') > -1) {
+      // Token is an ID selector
+      var bits = token.split('#');
+      var tagName = bits[0];
+      var id = bits[1];
+      var element = document.getElementById(id);
+      if (tagName && element.nodeName.toLowerCase() != tagName) {
+        // tag with that ID not found, return false
+        return new Array();
+      }
+      // Set currentContext to contain just this element
+      currentContext = new Array(element);
+      continue; // Skip to next token
+    }
+    if (token.indexOf('.') > -1) {
+      // Token contains a class selector
+      var bits = token.split('.');
+      var tagName = bits[0];
+      var className = bits[1];
+      if (!tagName) {
+        tagName = '*';
+      }
+      // Get elements matching tag, filter them for class selector
+      var found = new Array;
+      var foundCount = 0;
+      for (var h = 0; h < currentContext.length; h++) {
+        var elements;
+        if (tagName == '*') {
+            elements = getAllChildren(currentContext[h]);
+        } else {
+            elements = currentContext[h].getElementsByTagName(tagName);
+        }
+        for (var j = 0; j < elements.length; j++) {
+          found[foundCount++] = elements[j];
+        }
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      for (var k = 0; k < found.length; k++) {
+        if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
+          currentContext[currentContextIndex++] = found[k];
+        }
+      }
+      continue; // Skip to next token
+    }
+    // Code to deal with attribute selectors
+    if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
+      var tagName = RegExp.$1;
+      var attrName = RegExp.$2;
+      var attrOperator = RegExp.$3;
+      var attrValue = RegExp.$4;
+      if (!tagName) {
+        tagName = '*';
+      }
+      // Grab all of the tagName elements within current context
+      var found = new Array;
+      var foundCount = 0;
+      for (var h = 0; h < currentContext.length; h++) {
+        var elements;
+        if (tagName == '*') {
+            elements = getAllChildren(currentContext[h]);
+        } else {
+            elements = currentContext[h].getElementsByTagName(tagName);
+        }
+        for (var j = 0; j < elements.length; j++) {
+          found[foundCount++] = elements[j];
+        }
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      var checkFunction; // This function will be used to filter the elements
+      switch (attrOperator) {
+        case '=': // Equality
+          checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
+          break;
+        case '~': // Match one of space seperated words 
+          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
+          break;
+        case '|': // Match start with value followed by optional hyphen
+          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
+          break;
+        case '^': // Match starts with value
+          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
+          break;
+        case '$': // Match ends with value - fails with "Warning" in Opera 7
+          checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
+          break;
+        case '*': // Match ends with value
+          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
+          break;
+        default :
+          // Just test for existence of attribute
+          checkFunction = function(e) { return e.getAttribute(attrName); };
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      for (var k = 0; k < found.length; k++) {
+        if (checkFunction(found[k])) {
+          currentContext[currentContextIndex++] = found[k];
+        }
+      }
+      // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
+      continue; // Skip to next token
+    }
+    
+    if (!currentContext[0]){
+    	return;
+    }
+    
+    // If we get here, token is JUST an element (not a class or ID selector)
+    tagName = token;
+    var found = new Array;
+    var foundCount = 0;
+    for (var h = 0; h < currentContext.length; h++) {
+      var elements = currentContext[h].getElementsByTagName(tagName);
+      for (var j = 0; j < elements.length; j++) {
+        found[foundCount++] = elements[j];
+      }
+    }
+    currentContext = found;
+  }
+  return currentContext;
+}
+
+/* That revolting regular expression explained 
+/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
+  \---/  \---/\-------------/    \-------/
+    |      |         |               |
+    |      |         |           The value
+    |      |    ~,|,^,$,* or =
+    |   Attribute 
+   Tag
+*/

Added: hadoop/chukwa/trunk/src/web/hicc/js/gsv.js
URL: http://svn.apache.org/viewvc/hadoop/chukwa/trunk/src/web/hicc/js/gsv.js?rev=793443&view=auto
==============================================================================
--- hadoop/chukwa/trunk/src/web/hicc/js/gsv.js (added)
+++ hadoop/chukwa/trunk/src/web/hicc/js/gsv.js Mon Jul 13 02:31:17 2009
@@ -0,0 +1,433 @@
+/*
+  GSV 1.0, by Michal Migurski <mike-gsv@teczno.com>
+  $Id: gsv.js,v 1.6 2005/06/28 03:30:49 migurski Exp $
+
+  Description:
+    Generates a draggable and zoomable viewer for images that would be
+    otherwise too-large for a browser window, e.g. maps or hi-res
+    document scans. Images must be pre-cut into tiles by PowersOfTwo
+    Python library.
+   	
+  Usage:
+    For an HTML construct such as this:
+
+        <div class="imageViewer">
+            <div class="well"> </div>
+            <div class="surface"> </div>
+            <p class="status"> </p>
+        </div>
+
+    ...pass the DOM node for the top-level DIV, a directory name where
+    tile images can be found, and an integer describing the height of
+    each image tile to prepareViewer():
+
+        prepareViewer(element, 'tiles', 256);
+        
+    It is expected that the visual behavior of these nodes is determined
+    by a set of CSS rules.
+    
+    The "well" node is where generated IMG elements are appended. It
+    should have the CSS rule "overflow: hidden", to occlude image tiles
+    that have scrolled out of view.
+    
+    The "surface" node is the transparent mouse-responsive layer of the
+    image viewer, and should match the well in size.
+    
+    The "status" node is generally set to "display: none", but can be
+    shown when diagnostic information is desired. It's controlled by the
+    displayStatus() function here.
+
+  License:
+    Copyright (c) 2005 Michal Migurski <mike-gsv@teczno.com>
+    
+    Redistribution and use in source form, with or without modification,
+    are permitted provided that the following conditions are met:
+    1. Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+    2. The name of the author may not be used to endorse or promote products
+       derived from this software without specific prior written permission.
+    
+    THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+    IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/   
+
+function getEvent(event)
+{
+    if(event == undefined) {
+        return window.event;
+    }
+    
+    return event;
+}
+
+function prepareViewer(imageViewer, tileDir, tileSize)
+{
+    for(var child = imageViewer.firstChild; child; child = child.nextSibling) {
+        if(child.className == 'surface') {
+            imageViewer.activeSurface = child;
+            child.imageViewer = imageViewer;
+        
+        } else if(child.className == 'well') {
+            imageViewer.tileWell = child;
+            child.imageViewer = imageViewer;
+        
+        } else if(child.className == 'status') {
+            imageViewer.status = child;
+            child.imageViewer = imageViewer;
+        
+        }
+    }
+    
+    var width = imageViewer.offsetWidth;
+    var height = imageViewer.offsetHeight;
+    var zoomLevel = -1; // guaranteed at least one increment below, so start at less-than-zero
+    var fullSize = tileSize * Math.pow(2, zoomLevel); // full pixel size of the image at this zoom level
+    do {
+        zoomLevel += 1;
+        fullSize *= 2;
+    } while(fullSize < Math.max(width, height));
+
+    var center = {'x': ((fullSize - width) / -2), 'y': ((fullSize - height) / -2)}; // top-left pixel of viewer, if it were to be centered in the view window
+
+    imageViewer.style.width = width+'px';
+    imageViewer.style.height = height+'px';
+    
+    var top = 0;
+    var left = 0;
+    for(var node = imageViewer; node; node = node.offsetParent) {
+        top += node.offsetTop;
+        left += node.offsetLeft;
+    }
+
+    imageViewer.dimensions = {
+
+         // width and height of the viewer in pixels
+         'width': width, 'height': height,
+
+         // position of the viewer in the document, from the upper-left corner
+         'top': top, 'left': left,
+
+         // location and height of each tile; they're always square
+         'tileDir': tileDir, 'tileSize': tileSize,
+
+         // zero or higher; big number == big image, lots of tiles
+         'zoomLevel': zoomLevel,
+
+         // initial viewer position
+         // defined as window-relative x,y coordinate of upper-left hand corner of complete image
+         // usually negative. constant until zoomLevel changes
+         'x': center.x, 'y': center.y
+
+         };
+
+    imageViewer.start = {'x': 0, 'y': 0}; // this is reset each time that the mouse is pressed anew
+    imageViewer.pressed = false;
+
+    if(document.body.imageViewers == undefined) {
+        document.body.imageViewers = [imageViewer];
+        document.body.onmouseup = releaseViewer;
+
+    } else {
+        document.body.imageViewers.push(imageViewer);
+    
+    }
+
+    prepareTiles(imageViewer);
+}
+
+
+function prepareTiles(imageViewer)
+{
+    var activeSurface = imageViewer.activeSurface;
+    var tileWell = imageViewer.tileWell;
+    var dim = imageViewer.dimensions;
+
+    imageViewer.tiles = [];
+    
+    var rows = Math.ceil(dim.height / dim.tileSize) + 1;
+    var cols = Math.ceil(dim.width / dim.tileSize) + 1;
+    
+    displayStatus(imageViewer, 'rows: '+rows+', cols: '+cols);
+    
+    for(var c = 0; c < cols; c += 1) {
+        var tileCol = [];
+    
+        for(var r = 0; r < rows; r += 1) {
+
+            var tile = {'c': c, 'r': r, 'img': document.createElement('img'), 'imageViewer': imageViewer};
+
+            tile.img.className = 'tile';
+            tile.img.style.width = dim.tileSize+'px';
+            tile.img.style.height = dim.tileSize+'px';
+            setTileImage(tile, true);
+            
+            tileWell.appendChild(tile.img);
+            tileCol.push(tile);
+        }
+        
+        imageViewer.tiles.push(tileCol);
+    }
+    
+    activeSurface.onmousedown = pressViewer;
+    positionTiles(imageViewer, {'x': 0, 'y': 0}); // x, y should match imageViewer.start x, y
+}
+
+function positionTiles(imageViewer, mouse)
+{
+    var tiles = imageViewer.tiles;
+    var dim = imageViewer.dimensions;
+    var start = imageViewer.start;
+    
+    var statusTextLines = [];
+    statusTextLines.push('imageViewer.dimensions x,y: '+dim.x+','+dim.y);
+    
+    for(var c = 0; c < tiles.length; c += 1) {
+        for(var r = 0; r < tiles[c].length; r += 1) {
+
+            var tile = tiles[c][r];
+            
+            // wrappedAround will become true if any tile has to be wrapped around
+            var wrappedAround = false;
+            
+            tile.x = (tile.c * dim.tileSize) + dim.x + (mouse.x - start.x);
+            tile.y = (tile.r * dim.tileSize) + dim.y + (mouse.y - start.y);
+            
+            if(tile.x > dim.width) {
+                // tile is too far to the right
+                // shift it to the far-left until it's within the viewer window
+                do {
+                    tile.c -= tiles.length;
+                    tile.x = (tile.c * dim.tileSize) + dim.x + (mouse.x - start.x);
+                    wrappedAround = true;
+
+                } while(tile.x > dim.width);
+
+            } else {
+                // tile may be too far to the right
+                // if it is, shift it to the far-right until it's within the viewer window
+                while(tile.x < (-1 * dim.tileSize)) {
+                    tile.c += tiles.length;
+                    tile.x = (tile.c * dim.tileSize) + dim.x + (mouse.x - start.x);
+                    wrappedAround = true;
+
+                }
+            }
+            
+            if(tile.y > dim.height) {
+                // tile is too far down
+                // shift it to the very top until it's within the viewer window
+                do {
+                    tile.r -= tiles[c].length;
+                    tile.y = (tile.r * dim.tileSize) + dim.y + (mouse.y - start.y);
+                    wrappedAround = true;
+
+                } while(tile.y > dim.height);
+
+            } else {
+                // tile may be too far up
+                // if it is, shift it to the very bottom until it's within the viewer window
+                while(tile.y < (-1 * dim.tileSize)) {
+                    tile.r += tiles[c].length;
+                    tile.y = (tile.r * dim.tileSize) + dim.y + (mouse.y - start.y);
+                    wrappedAround = true;
+
+                }
+            }
+
+            statusTextLines.push('tile '+r+','+c+' at '+tile.c+','+tile.r);
+            
+            // set the tile image once to *maybe* null, then again to
+            // definitely the correct tile. this removes the wraparound
+            // artifacts seen over slower connections.
+            setTileImage(tile, wrappedAround);
+            setTileImage(tile, false);
+
+            tile.img.style.top = tile.y+'px';
+            tile.img.style.left = tile.x+'px';
+        }
+    }
+    
+    displayStatus(imageViewer, statusTextLines.join('<br>'));
+}
+
+function setTileImage(tile, nullOverride)
+{
+    var dim = tile.imageViewer.dimensions;
+
+    // request a particular image slice
+    var src = dim.tileDir+'-'+dim.zoomLevel+'-'+tile.c+'-'+tile.r+'.png';
+
+    // has the image been scrolled too far in any particular direction?
+    var left = tile.c < 0;
+    var high = tile.r < 0;
+    var right = tile.c >= Math.pow(2, tile.imageViewer.dimensions.zoomLevel);
+    var low = tile.r >= Math.pow(2, tile.imageViewer.dimensions.zoomLevel);
+    var outside = high || left || low || right;
+
+         if(nullOverride)     { src = '/hicc/images/blank.gif';          }
+
+    // note this "outside" clause overrides all those below
+    else if(outside)          { src = '/hicc/images/blank.gif';          }
+
+    else if(high && left)     { src = 'null/top-left.png';      }
+    else if(low  && left)     { src = 'null/bottom-left.png';   }
+    else if(high && right)    { src = 'null/top-right.png';     }
+    else if(low  && right)    { src = 'null/bottom-right.png';  }
+    else if(high)             { src = 'null/top.png';           }
+    else if(right)            { src = 'null/right.png';         }
+    else if(low)              { src = 'null/bottom.png';        }
+    else if(left)             { src = 'null/left.png';          }
+
+    tile.img.src = src;
+}
+
+function moveViewer(event)
+{
+    var imageViewer = this.imageViewer;
+    var ev = getEvent(event);
+    var mouse = localizeCoordinates(imageViewer, {'x': ev.clientX, 'y': ev.clientY});
+
+    displayStatus(imageViewer, 'mouse at: '+mouse.x+', '+mouse.y+', '+(imageViewer.tiles.length * imageViewer.tiles[0].length)+' tiles to process');
+    positionTiles(imageViewer, {'x': mouse.x, 'y': mouse.y});
+}
+
+function localizeCoordinates(imageViewer, client)
+{
+    var local = {'x': client.x, 'y': client.y};
+
+    for(var node = imageViewer; node; node = node.offsetParent) {
+        local.x -= node.offsetLeft;
+        local.y -= node.offsetTop;
+    }
+    
+    return local;
+}
+
+function pressViewer(event)
+{
+    var imageViewer = this.imageViewer;
+    var dim = imageViewer.dimensions;
+    var ev = getEvent(event);
+    var mouse = localizeCoordinates(imageViewer, {'x': ev.clientX, 'y': ev.clientY});
+
+    imageViewer.pressed = true;
+    imageViewer.tileWell.style.cursor = imageViewer.activeSurface.style.cursor = 'move';
+    
+    imageViewer.start = {'x': mouse.x, 'y': mouse.y};
+    this.onmousemove = moveViewer;
+
+    displayStatus(imageViewer, 'mouse pressed at '+mouse.x+','+mouse.y);
+}
+
+function releaseViewer(event)
+{
+    var ev = getEvent(event);
+    
+    for(var i = 0; i < document.body.imageViewers.length; i += 1) {
+        var imageViewer = document.body.imageViewers[i];
+        var mouse = localizeCoordinates(imageViewer, {'x': ev.clientX, 'y': ev.clientY});
+        var dim = imageViewer.dimensions;
+
+        if(imageViewer.pressed) {
+            imageViewer.activeSurface.onmousemove = null;
+            imageViewer.tileWell.style.cursor = imageViewer.activeSurface.style.cursor = 'default';
+            imageViewer.pressed = false;
+
+            dim.x += (mouse.x - imageViewer.start.x);
+            dim.y += (mouse.y - imageViewer.start.y);
+        }
+
+        displayStatus(imageViewer, 'mouse dragged from '+imageViewer.start.x+', '+imageViewer.start.y+' to '+mouse.x+','+mouse.y+'. image: '+dim.x+','+dim.y);
+    }
+}
+
+function displayStatus(imageViewer, message)
+{
+    imageViewer.status.innerHTML = message;
+}
+
+function dumpInfo(imageViewer)
+{
+    var dim = imageViewer.dimensions;
+    var tiles = imageViewer.tiles;
+
+    var statusTextLines = ['imageViewer '+(i + 1), 'current window position: '+dim.x+','+dim.y+'.', '----'];
+
+    for(var c = 0; c < tiles.length; c += 1) {
+        for(var r = 0; r < tiles[c].length; r += 1) {
+            statusTextLines.push('image ('+c+','+r+') has tile ('+dim.zoomLevel+','+tiles[c][r].c+','+tiles[c][r].r+')');
+        }
+    }
+    
+    alert(statusTextLines.join("\n"));
+}
+
+function dumpAllInfo()
+{
+    for(var i = 0; i < document.body.imageViewers.length; i += 1) {
+        dumpInfo(document.body.imageViewers[i]);
+    }
+}
+
+function zoomImage(imageViewer, mouse, direction, max)
+{
+    var dim = imageViewer.dimensions;
+    
+    if(mouse == undefined) {
+        var mouse = {'x': dim.width / 2, 'y': dim.height / 2};
+    }
+
+    var pos = {'before': {'x': 0, 'y': 0}};
+
+    // pixel position within the image is a function of the
+    // upper-left-hand corner of the viewe in the page (pos.before),
+    // the click position (event), and the image position within
+    // the viewer (dim).
+    pos.before.x = (mouse.x - pos.before.x) - dim.x;
+    pos.before.y = (mouse.y - pos.before.y) - dim.y;
+    pos.before.width = pos.before.height = Math.pow(2, dim.zoomLevel) * dim.tileSize;
+    
+    var statusMessage = ['at current zoom level, image is '+pos.before.width+' pixels wide',
+                         '...mouse position is now '+pos.before.x+','+pos.before.y+' in the full image at zoom '+dim.zoomLevel,
+                         '...with the corner at '+dim.x+','+dim.y];
+
+    if(dim.zoomLevel + direction >= 0 && dim.zoomLevel + direction <= max) {
+        pos.after = {'width': (pos.before.width * Math.pow(2, direction)), 'height': (pos.before.height * Math.pow(2, direction))};
+        statusMessage.push('at zoom level '+(dim.zoomLevel + direction)+', image is '+pos.after.width+' pixels wide');
+
+        pos.after.x = pos.before.x * Math.pow(2, direction);
+        pos.after.y = pos.before.y * Math.pow(2, direction);
+        statusMessage.push('...so the current mouse position would be '+pos.after.x+','+pos.after.y);
+
+        pos.after.left = mouse.x - pos.after.x;
+        pos.after.top = mouse.y - pos.after.y;
+        statusMessage.push('...with the corner at '+pos.after.left+','+pos.after.top);
+        
+        dim.x = pos.after.left;
+        dim.y = pos.after.top;
+        dim.zoomLevel += direction;
+        
+        imageViewer.start = mouse;
+        positionTiles(imageViewer, mouse);
+    }
+
+    displayStatus(imageViewer, statusMessage.join('<br>'));
+}
+
+function zoomImageUp(imageViewer, mouse, max)
+{
+    zoomImage(imageViewer, mouse, 1, max);
+}
+
+function zoomImageDown(imageViewer, mouse, max)
+{
+    zoomImage(imageViewer, mouse, -1, max);
+}



Mime
View raw message