package nl.tudelft.bw4t.server.view; import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileNotFoundException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.apache.log4j.Logger; import eis.exceptions.ManagementException; import eis.iilang.Identifier; import eis.iilang.Parameter; import nl.tudelft.bw4t.map.renderer.MapRenderer; import nl.tudelft.bw4t.server.controller.ServerMapController; import nl.tudelft.bw4t.server.environment.BW4TEnvironment; import nl.tudelft.bw4t.server.environment.Stepper; import nl.tudelft.bw4t.server.model.BW4TServerMap; import nl.tudelft.bw4t.server.repast.BW4TBuilder; /** * Used for directly displaying the simulation from the context, unlike * BW4TRenderer does not use percepts and can show all entities. Only used on * the server side (BW4TEnvironment side). * * Note, this renderer is largely independent of repast, so even though Repast * has its own rendering tools we don't use that. * * Also note that this is a runnable and runs in its own thread with a refresh * rate of 10Hz, started by {@link BW4TBuilder}, see {@link #run()}. */ @SuppressWarnings("serial") public class ServerContextDisplay extends JFrame { /** * The log4j logger, logs to the console. */ private static final Logger LOGGER = Logger.getLogger(ServerContextDisplay.class); /** The map renderer. */ private final MapRenderer myRenderer; /** The server map controller. */ private final ServerMapController controller; /** * Create a new instance of this class and initialize it. * * @param serverMap * the central data model of Repast * @throws VisualizationsException * the visualizations exception */ public ServerContextDisplay(BW4TServerMap serverMap) throws VisualizationsException { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) { LOGGER.warn("failed to setup java look and feel", e); } setTitle("BW4T"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); controller = new ServerMapController(serverMap); myRenderer = new MapRenderer(controller); add(new JScrollPane(myRenderer), BorderLayout.CENTER); try { add(new ControlPanel(this), BorderLayout.NORTH); } catch (FileNotFoundException e) { throw new VisualizationsException(e); } pack(); setVisible(true); } /** * We close ourselves if user clicks on reset. We need to since we are * created from {@link BW4TBuilder} which does not assume ownership; and * nobody else can create us it seems. */ public void close() { myRenderer.getController().setRunning(false); setVisible(false); dispose(); } } /** * local speed panel at top of the window. */ @SuppressWarnings("serial") class ControlPanel extends JPanel { /** * The log4j logger, logs to the console. */ private static final Logger LOGGER = Logger.getLogger(ControlPanel.class); /** * used to close the window when user presses reset. */ final private ServerContextDisplay displayer; /** The times per second display. */ final private JLabel tpsDisplay = new JLabel("0.0 tps"); /** The slider for the tps. */ private final JSlider slider; /** The collision checkbox. */ private final JCheckBox collisionCheckbox; /** * @param disp * is used to close the window when user presses reset. * @throws FileNotFoundException */ public ControlPanel(ServerContextDisplay disp) throws FileNotFoundException { this.displayer = disp; setLayout(new BorderLayout()); add(new JLabel("Speed"), BorderLayout.WEST); // slider goes in percentage, 100 is fastest slider = new JSlider(Stepper.MIN_DELAY, Stepper.MAX_DELAY, 20); slider.setEnabled(false); final JButton resetbutton = new JButton("Reset"); add(tpsDisplay, BorderLayout.WEST); add(slider, BorderLayout.CENTER); add(resetbutton, BorderLayout.EAST); add(new MapSelector(displayer), BorderLayout.NORTH); collisionCheckbox = new JCheckBox("Enable Collisions", BW4TEnvironment.getInstance().isCollisionEnabled()); add(collisionCheckbox, BorderLayout.SOUTH); collisionCheckbox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { BW4TEnvironment.getInstance().setCollisionEnabled(e.getStateChange() == ItemEvent.SELECTED); } }); slider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent arg0) { // now we have speed (on Hz axis) and we need delay (s axis). // first get interpolated speed. BW4TEnvironment.getInstance().setDelay(slider.getValue()); updateTpsDisplay(); } }); resetbutton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { try { BW4TEnvironment.getInstance().reset(true); } catch (ManagementException e) { LOGGER.error("failed to reset the environment", e); } } }); // 3196 FIXME delay startup of de UI till the stepper is ready. new Timer().schedule(new TimerTask() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { slider.setEnabled(true); updateDelay(); } }); } }, 1000); updateTpsDisplay(); BW4TEnvironment.getInstance().addChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { collisionCheckbox.setSelected(BW4TEnvironment.getInstance().isCollisionEnabled()); updateDelay(); } }); } private void updateDelay() { updateTpsDisplay(); slider.setValue((int) BW4TEnvironment.getInstance().getDelay()); } /** * Update the tps display. */ public void updateTpsDisplay() { tpsDisplay.setText(String.format("%3.1f fps", 1000.f / BW4TEnvironment.getInstance().getDelay())); } } /** * This combo box allows user to select a new map. Doing that will reset the * server and reload the new map. * * We assume that a directory named "Maps" is available in the current * directory, and that it only contains maps. */ @SuppressWarnings("serial") class MapSelector extends JPanel { /** * The log4j logger, logs to the console. */ private static final Logger LOGGER = Logger.getLogger(MapSelector.class); /** * Instantiates a new map selector. * * @param displayer * the displayer * @throws FileNotFoundException * the file not found exception */ public MapSelector(final ServerContextDisplay displayer) throws FileNotFoundException { setLayout(new BorderLayout()); add(new JLabel("Change Map"), BorderLayout.WEST); Vector<String> maps = getMaps(); final JComboBox<String> mapselector = new JComboBox<>(maps); // find the current map in the list and highlight it String mapname = BW4TEnvironment.getInstance().getMapName(); mapselector.setSelectedItem(mapname); add(mapselector, BorderLayout.CENTER); mapselector.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { Map<String, Parameter> parameters = new HashMap<>(1); parameters.put("map", new Identifier((String) mapselector.getSelectedItem())); try { BW4TEnvironment.getInstance().reset(parameters); } catch (ManagementException e) { LOGGER.error("failed to reset the environment", e); } } }); } /** * get list of available map names. * * @return vector with all available map names in the Maps directory. * @throws FileNotFoundException */ private Vector<String> getMaps() throws FileNotFoundException { File f = new File(System.getProperty("user.dir") + "/maps"); if (f.list() == null) { throw new FileNotFoundException("maps directory not found"); } return new Vector<String>(Arrays.asList(f.list())); } }