Switch to Maven project.

This commit is contained in:
Mikaël Capelle
2020-02-23 16:30:06 +01:00
parent fd503d148e
commit 5bb454a3b2
139 changed files with 2487 additions and 2198 deletions

View File

@@ -0,0 +1,381 @@
package org.insa.graphs.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import org.insa.graphs.algorithm.AbstractAlgorithm;
import org.insa.graphs.algorithm.AlgorithmFactory;
import org.insa.graphs.algorithm.ArcInspector;
import org.insa.graphs.algorithm.ArcInspectorFactory;
import org.insa.graphs.gui.NodesInputPanel.InputChangedEvent;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.utils.ColorUtils;
import org.insa.graphs.model.Node;
public class AlgorithmPanel extends JPanel implements DrawingChangeListener {
/**
*
*/
private static final long serialVersionUID = 1L;
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 List<Node> nodes;
private final Class<? extends AbstractAlgorithm<?>> algoClass;
private final ArcInspector arcFilter;
private final boolean graphicVisualization;
private final boolean textualVisualization;
public StartActionEvent(Class<? extends AbstractAlgorithm<?>> algoClass, List<Node> nodes,
ArcInspector arcFilter, boolean graphicVisualization,
boolean textualVisualization) {
super(AlgorithmPanel.this, START_EVENT_ID, START_EVENT_COMMAND);
this.nodes = nodes;
this.algoClass = algoClass;
this.graphicVisualization = graphicVisualization;
this.textualVisualization = textualVisualization;
this.arcFilter = arcFilter;
}
/**
* @return Nodes associated with this event.
*/
public List<Node> getNodes() {
return this.nodes;
}
/**
* @return Arc filter associated with this event.
*/
public ArcInspector getArcFilter() {
return this.arcFilter;
}
/**
* @return Algorithm class associated with this event.
*/
public Class<? extends AbstractAlgorithm<?>> getAlgorithmClass() {
return this.algoClass;
}
/**
* @return true if graphic visualization is enabled.
*/
public boolean isGraphicVisualizationEnabled() {
return this.graphicVisualization;
}
/**
* @return true if textual visualization is enabled.
*/
public boolean isTextualVisualizationEnabled() {
return this.textualVisualization;
}
};
// Input panels for node.
protected NodesInputPanel nodesInputPanel;
// Solution
protected SolutionPanel solutionPanel;
// Component that can be enabled/disabled.
private ArrayList<JComponent> components = new ArrayList<>();
// Graphic / Text checkbox observer
private final JCheckBox graphicObserverCheckbox, textualObserverCheckbox;
private JButton startAlgoButton;
// Start listeners
List<ActionListener> startActionListeners = new ArrayList<>();
/**
* Create a new AlgorithmPanel with the given parameters.
*
* @param parent Parent component for this panel. Only use for centering
* dialogs.
* @param baseAlgorithm Base algorithm for this algorithm panel.
* @param title Title of the panel.
* @param nodeNames Names of the input nodes.
* @param enableArcFilterSelection <code>true</code> to enable
* {@link ArcInspector} selection.
*
* @see ArcInspectorFactory
*/
public AlgorithmPanel(Component parent, Class<? extends AbstractAlgorithm<?>> baseAlgorithm,
String title, String[] nodeNames, boolean enableArcFilterSelection) {
super();
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
setBorder(new EmptyBorder(15, 15, 15, 15));
// Set title.
add(createTitleLabel(title));
add(Box.createVerticalStrut(8));
// Add algorithm selection
JComboBox<String> algoSelect = createAlgoritmSelectComboBox(baseAlgorithm);
if (algoSelect.getItemCount() > 1) {
add(algoSelect);
components.add(algoSelect);
}
// Add inputs for node.
this.nodesInputPanel = createNodesInputPanel(nodeNames);
add(this.nodesInputPanel);
components.add(this.nodesInputPanel);
JComboBox<ArcInspector> arcFilterSelect = new JComboBox<>(
ArcInspectorFactory.getAllFilters().toArray(new ArcInspector[0]));
arcFilterSelect.setBackground(Color.WHITE);
// Add mode selection
JPanel modeAndObserverPanel = new JPanel();
modeAndObserverPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
modeAndObserverPanel.setLayout(new GridBagLayout());
graphicObserverCheckbox = new JCheckBox("Graphic");
graphicObserverCheckbox.setSelected(true);
textualObserverCheckbox = new JCheckBox("Textual");
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.HORIZONTAL;
c.gridy = 2;
c.gridx = 0;
c.weightx = 0;
modeAndObserverPanel.add(new JLabel("Visualization: "), c);
c.gridx = 1;
c.weightx = 1;
modeAndObserverPanel.add(graphicObserverCheckbox, c);
c.gridx = 2;
c.weightx = 1;
modeAndObserverPanel.add(textualObserverCheckbox, c);
if (enableArcFilterSelection) {
c.gridy = 1;
c.gridx = 0;
c.weightx = 0;
modeAndObserverPanel.add(new JLabel("Mode: "), c);
c.gridx = 1;
c.gridwidth = 2;
c.weightx = 1;
modeAndObserverPanel.add(arcFilterSelect, c);
}
components.add(arcFilterSelect);
components.add(textualObserverCheckbox);
add(modeAndObserverPanel);
solutionPanel = new SolutionPanel(parent);
solutionPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
solutionPanel.setVisible(false);
add(Box.createVerticalStrut(10));
add(solutionPanel);
// Bottom panel
JPanel bottomPanel = new JPanel();
bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.LINE_AXIS));
startAlgoButton = new JButton("Start");
startAlgoButton.setEnabled(false);
startAlgoButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (ActionListener lis: startActionListeners) {
lis.actionPerformed(new StartActionEvent(
AlgorithmFactory.getAlgorithmClass(baseAlgorithm,
(String) algoSelect.getSelectedItem()),
nodesInputPanel.getNodeForInputs(),
(ArcInspector) arcFilterSelect.getSelectedItem(),
graphicObserverCheckbox.isSelected(),
textualObserverCheckbox.isSelected()));
}
}
});
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(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;
startAlgoButton.setEnabled(allNotNull(evt.getNodes()));
}
});
addComponentListener(new ComponentAdapter() {
@Override
public void componentShown(ComponentEvent e) {
setEnabled(true);
nodesInputPanel.setVisible(true);
}
@Override
public void componentHidden(ComponentEvent e) {
setEnabled(false);
nodesInputPanel.setVisible(false);
}
});
setEnabled(false);
}
/**
* Create the title JLabel for this panel.
*
* @param title Title for the label.
*
* @return A new JLabel containing the given title with proper font.
*/
protected JLabel createTitleLabel(String title) {
JLabel titleLabel = new JLabel(title);
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);
return titleLabel;
}
/**
* Create the combo box for the algorithm selection.
*
* @param baseAlgorithm Base algorithm for which the select box should be
* created.
*
* @return A new JComboBox containing algorithms for the given base algorithm.
*
* @see AlgorithmFactory
*/
protected JComboBox<String> createAlgoritmSelectComboBox(
Class<? extends AbstractAlgorithm<?>> baseAlgorithm) {
JComboBox<String> algoSelect = new JComboBox<>(
AlgorithmFactory.getAlgorithmNames(baseAlgorithm).toArray(new String[0]));
algoSelect.setBackground(Color.WHITE);
algoSelect.setAlignmentX(Component.LEFT_ALIGNMENT);
return algoSelect;
}
/**
* Create a node input panel with the given node input names.
*
* @param nodeNames Field names for the inputs to create.
*
* @return A new NodesInputPanel containing inputs for the given names.
*/
protected NodesInputPanel createNodesInputPanel(String[] nodeNames) {
NodesInputPanel panel = new NodesInputPanel();
panel.setAlignmentX(Component.LEFT_ALIGNMENT);
for (int i = 0; i < nodeNames.length; ++i) {
panel.addTextField(nodeNames[i] + ": ", ColorUtils.getColor(i));
}
panel.setEnabled(false);
return panel;
}
/**
* Check if the given list of nodes does not contain any <code>null</code>
* value.
*
* @param nodes List of {@link Node} to check.
*
* @return <code>true</code> if the list does not contain any <code>null</code>
* value, <code>false</code> otherwise.
*/
protected boolean allNotNull(List<Node> nodes) {
boolean allNotNull = true;
for (Node node: nodes) {
allNotNull = allNotNull && node != null;
}
return allNotNull;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
nodesInputPanel.setEnabled(enabled);
solutionPanel.setEnabled(enabled);
for (JComponent component: components) {
component.setEnabled(enabled);
}
graphicObserverCheckbox.setEnabled(enabled);
enabled = enabled && allNotNull(this.nodesInputPanel.getNodeForInputs());
startAlgoButton.setEnabled(enabled);
}
/**
* Add a new start action listener to this class.
*
* @param listener Listener to add.
*/
public void addStartActionListener(ActionListener listener) {
this.startActionListeners.add(listener);
}
@Override
public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) {
}
@Override
public void onRedrawRequest() {
}
}

View File

@@ -0,0 +1,56 @@
package org.insa.graphs.gui;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.JOptionPane;
public class BlockingActionFactory {
// List of running actions.
private ArrayList<RunningAction> actions = new ArrayList<>();
// Parent component.
private Component parentComponent;
public BlockingActionFactory(Component parentComponent) {
this.parentComponent = parentComponent;
}
public void addAction(RunningAction action) {
actions.add(action);
}
public ActionListener createBlockingAction(ActionListener listener) {
return new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
boolean accepted = true;
// Check if actions...
for (int i = 0; i < actions.size() && accepted; ++i) {
RunningAction action = actions.get(i);
// If action is running, ask user...
if (action.isRunning()) {
if (JOptionPane.showConfirmDialog(parentComponent, "Action {"
+ action.getInformation()
+ "} is running, do you want to stop it?") == JOptionPane.OK_OPTION) {
action.interrupt();
}
else {
accepted = false;
}
}
}
// If action is accepted, run it...
if (accepted) {
listener.actionPerformed(e);
}
}
};
}
}

View File

@@ -0,0 +1,24 @@
package org.insa.graphs.gui;
import org.insa.graphs.gui.drawing.Drawing;
public interface DrawingChangeListener {
/**
* Event fired when a new drawing is loaded.
*
* @param oldDrawing Old drawing, may be null if no drawing exits prior to this
* one.
* @param newDrawing New drawing.
*/
public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing);
/**
* Event fired when a redraw request is emitted - This is typically emitted
* after a onDrawingLoaded event, but not always, and request that elements are
* drawn again on the new drawing.
*
*/
public void onRedrawRequest();
}

View File

@@ -0,0 +1,14 @@
package org.insa.graphs.gui;
import org.insa.graphs.model.Graph;
public interface GraphChangeListener {
/**
* Event fire when a new graph has been loaded.
*
* @param graph The new graph.
*/
public void newGraphLoaded(Graph graph);
}

View File

@@ -0,0 +1,123 @@
package org.insa.graphs.gui;
import java.awt.Component;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.border.EmptyBorder;
import org.insa.graphs.model.Arc;
import org.insa.graphs.model.Node;
import org.insa.graphs.model.RoadInformation;
import org.insa.graphs.model.io.GraphReaderObserver;
/**
* One-time use GraphReaderObserver that display progress in three different
* JProgressBar.
*
* @author Mikael
*
*/
public class GraphReaderProgressBar extends JDialog implements GraphReaderObserver {
/**
*
*/
private static final long serialVersionUID = -1;
// Index...
private static final int NODE = 0, DESC = 1, ARC = 2;
// Progress bar
private final JProgressBar[] progressBars = new JProgressBar[3];
// Current element read, and modulo.
private int[] counters = new int[]{ 0, 0, 0 };
private int[] modulos = new int[3];
public GraphReaderProgressBar(JFrame owner) {
super(owner);
this.setVisible(false);
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
final String[] infos = { "nodes", "road informations", "arcs" };
JPanel pane = new JPanel();
pane.setLayout(new BoxLayout(pane, BoxLayout.PAGE_AXIS));
pane.setBorder(new EmptyBorder(15, 15, 15, 15));
pane.add(Box.createVerticalGlue());
for (int i = 0; i < 3; ++i) {
JLabel label = new JLabel("Reading " + infos[i] + "... ");
label.setAlignmentX(Component.LEFT_ALIGNMENT);
progressBars[i] = new JProgressBar();
progressBars[i].setAlignmentX(Component.LEFT_ALIGNMENT);
pane.add(label);
pane.add(progressBars[i]);
}
pane.add(Box.createVerticalGlue());
setContentPane(pane);
pack();
}
@Override
public void notifyStartReading(String mapId) {
setTitle("Reading graph " + mapId + "... ");
setVisible(true);
}
@Override
public void notifyEndReading() {
setVisible(false);
dispose();
}
protected void initProgressBar(int index, int max) {
progressBars[index].setMaximum(max);
modulos[index] = Math.max(max / 100, 1);
}
protected void incCounter(int index) {
counters[index] += 1;
if (counters[index] % modulos[index] == 0) {
progressBars[index].setValue(counters[index]);
}
}
@Override
public void notifyStartReadingNodes(int nNodes) {
initProgressBar(NODE, nNodes);
}
@Override
public void notifyNewNodeRead(Node node) {
incCounter(NODE);
}
@Override
public void notifyStartReadingDescriptors(int nDesc) {
initProgressBar(DESC, nDesc);
}
@Override
public void notifyNewDescriptorRead(RoadInformation desc) {
incCounter(DESC);
}
@Override
public void notifyStartReadingArcs(int nArcs) {
initProgressBar(ARC, nArcs);
}
@Override
public void notifyNewArcRead(Arc arc) {
incCounter(ARC);
}
}

View File

