package gis2.scenario; import gis2.Scenario; import gis2.ScenarioException; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.FileReader; import java.io.FileOutputStream; import java.io.IOException; import java.text.DecimalFormat; import java.text.NumberFormat; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ButtonGroup; import javax.swing.JCheckBoxMenuItem; import javax.swing.JFileChooser; import javax.swing.filechooser.FileFilter; 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.JSplitPane; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.WindowConstants; import javax.swing.SwingUtilities; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import maps.MapException; import maps.MapReader; import maps.gml.GMLMap; import maps.gml.view.GMLMapViewer; import maps.gml.view.GMLObjectInspector; import maps.gml.view.DecoratorOverlay; import maps.gml.view.FilledShapeDecorator; import rescuecore2.log.Logger; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.DocumentHelper; import org.dom4j.io.SAXReader; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; /** A component for editing scenarios. */ public class ScenarioEditor 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 NumberFormat FORMAT = new DecimalFormat("#0.000"); private static final Color FIRE_COLOUR = new Color(255, 0, 0, 128); private static final Color FIRE_STATION_COLOUR = new Color(255, 255, 0); private static final Color POLICE_OFFICE_COLOUR = new Color(0, 0, 255); private static final Color AMBULANCE_CENTRE_COLOUR = new Color(255, 255, 255); private static final Color REFUGE_COLOUR = new Color(0, 128, 0); private GMLMap map; private GMLMapViewer viewer; private GMLObjectInspector inspector; private DecoratorOverlay fireOverlay; private DecoratorOverlay centreOverlay; private AgentOverlay agentOverlay; private Scenario scenario; private Tool currentTool; private JLabel statusLabel; private boolean changed; private UndoManager undoManager; private Action undoAction; private Action redoAction; private File baseDir; private File saveFile; private FilledShapeDecorator fireDecorator = new FilledShapeDecorator(FIRE_COLOUR, null, null); private FilledShapeDecorator fireStationDecorator = new FilledShapeDecorator(FIRE_STATION_COLOUR, null, null); private FilledShapeDecorator policeOfficeDecorator = new FilledShapeDecorator(POLICE_OFFICE_COLOUR, null, null); private FilledShapeDecorator ambulanceCentreDecorator = new FilledShapeDecorator(AMBULANCE_CENTRE_COLOUR, null, null); private FilledShapeDecorator refugeDecorator = new FilledShapeDecorator(REFUGE_COLOUR, null, null); /** Construct a new ScenarioEditor. @param menuBar The menu bar to add menus to. */ public ScenarioEditor(JMenuBar menuBar) { this(menuBar, null, null); } /** Construct a new ScenarioEditor. @param menuBar The menu bar to add menus to. @param map The GMLMap to view. @param scenario The scenario to edit. */ public ScenarioEditor(JMenuBar menuBar, GMLMap map, Scenario scenario) { super(new BorderLayout()); this.map = map; this.scenario = scenario; viewer = new GMLMapViewer(map); statusLabel = new JLabel("Status"); fireOverlay = new DecoratorOverlay(); centreOverlay = new DecoratorOverlay(); agentOverlay = new AgentOverlay(this); viewer.addOverlay(fireOverlay); viewer.addOverlay(centreOverlay); viewer.addOverlay(agentOverlay); 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(); changed = false; JToolBar fileToolbar = new JToolBar("File"); JToolBar editToolbar = new JToolBar("Edit"); JToolBar toolsToolbar = new JToolBar("Tools"); JToolBar functionsToolbar = new JToolBar("Functions"); JMenu fileMenu = new JMenu("File", false); JMenu editMenu = new JMenu("Edit", false); JMenu toolsMenu = new JMenu("Tools", false); JMenu functionsMenu = new JMenu("Functions", false); createFileActions(fileMenu, fileToolbar); createEditActions(editMenu, editToolbar); createToolActions(toolsMenu, toolsToolbar); createFunctionActions(functionsMenu, functionsToolbar); JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, viewer, inspector); add(split, BorderLayout.CENTER); JPanel toolbars = new JPanel(new GridLayout(0, 1)); toolbars.add(fileToolbar); toolbars.add(editToolbar); toolbars.add(toolsToolbar); toolbars.add(functionsToolbar); add(toolbars, BorderLayout.NORTH); add(statusLabel, BorderLayout.SOUTH); menuBar.add(fileMenu); menuBar.add(editMenu); menuBar.add(toolsMenu); menuBar.add(functionsMenu); baseDir = new File(System.getProperty("user.dir")); saveFile = null; } /** Entry point. @param args Command line arguments. */ public static void main(String[] args) { final JFrame frame = new JFrame("Scenario Editor"); JMenuBar menuBar = new JMenuBar(); final ScenarioEditor editor = new ScenarioEditor(menuBar); if (args.length > 0 && args[0].length() > 0) { try { editor.load(args[0]); } catch (CancelledByUserException e) { return; } catch (MapException e) { e.printStackTrace(); } catch (ScenarioException 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 and scenario 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. @throws ScenarioException If there is a problem reading the scenario. */ public void load() throws CancelledByUserException, MapException, ScenarioException { JFileChooser chooser = new JFileChooser(baseDir); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.setFileFilter(new FileFilter() { @Override public boolean accept(File f) { return f.isDirectory(); } @Override public String getDescription() { return "Directories"; } }); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { load(chooser.getSelectedFile()); } } /** Load a map and scenario from a directory. @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. @throws ScenarioException If there is a problem reading the scenario. */ public void load(String filename) throws CancelledByUserException, MapException, ScenarioException { load(new File(filename)); } /** Load a map and scenario from a directory. @param dir The directory to read. @throws CancelledByUserException If the user cancels the change due to unsaved changes. @throws MapException If there is a problem reading the map. @throws ScenarioException If there is a problem reading the scenario. */ public void load(File dir) throws CancelledByUserException, MapException, ScenarioException { FileReader r = null; try { GMLMap newMap = (GMLMap)MapReader.readMap(new File(dir, "map.gml")); File f = new File(dir, "scenario.xml"); SAXReader saxReader = new SAXReader(); r = new FileReader(f); Document doc = saxReader.read(r); Scenario newScenario = new Scenario(doc); setScenario(newMap, newScenario); baseDir = dir; saveFile = f; } catch (IOException e) { throw new ScenarioException(e); } catch (DocumentException e) { throw new ScenarioException(e); } finally { if (r != null) { try { r.close(); } catch (IOException e) { throw new ScenarioException(e); } } } } /** Set the map and scenario. @param newMap The new map. @param newScenario The new scenario. @throws CancelledByUserException If the user cancels the change due to unsaved changes. */ public void setScenario(GMLMap newMap, Scenario newScenario) throws CancelledByUserException { checkForChanges(); if (!checkScenario(newMap, newScenario)) { JOptionPane.showMessageDialog(null, "The scenario file contained errors."); return; } map = newMap; scenario = newScenario; changed = false; viewer.setMap(map); inspector.setMap(map); updateOverlays(); } /** Get the map. @return The map. */ public GMLMap getMap() { return map; } /** Get the scenario. @return The scenario. */ public Scenario getScenario() { return scenario; } /** Save the scenario. @throws ScenarioException If there is a problem saving the scenario. */ public void save() throws ScenarioException { if (saveFile == null) { saveAs(); } if (saveFile != null) { Logger.debug("Saving to " + saveFile.getAbsolutePath()); Document doc = DocumentHelper.createDocument(); scenario.write(doc); try { if (!saveFile.exists()) { File parent = saveFile.getParentFile(); if (!parent.exists()) { if (!saveFile.getParentFile().mkdirs()) { throw new ScenarioException("Couldn't create file " + saveFile.getPath()); } } if (!saveFile.createNewFile()) { throw new ScenarioException("Couldn't create file " + saveFile.getPath()); } } XMLWriter writer = new XMLWriter(new FileOutputStream(saveFile), OutputFormat.createPrettyPrint()); writer.write(doc); writer.flush(); writer.close(); } catch (IOException e) { throw new ScenarioException(e); } baseDir = saveFile.getParentFile(); changed = false; } } /** Save the scenario. @throws ScenarioException If there is a problem saving the scenario. */ public void saveAs() throws ScenarioException { JFileChooser chooser = new JFileChooser(baseDir); if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { saveFile = chooser.getSelectedFile(); save(); } } /** 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()); } /** Update the overlay views. */ public void updateOverlays() { updateFireOverlay(); updateCentreOverlay(); updateAgentOverlay(); updateStatusLabel(); viewer.repaint(); } private void checkForChanges() throws CancelledByUserException { if (changed) { switch (JOptionPane.showConfirmDialog(null, "The current scenario has changes. Do you want to save them?")) { case JOptionPane.YES_OPTION: try { save(); } catch (ScenarioException 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(); setScenario(map, new Scenario()); } 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); } catch (ScenarioException ex) { JOptionPane.showMessageDialog(null, ex); } } }; Action saveAction = new AbstractAction("Save") { @Override public void actionPerformed(ActionEvent e) { try { save(); } catch (ScenarioException ex) { JOptionPane.showMessageDialog(null, ex); } } }; Action saveAsAction = new AbstractAction("Save as") { @Override public void actionPerformed(ActionEvent e) { try { saveAs(); } catch (ScenarioException 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 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 PlaceFireTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveFireTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new PlaceRefugeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveRefugeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new PlaceCivilianTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveCivilianTool(this), menu, toolbar, menuGroup, toolbarGroup); menu.addSeparator(); toolbar.addSeparator(); addTool(new PlaceFireBrigadeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveFireBrigadeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new PlacePoliceForceTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemovePoliceForceTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new PlaceAmbulanceTeamTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveAmbulanceTeamTool(this), menu, toolbar, menuGroup, toolbarGroup); menu.addSeparator(); toolbar.addSeparator(); addTool(new PlaceFireStationTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveFireStationTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new PlacePoliceOfficeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemovePoliceOfficeTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new PlaceAmbulanceCentreTool(this), menu, toolbar, menuGroup, toolbarGroup); addTool(new RemoveAmbulanceCentreTool(this), menu, toolbar, menuGroup, toolbarGroup); } private void createFunctionActions(JMenu menu, JToolBar toolbar) { addFunction(new RandomiseFunction(this), menu, toolbar); addFunction(new ClearFiresFunction(this), menu, toolbar); addFunction(new ClearAgentsFunction(this), menu, toolbar); addFunction(new ClearAllFunction(this), menu, toolbar); addFunction(new PlaceAgentsFunction(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); toolbar.add(toggle); menuGroup.add(check); toolbarGroup.add(toggle); } 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 boolean checkScenario(GMLMap newMap, Scenario newScenario) { boolean valid = true; for (int id : newScenario.getFires()) { if (newMap.getBuilding(id) == null) { valid = false; Logger.warn("Fire at non-existing building " + id); } } for (int id : newScenario.getRefuges()) { if (newMap.getBuilding(id) == null) { valid = false; Logger.warn("Refuge at non-existing building " + id); } } for (int id : newScenario.getFireStations()) { if (newMap.getBuilding(id) == null) { valid = false; Logger.warn("Fire station at non-existing building " + id); } } for (int id : newScenario.getAmbulanceCentres()) { if (newMap.getBuilding(id) == null) { valid = false; Logger.warn("Ambulance centre at non-existing building " + id); } } for (int id : newScenario.getPoliceOffices()) { if (newMap.getBuilding(id) == null) { valid = false; Logger.warn("Police office at non-existing building " + id); } } for (int id : newScenario.getCivilians()) { if (newMap.getShape(id) == null) { valid = false; Logger.warn("Civilian at non-existing shape " + id); } } for (int id : newScenario.getFireBrigades()) { if (newMap.getShape(id) == null) { valid = false; Logger.warn("Fire brigade at non-existing shape " + id); } } for (int id : newScenario.getAmbulanceTeams()) { if (newMap.getShape(id) == null) { valid = false; Logger.warn("Ambulance team at non-existing shape " + id); } } for (int id : newScenario.getPoliceForces()) { if (newMap.getShape(id) == null) { valid = false; Logger.warn("Police force at non-existing shape " + id); } } return valid; } private void updateStatusLabel() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { statusLabel.setText(scenario.getFires().size() + " fires, " + scenario.getRefuges().size() + " refuges, " + scenario.getCivilians().size() + " civilians, " + scenario.getFireBrigades().size() + " fb, " + scenario.getFireStations().size() + " fs, " + scenario.getPoliceForces().size() + " pf, " + scenario.getPoliceOffices().size() + " po, " + scenario.getAmbulanceTeams().size() + " at, " + scenario.getAmbulanceCentres().size() + " ac"); } }); } private void updateFireOverlay() { fireOverlay.clearAllBuildingDecorators(); for (int next : scenario.getFires()) { fireOverlay.setBuildingDecorator(fireDecorator, map.getBuilding(next)); } } private void updateCentreOverlay() { centreOverlay.clearAllBuildingDecorators(); for (int next : scenario.getFireStations()) { centreOverlay.setBuildingDecorator(fireStationDecorator, map.getBuilding(next)); } for (int next : scenario.getPoliceOffices()) { centreOverlay.setBuildingDecorator(policeOfficeDecorator, map.getBuilding(next)); } for (int next : scenario.getAmbulanceCentres()) { centreOverlay.setBuildingDecorator(ambulanceCentreDecorator, map.getBuilding(next)); } for (int next : scenario.getRefuges()) { centreOverlay.setBuildingDecorator(refugeDecorator, map.getBuilding(next)); } } private void updateAgentOverlay() { } }