rusefi/java_console/ui/src/main/java/com/rusefi/ui/SensorLiveGraph.java

338 lines
11 KiB
Java

package com.rusefi.ui;
import com.rusefi.NamedThreadFactory;
import com.rusefi.core.Sensor;
import com.rusefi.core.SensorCategory;
import com.rusefi.core.SensorCentral;
import com.rusefi.core.preferences.storage.Node;
import com.rusefi.ui.util.UiUtils;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.LinkedList;
import java.util.concurrent.ThreadFactory;
/**
* Andrey Belomutskiy, (c) 2013-2020
* 8/22/2015.
*/
@SuppressWarnings("InfiniteLoopStatement")
public class SensorLiveGraph extends JPanel {
private final static ThreadFactory THREAD_FACTORY = new NamedThreadFactory("SensorLiveGraph", true);
private static final int COUNT = 30;
private static final String SENSOR_TYPE = "sensor";
private static final String PERIOD = "period";
private static final String USE_AUTO_SCALE = "auto_scale";
private static final String UPPER = "upper";
private static final String LOWER = "lower";
private final LinkedList<Double> values = new LinkedList<>();
private final Node config;
private final JMenuItem extraItem;
@NotNull
private ChangePeriod period;
private Sensor sensor;
private boolean autoScale;
private double customUpper;
private double customLower;
public SensorLiveGraph(Node config, final Sensor defaultSensor, JMenuItem extraItem) {
this.config = config;
this.extraItem = extraItem;
String gaugeName = config.getProperty(SENSOR_TYPE, defaultSensor.name());
this.sensor = Sensor.lookup(gaugeName, defaultSensor);
Thread thread = THREAD_FACTORY.newThread(createRunnable());
thread.start();
period = ChangePeriod.lookup(config.getProperty(PERIOD));
autoScale = config.getBoolProperty(USE_AUTO_SCALE);
customUpper = config.getDoubleProperty(UPPER, Double.NaN);
customLower = config.getDoubleProperty(LOWER, Double.NaN);
MouseListener mouseListener = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isRightMouseButton(e)) {
showPopupMenu(e);
// } else if (e.getClickCount() == 2) {
// handleDoubleClick(e, gauge, sensor);
}
}
};
addMouseListener(mouseListener);
setBorder(BorderFactory.createLineBorder(Color.black));
}
@NotNull
private Runnable createRunnable() {
return new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(period.getMs());
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
if (!GaugesPanel.IS_PAUSED)
grabNewValue();
}
}
};
}
private void grabNewValue() {
double value = SensorCentral.getInstance().getValue(sensor);
addValue(value);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
UiUtils.trueRepaint(SensorLiveGraph.this);
}
});
}
private void showPopupMenu(MouseEvent e) {
JPopupMenu pm = new JPopupMenu();
addChangeSensorItems(pm);
pm.add(new JSeparator());
addChangePeriodItems(pm);
JMenuItem scale = new JMenu("Scale");
final JCheckBoxMenuItem as = new JCheckBoxMenuItem("Auto scale");
as.setSelected(autoScale);
as.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
autoScale = as.isSelected();
config.setBoolProperty(USE_AUTO_SCALE, autoScale);
}
});
scale.add(as);
final JCheckBoxMenuItem lowerValue = new JCheckBoxMenuItem("Use custom lower");
lowerValue.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (lowerValue.isSelected()) {
String initial = Double.isNaN(customLower) ? "" : Double.toString(customLower);
String lower = JOptionPane.showInputDialog("Enter lower value", initial);
if (!Node.isNumeric(lower))
return;
customLower = Double.parseDouble(lower);
config.setProperty(LOWER, customLower);
}
}
});
final JCheckBoxMenuItem upperValue = new JCheckBoxMenuItem("Use custom upper");
upperValue.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (upperValue.isSelected()) {
String initial = Double.isNaN(customUpper) ? "" : Double.toString(customUpper);
String upper = JOptionPane.showInputDialog("Enter upper value", initial);
if (!Node.isNumeric(upper))
return;
customUpper = Double.parseDouble(upper);
config.setProperty(UPPER, customUpper);
}
}
});
scale.add(upperValue);
scale.add(lowerValue);
pm.add(scale);
pm.add(extraItem);
pm.show(e.getComponent(), e.getX(), e.getY());
}
enum ChangePeriod {
_3000(3000, "3 seconds"),
_1000(1000, "1 second"),
_200(200, "second / 5"),
_100(100, "second / 10"),
_50(50, "second / 20"),;
private final int ms;
private final String text;
ChangePeriod(int ms, String text) {
this.ms = ms;
this.text = text;
}
public int getMs() {
return ms;
}
public String getText() {
return text;
}
public static ChangePeriod lookup(String value) {
for (ChangePeriod cp : ChangePeriod.values()) {
if ((cp.getMs() + "").equals(value))
return cp;
}
return ChangePeriod._200;
}
}
private void addChangePeriodItems(JPopupMenu pm) {
JMenuItem mi = new JMenu("Refresh period");
pm.add(mi);
for (final ChangePeriod cp : ChangePeriod.values()) {
JCheckBoxMenuItem i = new JCheckBoxMenuItem(cp.getText());
i.setSelected(cp == period);
i.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
period = cp;
config.setProperty(PERIOD, period.getMs());
}
});
mi.add(i);
}
}
private void addChangeSensorItems(JPopupMenu pm) {
for (final SensorCategory sc : SensorCategory.values()) {
JMenuItem cmi = new JMenu(sc.getName());
pm.add(cmi);
for (final Sensor s : Sensor.getSensorsForCategory(sc.getName())) {
JMenuItem mi = new JMenuItem(s.getName());
mi.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
setSensor(s);
}
});
cmi.add(mi);
}
}
}
private synchronized void setSensor(Sensor sensor) {
this.sensor = sensor;
values.clear();
config.setProperty(SENSOR_TYPE, sensor.name());
}
private synchronized void addValue(double value) {
if (values.size() > COUNT)
values.removeFirst();
values.add(value);
}
@Override
public synchronized void paint(Graphics g) {
super.paint(g);
Dimension d = getSize();
if (d.height < 5)
return; // it's hopeless
g.setColor(Color.black);
VisibleRange range = getRange();
paintGraph(g, d, range.minValue, range.maxValue);
g.setColor(Color.red);
int minY = d.height;
int maxY = g.getFont().getSize();
g.drawString(String.format("%.2f", range.minValue), 5, minY);
g.drawString(String.format("%.2f", (range.minValue + range.maxValue) / 2), 5, (minY + maxY) / 2);
g.drawString(String.format("%.2f", range.maxValue), 5, maxY);
g.setColor(Color.blue);
String sensorName = sensor.getName() + " ";
int nameWidth = g.getFontMetrics().stringWidth(sensorName);
g.drawString(sensorName, d.width - nameWidth, g.getFont().getSize());
Font f = g.getFont();
g.setFont(new Font(f.getName(), f.getStyle(), 3 * f.getSize()));
if (!values.isEmpty())
paintLastValue(g, d);
}
private VisibleRange getRange() {
VisibleRange range;
if (autoScale) {
range = VisibleRange.findRange(values);
} else {
range = new VisibleRange(Double.isNaN(customLower) ? sensor.getMinValue() : customLower,
Double.isNaN(customUpper) ? sensor.getMaxValue() : customUpper);
}
return range;
}
private void paintLastValue(Graphics g, Dimension d) {
Double last = values.getLast();
if (!Double.isNaN(last)) {
String currentValue = String.format("%.2f", last);
g.drawString(currentValue, (d.width - g.getFontMetrics().stringWidth(currentValue)) / 2, d.height / 2 + g.getFont().getSize() / 2);
}
}
private void paintGraph(Graphics g, Dimension d, double minValue, double maxValue) {
int index = 0;
int prevX = 0;
int prevY = 0;
for (double value : values) {
int x = d.width * index / values.size();
int y = (int) (d.height - (value - minValue) * d.height / (maxValue - minValue));
g.drawOval(x, y, 3, 3);
if (index > 0) {
g.drawLine(x, y, prevX, prevY);
}
prevX = x;
prevY = y;
index++;
}
}
private static class VisibleRange {
private final double minValue;
private final double maxValue;
public VisibleRange(double minValue, double maxValue) {
this.minValue = minValue;
this.maxValue = maxValue;
}
public static VisibleRange findRange(LinkedList<Double> values) {
double minValue = Double.MAX_VALUE;
double maxValue = -Double.MAX_VALUE;
for (double value : values) {
minValue = Math.min(minValue, value);
maxValue = Math.max(maxValue, value);
}
if (minValue == maxValue) { // double equals should work here, should it?
minValue = 0.9 * maxValue - 1;
maxValue = 1.1 * maxValue + 1;
} else {
// expand the range just a bit for borders
double diff = maxValue - minValue;
minValue -= 0.05 * diff;
maxValue += 0.05 * diff;
}
return new VisibleRange(minValue, maxValue);
}
}
}