@@ -0,0 +1,862 @@
package org.insa.graphs.gui;
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;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import org.insa.graphs.algorithm.AbstractSolution;
import org.insa.graphs.algorithm.AlgorithmFactory;
import org.insa.graphs.algorithm.carpooling.CarPoolingAlgorithm;
import org.insa.graphs.algorithm.packageswitch.PackageSwitchAlgorithm;
import org.insa.graphs.algorithm.shortestpath.ShortestPathAlgorithm;
import org.insa.graphs.algorithm.shortestpath.ShortestPathData;
import org.insa.graphs.algorithm.shortestpath.ShortestPathSolution;
import org.insa.graphs.algorithm.shortestpath.ShortestPathTextObserver;
import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentTextObserver;
import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentsAlgorithm;
import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentsData;
import org.insa.graphs.gui.AlgorithmPanel.StartActionEvent;
import org.insa.graphs.gui.drawing.BasicGraphPalette;
import org.insa.graphs.gui.drawing.BlackAndWhiteGraphPalette;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.GraphPalette;
import org.insa.graphs.gui.drawing.components.BasicDrawing;
import org.insa.graphs.gui.drawing.components.MapViewDrawing;
import org.insa.graphs.gui.observers.ShortestPathGraphicObserver;
import org.insa.graphs.gui.observers.WeaklyConnectedComponentGraphicObserver;
import org.insa.graphs.gui.utils.FileUtils;
import org.insa.graphs.gui.utils.FileUtils.FolderType;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Path;
import org.insa.graphs.model.io.BinaryGraphReader;
import org.insa.graphs.model.io.BinaryPathReader;
import org.insa.graphs.model.io.GraphReader;
import org.insa.graphs.model.io.MapMismatchException;
public class MainWindow extends JFrame {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
*
*/
private static final String WINDOW_TITLE = "BE Graphes INSA";
/**
*
*/
private static final int THREAD_TIMER_DELAY = 1000; // in milliseconds
// Current graph.
protected Graph graph;
// Path to the last opened graph file.
private String graphFilePath;
// Drawing and click adapter.
protected Drawing drawing;
private final MapViewDrawing mapViewDrawing;
private final BasicDrawing basicDrawing;
private final GraphPalette basicPalette, blackAndWhitePalette;
private GraphPalette currentPalette;
// Main panel.
private final JSplitPane mainPanel;
// Algorithm panels
private final List<AlgorithmPanel> algoPanels = new ArrayList<>();
private final AlgorithmPanel wccPanel, spPanel, cpPanel, psPanel;
// Path panel
private final PathsPanel pathPanel;
// List of items that cannot be used without a graph
private final ArrayList<JMenuItem> graphLockItems = new ArrayList<JMenuItem>();
// Label containing the map ID of the current graph.
private JLabel graphInfoPanel;
// Thread information
private Timer threadTimer;
private JPanel threadPanel;
// Log stream and print stream
private StreamCapturer logStream;
private PrintStream printStream;
// Current running thread
private ThreadWrapper currentThread;
// Factory
private BlockingActionFactory baf;
// Observers
private List<DrawingChangeListener> drawingChangeListeners = new ArrayList<>();
private List<GraphChangeListener> graphChangeListeneres = new ArrayList<>();
public MainWindow() {
super(WINDOW_TITLE);
setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
setLayout(new BorderLayout());
setMinimumSize(new Dimension(800, 600));
// Create drawing and action listeners...
this.basicDrawing = new BasicDrawing();
this.mapViewDrawing = new MapViewDrawing();
this.drawing = basicDrawing;
// Createa palettes
this.basicPalette = new BasicGraphPalette();
this.blackAndWhitePalette = new BlackAndWhiteGraphPalette();
this.currentPalette = this.basicPalette;
wccPanel = new AlgorithmPanel(this, WeaklyConnectedComponentsAlgorithm.class,
"Weakly-Connected Components", new String[] {}, false);
wccPanel.addStartActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
StartActionEvent evt = (StartActionEvent) e;
WeaklyConnectedComponentsData data = new WeaklyConnectedComponentsData(graph);
WeaklyConnectedComponentsAlgorithm wccAlgorithm = null;
try {
wccAlgorithm = (WeaklyConnectedComponentsAlgorithm) AlgorithmFactory
.createAlgorithm(evt.getAlgorithmClass(), data);
}
catch (Exception e1) {
JOptionPane.showMessageDialog(MainWindow.this,
"An error occurred while creating the specified algorithm.",
"Internal error: Algorithm instantiation failure",
JOptionPane.ERROR_MESSAGE);
e1.printStackTrace();
return;
}
wccPanel.setEnabled(false);
if (evt.isGraphicVisualizationEnabled()) {
wccAlgorithm.addObserver(new WeaklyConnectedComponentGraphicObserver(drawing));
}
if (evt.isTextualVisualizationEnabled()) {
wccAlgorithm.addObserver(new WeaklyConnectedComponentTextObserver(printStream));
}
// We love Java...
final WeaklyConnectedComponentsAlgorithm copyAlgorithm = wccAlgorithm;
launchThread(new Runnable() {
@Override
public void run() {
AbstractSolution solution = copyAlgorithm.run();
wccPanel.solutionPanel.addSolution(solution, false);
wccPanel.solutionPanel.setVisible(true);
wccPanel.setEnabled(true);
}
});
}
});
spPanel = new AlgorithmPanel(this, ShortestPathAlgorithm.class, "Shortest-Path",
new String[] { "Origin", "Destination" }, true);
spPanel.addStartActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
StartActionEvent evt = (StartActionEvent) e;
ShortestPathData data = new ShortestPathData(graph, evt.getNodes().get(0),
evt.getNodes().get(1), evt.getArcFilter());
ShortestPathAlgorithm spAlgorithm = null;
try {
spAlgorithm = (ShortestPathAlgorithm) AlgorithmFactory
.createAlgorithm(evt.getAlgorithmClass(), data);
}
catch (Exception e1) {
JOptionPane.showMessageDialog(MainWindow.this,
"An error occurred while creating the specified algorithm.",
"Internal error: Algorithm instantiation failure",
JOptionPane.ERROR_MESSAGE);
e1.printStackTrace();
return;
}
spPanel.setEnabled(false);
if (evt.isGraphicVisualizationEnabled()) {
spAlgorithm.addObserver(new ShortestPathGraphicObserver(drawing));
}
if (evt.isTextualVisualizationEnabled()) {
spAlgorithm.addObserver(new ShortestPathTextObserver(printStream));
}
final ShortestPathAlgorithm copyAlgorithm = spAlgorithm;
launchThread(new Runnable() {
@Override
public void run() {
// Run the algorithm.
ShortestPathSolution solution = copyAlgorithm.run();
// Add the solution to the solution panel (but do not display
// overlay).
spPanel.solutionPanel.addSolution(solution, false);
// If the solution is feasible, add the path to the path panel.
if (solution.isFeasible()) {
pathPanel.addPath(solution.getPath());
}
// Show the solution panel and enable the shortest-path panel.
spPanel.solutionPanel.setVisible(true);
spPanel.setEnabled(true);
}
});
}
});
cpPanel = new AlgorithmPanel(this, CarPoolingAlgorithm.class, "Car-Pooling", new String[] {
"Origin Car", "Origin Pedestrian", "Destination Car", "Destination Pedestrian" },
true);
psPanel = new AlgorithmPanel(this, PackageSwitchAlgorithm.class, "Car-Pooling",
new String[] { "Oribin A", "Origin B", "Destination A", "Destination B" }, true);
// add algorithm panels
algoPanels.add(wccPanel);
algoPanels.add(spPanel);
algoPanels.add(cpPanel);
algoPanels.add(psPanel);
this.pathPanel = new PathsPanel(this);
// Add click listeners to both drawing.
for (AlgorithmPanel panel: algoPanels) {
this.basicDrawing.addDrawingClickListener(panel.nodesInputPanel);
this.mapViewDrawing.addDrawingClickListener(panel.nodesInputPanel);
this.graphChangeListeneres.add(panel.nodesInputPanel);
this.graphChangeListeneres.add(panel.solutionPanel);
this.drawingChangeListeners.add(panel.nodesInputPanel);
this.drawingChangeListeners.add(panel.solutionPanel);
this.drawingChangeListeners.add(panel);
}
this.graphChangeListeneres.add(pathPanel);
this.drawingChangeListeners.add(pathPanel);
// Create action factory.
this.currentThread = new ThreadWrapper(this);
this.baf = new BlockingActionFactory(this);
this.baf.addAction(currentThread);
// Click adapter
ActionListener openMapActionListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JFileChooser chooser = FileUtils.createFileChooser(FolderType.Map);
if (chooser.showOpenDialog(MainWindow.this) == JFileChooser.APPROVE_OPTION) {
graphFilePath = chooser.getSelectedFile().getAbsolutePath();
// Note: Don't use a try-resources block since loadGraph is asynchronous.
final DataInputStream stream;
try {
stream = new DataInputStream(new BufferedInputStream(
new FileInputStream(chooser.getSelectedFile())));
}
catch (IOException e1) {
JOptionPane.showMessageDialog(MainWindow.this,
"Cannot open the selected file.");
return;
}
loadGraph(new BinaryGraphReader(stream));
}
}
};
setJMenuBar(createMenuBar(openMapActionListener));
// Initial panel to show "Open Map... "
JPanel openPanel = new JPanel();
openPanel.setLayout(new BoxLayout(openPanel, BoxLayout.PAGE_AXIS));
JButton openButton = new JButton("Open Map... ");
openButton.setAlignmentX(Component.CENTER_ALIGNMENT);
openButton.addActionListener(openMapActionListener);
openButton.setFocusPainted(false);
openPanel.add(Box.createVerticalGlue());
openPanel.add(openButton);
openPanel.add(Box.createVerticalGlue());
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
int confirmed = JOptionPane.showConfirmDialog(MainWindow.this,
"Are you sure you want to close the application?", "Exit Confirmation",
JOptionPane.YES_NO_OPTION);
if (confirmed == JOptionPane.YES_OPTION) {
dispose();
System.exit(0);
}
}
});
// Create graph area
mainPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
JTextArea infoPanel = new JTextArea();
infoPanel.setMinimumSize(new Dimension(200, 50));
infoPanel.setBackground(Color.WHITE);
infoPanel.setLineWrap(true);
infoPanel.setEditable(false);
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 = 0;
c.fill = GridBagConstraints.HORIZONTAL;
rightComponent.add(pathPanel, c);
c.gridy = 1;
for (AlgorithmPanel panel: algoPanels) {
panel.setVisible(false);
rightComponent.add(panel, c);
}
c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 2;
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(openPanel);
mainPanel.setRightComponent(rightComponent);
this.add(mainPanel, BorderLayout.CENTER);
// Top Panel
this.add(createStatusBar(), BorderLayout.SOUTH);
// Notify everythin
notifyDrawingLoaded(null, drawing);
}
/**
* @param runnable
* @param canInterrupt
*/
private void launchThread(Runnable runnable, boolean canInterrupt) {
if (canInterrupt) {
currentThread.setThread(new Thread(new Runnable() {
@Override
public void run() {
threadTimer.restart();
threadPanel.setVisible(true);
runnable.run();
clearCurrentThread();
}
}));
}
else {
currentThread.setThread(new Thread(runnable));
}
currentThread.startThread();
}
private void launchThread(Runnable runnable) {
launchThread(runnable, true);
}
protected void clearCurrentThread() {
threadTimer.stop();
threadPanel.setVisible(false);
currentThread.setThread(null);
if (spPanel.isVisible()) {
spPanel.setEnabled(true);
}
}
/**
* Notify all listeners that a new graph has been loaded.
*/
private void notifyNewGraphLoaded() {
for (GraphChangeListener listener: graphChangeListeneres) {
listener.newGraphLoaded(graph);
}
}
/**
* Notify all listeners that a new drawing has been set up.
*
* @param oldDrawing
* @param newDrawing
*/
private void notifyDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) {
for (DrawingChangeListener listener: drawingChangeListeners) {
listener.onDrawingLoaded(oldDrawing, newDrawing);
}
}
/**
* Notify all listeners that a redraw request is emitted.
*/
private void notifyRedrawRequest() {
for (DrawingChangeListener listener: drawingChangeListeners) {
listener.onRedrawRequest();
}
}
/**
* Draw the stored graph on the drawing.
*/
private void drawGraph(Class<? extends Drawing> newClass, GraphPalette palette) {
// Save old divider location
int oldLocation = mainPanel.getDividerLocation();
// Set drawing if not set
if (!(mainPanel.getLeftComponent() instanceof Drawing)) {
mainPanel.setLeftComponent((Component) this.drawing);
mainPanel.setDividerLocation(oldLocation);
// Need to re-validate or the drawing will not have the
// correct size prior to drawing, which can cause issue.
this.revalidate();
}
boolean isNewGraph = newClass == null;
boolean isMapView = (isNewGraph && drawing == mapViewDrawing)
|| (!isNewGraph && newClass.equals(MapViewDrawing.class));
// We need to draw MapView, we have to check if the file exists.
File mfile = null;
if (isMapView) {
String mfpath = graphFilePath.substring(0, graphFilePath.lastIndexOf(".map"))
+ ".mapfg";
mfile = new File(mfpath);
if (!mfile.exists()) {
if (JOptionPane.showConfirmDialog(this,
"The associated mapsforge (.mapfg) file has not been found, do you want to specify it manually?",
"File not found",
JOptionPane.YES_NO_CANCEL_OPTION) == JOptionPane.YES_OPTION) {
JFileChooser chooser = new JFileChooser(mfile.getParentFile());
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
mfile = chooser.getSelectedFile();
}
else {
mfile = null;
}
}
else {
mfile = null;
}
}
}
Runnable runnable = null;
if (isMapView && mfile != null) {
final File mfileFinal = mfile;
// It is a mapview drawing and the file was found, so:
// 1. We create the drawing if necessary.
if (drawing != mapViewDrawing) {
drawing.clear();
drawing = mapViewDrawing;
mainPanel.setLeftComponent(mapViewDrawing);
mainPanel.setDividerLocation(oldLocation);
notifyDrawingLoaded(basicDrawing, mapViewDrawing);
drawing.clear();
isNewGraph = true;
mainPanel.revalidate();
}
if (isNewGraph) {
drawing.clear();
runnable = new Runnable() {
public void run() {
((MapViewDrawing) drawing).drawGraph(mfileFinal);
notifyRedrawRequest();
}
};
}
}
else if (!isMapView || (isMapView && mfile == null && isNewGraph)) {
if (drawing == mapViewDrawing) {
mapViewDrawing.clear();
drawing = basicDrawing;
mainPanel.setLeftComponent(basicDrawing);
mainPanel.setDividerLocation(oldLocation);
notifyDrawingLoaded(mapViewDrawing, basicDrawing);
isNewGraph = true;
}
if (isNewGraph || palette != this.currentPalette) {
this.currentPalette = palette;
drawing.clear();
runnable = new Runnable() {
public void run() {
drawing.drawGraph(graph, palette);
notifyRedrawRequest();
}
};
}
}
if (runnable != null) {
launchThread(runnable, false);
}
else {
drawing.clearOverlays();
notifyRedrawRequest();
}
}
/**
* @param newClass
*/
private void drawGraph(Class<? extends Drawing> newClass) {
drawGraph(newClass, new BasicGraphPalette());
}
/**
*
*/
private void drawGraph() {
drawGraph(null, this.currentPalette);
}
private void loadGraph(GraphReader reader) {
launchThread(new Runnable() {
@Override
public void run() {
GraphReaderProgressBar progressBar = new GraphReaderProgressBar(MainWindow.this);
progressBar.setLocationRelativeTo(mainPanel.getLeftComponent());
reader.addObserver(progressBar);
try {
graph = reader.read();
reader.close();
}
catch (Exception exception) {
progressBar.setVisible(false);
progressBar.dispose();
progressBar = null;
JOptionPane.showMessageDialog(MainWindow.this,
"<html><p>Unable to read graph from the selected file:</p><p>"
+ exception.getMessage() + "</p>");
exception.printStackTrace(System.out);
return;
}
// In case of....
progressBar.setVisible(false);
progressBar.dispose();
progressBar = null;
String info = graph.getMapId();
if (graph.getMapName() != null && !graph.getMapName().isEmpty()) {
// The \u200e character is the left-to-right mark, we need to avoid issue with
// name that are right-to-left (e.g. arabic names).
info += " - " + graph.getMapName() + "\u200e";
}
info += ", " + graph.size() + " nodes, " + graph.getGraphInformation().getArcCount()
+ " arcs.";
graphInfoPanel.setText(info);
drawGraph();
notifyNewGraphLoaded();
for (JMenuItem item: graphLockItems) {
item.setEnabled(true);
}
}
}, false);
}
/**
* Show and enable the given AlgorithmPanel (and hide all others).
*
* @param algorithmPanel
*/
private void enableAlgorithmPanel(AlgorithmPanel algorithmPanel) {
int dividerLocation = mainPanel.getDividerLocation();
for (AlgorithmPanel panel: algoPanels) {
panel.setVisible(panel == algorithmPanel);
}
mainPanel.setDividerLocation(dividerLocation);
}
private JMenuBar createMenuBar(ActionListener openMapActionListener) {
// Open Map item...
JMenuItem openMapItem = new JMenuItem("Open Map... ", KeyEvent.VK_O);
openMapItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.ALT_MASK));
openMapItem.addActionListener(baf.createBlockingAction(openMapActionListener));
// Open Path item...
JMenuItem openPathItem = new JMenuItem("Open Path... ", KeyEvent.VK_P);
openPathItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.ALT_MASK));
openPathItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JFileChooser chooser = FileUtils.createFileChooser(FolderType.PathInput);
if (chooser.showOpenDialog(MainWindow.this) == JFileChooser.APPROVE_OPTION) {
try (BinaryPathReader reader = new BinaryPathReader(new DataInputStream(new BufferedInputStream(
new FileInputStream(chooser.getSelectedFile()))))){
Path path = reader.readPath(graph);
pathPanel.addPath(path);
}
catch (MapMismatchException exception) {
JOptionPane.showMessageDialog(MainWindow.this,
"The selected file does not contain a path for the current graph.");
}
catch (IOException e1) {
JOptionPane.showMessageDialog(MainWindow.this,
"Cannot open the selected file.");
}
catch (Exception exception) {
JOptionPane.showMessageDialog(MainWindow.this,
"Unable to read path from the selected file.");
}
}
}
}));
graphLockItems.add(openPathItem);
// Close item
JMenuItem closeItem = new JMenuItem("Quit", KeyEvent.VK_Q);
closeItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.ALT_MASK));
closeItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
MainWindow.this.dispatchEvent(
new WindowEvent(MainWindow.this, WindowEvent.WINDOW_CLOSING));
}
});
// Build the first menu.
JMenu fileMenu = new JMenu("File");
fileMenu.add(openMapItem);
fileMenu.add(openPathItem);
fileMenu.addSeparator();
fileMenu.add(closeItem);
// Second menu
JMenuItem drawGraphItem = new JMenuItem("Redraw", KeyEvent.VK_R);
drawGraphItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, ActionEvent.ALT_MASK));
drawGraphItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
drawGraph(BasicDrawing.class, basicPalette);
}
}));
graphLockItems.add(drawGraphItem);
JMenuItem drawGraphBWItem = new JMenuItem("Redraw (B&W)", KeyEvent.VK_B);
drawGraphBWItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, ActionEvent.ALT_MASK));
drawGraphBWItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
drawGraph(BasicDrawing.class, blackAndWhitePalette);
}
}));
graphLockItems.add(drawGraphBWItem);
JMenuItem drawGraphMapsforgeItem = new JMenuItem("Redraw (Map)", KeyEvent.VK_M);
drawGraphMapsforgeItem
.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, ActionEvent.ALT_MASK));
drawGraphMapsforgeItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
drawGraph(MapViewDrawing.class);
}
}));
graphLockItems.add(drawGraphMapsforgeItem);
JMenu graphMenu = new JMenu("Graph");
graphMenu.add(drawGraphItem);
graphMenu.add(drawGraphBWItem);
graphMenu.addSeparator();
graphMenu.add(drawGraphMapsforgeItem);
// Algo menu
JMenu algoMenu = new JMenu("Algorithms");
// Weakly connected components
JMenuItem wccItem = new JMenuItem("Weakly Connected Components");
wccItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
enableAlgorithmPanel(wccPanel);
}
}));
// Shortest path
JMenuItem spItem = new JMenuItem("Shortest-Path");
spItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
enableAlgorithmPanel(spPanel);
}
}));
// Car pooling
JMenuItem cpItem = new JMenuItem("Car Pooling");
cpItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
enableAlgorithmPanel(cpPanel);
}
}));
// Car pooling
JMenuItem psItem = new JMenuItem("Package Switch");
psItem.addActionListener(baf.createBlockingAction(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
enableAlgorithmPanel(psPanel);
}
}));
graphLockItems.add(wccItem);
graphLockItems.add(spItem);
graphLockItems.add(cpItem);
graphLockItems.add(psItem);
algoMenu.add(wccItem);
algoMenu.addSeparator();
algoMenu.add(spItem);
algoMenu.add(cpItem);
algoMenu.add(psItem);
// Create the menu bar.
JMenuBar menuBar = new JMenuBar();
menuBar.add(fileMenu);
menuBar.add(graphMenu);
menuBar.add(algoMenu);
for (JMenuItem item: graphLockItems) {
item.setEnabled(false);
}
return menuBar;
}
private JPanel createStatusBar() {
// create the status bar panel and shove it down the bottom of the frame
JPanel statusPanel = new JPanel();
statusPanel.setBorder(
new CompoundBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY),
new EmptyBorder(0, 15, 0, 15)));
statusPanel.setPreferredSize(new Dimension(getWidth(), 38));
statusPanel.setLayout(new BorderLayout());
graphInfoPanel = new JLabel();
graphInfoPanel.setHorizontalAlignment(SwingConstants.LEFT);
statusPanel.add(graphInfoPanel, BorderLayout.WEST);
JLabel threadInfo = new JLabel("Thread running... ");
JLabel threadTimerLabel = new JLabel("00:00:00");
JButton threadButton = new JButton("Stop");
threadButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (currentThread.isRunning()) {
int confirmed = JOptionPane.showConfirmDialog(MainWindow.this,
"Are you sure you want to kill the running thread?",
"Kill Confirmation", JOptionPane.YES_NO_OPTION);
if (confirmed == JOptionPane.YES_OPTION) {
currentThread.interrupt();
}
}
}
});
threadTimer = new Timer(THREAD_TIMER_DELAY, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
long seconds = currentThread.getDuration().getSeconds();
threadTimerLabel.setText(String.format("%02d:%02d:%02d", seconds / 3600,
seconds / 60 % 60, seconds % 60));
}
});
threadTimer.setInitialDelay(0);
threadPanel = new JPanel();
threadPanel.add(threadInfo);
threadPanel.add(threadTimerLabel);
threadPanel.add(threadButton);
threadPanel.setVisible(false);
statusPanel.add(threadPanel, BorderLayout.EAST);
return statusPanel;
}
public static void main(final String[] args) {
// Try to set system look and feel.
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch (Exception e) {
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
MainWindow w = new MainWindow();
w.setExtendedState(JFrame.MAXIMIZED_BOTH);
w.setVisible(true);
}
});
}
}

