diff --git a/.classpath b/.classpath deleted file mode 100644 index fe49848..0000000 --- a/.classpath +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/.gitignore b/.gitignore index 6b795ff..bb52d10 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,11 @@ /NMONVisualizer_*.jar /NMONVisualizerAPI_*.jar /MANIFEST.MF +/dependency-reduced-pom.xml +/target +.DS_Store +.vscode +launch.json +/.classpath +/.project +/.settings diff --git a/.project b/.project deleted file mode 100644 index d292c6d..0000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - NMONVisualizer - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - diff --git a/README.md b/README.md index 1f3e1e8..e0b3325 100644 --- a/README.md +++ b/README.md @@ -4,5 +4,10 @@ NMON Visualizer is a Java GUI tool for analyzing NMON system files from both AIX For more information, including links to download an executable JAR file, see [the website](http://nmonvisualizer.github.io/nmonvisualizer/). ## Build from Source -1. Download [Apache Ant](http://ant.apache.org/bindownload.cgi) and unpack into a directory, which we'll refer to as `${ANT_HOME}` -2. From the root directory of nmonvisualizer, run `${ANT_HOME}/bin/ant`. This will create an executable JAR file in the root directory. +1. Ensure a Java 11 or newer JDK is installed and configured. +1. Download [Apache Maven](https://maven.apache.org/download.cgi) and unpack into a directory, which we'll refer to as `${MAVEN_HOME}` +1. From the root directory of nmonvisualizer, run `${MAVEN_HOME}/bin/mvn clean install`. This will create an executable JAR file in the root directory. + +## Running +A Java 11 JVM or newer is required. Assuming `java` is in your `$PATH`, then the executable JAR file can be run with +`java -jar NMONVisualizer_.jar` or by double clicking on it in a GUI. diff --git a/build.xml b/build.xml deleted file mode 100644 index d849e70..0000000 --- a/build.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/lib/jackson-annotations-2.9.3.jar b/lib/jackson-annotations-2.9.3.jar deleted file mode 100644 index b830738..0000000 Binary files a/lib/jackson-annotations-2.9.3.jar and /dev/null differ diff --git a/lib/jackson-core-2.9.3.jar b/lib/jackson-core-2.9.3.jar deleted file mode 100644 index 40a04ce..0000000 Binary files a/lib/jackson-core-2.9.3.jar and /dev/null differ diff --git a/lib/jackson-databind-2.9.3.jar b/lib/jackson-databind-2.9.3.jar deleted file mode 100644 index 4cfc778..0000000 Binary files a/lib/jackson-databind-2.9.3.jar and /dev/null differ diff --git a/lib/jcommon-1.0.23.jar b/lib/jcommon-1.0.23.jar deleted file mode 100644 index 4dbb094..0000000 Binary files a/lib/jcommon-1.0.23.jar and /dev/null differ diff --git a/lib/jfreechart-1.0.19.jar b/lib/jfreechart-1.0.19.jar deleted file mode 100644 index 10f276c..0000000 Binary files a/lib/jfreechart-1.0.19.jar and /dev/null differ diff --git a/lib/slf4j-api-1.7.16.jar b/lib/slf4j-api-1.7.16.jar deleted file mode 100644 index 6828595..0000000 Binary files a/lib/slf4j-api-1.7.16.jar and /dev/null differ diff --git a/lib/slf4j-jdk14-1.7.16.jar b/lib/slf4j-jdk14-1.7.16.jar deleted file mode 100644 index a180692..0000000 Binary files a/lib/slf4j-jdk14-1.7.16.jar and /dev/null differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..39eca33 --- /dev/null +++ b/pom.xml @@ -0,0 +1,164 @@ + + + 4.0.0 + + com.ibm + NMONVisualizer + 1.0-SNAPSHOT + NMONVisualizer + + + UTF-8 + 1.8 + 1.8 + yyyy-MM-dd + + + + + com.fasterxml.jackson.core + jackson-core + 2.16.1 + + + com.fasterxml.jackson.core + jackson-annotations + 2.16.1 + + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + + + org.slf4j + slf4j-jdk14 + 2.0.12 + + + org.slf4j + slf4j-api + 2.0.12 + + + org.jfree + jfreechart + 1.0.19 + + + org.jfree + jcommon + 1.0.23 + + + + + ${project.artifactId}_${maven.build.timestamp} + src/ + + + src/ + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + + com.ibm.nmon.DelegatingMain + + + + LICENSE + LICENSE + + + NOTICE + NOTICE + + + + + *:* + + META-INF/MANIFEST.MF + META-INF/NOTICE + META-INF/LICENSE + META-INF/LICENSE.txt + **/module-info.class + + + + org.jfree:jfreechart + + org/jfree/chart/renderer/xy/StandardXYItemRenderer* + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + create-version-file + install + + run + + + + version=${maven.build.timestamp} + + + + + after-install + install + + run + + + + + + + + + + + maven-clean-plugin + 3.3.2 + + + + ${basedir} + + ${project.artifactId}*jar + + false + + + + + + + diff --git a/src/com/fasterxml/jackson/core/type/TypeReference.java b/src/com/fasterxml/jackson/core/type/TypeReference.java deleted file mode 100644 index 263a1c7..0000000 --- a/src/com/fasterxml/jackson/core/type/TypeReference.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.fasterxml.jackson.core.type; - -/* - * (C) Copyright IBM Corp, 2012 - * - * Clean room implementation of Jackson code - */ -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; - -public class TypeReference { - private final Type genericType; - - /* - * Needs to have zero-arg constructor - */ - public TypeReference() { - Type superClassType = getClass().getGenericSuperclass(); - this.genericType = ((ParameterizedType) superClassType).getActualTypeArguments()[0]; - } - - /* - * This method needs to return a value such that: - * - * new TypeReference(){}.getType() == T.class - * - * See JUnit - */ - public Type getType() { - return genericType; - } -} \ No newline at end of file diff --git a/src/com/ibm/nmon/NMONVisualizerApp.java b/src/com/ibm/nmon/NMONVisualizerApp.java index 27e1f15..265440a 100644 --- a/src/com/ibm/nmon/NMONVisualizerApp.java +++ b/src/com/ibm/nmon/NMONVisualizerApp.java @@ -48,6 +48,7 @@ public abstract class NMONVisualizerApp implements IntervalListener { private final TopasOutParser topasoutParser; private final FIOParser fioParser; private final ZPoolIOStatParser zpoolParser; + private final JMeterAggregateParser jMeterParser; private HostRenamer hostRenamer; @@ -80,6 +81,7 @@ protected NMONVisualizerApp() { topasoutParser = new TopasOutParser(nmonParser); fioParser = new FIOParser(); zpoolParser = new ZPoolIOStatParser(); + jMeterParser = new JMeterAggregateParser(); TimeZone defaultTz = TimeZone.getDefault(); @@ -222,6 +224,9 @@ else if (filter.getHATJFileFilter().accept(fileToParse)) { data.setHostname(hostname); } } + else if (filter.getJMeterFileFilter().accept(fileToParse)) { + data = jMeterParser.parse(fileToParse); + } else if (filter.getPerfmonFileFilter().accept(fileToParse)) { data = perfmonParser.parse(fileToParse, getBooleanProperty("scaleProcessesByCPUs")); } diff --git a/src/com/ibm/nmon/ReportGenerator.java b/src/com/ibm/nmon/ReportGenerator.java index aa87575..e08be60 100755 --- a/src/com/ibm/nmon/ReportGenerator.java +++ b/src/com/ibm/nmon/ReportGenerator.java @@ -35,6 +35,9 @@ import com.ibm.nmon.report.ReportCache; import com.ibm.nmon.chart.definition.BaseChartDefinition; +import com.ibm.nmon.data.transform.name.HostRenamer; +import com.ibm.nmon.data.transform.name.HostRenamerFactory; + import com.ibm.nmon.util.CSVWriter; import com.ibm.nmon.util.GranularityHelper; @@ -74,8 +77,9 @@ public static void main(String[] args) { List multiplexedFieldCharts = new java.util.ArrayList(); List multiplexedTypeCharts = new java.util.ArrayList(); - String intervalsFile = ""; String formatFile = ""; + String renamerFile = ""; + String intervalsFile = ""; boolean summaryCharts = true; boolean dataSetCharts = true; @@ -87,6 +91,8 @@ public static void main(String[] args) { boolean writeChartData = false; int granularity = -1; + int width = -1; + int height = -1; for (int i = 0; i < args.length; i++) { String arg = args[i]; @@ -143,7 +149,7 @@ public static void main(String[] args) { ++i; if (i > args.length) { - System.err.println("Chart format properties must be specified " + '-' + 'f'); + System.err.println("Chart format properties" + " must be specified for " + '-' + 'f'); return; } @@ -155,7 +161,7 @@ public static void main(String[] args) { ++i; if (i > args.length) { - System.err.println("Granularity value must be specified " + '-' + 'g'); + System.err.println("Granularity" + " must be specified for " + '-' + 'g'); return; } @@ -163,16 +169,27 @@ public static void main(String[] args) { granularity = Integer.parseInt(args[i]) * 1000; } catch (NumberFormatException e) { - System.err.println("Granularity value must be an integer " + args[i]); + System.err.println("Granularity" + " value " + args[i] + " must be an integer"); + } + + break nextarg; + } + case 'h': { + ++i; + + if (i > args.length) { + System.err.println("file" + " must be specified for " + '-' + 'h'); + return; } + renamerFile = args[i]; break nextarg; } case 'i': { ++i; if (i > args.length) { - System.err.println("file must be specified for " + '-' + 's'); + System.err.println("file" + " must be specified for " + '-' + 'i'); return; } @@ -189,6 +206,36 @@ public static void main(String[] args) { else if ("nosummary".equals(param)) { summaryCharts = false; } + else if ("width".equals(param)) { + ++i; + + if (i > args.length) { + System.err.println("Width" + " must be specified for " + '-' + '-' + "width"); + return; + } + + try { + width = Integer.parseInt(args[i]); + } + catch (NumberFormatException e) { + System.err.println("Width" + " value " + args[i] + " must be an integer"); + } + } + else if ("height".equals(param)) { + ++i; + + if (i > args.length) { + System.err.println("Height" + " must be specified for " + '-' + '-' + "height"); + return; + } + + try { + height = Integer.parseInt(args[i]); + } + catch (NumberFormatException e) { + System.err.println("Height" + " value " + args[i] + " must be an integer"); + } + } else if ("mf".equals(param)) { ++i; @@ -268,7 +315,7 @@ else if ("chartdata".equals(param)) { } ReportGenerator generator = new ReportGenerator(customSummaryCharts, customDataCharts, multiplexedFieldCharts, - multiplexedTypeCharts, granularity); + multiplexedTypeCharts, granularity, width, height); File outputDirectory = null; @@ -310,6 +357,17 @@ else if ("chartdata".equals(param)) { generator.factory.setFormatter(chartFormatter); + if (!"".equals(renamerFile)) { + try { + HostRenamer hostRenamer = HostRenamerFactory.loadFromFile(new File(renamerFile)); + generator.setHostRenamer(hostRenamer); + } + catch (Exception e) { + System.err.println("cannot parse host renamer from '" + renamerFile + "'\n" + e.getMessage()); + return; + } + } + // parse files generator.parse(filesToParse); @@ -380,8 +438,23 @@ private static long parseTime(String[] args, int index, char param) { private boolean writeChartData = false; + private final int width; + private final int height; + private ReportGenerator(List customSummaryCharts, List customDataCharts, - List multiplexedFieldCharts, List multiplexedTypeCharts, int granularity) { + List multiplexedFieldCharts, List multiplexedTypeCharts, int granularity, int width, + int height) { + // use -1 to default to sized set in BaseChartDefinition + if ((width < 1) && (width != -1)) { + throw new IllegalArgumentException("width" + " must be > 0"); + } + if (height < 1 && (height != -1)) { + throw new IllegalArgumentException("height" + " must be > 0"); + } + + this.width = width; + this.height = height; + factory = new ChartFactory(this); cache = new ReportCache(); @@ -427,6 +500,16 @@ private void parse(List filesToParse) { System.out.println("Parsing NMON files..."); for (String fileToParse : filesToParse) { + // ignore ReportGenerator error log files from previous executions + if (fileToParse.endsWith(".log") && fileToParse.contains("ReportGenerator")) { + continue; + } + // ignore chart directories from previous executions + // these may contain CSV files from --rawdata or --chartdata + if (fileToParse.endsWith(".csv") && fileToParse.contains("/charts/")) { + continue; + } + System.out.print("\t" + fileToParse + "... "); System.out.flush(); @@ -698,7 +781,8 @@ private boolean saveChart(BaseChartDefinition definition, Iterable data; - - private NamingMode subtitleNamingMode = NamingMode.HOST; - - protected BaseChartDefinition(String shortName, String title) { - setShortName(shortName); - setTitle(title); - - this.width = 1920 / 2; - this.height = 1080 / 2; - - data = new java.util.ArrayList(3); - } - - protected BaseChartDefinition(BaseChartDefinition copy, boolean copyData) { - this.shortName = copy.shortName; - this.title = copy.title; - - this.width = copy.width; - this.height = copy.height; - - this.subtitleNamingMode = copy.subtitleNamingMode; - - if (copyData) { - this.data = new java.util.ArrayList(copy.data); - } - else { - this.data = new java.util.ArrayList(3); - } - } - - public final String getShortName() { - return shortName; - } - - public final void setShortName(String shortName) { - if (shortName == null) { - this.shortName = ""; - } - else { - this.shortName = shortName; - } - } - - public final String getTitle() { - return title; - } - - public final void setTitle(String title) { - if (title == null) { - this.title = ""; - } - else { - this.title = title; - } - } - - public int getWidth() { - return width; - } - - public void setWidth(int width) { - if (height < 1) { - throw new IllegalArgumentException("height" + "must be greater than 0"); - } - - this.width = width; - } - - public int getHeight() { - return height; - } - - public void setHeight(int height) { - if (height < 1) { - throw new IllegalArgumentException("height" + "must be greater than 0"); - } - - this.height = height; - } - - public final NamingMode getSubtitleNamingMode() { - return subtitleNamingMode; - } - - public final void setSubtitleNamingMode(NamingMode subtitleNamingMode) { - if (subtitleNamingMode == null) { - this.subtitleNamingMode = NamingMode.NONE; - } - else { - this.subtitleNamingMode = subtitleNamingMode; - } - } - - public final void addData(DataDefinition dataDefinition) { - data.add(dataDefinition); - } - - public final Iterable getData() { - return java.util.Collections.unmodifiableList(data); - } - - @Override - public final String toString() { - if ("".equals(shortName)) { - if ("".equals(title)) { - return ""; - } - else { - return title; - } - } - else { - return shortName; - } - } -} +package com.ibm.nmon.chart.definition; + +import java.util.List; + +import com.ibm.nmon.data.definition.DataDefinition; +import com.ibm.nmon.data.definition.NamingMode; + +/** + * Base class for chart definitions. A chart definition will have a short name and a title. The short name is used as an + * identifier in the GUI. The title will be used as the title in the chart. + */ +public abstract class BaseChartDefinition implements Cloneable { + private String shortName; + private String title; + + private int width; + private int height; + + private final List data; + + private NamingMode subtitleNamingMode = NamingMode.HOST; + + protected BaseChartDefinition(String shortName, String title) { + setShortName(shortName); + setTitle(title); + + this.width = 1920; + this.height = 1080; + + data = new java.util.ArrayList(3); + } + + protected BaseChartDefinition(BaseChartDefinition copy, boolean copyData) { + this.shortName = copy.shortName; + this.title = copy.title; + + this.width = copy.width; + this.height = copy.height; + + this.subtitleNamingMode = copy.subtitleNamingMode; + + if (copyData) { + this.data = new java.util.ArrayList(copy.data); + } + else { + this.data = new java.util.ArrayList(3); + } + } + + public final String getShortName() { + return shortName; + } + + public final void setShortName(String shortName) { + if (shortName == null) { + this.shortName = ""; + } + else { + this.shortName = shortName; + } + } + + public final String getTitle() { + return title; + } + + public final void setTitle(String title) { + if (title == null) { + this.title = ""; + } + else { + this.title = title; + } + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + if (height < 1) { + throw new IllegalArgumentException("height" + "must be greater than 0"); + } + + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + if (height < 1) { + throw new IllegalArgumentException("height" + "must be greater than 0"); + } + + this.height = height; + } + + public final NamingMode getSubtitleNamingMode() { + return subtitleNamingMode; + } + + public final void setSubtitleNamingMode(NamingMode subtitleNamingMode) { + if (subtitleNamingMode == null) { + this.subtitleNamingMode = NamingMode.NONE; + } + else { + this.subtitleNamingMode = subtitleNamingMode; + } + } + + public final void addData(DataDefinition dataDefinition) { + data.add(dataDefinition); + } + + public final Iterable getData() { + return java.util.Collections.unmodifiableList(data); + } + + @Override + public final String toString() { + if ("".equals(shortName)) { + if ("".equals(title)) { + return ""; + } + else { + return title; + } + } + else { + return shortName; + } + } +} diff --git a/src/com/ibm/nmon/chart/definition/ChartDefinitionParser.java b/src/com/ibm/nmon/chart/definition/ChartDefinitionParser.java index d47b775..9407990 100644 --- a/src/com/ibm/nmon/chart/definition/ChartDefinitionParser.java +++ b/src/com/ibm/nmon/chart/definition/ChartDefinitionParser.java @@ -185,7 +185,7 @@ else if (currentChart instanceof HistogramChartDefinition) { catch (IllegalArgumentException iae) { logger.warn( "ignoring " + "" + " attributes 'min' and 'max' " - + "with values '{}' and '{}'" + " at line {}" + ", invalid range", + + "with values '{}' and '{}'" + " at line {}" + ", invalid " + "range", minString, maxString, getLineNumber()); } } @@ -218,6 +218,16 @@ else if ("data".equals(element)) { logger.warn("ignoring " + "invalid " + "'stat'" + " attribute {} at line {}", stat, getLineNumber()); } + + if (currentChart.getClass() == LineChartDefinition.class) { + if ((currentStat != Statistic.AVERAGE) && (currentStat != Statistic.MAXIMUM) + && (currentStat != Statistic.MINIMUM) && (currentStat != Statistic.COUNT) + && (currentStat != Statistic.SUM)) { + logger.warn("ignoring " + "invalid " + "'stat'" + " attribute {} at line {}" + + "; only AVERAGE, MAXIMUM, MINIMUM, COUNT and SUM are valid in ", stat, + getLineNumber()); + } + } } useSecondaryYAxis = Boolean.parseBoolean(attributes.get("useYAxis2")); @@ -677,24 +687,25 @@ private void parseSize(String elementName, Map attributes) { } private void parseDateFormat(NamingMode mode, Map attributes) { - if (mode != NamingMode.DATE) { + // only parse once + if (dateFormat != null) { + return; + } + + // if mode != NamingMode.DATE then dateFormat will just be ignored by all other NamingModes + String format = attributes.get("dateFormat"); + + if (format == null) { dateFormat = null; } else { - String format = attributes.get("dateFormat"); - - if (format == null) { - dateFormat = null; + try { + dateFormat = new SimpleDateFormat(format); } - else { - try { - dateFormat = new SimpleDateFormat(format); - } - catch (Exception e) { - logger.warn("ignoring invalid '" + "dateFormat" + "' attribute" + " '{}'" + " at line {}" - + "; it will be ignored", format, getLineNumber()); - dateFormat = null; - } + catch (Exception e) { + logger.warn("ignoring " + "invalid '" + "dateFormat" + "' attribute" + " '{}'" + " at line {}", format, + getLineNumber()); + dateFormat = null; } } } @@ -805,7 +816,7 @@ private NameTransformer createTransformer(Map attributes, boolea return new SimpleNameTransformer(alias); } else { - logger.warn("ignoring invalid '" + "alias" + "' attribute" + " at line {}", getLineNumber()); + logger.warn("ignoring " + "invalid '" + "alias" + "' attribute" + " at line {}", getLineNumber()); return existing; } } diff --git a/src/com/ibm/nmon/data/Process.java b/src/com/ibm/nmon/data/Process.java index fbee24a..05f6d39 100644 --- a/src/com/ibm/nmon/data/Process.java +++ b/src/com/ibm/nmon/data/Process.java @@ -114,12 +114,7 @@ else if (obj instanceof Process) { @Override public int hashCode() { - if (id == -1) { - return name.hashCode(); - } - else { - return ((id * 17) << 16) & ((int) (startTime ^ (startTime >>> 32))); - } + return typeId.hashCode(); } @Override diff --git a/src/com/ibm/nmon/data/matcher/ExactFieldMatcher.java b/src/com/ibm/nmon/data/matcher/ExactFieldMatcher.java index e27a718..661a951 100644 --- a/src/com/ibm/nmon/data/matcher/ExactFieldMatcher.java +++ b/src/com/ibm/nmon/data/matcher/ExactFieldMatcher.java @@ -25,7 +25,7 @@ public List getMatchingFields(DataType type) { return java.util.Collections.emptyList(); } else { - List toReturn = new java.util.ArrayList(type.getFieldCount()); + List toReturn = new java.util.ArrayList(); for (String field : type.getFields()) { if (this.field.equals(field)) { diff --git a/src/com/ibm/nmon/data/matcher/RegexFieldMatcher.java b/src/com/ibm/nmon/data/matcher/RegexFieldMatcher.java index ee58168..79e8ba1 100644 --- a/src/com/ibm/nmon/data/matcher/RegexFieldMatcher.java +++ b/src/com/ibm/nmon/data/matcher/RegexFieldMatcher.java @@ -22,7 +22,7 @@ public List getMatchingFields(DataType type) { return java.util.Collections.emptyList(); } else { - List toReturn = new java.util.ArrayList(type.getFieldCount()); + List toReturn = new java.util.ArrayList(); for (String field : type.getFields()) { if (matcher.reset(field).matches()) { diff --git a/src/com/ibm/nmon/data/matcher/SetFieldMatcher.java b/src/com/ibm/nmon/data/matcher/SetFieldMatcher.java index 9e2b081..c7f9c80 100644 --- a/src/com/ibm/nmon/data/matcher/SetFieldMatcher.java +++ b/src/com/ibm/nmon/data/matcher/SetFieldMatcher.java @@ -26,7 +26,7 @@ public List getMatchingFields(DataType type) { return java.util.Collections.emptyList(); } else { - List toReturn = new java.util.ArrayList(type.getFieldCount()); + List toReturn = new java.util.ArrayList(fields.size()); for (String field : fields) { if (type.hasField(field)) { diff --git a/src/com/ibm/nmon/data/transform/AIXLPARTransform.java b/src/com/ibm/nmon/data/transform/AIXLPARTransform.java index c139655..7abc6be 100644 --- a/src/com/ibm/nmon/data/transform/AIXLPARTransform.java +++ b/src/com/ibm/nmon/data/transform/AIXLPARTransform.java @@ -3,7 +3,14 @@ import com.ibm.nmon.data.DataType; /** - * Changes the EC_Idle% and VP_Idle% metric for AIX LPARs to CPU% (i.e. Usr% + Sys%). + *

Changes the EC_Idle% and VP_Idle% metric for AIX LPARs to CPU% (i.e. the total). + * Note that EC_CPU% cannot exceed 100% even if the LPAR configuration allows CPU sharing.

+ * + *

Adds an EC_Used% value that matches vmstat's ec value. This value can exceed 100% + * which indicates the LPAR is sharing CPU and currently using more than its entitle capacity. + * Also adds an OtherLPARs value which is the total CPUS being used by other LPARs plus idle CPUs.

+ * + *

Adds an Unfolded value if Folded exists which is the virtualCPUs - Folded.

*/ public final class AIXLPARTransform implements DataTransform { @Override @@ -11,10 +18,10 @@ public DataType buildDataType(String id, String subId, String name, String... fi String[] newFields = null; if (java.util.Arrays.binarySearch(fields, "Folded") != -1) { - newFields = new String[fields.length + 2]; + newFields = new String[fields.length + 3]; } else { - newFields = new String[fields.length + 1]; + newFields = new String[fields.length + 2]; } for (int i = 0, j = 0; i < fields.length; i++, j++) { @@ -35,6 +42,7 @@ else if ("Folded".equals(field)) { } } + newFields[newFields.length - 2] = "EC_Used%"; newFields[newFields.length - 1] = "OtherLPARs"; return new DataType(id, name, newFields); @@ -51,7 +59,8 @@ public double[] transform(DataType type, double[] data) { String field = type.getField(j); if (field.equals("EC" + "_CPU%")) { - newData[j] = data[type.getFieldIndex("EC" + "_User%")] + data[type.getFieldIndex("EC" + "_Sys%")]; + // per https://www.ibm.com/docs/en/aix/7.3?topic=tool-processor-statistics, total _includes_ Idle% + newData[j] = data[type.getFieldIndex("EC" + "_User%")] + data[type.getFieldIndex("EC" + "_Sys%")] + data[type.getFieldIndex("EC" + "_Wait%")] + data[type.getFieldIndex("EC" + "_CPU%")]; } else if (field.equals("VP" + "_CPU%")) { newData[j] = data[type.getFieldIndex("VP" + "_User%")] + data[type.getFieldIndex("VP" + "_Sys%")]; @@ -66,6 +75,9 @@ else if (field.equals("Unfolded")) { } } + // EC_Used% = PhysicalCPU / entitled (i.e. CPUs in use vs entitled) + newData[newData.length - 2] = data[type.getFieldIndex("PhysicalCPU")] / data[type.getFieldIndex("entitled")] * 100; + // OtherLPARs = total pool CPUs - idle CPU - this LPAR's CPU newData[newData.length - 1] = data[type.getFieldIndex("poolCPUs")] - data[type.getFieldIndex("PoolIdle")] - data[type.getFieldIndex("PhysicalCPU")]; diff --git a/src/com/ibm/nmon/data/transform/WindowsBytesTransform.java b/src/com/ibm/nmon/data/transform/WindowsBytesTransform.java index 45954ed..8fd0940 100644 --- a/src/com/ibm/nmon/data/transform/WindowsBytesTransform.java +++ b/src/com/ibm/nmon/data/transform/WindowsBytesTransform.java @@ -22,8 +22,8 @@ public class WindowsBytesTransform implements DataTransform { private static final Matcher VALID_TYPES = Pattern.compile( "LogicalDisk.*|PhysicalDisk.*|Network Interface.*|Memory|System").matcher(""); - // match all Bytes, but not KBytes or MBytes - private static final Matcher VALID_FIELDS = Pattern.compile("(.*?[^KM])?Bytes(.*)").matcher(""); + // match all Bytes, but not KBytes, MBytes, etc + private static final Matcher VALID_FIELDS = Pattern.compile("([^%].*?[^KMGTEP])?Bytes(.*)").matcher(""); private Map> changedFields = new java.util.HashMap>(); diff --git a/src/com/ibm/nmon/data/transform/name/RegexNameTransformer.java b/src/com/ibm/nmon/data/transform/name/RegexNameTransformer.java index 419a285..26b83d0 100644 --- a/src/com/ibm/nmon/data/transform/name/RegexNameTransformer.java +++ b/src/com/ibm/nmon/data/transform/name/RegexNameTransformer.java @@ -82,7 +82,6 @@ public String transform(String original) { return original; } else if (matcher.groupCount() == 0) { - logger.warn("regex '{}' does not contain any groups", matcher); return original; } else if (matcher.groupCount() < group) { diff --git a/src/com/ibm/nmon/file/CombinedFileFilter.java b/src/com/ibm/nmon/file/CombinedFileFilter.java index b633b90..68ca465 100644 --- a/src/com/ibm/nmon/file/CombinedFileFilter.java +++ b/src/com/ibm/nmon/file/CombinedFileFilter.java @@ -49,20 +49,24 @@ public HATJFileFilter getHATJFileFilter() { return (HATJFileFilter) filters.get(4).getFilter(); } + public JMeterFileFilter getJMeterFileFilter() { + return (JMeterFileFilter) filters.get(5).getFilter(); + } + public PerfmonFileFilter getPerfmonFileFilter() { - return (PerfmonFileFilter) filters.get(5).getFilter(); + return (PerfmonFileFilter) filters.get(6).getFilter(); } public ZPoolIOStatFileFilter getZPoolIOStatOutFileFilter() { - return (ZPoolIOStatFileFilter) filters.get(6).getFilter(); + return (ZPoolIOStatFileFilter) filters.get(7).getFilter(); } public TopasOutFileFilter getTopasOutFileFilter() { - return (TopasOutFileFilter) filters.get(7).getFilter(); + return (TopasOutFileFilter) filters.get(8).getFilter(); } public FIOFileFilter getFIOFileFilter() { - return (FIOFileFilter) filters.get(8).getFilter(); + return (FIOFileFilter) filters.get(9).getFilter(); } private CombinedFileFilter(boolean acceptDirectories) { @@ -71,6 +75,7 @@ private CombinedFileFilter(boolean acceptDirectories) { filters.add(new SwingAndIOFileFilter("IOStat Files", new IOStatFileFilter(), acceptDirectories)); filters.add(new SwingAndIOFileFilter("JSON Files", new JSONFileFilter(), acceptDirectories)); filters.add(new SwingAndIOFileFilter("HATJ CSV Files", new HATJFileFilter(), acceptDirectories)); + filters.add(new SwingAndIOFileFilter("JMeter Aggregate Files", new JMeterFileFilter(), acceptDirectories)); filters.add(new SwingAndIOFileFilter("Perfmon CSV Files", new PerfmonFileFilter(), acceptDirectories)); filters.add(new SwingAndIOFileFilter("ZPool IOStat Files", new ZPoolIOStatFileFilter(), acceptDirectories)); filters.add(new SwingAndIOFileFilter("topasout -a Files", new TopasOutFileFilter(), acceptDirectories)); diff --git a/src/com/ibm/nmon/file/JMeterFileFilter.java b/src/com/ibm/nmon/file/JMeterFileFilter.java new file mode 100644 index 0000000..fc26204 --- /dev/null +++ b/src/com/ibm/nmon/file/JMeterFileFilter.java @@ -0,0 +1,10 @@ +package com.ibm.nmon.file; + +public final class JMeterFileFilter extends BaseFileFilter { + public boolean accept(String pathname) { + String name = pathname.toLowerCase(); + return name.endsWith(".csv") && (name.contains("jmeter") || name.contains("aggregate")); + } + + public JMeterFileFilter() {} +} diff --git a/src/com/ibm/nmon/gui/analysis/SummaryTablePanel.java b/src/com/ibm/nmon/gui/analysis/SummaryTablePanel.java index 076ff8a..f0b9558 100644 --- a/src/com/ibm/nmon/gui/analysis/SummaryTablePanel.java +++ b/src/com/ibm/nmon/gui/analysis/SummaryTablePanel.java @@ -1,5 +1,7 @@ package com.ibm.nmon.gui.analysis; +import java.util.Set; + import java.awt.BorderLayout; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; @@ -9,7 +11,6 @@ import java.awt.event.MouseEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; -import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; @@ -31,6 +32,8 @@ import javax.swing.SwingConstants; import javax.swing.table.TableRowSorter; +import java.awt.Toolkit; + import com.ibm.nmon.analysis.AnalysisSet; import com.ibm.nmon.analysis.AnalysisSetListener; import com.ibm.nmon.analysis.Statistic; @@ -438,14 +441,14 @@ private void setupMenu(JFrame parent) { JMenuItem item = new JMenuItem("Load Definition..."); item.setMnemonic('n'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.addActionListener(loadAnalysis); menu.add(item); item = new JMenuItem("Save Definition..."); item.setMnemonic('s'); item.setIcon(Styles.SAVE_ICON); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.addActionListener(saveAnalysis); item.setEnabled(analysisSet.size() > 0); menu.add(item); @@ -456,7 +459,7 @@ private void setupMenu(JFrame parent) { item.setMnemonic('a'); item.setIcon(Styles.COPY_ICON); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); item.addActionListener(copyTable); menu.add(item); @@ -464,7 +467,7 @@ private void setupMenu(JFrame parent) { item.setMnemonic('c'); item.setIcon(Styles.CLEAR_ICON); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); item.addActionListener(clearTable); menu.add(item); @@ -473,7 +476,7 @@ private void setupMenu(JFrame parent) { item = new JMenuItem("Select Columns..."); item.setMnemonic('m'); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -488,7 +491,7 @@ public void actionPerformed(ActionEvent e) { item.setMnemonic('t'); item.setIcon(TRANSPOSE_ICON); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); item.addActionListener(transposeTable); menu.add(item); } diff --git a/src/com/ibm/nmon/gui/chart/BaseChartPanel.java b/src/com/ibm/nmon/gui/chart/BaseChartPanel.java index bca320f..1f5a426 100644 --- a/src/com/ibm/nmon/gui/chart/BaseChartPanel.java +++ b/src/com/ibm/nmon/gui/chart/BaseChartPanel.java @@ -69,7 +69,7 @@ protected BaseChartPanel(NMONVisualizerGui gui, JFrame parent) { setBackground(java.awt.Color.WHITE); - // keep the chart for resizing when empty + // keep the chart from resizing when empty setPreferredSize(null); // disable zooming @@ -81,8 +81,8 @@ protected BaseChartPanel(NMONVisualizerGui gui, JFrame parent) { this.gui = gui; this.parent = parent; - this.saveWidth = 1920 / 2; - this.saveHeight = 1080 / 2; + this.saveWidth = 1920; + this.saveHeight = 1080; setEnabled(false); clearChart(); diff --git a/src/com/ibm/nmon/gui/chart/ChartFactory.java b/src/com/ibm/nmon/gui/chart/ChartFactory.java index 0f03481..55639aa 100644 --- a/src/com/ibm/nmon/gui/chart/ChartFactory.java +++ b/src/com/ibm/nmon/gui/chart/ChartFactory.java @@ -35,11 +35,11 @@ public ChartFactory(NMONVisualizerApp app) { this.app = app; lineChartBuilder = new LineChartBuilder(); - lineChartBuilder.addPlugin(new LineChartBuilderPlugin(app)); - barChartBuilder = new BarChartBuilder(); intervalChartBuilder = new IntervalChartBuilder(); histogramChartBuilder = new HistogramChartBuilder(); + + lineChartBuilder.addPlugin(new LineChartBuilderPlugin(app)); } public void setGranularity(int granularity) { @@ -67,13 +67,6 @@ public void setFormatter(ChartFormatter formatter) { histogramChartBuilder.setFormatter(formatter); } - public void addPlugin(ChartBuilderPlugin plugin) { - lineChartBuilder.addPlugin(plugin); - barChartBuilder.addPlugin(plugin); - intervalChartBuilder.addPlugin(plugin); - histogramChartBuilder.addPlugin(plugin); - } - /** * Create a chart given a definition and some data. * diff --git a/src/com/ibm/nmon/gui/chart/DataTypeChartPanel.java b/src/com/ibm/nmon/gui/chart/DataTypeChartPanel.java index d9d44ba..9f4c5fe 100644 --- a/src/com/ibm/nmon/gui/chart/DataTypeChartPanel.java +++ b/src/com/ibm/nmon/gui/chart/DataTypeChartPanel.java @@ -403,10 +403,7 @@ protected void finalize() throws Throwable { tempFields.put("throughput", "Tx / s"); tempFields.put("hits", "Hits / s"); - tempExceptions.add("EC_User%"); - tempExceptions.add("EC_Sys%"); - tempExceptions.add("EC_Wait%"); - tempExceptions.add("EC_CPU%"); + tempExceptions.add("EC_Used%"); tempExceptions.add("VP_User%"); tempExceptions.add("VP_Sys%"); tempExceptions.add("VP_Wait%"); diff --git a/src/com/ibm/nmon/gui/chart/builder/BarChartBuilder.java b/src/com/ibm/nmon/gui/chart/builder/BarChartBuilder.java index 42ebb04..e9d2db8 100644 --- a/src/com/ibm/nmon/gui/chart/builder/BarChartBuilder.java +++ b/src/com/ibm/nmon/gui/chart/builder/BarChartBuilder.java @@ -5,6 +5,7 @@ import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.axis.CategoryLabelPositions; import org.jfree.chart.plot.CategoryPlot; @@ -98,6 +99,9 @@ protected void formatChart() { plot.getDomainAxis().setCategoryMargin(0.15d); plot.getDomainAxis().setTickMarksVisible(false); + // assume bar names will usually be hostnames or longer values; draw them on the chart at a 45 degree angle + plot.getDomainAxis().setCategoryLabelPositions(CategoryLabelPositions.UP_45); + // position of first bar start and last bar end // 1.5% of the chart area within the axis will be blank space on each end plot.getDomainAxis().setLowerMargin(.015); diff --git a/src/com/ibm/nmon/gui/chart/builder/BaseChartBuilder.java b/src/com/ibm/nmon/gui/chart/builder/BaseChartBuilder.java index 6939fb5..a8af225 100755 --- a/src/com/ibm/nmon/gui/chart/builder/BaseChartBuilder.java +++ b/src/com/ibm/nmon/gui/chart/builder/BaseChartBuilder.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Set; +import java.util.Iterator; import org.jfree.chart.JFreeChart; import org.jfree.chart.plot.CategoryPlot; @@ -395,9 +396,18 @@ else if (fields.size() == 1) { // note that getName() ignores data, type, field, etc if not needed, so it is safe to pass the first element // in each Set + // but, handle null values when no data selected due to bad report definitions + Iterator ids = dataSets.iterator(); + DataSet set = ids.hasNext() ? ids.next() : null; + + Iterator idt = dataTypes.iterator(); + DataType type = idt.hasNext() ? idt.next() : null; + + Iterator ifd = fields.iterator(); + String field = ifd.hasNext() ? ifd.next() : null; + // the above switch logic has already set the NamingMode to the necessary value - subtitle = actualMode.getName(dataDefinition, dataSets.iterator().next(), dataTypes.iterator().next(), - fields.iterator().next(), interval, granularity); + subtitle = actualMode.getName(dataDefinition, set, type, field, interval, granularity); // end after first successful rename if (!subtitle.equals(oldSubtitle)) { diff --git a/src/com/ibm/nmon/gui/chart/builder/ChartFormatterParser.java b/src/com/ibm/nmon/gui/chart/builder/ChartFormatterParser.java index 57f250a..1c2346c 100644 --- a/src/com/ibm/nmon/gui/chart/builder/ChartFormatterParser.java +++ b/src/com/ibm/nmon/gui/chart/builder/ChartFormatterParser.java @@ -187,7 +187,6 @@ else if (data.startsWith("[")) { catch (NumberFormatException nfe) { throw new IllegalArgumentException( "cannot parse '" + rgb[2] + "' from '" + data + "' as a color for " + name + "."); - } try { @@ -196,7 +195,7 @@ else if (data.startsWith("[")) { catch (IllegalArgumentException iae) { // handle illegal values (e.g. -1 or 257) throw new IllegalArgumentException( - "cannot parse '" + data + "' as a color for " + name + "." + "\n" + iae.getMessage()); + "cannot parse '" + data + "' as a color for " + name + ".", iae); } } else { diff --git a/src/com/ibm/nmon/gui/chart/builder/LineChartBuilder.java b/src/com/ibm/nmon/gui/chart/builder/LineChartBuilder.java index d1529fa..1d3f2b5 100644 --- a/src/com/ibm/nmon/gui/chart/builder/LineChartBuilder.java +++ b/src/com/ibm/nmon/gui/chart/builder/LineChartBuilder.java @@ -32,6 +32,8 @@ import com.ibm.nmon.chart.definition.LineChartDefinition; +import com.ibm.nmon.analysis.Statistic; + public class LineChartBuilder extends BaseChartBuilder { private boolean showLegends = true; @@ -69,7 +71,8 @@ protected JFreeChart createChart() { valueAxis = new NumberAxis(); valueAxis.setAutoRangeIncludesZero(true); - // secondary axis data cannot be stacked, so use the the standard, line based rendering + // secondary axis data cannot be stacked, so use the the standard, line based + // rendering // for both types StandardXYItemRenderer renderer = new StandardXYItemRenderer(); renderer.setBaseSeriesVisible(true, false); @@ -124,7 +127,8 @@ private void formatRenderer(int index) { renderer.setBaseShapesVisible(true); renderer.setBaseShapesFilled(true); - // if no data for more than 1 granularity's time period, do not draw a connecting line + // if no data for more than 1 granularity's time period, do not draw a + // connecting line renderer.setPlotDiscontinuous(true); renderer.setGapThresholdType(UnitType.ABSOLUTE); @@ -184,13 +188,13 @@ private void addMatchingData(DataTupleXYDataset dataset, DataDefinition definiti lineNamingMode.getName(definition, data, type, field, getInterval(), getGranularity())); } - addData(dataset, data, type, fields, fieldNames); + addData(definition, dataset, data, type, fields, fieldNames); } } } - private void addData(DataTupleXYDataset dataset, DataSet data, DataType type, List fields, - List fieldNames) { + private void addData(DataDefinition dataDefinition, DataTupleXYDataset dataset, DataSet data, DataType type, + List fields, List fieldNames) { long start = System.nanoTime(); double[] totals = new double[fields.size()]; @@ -209,10 +213,41 @@ private void addData(DataTupleXYDataset dataset, DataSet data, DataType type, Li if (!Double.isNaN(value)) { if (Double.isNaN(totals[i])) { - totals[i] = 0; + if (dataDefinition.getStatistic() == Statistic.MINIMUM) { + totals[i] = Double.MAX_VALUE; + } + else if (dataDefinition.getStatistic() == Statistic.MAXIMUM) { + totals[i] = Double.MIN_VALUE; + } + else { + totals[i] = 0; + } } - totals[i] += value; + switch (dataDefinition.getStatistic()) { + case AVERAGE: + totals[i] += value; + break; + case MAXIMUM: + if (value > totals[i]) { + totals[i] = value; + } + break; + case MINIMUM: + if (value < totals[i]) { + totals[i] = value; + } + break; + case COUNT: + ++totals[i]; + break; + case SUM: + totals[i] += value; + break; + default: + throw new IllegalArgumentException( + "canonot calculate " + dataDefinition.getStatistic() + " on a line chart"); + } } } } @@ -231,13 +266,17 @@ private void addData(DataTupleXYDataset dataset, DataSet data, DataType type, Li } if (!Double.isNaN(totals[i])) { - // if the plot is listening for dataset changes, it will fire an event for - // every data point - // this causes a huge amount of GC and very slow response times so the false - // value is important here - dataset.add(graphTime, totals[i] / n, fieldNames.get(i), false); + // if the plot is listening for dataset changes, it will fire an event for every data point + // this causes a huge amount of GC and very slow response times so the false value is important + if (dataDefinition.getStatistic() == Statistic.AVERAGE) { + dataset.add(graphTime, totals[i] / n, fieldNames.get(i), false); + } + else { + dataset.add(graphTime, totals[i], fieldNames.get(i), false); + } } + // reset totals totals[i] = Double.NaN; } @@ -259,7 +298,12 @@ private void addData(DataTupleXYDataset dataset, DataSet data, DataType type, Li } if (!Double.isNaN(totals[i])) { - dataset.add(graphTime, totals[i] / n, fieldNames.get(i), false); + if (dataDefinition.getStatistic() == Statistic.AVERAGE) { + dataset.add(graphTime, totals[i] / n, fieldNames.get(i), false); + } + else { + dataset.add(graphTime, totals[i], fieldNames.get(i), false); + } } } } @@ -275,9 +319,8 @@ private void addData(DataTupleXYDataset dataset, DataSet data, DataType type, Li } if (logger.isDebugEnabled()) { - logger.debug("{}: {}-({} fields) added {} data points to chart '{}' in {}ms", data, type, - fieldNames.size(), dataset.getItemCount(), definition.getTitle(), - (System.nanoTime() - start) / 1000000.0d); + logger.debug("{}: {}-({} fields) added {} data points to chart '{}' in {}ms", data, type, fieldNames.size(), + dataset.getItemCount(), definition.getTitle(), (System.nanoTime() - start) / 1000000.0d); } } @@ -307,59 +350,78 @@ private void recalculateGapThreshold(int datasetIndex) { if (definition.isStacked() && (datasetIndex == 0)) { return; } - else { - long start = System.nanoTime(); - XYPlot plot = chart.getXYPlot(); + long start = System.nanoTime(); - if (plot.getDataset(datasetIndex).getItemCount(0) > 0) { - DataTupleXYDataset dataset = (DataTupleXYDataset) plot.getDataset(datasetIndex); - int seriesCount = dataset.getSeriesCount(); + XYPlot plot = chart.getXYPlot(); + DataTupleXYDataset dataset = (DataTupleXYDataset) plot.getDataset(datasetIndex); + int seriesCount = dataset.getSeriesCount(); - double[] averageDistance = new double[seriesCount]; - int[] count = new int[seriesCount]; + // for each series, calculate the average distance between X values + // use the maximum of those averages as the gap threshold + double maxAverage = Double.MIN_VALUE; - double[] previousX = new double[seriesCount]; + for (int i = 0; i < seriesCount; i++) { + int itemCount = dataset.getItemCount(); - java.util.Arrays.fill(averageDistance, 0); - java.util.Arrays.fill(count, 0); - java.util.Arrays.fill(previousX, dataset.getXValue(0, 0)); + // no data; gaps are meaningless + if (itemCount == 0) { + continue; + } - for (int i = 1; i < dataset.getItemCount(0); i++) { - double currentX = dataset.getXValue(0, i); + // not all X values will have data; so count rather than using itemCount for the average + int actualCount = 0; + double averageDistance = 0; + // do not start calculations until the first value is found + double previousX = dataset.getXValue(i, 0); + double previousY = dataset.getYValue(i, 0); + + for (int j = 1; j < itemCount; j++) { + // find the first non-NaN Y value + if (Double.isNaN(previousY)) { + previousX = dataset.getXValue(i, j); + previousY = dataset.getYValue(i, j); + continue; + } - for (int j = 0; j < seriesCount; j++) { - double y = dataset.getYValue(j, i); + // only calculate if there is a Y value at this X value + double currentX = dataset.getXValue(i, j); + double currentY = dataset.getYValue(i, j); - if (!Double.isNaN(y)) { - averageDistance[j] += currentX - previousX[j]; - previousX[j] = currentX; - ++count[j]; - } - } + // find the next non-NaN Y value + if (Double.isNaN(currentY)) { + continue; } - double maxAverage = Double.MIN_VALUE; + averageDistance += currentX - previousX; + ++actualCount; - for (int i = 0; i < seriesCount; i++) { - averageDistance[i] /= count[i]; + previousX = currentX; + previousY = currentY; + } - if (averageDistance[i] > maxAverage) { - maxAverage = averageDistance[i]; - } - } + averageDistance /= actualCount; - ((StandardXYItemRenderer) plot.getRenderer(datasetIndex)).setGapThreshold(maxAverage * 1.25); - } - else { - ((StandardXYItemRenderer) plot.getRenderer()).setGapThreshold(Integer.MAX_VALUE); + if (averageDistance > maxAverage) { + maxAverage = averageDistance; } if (logger.isTraceEnabled()) { - logger.trace("complete for chart '{}', series {} in {}ms", definition.getTitle(), datasetIndex, - (System.nanoTime() - start) / 1000000.0d); + logger.trace("average gap distance is {} for chart '{}', series '{}'", averageDistance, + definition.getTitle(), dataset.getSeriesKey(i)); } } + + // no data => no gaps + if (maxAverage != Double.MIN_VALUE) { + // use the max average for all series, plus some leeway, as the threshold + ((StandardXYItemRenderer) plot.getRenderer(datasetIndex)).setGapThreshold(maxAverage * 1.25); + } + + if (logger.isDebugEnabled()) { + logger.debug("gap threshold recalculated to {} for chart '{}', series {} in {}ms", maxAverage * 1.25, + definition.getTitle(), datasetIndex, (System.nanoTime() - start) / 1E6d); + } } public void showLegends(boolean showLegends) { diff --git a/src/com/ibm/nmon/gui/chart/data/DataTupleHistogramDataset.java b/src/com/ibm/nmon/gui/chart/data/DataTupleHistogramDataset.java index cd52dd8..fafc404 100644 --- a/src/com/ibm/nmon/gui/chart/data/DataTupleHistogramDataset.java +++ b/src/com/ibm/nmon/gui/chart/data/DataTupleHistogramDataset.java @@ -43,7 +43,7 @@ public Number getY(int series, int item) { // default implementation uses a 0 to 1 scale; we want 0 to 100 if (getType() == HistogramType.RELATIVE_FREQUENCY) { - return new Double((Double) toReturn * 100); + return toReturn.doubleValue() * 100; } else { return toReturn; diff --git a/src/com/ibm/nmon/gui/file/FileLoadAction.java b/src/com/ibm/nmon/gui/file/FileLoadAction.java index 15e3ffe..9751e11 100644 --- a/src/com/ibm/nmon/gui/file/FileLoadAction.java +++ b/src/com/ibm/nmon/gui/file/FileLoadAction.java @@ -17,9 +17,8 @@ import java.util.List; /** - * Creates a JFileChooser so the user can select files to parse. Delegates actual parsing to - * {@link ParserRunner}. Directory selection is supported as is recursion into a directory - * structure. + * Creates a JFileChooser so the user can select files to parse. Delegates actual parsing to {@link ParserRunner}. + * Directory selection is supported as is recursion into a directory structure. */ public final class FileLoadAction implements ActionListener { private final JFileChooser chooser; @@ -32,6 +31,10 @@ public FileLoadAction(NMONVisualizerGui gui) { String directory = gui.getPreferences().get("lastDirectory", null); + if (directory == null) { + directory = "."; + } + chooser = new JFileChooser(directory); chooser.setAcceptAllFileFilterUsed(true); chooser.setMultiSelectionEnabled(true); @@ -79,8 +82,8 @@ public void actionPerformed(ActionEvent event) { if (!toParse.isEmpty()) { // parse files outside of the Swing event thread - new Thread(new ParserRunner(gui, toParse, timeZones.getSelectedTimeZone()), getClass().getName() - + " Parser").start(); + new Thread(new ParserRunner(gui, toParse, timeZones.getSelectedTimeZone()), + getClass().getName() + " Parser").start(); } } } diff --git a/src/com/ibm/nmon/gui/file/GUIFileChooser.java b/src/com/ibm/nmon/gui/file/GUIFileChooser.java index 00e740f..476ee18 100644 --- a/src/com/ibm/nmon/gui/file/GUIFileChooser.java +++ b/src/com/ibm/nmon/gui/file/GUIFileChooser.java @@ -14,8 +14,8 @@ import com.ibm.nmon.gui.main.NMONVisualizerGui; /** - * Base class for file selection dialogs. Supports the notion of a default file name that can be - * used when the user has not entered a file name. + * Base class for file selection dialogs. Supports the notion of a default file name that can be used when the user has + * not entered a file name. */ public class GUIFileChooser extends JFileChooser { private static final long serialVersionUID = 3483905864541979923L; @@ -42,6 +42,10 @@ public GUIFileChooser(NMONVisualizerGui gui, String title, String filename) { String directory = gui.getPreferences().get("lastSaveDirectory", null); + if (directory == null) { + directory = "."; + } + if (defaultFileName != null) { setSelectedFile(new File(directory, defaultFileName)); } diff --git a/src/com/ibm/nmon/gui/interval/BaseIntervalPanel.java b/src/com/ibm/nmon/gui/interval/BaseIntervalPanel.java index 3186d85..9d4930d 100644 --- a/src/com/ibm/nmon/gui/interval/BaseIntervalPanel.java +++ b/src/com/ibm/nmon/gui/interval/BaseIntervalPanel.java @@ -2,7 +2,6 @@ import java.awt.event.ActionListener; import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.beans.PropertyChangeListener; @@ -104,7 +103,7 @@ public void actionPerformed(ActionEvent e) { actions.put("endToStart", endToStartAction); - inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK), "endToStart"); + inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_T, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "endToStart"); } final String getIntervalName() { diff --git a/src/com/ibm/nmon/gui/interval/IntervalList.java b/src/com/ibm/nmon/gui/interval/IntervalList.java index 1a9ce65..4bd052b 100644 --- a/src/com/ibm/nmon/gui/interval/IntervalList.java +++ b/src/com/ibm/nmon/gui/interval/IntervalList.java @@ -12,7 +12,6 @@ import java.awt.event.HierarchyListener; import java.awt.event.HierarchyEvent; -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import javax.swing.BorderFactory; @@ -179,7 +178,7 @@ public void actionPerformed(ActionEvent e) { } }); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( - KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.CTRL_DOWN_MASK), "clear"); + KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "clear"); } GUITable getTable() { diff --git a/src/com/ibm/nmon/gui/interval/IntervalManagerDialog.java b/src/com/ibm/nmon/gui/interval/IntervalManagerDialog.java index 9fc2d63..2337145 100755 --- a/src/com/ibm/nmon/gui/interval/IntervalManagerDialog.java +++ b/src/com/ibm/nmon/gui/interval/IntervalManagerDialog.java @@ -2,6 +2,7 @@ import java.awt.BorderLayout; +import java.awt.Toolkit; import java.awt.KeyboardFocusManager; import java.awt.event.ActionListener; @@ -9,8 +10,6 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; - -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.beans.PropertyChangeListener; @@ -131,11 +130,11 @@ public void setEnabled(boolean b) { if (b) { ((JPanel) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .put(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK), "save"); + .put(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "save"); } else { ((JPanel) getContentPane()).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW) - .remove(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); + .remove(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); } }; }; @@ -210,7 +209,7 @@ public void setEnabled(boolean b) { actions.put("load", loadAction); actions.put("save", saveAction); - inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK), "load"); + inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "load"); } @Override diff --git a/src/com/ibm/nmon/gui/main/MainMenu.java b/src/com/ibm/nmon/gui/main/MainMenu.java index e2138a8..ec58666 100644 --- a/src/com/ibm/nmon/gui/main/MainMenu.java +++ b/src/com/ibm/nmon/gui/main/MainMenu.java @@ -32,6 +32,7 @@ import com.ibm.nmon.util.TimeFormatCache; import com.ibm.nmon.util.VersionInfo; +import java.awt.Toolkit; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; @@ -80,7 +81,7 @@ private JMenu createFileMenu() { JMenuItem item = new JMenuItem("Load..."); item.setMnemonic('l'); item.addActionListener(new FileLoadAction(gui)); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); menu.add(item); @@ -121,7 +122,7 @@ private JMenu createIntervalsMenu() { item.setMnemonic('a'); item.setSelected(true); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_0, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_0, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.ALT_DOWN_MASK)); item.addActionListener(new ActionListener() { @Override @@ -142,7 +143,7 @@ public void actionPerformed(ActionEvent e) { item.getActionMap().put("doClick", doClick); InputMap inputMap = item.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK), + inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.ALT_DOWN_MASK), "doClick"); group.add(item); @@ -161,10 +162,10 @@ public void actionPerformed(ActionEvent e) { inputMap = item.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_0 + n, - InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK), "doClick"); + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.ALT_DOWN_MASK), "doClick"); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0 + n, - InputEvent.CTRL_DOWN_MASK | InputEvent.ALT_DOWN_MASK), "doClick"); + Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.ALT_DOWN_MASK), "doClick"); ++n; } @@ -191,7 +192,7 @@ public void actionPerformed(ActionEvent e) { item.setMnemonic('r'); item.setIcon(Styles.CLEAR_ICON); item.addActionListener(new RemoveAllIntervalsAction(gui, gui.getMainFrame())); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); menu.add(item); } @@ -212,7 +213,7 @@ public void actionPerformed(ActionEvent e) { new IntervalManagerDialog(gui).setVisible(true); } }); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); menu.add(item); @@ -225,7 +226,7 @@ private JMenu createViewMenu() { JMenuItem item = new JMenuItem("Set Granularity..."); item.setMnemonic('g'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.addActionListener(new ActionListener() { @Override @@ -245,7 +246,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem("Table Columns..."); item.setMnemonic('c'); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); item.addActionListener(new ActionListener() { @Override @@ -259,7 +260,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem("Custom Format..."); item.setMnemonic('f'); item.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_F, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); item.addActionListener(new ActionListener() { @Override @@ -299,7 +300,7 @@ public void actionPerformed(ActionEvent e) { checkItem.setMnemonic('l'); checkItem.setSelected(gui.getBooleanProperty("lineChartLegend")); checkItem.setAccelerator( - KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK)); + KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK)); checkItem.addActionListener(new ActionListener() { @Override @@ -349,7 +350,7 @@ public void actionPerformed(ActionEvent e) { checkItem = new JCheckBoxMenuItem("Summary Table"); checkItem.setMnemonic('t'); checkItem.setIcon(Styles.buildIcon("table.png")); - checkItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.CTRL_DOWN_MASK)); + checkItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); checkItem.setSelected(!gui.getBooleanProperty("chartsDisplayed")); checkItem.addActionListener(new ActionListener() { @@ -362,7 +363,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem("Custom Report..."); item.setMnemonic('r'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.setIcon(Styles.REPORT_ICON); item.addActionListener(new ActionListener() { @Override @@ -523,7 +524,7 @@ else if (gui.getIntervalManager().getIntervalCount() == 0) { item = new JMenuItem("View Log..."); item.setMnemonic('l'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.setIcon(LogViewerDialog.LOG_ICON); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { @@ -709,7 +710,7 @@ public void actionPerformed(ActionEvent e) { item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog(gui.getMainFrame(), - "Copyright \u00A9 2011-2019\n" + "Copyright \u00A9 2011-2024\n" + "IBM Software Group, Collaboration Services.\nAll Rights Reserved.\n\n" + "Support is on an 'as-is', 'best-effort' basis only.\n\n" + "Version " + VersionInfo.getVersion() + "\n\n" + "Icons from " diff --git a/src/com/ibm/nmon/gui/main/SummaryView.java b/src/com/ibm/nmon/gui/main/SummaryView.java index 0f4e3bb..36f468a 100644 --- a/src/com/ibm/nmon/gui/main/SummaryView.java +++ b/src/com/ibm/nmon/gui/main/SummaryView.java @@ -19,11 +19,6 @@ import javax.swing.SwingConstants; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.axis.CategoryLabelPositions; - -import org.jfree.chart.plot.CategoryPlot; - import com.ibm.nmon.data.DataSet; import com.ibm.nmon.interval.Interval; @@ -32,7 +27,6 @@ import com.ibm.nmon.gui.Styles; import com.ibm.nmon.gui.chart.BaseChartPanel; -import com.ibm.nmon.gui.chart.builder.ChartBuilderPlugin; import com.ibm.nmon.gui.report.ReportPanel; @@ -58,18 +52,6 @@ public SummaryView(NMONVisualizerGui gui) { singleIntervalReport = new ReportPanel(gui, ReportCache.DEFAULT_SUMMARY_CHARTS_KEY); singleIntervalReport.setBorder(null); // make consistent with addBorderIfNecessary - singleIntervalReport.addPlugin(new ChartBuilderPlugin() { - @Override - public void configureChart(JFreeChart chart) { - if (chart.getPlot() instanceof CategoryPlot) { - // assume bar names will usually be hostnames - // draw them on the chart at a 45 degree angle - CategoryPlot plot = (CategoryPlot) chart.getPlot(); - plot.getDomainAxis().setCategoryLabelPositions(CategoryLabelPositions.UP_45); - } - } - }); - allIntervalsReport = new ReportPanel(gui, ReportCache.DEFAULT_INTERVAL_CHARTS_KEY); allIntervalsReport.setBorder(null); // make consistent with addBorderIfNecessary @@ -112,7 +94,7 @@ public void actionPerformed(ActionEvent e) { } }); - inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_I, InputEvent.CTRL_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK), + inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_I, java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx() | InputEvent.SHIFT_DOWN_MASK), "allIntervals"); allIntervalsReport.addPropertyChangeListener("chart", summaryTable); diff --git a/src/com/ibm/nmon/gui/report/ReportMenu.java b/src/com/ibm/nmon/gui/report/ReportMenu.java index f1dd7f3..d88ea60 100644 --- a/src/com/ibm/nmon/gui/report/ReportMenu.java +++ b/src/com/ibm/nmon/gui/report/ReportMenu.java @@ -4,9 +4,10 @@ import javax.swing.JMenu; import javax.swing.JMenuItem; +import java.awt.Toolkit; + import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import javax.swing.KeyStroke; @@ -39,7 +40,7 @@ final class ReportMenu extends JMenuBar { JMenuItem item = new JMenuItem("Load..."); item.setMnemonic('l'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -109,7 +110,7 @@ public void actionPerformed(ActionEvent e) { save = new JMenuItem("Save..."); save.setMnemonic('s'); - save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK)); + save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); save.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -124,7 +125,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem("Close"); item.setMnemonic('c'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.addActionListener(new ActionListener() { @Override @@ -142,7 +143,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem("Set Granularity..."); item.setMnemonic('g'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_G, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.addActionListener(new ActionListener() { @Override @@ -192,7 +193,7 @@ public void actionPerformed(ActionEvent e) { item = new JMenuItem("View Log..."); item.setMnemonic('l'); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, InputEvent.CTRL_DOWN_MASK)); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.setIcon(LogViewerDialog.LOG_ICON); item.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { diff --git a/src/com/ibm/nmon/gui/report/ReportPanel.java b/src/com/ibm/nmon/gui/report/ReportPanel.java index d06f71c..884b236 100755 --- a/src/com/ibm/nmon/gui/report/ReportPanel.java +++ b/src/com/ibm/nmon/gui/report/ReportPanel.java @@ -288,10 +288,6 @@ public int getPreviousTab() { return previousTab; } - public void addPlugin(ChartBuilderPlugin plugin) { - chartFactory.addPlugin(plugin); - } - @Override public void propertyChange(PropertyChangeEvent evt) { if ("chart".equals(evt.getPropertyName())) { diff --git a/src/com/ibm/nmon/gui/table/TableColumnChooser.java b/src/com/ibm/nmon/gui/table/TableColumnChooser.java index cc2f450..89b0937 100644 --- a/src/com/ibm/nmon/gui/table/TableColumnChooser.java +++ b/src/com/ibm/nmon/gui/table/TableColumnChooser.java @@ -2,10 +2,10 @@ import java.awt.event.ActionListener; import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import javax.swing.KeyStroke; +import java.awt.Toolkit; import java.awt.BorderLayout; import java.awt.Component; @@ -148,9 +148,9 @@ public void actionPerformed(ActionEvent e) { actions.put("none", noneAction); actions.put("defaults", defaultsAction); - inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK), "all"); - inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_DOWN_MASK), "none"); - inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, InputEvent.CTRL_DOWN_MASK), "defaults"); + inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "all"); + inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "none"); + inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "defaults"); setVisible(true); } diff --git a/src/com/ibm/nmon/gui/util/LogViewerDialog.java b/src/com/ibm/nmon/gui/util/LogViewerDialog.java index a360c50..7467bd3 100644 --- a/src/com/ibm/nmon/gui/util/LogViewerDialog.java +++ b/src/com/ibm/nmon/gui/util/LogViewerDialog.java @@ -6,7 +6,6 @@ import javax.swing.AbstractAction; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.ItemListener; @@ -113,7 +112,7 @@ public void actionPerformed(ActionEvent e) { ((JComponent) getComponent(0)).getActionMap().put("clear", clearAction); ((JComponent) getComponent(0)).getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put( - KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.CTRL_DOWN_MASK), "clear"); + KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()), "clear"); JPanel header = new JPanel(); header.add(logLevel); diff --git a/src/com/ibm/nmon/parser/JMeterAggregateParser.java b/src/com/ibm/nmon/parser/JMeterAggregateParser.java new file mode 100644 index 0000000..a9bb56f --- /dev/null +++ b/src/com/ibm/nmon/parser/JMeterAggregateParser.java @@ -0,0 +1,398 @@ +package com.ibm.nmon.parser; + +import org.slf4j.Logger; + +import java.io.File; +import java.io.FileReader; +import java.io.LineNumberReader; +import java.io.IOException; + +import java.text.ParseException; + +import java.util.Map; +import java.util.Set; + +import java.util.regex.Pattern; + +import com.ibm.nmon.data.BasicDataSet; +import com.ibm.nmon.data.DataRecord; +import com.ibm.nmon.data.DataType; + +import com.ibm.nmon.util.DataHelper; + +public final class JMeterAggregateParser { + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(JMeterAggregateParser.class); + + private static final Pattern DATA_SPLITTER = Pattern.compile(","); + + private static final Set REQUIRED_FIELDS; + private static final Set IGNORED_FIELDS; + + // when aggregating fields, record max or sum instead of average + private static final Set MAX_FIELDS; + private static final Set SUM_FIELDS; + + private LineNumberReader in = null; + + private BasicDataSet dataSet = null; + + // indexes into the data that require special handling + private int labelIndex = -1; + private int timestampIndex = -1; + private int successIndex = -1; + private int hostnameIndex = -1; + private int responseCodeIndex = -1; + + private final Set transactionNames = new java.util.HashSet(); + + // map the average, max or sum aggregation action to the actual parsed fields + private char[] fieldActions; + + public BasicDataSet parse(File file) throws IOException, ParseException { + return parse(file.getAbsolutePath()); + } + + public BasicDataSet parse(String filename) throws IOException, ParseException { + long start = System.nanoTime(); + + dataSet = new BasicDataSet(filename); + dataSet.setMetadata("hostname", "JMeter"); + + try { + in = new LineNumberReader(new FileReader(filename)); + + String line = in.readLine(); + + Map fieldIndexes = parseHeader(DATA_SPLITTER.split(line)); + + // for every millisecond, there will be one or more double[] for each transaction + Map> dataByMilli = parseData(fieldIndexes, in); + + LOGGER.debug("parsed {} lines into {} seconds of data", in.getLineNumber(), dataByMilli.size()); + + convertData(dataByMilli, fieldIndexes); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Parse" + " complete for {} in {}ms", dataSet.getSourceFile(), + (System.nanoTime() - start) / 1000000.0d); + } + + return dataSet; + } + finally { + in.close(); + + dataSet = null; + + labelIndex = -1; + timestampIndex = -1; + successIndex = -1; + hostnameIndex = -1; + responseCodeIndex = -1; + + transactionNames.clear(); + fieldActions = null; + } + } + + private Map parseHeader(String[] headers) throws IOException { + // used LinkedHashMap to maintain correspondence between array indexes of headers and values + Map fieldIndexes = new java.util.LinkedHashMap(headers.length); + + for (int i = 0; i < headers.length; i++) { + if (IGNORED_FIELDS.contains(headers[i])) { + continue; + } + + fieldIndexes.put(DataHelper.newString(headers[i]), i); + } + + if (REQUIRED_FIELDS.containsAll(fieldIndexes.keySet())) { + throw new IOException("header does not contain all required fields: " + REQUIRED_FIELDS); + } + + // remove these so the rest of the map can be iterated to get the rest of the data + labelIndex = fieldIndexes.remove("label"); + timestampIndex = fieldIndexes.remove("timeStamp"); + + // optionally, gather all the load driver names and add as metadata + Integer temp = fieldIndexes.remove("Hostname"); + + if (temp != null) { + hostnameIndex = temp; + } + + // do not remove; will be converted to 0/1 + temp = fieldIndexes.get("success"); + + if (temp != null) { + successIndex = temp; + } + + // if there is an exception, JMeter writes to the responseCode field + // handle that by converting to a 500 + temp = fieldIndexes.get("responseCode"); + + if (temp != null) { + responseCodeIndex = temp; + } + + fieldActions = new char[fieldIndexes.size()]; + int n = 0; + + for (String field : fieldIndexes.keySet()) { + // max, sum and average actions on aggregated data, respectively + if (MAX_FIELDS.contains(field)) { + fieldActions[n] = 'm'; + } + else if (SUM_FIELDS.contains(field)) { + fieldActions[n] = 's'; + } + else { + fieldActions[n] = 'a'; + } + + ++n; + } + + return fieldIndexes; + } + + // map every time to a set of TransactionAggregates + private Map> parseData(Map fieldIndexes, + LineNumberReader in) throws IOException { + int actualFields = fieldIndexes.size(); + int expectedFields = actualFields + 2 + (successIndex == -1 ? 0 : 1) + (hostnameIndex == -1 ? 0 : 1) + + (successIndex == -1 ? 0 : 1); + + Set hostnames = new java.util.HashSet(); + + // since multiple threads can be running, it is possible to have multiple entries at the same + // millisecond + // for every timestamp, store that data for each transaction name and aggregate it + Map> dataBySecond = new java.util.HashMap>(); + String line = null; + + while ((line = in.readLine()) != null) { + String[] data = DATA_SPLITTER.split(line); + + if (data.length < expectedFields) { + LOGGER.warn("skipping invalid data record '{}' at line {}; " + "expected at least {} fields", line, + in.getLineNumber(), expectedFields); + continue; + } + + if (hostnameIndex > -1) { + hostnames.add(data[hostnameIndex]); + } + + long time = 0; + double[] values = new double[actualFields]; + + // round time to the nearest second + try { + time = Math.round(Long.parseLong(data[timestampIndex]) / 1000.0d) * 1000; + } + catch (NumberFormatException nfe) { + LOGGER.warn("skipping invalid data record '{}' at line {}; " + "invalid timeStamp '{}'", line, + in.getLineNumber(), data[timestampIndex]); + continue; + } + + String transactionName = data[labelIndex]; + transactionNames.add(transactionName); + + int n = 0; + + for (int idx : fieldIndexes.values()) { + if (idx == successIndex) { // convert success to 0 or 1 so throughput can be calculated from + // successes + values[n++] = Boolean.parseBoolean(data[idx]) ? 1 : 0; + } + else { + try { + values[n++] = Double.parseDouble(data[idx]); + } + catch (NumberFormatException nfe) { + if (idx == responseCodeIndex) { + values[n] = 500; + continue; + } + LOGGER.warn("skipping invalid data record '{}' at line {}; " + "invalid number '{}'", line, + in.getLineNumber(), data[idx]); + break; + } + } + } + + Map timeData = dataBySecond.get(time); + + if (timeData == null) { // new time; add current values + timeData = new java.util.HashMap(transactionNames.size()); + + timeData.put(transactionName, new TransactionAggregate(values)); + dataBySecond.put(time, timeData); + } + else { + TransactionAggregate a = timeData.get(transactionName); + + if (a == null) { // existing time, new transaction; add current values + timeData.put(transactionName, new TransactionAggregate(values)); + } + else { // update aggregated data with new values + a.aggregate(values); + } + } + } + + if (!hostnames.isEmpty()) { + dataSet.setMetadata("loadDrivers", hostnames.toString()); + } + + return dataBySecond; + } + + private void convertData(Map> dataByMilli, + Map fieldIndexes) { + String[] transactions = transactionNames.toArray(new String[transactionNames.size()]); + int actualFields = fieldIndexes.size(); + // deal with memory savings now that all names are known + for (int i = 0; i < transactions.length; i++) { + transactions[i] = DataHelper.newString(transactions[i]); + } + + // create a data type for each field in the CSV + // every type will have the transaction names as fields + for (String field : fieldIndexes.keySet()) { + field = DataHelper.newString(field); + dataSet.addType(new DataType(field, field, transactions)); + } + + // for every millisecond of data, get each transaction + // for every transaction, calculated the aggregated value + for (Map.Entry> forSecond : dataByMilli.entrySet()) { + long time = forSecond.getKey(); + // pivot data into a transaction sized array for each field / data type + double[][] fieldsByTransaction = new double[actualFields][]; + + for (int i = 0; i < fieldsByTransaction.length; i++) { + fieldsByTransaction[i] = new double[transactions.length]; + } + + Map byTransaction = forSecond.getValue(); + + // missing transactions at each time will happen so iterate over array instead of values + for (int i = 0; i < transactions.length; i++) { + TransactionAggregate a = byTransaction.get(transactions[i]); + + // actually pivot the data; note j, then i in array assignment + if (a == null) { + // use NaN as chart data when no values exist rather than 0 + for (int j = 0; j < actualFields; j++) { + fieldsByTransaction[j][i] = Double.NaN; + } + } + else { + double[] data = a.getAggregated(); + + for (int j = 0; j < actualFields; j++) { + fieldsByTransaction[j][i] += data[j]; + } + } + } + + DataRecord record = new DataRecord(time, Long.toString(time)); + + int n = 0; + + for (String field : fieldIndexes.keySet()) { + // iteration order == array index because fieldIndexes is a LinkedHashMap + record.addData(dataSet.getType(field), fieldsByTransaction[n++]); + } + + dataSet.addRecord(record); + } + } + + static { + Set temp = new java.util.HashSet(); + + temp.add("label"); + temp.add("timeStamp"); + + REQUIRED_FIELDS = java.util.Collections.unmodifiableSet(temp); + + temp = new java.util.HashSet(); + + // in general, ignore text fields + temp.add("responseMessage"); + temp.add("failureMessage"); + temp.add("threadName"); + temp.add("dataType"); + temp.add("URL"); + temp.add("Filename"); + temp.add("Encoding"); + + IGNORED_FIELDS = java.util.Collections.unmodifiableSet(temp); + + temp = new java.util.HashSet(); + + temp.add("responseCode"); + temp.add("grpThreads"); + temp.add("allThreads"); + + MAX_FIELDS = java.util.Collections.unmodifiableSet(temp); + + temp = new java.util.HashSet(); + + temp.add("success"); + temp.add("SampleCount"); + temp.add("ErrorCount"); + + SUM_FIELDS = java.util.Collections.unmodifiableSet(temp); + + for (String field : SUM_FIELDS) { + if (MAX_FIELDS.contains(field)) { + throw new IllegalStateException("a SUM_FIELD cannot also be a MAX_FIELD"); + } + } + } + + // for each transaction, at each time, hold a running aggregation of the data + private final class TransactionAggregate { + private final double[] data; + private int count; + + TransactionAggregate(double[] data) { + this.data = data; + this.count = 1; + } + + void aggregate(double[] toAggregate) { + for (int i = 0; i < data.length; i++) { + if (fieldActions[i] == 'm') { + if (toAggregate[i] > data[i]) { + data[i] = toAggregate[i]; + } + // else ignore + } + else { // sum or average + data[i] += toAggregate[i]; + } + } + ++count; + } + + // note cannot be called more than once! + double[] getAggregated() { + for (int i = 0; i < data.length; i++) { + if (fieldActions[i] == 'a') { + data[i] /= count; + } + // else leave sum and max as-is + } + + return data; + } + } +} diff --git a/src/com/ibm/nmon/parser/NMONParser.java b/src/com/ibm/nmon/parser/NMONParser.java index 794ad4d..1741e12 100644 --- a/src/com/ibm/nmon/parser/NMONParser.java +++ b/src/com/ibm/nmon/parser/NMONParser.java @@ -10,7 +10,6 @@ import java.text.SimpleDateFormat; import java.text.ParseException; - import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -321,14 +320,15 @@ else if ("SUMMARY".equals(values[0])) { LOGGER.warn("undefined data type {} at line {}", values[0], in.getLineNumber()); return; } - + int commandIdx = values.length - 1; // command name is the last value type = data.getType(SubDataType.buildId("SUMMARY", values[commandIdx])); - if (type == null) { - type = new SubDataType("SUMMARY", values[commandIdx], "Summary of Processes", false, summaryFields); - data.addType(type); - } + if (type == null) { + type = new SubDataType("SUMMARY", values[commandIdx], "Summary of Processes", false, + summaryFields); + data.addType(type); + } // remove the trailing command String[] withoutCommand = new String[values.length - 1]; @@ -553,7 +553,11 @@ else if ("INF".equals(data)) { currentRecord.addData(type, recordData); } catch (IllegalArgumentException ile) { - // assume wrong number of columns, so add missing data in at the end + if (type.getFieldCount() == recordData.length) { + throw ile; + } + + // else assume wrong number of columns, so add missing data in at the end double[] newData = new double[type.getFieldCount()]; System.arraycopy(recordData, 0, newData, 0, recordData.length); @@ -788,6 +792,11 @@ private DataType buildDataType(String[] values) { String id = DataHelper.newString(values[0]); + if ("".equals(id)) { + LOGGER.warn("not creating data type with empty id" + " at line {} for data {}", in.getLineNumber(), + java.util.Arrays.toString(values)); + return null; + } // the type name may contain the hostname, remove it if so String name = values[1]; int idx = name.indexOf(data.getHostname()); @@ -833,11 +842,17 @@ private double[] scaleProcessDataByCPUs(ProcessDataType processType, double[] va double CPUs = fileCPUs; if (isAIX) { - DataType cpuAll = data.getType("PCPU_ALL"); + DataType lpar = data.getType("LPAR"); + if (currentRecord.hasData(lpar)) { + CPUs = currentRecord.getData(lpar, "entitled"); + } + else { + DataType cpuAll = data.getType("PCPU_ALL"); - // hasData should also cover cpuAll == null - if (currentRecord.hasData(cpuAll)) { - CPUs = currentRecord.getData(cpuAll, "Entitled Capacity"); + // hasData should also cover cpuAll == null + if (currentRecord.hasData(cpuAll)) { + CPUs = currentRecord.getData(cpuAll, "Entitled Capacity"); + } } } else { diff --git a/src/com/ibm/nmon/parser/PerfmonParser.java b/src/com/ibm/nmon/parser/PerfmonParser.java index aa698a1..25020a8 100644 --- a/src/com/ibm/nmon/parser/PerfmonParser.java +++ b/src/com/ibm/nmon/parser/PerfmonParser.java @@ -1,509 +1,528 @@ -package com.ibm.nmon.parser; - -import org.slf4j.Logger; - -import java.io.IOException; -import java.io.File; -import java.io.FileReader; -import java.io.LineNumberReader; -import java.text.SimpleDateFormat; -import java.text.ParseException; -import java.util.List; -import java.util.Map; -import java.util.TimeZone; -import java.util.regex.Pattern; -import java.util.regex.Matcher; - -import com.ibm.nmon.data.PerfmonDataSet; -import com.ibm.nmon.data.DataRecord; -import com.ibm.nmon.data.DataType; -import com.ibm.nmon.data.SubDataType; -import com.ibm.nmon.data.ProcessDataType; -import com.ibm.nmon.data.Process; -import com.ibm.nmon.data.transform.WindowsBytesTransform; -import com.ibm.nmon.data.transform.WindowsNetworkPostProcessor; -import com.ibm.nmon.data.transform.WindowsProcessPostProcessor; -import com.ibm.nmon.util.DataHelper; - -public final class PerfmonParser { - private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(PerfmonParser.class); - - private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); - - // Issue #26 \D to prevent splitting on something like \SRVXYZ\Processor Information(2,10)\% DPC Time see in Win2012 - private static final Pattern DATA_SPLITTER = Pattern.compile("\"?,\\D\"?"); - private static final Pattern SUBCATEGORY_SPLITTER = Pattern.compile(":"); - // "\\hostname\category (optional subcategory)\metric" - // note storing a matcher vs a pattern is _NOT_ thread safe - // first group is non-greedy (.*?) to allow proper parsing of strings like - // \\SYSTEM\Paging File(\??\D:\pagefile.sys)\% Usage - private static final Matcher METRIC_MATCHER = Pattern.compile("\\\\\\\\(.*?)\\\\(.*)\\\\(.*)\"?").matcher(""); - - private LineNumberReader in = null; - - private PerfmonDataSet data = null; - - private final WindowsBytesTransform bytesTransform = new WindowsBytesTransform(); - - // builders for each column - private DataTypeBuilder[] buildersByColumn; - // builders by type id - private Map buildersById = new java.util.HashMap(); - - public PerfmonDataSet parse(File file, boolean scaleProcessesByCPU) throws IOException, ParseException { - return parse(file.getAbsolutePath(), scaleProcessesByCPU); - } - - public PerfmonDataSet parse(String filename, boolean scaleProcessesByCPU) throws IOException, ParseException { - long start = System.nanoTime(); - - data = new PerfmonDataSet(filename); - data.setMetadata("OS", "Perfmon"); - - try { - in = new LineNumberReader(new FileReader(filename)); - - String line = in.readLine(); - - parseHeader(DATA_SPLITTER.split(line)); - - while ((line = in.readLine()) != null) { - parseData(DATA_SPLITTER.split(line)); - } - - long postProcessStart = System.nanoTime(); - - // post process after parsing all the data since DataTypes are built lazily - WindowsNetworkPostProcessor networkPostProcessor = new WindowsNetworkPostProcessor(); - WindowsProcessPostProcessor processPostProcessor = null; - - networkPostProcessor.addDataTypes(data); - - if (scaleProcessesByCPU) { - processPostProcessor = new WindowsProcessPostProcessor(); - processPostProcessor.addDataTypes(data); - } - - for (DataRecord record : data.getRecords()) { - networkPostProcessor.postProcess(data, record); - - if (scaleProcessesByCPU) { - processPostProcessor.postProcess(data, record); - } - } - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Post processing" + " complete for {} in {}ms", data.getSourceFile(), - (System.nanoTime() - postProcessStart) / 1000000.0d); - } - - DataHelper.aggregateProcessData(data, LOGGER); - - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Parse" + " complete for {} in {}ms", data.getSourceFile(), - (System.nanoTime() - start) / 1000000.0d); - } - - return data; - - } - finally { - in.close(); - - data = null; - - // columnTypes.clear(); - buildersById.clear(); - // processes.clear(); - buildersByColumn = null; - - bytesTransform.reset(); - } - } - - private void parseHeader(String[] header) { - buildersByColumn = new DataTypeBuilder[header.length]; - - // remove trailing " or , - String lastData = header[header.length - 1]; - - if (lastData.endsWith("\"")) { - header[header.length - 1] = lastData.substring(0, lastData.length() - 1); - } - else if (lastData.endsWith(",")) { - header[header.length - 1] = lastData.substring(0, lastData.length() - 2); - } - - // parse out the timezone in a format like (PDH-CSV 4.0) (GMT Daylight Time)(-60) - int idx = header[0].lastIndexOf('('); - - if (idx == -1) { - LOGGER.warn("version header '{0}' is not in the right format, the time zone will default to UTC", header[0]); - TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - } - else { - String temp = header[0].substring(idx + 1, header[0].length() - 1); - - try { - // timezone format in negative minutes from UTC - double offset = Integer.parseInt(temp) / -60.0d; - - TIMESTAMP_FORMAT.setTimeZone(new java.util.SimpleTimeZone((int) (offset * 3600000), temp)); - } - catch (NumberFormatException nfe) { - LOGGER.warn("version header '{0}' is not in the right format, the time zone will default to UTC", - header[0]); - TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - } - } - - // timestamp does not belong to a category - // columnTypes.add(null); - buildersByColumn[0] = null; - - // read the first column to get the hostname - METRIC_MATCHER.reset(header[1]); - - if (METRIC_MATCHER.matches()) { - // assume hostname does not change - data.setHostname(METRIC_MATCHER.group(1).toLowerCase()); - } - else { - throw new IllegalArgumentException("hostname not found in '" + header[1] + "'"); - } - - for (int i = 1; i < header.length; i++) { - METRIC_MATCHER.reset(header[i]); - - if (!METRIC_MATCHER.matches()) { - LOGGER.warn("'{}' is not a valid header column", header[i]); - buildersByColumn[i] = null; - continue; - } - - // looking for type id (sub type id) - String toParse = METRIC_MATCHER.group(2); - - String uniqueId = null; - String id = null; - String subId = null; - - idx = toParse.indexOf('('); - - if (idx != -1) { // has sub type - int endIdx = toParse.indexOf(')', idx + 1); - - if (endIdx == -1) { - LOGGER.warn("no end parentheses found in header column '{}'", toParse); - // columnTypes.add(null); - buildersByColumn[i] = null; - continue; - } - else { - id = DataHelper.newString(toParse.substring(0, idx)); - subId = DataHelper.newString(parseSubId(id, toParse.substring(idx + 1, endIdx))); - uniqueId = SubDataType.buildId(id, subId); - } - } - else { - id = uniqueId = DataHelper.newString(toParse); - } - - String field = parseField(id, METRIC_MATCHER.group(3)); - - DataTypeBuilder builder = buildersById.get(uniqueId); - - if (builder == null) { - builder = new DataTypeBuilder(uniqueId, id, subId); - buildersById.put(uniqueId, builder); - } - - if (data.getTypeIdPrefix().equals(id)) { // Process - // skip Total and Idle processes - if ("Idle".equals(subId) || "Total".equals(subId)) { - buildersByColumn[i] = null; - } - // skip ID Process field but use is as the process id - else if ("ID Process".equals(field)) { - buildersByColumn[i] = null; - builder.setProcessIdColumn(i); - } - else { - buildersByColumn[i] = builder; - builder.addField(field); - } - } - else { - buildersByColumn[i] = builder; - builder.addField(field); - } - } - } - - private void parseData(String[] rawData) { - if (rawData.length != buildersByColumn.length) { - LOGGER.warn("invalid number of data columns at line {}, this data will be skipped", in.getLineNumber()); - return; - } - - // remove trailing " or , - String lastData = rawData[rawData.length - 1]; - - if (lastData.endsWith("\"")) { - rawData[rawData.length - 1] = lastData.substring(0, lastData.length() - 1); - } - else if (lastData.endsWith(",")) { - rawData[rawData.length - 1] = lastData.substring(0, lastData.length() - 2); - } - - // remove leading " on timestamp - String timestamp = DataHelper.newString(rawData[0].substring(1)); - long time = 0; - - try { - time = TIMESTAMP_FORMAT.parse(timestamp).getTime(); - } - catch (ParseException pe) { - LOGGER.warn("invalid timestamp format at line {}, this data will be skipped", in.getLineNumber()); - return; - } - - Map dataByType = new java.util.HashMap(); - - for (int i = 1; i < rawData.length; i++) { - DataTypeBuilder builder = buildersByColumn[i]; - - if (builder == null) { - continue; - } - else { - DataHolder holder = dataByType.get(builder.unique); - - if (holder == null) { - holder = new DataHolder(builder.fields.size()); - - dataByType.put(builder.unique, holder); - } - - holder.add(parseDouble(rawData[i])); - } - } - - DataRecord record = new DataRecord(time, timestamp); - - for (String unique : dataByType.keySet()) { - DataTypeBuilder builder = buildersById.get(unique); - DataHolder holder = dataByType.get(unique); - - DataType type = builder.build(time, rawData); - - double[] values = holder.data; - - if (bytesTransform.isValidFor(builder.id, builder.subId)) { - if (type.hasField("% Used Space")) { - int idx = type.getFieldIndex("% Used Space"); - - values[idx] = 100 - values[idx]; - } - - values = bytesTransform.transform(type, values); - } - - record.addData(type, values); - } - - data.addRecord(record); - } - - private String parseSubId(String id, String toParse) { - // some ESXTop data need special handling - if ("Interrupt Vector".equals(id)) { - String[] split = SUBCATEGORY_SPLITTER.split(toParse); - - // interrupt id - return split[0]; - } - else if (id.startsWith("Group")) { - // remove leading process id - String[] split = SUBCATEGORY_SPLITTER.split(toParse); - - return split[1]; - } - else if ("Vcpu".equals(id)) { - // remove leading process id - String[] split = SUBCATEGORY_SPLITTER.split(toParse); - - return split[1]; - } - else if (toParse.charAt(0) == '_') { - // _Total = Total - return toParse.substring(1); - } - else { - return toParse; - } - } - - private double parseDouble(String value) { - // assume start with space, whole string is space (i.e. empty) - if (value.charAt(0) == ' ') { - return Double.NaN; - } - else { - return Double.parseDouble(value); - } - } - - private String parseField(String id, String toParse) { - if ("Interrupt Vector".equals(id)) { - String[] split = SUBCATEGORY_SPLITTER.split(toParse); - - // total stats for interrupt - if (split.length > 1) { - return DataHelper.newString(split[1]); - } - else { - return toParse; - } - } - else { - return toParse; - } - } - - // builder class for DataTypes - // needed due to Perfmon interleaving Process data columns - // Processes also need to be created with a start time and pid are unknown until data is parsed - private final class DataTypeBuilder { - // id + subId, used for hashCode and equals - private final String unique; - - private final String id; - private final String subId; - - // possible column mapping for ID Process column - private int processIdColumn = -1; - - private final List fields = new java.util.ArrayList(); - - private DataType type; - - DataTypeBuilder(String unique, String id, String subId) { - this.unique = unique; - - this.id = id; - this.subId = subId; - } - - void addField(String field) { - // assume no duplicates will happen - fields.add(field); - } - - void setProcessIdColumn(int processIdColumn) { - this.processIdColumn = processIdColumn; - } - - @Override - public int hashCode() { - return unique.hashCode(); - } - - @Override - public boolean equals(Object o) { - return unique.equals(o); - } - - DataType build(long startTime, String[] rawData) { - if (type != null) { - return type; - } - - String[] fieldsArray = new String[fields.size()]; - fields.toArray(fieldsArray); - - if (data.getTypeIdPrefix().equals(id)) { // Process - int pid = (int) (processIdColumn != -1 ? parseDouble(rawData[processIdColumn]) : 0); - String processName = subId; // store processes with full name - - // parse out pid, if available via - // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PerfProc\Performance - // ProcessNameFormat=2 - int idx = processName.indexOf('_'); - - if (idx != -1) { - String temp = processName.substring(idx + 1, processName.length()); - - try { - pid = Integer.parseInt(temp); - processName = DataHelper.newString(processName.substring(0, idx)); - } - catch (NumberFormatException nfe) { - LOGGER.warn("invalid pid {} at line {}; using {} instead", temp, in.getLineNumber(), pid); - } - } - else { - idx = processName.indexOf('#'); - - if (idx != -1) { - processName = DataHelper.newString(processName.substring(0, idx)); - } - } - - if (pid == 0) { - // artificial process id - pid = data.getProcessCount() + 1; - } - - Process process = new Process(pid, startTime, processName, data.getTypeIdPrefix()); - data.addProcess(process); - - type = new ProcessDataType(process, fieldsArray); - } - else { - String name = SubDataType.buildId(id, subId); - - if (bytesTransform.isValidFor(id, subId)) { - // cannot use a DataTransform for disk free -> disk used since disks also need - // to have WindowsBytesTransform applied - if ("LogicalDisk".equals(id) || "PhysicalDisk".equals(id)) { - for (int i = 0; i < fieldsArray.length; i++) { - String field = fieldsArray[i]; - - if ("% Free Space".equals(field)) { - fieldsArray[i] = "% Used Space"; - } - } - } - - type = bytesTransform.buildDataType(id, subId, name, fieldsArray); - } - else { - if (subId == null) { - type = new DataType(id, name, fieldsArray); - } - else { - type = new SubDataType(id, subId, name, fieldsArray); - } - } - } - - data.addType(type); - return type; - } - } - - // simple holder for field data as it is being read - private final class DataHolder { - private int nextIdx = 0; - private final double[] data; - - DataHolder(int size) { - data = new double[size]; - } - - void add(double d) { - data[nextIdx++] = d; - } - } -} +package com.ibm.nmon.parser; + +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.File; +import java.io.FileReader; +import java.io.LineNumberReader; +import java.text.SimpleDateFormat; +import java.text.ParseException; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import com.ibm.nmon.data.PerfmonDataSet; +import com.ibm.nmon.data.DataRecord; +import com.ibm.nmon.data.DataType; +import com.ibm.nmon.data.SubDataType; +import com.ibm.nmon.data.ProcessDataType; +import com.ibm.nmon.data.Process; +import com.ibm.nmon.data.transform.WindowsBytesTransform; +import com.ibm.nmon.data.transform.WindowsNetworkPostProcessor; +import com.ibm.nmon.data.transform.WindowsProcessPostProcessor; +import com.ibm.nmon.util.DataHelper; + +public final class PerfmonParser { + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(PerfmonParser.class); + + private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); + + // older versions of Windows output CSV without " + private static final Pattern DATA_SPLITTER = Pattern.compile(","); + private static final Pattern DATA_SPLITTER_QUOTES = Pattern.compile("\",\""); + + private static final Pattern SUBCATEGORY_SPLITTER = Pattern.compile(":"); + // "\\hostname\category (optional subcategory)\metric" + // note storing a matcher vs a pattern is _NOT_ thread safe + // first group is non-greedy (.*?) to allow proper parsing of strings like + // \\SYSTEM\Paging File(\??\D:\pagefile.sys)\% Usage + private static final Matcher METRIC_MATCHER = Pattern.compile("\\\\\\\\(.*?)\\\\(.*)\\\\(.*)\"?").matcher(""); + + private LineNumberReader in = null; + + private PerfmonDataSet data = null; + + private final WindowsBytesTransform bytesTransform = new WindowsBytesTransform(); + + // builders for each column + private DataTypeBuilder[] buildersByColumn; + // builders by type id + private Map buildersById = new java.util.HashMap(); + + public PerfmonDataSet parse(File file, boolean scaleProcessesByCPU) throws IOException, ParseException { + return parse(file.getAbsolutePath(), scaleProcessesByCPU); + } + + public PerfmonDataSet parse(String filename, boolean scaleProcessesByCPU) throws IOException, ParseException { + long start = System.nanoTime(); + + data = new PerfmonDataSet(filename); + data.setMetadata("OS", "Perfmon"); + + try { + in = new LineNumberReader(new FileReader(filename)); + + String line = in.readLine(); + + // assume all columns will be quoted if the first one is + Pattern splitter = null; + if (line.startsWith("\"")) { + splitter = DATA_SPLITTER_QUOTES; + } + else { + splitter = DATA_SPLITTER; + } + + parseHeader(splitter.split(line)); + + while ((line = in.readLine()) != null) { + parseData(splitter.split(line)); + } + + long postProcessStart = System.nanoTime(); + + // post process after parsing all the data since DataTypes are built lazily + WindowsNetworkPostProcessor networkPostProcessor = new WindowsNetworkPostProcessor(); + WindowsProcessPostProcessor processPostProcessor = null; + + networkPostProcessor.addDataTypes(data); + + if (scaleProcessesByCPU) { + processPostProcessor = new WindowsProcessPostProcessor(); + processPostProcessor.addDataTypes(data); + } + + for (DataRecord record : data.getRecords()) { + networkPostProcessor.postProcess(data, record); + + if (scaleProcessesByCPU) { + processPostProcessor.postProcess(data, record); + } + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Post processing" + " complete for {} in {}ms", data.getSourceFile(), + (System.nanoTime() - postProcessStart) / 1000000.0d); + } + + DataHelper.aggregateProcessData(data, LOGGER); + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Parse" + " complete for {} in {}ms", data.getSourceFile(), + (System.nanoTime() - start) / 1000000.0d); + } + + return data; + + } + finally { + in.close(); + + data = null; + + // columnTypes.clear(); + buildersById.clear(); + // processes.clear(); + buildersByColumn = null; + + bytesTransform.reset(); + } + } + + private void parseHeader(String[] header) { + buildersByColumn = new DataTypeBuilder[header.length]; + + // remove trailing " or , + String lastData = header[header.length - 1]; + + if (lastData.endsWith("\"")) { + header[header.length - 1] = lastData.substring(0, lastData.length() - 1); + } + else if (lastData.endsWith(",")) { + header[header.length - 1] = lastData.substring(0, lastData.length() - 2); + } + + // parse out the timezone in a format like (PDH-CSV 4.0) (GMT Daylight Time)(-60) + int idx = header[0].lastIndexOf('('); + + if (idx == -1) { + LOGGER.warn("version header '{0}' is not in the right format, the time zone will default to UTC", + header[0]); + TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + else { + String temp = header[0].substring(idx + 1, header[0].length() - 1); + + try { + // timezone format in negative minutes from UTC + double offset = Integer.parseInt(temp) / -60.0d; + + TIMESTAMP_FORMAT.setTimeZone(new java.util.SimpleTimeZone((int) (offset * 3600000), temp)); + } + catch (NumberFormatException nfe) { + LOGGER.warn("version header '{0}' is not in the right format, the time zone will default to UTC", + header[0]); + TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + } + + // timestamp does not belong to a category + // columnTypes.add(null); + buildersByColumn[0] = null; + + // read the first column to get the hostname + METRIC_MATCHER.reset(header[1]); + + if (METRIC_MATCHER.matches()) { + // assume hostname does not change + data.setHostname(METRIC_MATCHER.group(1).toLowerCase()); + } + else { + throw new IllegalArgumentException("hostname not found in '" + header[1] + "'"); + } + + for (int i = 1; i < header.length; i++) { + METRIC_MATCHER.reset(header[i]); + + if (!METRIC_MATCHER.matches()) { + LOGGER.warn("'{}' is not a valid header column", header[i]); + buildersByColumn[i] = null; + continue; + } + + // looking for type id (sub type id) + String toParse = METRIC_MATCHER.group(2); + + String uniqueId = null; + String id = null; + String subId = null; + + idx = toParse.indexOf('('); + + if (idx != -1) { // has sub type + int endIdx = toParse.indexOf(')', idx + 1); + + if (endIdx == -1) { + LOGGER.warn("no end parentheses found in header column '{}'", toParse); + // columnTypes.add(null); + buildersByColumn[i] = null; + continue; + } + else { + id = DataHelper.newString(toParse.substring(0, idx)); + subId = DataHelper.newString(parseSubId(id, toParse.substring(idx + 1, endIdx))); + uniqueId = SubDataType.buildId(id, subId); + } + } + else { + id = uniqueId = DataHelper.newString(toParse); + } + + String field = parseField(id, METRIC_MATCHER.group(3)); + + DataTypeBuilder builder = buildersById.get(uniqueId); + + if (builder == null) { + builder = new DataTypeBuilder(uniqueId, id, subId); + buildersById.put(uniqueId, builder); + } + + if (data.getTypeIdPrefix().equals(id)) { // Process + // skip Total and Idle processes + if ("Idle".equals(subId) || "Total".equals(subId)) { + buildersByColumn[i] = null; + } + // skip ID Process field but use is as the process id + else if ("ID Process".equals(field)) { + buildersByColumn[i] = null; + builder.setProcessIdColumn(i); + } + else { + buildersByColumn[i] = builder; + builder.addField(field); + } + } + else { + buildersByColumn[i] = builder; + builder.addField(field); + } + } + } + + private void parseData(String[] rawData) { + if (rawData.length != buildersByColumn.length) { + LOGGER.warn("invalid number of data columns at line {}, this data will be skipped", in.getLineNumber()); + return; + } + + // remove trailing " or , + String lastData = rawData[rawData.length - 1]; + + if (lastData.endsWith("\"")) { + rawData[rawData.length - 1] = lastData.substring(0, lastData.length() - 1); + } + else if (lastData.endsWith(",")) { + rawData[rawData.length - 1] = lastData.substring(0, lastData.length() - 2); + } + + // remove leading " on timestamp + String timestamp = DataHelper.newString(rawData[0].substring(1)); + long time = 0; + + try { + time = TIMESTAMP_FORMAT.parse(timestamp).getTime(); + } + catch (ParseException pe) { + LOGGER.warn("invalid timestamp format at line {}, this data will be skipped", in.getLineNumber()); + return; + } + + Map dataByType = new java.util.HashMap(); + + for (int i = 1; i < rawData.length; i++) { + DataTypeBuilder builder = buildersByColumn[i]; + + if (builder == null) { + continue; + } + else { + DataHolder holder = dataByType.get(builder.unique); + + if (holder == null) { + holder = new DataHolder(builder.fields.size()); + + dataByType.put(builder.unique, holder); + } + + try { + holder.add(parseDouble(rawData[i])); + } + catch (NumberFormatException nfe) { + LOGGER.warn("invalid double '{}' at line {}, column {}; it will be NaN", rawData[i], + in.getLineNumber(), i + 1); + holder.add(Double.NaN); + } + } + } + + DataRecord record = new DataRecord(time, timestamp); + + for (String unique : dataByType.keySet()) { + DataTypeBuilder builder = buildersById.get(unique); + DataHolder holder = dataByType.get(unique); + + DataType type = builder.build(time, rawData); + + double[] values = holder.data; + + if (bytesTransform.isValidFor(builder.id, builder.subId)) { + if (type.hasField("% Used Space")) { + int idx = type.getFieldIndex("% Used Space"); + + values[idx] = 100 - values[idx]; + } + + values = bytesTransform.transform(type, values); + } + + record.addData(type, values); + } + + data.addRecord(record); + } + + private String parseSubId(String id, String toParse) { + // some ESXTop data need special handling + if ("Interrupt Vector".equals(id)) { + String[] split = SUBCATEGORY_SPLITTER.split(toParse); + + // interrupt id + return split[0]; + } + else if (id.startsWith("Group")) { + // remove leading process id + String[] split = SUBCATEGORY_SPLITTER.split(toParse); + + return split[1]; + } + else if ("Vcpu".equals(id)) { + // remove leading process id + String[] split = SUBCATEGORY_SPLITTER.split(toParse); + + return split[1]; + } + else if (toParse.charAt(0) == '_') { + // _Total = Total + return toParse.substring(1); + } + else { + return toParse; + } + } + + private double parseDouble(String value) { + // assume start with space, whole string is space (i.e. empty) + if (value.isEmpty() || (value.charAt(0) == ' ')) { + return Double.NaN; + } + else { + return Double.parseDouble(value); + } + } + + private String parseField(String id, String toParse) { + if ("Interrupt Vector".equals(id)) { + String[] split = SUBCATEGORY_SPLITTER.split(toParse); + + // total stats for interrupt + if (split.length > 1) { + return DataHelper.newString(split[1]); + } + else { + return toParse; + } + } + else { + return toParse; + } + } + + // builder class for DataTypes + // needed due to Perfmon interleaving Process data columns + // Processes also need to be created with a start time and pid are unknown until data is parsed + private final class DataTypeBuilder { + // id + subId, used for hashCode and equals + private final String unique; + + private final String id; + private final String subId; + + // possible column mapping for ID Process column + private int processIdColumn = -1; + + private final List fields = new java.util.ArrayList(); + + private DataType type; + + DataTypeBuilder(String unique, String id, String subId) { + this.unique = unique; + + this.id = id; + this.subId = subId; + } + + void addField(String field) { + // assume no duplicates will happen + fields.add(field); + } + + void setProcessIdColumn(int processIdColumn) { + this.processIdColumn = processIdColumn; + } + + @Override + public int hashCode() { + return unique.hashCode(); + } + + @Override + public boolean equals(Object o) { + return unique.equals(o); + } + + DataType build(long startTime, String[] rawData) { + if (type != null) { + return type; + } + + String[] fieldsArray = new String[fields.size()]; + fields.toArray(fieldsArray); + + if (data.getTypeIdPrefix().equals(id)) { // Process + int pid = (int) (processIdColumn != -1 ? parseDouble(rawData[processIdColumn]) : 0); + String processName = subId; // store processes with full name + + // parse out pid, if available via + // HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\PerfProc\Performance + // ProcessNameFormat=2 + int idx = processName.indexOf('_'); + + if (idx != -1) { + String temp = processName.substring(idx + 1, processName.length()); + + try { + pid = Integer.parseInt(temp); + processName = DataHelper.newString(processName.substring(0, idx)); + } + catch (NumberFormatException nfe) { + // process name might contain _ + // ignore and continue parsing pid using other methods + } + } + + idx = processName.indexOf('#'); + + if (idx != -1) { + processName = DataHelper.newString(processName.substring(0, idx)); + } + + if (pid == 0) { + // artificial process id + pid = data.getProcessCount() + 1; + } + + Process process = new Process(pid, startTime, processName, data.getTypeIdPrefix() + " " + processName); + data.addProcess(process); + + type = new ProcessDataType(process, fieldsArray); + } + else { + String name = SubDataType.buildId(id, subId); + + if (bytesTransform.isValidFor(id, subId)) { + // cannot use a DataTransform for disk free -> disk used since disks also need + // to have WindowsBytesTransform applied + if ("LogicalDisk".equals(id) || "PhysicalDisk".equals(id)) { + for (int i = 0; i < fieldsArray.length; i++) { + String field = fieldsArray[i]; + + if ("% Free Space".equals(field)) { + fieldsArray[i] = "% Used Space"; + } + } + } + + type = bytesTransform.buildDataType(id, subId, name, fieldsArray); + } + else { + if (subId == null) { + type = new DataType(id, name, fieldsArray); + } + else { + type = new SubDataType(id, subId, name, fieldsArray); + } + } + } + + data.addType(type); + return type; + } + } + + // simple holder for field data as it is being read + private final class DataHolder { + private int nextIdx = 0; + private final double[] data; + + DataHolder(int size) { + data = new double[size]; + } + + void add(double d) { + data[nextIdx++] = d; + } + } +} diff --git a/src/com/ibm/nmon/parser/gc/state/Initialized.java b/src/com/ibm/nmon/parser/gc/state/Initialized.java index c284bfd..fa38f58 100644 --- a/src/com/ibm/nmon/parser/gc/state/Initialized.java +++ b/src/com/ibm/nmon/parser/gc/state/Initialized.java @@ -1,67 +1,57 @@ -package com.ibm.nmon.parser.gc.state; - -import com.ibm.nmon.parser.gc.GCParserContext; - -final class Initialized extends GCStateWithParent { - Initialized(GCState parent) { - super("initialized", parent); - } - - public GCState startElement(GCParserContext context, String elementName, String unparsedAttributes) { - if ("attribute".equals(elementName)) { - // save attributes directly as metadata - context.parseAttributes(unparsedAttributes); - - String name = context.getAttribute("name"); - String value = context.getAttribute("value"); - - if (name == null) { - context.logMissingAttribute("name"); - } - else if (value == null) { - context.logMissingAttribute("value"); - } - else { - context.getData().setMetadata(name, value); - - if ("gcPolicy".equals(name)) { - context.setGencon("-Xgcpolicy:gencon".equals(value) || "-Xgcpolicy:balanced".equals(value)); - } - } - } - else if ("vmarg".equals(elementName)) { - // parse vmargs and save the values to a single string - context.parseAttributes(unparsedAttributes); - - String name = context.getAttribute("name"); - - if (name == null) { - context.logMissingAttribute("name"); - } - else { - String vmargs = context.getData().getMetadata("vmargs"); - - if (vmargs == null) { - vmargs = name; - } - else { - vmargs += "\n\t" + name; - } - - context.getData().setMetadata("vmargs", vmargs); - } - } - - return this; - } - - @Override - public GCState endElement(GCParserContext context, String elementName) { - if (transitionElement.equals(elementName)) { - return parent; - } - else { - return this; - } - } -} +package com.ibm.nmon.parser.gc.state; + +import com.ibm.nmon.parser.gc.GCParserContext; + +final class Initialized extends GCStateWithParent { + Initialized(GCState parent) { + super("initialized", parent); + } + + public GCState startElement(GCParserContext context, String elementName, String unparsedAttributes) { + if ("attribute".equals(elementName)) { + // save attributes directly as metadata + context.parseAttributes(unparsedAttributes); + + String name = context.getAttribute("name"); + String value = context.getAttribute("value"); + + if (name == null) { + context.logMissingAttribute("name"); + } + else if (value == null) { + context.logMissingAttribute("value"); + } + else { + context.getData().setMetadata(name, value); + + if ("gcPolicy".equals(name)) { + context.setGencon("-Xgcpolicy:gencon".equals(value) || "-Xgcpolicy:balanced".equals(value)); + } + } + } + else if ("vmarg".equals(elementName)) { + // parse vmargs and save the values to a single string + context.parseAttributes(unparsedAttributes); + + String name = context.getAttribute("name"); + + if (name == null) { + context.logMissingAttribute("name"); + } + else { + String vmargs = context.getData().getMetadata("vmargs"); + + if (vmargs == null) { + vmargs = name; + } + else { + vmargs += "\n\t" + name; + } + + context.getData().setMetadata("vmargs", vmargs); + } + } + + return this; + } +} diff --git a/src/com/ibm/nmon/parser/gc/state/JavaGCCycle.java b/src/com/ibm/nmon/parser/gc/state/JavaGCCycle.java index 5cbb582..14df351 100644 --- a/src/com/ibm/nmon/parser/gc/state/JavaGCCycle.java +++ b/src/com/ibm/nmon/parser/gc/state/JavaGCCycle.java @@ -1,109 +1,113 @@ -package com.ibm.nmon.parser.gc.state; - -import com.ibm.nmon.data.DataRecord; -import com.ibm.nmon.data.DataType; -import com.ibm.nmon.parser.gc.GCParserContext; - -abstract class JavaGCCycle implements GCState { - - protected boolean beforeGC; - protected boolean error; - - public JavaGCCycle() { - reset(); - } - - @Override - public void reset() { - this.error = false; - this.beforeGC = true; - } - - /** - * Convert free and total bytes to KB then calculate used KB. - */ - protected final void calculateSizes(GCParserContext context, String type, String freeAttribute, - String totalAttribute) { - String typeId = beforeGC ? "GCBEF" : "GCAFT"; - - double free = context.parseDouble(freeAttribute) / 1024.0 / 1024.0; - double total = context.parseDouble(totalAttribute) / 1024.0 / 1024.0; - double used = total - free; - - context.setValue(typeId, "free_" + type, free); - context.setValue(typeId, "used_" + type, used); - context.setValue(typeId, "total_" + type, total); - } - - /** - * Calculate total values from the nursery and tenured values. - */ - protected final void calculateTotalSizes(GCParserContext context) { - DataType type = context.getDataType("GCBEF"); - DataRecord currentRecord = context.getCurrentRecord(); - - double freeNurseryBefore = currentRecord.getData(type, "free_nursery"); - double usedNurseryBefore = currentRecord.getData(type, "used_nursery"); - double totalNurseryBefore = currentRecord.getData(type, "total_nursery"); - - double freeTenuredBefore = currentRecord.getData(type, "free_tenured"); - double usedTenuredBefore = currentRecord.getData(type, "used_tenured"); - double totalTenuredBefore = currentRecord.getData(type, "total_tenured"); - - // if GC is not gencon, then do not add as the nursery values will be NaN - if (context.isGencon()) { - currentRecord.setValue(type, "free", freeNurseryBefore + freeTenuredBefore); - currentRecord.setValue(type, "used", usedNurseryBefore + usedTenuredBefore); - currentRecord.setValue(type, "total", totalNurseryBefore + totalTenuredBefore); - } - else { - currentRecord.setValue(type, "free", freeTenuredBefore); - currentRecord.setValue(type, "used", usedTenuredBefore); - currentRecord.setValue(type, "total", totalTenuredBefore); - } - - type = context.getDataType("GCAFT"); - - double freeNurseryAfter = currentRecord.getData(type, "free_nursery"); - double usedNurseryAfter = currentRecord.getData(type, "used_nursery"); - double totalNurseryAfter = currentRecord.getData(type, "total_nursery"); - - double freeTenuredAfter = currentRecord.getData(type, "free_tenured"); - double usedTenuredAfter = currentRecord.getData(type, "used_tenured"); - double totalTenuredAfter = currentRecord.getData(type, "total_tenured"); - - if (context.isGencon()) { - currentRecord.setValue(type, "free", freeNurseryAfter + freeTenuredAfter); - currentRecord.setValue(type, "used", usedNurseryAfter + usedTenuredAfter); - currentRecord.setValue(type, "total", totalNurseryAfter + totalTenuredAfter); - } - else { - currentRecord.setValue(type, "free", freeTenuredAfter); - currentRecord.setValue(type, "used", usedTenuredAfter); - currentRecord.setValue(type, "total", totalTenuredAfter); - } - - type = context.getDataType("GCMEM"); - - // add the requested size back if any - // the after stats are after the request has been filled - double requested = 0; - - if (currentRecord.hasData(type)) { - requested = currentRecord.getData(type, "requested") / 1024.0 / 1024.0; - } - - double nurseryFreed = freeNurseryAfter - freeNurseryBefore + requested; - double tenuredFreed = freeTenuredAfter - freeTenuredBefore + requested; - - currentRecord.setValue(type, "nursery_freed", nurseryFreed); - currentRecord.setValue(type, "tenured_freed", tenuredFreed); - - if (context.isGencon()) { - currentRecord.setValue(type, "total_freed", nurseryFreed + tenuredFreed); - } - else { - currentRecord.setValue(type, "total_freed", tenuredFreed); - } - } +package com.ibm.nmon.parser.gc.state; + +import com.ibm.nmon.data.DataRecord; +import com.ibm.nmon.data.DataType; +import com.ibm.nmon.parser.gc.GCParserContext; + +abstract class JavaGCCycle implements GCState { + + protected boolean beforeGC; + protected boolean error; + + public JavaGCCycle() { + reset(); + } + + @Override + public void reset() { + this.error = false; + this.beforeGC = true; + } + + /** + * Convert free and total bytes to KB then calculate used KB. + */ + protected final void calculateSizes(GCParserContext context, String type, String freeAttribute, + String totalAttribute) { + String typeId = beforeGC ? "GCBEF" : "GCAFT"; + + double free = context.parseDouble(freeAttribute) / 1024.0 / 1024.0; + double total = context.parseDouble(totalAttribute) / 1024.0 / 1024.0; + double used = total - free; + + context.setValue(typeId, "free_" + type, free); + context.setValue(typeId, "used_" + type, used); + context.setValue(typeId, "total_" + type, total); + } + + /** + * Calculate total values from the nursery and tenured values. + */ + protected final void calculateTotalSizes(GCParserContext context) { + DataType type = context.getDataType("GCBEF"); + DataRecord currentRecord = context.getCurrentRecord(); + + if (!currentRecord.hasData(type)) { + return; + } + + double freeNurseryBefore = currentRecord.getData(type, "free_nursery"); + double usedNurseryBefore = currentRecord.getData(type, "used_nursery"); + double totalNurseryBefore = currentRecord.getData(type, "total_nursery"); + + double freeTenuredBefore = currentRecord.getData(type, "free_tenured"); + double usedTenuredBefore = currentRecord.getData(type, "used_tenured"); + double totalTenuredBefore = currentRecord.getData(type, "total_tenured"); + + // if GC is not gencon, then do not add as the nursery values will be NaN + if (context.isGencon()) { + currentRecord.setValue(type, "free", freeNurseryBefore + freeTenuredBefore); + currentRecord.setValue(type, "used", usedNurseryBefore + usedTenuredBefore); + currentRecord.setValue(type, "total", totalNurseryBefore + totalTenuredBefore); + } + else { + currentRecord.setValue(type, "free", freeTenuredBefore); + currentRecord.setValue(type, "used", usedTenuredBefore); + currentRecord.setValue(type, "total", totalTenuredBefore); + } + + type = context.getDataType("GCAFT"); + + double freeNurseryAfter = currentRecord.getData(type, "free_nursery"); + double usedNurseryAfter = currentRecord.getData(type, "used_nursery"); + double totalNurseryAfter = currentRecord.getData(type, "total_nursery"); + + double freeTenuredAfter = currentRecord.getData(type, "free_tenured"); + double usedTenuredAfter = currentRecord.getData(type, "used_tenured"); + double totalTenuredAfter = currentRecord.getData(type, "total_tenured"); + + if (context.isGencon()) { + currentRecord.setValue(type, "free", freeNurseryAfter + freeTenuredAfter); + currentRecord.setValue(type, "used", usedNurseryAfter + usedTenuredAfter); + currentRecord.setValue(type, "total", totalNurseryAfter + totalTenuredAfter); + } + else { + currentRecord.setValue(type, "free", freeTenuredAfter); + currentRecord.setValue(type, "used", usedTenuredAfter); + currentRecord.setValue(type, "total", totalTenuredAfter); + } + + type = context.getDataType("GCMEM"); + + // add the requested size back if any + // the after stats are after the request has been filled + double requested = 0; + + if (currentRecord.hasData(type)) { + requested = currentRecord.getData(type, "requested") / 1024.0 / 1024.0; + } + + double nurseryFreed = freeNurseryAfter - freeNurseryBefore + requested; + double tenuredFreed = freeTenuredAfter - freeTenuredBefore + requested; + + currentRecord.setValue(type, "nursery_freed", nurseryFreed); + currentRecord.setValue(type, "tenured_freed", tenuredFreed); + + if (context.isGencon()) { + currentRecord.setValue(type, "total_freed", nurseryFreed + tenuredFreed); + } + else { + currentRecord.setValue(type, "total_freed", tenuredFreed); + } + } } \ No newline at end of file diff --git a/src/com/ibm/nmon/report/dataset_report.xml b/src/com/ibm/nmon/report/dataset_report.xml index b86eb74..b64a98a 100644 --- a/src/com/ibm/nmon/report/dataset_report.xml +++ b/src/com/ibm/nmon/report/dataset_report.xml @@ -65,7 +65,7 @@
- + diff --git a/src/com/ibm/nmon/report/jmeter_report.xml b/src/com/ibm/nmon/report/jmeter_report.xml new file mode 100644 index 0000000..46b9f9e --- /dev/null +++ b/src/com/ibm/nmon/report/jmeter_report.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/ibm/nmon/report/summary_all_intervals.xml b/src/com/ibm/nmon/report/summary_all_intervals.xml index 3b5593d..8345910 100755 --- a/src/com/ibm/nmon/report/summary_all_intervals.xml +++ b/src/com/ibm/nmon/report/summary_all_intervals.xml @@ -174,7 +174,7 @@ - + @@ -183,12 +183,12 @@ - + - + \ No newline at end of file diff --git a/src/com/ibm/nmon/report/summary_single_interval.xml b/src/com/ibm/nmon/report/summary_single_interval.xml index a16f89d..c220d7e 100644 --- a/src/com/ibm/nmon/report/summary_single_interval.xml +++ b/src/com/ibm/nmon/report/summary_single_interval.xml @@ -211,7 +211,7 @@ - + @@ -219,23 +219,36 @@ - + - - - + + - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/ibm/nmon/util/TimeZoneFactory.java b/src/com/ibm/nmon/util/TimeZoneFactory.java index 2891c2f..8733272 100644 --- a/src/com/ibm/nmon/util/TimeZoneFactory.java +++ b/src/com/ibm/nmon/util/TimeZoneFactory.java @@ -6,33 +6,61 @@ import java.util.SimpleTimeZone; /** - * Creates a list of TimeZones for use in the UI. The current implementation just creates a - * {@link SimpleTimeZone} for all 25 hours from -12 UTC to +12 UTC. + * Creates a list of TimeZones for use in the UI. Create a + * {@link SimpleTimeZone} for all well known time zones. */ public final class TimeZoneFactory { public static final List TIMEZONES; + private static final int SECONDS_IN_MINUTE = 60; + private static final int MINUTES_IN_HOUR = 60; + private static final int MILLISECONDS_IN_SECOND = 1000; static { List temp = new java.util.ArrayList(25); - for (int i = -12; i <= 12; i++) { - String id = "UTC"; - - if (i < 0) { - id += i; - id += ":00"; - } - else if (i > 0) { - id += '+'; - id += i; - id += ":00"; - } - - temp.add(new SimpleTimeZone(i * 3600000, id)); + for (int i = -12; i <= 14; i++) { + temp.add(createTimeZone(i, 0)); + // https://en.wikipedia.org/wiki/List_of_UTC_offsets + if (i == -9 || i == -3 || i == 3 || i == 4 || i == 6 || i == 9 || i == 10) { + temp.add(createTimeZone(i, 30)); + } else if (i == 5) { + temp.add(createTimeZone(i, 30)); + temp.add(createTimeZone(i, 45)); + } else if (i == 8 || i == 12) { + temp.add(createTimeZone(i, 45)); + } } TIMEZONES = java.util.Collections.unmodifiableList(temp); } + private static SimpleTimeZone createTimeZone(int i, int extraOffsetMinutes) { + String id = "UTC"; + + int rawOffset = i * SECONDS_IN_MINUTE * MINUTES_IN_HOUR * MILLISECONDS_IN_SECOND; + + if (i != 0) { + if (i > 0) { + id += '+'; + } + + id += i; + + if (extraOffsetMinutes == 0) { + id += ":00"; + } else { + id += ":" + extraOffsetMinutes; + extraOffsetMinutes *= SECONDS_IN_MINUTE * MILLISECONDS_IN_SECOND; + if (i > 0) { + rawOffset += extraOffsetMinutes; + } else { + rawOffset -= extraOffsetMinutes; + } + } + } + + return new SimpleTimeZone(rawOffset, id); + } + private TimeZoneFactory() {} } diff --git a/src/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java b/src/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java index bbb0206..808f8e8 100644 --- a/src/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java +++ b/src/org/jfree/chart/renderer/xy/StandardXYItemRenderer.java @@ -1,1171 +1,1154 @@ -/* =========================================================== - * JFreeChart : a free chart library for the Java(tm) platform - * =========================================================== - * - * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. - * - * Project Info: http://www.jfree.org/jfreechart/index.html - * - * This library is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation; either version 2.1 of the License, or - * (at your option) any later version. - * - * This library 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 Lesser General Public - * License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, - * USA. - * - * [Java is a trademark or registered trademark of Sun Microsystems, Inc. - * in the United States and other countries.] - * - * --------------------------- - * StandardXYItemRenderer.java - * --------------------------- - * (C) Copyright 2001-2008, by Object Refinery Limited and Contributors. - * - * Original Author: David Gilbert (for Object Refinery Limited); - * Contributor(s): Mark Watson (www.markwatson.com); - * Jonathan Nash; - * Andreas Schneider; - * Norbert Kiesel (for TBD Networks); - * Christian W. Zuckschwerdt; - * Bill Kelemen; - * Nicolas Brodu (for Astrium and EADS Corporate Research - * Center); - * - * Changes: - * -------- - * 19-Oct-2001 : Version 1, based on code by Mark Watson (DG); - * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); - * 21-Dec-2001 : Added working line instance to improve performance (DG); - * 22-Jan-2002 : Added code to lock crosshairs to data points. Based on code - * by Jonathan Nash (DG); - * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG); - * 28-Mar-2002 : Added a property change listener mechanism so that the - * renderer no longer needs to be immutable (DG); - * 02-Apr-2002 : Modified to handle null values (DG); - * 09-Apr-2002 : Modified draw method to return void. Removed the translated - * zero from the drawItem method. Override the initialise() - * method to calculate it (DG); - * 13-May-2002 : Added code from Andreas Schneider to allow changing - * shapes/colors per item (DG); - * 24-May-2002 : Incorporated tooltips into chart entities (DG); - * 25-Jun-2002 : Removed redundant code (DG); - * 05-Aug-2002 : Incorporated URLs for HTML image maps into chart entities (RA); - * 08-Aug-2002 : Added discontinuous lines option contributed by - * Norbert Kiesel (DG); - * 20-Aug-2002 : Added user definable default values to be returned by - * protected methods unless overridden by a subclass (DG); - * 23-Sep-2002 : Updated for changes in the XYItemRenderer interface (DG); - * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG); - * 25-Mar-2003 : Implemented Serializable (DG); - * 01-May-2003 : Modified drawItem() method signature (DG); - * 15-May-2003 : Modified to take into account the plot orientation (DG); - * 29-Jul-2003 : Amended code that doesn't compile with JDK 1.2.2 (DG); - * 30-Jul-2003 : Modified entity constructor (CZ); - * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); - * 24-Aug-2003 : Added null/NaN checks in drawItem (BK); - * 08-Sep-2003 : Fixed serialization (NB); - * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); - * 21-Jan-2004 : Override for getLegendItem() method (DG); - * 27-Jan-2004 : Moved working line into state object (DG); - * 10-Feb-2004 : Changed drawItem() method to make cut-and-paste overriding - * easier (DG); - * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed - * XYToolTipGenerator --> XYItemLabelGenerator (DG); - * 08-Jun-2004 : Modified to use getX() and getY() methods (DG); - * 15-Jul-2004 : Switched getX() with getXValue() and getY() with - * getYValue() (DG); - * 25-Aug-2004 : Created addEntity() method in superclass (DG); - * 08-Oct-2004 : Added 'gapThresholdType' as suggested by Mike Watts (DG); - * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); - * 23-Feb-2005 : Fixed getLegendItem() method to show lines. Fixed bug - * 1077108 (shape not visible for first item in series) (DG); - * 10-Apr-2005 : Fixed item label positioning with horizontal orientation (DG); - * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG); - * 27-Apr-2005 : Use generator for series label in legend (DG); - * ------------- JFREECHART 1.0.x --------------------------------------------- - * 15-Jun-2006 : Fixed bug (1380480) for rendering series as path (DG); - * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); - * 14-Mar-2007 : Fixed problems with the equals() and clone() methods (DG); - * 23-Mar-2007 : Clean-up of shapesFilled attributes (DG); - * 20-Apr-2007 : Updated getLegendItem() and drawItem() for renderer - * change (DG); - * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() - * method (DG); - * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); - * 08-Jun-2007 : Fixed bug in entity creation (DG); - * 21-Nov-2007 : Deprecated override flag methods (DG); - * 02-Jun-2008 : Fixed tooltips for data items at lower edges of data area (DG); - * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); - * - */ - -/* =========================================================== - * IBM Modifications - * =========================================================== - * (C) Copyright IBM Corp, 2011 - * - * 09-Sep-2011 : Modified drawItem to handle missing data values (see http://sourceforge.net/tracker/?func=detail&aid=3381041&group_id=15494&atid=115494) (Hunter Presnall / IBM) - */ - -package org.jfree.chart.renderer.xy; - -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.Paint; -import java.awt.Point; -import java.awt.Shape; -import java.awt.Stroke; -import java.awt.geom.GeneralPath; -import java.awt.geom.Line2D; -import java.awt.geom.Rectangle2D; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; - -import org.jfree.chart.LegendItem; -import org.jfree.chart.axis.ValueAxis; -import org.jfree.chart.entity.EntityCollection; -import org.jfree.chart.event.RendererChangeEvent; -import org.jfree.chart.labels.XYToolTipGenerator; -import org.jfree.chart.plot.CrosshairState; -import org.jfree.chart.plot.Plot; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.PlotRenderingInfo; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.urls.XYURLGenerator; -import org.jfree.data.xy.XYDataset; -import org.jfree.io.SerialUtilities; -import org.jfree.ui.RectangleEdge; -import org.jfree.util.BooleanList; -import org.jfree.util.BooleanUtilities; -import org.jfree.util.ObjectUtilities; -import org.jfree.util.PublicCloneable; -import org.jfree.util.ShapeUtilities; -import org.jfree.util.UnitType; - -/** - * Standard item renderer for an {@link XYPlot}. This class can draw (a) - * shapes at each point, or (b) lines between points, or (c) both shapes and - * lines. - *

- * This renderer has been retained for historical reasons and, in general, you - * should use the {@link XYLineAndShapeRenderer} class instead. - */ -public class StandardXYItemRenderer extends AbstractXYItemRenderer - implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { - - /** For serialization. */ - private static final long serialVersionUID = -3271351259436865995L; - - /** Constant for the type of rendering (shapes only). */ - public static final int SHAPES = 1; - - /** Constant for the type of rendering (lines only). */ - public static final int LINES = 2; - - /** Constant for the type of rendering (shapes and lines). */ - public static final int SHAPES_AND_LINES = SHAPES | LINES; - - /** Constant for the type of rendering (images only). */ - public static final int IMAGES = 4; - - /** Constant for the type of rendering (discontinuous lines). */ - public static final int DISCONTINUOUS = 8; - - /** Constant for the type of rendering (discontinuous lines). */ - public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS; - - /** A flag indicating whether or not shapes are drawn at each XY point. */ - private boolean baseShapesVisible; - - /** A flag indicating whether or not lines are drawn between XY points. */ - private boolean plotLines; - - /** A flag indicating whether or not images are drawn between XY points. */ - private boolean plotImages; - - /** A flag controlling whether or not discontinuous lines are used. */ - private boolean plotDiscontinuous; - - /** Specifies how the gap threshold value is interpreted. */ - private UnitType gapThresholdType = UnitType.RELATIVE; - - /** Threshold for deciding when to discontinue a line. */ - private double gapThreshold = 1.0; - - /** - * A flag that controls whether or not shapes are filled for ALL series. - * - * @deprecated As of 1.0.8, this override should not be used. - */ - private Boolean shapesFilled; - - /** - * A table of flags that control (per series) whether or not shapes are - * filled. - */ - private BooleanList seriesShapesFilled; - - /** The default value returned by the getShapeFilled() method. */ - private boolean baseShapesFilled; - - /** - * A flag that controls whether or not each series is drawn as a single - * path. - */ - private boolean drawSeriesLineAsPath; - - /** - * The shape that is used to represent a line in the legend. - * This should never be set to null. - */ - private transient Shape legendLine; - - /** - * Constructs a new renderer. - */ - public StandardXYItemRenderer() { - this(LINES, null); - } - - /** - * Constructs a new renderer. To specify the type of renderer, use one of - * the constants: {@link #SHAPES}, {@link #LINES} or - * {@link #SHAPES_AND_LINES}. - * - * @param type the type. - */ - public StandardXYItemRenderer(int type) { - this(type, null); - } - - /** - * Constructs a new renderer. To specify the type of renderer, use one of - * the constants: {@link #SHAPES}, {@link #LINES} or - * {@link #SHAPES_AND_LINES}. - * - * @param type the type of renderer. - * @param toolTipGenerator the item label generator (null - * permitted). - */ - public StandardXYItemRenderer(int type, - XYToolTipGenerator toolTipGenerator) { - this(type, toolTipGenerator, null); - } - - /** - * Constructs a new renderer. To specify the type of renderer, use one of - * the constants: {@link #SHAPES}, {@link #LINES} or - * {@link #SHAPES_AND_LINES}. - * - * @param type the type of renderer. - * @param toolTipGenerator the item label generator (null - * permitted). - * @param urlGenerator the URL generator. - */ - public StandardXYItemRenderer(int type, - XYToolTipGenerator toolTipGenerator, - XYURLGenerator urlGenerator) { - - super(); - setBaseToolTipGenerator(toolTipGenerator); - setURLGenerator(urlGenerator); - if ((type & SHAPES) != 0) { - this.baseShapesVisible = true; - } - if ((type & LINES) != 0) { - this.plotLines = true; - } - if ((type & IMAGES) != 0) { - this.plotImages = true; - } - if ((type & DISCONTINUOUS) != 0) { - this.plotDiscontinuous = true; - } - - this.shapesFilled = null; - this.seriesShapesFilled = new BooleanList(); - this.baseShapesFilled = true; - this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); - this.drawSeriesLineAsPath = false; - } - - /** - * Returns true if shapes are being plotted by the renderer. - * - * @return true if shapes are being plotted by the renderer. - * - * @see #setBaseShapesVisible - */ - public boolean getBaseShapesVisible() { - return this.baseShapesVisible; - } - - /** - * Sets the flag that controls whether or not a shape is plotted at each - * data point. - * - * @param flag the flag. - * - * @see #getBaseShapesVisible - */ - public void setBaseShapesVisible(boolean flag) { - if (this.baseShapesVisible != flag) { - this.baseShapesVisible = flag; - fireChangeEvent(); - } - } - - // SHAPES FILLED - - /** - * Returns the flag used to control whether or not the shape for an item is - * filled. - *

- * The default implementation passes control to the - * getSeriesShapesFilled method. You can override this method - * if you require different behaviour. - * - * @param series the series index (zero-based). - * @param item the item index (zero-based). - * - * @return A boolean. - * - * @see #getSeriesShapesFilled(int) - */ - public boolean getItemShapeFilled(int series, int item) { - // return the overall setting, if there is one... - if (this.shapesFilled != null) { - return this.shapesFilled.booleanValue(); - } - - // otherwise look up the paint table - Boolean flag = this.seriesShapesFilled.getBoolean(series); - if (flag != null) { - return flag.booleanValue(); - } - else { - return this.baseShapesFilled; - } - } - - /** - * Returns the override flag that controls whether or not shapes are filled - * for ALL series. - * - * @return The flag (possibly null). - * - * @since 1.0.5 - * - * @deprecated As of 1.0.8, you should avoid using this method and rely - * on just the per-series ({@link #getSeriesShapesFilled(int)}) - * and base-level ({@link #getBaseShapesFilled()}) settings. - */ - public Boolean getShapesFilled() { - return this.shapesFilled; - } - - /** - * Sets the override flag that controls whether or not shapes are filled - * for ALL series and sends a {@link RendererChangeEvent} to all registered - * listeners. - * - * @param filled the flag. - * - * @see #setShapesFilled(Boolean) - * - * @deprecated As of 1.0.8, you should avoid using this method and rely - * on just the per-series ({@link #setSeriesShapesFilled(int, - * Boolean)}) and base-level ({@link #setBaseShapesVisible( - * boolean)}) settings. - */ - public void setShapesFilled(boolean filled) { - // here we use BooleanUtilities to remain compatible with JDKs < 1.4 - setShapesFilled(BooleanUtilities.valueOf(filled)); - } - - /** - * Sets the override flag that controls whether or not shapes are filled - * for ALL series and sends a {@link RendererChangeEvent} to all registered - * listeners. - * - * @param filled the flag (null permitted). - * - * @see #setShapesFilled(boolean) - * - * @deprecated As of 1.0.8, you should avoid using this method and rely - * on just the per-series ({@link #setSeriesShapesFilled(int, - * Boolean)}) and base-level ({@link #setBaseShapesVisible( - * boolean)}) settings. - */ - public void setShapesFilled(Boolean filled) { - this.shapesFilled = filled; - fireChangeEvent(); - } - - /** - * Returns the flag used to control whether or not the shapes for a series - * are filled. - * - * @param series the series index (zero-based). - * - * @return A boolean. - */ - public Boolean getSeriesShapesFilled(int series) { - return this.seriesShapesFilled.getBoolean(series); - } - - /** - * Sets the 'shapes filled' flag for a series and sends a - * {@link RendererChangeEvent} to all registered listeners. - * - * @param series the series index (zero-based). - * @param flag the flag. - * - * @see #getSeriesShapesFilled(int) - */ - public void setSeriesShapesFilled(int series, Boolean flag) { - this.seriesShapesFilled.setBoolean(series, flag); - fireChangeEvent(); - } - - /** - * Returns the base 'shape filled' attribute. - * - * @return The base flag. - * - * @see #setBaseShapesFilled(boolean) - */ - public boolean getBaseShapesFilled() { - return this.baseShapesFilled; - } - - /** - * Sets the base 'shapes filled' flag and sends a - * {@link RendererChangeEvent} to all registered listeners. - * - * @param flag the flag. - * - * @see #getBaseShapesFilled() - */ - public void setBaseShapesFilled(boolean flag) { - this.baseShapesFilled = flag; - } - - /** - * Returns true if lines are being plotted by the renderer. - * - * @return true if lines are being plotted by the renderer. - * - * @see #setPlotLines(boolean) - */ - public boolean getPlotLines() { - return this.plotLines; - } - - /** - * Sets the flag that controls whether or not a line is plotted between - * each data point and sends a {@link RendererChangeEvent} to all - * registered listeners. - * - * @param flag the flag. - * - * @see #getPlotLines() - */ - public void setPlotLines(boolean flag) { - if (this.plotLines != flag) { - this.plotLines = flag; - fireChangeEvent(); - } - } - - /** - * Returns the gap threshold type (relative or absolute). - * - * @return The type. - * - * @see #setGapThresholdType(UnitType) - */ - public UnitType getGapThresholdType() { - return this.gapThresholdType; - } - - /** - * Sets the gap threshold type and sends a {@link RendererChangeEvent} to - * all registered listeners. - * - * @param thresholdType the type (null not permitted). - * - * @see #getGapThresholdType() - */ - public void setGapThresholdType(UnitType thresholdType) { - if (thresholdType == null) { - throw new IllegalArgumentException( - "Null 'thresholdType' argument."); - } - this.gapThresholdType = thresholdType; - fireChangeEvent(); - } - - /** - * Returns the gap threshold for discontinuous lines. - * - * @return The gap threshold. - * - * @see #setGapThreshold(double) - */ - public double getGapThreshold() { - return this.gapThreshold; - } - - /** - * Sets the gap threshold for discontinuous lines and sends a - * {@link RendererChangeEvent} to all registered listeners. - * - * @param t the threshold. - * - * @see #getGapThreshold() - */ - public void setGapThreshold(double t) { - this.gapThreshold = t; - fireChangeEvent(); - } - - /** - * Returns true if images are being plotted by the renderer. - * - * @return true if images are being plotted by the renderer. - * - * @see #setPlotImages(boolean) - */ - public boolean getPlotImages() { - return this.plotImages; - } - - /** - * Sets the flag that controls whether or not an image is drawn at each - * data point and sends a {@link RendererChangeEvent} to all registered - * listeners. - * - * @param flag the flag. - * - * @see #getPlotImages() - */ - public void setPlotImages(boolean flag) { - if (this.plotImages != flag) { - this.plotImages = flag; - fireChangeEvent(); - } - } - - /** - * Returns a flag that controls whether or not the renderer shows - * discontinuous lines. - * - * @return true if lines should be discontinuous. - */ - public boolean getPlotDiscontinuous() { - return this.plotDiscontinuous; - } - - /** - * Sets the flag that controls whether or not the renderer shows - * discontinuous lines, and sends a {@link RendererChangeEvent} to all - * registered listeners. - * - * @param flag the new flag value. - * - * @since 1.0.5 - */ - public void setPlotDiscontinuous(boolean flag) { - if (this.plotDiscontinuous != flag) { - this.plotDiscontinuous = flag; - fireChangeEvent(); - } - } - - /** - * Returns a flag that controls whether or not each series is drawn as a - * single path. - * - * @return A boolean. - * - * @see #setDrawSeriesLineAsPath(boolean) - */ - public boolean getDrawSeriesLineAsPath() { - return this.drawSeriesLineAsPath; - } - - /** - * Sets the flag that controls whether or not each series is drawn as a - * single path. - * - * @param flag the flag. - * - * @see #getDrawSeriesLineAsPath() - */ - public void setDrawSeriesLineAsPath(boolean flag) { - this.drawSeriesLineAsPath = flag; - } - - /** - * Returns the shape used to represent a line in the legend. - * - * @return The legend line (never null). - * - * @see #setLegendLine(Shape) - */ - public Shape getLegendLine() { - return this.legendLine; - } - - /** - * Sets the shape used as a line in each legend item and sends a - * {@link RendererChangeEvent} to all registered listeners. - * - * @param line the line (null not permitted). - * - * @see #getLegendLine() - */ - public void setLegendLine(Shape line) { - if (line == null) { - throw new IllegalArgumentException("Null 'line' argument."); - } - this.legendLine = line; - fireChangeEvent(); - } - - /** - * Returns a legend item for a series. - * - * @param datasetIndex the dataset index (zero-based). - * @param series the series index (zero-based). - * - * @return A legend item for the series. - */ - public LegendItem getLegendItem(int datasetIndex, int series) { - XYPlot plot = getPlot(); - if (plot == null) { - return null; - } - LegendItem result = null; - XYDataset dataset = plot.getDataset(datasetIndex); - if (dataset != null) { - if (getItemVisible(series, 0)) { - String label = getLegendItemLabelGenerator().generateLabel( - dataset, series); - String description = label; - String toolTipText = null; - if (getLegendItemToolTipGenerator() != null) { - toolTipText = getLegendItemToolTipGenerator().generateLabel( - dataset, series); - } - String urlText = null; - if (getLegendItemURLGenerator() != null) { - urlText = getLegendItemURLGenerator().generateLabel( - dataset, series); - } - Shape shape = lookupLegendShape(series); - boolean shapeFilled = getItemShapeFilled(series, 0); - Paint paint = lookupSeriesPaint(series); - Paint linePaint = paint; - Stroke lineStroke = lookupSeriesStroke(series); - result = new LegendItem(label, description, toolTipText, - urlText, this.baseShapesVisible, shape, shapeFilled, - paint, !shapeFilled, paint, lineStroke, - this.plotLines, this.legendLine, lineStroke, linePaint); - result.setLabelFont(lookupLegendTextFont(series)); - Paint labelPaint = lookupLegendTextPaint(series); - if (labelPaint != null) { - result.setLabelPaint(labelPaint); - } - result.setDataset(dataset); - result.setDatasetIndex(datasetIndex); - result.setSeriesKey(dataset.getSeriesKey(series)); - result.setSeriesIndex(series); - } - } - return result; - } - - /** - * Records the state for the renderer. This is used to preserve state - * information between calls to the drawItem() method for a single chart - * drawing. - */ - public static class State extends XYItemRendererState { - - /** The path for the current series. */ - public GeneralPath seriesPath; - - /** The series index. */ - private int seriesIndex; - - /** - * A flag that indicates if the last (x, y) point was 'good' - * (non-null). - */ - private boolean lastPointGood; - - /** - * Creates a new state instance. - * - * @param info the plot rendering info. - */ - public State(PlotRenderingInfo info) { - super(info); - } - - /** - * Returns a flag that indicates if the last point drawn (in the - * current series) was 'good' (non-null). - * - * @return A boolean. - */ - public boolean isLastPointGood() { - return this.lastPointGood; - } - - /** - * Sets a flag that indicates if the last point drawn (in the current - * series) was 'good' (non-null). - * - * @param good the flag. - */ - public void setLastPointGood(boolean good) { - this.lastPointGood = good; - } - - /** - * Returns the series index for the current path. - * - * @return The series index for the current path. - */ - public int getSeriesIndex() { - return this.seriesIndex; - } - - /** - * Sets the series index for the current path. - * - * @param index the index. - */ - public void setSeriesIndex(int index) { - this.seriesIndex = index; - } - } - - /** - * Initialises the renderer. - *

- * This method will be called before the first item is rendered, giving the - * renderer an opportunity to initialise any state information it wants to - * maintain. The renderer can do nothing if it chooses. - * - * @param g2 the graphics device. - * @param dataArea the area inside the axes. - * @param plot the plot. - * @param data the data. - * @param info an optional info collection object to return data back to - * the caller. - * - * @return The renderer state. - */ - public XYItemRendererState initialise(Graphics2D g2, - Rectangle2D dataArea, - XYPlot plot, - XYDataset data, - PlotRenderingInfo info) { - - State state = new State(info); - state.seriesPath = new GeneralPath(); - state.seriesIndex = -1; - return state; - - } - - /** - * Draws the visual representation of a single data item. - * - * @param g2 the graphics device. - * @param state the renderer state. - * @param dataArea the area within which the data is being drawn. - * @param info collects information about the drawing. - * @param plot the plot (can be used to obtain standard color information - * etc). - * @param domainAxis the domain axis. - * @param rangeAxis the range axis. - * @param dataset the dataset. - * @param series the series index (zero-based). - * @param item the item index (zero-based). - * @param crosshairState crosshair information for the plot - * (null permitted). - * @param pass the pass index. - */ - public void drawItem(Graphics2D g2, - XYItemRendererState state, - Rectangle2D dataArea, - PlotRenderingInfo info, - XYPlot plot, - ValueAxis domainAxis, - ValueAxis rangeAxis, - XYDataset dataset, - int series, - int item, - CrosshairState crosshairState, - int pass) { - - boolean itemVisible = getItemVisible(series, item); - - // setup for collecting optional entity info... - Shape entityArea = null; - EntityCollection entities = null; - if (info != null) { - entities = info.getOwner().getEntityCollection(); - } - - PlotOrientation orientation = plot.getOrientation(); - Paint paint = getItemPaint(series, item); - Stroke seriesStroke = getItemStroke(series, item); - g2.setPaint(paint); - g2.setStroke(seriesStroke); - - // get the data point... - double x1 = dataset.getXValue(series, item); - double y1 = dataset.getYValue(series, item); - if (Double.isNaN(x1) || Double.isNaN(y1)) { - itemVisible = false; - } - - RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); - RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); - double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); - double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); - - if (getPlotLines()) { - if (this.drawSeriesLineAsPath) { - State s = (State) state; - if (s.getSeriesIndex() != series) { - // we are starting a new series path - s.seriesPath.reset(); - s.lastPointGood = false; - s.setSeriesIndex(series); - } - - // update path to reflect latest point - if (itemVisible && !Double.isNaN(transX1) - && !Double.isNaN(transY1)) { - float x = (float) transX1; - float y = (float) transY1; - if (orientation == PlotOrientation.HORIZONTAL) { - x = (float) transY1; - y = (float) transX1; - } - if (s.isLastPointGood()) { - // TODO: check threshold - s.seriesPath.lineTo(x, y); - } - else { - s.seriesPath.moveTo(x, y); - } - s.setLastPointGood(true); - } - else { - s.setLastPointGood(false); - } - if (item == dataset.getItemCount(series) - 1) { - if (s.seriesIndex == series) { - // draw path - g2.setStroke(lookupSeriesStroke(series)); - g2.setPaint(lookupSeriesPaint(series)); - g2.draw(s.seriesPath); - } - } - } - - else if (item != 0 && itemVisible) { - // get the previous data point... - /* -- begin IBM changes -- */ - int n = 1; - double x0 = 0; - double y0 = 0; - - // continue until both x0 and y0 are not NaN or there is no previous data - do { - x0 = dataset.getXValue(series, item - n); - y0 = dataset.getYValue(series, item - n); - ++n; - } - while ((n <= item) && (Double.isNaN(x0) || Double.isNaN(y0))); - /* -- end IBM changes -- */ - - if (!Double.isNaN(x0) && !Double.isNaN(y0)) { - boolean drawLine = true; - if (getPlotDiscontinuous()) { - // only draw a line if the gap between the current and - // previous data point is within the threshold - int numX = dataset.getItemCount(series); - double minX = dataset.getXValue(series, 0); - double maxX = dataset.getXValue(series, numX - 1); - if (this.gapThresholdType == UnitType.ABSOLUTE) { - drawLine = Math.abs(x1 - x0) <= this.gapThreshold; - } - else { - drawLine = Math.abs(x1 - x0) <= ((maxX - minX) - / numX * getGapThreshold()); - } - } - if (drawLine) { - double transX0 = domainAxis.valueToJava2D(x0, dataArea, - xAxisLocation); - double transY0 = rangeAxis.valueToJava2D(y0, dataArea, - yAxisLocation); - - // only draw if we have good values - if (Double.isNaN(transX0) || Double.isNaN(transY0) - || Double.isNaN(transX1) || Double.isNaN(transY1)) { - return; - } - - if (orientation == PlotOrientation.HORIZONTAL) { - state.workingLine.setLine(transY0, transX0, - transY1, transX1); - } - else if (orientation == PlotOrientation.VERTICAL) { - state.workingLine.setLine(transX0, transY0, - transX1, transY1); - } - - if (state.workingLine.intersects(dataArea)) { - g2.draw(state.workingLine); - } - } - } - } - } - - // we needed to get this far even for invisible items, to ensure that - // seriesPath updates happened, but now there is nothing more we need - // to do for non-visible items... - if (!itemVisible) { - return; - } - - if (getBaseShapesVisible()) { - - Shape shape = getItemShape(series, item); - if (orientation == PlotOrientation.HORIZONTAL) { - shape = ShapeUtilities.createTranslatedShape(shape, transY1, - transX1); - } - else if (orientation == PlotOrientation.VERTICAL) { - shape = ShapeUtilities.createTranslatedShape(shape, transX1, - transY1); - } - if (shape.intersects(dataArea)) { - if (getItemShapeFilled(series, item)) { - g2.fill(shape); - } - else { - g2.draw(shape); - } - } - entityArea = shape; - - } - - if (getPlotImages()) { - Image image = getImage(plot, series, item, transX1, transY1); - if (image != null) { - Point hotspot = getImageHotspot(plot, series, item, transX1, - transY1, image); - g2.drawImage(image, (int) (transX1 - hotspot.getX()), - (int) (transY1 - hotspot.getY()), null); - entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(), - transY1 - hotspot.getY(), image.getWidth(null), - image.getHeight(null)); - } - - } - - double xx = transX1; - double yy = transY1; - if (orientation == PlotOrientation.HORIZONTAL) { - xx = transY1; - yy = transX1; - } - - // draw the item label if there is one... - if (isItemLabelVisible(series, item)) { - drawItemLabel(g2, orientation, dataset, series, item, xx, yy, - (y1 < 0.0)); - } - - int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); - int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); - updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, - rangeAxisIndex, transX1, transY1, orientation); - - // add an entity for the item... - if (entities != null && isPointInRect(dataArea, xx, yy)) { - addEntity(entities, entityArea, dataset, series, item, xx, yy); - } - - } - - /** - * Tests this renderer for equality with another object. - * - * @param obj the object (null permitted). - * - * @return A boolean. - */ - public boolean equals(Object obj) { - - if (obj == this) { - return true; - } - if (!(obj instanceof StandardXYItemRenderer)) { - return false; - } - StandardXYItemRenderer that = (StandardXYItemRenderer) obj; - if (this.baseShapesVisible != that.baseShapesVisible) { - return false; - } - if (this.plotLines != that.plotLines) { - return false; - } - if (this.plotImages != that.plotImages) { - return false; - } - if (this.plotDiscontinuous != that.plotDiscontinuous) { - return false; - } - if (this.gapThresholdType != that.gapThresholdType) { - return false; - } - if (this.gapThreshold != that.gapThreshold) { - return false; - } - if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) { - return false; - } - if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) { - return false; - } - if (this.baseShapesFilled != that.baseShapesFilled) { - return false; - } - if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) { - return false; - } - if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { - return false; - } - return super.equals(obj); - - } - - /** - * Returns a clone of the renderer. - * - * @return A clone. - * - * @throws CloneNotSupportedException if the renderer cannot be cloned. - */ - public Object clone() throws CloneNotSupportedException { - StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone(); - clone.seriesShapesFilled - = (BooleanList) this.seriesShapesFilled.clone(); - clone.legendLine = ShapeUtilities.clone(this.legendLine); - return clone; - } - - //////////////////////////////////////////////////////////////////////////// - // PROTECTED METHODS - // These provide the opportunity to subclass the standard renderer and - // create custom effects. - //////////////////////////////////////////////////////////////////////////// - - /** - * Returns the image used to draw a single data item. - * - * @param plot the plot (can be used to obtain standard color information - * etc). - * @param series the series index. - * @param item the item index. - * @param x the x value of the item. - * @param y the y value of the item. - * - * @return The image. - * - * @see #getPlotImages() - */ - protected Image getImage(Plot plot, int series, int item, - double x, double y) { - // this method must be overridden if you want to display images - return null; - } - - /** - * Returns the hotspot of the image used to draw a single data item. - * The hotspot is the point relative to the top left of the image - * that should indicate the data item. The default is the center of the - * image. - * - * @param plot the plot (can be used to obtain standard color information - * etc). - * @param image the image (can be used to get size information about the - * image) - * @param series the series index - * @param item the item index - * @param x the x value of the item - * @param y the y value of the item - * - * @return The hotspot used to draw the data item. - */ - protected Point getImageHotspot(Plot plot, int series, int item, - double x, double y, Image image) { - - int height = image.getHeight(null); - int width = image.getWidth(null); - return new Point(width / 2, height / 2); - - } - - /** - * Provides serialization support. - * - * @param stream the input stream. - * - * @throws IOException if there is an I/O error. - * @throws ClassNotFoundException if there is a classpath problem. - */ - private void readObject(ObjectInputStream stream) - throws IOException, ClassNotFoundException { - stream.defaultReadObject(); - this.legendLine = SerialUtilities.readShape(stream); - } - - /** - * Provides serialization support. - * - * @param stream the output stream. - * - * @throws IOException if there is an I/O error. - */ - private void writeObject(ObjectOutputStream stream) throws IOException { - stream.defaultWriteObject(); - SerialUtilities.writeShape(this.legendLine, stream); - } - -} +/* =========================================================== + * JFreeChart : a free chart library for the Java(tm) platform + * =========================================================== + * + * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. + * + * Project Info: http://www.jfree.org/jfreechart/index.html + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library 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 Lesser General Public + * License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, + * USA. + * + * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners.] + * + * --------------------------- + * StandardXYItemRenderer.java + * --------------------------- + * (C) Copyright 2001-2013, by Object Refinery Limited and Contributors. + * + * Original Author: David Gilbert (for Object Refinery Limited); + * Contributor(s): Mark Watson (www.markwatson.com); + * Jonathan Nash; + * Andreas Schneider; + * Norbert Kiesel (for TBD Networks); + * Christian W. Zuckschwerdt; + * Bill Kelemen; + * Nicolas Brodu (for Astrium and EADS Corporate Research + * Center); + * + * Changes: + * -------- + * 19-Oct-2001 : Version 1, based on code by Mark Watson (DG); + * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); + * 21-Dec-2001 : Added working line instance to improve performance (DG); + * 22-Jan-2002 : Added code to lock crosshairs to data points. Based on code + * by Jonathan Nash (DG); + * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG); + * 28-Mar-2002 : Added a property change listener mechanism so that the + * renderer no longer needs to be immutable (DG); + * 02-Apr-2002 : Modified to handle null values (DG); + * 09-Apr-2002 : Modified draw method to return void. Removed the translated + * zero from the drawItem method. Override the initialise() + * method to calculate it (DG); + * 13-May-2002 : Added code from Andreas Schneider to allow changing + * shapes/colors per item (DG); + * 24-May-2002 : Incorporated tooltips into chart entities (DG); + * 25-Jun-2002 : Removed redundant code (DG); + * 05-Aug-2002 : Incorporated URLs for HTML image maps into chart entities (RA); + * 08-Aug-2002 : Added discontinuous lines option contributed by + * Norbert Kiesel (DG); + * 20-Aug-2002 : Added user definable default values to be returned by + * protected methods unless overridden by a subclass (DG); + * 23-Sep-2002 : Updated for changes in the XYItemRenderer interface (DG); + * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG); + * 25-Mar-2003 : Implemented Serializable (DG); + * 01-May-2003 : Modified drawItem() method signature (DG); + * 15-May-2003 : Modified to take into account the plot orientation (DG); + * 29-Jul-2003 : Amended code that doesn't compile with JDK 1.2.2 (DG); + * 30-Jul-2003 : Modified entity constructor (CZ); + * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); + * 24-Aug-2003 : Added null/NaN checks in drawItem (BK); + * 08-Sep-2003 : Fixed serialization (NB); + * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); + * 21-Jan-2004 : Override for getLegendItem() method (DG); + * 27-Jan-2004 : Moved working line into state object (DG); + * 10-Feb-2004 : Changed drawItem() method to make cut-and-paste overriding + * easier (DG); + * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed + * XYToolTipGenerator --> XYItemLabelGenerator (DG); + * 08-Jun-2004 : Modified to use getX() and getY() methods (DG); + * 15-Jul-2004 : Switched getX() with getXValue() and getY() with + * getYValue() (DG); + * 25-Aug-2004 : Created addEntity() method in superclass (DG); + * 08-Oct-2004 : Added 'gapThresholdType' as suggested by Mike Watts (DG); + * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); + * 23-Feb-2005 : Fixed getLegendItem() method to show lines. Fixed bug + * 1077108 (shape not visible for first item in series) (DG); + * 10-Apr-2005 : Fixed item label positioning with horizontal orientation (DG); + * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG); + * 27-Apr-2005 : Use generator for series label in legend (DG); + * ------------- JFREECHART 1.0.x --------------------------------------------- + * 15-Jun-2006 : Fixed bug (1380480) for rendering series as path (DG); + * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); + * 14-Mar-2007 : Fixed problems with the equals() and clone() methods (DG); + * 23-Mar-2007 : Clean-up of shapesFilled attributes (DG); + * 20-Apr-2007 : Updated getLegendItem() and drawItem() for renderer + * change (DG); + * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() + * method (DG); + * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); + * 08-Jun-2007 : Fixed bug in entity creation (DG); + * 21-Nov-2007 : Deprecated override flag methods (DG); + * 02-Jun-2008 : Fixed tooltips for data items at lower edges of data area (DG); + * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); + * 03-Jul-2013 : Use ParamChecks (DG); + * + */ + +package org.jfree.chart.renderer.xy; + +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Point; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.GeneralPath; +import java.awt.geom.Line2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import org.jfree.chart.LegendItem; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.entity.EntityCollection; +import org.jfree.chart.event.RendererChangeEvent; +import org.jfree.chart.labels.XYToolTipGenerator; +import org.jfree.chart.plot.CrosshairState; +import org.jfree.chart.plot.Plot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.PlotRenderingInfo; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.urls.XYURLGenerator; +import org.jfree.chart.util.ParamChecks; +import org.jfree.data.xy.XYDataset; +import org.jfree.io.SerialUtilities; +import org.jfree.ui.RectangleEdge; +import org.jfree.util.BooleanList; +import org.jfree.util.BooleanUtilities; +import org.jfree.util.ObjectUtilities; +import org.jfree.util.PublicCloneable; +import org.jfree.util.ShapeUtilities; +import org.jfree.util.UnitType; + +/** + * Standard item renderer for an {@link XYPlot}. This class can draw (a) + * shapes at each point, or (b) lines between points, or (c) both shapes and + * lines. + *

+ * This renderer has been retained for historical reasons and, in general, you + * should use the {@link XYLineAndShapeRenderer} class instead. + */ +public class StandardXYItemRenderer extends AbstractXYItemRenderer + implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { + + /** For serialization. */ + private static final long serialVersionUID = -3271351259436865995L; + + /** Constant for the type of rendering (shapes only). */ + public static final int SHAPES = 1; + + /** Constant for the type of rendering (lines only). */ + public static final int LINES = 2; + + /** Constant for the type of rendering (shapes and lines). */ + public static final int SHAPES_AND_LINES = SHAPES | LINES; + + /** Constant for the type of rendering (images only). */ + public static final int IMAGES = 4; + + /** Constant for the type of rendering (discontinuous lines). */ + public static final int DISCONTINUOUS = 8; + + /** Constant for the type of rendering (discontinuous lines). */ + public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS; + + /** A flag indicating whether or not shapes are drawn at each XY point. */ + private boolean baseShapesVisible; + + /** A flag indicating whether or not lines are drawn between XY points. */ + private boolean plotLines; + + /** A flag indicating whether or not images are drawn between XY points. */ + private boolean plotImages; + + /** A flag controlling whether or not discontinuous lines are used. */ + private boolean plotDiscontinuous; + + /** Specifies how the gap threshold value is interpreted. */ + private UnitType gapThresholdType = UnitType.RELATIVE; + + /** Threshold for deciding when to discontinue a line. */ + private double gapThreshold = 1.0; + + /** + * A flag that controls whether or not shapes are filled for ALL series. + * + * @deprecated As of 1.0.8, this override should not be used. + */ + private Boolean shapesFilled; + + /** + * A table of flags that control (per series) whether or not shapes are + * filled. + */ + private BooleanList seriesShapesFilled; + + /** The default value returned by the getShapeFilled() method. */ + private boolean baseShapesFilled; + + /** + * A flag that controls whether or not each series is drawn as a single + * path. + */ + private boolean drawSeriesLineAsPath; + + /** + * The shape that is used to represent a line in the legend. + * This should never be set to null. + */ + private transient Shape legendLine; + + /** + * Constructs a new renderer. + */ + public StandardXYItemRenderer() { + this(LINES, null); + } + + /** + * Constructs a new renderer. To specify the type of renderer, use one of + * the constants: {@link #SHAPES}, {@link #LINES} or + * {@link #SHAPES_AND_LINES}. + * + * @param type the type. + */ + public StandardXYItemRenderer(int type) { + this(type, null); + } + + /** + * Constructs a new renderer. To specify the type of renderer, use one of + * the constants: {@link #SHAPES}, {@link #LINES} or + * {@link #SHAPES_AND_LINES}. + * + * @param type the type of renderer. + * @param toolTipGenerator the item label generator (null + * permitted). + */ + public StandardXYItemRenderer(int type, + XYToolTipGenerator toolTipGenerator) { + this(type, toolTipGenerator, null); + } + + /** + * Constructs a new renderer. To specify the type of renderer, use one of + * the constants: {@link #SHAPES}, {@link #LINES} or + * {@link #SHAPES_AND_LINES}. + * + * @param type the type of renderer. + * @param toolTipGenerator the item label generator (null + * permitted). + * @param urlGenerator the URL generator. + */ + public StandardXYItemRenderer(int type, + XYToolTipGenerator toolTipGenerator, + XYURLGenerator urlGenerator) { + + super(); + setBaseToolTipGenerator(toolTipGenerator); + setURLGenerator(urlGenerator); + if ((type & SHAPES) != 0) { + this.baseShapesVisible = true; + } + if ((type & LINES) != 0) { + this.plotLines = true; + } + if ((type & IMAGES) != 0) { + this.plotImages = true; + } + if ((type & DISCONTINUOUS) != 0) { + this.plotDiscontinuous = true; + } + + this.shapesFilled = null; + this.seriesShapesFilled = new BooleanList(); + this.baseShapesFilled = true; + this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); + this.drawSeriesLineAsPath = false; + } + + /** + * Returns true if shapes are being plotted by the renderer. + * + * @return true if shapes are being plotted by the renderer. + * + * @see #setBaseShapesVisible + */ + public boolean getBaseShapesVisible() { + return this.baseShapesVisible; + } + + /** + * Sets the flag that controls whether or not a shape is plotted at each + * data point. + * + * @param flag the flag. + * + * @see #getBaseShapesVisible + */ + public void setBaseShapesVisible(boolean flag) { + if (this.baseShapesVisible != flag) { + this.baseShapesVisible = flag; + fireChangeEvent(); + } + } + + // SHAPES FILLED + + /** + * Returns the flag used to control whether or not the shape for an item is + * filled. + *

+ * The default implementation passes control to the + * getSeriesShapesFilled method. You can override this method + * if you require different behaviour. + * + * @param series the series index (zero-based). + * @param item the item index (zero-based). + * + * @return A boolean. + * + * @see #getSeriesShapesFilled(int) + */ + public boolean getItemShapeFilled(int series, int item) { + // return the overall setting, if there is one... + if (this.shapesFilled != null) { + return this.shapesFilled.booleanValue(); + } + + // otherwise look up the paint table + Boolean flag = this.seriesShapesFilled.getBoolean(series); + if (flag != null) { + return flag.booleanValue(); + } + else { + return this.baseShapesFilled; + } + } + + /** + * Returns the override flag that controls whether or not shapes are filled + * for ALL series. + * + * @return The flag (possibly null). + * + * @since 1.0.5 + * + * @deprecated As of 1.0.8, you should avoid using this method and rely + * on just the per-series ({@link #getSeriesShapesFilled(int)}) + * and base-level ({@link #getBaseShapesFilled()}) settings. + */ + public Boolean getShapesFilled() { + return this.shapesFilled; + } + + /** + * Sets the override flag that controls whether or not shapes are filled + * for ALL series and sends a {@link RendererChangeEvent} to all registered + * listeners. + * + * @param filled the flag. + * + * @see #setShapesFilled(Boolean) + * + * @deprecated As of 1.0.8, you should avoid using this method and rely + * on just the per-series ({@link #setSeriesShapesFilled(int, + * Boolean)}) and base-level ({@link #setBaseShapesVisible( + * boolean)}) settings. + */ + public void setShapesFilled(boolean filled) { + // here we use BooleanUtilities to remain compatible with JDKs < 1.4 + setShapesFilled(BooleanUtilities.valueOf(filled)); + } + + /** + * Sets the override flag that controls whether or not shapes are filled + * for ALL series and sends a {@link RendererChangeEvent} to all registered + * listeners. + * + * @param filled the flag (null permitted). + * + * @see #setShapesFilled(boolean) + * + * @deprecated As of 1.0.8, you should avoid using this method and rely + * on just the per-series ({@link #setSeriesShapesFilled(int, + * Boolean)}) and base-level ({@link #setBaseShapesVisible( + * boolean)}) settings. + */ + public void setShapesFilled(Boolean filled) { + this.shapesFilled = filled; + fireChangeEvent(); + } + + /** + * Returns the flag used to control whether or not the shapes for a series + * are filled. + * + * @param series the series index (zero-based). + * + * @return A boolean. + */ + public Boolean getSeriesShapesFilled(int series) { + return this.seriesShapesFilled.getBoolean(series); + } + + /** + * Sets the 'shapes filled' flag for a series and sends a + * {@link RendererChangeEvent} to all registered listeners. + * + * @param series the series index (zero-based). + * @param flag the flag. + * + * @see #getSeriesShapesFilled(int) + */ + public void setSeriesShapesFilled(int series, Boolean flag) { + this.seriesShapesFilled.setBoolean(series, flag); + fireChangeEvent(); + } + + /** + * Returns the base 'shape filled' attribute. + * + * @return The base flag. + * + * @see #setBaseShapesFilled(boolean) + */ + public boolean getBaseShapesFilled() { + return this.baseShapesFilled; + } + + /** + * Sets the base 'shapes filled' flag and sends a + * {@link RendererChangeEvent} to all registered listeners. + * + * @param flag the flag. + * + * @see #getBaseShapesFilled() + */ + public void setBaseShapesFilled(boolean flag) { + this.baseShapesFilled = flag; + } + + /** + * Returns true if lines are being plotted by the renderer. + * + * @return true if lines are being plotted by the renderer. + * + * @see #setPlotLines(boolean) + */ + public boolean getPlotLines() { + return this.plotLines; + } + + /** + * Sets the flag that controls whether or not a line is plotted between + * each data point and sends a {@link RendererChangeEvent} to all + * registered listeners. + * + * @param flag the flag. + * + * @see #getPlotLines() + */ + public void setPlotLines(boolean flag) { + if (this.plotLines != flag) { + this.plotLines = flag; + fireChangeEvent(); + } + } + + /** + * Returns the gap threshold type (relative or absolute). + * + * @return The type. + * + * @see #setGapThresholdType(UnitType) + */ + public UnitType getGapThresholdType() { + return this.gapThresholdType; + } + + /** + * Sets the gap threshold type and sends a {@link RendererChangeEvent} to + * all registered listeners. + * + * @param thresholdType the type (null not permitted). + * + * @see #getGapThresholdType() + */ + public void setGapThresholdType(UnitType thresholdType) { + ParamChecks.nullNotPermitted(thresholdType, "thresholdType"); + this.gapThresholdType = thresholdType; + fireChangeEvent(); + } + + /** + * Returns the gap threshold for discontinuous lines. + * + * @return The gap threshold. + * + * @see #setGapThreshold(double) + */ + public double getGapThreshold() { + return this.gapThreshold; + } + + /** + * Sets the gap threshold for discontinuous lines and sends a + * {@link RendererChangeEvent} to all registered listeners. + * + * @param t the threshold. + * + * @see #getGapThreshold() + */ + public void setGapThreshold(double t) { + this.gapThreshold = t; + fireChangeEvent(); + } + + /** + * Returns true if images are being plotted by the renderer. + * + * @return true if images are being plotted by the renderer. + * + * @see #setPlotImages(boolean) + */ + public boolean getPlotImages() { + return this.plotImages; + } + + /** + * Sets the flag that controls whether or not an image is drawn at each + * data point and sends a {@link RendererChangeEvent} to all registered + * listeners. + * + * @param flag the flag. + * + * @see #getPlotImages() + */ + public void setPlotImages(boolean flag) { + if (this.plotImages != flag) { + this.plotImages = flag; + fireChangeEvent(); + } + } + + /** + * Returns a flag that controls whether or not the renderer shows + * discontinuous lines. + * + * @return true if lines should be discontinuous. + */ + public boolean getPlotDiscontinuous() { + return this.plotDiscontinuous; + } + + /** + * Sets the flag that controls whether or not the renderer shows + * discontinuous lines, and sends a {@link RendererChangeEvent} to all + * registered listeners. + * + * @param flag the new flag value. + * + * @since 1.0.5 + */ + public void setPlotDiscontinuous(boolean flag) { + if (this.plotDiscontinuous != flag) { + this.plotDiscontinuous = flag; + fireChangeEvent(); + } + } + + /** + * Returns a flag that controls whether or not each series is drawn as a + * single path. + * + * @return A boolean. + * + * @see #setDrawSeriesLineAsPath(boolean) + */ + public boolean getDrawSeriesLineAsPath() { + return this.drawSeriesLineAsPath; + } + + /** + * Sets the flag that controls whether or not each series is drawn as a + * single path. + * + * @param flag the flag. + * + * @see #getDrawSeriesLineAsPath() + */ + public void setDrawSeriesLineAsPath(boolean flag) { + this.drawSeriesLineAsPath = flag; + } + + /** + * Returns the shape used to represent a line in the legend. + * + * @return The legend line (never null). + * + * @see #setLegendLine(Shape) + */ + public Shape getLegendLine() { + return this.legendLine; + } + + /** + * Sets the shape used as a line in each legend item and sends a + * {@link RendererChangeEvent} to all registered listeners. + * + * @param line the line (null not permitted). + * + * @see #getLegendLine() + */ + public void setLegendLine(Shape line) { + ParamChecks.nullNotPermitted(line, "line"); + this.legendLine = line; + fireChangeEvent(); + } + + /** + * Returns a legend item for a series. + * + * @param datasetIndex the dataset index (zero-based). + * @param series the series index (zero-based). + * + * @return A legend item for the series. + */ + @Override + public LegendItem getLegendItem(int datasetIndex, int series) { + XYPlot plot = getPlot(); + if (plot == null) { + return null; + } + LegendItem result = null; + XYDataset dataset = plot.getDataset(datasetIndex); + if (dataset != null) { + if (getItemVisible(series, 0)) { + String label = getLegendItemLabelGenerator().generateLabel( + dataset, series); + String description = label; + String toolTipText = null; + if (getLegendItemToolTipGenerator() != null) { + toolTipText = getLegendItemToolTipGenerator().generateLabel( + dataset, series); + } + String urlText = null; + if (getLegendItemURLGenerator() != null) { + urlText = getLegendItemURLGenerator().generateLabel( + dataset, series); + } + Shape shape = lookupLegendShape(series); + boolean shapeFilled = getItemShapeFilled(series, 0); + Paint paint = lookupSeriesPaint(series); + Paint linePaint = paint; + Stroke lineStroke = lookupSeriesStroke(series); + result = new LegendItem(label, description, toolTipText, + urlText, this.baseShapesVisible, shape, shapeFilled, + paint, !shapeFilled, paint, lineStroke, + this.plotLines, this.legendLine, lineStroke, linePaint); + result.setLabelFont(lookupLegendTextFont(series)); + Paint labelPaint = lookupLegendTextPaint(series); + if (labelPaint != null) { + result.setLabelPaint(labelPaint); + } + result.setDataset(dataset); + result.setDatasetIndex(datasetIndex); + result.setSeriesKey(dataset.getSeriesKey(series)); + result.setSeriesIndex(series); + } + } + return result; + } + + /** + * Records the state for the renderer. This is used to preserve state + * information between calls to the drawItem() method for a single chart + * drawing. + */ + public static class State extends XYItemRendererState { + + /** The path for the current series. */ + public GeneralPath seriesPath; + + /** The series index. */ + private int seriesIndex; + + /** + * A flag that indicates if the last (x, y) point was 'good' + * (non-null). + */ + private boolean lastPointGood; + + /** + * Creates a new state instance. + * + * @param info the plot rendering info. + */ + public State(PlotRenderingInfo info) { + super(info); + } + + /** + * Returns a flag that indicates if the last point drawn (in the + * current series) was 'good' (non-null). + * + * @return A boolean. + */ + public boolean isLastPointGood() { + return this.lastPointGood; + } + + /** + * Sets a flag that indicates if the last point drawn (in the current + * series) was 'good' (non-null). + * + * @param good the flag. + */ + public void setLastPointGood(boolean good) { + this.lastPointGood = good; + } + + /** + * Returns the series index for the current path. + * + * @return The series index for the current path. + */ + public int getSeriesIndex() { + return this.seriesIndex; + } + + /** + * Sets the series index for the current path. + * + * @param index the index. + */ + public void setSeriesIndex(int index) { + this.seriesIndex = index; + } + } + + /** + * Initialises the renderer. + *

+ * This method will be called before the first item is rendered, giving the + * renderer an opportunity to initialise any state information it wants to + * maintain. The renderer can do nothing if it chooses. + * + * @param g2 the graphics device. + * @param dataArea the area inside the axes. + * @param plot the plot. + * @param data the data. + * @param info an optional info collection object to return data back to + * the caller. + * + * @return The renderer state. + */ + @Override + public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, + XYPlot plot, XYDataset data, PlotRenderingInfo info) { + + State state = new State(info); + state.seriesPath = new GeneralPath(); + state.seriesIndex = -1; + return state; + + } + + /** + * Draws the visual representation of a single data item. + * + * @param g2 the graphics device. + * @param state the renderer state. + * @param dataArea the area within which the data is being drawn. + * @param info collects information about the drawing. + * @param plot the plot (can be used to obtain standard color information + * etc). + * @param domainAxis the domain axis. + * @param rangeAxis the range axis. + * @param dataset the dataset. + * @param series the series index (zero-based). + * @param item the item index (zero-based). + * @param crosshairState crosshair information for the plot + * (null permitted). + * @param pass the pass index. + */ + @Override + public void drawItem(Graphics2D g2, XYItemRendererState state, + Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, + ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, + int series, int item, CrosshairState crosshairState, int pass) { + + boolean itemVisible = getItemVisible(series, item); + + // setup for collecting optional entity info... + Shape entityArea = null; + EntityCollection entities = null; + if (info != null) { + entities = info.getOwner().getEntityCollection(); + } + + PlotOrientation orientation = plot.getOrientation(); + Paint paint = getItemPaint(series, item); + Stroke seriesStroke = getItemStroke(series, item); + g2.setPaint(paint); + g2.setStroke(seriesStroke); + + // get the data point... + double x1 = dataset.getXValue(series, item); + double y1 = dataset.getYValue(series, item); + if (Double.isNaN(x1) || Double.isNaN(y1)) { + itemVisible = false; + } + + RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); + RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); + double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); + double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); + + if (getPlotLines()) { + if (this.drawSeriesLineAsPath) { + State s = (State) state; + if (s.getSeriesIndex() != series) { + // we are starting a new series path + s.seriesPath.reset(); + s.lastPointGood = false; + s.setSeriesIndex(series); + } + + // update path to reflect latest point + if (itemVisible && !Double.isNaN(transX1) + && !Double.isNaN(transY1)) { + float x = (float) transX1; + float y = (float) transY1; + if (orientation == PlotOrientation.HORIZONTAL) { + x = (float) transY1; + y = (float) transX1; + } + if (s.isLastPointGood()) { + // TODO: check threshold + s.seriesPath.lineTo(x, y); + } + else { + s.seriesPath.moveTo(x, y); + } + s.setLastPointGood(true); + } + else { + s.setLastPointGood(false); + } + if (item == dataset.getItemCount(series) - 1) { + if (s.seriesIndex == series) { + // draw path + g2.setStroke(lookupSeriesStroke(series)); + g2.setPaint(lookupSeriesPaint(series)); + g2.draw(s.seriesPath); + } + } + } + + else if (item != 0 && itemVisible) { + // get the previous data point... + /* -- begin IBM changes -- */ + int n = 1; + double x0 = 0; + double y0 = 0; + + // continue until both x0 and y0 are not NaN or there is no previous data + do { + x0 = dataset.getXValue(series, item - n); + y0 = dataset.getYValue(series, item - n); + ++n; + } + while ((n <= item) && (Double.isNaN(x0) || Double.isNaN(y0))); + /* -- end IBM changes -- */ + + if (!Double.isNaN(x0) && !Double.isNaN(y0)) { + boolean drawLine = true; + if (getPlotDiscontinuous()) { + // only draw a line if the gap between the current and + // previous data point is within the threshold + int numX = dataset.getItemCount(series); + double minX = dataset.getXValue(series, 0); + double maxX = dataset.getXValue(series, numX - 1); + if (this.gapThresholdType == UnitType.ABSOLUTE) { + drawLine = Math.abs(x1 - x0) <= this.gapThreshold; + } + else { + drawLine = Math.abs(x1 - x0) <= ((maxX - minX) + / numX * getGapThreshold()); + } + } + if (drawLine) { + double transX0 = domainAxis.valueToJava2D(x0, dataArea, + xAxisLocation); + double transY0 = rangeAxis.valueToJava2D(y0, dataArea, + yAxisLocation); + + // only draw if we have good values + if (Double.isNaN(transX0) || Double.isNaN(transY0) + || Double.isNaN(transX1) || Double.isNaN(transY1)) { + return; + } + + if (orientation == PlotOrientation.HORIZONTAL) { + state.workingLine.setLine(transY0, transX0, + transY1, transX1); + } + else if (orientation == PlotOrientation.VERTICAL) { + state.workingLine.setLine(transX0, transY0, + transX1, transY1); + } + + if (state.workingLine.intersects(dataArea)) { + g2.draw(state.workingLine); + } + } + } + } + } + + // we needed to get this far even for invisible items, to ensure that + // seriesPath updates happened, but now there is nothing more we need + // to do for non-visible items... + if (!itemVisible) { + return; + } + + if (getBaseShapesVisible()) { + + Shape shape = getItemShape(series, item); + if (orientation == PlotOrientation.HORIZONTAL) { + shape = ShapeUtilities.createTranslatedShape(shape, transY1, + transX1); + } + else if (orientation == PlotOrientation.VERTICAL) { + shape = ShapeUtilities.createTranslatedShape(shape, transX1, + transY1); + } + if (shape.intersects(dataArea)) { + if (getItemShapeFilled(series, item)) { + g2.fill(shape); + } + else { + g2.draw(shape); + } + } + entityArea = shape; + + } + + if (getPlotImages()) { + Image image = getImage(plot, series, item, transX1, transY1); + if (image != null) { + Point hotspot = getImageHotspot(plot, series, item, transX1, + transY1, image); + g2.drawImage(image, (int) (transX1 - hotspot.getX()), + (int) (transY1 - hotspot.getY()), null); + entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(), + transY1 - hotspot.getY(), image.getWidth(null), + image.getHeight(null)); + } + + } + + double xx = transX1; + double yy = transY1; + if (orientation == PlotOrientation.HORIZONTAL) { + xx = transY1; + yy = transX1; + } + + // draw the item label if there is one... + if (isItemLabelVisible(series, item)) { + drawItemLabel(g2, orientation, dataset, series, item, xx, yy, + (y1 < 0.0)); + } + + int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); + int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); + updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, + rangeAxisIndex, transX1, transY1, orientation); + + // add an entity for the item... + if (entities != null && isPointInRect(dataArea, xx, yy)) { + addEntity(entities, entityArea, dataset, series, item, xx, yy); + } + + } + + /** + * Tests this renderer for equality with another object. + * + * @param obj the object (null permitted). + * + * @return A boolean. + */ + @Override + public boolean equals(Object obj) { + + if (obj == this) { + return true; + } + if (!(obj instanceof StandardXYItemRenderer)) { + return false; + } + StandardXYItemRenderer that = (StandardXYItemRenderer) obj; + if (this.baseShapesVisible != that.baseShapesVisible) { + return false; + } + if (this.plotLines != that.plotLines) { + return false; + } + if (this.plotImages != that.plotImages) { + return false; + } + if (this.plotDiscontinuous != that.plotDiscontinuous) { + return false; + } + if (this.gapThresholdType != that.gapThresholdType) { + return false; + } + if (this.gapThreshold != that.gapThreshold) { + return false; + } + if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) { + return false; + } + if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) { + return false; + } + if (this.baseShapesFilled != that.baseShapesFilled) { + return false; + } + if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) { + return false; + } + if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { + return false; + } + return super.equals(obj); + + } + + /** + * Returns a clone of the renderer. + * + * @return A clone. + * + * @throws CloneNotSupportedException if the renderer cannot be cloned. + */ + @Override + public Object clone() throws CloneNotSupportedException { + StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone(); + clone.seriesShapesFilled + = (BooleanList) this.seriesShapesFilled.clone(); + clone.legendLine = ShapeUtilities.clone(this.legendLine); + return clone; + } + + //////////////////////////////////////////////////////////////////////////// + // PROTECTED METHODS + // These provide the opportunity to subclass the standard renderer and + // create custom effects. + //////////////////////////////////////////////////////////////////////////// + + /** + * Returns the image used to draw a single data item. + * + * @param plot the plot (can be used to obtain standard color information + * etc). + * @param series the series index. + * @param item the item index. + * @param x the x value of the item. + * @param y the y value of the item. + * + * @return The image. + * + * @see #getPlotImages() + */ + protected Image getImage(Plot plot, int series, int item, + double x, double y) { + // this method must be overridden if you want to display images + return null; + } + + /** + * Returns the hotspot of the image used to draw a single data item. + * The hotspot is the point relative to the top left of the image + * that should indicate the data item. The default is the center of the + * image. + * + * @param plot the plot (can be used to obtain standard color information + * etc). + * @param image the image (can be used to get size information about the + * image) + * @param series the series index + * @param item the item index + * @param x the x value of the item + * @param y the y value of the item + * + * @return The hotspot used to draw the data item. + */ + protected Point getImageHotspot(Plot plot, int series, int item, + double x, double y, Image image) { + + int height = image.getHeight(null); + int width = image.getWidth(null); + return new Point(width / 2, height / 2); + + } + + /** + * Provides serialization support. + * + * @param stream the input stream. + * + * @throws IOException if there is an I/O error. + * @throws ClassNotFoundException if there is a classpath problem. + */ + private void readObject(ObjectInputStream stream) + throws IOException, ClassNotFoundException { + stream.defaultReadObject(); + this.legendLine = SerialUtilities.readShape(stream); + } + + /** + * Provides serialization support. + * + * @param stream the output stream. + * + * @throws IOException if there is an I/O error. + */ + private void writeObject(ObjectOutputStream stream) throws IOException { + stream.defaultWriteObject(); + SerialUtilities.writeShape(this.legendLine, stream); + } + +}