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.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 static final int V_MAX = 255;
private static final int U_MAX = 1279;
private Stroke dataStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
private Axis xAxis;
private Axis yAxis;
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(int initialThreshold, int initialWaitDuration) {
threshold = initialThreshold;
waitDuration = initialWaitDuration;
addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent event) {
Map<Rule, Point> anchors = new HashMap<Rule, Point>();
Point thresholdRuleAnchorLocation = toGraphArea(U_MAX, threshold);
SwingUtilities.convertPointToScreen(thresholdRuleAnchorLocation, GraphPane.this);
anchors.put(Rule.THRESHOLD_RULE, thresholdRuleAnchorLocation);
Point waitDurationRuleAnchorLocation = toGraphArea(waitDuration, V_MAX);
SwingUtilities.convertPointToScreen(waitDurationRuleAnchorLocation, GraphPane.this);
anchors.put(Rule.WAIT_DURATION_RULE, waitDurationRuleAnchorLocation);
if (grabbedRule != null) {
boolean stillLocked = anchors.get(grabbedRule).distance(event.getLocationOnScreen()) < 16;
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()) < 16;
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;
GraphPane.this.threshold = Math.max(0, Math.min(newThreshold, V_MAX));
break;
case WAIT_DURATION_RULE:
int newWaitDuration = uv.x;
GraphPane.this.waitDuration = Math.max(0, Math.min(newWaitDuration, U_MAX));
break;
default:
throw new IllegalArgumentException(grabbedRule.name());
}
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 setData(byte[] data) {
this.data = data;
repaint();
}
public int getThreshold() {
return threshold;
}
public int getWaitDuration() {
return waitDuration;
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
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 = U_MAX;
Point previousPoint = null;
for (byte b : data) {
Point point = toGraphArea(u--, b & 0xFF);
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(U_MAX, 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, V_MAX);
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 = (int) Math.round((U_MAX - u) * graphArea.width / U_MAX + graphArea.x);
int y = (int) Math.round((V_MAX - v) * graphArea.height / V_MAX + graphArea.y);
return new Point(x, y);
}
private Point toData(int x, int y) {
int u = (int) Math.round(U_MAX - (x - graphArea.x) * U_MAX / graphArea.width);
int v = (int) Math.round(V_MAX - (y - graphArea.y) * V_MAX / graphArea.height);
return new Point(u, v);
}
}