View File

@@ -0,0 +1,424 @@
package org.insa.graphs.gui;
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.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.Drawing.AlphaMode;
import org.insa.graphs.gui.drawing.DrawingClickListener;
import org.insa.graphs.gui.drawing.overlays.MarkerOverlay;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Node;
import org.insa.graphs.model.Point;
public class NodesInputPanel extends JPanel
implements DrawingClickListener, DrawingChangeListener, GraphChangeListener {
/**
*
*/
private static final long serialVersionUID = 1L;
private static final Color DEFAULT_MARKER_COLOR = Color.BLUE;
/**
* Utility class that can be used to find a node from coordinates in a "fast"
* way.
*
*/
private static class NodeFinder {
// Graph associated with this node finder.
private Graph graph;
/**
* @param graph
*/
public NodeFinder(Graph graph) {
this.graph = graph;
}
/**
* @param point
*
* @return the closest node to the given point, or null if no node is "close
* enough".
*/
public Node findClosestNode(Point point) {
Node minNode = null;
double minDis = Double.POSITIVE_INFINITY;
for (Node node: graph.getNodes()) {
double dlon = point.getLongitude() - node.getPoint().getLongitude();
double dlat = point.getLatitude() - node.getPoint().getLatitude();
double dis = dlon * dlon + dlat * dlat; // No need to square
if (dis < minDis) {
minNode = node;
minDis = dis;
}
}
return minNode;
}
}
/**
* Event data send when a node input has changed.
*
*/
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<Node> nodes;
public InputChangedEvent(List<Node> nodes2) {
super(NodesInputPanel.this, ALL_INPUT_FILLED_EVENT_ID, ALL_INPUT_FILLED_EVENT_COMMAND);
this.nodes = nodes2;
}
List<Node> getNodes() {
return Collections.unmodifiableList(nodes);
}
};
// Node inputs and markers.
private final ArrayList<JTextField> nodeInputs = new ArrayList<>();
private final Map<JTextField, MarkerOverlay> markerTrackers = new IdentityHashMap<JTextField, MarkerOverlay>();
// Component that can be enabled/disabled.
private ArrayList<JComponent> components = new ArrayList<>();
private int inputToFillIndex;
// ActionListener called when all inputs are filled.
private ArrayList<ActionListener> inputChangeListeners = new ArrayList<>();
// Drawing and graph
private Drawing drawing;
private Graph graph;
private NodeFinder nodeFinder;
/**
* Create a new NodesInputPanel.
*
*/
public NodesInputPanel() {
super(new GridBagLayout());
initInputToFill();
}
/**
* Add an InputChanged listener to this panel. This listener will be notified by
* a {@link InputChangedEvent} each time an input in this panel change (click,
* clear, manual input).
*
* @param listener Listener to add.
*
* @see InputChangedEvent
*/
public void addInputChangedListener(ActionListener listener) {
inputChangeListeners.add(listener);
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
for (JTextField input: nodeInputs) {
MarkerOverlay marker = markerTrackers.getOrDefault(input, null);
if (marker != null) {
marker.setVisible(visible && !input.getText().trim().isEmpty());
}
}
}
@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("");
markerTrackers.put(field, null);
}
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);
MarkerOverlay tracker = markerTrackers.getOrDefault(textField, null);
if (curnode != null) {
if (tracker == null) {
tracker = drawing.drawMarker(curnode.getPoint(), markerColor, Color.BLACK,
AlphaMode.TRANSPARENT);
markerTrackers.put(textField, tracker);
}
else {
tracker.moveTo(curnode.getPoint());
}
tracker.setVisible(true);
}
else if (tracker != null) {
tracker.setVisible(false);
if (getInputToFill() == null) {
nextInputToFill();
}
}
// Create array of nodes
List<Node> 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 Current graph associated with this input panel.
*/
protected Graph getGraph() {
return this.graph;
}
/**
* @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.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<Node> getNodeForInputs() {
List<Node> 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;
if (inputToFillIndex == -1) {
inputToFillIndex = 0;
}
for (int i = 0; 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) {
JTextField input = getInputToFill();
if (input != null) {
Node node = nodeFinder.findClosestNode(point);
input.setText(String.valueOf(node.getId()));
nextInputToFill();
}
}
@Override
public void newGraphLoaded(Graph graph) {
if (graph != this.graph) {
this.clear();
this.graph = graph;
nodeFinder = new NodeFinder(graph);
// Disable if previously disabled...
setEnabled(this.isEnabled());
}
}
@Override
public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) {
if (newDrawing != drawing) {
this.drawing = newDrawing;
}
}
@Override
public void onRedrawRequest() {
for (JTextField input: nodeInputs) {
MarkerOverlay tracker = markerTrackers.getOrDefault(input, null);
if (tracker != null) {
MarkerOverlay newMarker = this.drawing.drawMarker(tracker.getPoint(),
tracker.getColor(), Color.BLACK, AlphaMode.TRANSPARENT);
markerTrackers.put(input, newMarker);
newMarker.setVisible(tracker.isVisible());
tracker.delete();
}
}
}
}

View File

@@ -0,0 +1,361 @@
package org.insa.graphs.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JColorChooser;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.overlays.PathOverlay;
import org.insa.graphs.gui.utils.ColorUtils;
import org.insa.graphs.gui.utils.FileUtils;
import org.insa.graphs.gui.utils.FileUtils.FolderType;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Path;
import org.insa.graphs.model.io.BinaryPathWriter;
public class PathsPanel extends JPanel implements DrawingChangeListener, GraphChangeListener {
/**
*
*/
private static final long serialVersionUID = 1L;
private class PathPanel extends JPanel {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* Simple icon that represents a unicolor rectangle.
*
*/
protected class ColorIcon implements Icon {
private Color color;
private int width, height;
public ColorIcon(Color color, int width, int height) {
this.width = width;
this.height = height;
this.color = color;
}
public void setColor(Color color) {
this.color = color;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
g.setColor(this.color);
g.fillRect(x, y, getIconWidth(), getIconHeight());
}
@Override
public int getIconWidth() {
return this.width;
}
@Override
public int getIconHeight() {
return this.height;
}
}
// Solution
private final Path path;
// Path Overlay (not final due to redraw)
private PathOverlay overlay;
// Color button
private final JButton colorButton;
/**
* 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.
*
* @throws IOException If a resource was not found.
*
*/
public PathPanel(Path path, Color color) throws IOException {
super();
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY),
new EmptyBorder(5, 0, 5, 0)));
this.path = path;
this.overlay = drawing.drawPath(this.path, color);
JCheckBox checkbox = new JCheckBox();
checkbox.setSelected(true);
checkbox.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
overlay.setVisible(checkbox.isSelected());
}
});
JLabel infoPanel = new JLabel();
String info = "";
// Display length
float length = path.getLength();
if (length < 2000) {
info += String.format("Length = %.1f meters", length);
}
else {
info += String.format("Length = %.3f kilometers", length / 1000.);
}
// Display time
info += ", Duration=";
double time = path.getMinimumTravelTime();
int hours = (int) (time / 3600);
int minutes = (int) (time / 60) % 60;
int seconds = ((int) time) % 60;
if (hours > 0) {
info += String.format("%d hours, ", hours);
}
if (minutes > 0) {
info += String.format("%d minutes, ", minutes);
}
info += String.format("%d seconds.", seconds);
infoPanel.setText("<html>" + toString() + "<br/>" + info + "</html>");
infoPanel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
checkbox.setSelected(!checkbox.isSelected());
overlay.setVisible(checkbox.isSelected());
}
});
Dimension size = new Dimension(24, 24);
ColorIcon icon = new ColorIcon(overlay.getColor(), 14, 14);
colorButton = new JButton(icon);
colorButton.setFocusable(false);
colorButton.setFocusPainted(false);
colorButton.setMinimumSize(size);
colorButton.setPreferredSize(size);
colorButton.setMaximumSize(size);
colorButton.setToolTipText("Pick a color");
colorButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final Color originalColor = overlay.getColor();
JColorChooser chooser = new JColorChooser(overlay.getColor());
chooser.getSelectionModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
icon.setColor(chooser.getSelectionModel().getSelectedColor());
colorButton.repaint();
overlay.setColor(chooser.getSelectionModel().getSelectedColor());
overlay.redraw();
}
});
JColorChooser.createDialog(getTopLevelAncestor(), "Pick a new color", true,
chooser, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
icon.setColor(chooser.getSelectionModel().getSelectedColor());
colorButton.repaint();
overlay.setColor(
chooser.getSelectionModel().getSelectedColor());
overlay.redraw();
}
}, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
icon.setColor(originalColor);
colorButton.repaint();
overlay.setColor(originalColor);
overlay.redraw();
}
}).setVisible(true);
;
}
});
Image saveImg = ImageIO.read(getClass().getResourceAsStream("/save-icon.png"))
.getScaledInstance(14, 14, java.awt.Image.SCALE_SMOOTH);
JButton saveButton = new JButton(new ImageIcon(saveImg));
saveButton.setFocusPainted(false);
saveButton.setFocusable(false);
saveButton.setMinimumSize(size);
saveButton.setPreferredSize(size);
saveButton.setMaximumSize(size);
saveButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String filepath = String.format("path_%s_%d_%d.path",
path.getGraph().getMapId().toLowerCase().replaceAll("[^a-z0-9_]", ""),
path.getOrigin().getId(), path.getDestination().getId());
JFileChooser chooser = FileUtils.createFileChooser(FolderType.PathOutput,
filepath);
if (chooser
.showSaveDialog(getTopLevelAncestor()) == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try (BinaryPathWriter writer = new BinaryPathWriter(new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(file))))) {
writer.writePath(path);
}
catch (IOException e1) {
JOptionPane.showMessageDialog(getTopLevelAncestor(),
"Unable to write path to the selected file.");
e1.printStackTrace();
}
}
}
});
Image newimg = ImageIO.read(getClass().getResourceAsStream("/delete-icon.png"))
.getScaledInstance(14, 14, java.awt.Image.SCALE_SMOOTH);
JButton deleteButton = new JButton(new ImageIcon(newimg));
deleteButton.setFocusPainted(false);
deleteButton.setFocusable(false);
deleteButton.setMinimumSize(size);
deleteButton.setPreferredSize(size);
deleteButton.setMaximumSize(size);
deleteButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
overlay.delete();
PathsPanel.this.removePath(PathPanel.this);
}
});
add(checkbox);
add(Box.createHorizontalStrut(5));
add(infoPanel);
add(Box.createHorizontalGlue());
add(colorButton);
add(saveButton);
add(deleteButton);
}
/**
* 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.setColor(oldOverlay.getColor());
((ColorIcon) this.colorButton.getIcon()).setColor(this.overlay.getColor());
this.colorButton.repaint();
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;
public PathsPanel(Component parent) {
super();
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
setBorder(new EmptyBorder(15, 15, 15, 15));
// Default hidden
this.setVisible(false);
}
public void addPath(Path path) {
try {
this.add(new PathPanel(path, ColorUtils.getColor(this.getComponentCount())));
this.setVisible(true);
this.revalidate();
this.repaint();
}
catch (Exception e) {
e.printStackTrace();
}
}
protected void removePath(PathPanel panel) {
PathsPanel.this.remove(panel);
PathsPanel.this.revalidate();
PathsPanel.this.repaint();
if (this.getComponentCount() == 0) {
this.setVisible(false);
}
}
@Override
public void newGraphLoaded(Graph graph) {
for (Component c: this.getComponents()) {
if (c instanceof PathPanel) {
((PathPanel) c).overlay.delete();
}
}
this.removeAll();
this.setVisible(false);
}
@Override
public void onDrawingLoaded(Drawing oldDrawing, Drawing newDrawing) {
if (newDrawing != drawing) {
drawing = newDrawing;
}
}
@Override
public void onRedrawRequest() {
for (Component c: this.getComponents()) {
if (c instanceof PathPanel) {
((PathPanel) c).updateOverlay();
}
}
}
}

View File

@@ -0,0 +1,33 @@
package org.insa.graphs.gui;
import java.time.Duration;
import java.time.Instant;
public interface RunningAction {
/**
* @return true if this action is running.
*/
public boolean isRunning();
/**
* Interrupt this action.
*/
public void interrupt();
/**
* @return Starting time of this action.
*/
public Instant getStartingTime();
/**
* @return Current duration of this action.
*/
public Duration getDuration();
/**
* @return Information for this action.
*/
public String getInformation();
}

View File

