org.jcurl.demo.tactics.JCurlShotPlanner.java Source code

Java tutorial

Introduction

Here is the source code for org.jcurl.demo.tactics.JCurlShotPlanner.java

Source

/*
 * jcurl java curling software framework http://www.jcurl.org Copyright (C)
 * 2005-2009 M. Rohrmoser
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

package org.jcurl.demo.tactics;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.List;
import java.util.Locale;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
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.JSeparator;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.filechooser.FileFilter;
import javax.swing.undo.CompoundEdit;

import org.apache.commons.logging.Log;
import org.jcurl.batik.BatikWrapper;
import org.jcurl.core.api.ComputedTrajectorySet;
import org.jcurl.core.api.IceSize;
import org.jcurl.core.api.RockProps;
import org.jcurl.core.api.RockSet;
import org.jcurl.core.api.RockSetUtils;
import org.jcurl.core.api.TrajectorySet;
import org.jcurl.core.api.Unit;
import org.jcurl.core.api.RockType.Pos;
import org.jcurl.core.api.RockType.Vel;
import org.jcurl.core.helpers.BatikButler;
import org.jcurl.core.io.IONode;
import org.jcurl.core.io.IOTrajectories;
import org.jcurl.core.io.JCurlSerializer;
import org.jcurl.core.io.JDKSerializer;
import org.jcurl.core.log.JCLoggerFactory;
import org.jcurl.core.ui.BroomPromptModel;
import org.jcurl.core.ui.ChangeManager;
import org.jcurl.core.ui.FileNameExtensionFilter;
import org.jcurl.core.ui.PosMemento;
import org.jcurl.core.ui.SuspendMemento;
import org.jcurl.core.ui.UndoableMemento;
import org.jcurl.core.ui.BroomPromptModel.HandleMemento;
import org.jcurl.core.ui.BroomPromptModel.IndexMemento;
import org.jcurl.core.ui.BroomPromptModel.SplitMemento;
import org.jcurl.core.ui.BroomPromptModel.XYMemento;
import org.jdesktop.application.Action;
import org.jdesktop.application.Application;
import org.jdesktop.application.ApplicationAction;
import org.jdesktop.application.ApplicationContext;
import org.jdesktop.application.ResourceMap;
import org.jdesktop.application.SingleFrameApplication;
import org.jdesktop.application.Task;
import org.jdesktop.application.Task.BlockingScope;

/**
 * Makes heavy use of the <a
 * href="https://appframework.dev.java.net/intro/index.html">Swing Application
 * Framework</a>.
 * 
 * @author <a href="mailto:m@jcurl.org">M. Rohrmoser </a>
 * @version $Id$
 */
public class JCurlShotPlanner extends SingleFrameApplication implements UndoableEditListener {
    private static class ChangeListenerManager implements ChangeListener {
        private final JCurlShotPlanner host;

        public ChangeListenerManager(final JCurlShotPlanner host) {
            this.host = host;
        }

        public void deregister(final ComputedTrajectorySet cts) {
            if (cts == null)
                return;
            cts.getInitialPos().removeRockListener(this);
            cts.getInitialVel().removeRockListener(this);
            // cts.getCurrentPos().removeRockListener(this);
            // cts.getCurrentSpeed().removeRockListener(this);
        }

        public void register(final ComputedTrajectorySet cts) {
            if (cts == null)
                return;
            cts.getInitialPos().addRockListener(this);
            cts.getInitialVel().addRockListener(this);
            // cts.getCurrentPos().addRockListener(this);
            // cts.getCurrentSpeed().addRockListener(this);
        }

        public void stateChanged(final ChangeEvent e) {
            host.setModified(true);
        }
    }

    static class GuiUtil {

        private static final Insets zeroInsets = new Insets(0, 0, 0, 0);

        private final ApplicationContext act;;

        public GuiUtil(final ApplicationContext act) {
            this.act = act;
        }

