View Javadoc

1   /*
2    * jcurl java curling software framework https://JCurl.mro.name Copyright (C)
3    * 2005-2009 M. Rohrmoser
4    * 
5    * This program is free software; you can redistribute it and/or modify it under
6    * the terms of the GNU General Public License as published by the Free Software
7    * Foundation; either version 2 of the License, or (at your option) any later
8    * version.
9    * 
10   * This program is distributed in the hope that it will be useful, but WITHOUT
11   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12   * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13   * details.
14   * 
15   * You should have received a copy of the GNU General Public License along with
16   * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
17   * Place, Suite 330, Boston, MA 02111-1307 USA
18   */
19  
20  package org.jcurl.demo.tactics;
21  
22  import java.awt.BorderLayout;
23  import java.awt.Container;
24  import java.awt.Cursor;
25  import java.awt.Dimension;
26  import java.awt.Frame;
27  import java.awt.Graphics;
28  import java.awt.GridBagConstraints;
29  import java.awt.GridBagLayout;
30  import java.awt.Image;
31  import java.awt.Insets;
32  import java.awt.Toolkit;
33  import java.awt.geom.Point2D;
34  import java.awt.geom.Rectangle2D;
35  import java.awt.geom.RectangularShape;
36  import java.awt.image.BufferedImage;
37  import java.io.File;
38  import java.io.IOException;
39  import java.net.MalformedURLException;
40  import java.net.URISyntaxException;
41  import java.net.URL;
42  import java.util.ArrayList;
43  import java.util.Collections;
44  import java.util.EventObject;
45  import java.util.List;
46  import java.util.Locale;
47  
48  import javax.imageio.ImageIO;
49  import javax.swing.JButton;
50  import javax.swing.JComponent;
51  import javax.swing.JDialog;
52  import javax.swing.JFileChooser;
53  import javax.swing.JLabel;
54  import javax.swing.JMenu;
55  import javax.swing.JMenuBar;
56  import javax.swing.JMenuItem;
57  import javax.swing.JOptionPane;
58  import javax.swing.JPanel;
59  import javax.swing.JSeparator;
60  import javax.swing.JTabbedPane;
61  import javax.swing.JTextField;
62  import javax.swing.JToolBar;
63  import javax.swing.SwingConstants;
64  import javax.swing.border.EmptyBorder;
65  import javax.swing.event.ChangeEvent;
66  import javax.swing.event.ChangeListener;
67  import javax.swing.event.UndoableEditEvent;
68  import javax.swing.event.UndoableEditListener;
69  import javax.swing.filechooser.FileFilter;
70  import javax.swing.undo.CompoundEdit;
71  
72  import org.apache.commons.logging.Log;
73  import org.jcurl.batik.BatikWrapper;
74  import org.jcurl.core.api.ComputedTrajectorySet;
75  import org.jcurl.core.api.IceSize;
76  import org.jcurl.core.api.RockProps;
77  import org.jcurl.core.api.RockSet;
78  import org.jcurl.core.api.RockSetUtils;
79  import org.jcurl.core.api.TrajectorySet;
80  import org.jcurl.core.api.Unit;
81  import org.jcurl.core.api.RockType.Pos;
82  import org.jcurl.core.api.RockType.Vel;
83  import org.jcurl.core.helpers.BatikButler;
84  import org.jcurl.core.io.IONode;
85  import org.jcurl.core.io.IOTrajectories;
86  import org.jcurl.core.io.JCurlSerializer;
87  import org.jcurl.core.io.JDKSerializer;
88  import org.jcurl.core.log.JCLoggerFactory;
89  import org.jcurl.core.ui.BroomPromptModel;
90  import org.jcurl.core.ui.ChangeManager;
91  import org.jcurl.core.ui.FileNameExtensionFilter;
92  import org.jcurl.core.ui.PosMemento;
93  import org.jcurl.core.ui.SuspendMemento;
94  import org.jcurl.core.ui.UndoableMemento;
95  import org.jcurl.core.ui.BroomPromptModel.HandleMemento;
96  import org.jcurl.core.ui.BroomPromptModel.IndexMemento;
97  import org.jcurl.core.ui.BroomPromptModel.SplitMemento;
98  import org.jcurl.core.ui.BroomPromptModel.XYMemento;
99  import org.jdesktop.application.Action;
100 import org.jdesktop.application.Application;
101 import org.jdesktop.application.ApplicationAction;
102 import org.jdesktop.application.ApplicationContext;
103 import org.jdesktop.application.ResourceMap;
104 import org.jdesktop.application.SingleFrameApplication;
105 import org.jdesktop.application.Task;
106 import org.jdesktop.application.Task.BlockingScope;
107 
108 /**
109  * Makes heavy use of the <a
110  * href="https://appframework.dev.java.net/intro/index.html">Swing Application
111  * Framework</a>.
112  * 
113  * @author <a href="mailto:JCurl@mro.name">M. Rohrmoser </a>
114  * @version $Id$
115  */
116 public class JCurlShotPlanner extends SingleFrameApplication implements
117 		UndoableEditListener {
118 	private static class ChangeListenerManager implements ChangeListener {
119 		private final JCurlShotPlanner host;
120 
121 		public ChangeListenerManager(final JCurlShotPlanner host) {
122 			this.host = host;
123 		}
124 
125 		public void deregister(final ComputedTrajectorySet cts) {
126 			if (cts == null)
127 				return;
128 			cts.getInitialPos().removeRockListener(this);
129 			cts.getInitialVel().removeRockListener(this);
130 			// cts.getCurrentPos().removeRockListener(this);
131 			// cts.getCurrentSpeed().removeRockListener(this);
132 		}
133 
134 		public void register(final ComputedTrajectorySet cts) {
135 			if (cts == null)
136 				return;
137 			cts.getInitialPos().addRockListener(this);
138 			cts.getInitialVel().addRockListener(this);
139 			// cts.getCurrentPos().addRockListener(this);
140 			// cts.getCurrentSpeed().addRockListener(this);
141 		}
142 
143 		public void stateChanged(final ChangeEvent e) {
144 			host.setModified(true);
145 		}
146 	}
147 
148 	static class GuiUtil {
149 
150 		private static final Insets zeroInsets = new Insets(0, 0, 0, 0);
151 
152 		private final ApplicationContext act;;
153 
154 		public GuiUtil(final ApplicationContext act) {
155 			this.act = act;
156 		}
157 
158 		/**
159 		 * Create a simple about box JDialog that displays the standard
160 		 * Application resources, like {@code Application.title} and
161 		 * {@code Application.description}. The about box's labels and fields
162 		 * are configured by resources that are injected when the about box is
163 		 * shown (see SingleFrameApplication#show). The resources are defined in
164 		 * the application resource file: resources/DocumentExample.properties.
165 		 * 
166 		 * From:
167 		 * https://appframework.dev.java.net/downloads/AppFramework-1.03-src.zip
168 		 * DocumentExample
169 		 */
170 		private JDialog createAboutBox(final Frame owner) {
171 			final JPanel panel = new JPanel(new GridBagLayout());
172 			panel.setBorder(new EmptyBorder(0, 28, 16, 28)); // top, left,
173 			// bottom, right
174 			final JLabel titleLabel = new JLabel();
175 			titleLabel.setName("aboutTitleLabel");
176 			final GridBagConstraints c = new GridBagConstraints();
177 			initGridBagConstraints(c);
178 			c.anchor = GridBagConstraints.WEST;
179 			c.gridwidth = GridBagConstraints.REMAINDER;
180 			c.fill = GridBagConstraints.HORIZONTAL;
181 			c.ipady = 32;
182 			c.weightx = 1.0;
183 			panel.add(titleLabel, c);
184 			final String[] fields = { "description", "version", "vendor",
185 					"home" };
186 			for (final String field : fields) {
187 				final JLabel label = new JLabel();
188 				label.setName(field + "Label");
189 				initGridBagConstraints(c);
190 				// c.anchor = GridBagConstraints.BASELINE_TRAILING; 1.6 ONLY
191 				c.anchor = GridBagConstraints.EAST;
192 				panel.add(label, c);
193 				initGridBagConstraints(c);
194 				c.weightx = 1.0;
195 				c.gridwidth = GridBagConstraints.REMAINDER;
196 				c.fill = GridBagConstraints.HORIZONTAL;
197 				final JTextField textField = new JTextField();
198 				textField.setName(field + "TextField");
199 				textField.setEditable(false);
200 				textField.setBorder(null);
201 				panel.add(textField, c);
202 			}
203 			final JButton closeAboutButton = new JButton();
204 			closeAboutButton.setAction(findAction("closeAboutBox"));
205 			initGridBagConstraints(c);
206 			c.anchor = GridBagConstraints.EAST;
207 			c.gridx = 1;
208 			panel.add(closeAboutButton, c);
209 			final JDialog dialog = new JDialog(owner);
210 			dialog.setName("aboutDialog");
211 			dialog.add(panel, BorderLayout.CENTER);
212 			return dialog;
213 		}
214 
215 		private JFileChooser createFileChooser(final File base,
216 				final String resourceName, final FileFilter filter) {
217 			final JFileChooser fc = new JFileChooser(base);
218 			fc.setName(resourceName);
219 			fc.setMultiSelectionEnabled(false);
220 			fc.setAcceptAllFileFilterUsed(true);
221 			fc.setFileFilter(filter);
222 			getContext().getResourceMap().injectComponents(fc);
223 			return fc;
224 		}
225 
226 		private FileNameExtensionFilter createFileFilter(
227 				final String resourceName, final String... extensions) {
228 			final ResourceMap appResourceMap = getContext().getResourceMap();
229 			final String key = resourceName + ".description";
230 			final String desc = appResourceMap.getString(key);
231 			return new FileNameExtensionFilter(desc == null ? key : desc,
232 					extensions);
233 		}
234 
235 		private JMenu createMenu(final String menuName,
236 				final String[] actionNames) {
237 			final JMenu menu = new JMenu();
238 			menu.setName(menuName);
239 			for (final String actionName : actionNames)
240 				if (actionName.equals("---"))
241 					menu.add(new JSeparator());
242 				else {
243 					final JMenuItem menuItem = new JMenuItem();
244 					menuItem.setAction(findAction(actionName));
245 					menuItem.setIcon(null);
246 					menu.add(menuItem);
247 				}
248 			return menu;
249 		}
250 
251 		private File ensureSuffix(final File dst,
252 				final FileNameExtensionFilter pat) {
253 			if (pat.accept(dst))
254 				return dst;
255 			return new File(dst.getAbsoluteFile() + "."
256 					+ pat.getExtensions()[0]);
257 		}
258 
259 		private javax.swing.Action findAction(final String actionName) {
260 			return getContext().getActionMap().get(actionName);
261 		};
262 
263 		public ApplicationContext getContext() {
264 			return act;
265 		};
266 
267 		private void initGridBagConstraints(final GridBagConstraints c) {
268 			c.anchor = GridBagConstraints.CENTER;
269 			c.fill = GridBagConstraints.NONE;
270 			c.gridwidth = 1;
271 			c.gridheight = 1;
272 			c.gridx = GridBagConstraints.RELATIVE;
273 			c.gridy = GridBagConstraints.RELATIVE;
274 			c.insets = zeroInsets;
275 			c.ipadx = 4; // not the usual default
276 			c.ipady = 4; // not the usual default
277 			c.weightx = 0.0;
278 			c.weighty = 0.0;
279 		};
280 	}
281 
282 	abstract static class WaitCursorTask<T, V> extends Task<T, V> {
283 		private final SingleFrameApplication app;
284 
285 		public WaitCursorTask(final SingleFrameApplication app) {
286 			super(app);
287 			this.app = app;
288 		}
289 
290 		protected abstract T doCursor() throws Exception;
291 
292 		@Override
293 		protected T doInBackground() throws Exception {
294 			final Cursor cu = app.getMainFrame().getCursor();
295 			try {
296 				app.getMainFrame().setCursor(waitc);
297 				Thread.yield();
298 				return doCursor();
299 			} finally {
300 				app.getMainFrame().setCursor(cu);
301 				Thread.yield();
302 			}
303 		}
304 	}
305 
306 	public static class ZoomHelper {
307 		/**
308 		 * Inter-hog area area plus house area plus 1 rock margin plus "out"
309 		 * rock space.
310 		 */
311 		public static final Rectangle2D ActivePlus;
312 		/** All from back to back */
313 		public static final Rectangle2D CompletePlus;
314 		/** House area plus 1 rock margin plus "out" rock space. */
315 		public static final Rectangle2D HousePlus;
316 		/** 12-foot circle plus 1 rock */
317 		public static final Rectangle2D TwelvePlus;
318 		static {
319 			final double r2 = 2 * RockProps.DEFAULT.getRadius();
320 			final double x = IceSize.SIDE_2_CENTER + r2;
321 			HousePlus = new Rectangle2D.Double(-x, -(IceSize.HOG_2_TEE + r2),
322 					2 * x, IceSize.HOG_2_TEE + IceSize.BACK_2_TEE + 3 * r2 + 2
323 							* r2);
324 			final double c12 = r2 + Unit.f2m(6.0);
325 			TwelvePlus = new Rectangle2D.Double(-c12, -c12, 2 * c12, 2 * c12);
326 			ActivePlus = new Rectangle2D.Double(-x, -(IceSize.HOG_2_HOG
327 					+ IceSize.HOG_2_TEE + r2), 2 * x, IceSize.HOG_2_HOG
328 					+ IceSize.HOG_2_TEE + IceSize.BACK_2_TEE + 3 * r2 + 2 * r2);
329 			CompletePlus = new Rectangle2D.Double(-x, -(IceSize.HOG_2_TEE
330 					+ IceSize.HOG_2_HOG + IceSize.HACK_2_HOG + r2), 2 * x,
331 					IceSize.HOG_2_HOG + 2 * IceSize.HACK_2_HOG);
332 		}
333 
334 		private void pan(final Zoomable dst, final double rx, final double ry,
335 				final int dt) {
336 			if (dst == null)
337 				return;
338 			final RectangularShape src = dst.getZoom();
339 			zoom(dst, new Rectangle2D.Double(src.getX() + src.getWidth() * rx,
340 					src.getY() + src.getHeight() * ry, src.getWidth(), src
341 							.getHeight()), dt);
342 		}
343 
344 		private void zoom(final Zoomable dst, final Point2D center,
345 				final double ratio, final int dt) {
346 			if (dst == null)
347 				return;
348 			final RectangularShape src = dst.getZoom();
349 			final double w = src.getWidth() * ratio;
350 			final double h = src.getHeight() * ratio;
351 			final double cx, cy;
352 			if (center == null) {
353 				cx = src.getCenterX();
354 				cy = src.getCenterY();
355 			} else {
356 				cx = center.getX();
357 				cy = center.getY();
358 			}
359 			zoom(dst, new Rectangle2D.Double(cx - w / 2, cy - h / 2, Math
360 					.abs(w), Math.abs(h)), dt);
361 		}
362 
363 		private void zoom(final Zoomable dst, final RectangularShape viewport,
364 				final int dt) {
365 			if (dst == null)
366 				return;
367 			dst.setZoom(viewport, dt);
368 		}
369 	}
370 
371 	private static final BatikButler batik = new BatikButler();
372 	private static final double currentTime = 30;
373 	private static final int FAST = 200;
374 	private static URL initialScene = null;
375 	private static final Log log = JCLoggerFactory
376 			.getLogger(JCurlShotPlanner.class);
377 	private static final int SLOW = 333;
378 	private static URL templateScene = null;
379 	private static final Cursor waitc = Cursor
380 			.getPredefinedCursor(Cursor.WAIT_CURSOR);
381 	private static final ZoomHelper zh = new ZoomHelper();
382 
383 	public static void main(final String[] args) {
384 		// for debugging reasons only:
385 		Locale.setDefault(Locale.CANADA);
386 		launch(JCurlShotPlanner.class, args);
387 	}
388 
389 	private static void renderPng(final Container src, final File dst)
390 			throws IOException {
391 		final BufferedImage img = new BufferedImage(src.getWidth(), src
392 				.getHeight(), BufferedImage.TYPE_INT_ARGB);
393 		final Graphics g = img.getGraphics();
394 		try {
395 			// SwingUtilities.paintComponent(g, src, src.getBounds(), null);
396 			src.paintAll(g);
397 		} finally {
398 			g.dispose();
399 		}
400 		ImageIO.write(img, "png", dst);
401 	}
402 
403 	@SuppressWarnings("unchecked")
404 	private static CompoundEdit reset(final ComputedTrajectorySet cts,
405 			final BroomPromptModel broom, final boolean outPosition) {
406 		final RockSet<Pos> ipos = cts.getInitialPos();
407 		final RockSet<Vel> ivel = cts.getInitialVel();
408 		// store the initial state:
409 		final PosMemento[] pm = new PosMemento[RockSet.ROCKS_PER_SET];
410 		for (int i16 = RockSet.ROCKS_PER_SET - 1; i16 >= 0; i16--)
411 			pm[i16] = new PosMemento(ipos, i16, ipos.getRock(i16).p());
412 		final IndexMemento bi = new IndexMemento(broom, broom.getIdx16());
413 		final HandleMemento bh = new HandleMemento(broom, broom.getOutTurn());
414 		final XYMemento bxy = new XYMemento(broom, broom.getBroom());
415 		final SplitMemento bs = new SplitMemento(broom, broom
416 				.getSplitTimeMillis().getValue());
417 		final boolean preS = cts.getSuspended();
418 		cts.setSuspended(true);
419 		try {
420 			// reset:
421 			RockSet.allZero(ivel);
422 			broom.setIdx16(-1);
423 			if (outPosition)
424 				RockSetUtils.allOut(ipos);
425 			else
426 				RockSetUtils.allHome(ipos);
427 			broom.setIdx16(1);
428 			broom.setBroom(new Point2D.Double(0, 0));
429 			broom.getSplitTimeMillis().setValue(3300);
430 		} finally {
431 			cts.setSuspended(preS);
432 		}
433 		// create a compound edit
434 		final CompoundEdit ce = new CompoundEdit();
435 		ce.addEdit(new UndoableMemento(new SuspendMemento(cts, preS),
436 				new SuspendMemento(cts, true)));
437 		for (int i16 = RockSet.ROCKS_PER_SET - 1; i16 >= 0; i16--)
438 			ce.addEdit(new UndoableMemento(pm[i16], new PosMemento(ipos, i16,
439 					ipos.getRock(i16).p())));
440 		ce.addEdit(new UndoableMemento(bi, new IndexMemento(broom, broom
441 				.getIdx16())));
442 		ce.addEdit(new UndoableMemento(bh, new HandleMemento(broom, broom
443 				.getOutTurn())));
444 		ce.addEdit(new UndoableMemento(bxy, new XYMemento(broom, broom
445 				.getBroom())));
446 		ce.addEdit(new UndoableMemento(bs, new SplitMemento(broom, broom
447 				.getSplitTimeMillis().getValue())));
448 		ce.addEdit(new UndoableMemento(new SuspendMemento(cts, true),
449 				new SuspendMemento(cts, preS)));
450 		ce.end();
451 		return ce;
452 	}
453 
454 	private JDialog aboutBox = null;
455 	private final BirdPiccoloBean birdPiccolo = new BirdPiccoloBean();
456 	private final BroomPromptSwingBean broomSwing = new BroomPromptSwingBean();
457 	private boolean canRedo = false;
458 	private boolean canUndo = false;
459 	private final ChangeManager change = new ChangeManager();
460 	private final ChangeListenerManager cm = new ChangeListenerManager(this);
461 	private final CurlerSwingBean curlerSwing = new CurlerSwingBean();
462 	private URL document;
463 	private File file;
464 	private final GuiUtil gui = new GuiUtil(getContext());
465 	private FileNameExtensionFilter jcxzPat;
466 	private boolean modified = false;
467 	private FileNameExtensionFilter pngPat;
468 	private FileNameExtensionFilter svgPat;
469 	private final TrajectoryBean tactics = new TrajectoryPiccoloBean();
470 	private final JLabel url = new JLabel();
471 
472 	private JCurlShotPlanner() {
473 		change.addUndoableEditListener(this);
474 		tactics.setChanger(change);
475 		broomSwing.setChanger(change);
476 		curlerSwing.setChanger(change);
477 		birdPiccolo.setMaster(tactics);
478 		// tactics.setName("tactics");
479 		url.setName("urlLabel");
480 	}
481 
482 	public boolean askDiscardUnsaved(final javax.swing.Action action) {
483 		if (!isModified())
484 			return true;
485 		final String title, msg;
486 		if (true) {
487 			final ResourceMap r = getContext().getResourceMap();
488 			title = r.getString("discard" + ".Dialog" + ".title", action
489 					.getValue(javax.swing.Action.NAME));
490 			msg = r.getString("discard" + ".Dialog" + ".message");
491 		} else if (action instanceof ApplicationAction) {
492 			final ApplicationAction aa = (ApplicationAction) action;
493 			final ResourceMap r = getContext().getResourceMap();
494 			title = action == null ? null : r.getString(aa.getName()
495 					+ ".Dialog" + ".title", aa
496 					.getValue(javax.swing.Action.NAME));
497 			msg = r.getString(aa.getName() + ".Dialog" + ".message");
498 		} else {
499 			title = null;
500 			msg = "Discard unsaved changes?";
501 		}
502 		return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
503 				getMainFrame(), msg, title, JOptionPane.YES_NO_OPTION,
504 				JOptionPane.WARNING_MESSAGE);
505 	}
506 
507 	public boolean askOverwrite(final File f) {
508 		if (!f.exists())
509 			return true;
510 		final String title, msg;
511 		{
512 			final ResourceMap r = getContext().getResourceMap();
513 			title = r.getString("overwrite" + ".Dialog" + ".title");
514 			msg = r.getString("overwrite" + ".Dialog" + ".message", f
515 					.toString());
516 		}
517 		return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(
518 				getMainFrame(), msg, title, JOptionPane.YES_NO_OPTION,
519 				JOptionPane.WARNING_MESSAGE);
520 	};
521 
522 	@Action
523 	public void closeAboutBox() {
524 		if (aboutBox == null)
525 			return;
526 		aboutBox.setVisible(false);
527 		aboutBox = null;
528 	}
529 
530 	private JFileChooser createJcxChooser(final File base, final String name) {
531 		return gui.createFileChooser(base, name, jcxzPat);
532 	};
533 
534 	private JMenuBar createMenuBar() {
535 		final JMenuBar menuBar = new JMenuBar();
536 
537 		final String[] fileMenuActionNames = { /*"fileClear",*/
538 		"fileNewDoc", "fileHammy", "---", "fileOpen", "fileOpenURL", "---",
539 				"fileReset", "fileSave", "fileSaveAs", "fileSaveCopyAs", "---",
540 				"fileExportPng", "fileExportSvg", "---", "quit" };
541 		menuBar.add(gui.createMenu("fileMenu", fileMenuActionNames));
542 
543 		final String[] editMenuActionNames = { "editUndo", "editRedo", "---",
544 				"editOut", "editHome", "---", "editProperties", "---",
545 				"editPreferences" };
546 		menuBar.add(gui.createMenu("editMenu", editMenuActionNames));
547 
548 		final String[] viewMenuActionNames = { "viewHouse", "view12Foot",
549 				"viewComplete", "viewActive", "---", "viewZoomIn",
550 				"viewZoomOut", "---", "viewPanNorth", "viewPanSouth",
551 				"viewPanEast", "viewPanWest" };
552 		menuBar.add(gui.createMenu("viewMenu", viewMenuActionNames));
553 
554 		final String[] helpMenuActionNames = { "showAboutBox",
555 				"helpDumpProperties" };
556 		menuBar.add(gui.createMenu("helpMenu", helpMenuActionNames));
557 
558 		return menuBar;
559 	}
560 
561 	private JFileChooser createPngChooser(final File base, final String name) {
562 		return gui.createFileChooser(base, name, pngPat);
563 	}
564 
565 	private JFileChooser createSvgChooser(final File base, final String name) {
566 		return gui.createFileChooser(base, name, svgPat);
567 	}
568 
569 	private JComponent createToolBar() {
570 		final String[] toolbarActionNames = { "cut", "copy", "paste" };
571 		final JToolBar toolBar = new JToolBar();
572 		toolBar.setFloatable(false);
573 		for (final String actionName : toolbarActionNames) {
574 			final JButton button = new JButton();
575 			button.setAction(gui.findAction(actionName));
576 			button.setFocusable(false);
577 			toolBar.add(button);
578 		}
579 		return toolBar;
580 	}
581 
582 	/** Edit Menu Action */
583 	@Action
584 	public void editHome() {
585 		final ComputedTrajectorySet cts = tactics.getCurves();
586 		if (cts == null)
587 			return;
588 		change.addEdit(reset(cts, tactics.getBroom(), false));
589 	}
590 
591 	/** Edit Menu Action */
592 	@Action
593 	public void editOut() {
594 		final ComputedTrajectorySet cts = tactics.getCurves();
595 		if (cts == null)
596 			return;
597 		change.addEdit(reset(cts, tactics.getBroom(), true));
598 	}
599 
600 	/** Edit Menu Action */
601 	@Action(enabledProperty = "alwaysFalse")
602 	public void editPreferences() {}
603 
604 	/** Edit Menu Action */
605 	@Action(enabledProperty = "alwaysFalse")
606 	public void editProperties() {}
607 
608 	/** Edit Menu Action */
609 	@Action(enabledProperty = "canRedo")
610 	public void editRedo() {
611 		change.redo();
612 	}
613 
614 	/** Edit Menu Action */
615 	@Action(enabledProperty = "canUndo")
616 	public void editUndo() {
617 		change.undo();
618 	}
619 
620 	/** File Menu Action */
621 	@Action
622 	private void fileClear() {
623 		if (!askDiscardUnsaved(gui.findAction("fileClear")))
624 			return;
625 		try {
626 			setDocument(null);
627 		} catch (final IOException e) {
628 			throw new RuntimeException("Unhandled", e);
629 		}
630 	}
631 
632 	/**
633 	 * Render the current view into a <a
634 	 * href="http://en.wikipedia.org/wiki/Portable_Network_Graphics">PNG</a>
635 	 * image (File Menu Action).
636 	 * 
637 	 * @see ImageIO#write(java.awt.image.RenderedImage, String, File)
638 	 */
639 	@Action(block = BlockingScope.ACTION)
640 	public Task<Void, Void> fileExportPng() {
641 		final JFileChooser fcPng = createPngChooser(getFile(),
642 				"exportPngFileChooser");
643 		for (;;) {
644 			if (JFileChooser.APPROVE_OPTION != fcPng
645 					.showSaveDialog(getMainFrame()))
646 				return null;
647 			final File dst = gui.ensureSuffix(fcPng.getSelectedFile(), pngPat);
648 			if (!askOverwrite(dst))
649 				continue;
650 
651 			return new WaitCursorTask<Void, Void>(this) {
652 				@Override
653 				protected Void doCursor() throws Exception {
654 					renderPng(tactics, dst);
655 					return null;
656 				}
657 			};
658 		}
659 	}
660 
661 	/**
662 	 * Render the current view into a <a
663 	 * href="http://en.wikipedia.org/wiki/Svg">SVG</a>
664 	 * image (File Menu Action).
665 	 * 
666 	 * @see BatikWrapper#renderSvg(Container, java.io.OutputStream)
667 	 */
668 	@Action(enabledProperty = "renderSvgAvailable", block = BlockingScope.ACTION)
669 	public Task<Void, Void> fileExportSvg() {
670 		final JFileChooser fcSvg = createSvgChooser(getFile(),
671 				"exportSvgFileChooser");
672 		for (;;) {
673 			if (JFileChooser.APPROVE_OPTION != fcSvg
674 					.showSaveDialog(getMainFrame()))
675 				return null;
676 			final File dst = gui.ensureSuffix(fcSvg.getSelectedFile(), svgPat);
677 			if (!askOverwrite(dst))
678 				continue;
679 
680 			return new WaitCursorTask<Void, Void>(this) {
681 				@Override
682 				protected Void doCursor() throws Exception {
683 					batik.renderSvg(tactics, dst);
684 					return null;
685 				}
686 			};
687 		}
688 	}
689 
690 	/** File Menu Action */
691 	@Action(block = BlockingScope.APPLICATION)
692 	public void fileHammy() {
693 		if (!askDiscardUnsaved(gui.findAction("fileHammy")))
694 			return;
695 		try {
696 			setDocument(initialScene);
697 		} catch (final IOException e) {
698 			throw new RuntimeException("Unhandled", e);
699 		}
700 	}
701 
702 	/** File Menu Action */
703 	@Action
704 	public void fileNewDoc() {
705 		if (!askDiscardUnsaved(gui.findAction("fileNewDoc")))
706 			return;
707 		try {
708 			setDocument(templateScene);
709 		} catch (final IOException e) {
710 			throw new RuntimeException("Unhandled", e);
711 		}
712 	}
713 
714 	/** File Menu Action */
715 	@Action
716 	public void fileOpen() {
717 		if (!askDiscardUnsaved(gui.findAction("fileOpen")))
718 			return;
719 		final JFileChooser chooser = createJcxChooser(getFile(),
720 				"openFileChooser");
721 		if (chooser.showOpenDialog(getMainFrame()) == JFileChooser.APPROVE_OPTION) {
722 			final File file = chooser.getSelectedFile();
723 			try {
724 				setDocument(file.toURI().toURL());
725 			} catch (final MalformedURLException e) {
726 				// shouldn't happen unless the JRE fails
727 				log.warn("File.toURI().toURL() failed", e);
728 			} catch (final IOException e) {
729 				showErrorDialog("can't open \"" + file + "\"", e);
730 			}
731 		}
732 	};
733 
734 	/** File Menu Action */
735 	@Action
736 	public void fileOpenURL() {
737 		final String a = "fileOpenURL";
738 		if (!askDiscardUnsaved(gui.findAction(a)))
739 			return;
740 		final ResourceMap r = getContext().getResourceMap();
741 		final String title = r.getString(a + ".Dialog" + ".title");
742 		final String msg = r.getString(a + ".Dialog" + ".message");
743 		for (;;) {
744 			final String url = JOptionPane.showInputDialog(getMainFrame(), msg,
745 					title, JOptionPane.QUESTION_MESSAGE);
746 			if (url == null)
747 				return;
748 			try {
749 				setDocument(new URL(url));
750 				return;
751 			} catch (final IOException e) {
752 				showErrorDialog(r.getString(a + ".Dialog" + ".error", url), e);
753 			}
754 		}
755 	}
756 
757 	/**
758 	 * File Menu Action
759 	 * 
760 	 * @throws IOException
761 	 */
762 	@Action(enabledProperty = "modified")
763 	public void fileReset() throws IOException {
764 		if (!askDiscardUnsaved(gui.findAction("fileReset")))
765 			return;
766 		final URL tmp = getDocument();
767 		setDocument(null);
768 		setDocument(tmp);
769 	};
770 
771 	/** File Menu Action */
772 	@Action(enabledProperty = "modified")
773 	public void fileSave() {
774 		if (!isModified())
775 			return;
776 		final File f = saveHelper(getFile(), getFile(), "saveFileChooser", true);
777 		log.info(f);
778 		if (f != null) {
779 			try {
780 				setDocument(f.toURL(), false);
781 			} catch (final IOException e) {
782 				throw new RuntimeException("Unhandled", e);
783 			}
784 			setModified(false);
785 		}
786 	};
787 
788 	/** File Menu Action */
789 	@Action
790 	public void fileSaveAs() {
791 		final File f = saveHelper(null, getFile(), "saveAsFileChooser", false);
792 		log.info(f);
793 		if (f != null) {
794 			try {
795 				setDocument(f.toURL(), false);
796 			} catch (final IOException e) {
797 				throw new RuntimeException("Unhandled", e);
798 			}
799 			setModified(false);
800 		}
801 	}
802 
803 	/** File Menu Action */
804 	@Action
805 	public void fileSaveCopyAs() {
806 		log.info(saveHelper(null, getFile(), "saveCopyAsFileChooser", false));
807 	}
808 
809 	private URL getDocument() {
810 		return document;
811 	}
812 
813 	private File getFile() {
814 		return file;
815 	}
816 
817 	@Action()
818 	public void helpDumpProperties() {
819 		final List keys = new ArrayList();
820 		keys.addAll(System.getProperties().keySet());
821 		Collections.sort(keys);
822 		for (final Object key : keys)
823 			System.out.println(key + "=" + System.getProperty((String) key));
824 	}
825 
826 	/**
827 	 * Setting the internal field {@link #document} directly (bypassing
828 	 * {@link #setDocument(URL)}) is used to deplay the document loading until
829 	 * {@link #ready()}.
830 	 */
831 	@Override
832 	protected void initialize(final String[] as) {
833 		if ("Linux".equals(System.getProperty("os.name")))
834 			getContext().getResourceManager().setPlatform("linux");
835 
836 		final Class<?> mc = this.getClass();
837 		{
838 			final ResourceMap r = Application.getInstance().getContext()
839 					.getResourceMap();
840 			initialScene = mc.getResource("/" + r.getResourcesDir()
841 					+ r.getString("Application.defaultDocument"));
842 			templateScene = mc.getResource("/" + r.getResourcesDir()
843 					+ r.getString("Application.templateDocument"));
844 		}
845 
846 		// schedule the document to load in #ready()
847 		document = initialScene;
848 		for (final String p : as) {
849 			// ignore javaws parameters
850 			if ("-open".equals(p) || "-print".equals(p))
851 				continue;
852 			try {
853 				document = new URL(p);
854 				break;
855 			} catch (final MalformedURLException e) {
856 				final File f = new File(p);
857 				if (f.canRead())
858 					try {
859 						document = f.toURL();
860 						break;
861 					} catch (final MalformedURLException e2) {
862 						log.warn("Cannot load '" + p + "'.", e);
863 					}
864 				else
865 					log.warn("Cannot load '" + p + "'.", e);
866 			}
867 		}
868 	}
869 
870 	public boolean isAlwaysFalse() {
871 		return false;
872 	}
873 
874 	public boolean isCanRedo() {
875 		return canRedo;
876 	}
877 
878 	public boolean isCanUndo() {
879 		return canUndo;
880 	}
881 
882 	public boolean isModified() {
883 		return modified;
884 	}
885 
886 	public boolean isRenderSvgAvailable() {
887 		return batik.isBatikAvailable();
888 	}
889 
890 	@Override
891 	protected void ready() {
892 		final URL tmp = document;
893 		document = null;
894 		try {
895 			setDocument(tmp);
896 		} catch (final IOException e) {
897 			log.warn("Couldn't load '" + tmp + "'.", e);
898 		}
899 		addExitListener(new Application.ExitListener() {
900 			public boolean canExit(final EventObject e) {
901 				return askDiscardUnsaved(gui.findAction("quit"));
902 			}
903 
904 			public void willExit(final EventObject e) {
905 				log.info("Good bye!");
906 			}
907 		});
908 	};
909 
910 	private final void save(final TrajectorySet cts, final File dst)
911 			throws IOException {
912 		final Cursor cu = switchCursor(waitc);
913 		try {
914 			final IOTrajectories t = new IOTrajectories();
915 			// TODO add annotations
916 			t.trajectories().add(cts);
917 			new JCurlSerializer().write(t, dst, JDKSerializer.class);
918 		} finally {
919 			switchCursor(cu);
920 		}
921 	}
922 
923 	private File saveHelper(File dst, final File base, final String name,
924 			final boolean forceOverwrite) {
925 		JFileChooser fcJcx = null;
926 		for (;;) {
927 			if (fcJcx == null)
928 				fcJcx = createJcxChooser(base, name);
929 			if (dst == null) {
930 				if (JFileChooser.APPROVE_OPTION != fcJcx
931 						.showSaveDialog(getMainFrame()))
932 					return null;
933 				dst = fcJcx.getSelectedFile();
934 			}
935 			if (dst == null)
936 				continue;
937 			dst = gui.ensureSuffix(dst, jcxzPat);
938 			if (forceOverwrite || askOverwrite(dst))
939 				try {
940 					save(tactics.getCurves(), dst);
941 					return dst;
942 				} catch (final Exception e) {
943 					showErrorDialog("Couldn't save to '" + dst + "'", e);
944 				}
945 			else
946 				dst = null;
947 		}
948 	}
949 
950 	private void setCanRedo(final boolean canRedo) {
951 		final boolean old = this.canRedo;
952 		if (old == canRedo)
953 			return;
954 		firePropertyChange("canRedo", old, this.canRedo = canRedo);
955 	}
956 
957 	private void setCanUndo(final boolean canUndo) {
958 		final boolean old = this.canUndo;
959 		if (old == canUndo)
960 			return;
961 		firePropertyChange("canUndo", old, this.canUndo = canUndo);
962 	}
963 
964 	private void setDocument(final URL document) throws IOException {
965 		this.setDocument(document, true);
966 	}
967 
968 	private void setDocument(final URL document, boolean load)
969 			throws IOException {
970 		final Cursor cu = switchCursor(waitc);
971 		try {
972 			log.info(document);
973 			final URL old = this.document;
974 			this.firePropertyChange("document", old, this.document = document);
975 			setFile(this.document);
976 			url.setText(this.document == null ? "{null}" : this.document
977 					.toString());
978 			if (!load)
979 				return;
980 
981 			cm.deregister(tactics.getCurves());
982 			final ComputedTrajectorySet cts;
983 			if (this.document == null)
984 				cts = null;
985 			else {
986 				final IONode n = new JCurlSerializer().read(this.document);
987 				final IOTrajectories it = (IOTrajectories) n;
988 				final TrajectorySet ts = it.trajectories().get(0);
989 				cts = (ComputedTrajectorySet) ts;
990 			}
991 			change.discardAllEdits();
992 			if (cts != null)
993 				cts.setCurrentTime(currentTime);
994 			tactics.setCurves(cts);
995 			broomSwing.setBroom(tactics.getBroom());
996 			cm.register(cts);
997 			setModified(false);
998 		} finally {
999 			switchCursor(cu);
1000 		}
1001 	}
1002 
1003 	private void setFile(final URL url) {
1004 		File file;
1005 		if (url != null && "file".equals(url.getProtocol()))
1006 			try {
1007 				file = new File(url.toURI());
1008 			} catch (final URISyntaxException e) {
1009 				file = null;
1010 			}
1011 		else
1012 			file = null;
1013 		final File old = this.file;
1014 		this.firePropertyChange("file", old, this.file = file);
1015 	}
1016 
1017 	public void setModified(final boolean modified) {
1018 		final boolean old = this.modified;
1019 		if (old == modified)
1020 			return;
1021 		firePropertyChange("modified", old, this.modified = modified);
1022 	}
1023 
1024 	/** Show the about box dialog. */
1025 	@Action(block = BlockingScope.COMPONENT)
1026 	public void showAboutBox() {
1027 		if (aboutBox == null)
1028 			aboutBox = gui.createAboutBox(getMainFrame());
1029 		show(aboutBox);
1030 	}
1031 
1032 	private void showErrorDialog(final String message, final Exception e) {
1033 		JOptionPane.showMessageDialog(getMainFrame(), "Error: " + message,
1034 				"Error", JOptionPane.ERROR_MESSAGE);
1035 	}
1036 
1037 	@Override
1038 	protected void startup() {
1039 		// set the window icon:
1040 		{
1041 			final Image img;
1042 			if (true)
1043 				img = getContext().getResourceMap().getImageIcon(
1044 						"Application.icon").getImage();
1045 			else {
1046 				final ResourceMap r = getContext().getResourceMap();
1047 				if (true)
1048 					try {
1049 						img = ImageIO.read(this.getClass().getResource(
1050 								"/" + r.getResourcesDir() + "/"
1051 										+ r.getString("Application.icon")));
1052 					} catch (final IOException e) {
1053 						throw new RuntimeException("Unhandled", e);
1054 					}
1055 				else
1056 					img = Toolkit.getDefaultToolkit().createImage(
1057 							this.getClass().getResource(
1058 									"/" + r.getResourcesDir() + "/"
1059 											+ r.getString("Application.icon")));
1060 			}
1061 			getMainFrame().setIconImage(img);
1062 			// SystemTray tray = SystemTray.getSystemTray();
1063 		}
1064 
1065 		// File Filter
1066 		jcxzPat = gui.createFileFilter("fileFilterJcxz", "jcz", "jcx");
1067 		pngPat = gui.createFileFilter("fileFilterPng", "png");
1068 		svgPat = gui.createFileFilter("fileFilterSvg", "svgz", "svg");
1069 
1070 		getMainFrame().setJMenuBar(createMenuBar());
1071 
1072 		final JComponent c = new JPanel();
1073 		c.setLayout(new BorderLayout());
1074 		tactics.setPreferredSize(new Dimension(400, 600));
1075 		c.add(tactics, BorderLayout.CENTER);
1076 		c.add(url, BorderLayout.NORTH);
1077 		{
1078 			final JPanel b = new JPanel();
1079 			b.setLayout(new BorderLayout());
1080 			final JTabbedPane t = new JTabbedPane(SwingConstants.TOP,
1081 					JTabbedPane.SCROLL_TAB_LAYOUT);
1082 			t.add("Rock", broomSwing);
1083 			t.setMnemonicAt(0, 'R');
1084 			t.add("Ice", curlerSwing);
1085 			t.setMnemonicAt(1, 'I');
1086 			t.add("Collission", new JLabel("TODO: Collission settings"));
1087 			t.setMnemonicAt(2, 'C');
1088 			b.add(t, BorderLayout.NORTH);
1089 			if (false)
1090 				b.add(new JLabel("TODO: Bird's eye view"), BorderLayout.CENTER);
1091 			else
1092 				b.add(birdPiccolo, BorderLayout.CENTER);
1093 			c.add(b, BorderLayout.EAST);
1094 		}
1095 
1096 		show(c);
1097 		view12Foot();
1098 	}
1099 
1100 	private Cursor switchCursor(final Cursor neo) {
1101 		final Cursor cu = getMainFrame().getCursor();
1102 		getMainFrame().setCursor(neo);
1103 		Thread.yield();
1104 		return cu;
1105 	}
1106 
1107 	public void undoableEditHappened(final UndoableEditEvent e) {
1108 		setCanRedo(change.canRedo());
1109 		setCanUndo(change.canUndo());
1110 	}
1111 
1112 	/** View Menu Action */
1113 	@Action
1114 	public void view12Foot() {
1115 		zh.zoom(tactics, ZoomHelper.TwelvePlus, SLOW);
1116 	}
1117 
1118 	/** View Menu Action */
1119 	@Action
1120 	public void viewActive() {
1121 		zh.zoom(tactics, ZoomHelper.ActivePlus, SLOW);
1122 	}
1123 
1124 	/** View Menu Action */
1125 	@Action
1126 	public void viewComplete() {
1127 		zh.zoom(tactics, ZoomHelper.CompletePlus, SLOW);
1128 	}
1129 
1130 	/** View Menu Action */
1131 	@Action
1132 	public void viewHouse() {
1133 		zh.zoom(tactics, ZoomHelper.HousePlus, SLOW);
1134 	}
1135 
1136 	/** View Menu Action */
1137 	@Action
1138 	public void viewPanEast() {
1139 		zh.pan(tactics, 0.2, 0, FAST);
1140 	}
1141 
1142 	/** View Menu Action */
1143 	@Action
1144 	public void viewPanNorth() {
1145 		zh.pan(tactics, 0, -0.2, FAST);
1146 	}
1147 
1148 	/** View Menu Action */
1149 	@Action
1150 	public void viewPanSouth() {
1151 		zh.pan(tactics, 0, 0.2, FAST);
1152 	}
1153 
1154 	/** View Menu Action */
1155 	@Action
1156 	public void viewPanWest() {
1157 		zh.pan(tactics, -0.2, 0, FAST);
1158 	}
1159 
1160 	/** View Menu Action */
1161 	@Action
1162 	public void viewZoomIn() {
1163 		zh.zoom(tactics, null, 0.75, FAST);
1164 	}
1165 
1166 	/** View Menu Action */
1167 	@Action
1168 	public void viewZoomOut() {
1169 		zh.zoom(tactics, null, 1.25, FAST);
1170 	}
1171 }