@@ -0,0 +1,289 @@
package org.insa.graphs.gui;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import org.insa.graphs.algorithm.AbstractInputData;
import org.insa.graphs.algorithm.AbstractSolution;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.overlays.PathOverlay;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Path;
public class SolutionPanel extends JPanel implements DrawingChangeListener, GraphChangeListener {
/**
*
*/
private static final long serialVersionUID = 1L;
private class SolutionBundle {
// Solution
private final AbstractSolution solution;
// Path Overlay (not final due to redraw)
private List<PathOverlay> overlays = new ArrayList<>();
/**
* Create a new bundle with the given solution and create a new overlay
* corresponding to the solution (if the solution is feasible).
*
* @param solution Solution for this bundle, must not be null.
*
*/
public SolutionBundle(AbstractSolution solution, boolean createOverlays) {
this.solution = solution;
if (createOverlays) {
this.overlays = createOverlaysFromSolution();
}
}
/**
* @return Solution associated with this bundle.
*/
public AbstractSolution getSolution() {
return this.solution;
}
/**
* @return Data assocaited with this bundle.
*/
public AbstractInputData getData() {
return this.solution.getInputData();
}
/**
* @return Overlays associated with this bundle, or null.
*/
public List<PathOverlay> getOverlays() {
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<PathOverlay> oldOverlays = this.overlays;
this.overlays = createOverlaysFromSolution();
for (int i = 0; i < oldOverlays.size(); ++i) {
oldOverlays.get(i).delete();
}
}
private List<PathOverlay> createOverlaysFromSolution() {
List<PathOverlay> overlays = new ArrayList<>();
if (solution.isFeasible()) {
Method[] methods = this.solution.getClass().getDeclaredMethods();
for (Method method: methods) {
if (method.getReturnType().equals(Path.class)
&& method.getParameterCount() == 0) {
try {
Path path = (Path) method.invoke(this.solution);
overlays.add(drawing.drawPath(path));
}
catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
// This has been check before, so should never happen...
e.printStackTrace();
}
}
}
}
return overlays;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
public String toString() {
return getData().toString();
}
}
// Solution
private Drawing drawing;
// Solution selector
private final JComboBox<SolutionBundle> solutionSelect;
private final JLabel informationPanel;
// Current bundle
private SolutionBundle currentBundle = null;
public SolutionPanel(Component parent) {
super();
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
setBorder(new CompoundBorder(BorderFactory.createMatteBorder(1, 0, 1, 0, Color.LIGHT_GRAY),
new EmptyBorder(10, 0, 10, 0)));
solutionSelect = new JComboBox<>();
solutionSelect.setBackground(Color.WHITE);
solutionSelect.setAlignmentX(Component.LEFT_ALIGNMENT);
add(solutionSelect);
informationPanel = new JLabel();
informationPanel.setOpaque(true);
informationPanel.setFocusable(false);
// informationPanel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
informationPanel.setHorizontalAlignment(JLabel.LEFT);
add(Box.createVerticalStrut(8));
add(informationPanel);
JButton clearButton = new JButton("Hide");
clearButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (PathOverlay overlay: currentBundle.getOverlays()) {
if (overlay.isVisible()) {
overlay.setVisible(false);
clearButton.setText("Show");
}
else {
overlay.setVisible(true);
clearButton.setText("Hide");
}
}
}
});
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS));
buttonPanel.add(Box.createHorizontalGlue());
buttonPanel.add(clearButton);
buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
add(Box.createVerticalStrut(4));
add(buttonPanel);
solutionSelect.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (currentBundle != null) {
for (PathOverlay overlay: currentBundle.getOverlays()) {
overlay.setVisible(false);
}
}
SolutionBundle bundle = (SolutionBundle) solutionSelect.getSelectedItem();
if (bundle != null) {
updateInformationLabel(bundle);
buttonPanel
.setVisible(bundle.getSolution().isFeasible() && bundle.hasOverlays());
clearButton.setText(bundle.getSolution().isFeasible() ? "Hide" : "Show");
for (PathOverlay overlay: bundle.getOverlays()) {
overlay.setVisible(true);
}
}
currentBundle = bundle;
}
});
}
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);
}
protected void updateInformationLabel(SolutionBundle bundle) {
informationPanel.setText(bundle.getSolution().toString());
revalidate();
repaint();
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
solutionSelect.setEnabled(enabled);
if (enabled) {
// Trigger event
solutionSelect.setSelectedItem(currentBundle);
}
else {
SolutionBundle bundle = (SolutionBundle) this.solutionSelect.getSelectedItem();
if (bundle != null) {
for (PathOverlay overlay: bundle.getOverlays()) {
overlay.setVisible(false);
}
}
}
}
@Override
public void newGraphLoaded(Graph graph) {
for (int i = 0; i < this.solutionSelect.getItemCount(); ++i) {
for (PathOverlay overlay: this.solutionSelect.getItemAt(i).getOverlays()) {
overlay.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).updateOverlays();
}
}
}

View File

@@ -0,0 +1,54 @@
package org.insa.graphs.gui;
import java.io.IOException;
import java.io.OutputStream;
import javax.swing.JTextArea;
public class StreamCapturer extends OutputStream {
private StringBuilder buffer;
private String prefix = null;
private JTextArea output;
/**
* @param output Output JTextArea to which this stream should print.
* @param prefix Prefix to add to each line of this output.
*/
public StreamCapturer(JTextArea output, String prefix) {
this.prefix = prefix;
buffer = new StringBuilder(128);
this.output = output;
}
/**
* Create a new StreamCapturer without prefix.
*
* @param output Output JTextArea to which this stream should print.
*/
public StreamCapturer(JTextArea output) {
this(output, null);
}
@Override
public void write(int b) throws IOException {
char c = (char) b;
String value = Character.toString(c);
buffer.append(value);
if (value.equals("\n")) {
output.append(getPrefix() + buffer.toString());
output.setCaretPosition(output.getText().length());
buffer.delete(0, buffer.length());
}
}
/**
* @return Formatted prefix, or empty string if no prefix is set.
*/
public String getPrefix() {
if (this.prefix == null) {
return "";
}
return "[" + prefix + "] ";
}
}

View File

@@ -0,0 +1,62 @@
package org.insa.graphs.gui;
import java.time.Duration;
import java.time.Instant;
public class ThreadWrapper implements RunningAction {
// Thread hold by this wrapper.
private Thread thread;
// Starting time of the thread.
Instant startingTime;
// MainWindow
private MainWindow mainWindow;
public ThreadWrapper(MainWindow mainWindow) {
this.thread = null;
this.mainWindow = mainWindow;
}
public void setThread(Thread thread) {
this.thread = thread;
}
public void startThread() {
this.startingTime = Instant.now();
this.thread.start();
}
public Thread getThread() {
return this.thread;
}
@Override
public boolean isRunning() {
return thread != null && thread.isAlive();
}
@SuppressWarnings("deprecation")
@Override
public void interrupt() {
thread.stop();
this.mainWindow.clearCurrentThread();
}
@Override
public Instant getStartingTime() {
return startingTime;
}
@Override
public Duration getDuration() {
return Duration.between(getStartingTime(), Instant.now());
}
@Override
public String getInformation() {
return getClass().getName();
}
}

View File

@@ -0,0 +1,82 @@
package org.insa.graphs.gui.drawing;
import java.awt.Color;
import org.insa.graphs.model.Arc;
import org.insa.graphs.model.RoadInformation.RoadType;
public class BasicGraphPalette implements GraphPalette {
// Color types for arc.
private static final Color MOTORWAY_COLOR = Color.RED;
private static final Color BIG_ROAD_COLOR = new Color(255, 105, 0);
private static final Color SMALL_ROAD_COLOR = new Color(255, 200, 0);
private static final Color COASTLINE_COLOR = Color.BLUE;
@Override
public Color getColorForArc(Arc arc) {
RoadType type = arc.getRoadInformation().getType();
switch (type) {
case MOTORWAY:
return MOTORWAY_COLOR;
case TRUNK:
case PRIMARY:
case SECONDARY:
case MOTORWAY_LINK:
case TRUNK_LINK:
case PRIMARY_LINK:
return BIG_ROAD_COLOR;
case SECONDARY_LINK:
case TERTIARY:
case RESIDENTIAL:
case UNCLASSIFIED:
case LIVING_STREET:
case SERVICE:
case ROUNDABOUT:
case PEDESTRIAN:
case CYCLEWAY:
case TRACK:
return SMALL_ROAD_COLOR;
case COASTLINE:
return COASTLINE_COLOR;
}
return Color.BLACK;
}
@Override
public int getWidthForArc(Arc arc) {
RoadType type = arc.getRoadInformation().getType();
int width = 1;
switch (type) {
case MOTORWAY:
width = 2;
break;
case TRUNK:
case PRIMARY:
case SECONDARY:
case MOTORWAY_LINK:
case TRUNK_LINK:
case PRIMARY_LINK:
width = 1;
break;
case SECONDARY_LINK:
case TERTIARY:
case RESIDENTIAL:
case UNCLASSIFIED:
case LIVING_STREET:
case SERVICE:
case ROUNDABOUT:
case PEDESTRIAN:
case CYCLEWAY:
case TRACK:
width = 1;
break;
case COASTLINE:
width = 4;
break;
}
return width;
}
}

View File

@@ -0,0 +1,19 @@
package org.insa.graphs.gui.drawing;
import java.awt.Color;
import org.insa.graphs.model.Arc;
public class BlackAndWhiteGraphPalette extends BasicGraphPalette {
// Road colors (index
private final static Color[] ROAD_COLOR_FROM_WIDTH = { null, new Color(140, 140, 140),
new Color(80, 80, 80), new Color(40, 40, 40), new Color(30, 30, 30) };
@Override
public Color getColorForArc(Arc arc) {
int width = getWidthForArc(arc);
return ROAD_COLOR_FROM_WIDTH[width];
}
}

View File

@@ -0,0 +1,157 @@
package org.insa.graphs.gui.drawing;
import java.awt.Color;
import org.insa.graphs.gui.drawing.overlays.MarkerOverlay;
import org.insa.graphs.gui.drawing.overlays.PathOverlay;
import org.insa.graphs.gui.drawing.overlays.PointSetOverlay;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Path;
import org.insa.graphs.model.Point;
public interface Drawing {
/**
* Available fill mode for the creation of markers, see the documentation of
* each value for more details.
*/
enum AlphaMode {
/**
* Do not use the original transparency of the inner part to fill it.
*/
OPAQUE,
/**
* Use the original transparency of the inner part to fill it.
*/
TRANSPARENT
}
/**
* Add a listener to click to this drawing.
*
* @param listener DrawingClickListener to add to this Drawing.
*/
public void addDrawingClickListener(DrawingClickListener listener);
/**
* Remove the given listener from the drawing.
*
* @param listener DrawingClickListener to remove from this Drawing.
*/
public void removeDrawingClickListener(DrawingClickListener listener);
/**
* Clear the drawing (overlays and underlying graph/map).
*/
public void clear();
/**
* Remove overlays from the drawing (do not remove the underlying graph/map).
*/
public void clearOverlays();
/**
* Draw a marker at the given position using the given colors and according to
* the given mode.
*
* @param point Position of the marker to draw.
* @param outer Color for the outer part of the marker to draw.
* @param inner Color for the inner part of the marker to draw.
* @param mode Mode for filling the inner par of the marker.
*
* @return A MarkerOverlay instance representing the newly drawn marker.
*/
public MarkerOverlay drawMarker(Point point, Color outer, Color inner, AlphaMode mode);
/**
* Create a new PointSetOverlay that can be used to add overlay points to this
* drawing.
*
* PointSetOverlay are heavy memory resources, do not use one for each point!
*
* @return A new PointSetOverlay for this drawing.
*/
public PointSetOverlay createPointSetOverlay();
/**
* Create a new PointSetOverlay with the given initial width and color that can
* be used to add overlay points to this drawing.
*
* PointSetOverlay are heavy memory resources, do not use one for each point!
*
* @param width Initial width of points in the overlay.
* @param color Initial width of points in the overlay.
*
* @return A new PointSetOverlay for this drawing.
*/
public PointSetOverlay createPointSetOverlay(int width, Color color);
/**
* Draw the given graph using the given palette.
*
* @param graph Graph to draw.
* @param palette Palette to use to draw the graph.
*
* @see BasicGraphPalette
* @see BlackAndWhiteGraphPalette
*/
public void drawGraph(Graph graph, GraphPalette palette);
/**
* Draw the given graph using a default palette specific to the implementation.
*
* @param graph Graph to draw.
*/
public void drawGraph(Graph graph);
/**
* Draw a path using the given color.
*
* @param path Path to draw.
* @param color Color of the path to draw.
* @param markers true to show origin and destination markers.
*
* @return A PathOverlay instance representing the newly drawn path.
*/
public PathOverlay drawPath(Path path, Color color, boolean markers);
/**
* Draw a path with both origin and destination markers using the given color.
*
* @param path Path to draw.
* @param color Color of the path to draw.
*
* @return A PathOverlay instance representing the newly drawn path.
*
* @see Drawing#drawPath(Path, Color, boolean)
*/
public PathOverlay drawPath(Path path, Color color);
/**
* Draw a path using a default color specific to the implementation
*
* @param path Path to draw.
* @param markers true to show origin and destination markers.
*
* @return A PathOverlay instance representing the newly drawn path.
*
* @see Drawing#drawPath(Path, Color, boolean)
*/
public PathOverlay drawPath(Path path, boolean markers);
/**
* Draw a path with both origin and destination markers using a default color
* specific to the implementation
*
*
* @param path Path to draw.
*
* @return A PathOverlay instance representing the newly drawn path.
*
* @see Drawing#drawPath(Path, Color, boolean)
*/
public PathOverlay drawPath(Path path);
}

View File

@@ -0,0 +1,14 @@
package org.insa.graphs.gui.drawing;
import org.insa.graphs.model.Point;
public interface DrawingClickListener {
/**
* Event triggered when a click is made on the map.
*
* @param point Position (on the map) of the mouse click.
*/
public void mouseClicked(Point point);
}

View File

@@ -0,0 +1,23 @@
package org.insa.graphs.gui.drawing;
import java.awt.Color;
import org.insa.graphs.model.Arc;
public interface GraphPalette {
/**
* @param arc Arc for which color should be retrieved.
*
* @return Color associated with the given arc.
*/
public Color getColorForArc(Arc arc);
/**
* @param arc Arc for which width should be retrieved.
*
* @return Width associated with the given arc.
*/
public int getWidthForArc(Arc arc);
}

View File

@@ -0,0 +1,116 @@
package org.insa.graphs.gui.drawing;
import java.awt.Dimension;
import org.insa.graphs.model.GraphStatistics.BoundingBox;
public class MercatorProjection implements Projection {
public static final double MAX_LATITUDE = 82;
public static final double MIN_LATITUDE = -MAX_LATITUDE;
// From Wikipedia... for the above max/min latitude.
private static final double IMAGE_WIDTH = 2058, IMAGE_HEIGHT = 1746;
private static final double MAX_LATITUDE_PROJ = projectY(MAX_LATITUDE);
private static final double MIN_LATITUDE_PROJ = projectY(MIN_LATITUDE);
// Bounding box
private final float minLatitude, minLongitude, maxLatitude, maxLongitude;
// Projection of min and max latitude.
private final double minLatitudeProj, maxLatitudeProj;
// Dimension of the image
private final double width, height;
/**
* Create a new MercatorProjection corresponding to the given BoundingBox and
* maxSize.
*
* @param boundingBox Box for this projection.
* @param maxSize Maximum size of any side (width / height) of the image to
* which this projection should draw.
*/
public MercatorProjection(BoundingBox boundingBox, int maxSize) {
// Find minimum/maximum longitude and latitude.
this.minLongitude = boundingBox.getTopLeftPoint().getLongitude();
this.maxLongitude = boundingBox.getBottomRightPoint().getLongitude();
this.minLatitude = boundingBox.getBottomRightPoint().getLatitude();
this.maxLatitude = boundingBox.getTopLeftPoint().getLatitude();
// Compute projection
this.minLatitudeProj = projectY(this.minLatitude);
this.maxLatitudeProj = projectY(this.maxLatitude);
Dimension imageDimension = computeImageSize(maxSize);
this.width = imageDimension.getWidth();
this.height = imageDimension.getHeight();
}
/**
* Compute the projection (without scaling) of the given latitude.
*
* @param latitude Latitude to project.
*
* @return Projection of the given latitude (without scaling).
*/
private static double projectY(double latitude) {
double sinLatitude = Math.sin(latitude * Math.PI / 180.0);
return Math.log((1 + sinLatitude) / (1 - sinLatitude)) / 2;
}
/**
* Compute the dimension required for drawing a projection of the given box on
* an image, ensuring that none of the side of image is greater than maxSize.
*
* @param maxSize Maximum side of any side of the image.
*
* @return Dimension corresponding to the preferred size for the image.
*/
protected Dimension computeImageSize(int maxSize) {
double propWidth = (maxLongitude - minLongitude) * IMAGE_WIDTH / 360.0;
double propHeight = (this.maxLatitudeProj - this.minLatitudeProj)
/ (MAX_LATITUDE_PROJ - MIN_LATITUDE_PROJ) * IMAGE_HEIGHT;
return propWidth < propHeight
? new Dimension((int) (maxSize * propWidth / propHeight), maxSize)
: new Dimension(maxSize, (int) (maxSize * propHeight / propWidth));
}
@Override
public double getImageWidth() {
return this.width;
}
@Override
public double getImageHeight() {
return this.height;
}
@Override
public int latitudeToPixelY(float latitude) {
return (int) ((this.maxLatitudeProj - projectY(latitude))
/ (this.maxLatitudeProj - this.minLatitudeProj) * this.height);
}
@Override
public int longitudeToPixelX(float longitude) {
return (int) (width * (longitude - minLongitude) / (maxLongitude - minLongitude));
}
@Override
public float pixelYToLatitude(double py) {
float y = (float) (this.maxLatitudeProj
- (py / this.height) * (this.maxLatitudeProj - this.minLatitudeProj));
return (float) (180 * (2 * Math.atan(Math.exp(y)) - Math.PI / 2) / Math.PI);
}
@Override
public float pixelXToLongitude(double px) {
return (float) ((px / this.width) * (this.maxLongitude - this.minLongitude)
+ this.minLongitude);
}
}