        /**
         * Create a simple about box JDialog that displays the standard
         * Application resources, like {@code Application.title} and
         * {@code Application.description}. The about box's labels and fields
         * are configured by resources that are injected when the about box is
         * shown (see SingleFrameApplication#show). The resources are defined in
         * the application resource file: resources/DocumentExample.properties.
         * 
         * From:
         * https://appframework.dev.java.net/downloads/AppFramework-1.03-src.zip
         * DocumentExample
         */
        private JDialog createAboutBox(final Frame owner) {
            final JPanel panel = new JPanel(new GridBagLayout());
            panel.setBorder(new EmptyBorder(0, 28, 16, 28)); // top, left,
            // bottom, right
            final JLabel titleLabel = new JLabel();
            titleLabel.setName("aboutTitleLabel");
            final GridBagConstraints c = new GridBagConstraints();
            initGridBagConstraints(c);
            c.anchor = GridBagConstraints.WEST;
            c.gridwidth = GridBagConstraints.REMAINDER;
            c.fill = GridBagConstraints.HORIZONTAL;
            c.ipady = 32;
            c.weightx = 1.0;
            panel.add(titleLabel, c);
            final String[] fields = { "description", "version", "vendor", "home" };
            for (final String field : fields) {
                final JLabel label = new JLabel();
                label.setName(field + "Label");
                initGridBagConstraints(c);
                // c.anchor = GridBagConstraints.BASELINE_TRAILING; 1.6 ONLY
                c.anchor = GridBagConstraints.EAST;
                panel.add(label, c);
                initGridBagConstraints(c);
                c.weightx = 1.0;
                c.gridwidth = GridBagConstraints.REMAINDER;
                c.fill = GridBagConstraints.HORIZONTAL;
                final JTextField textField = new JTextField();
                textField.setName(field + "TextField");
                textField.setEditable(false);
                textField.setBorder(null);
                panel.add(textField, c);
            }
            final JButton closeAboutButton = new JButton();
            closeAboutButton.setAction(findAction("closeAboutBox"));
            initGridBagConstraints(c);
            c.anchor = GridBagConstraints.EAST;
            c.gridx = 1;
            panel.add(closeAboutButton, c);
            final JDialog dialog = new JDialog(owner);
            dialog.setName("aboutDialog");
            dialog.add(panel, BorderLayout.CENTER);
            return dialog;
        }

        private JFileChooser createFileChooser(final File base, final String resourceName,
                final FileFilter filter) {
            final JFileChooser fc = new JFileChooser(base);
            fc.setName(resourceName);
            fc.setMultiSelectionEnabled(false);
            fc.setAcceptAllFileFilterUsed(true);
            fc.setFileFilter(filter);
            getContext().getResourceMap().injectComponents(fc);
            return fc;
        }

        private FileNameExtensionFilter createFileFilter(final String resourceName, final String... extensions) {
            final ResourceMap appResourceMap = getContext().getResourceMap();
            final String key = resourceName + ".description";
            final String desc = appResourceMap.getString(key);
            return new FileNameExtensionFilter(desc == null ? key : desc, extensions);
        }

        private JMenu createMenu(final String menuName, final String[] actionNames) {
            final JMenu menu = new JMenu();
            menu.setName(menuName);
            for (final String actionName : actionNames)
                if (actionName.equals("---"))
                    menu.add(new JSeparator());
                else {
                    final JMenuItem menuItem = new JMenuItem();
                    menuItem.setAction(findAction(actionName));
                    menuItem.setIcon(null);
                    menu.add(menuItem);
                }
            return menu;
        }

        private File ensureSuffix(final File dst, final FileNameExtensionFilter pat) {
            if (pat.accept(dst))
                return dst;
            return new File(dst.getAbsoluteFile() + "." + pat.getExtensions()[0]);
        }

        private javax.swing.Action findAction(final String actionName) {
            return getContext().getActionMap().get(actionName);
        };

        public ApplicationContext getContext() {
            return act;
        };

        private void initGridBagConstraints(final GridBagConstraints c) {
            c.anchor = GridBagConstraints.CENTER;
            c.fill = GridBagConstraints.NONE;
            c.gridwidth = 1;
            c.gridheight = 1;
            c.gridx = GridBagConstraints.RELATIVE;
            c.gridy = GridBagConstraints.RELATIVE;
            c.insets = zeroInsets;
            c.ipadx = 4; // not the usual default
            c.ipady = 4; // not the usual default
            c.weightx = 0.0;
            c.weighty = 0.0;
        };
    }

    abstract static class WaitCursorTask<T, V> extends Task<T, V> {
        private final SingleFrameApplication app;

        public WaitCursorTask(final SingleFrameApplication app) {
            super(app);
            this.app = app;
        }

        protected abstract T doCursor() throws Exception;

        @Override
        protected T doInBackground() throws Exception {
            final Cursor cu = app.getMainFrame().getCursor();
            try {
                app.getMainFrame().setCursor(waitc);
                Thread.yield();
                return doCursor();
            } finally {
                app.getMainFrame().setCursor(cu);
                Thread.yield();
            }
        }
    }

