Saturday, April 24, 2010

JSP: Nested custom tags

Here is a simple example of a nested custom tag (which renders a pie chart). Note that I use JQuery to manipulate the DOM.

Here is the test JSP (PieChartTest.jsp):
<html>
<head>
<title>Pie Chart</title>
<script type="text/javascript" src="js/jquery-1.4.2.min.js"></script>
<script type="text/javascript" src="js/fasttags.js"></script>
<%@ taglib uri="/WEB-INF/tlds/FastTags.tld" prefix="fast" %>
</head>
<body>
<fast:piechart radius="100">
<fast:pieslice name='Vanguard 2045' value='75000'/>
<fast:pieslice name='Fidelity 2040' value='50000'/>
<fast:pieslice name='Principal 2035' value='20000'/>
<fast:pieslice name='Citibank Money Market' value='5000'/>
</fast:piechart>
</body>
</html>



Here is the PieChart Tag class (PieChartTag.java):
package org.fastkangaroo.fasttags;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;


public class PieChartTag implements Tag, Serializable {

private PageContext pc = null;
private Tag parent = null;
private int radius = 90;
private List<String> namesList = new ArrayList<String>();
private List<String> valuesList = new ArrayList<String>();

public void addSlice(String name, String value){
namesList.add(name);
valuesList.add(value);
}

public void setPageContext(PageContext p) {
pc = p;
}

public void setParent(Tag t) {
parent = t;
}

public Tag getParent() {
return parent;
}

public int doStartTag() throws JspException {
namesList = new ArrayList<String>();
valuesList = new ArrayList<String>();
return EVAL_BODY_INCLUDE;
}

public int doEndTag() throws JspException {
try {
pc.getOut().write("<table id='legend'/>");
pc.getOut().write("<br/>");
pc.getOut().write("<div id='tooltip'></div>");
pc.getOut().write("<canvas id='canvas' width='" + (radius+5)*2 + "' height='" + (radius+5)*2 + "'/>");
pc.getOut().write("<script>draw(" + this.namesList.toString() + "," + this.valuesList.toString() + "," + radius + ");</script>");
} catch(IOException e) {
throw new JspTagException("An IOException occurred.");
}
return EVAL_PAGE;
}

public void release() {
pc = null;
parent = null;
}


/**
* @param radius the radius to set
*/
public void setRadius(int radius) {
this.radius = radius;
}

/**
* @return the radius
*/
public int getRadius() {
return radius;
}

}



Here is the PieSlice Tag class (PieSliceTag.java):
package org.fastkangaroo.fasttags;

import java.io.*;
import javax.servlet.jsp.*;
import javax.servlet.jsp.tagext.*;


public class PieSliceTag extends BodyTagSupport {

private PageContext pc = null;
private Tag parent = null;
private String name = null;
private String value = null;

public void setPageContext(PageContext p) {
pc = p;
}

public void setParent(Tag t) {
parent = t;
}

public Tag getParent() {
return parent;
}

public int doStartTag() throws JspException {
try {
PieChartTag parent =
(PieChartTag)findAncestorWithClass(this, PieChartTag.class);
if (parent != null){
parent.addSlice("'" + this.name + "'", this.value);
}
}
catch(Exception e) {
throw new JspTagException("An IOException occurred.");
}
return SKIP_BODY;
}

public int doEndTag() throws JspException {
return EVAL_PAGE;
}

public void release() {
pc = null;
parent = null;
}

/**
* @return the name
*/
public String getName() {
return name;
}

/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}

/**
* @return the value
*/
public String getValue() {
return value;
}

/**
* @param value the value to set
*/
public void setValue(String value) {
this.value = value;
}

}

Here is the TLD (FastTags.tld):
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN"
"http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">

<taglib>
<tlibversion>1.0</tlibversion>
<jspversion>1.1</jspversion>
<shortname>FastTags</shortname>
<uri>http://www.fastkangaroo-software.org</uri>
<info>Fast Tags Library</info>