View File

@@ -0,0 +1,68 @@
package org.insa.graphs.gui.drawing;
import org.insa.graphs.model.GraphStatistics.BoundingBox;
public class PlateCarreProjection implements Projection {
// Bounding box
private final float minLatitude, minLongitude, maxLatitude, maxLongitude;
// Dimension of the image
private final double width, height;
/**
* Create a new PlateCarreProjection corresponding to the given BoundingBox and
* maxSize.
*
* @param boundingBox Box for this projection.
* @param maxSize Maximum size of any side (width / height) of the image to
* which this projection should draw.
*/
public PlateCarreProjection(BoundingBox boundingBox, int maxSize) {
// Find minimum/maximum longitude and latitude.
this.minLongitude = boundingBox.getTopLeftPoint().getLongitude();
this.maxLongitude = boundingBox.getBottomRightPoint().getLongitude();
this.minLatitude = boundingBox.getBottomRightPoint().getLatitude();
this.maxLatitude = boundingBox.getTopLeftPoint().getLatitude();
float diffLon = maxLongitude - minLongitude, diffLat = maxLatitude - minLatitude;
this.width = diffLon < diffLat ? (int) (maxSize * diffLon / diffLat) : maxSize;
this.height = diffLon < diffLat ? maxSize : (int) (maxSize * diffLat / diffLon);
}
@Override
public double getImageWidth() {
return this.width;
}
@Override
public double getImageHeight() {
return this.height;
}
@Override
public int latitudeToPixelY(float latitude) {
return (int) (this.height * (this.maxLatitude - latitude)
/ (this.maxLatitude - this.minLatitude));
}
@Override
public int longitudeToPixelX(float longitude) {
return (int) (this.width * (longitude - this.minLongitude)
/ (this.maxLongitude - this.minLongitude));
}
@Override
public float pixelYToLatitude(double py) {
return (float) (this.maxLatitude
- py / this.height * (this.maxLatitude - this.minLatitude));
}
@Override
public float pixelXToLongitude(double px) {
return (float) (px / this.width * (this.maxLongitude - this.minLongitude)
+ this.minLongitude);
}
}

View File

@@ -0,0 +1,51 @@
package org.insa.graphs.gui.drawing;
public interface Projection {
/**
* @return Image width for this projection to work properly.
*/
public double getImageWidth();
/**
* @return Image weight for this projection to work properly.
*/
public double getImageHeight();
/**
* Project the given latitude on the image.
*
* @param latitude Latitude to project.
*
* @return Projected position of the latitude on the image.
*/
public int latitudeToPixelY(float latitude);
/**
* Project the given longitude on the image.
*
* @param longitude Longitude to project.
*
* @return Projected position of the longitude on the image.
*/
public int longitudeToPixelX(float longitude);
/**
* Retrieve the latitude associated to the given projected point.
*
* @param py Projected y-position for which latitude should be retrieved.
*
* @return The original latitude of the point.
*/
public float pixelYToLatitude(double py);
/**
* Retrieve the longitude associated to the given projected point.
*
* @param px Projected x-position for which longitude should be retrieved.
*
* @return The original longitude of the point.
*/
public float pixelXToLongitude(double px);
}

View File