    public static class ZoomHelper {
        /**
         * Inter-hog area area plus house area plus 1 rock margin plus "out"
         * rock space.
         */
        public static final Rectangle2D ActivePlus;
        /** All from back to back */
        public static final Rectangle2D CompletePlus;
        /** House area plus 1 rock margin plus "out" rock space. */
        public static final Rectangle2D HousePlus;
        /** 12-foot circle plus 1 rock */
        public static final Rectangle2D TwelvePlus;
        static {
            final double r2 = 2 * RockProps.DEFAULT.getRadius();
            final double x = IceSize.SIDE_2_CENTER + r2;
            HousePlus = new Rectangle2D.Double(-x, -(IceSize.HOG_2_TEE + r2), 2 * x,
                    IceSize.HOG_2_TEE + IceSize.BACK_2_TEE + 3 * r2 + 2 * r2);
            final double c12 = r2 + Unit.f2m(6.0);
            TwelvePlus = new Rectangle2D.Double(-c12, -c12, 2 * c12, 2 * c12);
            ActivePlus = new Rectangle2D.Double(-x, -(IceSize.HOG_2_HOG + IceSize.HOG_2_TEE + r2), 2 * x,
                    IceSize.HOG_2_HOG + IceSize.HOG_2_TEE + IceSize.BACK_2_TEE + 3 * r2 + 2 * r2);
            CompletePlus = new Rectangle2D.Double(-x,
                    -(IceSize.HOG_2_TEE + IceSize.HOG_2_HOG + IceSize.HACK_2_HOG + r2), 2 * x,
                    IceSize.HOG_2_HOG + 2 * IceSize.HACK_2_HOG);
        }

        private void pan(final Zoomable dst, final double rx, final double ry, final int dt) {
            if (dst == null)
                return;
            final RectangularShape src = dst.getZoom();
            zoom(dst, new Rectangle2D.Double(src.getX() + src.getWidth() * rx, src.getY() + src.getHeight() * ry,
                    src.getWidth(), src.getHeight()), dt);
        }

        private void zoom(final Zoomable dst, final Point2D center, final double ratio, final int dt) {
            if (dst == null)
                return;
            final RectangularShape src = dst.getZoom();
            final double w = src.getWidth() * ratio;
            final double h = src.getHeight() * ratio;
            final double cx, cy;
            if (center == null) {
                cx = src.getCenterX();
                cy = src.getCenterY();
            } else {
                cx = center.getX();
                cy = center.getY();
            }
            zoom(dst, new Rectangle2D.Double(cx - w / 2, cy - h / 2, Math.abs(w), Math.abs(h)), dt);
        }

        private void zoom(final Zoomable dst, final RectangularShape viewport, final int dt) {
            if (dst == null)
                return;
            dst.setZoom(viewport, dt);
        }
    }

