package org.hihan.girinoscope.ui; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.util.HashMap; import java.util.Map; import javax.swing.JPanel; import javax.swing.SwingUtilities; import org.hihan.girinoscope.comm.FrameFormat; import org.hihan.girinoscope.ui.Axis.GraphLabel; @SuppressWarnings("serial") public class GraphPane extends JPanel { private static final Color DIVISION_COLOR = new Color(0xbcbcbc); private static final Color SUB_DIVISION_COLOR = new Color(0xcdcdcd); private static final Color TEXT_COLOR = new Color(0x9a9a9a); private static final Color DATA_COLOR = Color.CYAN.darker(); private static final Color THRESHOLD_COLOR = Color.ORANGE.darker(); private static final Color WAIT_DURATION_COLOR = Color.GREEN.darker(); private static final Font FONT = Font.decode(Font.MONOSPACED); private static final Stroke DASHED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{5}, 0); private static final Stroke DOTTED = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{3}, 0); private Stroke dataStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); private Axis xAxis; private Axis yAxis; private FrameFormat frameFormat; private int uMax; private int vMax; private byte[] data; private Rectangle graphArea; private int threshold; private int waitDuration; private enum Rule { THRESHOLD_RULE, WAIT_DURATION_RULE } private Rule grabbedRule; public GraphPane() { super.addMouseMotionListener(new MouseMotionListener() { private final int HAND_RADIUS = 16; @Override public void mouseMoved(MouseEvent event) { Map<Rule, Point> anchors = new HashMap<>(); Point thresholdRuleAnchorLocation = toGraphArea(uMax, threshold); SwingUtilities.convertPointToScreen(thresholdRuleAnchorLocation, GraphPane.this); anchors.put(Rule.THRESHOLD_RULE, thresholdRuleAnchorLocation); Point waitDurationRuleAnchorLocation = toGraphArea(waitDuration, vMax); SwingUtilities.convertPointToScreen(waitDurationRuleAnchorLocation, GraphPane.this); anchors.put(Rule.WAIT_DURATION_RULE, waitDurationRuleAnchorLocation); if (grabbedRule != null) { boolean stillLocked = anchors.get(grabbedRule).distance(event.getLocationOnScreen()) < HAND_RADIUS; if (!stillLocked) { setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); grabbedRule = null; repaint(); } } if (grabbedRule == null) { for (Map.Entry<Rule, Point> entry : anchors.entrySet()) { boolean nowLocked = entry.getValue().distance(event.getLocationOnScreen()) < HAND_RADIUS; if (nowLocked) { setCursor(new Cursor(Cursor.HAND_CURSOR)); grabbedRule = entry.getKey(); repaint(); return; } } } } @Override public void mouseDragged(MouseEvent event) { if (grabbedRule != null) { Point graphAreaPosition = event.getLocationOnScreen(); SwingUtilities.convertPointFromScreen(graphAreaPosition, GraphPane.this); Point uv = toData(graphAreaPosition.x, graphAreaPosition.y); switch (grabbedRule) { case THRESHOLD_RULE: int newThreshold = uv.y; setThreshold(Math.max(0, Math.min(newThreshold, vMax))); break; case WAIT_DURATION_RULE: int newWaitDuration = uv.x; setWaitDuration(Math.max(0, Math.min(newWaitDuration, uMax))); break; default: throw new IllegalArgumentException(grabbedRule.name()); } repaint(); } } }); } public void setThreshold(int threshold) { this.threshold = threshold; repaint(); } public void setWaitDuration(int waitDuration) { this.waitDuration = waitDuration; repaint(); } public void setDataStrokeWidth(float width) { dataStroke = new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); } public void setCoordinateSystem(Axis xAxis, Axis yAxis) { this.xAxis = xAxis; this.yAxis = yAxis; repaint(); } public void setXCoordinateSystem(Axis xAxis) { this.xAxis = xAxis; repaint(); } public void setYCoordinateSystem(Axis yAxis) { this.yAxis = yAxis; repaint(); } public void setFrameFormat(FrameFormat frameFormat) { this.frameFormat = frameFormat; uMax = frameFormat.sampleCount - 1; vMax = frameFormat.sampleMaxValue; } public void setData(byte[] data) { this.data = data; repaint(); } public byte[] getData() { return data; } public int[] getValues() { return frameFormat.readValues(data); } public int getThreshold() { return threshold; } public int getWaitDuration() { return waitDuration; } @Override protected void paintComponent(Graphics g) { Graphics2D g2d = (Graphics2D) g; // Performance issues on Linux without an explicit -Dsun.java2d.opengl=true. g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); int w = getWidth(); int h = getHeight(); g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, w, h); if (xAxis != null && yAxis != null) { xAxis.complete(g2d, FONT); yAxis.complete(g2d, FONT); graphArea = new Rectangle(16, 16, w - 32, h - 32); Insets labelInsets = new Insets(2, 12, 0, 0); graphArea.width -= yAxis.getMaxBounds().width + labelInsets.left + labelInsets.right; graphArea.height -= xAxis.getMaxBounds().height + labelInsets.top + labelInsets.bottom; if (w > 0 && h > 0) { paintXAxis(g2d, labelInsets); paintYAxis(g2d, labelInsets); if (data != null) { paintData(g2d); } paintWaitDurationRule(g2d); paintThresholdRule(g2d); } } } private void paintXAxis(Graphics2D g, Insets labelInsets) { g.translate(graphArea.x, graphArea.y); GraphLabel[] xLabels = xAxis.graphLabels(); for (int i = 0; i < xLabels.length; ++i) { GraphLabel xLabel = xLabels[i]; int xOffset = (int) (i * graphArea.width / xAxis.getFraction()); Stroke defaultStroke = g.getStroke(); g.setStroke(i % (xLabels.length / 2) != 0 ? DASHED : defaultStroke); g.setColor(i % (xLabels.length - 1) == 0 ? DIVISION_COLOR : SUB_DIVISION_COLOR); g.drawLine(xOffset, 0, xOffset, graphArea.height); g.setStroke(defaultStroke); g.setColor(TEXT_COLOR); Rectangle bounds = xLabel.getBounds(); int dx; if (i == 0) { dx = 0; } else { dx = -bounds.width / 2; } int dy = labelInsets.top; g.drawString(xLabel.getLabel(), xOffset + dx, graphArea.height + bounds.height + dy); } g.drawLine(graphArea.width, 0, graphArea.width, graphArea.height); g.translate(-graphArea.x, -graphArea.y); } private void paintYAxis(Graphics2D g, Insets labelInsets) { g.translate(graphArea.x, graphArea.y); GraphLabel[] yLabels = yAxis.graphLabels(); for (int i = 0; i < yLabels.length; ++i) { GraphLabel yLabel = yLabels[yLabels.length - i - 1]; int yOffset = (int) (i * graphArea.height / yAxis.getFraction()); Stroke defaultStroke = g.getStroke(); g.setStroke(i % (yLabels.length / 2) != 0 ? DASHED : defaultStroke); g.setColor(i % (yLabels.length - 1) == 0 ? DIVISION_COLOR : SUB_DIVISION_COLOR); g.drawLine(0, yOffset, graphArea.width, yOffset); g.setStroke(defaultStroke); g.setColor(TEXT_COLOR); Rectangle bounds = yLabel.getBounds(); int dy = bounds.height / 2; int dx = labelInsets.left; g.drawString(yLabel.getLabel(), graphArea.width + dx, yOffset + dy); } g.drawLine(0, graphArea.height, graphArea.width, graphArea.height); g.translate(-graphArea.x, -graphArea.y); } private void paintData(Graphics2D g) { g.setColor(DATA_COLOR); Stroke defaultStroke = g.getStroke(); g.setStroke(dataStroke); int u = uMax; Point previousPoint = null; for (int value : getValues()) { if (value < 0 || value > frameFormat.sampleMaxValue) { System.err.println("value -> " + value); } Point point = toGraphArea(u--, value); if (previousPoint != null) { g.drawLine(previousPoint.x, previousPoint.y, point.x, point.y); } previousPoint = point; } g.setStroke(defaultStroke); } private void paintThresholdRule(Graphics2D g) { g.setColor(THRESHOLD_COLOR); Point point = toGraphArea(uMax, threshold); Stroke defaultStroke = g.getStroke(); g.setStroke(DOTTED); g.drawLine(point.x, point.y, point.x + graphArea.width, point.y); g.setStroke(defaultStroke); Graphics2D gg = (Graphics2D) g.create(); gg.translate(point.x, point.y); gg.rotate(Math.PI / 4); if (grabbedRule == Rule.THRESHOLD_RULE) { gg.fill3DRect(-4, -4, 9, 9, true); } else { gg.fill3DRect(-3, -3, 7, 7, true); } } private void paintWaitDurationRule(Graphics2D g) { g.setColor(WAIT_DURATION_COLOR); Point point = toGraphArea(waitDuration, vMax); Stroke defaultStroke = g.getStroke(); g.setStroke(DOTTED); g.drawLine(point.x, point.y, point.x, point.y + graphArea.height); g.setStroke(defaultStroke); Graphics2D gg = (Graphics2D) g.create(); gg.translate(point.x, point.y); gg.rotate(Math.PI / 4); if (grabbedRule == Rule.WAIT_DURATION_RULE) { gg.fill3DRect(-4, -4, 9, 9, true); } else { gg.fill3DRect(-3, -3, 7, 7, true); } } private Point toGraphArea(int u, int v) { int x = Math.round((uMax - u) * graphArea.width / uMax + graphArea.x); int y = Math.round((vMax - v) * graphArea.height / vMax + graphArea.y); return new Point(x, y); } private Point toData(int x, int y) { int u = Math.round(uMax - (x - graphArea.x) * uMax / graphArea.width); int v = Math.round(vMax - (y - graphArea.y) * vMax / graphArea.height); return new Point(u, v); } }