diff --git a/src/main/org/insa/graphics/AlgorithmPanel.java b/src/main/org/insa/graphics/AlgorithmPanel.java index bffebdc..a6c7e04 100644 --- a/src/main/org/insa/graphics/AlgorithmPanel.java +++ b/src/main/org/insa/graphics/AlgorithmPanel.java @@ -61,8 +61,8 @@ public class AlgorithmPanel extends JPanel { private final boolean graphicVisualization; private final boolean textualVisualization; - public StartActionEvent(Class> algoClass, List nodes, AbstractInputData.Mode mode, - AbstractInputData.ArcFilter arcFilter, boolean graphicVisualization, boolean textualVisualization) { + public StartActionEvent(Class> algoClass, List nodes, Mode mode, + ArcFilter arcFilter, boolean graphicVisualization, boolean textualVisualization) { super(AlgorithmPanel.this, START_EVENT_ID, START_EVENT_COMMAND); this.nodes = nodes; this.mode = mode; @@ -82,14 +82,14 @@ public class AlgorithmPanel extends JPanel { /** * @return Mode associated with this event. */ - public AbstractInputData.Mode getMode() { + public Mode getMode() { return this.mode; } /** * @return Arc filter associated with this event. */ - public AbstractInputData.ArcFilter getArcFilter() { + public ArcFilter getArcFilter() { return this.arcFilter; } @@ -168,28 +168,29 @@ public class AlgorithmPanel extends JPanel { add(this.nodesInputPanel); components.add(this.nodesInputPanel); - JComboBox arcFilterSelect = new JComboBox<>(new AbstractInputData.ArcFilter[] { new AbstractInputData.ArcFilter() { - @Override - public boolean isAllowed(Arc arc) { - return true; - } + JComboBox arcFilterSelect = new JComboBox<>( + new AbstractInputData.ArcFilter[] { new AbstractInputData.ArcFilter() { + @Override + public boolean isAllowed(Arc arc) { + return true; + } - @Override - public String toString() { - return "All arcs are allowed"; - } - }, new AbstractInputData.ArcFilter() { - @Override - public boolean isAllowed(Arc arc) { - return arc.getRoadInformation().getAccessRestrictions().isAllowedFor(AccessMode.MOTORCAR) - && !arc.getRoadInformation().getAccessRestrictions().isPrivate(); - } + @Override + public String toString() { + return "All arcs are allowed"; + } + }, new AbstractInputData.ArcFilter() { + @Override + public boolean isAllowed(Arc arc) { + return arc.getRoadInformation().getAccessRestrictions().isAllowedFor(AccessMode.MOTORCAR) + && !arc.getRoadInformation().getAccessRestrictions().isPrivate(); + } - @Override - public String toString() { - return "Only non-private roads allowed for motorcars"; - } - } }); + @Override + public String toString() { + return "Only non-private roads allowed for motorcars"; + } + } }); arcFilterSelect.setBackground(Color.WHITE); // Add mode selection @@ -265,13 +266,17 @@ public class AlgorithmPanel extends JPanel { startAlgoButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - AbstractInputData.Mode mode = lengthModeButton.isSelected() ? AbstractInputData.Mode.LENGTH : AbstractInputData.Mode.TIME; + AbstractInputData.Mode mode = lengthModeButton.isSelected() ? AbstractInputData.Mode.LENGTH + : AbstractInputData.Mode.TIME; for (ActionListener lis: startActionListeners) { - lis.actionPerformed(new StartActionEvent( - AlgorithmFactory.getAlgorithmClass(baseAlgorithm, (String) algoSelect.getSelectedItem()), - nodesInputPanel.getNodeForInputs(), mode, (AbstractInputData.ArcFilter) arcFilterSelect.getSelectedItem(), - graphicObserver.isSelected(), textObserver.isSelected())); + lis.actionPerformed( + new StartActionEvent( + AlgorithmFactory.getAlgorithmClass(baseAlgorithm, + (String) algoSelect.getSelectedItem()), + nodesInputPanel.getNodeForInputs(), mode, + (AbstractInputData.ArcFilter) arcFilterSelect.getSelectedItem(), + graphicObserver.isSelected(), textObserver.isSelected())); } } }); diff --git a/src/main/org/insa/graphics/MainWindow.java b/src/main/org/insa/graphics/MainWindow.java index dee15fe..220844d 100644 --- a/src/main/org/insa/graphics/MainWindow.java +++ b/src/main/org/insa/graphics/MainWindow.java @@ -106,9 +106,12 @@ public class MainWindow extends JFrame { // Main panel. private JSplitPane mainPanel; - // Shortest path panel + // Algorithm panel private AlgorithmPanel spPanel; + // Path panel + private PathsPanel pathPanel; + // List of items that cannot be used without a graph private ArrayList graphLockItems = new ArrayList(); @@ -183,14 +186,18 @@ public class MainWindow extends JFrame { }); spPanel.setVisible(false); + this.pathPanel = new PathsPanel(this); + // Add click listeners to both drawing. basicDrawing.addDrawingClickListener(spPanel.nodesInputPanel); mapViewDrawing.addDrawingClickListener(spPanel.nodesInputPanel); this.graphChangeListeneres.add(spPanel.nodesInputPanel); this.graphChangeListeneres.add(spPanel.solutionPanel); + this.graphChangeListeneres.add(pathPanel); this.drawingChangeListeners.add(spPanel.nodesInputPanel); this.drawingChangeListeners.add(spPanel.solutionPanel); + this.drawingChangeListeners.add(pathPanel); // Create action factory. this.currentThread = new ThreadWrapper(this); @@ -231,11 +238,14 @@ public class MainWindow extends JFrame { c.gridx = 0; c.gridy = 0; c.fill = GridBagConstraints.HORIZONTAL; + rightComponent.add(pathPanel, c); + + c.gridy = 1; rightComponent.add(spPanel, c); c = new GridBagConstraints(); c.gridx = 0; - c.gridy = 1; + c.gridy = 2; c.weightx = 1; c.weighty = 1; c.fill = GridBagConstraints.BOTH; @@ -293,7 +303,10 @@ public class MainWindow extends JFrame { } private void displayShortestPathSolution(ShortestPathSolution solution) { - spPanel.solutionPanel.addSolution(solution); + spPanel.solutionPanel.addSolution(solution, false); // Do not add overlay in the solution panel. + if (solution.isFeasible()) { + pathPanel.addPath(solution.getPath()); + } spPanel.solutionPanel.setVisible(true); } @@ -526,7 +539,7 @@ public class MainWindow extends JFrame { } try { Path path = reader.readPath(graph); - drawing.drawPath(path); + pathPanel.addPath(path); } catch (MapMismatchException exception) { JOptionPane.showMessageDialog(MainWindow.this, diff --git a/src/main/org/insa/graphics/PathsPanel.java b/src/main/org/insa/graphics/PathsPanel.java new file mode 100644 index 0000000..5e6a12c --- /dev/null +++ b/src/main/org/insa/graphics/PathsPanel.java @@ -0,0 +1,272 @@ +package org.insa.graphics; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; + +import org.insa.graph.Graph; +import org.insa.graph.Path; +import org.insa.graph.io.BinaryPathWriter; +import org.insa.graphics.drawing.Drawing; +import org.insa.graphics.drawing.overlays.PathOverlay; + +public class PathsPanel extends JPanel implements DrawingChangeListener, GraphChangeListener { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private class PathBundle { + + // Solution + private final Path path; + + // Path Overlay (not final due to redraw) + private PathOverlay overlay; + + /** + * Create a new bundle with the given path and create a new overlay + * corresponding to the path. + * + * @param path Path for this bundle, must not be null. + * + */ + public PathBundle(Path path) { + this.path = path; + this.overlay = drawing.drawPath(this.path); + } + + /** + * @return Path associated with this bundle. + */ + public Path getPath() { + return this.path; + } + + /** + * @return Overlay associated with this bundle (never null). + */ + public PathOverlay getOverlay() { + return this.overlay; + } + + /** + * Re-draw the current overlay (if any) on the new drawing. + * + */ + public void updateOverlay() { + PathOverlay oldOverlay = this.overlay; + this.overlay = drawing.drawPath(path); + this.overlay.setVisible(oldOverlay.isVisible()); + oldOverlay.delete(); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + public String toString() { + return "Path from #" + path.getOrigin().getId() + " to #" + path.getDestination().getId(); + } + + } + + // Solution + private Drawing drawing; + + // Solution selector + private final JComboBox solutionSelect; + + // Map solution -> panel + private final JTextArea informationPanel; + + // Current bundle + private PathBundle currentBundle = null; + + public PathsPanel(Component parent) { + super(); + setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS)); + + setBorder(new EmptyBorder(15, 15, 15, 15)); + + solutionSelect = new JComboBox<>(); + solutionSelect.setBackground(Color.WHITE); + solutionSelect.setAlignmentX(Component.LEFT_ALIGNMENT); + add(solutionSelect); + + informationPanel = new JTextArea(); + informationPanel.setWrapStyleWord(true); + informationPanel.setLineWrap(true); + informationPanel.setOpaque(true); + informationPanel.setFocusable(false); + informationPanel.setEditable(false); + informationPanel.setBackground(UIManager.getColor("Label.background")); + informationPanel.setFont(UIManager.getFont("Label.font")); + informationPanel.setBorder(UIManager.getBorder("Label.border")); + informationPanel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(8)); + add(informationPanel); + + JButton clearButton = new JButton("Hide"); + clearButton.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + if (currentBundle != null) { + if (currentBundle.getOverlay().isVisible()) { + currentBundle.getOverlay().setVisible(false); + clearButton.setText("Show"); + } + else { + currentBundle.getOverlay().setVisible(true); + clearButton.setText("Hide"); + } + } + } + }); + + JButton saveButton = new JButton("Save"); + saveButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String filepath = System.getProperty("user.dir"); + filepath += File.separator + String.format("path_%s_%d_%d.path", + currentBundle.getPath().getGraph().getMapId().toLowerCase().replaceAll("[^a-z0-9_]", "_"), + currentBundle.getPath().getOrigin().getId(), currentBundle.getPath().getDestination().getId()); + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setSelectedFile(new File(filepath)); + fileChooser.setApproveButtonText("Save"); + + if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { + File file = fileChooser.getSelectedFile(); + try { + BinaryPathWriter writer = new BinaryPathWriter( + new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))); + writer.writePath(currentBundle.getPath()); + } + catch (IOException e1) { + JOptionPane.showMessageDialog(parent, "Unable to write path to the selected file."); + e1.printStackTrace(); + } + } + } + }); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); + buttonPanel.add(Box.createHorizontalGlue()); + buttonPanel.add(clearButton); + buttonPanel.add(saveButton); + buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(4)); + add(buttonPanel); + + solutionSelect.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + + PathBundle bundle = (PathBundle) solutionSelect.getSelectedItem(); + + // Handle case when the JComboBox is empty. + if (bundle == null) { + return; + } + + if (currentBundle != null) { + currentBundle.getOverlay().setVisible(false); + } + + updateInformationLabel(bundle); + clearButton.setText("Hide"); + bundle.getOverlay().setVisible(true); + currentBundle = bundle; + } + }); + + // Default hidden + this.setVisible(false); + + } + + public void addPath(Path path) { + PathBundle bundle = new PathBundle(path); + solutionSelect.addItem(bundle); + solutionSelect.setSelectedItem(bundle); + + this.setVisible(true); + } + + protected void updateInformationLabel(PathBundle bundle) { + String info = ""; + info += String.format("Length = %.3f kilometers, duration = ", bundle.getPath().getLength() / 1000.); + double time = bundle.getPath().getMinimumTravelTime(); + int hours = (int) (time / 3600); + int minutes = (int) (time / 60) % 60; + int seconds = ((int) time) % 60; + info += String.format("%d hours, %d minutes, %d seconds.", hours, minutes, seconds); + informationPanel.setText(info); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + solutionSelect.setEnabled(enabled); + + if (enabled) { + // Trigger event + solutionSelect.actionPerformed(null); + } + else { + PathBundle bundle = (PathBundle) this.solutionSelect.getSelectedItem(); + if (bundle != null) { + bundle.getOverlay().setVisible(false); + } + } + } + + @Override + public void newGraphLoaded(Graph graph) { + for (int i = 0; i < this.solutionSelect.getItemCount(); ++i) { + this.solutionSelect.getItemAt(i).getOverlay().delete(); + } + this.solutionSelect.removeAllItems(); + this.currentBundle = null; + this.setVisible(false); + } + + @Override + public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) { + if (newDrawing != drawing) { + drawing = newDrawing; + } + } + + @Override + public void onRedrawRequest() { + for (int i = 0; i < this.solutionSelect.getItemCount(); ++i) { + this.solutionSelect.getItemAt(i).updateOverlay(); + } + } + +} diff --git a/src/main/org/insa/graphics/SolutionPanel.java b/src/main/org/insa/graphics/SolutionPanel.java index d059c4d..5c81e47 100644 --- a/src/main/org/insa/graphics/SolutionPanel.java +++ b/src/main/org/insa/graphics/SolutionPanel.java @@ -23,7 +23,6 @@ import javax.swing.border.EmptyBorder; import org.insa.algo.AbstractInputData; import org.insa.algo.AbstractSolution; -import org.insa.algo.shortestpath.ShortestPathSolution; import org.insa.graph.Graph; import org.insa.graph.Path; import org.insa.graphics.drawing.Drawing; @@ -51,9 +50,11 @@ public class SolutionPanel extends JPanel implements DrawingChangeListener, Grap * @param solution Solution for this bundle, must not be null. * */ - public SolutionBundle(AbstractSolution solution) { + public SolutionBundle(AbstractSolution solution, boolean createOverlays) { this.solution = solution; - this.overlays = createOverlaysFromSolution(); + if (createOverlays) { + this.overlays = createOverlaysFromSolution(); + } } /** @@ -77,11 +78,21 @@ public class SolutionPanel extends JPanel implements DrawingChangeListener, Grap return this.overlays; } + /** + * @return true if this bundle has overlays. + */ + public boolean hasOverlays() { + return !this.overlays.isEmpty(); + } + /** * Re-draw the current overlay (if any) on the new drawing. * */ public void updateOverlays() { + if (this.overlays.isEmpty()) { + return; // This bundle has no overlay. + } List oldOverlays = this.overlays; this.overlays = createOverlaysFromSolution(); for (int i = 0; i < oldOverlays.size(); ++i) { @@ -153,6 +164,8 @@ public class SolutionPanel extends JPanel implements DrawingChangeListener, Grap informationPanel.setFont(UIManager.getFont("Label.font")); informationPanel.setBorder(UIManager.getBorder("Label.border")); informationPanel.setAlignmentX(JLabel.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(8)); add(informationPanel); JButton clearButton = new JButton("Hide"); @@ -173,42 +186,13 @@ public class SolutionPanel extends JPanel implements DrawingChangeListener, Grap } }); - JButton saveButton = new JButton("Save"); - saveButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - // String filepath = System.getProperty("user.dir"); - // filepath += File.separator + String.format("path_%s_%d_%d.path", - // currentBundle.getData().getGraph().getMapId().toLowerCase().replaceAll("[^a-z0-9_]", - // "_"), - // currentBundle.getData().getOrigin().getId(), - // currentBundle.getData().getDestination().getId()); - // JFileChooser fileChooser = new JFileChooser(); - // fileChooser.setSelectedFile(new File(filepath)); - // fileChooser.setApproveButtonText("Save"); - // - // if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) { - // File file = fileChooser.getSelectedFile(); - // try { - // BinaryPathWriter writer = new BinaryPathWriter( - // new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)))); - // writer.writePath(currentBundle.getSolution().getPath()); - // } - // catch (IOException e1) { - // JOptionPane.showMessageDialog(parent, "Unable to write path to the selected - // file."); - // e1.printStackTrace(); - // } - // } - } - }); - JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS)); buttonPanel.add(Box.createHorizontalGlue()); buttonPanel.add(clearButton); - buttonPanel.add(saveButton); buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + + add(Box.createVerticalStrut(4)); add(buttonPanel); solutionSelect.addActionListener(new ActionListener() { @@ -228,7 +212,7 @@ public class SolutionPanel extends JPanel implements DrawingChangeListener, Grap } updateInformationLabel(bundle); - buttonPanel.setVisible(bundle.getSolution().isFeasible()); + buttonPanel.setVisible(bundle.getSolution().isFeasible() && bundle.hasOverlays()); clearButton.setText(bundle.getSolution().isFeasible() ? "Hide" : "Show"); for (PathOverlay overlay: bundle.getOverlays()) { @@ -241,8 +225,19 @@ public class SolutionPanel extends JPanel implements DrawingChangeListener, Grap } - public void addSolution(ShortestPathSolution solution) { - SolutionBundle bundle = new SolutionBundle(solution); + public void addSolution(AbstractSolution solution) { + addSolution(solution, true); + } + + /** + * Add the given solution to the panel. + * + * @param solution the solution to add to the panel + * @param createOverlays Whether or not overlay should be created for this + * solution. + */ + public void addSolution(AbstractSolution solution, boolean createOverlays) { + SolutionBundle bundle = new SolutionBundle(solution, createOverlays); solutionSelect.addItem(bundle); solutionSelect.setSelectedItem(bundle); }