package aima.gui.swing.applications.robotics;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.io.File;
import java.io.FileInputStream;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileNameExtensionFilter;
import aima.core.robotics.IMclRobot;
import aima.core.robotics.MonteCarloLocalization;
import aima.core.robotics.datatypes.IMclMove;
import aima.core.robotics.datatypes.RobotException;
import aima.core.robotics.impl.datatypes.AbstractRangeReading;
import aima.core.robotics.impl.datatypes.Angle;
import aima.core.robotics.impl.datatypes.IPose2D;
import aima.core.robotics.impl.map.MclCartesianPlot2D;
import aima.core.util.math.geom.shapes.IGeometric2D;
import aima.core.util.math.geom.shapes.Rect2D;
import aima.gui.swing.applications.robotics.components.AbstractSettingsListener;
import aima.gui.swing.applications.robotics.components.IRobotGui;
import aima.gui.swing.applications.robotics.components.Settings;
import aima.gui.swing.applications.robotics.simple.VirtualRobot;
import aima.gui.swing.framework.util.GraphicsTransfer2D;
import aima.gui.swing.framework.util.GuiBase;
import aima.gui.swing.framework.util.ListTableModel;
/**
* This generic class provides a graphical user interface for the {@link MonteCarloLocalization} in a two-dimensional environment.<br/>
* It makes use of the {@link Settings} class to store and retrieve all its modifiable parameters (e.g. size of the particle cloud).<br/>
* Please note that an {@code ISettingsListener} is registered for {@link AbstractSettingsListener}{@code .PARTICLE_COUNT_KEY} in the settings
* to be able to change the particle cloud's size while "Auto Locate" is running. Thus one should not register another listener to manage the particle cloud size.<br/>
* Additionally, the key {@link AbstractSettingsListener}{@code .MAP_FILE_KEY} is used to store the last used map path in the settings.<br/>
*
* @author Arno von Borries
* @author Jan Phillip Kretzschmar
* @author Andreas Walscheid
*
* @param <P> a pose implementing {@link IPose2D}.
* @param <M> a movement (or sequence of movements) of the robot, implementing {@link IMclMove}.
* @param <R> a range measurement, implementing {@link AbstractRangeReading}.
*/
public class GenericMonteCarloLocalization2DApp<P extends IPose2D<P,M>,M extends IMclMove<M>, R extends AbstractRangeReading> {
protected ExecutorService backgroundThread = Executors.newSingleThreadExecutor();
protected MonteCarloLocalization<P,Angle,M,AbstractRangeReading> mcl;
protected MclCartesianPlot2D<P, M, AbstractRangeReading> map;
protected IMclRobot<Angle,M,AbstractRangeReading> robot;
protected IRobotGui robotGui;
protected Settings settingsGui;
protected MclCore core;
protected MclGui gui;
protected File lastMapFile = null;
/**
* @param mcl an instance of {@link MonteCarloLocalization} for the type parameters {@code <P>}, {@code Angle}, {@code <M>}, {@code AbstractRangeReading}.
* @param map an instance of {@link MclCartesianPlot2D} for the type parameters {@code <P>}, {@code <M>}, {@code AbstractRangeReading}.
* @param robot an instance of a class implementing {@link IMclRobot} for the type parameters {@code Angle}, {@code <M>}, {@code AbstractRangeReading}.
* @param robotGui an instance of a class implementing {@link IRobotGui} that is able to manage the robot used with this Monte-Carlo-Localization.
* @param settingsGui an instance of {@link Settings} which has all those settings loaded that should be accessible for manipulation by the user and
* those settings mentioned in this class' documentation.
*/
public GenericMonteCarloLocalization2DApp(MonteCarloLocalization<P,Angle,M,AbstractRangeReading> mcl, MclCartesianPlot2D<P, M, AbstractRangeReading> map, IMclRobot<Angle,M,AbstractRangeReading> robot, IRobotGui robotGui, Settings settingsGui) {
this.mcl = mcl;
this.map = map;
this.robot = robot;
this.robotGui = robotGui;
this.settingsGui = settingsGui;
this.core = new MclCore();
this.gui = new MclGui();
String mapPath = settingsGui.getSetting(AbstractSettingsListener.MAP_FILE_KEY);
if(mapPath != null) this.lastMapFile = new File(mapPath);
this.settingsGui.registerListener(AbstractSettingsListener.PARTICLE_COUNT_KEY, this.core);
this.settingsGui.registerListener(AbstractSettingsListener.MAX_DISTANCE_KEY, this.core);
}
/**
* Shows the application.
*/
public void show() {
gui.setVisible(true);
}
/**
* Creates the {@code JFrame} of the application and returns it.
* @return the main frame of the application.
*/
public JFrame constructApplicationFrame() {
gui.buildPanels();
return gui;
}
/**
* Executes a runnable in the background without freezing the GUI. Therefore it disables and re-enables the GUI's buttons to prevent concurrency.
* @param runnable the code to be run in background.
*/
public void runInBackground(final Runnable runnable) {
gui.saveButtonState();
gui.enableButtons(gui.buttonStateInit);
backgroundThread.execute(new Runnable() {
@Override
public void run() {
runnable.run();
gui.enableButtons(gui.previousButtonState);
}
});
}
/**
* The core manages the particle cloud size and issues commands to the robot whilst updating the GUI according to the results of these commands.
*
*/
protected class MclCore implements Runnable, Settings.ISettingsListener {
private static final int stepPause = 100;//ms
private JButton button;
private Semaphore runningLock = new Semaphore(1);
private boolean running = false;
private double maxParticleDistance;
private int cloudSize;
private Set<P> samples;
private void setButton(JButton button) {
this.button = button;
}
private void setRunning(boolean running) {
try {
runningLock.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
this.running = running;
runningLock.release();
}
private boolean isRunning() {
try {
runningLock.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
gui.enableButtons(gui.buttonStateInit);
final boolean result = running;
runningLock.release();
return result;
}
private synchronized void updateCloudSize(int size) {
cloudSize = size;
if(map.isLoaded()) {
samples = mcl.generateCloud(cloudSize);
gui.displaySamples(samples);
}
}
private synchronized void generateParticles() {
samples = mcl.generateCloud(cloudSize);
gui.displaySamples(samples);
}
private synchronized void step() throws RobotException, NullPointerException {
M move = robot.performMove();
if(move == null) {
throw new NullPointerException();
}
AbstractRangeReading[] rangeReadings = robot.getRangeReadings();
if(rangeReadings == null) {
throw new NullPointerException();
}
samples = mcl.localize(samples, move, rangeReadings);
P result = map.checkDistanceOfPoses(samples, maxParticleDistance);
gui.displayMove(move);
gui.displayRangeReadings(rangeReadings);
gui.displaySamples(samples);
gui.displayResult(result);
}
/**
* Called upon pressing "Auto Locate". This performs the individual steps of the Monte-Carlo-Localization algorithm looped one after the other until
* the terminating condition is met, that is, the robot is located with sufficient accuracy.
*/
@Override
public void run() {
try {
while(running) {
step();
try{
Thread.sleep(stepPause);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
} catch (NullPointerException e) {
/*A NullPointerException may happen if the robot wasn't initialized before.*/
robotGui.notifyInitialize();
} catch (RobotException e) {
/*A RobotException may be thrown if the robot disconnected for some reason.*/
}
try {
runningLock.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
running = false;
gui.enableButtons(gui.buttonStateNormal);
button.setText(gui.autoLocateTitle);
runningLock.release();
}
@Override
public boolean notifySetting(String key, String value) {
if(gui.isVisible()) gui.runNotifyClean.run();
try {
if(key.equals(AbstractSettingsListener.PARTICLE_COUNT_KEY)) {
updateCloudSize(Integer.parseInt(value));
} else if(key.equals(AbstractSettingsListener.MAX_DISTANCE_KEY)) {
maxParticleDistance = Double.parseDouble(value);
} else {
throw new NumberFormatException();
}
} catch(NumberFormatException e) {
return false;
}
return true;
}
}
/**
* The main application {@code JFrame}.
*/
protected class MclGui extends JFrame {
private static final long serialVersionUID = 1L;
private static final int WINDOW_WIDTH = 800;
private static final int WINDOW_HEIGHT = 500;
protected final boolean[] buttonStateStart = {true, true, false, false, false, true};
protected final boolean[] buttonStateNormal = {true, true, true, true, true, true};
protected final boolean[] buttonStateInit = {false, false, false, false, false, true};
protected final boolean[] buttonStateAuto = {false, false, false, true, false, true};
private final int clearance = GuiBase.getClearance();
private final String autoLocateTitle = "Auto Locate";
private final String autoLocateStopTitle = "Stop";
protected JButton[] buttons;
protected boolean[] previousButtonState;
protected JLabel localizationResult;
protected JTextArea jtARangeReading;
protected ListTableModel movesModel;
protected JTable jTMoves;
protected JScrollPane movesScrollPane;
protected JSlider jSliderZoom;
protected JScrollBar horizontalScroll;
protected JScrollBar verticalScroll;
protected ScrollListener scrollListener = new ScrollListener();
protected MapDrawer md;
private double mapWidth = 1.0d;
private double mapHeight = 1.0d;
private double translateX = 0.0d;
private double translateY = 0.0d;
private int moveRowHeight;
private int horizontalScrollValue;
private int verticalScrollValue;
/**
* Called upon pressing "Initialize Robot".
*/
protected Runnable runInitRobot = new Runnable() {
@Override
public void run() {
md.drawRobot(robotGui.initializeRobot());
buttons[0].setText(robotGui.getButtonString());
enableButtons(previousButtonState);
}
};
/**
* Called upon pressing "Load Map".
*/
protected MapLoader mapLoader = new MapLoader();
/**
* Called upon pressing "Step".
*/
protected Runnable runStep = new Runnable() {
@Override
public void run() {
try {
core.step();
} catch(NullPointerException e) {
/*A NullPointerException may happen if the robot wasn't initialized before.*/
robotGui.notifyInitialize();
} catch(RobotException e) {
/*A RobotException may be thrown if the robot disconnected for some reason.*/
}
enableButtons(gui.buttonStateNormal);
}
};
/**
* Called upon pressing "Clear GUI".
*/
protected Runnable runClean = new Runnable() {
@Override
public void run() {
runNotifyClean.run();
core.generateParticles();
enableButtons(gui.buttonStateNormal);
}
};
protected Runnable runNotifyClean = new Runnable() {
@Override
public void run() {
movesModel.clear();
clearResult();
md.clearMap();
jTMoves.setRowHeight(1);
jtARangeReading.setText("");
}
};
/**
* Creates all components and action listeners for the GUI.
*/
protected void buildPanels() {
setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
setMinimumSize(new Dimension(WINDOW_WIDTH, WINDOW_HEIGHT));
setTitle("AIMA3e-Java: Monte-Carlo-Localization");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//leftPanel:
buttons = new JButton[6];
previousButtonState = new boolean[buttons.length];
buttons[0] = new JButton(IRobotGui.DEFAULT_BUTTON_STRING);
buttons[0].addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
saveButtonState();
enableButtons(gui.buttonStateInit);
backgroundThread.execute(runInitRobot);
}
});
buttons[1] = new JButton("Load Map");
buttons[1].addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
saveButtonState();
enableButtons(gui.buttonStateInit);
backgroundThread.execute(mapLoader);
}
});
buttons[2] = new JButton("Step");
buttons[2].addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
enableButtons(gui.buttonStateInit);
backgroundThread.execute(runStep);
}
});
buttons[3] = new JButton(autoLocateTitle);
buttons[3].addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
if(!core.isRunning()) {
JButton button = (JButton) arg0.getSource();
button.setText(autoLocateStopTitle);
core.setButton(button);
core.setRunning(true);
if(md.clearResult()) {
core.generateParticles();
}
enableButtons(buttonStateAuto);
backgroundThread.execute(core);
} else {
core.setRunning(false);
}
}
});
buttons[4] = new JButton("Clear GUI");
buttons[4].addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
enableButtons(gui.buttonStateInit);
backgroundThread.execute(runClean);
}
});
buttons[5] = new JButton("Settings");
buttons[5].addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
settingsGui.show();
}
});
enableButtons(buttonStateStart);
localizationResult = new JLabel("<HTML>Result: <BR><BR><BR></HTML>");
JPanel leftPanel = new JPanel();
leftPanel.setBorder(GuiBase.getClearanceBorder());
leftPanel.setLayout(new GridLayout(0,1,clearance,clearance));
for(JButton button:buttons) leftPanel.add(button);
leftPanel.add(localizationResult);
//centerPanel:
md = new MapDrawer();
verticalScroll = new JScrollBar(JScrollBar.VERTICAL);
verticalScroll.setEnabled(false);
verticalScroll.addAdjustmentListener(scrollListener);
horizontalScroll = new JScrollBar(JScrollBar.HORIZONTAL);
horizontalScroll.setEnabled(false);
horizontalScroll.addAdjustmentListener(scrollListener);
jSliderZoom = new JSlider(1, 200);
jSliderZoom.setValue(1);
jSliderZoom.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
horizontalScrollValue = horizontalScroll.getValue();
verticalScrollValue = verticalScroll.getValue();
md.scaleMap();
}
});
JPanel centerPanel = new JPanel();
centerPanel.setBorder(GuiBase.getClearanceBorder());
centerPanel.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
c.weightx = 1;
c.weighty = 1;
c.fill = GridBagConstraints.BOTH;
centerPanel.add(md, c);
c.gridx = 1;
c.gridy = 0;
c.weightx = 0;
c.weighty = 0;
c.fill = GridBagConstraints.VERTICAL;
centerPanel.add(verticalScroll, c);
c.gridx = 0;
c.gridy = 1;
c.weightx = 1;
c.weighty = 0;
c.fill = GridBagConstraints.HORIZONTAL;
centerPanel.add(horizontalScroll, c);
c.gridx = 0;
c.gridy = 2;
c.weightx = 1;
c.weighty = 0;
c.fill = GridBagConstraints.HORIZONTAL;
centerPanel.add(jSliderZoom, c);
//rightPanel:
JLabel jLRangeReading = new JLabel("Range Reading:");
jtARangeReading = new JTextArea();
jtARangeReading.setEditable(false);
jtARangeReading.setLineWrap(true);
jtARangeReading.setWrapStyleWord(true);
JScrollPane rangeReadingScrollPane = new JScrollPane(jtARangeReading);
rangeReadingScrollPane.setAlignmentX(LEFT_ALIGNMENT);
jLRangeReading.setLabelFor(rangeReadingScrollPane);
JPanel rangeReadingPanel = new JPanel();
rangeReadingPanel.setLayout(new BoxLayout(rangeReadingPanel, BoxLayout.Y_AXIS));
rangeReadingPanel.add(jLRangeReading);
rangeReadingPanel.add(GuiBase.getClearanceComp());
rangeReadingPanel.add(rangeReadingScrollPane);
movesModel = new ListTableModel("Moves:");
JLabel jLMoves = new JLabel(movesModel.getColumnName(0));
jTMoves = new JTable(movesModel) {
private static final long serialVersionUID = 1L;
@Override
protected void configureEnclosingScrollPane() { }
};
movesScrollPane = new JScrollPane(jTMoves, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
jTMoves.setFillsViewportHeight(true);
moveRowHeight = jTMoves.getRowHeight();
movesScrollPane.setAlignmentX(LEFT_ALIGNMENT);
jLMoves.setLabelFor(movesScrollPane);
JPanel movesPanel = new JPanel();
movesPanel.setLayout(new BoxLayout(movesPanel, BoxLayout.Y_AXIS));
movesPanel.add(jLMoves);
movesPanel.add(GuiBase.getClearanceComp());
movesPanel.add(movesScrollPane);
JPanel rightPanel = new JPanel();
rightPanel.setBorder(GuiBase.getClearanceBorder());
rightPanel.setPreferredSize(new Dimension(170,1));
rightPanel.setLayout(new GridLayout(0,1,clearance,clearance));
rightPanel.add(rangeReadingPanel);
rightPanel.add(movesPanel);
//Put all panels together:
JPanel mainPanel = new JPanel();
mainPanel.setLayout(new BorderLayout());
mainPanel.add(leftPanel, BorderLayout.WEST);
mainPanel.add(centerPanel, BorderLayout.CENTER);
mainPanel.add(rightPanel, BorderLayout.EAST);
getContentPane().add(mainPanel, BorderLayout.CENTER);
//Listen for resizes on the center panel:
centerPanel.addComponentListener(new ComponentListener() {
@Override
public void componentResized(ComponentEvent e) {
movesScrollPane.getVerticalScrollBar().setValue(movesScrollPane.getVerticalScrollBar().getMaximum()-movesScrollPane.getVerticalScrollBar().getVisibleAmount());
//Invalidate the values: Scrollbars only repaint on a change of these values!
horizontalScrollValue = horizontalScroll.getValue();
verticalScrollValue = verticalScroll.getValue();
horizontalScroll.setValues(-1, 0, -1, -1);
verticalScroll.setValues(-1, 0, -1, -1);
//Recalculate all scale Factors of the map:
md.scaleMap();
}
@Override
public void componentHidden(ComponentEvent arg0) { }
@Override
public void componentMoved(ComponentEvent arg0) { }
@Override
public void componentShown(ComponentEvent arg0) { }
});
}
/**
* (de)activate all buttons. For each button a boolean describing its state is passed.
* @param buttonStates one of the four boolean arrays {@code buttonStateStart, buttonStateNormal, buttonStateInit, buttonStateAuto}.
*/
protected void enableButtons(boolean[] buttonStates) {
for(int i=0; i < buttons.length; i++) buttons[i].setEnabled(buttonStates[i]);
}
/**
* Saves the current activation state of all buttons into {@code previousButtonState}.
*/
protected void saveButtonState() {
previousButtonState = new boolean[buttons.length];
for(int i=0; i < buttons.length; i++) previousButtonState[i] = buttons[i].isEnabled();
}
/**
* Tells the {@link MapDrawer} {@code md} to display the loaded {@link MclCartesianPlot2D} {@code map}.
*/
protected void createMap() {
findMapSize();
if(mapWidth <= 1.0 || mapHeight <= 1.0){
GuiBase.showMessageBox("Map size could not be calculated!");
return;
}
horizontalScrollValue = 0;
verticalScrollValue = 0;
md.drawMap();
md.scaleMap();
enableButtons(buttonStateNormal);
}
/**
* Determines the map size by the smallest and greatest values of the shapes to be drawn.
*/
protected void findMapSize() {
Iterator<Rect2D> areaIterator = map.getAreaBoundaries();
Iterator<Rect2D> obstacleIterator = map.getObstacleBoundaries();
double minX = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
while(areaIterator.hasNext()) {
Rect2D rect = areaIterator.next();
minX = minX > rect.getLowerLeft().getX() ? rect.getLowerLeft().getX() : minX;
minY = minY > rect.getLowerLeft().getY() ? rect.getLowerLeft().getY() : minY;
maxX = maxX < rect.getUpperRight().getX() ? rect.getUpperRight().getX() : maxX;
maxY = maxY < rect.getUpperRight().getY() ? rect.getUpperRight().getY() : maxY;
}
while(obstacleIterator.hasNext()) {
Rect2D rect = obstacleIterator.next();
minX = minX > rect.getLowerLeft().getX() ? rect.getLowerLeft().getX() : minX;
minY = minY > rect.getLowerLeft().getY() ? rect.getLowerLeft().getY() : minY;
maxX = maxX < rect.getUpperRight().getX() ? rect.getUpperRight().getX() : maxX;
maxY = maxY < rect.getUpperRight().getY() ? rect.getUpperRight().getY() : maxY;
}
mapWidth = maxX - minX;
mapHeight = maxY - minY;
final double mapBorder = 0.1d * mapWidth > mapHeight ? mapWidth : mapHeight;
translateX = -minX + mapBorder;
translateY = -minY + mapBorder;
mapWidth += 2 * mapBorder;
mapHeight += 2 * mapBorder;
}
/**
* Shows a set of {@link AbstractRangeReading}.
* @param rangeReadings the range readings to be displayed.
*/
protected void displayRangeReadings(AbstractRangeReading[] rangeReadings) {
StringBuilder ranges = new StringBuilder();
for(AbstractRangeReading rangeReading: rangeReadings) ranges.append(rangeReading.toString()).append("\n");
jtARangeReading.setText(ranges.toString());
}
/**
* Shows a {@code M} move.
* @param move the move to be displayed.
*/
protected void displayMove(final M move) {
final int size = move.toString().split("<BR>").length;
final int rowHeight = (int) (size * 1.25d * moveRowHeight);
if(rowHeight > jTMoves.getRowHeight()) jTMoves.setRowHeight(rowHeight);
movesModel.add("<HTML>" + move.toString() + "</HTML>");
movesScrollPane.getVerticalScrollBar().setValue(movesScrollPane.getVerticalScrollBar().getMaximum()-movesScrollPane.getVerticalScrollBar().getVisibleAmount());
}
/**
* Displays the sample cloud on the map.
* @param samples the set of samples to be displayed.
*/
protected void displaySamples(Set<P> samples) {
md.drawParticles(samples);
}
/**
* Shows the localized position of the robot on the map and in a label.
* @param result the position of the robot to be displayed.
*/
protected void displayResult(P result) {
if(result != null) {
String resultOutputString = "X: " + GuiBase.getFormat().format(result.getX()) + ",<BR>Y: " + GuiBase.getFormat().format(result.getY());
localizationResult.setText("<HTML>Result: <BR>" + resultOutputString + "</HTML>");
md.showResult(result);
} else {
clearResult();
}
}
/**
* Clears the result that may has been set.
*/
protected void clearResult() {
md.clearResult();
localizationResult.setText("<HTML>Result: <BR><BR><BR></HTML>");
}
/**
* The scroll listener is registered on both of the map's scroll bars. It is used to scroll over the map.
*/
protected class ScrollListener implements AdjustmentListener {
private boolean notifyMapDrawer = false;
@Override
public void adjustmentValueChanged(AdjustmentEvent arg0) {
if(notifyMapDrawer) {
md.repaint();
}
}
/**
* Sets whether the map should be notified of an adjustment event.
* @param b true if the map should be notified.
*/
protected void setNotify(boolean b) {
notifyMapDrawer = b;
}
}
/**
* Hands off the loading of a map file to a different thread without freezing the GUI.
* Handles exceptions during the loading and processing of a file.
*/
protected class MapLoader implements Runnable {
private JFileChooser chooser;
/**
* Constructs a new map loader.
*/
protected MapLoader() {
chooser = new JFileChooser();
//Change the background color of the component holding the "Search in:" label.
for(int i=0;i<chooser.getComponentCount();i++) {
Component component = chooser.getComponent(i);
if(component instanceof JComponent) {
((JComponent) component).setBackground(GuiBase.getBackgroundColor());
}
}
FileNameExtensionFilter filter = new FileNameExtensionFilter(
"Map (*.svg)", "svg");
chooser.setFileFilter(filter);
GuiBase.initMessageBox();
}
@Override
public void run() {
if(lastMapFile != null) chooser.setSelectedFile(lastMapFile);
int returnVal = chooser.showDialog(null, null);
if(returnVal == JFileChooser.APPROVE_OPTION) {
lastMapFile = chooser.getSelectedFile();
settingsGui.setSetting(AbstractSettingsListener.MAP_FILE_KEY, lastMapFile.getPath());
GuiBase.showMessageBox("Loading map, please wait...", false);
try {
loadMap();
return;
} catch (Exception e) {
GuiBase.showMessageBox(e.getMessage());
}
}
enableButtons(previousButtonState);
}
/**
* Tries to load the map file into the {@link MclCartesianPlot2D} {@code map}.
* @throws Exception may be thrown if either the map file is not found or is not valid in some way.
*/
protected void loadMap() throws Exception {
map.loadMap(new FileInputStream(lastMapFile), new FileInputStream(lastMapFile));
if(!map.getAreas().hasNext()) {
throw new Exception("Map has no valid area.");
}
else if(!map.getObstacles().hasNext()) {
throw new Exception("Map has no obstacles.");
}
core.generateParticles();
gui.createMap();
GuiBase.hideMessageBox();
}
}
/**
* This class is a panel where the map, the samples and the robot are displayed.
*/
protected class MapDrawer extends JPanel {
private static final long serialVersionUID = 1L;
private static final int POSE_WIDTH = 4;
private static final int POSE_HEIGHT = 4;
private boolean robotInitialized = false;
private boolean mapLoaded = false;
private Set<P> samples;
private P locResult;
private boolean gotResult = false;
private double minScaleFactor = 1.0d;
private double realScaleFactor = 1.0d;
/**
* Default constructor. Sets the background color to the GUI foreground color.
*/
protected MapDrawer() {
this.setBackground(GuiBase.getTextColor());
}
/**
* With this method, the {@code MapDrawer} is told that it can commence the drawing of the map.
*/
protected void drawMap() {
mapLoaded = true;
repaint();
}
/**
*This method clears the panel of any map data.
*/
protected void deleteMap() {
mapLoaded = false;
repaint();
}
/**
* Deletes everything but the map data from the panel.
*/
protected void clearMap() {
samples = null;
gotResult = false;
repaint();
}
/**
* Draws the sample cloud.
* @param samples the {@code Set} containing the samples.
*/
protected void drawParticles(Set<P> samples) {
this.samples = samples;
repaint();
}
/**
* Draws the localized position of the robot.
* @param result the estimated position of the robot.
*/
protected void showResult(P result) {
locResult = result;
gotResult = true;
repaint();
}
/**
* Deletes any previous localization result.
* @return true if a result was actually deleted.
*/
protected boolean clearResult() {
if(gotResult) {
gotResult = false;
return true;
}
return false;
}
/**
* Draws the actual position of the robot.
* @param initialized true if the robot is to be drawn.
*/
protected void drawRobot(boolean initialized) {
robotInitialized = initialized;
repaint();
}
/**
* This method calculates the scale factors to scale the map by.
*/
protected void scaleMap() {
final double calcXScaleFactor = (double) getWidth() / mapWidth;
final double calcYScaleFactor = (double) getHeight() / mapHeight;
if((Double.isInfinite(calcXScaleFactor) && Double.isInfinite(calcYScaleFactor)) || (Double.isNaN(calcXScaleFactor) && Double.isNaN(calcYScaleFactor))) {
minScaleFactor = 1.0d;
} else if(calcXScaleFactor >= calcYScaleFactor || Double.isNaN(calcXScaleFactor)) {
minScaleFactor = calcYScaleFactor - 0.0010d;
} else {
minScaleFactor = calcXScaleFactor - 0.0005d;
}
realScaleFactor = jSliderZoom.getValue() == 1 ? minScaleFactor : jSliderZoom.getValue() / 10 + 1;
double realMapWidth = mapWidth * realScaleFactor;
double realMapHeight = mapHeight * realScaleFactor;
scrollListener.setNotify(false);
if(realMapWidth <= getWidth()) {
horizontalScroll.setEnabled(false);
horizontalScroll.setValues(0,0,0,0);
} else {
realMapWidth *= 1.2d;
horizontalScroll.setValues((int) (horizontalScrollValue + getWidth() > realMapWidth ? realMapWidth - getWidth() : horizontalScrollValue), getWidth(), 0, (int) realMapWidth);
if(!horizontalScroll.isEnabled()) horizontalScroll.setEnabled(true);
}
if(realMapHeight <= getHeight()) {
verticalScroll.setEnabled(false);
verticalScroll.setValues(0,0,0,0);
} else {
realMapHeight *= 1.2d;
verticalScroll.setValues((int) (verticalScrollValue + getHeight() > realMapHeight ? realMapHeight - getHeight() : verticalScrollValue), getHeight(), 0, (int) realMapHeight);
if(!verticalScroll.isEnabled()) verticalScroll.setEnabled(true);
}
scrollListener.setNotify(true);
repaint();
}
@Override
public void paint(Graphics gra) {
super.paint(gra);
if(mapLoaded) {
Graphics2D g2d = (Graphics2D) gra;
g2d.translate(translateX*realScaleFactor-horizontalScroll.getValue(),translateY*realScaleFactor-verticalScroll.getValue());
g2d.scale(realScaleFactor, realScaleFactor);
//Draw the map:
Iterator<IGeometric2D> areaIterator = map.getAreas();
Iterator<IGeometric2D> obstacleIterator = map.getObstacles();
while (areaIterator.hasNext()) {
g2d.setColor(GuiBase.getAreaColor());
g2d.draw(GraphicsTransfer2D.transfer(areaIterator.next()));
}
while (obstacleIterator.hasNext()) {
g2d.setColor(GuiBase.getBackgroundColor());
Shape shape = GraphicsTransfer2D.transfer(obstacleIterator.next());
g2d.draw(shape);
g2d.fill(shape);
}
if(samples != null) {
//Draw the samples onto the map:
for (P sample: samples) {
g2d.setColor(Color.GREEN);
g2d.fillOval((int) sample.getX() - POSE_WIDTH/2, (int) sample.getY() - POSE_HEIGHT/2, POSE_WIDTH, POSE_HEIGHT);
final double h = sample.getHeading();
final int x2 = (int) (sample.getX() + POSE_WIDTH * Math.cos(h));
final int y2 = (int) (sample.getY() + POSE_HEIGHT * Math.sin(h));
g2d.drawLine((int) sample.getX(), (int) sample.getY(), x2, y2);
}
}
if(robotInitialized && robot instanceof VirtualRobot) {
g2d.setColor(Color.BLACK);
g2d.fillOval((int) ((VirtualRobot) robot).getPose().getX() - POSE_WIDTH/2, (int) ((VirtualRobot) robot).getPose().getY() - POSE_HEIGHT/2, POSE_WIDTH, POSE_HEIGHT);
}
if(gotResult) {
g2d.setColor(Color.BLUE);
g2d.drawOval((int) (locResult.getX() - core.maxParticleDistance / 2), (int) (locResult.getY() - core.maxParticleDistance / 2), (int) core.maxParticleDistance, (int) core.maxParticleDistance);
}
}
}
}
}
}