    private static final BatikButler batik = new BatikButler();
    private static final double currentTime = 30;
    private static final int FAST = 200;
    private static URL initialScene = null;
    private static final Log log = JCLoggerFactory.getLogger(JCurlShotPlanner.class);
    private static final int SLOW = 333;
    private static URL templateScene = null;
    private static final Cursor waitc = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR);
    private static final ZoomHelper zh = new ZoomHelper();

    public static void main(final String[] args) {
        // for debugging reasons only:
        Locale.setDefault(Locale.CANADA);
        launch(JCurlShotPlanner.class, args);
    }

    private static void renderPng(final Container src, final File dst) throws IOException {
        final BufferedImage img = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_INT_ARGB);
        final Graphics g = img.getGraphics();
        try {
            // SwingUtilities.paintComponent(g, src, src.getBounds(), null);
            src.paintAll(g);
        } finally {
            g.dispose();
        }
        ImageIO.write(img, "png", dst);
    }

    @SuppressWarnings("unchecked")
    private static CompoundEdit reset(final ComputedTrajectorySet cts, final BroomPromptModel broom,
            final boolean outPosition) {
        final RockSet<Pos> ipos = cts.getInitialPos();
        final RockSet<Vel> ivel = cts.getInitialVel();
        // store the initial state:
        final PosMemento[] pm = new PosMemento[RockSet.ROCKS_PER_SET];
        for (int i16 = RockSet.ROCKS_PER_SET - 1; i16 >= 0; i16--)
            pm[i16] = new PosMemento(ipos, i16, ipos.getRock(i16).p());
        final IndexMemento bi = new IndexMemento(broom, broom.getIdx16());
        final HandleMemento bh = new HandleMemento(broom, broom.getOutTurn());
        final XYMemento bxy = new XYMemento(broom, broom.getBroom());
        final SplitMemento bs = new SplitMemento(broom, broom.getSplitTimeMillis().getValue());
        final boolean preS = cts.getSuspended();
        cts.setSuspended(true);
        try {
            // reset:
            RockSet.allZero(ivel);
            broom.setIdx16(-1);
            if (outPosition)
                RockSetUtils.allOut(ipos);
            else
                RockSetUtils.allHome(ipos);
            broom.setIdx16(1);
            broom.setBroom(new Point2D.Double(0, 0));
            broom.getSplitTimeMillis().setValue(3300);
        } finally {
            cts.setSuspended(preS);
        }
        // create a compound edit
        final CompoundEdit ce = new CompoundEdit();
        ce.addEdit(new UndoableMemento(new SuspendMemento(cts, preS), new SuspendMemento(cts, true)));
        for (int i16 = RockSet.ROCKS_PER_SET - 1; i16 >= 0; i16--)
            ce.addEdit(new UndoableMemento(pm[i16], new PosMemento(ipos, i16, ipos.getRock(i16).p())));
        ce.addEdit(new UndoableMemento(bi, new IndexMemento(broom, broom.getIdx16())));
        ce.addEdit(new UndoableMemento(bh, new HandleMemento(broom, broom.getOutTurn())));
        ce.addEdit(new UndoableMemento(bxy, new XYMemento(broom, broom.getBroom())));
        ce.addEdit(new UndoableMemento(bs, new SplitMemento(broom, broom.getSplitTimeMillis().getValue())));
        ce.addEdit(new UndoableMemento(new SuspendMemento(cts, true), new SuspendMemento(cts, preS)));
        ce.end();
        return ce;
    }

    private JDialog aboutBox = null;
    private final BirdPiccoloBean birdPiccolo = new BirdPiccoloBean();
    private final BroomPromptSwingBean broomSwing = new BroomPromptSwingBean();
    private boolean canRedo = false;
    private boolean canUndo = false;
    private final ChangeManager change = new ChangeManager();
    private final ChangeListenerManager cm = new ChangeListenerManager(this);
    private final CurlerSwingBean curlerSwing = new CurlerSwingBean();
    private URL document;
    private File file;
    private final GuiUtil gui = new GuiUtil(getContext());
    private FileNameExtensionFilter jcxzPat;
    private boolean modified = false;
    private FileNameExtensionFilter pngPat;
    private FileNameExtensionFilter svgPat;
    private final TrajectoryBean tactics = new TrajectoryPiccoloBean();
    private final JLabel url = new JLabel();

    private JCurlShotPlanner() {
        change.addUndoableEditListener(this);
        tactics.setChanger(change);
        broomSwing.setChanger(change);
        curlerSwing.setChanger(change);
        birdPiccolo.setMaster(tactics);
        // tactics.setName("tactics");
        url.setName("urlLabel");
    }

    public boolean askDiscardUnsaved(final javax.swing.Action action) {
        if (!isModified())
            return true;
        final String title, msg;
        if (true) {
            final ResourceMap r = getContext().getResourceMap();
            title = r.getString("discard" + ".Dialog" + ".title", action.getValue(javax.swing.Action.NAME));
            msg = r.getString("discard" + ".Dialog" + ".message");
        } else if (action instanceof ApplicationAction) {
            final ApplicationAction aa = (ApplicationAction) action;
            final ResourceMap r = getContext().getResourceMap();
            title = action == null ? null
                    : r.getString(aa.getName() + ".Dialog" + ".title", aa.getValue(javax.swing.Action.NAME));
            msg = r.getString(aa.getName() + ".Dialog" + ".message");
        } else {
            title = null;
            msg = "Discard unsaved changes?";
        }
        return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(getMainFrame(), msg, title,
                JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
    }

    public boolean askOverwrite(final File f) {
        if (!f.exists())
            return true;
        final String title, msg;
        {
            final ResourceMap r = getContext().getResourceMap();
            title = r.getString("overwrite" + ".Dialog" + ".title");
            msg = r.getString("overwrite" + ".Dialog" + ".message", f.toString());
        }
        return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(getMainFrame(), msg, title,
                JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
    };

    @Action
    public void closeAboutBox() {
        if (aboutBox == null)
            return;
        aboutBox.setVisible(false);
        aboutBox = null;
    }

    private JFileChooser createJcxChooser(final File base, final String name) {
        return gui.createFileChooser(base, name, jcxzPat);
    };

    private JMenuBar createMenuBar() {
        final JMenuBar menuBar = new JMenuBar();

        final String[] fileMenuActionNames = { /*"fileClear",*/
                "fileNewDoc", "fileHammy", "---", "fileOpen", "fileOpenURL", "---", "fileReset", "fileSave",
                "fileSaveAs", "fileSaveCopyAs", "---", "fileExportPng", "fileExportSvg", "---", "quit" };
        menuBar.add(gui.createMenu("fileMenu", fileMenuActionNames));

        final String[] editMenuActionNames = { "editUndo", "editRedo", "---", "editOut", "editHome", "---",
                "editProperties", "---", "editPreferences" };
        menuBar.add(gui.createMenu("editMenu", editMenuActionNames));

        final String[] viewMenuActionNames = { "viewHouse", "view12Foot", "viewComplete", "viewActive", "---",
                "viewZoomIn", "viewZoomOut", "---", "viewPanNorth", "viewPanSouth", "viewPanEast", "viewPanWest" };
        menuBar.add(gui.createMenu("viewMenu", viewMenuActionNames));

        final String[] helpMenuActionNames = { "showAboutBox", "helpDumpProperties" };
        menuBar.add(gui.createMenu("helpMenu", helpMenuActionNames));

        return menuBar;
    }

    private JFileChooser createPngChooser(final File base, final String name) {
        return gui.createFileChooser(base, name, pngPat);
    }

    private JFileChooser createSvgChooser(final File base, final String name) {
        return gui.createFileChooser(base, name, svgPat);
    }

    private JComponent createToolBar() {
        final String[] toolbarActionNames = { "cut", "copy", "paste" };
        final JToolBar toolBar = new JToolBar();
        toolBar.setFloatable(false);
        for (final String actionName : toolbarActionNames) {
            final JButton button = new JButton();
            button.setAction(gui.findAction(actionName));
            button.setFocusable(false);
            toolBar.add(button);
        }
        return toolBar;
    }

    /** Edit Menu Action */
    @Action
    public void editHome() {
        final ComputedTrajectorySet cts = tactics.getCurves();
        if (cts == null)
            return;
        change.addEdit(reset(cts, tactics.getBroom(), false));
    }

    /** Edit Menu Action */
    @Action
    public void editOut() {
        final ComputedTrajectorySet cts = tactics.getCurves();
        if (cts == null)
            return;
        change.addEdit(reset(cts, tactics.getBroom(), true));
    }

    /** Edit Menu Action */
    @Action(enabledProperty = "alwaysFalse")
    public void editPreferences() {
    }

    /** Edit Menu Action */
    @Action(enabledProperty = "alwaysFalse")
    public void editProperties() {
    }

    /** Edit Menu Action */
    @Action(enabledProperty = "canRedo")
    public void editRedo() {
        change.redo();
    }

    /** Edit Menu Action */
    @Action(enabledProperty = "canUndo")
    public void editUndo() {
        change.undo();
    }

    /** File Menu Action */
    @Action
    private void fileClear() {
        if (!askDiscardUnsaved(gui.findAction("fileClear")))
            return;
        try {
            setDocument(null);
        } catch (final IOException e) {
            throw new RuntimeException("Unhandled", e);
        }
    }

    /**
     * Render the current view into a <a
     * href="http://en.wikipedia.org/wiki/Portable_Network_Graphics">PNG</a>
     * image (File Menu Action).
     * 
     * @see ImageIO#write(java.awt.image.RenderedImage, String, File)
     */
    @Action(block = BlockingScope.ACTION)
    public Task<Void, Void> fileExportPng() {
        final JFileChooser fcPng = createPngChooser(getFile(), "exportPngFileChooser");
        for (;;) {
            if (JFileChooser.APPROVE_OPTION != fcPng.showSaveDialog(getMainFrame()))
                return null;
            final File dst = gui.ensureSuffix(fcPng.getSelectedFile(), pngPat);
            if (!askOverwrite(dst))
                continue;

            return new WaitCursorTask<Void, Void>(this) {
                @Override
                protected Void doCursor() throws Exception {
                    renderPng(tactics, dst);
                    return null;
                }
            };
        }
    }

    /**
     * Render the current view into a <a
     * href="http://en.wikipedia.org/wiki/Svg">SVG</a>
     * image (File Menu Action).
     * 
     * @see BatikWrapper#renderSvg(Container, java.io.OutputStream)
     */
    @Action(enabledProperty = "renderSvgAvailable", block = BlockingScope.ACTION)
    public Task<Void, Void> fileExportSvg() {
        final JFileChooser fcSvg = createSvgChooser(getFile(), "exportSvgFileChooser");
        for (;;) {
            if (JFileChooser.APPROVE_OPTION != fcSvg.showSaveDialog(getMainFrame()))
                return null;
            final File dst = gui.ensureSuffix(fcSvg.getSelectedFile(), svgPat);
            if (!askOverwrite(dst))
                continue;

            return new WaitCursorTask<Void, Void>(this) {
                @Override
                protected Void doCursor() throws Exception {
                    batik.renderSvg(tactics, dst);
                    return null;
                }
            };
        }
    }

    /** File Menu Action */
    @Action(block = BlockingScope.APPLICATION)
    public void fileHammy() {
        if (!askDiscardUnsaved(gui.findAction("fileHammy")))
            return;
        try {
            setDocument(initialScene);
        } catch (final IOException e) {
            throw new RuntimeException("Unhandled", e);
        }
    }

    /** File Menu Action */
    @Action
    public void fileNewDoc() {
        if (!askDiscardUnsaved(gui.findAction("fileNewDoc")))
            return;
        try {
            setDocument(templateScene);
        } catch (final IOException e) {
            throw new RuntimeException("Unhandled", e);
        }
    }

    /** File Menu Action */
    @Action
    public void fileOpen() {
        if (!askDiscardUnsaved(gui.findAction("fileOpen")))
            return;
        final JFileChooser chooser = createJcxChooser(getFile(), "openFileChooser");
        if (chooser.showOpenDialog(getMainFrame()) == JFileChooser.APPROVE_OPTION) {
            final File file = chooser.getSelectedFile();
            try {
                setDocument(file.toURI().toURL());
            } catch (final MalformedURLException e) {
                // shouldn't happen unless the JRE fails
                log.warn("File.toURI().toURL() failed", e);
            } catch (final IOException e) {
                showErrorDialog("can't open \"" + file + "\"", e);
            }
        }
    };

    /** File Menu Action */
    @Action
    public void fileOpenURL() {
        final String a = "fileOpenURL";
        if (!askDiscardUnsaved(gui.findAction(a)))
            return;
        final ResourceMap r = getContext().getResourceMap();
        final String title = r.getString(a + ".Dialog" + ".title");
        final String msg = r.getString(a + ".Dialog" + ".message");
        for (;;) {
            final String url = JOptionPane.showInputDialog(getMainFrame(), msg, title,
                    JOptionPane.QUESTION_MESSAGE);
            if (url == null)
                return;
            try {
                setDocument(new URL(url));
                return;
            } catch (final IOException e) {
                showErrorDialog(r.getString(a + ".Dialog" + ".error", url), e);
            }
        }
    }

    /**
     * File Menu Action
     * 
     * @throws IOException
     */
    @Action(enabledProperty = "modified")
    public void fileReset() throws IOException {
        if (!askDiscardUnsaved(gui.findAction("fileReset")))
            return;
        final URL tmp = getDocument();
        setDocument(null);
        setDocument(tmp);
    };

    /** File Menu Action */
    @Action(enabledProperty = "modified")
    public void fileSave() {
        if (!isModified())
            return;
        final File f = saveHelper(getFile(), getFile(), "saveFileChooser", true);
        log.info(f);
        if (f != null) {
            try {
                setDocument(f.toURL(), false);
            } catch (final IOException e) {
                throw new RuntimeException("Unhandled", e);
            }
            setModified(false);
        }
    };

    /** File Menu Action */
    @Action
    public void fileSaveAs() {
        final File f = saveHelper(null, getFile(), "saveAsFileChooser", false);
        log.info(f);
        if (f != null) {
            try {
                setDocument(f.toURL(), false);
            } catch (final IOException e) {
                throw new RuntimeException("Unhandled", e);
            }
            setModified(false);
        }
    }

    /** File Menu Action */
    @Action
    public void fileSaveCopyAs() {
        log.info(saveHelper(null, getFile(), "saveCopyAsFileChooser", false));
    }

    private URL getDocument() {
        return document;
    }

    private File getFile() {
        return file;
    }

    @Action()
    public void helpDumpProperties() {
        final List keys = new ArrayList();
        keys.addAll(System.getProperties().keySet());
        Collections.sort(keys);
        for (final Object key : keys)
            System.out.println(key + "=" + System.getProperty((String) key));
    }

    /**
     * Setting the internal field {@link #document} directly (bypassing
     * {@link #setDocument(URL)}) is used to deplay the document loading until
     * {@link #ready()}.
     */
    @Override
    protected void initialize(final String[] as) {
        if ("Linux".equals(System.getProperty("os.name")))
            getContext().getResourceManager().setPlatform("linux");

        final Class<?> mc = this.getClass();
        {
            final ResourceMap r = Application.getInstance().getContext().getResourceMap();
            initialScene = mc.getResource("/" + r.getResourcesDir() + r.getString("Application.defaultDocument"));
            templateScene = mc.getResource("/" + r.getResourcesDir() + r.getString("Application.templateDocument"));
        }

        // schedule the document to load in #ready()
        document = initialScene;
        for (final String p : as) {
            // ignore javaws parameters
            if ("-open".equals(p) || "-print".equals(p))
                continue;
            try {
                document = new URL(p);
                break;
            } catch (final MalformedURLException e) {
                final File f = new File(p);
                if (f.canRead())
                    try {
                        document = f.toURL();
                        break;
                    } catch (final MalformedURLException e2) {
                        log.warn("Cannot load '" + p + "'.", e);
                    }
                else
                    log.warn("Cannot load '" + p + "'.", e);
            }
        }
    }

    public boolean isAlwaysFalse() {
        return false;
    }

    public boolean isCanRedo() {
        return canRedo;
    }

    public boolean isCanUndo() {
        return canUndo;
    }

    public boolean isModified() {
        return modified;
    }

    public boolean isRenderSvgAvailable() {
        return batik.isBatikAvailable();
    }

    @Override
    protected void ready() {
        final URL tmp = document;
        document = null;
        try {
            setDocument(tmp);
        } catch (final IOException e) {
            log.warn("Couldn't load '" + tmp + "'.", e);
        }
        addExitListener(new Application.ExitListener() {
            public boolean canExit(final EventObject e) {
                return askDiscardUnsaved(gui.findAction("quit"));
            }

            public void willExit(final EventObject e) {
                log.info("Good bye!");
            }
        });
    };

    private final void save(final TrajectorySet cts, final File dst) throws IOException {
        final Cursor cu = switchCursor(waitc);
        try {
            final IOTrajectories t = new IOTrajectories();
            // TODO add annotations
            t.trajectories().add(cts);
            new JCurlSerializer().write(t, dst, JDKSerializer.class);
        } finally {
            switchCursor(cu);
        }
    }

    private File saveHelper(File dst, final File base, final String name, final boolean forceOverwrite) {
        JFileChooser fcJcx = null;
        for (;;) {
            if (fcJcx == null)
                fcJcx = createJcxChooser(base, name);
            if (dst == null) {
                if (JFileChooser.APPROVE_OPTION != fcJcx.showSaveDialog(getMainFrame()))
                    return null;
                dst = fcJcx.getSelectedFile();
            }
            if (dst == null)
                continue;
            dst = gui.ensureSuffix(dst, jcxzPat);
            if (forceOverwrite || askOverwrite(dst))
                try {
                    save(tactics.getCurves(), dst);
                    return dst;
                } catch (final Exception e) {
                    showErrorDialog("Couldn't save to '" + dst + "'", e);
                }
            else
                dst = null;
        }
    }

    private void setCanRedo(final boolean canRedo) {
        final boolean old = this.canRedo;
        if (old == canRedo)
            return;
        firePropertyChange("canRedo", old, this.canRedo = canRedo);
    }

    private void setCanUndo(final boolean canUndo) {
        final boolean old = this.canUndo;
        if (old == canUndo)
            return;
        firePropertyChange("canUndo", old, this.canUndo = canUndo);
    }

    private void setDocument(final URL document) throws IOException {
        this.setDocument(document, true);
    }

    private void setDocument(final URL document, boolean load) throws IOException {
        final Cursor cu = switchCursor(waitc);
        try {
            log.info(document);
            final URL old = this.document;
            this.firePropertyChange("document", old, this.document = document);
            setFile(this.document);
            url.setText(this.document == null ? "{null}" : this.document.toString());
            if (!load)
                return;

            cm.deregister(tactics.getCurves());
            final ComputedTrajectorySet cts;
            if (this.document == null)
                cts = null;
            else {
                final IONode n = new JCurlSerializer().read(this.document);
                final IOTrajectories it = (IOTrajectories) n;
                final TrajectorySet ts = it.trajectories().get(0);
                cts = (ComputedTrajectorySet) ts;
            }
            change.discardAllEdits();
            if (cts != null)
                cts.setCurrentTime(currentTime);
            tactics.setCurves(cts);
            broomSwing.setBroom(tactics.getBroom());
            cm.register(cts);
            setModified(false);
        } finally {
            switchCursor(cu);
        }
    }

    private void setFile(final URL url) {
        File file;
        if (url != null && "file".equals(url.getProtocol()))
            try {
                file = new File(url.toURI());
            } catch (final URISyntaxException e) {
                file = null;
            }
        else
            file = null;
        final File old = this.file;
        this.firePropertyChange("file", old, this.file = file);
    }

    public void setModified(final boolean modified) {
        final boolean old = this.modified;
        if (old == modified)
            return;
        firePropertyChange("modified", old, this.modified = modified);
    }

    /** Show the about box dialog. */
    @Action(block = BlockingScope.COMPONENT)
    public void showAboutBox() {
        if (aboutBox == null)
            aboutBox = gui.createAboutBox(getMainFrame());
        show(aboutBox);
    }

    private void showErrorDialog(final String message, final Exception e) {
        JOptionPane.showMessageDialog(getMainFrame(), "Error: " + message, "Error", JOptionPane.ERROR_MESSAGE);
    }

    @Override
    protected void startup() {
        // set the window icon:
        {
            final Image img;
            if (true)
                img = getContext().getResourceMap().getImageIcon("Application.icon").getImage();
            else {
                final ResourceMap r = getContext().getResourceMap();
                if (true)
                    try {
                        img = ImageIO.read(this.getClass()
                                .getResource("/" + r.getResourcesDir() + "/" + r.getString("Application.icon")));
                    } catch (final IOException e) {
                        throw new RuntimeException("Unhandled", e);
                    }
                else
                    img = Toolkit.getDefaultToolkit().createImage(this.getClass()
                            .getResource("/" + r.getResourcesDir() + "/" + r.getString("Application.icon")));
            }
            getMainFrame().setIconImage(img);
            // SystemTray tray = SystemTray.getSystemTray();
        }

        // File Filter
        jcxzPat = gui.createFileFilter("fileFilterJcxz", "jcz", "jcx");
        pngPat = gui.createFileFilter("fileFilterPng", "png");
        svgPat = gui.createFileFilter("fileFilterSvg", "svgz", "svg");

        getMainFrame().setJMenuBar(createMenuBar());

        final JComponent c = new JPanel();
        c.setLayout(new BorderLayout());
        tactics.setPreferredSize(new Dimension(400, 600));
        c.add(tactics, BorderLayout.CENTER);
        c.add(url, BorderLayout.NORTH);
        {
            final JPanel b = new JPanel();
            b.setLayout(new BorderLayout());
            final JTabbedPane t = new JTabbedPane(SwingConstants.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
            t.add("Rock", broomSwing);
            t.setMnemonicAt(0, 'R');
            t.add("Ice", curlerSwing);
            t.setMnemonicAt(1, 'I');
            t.add("Collission", new JLabel("TODO: Collission settings"));
            t.setMnemonicAt(2, 'C');
            b.add(t, BorderLayout.NORTH);
            if (false)
                b.add(new JLabel("TODO: Bird's eye view"), BorderLayout.CENTER);
            else
                b.add(birdPiccolo, BorderLayout.CENTER);
            c.add(b, BorderLayout.EAST);
        }

        show(c);
        view12Foot();
    }

    private Cursor switchCursor(final Cursor neo) {
        final Cursor cu = getMainFrame().getCursor();
        getMainFrame().setCursor(neo);
        Thread.yield();
        return cu;
    }

    public void undoableEditHappened(final UndoableEditEvent e) {
        setCanRedo(change.canRedo());
        setCanUndo(change.canUndo());
    }

    /** View Menu Action */
    @Action
    public void view12Foot() {
        zh.zoom(tactics, ZoomHelper.TwelvePlus, SLOW);
    }

    /** View Menu Action */
    @Action
    public void viewActive() {
        zh.zoom(tactics, ZoomHelper.ActivePlus, SLOW);
    }

    /** View Menu Action */
    @Action
    public void viewComplete() {
        zh.zoom(tactics, ZoomHelper.CompletePlus, SLOW);
    }

    /** View Menu Action */
    @Action
    public void viewHouse() {
        zh.zoom(tactics, ZoomHelper.HousePlus, SLOW);
    }

    /** View Menu Action */
    @Action
    public void viewPanEast() {
        zh.pan(tactics, 0.2, 0, FAST);
    }

    /** View Menu Action */
    @Action
    public void viewPanNorth() {
        zh.pan(tactics, 0, -0.2, FAST);
    }

    /** View Menu Action */
    @Action
    public void viewPanSouth() {
        zh.pan(tactics, 0, 0.2, FAST);
    }

    /** View Menu Action */
    @Action
    public void viewPanWest() {
        zh.pan(tactics, -0.2, 0, FAST);
    }

    /** View Menu Action */
    @Action
    public void viewZoomIn() {
        zh.zoom(tactics, null, 0.75, FAST);
    }

    /** View Menu Action */
    @Action
    public void viewZoomOut() {
        zh.zoom(tactics, null, 1.25, FAST);
    }
}