<tag>
<name>piechart</name>
<tagclass>org.fastkangaroo.fasttags.PieChartTag</tagclass>
<bodycontent>scriptless</bodycontent>
<info>PieChart Tag</info>
<attribute>
<name>radius</name>
<required>false</required>
</attribute>
</tag>

<tag>
<name>pieslice</name>
<tagclass>org.fastkangaroo.fasttags.PieSliceTag</tagclass>
<bodycontent>empty</bodycontent>
<info>PieSlice Tag</info>
<attribute>
<name>name</name>
<required>true</required>
</attribute>
<attribute>
<name>value</name>
<required>true</required>
</attribute>
</tag>
</taglib>


Here is the javascript (fasttags.js)
var slices = new Array();
var pieTooltips = new Array();
var pie_radius = 90;
jQuery(document).ready(function(){
$("#canvas").mousemove(function(e){
tooltip(e);
});
})

function tooltip(e) {
var offset = $("#canvas").offset();
xpos = e.pageX - offset.left;
ypos = e.pageY - offset.top;
radius = Math.sqrt((xpos-(pie_radius+5))*(xpos-(pie_radius+5)) + (ypos-(pie_radius+5))*(ypos-(pie_radius+5)));
if (radius < pie_radius){
if (xpos > (pie_radius+5)) {
if (ypos < (pie_radius+5)) {
tan = ((pie_radius+5) - ypos)/(xpos -(pie_radius+5));
angle = 2*Math.PI - Math.atan(tan);
}
else {
tan = (ypos - (pie_radius+5))/(xpos -(pie_radius+5));
angle = Math.atan(tan);
}
}
else {
if (ypos < (pie_radius+5)) {
tan = ((pie_radius+5) - ypos)/((pie_radius+5) - xpos);
angle = Math.PI + Math.atan(tan);
}
else {
tan = (ypos - (pie_radius+5))/((pie_radius+5) - xpos);
angle = Math.PI - Math.atan(tan);
}
}
for (i = 0; i < slices.length; i++) {
if (angle < slices[i]){
$("#tooltip").html(pieTooltips[i-1]);
break;
}
}
}
}

function draw(names,values,radius) {
pie_radius = radius;
var canvas = document.getElementById("canvas");
if (canvas.getContext) {
var ctx = canvas.getContext("2d");
var total = 0;
for (i=0; i < values.length; i++){
total += values[i];
}
slices = new Array();
slices[0] = 0;
for (i=0; i < values.length; i++){
slices[i+1] = slices[i] + 2*Math.PI*values[i]/total;
}
var colors = ["#F1CAA0", "#AB1100","#22CC00","#CCBB22","#FF6600","#FFDD00","#00CC00","#FF1100","#44CC00","#FFCC11"]
$('#legend tr').remove();
pieTooltips = new Array();
for (i = 0; i < slices.length-1; i++) {
// Draw shapes
ctx.beginPath();
ctx.moveTo(radius+5,radius+5);
ctx.arc(radius+5,radius+5,radius,slices[i],slices[i+1],false);
ctx.moveTo(radius+5,radius+5);
ctx.fillStyle = colors[i];
ctx.fill();
var newRow = '<tr><td bgcolor="' + colors[i] + '" width="20px"></td><td style="padding-left:10px">' + names[i] + '</td><td style="padding-left:10px">' + values[i] + '</td><td style="padding-left:10px">(' + divide(values[i]*100,total) + '%)</td></tr>';
$(newRow).appendTo('#legend');
pieTooltips[i] = names[i] + ' : ' + values[i] + ' (' + divide(values[i]*100,total) + '%)';
}
}
}

function divide ( numerator, denominator ) {
// In JavaScript, dividing integer values yields a floating point result (unlike in Java, C++, C)
// To find the integer quotient, reduce the numerator by the remainder first, then divide.
var remainder = numerator % denominator;
var quotient = ( numerator - remainder ) / denominator;

// Another possible solution: Convert quotient to an integer by truncating toward 0.
// Thanks to Frans Janssens for pointing out that the floor function is not correct for negative quotients.
if ( quotient >= 0 )
quotient = Math.floor( quotient );
else // negative
quotient = Math.ceil( quotient );

return quotient;

}