@@ -0,0 +1,747 @@
package org.insa.graphs.gui.drawing.components;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JPanel;
import org.insa.graphs.gui.drawing.BasicGraphPalette;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.DrawingClickListener;
import org.insa.graphs.gui.drawing.GraphPalette;
import org.insa.graphs.gui.drawing.MercatorProjection;
import org.insa.graphs.gui.drawing.PlateCarreProjection;
import org.insa.graphs.gui.drawing.Projection;
import org.insa.graphs.gui.drawing.overlays.MarkerOverlay;
import org.insa.graphs.gui.drawing.overlays.MarkerUtils;
import org.insa.graphs.gui.drawing.overlays.Overlay;
import org.insa.graphs.gui.drawing.overlays.PathOverlay;
import org.insa.graphs.gui.drawing.overlays.PointSetOverlay;
import org.insa.graphs.model.Arc;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Node;
import org.insa.graphs.model.Path;
import org.insa.graphs.model.Point;
import org.insa.graphs.model.GraphStatistics.BoundingBox;
/**
*
*/
public class BasicDrawing extends JPanel implements Drawing {
/**
*
*/
private static final long serialVersionUID = 96779785877771827L;
private abstract class BasicOverlay implements Overlay {
// Visible?
protected boolean visible;
// Color
protected Color color;
public BasicOverlay(Color color) {
this.visible = true;
this.color = color;
}
/**
* @return The Z level of this overlay (>= 1).
*/
public abstract int getZLevel();
@Override
public void setColor(Color color) {
this.color = color;
}
@Override
public Color getColor() {
return this.color;
}
@Override
public void setVisible(boolean visible) {
this.visible = visible;
BasicDrawing.this.repaint();
}
@Override
public boolean isVisible() {
return this.visible;
}
@Override
public void delete() {
BasicDrawing.this.overlays.remove(this);
}
/**
* Draw the given overlay.
*/
public void draw(Graphics2D g) {
if (this.visible) {
drawImpl(g);
}
}
public abstract void drawImpl(Graphics2D g);
public void redraw() {
BasicDrawing.this.repaint();
}
};
private class BasicMarkerOverlay extends BasicOverlay implements MarkerOverlay {
// Marker width and height
public static final int MARKER_WIDTH = 30, MARKER_HEIGHT = 60;
// Point of the marker.
private Point point;
// Image to draw
private Image image;
// Inner color and fill mode.
private Color innerColor;
private final AlphaMode alphaMode;
public BasicMarkerOverlay(Point point, Color color, Color inner, AlphaMode alphaMode) {
super(color);
this.point = point;
this.image = MarkerUtils.getMarkerForColor(color, inner, alphaMode);
this.innerColor = inner;
this.alphaMode = alphaMode;
}
public int getZLevel() {
return 3;
}
@Override
public Point getPoint() {
return point;
}
@Override
public void setColor(Color color) {
this.innerColor = this.innerColor.equals(this.color) ? color : innerColor;
super.setColor(color);
this.image = MarkerUtils.getMarkerForColor(color, this.innerColor, alphaMode);
}
@Override
public void moveTo(Point point) {
this.point = point;
BasicDrawing.this.repaint();
}
@Override
public void drawImpl(Graphics2D graphics) {
int px = projection.longitudeToPixelX(getPoint().getLongitude());
int py = projection.latitudeToPixelY(getPoint().getLatitude());
graphics.drawImage(this.image, px - MARKER_WIDTH / 2, py - MARKER_HEIGHT, MARKER_WIDTH,
MARKER_HEIGHT, BasicDrawing.this);
}
};
private class BasicPathOverlay extends BasicOverlay implements PathOverlay {
// List of points
private final List<Point> points;
// Origin / Destination markers.
private BasicMarkerOverlay origin, destination;
public BasicPathOverlay(List<Point> points, Color color, BasicMarkerOverlay origin,
BasicMarkerOverlay destination) {
super(color);
this.points = points;
this.origin = origin;
this.destination = destination;
this.color = color;
}
public int getZLevel() {
return 2;
}
@Override
public void setColor(Color color) {
super.setColor(color);
this.origin.setColor(color);
this.destination.setColor(color);
}
@Override
public void drawImpl(Graphics2D graphics) {
if (!points.isEmpty()) {
graphics.setStroke(new BasicStroke(2));
graphics.setColor(getColor());
Iterator<Point> itPoint = points.iterator();
Point prev = itPoint.next();
while (itPoint.hasNext()) {
Point curr = itPoint.next();
int x1 = projection.longitudeToPixelX(prev.getLongitude());
int x2 = projection.longitudeToPixelX(curr.getLongitude());
int y1 = projection.latitudeToPixelY(prev.getLatitude());
int y2 = projection.latitudeToPixelY(curr.getLatitude());
graphics.drawLine(x1, y1, x2, y2);
prev = curr;
}
}
if (this.origin != null) {
this.origin.draw(graphics);
}
if (this.destination != null) {
this.destination.draw(graphics);
}
}
};
private class BasicPointSetOverlay extends BasicOverlay implements PointSetOverlay {
// Default point width
private static final int DEFAULT_POINT_WIDTH = 5;
// Image for path / points
private final BufferedImage image;
private final Graphics2D graphics;
private int width = DEFAULT_POINT_WIDTH;
public BasicPointSetOverlay() {
super(Color.BLACK);
this.image = new BufferedImage(BasicDrawing.this.width, BasicDrawing.this.height,
BufferedImage.TYPE_4BYTE_ABGR);
this.graphics = image.createGraphics();
this.graphics.setBackground(new Color(0, 0, 0, 0));
}
public int getZLevel() {
return 1;
}
@Override
public void setColor(Color color) {
super.setColor(color);
this.graphics.setColor(color);
}
@Override
public void setWidth(int width) {
this.width = Math.max(2, width);
}
@Override
public void setWidthAndColor(int width, Color color) {
setWidth(width);
setColor(color);
}
@Override
public void addPoint(Point point) {
int x = projection.longitudeToPixelX(point.getLongitude()) - this.width / 2;
int y = projection.latitudeToPixelY(point.getLatitude()) - this.width / 2;
this.graphics.fillOval(x, y, this.width, this.width);
BasicDrawing.this.repaint();
}
@Override
public void addPoint(Point point, int width) {
setWidth(width);
addPoint(point);
}
@Override
public void addPoint(Point point, Color color) {
setColor(color);
addPoint(point);
}
@Override
public void addPoint(Point point, int width, Color color) {
setWidth(width);
setColor(color);
addPoint(point);
}
@Override
public void drawImpl(Graphics2D g) {
g.drawImage(this.image, 0, 0, BasicDrawing.this);
}
}
/**
* Class encapsulating a set of overlays.
*
*/
private class BasicOverlays {
// List of overlays.
private ArrayList<ArrayList<BasicOverlay>> overlays = new ArrayList<>();
public synchronized void draw(Graphics2D g) {
// Clear overlays.
for (ArrayList<BasicOverlay> arr: this.overlays) {
for (BasicOverlay overlay: arr) {
overlay.draw(g);
}
}
}
public synchronized void remove(BasicOverlay overlay) {
overlays.get(overlay.getZLevel() - 1).remove(overlay);
BasicDrawing.this.repaint();
}
public void clear() {
clear(true);
}
public void clear(boolean repaint) {
// Clear overlays.
for (ArrayList<BasicOverlay> arr: this.overlays) {
arr.clear();
}
// Repaint if requested.
if (repaint) {
BasicDrawing.this.repaint();
}
}
public BasicOverlay add(BasicOverlay marker) {
return add(marker, true);
}
public synchronized BasicOverlay add(BasicOverlay overlay, boolean repaint) {
// Check if we have a level for this...
for (int i = overlays.size(); i < overlay.getZLevel(); ++i) {
overlays.add(new ArrayList<>());
}
// Add overlay to the given list.
overlays.get(overlay.getZLevel() - 1).add(overlay);
// Repaint if requested.
if (repaint) {
BasicDrawing.this.repaint();
}
return overlay;
}
};
// Default path color.
public static final Color DEFAULT_PATH_COLOR = new Color(66, 134, 244);
// Default palette.
public static final GraphPalette DEFAULT_PALETTE = new BasicGraphPalette();
// Maximum width for the drawing (in pixels).
private static final int MAXIMUM_DRAWING_WIDTH = 2000;
private Projection projection;
// Width and height of the image
private int width, height;
// Zoom controls
private MapZoomControls zoomControls;
private ZoomAndPanListener zoomAndPanListener;
//
private Image graphImage = null;
private Graphics2D graphGraphics = null;
// List of image for markers
private BasicOverlays overlays = new BasicOverlays();
// Mapping DrawingClickListener -> MouseEventListener
private List<DrawingClickListener> drawingClickListeners = new ArrayList<>();
/**
* Create a new BasicDrawing.
*
*/
public BasicDrawing() {
setLayout(null);
this.setBackground(new Color(245, 245, 245));
this.zoomAndPanListener = new ZoomAndPanListener(this,
ZoomAndPanListener.DEFAULT_MIN_ZOOM_LEVEL, 20, 1.2);
// Try...
try {
this.zoomControls = new MapZoomControls(this, 0,
ZoomAndPanListener.DEFAULT_MIN_ZOOM_LEVEL, 20);
this.zoomControls.addZoomInListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
zoomAndPanListener.zoomIn();
}
});
this.zoomControls.addZoomOutListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
zoomAndPanListener.zoomOut();
}
});
}
catch (IOException e) {
e.printStackTrace();
}
this.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent evt) {
if (zoomControls.contains(evt.getPoint())) {
return;
}
Point lonlat = null;
try {
lonlat = getLongitudeLatitude(evt);
}
catch (NoninvertibleTransformException e) {
return;
}
for (DrawingClickListener listener: drawingClickListeners) {
listener.mouseClicked(lonlat);
}
}
});
}
@Override
public void paintComponent(Graphics g1) {
super.paintComponent(g1);
Graphics2D g = (Graphics2D) g1;
AffineTransform sTransform = g.getTransform();
g.setColor(this.getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
g.setTransform(zoomAndPanListener.getCoordTransform());
if (graphImage != null) {
// Draw graph
g.drawImage(graphImage, 0, 0, this);
}
// Draw markers
this.overlays.draw(g);
g.setTransform(sTransform);
if (this.zoomControls != null) {
this.zoomControls.setZoomLevel(this.zoomAndPanListener.getZoomLevel());
this.zoomControls.draw(g, getWidth() - this.zoomControls.getWidth() - 20,
this.getHeight() - this.zoomControls.getHeight() - 10, this);
}
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#clear()
*/
@Override
public void clear() {
if (this.graphGraphics != null) {
this.graphGraphics.clearRect(0, 0, this.width, this.height);
}
this.overlays.clear(false);
this.repaint();
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#clearOverlays()
*/
@Override
public void clearOverlays() {
this.overlays.clear();
}
/**
* @return The current ZoomAndPanListener associated with this drawing.
*/
public ZoomAndPanListener getZoomAndPanListener() {
return this.zoomAndPanListener;
}
/**
* Return the longitude and latitude corresponding to the given position of the
* MouseEvent.
*
* @param event MouseEvent from which longitude/latitude should be retrieved.
*
* @return Point representing the projection of the MouseEvent position in the
* graph/map.
*
* @throws NoninvertibleTransformException if the actual transformation is
* invalid.
*/
protected Point getLongitudeLatitude(MouseEvent event) throws NoninvertibleTransformException {
// Get the point using the inverse transform of the Zoom/Pan object, this gives
// us
// a point within the drawing box (between [0, 0] and [width, height]).
Point2D ptDst = this.zoomAndPanListener.getCoordTransform()
.inverseTransform(event.getPoint(), null);
// Inverse the "projection" on x/y to get longitude and latitude.
return new Point(projection.pixelXToLongitude(ptDst.getX()),
projection.pixelYToLatitude(ptDst.getY()));
}
/*
* (non-Javadoc)
*
* @see
* org.insa.graphics.drawing.Drawing#addDrawingClickListener(org.insa.graphics.
* drawing.DrawingClickListener)
*/
@Override
public void addDrawingClickListener(DrawingClickListener listener) {
this.drawingClickListeners.add(listener);
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#removeDrawingClickListener(org.insa.
* graphics.drawing.DrawingClickListener)
*/
@Override
public void removeDrawingClickListener(DrawingClickListener listener) {
this.drawingClickListeners.remove(listener);
}
public BasicMarkerOverlay createMarker(Point point, Color outer, Color inner, AlphaMode mode) {
return new BasicMarkerOverlay(point, outer, inner, mode);
}
@Override
public MarkerOverlay drawMarker(Point point, Color outer, Color inner, AlphaMode mode) {
return (MarkerOverlay) this.overlays.add(createMarker(point, outer, inner, mode));
}
@Override
public PointSetOverlay createPointSetOverlay() {
return (PointSetOverlay) this.overlays.add(new BasicPointSetOverlay(), false);
}
@Override
public PointSetOverlay createPointSetOverlay(int width, Color color) {
PointSetOverlay ps = createPointSetOverlay();
ps.setWidthAndColor(width, color);
return ps;
}
/**
* Draw the given arc.
*
* @param arc Arc to draw.
* @param palette Palette to use to retrieve color and width for arc, or null to
* use current settings.
*/
protected void drawArc(Arc arc, GraphPalette palette, boolean repaint) {
List<Point> pts = arc.getPoints();
if (!pts.isEmpty()) {
if (palette != null) {
this.graphGraphics.setColor(palette.getColorForArc(arc));
this.graphGraphics.setStroke(new BasicStroke(palette.getWidthForArc(arc)));
}
Iterator<Point> it1 = pts.iterator();
Point prev = it1.next();
while (it1.hasNext()) {
Point curr = it1.next();
int x1 = projection.longitudeToPixelX(prev.getLongitude());
int x2 = projection.longitudeToPixelX(curr.getLongitude());
int y1 = projection.latitudeToPixelY(prev.getLatitude());
int y2 = projection.latitudeToPixelY(curr.getLatitude());
graphGraphics.drawLine(x1, y1, x2, y2);
prev = curr;
}
}
if (repaint) {
this.repaint();
}
}
/**
* Initialize the drawing for the given graph.
*
* @param graph
*/
protected void initialize(Graph graph) {
// Clear everything.
this.clear();
BoundingBox box = graph.getGraphInformation().getBoundingBox();
// Find minimum/maximum longitude and latitude.
float minLon = box.getTopLeftPoint().getLongitude(),
maxLon = box.getBottomRightPoint().getLongitude(),
minLat = box.getBottomRightPoint().getLatitude(),
maxLat = box.getTopLeftPoint().getLatitude();
// Add a little delta to avoid drawing on the edge...
float diffLon = maxLon - minLon, diffLat = maxLat - minLat;
float deltaLon = 0.01f * diffLon, deltaLat = 0.01f * diffLat;
// Create the projection and retrieve width and height for the box.
BoundingBox extendedBox = box.extend(deltaLon, deltaLat, deltaLon, deltaLat);
// Special projection for non-realistic maps...
if (graph.getMapId().startsWith("0x")) {
projection = new PlateCarreProjection(extendedBox, MAXIMUM_DRAWING_WIDTH / 4);
}
else {
projection = new MercatorProjection(extendedBox, MAXIMUM_DRAWING_WIDTH);
}
this.width = (int) projection.getImageWidth();
this.height = (int) projection.getImageHeight();
// Create the image
BufferedImage img = new BufferedImage(this.width, this.height,
BufferedImage.TYPE_3BYTE_BGR);
this.graphImage = img;
this.graphGraphics = img.createGraphics();
this.graphGraphics.setBackground(this.getBackground());
this.graphGraphics.clearRect(0, 0, this.width, this.height);
// Set the zoom and pan listener
double scale = 1 / Math.max(this.width / (double) this.getWidth(),
this.height / (double) this.getHeight());
this.zoomAndPanListener.setCoordTransform(this.graphGraphics.getTransform());
this.zoomAndPanListener.getCoordTransform().translate(
(this.getWidth() - this.width * scale) / 2,
(this.getHeight() - this.height * scale) / 2);
this.zoomAndPanListener.getCoordTransform().scale(scale, scale);
this.zoomAndPanListener.setZoomLevel(0);
this.zoomControls.setZoomLevel(0);
// Repaint
this.repaint();
}
@Override
public void drawGraph(Graph graph, GraphPalette palette) {
int repaintModulo = Math.max(1, graph.size() / 100);
// Initialize the buffered image
this.initialize(graph);
// Remove zoom and pan listener
this.removeMouseListener(zoomAndPanListener);
this.removeMouseMotionListener(zoomAndPanListener);
this.removeMouseWheelListener(zoomAndPanListener);
for (Node node: graph.getNodes()) {
for (Arc arc: node.getSuccessors()) {
// Draw arcs only if there are one-way arcs or if origin is lower than
// destination, avoid drawing two-ways arc twice.
if (arc.getRoadInformation().isOneWay()
|| arc.getOrigin().compareTo(arc.getDestination()) < 0) {
drawArc(arc, palette, false);
}
}
if (node.getId() % repaintModulo == 0) {
this.repaint();
}
}
this.repaint();
// Re-add zoom and pan listener
this.addMouseListener(zoomAndPanListener);
this.addMouseMotionListener(zoomAndPanListener);
this.addMouseWheelListener(zoomAndPanListener);
}
@Override
public void drawGraph(Graph graph) {
drawGraph(graph, DEFAULT_PALETTE);
}
@Override
public PathOverlay drawPath(Path path, Color color, boolean markers) {
List<Point> points = new ArrayList<Point>();
if (!path.isEmpty()) {
points.add(path.getOrigin().getPoint());
for (Arc arc: path.getArcs()) {
Iterator<Point> itPoint = arc.getPoints().iterator();
// Discard origin each time
itPoint.next();
while (itPoint.hasNext()) {
points.add(itPoint.next());
}
}
}
BasicMarkerOverlay origin = null, destination = null;
if (markers && !path.isEmpty()) {
origin = createMarker(path.getOrigin().getPoint(), color, color, AlphaMode.TRANSPARENT);
destination = createMarker(path.getDestination().getPoint(), color, color,
AlphaMode.TRANSPARENT);
}
return (PathOverlay) this.overlays
.add(new BasicPathOverlay(points, color, origin, destination));
}
@Override
public PathOverlay drawPath(Path path, Color color) {
return drawPath(path, color, true);
}
@Override
public PathOverlay drawPath(Path path) {
return drawPath(path, DEFAULT_PATH_COLOR);
}
@Override
public PathOverlay drawPath(Path path, boolean markers) {
return drawPath(path, DEFAULT_PATH_COLOR, markers);
}
}

View File

@@ -0,0 +1,521 @@
package org.insa.graphs.gui.drawing.components;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.DrawingClickListener;
import org.insa.graphs.gui.drawing.GraphPalette;
import org.insa.graphs.gui.drawing.overlays.MarkerAutoScaling;
import org.insa.graphs.gui.drawing.overlays.MarkerOverlay;
import org.insa.graphs.gui.drawing.overlays.MarkerUtils;
import org.insa.graphs.gui.drawing.overlays.Overlay;
import org.insa.graphs.gui.drawing.overlays.PathOverlay;
import org.insa.graphs.gui.drawing.overlays.PointSetOverlay;
import org.insa.graphs.gui.drawing.overlays.PolylineAutoScaling;
import org.insa.graphs.model.Arc;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Path;
import org.insa.graphs.model.Point;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.core.util.LatLongUtils;
import org.mapsforge.core.util.Parameters;
import org.mapsforge.map.awt.graphics.AwtGraphicFactory;
import org.mapsforge.map.awt.util.AwtUtil;
import org.mapsforge.map.awt.view.MapView;
import org.mapsforge.map.datastore.MapDataStore;
import org.mapsforge.map.layer.Layer;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.layer.cache.TileCache;
import org.mapsforge.map.layer.hills.HillsRenderConfig;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.layer.overlay.Polygon;
import org.mapsforge.map.layer.renderer.TileRendererLayer;
import org.mapsforge.map.model.DisplayModel;
import org.mapsforge.map.model.IMapViewPosition;
import org.mapsforge.map.model.Model;
import org.mapsforge.map.reader.MapFile;
import org.mapsforge.map.rendertheme.InternalRenderTheme;
/**
*
*/
public class MapViewDrawing extends MapView implements Drawing {
/**
*
*/
private static final long serialVersionUID = 8606967833704938092L;
/**
* Base Overlay for MapViewDrawing overlays.
*
*/
private abstract class MapViewOverlay implements Overlay {
// Marker associated.
protected Layer[] layers;
// Current color
protected Color color;
public MapViewOverlay(Layer[] layers, Color color) {
this.layers = layers;
for (Layer layer: this.layers) {
MapViewDrawing.this.getLayerManager().getLayers().add(layer);
}
this.color = color;
}
@Override
public void setColor(Color color) {
this.color = color;
}
@Override
public Color getColor() {
return this.color;
}
@Override
public void setVisible(boolean visible) {
for (Layer layer: layers) {
layer.setVisible(visible);
}
}
@Override
public boolean isVisible() {
if (this.layers.length == 0) {
return true;
}
return this.layers[0].isVisible();
}
@Override
public void delete() {
Layers mlayers = MapViewDrawing.this.getLayerManager().getLayers();
for (Layer layer: layers) {
mlayers.remove(layer);
}
}
@Override
public void redraw() {
MapViewDrawing.this.getLayerManager().redrawLayers();
}
};
/**
* MarkerOverlay for MapViewDrawing.
*
*/
private class MapViewMarkerOverlay extends MapViewOverlay implements MarkerOverlay {
private final AlphaMode alphaMode;
private Color innerColor;
public MapViewMarkerOverlay(Marker marker, Color outer, Color innerColor,
AlphaMode alphaMode) {
super(new Layer[] { marker }, outer);
this.innerColor = innerColor;
this.alphaMode = alphaMode;
}
@Override
public Point getPoint() {
Marker marker = (Marker) super.layers[0];
return new Point((float) marker.getLatLong().getLongitude(),
(float) marker.getLatLong().getLatitude());
}
@Override
public void setColor(Color outer) {
this.innerColor = this.innerColor.equals(this.color) ? outer : this.innerColor;
super.setColor(color);
MarkerAutoScaling marker = (MarkerAutoScaling) super.layers[0];
marker.setImage(MarkerUtils.getMarkerForColor(color, this.innerColor, this.alphaMode));
}
@Override
public void moveTo(Point point) {
MarkerAutoScaling marker = (MarkerAutoScaling) this.layers[0];
this.delete();
marker = new MarkerAutoScaling(convertPoint(point), marker.getImage());
this.layers[0] = marker;
MapViewDrawing.this.getLayerManager().getLayers().add(marker);
}
};
/**
* PathOverlay for MapViewDrawing.
*
*/
private class MapViewPathOverlay extends MapViewOverlay implements PathOverlay {
public MapViewPathOverlay(PolylineAutoScaling path, MarkerAutoScaling origin,
MarkerAutoScaling destination) {
super(new Layer[] { path, origin, destination }, path.getColor());
}
public MapViewPathOverlay(PolylineAutoScaling path) {
super(new Layer[] { path }, path.getColor());
}
@Override
public void setColor(Color color) {
super.setColor(color);
((PolylineAutoScaling) this.layers[0]).setColor(color);
((MarkerAutoScaling) this.layers[1])
.setImage(MarkerUtils.getMarkerForColor(color, color, AlphaMode.TRANSPARENT));
((MarkerAutoScaling) this.layers[2])
.setImage(MarkerUtils.getMarkerForColor(color, color, AlphaMode.TRANSPARENT));
}
}
/**
* PointSetOverlay for MapViewDrawing - Not currently implemented.
*
*/
private class MapViewPointSetOverlay extends MapViewOverlay implements PointSetOverlay {
private List<Point> points = new ArrayList<>();
private final Polygon polygon;
private List<Point> convexHull(List<Point> p) {
if (p.isEmpty()) {
return new ArrayList<>();
}
p.sort((p1, p2) -> Float.compare(p1.getLongitude(), p2.getLongitude()));
List<Point> h = new ArrayList<>();
// lower hull
for (Point pt: p) {
while (h.size() >= 2 && !ccw(h.get(h.size() - 2), h.get(h.size() - 1), pt)) {
h.remove(h.size() - 1);
}
h.add(pt);
}
// upper hull
int t = h.size() + 1;
for (int i = p.size() - 1; i >= 0; i--) {
Point pt = p.get(i);
while (h.size() >= t && !ccw(h.get(h.size() - 2), h.get(h.size() - 1), pt)) {
h.remove(h.size() - 1);
}
h.add(pt);
}
h.remove(h.size() - 1);
return h;
}
// ccw returns true if the three points make a counter-clockwise turn
private boolean ccw(Point a, Point b, Point c) {
return ((b.getLongitude() - a.getLongitude())
* (c.getLatitude() - a.getLatitude())) > ((b.getLatitude() - a.getLatitude())
* (c.getLongitude() - a.getLongitude()));
}
public MapViewPointSetOverlay() {
super(new Layer[] { new Polygon(GRAPHIC_FACTORY.createPaint(), null, GRAPHIC_FACTORY) },
Color.BLACK);
polygon = (Polygon) this.layers[0];
}
@Override
public void setColor(Color color) {
super.setColor(color);
polygon.getPaintFill().setColor(GRAPHIC_FACTORY.createColor(100, color.getRed(),
color.getGreen(), color.getBlue()));
}
@Override
public void setWidth(int width) {
}
@Override
public void setWidthAndColor(int width, Color color) {
setWidth(width);
setColor(color);
}
@Override
public void addPoint(Point point) {
points.add(point);
this.points = convexHull(points);
polygon.setPoints(this.points.stream().map(MapViewDrawing.this::convertPoint)
.collect(Collectors.toList()));
polygon.requestRedraw();
}
@Override
public void addPoint(Point point, int width) {
setWidth(width);
addPoint(point);
}
@Override
public void addPoint(Point point, Color color) {
setColor(color);
addPoint(point);
}
@Override
public void addPoint(Point point, int width, Color color) {
setWidth(width);
setColor(color);
addPoint(point);
}
};
// Default path color.
public static final Color DEFAULT_PATH_COLOR = new Color(66, 134, 244);
// Graphic factory.
private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE;
// Default tile size.
private static final int DEFAULT_TILE_SIZE = 512;
// List of listeners.
private ArrayList<DrawingClickListener> drawingClickListeners = new ArrayList<>();
// Tile size
private int tileSize;
// Zoom controls
private MapZoomControls zoomControls;
public MapViewDrawing() {
super();
Parameters.NUMBER_OF_THREADS = 2;
Parameters.SQUARE_FRAME_BUFFER = false;
getMapScaleBar().setVisible(true);
DisplayModel model = getModel().displayModel;
this.tileSize = DEFAULT_TILE_SIZE;
model.setFixedTileSize(this.tileSize);
this.setZoomLevelMin((byte) 0);
this.setZoomLevelMax((byte) 20);
// Try...
try {
this.zoomControls = new MapZoomControls(this, 0, 0, 20);
this.zoomControls.addZoomInListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getModel().mapViewPosition.zoomIn();
}
});
this.zoomControls.addZoomOutListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
getModel().mapViewPosition.zoomOut();
}
});
}
catch (IOException e) {
e.printStackTrace();
}
}
/*
* (non-Javadoc)
*
* @see org.mapsforge.map.awt.view.MapView#paint(java.awt.Graphics)
*/
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
if (this.zoomControls != null) {
this.zoomControls.setZoomLevel(this.getModel().mapViewPosition.getZoomLevel());
this.zoomControls.draw((Graphics2D) graphics,
getWidth() - this.zoomControls.getWidth() - 20,
this.getHeight() - this.zoomControls.getHeight() - 10, this);
}
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#clear()
*/
@Override
public void clear() {
getLayerManager().getLayers().clear();
repaint();
}
/*
* (non-Javadoc)
*
* @see org.insa.graphics.drawing.Drawing#clearOverlays()
*/
@Override
public void clearOverlays() {
Layers layers = getLayerManager().getLayers();
for (Layer layer: layers) {
if (layer instanceof PolylineAutoScaling || layer instanceof MarkerAutoScaling) {
getLayerManager().getLayers().remove(layer, false);
}
}
repaint();
}
protected LatLong convertPoint(Point point) {
return new LatLong(point.getLatitude(), point.getLongitude());
}
private TileRendererLayer createTileRendererLayer(TileCache tileCache,
MapDataStore mapDataStore, IMapViewPosition mapViewPosition,
HillsRenderConfig hillsRenderConfig) {
TileRendererLayer tileRendererLayer = new TileRendererLayer(tileCache, mapDataStore,
mapViewPosition, false, true, false, GRAPHIC_FACTORY, hillsRenderConfig) {
@Override
public boolean onTap(LatLong tapLatLong, org.mapsforge.core.model.Point layerXY,
org.mapsforge.core.model.Point tapXY) {
if (zoomControls.contains(new java.awt.Point((int) tapXY.x, (int) tapXY.y))) {
return false;
}
Point pt = new Point((float) tapLatLong.getLongitude(),
(float) tapLatLong.getLatitude());
for (DrawingClickListener listener: MapViewDrawing.this.drawingClickListeners) {
listener.mouseClicked(pt);
}
return true;
}
};
tileRendererLayer.setXmlRenderTheme(InternalRenderTheme.DEFAULT);
return tileRendererLayer;
}
@Override
public void addDrawingClickListener(DrawingClickListener listener) {
this.drawingClickListeners.add(listener);
}
@Override
public void removeDrawingClickListener(DrawingClickListener listener) {
this.drawingClickListeners.remove(listener);
}
protected MarkerAutoScaling createMarker(Point point, Color outer, Color inner,
AlphaMode mode) {
Image image = MarkerUtils.getMarkerForColor(outer, inner, mode);
return new MarkerAutoScaling(convertPoint(point), image);
}
@Override
public MarkerOverlay drawMarker(Point point, Color outer, Color inner, AlphaMode mode) {
return new MapViewMarkerOverlay(createMarker(point, outer, inner, mode), outer, inner,
mode);
}
@Override
public PointSetOverlay createPointSetOverlay() {
return new MapViewPointSetOverlay();
}
@Override
public PointSetOverlay createPointSetOverlay(int width, Color color) {
PointSetOverlay ps = new MapViewPointSetOverlay();
ps.setWidthAndColor(width, color);
return ps;
}
public void drawGraph(File file) {
// Tile cache
TileCache tileCache = AwtUtil.createTileCache(tileSize,
getModel().frameBufferModel.getOverdrawFactor(), 1024,
new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()));
// Layers
Layers layers = getLayerManager().getLayers();
MapDataStore mapDataStore = new MapFile(file);
TileRendererLayer tileRendererLayer = createTileRendererLayer(tileCache, mapDataStore,
getModel().mapViewPosition, null);
layers.add(tileRendererLayer);
BoundingBox boundingBox = mapDataStore.boundingBox();
final Model model = getModel();
if (model.mapViewPosition.getZoomLevel() == 0
|| !boundingBox.contains(model.mapViewPosition.getCenter())) {
byte zoomLevel = LatLongUtils.zoomForBounds(model.mapViewDimension.getDimension(),
boundingBox, model.displayModel.getTileSize());
model.mapViewPosition
.setMapPosition(new MapPosition(boundingBox.getCenterPoint(), zoomLevel));
zoomControls.setZoomLevel(zoomLevel);
}
}
@Override
public void drawGraph(Graph graph, GraphPalette palette) {
throw new RuntimeException("Not implemented, use drawGraph(File).");
}
@Override
public void drawGraph(Graph graph) {
throw new RuntimeException("Not implemented, use drawGraph(File).");
}
@Override
public PathOverlay drawPath(Path path, Color color, boolean markers) {
PolylineAutoScaling line = new PolylineAutoScaling(1, color);
ArrayList<Point> points = new ArrayList<>(path.getArcs().size() * 4);
for (Arc arc: path.getArcs()) {
points.addAll(arc.getPoints());
}
line.addAll(points);
PathOverlay overlay = null;
if (markers) {
MarkerAutoScaling origin = createMarker(path.getOrigin().getPoint(), color, color,
AlphaMode.TRANSPARENT),
destination = createMarker(path.getDestination().getPoint(), color, color,
AlphaMode.TRANSPARENT);
overlay = new MapViewPathOverlay(line, origin, destination);
}
else {
overlay = new MapViewPathOverlay(line);
}
return overlay;
}
@Override
public PathOverlay drawPath(Path path, Color color) {
return drawPath(path, color, true);
}
@Override
public PathOverlay drawPath(Path path) {
return drawPath(path, DEFAULT_PATH_COLOR, true);
}
@Override
public PathOverlay drawPath(Path path, boolean markers) {
return drawPath(path, DEFAULT_PATH_COLOR, markers);
}
}

