package maps.gml.editor; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.Insets; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JSplitPane; import javax.swing.JTextField; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import maps.MapException; import maps.MapReader; import maps.MapWriter; import maps.gml.GMLCoordinates; import maps.gml.GMLMap; import maps.gml.GMLObject; import maps.gml.formats.RobocupFormat; import maps.gml.view.GMLMapViewer; import maps.gml.view.GMLObjectInspector; import rescuecore2.log.Logger; /** A component for editing GML maps. */ public class GMLEditor extends JPanel { private static final int VIEWER_PREFERRED_SIZE = 500; private static final int INSPECTOR_PREFERRED_WIDTH = 300; private static final int INSPECTOR_PREFERRED_HEIGHT = 500; private static final double SNAP_MIN_RESOLUTION = 0.001; private static final double SNAP_MAX_RESOLUTION = 1000; private static final NumberFormat FORMAT = new DecimalFormat("#0.000"); private GMLMap map; private GMLMapViewer viewer; private GMLObjectInspector inspector; private JLabel x; private JLabel y; private boolean changed; private ViewerMouseListener viewerMouseListener; private Tool currentTool; private UndoManager undoManager; private Action undoAction; private Action redoAction; private File saveFile; private File baseDir; private Snap snap; /** Construct a new GMLEditor. @param menuBar The menu bar to add menus to. */ public GMLEditor(JMenuBar menuBar) { super(new BorderLayout()); map = new GMLMap(); viewer = new GMLMapViewer(map); inspector = new GMLObjectInspector(map); undoManager = new UndoManager(); viewer.setPreferredSize(new Dimension(VIEWER_PREFERRED_SIZE, VIEWER_PREFERRED_SIZE)); inspector.setPreferredSize(new Dimension(INSPECTOR_PREFERRED_WIDTH, INSPECTOR_PREFERRED_HEIGHT)); viewer.setBackground(Color.GRAY); viewer.getPanZoomListener().setPanOnRightMouse(); snap = new Snap(); changed = false; x = new JLabel("X: "); y = new JLabel("Y: "); JToolBar fileToolbar = new JToolBar("File"); JToolBar viewToolbar = new JToolBar("View"); JToolBar editToolbar = new JToolBar("Edit"); JToolBar toolsToolbar = new JToolBar("Tools"); JToolBar functionsToolbar = new JToolBar("Functions"); JMenu fileMenu = new JMenu("File", false); JMenu viewMenu = new JMenu("View", false); JMenu editMenu = new JMenu("Edit", false); JMenu toolsMenu = new JMenu("Tools", false); JMenu functionsMenu = new JMenu("Functions", false); createFileActions(fileMenu, fileToolbar); createViewActions(viewMenu, viewToolbar); createEditActions(editMenu, editToolbar); createToolActions(toolsMenu, toolsToolbar); createFunctionActions(functionsMenu, functionsToolbar); JPanel main = new JPanel(new BorderLayout()); JPanel labels = new JPanel(new GridLayout(1, 2)); labels.add(x); labels.add(y); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, viewer, inspector); main.add(split, BorderLayout.CENTER); main.add(labels, BorderLayout.SOUTH); add(main, BorderLayout.CENTER); JPanel toolbars = new JPanel(new GridLayout(0, 1)); toolbars.add(fileToolbar); toolbars.add(viewToolbar); toolbars.add(editToolbar); toolbars.add(toolsToolbar); toolbars.add(functionsToolbar); add(toolbars, BorderLayout.NORTH); menuBar.add(fileMenu); menuBar.add(viewMenu); menuBar.add(editMenu); menuBar.add(toolsMenu); menuBar.add(functionsMenu); viewerMouseListener = new ViewerMouseListener(); viewer.addMouseListener(viewerMouseListener); viewer.addMouseMotionListener(viewerMouseListener); baseDir = new File(System.getProperty("user.dir")); } /** Entry point. @param args Command line arguments. */ public static void main(String[] args) { final JFrame frame = new JFrame("GMLEditor"); JMenuBar menuBar = new JMenuBar(); final GMLEditor editor = new GMLEditor(menuBar); if (args.length > 0 && args[0].length() > 0) { try { editor.load(args[0]); } catch (CancelledByUserException e) { return; } catch (MapException e) { e.printStackTrace(); } } frame.setJMenuBar(menuBar); frame.setContentPane(editor); frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); frame.pack(); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { try { editor.close(); frame.setVisible(false); frame.dispose(); System.exit(0); } catch (CancelledByUserException ex) { frame.setVisible(true); } } }); frame.setVisible(true); } /** Load a map by showing a file chooser dialog. @throws CancelledByUserException If the user cancels the change due to unsaved changes. @throws MapException If there is a problem reading the map. */ public void load() throws CancelledByUserException, MapException { JFileChooser chooser = new JFileChooser(baseDir); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { load(chooser.getSelectedFile()); } } /** Load a map from a file. @param filename The name of the file to read. @throws CancelledByUserException If the user cancels the change due to unsaved changes. @throws MapException If there is a problem reading the map. */ public void load(String filename) throws CancelledByUserException, MapException { load(new File(filename)); } /** Load a map from a file. @param file The file to read. @throws CancelledByUserException If the user cancels the change due to unsaved changes. @throws MapException If there is a problem reading the map. */ public void load(File file) throws CancelledByUserException, MapException { setMap((GMLMap)MapReader.readMap(file)); saveFile = file; baseDir = saveFile.getParentFile(); } /** Load a map from a reader. @param reader The reader to read. @throws CancelledByUserException If the user cancels the change due to unsaved changes. @throws IOException If there is a problem reading the map. @throws GMLException If there is a problem reading the map. */ // public void load(Reader reader) throws CancelledByUserException, IOException, GMLException { // setMap((GMLMap)MapReader.readGMLMap(reader)); // } /** Load a map from an XML document. @param document The document to load. @throws CancelledByUserException If the user cancels the change due to unsaved changes. @throws GMLException If there is a problem reading the map. */ // public void load(Document document) throws CancelledByUserException, GMLException { // setMap(MapReader.readGMLMap(document)); // } /** Set the map. @param newMap The new map. @throws CancelledByUserException If the user cancels the change due to unsaved changes. */ public void setMap(GMLMap newMap) throws CancelledByUserException { checkForChanges(); map = newMap; changed = false; viewer.setMap(map); inspector.setMap(map); viewer.repaint(); } /** Get the map. @return The map. */ public GMLMap getMap() { return map; } /** Save the map. @throws MapException If there is a problem saving the map. */ public void save() throws MapException { if (saveFile == null) { JFileChooser chooser = new JFileChooser(baseDir); if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { saveFile = chooser.getSelectedFile(); } } if (saveFile != null) { Logger.debug("Saving to " + saveFile.getAbsolutePath()); MapWriter.writeMap(map, saveFile, RobocupFormat.INSTANCE); baseDir = saveFile.getParentFile(); changed = false; } } /** Close the editor. @throws CancelledByUserException If the user cancels the close due to unsaved changes." */ public void close() throws CancelledByUserException { checkForChanges(); } /** Get the map viewer. @return The map viewer. */ public GMLMapViewer getViewer() { return viewer; } /** Get the object inspector. @return The object inspector. */ public GMLObjectInspector getInspector() { return inspector; } /** Register a change to the map. */ public void setChanged() { changed = true; } /** Register an undoable edit. @param edit The edit to add. */ public void addEdit(UndoableEdit edit) { undoManager.addEdit(edit); undoAction.setEnabled(undoManager.canUndo()); redoAction.setEnabled(undoManager.canRedo()); } /** Snap coordinates to the grid. @param c The coordinates to snap. @return The passed-in coordinates object. */ public GMLCoordinates snap(GMLCoordinates c) { snap.snap(c); return c; } private void updatePositionLabels(final Point p) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { GMLCoordinates c = viewer.getCoordinatesAtPoint(p.x, p.y); x.setText("X: " + FORMAT.format(c.getX())); y.setText("Y: " + FORMAT.format(c.getY())); } }); } private void checkForChanges() throws CancelledByUserException { if (changed) { switch (JOptionPane.showConfirmDialog(null, "The current map has changes. Do you want to save them?")) { case JOptionPane.YES_OPTION: try { save(); } catch (MapException e) { JOptionPane.showMessageDialog(null, e); throw new CancelledByUserException(); } break; case JOptionPane.NO_OPTION: changed = false; return; case JOptionPane.CANCEL_OPTION: throw new CancelledByUserException(); default: throw new RuntimeException("JOptionPane.showConfirmDialog returned something weird"); } } } private void createFileActions(JMenu menu, JToolBar toolbar) { Action newAction = new AbstractAction("New") { @Override public void actionPerformed(ActionEvent e) { try { checkForChanges(); setMap(new GMLMap()); } catch (CancelledByUserException ex) { return; } } }; Action loadAction = new AbstractAction("Load") { @Override public void actionPerformed(ActionEvent e) { try { checkForChanges(); load(); } catch (CancelledByUserException ex) { return; } catch (MapException ex) { JOptionPane.showMessageDialog(null, ex); } } }; Action saveAction = new AbstractAction("Save") { @Override public void actionPerformed(ActionEvent e) { try { save(); } catch (MapException ex) { JOptionPane.showMessageDialog(null, ex); } } }; Action saveAsAction = new AbstractAction("Save as") { @Override public void actionPerformed(ActionEvent e) { try { saveFile = null; save(); } catch (MapException ex) { JOptionPane.showMessageDialog(null, ex); } } }; toolbar.add(newAction); toolbar.add(loadAction); toolbar.add(saveAction); toolbar.add(saveAsAction); menu.add(newAction); menu.add(loadAction); menu.add(saveAction); menu.add(saveAsAction); } private void createViewActions(JMenu menu, JToolBar toolbar) { final JCheckBox snapBox = new JCheckBox("Snap to grid", snap.isEnabled()); final JSpinner snapSpinner = new JSpinner(new SpinnerNumberModel(snap.getResolution(), SNAP_MIN_RESOLUTION, SNAP_MAX_RESOLUTION, SNAP_MIN_RESOLUTION)); snapSpinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { snap.setResolution((Double)snapSpinner.getValue()); } }); snapBox.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { snap.setEnabled(snapBox.isSelected()); } }); Action gridAction = new AbstractAction("Show grid") { @Override public void actionPerformed(ActionEvent e) { viewer.setGridEnabled((Boolean)getValue(Action.SELECTED_KEY)); viewer.repaint(); } }; gridAction.putValue(Action.SELECTED_KEY, false); toolbar.add(snapSpinner); toolbar.add(snapBox); toolbar.add(new JToggleButton(gridAction)); menu.add(new JCheckBoxMenuItem(gridAction)); // Create the "show objects" button and textfield final JTextField showField = new JTextField(); JButton showButton = new JButton("Show"); showButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String s = showField.getText(); List<GMLObject> objects = new ArrayList<GMLObject>(); for (String next : s.split(",")) { int id = Integer.parseInt(next.trim()); GMLObject o = map.getObject(id); if (o != null) { objects.add(o); } } viewer.view(objects); viewer.repaint(); } }); toolbar.addSeparator(); toolbar.add(showField); toolbar.add(showButton); // Add the reset zoom action Action resetZoom = new AbstractAction("Reset zoom") { @Override public void actionPerformed(ActionEvent e) { viewer.viewAll(); viewer.repaint(); } }; toolbar.addSeparator(); menu.addSeparator(); toolbar.add(resetZoom); menu.add(resetZoom); } private void createEditActions(JMenu menu, JToolBar toolbar) { undoAction = new AbstractAction("Undo") { @Override public void actionPerformed(ActionEvent e) { try { undoManager.undo(); } catch (CannotUndoException ex) { JOptionPane.showMessageDialog(null, ex); } setEnabled(undoManager.canUndo()); redoAction.setEnabled(undoManager.canRedo()); } }; redoAction = new AbstractAction("Redo") { @Override public void actionPerformed(ActionEvent e) { try { undoManager.redo(); } catch (CannotUndoException ex) { JOptionPane.showMessageDialog(null, ex); } setEnabled(undoManager.canRedo()); undoAction.setEnabled(undoManager.canUndo()); } }; undoAction.setEnabled(false); redoAction.setEnabled(false); toolbar.add(undoAction); toolbar.add(redoAction); menu.add(undoAction); menu.add(redoAction); } private void createToolActions(JMenu menu, JToolBar toolbar) { ButtonGroup toolbarGroup = new ButtonGroup(); ButtonGroup menuGroup = new ButtonGroup(); addTool(new InspectTool(this), menu, toolbar, menuGroup, toolbarGroup); menu.addSeparator(); toolbar.addSeparator(); addTool(new CreateNodeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new CreateEdgeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new CreateRoadTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new CreateBuildingTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new CreateSpaceTool(this), menu, null, menuGroup, null); menu.addSeparator(); toolbar.addSeparator(); addTool(new DeleteNodeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new DeleteEdgeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new DeleteShapeTool(this), menu, toolbar, menuGroup, toolbarGroup); menu.addSeparator(); toolbar.addSeparator(); addTool(new MoveNodeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new MergeNodesTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new MergeLinesTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new SplitEdgeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new SplitShapeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new TogglePassableTool(this), menu, toolbar, menuGroup, toolbarGroup); } private void createFunctionActions(JMenu menu, JToolBar toolbar) { addFunction(new ScaleFunction(this), menu, toolbar); addFunction(new FixNearbyNodesFunction(this), menu, toolbar); addFunction(new FixDuplicateEdgesFunction(this), menu, toolbar); addFunction(new SplitEdgesFunction(this), menu, toolbar); addFunction(new ComputePassableEdgesFunction(this), menu, toolbar); addFunction(new PruneOrphanNodesFunction(this), menu, toolbar); addFunction(new PruneOrphanEdgesFunction(this), menu, toolbar); addFunction(new FixDegenerateShapesFunction(this), menu, toolbar); addFunction(new FixAttachedObjectsFunction(this), menu, toolbar); addFunction(new ValidateFunction(this), menu, toolbar); addFunction(new AddNoiseFunction(this), menu, toolbar); } private void addTool(final Tool t, JMenu menu, JToolBar toolbar, ButtonGroup menuGroup, ButtonGroup toolbarGroup) { final JToggleButton toggle = new JToggleButton(); final JCheckBoxMenuItem check = new JCheckBoxMenuItem(); Action action = new AbstractAction(t.getName()) { @Override public void actionPerformed(ActionEvent e) { if (currentTool != null) { currentTool.deactivate(); } currentTool = t; toggle.setSelected(true); check.setSelected(true); currentTool.activate(); } }; toggle.setAction(action); check.setAction(action); menu.add(check); if (toolbar != null) { toolbar.add(toggle); toolbarGroup.add(toggle); } menuGroup.add(check); } private void addFunction(final Function f, JMenu menu, JToolBar toolbar) { Action action = new AbstractAction(f.getName()) { @Override public void actionPerformed(ActionEvent e) { f.execute(); } }; toolbar.add(action); menu.add(action); } private class ViewerMouseListener implements MouseListener, MouseMotionListener { @Override public void mouseMoved(MouseEvent e) { updatePositionLabels(fixEventPoint(e.getPoint())); } @Override public void mouseDragged(MouseEvent e) { updatePositionLabels(fixEventPoint(e.getPoint())); } @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { updatePositionLabels(fixEventPoint(e.getPoint())); } private Point fixEventPoint(Point p) { Insets insets = viewer.getInsets(); return new Point(p.x - insets.left, p.y - insets.top); } } }