Java tutorial
//============================================================================== // // Copyright (c) 2002- // Authors: // * Alistair John Strachan <alistair@devzero.co.uk> (University of Edinburgh) // * Mike Arthur <mike@mikearthur.co.uk> (University of Edinburgh) // * Zak Cohen <zakcohen@gmail.com> (University of Edinburgh) // * Mark Kattenbelt <mark.kattenbelt@comlab.ox.ac.uk> (University of Oxford, formerly University of Birmingham) // //------------------------------------------------------------------------------ // // This file is part of PRISM. // // PRISM is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // PRISM is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with PRISM; if not, write to the Free Software Foundation, // Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // //============================================================================== package userinterface.graph; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.print.Printable; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.StringTokenizer; import java.util.TimerTask; import javax.swing.JOptionPane; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.encoders.KeypointPNGEncoderAdapter; import org.jfree.chart.labels.XYToolTipGenerator; import org.jfree.chart.plot.DefaultDrawingSupplier; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.LegendTitle; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.jfree.ui.RectangleEdge; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.itextpdf.awt.DefaultFontMapper; import com.itextpdf.text.PageSize; import com.itextpdf.text.pdf.PdfContentByte; import com.itextpdf.text.pdf.PdfName; import com.itextpdf.text.pdf.PdfPage; import com.itextpdf.text.pdf.PdfPageEvent; import com.itextpdf.text.pdf.PdfPageEventHelper; import com.itextpdf.text.pdf.PdfTemplate; import com.itextpdf.text.pdf.PdfWriter; import net.sf.epsgraphics.ColorMode; import net.sf.epsgraphics.EpsGraphics; import prism.PrismException; import settings.BooleanSetting; import settings.ChoiceSetting; import settings.DoubleSetting; import settings.FontColorPair; import settings.FontColorSetting; import settings.MultipleLineStringSetting; import settings.Setting; import settings.SettingDisplay; import settings.SettingException; import settings.SettingOwner; import userinterface.GUIPrism; /** * This class represents multiple series on a single unit graph; all series are * rendered together by MultiGraphView. */ public class Graph extends ChartPanel implements SettingOwner, EntityResolver, Observer, Printable { /** Interval for graph updates (in ms). */ private static final long updateInterval = 250; /** Actual JFreeChart representation of datasets. */ private JFreeChart chart; /** XYPlot of this JFreeChart */ private XYPlot plot; /** * List of currently available series data to plot. (Make sure to * synchronize) */ private XYSeriesCollection seriesCollection; /** * Maps SeriesKeys to a XYSeries. (Make sure to synchronize on * seriesCollection) */ protected HashMap<SeriesKey, XYSeries> keyToSeries; /** * Maps SeriesKeys to a Graph Series. (Make sure to synchronize on * seriesCollection) */ private HashMap<SeriesKey, SeriesSettings> keyToGraphSeries; /** * Allows us to batch graph points (JFreeChart is not realtime). (Make sure * to synchronize on seriesCollection) */ protected HashMap<SeriesKey, LinkedList<PrismXYDataItem>> graphCache; /** * Allows us to render errors on the graph * @author Muhammad Omer Saeed */ private PrismErrorRenderer errorRenderer; /** Display for settings. Required to implement SettingsOwner */ private SettingDisplay display; /** Settings of this graph. */ private MultipleLineStringSetting graphTitle; private FontColorSetting titleFont; private BooleanSetting legendVisible; private ChoiceSetting legendPosition; private FontColorSetting legendFont; private BooleanSetting errorBarVisible; private DoubleSetting errorBarCapLength; private DoubleSetting errorAlpha; private ChoiceSetting currentErrorMethod; /** Settings of the axis. */ private AxisSettings xAxisSettings; private AxisSettings yAxisSettings; /** Display settings */ private DisplaySettings displaySettings; /** GraphSeriesList */ private SeriesSettingsList seriesList; /** legend position */ public static final int LEFT = 0; public static final int RIGHT = 1; public static final int BOTTOM = 2; public static final int TOP = 3; /** * Initialises the GraphModel's series and canvas list. Also starts off the * graph update timer (one per chart). */ public Graph() { this(""); } /** * Initialises the GraphModel's series and canvas list. Also starts off the * graph update timer (one per chart). * * @param title * Title of the graph. */ public Graph(String title) { super(ChartFactory.createXYLineChart(title, "X", "Y", new XYSeriesCollection(), PlotOrientation.VERTICAL, true, true, false)); graphCache = new HashMap<SeriesKey, LinkedList<PrismXYDataItem>>(); keyToSeries = new HashMap<SeriesKey, XYSeries>(); keyToGraphSeries = new HashMap<SeriesKey, SeriesSettings>(); /** * This sets the error renderer to the graph. It plots the error bars wherever the errors are available * @author Muhammad Omer Saeed */ errorRenderer = new PrismErrorRenderer(); super.getChart().getXYPlot().setRenderer(errorRenderer); setToolTipRenderer(); graphTitle = new MultipleLineStringSetting("title", title, "The main title heading for the chart.", this, false); titleFont = new FontColorSetting("title font", new FontColorPair(new Font("SansSerif", Font.PLAIN, 14), Color.black), "The font for the chart's title", this, false); legendVisible = new BooleanSetting("legend visible?", new Boolean(true), "Should the legend, which displays all of the series headings, be displayed?", this, false); String[] choices = { "Left", "Right", "Bottom", "Top" }; legendPosition = new ChoiceSetting("legend position", choices, choices[RIGHT], "The position of the legend", this, false); legendFont = new FontColorSetting("legend font", new FontColorPair(new Font("SansSerif", Font.PLAIN, 11), Color.black), "The font for the legend", this, false); errorBarVisible = new BooleanSetting("Error info visible?", new Boolean(true), "Should the graph show the information (if available)?", this, false); errorBarCapLength = new DoubleSetting("Width of the error bar cap", errorRenderer.getCapLength(), "Set the width of the error bar cap", this, false); errorAlpha = new DoubleSetting("Transparency alpha deviation plot", errorRenderer.getAlpha(), "Set the transparency of the deviation error plot", this, false); String[] methods = { "Error bars", "Deviation plot" }; currentErrorMethod = new ChoiceSetting("Error render method", methods, "Deviation plot", "Select the error render method", this, false); errorAlpha.setEnabled(false); // Some easy references chart = super.getChart(); plot = chart.getXYPlot(); plot.setBackgroundPaint((Paint) Color.white); seriesCollection = (XYSeriesCollection) plot.getDataset(); xAxisSettings = new AxisSettings("X", true, this); yAxisSettings = new AxisSettings("Y", false, this); xAxisSettings.addObserver(this); yAxisSettings.addObserver(this); displaySettings = new DisplaySettings(this); displaySettings.addObserver(this); seriesList = new SeriesSettingsList(this); // create a regular XY line chart XYItemRenderer r = plot.getRenderer(); // if possible, try to match the old grapher if (r instanceof XYLineAndShapeRenderer) { XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) r; renderer.setBaseShapesVisible(true); renderer.setBaseShapesFilled(true); renderer.setDrawSeriesLineAsPath(true); renderer.setAutoPopulateSeriesPaint(true); renderer.setAutoPopulateSeriesShape(true); } plot.setDrawingSupplier(new DefaultDrawingSupplier(SeriesSettings.DEFAULT_PAINTS, DefaultDrawingSupplier.DEFAULT_OUTLINE_PAINT_SEQUENCE, DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE, DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE, SeriesSettings.DEFAULT_SHAPES)); super.setPopupMenu(null); /* Make sure the graph resembles its default settings. */ updateGraph(); // schedule a periodic timer for graph updates new java.util.Timer().scheduleAtFixedRate(new GraphUpdateTask(), 0, // start now updateInterval); } public int compareTo(Object o) { if (o instanceof SettingOwner) { SettingOwner po = (SettingOwner) o; if (getSettingOwnerID() < po.getSettingOwnerID()) return -1; else if (getSettingOwnerID() > po.getSettingOwnerID()) return 1; else return 0; } else return 0; } /** * Get the current error renderer * @return errorRenderer */ public PrismErrorRenderer getErrorRenderer() { return errorRenderer; } /** * Should the graph show the error information? * * @param showErrors */ public void showErrorInformation(boolean showErrors) { errorRenderer.setDrawError(showErrors); } /** * Sets the current error displaying method of the graph * @param method */ public void setCurrentErrorInfoMethod(int method) { errorRenderer.setCurrentMethod(method); } /** * Displays the info about the vertex including the error in each vertex nicely */ public void setToolTipRenderer() { errorRenderer.setBaseToolTipGenerator(new XYToolTipGenerator() { @Override public String generateToolTip(XYDataset dataset, int seriesIndex, int item) { XYSeriesCollection collection = (XYSeriesCollection) dataset; XYSeries series = collection.getSeries(seriesIndex); Number x = series.getX(item); Number y = series.getY(item); double error = ((PrismXYDataItem) series.getDataItem(item)).getError(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(String.format("<html><p style='color:#0000ff;'>Series: '%s'</p>", dataset.getSeriesKey(seriesIndex))); stringBuilder.append("X: " + (Math.round(x.doubleValue() * 10000.0) / 10000.0) + "<br>"); stringBuilder.append("Y: " + (Math.round(y.doubleValue() * 10000.0) / 10000.0) + " +/- " + (Math.round(error * 10000.0) / 10000.0) + "<br>"); stringBuilder.append("</html>"); return stringBuilder.toString(); } }); } public int getNumSettings() { return 9; } public Setting getSetting(int index) { switch (index) { case 0: return graphTitle; case 1: return titleFont; case 2: return legendVisible; case 3: return legendPosition; case 4: return legendFont; case 5: return errorBarVisible; case 6: return currentErrorMethod; case 7: return errorBarCapLength; case 8: return errorAlpha; default: return null; } } public String getSettingOwnerClassName() { return "Model"; } public int getSettingOwnerID() { return prism.PropertyConstants.MODEL; } public String getSettingOwnerName() { return graphTitle.getStringValue(); } public void doEnables() { legendPosition.setEnabled(legendVisible.getBooleanValue()); legendFont.setEnabled(legendVisible.getBooleanValue()); } public void update(Observable o, Object arg) { if (o == xAxisSettings) { /* X axis changed */ super.repaint(); } else if (o == yAxisSettings) { /* Y axis changed */ super.repaint(); } else if (o == displaySettings) { /* Display settings changed */ super.repaint(); } else { for (Map.Entry<SeriesKey, SeriesSettings> entry : keyToGraphSeries.entrySet()) { /* Graph series settings changed */ if (entry.getValue().equals(o)) repaint(); } } } public void notifySettingChanged(Setting setting) { updateGraph(); } private void updateGraph() { /* Update title if necessary. */ if (!this.chart.getTitle().equals(graphTitle)) { this.chart.setTitle(graphTitle.getStringValue()); } /* Update title font if necessary. */ if (!titleFont.getFontColorValue().f.equals(this.chart.getTitle().getFont())) { this.chart.getTitle().setFont(titleFont.getFontColorValue().f); } /* Update title colour if necessary. */ if (!titleFont.getFontColorValue().c.equals(this.chart.getTitle().getPaint())) { this.chart.getTitle().setPaint(titleFont.getFontColorValue().c); } if (legendVisible.getBooleanValue() != (this.chart.getLegend() != null)) { if (!legendVisible.getBooleanValue()) { this.chart.removeLegend(); } else { LegendTitle legend = new LegendTitle(plot.getRenderer()); legend.setBackgroundPaint(Color.white); legend.setBorder(1, 1, 1, 1); this.chart.addLegend(legend); } } /**Update the setting for the error information * */ if (errorBarVisible.getBooleanValue() == true) { this.showErrorInformation(true); chart.fireChartChanged(); currentErrorMethod.setEnabled(true); if (currentErrorMethod.getStringValue() == "Error bars") errorBarCapLength.setEnabled(true); else errorAlpha.setEnabled(true); } else { this.showErrorInformation(false); chart.fireChartChanged(); currentErrorMethod.setEnabled(false); errorBarCapLength.setEnabled(false); errorAlpha.setEnabled(false); } /**Update the current error rendering method*/ if (currentErrorMethod.isEnabled()) { if (currentErrorMethod.getStringValue().equals("Error bars")) { errorRenderer.setCurrentMethod(PrismErrorRenderer.ERRORBARS); errorAlpha.setEnabled(false); } else { errorRenderer.setCurrentMethod(PrismErrorRenderer.ERRORDEVIATION); errorBarCapLength.setEnabled(false); } } /**Update the properties of the error renderer depending on which method is selected*/ if (errorRenderer.getCurrentMethod() == PrismErrorRenderer.ERRORBARS && errorBarCapLength.isEnabled()) { errorRenderer.setCapLength(errorBarCapLength.getDoubleValue()); chart.fireChartChanged(); } else if (errorRenderer.getCurrentMethod() == PrismErrorRenderer.ERRORDEVIATION && errorAlpha.isEnabled()) { errorRenderer.setAlpha(errorAlpha.getDoubleValue()); chart.fireChartChanged(); } if (this.chart.getLegend() != null) { LegendTitle legend = this.chart.getLegend(); /* Put legend on the left if appropriate. */ if ((legendPosition.getCurrentIndex() == LEFT) && !legend.getPosition().equals(RectangleEdge.LEFT)) { legend.setPosition(RectangleEdge.LEFT); } /* Put legend on the right if appropriate. */ if ((legendPosition.getCurrentIndex() == RIGHT) && !legend.getPosition().equals(RectangleEdge.RIGHT)) { legend.setPosition(RectangleEdge.RIGHT); } /* Put legend on the top if appropriate. */ if ((legendPosition.getCurrentIndex() == TOP) && !legend.getPosition().equals(RectangleEdge.TOP)) { legend.setPosition(RectangleEdge.TOP); } /* Put legend on the bottom if appropriate. */ if ((legendPosition.getCurrentIndex() == BOTTOM) && !legend.getPosition().equals(RectangleEdge.BOTTOM)) { legend.setPosition(RectangleEdge.BOTTOM); } /* Set legend font. */ if (!legend.getItemFont().equals(legendFont.getFontColorValue().f)) { legend.setItemFont(legendFont.getFontColorValue().f); } /* Set legend font colour. */ if (!legend.getItemPaint().equals(legendFont.getFontColorValue().c)) { legend.setItemPaint(legendFont.getFontColorValue().c); } } super.repaint(); doEnables(); } public void setDisplay(SettingDisplay display) { this.display = display; } public SettingDisplay getDisplay() { return display; } /** * Returns an object that you have to synchronise in one case: * - You depend on series not changing. */ public Object getSeriesLock() { return seriesCollection; } public java.util.Vector<SeriesKey> getAllSeriesKeys() { synchronized (seriesCollection) { java.util.Vector<SeriesKey> result = new java.util.Vector<SeriesKey>(); for (Map.Entry<SeriesKey, XYSeries> entries : keyToSeries.entrySet()) { result.add(entries.getKey()); } return result; } } public SeriesSettingsList getGraphSeriesList() { return seriesList; } /** * Should always be synchronised on seriesCollection when called. */ public SeriesSettings getGraphSeries(SeriesKey key) { synchronized (seriesCollection) { if (keyToGraphSeries.containsKey(key)) { return keyToGraphSeries.get(key); } return null; } } /** * Should always be synchronised on seriesCollection when called. */ public XYSeries getXYSeries(SeriesKey key) { synchronized (seriesCollection) { if (keyToSeries.containsKey(key)) { return keyToSeries.get(key); } return null; } } /** * Should always be synchronised on seriesCollection when called. * @return >0 when series found. */ public int getJFreeChartIndex(SeriesKey key) { synchronized (seriesCollection) { XYSeries series = keyToSeries.get(key); for (int i = 0; i < seriesCollection.getSeriesCount(); i++) { if (seriesCollection.getSeries(i).equals((series))) return i; } return -1; } } /** * Getter for property graphTitle. * @return Value of property graphTitle. */ public String getTitle() { return graphTitle.getStringValue(); } /** * Setter for property graphTitle. * @param value Value of property graphTitle. */ public void setTitle(String value) { try { graphTitle.setValue(value); doEnables(); updateGraph(); } catch (SettingException e) { // Shouldn't happen. } } /** * Getter for property titleFont. * @return Value of property titleFont. */ public FontColorPair getTitleFont() { return titleFont.getFontColorValue(); } /** * Setter for property titleFont. * @param font Value of property titleFont. */ public void setTitleFont(FontColorPair font) { try { titleFont.setValue(font); doEnables(); updateGraph(); } catch (SettingException e) { // Shouldn't happen. } } /** * Getter for property legendFont. * @return Value of property legendFont. */ public FontColorPair getLegendFont() { return legendFont.getFontColorValue(); } /** * Setter for property legendFont. * @param font Value of property legendFont. */ public void setLegendFont(FontColorPair font) { try { legendFont.setValue(font); doEnables(); updateGraph(); } catch (SettingException e) { // Shouldn't happen. } } /** * Getter for property legendVisible. * @return Value of property legendVisible. */ public boolean isLegendVisible() { return legendVisible.getBooleanValue(); } /** * Setter for property legendVisible. * @param visible Value of property legendVisible. */ public void setLegendVisible(boolean visible) { try { legendVisible.setValue(visible); doEnables(); updateGraph(); } catch (SettingException e) { // Shouldn't happen. } } /** * Returns {@code true} if the current graph is showing error information and {@code false} otherwise * @return */ public boolean isErrorBarShow() { return errorBarVisible.getBooleanValue(); } /** * Set whether the current graph should show the error information or not * @param visible */ public void setErrorBarVisible(boolean visible) { try { errorBarVisible.setValue(visible); doEnables(); updateGraph(); } catch (SettingException e) { } } /** * Getter for property logarithmic. * @return the legend's position index: * <ul> * <li>0: LEFT * <li>1: RIGHT * <li>2: BOTTOM * <li>3: TOP * </ul> */ public int getLegendPosition() { return legendPosition.getCurrentIndex(); } /** * Setter for property logarithmic. * @param value Represents legend position * <ul> * <li>0: LEFT * <li>1: RIGHT * <li>2: BOTTOM * <li>4: TOP * </ul> */ public void setLegendPosition(int value) throws SettingException { legendPosition.setSelectedIndex(value); doEnables(); updateGraph(); } /** * Return settings of the x-Axis. * * @return Settings of the x-Axis. */ public AxisSettings getXAxisSettings() { return xAxisSettings; } /** * Return settings of the y-Axis. * * @return Settings of the y-Axis. */ public AxisSettings getYAxisSettings() { return yAxisSettings; } /** * Return display settings of the graph. * * @return Display settings of the graph. */ public DisplaySettings getDisplaySettings() { return displaySettings; } /** * Creates a unique name of the series to avoid conflict with any existing series name * @param seriesName the series name that has to be made unique * @return the unique series name */ private String getUniqueSeriesName(String seriesName) { synchronized (seriesCollection) { int counter = 0; String name = seriesName; /* Name sure seriesName is unique */ while (true) { boolean nameExists = false; for (Map.Entry<SeriesKey, XYSeries> entry : keyToSeries.entrySet()) { if (name.equals(entry.getValue().getKey())) { nameExists = true; break; } } if (nameExists) { counter++; name = seriesName + " (" + counter + ")"; } else { break; } } return name; } } public void moveUp(java.util.Vector<SeriesKey> keys) { synchronized (seriesCollection) { XYSeries[] newOrder = new XYSeries[seriesCollection.getSeriesCount()]; java.util.Vector<XYSeries> moveUpSet = new java.util.Vector<XYSeries>(); for (int i = 0; i < newOrder.length; i++) newOrder[i] = seriesCollection.getSeries(i); for (SeriesKey key : keys) { if (keyToSeries.containsKey(key)) moveUpSet.add(keyToSeries.get(key)); } for (int i = 1; i < newOrder.length; i++) { if (moveUpSet.contains(newOrder[i])) { XYSeries tmp = newOrder[i]; newOrder[i] = newOrder[i - 1]; newOrder[i - 1] = tmp; } } XYSeriesCollection newCollection = new XYSeriesCollection(); for (int i = 0; i < newOrder.length; i++) newCollection.addSeries(newOrder[i]); plot.setDataset(newCollection); this.seriesCollection = newCollection; this.seriesList.updateSeriesList(); } } public void moveDown(java.util.Vector<SeriesKey> keys) { synchronized (seriesCollection) { XYSeries[] newOrder = new XYSeries[seriesCollection.getSeriesCount()]; java.util.Vector<XYSeries> moveDownSet = new java.util.Vector<XYSeries>(); for (int i = 0; i < newOrder.length; i++) newOrder[i] = seriesCollection.getSeries(i); for (SeriesKey key : keys) { if (keyToSeries.containsKey(key)) moveDownSet.add(keyToSeries.get(key)); } for (int i = newOrder.length - 2; i >= 0; i--) { if (moveDownSet.contains(newOrder[i])) { XYSeries tmp = newOrder[i]; newOrder[i] = newOrder[i + 1]; newOrder[i + 1] = tmp; } } XYSeriesCollection newCollection = new XYSeriesCollection(); for (int i = 0; i < newOrder.length; i++) newCollection.addSeries(newOrder[i]); plot.setDataset(newCollection); this.seriesCollection = newCollection; this.seriesList.updateSeriesList(); } } /** * Add a series to the buffered graph data. * * @param seriesName * Name of series to add to graph. */ public SeriesKey addSeries(String seriesName) { SeriesKey key; synchronized (seriesCollection) { seriesName = getUniqueSeriesName(seriesName); // create a new XYSeries without sorting, disallowing duplicates PrismXYSeries newSeries = new PrismXYSeries(seriesName); this.seriesCollection.addSeries(newSeries); // allocate a new cache for this series key = new SeriesKey(); this.keyToSeries.put(key, newSeries); this.graphCache.put(key, new LinkedList<PrismXYDataItem>()); SeriesSettings graphSeries = new SeriesSettings(this, key); this.keyToGraphSeries.put(key, graphSeries); graphSeries.addObserver(this); this.seriesList.updateSeriesList(); } return key; } /** * Changes the name of a series. * * @param key The key identifying the series. * @param seriesName New name of series. */ public void changeSeriesName(SeriesKey key, String seriesName) { synchronized (seriesCollection) { seriesName = getUniqueSeriesName(seriesName); if (keyToSeries.containsKey(key)) { XYSeries series = keyToSeries.get(key); series.setKey(seriesName); } } } /** * Wholly remove a series from the current graph, by key. * @param seriesKey SeriesKey of series to remove. */ public void removeSeries(SeriesKey seriesKey) { synchronized (seriesCollection) { // Delete from keyToSeries and seriesCollection. if (keyToSeries.containsKey(seriesKey)) { XYSeries series = keyToSeries.get(seriesKey); seriesCollection.removeSeries(series); keyToSeries.remove(seriesKey); } // Remove any cache. if (graphCache.containsKey(seriesKey)) { graphCache.remove(seriesKey); } if (keyToGraphSeries.containsKey(seriesKey)) { keyToGraphSeries.get(seriesKey).deleteObservers(); keyToGraphSeries.remove(seriesKey); } this.seriesList.updateSeriesList(); } seriesList.updateSeriesList(); } /** * Add a point to the specified graph series. * @param seriesKey Key of series to update. * @param dataItem XYDataItem object to insert into this series. */ public void addPointToSeries(SeriesKey seriesKey, PrismXYDataItem dataItem) { synchronized (seriesCollection) { if (graphCache.containsKey(seriesKey)) { if (true) { LinkedList<PrismXYDataItem> seriesCache = graphCache.get(seriesKey); seriesCache.add(dataItem); } } } } /** * Remove all points from a graph series and its cache. * * @param seriesKey * Key of series to update. */ public void removeAllPoints(SeriesKey seriesKey) { synchronized (seriesCollection) { if (graphCache.containsKey(seriesKey)) { LinkedList<PrismXYDataItem> seriesCache = graphCache.get(seriesKey); seriesCache.clear(); } if (keyToSeries.containsKey(seriesKey)) { XYSeries series = keyToSeries.get(seriesKey); series.clear(); } } } /** * TODO: Document this! */ public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { InputSource inputSource = null; // override the resolve method for the dtd if (systemId.endsWith("dtd")) { // get appropriate dtd from classpath InputStream inputStream = Graph.class.getClassLoader().getResourceAsStream("dtds/chartformat.dtd"); if (inputStream != null) inputSource = new InputSource(inputStream); } return inputSource; } /** Refactored out from load(), parses an Axis. */ public static void parseAxis(ValueAxis axis, String minValue, String maxValue, String majorGridInterval, String minorGridInterval) { double min, max, major, minor; // can't work with null axis if (axis == null) return; try { min = Double.parseDouble(minValue); } catch (NumberFormatException e) { min = 0; } try { max = Double.parseDouble(maxValue); } catch (NumberFormatException e) { if (min < 1) { max = 1.0; } else { max = min + 1; } } try { major = Double.parseDouble(majorGridInterval); } catch (NumberFormatException e) { major = max / 5; } try { minor = Double.parseDouble(minorGridInterval); } catch (NumberFormatException e) { minor = major / 10; } // set parameters for axis axis.setLowerBound(min); axis.setUpperBound(max); axis.setTickMarkInsideLength((float) minor); axis.setTickMarkInsideLength((float) major); } public static boolean parseBoolean(String boolStr) { return ("true").equals(boolStr); } public static int parseInt(String intStr) { try { int d = Integer.parseInt(intStr); return d; } catch (NumberFormatException e) { return 0; } } public static double parseDouble(String doubleStr) { try { double d = Double.parseDouble(doubleStr); return d; } catch (NumberFormatException e) { return Double.NaN; } } /** Refactored out from load(), parses a Font */ public static Font parseFont(String fontName, String fontStyle, String fontSize) { int style, size; try { size = Integer.parseInt(fontSize); style = Integer.parseInt(fontStyle); } catch (NumberFormatException e) { // If there's an error, set defaults size = 14; style = Font.PLAIN; } if (size <= 0) size = 12; if (fontName.equals("")) fontName = "SansSerif"; return new Font(fontName, style, size); } /** Refactored out from load(), parses a Color. */ public static Color parseColor(String red, String green, String blue) { int r, g, b; try { r = Integer.parseInt(red); g = Integer.parseInt(green); b = Integer.parseInt(blue); if (r > 255) r = 255; if (r < 0) r = 0; if (g > 255) g = 255; if (g < 0) g = 0; if (b > 255) b = 255; if (b < 0) b = 0; } catch (NumberFormatException e) { // If theres an error, set defaults r = 0; g = 0; b = 0; } return new Color(r, g, b); } /** * Method to load a PRISM 'gra' file into the application. * @param file Name of the file to load. * @return The model of the graph contained in the file. * @throws GraphException if I/O errors have occurred. */ public static Graph load(File file) throws GraphException { Graph graph = new Graph(); try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); factory.setIgnoringElementContentWhitespace(true); DocumentBuilder builder = factory.newDocumentBuilder(); builder.setEntityResolver(graph); Document doc = builder.parse(file); Element chartFormat = doc.getDocumentElement(); graph.setTitle(chartFormat.getAttribute("graphTitle")); String titleFontName = chartFormat.getAttribute("titleFontName"); String titleFontSize = chartFormat.getAttribute("titleFontSize"); String titleFontStyle = chartFormat.getAttribute("titleFontStyle"); Font titleFont = parseFont(titleFontName, titleFontStyle, titleFontSize); String titleFontColourR = chartFormat.getAttribute("titleFontColourR"); String titleFontColourG = chartFormat.getAttribute("titleFontColourG"); String titleFontColourB = chartFormat.getAttribute("titleFontColourB"); Color titleFontColour = parseColor(titleFontColourR, titleFontColourG, titleFontColourB); graph.setTitleFont(new FontColorPair(titleFont, titleFontColour)); graph.setLegendVisible(parseBoolean(chartFormat.getAttribute("legendVisible"))); String legendPosition = chartFormat.getAttribute("legendPosition"); // Facilitate for bugs export in previous prism versions. if (chartFormat.getAttribute("versionString").equals("")) graph.setLegendPosition(RIGHT); else { if (legendPosition.equals("left")) graph.setLegendPosition(LEFT); else if (legendPosition.equals("right")) graph.setLegendPosition(RIGHT); else if (legendPosition.equals("bottom")) graph.setLegendPosition(BOTTOM); else if (legendPosition.equals("top")) graph.setLegendPosition(TOP); else // Probably was manual, now depricated graph.setLegendPosition(RIGHT); } //Get the nodes used to describe the various parts of the graph NodeList rootChildren = chartFormat.getChildNodes(); // Element layout is depricated for now. Element layout = (Element) rootChildren.item(0); Element xAxis = (Element) rootChildren.item(1); Element yAxis = (Element) rootChildren.item(2); graph.getXAxisSettings().load(xAxis); graph.getYAxisSettings().load(yAxis); //Read the headings and widths for each series for (int i = 3; i < rootChildren.getLength(); i++) { Element series = (Element) rootChildren.item(i); SeriesKey key = graph.addSeries(series.getAttribute("seriesHeading")); synchronized (graph.getSeriesLock()) { SeriesSettings seriesSettings = graph.getGraphSeries(key); seriesSettings.load(series); NodeList graphChildren = series.getChildNodes(); //Read each series out of the file and add its points to the graph for (int j = 0; j < graphChildren.getLength(); j++) { Element point = (Element) graphChildren.item(j); graph.addPointToSeries(key, new PrismXYDataItem(parseDouble(point.getAttribute("x")), parseDouble(point.getAttribute("y")))); } } } //Return the model of the graph return graph; } catch (Exception e) { throw new GraphException("Error in loading chart: " + e); } } /** * Allows graphs to be saved to the PRISM 'gra' file format. * * @param file * The file to save the graph to. * @throws GraphException * If the file cannot be written. */ public void save(File file) throws PrismException { try { JFreeChart chart = getChart(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document doc = builder.newDocument(); Element chartFormat = doc.createElement("chartFormat"); chartFormat.setAttribute("versionString", prism.Prism.getVersion()); chartFormat.setAttribute("graphTitle", getTitle()); Font titleFont = getTitleFont().f; chartFormat.setAttribute("titleFontName", titleFont.getName()); chartFormat.setAttribute("titleFontSize", "" + titleFont.getSize()); chartFormat.setAttribute("titleFontStyle", "" + titleFont.getStyle()); Color titleFontColor = (Color) getTitleFont().c; chartFormat.setAttribute("titleFontColourR", "" + titleFontColor.getRed()); chartFormat.setAttribute("titleFontColourG", "" + titleFontColor.getGreen()); chartFormat.setAttribute("titleFontColourB", "" + titleFontColor.getBlue()); chartFormat.setAttribute("legendVisible", isLegendVisible() ? "true" : "false"); Font legendFont = getLegendFont().f; chartFormat.setAttribute("legendFontName", "" + legendFont.getName()); chartFormat.setAttribute("legendFontSize", "" + legendFont.getSize()); chartFormat.setAttribute("legendFontStyle", "" + legendFont.getStyle()); Color legendFontColor = getLegendFont().c; chartFormat.setAttribute("legendFontColourR", "" + legendFontColor.getRed()); chartFormat.setAttribute("legendFontColourG", "" + legendFontColor.getGreen()); chartFormat.setAttribute("legendFontColourB", "" + legendFontColor.getBlue()); switch (getLegendPosition()) { case LEFT: chartFormat.setAttribute("legendPosition", "left"); break; case BOTTOM: chartFormat.setAttribute("legendPosition", "bottom"); break; case TOP: chartFormat.setAttribute("legendPosition", "top"); break; default: chartFormat.setAttribute("legendPosition", "right"); } Element layout = doc.createElement("layout"); chartFormat.appendChild(layout); Element xAxis = doc.createElement("axis"); getXAxisSettings().save(xAxis); chartFormat.appendChild(xAxis); Element yAxis = doc.createElement("axis"); getYAxisSettings().save(yAxis); chartFormat.appendChild(yAxis); synchronized (getSeriesLock()) { /* Make sure we preserve ordering. */ for (int i = 0; i < seriesList.getSize(); i++) { SeriesKey key = seriesList.getKeyAt(i); Element series = doc.createElement("graph"); SeriesSettings seriesSettings = getGraphSeries(key); seriesSettings.save(series); XYSeries seriesData = getXYSeries(key); for (int j = 0; j < seriesData.getItemCount(); j++) { Element point = doc.createElement("point"); point.setAttribute("x", "" + seriesData.getX(j)); point.setAttribute("y", "" + seriesData.getY(j)); series.appendChild(point); } chartFormat.appendChild(series); } } doc.appendChild(chartFormat); // File writing Transformer t = TransformerFactory.newInstance().newTransformer(); t.setOutputProperty("doctype-system", "chartformat.dtd"); t.setOutputProperty("indent", "yes"); t.transform(new DOMSource(doc), new StreamResult(new FileOutputStream(file))); } catch (IOException e) { throw new PrismException(e.getMessage()); } catch (DOMException e) { throw new PrismException("Problem saving graph: DOM Exception: " + e); } catch (ParserConfigurationException e) { throw new PrismException("Problem saving graph: Parser Exception: " + e); } catch (TransformerConfigurationException e) { throw new PrismException("Problem saving graph: Error in creating XML: " + e); } catch (TransformerException e) { throw new PrismException("Problem saving graph: Transformer Exception: " + e); } catch (SettingException e) { throw new PrismException(e.getMessage()); } } /** * Checks if the data in the specified series has error information available or not * @param series the series to check * @return {@code true} if error info exists, {@code false} otherwise */ public boolean checkErrorBarsExists(PrismXYSeries series) { for (int i = 0; i < series.getItemCount(); i++) { PrismXYDataItem item = (PrismXYDataItem) series.getDataItem(i); if (item.getError() != 0.0) { return true; } } return false; } /** * Exports the current graph to the GNU plot readable file * @param file the file to which the data should be saved * @throws IOException */ public void exportToGnuplot(File file) throws IOException { PrintWriter out = new PrintWriter(new FileWriter(file)); //write some info for the user out.println("#========================================="); out.println("# Generated by PRISM Chart Package"); out.println("#========================================="); out.println("# usage: gnuplot <filename>"); out.println("# Written by Muhammad Omer Saeed <muhammad.omar555@gmail.com>"); out.println(); // set up some properties for the GNU plot synchronized (getSeriesLock()) { out.println("set xrange [" + getChart().getXYPlot().getDomainAxis().getRange().getLowerBound() + ":" + getChart().getXYPlot().getDomainAxis().getRange().getUpperBound() + "]"); out.println("set yrange [" + getChart().getXYPlot().getRangeAxis().getRange().getLowerBound() + ":" + getChart().getXYPlot().getRangeAxis().getRange().getUpperBound() * 1.5 + "]"); out.println("set title " + "\"" + getChart().getTitle().getText() + "\""); out.println("set xlabel " + "\"" + getChart().getXYPlot().getDomainAxis().getLabel() + "\""); out.println("set ylabel " + "\"" + getChart().getXYPlot().getRangeAxis().getLabel() + "\""); out.println(); for (int i = getAllSeriesKeys().size() - 1; i >= 0; i--) { if (i == getAllSeriesKeys().size() - 1) { PrismXYSeries series = (PrismXYSeries) keyToSeries.get(getAllSeriesKeys().get(i)); boolean errorExists = checkErrorBarsExists(series); if (errorExists) { out.print("plot '-' using 1:2:3 with yerrorbars notitle, \"\" using 1:2 with lines title " + "\"" + series.getKey() + "\""); } else out.print("plot '-' using 1:2 with linespoints title " + "\"" + series.getKey() + "\""); } else { PrismXYSeries series = (PrismXYSeries) keyToSeries.get(getAllSeriesKeys().get(i)); boolean errorExists = checkErrorBarsExists(series); if (errorExists) { out.print(", \"\" using 1:2:3 with yerrorbars notitle, \"\" using 1:2 with lines title " + "\"" + series.getKey() + "\""); } else out.print(",\"\" using 1:2 with linespoints title " + "\"" + series.getKey() + "\""); } } out.println("\n"); // write the data for (int i = getAllSeriesKeys().size() - 1; i >= 0; i--) { PrismXYSeries series = (PrismXYSeries) keyToSeries.get(getAllSeriesKeys().get(i)); boolean errorExists = checkErrorBarsExists(series); if (errorExists) { out.println("#X #Y #Error"); for (int j = 0; j < series.getItemCount(); j++) { PrismXYDataItem item = (PrismXYDataItem) series.getDataItem(j); out.println(item.getXValue() + " " + item.getYValue() + " " + item.getError()); } out.println("end series"); } out.println("#X #Y"); for (int j = 0; j < series.getItemCount(); j++) { out.println(series.getX(j) + " " + series.getY(j)); } out.println("end series"); } } //finishing up out.println(); out.println("pause -1"); out.flush(); out.close(); } /** * Exports the current graph to Matlab file format. * @param file The file to write the data to. */ public void exportToMatlab(File file) throws IOException { PrintWriter out = new PrintWriter(new FileWriter(file)); out.println("%========================================="); out.println("%Generated by PRISM Chart Package"); out.println("%========================================="); out.println(); //Seriesdata synchronized (getSeriesLock()) { /* Make sure we preserve ordering. */ for (int i = 0; i < seriesList.getSize(); i++) { StringBuffer x = new StringBuffer("x" + i + " = ["); StringBuffer y = new StringBuffer("y" + i + " = ["); SeriesKey key = seriesList.getKeyAt(i); XYSeries seriesData = getXYSeries(key); for (int j = 0; j < seriesData.getItemCount(); j++) { x.append(seriesData.getX(j) + " "); y.append(seriesData.getY(j) + " "); } x.append("];"); y.append("];"); out.println(x.toString()); out.println(y.toString()); } //Create a figure out.println(); out.println( "figure1 = figure('Color', [1 1 1], 'PaperPosition',[0.6345 6.345 20.3 15.23],'PaperSize',[20.98 29.68]);"); //Create axes boolean xLog = getXAxisSettings().isLogarithmic(); boolean yLog = getYAxisSettings().isLogarithmic(); out.println(); if (xLog && yLog) out.println("axes1 = axes('Parent', figure1, 'FontSize', 16, 'XScale', 'log', 'YScale', 'log');"); else if (xLog) out.println("axes1 = axes('Parent', figure1, 'FontSize', 16, 'XScale', 'log');"); else if (yLog) out.println("axes1 = axes('Parent', figure1, 'FontSize', 16, 'YScale', 'log');"); else out.println("axes1 = axes('Parent', figure1, 'FontSize', 16);"); out.println("xlabel(axes1, '" + getXAxisSettings().getHeading() + "');"); out.println("ylabel(axes1, '" + getYAxisSettings().getHeading() + "');"); out.println("box(axes1, 'on');"); out.println("hold(axes1, 'all');"); //Graph title out.println(); String title = ""; StringTokenizer st = new StringTokenizer(getTitle(), "\n"); int num = st.countTokens(); for (int i = 0; i < num; i++) { title += "'" + st.nextToken() + "'"; if (i < num - 1) title += ", char(10),"; } out.println("title(axes1,[" + title + "])"); //Sort out logarithmic scales String scaleType = "plot"; if (seriesList.getSize() > 0) { if (xLog && yLog) scaleType = "loglog"; else if (xLog) scaleType = "semilogx"; else if (yLog) scaleType = "semilogy"; } //Create plots for (int i = 0; i < seriesList.getSize(); i++) { SeriesKey key = seriesList.getKeyAt(i); SeriesSettings seriesSettings = getGraphSeries(key); String marker = "'"; if (seriesSettings.showPoints() && seriesSettings.getSeriesShape() != SeriesSettings.NONE) { switch (seriesSettings.getSeriesShape()) { case SeriesSettings.CIRCLE: marker += "o"; break; case SeriesSettings.SQUARE: marker += "s"; break; case SeriesSettings.TRIANGLE: marker += "^"; break; case SeriesSettings.RECTANGLE_H: marker += "d"; break; case SeriesSettings.RECTANGLE_V: marker += "x"; break; } } if (seriesSettings.showLines()) { switch (seriesSettings.getLineStyle()) { case SeriesSettings.SOLID: marker += "-"; break; case SeriesSettings.DASHED: marker += "--"; break; case SeriesSettings.DOT_DASHED: marker += "-."; break; } } marker += "'"; out.println("plot" + i + " = " + scaleType + "(x" + i + ", y" + i + ", " + marker + ", 'Parent', axes1, 'LineWidth', 2);"); } // Create legend String seriesNames = ""; for (int i = 0; i < seriesList.getSize(); i++) { SeriesKey key = seriesList.getKeyAt(i); SeriesSettings seriesSettings = getGraphSeries(key); seriesNames += "'" + seriesSettings.getSeriesHeading() + "'"; if (i < seriesList.getSize() - 1) seriesNames += ", "; } //Determine location String loc = ""; switch (legendPosition.getCurrentIndex()) { case LEFT: loc = "'WestOutside'"; break; case RIGHT: loc = "'EastOutside'"; break; case BOTTOM: loc = "'SouthOutside'"; break; case TOP: loc = "'NorthOutside'"; break; } if (isLegendVisible()) out.println("legend1 = legend(axes1,{" + seriesNames + "},'Location', " + loc + ");"); out.flush(); out.close(); } } /** * Renders the current graph to a JPEG file. * * @param file * The file to export the JPEG data to. * @throws GraphException, IOException * If file cannot be written to. */ public static void exportToJPEG(File file, JFreeChart chart, int width, int height) throws GraphException, IOException { ChartUtilities.saveChartAsJPEG(file, 1.0f, chart, width, height); } public static void exportToEPS(File file, int width, int height, JFreeChart chart) throws GraphException, IOException { FileOutputStream fileOutputStream = new FileOutputStream(file); EpsGraphics g2d = new EpsGraphics(chart.getTitle().getText(), fileOutputStream, 0, 0, width, height, ColorMode.COLOR_RGB); // Don't export fonts as vectors, no hope of getting same font as publication. // g2d.setAccurateTextMode(false); // Does not rotate y-axis label. chart.draw(g2d, new Rectangle(width, height)); g2d.close(); g2d.dispose(); } /** * Renders the current graph to a JPEG file. * * @param file * The file to export the JPEG data to. * @throws GraphException, IOException * If file cannot be written to. */ public static void exportToPNG(File file, JFreeChart chart, int width, int height, boolean alpha) throws GraphException, IOException { FileOutputStream fileOutputStream = new FileOutputStream(file); KeypointPNGEncoderAdapter encoder = new KeypointPNGEncoderAdapter(); encoder.setEncodingAlpha(alpha); Paint bgPaint = chart.getBackgroundPaint(); if (alpha) { chart.setBackgroundPaint(null); } BufferedImage bufferedImage = chart.createBufferedImage(width, height, alpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB, null); if (alpha) { chart.setBackgroundPaint(bgPaint); } encoder.encode(bufferedImage, fileOutputStream); fileOutputStream.flush(); fileOutputStream.close(); //ChartUtilities.saveChartAsPNG(file, chart, width, height, null, alpha, 9); } /** * Exports the given chart to a pdf format file * @param file the file that has to be saved in pdf format * @param chart the chart that needs to be exported into pdf */ public static void exportToPDF(File file, JFreeChart chart) { PdfWriter out = null; com.itextpdf.text.Document document = new com.itextpdf.text.Document(PageSize.A4.rotate()); int width = 800, height = 500; try { out = PdfWriter.getInstance(document, new FileOutputStream(file)); document.open(); PdfContentByte contentByte = out.getDirectContent(); PdfTemplate template = contentByte.createTemplate(width, height); @SuppressWarnings("deprecation") Graphics2D graphics2d = template.createGraphics(width, height, new DefaultFontMapper()); Rectangle2D rectangle2d = new Rectangle2D.Double(0, 0, width, height); chart.draw(graphics2d, rectangle2d); graphics2d.dispose(); contentByte.addTemplate(template, 0, 0); } catch (Exception e) { JOptionPane.showMessageDialog(GUIPrism.getGUI(), e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); e.printStackTrace(); return; } document.close(); } /** * Exports the current data sets to a comma seperated value file. * * @param file * The file to export the csv data to. * @throws GraphException * If file cannont be written to. */ public void exportToCSV(File file) throws GraphException { /*try { PrintWriter out = new PrintWriter(new FileWriter(file)); // Create the header, this is a comma seperate list of the titles // of the series followed by an x or y String header = ""; synchronize(this.getSeriesLock()) { } int largestGraph = 0; // Consider every series in this model for (int i = 0; i < getNumSeries(); i++) { // While were iterating through the graphs, record the size of the largest graph if (getGraphPoints(i).getItemCount() > largestGraph) { largestGraph = getGraphPoints(i).getItemCount(); } // Add the series titles header += "[" + getGraphPoints(i).getKey() + "].x,"; header += "[" + getGraphPoints(i).getKey() + "].y"; // If this isnt the last series add the comma if (i < getNumSeries() - * 1) header += ", "; } // Print the header to the file out.println(header); // Format of the file has the data in columns, so iterate through // the points in all the graphs adding the next point from each graph // on the same line String line = ""; for (int i = 0; i < largestGraph; i++) { line = ""; // For each point, iterate through all the graphs for (int j = 0; j < getNumSeries(); j++) { XYSeries gs = getGraphPoints(j); // If the current graph doesnt have this point, ie its smaller // than the largest supply "","" for the data if (i >= gs.getItemCount()) { line += "\"\",\"\""; } else { // Otherwise add the next data point line += gs.getX(i) + ","; line += gs.getY(i); } if (j < getNumSeries() - 1) line += ", "; } out.println(line); } out.flush(); out.close(); } catch (Exception e) { throw new ChartException(e); } */ } /** * This inner class provides a means of asynchronously performing graph * updates. This has two advantages. * * Firstly, it improves the callee's performance as adding data is * effectively non-blocking. * * Secondly, it improves the interactive response of the chart, as fewer * draws are performed. According to JFreeChart's own FAQ, it is not * designed for real-time charting. */ private class GraphUpdateTask extends TimerTask { private void processGraphCache(HashMap<SeriesKey, LinkedList<PrismXYDataItem>> graphCache) { synchronized (seriesCollection) { for (Map.Entry<SeriesKey, LinkedList<PrismXYDataItem>> entry : graphCache.entrySet()) { /* The series key should map to a series. */ if (keyToSeries.containsKey(entry.getKey())) { PrismXYSeries series = (PrismXYSeries) keyToSeries.get(entry.getKey()); LinkedList<PrismXYDataItem> seriesCache = entry.getValue(); while (!seriesCache.isEmpty()) { PrismXYDataItem item = seriesCache.removeFirst(); //series.addOrUpdate(item.getX(), item.getY()); series.addOrUpdate(item); } } } } } public void run() { processGraphCache(graphCache); } } }