package nbtool.gui.utilitypanes;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTabbedPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import nbtool.data.calibration.ColorParam;
import nbtool.data.json.Json;
import nbtool.data.json.JsonParser.JsonParseException;
import nbtool.data.log.Block;
import nbtool.data.log.Log;
import nbtool.gui.ToolMessage;
import nbtool.gui.logdnd.LogDND;
import nbtool.gui.logdnd.LogDND.LogDNDTarget;
import nbtool.gui.logviews.images.ImageDisplay;
import nbtool.gui.logviews.misc.VisionView;
import nbtool.images.Y8Image;
import nbtool.io.CommonIO.IOFirstResponder;
import nbtool.io.CommonIO.IOInstance;
import nbtool.nio.CrossServer;
import nbtool.nio.CrossServer.CrossInstance;
import nbtool.nio.LogRPC;
import nbtool.nio.RobotConnection;
import nbtool.util.Center;
import nbtool.util.Debug;
import nbtool.util.Events;
import nbtool.util.SharedConstants;
import nbtool.util.Utility;
public class ColorCalibrationUtility extends UtilityProvider<ColorParam.Set, ColorCalibrationListener> {
private static ColorCalibrationUtility instance = null;
private static Debug.DebugSettings debug = Debug.createSettings(Debug.INFO);
ColorCalibrationUtility() {
if (instance != null) {
throw new RuntimeException("singeton class");
} else {
instance = this;
}
}
@Override
public ColorParam.Set getLatest() {
return color_parameters;
}
public boolean appliedGlobally() {
return (display == null) ? false : display.applyGlobally;
}
private static Display display = null;
@Override
public JFrame supplyDisplay() {
if (display != null) {
return display;
} else {
try {
return (display = new Display());
} catch (java.lang.OutOfMemoryError err) {
err.printStackTrace();
Debug.print("%s", err.getMessage());
return null;
}
}
}
@Override
public String purpose() {
return "calibrate the robot's vision color parameters";
}
@Override
public char preferredMemnonic() {
return 'c';
}
private static ColorParam.Set color_parameters = null;
private static void colorParametersUpdated() {
Debug.print("color parameters have updated.");
if (display != null && display.applyGlobally) {
instance.fireChanged();
}
}
private static class Display extends JFrame implements Events.LogSelected {
JTabbedPane tabs = new JTabbedPane();
TabHandler[] handlers = new TabHandler[4];
boolean applyGlobally = true;
boolean takeSelection = false;
private ActionListener saveListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Debug.warn("saving color parameters to NBITES_DIR !");
try {
Files.write(ColorParam.getPath(),
color_parameters.serialize().print().getBytes(StandardCharsets.UTF_8));
ToolMessage.displayWarn("wrote color parameters");
} catch (IOException e1) {
e1.printStackTrace();
ToolMessage.displayError("could not write color parameters.");
}
}
};
private ActionListener sendListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
RobotConnection robot = RobotConnection.getByIndex(0);
if (robot == null) {
ToolMessage.displayError("CCU: send: no robot connected!");
return;
}
if (color_parameters == null) {
ToolMessage.displayError("CCU: send: color params!");
return;
}
LogRPC.setFileContents(LogRPC.NULL_RESPONDER, robot, "/home/nao/nbites/Config/colorParams.json",
color_parameters.serialize().print());
ToolMessage.displayAndPrint("< params sent to %s >", robot.host());
}
};
private ChangeListener globalListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
boolean nv = ((JCheckBox) e.getSource()).isSelected();
for (TabHandler th : handlers) {
th.tab.globalCheckBox.setSelected(nv);
}
applyGlobally = nv;
}
};
private ChangeListener takeListener = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
boolean nv = ((JCheckBox) e.getSource()).isSelected();
for (TabHandler th : handlers) {
th.tab.takeCheckBox.setSelected(nv);
}
takeSelection = nv;
}
};
@Override
public void logSelected(Object source, Log first, List<Log> alsoSelected) {
if (takeSelection) {
if (first.logClass.equals("tripoint")) {
for (TabHandler handler : handlers) {
if (handler.top == Utility.camera(first)) {
handler.useLog(first);
}
}
}
}
}
private LogDNDTarget topCameraTarget = new LogDNDTarget() {
@Override
public void takeLogsFromDrop(Log[] log) {
if (log.length > 0) {
Log attempt = log[0];
if (!attempt.logClass.equals("tripoint")) {
ToolMessage.displayError("must use tripoint log for ColorCalibration, not %s!",
attempt.logClass);
return;
}
String reqFrom = "camera_TOP";
if (!attempt.blocks.get(0).whereFrom.equals(reqFrom)) {
ToolMessage.displayError("tab {%s} must have log from: %s",
"TOP", reqFrom);
return;
}
for (TabHandler handler : handlers) {
if (handler.top) {
handler.useLog(attempt);
}
}
}
}
};
private LogDNDTarget botCameraTarget = new LogDNDTarget() {
@Override
public void takeLogsFromDrop(Log[] log) {
if (log.length > 0) {
Log attempt = log[0];
if (!attempt.logClass.equals("tripoint")) {
ToolMessage.displayError("must use tripoint log for ColorCalibration, not %s!",
attempt.logClass);
return;
}
String reqFrom = "camera_BOT";
if (!attempt.blocks.get(0).whereFrom.equals(reqFrom)) {
ToolMessage.displayError("tab {%s} must have log from: %s",
"BOT", reqFrom);
return;
}
for (TabHandler handler : handlers) {
if (!handler.top) {
handler.useLog(attempt);
}
}
}
}
};
Display() {
super("color calibration utility");
Debug.plain("getting initial color parameters");
Path colorPath = ColorParam.getPath();
assert (Files.exists(colorPath) && Files.isRegularFile(colorPath));
ColorParam.Set set = null;
try {
String contents = new String(Files.readAllBytes(colorPath));
set = ColorParam.Set.parse(Json.parse(contents).asObject());
} catch (IOException ie) {
ie.printStackTrace();
Debug.error("IOExceptionL %s", ie.getMessage());
ToolMessage.displayError("error parsing json file %s", colorPath);
return;
} catch (JsonParseException e1) {
e1.printStackTrace();
Debug.error("JsonParseException: %s", e1.getMessage());
ToolMessage.displayError("error parsing json file %s !", colorPath);
return;
}
assert (set != null);
color_parameters = set;
/**
* THIS IS WHERE THE TABS ARE SET UP:
* true is top, false is bottom!
* Camera.Which enum is defined in nbtool.data.calibration.ColorParam.java
* */
handlers[0] = new TabHandler(true, ColorParam.Camera.Which.white);
handlers[1] = new TabHandler(false, ColorParam.Camera.Which.white);
handlers[2] = new TabHandler(true, ColorParam.Camera.Which.green);
handlers[3] = new TabHandler(false, ColorParam.Camera.Which.green);
for (TabHandler th : handlers) {
tabs.add(th.title(), th.tab);
th.tab.SaveButton.addActionListener(saveListener);
th.tab.SendButton.addActionListener(sendListener);
th.tab.globalCheckBox.addChangeListener(globalListener);
th.tab.takeCheckBox.addChangeListener(takeListener);
}
for (int i = 0; i < handlers.length; ++i) {
handlers[i].takeFrom(set);
}
for (int i = 0; i < handlers.length; ++i) {
handlers[i].installListeners();
}
Dimension minSize = new Dimension(tabs.getMinimumSize());
minSize.width += 50;
minSize.height += 50;
Center.listen(Events.LogSelected.class, this, true);
this.setContentPane(tabs);
this.setMinimumSize(minSize);
}
private class TabHandler {
boolean top;
ColorParam.Camera.Which camera;
ColorCalibrationTab tab;
ImageDisplay imageDisplay;
ImageDisplay imageDisplayNorm;
Log dropped = null;
int dropped_width, dropped_height;
public String title() {
return (top ? "top" : "bot") + " " + camera.toString();
}
class Group {
JSlider slider;
JSpinner spinner;
ColorParam.Part part;
Group (JSlider slider, JSpinner spinner, ColorParam.Part part) {
this.slider = slider; this.spinner = spinner; this.part = part;
}
}
class Change {
Group group;
int previousValue;
}
private ArrayList<Group> groups = new ArrayList<>();
private ArrayList<Change> undoStack = new ArrayList<>();
boolean ignoreChangeEvents = false;
private void undoTheChange() {
if (undoStack.isEmpty()) return;
Debug.print("undoing!");
ignoreChangeEvents = true;
int index = Math.max(undoStack.size() - 10, 0);
Change latest = undoStack.get(index);
undoStack.removeAll(undoStack.subList(index, undoStack.size()));
latest.group.slider.setValue(latest.previousValue);
latest.group.spinner.setValue(latest.previousValue);
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
Debug.print("undo finished!");
ignoreChangeEvents = false;
}
});
wasUpdated();
}
private void addChange(Group group, int previous) {
Change change = new Change();
change.group = group;
change.previousValue = previous;
undoStack.add(change);
while(undoStack.size() > 1000) {
undoStack.remove(0);
}
}
private void visionCall() {
assert(dropped != null);
assert(color_parameters != null);
dropped.topLevelDictionary.put("ModifiedColorParams", color_parameters.get(top).serialize());
debug.info("%s", dropped.topLevelDictionary.get("ModifiedColorParams").print());
CrossInstance ci = CrossServer.instanceByIndex(0);
if (ci == null) {
ToolMessage.displayWarn("{%s} cannot call vision, no CrossInstance!", title());
return;
}
assert(ci.tryAddCall(new IOFirstResponder(){
@Override
public void ioFinished(IOInstance instance) { }
@Override
public void ioReceived(IOInstance inst, int ret, Log... out) {
assert(out.length == 1);
assert(out[0].logClass.equals("VisionReturn"));
String search = camera.toString() + "Ret";
debug.info("TabHandler looking for: %s", search);
Block block = out[0].find(search);
Y8Image image = new Y8Image(dropped_width / 2, dropped_height / 2, block.data);
if (imageDisplay != null)
imageDisplay.setImage(image.toBufferedImage());
else debug.error("{%s} null image display!", title());
}
@Override
public boolean ioMayRespondOnCenterThread(IOInstance inst) {
return false;
}
}, VisionView.DEFAULT_VISION_FUNCTION_NAME, dropped));
}
private void wasUpdated() {
setTo(color_parameters);
if (dropped != null) {
debug.info("{%s} making vision call", title());
visionCall();
}
colorParametersUpdated();
}
public void useLog(Log dropped) {
Debug.print("{%s} using log: %s", title(), dropped);
this.dropped = dropped;
dropped_width = dropped.blocks.get(0).dict.get(SharedConstants.LOG_BLOCK_IMAGE_WIDTH_PIXELS())
.asNumber().asInt();
dropped_height = dropped.blocks.get(0).dict.get(SharedConstants.LOG_BLOCK_IMAGE_HEIGHT_PIXELS())
.asNumber().asInt();
if (imageDisplayNorm != null)
imageDisplayNorm.setImage(dropped.blocks.get(0).parseAsYUVImage().toBufferedImage());
else debug.error("{%s} null image display for normal pic!", title());
visionCall();
}
public void takeFrom(ColorParam.Set set) {
ColorParam.Camera cam = set.get(top);
ColorParam param = cam.get(camera);
for (Group g : groups) {
int val = (int) param.get(g.part);
g.slider.setValue(val);
g.spinner.setValue(val);
}
}
public void setTo(ColorParam.Set set) {
ColorParam.Camera cam = set.get(top);
ColorParam param = cam.get(camera);
for (Group g : groups) {
param.set(g.part, g.slider.getValue());
}
}
private ChangeListener fromSpinner = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (ignoreChangeEvents) return;
Group from = null;
for (Group g : groups) {
if (g.spinner == e.getSource()) {
from = g;
break;
}
}
int previous = (Integer) from.spinner.getValue();
from.slider.setValue(previous);
addChange(from, previous);
wasUpdated();
}
};
private ChangeListener fromSlider = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (ignoreChangeEvents) return;
Group from = null;
for (Group g : groups) {
if (g.slider == e.getSource()) {
from = g;
break;
}
}
int previous = from.slider.getValue();
from.spinner.setValue(previous);
addChange(from, previous);
wasUpdated();
}
};
private ActionListener undoListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
undoTheChange();
}
};
protected void installListeners() {
for (Group g : groups) {
g.slider.addChangeListener(fromSlider);
g.spinner.addChangeListener(fromSpinner);
}
tab.UndoButton.addActionListener(undoListener);
}
public TabHandler(final boolean top, ColorParam.Camera.Which camera) {
debug.info("in constructor");
this.top = top; this.camera = camera;
this.tab = new ColorCalibrationTab();
this.tab.tabTitle.setText(title());
tab.SendButton.setText("send to robot");
tab.SaveButton.setText("save to config");
groups.add(new Group(tab.Y0USlider, tab.Y0USpinner, ColorParam.Part.uAtY0));
groups.add(new Group(tab.Y255USlider, tab.Y255USpinner, ColorParam.Part.uAtY255));
groups.add(new Group(tab.Y0VSlider, tab.Y0VSpinner, ColorParam.Part.vAtY0));
groups.add(new Group(tab.Y255VSlider, tab.Y255VSpinner, ColorParam.Part.vAtY255));
groups.add(new Group(tab.FuzzyUSlider, tab.FuzzyUSpinner, ColorParam.Part.u_fuzzy_range));
groups.add(new Group(tab.FuzzyVSlider, tab.FuzzyVSpinner, ColorParam.Part.v_fuzzy_range));
for (Group g : groups) {
g.spinner.setModel(new SpinnerNumberModel(0,0,255,1));
g.slider.setMinimum(0);
g.slider.setMaximum(255);
}
Dimension ratio = new Dimension(320,240);
imageDisplay = new ImageDisplay(ratio);
tab.imageSplitPane.setLeftComponent(imageDisplay);
imageDisplayNorm = new ImageDisplay(ratio);
tab.imageSplitPane.setRightComponent(imageDisplayNorm);
tab.imageSplitPane.setResizeWeight(0.5);
tab.imageSplitPane.setEnabled(false);
tab.imageSplitPane.setBackground(Color.BLACK);
tab.imageSplitPane.setForeground(Color.BLACK);
tab.imageSplitPane.setDividerLocation(0.5);
tab.imageSplitPane.addComponentListener(new ComponentAdapter(){
@Override
public void componentResized(ComponentEvent e) {
tab.imageSplitPane.setDividerLocation(.5d);
}
@Override
public void componentMoved(ComponentEvent e) {
tab.imageSplitPane.setDividerLocation(.5d);
}
});
LogDND.makeComponentTarget(tab, top ? topCameraTarget : botCameraTarget);
debug.info("leaving constructor");
}
}
}
}