From 8fbd7acf2a1e2edd4e3e50bd5400d970d888096b Mon Sep 17 00:00:00 2001 From: jrandom Date: Fri, 16 Apr 2004 02:16:55 +0000 Subject: [PATCH] initial impl (public domain, yadda yadda yadda) --- apps/netmonitor/java/build.xml | 77 +++++ .../src/net/i2p/netmonitor/DataHarvester.java | 264 +++++++++++++++++ .../src/net/i2p/netmonitor/NetMonitor.java | 212 +++++++++++++ .../net/i2p/netmonitor/NetMonitorRunner.java | 145 +++++++++ .../java/src/net/i2p/netmonitor/PeerStat.java | 46 +++ .../src/net/i2p/netmonitor/PeerSummary.java | 119 ++++++++ .../net/i2p/netmonitor/PeerSummaryReader.java | 103 +++++++ .../net/i2p/netmonitor/PeerSummaryWriter.java | 80 +++++ .../i2p/netmonitor/gui/JFreeChartAdapter.java | 113 +++++++ .../gui/JFreeChartHeartbeatPlotPane.java | 58 ++++ .../src/net/i2p/netmonitor/gui/NetViewer.java | 86 ++++++ .../netmonitor/gui/NetViewerCommandBar.java | 70 +++++ .../netmonitor/gui/NetViewerControlPane.java | 107 +++++++ .../net/i2p/netmonitor/gui/NetViewerGUI.java | 104 +++++++ .../i2p/netmonitor/gui/NetViewerPlotPane.java | 50 ++++ .../i2p/netmonitor/gui/NetViewerRunner.java | 16 + .../i2p/netmonitor/gui/PeerPlotConfig.java | 218 ++++++++++++++ .../netmonitor/gui/PeerPlotConfigPane.java | 280 ++++++++++++++++++ 18 files changed, 2148 insertions(+) create mode 100644 apps/netmonitor/java/build.xml create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/DataHarvester.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitor.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerCommandBar.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerControlPane.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerGUI.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerPlotPane.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerRunner.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfig.java create mode 100644 apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfigPane.java diff --git a/apps/netmonitor/java/build.xml b/apps/netmonitor/java/build.xml new file mode 100644 index 000000000..2f4023f49 --- /dev/null +++ b/apps/netmonitor/java/build.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/DataHarvester.java b/apps/netmonitor/java/src/net/i2p/netmonitor/DataHarvester.java new file mode 100644 index 000000000..1ac63cb6b --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/DataHarvester.java @@ -0,0 +1,264 @@ +package net.i2p.netmonitor; + +import net.i2p.data.RouterInfo; +import net.i2p.util.Log; +import net.i2p.util.Clock; +import java.util.Properties; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Locale; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; + +/** + * Pull out important data from the published routerInfo and stash it away + * in the netMonitor + * + */ +class DataHarvester { + private static final Log _log = new Log(DataHarvester.class); + private static final DataHarvester _instance = new DataHarvester(); + public static final DataHarvester getInstance() { return _instance; } + + protected DataHarvester() {} + + /** + * Harvest all of the data from the peers and store it in the monitor. + * + * @param peers list of RouterInfo structures to harvest from + */ + public void harvestData(NetMonitor monitor, List peers) { + for (int i = 0; i < peers.size(); i++) { + harvestData(monitor, (RouterInfo)peers.get(i), peers); + } + } + + /** + * Pull out all the data we can for the specified peer + * + * @param peer who are we focusing on in this pass + * @param peers everyone on the network, to co + */ + private void harvestData(NetMonitor monitor, RouterInfo peer, List peers) { + _log.info("Harvest the data from " + peer.getIdentity().getHash().toBase64()); + harvestRank(monitor, peer, peers); + harvestRankAs(monitor, peer); + harvestEncryptionTime(monitor, peer); + harvestDroppedJobs(monitor, peer); + harvestProcessingTime(monitor, peer); + } + + /** + * How does the peer rank other routers? Stored in the peer summary as + * "rankAs", containing 4 longs (numFast, numReliable, numNotFailing, numFailing) + * + * @param peer who is doing the ranking + */ + private void harvestRankAs(NetMonitor monitor, RouterInfo peer) { + int numFast = 0; + int numReliable = 0; + int numNotFailing = 0; + int numFailing = 0; + + Properties props = peer.getOptions(); + for (Iterator iter = props.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + if (key.startsWith("profile.")) { + String val = (String)props.get(key); + if (val.indexOf("fastReliable") != -1) + numFast++; + else if (val.indexOf("reliable") != -1) + numReliable++; + else if (val.indexOf("notFailing") != -1) + numNotFailing++; + else if (val.indexOf("failing") != -1) + numFailing++; + } + } + + long rankAs[] = new long[4]; + rankAs[0] = numFast; + rankAs[1] = numReliable; + rankAs[2] = numNotFailing; + rankAs[3] = numFailing; + String description = "how we rank peers"; + String valDescr[] = new String[4]; + valDescr[0] = "# peers we rank as fast"; + valDescr[1] = "# peers we rank as reliable"; + valDescr[2] = "# peers we rank as not failing"; + valDescr[3] = "# peers we rank as failing"; + monitor.addData(peer.getIdentity().getHash().toBase64(), "rankAs", description, valDescr, peer.getPublished(), rankAs); + } + + /** + * How do other peers rank the current peer? Stored in the peer summary as + * "rank", containing 4 longs (numFast, numReliable, numNotFailing, numFailing) + * + * @param peer who do we want to check the network's perception of + * @param peers peers whose rankings we will use + */ + private void harvestRank(NetMonitor monitor, RouterInfo peer, List peers) { + int numFast = 0; + int numReliable = 0; + int numNotFailing = 0; + int numFailing = 0; + + // now count 'em + for (int i = 0; i < peers.size(); i++) { + RouterInfo cur = (RouterInfo)peers.get(i); + if (peer == cur) continue; + String prop = "profile." + peer.getIdentity().getHash().toBase64().replace('=', '_'); + String val = cur.getOptions().getProperty(prop); + if ( (val == null) || (val.length() <= 0) ) continue; + if (val.indexOf("fastReliable") != -1) + numFast++; + else if (val.indexOf("reliable") != -1) + numReliable++; + else if (val.indexOf("notFailing") != -1) + numNotFailing++; + else if (val.indexOf("failing") != -1) + numFailing++; + } + + long rank[] = new long[4]; + rank[0] = numFast; + rank[1] = numReliable; + rank[2] = numNotFailing; + rank[3] = numFailing; + String description = "how peers rank us"; + String valDescr[] = new String[4]; + valDescr[0] = "# peers ranking us as fast"; + valDescr[1] = "# peers ranking us as reliable"; + valDescr[2] = "# peers ranking us as not failing"; + valDescr[3] = "# peers ranking us as failing"; + // we use the current date, not the published date, since this sample doesnt come from them + monitor.addData(peer.getIdentity().getHash().toBase64(), "rank", description, valDescr, Clock.getInstance().now(), rank); + } + + /** + * How long does it take the peer to perform an elGamal encryption? Stored in + * the peer summary as "encryptTime", containing 4 doubles (numMs for 1 minute, + * quantity in the last minute, numMs for 1 hour, quantity in the last hour) + * + * @param peer who are we checking the encryption time of + */ + private void harvestEncryptionTime(NetMonitor monitor, RouterInfo peer) { + double minuteMs = getDouble(peer, "stat_crypto.elGamal.encrypt.60s", 0); + double hourMs = getDouble(peer, "stat_crypto.elGamal.encrypt.60m", 0); + double minuteQuantity = getDouble(peer, "stat_crypto.elGamal.encrypt.60s", 7); + double hourQuantity = getDouble(peer, "stat_crypto.elGamal.encrypt.60m", 7); + if ( (minuteMs == -1) || (hourMs == -1) || (minuteQuantity == -1) || (hourQuantity == -1) ) + return; + + double times[] = new double[4]; + times[0] = minuteMs; + times[1] = minuteQuantity; + times[2] = hourMs; + times[3] = hourQuantity; + + String description = "how long it takes to do an ElGamal encryption"; + String valDescr[] = new String[4]; + valDescr[0] = "encryption time avg ms (minute)"; + valDescr[1] = "# encryptions (minute)"; + valDescr[2] = "encryption time avg ms (hour)"; + valDescr[3] = "# encryptions (hour)"; + monitor.addData(peer.getIdentity().getHash().toBase64(), "encryptTime", description, valDescr, peer.getPublished(), times); + } + + /** + * How jobs has the peer dropped in the last minute / hour? Stored in + * the peer summary as "droppedJobs", containing 2 doubles (num jobs for 1 minute, + * num jobs for 1 hour) + * + * @param peer who are we checking the frequency of dropping jobs for + */ + private void harvestDroppedJobs(NetMonitor monitor, RouterInfo peer) { + double minute = getDouble(peer, "stat_jobQueue.droppedJobs.60s", 0); + double hour = getDouble(peer, "stat_jobQueue.droppedJobs.60m", 0); + double quantity[] = new double[2]; + quantity[0] = minute; + quantity[1] = hour; + if ( (minute == -1) || (hour == -1) ) + return; + + String valDescr[] = new String[2]; + valDescr[0] = "# dropped jobs (minute)"; + valDescr[1] = "# dropped jobs (hour)"; + String description = "how many dropped jobs"; + monitor.addData(peer.getIdentity().getHash().toBase64(), "droppedJobs", description, valDescr, peer.getPublished(), quantity); + } + + /** + * How long does it take to process an outbound message? Stored in + * the peer summary as "processingTime", containing 4 doubles (avg ms for 1 minute, + * num messages for 1 minute, avg ms for 1 hour, num messages for 1 hour) + * + * @param peer who are we checking the frequency of dropping jobs for + */ + private void harvestProcessingTime(NetMonitor monitor, RouterInfo peer) { + double minuteMs = getDouble(peer, "stat_transport.sendProcessingTime.60s", 0); + double minuteFreq = getDouble(peer, "stat_transport.sendProcessingTime.60s", 7); + double hourMs = getDouble(peer, "stat_transport.sendProcessingTime.60m", 0); + double hourFreq = getDouble(peer, "stat_transport.sendProcessingTime.60m", 7); + if ( (minuteMs == -1) || (hourMs == -1) || (minuteFreq == -1) || (hourFreq == -1) ) + return; + + double times[] = new double[4]; + times[0] = minuteMs; + times[1] = minuteFreq; + times[2] = hourMs; + times[3] = hourFreq; + + String valDescr[] = new String[4]; + valDescr[0] = "process time avg ms (minute)"; + valDescr[1] = "process events (minute)"; + valDescr[2] = "process time avg ms (hour)"; + valDescr[3] = "process events (hour)"; + String description = "how long does it take to process a message"; + monitor.addData(peer.getIdentity().getHash().toBase64(), "processingTime", description, valDescr, peer.getPublished(), times); + } + + /** + * Pull a value from the peer's option as a double, assuming the standard semicolon + * delimited formatting + * + * @param peer peer to query + * @param key peer option to check + * @param index 0-based index into the semicolon delimited values to pull out + * @return value, or -1 if there was an error + */ + private static final double getDouble(RouterInfo peer, String key, int index) { + String val = peer.getOptions().getProperty(key); + if (val == null) return -1; + StringTokenizer tok = new StringTokenizer(val, ";"); + for (int i = 0; i < index; i++) { + if (!tok.hasMoreTokens()) return -1; + tok.nextToken(); // ignore + } + if (!tok.hasMoreTokens()) return -1; + String cur = tok.nextToken(); + try { + return getDoubleValue(cur); + } catch (ParseException pe) { + _log.warn("Unable to parse out the double from field " + index + " out of " + val + " for " + key, pe); + return -1; + } + } + + /** this mimics the format used in the router's StatisticsManager */ + private static final DecimalFormat _numFmt = new DecimalFormat("###,###,###,###,##0.00", new DecimalFormatSymbols(Locale.UK)); + + /** + * Converts a number (double) to text + * @param val the number to convert + * @return the textual representation + */ + private static final double getDoubleValue(String val) throws ParseException { + synchronized (_numFmt) { + Number n = _numFmt.parse(val); + return n.doubleValue(); + } + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitor.java b/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitor.java new file mode 100644 index 000000000..6b9fc749a --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitor.java @@ -0,0 +1,212 @@ +package net.i2p.netmonitor; + +import net.i2p.util.Log; +import net.i2p.util.I2PThread; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Map; +import java.util.HashMap; +import java.io.IOException; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.File; + +/** + * Main driver for the app that harvests data about the performance of the network, + * building summaries for each peer that change over time.

+ * + * Usage: NetMonitor [configFilename]

+ * + * + * + */ +public class NetMonitor { + private static final Log _log = new Log(NetMonitor.class); + public static final String CONFIG_LOCATION_DEFAULT = "netmonitor.config"; + public static final String HARVEST_DELAY_PROP = "harvestDelaySeconds"; + public static final int HARVEST_DELAY_DEFAULT = 60; + public static final String EXPORT_DELAY_PROP = "exportDelaySeconds"; + public static final int EXPORT_DELAY_DEFAULT = 120; + public static final String SUMMARY_DURATION_PROP = "summaryDurationHours"; + public static final int SUMMARY_DURATION_DEFAULT = 72; + public static final String NETDB_DIR_PROP = "netDbDir"; + public static final String NETDB_DIR_DEFAULT = "netDb"; + public static final String EXPORT_DIR_PROP = "exportDir"; + public static final String EXPORT_DIR_DEFAULT = "monitorData"; + private String _configLocation; + private int _harvestDelay; + private int _exportDelay; + private String _exportDir; + private String _netDbDir; + private int _summaryDurationHours; + private boolean _isRunning; + private Map _peerSummaries; + + public NetMonitor() { + this(CONFIG_LOCATION_DEFAULT); + } + public NetMonitor(String configLocation) { + _configLocation = configLocation; + _peerSummaries = new HashMap(32); + loadConfig(); + } + + /** read and call parse */ + private void loadConfig() { + Properties props = new Properties(); + FileInputStream fis = null; + try { + fis = new FileInputStream(_configLocation); + props.load(fis); + } catch (IOException ioe) { + _log.warn("Error loading the net monitor config", ioe); + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + parseConfig(props); + } + + /** interpret the config elements and shove 'em in the vars */ + private void parseConfig(Properties props) { + String val = props.getProperty(HARVEST_DELAY_PROP, ""+HARVEST_DELAY_DEFAULT); + try { + _harvestDelay = Integer.parseInt(val); + } catch (NumberFormatException nfe) { + _log.warn("Error parsing the harvest delay [" + val + "]", nfe); + _harvestDelay = HARVEST_DELAY_DEFAULT; + } + + val = props.getProperty(EXPORT_DELAY_PROP, ""+EXPORT_DELAY_DEFAULT); + try { + _exportDelay = Integer.parseInt(val); + } catch (NumberFormatException nfe) { + _log.warn("Error parsing the export delay [" + val + "]", nfe); + _exportDelay = EXPORT_DELAY_DEFAULT; + } + + val = props.getProperty(SUMMARY_DURATION_PROP, ""+SUMMARY_DURATION_DEFAULT); + try { + _summaryDurationHours = Integer.parseInt(val); + } catch (NumberFormatException nfe) { + _log.warn("Error parsing the summary duration [" + val + "]", nfe); + _summaryDurationHours = SUMMARY_DURATION_DEFAULT; + } + + _netDbDir = props.getProperty(NETDB_DIR_PROP, NETDB_DIR_DEFAULT); + _exportDir = props.getProperty(EXPORT_DIR_PROP, EXPORT_DIR_DEFAULT); + } + + public void startMonitor() { + _isRunning = true; + I2PThread t = new I2PThread(new NetMonitorRunner(this)); + t.setName("DataHarvester"); + t.setPriority(I2PThread.MIN_PRIORITY); + t.setDaemon(false); + t.start(); + } + + public void stopMonitor() { _isRunning = false; } + public boolean isRunning() { return _isRunning; } + /** how many seconds should we wait between harvestings? */ + public int getHarvestDelay() { return _harvestDelay; } + /** how many seconds should we wait between exporting the data? */ + public int getExportDelay() { return _exportDelay; } + /** where should we export the data? */ + public String getExportDir() { return _exportDir; } + public void setExportDir(String dir) { _exportDir = dir; } + public int getSummaryDurationHours() { return _summaryDurationHours; } + /** where should we read the data from? */ + public String getNetDbDir() { return _netDbDir; } + /** + * what peers are we keeping track of? + * + * @return list of peer names (H(routerIdentity).toBase64()) + */ + public List getPeers() { + synchronized (_peerSummaries) { + return new ArrayList(_peerSummaries.keySet()); + } + } + + /** what data do we have for the peer? */ + public PeerSummary getSummary(String peer) { + synchronized (_peerSummaries) { + return (PeerSummary)_peerSummaries.get(peer); + } + } + + /** keep track of the given stat on the given peer */ + public void addData(String peer, String stat, String descr, String valDescr[], long sampleDate, double val[]) { + synchronized (_peerSummaries) { + if (!_peerSummaries.containsKey(peer)) + _peerSummaries.put(peer, new PeerSummary(peer)); + PeerSummary summary = (PeerSummary)_peerSummaries.get(peer); + summary.addData(stat, descr, valDescr, sampleDate, val); + } + } + + /** keep track of the given stat on the given peer */ + public void addData(String peer, String stat, String descr, String valDescr[], long sampleDate, long val[]) { + synchronized (_peerSummaries) { + if (!_peerSummaries.containsKey(peer)) + _peerSummaries.put(peer, new PeerSummary(peer)); + PeerSummary summary = (PeerSummary)_peerSummaries.get(peer); + summary.addData(stat, descr, valDescr, sampleDate, val); + } + } + + /** keep track of the loaded summary, overwriting any existing summary for the specified peer */ + public void addSummary(PeerSummary summary) { + synchronized (_peerSummaries) { + Object rv = _peerSummaries.put(summary.getPeer(), summary); + if (rv != summary) _log.error("Updating the peer summary changed objects! old = " + rv + " new = " + summary); + } + } + + public void importData() { + _log.debug("Running import"); + File dataDir = new File(getExportDir()); + if (!dataDir.exists()) return; + File dataFiles[] = dataDir.listFiles(new FilenameFilter() { + public boolean accept(File f, String name) { + return name.endsWith(".txt"); + } + }); + if (dataFiles == null) return; + for (int i = 0; i < dataFiles.length; i++) { + FileInputStream fis = null; + boolean delete = false; + try { + fis = new FileInputStream(dataFiles[i]); + PeerSummaryReader.getInstance().read(this, fis); + } catch (IOException ioe) { + _log.error("Error reading the data file " + dataFiles[i].getAbsolutePath(), ioe); + delete = true; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + if (delete) dataFiles[i].delete(); + } + } + _log.debug(dataFiles.length + " summaries imported"); + } + + /** drop all the old summary data */ + public void coallesceData() { + synchronized (_peerSummaries) { + for (Iterator iter = _peerSummaries.values().iterator(); iter.hasNext(); ) { + PeerSummary summary = (PeerSummary)iter.next(); + summary.coallesceData(_summaryDurationHours * 60*60*1000); + } + } + } + + public static final void main(String args[]) { + if (args.length == 1) + new NetMonitor(args[0]).startMonitor(); + else + new NetMonitor(CONFIG_LOCATION_DEFAULT).startMonitor(); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java b/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java new file mode 100644 index 000000000..5995270ad --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java @@ -0,0 +1,145 @@ +package net.i2p.netmonitor; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import java.io.IOException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; + +import net.i2p.util.Clock; +import net.i2p.util.Log; +import net.i2p.data.DataFormatException; +import net.i2p.data.RouterInfo; + +/** + * Active process that drives the monitoring by periodically rading the + * netDb dir, pumping the router data throug the data harvester, updating + * the state, and coordinating the export. + * + */ +class NetMonitorRunner implements Runnable { + private static final Log _log = new Log(NetMonitorRunner.class); + private NetMonitor _monitor; + /** + * @param monitor who do we give our data to? + */ + public NetMonitorRunner(NetMonitor monitor) { + _monitor = monitor; + } + + public void run() { + runImport(); + long now = Clock.getInstance().now(); + long nextHarvest = now; + long nextExport = now + _monitor.getExportDelay() * 1000; + while (_monitor.isRunning()) { + now = Clock.getInstance().now(); + _monitor.coallesceData(); + if (now >= nextHarvest) { + runHarvest(); + nextHarvest = now + _monitor.getHarvestDelay() * 1000; + } + if (now >= nextExport) { + runExport(); + nextExport = now + _monitor.getExportDelay() * 1000; + } + pauseHarvesting(); + } + } + + private void runHarvest() { + List routers = getRouters(); + DataHarvester.getInstance().harvestData(_monitor, routers); + } + + /** + * Fetch all of the available RouterInfo structures + * + */ + private List getRouters() { + File routers[] = listRouters(); + List rv = new ArrayList(64); + if (routers != null) { + for (int i = 0; i < routers.length; i++) { + FileInputStream fis = null; + try { + fis = new FileInputStream(routers[i]); + RouterInfo ri = new RouterInfo(); + ri.readBytes(fis); + rv.add(ri); + } catch (DataFormatException dfe) { + _log.warn("Unable to parse the routerInfo from " + routers[i].getAbsolutePath(), dfe); + } catch (IOException ioe) { + _log.warn("Unable to read the routerInfo from " + routers[i].getAbsolutePath(), ioe); + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + } + return rv; + } + + /** + * dump the data to the filesystem + */ + private void runExport() { + _log.info("Export"); + List peers = _monitor.getPeers(); + File exportDir = new File(_monitor.getExportDir()); + if (!exportDir.exists()) + exportDir.mkdirs(); + for (int i = 0; i < peers.size(); i++) { + String peerName = (String)peers.get(i); + PeerSummary summary = (PeerSummary)_monitor.getSummary(peerName); + FileOutputStream fos = null; + try { + File summaryFile = new File(exportDir, peerName + ".txt"); + fos = new FileOutputStream(summaryFile); + PeerSummaryWriter.getInstance().write(summary, fos); + _log.debug("Peer summary written to " + summaryFile.getAbsolutePath()); + } catch (IOException ioe) { + _log.error("Error exporting the peer summary for " + peerName, ioe); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + } + + /** + * Read in all the peer summaries we had previously exported, overwriting any + * existing ones in memory + * + */ + private void runImport() { + _monitor.importData(); + } + + /** + * Find all of the routers to load + * + * @return list of File objects pointing at the routers around + */ + private File[] listRouters() { + File dbDir = new File(_monitor.getNetDbDir()); + File files[] = dbDir.listFiles(new FilenameFilter() { + public boolean accept(File f, String name) { + return name.startsWith("routerInfo-"); + } + }); + return files; + } + + /** + * Wait the correct amount of time before harvesting again + * + */ + private void pauseHarvesting() { + try { + Thread.sleep(_monitor.getHarvestDelay()); + } catch (InterruptedException ie) {} + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java new file mode 100644 index 000000000..d19e31dd2 --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java @@ -0,0 +1,46 @@ +package net.i2p.netmonitor; + +/** + * Actual data point (though its not fully normalized, but KISS) + * + */ +public class PeerStat { + private String _statName; + private String _description; + private String _valueDescriptions[]; + private long _sampleDate; + private long _lvalues[]; + private double _dvalues[]; + + public PeerStat(String name, String description, String valueDescriptions[], long sampleDate, double values[]) { + this(name, description, valueDescriptions, sampleDate, null, values); + } + public PeerStat(String name, String description, String valueDescriptions[], long sampleDate, long values[]) { + this(name, description, valueDescriptions, sampleDate, values, null); + } + private PeerStat(String name, String description, String valueDescriptions[], long sampleDate, long lvalues[], double dvalues[]) { + _statName = name; + _description = description; + _valueDescriptions = valueDescriptions; + _sampleDate = sampleDate; + _lvalues = lvalues; + _dvalues = dvalues; + } + + /** unique name of the stat */ + public String getStatName() { return _statName; } + /** one line summary of the stat */ + public String getDescription() { return _description; } + /** description of each value */ + public String getValueDescription(int index) { return _valueDescriptions[index]; } + /** description of all values */ + public String[] getValueDescriptions() { return _valueDescriptions; } + /** when did the router publish the info being sampled? */ + public long getSampleDate() { return _sampleDate; } + /** if the values are integers, this contains their values */ + public long[] getLongValues() { return _lvalues; } + /** if the values are floating point numbers, this contains their values */ + public double[] getDoubleValues() { return _dvalues; } + /** are the values floating poing numbers? */ + public boolean getIsDouble() { return _dvalues != null; } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java new file mode 100644 index 000000000..8f3493cda --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java @@ -0,0 +1,119 @@ +package net.i2p.netmonitor; + +import net.i2p.util.Clock; +import net.i2p.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Set; + +/** + * coordinate the data points summarizing the performance of a particular peer + * within the network + */ +public class PeerSummary { + private static final Log _log = new Log(PeerSummary.class); + private String _peer; + /** statName to a List of PeerStat elements (sorted by sample date, earliest first) */ + private Map _stats; + /** lock on this when accessing stat data */ + private Object _coallesceLock = new Object(); + + public PeerSummary(String peer) { + _peer = peer; + _stats = new HashMap(16); + } + + /** + * Track a data point + * + * @param stat what data are we tracking? + * @param description what does this data mean? (and what are the values?) + * @param when what data set is this sample based off? + * @param val actual data harvested + */ + public void addData(String stat, String description, String valueDescriptions[], long when, double val[]) { + synchronized (_coallesceLock) { + TreeMap stats = locked_getData(stat); + stats.put(new Long(when), new PeerStat(stat, description, valueDescriptions, when, val)); + } + } + + /** + * Track a data point + * + * @param stat what data are we tracking? + * @param description what does this data mean? (and what are the values?) + * @param when what data set is this sample based off? + * @param val actual data harvested + */ + public void addData(String stat, String description, String valueDescriptions[], long when, long val[]) { + synchronized (_coallesceLock) { + TreeMap stats = locked_getData(stat); + stats.put(new Long(when), new PeerStat(stat, description, valueDescriptions, when, val)); + } + } + + /** get the peer's name (H(routerIdentity).toBase64()) */ + public String getPeer() { return _peer; } + + /** + * fetch the ordered list of PeerStat objects for the given stat (or null if + * isn't being tracked or has no data) + * + */ + public List getData(String statName) { + synchronized (_coallesceLock) { + return new ArrayList(((TreeMap)_stats.get(statName)).values()); + } + } + + /** + * Get the names of all of the stats that are being tracked + * + */ + public Set getStatNames() { + synchronized (_coallesceLock) { + return new HashSet(_stats.keySet()); + } + } + + /** drop old data points */ + public void coallesceData(long summaryDurationMs) { + long earliest = Clock.getInstance().now() - summaryDurationMs; + synchronized (_coallesceLock) { + locked_coallesce(earliest); + } + } + + /** go through all the stats and remove ones from before the given date */ + private void locked_coallesce(long earliestSampleDate) { + if (true) return; + for (Iterator iter = _stats.keySet().iterator(); iter.hasNext(); ) { + String statName = (String)iter.next(); + TreeMap stats = (TreeMap)_stats.get(statName); + while (stats.size() > 0) { + Long when = (Long)stats.keySet().iterator().next(); + if (when.longValue() < earliestSampleDate) { + stats.remove(when); + } else { + break; + } + } + } + } + + /** + * @return PeerStat elements, ordered by sample date (earliest first) + */ + private TreeMap locked_getData(String statName) { + if (!_stats.containsKey(statName)) + _stats.put(statName, new TreeMap()); + return (TreeMap)_stats.get(statName); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java new file mode 100644 index 000000000..d1cf2042a --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java @@ -0,0 +1,103 @@ +package net.i2p.netmonitor; + +import net.i2p.util.Log; +import java.io.IOException; +import java.io.InputStream; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.TreeSet; +import java.util.Set; +import java.util.Locale; +import java.util.Date; +import java.util.StringTokenizer; +import java.text.SimpleDateFormat; +import java.text.ParseException; + +/** + * Load up the peer summary + * + */ +class PeerSummaryReader { + private static final Log _log = new Log(PeerSummaryReader.class); + private static final PeerSummaryReader _instance = new PeerSummaryReader(); + public static final PeerSummaryReader getInstance() { return _instance; } + private PeerSummaryReader() {} + + /** */ + public void read(NetMonitor monitor, InputStream in) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + String line = null; + PeerSummary summary = null; + String curDescription = null; + List curArgs = null; + while ((line = reader.readLine()) != null) { + if (line.startsWith("peer\t")) { + String name = line.substring("peer\t".length()).trim(); + summary = monitor.getSummary(name); + if (summary == null) + summary = new PeerSummary(name); + } else if (line.startsWith("## ")) { + curDescription = line.substring("## ".length()).trim(); + curArgs = new ArrayList(4); + } else if (line.startsWith("# param ")) { + int start = line.indexOf(':'); + String arg = line.substring(start+1).trim(); + curArgs.add(arg); + } else { + StringTokenizer tok = new StringTokenizer(line); + String name = tok.nextToken(); + try { + long when = getTime(tok.nextToken()); + boolean isDouble = false; + List argVals = new ArrayList(curArgs.size()); + while (tok.hasMoreTokens()) { + String val = (String)tok.nextToken(); + if (val.indexOf('.') >= 0) { + argVals.add(new Double(val)); + isDouble = true; + } else { + argVals.add(new Long(val)); + } + } + String valDescriptions[] = new String[curArgs.size()]; + for (int i = 0; i < curArgs.size(); i++) + valDescriptions[i] = (String)curArgs.get(i); + if (isDouble) { + double values[] = new double[argVals.size()]; + for (int i = 0; i < argVals.size(); i++) + values[i] = ((Double)argVals.get(i)).doubleValue(); + summary.addData(name, curDescription, valDescriptions, when, values); + } else { + long values[] = new long[argVals.size()]; + for (int i = 0; i < argVals.size(); i++) + values[i] = ((Long)argVals.get(i)).longValue(); + summary.addData(name, curDescription, valDescriptions, when, values); + } + } catch (ParseException pe) { + _log.error("Error parsing the data line [" + line + "]", pe); + } catch (NumberFormatException nfe) { + _log.error("Error parsing the data line [" + line + "]", nfe); + } + } + } + + summary.coallesceData(monitor.getSummaryDurationHours() * 60*60*1000); + monitor.addSummary(summary); + } + + private static final SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd.HH:mm:ss.SSS", Locale.UK); + + /** + * Converts a time (long) to text + * @param when the time to convert + * @return the textual representation + */ + public long getTime(String when) throws ParseException { + synchronized (_fmt) { + return _fmt.parse(when).getTime(); + } + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java new file mode 100644 index 000000000..be9b9bbbe --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java @@ -0,0 +1,80 @@ +package net.i2p.netmonitor; + +import net.i2p.util.Log; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.List; +import java.util.TreeSet; +import java.util.Set; +import java.util.Locale; +import java.util.Date; +import java.text.SimpleDateFormat; + +/** + * Dump various peer summaries to disk (so external apps (or good ol' vi) can + * peek into what we're harvesting + * + */ +class PeerSummaryWriter { + private static final Log _log = new Log(PeerSummaryWriter.class); + private static final PeerSummaryWriter _instance = new PeerSummaryWriter(); + public static final PeerSummaryWriter getInstance() { return _instance; } + private PeerSummaryWriter() {} + + /** write out the peer summary to the stream specified */ + public void write(PeerSummary summary, OutputStream out) throws IOException { + StringBuffer buf = new StringBuffer(4*1024); + buf.append("peer\t").append(summary.getPeer()).append('\n'); + TreeSet names = new TreeSet(summary.getStatNames()); + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + String statName = (String)iter.next(); + List stats = summary.getData(statName); + for (int i = 0; i < stats.size(); i++) { + PeerStat stat = (PeerStat)stats.get(i); + if (i == 0) { + buf.append("## ").append(stat.getDescription()).append('\n'); + String descr[] = stat.getValueDescriptions(); + if (descr != null) { + for (int j = 0; j < descr.length; j++) + buf.append("# param ").append(j).append(": ").append(descr[j]).append('\n'); + } + } + buf.append(statName).append('\t'); + buf.append(getTime(stat.getSampleDate())).append('\t'); + + if (stat.getIsDouble()) { + double vals[] = stat.getDoubleValues(); + if (vals != null) { + for (int j = 0; j < vals.length; j++) + buf.append(vals[j]).append('\t'); + } + } else { + long vals[] = stat.getLongValues(); + if (vals != null) { + for (int j = 0; j < vals.length; j++) + buf.append(vals[j]).append('\t'); + } + } + buf.append('\n'); + } + } + String data = buf.toString(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Stat: \n" + data); + out.write(data.getBytes()); + } + + private static final SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd.HH:mm:ss.SSS", Locale.UK); + + /** + * Converts a time (long) to text + * @param when the time to convert + * @return the textual representation + */ + public String getTime(long when) { + synchronized (_fmt) { + return _fmt.format(new Date(when)); + } + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java new file mode 100644 index 000000000..a9fbebebb --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java @@ -0,0 +1,113 @@ +package net.i2p.netmonitor.gui; + +import net.i2p.netmonitor.PeerSummary; +import net.i2p.netmonitor.PeerStat; +import net.i2p.util.Log; + +import org.jfree.data.XYSeries; +import org.jfree.data.XYSeriesCollection; +import org.jfree.data.MovingAverage; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.axis.DateAxis; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.renderer.XYLineAndShapeRenderer; +import org.jfree.chart.renderer.XYItemRenderer; +import org.jfree.chart.renderer.XYDotRenderer; +import java.util.List; +import java.util.Set; +import java.util.Iterator; + +import javax.swing.JPanel; +import java.awt.Font; +import java.awt.Color; + +class JFreeChartAdapter { + private final static Log _log = new Log(JFreeChartAdapter.class); + private final static Color WHITE = new Color(255, 255, 255); + + ChartPanel createPanel(NetViewer state) { + ChartPanel panel = new ChartPanel(createChart(state)); + panel.setDisplayToolTips(true); + panel.setEnforceFileExtensions(true); + panel.setHorizontalZoom(true); + panel.setVerticalZoom(true); + panel.setMouseZoomable(true, true); + panel.getChart().setBackgroundPaint(WHITE); + return panel; + } + + JFreeChart createChart(NetViewer state) { + Plot plot = createPlot(state); + JFreeChart chart = new JFreeChart("I2P network performance", Font.getFont("arial"), plot, true); + return chart; + } + + void updateChart(ChartPanel panel, NetViewer state) { + XYPlot plot = (XYPlot)panel.getChart().getPlot(); + plot.setDataset(getCollection(state)); + } + + Plot createPlot(NetViewer state) { + XYItemRenderer renderer = new XYLineAndShapeRenderer(); // new XYDotRenderer(); // + XYPlot plot = new XYPlot(getCollection(state), new DateAxis(), new NumberAxis("val"), renderer); + return plot; + } + + XYSeriesCollection getCollection(NetViewer state) { + XYSeriesCollection col = new XYSeriesCollection(); + if (state != null) { + List names = state.getConfigNames(); + for (int i = 0; i < names.size(); i++) { + addPeer(col, state.getConfig((String)names.get(i))); + } + } else { + XYSeries series = new XYSeries("latency", false, false); + series.add(System.currentTimeMillis(), 0); + col.addSeries(series); + } + return col; + } + + void addPeer(XYSeriesCollection col, PeerPlotConfig config) { + Set statNames = config.getSummary().getStatNames(); + for (Iterator iter = statNames.iterator(); iter.hasNext(); ) { + String statName = (String)iter.next(); + List statData = config.getSummary().getData(statName); + if (statData.size() > 0) { + PeerStat data = (PeerStat)statData.get(0); + String args[] = data.getValueDescriptions(); + if (args != null) { + for (int i = 0; i < args.length; i++) { + if (config.getSeriesConfig().getShouldPlotValue(statName, args[i], false)) + addLine(col, config.getPeerName(), statName, args[i], statData); + } + } + } + } + } + + /** + */ + void addLine(XYSeriesCollection col, String peer, String statName, String argName, List dataPoints) { + XYSeries series = new XYSeries(peer.substring(0, 4) + ": " + argName, false, false); + for (int i = 0; i < dataPoints.size(); i++) { + PeerStat data = (PeerStat)dataPoints.get(i); + String argNames[] = data.getValueDescriptions(); + int argIndex = -1; + for (int j = 0; j < argNames.length; j++) { + if (argNames[j].equals(argName)) { + argIndex = j; + break; + } + } + if (data.getIsDouble()) + series.add(data.getSampleDate(), data.getDoubleValues()[argIndex]); + else + series.add(data.getSampleDate(), data.getLongValues()[argIndex]); + } + col.addSeries(series); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java new file mode 100644 index 000000000..bfe864ac8 --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java @@ -0,0 +1,58 @@ +package net.i2p.netmonitor.gui; + +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JScrollPane; +import javax.swing.JLabel; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import net.i2p.util.Log; + +import org.jfree.chart.ChartPanel; + +/** + * Render the graph and legend + * + */ +class JFreeChartHeartbeatPlotPane extends NetViewerPlotPane { + private final static Log _log = new Log(JFreeChartHeartbeatPlotPane.class); + private ChartPanel _panel; + private JFreeChartAdapter _adapter; + + public JFreeChartHeartbeatPlotPane(NetViewerGUI gui) { + super(gui); + } + + public void stateUpdated() { + if (_panel == null) { + remove(0); // remove the dummy + + _adapter = new JFreeChartAdapter(); + _panel = _adapter.createPanel(_gui.getViewer()); + _panel.setBackground(_gui.getBackground()); + JScrollPane pane = new JScrollPane(_panel); + pane.setBackground(_gui.getBackground()); + add(pane, BorderLayout.CENTER); + } else { + _adapter.updateChart(_panel, _gui.getViewer()); + //_gui.pack(); + } + } + + protected void initializeComponents() { + // noop + setLayout(new BorderLayout()); + add(new JLabel(), BorderLayout.CENTER); + //dummy.setBackground(_gui.getBackground()); + //dummy.setPreferredSize(new Dimension(800,600)); + //add(dummy); + + //add(_panel); + + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java new file mode 100644 index 000000000..1f90057fa --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java @@ -0,0 +1,86 @@ +package net.i2p.netmonitor.gui; + +import net.i2p.netmonitor.NetMonitor; +import net.i2p.netmonitor.PeerSummary; +import net.i2p.netmonitor.PeerStat; +import net.i2p.util.Log; +import net.i2p.util.I2PThread; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * Coordinate the visualization of the network monitor.

+ * + * Usage: NetViewer [exportDir]
+ * (exportDir is where the NetMonitor exports its state, "monitorData" by default) + */ +public class NetViewer { + private static final Log _log = new Log(NetViewer.class); + private NetMonitor _monitor; + private NetViewerGUI _gui; + private Map _plotConfigs; + private boolean _isClosed; + + public NetViewer() { + this(NetMonitor.EXPORT_DIR_DEFAULT); + } + public NetViewer(String dataDir) { + _monitor = new NetMonitor(); + _monitor.setExportDir(dataDir); + _isClosed = false; + _gui = new NetViewerGUI(this); + _plotConfigs = new HashMap(); + } + + public void runViewer() { + I2PThread t = new I2PThread(new NetViewerRunner(this)); + t.setName("NetViewer"); + t.setDaemon(false); + t.start(); + } + + void refreshGUI() { + _gui.stateUpdated(); + } + + void reloadData() { + _log.debug("Reloading data"); + _monitor.importData(); + refreshGUI(); + } + + public void addConfig(String peerName, PeerPlotConfig config) { + synchronized (_plotConfigs) { + _plotConfigs.put(peerName, config); + } + } + + public PeerPlotConfig getConfig(String peerName) { + synchronized (_plotConfigs) { + return (PeerPlotConfig)_plotConfigs.get(peerName); + } + } + + public List getConfigNames() { + synchronized (_plotConfigs) { + return new ArrayList(_plotConfigs.keySet()); + } + } + + public NetMonitor getMonitor() { return _monitor; } + + long getDataLoadDelay() { return _monitor.getExportDelay(); } + + /** has the viewer been closed? */ + public boolean getIsClosed() { return _isClosed; } + public void setIsClosed(boolean closed) { _isClosed = closed; } + + public static final void main(String args[]) { + if (args.length == 1) + new NetViewer(args[0]).runViewer(); + else + new NetViewer().runViewer(); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerCommandBar.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerCommandBar.java new file mode 100644 index 000000000..b1b0b923b --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerCommandBar.java @@ -0,0 +1,70 @@ +package net.i2p.netmonitor.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +class NetViewerCommandBar extends JPanel { + private NetViewerGUI _gui; + private JComboBox _refreshRate; + private JTextField _location; + + /** + * Constructs a command bar onto the gui + * @param gui the gui the command bar is associated with + */ + public NetViewerCommandBar(NetViewerGUI gui) { + _gui = gui; + initializeComponents(); + } + + private void refreshChanged(ItemEvent evt) {} + private void loadCalled() { + //_gui.getMonitor().load(_location.getText()); + } + + private void browseCalled() { + JFileChooser chooser = new JFileChooser(_location.getText()); + chooser.setBackground(_gui.getBackground()); + chooser.setMultiSelectionEnabled(false); + int rv = chooser.showDialog(this, "Load"); + //if (rv == JFileChooser.APPROVE_OPTION) + // _gui.getMonitor().load(chooser.getSelectedFile().getAbsolutePath()); + } + + private void initializeComponents() { + _refreshRate = new JComboBox(new DefaultComboBoxModel(new Object[] {"10 second refresh", "30 second refresh", "1 minute refresh", "5 minute refresh"})); + _refreshRate.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent evt) { refreshChanged(evt); } }); + _refreshRate.setEnabled(false); + _refreshRate.setBackground(_gui.getBackground()); + //add(_refreshRate); + JLabel loadLabel = new JLabel("Load from: "); + loadLabel.setBackground(_gui.getBackground()); + add(loadLabel); + _location = new JTextField(20); + _location.setToolTipText("Either specify a local filename or a fully qualified URL"); + _location.setBackground(_gui.getBackground()); + _location.setEnabled(false); + add(_location); + JButton browse = new JButton("Browse..."); + browse.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { browseCalled(); } }); + browse.setBackground(_gui.getBackground()); + browse.setEnabled(false); + add(browse); + JButton load = new JButton("Load"); + load.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { loadCalled(); } }); + load.setBackground(_gui.getBackground()); + load.setEnabled(false); + add(load); + setBackground(_gui.getBackground()); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerControlPane.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerControlPane.java new file mode 100644 index 000000000..286662031 --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerControlPane.java @@ -0,0 +1,107 @@ +package net.i2p.netmonitor.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.util.ArrayList; +import java.util.List; + +import java.awt.Dimension; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; + +import net.i2p.util.Log; + +/** + * Render the control widgets (refresh/load/snapshot and the + * tabbed panel with the plot config data) + * + */ +class NetViewerControlPane extends JPanel { + private final static Log _log = new Log(NetViewerControlPane.class); + private NetViewerGUI _gui; + private JTabbedPane _configPane; + private final static Color WHITE = new Color(255, 255, 255); + private final static Color LIGHT_BLUE = new Color(180, 180, 255); + private final static Color BLACK = new Color(0, 0, 0); + private Color _background = WHITE; + private Color _foreground = BLACK; + + /** + * Constructs a control panel onto the gui + * @param gui the gui the panel is associated with + */ + public NetViewerControlPane(NetViewerGUI gui) { + _gui = gui; + initializeComponents(); + } + + /** the settings have changed - revise */ + void refreshView() { + _gui.refreshView(); + } + + /** + * Callback: when tests have changed + */ + public synchronized void stateUpdated() { + List knownNames = new ArrayList(8); + List peers = _gui.getViewer().getMonitor().getPeers(); + _log.debug("GUI updated with peers: " + peers); + for (int i = 0; i < peers.size(); i++) { + String name = (String)peers.get(i); + String shortName = name.substring(0,4); + knownNames.add(shortName); + if (_configPane.indexOfTab(shortName) >= 0) { + JScrollPane pane = (JScrollPane)_configPane.getComponentAt(_configPane.indexOfTab(shortName)); + PeerPlotConfigPane cfgPane = (PeerPlotConfigPane)pane.getViewport().getView(); + cfgPane.stateUpdated(); + _log.debug("We already know about [" + name + "]"); + } else { + _log.info("The peer [" + name + "] is new to us"); + PeerPlotConfig cfg = new PeerPlotConfig(_gui.getViewer().getMonitor().getSummary(name)); + _gui.getViewer().addConfig(name, cfg); + PeerPlotConfigPane pane = new PeerPlotConfigPane(cfg, this); + JScrollPane p = new JScrollPane(pane); + p.setBackground(_background); + _configPane.addTab(shortName, null, p, "Peer " + name); + _configPane.setBackgroundAt(_configPane.getTabCount()-1, _background); + _configPane.setForegroundAt(_configPane.getTabCount()-1, _foreground); + } + } + List toRemove = new ArrayList(4); + for (int i = 0; i < _configPane.getTabCount(); i++) { + if (knownNames.contains(_configPane.getTitleAt(i))) { + // noop + } else { + toRemove.add(_configPane.getTitleAt(i)); + } + } + for (int i = 0; i < toRemove.size(); i++) { + String title = (String)toRemove.get(i); + _log.info("Removing peer [" + title + "]"); + _configPane.removeTabAt(_configPane.indexOfTab(title)); + } + } + + private void initializeComponents() { + if (_gui != null) + setBackground(_gui.getBackground()); + else + setBackground(_background); + setLayout(new BorderLayout()); + NetViewerCommandBar bar = new NetViewerCommandBar(_gui); + bar.setBackground(getBackground()); + add(bar, BorderLayout.NORTH); + _configPane = new JTabbedPane(JTabbedPane.LEFT); + _configPane.setBackground(_background); + JScrollPane pane = new JScrollPane(_configPane); + pane.setBackground(_background); + add(pane, BorderLayout.CENTER); + //setPreferredSize(new Dimension(800, 400)); + //setMinimumSize(new Dimension(800, 400)); + //setMaximumSize(new Dimension(800, 400)); + } + + NetViewer getViewer() { return _gui.getViewer(); } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerGUI.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerGUI.java new file mode 100644 index 000000000..19650def8 --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerGUI.java @@ -0,0 +1,104 @@ +package net.i2p.netmonitor.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; + +class NetViewerGUI extends JFrame { + private NetViewer _viewer; + private NetViewerPlotPane _plotPane; + private NetViewerControlPane _controlPane; + private final static Color WHITE = new Color(255, 255, 255); + private Color _background = WHITE; + + /** + * Creates the GUI for all youz who be too shoopid for text based shitz + * @param monitor the monitor the gui operates over + */ + public NetViewerGUI(NetViewer viewer) { + super("Network Viewer"); + _viewer = viewer; + initializeComponents(); + pack(); + //setResizable(false); + setVisible(true); + } + + NetViewer getViewer() { return _viewer; } + + /** build up all our widgets */ + private void initializeComponents() { + getContentPane().setLayout(new BorderLayout()); + + setBackground(_background); + + _plotPane = new JFreeChartHeartbeatPlotPane(this); // // new NetViewerPlotPane(this); // + _plotPane.setBackground(_background); + JScrollPane pane = new JScrollPane(_plotPane); + pane.setBackground(_background); + //getContentPane().add(pane, BorderLayout.CENTER); + + _controlPane = new NetViewerControlPane(this); + _controlPane.setBackground(_background); + //getContentPane().add(_controlPane, BorderLayout.SOUTH); + + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, pane, new JScrollPane(_controlPane)); + getContentPane().add(split, BorderLayout.CENTER); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + initializeMenus(); + } + + /** + * Callback: when the state of the world changes . . . + */ + public void stateUpdated() { + _controlPane.stateUpdated(); + _plotPane.stateUpdated(); + pack(); + } + + public void refreshView() { + _plotPane.stateUpdated(); + } + + private void exitCalled() { + _viewer.setIsClosed(true); + setVisible(false); + System.exit(0); + } + private void loadConfigCalled() {} + private void saveConfigCalled() {} + private void loadSnapshotCalled() {} + private void saveSnapshotCalled() {} + + private void initializeMenus() { + JMenuBar bar = new JMenuBar(); + JMenu fileMenu = new JMenu("File"); + JMenuItem loadConfig = new JMenuItem("Load config"); + loadConfig.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { loadConfigCalled(); } }); + JMenuItem saveConfig = new JMenuItem("Save config"); + saveConfig.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { saveConfigCalled(); } }); + JMenuItem saveSnapshot = new JMenuItem("Save snapshot"); + saveSnapshot.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { saveSnapshotCalled(); } }); + JMenuItem loadSnapshot = new JMenuItem("Load snapshot"); + loadSnapshot.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { loadSnapshotCalled(); } }); + JMenuItem exit = new JMenuItem("Exit"); + exit.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { exitCalled(); } }); + + fileMenu.add(loadConfig); + fileMenu.add(saveConfig); + fileMenu.add(loadSnapshot); + fileMenu.add(saveSnapshot); + fileMenu.add(exit); + bar.add(fileMenu); + setJMenuBar(bar); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerPlotPane.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerPlotPane.java new file mode 100644 index 000000000..fa52bf8f1 --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerPlotPane.java @@ -0,0 +1,50 @@ +package net.i2p.netmonitor.gui; + +import java.awt.Color; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.swing.JPanel; +import javax.swing.JTextArea; + +import net.i2p.util.Log; + +/** + * Render the graph and legend + */ +class NetViewerPlotPane extends JPanel { + private final static Log _log = new Log(NetViewerPlotPane.class); + protected NetViewerGUI _gui; + private JTextArea _text; + + /** + * Constructs the plot pane + * @param gui the gui the pane is attached to + */ + public NetViewerPlotPane(NetViewerGUI gui) { + _gui = gui; + initializeComponents(); + } + + /** + * Callback: when things change . . . + */ + public void stateUpdated() { + StringBuffer buf = new StringBuffer(32*1024); + buf.append("moo"); + _text.setText(buf.toString()); + } + + protected void initializeComponents() { + setBackground(_gui.getBackground()); + //Dimension size = new Dimension(800, 600); + _text = new JTextArea("",30,80); // 16, 60); + _text.setAutoscrolls(true); + _text.setEditable(false); + // _text.setLineWrap(true); + // add(new JScrollPane(_text)); + add(_text); + // add(new JScrollPane(_text, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS)); + // setPreferredSize(size); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerRunner.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerRunner.java new file mode 100644 index 000000000..2917e14ee --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewerRunner.java @@ -0,0 +1,16 @@ +package net.i2p.netmonitor.gui; + +class NetViewerRunner implements Runnable { + private NetViewer _viewer; + + public NetViewerRunner(NetViewer viewer) { + _viewer = viewer; + } + + public void run() { + while (!_viewer.getIsClosed()) { + _viewer.reloadData(); + try { Thread.sleep(_viewer.getDataLoadDelay()*1000); } catch (InterruptedException ie) {} + } + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfig.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfig.java new file mode 100644 index 000000000..a255ade3c --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfig.java @@ -0,0 +1,218 @@ +package net.i2p.netmonitor.gui; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map; +import java.util.HashMap; + +import net.i2p.data.Destination; +import net.i2p.util.Log; +import net.i2p.netmonitor.PeerSummary; + +/** + * Configure how we want to render a particular peerSummary in the GUI + */ +class PeerPlotConfig { + private final static Log _log = new Log(PeerPlotConfig.class); + /** where can we find the current state/data (either as a filename or a URL)? */ + private String _location; + /** what test are we defining the plot data for? */ + private PeerSummary _summary; + /** how should we render the current data set? */ + private PlotSeriesConfig _seriesConfig; + private Set _listeners; + private boolean _disabled; + + /** + * Delegating constructor . . . + * @param location the name of the file/URL to get the data from + */ + public PeerPlotConfig(String location) { + this(location, null, null); + } + + /** + * Delegating constructor . . . + * @param location the name of the file/URL to get the data from + */ + public PeerPlotConfig(PeerSummary summary) { + this(null, summary, null); + } + + /** + * Constructs a config =) + * @param location the location of the file/URL to get the data from + * @param config the client's configuration + * @param seriesConfig the series config + */ + public PeerPlotConfig(String location, PeerSummary summary, PlotSeriesConfig seriesConfig) { + _location = location; + _summary = summary; + if (seriesConfig != null) + _seriesConfig = seriesConfig; + else + _seriesConfig = new PlotSeriesConfig(); + + _listeners = Collections.synchronizedSet(new HashSet(2)); + _disabled = false; + } + + /** + * Where is the current state data supposed to be found? This must either be a + * local file path or a URL + * @return the current location + */ + public String getLocation() { return _location; } + + /** + * The location the current state data is supposed to be found. This must either be + * a local file path or a URL + * @param location the location + */ + public void setLocation(String location) { + _location = location; + fireUpdate(); + } + + /** + * What are we configuring? + * @return the client configuration + */ + public PeerSummary getSummary() { return _summary; } + + /** + * Sets what we are currently configuring + * @param config the new config + */ + public void setPeerSummary(PeerSummary summary) { + _summary = summary; + fireUpdate(); + } + + /** + * How do we want to render the current data set? + * @return the way we currently render the data + */ + public PlotSeriesConfig getSeriesConfig() { return _seriesConfig; } + + /** + * Sets how we want to render the current data set. + * @param config the new config + */ + public void setSeriesConfig(PlotSeriesConfig config) { + _seriesConfig = config; + fireUpdate(); + } + + /** + * four char description of the peer + * @return the name + */ + public String getPeerName() { return _summary.getPeer(); } + + /** + * we've got someone who wants to be notified of changes to the plot config + * @param lsnr the listener to be added + */ + public void addListener(UpdateListener lsnr) { _listeners.add(lsnr); } + + /** + * remove a listener + * @param lsnr the listener to remove + */ + public void removeListener(UpdateListener lsnr) { _listeners.remove(lsnr); } + + void fireUpdate() { + if (_disabled) return; + for (Iterator iter = _listeners.iterator(); iter.hasNext(); ) { + ((UpdateListener)iter.next()).configUpdated(this); + } + } + + /** + * Disables notification of events listeners + * @see PeerPlotConfig#fireUpdate() + */ + public void disableEvents() { _disabled = true; } + + /** + * Enables notification of events listeners + * @see PeerPlotConfig#fireUpdate() + */ + public void enableEvents() { _disabled = false; } + + /** + * How do we want to render a particular dataset (either the current or the averaged values)? + */ + public class PlotSeriesConfig { + private Map _shouldPlot; + private Map _plotColors; + + /** + * Creates a config for the rendering of a particular dataset) + * @param plotLost do we plot lost packets? + * @param plotColor in what color? + */ + public PlotSeriesConfig() { + _shouldPlot = new HashMap(16); + _plotColors = new HashMap(16); + } + + /** + * Retrieves the plot config this plot series config is a part of + * @return the plot config + */ + public PeerPlotConfig getPlotConfig() { return PeerPlotConfig.this; } + + public boolean getShouldPlotValue(String statName, String argName, boolean defaultVal) { + Boolean val = (Boolean)_shouldPlot.get(statName + argName); + if (val == null) + return defaultVal; + else + return val.booleanValue(); + } + + public void setShouldPlotValue(String statName, String argName, boolean shouldPlot) { + _shouldPlot.put(statName + argName, new Boolean(shouldPlot)); + fireUpdate(); + } + + /** + * What color should we plot the data with? + * @return the color + */ + public Color getPlotLineColor(String statName, String argName) { + return (Color)_plotColors.get(statName + argName); + } + + /** + * Sets the color we should plot the data with + * @param color the color to use + */ + public void setPlotLineColor(String statName, String argName, Color color) { + if (color == null) + _plotColors.remove(statName + argName); + else + _plotColors.put(statName + argName, color); + fireUpdate(); + } + } + + /** + * An interface for listening to updates . . . + */ + public interface UpdateListener { + /** + * @param config the peer plot config that changes + * @see PeerPlotConfig#fireUpdate() + */ + void configUpdated(PeerPlotConfig config); + } +} \ No newline at end of file diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfigPane.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfigPane.java new file mode 100644 index 000000000..e0430005d --- /dev/null +++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/PeerPlotConfigPane.java @@ -0,0 +1,280 @@ +package net.i2p.netmonitor.gui; + +import java.awt.Color; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.TreeMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.Iterator; +import java.util.Random; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import net.i2p.util.Log; +import net.i2p.netmonitor.PeerStat; +import net.i2p.netmonitor.PeerSummary; + +class PeerPlotConfigPane extends JPanel implements PeerPlotConfig.UpdateListener { + private final static Log _log = new Log(PeerPlotConfigPane.class); + private PeerPlotConfig _config; + private NetViewerControlPane _parent; + private JLabel _peer; + private JButton _toggleAll; + private OptionGroup _options[]; + private Random _rnd = new Random(); + private final static Color WHITE = new Color(255, 255, 255); + private Color _background = WHITE; + + /** + * Constructs a pane + * @param config the plot config it represents + * @param pane the pane this one is attached to + */ + public PeerPlotConfigPane(PeerPlotConfig config, NetViewerControlPane pane) { + _config = config; + _parent = pane; + if (_parent != null) + _background = _parent.getBackground(); + _config.addListener(this); + initializeComponents(); + } + + private void initializeComponents() { + buildComponents(); + placeComponents(this); + refreshView(); + //setBorder(new BevelBorder(BevelBorder.RAISED)); + setBackground(_background); + } + + /** + * place all the gui components onto the given panel + * @param body the panel to place the components on + */ + private void placeComponents(JPanel body) { + body.setLayout(new GridBagLayout()); + GridBagConstraints cts = new GridBagConstraints(); + + // row 0: peer name + toggle All + cts.gridx = 0; + cts.gridy = 0; + cts.gridwidth = 2; + cts.anchor = GridBagConstraints.WEST; + cts.fill = GridBagConstraints.NONE; + body.add(_peer, cts); + cts.gridx = 2; + cts.gridy = 0; + cts.gridwidth = 1; + cts.anchor = GridBagConstraints.EAST; + cts.fill = GridBagConstraints.NONE; + body.add(_toggleAll, cts); + + int row = 0; + for (int i = 0; i < _options.length; i++) { + row++; + cts.gridx = 0; + cts.gridy = row; + cts.gridwidth = 3; + cts.weightx = 5; + cts.fill = GridBagConstraints.BOTH; + cts.anchor = GridBagConstraints.WEST; + body.add(_options[i]._statName, cts); + + for (int j = 0; j < _options[i]._statAttributes.size(); j++) { + row++; + cts.gridx = 0; + cts.gridy = row; + cts.gridwidth = 1; + cts.weightx = 1; + cts.fill = GridBagConstraints.NONE; + body.add(new JLabel(" "), cts); + cts.gridx = 1; + cts.gridy = row; + cts.gridwidth = 1; + cts.weightx = 5; + cts.fill = GridBagConstraints.BOTH; + JCheckBox box = (JCheckBox)_options[i]._statAttributes.get(j); + box.setBackground(_background); + body.add(box, cts); + cts.gridx = 2; + cts.fill = GridBagConstraints.NONE; + cts.anchor = GridBagConstraints.EAST; + JButton toggleAttr = new JButton("Toggle all"); + toggleAttr.setBackground(_background); + toggleAttr.addActionListener(new ToggleAttribute(_options[i].getStatName(), box.getText(), box)); + body.add(toggleAttr, cts); + } + } + } + + private class ToggleAttribute implements ActionListener { + private String _statName; + private String _attrName; + private JCheckBox _box; + public ToggleAttribute(String statName, String attrName, JCheckBox box) { + _statName = statName; + _attrName = attrName; + _box = box; + } + public void actionPerformed(ActionEvent evt) { + boolean setActive = true; + if (_box.isSelected()) { + setActive = false; + } + + List names = _parent.getViewer().getConfigNames(); + for (int i = 0; i < names.size(); i++) { + String name = (String)names.get(i); + PeerPlotConfig cfg = _parent.getViewer().getConfig(name); + cfg.getSeriesConfig().setShouldPlotValue(_statName, _attrName, setActive); + _log.debug("Setting " + _statName + "." + _attrName + " to " + setActive + " for " + name); + } + _parent.stateUpdated(); + _parent.refreshView(); + } + } + + /** build all of the gui components */ + private void buildComponents() { + _peer = new JLabel("Router: " + _config.getPeerName()); + + _toggleAll = new JButton("Show all"); + _toggleAll.setBackground(_background); + _toggleAll.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + _config.disableEvents(); + for (int i = 0; i < _options.length; i++) { + for (int j = 0; j < _options[i]._statAttributes.size(); j++) { + JCheckBox box = (JCheckBox)_options[i]._statAttributes.get(j); + String statName = _options[i].getStatName(); + String attrName = box.getText(); + if (_toggleAll.getText().equals("Show all")) { + box.setSelected(true); + _config.getSeriesConfig().setShouldPlotValue(statName, attrName, true); + } else { + box.setSelected(false); + _config.getSeriesConfig().setShouldPlotValue(statName, attrName, false); + } + } + } + if (_toggleAll.getText().equals("Show all")) + _toggleAll.setText("Hide all"); + else + _toggleAll.setText("Show all"); + _config.enableEvents(); + _config.fireUpdate(); + } + }); + + Set statNames = _config.getSummary().getStatNames(); + List options = new ArrayList(statNames.size()); + for (Iterator iter = statNames.iterator(); iter.hasNext(); ) { + String statName = (String)iter.next(); + List data = _config.getSummary().getData(statName); + if ( (data != null) && (data.size() > 0) ) { + PeerStat stat = (PeerStat)data.get(0); + String attributes[] = stat.getValueDescriptions(); + OptionGroup group = new OptionGroup(statName, attributes); + options.add(group); + } + } + TreeMap orderedOptions = new TreeMap(); + for (int i = 0; i < options.size(); i++) { + OptionGroup grp = (OptionGroup)options.get(i); + orderedOptions.put(grp.getStatName(), grp); + } + _options = new OptionGroup[options.size()]; + int i = 0; + for (Iterator iter = orderedOptions.values().iterator(); iter.hasNext(); ) { + OptionGroup grp = (OptionGroup)iter.next(); + _options[i] = grp; + i++; + } + } + + /** the settings have changed - revise */ + private void refreshView() { + _parent.refreshView(); + } + + /** + * notified that the config has been updated + * @param config the config that was been updated + */ + public void configUpdated(PeerPlotConfig config) { refreshView(); } + + public void stateUpdated() { + for (int i = 0; i < _options.length; i++) { + for (int j = 0; j < _options[i]._statAttributes.size(); j++) { + JCheckBox box = (JCheckBox)_options[i]._statAttributes.get(j); + if (_config.getSeriesConfig().getShouldPlotValue(_options[i].getStatName(), box.getText(), false)) + box.setSelected(true); + else + box.setSelected(false); + } + } + } + + private class OptionGroup { + JTextField _statName; + List _statAttributes; + + public String getStatName() { return _statName.getText(); } + + /** + * Creates an OptionLine. + * @param statName statistic group in question + * @param statAttributes list of attributes we keep about this stat + */ + public OptionGroup(String statName, String statAttributes[]) { + _statName = new JTextField(statName); + _statName.setEditable(false); + _statName.setBackground(_background); + _statAttributes = new ArrayList(4); + if (statAttributes != null) { + for (int i = 0; i < statAttributes.length; i++) { + JCheckBox box = new JCheckBox(statAttributes[i]); + box.addActionListener(new UpdateListener(OptionGroup.this)); + _statAttributes.add(box); + } + } + } + } + + private class UpdateListener implements ActionListener { + private OptionGroup _group; + + /** + * Update Listener constructor . . . + * @param group the group of stats to watch + */ + public UpdateListener(OptionGroup group) { + _group = group; + } + + /* (non-Javadoc) + * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) + */ + public void actionPerformed(ActionEvent evt) { + _config.disableEvents(); + + for (int i = 0; i < _group._statAttributes.size(); i++) { + JCheckBox box = (JCheckBox)_group._statAttributes.get(i); + _config.getSeriesConfig().setShouldPlotValue(_group.getStatName(), box.getText(), box.isSelected()); + } + _config.enableEvents(); + _config.fireUpdate(); + } + } +} \ No newline at end of file