View File

@@ -0,0 +1,209 @@
package org.insa.graphs.gui.drawing.components;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.ImageObserver;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
public class MapZoomControls {
// Default ID for action events
private static final int ZOOM_IN_ACTION_ID = 0x1;
private static final String ZOOM_IN_ACTION_NAME = "ZoomIn";
private static final int ZOOM_OUT_ACTION_ID = 0x2;
private static final String ZOOM_OUT_ACTION_NAME = "ZoomOut";
// Height
private static final int DEFAULT_HEIGHT = 20;
// Default spacing between mark
private static final int DEFAULT_SPACING = 4;
// Zoom ticks ratio from height (not the current one)
private static final double ZOOM_TICK_HEIGHT_RATIO = 0.3;
// Zoom ticks color
private static final Color ZOOM_TICK_COLOR = Color.GRAY;
// Current zoom ticks ratio from height
private static final double CURRENT_ZOOM_TICK_HEIGHT_RATIO = 0.8;
// Zoom ticks color
private static final Color CURRENT_ZOOM_TICK_COLOR = new Color(25, 25, 25);
// Use half mark or not
private boolean halfMark = true;
private int currentLevel = 0;
private final int minLevel, maxLevel;
// Zoom in/out image and their rectangles.
private final Image zoomIn, zoomOut;
private final Rectangle zoomInRect = new Rectangle(0, 0, 0, 0),
zoomOutRect = new Rectangle(0, 0, 0, 0);
// List of listeners
private final List<ActionListener> zoomInListeners = new ArrayList<>();
private final List<ActionListener> zoomOutListeners = new ArrayList<>();
public MapZoomControls(Component component, final int defaultZoom, final int minZoom,
final int maxZoom) throws IOException {
zoomIn = ImageIO.read(getClass().getResourceAsStream("/zoomIn.png"))
.getScaledInstance(DEFAULT_HEIGHT, DEFAULT_HEIGHT, Image.SCALE_SMOOTH);
zoomOut = ImageIO.read(getClass().getResourceAsStream("/zoomOut.png"))
.getScaledInstance(DEFAULT_HEIGHT, DEFAULT_HEIGHT, Image.SCALE_SMOOTH);
this.currentLevel = defaultZoom;
this.minLevel = minZoom;
this.maxLevel = maxZoom;
component.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (zoomInRect.contains(e.getPoint()) || zoomOutRect.contains(e.getPoint())) {
component.setCursor(new Cursor(Cursor.HAND_CURSOR));
}
else {
component.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
}
});
component.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (zoomInRect.contains(e.getPoint()) && currentLevel < maxLevel) {
currentLevel += 1;
for (ActionListener al: zoomInListeners) {
al.actionPerformed(
new ActionEvent(this, ZOOM_IN_ACTION_ID, ZOOM_IN_ACTION_NAME));
}
}
else if (zoomOutRect.contains(e.getPoint()) && currentLevel > minLevel) {
currentLevel -= 1;
for (ActionListener al: zoomOutListeners) {
al.actionPerformed(
new ActionEvent(this, ZOOM_OUT_ACTION_ID, ZOOM_OUT_ACTION_NAME));
}
}
component.repaint();
}
});
}
/**
* Add a zoom-in listener.
*
* @param listener Zoom-in listener to add to this MapZoomControls instance.
*/
public void addZoomInListener(ActionListener listener) {
this.zoomInListeners.add(listener);
}
/**
* Add a zoom-out listener.
*
* @param listener Zoom-out listener to add to this MapZoomControls instance.
*/
public void addZoomOutListener(ActionListener listener) {
this.zoomOutListeners.add(listener);
}
/**
* @return the current zoom level.
*/
public int getZoomLevel() {
return this.currentLevel;
}
/**
* Set the current zoom level without requesting a redraw.
*
* @param level Zoom level to set.
*/
public void setZoomLevel(int level) {
this.currentLevel = level;
}
/**
* @return Height of this "component" when drawn.
*/
public int getHeight() {
return DEFAULT_HEIGHT;
}
/**
* @return Width of this "component" when drawn.
*/
public int getWidth() {
return DEFAULT_HEIGHT + 2 + (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1 + 2
+ DEFAULT_HEIGHT;
}
/**
* Check if a point is contained inside an element of this zoom controls, useful
* to avoid spurious click listeners.
*
* @param point Point to check.
*
* @return true if the given point correspond to an element of this zoom
* controls.
*/
public boolean contains(Point point) {
return zoomInRect.contains(point) || zoomOutRect.contains(point);
}
protected void draw(Graphics2D g, int xoffset, int yoffset, ImageObserver observer) {
int height = getHeight();
// Draw icon
g.drawImage(zoomOut, xoffset, yoffset, observer);
zoomOutRect.setBounds(xoffset, yoffset, DEFAULT_HEIGHT, DEFAULT_HEIGHT);
g.setStroke(new BasicStroke(1));
// Draw ticks
xoffset += DEFAULT_HEIGHT + 2;
g.setColor(ZOOM_TICK_COLOR);
g.drawLine(xoffset, yoffset + height / 2,
xoffset + (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1,
yoffset + height / 2);
for (int i = 0; i <= (this.maxLevel - this.minLevel); i += halfMark ? 2 : 1) {
g.drawLine(xoffset + i * DEFAULT_SPACING,
yoffset + (int) (height * (1 - ZOOM_TICK_HEIGHT_RATIO) / 2),
xoffset + i * DEFAULT_SPACING,
yoffset + (int) (height * (1 + ZOOM_TICK_HEIGHT_RATIO) / 2));
}
// Draw current ticks
g.setColor(CURRENT_ZOOM_TICK_COLOR);
g.drawLine(xoffset + (currentLevel - this.minLevel) * DEFAULT_SPACING,
yoffset + (int) (height * (1 - CURRENT_ZOOM_TICK_HEIGHT_RATIO) / 2),
xoffset + (currentLevel - this.minLevel) * DEFAULT_SPACING,
yoffset + (int) (height * (1 + CURRENT_ZOOM_TICK_HEIGHT_RATIO) / 2));
xoffset += (this.maxLevel - this.minLevel) * DEFAULT_SPACING + 1 + 2;
g.drawImage(zoomIn, xoffset, yoffset, observer);
zoomInRect.setBounds(xoffset, yoffset, DEFAULT_HEIGHT, DEFAULT_HEIGHT);
}
}

View File

@@ -0,0 +1,177 @@
package org.insa.graphs.gui.drawing.components;
import java.awt.Component;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
public class ZoomAndPanListener implements MouseListener, MouseMotionListener, MouseWheelListener {
public static final int DEFAULT_MIN_ZOOM_LEVEL = -20;
public static final int DEFAULT_MAX_ZOOM_LEVEL = 10;
public static final double DEFAULT_ZOOM_MULTIPLICATION_FACTOR = 1.2;
private Component targetComponent;
private int zoomLevel = 0;
private int minZoomLevel = DEFAULT_MIN_ZOOM_LEVEL;
private int maxZoomLevel = DEFAULT_MAX_ZOOM_LEVEL;
private double zoomMultiplicationFactor = DEFAULT_ZOOM_MULTIPLICATION_FACTOR;
private Point dragStartScreen;
private Point dragEndScreen;
private AffineTransform coordTransform = new AffineTransform();
public ZoomAndPanListener(Component targetComponent) {
this.targetComponent = targetComponent;
}
public ZoomAndPanListener(Component targetComponent, int minZoomLevel, int maxZoomLevel,
double zoomMultiplicationFactor) {
this.targetComponent = targetComponent;
this.minZoomLevel = minZoomLevel;
this.maxZoomLevel = maxZoomLevel;
this.zoomMultiplicationFactor = zoomMultiplicationFactor;
}
public void translate(double dx, double dy) {
coordTransform.translate(dx, dy);
targetComponent.repaint();
}
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
dragStartScreen = e.getPoint();
dragEndScreen = null;
}
public void mouseReleased(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mouseMoved(MouseEvent e) {
}
public void mouseDragged(MouseEvent e) {
moveCamera(e);
}
public void mouseWheelMoved(MouseWheelEvent e) {
zoomCamera(e);
}
private void moveCamera(MouseEvent e) {
try {
dragEndScreen = e.getPoint();
Point2D.Float dragStart = transformPoint(dragStartScreen);
Point2D.Float dragEnd = transformPoint(dragEndScreen);
double dx = dragEnd.getX() - dragStart.getX();
double dy = dragEnd.getY() - dragStart.getY();
coordTransform.translate(dx, dy);
dragStartScreen = dragEndScreen;
dragEndScreen = null;
targetComponent.repaint();
}
catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
private void zoomCamera(MouseWheelEvent e) {
try {
int wheelRotation = e.getWheelRotation();
Point p = e.getPoint();
if (wheelRotation < 0) {
if (zoomLevel < maxZoomLevel) {
zoomLevel++;
Point2D p1 = transformPoint(p);
coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor);
Point2D p2 = transformPoint(p);
coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY());
targetComponent.repaint();
}
}
else {
if (zoomLevel > minZoomLevel) {
zoomLevel--;
Point2D p1 = transformPoint(p);
coordTransform.scale(1 / zoomMultiplicationFactor,
1 / zoomMultiplicationFactor);
Point2D p2 = transformPoint(p);
coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY());
targetComponent.repaint();
}
}
}
catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
private Point2D.Float transformPoint(Point p1) throws NoninvertibleTransformException {
AffineTransform inverse = coordTransform.createInverse();
Point2D.Float p2 = new Point2D.Float();
inverse.transform(p1, p2);
return p2;
}
public int getZoomLevel() {
return zoomLevel;
}
public void setZoomLevel(int zoomLevel) {
this.zoomLevel = zoomLevel;
}
public void zoomIn() {
try {
Point p = new Point(targetComponent.getWidth() / 2, targetComponent.getHeight() / 2);
zoomLevel++;
Point2D p1 = transformPoint(p);
coordTransform.scale(zoomMultiplicationFactor, zoomMultiplicationFactor);
Point2D p2 = transformPoint(p);
coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY());
targetComponent.repaint();
}
catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
public void zoomOut() {
try {
Point p = new Point(targetComponent.getWidth() / 2, targetComponent.getHeight() / 2);
zoomLevel--;
Point2D p1 = transformPoint(p);
coordTransform.scale(1 / zoomMultiplicationFactor, 1 / zoomMultiplicationFactor);
Point2D p2 = transformPoint(p);
coordTransform.translate(p2.getX() - p1.getX(), p2.getY() - p1.getY());
targetComponent.repaint();
}
catch (NoninvertibleTransformException ex) {
ex.printStackTrace();
}
}
public AffineTransform getCoordTransform() {
return coordTransform;
}
public void setCoordTransform(AffineTransform coordTransform) {
this.coordTransform = coordTransform;
}
}

View File

@@ -0,0 +1,73 @@
package org.insa.graphs.gui.drawing.overlays;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import org.mapsforge.core.graphics.Canvas;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.Point;
import org.mapsforge.map.awt.graphics.AwtBitmap;
import org.mapsforge.map.layer.overlay.Marker;
/**
* Class extending the default Mapsforge's {@link Marker} with auto-scaling.
*
* Mapsforge's Markers do not scale with zoom level, this class aims at
* correcting this. Internally, this image stores an {@link Image} instance and
* scale it when a redraw is requested.
*
* @see MarkerUtils#getMarkerForColor(java.awt.Color, java.awt.Color,
* org.insa.graphics.drawing.Drawing.AlphaMode)
* @see PaintUtils#getStrokeWidth(int, byte)
*/
public class MarkerAutoScaling extends Marker {
// Original image.
private Image image;
/**
* Create a new MarkerAutoScaling at the given position with the given image.
*
* @param latLong Initial position of the marker.
* @param image Image for this marker.
*/
public MarkerAutoScaling(LatLong latLong, Image image) {
super(latLong, null, 0, 0);
this.image = image;
}
/**
* Set a new image for this marker overlay
*
* @param image New image to set.
*/
public void setImage(Image image) {
this.image = image;
}
/**
* @return Current image (marker) of this overlay.
*/
public Image getImage() {
return image;
}
@Override
public synchronized void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas,
Point topLeftPoint) {
int width = (int) PaintUtils.getStrokeWidth(8, zoomLevel),
height = (int) PaintUtils.getStrokeWidth(16, zoomLevel);
BufferedImage bfd = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
Graphics2D g = bfd.createGraphics();
g.drawImage(
this.image.getScaledInstance(bfd.getWidth(), bfd.getHeight(), Image.SCALE_SMOOTH),
0, 0, null);
setBitmap(new AwtBitmap(bfd));
setVerticalOffset(-height / 2);
super.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
g.dispose();
}
}

View File

@@ -0,0 +1,19 @@
package org.insa.graphs.gui.drawing.overlays;
import org.insa.graphs.model.Point;
public interface MarkerOverlay extends Overlay {
/**
* @return The current position of this marker.
*/
public Point getPoint();
/**
* Move this marker to the specified location.
*
* @param point New position for the marker.
*/
public void moveTo(Point point);
}

View File

