package org.richfaces.sandbox.chart;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import javax.faces.component.UIComponent;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;
import org.richfaces.json.JSONException;
import org.richfaces.json.JSONObject;
import org.richfaces.renderkit.RendererBase;
import org.richfaces.sandbox.chart.component.AbstractChart;
import org.richfaces.sandbox.chart.component.AbstractLegend;
import org.richfaces.sandbox.chart.component.AbstractSeries;
import org.richfaces.sandbox.chart.component.AbstractXaxis;
import org.richfaces.sandbox.chart.component.AbstractYaxis;
import static java.util.Arrays.asList;
import java.util.HashMap;
import java.util.Map;
import javax.faces.FacesException;
import org.richfaces.javascript.JSFunctionDefinition;
import org.richfaces.javascript.JSReference;
import org.richfaces.json.JSONArray;
import org.richfaces.renderkit.RenderKitUtils;
import static org.richfaces.renderkit.RenderKitUtils.addToScriptHash;
import static org.richfaces.renderkit.RenderKitUtils.attributes;
import org.richfaces.sandbox.chart.component.AbstractPoint;
import org.richfaces.sandbox.chart.model.ChartDataModel;
import org.richfaces.sandbox.chart.model.NumberChartDataModel;
import org.richfaces.sandbox.chart.model.StringChartDataModel;
import org.richfaces.sandbox.chart.model.ChartDataModel.ChartType;
import org.richfaces.ui.common.AjaxFunction;
import org.richfaces.util.AjaxRendererUtils;
/**
*
*
* @author Lukas Macko
*/
public abstract class ChartRendererBase extends RendererBase {
public static final String RENDERER_TYPE = "org.richfaces.sandbox.ChartRenderer";
private static final String X_VALUE = "x";
private static final String Y_VALUE = "y";
private static final String POINT_INDEX = "pointIndex";
private static final String SERIES_INDEX = "seriesIndex";
private static final String EVENT_TYPE = "eventType";
private static final String PLOT_CLICK_TYPE = "plotclick";
/**
* Method adds key-value pair to object.
*
* @param obj
* @param key
* @param value
* @return
* @throws IOException if put to JSONObject fails
*/
public static JSONObject addAttribute(JSONObject obj, String key, Object value) throws IOException {
try {
if (value != null && !value.equals("")) {
obj.put(key, value);
}
} catch (JSONException ex) {
throw new IOException("JSONObject put failed.");
}
return obj;
}
/**
* Method creates JSON containing chart options
* @param context
* @param component
* @return
* @throws IOException
*/
public JSONObject getOpts(FacesContext context, UIComponent component) throws IOException {
JSONObject obj = new JSONObject();
addAttribute(obj, "zoom", component.getAttributes().get("zoom"));
addAttribute(obj, "charttype", component.getAttributes().get("charttype"));
addAttribute(obj, "xtype", component.getAttributes().get("xtype"));
addAttribute(obj, "ytype", component.getAttributes().get("ytype"));
JSONObject xaxis = new JSONObject();
addAttribute(xaxis, "min", component.getAttributes().get("xmin"));
addAttribute(xaxis, "max", component.getAttributes().get("xmax"));
addAttribute(xaxis, "autoscaleMargin", component.getAttributes().get("xpad"));
addAttribute(xaxis, "axisLabel", component.getAttributes().get("xlabel"));
addAttribute(xaxis, "format", component.getAttributes().get("xformat"));
JSONObject yaxis = new JSONObject();
addAttribute(yaxis, "min", component.getAttributes().get("ymin"));
addAttribute(yaxis, "max", component.getAttributes().get("ymax"));
addAttribute(yaxis, "autoscaleMargin", component.getAttributes().get("ypad"));
addAttribute(yaxis, "axisLabel", component.getAttributes().get("ylabel"));
addAttribute(yaxis, "format", component.getAttributes().get("yformat"));
JSONObject legend = new JSONObject();
addAttribute(legend, "position", component.getAttributes().get("position"));
addAttribute(legend, "sorted", component.getAttributes().get("sorting"));
addAttribute(obj, "xaxis", xaxis);
addAttribute(obj, "yaxis", yaxis);
addAttribute(obj, "legend", legend);
return obj;
}
@Override
public void decode(FacesContext context, UIComponent component) {
super.decode(context, component);
if(!component.isRendered()){
return;
}
Map<String, String> requestParameterMap = context.getExternalContext().getRequestParameterMap();
if (requestParameterMap.get(component.getClientId(context)) != null) {
String xParam = requestParameterMap.get(getFieldId(component, X_VALUE));
String yParam = requestParameterMap.get(getFieldId(component, Y_VALUE));
String pointIndexParam = requestParameterMap.get(getFieldId(component, POINT_INDEX));
String eventTypeParam = requestParameterMap.get(getFieldId(component, EVENT_TYPE));
String seriesIndexParam = requestParameterMap.get(getFieldId(component, SERIES_INDEX));
try {
if (PLOT_CLICK_TYPE.equals(eventTypeParam)) {
double y = Double.parseDouble(yParam);
int seriesIndex = Integer.parseInt(seriesIndexParam);
int pointIndex = Integer.parseInt(pointIndexParam);
String x = xParam;
new PlotClickEvent(component, seriesIndex, pointIndex, x, y).queue();
}
} catch (NumberFormatException ex) {
throw new FacesException("Cannot convert request parmeters", ex);
}
}
}
/**
* Returns chart chart data
* @param ctx
* @param component
* @return
*/
public JSONArray getData(FacesContext ctx, UIComponent component) {
return (JSONArray) component.getAttributes().get("data");
}
/**
* Method process chart tags, it collects chart options and data.
*/
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
super.encodeBegin(context, component);
AbstractChart chart = (AbstractChart) component;
VisitChart visitCallback = new VisitChart(chart);
//copy attributes to parent tag and process data
chart.visitTree(VisitContext.createVisitContext(FacesContext.getCurrentInstance()), visitCallback);
//store data to parent tag
component.getAttributes().put("data", visitCallback.getData());
if(!visitCallback.isDataEmpty()){
component.getAttributes().put("charttype", visitCallback.getChartType());
component.getAttributes().put("xtype", visitCallback.getKeyType());
component.getAttributes().put("ytype", visitCallback.getValType());
}
component.getAttributes().put("handlers", visitCallback.getSeriesSpecificHandlers());
}
public JSONObject getParticularSeriesHandler(FacesContext context,UIComponent component){
return (JSONObject) component.getAttributes().get("handlers");
};
public JSFunctionDefinition createEventFunction(FacesContext context, UIComponent component){
Map<String,Object> params = new HashMap<String, Object>();
params.put(getFieldId(component, SERIES_INDEX), new JSReference(SERIES_INDEX));
params.put(getFieldId(component, POINT_INDEX), new JSReference(POINT_INDEX));
params.put(getFieldId(component, X_VALUE), new JSReference(X_VALUE));
params.put(getFieldId(component, Y_VALUE), new JSReference(Y_VALUE));
params.put(getFieldId(component, EVENT_TYPE), new JSReference(EVENT_TYPE));
AjaxFunction ajaxFce = AjaxRendererUtils.buildAjaxFunction(context, component);
ajaxFce.getOptions().getParameters().putAll(params);
return new JSFunctionDefinition("event",EVENT_TYPE,SERIES_INDEX,
POINT_INDEX,X_VALUE,Y_VALUE).addToBody(ajaxFce);
}
/**
* Method creates unique identifier for request parameter.
*
* @param component
* @param attribute
* @return
*/
public String getFieldId(UIComponent component, String attribute) {
return component.getClientId() + "-" + attribute;
}
/**
* Callback loop through children tags: axis, series, legend
*/
class VisitChart implements VisitCallback {
private AbstractChart chart;
private JSONArray data;
private JSONObject seriesSpecificHandlers;
private JSONArray plotClickHandlers;
private JSONArray plothoverHandlers;
private ChartDataModel.ChartType chartType;
private Class keyType;
private Class valType;
private RenderKitUtils.ScriptHashVariableWrapper eventWrapper=RenderKitUtils.ScriptHashVariableWrapper.eventHandler;
private boolean nodata;
public VisitChart(AbstractChart ch) {
this.nodata=true;
this.chart = ch;
this.chartType = null;
this.data = new JSONArray();
this.seriesSpecificHandlers = new JSONObject();
this.plotClickHandlers = new JSONArray();
this.plothoverHandlers = new JSONArray();
try{
addAttribute(seriesSpecificHandlers, "onplotclick", plotClickHandlers);
addAttribute(seriesSpecificHandlers, "onplothover", plothoverHandlers);
} catch (IOException ex){
throw new FacesException(ex);
}
}
private void copyAttr(UIComponent src, UIComponent target, String prefix, String attr) {
Object val = src.getAttributes().get(attr);
if (val != null) {
target.getAttributes().put(prefix + attr, val);
}
}
/**
* Copy attributes from source UIComponent to target.
*
* @param src
* @param target
* @param prefix
* @param attrs
*/
private void copyAttrs(UIComponent src, UIComponent target, String prefix, List<String> attrs) {
for (Iterator<String> it = attrs.iterator(); it.hasNext();) {
String attr = it.next();
copyAttr(src, target, prefix, attr);
}
}
@Override
public VisitResult visit(VisitContext context, UIComponent target) {
if (target instanceof AbstractLegend) {
copyAttrs(target, chart, "", asList("position", "sorting"));
} else if (target instanceof AbstractSeries) {
AbstractSeries s = (AbstractSeries) target;
ChartDataModel model = s.getData();
//Collect Series specific handlers
Map<String,Object> optMap = new HashMap<String, Object>();
RenderKitUtils.Attributes seriesEvents = attributes()
.generic("onplothover","onplothover","plothover")
.generic("onplotclick","onplotclick","plotclick");
addToScriptHash(optMap, context.getFacesContext(), target, seriesEvents, RenderKitUtils.ScriptHashVariableWrapper.eventHandler);
if(optMap.get("onplotclick")!=null){
plotClickHandlers.put(new RawJSONString(optMap.get("onplotclick").toString()));
}
else{
plotClickHandlers.put(s.getOnplotclick());
}
if(optMap.get("onplothover")!=null){
plothoverHandlers.put(new RawJSONString(optMap.get("onplothover").toString()));
}
else{
plothoverHandlers.put(s.getOnplothover());
}
//end collect series specific handler
if (model == null) {
/**
* data model priority: if there is data model passed
* through data attribute use it. Otherwise nested point
* tags are expected.
*/
VisitSeries seriesCallback = new VisitSeries(s.getType());
s.visitTree(VisitContext.createVisitContext(FacesContext.getCurrentInstance()), seriesCallback);
model = seriesCallback.getModel();
//if series has no data create empty model
if(model==null){
switch (s.getType()) {
case line:
model = new NumberChartDataModel(ChartType.line);
break;
case bar:
model = new NumberChartDataModel(ChartType.bar);
break;
case pie:
model = new StringChartDataModel(ChartType.pie);
break;
default:
break;
}
}
else{
nodata=false;
}
}
else{
nodata=false;
}
model.setAttributes(s.getAttributes());
try {
//Check model/series compatibility
if(chartType==null && (!nodata)){
//if series is empty do not set types
chartType= model.getType();
keyType = model.getKeyType();
valType = model.getValueType();
}
else{
if(chartType== ChartDataModel.ChartType.pie){
throw new IllegalArgumentException("Pie chart supports only one series.");
}
}
if(keyType != model.getKeyType() || valType != model.getValueType()){
throw new IllegalArgumentException("Data model is not valid for this chart type.");
}
data.put(model.export());
} catch (IOException ex) {
throw new FacesException(ex);
}
} else if (target instanceof AbstractXaxis) {
copyAttrs(target, chart, "x", asList("min", "max", "pad", "label", "format"));
} else if (target instanceof AbstractYaxis) {
copyAttrs(target, chart, "y", asList("min", "max", "pad", "label", "format"));
}
return VisitResult.ACCEPT;
}
public boolean isDataEmpty(){
return nodata;
}
public JSONArray getData() {
return data;
}
public Class getKeyType() {
return keyType;
}
public Class getValType() {
return valType;
}
public ChartDataModel.ChartType getChartType() {
return chartType;
}
public JSONObject getSeriesSpecificHandlers() {
return seriesSpecificHandlers;
}
}
/**
* Callback loops through series children tags - points
*/
class VisitSeries implements VisitCallback {
private ChartDataModel model=null;
private ChartDataModel.ChartType type;
public VisitSeries(ChartDataModel.ChartType type) {
this.type = type;
}
@Override
public VisitResult visit(VisitContext context, UIComponent target) {
if (target instanceof AbstractPoint) {
AbstractPoint p = (AbstractPoint) target;
Object x = p.getX();
Object y = p.getY();
//the first point determine type of data model
if (model == null) {
if (x instanceof Number && y instanceof Number) {
model = new NumberChartDataModel(type);
}
else if(x instanceof String && y instanceof Number){
model = new StringChartDataModel(type);
}
else {
throw new IllegalArgumentException("Not supported type");
}
}
if (model.getKeyType().isAssignableFrom(x.getClass()) &&
model.getValueType().isAssignableFrom(y.getClass())) {
if(x instanceof Number && y instanceof Number){
model.put((Number)x,(Number)y);
}
else if(x instanceof String && y instanceof Number){
model.put((String) x, (Number)y);
}
else{
throw new IllegalArgumentException("Not supported types " + x.getClass()+" " +y.getClass()+" for "+model.getClass());
}
} else {
throw new IllegalArgumentException("Not supported types " + x.getClass()+" " +y.getClass()+" for "+model.getClass());
}
}
return VisitResult.ACCEPT;
}
public ChartDataModel getModel() {
return model;
}
}
}