From 42e38bd9295f5079576e93620048ae3f8332d0cc Mon Sep 17 00:00:00 2001 From: toni Date: Mon, 23 Jul 2018 20:32:48 +0200 Subject: [PATCH] added magnitude refactored plotting refactored some code --- java/src/main/java/Main.java | 242 ++-- java/src/main/java/Plot.java | 1027 ----------------- .../AccelerometerWindowBuffer.java | 86 +- .../main/java/bpmEstimation/BpmEstimator.java | 111 +- java/src/main/java/utilities/Utils.java | 170 ++- 5 files changed, 486 insertions(+), 1150 deletions(-) delete mode 100644 java/src/main/java/Plot.java diff --git a/java/src/main/java/Main.java b/java/src/main/java/Main.java index f5f124a..d84ac21 100644 --- a/java/src/main/java/Main.java +++ b/java/src/main/java/Main.java @@ -1,4 +1,6 @@ import bpmEstimation.*; +import uk.me.berndporr.iirj.Butterworth; +import utilities.Plot; import utilities.Utils; import java.awt.*; @@ -6,6 +8,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.util.Arrays; import java.util.LinkedList; import java.util.stream.IntStream; @@ -16,33 +19,46 @@ import java.util.stream.IntStream; public class Main { public static void main(String [ ] args) { - //File folder = new File("/home/toni/Documents/programme/dirigent/measurements/lgWear"); - File folder = new File("/home/toni/Documents/programme/dirigent/measurements/peter_failed"); + //File folder = new File("/home/toni/Documents/programme/dirigent/measurements/2017.06/lgWear"); + //File folder = new File("/home/toni/Documents/programme/dirigent/measurements/peter_failed"); + File folder = new File("/home/toni/Documents/programme/dirigent/measurements/2018.06/frank/mSensor"); File[] listOfFiles = folder.listFiles(); + Arrays.sort(listOfFiles); - Utils.ShowPNG windowRaw = new Utils.ShowPNG(); - Utils.ShowPNG windowAuto = new Utils.ShowPNG(); - Utils.ShowPNG windowPeaksX = new Utils.ShowPNG(); - Utils.ShowPNG windowPeaksY = new Utils.ShowPNG(); - Utils.ShowPNG windowPeaksZ = new Utils.ShowPNG(); + +// //TODO: write debug class, that is able to simply draw images... +// Utils.ShowPNG windowRaw = new Utils.ShowPNG(); +// Utils.ShowPNG windowAuto = new Utils.ShowPNG(); +// Utils.ShowPNG windowAutoButter = new Utils.ShowPNG(); +// Utils.ShowPNG windowPeaksX = new Utils.ShowPNG(); +// Utils.ShowPNG windowPeaksY = new Utils.ShowPNG(); +// Utils.ShowPNG windowPeaksZ = new Utils.ShowPNG(); // iterate trough files in measurements folder for (File file : listOfFiles) { if (file.isFile() && file.getName().contains(".csv")) { - //TODO: mach Fenster genau 6 sekunden groß. Egal wie viele Samples. - AccelerometerWindowBuffer accWindowBuffer = new AccelerometerWindowBuffer(6000, 1500); + //TODO: Die Raw Sensordaten sollte man vielleicht etwas glätten. Sieh Aufnahmen von Frank! + AccelerometerWindowBuffer accWindowBuffer = new AccelerometerWindowBuffer(6000, 750); BpmEstimator bpmEstimator = new BpmEstimator(accWindowBuffer, 0, 5000); + Butterworth butterLowpass = new Butterworth(); //read the file line by line try (BufferedReader br = new BufferedReader(new FileReader(file))) { + + //read the first three lines and print out what file it is! + String comment = br.readLine(); + br.readLine(); + br.readLine(); + System.out.println(comment); + for (String line; (line = br.readLine()) != null; ) { // process the line. String[] measurement = line.split(";"); //if linear acc - if(measurement[1].equals("10")){ + if(measurement[1].equals("3")){ long ts = Long.parseLong(measurement[0]); double x = Double.parseDouble(measurement[2]); double y = Double.parseDouble(measurement[3]); @@ -53,86 +69,132 @@ public class Main { //do calculation stuff if(accWindowBuffer.isNextWindowReady()){ - double curBpm = bpmEstimator.estimate(); - //System.out.println("BPM: " + curBpm); + LinkedList bpmList = new LinkedList<>(); - double sampleRate = 20; - AccelerometerInterpolator acInterp = new AccelerometerInterpolator(accWindowBuffer, sampleRate); - int peakWidth = (int) Math.round(250 / sampleRate); + // Calculate the BPM for different window sizes + double bpm60 = bpmEstimator.estimate(); + double bpm85 = bpmEstimator.estimate(3500, 750); + double bpm110 = bpmEstimator.estimate(2600, 750); + double bpm135 = bpmEstimator.estimate(2000, 750); + double bpm160 = bpmEstimator.estimate(1600,750); + double bpm200 = bpmEstimator.estimate(1200, 750); - //print raw x,y,z - double[] dTs = IntStream.range(0, accWindowBuffer.getTs().length).mapToDouble(i -> accWindowBuffer.getTs()[i]).toArray(); - double[] dTsInterp = IntStream.range(0, acInterp.getTs().length).mapToDouble(i -> acInterp.getTs()[i]).toArray(); - Plot plotRaw = Plot.plot(Plot.plotOpts(). - title("Raw Acc Data"). - legend(Plot.LegendFormat.BOTTOM)). - series("x", Plot.data().xy(dTsInterp, acInterp.getX()), Plot.seriesOpts().color(Color.RED)). - series("y", Plot.data().xy(dTsInterp, acInterp.getY()), Plot.seriesOpts().color(Color.BLUE)). - series("z", Plot.data().xy(dTsInterp, acInterp.getZ()), Plot.seriesOpts().color(Color.GREEN)); + System.out.println("--------------------------------------------------"); - windowRaw.set(plotRaw.draw()); + bpmList.add(bpm60); + bpmList.add(bpm85); + bpmList.add(bpm110); + bpmList.add(bpm135); + bpmList.add(bpm160); + bpmList.add(bpm200); - //auto corr - double[] xAutoCorr = new AutoCorrelation(acInterp.getX(), accWindowBuffer.size()).getCorr(); - double[] yAutoCorr = new AutoCorrelation(acInterp.getY(), accWindowBuffer.size()).getCorr(); - double[] zAutoCorr = new AutoCorrelation(acInterp.getZ(), accWindowBuffer.size()).getCorr(); - - //print autocorr - int[] tmp = IntStream.rangeClosed(-((xAutoCorr.length - 1)/2), ((xAutoCorr.length - 1)/2)).toArray(); - double[] rangeAuto = IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray(); - Plot plotCorr = Plot.plot(Plot.plotOpts(). - title("Auto Correlation"). - legend(Plot.LegendFormat.BOTTOM)). - series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)). - series("y", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.BLUE)). - series("z", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.GREEN)); - - windowAuto.set(plotCorr.draw()); - - Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false); - int xOffset = xAutoCorr.length / 2; - LinkedList peaksX = pX.getPeaksIdx(); - - double[] dPeaksXX = IntStream.range(0, peaksX.size()).mapToDouble(i -> (peaksX.get(i) - xOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); - double[] dPeaksXY = IntStream.range(0, peaksX.size()).mapToDouble(i -> (xAutoCorr[peaksX.get(i)])).toArray(); - Plot plotPeaksX = Plot.plot(Plot.plotOpts(). - title("Peak Detection on X"). - legend(Plot.LegendFormat.BOTTOM)). - series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)). - series("Peaks", Plot.data().xy(dPeaksXX, dPeaksXY), Plot.seriesOpts().color(Color.CYAN). - marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); - - windowPeaksX.set(plotPeaksX.draw()); - - Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false); - int yOffset = yAutoCorr.length / 2; - LinkedList peaksY = pY.getPeaksIdx(); - - double[] dPeaksYX = IntStream.range(0, peaksY.size()).mapToDouble(i -> (peaksY.get(i) - yOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); - double[] dPeaksYY = IntStream.range(0, peaksY.size()).mapToDouble(i -> (yAutoCorr[peaksY.get(i)])).toArray(); - Plot plotPeaksY = Plot.plot(Plot.plotOpts(). - title("Peak Detection on Y"). - legend(Plot.LegendFormat.BOTTOM)). - series("x", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.RED)). - series("Peaks", Plot.data().xy(dPeaksYX, dPeaksYY), Plot.seriesOpts().color(Color.CYAN). - marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); - - windowPeaksY.set(plotPeaksY.draw()); - - Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false); - int zOffset = zAutoCorr.length / 2; - LinkedList peaksZ = pZ.getPeaksIdx(); - - double[] dPeaksZX = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (peaksZ.get(i) - zOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); - double[] dPeaksZY = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (zAutoCorr[peaksZ.get(i)])).toArray(); - Plot plotPeaksZ = Plot.plot(Plot.plotOpts(). - title("Peak Detection on Z"). - legend(Plot.LegendFormat.BOTTOM)). - series("x", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.RED)). - series("Peaks", Plot.data().xy(dPeaksZX, dPeaksZY), Plot.seriesOpts().color(Color.CYAN). - marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); - - windowPeaksZ.set(plotPeaksZ.draw()); +// //TODO: plot the different autocorrelation of different window sizes +// // Draw the data for the complete 6 second window +// double sampleRate = 4; +// AccelerometerInterpolator acInterp = new AccelerometerInterpolator(accWindowBuffer, sampleRate); +// int peakWidth = (int) Math.round(250 / sampleRate); +// +// //print raw x,y,z +// double[] dTs = IntStream.range(0, accWindowBuffer.getTs().length).mapToDouble(i -> accWindowBuffer.getTs()[i]).toArray(); +// double[] dTsInterp = IntStream.range(0, acInterp.getTs().length).mapToDouble(i -> acInterp.getTs()[i]).toArray(); +// Plot plotRaw = Plot.plot(Plot.plotOpts(). +// title("Raw Acc Data"). +// legend(Plot.LegendFormat.BOTTOM)). +// series("x", Plot.data().xy(dTsInterp, acInterp.getX()), Plot.seriesOpts().color(Color.RED)). +// series("y", Plot.data().xy(dTsInterp, acInterp.getY()), Plot.seriesOpts().color(Color.BLUE)). +// series("z", Plot.data().xy(dTsInterp, acInterp.getZ()), Plot.seriesOpts().color(Color.GREEN)); +// +// windowRaw.set(plotRaw.draw()); +// +// //auto corr +// double[] xAutoCorr = new AutoCorrelation(acInterp.getX(), accWindowBuffer.size()).getCorr(); +// double[] yAutoCorr = new AutoCorrelation(acInterp.getY(), accWindowBuffer.size()).getCorr(); +// double[] zAutoCorr = new AutoCorrelation(acInterp.getZ(), accWindowBuffer.size()).getCorr(); +// +// //print autocorr raw +// int[] tmp = IntStream.rangeClosed(-((xAutoCorr.length - 1)/2), ((xAutoCorr.length - 1)/2)).toArray(); +// double[] rangeAuto = IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray(); +// Plot plotCorr = Plot.plot(Plot.plotOpts(). +// title("Auto Correlation"). +// legend(Plot.LegendFormat.BOTTOM)). +// series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)). +// series("y", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.BLUE)). +// series("z", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.GREEN)); +// +// windowAuto.set(plotCorr.draw()); +// +// //print autocorr butter +// // butterworth lowpass filter, cutoff at 3 hz (~180 bpm) +// butterLowpass.lowPass(1,(1/(sampleRate / 1000))/2, 1); +// +// int n = acInterp.getX().length; +// double[] xButter = new double[n]; +// double[] yButter = new double[n];; +// double[] zButter = new double[n];; +// for(int i = 0; i < acInterp.getX().length; ++i){ +// //xButter[i] = butterLowpass.filter(acInterp.getX()[i]); +// //yButter[i] = butterLowpass.filter(acInterp.getY()[i]); +// zButter[i] = butterLowpass.filter(acInterp.getZ()[i]); +// } +// +// //double[] xAutoCorrButter = new AutoCorrelation(xButter, accWindowBuffer.size()).getCorr(); +// //double[] yAutoCorrButter = new AutoCorrelation(yButter, accWindowBuffer.size()).getCorr(); +// double[] zAutoCorrButter = new AutoCorrelation(zButter, accWindowBuffer.size()).getCorr(); +// +// Plot plotCorrButter = Plot.plot(Plot.plotOpts(). +// title("Auto Correlation Butter"). +// legend(Plot.LegendFormat.BOTTOM)). +// //series("x", utilities.Plot.data().xy(rangeAuto, xAutoCorrButter), utilities.Plot.seriesOpts().color(Color.RED)). +// //series("y", utilities.Plot.data().xy(rangeAuto, yAutoCorrButter), utilities.Plot.seriesOpts().color(Color.BLUE)). +// series("z", Plot.data().xy(rangeAuto, zAutoCorrButter), Plot.seriesOpts().color(Color.GREEN)); +// +// windowAutoButter.set(plotCorrButter.draw()); +// +// //Print peaks +// Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false); +// int xOffset = xAutoCorr.length / 2; +// LinkedList peaksX = pX.getPeaksIdx(); +// +// double[] dPeaksXX = IntStream.range(0, peaksX.size()).mapToDouble(i -> (peaksX.get(i) - xOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); +// double[] dPeaksXY = IntStream.range(0, peaksX.size()).mapToDouble(i -> (xAutoCorr[peaksX.get(i)])).toArray(); +// Plot plotPeaksX = Plot.plot(Plot.plotOpts(). +// title("Peak Detection on X"). +// legend(Plot.LegendFormat.BOTTOM)). +// series("x", Plot.data().xy(rangeAuto, xAutoCorr), Plot.seriesOpts().color(Color.RED)). +// series("Peaks", Plot.data().xy(dPeaksXX, dPeaksXY), Plot.seriesOpts().color(Color.CYAN). +// marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); +// +// windowPeaksX.set(plotPeaksX.draw()); +// +// Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false); +// int yOffset = yAutoCorr.length / 2; +// LinkedList peaksY = pY.getPeaksIdx(); +// +// double[] dPeaksYX = IntStream.range(0, peaksY.size()).mapToDouble(i -> (peaksY.get(i) - yOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); +// double[] dPeaksYY = IntStream.range(0, peaksY.size()).mapToDouble(i -> (yAutoCorr[peaksY.get(i)])).toArray(); +// Plot plotPeaksY = Plot.plot(Plot.plotOpts(). +// title("Peak Detection on Y"). +// legend(Plot.LegendFormat.BOTTOM)). +// series("x", Plot.data().xy(rangeAuto, yAutoCorr), Plot.seriesOpts().color(Color.RED)). +// series("Peaks", Plot.data().xy(dPeaksYX, dPeaksYY), Plot.seriesOpts().color(Color.CYAN). +// marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); +// +// windowPeaksY.set(plotPeaksY.draw()); +// +// Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false); +// int zOffset = zAutoCorr.length / 2; +// LinkedList peaksZ = pZ.getPeaksIdx(); +// +// double[] dPeaksZX = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (peaksZ.get(i) - zOffset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); +// double[] dPeaksZY = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (zAutoCorr[peaksZ.get(i)])).toArray(); +// Plot plotPeaksZ = Plot.plot(Plot.plotOpts(). +// title("Peak Detection on Z"). +// legend(Plot.LegendFormat.BOTTOM)). +// series("x", Plot.data().xy(rangeAuto, zAutoCorr), Plot.seriesOpts().color(Color.RED)). +// series("Peaks", Plot.data().xy(dPeaksZX, dPeaksZY), Plot.seriesOpts().color(Color.CYAN). +// marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); +// +// windowPeaksZ.set(plotPeaksZ.draw()); //fill hols improve peaks @@ -141,8 +203,6 @@ public class Main { //System.out.println("BPM-Y: " + pY.getBPM(bpmEstimator.getSampleRate_ms())); //System.out.println("BPM-Z: " + pZ.getBPM(bpmEstimator.getSampleRate_ms())); - //todo: kleiner fenstergrößen testen. so ist doch etwas langsam auf der Uhr. - int dummyForBreakpoint = 0; } } @@ -158,6 +218,12 @@ public class Main { } } + + try { + System.in.read(); + } catch (IOException e) { + e.printStackTrace(); + } } } } diff --git a/java/src/main/java/Plot.java b/java/src/main/java/Plot.java deleted file mode 100644 index 4b82c49..0000000 --- a/java/src/main/java/Plot.java +++ /dev/null @@ -1,1027 +0,0 @@ -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Font; -import java.awt.FontMetrics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.Polygon; -import java.awt.Rectangle; -import java.awt.Stroke; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import javax.imageio.ImageIO; - -/** - * Simple implementation of plot. Minimal features, no dependencies besides standard libraries. - * Options are self-descriptive, see also samples. - * - * @author Yuriy Guskov - */ -public class Plot { - - public enum Line { NONE, SOLID, DASHED } - public enum Marker { NONE, CIRCLE, SQUARE, DIAMOND, COLUMN, BAR } - public enum AxisFormat { NUMBER, NUMBER_KGM, NUMBER_INT, TIME_HM, TIME_HMS, DATE, DATETIME_HM, DATETIME_HMS } - public enum LegendFormat { NONE, TOP, RIGHT, BOTTOM } - - private enum HorizAlign { LEFT, CENTER, RIGHT } - private enum VertAlign { TOP, CENTER, BOTTOM } - - private PlotOptions opts = new PlotOptions(); - - private Rectangle boundRect; - private PlotArea plotArea; - private Map xAxes = new HashMap(3); - private Map yAxes = new HashMap(3); - private Map dataSeriesMap = new LinkedHashMap(5); - - public static Plot plot(PlotOptions opts) { - return new Plot(opts); - } - - public static PlotOptions plotOpts() { - return new PlotOptions(); - } - - public static class PlotOptions { - - private String title = ""; - private int width = 800; - private int height = 600; - private Color backgroundColor = Color.WHITE; - private Color foregroundColor = Color.BLACK; - private Font titleFont = new Font("Arial", Font.BOLD, 16); - private int padding = 10; // padding for the entire image - private int plotPadding = 5; // padding for plot area (to have min and max values padded) - private int labelPadding = 10; - private int defaultLegendSignSize = 10; - private int legendSignSize = 10; - private Point grids = new Point(10 ,10); // grid lines by x and y - private Color gridColor = Color.GRAY; - private Stroke gridStroke = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, - BasicStroke.JOIN_MITER, 10.0f, new float[] { 5.0f }, 0.0f); - private int tickSize = 5; - private Font labelFont = new Font("Arial", 0, 12); - private LegendFormat legend = LegendFormat.NONE; - - private PlotOptions() {} - - public PlotOptions title(String title) { - this.title = title; - return this; - } - - public PlotOptions width(int width) { - this.width = width; - return this; - } - - public PlotOptions height(int height) { - this.height = height; - return this; - } - - public PlotOptions bgColor(Color color) { - this.backgroundColor = color; - return this; - } - - public PlotOptions fgColor(Color color) { - this.foregroundColor = color; - return this; - } - - public PlotOptions titleFont(Font font) { - this.titleFont = font; - return this; - } - - public PlotOptions padding(int padding) { - this.padding = padding; - return this; - } - - public PlotOptions plotPadding(int padding) { - this.plotPadding = padding; - return this; - } - - public PlotOptions labelPadding(int padding) { - this.labelPadding = padding; - return this; - } - - public PlotOptions labelFont(Font font) { - this.labelFont = font; - return this; - } - - public PlotOptions grids(int byX, int byY) { - this.grids = new Point(byX, byY); - return this; - } - - public PlotOptions gridColor(Color color) { - this.gridColor = color; - return this; - } - - public PlotOptions gridStroke(Stroke stroke) { - this.gridStroke = stroke; - return this; - } - - public PlotOptions tickSize(int value) { - this.tickSize = value; - return this; - } - - public PlotOptions legend(LegendFormat legend) { - this.legend = legend; - return this; - } - - } - - private Plot(PlotOptions opts) { - if (opts != null) - this.opts = opts; - boundRect = new Rectangle(0, 0, this.opts.width, this.opts.height); - plotArea = new PlotArea(); - } - - public PlotOptions opts() { - return opts; - } - - public Plot xAxis(String name, AxisOptions opts) { - xAxes.put(name, new Axis(name, opts)); - return this; - } - - public Plot yAxis(String name, AxisOptions opts) { - yAxes.put(name, new Axis(name, opts)); - return this; - } - - public Plot series(String name, Data data, DataSeriesOptions opts) { - DataSeries series = dataSeriesMap.get(name); - if (opts != null) - opts.setPlot(this); - if (series == null) { - series = new DataSeries(name, data, opts); - dataSeriesMap.put(name, series); - } else { - series.data = data; - series.opts = opts; - } - return this; - } - - public Plot series(String name, DataSeriesOptions opts) { - DataSeries series = dataSeriesMap.get(name); - if (opts != null) - opts.setPlot(this); - if (series != null) - series.opts = opts; - return this; - } - - private void calc(Graphics2D g) { - plotArea.calc(g); - } - - private void clear() { - plotArea.clear(); - for (DataSeries series : dataSeriesMap.values()) - series.clear(); - } - - public BufferedImage draw() { - BufferedImage image = new BufferedImage(opts.width, opts.height, BufferedImage.TYPE_INT_RGB); - Graphics2D g = image.createGraphics(); - try { - calc(g); - drawBackground(g); - plotArea.draw(g); - for (DataSeries series : dataSeriesMap.values()) - series.draw(g); - return image; - } finally { - g.dispose(); - } - } - - private void drawBackground(Graphics2D g) { - g.setColor(opts.backgroundColor); - g.fillRect(0, 0, opts.width, opts.height); - } - - public void save(String fileName, String type) throws IOException { - clear(); - BufferedImage bi = draw(); - File outputFile = new File(fileName + "." + type); - ImageIO.write(bi, type, outputFile); - } - - private class Legend { - Rectangle rect; - Rectangle2D labelRect; - public int entryWidth; - public int entryWidthPadded; - public int entryCount; - public int xCount; - public int yCount; - } - - private class PlotArea { - - private Rectangle plotBorderRect = new Rectangle(); // boundRect | labels/legend | plotBorderRect | plotPadding | plotRect/clipRect - private Rectangle plotRect = new Rectangle(); - private Rectangle plotClipRect = new Rectangle(); - private Legend legend = new Legend(); - - private Range xPlotRange = new Range(0, 0); - private Range yPlotRange = new Range(0, 0); - - public PlotArea() { - clear(); - } - - private void clear() { - plotBorderRect.setBounds(boundRect); - plotRectChanged(); - } - - private void offset(int dx, int dy, int dw, int dh) { - plotBorderRect.translate(dx, dy); - plotBorderRect.setSize(plotBorderRect.width - dx - dw, plotBorderRect.height - dy - dh); - plotRectChanged(); - } - - private void plotRectChanged() { - plotRect.setBounds(plotBorderRect.x + opts.plotPadding, plotBorderRect.y + opts.plotPadding, - plotBorderRect.width - opts.plotPadding * 2, plotBorderRect.height - opts.plotPadding * 2); - xPlotRange.setMin(plotRect.getX()); - xPlotRange.setMax(plotRect.getX() + plotRect.getWidth()); - yPlotRange.setMin(plotRect.getY()); - yPlotRange.setMax(plotRect.getY() + plotRect.getHeight()); - - plotClipRect.setBounds(plotBorderRect.x + 1, plotBorderRect.y + 1, plotBorderRect.width - 1, plotBorderRect.height - 1); - } - - private void calc(Graphics2D g) { - calcAxes(g); - calcRange(true); - calcRange(false); - calcAxisLabels(g, true); - calcAxisLabels(g, false); - g.setFont(opts.titleFont); - FontMetrics fm = g.getFontMetrics(); - Rectangle2D titleRect = fm.getStringBounds(opts.title, g); - g.setFont(opts.labelFont); - fm = g.getFontMetrics(); - int xAxesHeight = 0, xAxesHalfWidth = 0; - for (Map.Entry entry : xAxes.entrySet()) { - Axis xAxis = entry.getValue(); - xAxesHeight += toInt(xAxis.labelRect.getHeight()) + opts.labelPadding * 2; - if (xAxis.labelRect.getWidth() > xAxesHalfWidth) - xAxesHalfWidth = toInt(xAxis.labelRect.getWidth()); - } - int yAxesWidth = 0; - for (Map.Entry entry : yAxes.entrySet()) - yAxesWidth += toInt(entry.getValue().labelRect.getWidth()) + opts.labelPadding * 2; - int dx = opts.padding + yAxesWidth; - int dy = opts.padding + toInt(titleRect.getHeight() + opts.labelPadding); - int dw = opts.padding; - if (opts.legend != LegendFormat.RIGHT) - dw += xAxesHalfWidth; // half of label goes beyond a plot in right bottom corner - int dh = opts.padding + xAxesHeight; - // offset for legend - Rectangle temp = new Rectangle(plotBorderRect); // save plotRect - offset(dx, dy, dw, dh); - calcLegend(g); // use plotRect - plotBorderRect.setBounds(temp); // restore plotRect - switch (opts.legend) { - case TOP: dy += legend.rect.height + opts.labelPadding; break; - case RIGHT: dw += legend.rect.width + opts.labelPadding; break; - case BOTTOM: dh += legend.rect.height; break; - default: - } - offset(dx, dy, dw, dh); - } - - private void draw(Graphics2D g) { - drawPlotArea(g); - drawGrid(g); - drawAxes(g); - drawLegend(g); - // if check needed that content is inside padding - //g.setColor(Color.GRAY); - //g.drawRect(boundRect.x + opts.padding, boundRect.y + opts.padding, boundRect.width - opts.padding * 2, boundRect.height - opts.padding * 2); - } - - private void drawPlotArea(Graphics2D g) { - g.setColor(opts.foregroundColor); - g.drawRect(plotBorderRect.x, plotBorderRect.y, plotBorderRect.width, plotBorderRect.height); - g.setFont(opts.titleFont); - drawLabel(g, opts.title, plotBorderRect.x + toInt(plotBorderRect.getWidth() / 2), opts.padding, HorizAlign.CENTER, VertAlign.TOP); - } - - private void drawGrid(Graphics2D g) { - Stroke stroke = g.getStroke(); - g.setStroke(opts.gridStroke); - g.setColor(opts.gridColor); - - int leftX = plotBorderRect.x + 1; - int rightX = plotBorderRect.x + plotBorderRect.width - 1; - int topY = plotBorderRect.y + 1; - int bottomY = plotBorderRect.y + plotBorderRect.height - 1; - - for (int i = 0; i < opts.grids.x + 1; i++) { - int x = toInt(plotRect.x + (plotRect.getWidth() / opts.grids.x) * i); - g.drawLine(x, topY, x, bottomY); - } - - for (int i = 0; i < opts.grids.y + 1; i++) { - int y = toInt(plotRect.y + (plotRect.getHeight() / opts.grids.y) * i); - g.drawLine(leftX, y, rightX, y); - } - - g.setStroke(stroke); - } - - private void calcAxes(Graphics2D g) { - Axis xAxis = xAxes.isEmpty() ? new Axis("", null) : xAxes.values().iterator().next(); - Axis yAxis = yAxes.isEmpty() ? new Axis("", null) : yAxes.values().iterator().next(); - int xCount = 0, yCount = 0; - for (DataSeries series : dataSeriesMap.values()) { - if (series.opts.xAxis == null) { - series.opts.xAxis = xAxis; - xCount++; - } - if (series.opts.yAxis == null) { - series.opts.yAxis = yAxis; - yCount++; - } - series.addAxesToName(); - } - if (xAxes.isEmpty() && xCount > 0) - xAxes.put("x", xAxis); - if (yAxes.isEmpty() && yCount > 0) - yAxes.put("y", yAxis); - } - - private void calcAxisLabels(Graphics2D g, boolean isX) { - FontMetrics fm = g.getFontMetrics(); - Rectangle2D rect = null; - double w = 0, h = 0; - Map axes = isX ? xAxes : yAxes; - int grids = isX ? opts.grids.x : opts.grids.y; - for (Map.Entry entry : axes.entrySet()) { - Axis axis = entry.getValue(); - axis.labels = new String[grids + 1]; - axis.labelRect = fm.getStringBounds("", g); - double xStep = axis.opts.range.diff / grids; - for (int j = 0; j < grids + 1; j++) { - axis.labels[j] = formatDouble(axis.opts.range.min + xStep * j, axis.opts.format); - rect = fm.getStringBounds(axis.labels[j], g); - if (rect.getWidth() > w) - w = rect.getWidth(); - if (rect.getHeight() > h) - h = rect.getHeight(); - } - axis.labelRect.setRect(0, 0, w, h); - } - } - - private void calcRange(boolean isX) { - for (DataSeries series : dataSeriesMap.values()) { - Axis axis = isX ? series.opts.xAxis : series.opts.yAxis; - if (axis.opts.dynamicRange) { - Range range = isX ? series.xRange() : series.yRange(); - if (axis.opts.range == null) - axis.opts.range = range; - else { - if (range.max > axis.opts.range.max) - axis.opts.range.setMax(range.max); - if (range.min < axis.opts.range.min) - axis.opts.range.setMin(range.min); - } - } - } - Map axes = isX ? xAxes : yAxes; - for (Iterator it = axes.values().iterator(); it.hasNext(); ) { - Axis axis = it.next(); - if (axis.opts.range == null) - it.remove(); - } - } - - private void drawAxes(Graphics2D g) { - g.setFont(opts.labelFont); - g.setColor(opts.foregroundColor); - - int leftXPadded = plotBorderRect.x - opts.labelPadding; - int rightX = plotBorderRect.x + plotBorderRect.width; - int bottomY = plotBorderRect.y + plotBorderRect.height; - int bottomYPadded = bottomY + opts.labelPadding; - - int axisOffset = 0; - for (Map.Entry entry : xAxes.entrySet()) { - Axis axis = entry.getValue(); - double xStep = axis.opts.range.diff / opts.grids.x; - - drawLabel(g, axis.name, rightX + opts.labelPadding, bottomY + axisOffset, HorizAlign.LEFT, VertAlign.CENTER); - g.drawLine(plotRect.x, bottomY + axisOffset, plotRect.x + plotRect.width, bottomY + axisOffset); - - for (int j = 0; j < opts.grids.x + 1; j++) { - int x = toInt(plotRect.x + (plotRect.getWidth() / opts.grids.x) * j); - drawLabel(g, formatDouble(axis.opts.range.min + xStep * j, axis.opts.format), x, bottomYPadded + axisOffset, HorizAlign.CENTER, VertAlign.TOP); - g.drawLine(x, bottomY + axisOffset, x, bottomY + opts.tickSize + axisOffset); - } - axisOffset += toInt(axis.labelRect.getHeight() + opts.labelPadding * 2); - } - - axisOffset = 0; - for (Map.Entry entry : yAxes.entrySet()) { - Axis axis = entry.getValue(); - double yStep = axis.opts.range.diff / opts.grids.y; - - drawLabel(g, axis.name, leftXPadded - axisOffset, plotBorderRect.y - toInt(axis.labelRect.getHeight() + opts.labelPadding), HorizAlign.RIGHT, VertAlign.CENTER); - g.drawLine(plotBorderRect.x - axisOffset, plotRect.y + plotRect.height, plotBorderRect.x - axisOffset, plotRect.y); - - for (int j = 0; j < opts.grids.y + 1; j++) { - int y = toInt(plotRect.y + (plotRect.getHeight() / opts.grids.y) * j); - drawLabel(g, formatDouble(axis.opts.range.max - yStep * j, axis.opts.format), leftXPadded - axisOffset, y, HorizAlign.RIGHT, VertAlign.CENTER); - g.drawLine(plotBorderRect.x - axisOffset, y, plotBorderRect.x - opts.tickSize - axisOffset, y); - } - axisOffset += toInt(axis.labelRect.getWidth() + opts.labelPadding * 2); - } - } - - private void calcLegend(Graphics2D g) { - legend.rect = new Rectangle(0, 0); - if (opts.legend == LegendFormat.NONE) - return; - int size = dataSeriesMap.size(); - if (size == 0) - return; - - FontMetrics fm = g.getFontMetrics(); - Iterator it = dataSeriesMap.values().iterator(); - legend.labelRect = fm.getStringBounds(it.next().nameWithAxes, g); - int legendSignSize = opts.defaultLegendSignSize; - while (it.hasNext()) { - DataSeries series = it.next(); - Rectangle2D rect = fm.getStringBounds(series.nameWithAxes, g); - if (rect.getWidth() > legend.labelRect.getWidth()) - legend.labelRect.setRect(0, 0, rect.getWidth(), legend.labelRect.getHeight()); - if (rect.getHeight() > legend.labelRect.getHeight()) - legend.labelRect.setRect(0, 0, legend.labelRect.getWidth(), rect.getHeight()); - switch (series.opts.marker) { - case CIRCLE: case SQUARE: - if (series.opts.markerSize + opts.defaultLegendSignSize > legendSignSize) - legendSignSize = series.opts.markerSize + opts.defaultLegendSignSize; - break; - case DIAMOND: - if (series.getDiagMarkerSize() + opts.defaultLegendSignSize > legendSignSize) - legendSignSize = series.getDiagMarkerSize() + opts.defaultLegendSignSize; - break; - default: - } - } - opts.legendSignSize = legendSignSize; - - legend.entryWidth = legendSignSize + opts.labelPadding + toInt(legend.labelRect.getWidth()); - legend.entryWidthPadded = legend.entryWidth + opts.labelPadding; - - switch (opts.legend) { - case TOP: case BOTTOM: - legend.entryCount = (int) Math.floor((double) (plotBorderRect.width - opts.labelPadding) / legend.entryWidthPadded); - legend.xCount = size <= legend.entryCount ? size : legend.entryCount; - legend.yCount = size <= legend.entryCount ? 1 : (int) Math.ceil((double) size / legend.entryCount); - legend.rect.width = opts.labelPadding + (legend.xCount * legend.entryWidthPadded); - legend.rect.height = opts.labelPadding + toInt(legend.yCount * (opts.labelPadding + legend.labelRect.getHeight())); - legend.rect.x = plotBorderRect.x + (plotBorderRect.width - legend.rect.width) / 2; - if (opts.legend == LegendFormat.TOP) - legend.rect.y = plotBorderRect.y; - else - legend.rect.y = boundRect.height - legend.rect.height - opts.padding; - break; - case RIGHT: - legend.rect.width = opts.labelPadding * 3 + legendSignSize + toInt(legend.labelRect.getWidth()); - legend.rect.height = opts.labelPadding * (size + 1) + toInt(legend.labelRect.getHeight() * size); - legend.rect.x = boundRect.width - legend.rect.width - opts.padding; - legend.rect.y = plotBorderRect.y + plotBorderRect.height / 2 - legend.rect.height / 2; - break; - default: - } - } - - private void drawLegend(Graphics2D g) { - if (opts.legend == LegendFormat.NONE) - return; - - g.drawRect(legend.rect.x, legend.rect.y, legend.rect.width, legend.rect.height); - int labelHeight = toInt(legend.labelRect.getHeight()); - int x = legend.rect.x + opts.labelPadding; - int y = legend.rect.y + opts.labelPadding + labelHeight / 2; - - switch (opts.legend) { - case TOP: case BOTTOM: - int i = 0; - for (DataSeries series : dataSeriesMap.values()) { - drawLegendEntry(g, series, x, y); - x += legend.entryWidthPadded; - if ((i + 1) % legend.xCount == 0) { - x = legend.rect.x + opts.labelPadding; - y += opts.labelPadding + labelHeight; - } - i++; - } - break; - case RIGHT: - for (DataSeries series : dataSeriesMap.values()) { - drawLegendEntry(g, series, x, y); - y += opts.labelPadding + labelHeight; - } - break; - default: - } - } - - private void drawLegendEntry(Graphics2D g, DataSeries series, int x, int y) { - series.fillArea(g, x, y, x + opts.legendSignSize, y, y + opts.legendSignSize / 2); - series.drawLine(g, x, y, x + opts.legendSignSize, y); - series.drawMarker(g, x + opts.legendSignSize / 2, y, x, y + opts.legendSignSize / 2); - g.setColor(opts.foregroundColor); - drawLabel(g, series.nameWithAxes, x + opts.legendSignSize + opts.labelPadding, y, HorizAlign.LEFT, VertAlign.CENTER); - } - - } - - public static class Range { - - private double min; - private double max; - private double diff; - - public Range(double min, double max) { - this.min = min; - this.max = max; - this.diff = max - min; - } - - public Range(Range range) { - this.min = range.min; - this.max = range.max; - this.diff = max - min; - } - - public void setMin(double min) { - this.min = min; - this.diff = max - min; - } - - public void setMax(double max) { - this.max = max; - this.diff = max - min; - } - - @Override - public String toString() { - return "Range [min=" + min + ", max=" + max + "]"; - } - - } - - public static AxisOptions axisOpts() { - return new AxisOptions(); - } - - public static class AxisOptions { - - private AxisFormat format = AxisFormat.NUMBER; - private boolean dynamicRange = true; - private Range range; - - public AxisOptions format(AxisFormat format) { - this.format = format; - return this; - } - - public AxisOptions range(double min, double max) { - this.range = new Range(min, max); - this.dynamicRange = false; - return this; - } - - } - - private class Axis { - - private String name; - private AxisOptions opts = new AxisOptions(); - private Rectangle2D labelRect; - private String[] labels; - - public Axis(String name, AxisOptions opts) { - this.name = name; - if (opts != null) - this.opts = opts; - } - - @Override - public String toString() { - return "Axis [name=" + name + ", opts=" + opts + "]"; - } - - } - - public static DataSeriesOptions seriesOpts() { - return new DataSeriesOptions(); - } - - public static class DataSeriesOptions { - - private Color seriesColor = Color.BLUE; - private Line line = Line.SOLID; - private int lineWidth = 2; - private float[] lineDash = new float[] { 3.0f, 3.0f }; - private Marker marker = Marker.NONE; - private int markerSize = 10; - private Color markerColor = Color.WHITE; - private Color areaColor = null; - private String xAxisName; - private String yAxisName; - private Axis xAxis; - private Axis yAxis; - - public DataSeriesOptions color(Color seriesColor) { - this.seriesColor = seriesColor; - return this; - } - - public DataSeriesOptions line(Line line) { - this.line = line; - return this; - } - - public DataSeriesOptions lineWidth(int width) { - this.lineWidth = width; - return this; - } - - public DataSeriesOptions lineDash(float[] dash) { - this.lineDash = dash; - return this; - } - - public DataSeriesOptions marker(Marker marker) { - this.marker = marker; - return this; - } - - public DataSeriesOptions markerSize(int markerSize) { - this.markerSize = markerSize; - return this; - } - - public DataSeriesOptions markerColor(Color color) { - this.markerColor = color; - return this; - } - - public DataSeriesOptions areaColor(Color color) { - this.areaColor = color; - return this; - } - - public DataSeriesOptions xAxis(String name) { - this.xAxisName = name; - return this; - } - - public DataSeriesOptions yAxis(String name) { - this.yAxisName = name; - return this; - } - - private void setPlot(Plot plot) { - if (plot != null) - this.xAxis = plot.xAxes.get(xAxisName); - if (plot != null) - this.yAxis = plot.yAxes.get(yAxisName); - } - - } - - public static Data data() { - return new Data(); - } - - public static class Data { - - private double[] x1; - private double[] y1; - private List x2; - private List y2; - - private Data() {} - - public Data xy(double[] x, double[] y) { - this.x1 = x; - this.y1 = y; - return this; - } - - public Data xy(double x, double y) { - if (this.x2 == null || this.y2 == null) { - this.x2 = new ArrayList(10); - this.y2 = new ArrayList(10); - } - x2.add(x); - y2.add(y); - return this; - } - - public Data xy(List x, List y) { - this.x2 = x; - this.y2 = y; - return this; - } - - public int size() { - if (x1 != null) - return x1.length; - if (x2 != null) - return x2.size(); - return 0; - } - - public double x(int i) { - if (x1 != null) - return x1[i]; - if (x2 != null) - return x2.get(i); - return 0; - } - - public double y(int i) { - if (y1 != null) - return y1[i]; - if (y2 != null) - return y2.get(i); - return 0; - } - - } - - public class DataSeries { - - private String name; - private String nameWithAxes; - private DataSeriesOptions opts = new DataSeriesOptions(); - private Data data; - - public DataSeries(String name, Data data, DataSeriesOptions opts) { - if (opts != null) - this.opts = opts; - this.name = name; - this.data = data; - if (this.data == null) - this.data = data(); - } - - public void clear() { - } - - private void addAxesToName() { - this.nameWithAxes = this.name + " (" + opts.yAxis.name + "/" + opts.xAxis.name + ")"; - } - - private Range xRange() { - Range range = new Range(0, 0); - if (data != null && data.size() > 0) { - range = new Range(data.x(0), data.x(0)); - for (int i = 1; i < data.size(); i++) { - if (data.x(i) > range.max) - range.setMax(data.x(i)); - if (data.x(i) < range.min) - range.setMin(data.x(i)); - } - } - return range; - } - - private Range yRange() { - Range range = new Range(0, 0); - if (data != null && data.size() > 0) { - range = new Range(data.y(0), data.y(0)); - for (int i = 1; i < data.size(); i++) { - if (data.y(i) > range.max) - range.setMax(data.y(i)); - if (data.y(i) < range.min) - range.setMin(data.y(i)); - } - } - return range; - } - - private void draw(Graphics2D g) { - g.setClip(plotArea.plotClipRect); - if (data != null) { - double x1 = 0, y1 = 0; - int size = data.size(); - if (opts.line != Line.NONE) - for (int j = 0; j < size; j++) { - double x2 = x2x(data.x(j), opts.xAxis.opts.range, plotArea.xPlotRange); - double y2 = y2y(data.y(j), opts.yAxis.opts.range, plotArea.yPlotRange); - int ix1 = toInt(x1), iy1 = toInt(y1), ix2 = toInt(x2), iy2 = toInt(y2); - int iy3 = plotArea.plotRect.y + plotArea.plotRect.height; - // special case for the case when only the first point present - if (size == 1) { - ix1 = ix2; - iy1 = iy2; - } - if (j != 0 || size == 1) { - fillArea(g, ix1, iy1, ix2, iy2, iy3); - drawLine(g, ix1, iy1, ix2, iy2); - } - x1 = x2; - y1 = y2; - } - - int halfMarkerSize = opts.markerSize / 2; - int halfDiagMarkerSize = getDiagMarkerSize() / 2; - g.setStroke(new BasicStroke(2)); - if (opts.marker != Marker.NONE) - for (int j = 0; j < size; j++) { - double x2 = x2x(data.x(j), opts.xAxis.opts.range, plotArea.xPlotRange); - double y2 = y2y(data.y(j), opts.yAxis.opts.range, plotArea.yPlotRange); - drawMarker(g, halfMarkerSize, halfDiagMarkerSize, x2, y2, - plotArea.plotRect.x, plotArea.plotRect.y + plotArea.plotRect.height); - } - } - } - - private int getDiagMarkerSize() { - return (int) Math.round(Math.sqrt(2 * opts.markerSize * opts.markerSize)); - } - - private void fillArea(Graphics2D g, int ix1, int iy1, int ix2, int iy2, int iy3) { - if (opts.areaColor != null) { - g.setColor(opts.areaColor); - g.fill(new Polygon( - new int[] { ix1, ix2, ix2, ix1 }, - new int[] { iy1, iy2, iy3, iy3 }, - 4)); - g.setColor(opts.seriesColor); - } - } - - private void drawLine(Graphics2D g, int ix1, int iy1, int ix2, int iy2) { - if (opts.line != Line.NONE) { - g.setColor(opts.seriesColor); - setStroke(g); - g.drawLine(ix1, iy1, ix2, iy2); - } - } - - private void setStroke(Graphics2D g) { - switch (opts.line) { - case SOLID: - g.setStroke(new BasicStroke(opts.lineWidth)); - break; - case DASHED: - g.setStroke(new BasicStroke(opts.lineWidth, BasicStroke.CAP_ROUND, - BasicStroke.JOIN_ROUND, 10.0f, opts.lineDash, 0.0f)); - break; - default: - } - } - - private void drawMarker(Graphics2D g, int x2, int y2, int x3, int y3) { - int halfMarkerSize = opts.markerSize / 2; - int halfDiagMarkerSize = getDiagMarkerSize() / 2; - g.setStroke(new BasicStroke(2)); - drawMarker(g, halfMarkerSize, halfDiagMarkerSize, x2, y2, x3, y3); - } - - private void drawMarker(Graphics2D g, int halfMarkerSize, int halfDiagMarkerSize, double x2, double y2, double x3, double y3) { - switch (opts.marker) { - case CIRCLE: - g.setColor(opts.markerColor); - g.fillOval(toInt(x2 - halfMarkerSize), toInt(y2 - halfMarkerSize), opts.markerSize, opts.markerSize); - g.setColor(opts.seriesColor); - g.drawOval(toInt(x2 - halfMarkerSize), toInt(y2 - halfMarkerSize), opts.markerSize, opts.markerSize); - break; - case SQUARE: - g.setColor(opts.markerColor); - g.fillRect(toInt(x2 - halfMarkerSize), toInt(y2 - halfMarkerSize), opts.markerSize, opts.markerSize); - g.setColor(opts.seriesColor); - g.drawRect(toInt(x2 - halfMarkerSize), toInt(y2 - halfMarkerSize), opts.markerSize, opts.markerSize); - break; - case DIAMOND: - int[] xpts = { toInt(x2), toInt(x2 + halfDiagMarkerSize), toInt(x2), toInt(x2 - halfDiagMarkerSize) }; - int[] ypts = { toInt(y2 - halfDiagMarkerSize), toInt(y2), toInt(y2 + halfDiagMarkerSize), toInt(y2) }; - g.setColor(opts.markerColor); - g.fillPolygon(xpts, ypts, 4); - g.setColor(opts.seriesColor); - g.drawPolygon(xpts, ypts, 4); - break; - case COLUMN: - g.setColor(opts.markerColor); - g.fillRect(toInt(x2), toInt(y2), opts.markerSize, toInt(y3 - y2)); - g.setColor(opts.seriesColor); - g.drawRect(toInt(x2), toInt(y2), opts.markerSize, toInt(y3 - y2)); - break; - case BAR: - g.setColor(opts.markerColor); - g.fillRect(toInt(x3), toInt(y2), toInt(x2 - x3), opts.markerSize); - g.setColor(opts.seriesColor); - g.drawRect(toInt(x3), toInt(y2), toInt(x2 - x3), opts.markerSize); - break; - default: - } - } - - } - - private static void drawLabel(Graphics2D g, String s, int x, int y, HorizAlign hAlign, VertAlign vAlign) { - FontMetrics fm = g.getFontMetrics(); - Rectangle2D rect = fm.getStringBounds(s, g); - - // by default align by left - if (hAlign == HorizAlign.RIGHT) - x -= rect.getWidth(); - else if (hAlign == HorizAlign.CENTER) - x -= rect.getWidth() / 2; - - // by default align by bottom - if (vAlign == VertAlign.TOP) - y += rect.getHeight(); - else if (vAlign == VertAlign.CENTER) - y += rect.getHeight() / 2; - - g.drawString(s, x, y); - } - - public static String formatDouble(double d, AxisFormat format) { - switch (format) { - case TIME_HM: return String.format("%tR", new java.util.Date((long) d)); - case TIME_HMS: return String.format("%tT", new java.util.Date((long) d)); - case DATE: return String.format("%tF", new java.util.Date((long) d)); - case DATETIME_HM: return String.format("%tF %1$tR", new java.util.Date((long) d)); - case DATETIME_HMS: return String.format("%tF %1$tT", new java.util.Date((long) d)); - case NUMBER_KGM: return formatDoubleAsNumber(d, true); - case NUMBER_INT: return Integer.toString((int) d); - default: return formatDoubleAsNumber(d, false); - } - } - - private static String formatDoubleAsNumber(double d, boolean useKGM) { - if (useKGM && d > 1000 && d < 1000000000000l) { - long[] numbers = new long[] { 1000l, 1000000l, 1000000000l }; - char[] suffix = new char[] { 'K', 'M', 'G' }; - - int i = 0; - double r = 0; - for (long number : numbers) { - r = d / number; - if (r < 1000) - break; - i++; - } - if (i == suffix.length) - i--; - return String.format("%1$,.2f%2$c", r, suffix[i]); - } - else - return String.format("%1$.3G", d); - } - - private static double x2x(double x, Range xr1, Range xr2) { - return xr1.diff == 0 ? xr2.min + xr2.diff / 2 : xr2.min + (x - xr1.min) / xr1.diff * xr2.diff; - } - - // y axis is reverse in Graphics - private static double y2y(double x, Range xr1, Range xr2) { - return xr1.diff == 0 ? xr2.min + xr2.diff / 2 : xr2.max - (x - xr1.min) / xr1.diff * xr2.diff; - } - - private static int toInt(double d) { - return (int) Math.round(d); - } - -} diff --git a/java/src/main/java/bpmEstimation/AccelerometerWindowBuffer.java b/java/src/main/java/bpmEstimation/AccelerometerWindowBuffer.java index 09103a8..f97ea03 100644 --- a/java/src/main/java/bpmEstimation/AccelerometerWindowBuffer.java +++ b/java/src/main/java/bpmEstimation/AccelerometerWindowBuffer.java @@ -2,14 +2,15 @@ package bpmEstimation; import utilities.Utils; import java.util.ArrayList; +import java.util.List; /** * Created by toni on 15/12/17. */ public class AccelerometerWindowBuffer extends ArrayList { - private static int mWindowSize; // in ms - private static int mOverlapSize; // in ms + private final int mWindowSize; // in ms + private final int mOverlapSize; // in ms private long mOverlapCounter; public AccelerometerWindowBuffer(int windowSize, int overlap){ @@ -18,37 +19,55 @@ public class AccelerometerWindowBuffer extends ArrayList { mOverlapCounter = 0; } + public AccelerometerWindowBuffer(List list, int overlap){ + mWindowSize = list.size(); + mOverlapSize = overlap; + mOverlapCounter = 0; + super.addAll(list); + } + //TODO: add exception handling. falseArgument if ad has no numeric x,y,z public boolean add(AccelerometerData ad){ + synchronized (this){ - //do not add duplicates! - if(!isEmpty() && getYongest().equals(ad)){ - return false; + //do not add duplicates! + if(!isEmpty() && getYongest().equals(ad)){ + return false; + } + + // current - last to increment overlap time + if(!isEmpty()){ + mOverlapCounter += ad.ts - getYongest().ts; + } + + //add element + boolean r = super.add(ad); + removeOldElements(); + + return r; } + } - // current - last to increment overlap time - if(!isEmpty()){ - mOverlapCounter += ad.ts - getYongest().ts; - } + private void removeOldElements(){ + synchronized (this) { + if (!isEmpty()) { + if ((getYongest().ts - getOldest().ts) > mWindowSize) { - //add element - boolean r = super.add(ad); - if ((getYongest().ts - getOldest().ts) > mWindowSize){ - - long oldestTime = getYongest().ts - mWindowSize; - for(int i = 0; i < size(); ++i) { - if (get(i).ts > oldestTime) { - break; + long oldestTime = getYongest().ts - mWindowSize; + for (int i = 0; i < size(); ++i) { + if (get(i).ts > oldestTime) { + break; + } + super.remove(i); + } } - remove(i); } } - return r; } public boolean isNextWindowReady(){ if(!isEmpty()){ - if(((getYongest().ts - getOldest().ts) > mWindowSize / 2) && mOverlapCounter > mOverlapSize){ + if(((getYongest().ts - getOldest().ts) > mWindowSize / 4) && mOverlapCounter > mOverlapSize){ mOverlapCounter = 0; return true; @@ -57,6 +76,31 @@ public class AccelerometerWindowBuffer extends ArrayList { return false; } + public static AccelerometerWindowBuffer getNewInstance(AccelerometerWindowBuffer buffer, int size, int overlap) { + + double sampleRate = ((buffer.getYongest().ts - buffer.getOldest().ts) / buffer.size()); + + //if current size is smaller then wanted size, start at 0 and provide smaller list + int start = 0; + if ((buffer.getYongest().ts - buffer.getOldest().ts) > size) { + start = (int) Math.round(buffer.size() - (size / sampleRate)); + } + + // start should not be negative, this can happen due to rounding errors. + start = start < 0 ? 0 : start; + + List syncList; + synchronized (buffer) { + syncList = buffer.subList(start, buffer.size()); + } + + return new AccelerometerWindowBuffer(syncList, overlap); + } + + public static AccelerometerWindowBuffer getNewInstance(AccelerometerWindowBuffer buffer) { + return new AccelerometerWindowBuffer(buffer, buffer.getOverlapSize()); + } + public AccelerometerData getYongest() { return get(size() - 1); } @@ -84,4 +128,6 @@ public class AccelerometerWindowBuffer extends ArrayList { public int getOverlapSize(){ return mOverlapSize; } + + public int getWindowSize(){return mWindowSize; } } \ No newline at end of file diff --git a/java/src/main/java/bpmEstimation/BpmEstimator.java b/java/src/main/java/bpmEstimation/BpmEstimator.java index 8440bdd..75163d5 100644 --- a/java/src/main/java/bpmEstimation/BpmEstimator.java +++ b/java/src/main/java/bpmEstimation/BpmEstimator.java @@ -1,9 +1,13 @@ package bpmEstimation; +import static utilities.Utils.DEBUG_MODE; + import utilities.MovingFilter; import utilities.SimpleKalman; import utilities.Utils; +import uk.me.berndporr.iirj.*; + import java.util.LinkedList; /** @@ -17,6 +21,7 @@ public class BpmEstimator { private LinkedList mBpmHistory_X; private LinkedList mBpmHistory_Y; private LinkedList mBpmHistory_Z; + private LinkedList mBpmHistory_Mag; private LinkedList mBpmHistory; private int mResetCounter; @@ -25,6 +30,13 @@ public class BpmEstimator { private MovingFilter mMvg; //private SimpleKalman mKalman; + private Butterworth mButter_X; + private Butterworth mButter_Y; + private Butterworth mButter_Z; + private Butterworth mButter_Mag; + + //Debugging stuff + private Utils.DebugPlotter plotter; public BpmEstimator(AccelerometerWindowBuffer windowBuffer, double sampleRate_ms, int resetAfter_ms){ mBuffer = windowBuffer; @@ -33,39 +45,107 @@ public class BpmEstimator { mBpmHistory_X = new LinkedList<>(); mBpmHistory_Y = new LinkedList<>(); mBpmHistory_Z = new LinkedList<>(); + mBpmHistory_Mag = new LinkedList<>(); mBpmHistory = new LinkedList<>(); mResetCounter = 0; mResetLimit_ms = resetAfter_ms; - mMvg = new MovingFilter(10); + mMvg = new MovingFilter(2); //mKalman = new SimpleKalman(); + + mButter_X = new Butterworth(); + mButter_Y = new Butterworth(); + mButter_Z = new Butterworth(); + mButter_Mag = new Butterworth(); + + if(DEBUG_MODE){ + plotter = new Utils.DebugPlotter(); + } } public double estimate(){ + return estimate(mBuffer.getWindowSize(), mBuffer.getOverlapSize()); + } + + public double estimate(int length_ms, int overlap_ms){ + + AccelerometerWindowBuffer tmpBuffer = AccelerometerWindowBuffer.getNewInstance(mBuffer, length_ms, overlap_ms); double sampleRate = mSampleRate_ms; if(sampleRate <= 0){ - sampleRate = Math.round(Utils.mean(Utils.diff(mBuffer.getTs()))); + sampleRate = Math.round(Utils.mean(Utils.diff(tmpBuffer.getTs()))); } - AccelerometerInterpolator interp = new AccelerometerInterpolator(mBuffer, sampleRate); + assert sampleRate != 0 : "samplerate is zero"; - double[] xAutoCorr = new AutoCorrelation(interp.getX(), mBuffer.size()).getCorr(); - double[] yAutoCorr = new AutoCorrelation(interp.getY(), mBuffer.size()).getCorr(); - double[] zAutoCorr = new AutoCorrelation(interp.getZ(), mBuffer.size()).getCorr(); + // interpolate + AccelerometerInterpolator interp = new AccelerometerInterpolator(tmpBuffer, sampleRate); + double[] magRaw = Utils.magnitude(interp); + + //todo: aufräumen. funktion die eine achse bekommt und aus ihr dann die peaks zurück gibt. für debuggen gleich zeichenfunktionen dazu. + + // butterworth lowpass filter, cutoff at 3 hz (~180 bpm) + mButter_X.lowPass(1,(1/(sampleRate / 1000))/2, 1); + mButter_Y.lowPass(1,(1/(sampleRate / 1000))/2, 1); + mButter_Z.lowPass(1,(1/(sampleRate / 1000))/2, 1); + + int n = interp.getX().length; + double[] xButter = new double[n]; + double[] yButter = new double[n];; + double[] zButter = new double[n];; + for(int i = 0; i < interp.getX().length; ++i){ + xButter[i] = mButter_X.filter(interp.getX()[i]); + yButter[i] = mButter_Y.filter(interp.getY()[i]); + zButter[i] = mButter_Z.filter(interp.getZ()[i]); + } + + //TODO: compare with own mButter_Mag.filter + double[] magButter = Utils.magnitude(xButter, yButter, zButter); + + //auto correlation + double[] xAutoCorr = new AutoCorrelation(xButter, tmpBuffer.size()).getCorr(); + double[] yAutoCorr = new AutoCorrelation(yButter, tmpBuffer.size()).getCorr(); + double[] zAutoCorr = new AutoCorrelation(zButter, tmpBuffer.size()).getCorr(); + double[] magAutoCorr = new AutoCorrelation(magRaw, tmpBuffer.size()).getCorr(); //find a peak within range of 250 ms int peakWidth = (int) Math.round(250 / sampleRate); - Peaks pX = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false); - Peaks pY = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false); - Peaks pZ = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false); + Peaks xPeaks = new Peaks(xAutoCorr, peakWidth, 0.1f, 0, false); + Peaks yPeaks = new Peaks(yAutoCorr, peakWidth, 0.1f, 0, false); + Peaks zPeaks = new Peaks(zAutoCorr, peakWidth, 0.1f, 0, false); + Peaks magPeaks = new Peaks(magAutoCorr, peakWidth, 0.1f, 0, false); - mBpmHistory_X.add(pX.getBPM(sampleRate)); - mBpmHistory_Y.add(pY.getBPM(sampleRate)); - mBpmHistory_Z.add(pZ.getBPM(sampleRate)); + mBpmHistory_X.add(xPeaks.getBPM(sampleRate)); + mBpmHistory_Y.add(yPeaks.getBPM(sampleRate)); + mBpmHistory_Z.add(zPeaks.getBPM(sampleRate)); + mBpmHistory_Mag.add(magPeaks.getBPM(sampleRate)); - double estimatedBPM = getBestBpmEstimation(pX, pY, pZ); + + if(DEBUG_MODE){ + plotter.setPlotRawX(interp.getTs(), interp.getX()); + plotter.setPlotRawY(interp.getTs(), interp.getY()); + plotter.setPlotRawZ(interp.getTs(), interp.getZ()); + plotter.setPlotRawMag(interp.getTs(), magRaw); + + plotter.setPlotButterX(interp.getTs(), xButter); + plotter.setPlotButterY(interp.getTs(), yButter); + plotter.setPlotButterZ(interp.getTs(), zButter); + plotter.setPlotButterMag(interp.getTs(), magButter); + + plotter.setPlotCorrX(xAutoCorr, xPeaks); + plotter.setPlotCorrY(yAutoCorr, yPeaks); + plotter.setPlotCorrZ(zAutoCorr, zPeaks); + plotter.setPlotCorrMag(magAutoCorr, magPeaks); + + //printout the current BPM + System.out.println(length_ms + "; x: " + mBpmHistory_X.getLast() + + "; y: " + mBpmHistory_Y.getLast() + + "; z: " + mBpmHistory_Z.getLast() + + "; mag: " + mBpmHistory_Mag.getLast()); + } + + double estimatedBPM = getBestBpmEstimation(xPeaks, yPeaks, zPeaks); if(estimatedBPM != -1){ //moving avg (lohnt dann, wenn wir viele daten haben) @@ -86,7 +166,8 @@ public class BpmEstimator { if(++mResetCounter > resetAfter){ mBpmHistory.clear(); - mBuffer.clear(); + //TODO: send signal to clear from outside this function should just return the bpm + //mBuffer.clear(); mMvg.clear(); mResetCounter = 0; } @@ -105,6 +186,8 @@ public class BpmEstimator { return Utils.median(mBpmHistory); } + //TODO: die einzelnen achsen cleverer kombinieren. bspw. bei Peter brauchen wir zwei Achsen in einer Bewegung. + //TODO: Vielleicht die einzelnen Kombinationen / Magnitudes der Achsen noch mit einbeziehen. Also xy, xz, yz private double getBestBpmEstimation(Peaks peaksX, Peaks peaksY, Peaks peaksZ) throws IllegalArgumentException { int cntNumAxis = 0; diff --git a/java/src/main/java/utilities/Utils.java b/java/src/main/java/utilities/Utils.java index 68b284f..f2b85ab 100644 --- a/java/src/main/java/utilities/Utils.java +++ b/java/src/main/java/utilities/Utils.java @@ -1,5 +1,7 @@ package utilities; +import bpmEstimation.AccelerometerInterpolator; +import bpmEstimation.AccelerometerWindowBuffer; import bpmEstimation.Peaks; import javax.swing.*; import java.awt.*; @@ -9,9 +11,13 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; +import java.util.stream.IntStream; //TODO: change from double to generic type public class Utils { + + public static final boolean DEBUG_MODE = true; + public static double getDistance(double x1, double y1, double x2, double y2) { return (double) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); } @@ -164,6 +170,37 @@ public class Utils { return newArray; } + public static double magnitude(double x, double y, double z){ + return Math.sqrt((x*x) + (y*y) + (z*z)); + } + + public static double[] magnitude(double[] arrayX, double[] arrayY, double[] arrayZ){ + + //check of all arrays have the same size, if not crop to smallest one + int n = arrayZ.length; + if(arrayX.length < arrayY.length && arrayX.length < arrayZ.length){ + n = arrayX.length; + } else if (arrayY.length < arrayZ.length){ + n = arrayY.length; + } + + double[] output = new double[n]; + for(int i = 0; i < n; ++i){ + output[i] = magnitude(arrayX[i], arrayY[i], arrayZ[i]); + } + + return output; + } + + public static double[] magnitude(AccelerometerInterpolator interpolator){ + return magnitude(interpolator.getX(), interpolator.getY(), interpolator.getZ()); + } + + //TODO: this could be added to AccelerometerData.. saves some time, better design. + public static double[] magnitude(AccelerometerWindowBuffer buffer){ + return magnitude(buffer.getX(), buffer.getY(), buffer.getZ()); + } + @SuppressWarnings("serial") public static class ShowPNG extends JFrame { @@ -178,7 +215,55 @@ public class Utils { this.setVisible(true); } - public void set(BufferedImage bi){ + public void plotData(String title, String name, long[] ts, double[] data){ + + double[] dTs = IntStream.range(0, ts.length).mapToDouble(i -> ts[i]).toArray(); + Plot plot = Plot.plot(Plot.plotOpts(). + title(title). + legend(Plot.LegendFormat.BOTTOM)). + series(name, Plot.data().xy(dTs, data), Plot.seriesOpts().color(Color.RED)); + + this.set(plot.draw()); + } + + public void plotCorr(String title, String name, double[] data, int scale){ + + int[] tmp = IntStream.rangeClosed(-((data.length - 1)/scale), ((data.length - 1)/scale)).toArray(); + double[] range = IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray(); + Plot plot = Plot.plot(Plot.plotOpts(). + title(title). + legend(Plot.LegendFormat.BOTTOM)). + series(name, Plot.data().xy(range, data), Plot.seriesOpts().color(Color.RED)); + + this.set(plot.draw()); + } + + public void plotCorrWithPeaks(String title, String name, double[] corr, int scale, Peaks peaks, boolean isCorrelation){ + + int[] tmp = IntStream.rangeClosed(-((corr.length - 1)/scale), ((corr.length - 1)/scale)).toArray(); + double[] range= IntStream.range(0, tmp.length).mapToDouble(i -> tmp[i]).toArray(); + + //if we have a correlation, shift the peaks into negative by half the correlation size + int tmpOffset = 0; + if(isCorrelation){ + tmpOffset = corr.length / 2; + } + int offset = tmpOffset; + + LinkedList peaksZ = peaks.getPeaksIdx(); + double[] dPeaksZX = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (peaksZ.get(i) - offset)).toArray();//peaks.stream().mapToDouble(i->i).toArray(); + double[] dPeaksZY = IntStream.range(0, peaksZ.size()).mapToDouble(i -> (corr[peaksZ.get(i)])).toArray(); + Plot plot = Plot.plot(Plot.plotOpts(). + title(title). + legend(Plot.LegendFormat.BOTTOM)). + series(name, Plot.data().xy(range, corr), Plot.seriesOpts().color(Color.RED)). + series("Peaks", Plot.data().xy(dPeaksZX, dPeaksZY), Plot.seriesOpts().color(Color.CYAN). + marker(Plot.Marker.DIAMOND).line(Plot.Line.NONE)); + + this.set(plot.draw()); + } + + private void set(BufferedImage bi){ mIcon = new ImageIcon(bi); mLabel.setVisible(false); @@ -187,4 +272,87 @@ public class Utils { } } + public static class DebugPlotter { + + //what we want to draw + private Utils.ShowPNG plotRawX; + private Utils.ShowPNG plotRawY; + private Utils.ShowPNG plotRawZ; + private Utils.ShowPNG plotRawMag; + private Utils.ShowPNG plotButterX; + private Utils.ShowPNG plotButterY; + private Utils.ShowPNG plotButterZ; + private Utils.ShowPNG plotButterMag; + private Utils.ShowPNG plotCorrX; + private Utils.ShowPNG plotCorrY; + private Utils.ShowPNG plotCorrZ; + private Utils.ShowPNG plotCorrMag; + + public DebugPlotter(){ + + plotRawX = new Utils.ShowPNG(); + plotRawY = new Utils.ShowPNG(); + plotRawZ = new Utils.ShowPNG(); + plotRawMag = new Utils.ShowPNG(); + plotButterX = new Utils.ShowPNG(); + plotButterY = new Utils.ShowPNG(); + plotButterZ = new Utils.ShowPNG(); + plotButterMag = new Utils.ShowPNG(); + plotCorrX = new Utils.ShowPNG(); + plotCorrY = new Utils.ShowPNG(); + plotCorrZ = new Utils.ShowPNG(); + plotCorrMag = new Utils.ShowPNG(); + } + + public void setPlotRawX(long[] ts, double[] data){ + plotRawX.plotData("Raw Data X", "x", ts, data); + } + + public void setPlotRawY(long[] ts, double[] data){ + plotRawY.plotData("Raw Data Y", "y", ts, data); + } + + public void setPlotRawZ(long[] ts, double[] data){ + plotRawZ.plotData("Raw Data Z", "z", ts, data); + } + + public void setPlotRawMag(long[] ts, double[] data){ + plotRawMag.plotData("Raw Data Magnitude", "mag", ts, data); + } + + public void setPlotButterX(long[] ts, double[] data){ + plotButterX.plotData("Butter Data X", "x", ts, data); + } + + public void setPlotButterY(long[] ts, double[] data){ + plotButterY.plotData("Butter Data Y", "z", ts, data); + } + + public void setPlotButterZ(long[] ts, double[] data){ + plotButterZ.plotData("Butter Data Z", "y", ts, data); + } + + public void setPlotButterMag(long[] ts, double[] data){ + plotButterMag.plotData("Butter Data Mag", "mag", ts, data); + } + + public void setPlotCorrX(double[] data, Peaks peaks){ + plotCorrX.plotCorrWithPeaks("Autocorr X", "x", data,2, peaks, true); + } + + public void setPlotCorrY(double[] data, Peaks peaks){ + plotCorrY.plotCorrWithPeaks("Autocorr Y", "y", data,2, peaks, true); + } + + public void setPlotCorrZ(double[] data, Peaks peaks){ + plotCorrZ.plotCorrWithPeaks("Autocorr Z", "z", data,2, peaks, true); + } + + public void setPlotCorrMag(double[] data, Peaks peaks){ + plotCorrMag.plotCorrWithPeaks("Autocorr Mag", "mag", data,2, peaks, true); + } + + + } + }