mirror of https://github.com/rusefi/rusefi.git
338 lines
11 KiB
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);
|
|
}
|
|
}
|
|
}
|