diff --git a/src/main/org/insa/graphics/MainWindow.java b/src/main/org/insa/graphics/MainWindow.java index 590e6e7..e1aaadd 100644 --- a/src/main/org/insa/graphics/MainWindow.java +++ b/src/main/org/insa/graphics/MainWindow.java @@ -4,6 +4,8 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; @@ -34,10 +36,11 @@ import javax.swing.Timer; import javax.swing.UIManager; import javax.swing.filechooser.FileNameExtensionFilter; +import org.insa.algo.shortestpath.AStarAlgorithm; import org.insa.algo.shortestpath.BellmanFordAlgorithm; +import org.insa.algo.shortestpath.DijkstraAlgorithm; import org.insa.algo.shortestpath.ShortestPathAlgorithm; import org.insa.algo.shortestpath.ShortestPathData; -import org.insa.algo.shortestpath.ShortestPathData.Mode; import org.insa.algo.shortestpath.ShortestPathGraphicObserver; import org.insa.algo.shortestpath.ShortestPathSolution; import org.insa.algo.weakconnectivity.WeaklyConnectedComponentGraphicObserver; @@ -45,7 +48,6 @@ import org.insa.algo.weakconnectivity.WeaklyConnectedComponentTextObserver; import org.insa.algo.weakconnectivity.WeaklyConnectedComponentsAlgorithm; import org.insa.algo.weakconnectivity.WeaklyConnectedComponentsData; import org.insa.graph.Graph; -import org.insa.graph.Node; import org.insa.graph.Path; import org.insa.graph.io.BinaryGraphReader; import org.insa.graph.io.BinaryGraphReaderV2; @@ -53,7 +55,7 @@ import org.insa.graph.io.BinaryPathReader; import org.insa.graph.io.GraphReader; import org.insa.graph.io.MapMismatchException; import org.insa.graph.io.Openfile; -import org.insa.graphics.MultiPointsClickListener.CallableWithNodes; +import org.insa.graphics.ShortestPathPanel.StartActionEvent; import org.insa.graphics.drawing.BasicDrawing; import org.insa.graphics.drawing.BlackAndWhiteGraphPalette; import org.insa.graphics.drawing.Drawing; @@ -88,6 +90,9 @@ public class MainWindow extends JFrame { // Main panel. private JSplitPane mainPanel; + // Shortest path panel + private ShortestPathPanel spPanel; + // List of item for the top menus. private JMenuItem openMapItem; @@ -158,12 +163,24 @@ public class MainWindow extends JFrame { this.logStream = new StreamCapturer(infoPanel); this.printStream = new PrintStream(this.logStream); + JPanel rightComponent = new JPanel(); + rightComponent.setLayout(new GridBagLayout()); + + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 1; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + c.gridheight = GridBagConstraints.REMAINDER; + rightComponent.add(new JScrollPane(infoPanel), c); + mainPanel.setResizeWeight(0.8); mainPanel.setDividerSize(5); mainPanel.setBackground(Color.WHITE); mainPanel.setLeftComponent((Component) this.drawing); - mainPanel.setRightComponent(new JScrollPane(infoPanel)); + mainPanel.setRightComponent(rightComponent); this.add(mainPanel, BorderLayout.CENTER); // Top Panel @@ -201,6 +218,7 @@ public class MainWindow extends JFrame { threadTimer.stop(); threadPanel.setVisible(false); currentThread.setThread(null); + spPanel.setEnabled(true); } private void launchShortestPathThread(ShortestPathAlgorithm spAlgorithm) { @@ -422,33 +440,49 @@ public class MainWindow extends JFrame { })); // Shortest path - JMenuItem bellmanItem = new JMenuItem("Shortest Path (Bellman-Ford)"); - bellmanItem.addActionListener(baf.createBlockingAction(new ActionListener() { + JMenuItem spItem = new JMenuItem("Shortest Path"); + spItem.addActionListener(baf.createBlockingAction(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - int idx = JOptionPane.showOptionDialog(MainWindow.this, "Which mode do you want?", "Mode selection", - JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, Mode.values(), Mode.LENGTH); + int dividerLocation = mainPanel.getDividerLocation(); + spPanel = new ShortestPathPanel(drawing, graph); - if (idx != -1) { - Mode mode = Mode.values()[idx]; - clickAdapter.enable(2, new CallableWithNodes() { - @Override - public void call(ArrayList nodes) { - launchShortestPathThread(new BellmanFordAlgorithm( - new ShortestPathData(graph, nodes.get(0), nodes.get(1), mode))); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.fill = GridBagConstraints.HORIZONTAL; + ((JPanel) mainPanel.getRightComponent()).add(spPanel, c); + + mainPanel.setDividerLocation(dividerLocation); + + spPanel.addStartActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + StartActionEvent evt = (StartActionEvent) e; + ShortestPathData data = new ShortestPathData(graph, evt.getOrigin(), evt.getDestination(), + evt.getMode()); + ShortestPathAlgorithm spAlgorithm = null; + if (evt.getAlgorithmClass() == BellmanFordAlgorithm.class) { + spAlgorithm = new BellmanFordAlgorithm(data); } - }); - } + else if (evt.getAlgorithmClass() == DijkstraAlgorithm.class) { + spAlgorithm = new DijkstraAlgorithm(data); + } + else if (evt.getAlgorithmClass() == AStarAlgorithm.class) { + spAlgorithm = new AStarAlgorithm(data); + } + spPanel.setEnabled(false); + launchShortestPathThread(spAlgorithm); + } + }); } })); graphLockItems.add(wccItem); - graphLockItems.add(bellmanItem); + graphLockItems.add(spItem); algoMenu.add(wccItem); algoMenu.addSeparator(); - algoMenu.add(bellmanItem); - // algoMenu.add(djikstraItem); - // algoMenu.add(aStarItem); + algoMenu.add(spItem); // Create the menu bar. JMenuBar menuBar = new JMenuBar(); diff --git a/src/main/org/insa/graphics/NodesInputPanel.java b/src/main/org/insa/graphics/NodesInputPanel.java new file mode 100644 index 0000000..70335e5 --- /dev/null +++ b/src/main/org/insa/graphics/NodesInputPanel.java @@ -0,0 +1,307 @@ +package org.insa.graphics; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.insa.graph.Graph; +import org.insa.graph.Node; +import org.insa.graph.Point; +import org.insa.graphics.drawing.Drawing; +import org.insa.graphics.drawing.DrawingClickListener; +import org.insa.graphics.drawing.MarkerTracker; + +public class NodesInputPanel extends JPanel implements DrawingClickListener { + + /** + * + */ + private static final long serialVersionUID = -1638302070013027690L; + + private static final Color DEFAULT_MARKER_COLOR = Color.BLUE; + + public class InputChangedEvent extends ActionEvent { + + /** + * + */ + private static final long serialVersionUID = 3440024811352247731L; + + protected static final String ALL_INPUT_FILLED_EVENT_COMMAND = "allInputFilled"; + + protected static final int ALL_INPUT_FILLED_EVENT_ID = 0x1; + + // List of nodes + List nodes; + + public InputChangedEvent(List nodes2) { + super(NodesInputPanel.this, ALL_INPUT_FILLED_EVENT_ID, ALL_INPUT_FILLED_EVENT_COMMAND); + this.nodes = nodes2; + } + + List getNodes() { + return Collections.unmodifiableList(nodes); + } + + }; + + // Node inputs and markers. + private ArrayList nodeInputs = new ArrayList<>(); + private Map markerTrackers = new IdentityHashMap(); + + // Component that can be enabled/disabled. + private ArrayList components = new ArrayList<>(); + private int inputToFillIndex; + + // ActionListener called when all inputs are filled. + private ArrayList inputChangeListeners = new ArrayList<>(); + + // Graph & Drawing. + private Graph graph; + private Drawing drawing; + + public NodesInputPanel(Drawing drawing, Graph graph) { + super(new GridBagLayout()); + this.graph = graph; + this.drawing = drawing; + initInputToFill(); + drawing.addDrawingClickListener(this); + } + + public void addInputChangedListener(ActionListener listener) { + inputChangeListeners.add(listener); + } + + @Override + public void setEnabled(boolean enabled) { + for (JComponent component: components) { + component.setEnabled(enabled); + } + super.setEnabled(enabled); + if (enabled) { + // Enable: Check if there is an input to fill, otherwize find the next one. + if (getInputToFill() == null) { + nextInputToFill(); + } + } + else { + // Disable, next input to fill = -1. + this.inputToFillIndex = -1; + } + } + + public void clear() { + for (JTextField field: nodeInputs) { + field.setText(""); + } + initInputToFill(); + } + + public void addTextField(String label) { + addTextField(label, DEFAULT_MARKER_COLOR); + } + + public void addTextField(String label, Color markerColor) { + + GridBagConstraints c = new GridBagConstraints(); + c.insets = new Insets(3, 3, 3, 3); + + JLabel jLabel = new JLabel(label); + jLabel.setFont(jLabel.getFont().deriveFont(~Font.BOLD)); + JTextField textField = new JTextField(); + jLabel.setLabelFor(textField); + + c.gridx = 0; + c.gridy = nodeInputs.size(); + c.weightx = 0; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(jLabel, c); + + c.gridx = 1; + c.gridy = nodeInputs.size(); + c.weightx = 1; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(textField, c); + + JButton clearButton = new JButton("Clear"); + c.gridx = 2; + c.gridy = nodeInputs.size(); + c.weightx = 0; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(clearButton, c); + + JButton clickButton = new JButton("Click"); + c.gridx = 3; + c.gridy = nodeInputs.size(); + c.weightx = 0; + c.gridwidth = 1; + c.fill = GridBagConstraints.HORIZONTAL; + add(clickButton, c); + + // Did not find something easier that this... ? + textField.getDocument().addDocumentListener(new DocumentListener() { + + @Override + public void changedUpdate(DocumentEvent e) { + insertUpdate(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + insertUpdate(e); + } + + @Override + public void insertUpdate(DocumentEvent e) { + + // Draw marker if possible + Node curnode = getNodeForInput(textField); + MarkerTracker tracker = markerTrackers.getOrDefault(textField, null); + if (curnode != null) { + if (tracker == null) { + tracker = drawing.drawMarker(curnode.getPoint(), markerColor); + markerTrackers.put(textField, tracker); + } + else { + tracker.moveTo(curnode.getPoint()); + } + tracker.setVisible(true); + } + else if (tracker != null) { + tracker.setVisible(false); + } + + // Create array of nodes + List nodes = getNodeForInputs(); + + // Trigger change event. + for (ActionListener lis: inputChangeListeners) { + lis.actionPerformed(new InputChangedEvent(nodes)); + } + } + }); + + clearButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + textField.setText(""); + setInputToFill(textField); + } + }); + + clickButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setInputToFill(textField); + } + }); + + nodeInputs.add(textField); + components.add(textField); + components.add(clearButton); + components.add(clickButton); + } + + /** + * @return The node for the given text field, or null if the content of the text + * field is invalid. + */ + protected Node getNodeForInput(JTextField textfield) { + try { + Node node = graph.getNodes().get(Integer.valueOf(textfield.getText().trim())); + return node; + } + catch (IllegalArgumentException | IndexOutOfBoundsException ex) { + return null; + } + } + + /** + * @return List of nodes associated with the input. Some nodes may be null if + * their associated input is invalid. + */ + public List getNodeForInputs() { + List nodes = new ArrayList<>(nodeInputs.size()); + for (JTextField input: nodeInputs) { + nodes.add(getNodeForInput(input)); + } + return nodes; + } + + /** + * Get the next input that should be filled by a click, or null if none should + * be filled. + * + * @return + */ + protected JTextField getInputToFill() { + if (inputToFillIndex < 0 || inputToFillIndex >= nodeInputs.size()) { + return null; + } + return nodeInputs.get(inputToFillIndex); + } + + /** + * Initialize the next input to fill. + */ + protected void initInputToFill() { + inputToFillIndex = 0; + } + + /** + * Set the next input to fill to the given text field. + * + * @param input + */ + protected void setInputToFill(JTextField input) { + inputToFillIndex = nodeInputs.indexOf(input); + } + + /** + * Find the next input to fill, if any. + */ + protected void nextInputToFill() { + boolean found = false; + for (int i = 1; i < nodeInputs.size() && !found; ++i) { + int nextIndex = (i + inputToFillIndex) % nodeInputs.size(); + JTextField input = nodeInputs.get(nextIndex); + if (input.getText().trim().isEmpty()) { + inputToFillIndex = nextIndex; + found = true; + } + } + if (!found) { + inputToFillIndex = -1; + } + } + + @Override + public void mouseClicked(Point point) { + Node node = graph.findClosestNode(point); + JTextField input = getInputToFill(); + if (input != null) { + input.setText(String.valueOf(node.getId())); + nextInputToFill(); + } + } +} diff --git a/src/main/org/insa/graphics/ShortestPathPanel.java b/src/main/org/insa/graphics/ShortestPathPanel.java new file mode 100644 index 0000000..4ab4fec --- /dev/null +++ b/src/main/org/insa/graphics/ShortestPathPanel.java @@ -0,0 +1,271 @@ +package org.insa.graphics; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSplitPane; +import javax.swing.border.EmptyBorder; + +import org.insa.algo.shortestpath.AStarAlgorithm; +import org.insa.algo.shortestpath.BellmanFordAlgorithm; +import org.insa.algo.shortestpath.DijkstraAlgorithm; +import org.insa.algo.shortestpath.ShortestPathAlgorithm; +import org.insa.algo.shortestpath.ShortestPathData.Mode; +import org.insa.graph.Graph; +import org.insa.graph.Node; +import org.insa.graph.io.BinaryGraphReaderV2; +import org.insa.graph.io.GraphReader; +import org.insa.graph.io.Openfile; +import org.insa.graphics.NodesInputPanel.InputChangedEvent; +import org.insa.graphics.drawing.BasicDrawing; +import org.insa.graphics.drawing.Drawing; + +public class ShortestPathPanel extends JPanel { + + /** + * + */ + private static final long serialVersionUID = 406148710808045035L; + + public class StartActionEvent extends ActionEvent { + + /** + * + */ + private static final long serialVersionUID = 4090710269781229078L; + + protected static final String START_EVENT_COMMAND = "allInputFilled"; + + protected static final int START_EVENT_ID = 0x1; + + private final Node origin, destination; + private final Mode mode; + private final Class algoClass; + + public StartActionEvent(Class algoClass, Node origin, Node destination, + Mode mode) { + super(ShortestPathPanel.this, START_EVENT_ID, START_EVENT_COMMAND); + this.origin = origin; + this.destination = destination; + this.mode = mode; + this.algoClass = algoClass; + } + + /** + * @return Origin node associated with this event. + */ + public Node getOrigin() { + return this.origin; + } + + /** + * @return Destination node associated with this event. + */ + public Node getDestination() { + return this.destination; + } + + /** + * @return Mode associated with this event. + */ + public Mode getMode() { + return this.mode; + } + + /** + * @return Algorithm class associated with this event. + */ + public Class getAlgorithmClass() { + return this.algoClass; + } + + }; + + // Map between algorithm names and class, see end of this class for + // initialization. + private static Map> SHORTEST_PATH_ALGORITHMS = new HashMap<>(); + + // Input panels for node. + private NodesInputPanel nodesInputPanel; + + // Component that can be enabled/disabled. + private ArrayList components = new ArrayList<>(); + + // Start listeners + List startActionListeners = new ArrayList<>(); + + public ShortestPathPanel(Drawing drawing, Graph graph) { + super(); + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + + setBorder(new EmptyBorder(15, 15, 15, 15)); + + // Set title. + JLabel titleLabel = new JLabel("Shortest-Path"); + titleLabel.setBackground(Color.RED); + titleLabel.setHorizontalAlignment(JLabel.LEFT); + titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + Font font = titleLabel.getFont(); + font = font.deriveFont(Font.BOLD, 18); + titleLabel.setFont(font); + add(titleLabel); + + add(Box.createVerticalStrut(8)); + + // Add algorithm selection + JComboBox algoSelect = new JComboBox<>(SHORTEST_PATH_ALGORITHMS.keySet().toArray(new String[0])); + algoSelect.setBackground(Color.WHITE); + algoSelect.setAlignmentX(Component.LEFT_ALIGNMENT); + add(algoSelect); + components.add(algoSelect); + + // Add inputs for node. + this.nodesInputPanel = new NodesInputPanel(drawing, graph); + this.nodesInputPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + nodesInputPanel.addTextField("Origin: ", new Color(57, 172, 115)); + nodesInputPanel.addTextField("Destination: ", new Color(255, 77, 77)); + + add(this.nodesInputPanel); + components.add(this.nodesInputPanel); + + // Add mode selection + JPanel modePanel = new JPanel(); + modePanel.setAlignmentX(Component.LEFT_ALIGNMENT); + modePanel.setLayout(new BoxLayout(modePanel, BoxLayout.LINE_AXIS)); + JRadioButton lengthModeButton = new JRadioButton("Length"); + lengthModeButton.setSelected(true); + JRadioButton timeModeButton = new JRadioButton("Time"); + ButtonGroup group = new ButtonGroup(); + group.add(lengthModeButton); + group.add(timeModeButton); + modePanel.add(Box.createHorizontalGlue()); + modePanel.add(lengthModeButton); + modePanel.add(Box.createHorizontalGlue()); + modePanel.add(timeModeButton); + modePanel.add(Box.createHorizontalGlue()); + + add(modePanel); + components.add(timeModeButton); + components.add(lengthModeButton); + + // Bottom panel + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.LINE_AXIS)); + JButton startAlgoButton = new JButton("Start"); + startAlgoButton.setEnabled(false); + startAlgoButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + List nodes = nodesInputPanel.getNodeForInputs(); + Node origin = nodes.get(0), destination = nodes.get(1); + Mode mode = lengthModeButton.isSelected() ? Mode.LENGTH : Mode.TIME; + + for (ActionListener lis: startActionListeners) { + lis.actionPerformed(new StartActionEvent(SHORTEST_PATH_ALGORITHMS.get(algoSelect.getSelectedItem()), + origin, destination, mode)); + } + } + }); + + JButton hideButton = new JButton("Hide"); + hideButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + nodesInputPanel.setEnabled(false); + setVisible(false); + } + }); + + bottomPanel.add(startAlgoButton); + bottomPanel.add(Box.createHorizontalGlue()); + bottomPanel.add(hideButton); + + components.add(startAlgoButton); + components.add(hideButton); + + bottomPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(8)); + add(bottomPanel); + + nodesInputPanel.addInputChangedListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + InputChangedEvent evt = (InputChangedEvent) e; + boolean allNotNull = true; + for (Node node: evt.getNodes()) { + if (node == null) { + allNotNull = false; + } + } + startAlgoButton.setEnabled(allNotNull); + } + }); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + nodesInputPanel.setEnabled(enabled); + for (JComponent component: components) { + component.setEnabled(enabled); + } + } + + /** + * Add a new start action listener to this class. + * + * @param listener + */ + public void addStartActionListener(ActionListener listener) { + this.startActionListeners.add(listener); + } + + static { + SHORTEST_PATH_ALGORITHMS.put("Bellman-Ford", BellmanFordAlgorithm.class); + SHORTEST_PATH_ALGORITHMS.put("Dijkstra", DijkstraAlgorithm.class); + SHORTEST_PATH_ALGORITHMS.put("A*", AStarAlgorithm.class); + } + + public static void main(String[] args) throws IOException { + + String nomcarte = "../BE_Graphe_Maps/morbihan3.mapgr"; + GraphReader reader = new BinaryGraphReaderV2(Openfile.open(nomcarte)); + Graph graph = reader.read(); + + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLayout(new BorderLayout()); + JSplitPane p = new JSplitPane(); + BasicDrawing drawing = new BasicDrawing(); + JPanel pane = new ShortestPathPanel(drawing, graph); + p.setLeftComponent(drawing); + p.setRightComponent(pane); + p.setResizeWeight(0.8); + frame.add(p, BorderLayout.CENTER); + frame.show(); + frame.setSize(800, 600); + // pane.setSize(new Dimension(400, 0)); + + drawing.drawGraph(graph); + } +}