@@ -0,0 +1,97 @@
package org.insa.graphs.gui.drawing.overlays;
import java.awt.Color;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.DataInputStream;
import org.insa.graphs.gui.drawing.Drawing.AlphaMode;
public class MarkerUtils {
/**
* Create an image to represent a marker using the given color for the outer and
* inner part, and the given mode for the inner part.
*
* @param outer Outer color of the marker.
* @param inner Inner color of the marker.
* @param mode Mode to use to fill the inner part of the marker.
*
* @return An image representing a marker.
*/
public static Image getMarkerForColor(Color outer, Color inner, AlphaMode mode) {
// create image
int[][] mask = readMarkerMask();
BufferedImage image = new BufferedImage(mask[0].length, mask.length,
BufferedImage.TYPE_4BYTE_ABGR);
// Color[] map = getColorMapping(color);
int outerRGB = outer.getRGB() & 0x00ffffff;
for (int i = 0; i < image.getHeight(); ++i) {
for (int j = 0; j < image.getWidth(); ++j) {
// If we are in the "inner" part of the marker...
if (i >= MIN_Y_CENTER && i < MAX_Y_CENTER && j >= MIN_X_CENTER && j < MAX_X_CENTER
&& mask[i][j] != MAXIMUM_INNER_MASK_VALUE) {
// Don't ask... https://stackoverflow.com/a/29321264/2666289
// Basically, this compute a "middle" color between outer and inner depending on
// the current mask value.
double t = 1 - (mask[i][j] - MINIMUM_INNER_MASK_VALUE)
/ (double) (MAXIMUM_INNER_MASK_VALUE - MINIMUM_INNER_MASK_VALUE);
int r = (int) Math.sqrt((1 - t) * outer.getRed() * outer.getRed()
+ t * inner.getRed() * inner.getRed());
int g = (int) Math.sqrt((1 - t) * outer.getGreen() * outer.getGreen()
+ t * inner.getGreen() * inner.getGreen());
int b = (int) Math.sqrt((1 - t) * outer.getBlue() * outer.getBlue()
+ t * inner.getBlue() * inner.getBlue());
int a = mode == AlphaMode.OPAQUE ? MAXIMUM_INNER_MASK_VALUE : mask[i][j];
image.setRGB(j, i, (a << 24) | (r << 16) | (g << 8) | b);
}
// Otherwize, just fill with the outer color and set the alpha value properly.
else {
image.setRGB(j, i, outerRGB | (mask[i][j] << 24));
}
}
}
return image;
}
// Mask cache
private static int[][] MASK_CACHE = null;
// Hand-made... These defines the "center" of the marker, that can be filled
// with a different color.
private static final int MIN_X_CENTER = 40, MAX_X_CENTER = 101, MIN_Y_CENTER = 40,
MAX_Y_CENTER = 100;
private static final int MINIMUM_INNER_MASK_VALUE = 116, MAXIMUM_INNER_MASK_VALUE = 249;
/**
* @return Retrieve the mask from the mask file or from the cache.
*/
private static int[][] readMarkerMask() {
if (MASK_CACHE == null) {
try {
DataInputStream dis = new DataInputStream(
MarkerUtils.class.getResourceAsStream("/marker_mask.bin"));
int nrows = dis.readInt();
int ncols = dis.readInt();
MASK_CACHE = new int[nrows][ncols];
for (int i = 0; i < nrows; ++i) {
for (int j = 0; j < ncols; ++j) {
MASK_CACHE[i][j] = dis.readUnsignedByte();
}
}
dis.close();
}
catch (Exception e) {
e.printStackTrace();
MASK_CACHE = null;
}
}
return MASK_CACHE;
}
}

View File

@@ -0,0 +1,42 @@
package org.insa.graphs.gui.drawing.overlays;
import java.awt.Color;
public interface Overlay {
/**
* Set the color of this overlay.
*
* @param color New color for the overlay.
*/
public void setColor(Color color);
/**
* @return The current color of this overlay.
*/
public Color getColor();
/**
* Show or hide this marker - A marker should be visible when created.
*
* @param visible true to show the marker, false to hide.
*/
public void setVisible(boolean visible);
/**
* @return true if this overlay is visible.
*/
public boolean isVisible();
/**
* Delete this marker.
*/
public void delete();
/**
* Request a redraw of this overlay - This can start a full redraw of the inner
* drawing.
*/
public void redraw();
}

View File

@@ -0,0 +1,65 @@
package org.insa.graphs.gui.drawing.overlays;
import java.awt.Color;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.map.awt.graphics.AwtGraphicFactory;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.layer.overlay.Polyline;
public class PaintUtils {
// Graphic factory.
private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE;
/**
* Convert the given AWT color to a mapsforge compatible color.
*
* @param color AWT color to convert.
*
* @return Integer value representing a corresponding mapsforge color.
*/
public static int convertColor(Color color) {
return GRAPHIC_FACTORY.createColor(color.getAlpha(), color.getRed(), color.getGreen(),
color.getBlue());
}
/**
* Convert the given mapsforge color to an AWT Color.
*
* @param color Integer value representing a mapsforge color.
*
* @return AWT color corresponding to the given value.
*/
public static Color convertColor(int color) {
return new Color(color, true);
}
/**
* Compute an updated value for the given width at the given zoom level. This
* function can be used to automatically scale {@link Polyline} or
* {@link Marker} when zooming (which is not done by default in Mapsforge).
*
* @param width Original width to convert.
* @param zoomLevel Zoom level for which the width should be computed.
*
* @return Actual width at the given zoom level.
*/
public static float getStrokeWidth(int width, byte zoomLevel) {
float mul = 1;
if (zoomLevel < 6) {
mul = 1;
}
else if (zoomLevel < 10) {
mul += (zoomLevel - 5) * 0.5;
}
else if (zoomLevel < 13) {
mul = 3.5f;
}
else {
mul += 2 * (zoomLevel - 8) / 3;
}
return width * mul;
}
}

View File

@@ -0,0 +1,5 @@
package org.insa.graphs.gui.drawing.overlays;
public interface PathOverlay extends Overlay {
}

View File

@@ -0,0 +1,70 @@
package org.insa.graphs.gui.drawing.overlays;
import java.awt.Color;
import org.insa.graphs.model.Point;
public interface PointSetOverlay extends Overlay {
/**
* Set the width of this overlay for future addPoint().
*
* @param width New default width for this overlay.
*/
public void setWidth(int width);
/**
* Set color and width for this overlay for future addPoint().
*
* @param width New default width for this overlay.
* @param color New default color for this overlay.
*/
public void setWidthAndColor(int width, Color color);
/**
* Add a new point using the current width and color.
*
* @param point Position of the point to add.
*
* @see #setWidth(int)
* @see #setColor(Color)
*/
public void addPoint(Point point);
/**
* Set the current width and then add a new point.
*
* @param point Position of the point to add.
* @param width New default width for this overlay.
*
* @see #setWidth(int)
* @see PointSetOverlay#addPoint(Point)
*/
public void addPoint(Point point, int width);
/**
* Set the current color and then add a new point.
*
* @param point Position of the point to add.
* @param color New default color for this overlay.
*
* @see #setColor(Color)
* @see PointSetOverlay#addPoint(Point)
*/
public void addPoint(Point point, Color color);
/**
* Add a new point at the given location, with the given color and width, and
* update the current width and color.
*
* @param point Position of the point to add.
* @param width New default width for this overlay.
* @param color New default color for this overlay.
*
* @see #setWidth(int)
* @see #setColor(Color)
* @see PointSetOverlay#addPoint(Point)
*/
public void addPoint(Point point, int width, Color color);
}

View File

@@ -0,0 +1,92 @@
package org.insa.graphs.gui.drawing.overlays;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collection;
import org.insa.graphs.model.Point;
import org.mapsforge.core.graphics.Canvas;
import org.mapsforge.core.graphics.GraphicFactory;
import org.mapsforge.core.graphics.Style;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.awt.graphics.AwtGraphicFactory;
import org.mapsforge.map.layer.overlay.Polyline;
/**
* Class extending the default Mapsforge's {@link Polyline} with auto-scaling.
*
* Mapsforge's Polylines do not scale with zoom level, this class aims at
* correcting this. When a redraw is requested, the width of the line is
* recomputed for the current zoom level.
*
* @see PaintUtils#getStrokeWidth(int, byte)
*/
public class PolylineAutoScaling extends Polyline {
// Graphic factory.
private static final GraphicFactory GRAPHIC_FACTORY = AwtGraphicFactory.INSTANCE;
// Original width of the polyline.
private final int width;
/**
* Create a new PolylineAutoScaling with the given width and color.
*
* @param width Original width of the line (independent of the zoom level).
* @param color Color of the line.
*
* @see PaintUtils#getStrokeWidth(int, byte)
*/
public PolylineAutoScaling(int width, Color color) {
super(GRAPHIC_FACTORY.createPaint(), GRAPHIC_FACTORY);
getPaintStroke().setColor(PaintUtils.convertColor(color));
getPaintStroke().setStyle(Style.STROKE);
this.width = width;
}
/**
* Set the color for this polyline.
*
* @param color New color for this polyline.
*/
public void setColor(Color color) {
getPaintStroke().setColor(PaintUtils.convertColor(color));
}
/**
* @return Color of this polyline.
*/
public Color getColor() {
return PaintUtils.convertColor(getPaintStroke().getColor());
}
/**
* @param point Point to add to this line.
*/
public void add(Point point) {
getLatLongs().add(new LatLong(point.getLatitude(), point.getLongitude()));
}
/**
* @param points Points to add to this line.
*/
public void addAll(Collection<? extends Point> points) {
ArrayList<LatLong> latlongs = new ArrayList<>(points.size());
for (Point point: points) {
latlongs.add(new LatLong(point.getLatitude(), point.getLongitude()));
}
getLatLongs().addAll(latlongs);
}
@Override
public synchronized void draw(BoundingBox boundingBox, byte zoomLevel, Canvas canvas,
org.mapsforge.core.model.Point topLeftPoint) {
// Update paint stroke with width for level
this.getPaintStroke().setStrokeWidth(PaintUtils.getStrokeWidth(width, zoomLevel));
super.draw(boundingBox, zoomLevel, canvas, topLeftPoint);
}
}

View File

@@ -0,0 +1,42 @@
package org.insa.graphs.gui.observers;
import java.awt.Color;
import org.insa.graphs.algorithm.shortestpath.ShortestPathObserver;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.overlays.PointSetOverlay;
import org.insa.graphs.model.Node;
public class ShortestPathGraphicObserver implements ShortestPathObserver {
// Drawing and Graph drawing
protected Drawing drawing;
protected PointSetOverlay psOverlay1, psOverlay2;
public ShortestPathGraphicObserver(Drawing drawing) {
this.drawing = drawing;
psOverlay1 = drawing.createPointSetOverlay(1, Color.CYAN);
psOverlay2 = drawing.createPointSetOverlay(1, Color.BLUE);
}
@Override
public void notifyOriginProcessed(Node node) {
// drawing.drawMarker(node.getPoint(), Color.RED);
}
@Override
public void notifyNodeReached(Node node) {
psOverlay1.addPoint(node.getPoint());
}
@Override
public void notifyNodeMarked(Node node) {
psOverlay2.addPoint(node.getPoint());
}
@Override
public void notifyDestinationReached(Node node) {
// drawing.drawMarker(node.getPoint(), Color.RED);
}
}

View File

@@ -0,0 +1,42 @@
package org.insa.graphs.gui.observers;
import java.awt.Color;
import java.util.ArrayList;
import org.insa.graphs.algorithm.weakconnectivity.WeaklyConnectedComponentObserver;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.overlays.PointSetOverlay;
import org.insa.graphs.model.Node;
public class WeaklyConnectedComponentGraphicObserver implements WeaklyConnectedComponentObserver {
private static final Color[] COLORS = { Color.BLUE, Color.ORANGE, Color.GREEN, Color.YELLOW,
Color.RED };
// Drawing + Graph drawing
private PointSetOverlay grPoints;
// Current index color
private int cindex = 0;
public WeaklyConnectedComponentGraphicObserver(Drawing drawing) {
this.grPoints = drawing.createPointSetOverlay();
this.grPoints.setWidth(1);
}
@Override
public void notifyStartComponent(Node curNode) {
this.grPoints.setColor(COLORS[cindex]);
}
@Override
public void notifyNewNodeInComponent(Node node) {
this.grPoints.addPoint(node.getPoint());
}
@Override
public void notifyEndComponent(ArrayList<Node> nodes) {
cindex = (cindex + 1) % COLORS.length;
}
}

View File

@@ -0,0 +1,73 @@
package org.insa.graphs.gui.simple;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import org.insa.graphs.gui.drawing.Drawing;
import org.insa.graphs.gui.drawing.components.BasicDrawing;
import org.insa.graphs.model.Graph;
import org.insa.graphs.model.Path;
import org.insa.graphs.model.io.BinaryGraphReader;
import org.insa.graphs.model.io.GraphReader;
import org.insa.graphs.model.io.PathReader;
public class Launch {
/**
* Create a new Drawing inside a JFrame an return it.
*
* @return The created drawing.
*
* @throws Exception if something wrong happens when creating the graph.
*/
public static Drawing createDrawing() throws Exception {
BasicDrawing basicDrawing = new BasicDrawing();
SwingUtilities.invokeAndWait(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("BE Graphes - Launch");
frame.setLayout(new BorderLayout());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
frame.setSize(new Dimension(800, 600));
frame.setContentPane(basicDrawing);
frame.validate();
}
});
return basicDrawing;
}
public static void main(String[] args) throws Exception {
// Visit these directory to see the list of available files on Commetud.
final String mapName = "/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Maps/insa.mapgr";
final String pathName = "/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Paths/path_fr31insa_rangueil_r2.path";
// Create a graph reader.
final GraphReader reader = new BinaryGraphReader(
new DataInputStream(new BufferedInputStream(new FileInputStream(mapName))));
// TODO: Read the graph.
final Graph graph = null;
// Create the drawing:
final Drawing drawing = createDrawing();
// TODO: Draw the graph on the drawing.
// TODO: Create a PathReader.
final PathReader pathReader = null;
// TODO: Read the path.
final Path path = null;
// TODO: Draw the path.
}
}

View File

@@ -0,0 +1,21 @@
package org.insa.graphs.gui.utils;
import java.awt.Color;
public class ColorUtils {
private static final Color[] COLORS = { // List of available colors
new Color(57, 172, 115), // Forest (Green)
new Color(246, 67, 63), // Red
new Color(110, 56, 172), // Purple
new Color(53, 191, 179), // Cyan
new Color(219, 136, 48), // Orange / Brown
new Color(110, 110, 110), // Gray
new Color(56, 104, 172) // Blue
};
public static Color getColor(int i) {
return COLORS[i % COLORS.length];
}
}

View File

@@ -0,0 +1,151 @@
package org.insa.graphs.gui.utils;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.EnumMap;
import java.util.Map;
import java.util.prefs.Preferences;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
public class FileUtils {
// Preferences
private static Preferences preferences = Preferences.userRoot().node(FileUtils.class.getName());
/**
* Type of folder with associated preferred folder and path filters.
*
*/
public enum FolderType {
/**
* Folder type for graph files input (*.mapgr).
*/
Map,
/**
* Folder type for path inputs (*.path).
*/
PathInput,
/**
* Folder type for path outputs (*.path).
*/
PathOutput
}
private static class PreferencesEntry {
public String key;
public String value;
public PreferencesEntry(String key, String value) {
this.key = key;
this.value = value;
}
}
// Map folder type -> PreferencesEntry
private static final Map<FolderType, PreferencesEntry> folderToEntry = new EnumMap<>(
FolderType.class);
// Map folder type -> File Filter
private static final Map<FolderType, FileFilter> folderToFilter = new EnumMap<>(
FolderType.class);
static {
// Populate folderToEntry
folderToEntry.put(FolderType.Map, new PreferencesEntry("DefaultMapFolder",
"/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Maps"));
folderToEntry.put(FolderType.PathInput, new PreferencesEntry("DefaultPathInputFolder",
"/home/commetud/3eme Annee MIC/Graphes-et-Algorithmes/Paths"));
folderToEntry.put(FolderType.PathOutput,
new PreferencesEntry("DefaultPathOutputsFolder", "paths"));
// Populate folderToFilter
folderToFilter.put(FolderType.Map, new FileNameExtensionFilter("Graph files", "mapgr"));
folderToFilter.put(FolderType.PathInput, new FileNameExtensionFilter("Path files", "path"));
folderToFilter.put(FolderType.PathOutput,
new FileNameExtensionFilter("Path files", "path"));
}
/**
* @param folderType Type of folder to retrieve.
*
* @return A File instance pointing to the preferred folder for the given type.
*
* @see FolderType
*/
public static File getPreferredFolder(FolderType folderType) {
PreferencesEntry entry = folderToEntry.get(folderType);
File folder = new File(preferences.get(entry.key, entry.value));
if (!folder.exists()) {
folder = new File(System.getProperty("user.dir"));
}
return folder;
}
/**
* @param folderType Type of folder to update.
* @param newPreferredFolder New preferred folder.
*/
public static void updatePreferredFolder(FolderType folderType, File newPreferredFolder) {
PreferencesEntry entry = folderToEntry.get(folderType);
preferences.put(entry.key, newPreferredFolder.getAbsolutePath());
}
/**
* @param folderType Type of folder for which the filter should be retrieved.
*
* @return A FileFilter corresponding to input graph files.
*/
public static FileFilter getFileFilter(FolderType folderType) {
return folderToFilter.get(folderType);
}
/**
* @param folderType Type of folder for which a file chooser should be created.
* @param defaultFileName Default file name to show, or null to not show any
* file.
*
* @return A new JFileChooser pointing to the preferred folder for the given
* folderType, with the given default file selected (if given).
*/
public static JFileChooser createFileChooser(FolderType folderType, String defaultFileName) {
JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(getPreferredFolder(folderType));
if (defaultFileName != null) {
chooser.setSelectedFile(new File(chooser.getCurrentDirectory().getAbsolutePath()
+ File.separator + defaultFileName));
}
chooser.setFileFilter(getFileFilter(folderType));
chooser.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals(JFileChooser.APPROVE_SELECTION)) {
if (chooser.getSelectedFile().exists()) {
updatePreferredFolder(folderType,
chooser.getSelectedFile().getParentFile());
}
}
}
});
return chooser;
}
/**
* @param folderType Type of folder for which a file chooser should be created.
*
* @return A new JFileChooser pointing to the preferred folder for the given
* folderType.
*
* @see #createFileChooser(FolderType, String)
*/
public static JFileChooser createFileChooser(FolderType folderType) {
return createFileChooser(folderType, null);
}
}