package org.commcare.suite.model.graph; import org.commcare.graph.model.AnnotationData; import org.commcare.graph.model.BubblePointData; import org.commcare.graph.model.ConfigurableData; import org.commcare.graph.model.GraphData; import org.commcare.graph.model.SeriesData; import org.commcare.graph.model.XYPointData; import org.commcare.graph.util.GraphUtil; import org.commcare.suite.model.DetailTemplate; import org.commcare.suite.model.Text; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.core.util.externalizable.ExtWrapList; import org.javarosa.core.util.externalizable.ExtWrapListPoly; import org.javarosa.core.util.externalizable.ExtWrapMap; import org.javarosa.core.util.externalizable.Externalizable; import org.javarosa.core.util.externalizable.PrototypeFactory; import org.javarosa.model.xform.XPathReference; import org.javarosa.xpath.XPathParseTool; import org.javarosa.xpath.XPathTypeMismatchException; import org.javarosa.xpath.expr.XPathExpression; import org.javarosa.xpath.parser.XPathSyntaxException; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; /** * Defines a graph: type, set of series, set of text annotations, and key-value-based configuration. * * @author jschweers */ public class Graph implements Externalizable, DetailTemplate, Configurable { private String mType; private Vector<XYSeries> mSeries; private Hashtable<String, Text> mConfiguration; private Vector<Annotation> mAnnotations; public Graph() { mSeries = new Vector<>(); mConfiguration = new Hashtable<>(); mAnnotations = new Vector<>(); } public String getType() { return mType; } public void setType(String type) { mType = type; } public void addSeries(XYSeries s) { mSeries.addElement(s); } public void addAnnotation(Annotation a) { mAnnotations.addElement(a); } @Override public Text getConfiguration(String key) { return mConfiguration.get(key); } @Override public void setConfiguration(String key, Text value) { mConfiguration.put(key, value); } @Override public Enumeration getConfigurationKeys() { return mConfiguration.keys(); } @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { mType = ExtUtil.readString(in); mConfiguration = (Hashtable<String, Text>)ExtUtil.read(in, new ExtWrapMap(String.class, Text.class), pf); mSeries = (Vector<XYSeries>)ExtUtil.read(in, new ExtWrapListPoly(), pf); mAnnotations = (Vector<Annotation>)ExtUtil.read(in, new ExtWrapList(Annotation.class), pf); } @Override public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, mType); ExtUtil.write(out, new ExtWrapMap(mConfiguration)); ExtUtil.write(out, new ExtWrapListPoly(mSeries)); ExtUtil.write(out, new ExtWrapList(mAnnotations)); } @Override public GraphData evaluate(EvaluationContext context) { GraphData data = new GraphData(); data.setType(mType); evaluateConfiguration(this, data, context); evaluateSeries(data, context); evaluateAnnotations(data, context); return data; } /** * Helper for evaluate. Looks at annotations only. */ private void evaluateAnnotations(GraphData graphData, EvaluationContext context) { for (Annotation a : mAnnotations) { graphData.addAnnotation(new AnnotationData( a.getX().evaluate(context), a.getY().evaluate(context), a.getAnnotation().evaluate(context) )); } } /** * Helper for evaluate. Looks at configuration only. */ private void evaluateConfiguration(Configurable template, ConfigurableData data, EvaluationContext context) { Enumeration e = template.getConfigurationKeys(); Vector<String> nonvariables = new Vector<>(); String prefix = "var-"; while (e.hasMoreElements()) { String key = (String)e.nextElement(); if (key.startsWith(prefix)) { String value = template.getConfiguration(key).evaluate(context); context.setVariable(key.substring(prefix.length()), value); } else { nonvariables.addElement(key); } } for (String key : nonvariables) { String value = template.getConfiguration(key).evaluate(context); data.setConfiguration(key, value); } } /** * Helper for evaluate. Looks at all series. */ private void evaluateSeries(GraphData graphData, EvaluationContext context) { try { for (XYSeries s : mSeries) { Hashtable<String, Text> pointConfiguration = new Hashtable<>(); for (Enumeration e = s.getPointConfigurationKeys(); e.hasMoreElements();) { String key = (String) e.nextElement(); Text value = s.getPointConfiguration(key); if (value != null) { pointConfiguration.put(key, value); //s.removeConfiguration(key); } } SeriesData seriesData = new SeriesData(); EvaluationContext seriesContext = new EvaluationContext(context, context.getContextRef()); Vector<TreeReference> refList = expandNodeSet(s, context); Hashtable<String, Vector<String>> expandedConfiguration = new Hashtable(); for (Enumeration e = pointConfiguration.keys(); e.hasMoreElements();) { expandedConfiguration.put((String) e.nextElement(), new Vector<String>()); } for (TreeReference ref : refList) { EvaluationContext refContext = new EvaluationContext(seriesContext, ref); for (Enumeration e = pointConfiguration.keys(); e.hasMoreElements();) { String key = (String) e.nextElement(); String value = pointConfiguration.get(key).evaluate(refContext); if (value != null) { expandedConfiguration.get(key).addElement(value); } } String x = s.evaluateX(refContext); String y = s.evaluateY(refContext); if (x != null && (y != null || graphData.getType().equals(GraphUtil.TYPE_BAR))) { if (y == null) { y = "0"; } if (graphData.getType().equals(GraphUtil.TYPE_BUBBLE)) { String radius = ((BubbleSeries)s).evaluateRadius(refContext); seriesData.addPoint(new BubblePointData(x, y, radius)); } else { seriesData.addPoint(new XYPointData(x, y)); } } } graphData.addSeries(seriesData); for (Enumeration e = expandedConfiguration.keys(); e.hasMoreElements();) { String key = (String) e.nextElement(); StringBuffer json = new StringBuffer(); for (String pointValue : expandedConfiguration.get(key)) { json.append(",'"); json.append(pointValue); json.append("'"); } if (json.length() > 0) { json.deleteCharAt(0); } json.insert(0, "["); json.append("]"); Text value = Text.PlainText(json.toString()); s.setExpandedConfiguration(key, value); } // Handle configuration after data, since data processing may update configuration evaluateConfiguration(s, seriesData, seriesContext); // Guess at names for series, if they weren't provided if (seriesData.getConfiguration("name") == null) { seriesData.setConfiguration("name", s.getY()); } if (seriesData.getConfiguration("x-name") == null) { seriesData.setConfiguration("x-name", s.getX()); } } } catch (XPathSyntaxException e) { e.printStackTrace(); } } public static Vector<TreeReference> expandNodeSet(XYSeries series, EvaluationContext context) throws XPathSyntaxException { try { // Attempt to evaluate the nodeSet, which will succeed if this is just a path expression // (e.g., instance('casedb')/casedb/case[@case_type='point'][@status='open'][index/parent=current()/@case_id]) return context.expandReference(XPathReference.getPathExpr(series.getNodeSet()).getReference()); } catch (XPathTypeMismatchException e) { // If that fails, try treating the nodeSet as a more complex expression that itself returns a path // (e.g., if(true, "instance('item-list:rows1')/rows1/point", "instance('item-list:rows2')/rows2/point" )) XPathExpression xpe = XPathParseTool.parseXPath(series.getNodeSet()); String nodeSet = (String) xpe.eval(context); return context.expandReference(XPathReference.getPathExpr(nodeSet).getReference()); } } }