VASSAL.build.module.map.LOS_Thread.java Source code

Java tutorial

Introduction

Here is the source code for VASSAL.build.module.map.LOS_Thread.java

Source

/*
 * $Id$
 *
 * Copyright (c) 2000-2003 by Rodney Kinney
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
package VASSAL.build.module.map;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.KeyStroke;

import org.apache.commons.lang.StringUtils;

import VASSAL.build.AbstractConfigurable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.build.module.Map;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.build.module.map.boardPicker.Board;
import VASSAL.build.module.map.boardPicker.board.MapGrid;
import VASSAL.build.module.map.boardPicker.board.ZonedGrid;
import VASSAL.build.module.map.boardPicker.board.mapgrid.Zone;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.configure.BooleanConfigurer;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.PlayerIdFormattedStringConfigurer;
import VASSAL.configure.StringEnum;
import VASSAL.configure.VisibilityCondition;
import VASSAL.counters.GamePiece;
import VASSAL.i18n.Resources;
import VASSAL.i18n.TranslatableConfigurerFactory;
import VASSAL.tools.FormattedString;
import VASSAL.tools.LaunchButton;
import VASSAL.tools.NamedKeyStroke;
import VASSAL.tools.SequenceEncoder;
import VASSAL.tools.UniqueIdManager;

/**
 * A class that allows the user to draw a straight line on a Map (LOS
 * = Line Of Sight).  No automatic detection of obstacles is
 * performed; the user must simply observe the thread against the
 * image of the map.  However, if the user clicks on a board with a
 * {@link Map Grid}, the thread may snap to the grid and report the
 * distance between endpoints of the line
 * */
public class LOS_Thread extends AbstractConfigurable implements MouseListener, MouseMotionListener, Drawable,
        Configurable, UniqueIdManager.Identifyable, CommandEncoder {

    public static final String LOS_THREAD_COMMAND = "LOS\t";

    public static final String NAME = "threadName";
    public static final String SNAP_LOS = "snapLOS";
    public static final String SNAP_START = "snapStart";
    public static final String SNAP_END = "snapEnd";
    public static final String REPORT = "report";
    public static final String PERSISTENCE = "persistence";
    public static final String PERSISTENT_ICON_NAME = "persistentIconName";
    public static final String GLOBAL = "global";
    public static final String LOS_COLOR = "threadColor";
    public static final String HOTKEY = "hotkey";
    public static final String TOOLTIP = "tooltip";
    public static final String ICON_NAME = "iconName";
    public static final String LABEL = "label";
    public static final String DRAW_RANGE = "drawRange";
    public static final String HIDE_COUNTERS = "hideCounters";
    public static final String HIDE_OPACITY = "hideOpacity";
    public static final String RANGE_BACKGROUND = "rangeBg";
    public static final String RANGE_FOREGROUND = "rangeFg";
    public static final String RANGE_SCALE = "scale";
    public static final String RANGE_ROUNDING = "round";
    public static final String ROUND_UP = "Up";
    public static final String ROUND_DOWN = "Down";
    public static final String ROUND_OFF = "Nearest whole number";
    public static Font RANGE_FONT = new Font("Dialog", 0, 11);
    public static final String DEFAULT_ICON = "/images/thread.gif";

    public static final String FROM_LOCATION = "FromLocation";
    public static final String TO_LOCATION = "ToLocation";
    public static final String CHECK_COUNT = "NumberOfLocationsChecked";
    public static final String CHECK_LIST = "AllLocationsChecked";
    public static final String RANGE = "Range";

    public static final String NEVER = "Never";
    public static final String ALWAYS = "Always";
    public static final String CTRL_CLICK = "Ctrl-Click & Drag";
    public static final String WHEN_PERSISTENT = "When Persisting";

    protected static UniqueIdManager idMgr = new UniqueIdManager("LOS_Thread");

    protected boolean retainAfterRelease = false;
    protected long lastRelease = 0;

    protected Map map;
    protected LaunchButton launch;
    protected KeyStroke hotkey;
    protected Point anchor;
    protected Point arrow;
    protected boolean visible;
    protected boolean drawRange;
    protected int rangeScale;
    protected double rangeRounding = 0.5;
    protected boolean hideCounters;
    protected int hideOpacity = 0;
    protected String fixedColor;
    protected Color threadColor = Color.black, rangeFg = Color.white, rangeBg = Color.black;
    protected boolean snapStart;
    protected boolean snapEnd;
    protected Point lastAnchor = new Point();
    protected Point lastArrow = new Point();
    protected Rectangle lastRangeRect = new Rectangle();
    protected String anchorLocation = "";
    protected String lastLocation = "";
    protected String lastRange = "";
    protected FormattedString reportFormat = new FormattedString(
            "$playerId$ Checked LOS from $" + FROM_LOCATION + "$ to $" + CHECK_LIST + "$");
    protected List<String> checkList = new ArrayList<String>();
    protected String persistence = CTRL_CLICK;
    protected String persistentIconName;
    protected String global = ALWAYS;
    protected String threadId = "";
    protected boolean persisting = false;
    protected boolean mirroring = false;
    protected String iconName;
    protected boolean ctrlWhenClick = false;
    protected boolean initializing;

    public LOS_Thread() {
        anchor = new Point(0, 0);
        arrow = new Point(0, 0);
        visible = false;
        persisting = false;
        mirroring = false;
        ActionListener al = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                launch();
            }
        };
        launch = new LaunchButton("Thread", TOOLTIP, LABEL, HOTKEY, ICON_NAME, al);
        launch.setAttribute(ICON_NAME, DEFAULT_ICON);
        launch.setAttribute(TOOLTIP, "Show LOS Thread");
    }

    /**
     * @return whether the thread should be drawn
     */
    public boolean isVisible() {
        return visible;
    }

    /**
     * If true, draw the thread on the map
     */
    public void setVisible(boolean state) {
        visible = state;
    }

    /**
     * Expects to be added to a {@link Map}.  Adds a button to the map
     * window's toolbar.  Pushing the button pushes a MouseListener
     * onto the Map that draws the thread.  Adds some entries to
     * preferences
     *
     * @see Map#pushMouseListener*/
    public void addTo(Buildable b) {
        idMgr.add(this);
        map = (Map) b;
        map.getView().addMouseMotionListener(this);
        map.addDrawComponent(this);
        map.getToolBar().add(launch);
        GameModule.getGameModule().addCommandEncoder(this);
        GameModule.getGameModule().getPrefs().addOption(getConfigureName(),
                new BooleanConfigurer(SNAP_LOS, Resources.getString("LOS_Thread.snap_thread_preference")));

        if (fixedColor == null) {
            ColorConfigurer config = new ColorConfigurer(LOS_COLOR,
                    Resources.getString("LOS_Thread.thread_color_preference"));
            GameModule.getGameModule().getPrefs().addOption(getConfigureName(), config);
            threadColor = (Color) GameModule.getGameModule().getPrefs().getValue(LOS_COLOR);
            config.addPropertyChangeListener(new PropertyChangeListener() {
                public void propertyChange(PropertyChangeEvent evt) {
                    threadColor = (Color) evt.getNewValue();
                }
            });
            config.fireUpdate();
        }
    }

    public void removeFrom(Buildable b) {
        map = (Map) b;
        map.removeDrawComponent(this);
        map.getToolBar().remove(launch);
        GameModule.getGameModule().removeCommandEncoder(this);
        idMgr.remove(this);
    }

    /**
     * The attributes of an LOS_Thread are:
     * <pre>
     * <code>NAME</code>:  the name of the Preferences tab
     * <code>LABEL</code>:  the label of the button
     * <code>HOTKEY</code>:  the hotkey equivalent of the button
     * <code>DRAW_RANGE</code>:  If true, draw the distance between endpoints of the thread
     * <code>RANGE_FOREGROUND</code>:  the color of the text when drawing the distance
     * <code>RANGE_BACKGROUND</code>:  the color of the background rectangle when drawing the distance
     * <code>HIDE_COUNTERS</code>:  If true, hide all {@link GamePiece}s on the map when drawing the thread
     * </pre>
     */
    public String[] getAttributeNames() {
        return new String[] { NAME, LABEL, TOOLTIP, ICON_NAME, HOTKEY, REPORT, PERSISTENCE, PERSISTENT_ICON_NAME,
                GLOBAL, SNAP_START, SNAP_END, DRAW_RANGE, RANGE_SCALE, RANGE_ROUNDING, HIDE_COUNTERS, HIDE_OPACITY,
                LOS_COLOR, RANGE_FOREGROUND, RANGE_BACKGROUND };
    }

    public void setAttribute(String key, Object value) {
        if (DRAW_RANGE.equals(key)) {
            if (value instanceof String) {
                value = Boolean.valueOf((String) value);
            }
            drawRange = ((Boolean) value).booleanValue();
        } else if (NAME.equals(key)) {
            setConfigureName((String) value);
        } else if (RANGE_SCALE.equals(key)) {
            if (value instanceof String) {
                value = Integer.valueOf((String) value);
            }
            rangeScale = ((Integer) value).intValue();
        } else if (RANGE_ROUNDING.equals(key)) {
            if (ROUND_UP.equals(value)) {
                rangeRounding = 1.0;
            } else if (ROUND_DOWN.equals(value)) {
                rangeRounding = 0.0;
            } else {
                rangeRounding = 0.5;
            }
        } else if (HIDE_COUNTERS.equals(key)) {
            if (value instanceof String) {
                value = Boolean.valueOf((String) value);
            }
            hideCounters = ((Boolean) value).booleanValue();
        } else if (HIDE_OPACITY.equals(key)) {
            if (value instanceof String) {
                value = Integer.valueOf((String) value);
            }
            setTransparency(((Integer) value).intValue());
        } else if (RANGE_FOREGROUND.equals(key)) {
            if (value instanceof String) {
                value = ColorConfigurer.stringToColor((String) value);
            }
            rangeFg = (Color) value;
        } else if (RANGE_BACKGROUND.equals(key)) {
            if (value instanceof String) {
                value = ColorConfigurer.stringToColor((String) value);
            }
            rangeBg = (Color) value;
        } else if (LOS_COLOR.equals(key)) {
            if (value instanceof Color) {
                value = ColorConfigurer.colorToString((Color) value);
            }
            fixedColor = (String) value;
            threadColor = ColorConfigurer.stringToColor(fixedColor);
        } else if (SNAP_START.equals(key)) {
            if (value instanceof String) {
                value = Boolean.valueOf((String) value);
            }
            snapStart = ((Boolean) value).booleanValue();
        } else if (SNAP_END.equals(key)) {
            if (value instanceof String) {
                value = Boolean.valueOf((String) value);
            }
            snapEnd = ((Boolean) value).booleanValue();
        } else if (REPORT.equals(key)) {
            reportFormat.setFormat((String) value);
        } else if (PERSISTENCE.equals(key)) {
            persistence = (String) value;
        } else if (PERSISTENT_ICON_NAME.equals(key)) {
            persistentIconName = (String) value;
        } else if (GLOBAL.equals(key)) {
            global = (String) value;
        } else if (ICON_NAME.equals(key)) {
            iconName = (String) value;
            launch.setAttribute(ICON_NAME, iconName);
        } else {
            launch.setAttribute(key, value);
        }
    }

    protected void setTransparency(int h) {
        if (h < 0) {
            hideOpacity = 0;
        } else if (h > 100) {
            hideOpacity = 100;
        } else {
            hideOpacity = h;
        }
    }

    public String getAttributeValueString(String key) {
        if (DRAW_RANGE.equals(key)) {
            return String.valueOf(drawRange);
        } else if (NAME.equals(key)) {
            return getConfigureName();
        } else if (RANGE_SCALE.equals(key)) {
            return String.valueOf(rangeScale);
        } else if (RANGE_ROUNDING.equals(key)) {
            if (rangeRounding == 1.0) {
                return ROUND_UP;
            } else if (rangeRounding == 0.0) {
                return ROUND_DOWN;
            } else {
                return ROUND_OFF;
            }
        } else if (HIDE_COUNTERS.equals(key)) {
            return String.valueOf(hideCounters);
        } else if (HIDE_OPACITY.equals(key)) {
            return String.valueOf(hideOpacity);
        } else if (RANGE_FOREGROUND.equals(key)) {
            return ColorConfigurer.colorToString(rangeFg);
        } else if (RANGE_BACKGROUND.equals(key)) {
            return ColorConfigurer.colorToString(rangeBg);
        } else if (LOS_COLOR.equals(key)) {
            return fixedColor;
        } else if (SNAP_START.equals(key)) {
            return String.valueOf(snapStart);
        } else if (SNAP_END.equals(key)) {
            return String.valueOf(snapEnd);
        } else if (REPORT.equals(key)) {
            return reportFormat.getFormat();
        } else if (PERSISTENCE.equals(key)) {
            return persistence;
        } else if (PERSISTENT_ICON_NAME.equals(key)) {
            return persistentIconName;
        } else if (GLOBAL.equals(key)) {
            return global;
        } else if (ICON_NAME.equals(key)) {
            return iconName;
        } else {
            return launch.getAttributeValueString(key);
        }
    }

    public void setup(boolean show) {
        launch.setEnabled(show);
    }

    /**
     * With Global visibility, LOS_Thread now has a state that needs to be
     * communicated to clients on other machines
     */

    public String getState() {
        SequenceEncoder se = new SequenceEncoder(';');
        se.append(anchor.x).append(anchor.y).append(arrow.x).append(arrow.y);
        se.append(persisting);
        se.append(mirroring);
        return se.getValue();
    }

    public void setState(String state) {
        SequenceEncoder.Decoder sd = new SequenceEncoder.Decoder(state, ';');
        anchor.x = sd.nextInt(anchor.x);
        anchor.y = sd.nextInt(anchor.y);
        arrow.x = sd.nextInt(arrow.x);
        arrow.y = sd.nextInt(arrow.y);
        setPersisting(sd.nextBoolean(false));
        setMirroring(sd.nextBoolean(false));
    }

    public void draw(java.awt.Graphics g, Map m) {
        if (initializing || !visible) {
            return;
        }
        g.setColor(threadColor);
        Point mapAnchor = map.componentCoordinates(anchor);
        Point mapArrow = map.componentCoordinates(arrow);
        g.drawLine(mapAnchor.x, mapAnchor.y, mapArrow.x, mapArrow.y);
        Board b;

        if (drawRange) {
            if (rangeScale > 0) {
                int dist = (int) (rangeRounding + anchor.getLocation().distance(arrow.getLocation()) / rangeScale);
                drawRange(g, dist);
            } else {
                b = map.findBoard(anchor);
                MapGrid grid = null;
                if (b != null) {
                    grid = b.getGrid();
                }
                if (grid != null && grid instanceof ZonedGrid) {
                    Point bp = new Point(anchor);
                    bp.translate(-b.bounds().x, -b.bounds().y);
                    Zone z = ((ZonedGrid) b.getGrid()).findZone(bp);
                    if (z != null) {
                        grid = z.getGrid();
                    }
                }
                if (grid != null) {
                    drawRange(g, grid.range(anchor, arrow));
                }
            }
        }
        lastAnchor = mapAnchor;
        lastArrow = mapArrow;
    }

    public boolean drawAboveCounters() {
        return true;
    }

    protected void launch() {
        if (!visible) {
            map.pushMouseListener(this);
            if (hideCounters) {
                map.setPieceOpacity(hideOpacity / 100.0f);
                map.repaint();
            }
            visible = true;
            anchor.move(0, 0);
            arrow.move(0, 0);
            retainAfterRelease = false;
            initializing = true;
        } else if (persisting) {
            setPersisting(false);
        }
    }

    /**
     * Commands controlling persistence are passed between players, so LOS Threads
     * must have a unique ID.
     */
    public void setId(String id) {
        threadId = id;
    }

    public String getId() {
        return threadId;
    }

    /** Since we register ourselves as a MouseListener using {@link
     * Map#pushMouseListener}, these mouse events are received in map
     * coordinates */
    public void mouseEntered(MouseEvent e) {
    }

    public void mouseExited(MouseEvent e) {
    }

    public void mouseClicked(MouseEvent e) {
    }

    public void mousePressed(MouseEvent e) {
        initializing = false;
        if (visible && !persisting && !mirroring) {
            Point p = e.getPoint();
            if (Boolean.TRUE.equals(GameModule.getGameModule().getPrefs().getValue(SNAP_LOS)) || snapStart) {
                p = map.snapTo(p);
            }
            anchor = p;
            anchorLocation = map.localizedLocationName(anchor);
            lastLocation = anchorLocation;
            lastRange = "";
            checkList.clear();
            ctrlWhenClick = e.isControlDown();
        }
    }

    public void mouseReleased(MouseEvent e) {
        if (!persisting && !mirroring) {
            if (retainAfterRelease && !(ctrlWhenClick && persistence.equals(CTRL_CLICK))) {
                retainAfterRelease = false;
                if (global.equals(ALWAYS)) {
                    Command com = new LOSCommand(this, getAnchor(), getArrow(), false, true);
                    GameModule.getGameModule().sendAndLog(com);
                }
            } else if (e.getWhen() != lastRelease) {
                visible = false;
                if (global.equals(ALWAYS) || global.equals(WHEN_PERSISTENT)) {
                    if (persistence.equals(ALWAYS) || (ctrlWhenClick && persistence.equals(CTRL_CLICK))) {
                        anchor = lastAnchor;
                        Command com = new LOSCommand(this, getAnchor(), getArrow(), true, false);
                        GameModule.getGameModule().sendAndLog(com);
                        setPersisting(true);
                    } else {
                        Command com = new LOSCommand(this, getAnchor(), getArrow(), false, false);
                        GameModule.getGameModule().sendAndLog(com);
                    }
                }
                map.setPieceOpacity(1.0f);
                map.popMouseListener();
                map.repaint();
            }
            lastRelease = e.getWhen();

            if (getLosCheckCount() > 0) {
                reportFormat.setProperty(FROM_LOCATION, anchorLocation);
                reportFormat.setProperty(TO_LOCATION, lastLocation);
                reportFormat.setProperty(RANGE, lastRange);
                reportFormat.setProperty(CHECK_COUNT, String.valueOf(getLosCheckCount()));
                reportFormat.setProperty(CHECK_LIST, getLosCheckList());

                GameModule.getGameModule().getChatter().send(reportFormat.getLocalizedText());
            }
        }
        ctrlWhenClick = false;
    }

    protected void setPersisting(boolean b) {
        persisting = b;
        visible = b;
        setMirroring(false);
        if (persisting) {
            launch.setAttribute(ICON_NAME, persistentIconName);
        } else {
            launch.setAttribute(ICON_NAME, iconName);
            map.repaint();
        }
    }

    protected boolean isPersisting() {
        return persisting;
    }

    protected void setMirroring(boolean b) {
        mirroring = b;
        if (mirroring) {
            visible = true;
        }
    }

    protected boolean isMirroring() {
        return mirroring;
    }

    protected Point getAnchor() {
        return new Point(anchor);
    }

    protected void setEndPoints(Point newAnchor, Point newArrow) {
        anchor.x = newAnchor.x;
        anchor.y = newAnchor.y;
        arrow.x = newArrow.x;
        arrow.y = newArrow.y;
        map.repaint();
    }

    protected Point getArrow() {
        return new Point(arrow);
    }

    protected int getLosCheckCount() {
        return checkList.size();
    }

    protected String getLosCheckList() {
        return StringUtils.join(checkList, ", ");
    }

    /** Since we register ourselves as a MouseMotionListener directly,
     * these mouse events are received in component
     * coordinates */
    public void mouseMoved(MouseEvent e) {
    }

    public void mouseDragged(MouseEvent e) {
        if (visible && !persisting && !mirroring) {
            retainAfterRelease = true;

            Point p = e.getPoint();

            map.scrollAtEdge(p, 15);

            if (Boolean.TRUE.equals(GameModule.getGameModule().getPrefs().getValue(SNAP_LOS)) || snapEnd) {
                p = map.componentCoordinates(map.snapTo(map.mapCoordinates(p)));
            }
            arrow = map.mapCoordinates(p);

            String location = map.localizedLocationName(arrow);
            if (!checkList.contains(location) && !location.equals(anchorLocation)) {
                checkList.add(location);
                lastLocation = location;
            }

            Point mapAnchor = map.mapCoordinates(lastAnchor);
            Point mapArrow = map.mapCoordinates(lastArrow);
            int fudge = (int) (1.0 / map.getZoom() * 2);
            Rectangle r = new Rectangle(Math.min(mapAnchor.x, mapArrow.x) - fudge,
                    Math.min(mapAnchor.y, mapArrow.y) - fudge, Math.abs(mapAnchor.x - mapArrow.x) + 1 + fudge * 2,
                    Math.abs(mapAnchor.y - mapArrow.y) + 1 + fudge * 2);
            map.repaint(r);

            if (drawRange) {
                r = new Rectangle(lastRangeRect);
                r.width += (int) (r.width / map.getZoom()) + 1;
                r.height += (int) (r.height / map.getZoom()) + 1;
                map.repaint(r);
            }
        }
    }

    /**
     * Writes text showing the range
     *
     * @param range the range to display, in whatever units returned
     * by the {@link MapGrid} containing the thread */
    public void drawRange(Graphics g, int range) {
        Point mapArrow = map.componentCoordinates(arrow);
        Point mapAnchor = map.componentCoordinates(anchor);
        g.setColor(Color.black);
        g.setFont(RANGE_FONT);
        final FontMetrics fm = g.getFontMetrics();
        final StringBuilder buffer = new StringBuilder();
        int dummy = range;
        while (dummy >= 1) {
            dummy = dummy / 10;
            buffer.append("8");
        }
        if (buffer.length() == 0) {
            buffer.append("8");
        }
        String rangeMess = Resources.getString("LOS_Thread.range");
        int wid = fm.stringWidth(" " + rangeMess + "  " + buffer.toString());
        int hgt = fm.getAscent() + 2;
        int w = mapArrow.x - mapAnchor.x;
        int h = mapArrow.y - mapAnchor.y;
        int x0 = mapArrow.x + (int) ((wid / 2 + 20) * w / Math.sqrt(w * w + h * h));
        int y0 = mapArrow.y + (int) ((hgt / 2 + 20) * h / Math.sqrt(w * w + h * h));
        g.fillRect(x0 - wid / 2, y0 + hgt / 2 - fm.getAscent(), wid, hgt);
        g.setColor(Color.white);
        g.drawString(rangeMess + " " + range, x0 - wid / 2 + fm.stringWidth(" "), y0 + hgt / 2);
        lastRangeRect = new Rectangle(x0 - wid / 2, y0 + hgt / 2 - fm.getAscent(), wid + 1, hgt + 1);
        Point np = map.mapCoordinates(new Point(lastRangeRect.x, lastRangeRect.y));
        lastRangeRect.x = np.x;
        lastRangeRect.y = np.y;
        lastRange = String.valueOf(range);
    }

    public static String getConfigureTypeName() {
        return Resources.getString("Editor.LosThread.component_type"); //$NON-NLS-1$
    }

    public VASSAL.build.module.documentation.HelpFile getHelpFile() {
        return HelpFile.getReferenceManualPage("Map.htm", "LOS");
    }

    public String[] getAttributeDescriptions() {
        return new String[] { Resources.getString(Resources.NAME_LABEL), Resources.getString(Resources.BUTTON_TEXT),
                Resources.getString(Resources.TOOLTIP_TEXT), Resources.getString(Resources.BUTTON_ICON),
                Resources.getString(Resources.HOTKEY_LABEL), Resources.getString("Editor.report_format"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.persistence"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.icon_persist"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.visible"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.start_grid"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.end_grid"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.draw_range"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.pixel_range"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.round_fractions"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.hidden"), //$NON-NLS-1$
                Resources.getString("Editor.LosThread.opacity"), //$NON-NLS-1$
                Resources.getString(Resources.COLOR_LABEL), };
    }

    public Class<?>[] getAttributeTypes() {
        return new Class<?>[] { String.class, String.class, String.class, IconConfig.class, NamedKeyStroke.class,
                ReportFormatConfig.class, PersistenceOptions.class, IconConfig.class, GlobalOptions.class,
                Boolean.class, Boolean.class, Boolean.class, Integer.class, RoundingOptions.class, Boolean.class,
                Integer.class, Color.class };
    }

    public static class IconConfig implements ConfigurerFactory {
        public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
            return new IconConfigurer(key, name, DEFAULT_ICON);
        }
    }

    public static class ReportFormatConfig implements TranslatableConfigurerFactory {
        public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
            return new PlayerIdFormattedStringConfigurer(key, name,
                    new String[] { FROM_LOCATION, TO_LOCATION, RANGE, CHECK_COUNT, CHECK_LIST });
        }
    }

    public VisibilityCondition getAttributeVisibility(String name) {
        VisibilityCondition cond = null;
        if (RANGE_SCALE.equals(name) || RANGE_ROUNDING.equals(name)) {
            cond = new VisibilityCondition() {
                public boolean shouldBeVisible() {
                    return drawRange;
                }
            };
        } else if (HIDE_OPACITY.equals(name)) {
            cond = new VisibilityCondition() {
                public boolean shouldBeVisible() {
                    return hideCounters;
                }
            };
        } else if (PERSISTENT_ICON_NAME.equals(name)) {
            cond = new VisibilityCondition() {
                public boolean shouldBeVisible() {
                    return persistence.equals(CTRL_CLICK) || persistence.equals(ALWAYS);
                }
            };
        }

        return cond;
    }

    public static class RoundingOptions extends StringEnum {
        public String[] getValidValues(AutoConfigurable target) {
            return new String[] { ROUND_UP, ROUND_DOWN, ROUND_OFF };
        }
    }

    public static class PersistenceOptions extends StringEnum {
        public String[] getValidValues(AutoConfigurable target) {
            return new String[] { CTRL_CLICK, NEVER, ALWAYS };
        }
    }

    public static class GlobalOptions extends StringEnum {
        public String[] getValidValues(AutoConfigurable target) {
            return new String[] { WHEN_PERSISTENT, NEVER, ALWAYS };
        }
    }

    public Configurable[] getConfigureComponents() {
        return new Configurable[0];
    }

    public Class<?>[] getAllowableConfigureComponents() {
        return new Class[0];
    }

    public Command decode(String command) {
        SequenceEncoder.Decoder sd = null;
        if (command.startsWith(LOS_THREAD_COMMAND + getId())) {
            sd = new SequenceEncoder.Decoder(command, '\t');
            sd.nextToken();
            sd.nextToken();
            Point anchor = new Point(sd.nextInt(0), sd.nextInt(0));
            Point arrow = new Point(sd.nextInt(0), sd.nextInt(0));
            boolean persisting = sd.nextBoolean(false);
            boolean mirroring = sd.nextBoolean(false);
            return new LOSCommand(this, anchor, arrow, persisting, mirroring);
        }
        return null;
    }

    public String encode(Command c) {
        if (c instanceof LOSCommand) {
            LOSCommand com = (LOSCommand) c;
            SequenceEncoder se = new SequenceEncoder(com.target.getId(), '\t');
            se.append(com.newAnchor.x).append(com.newAnchor.y).append(com.newArrow.x).append(com.newArrow.y)
                    .append(com.newPersisting).append(com.newMirroring);
            return LOS_THREAD_COMMAND + se.getValue();
        } else {
            return null;
        }
    }

    public static class LOSCommand extends Command {
        protected LOS_Thread target;
        protected String oldState;
        protected Point newAnchor, oldAnchor;
        protected Point newArrow, oldArrow;
        protected boolean newPersisting, oldPersisting;
        protected boolean newMirroring, oldMirroring;

        public LOSCommand(LOS_Thread oTarget, Point anchor, Point arrow, boolean persisting, boolean mirroring) {
            target = oTarget;
            oldAnchor = target.getAnchor();
            oldArrow = target.getArrow();
            oldPersisting = target.isPersisting();
            oldMirroring = target.isMirroring();
            newAnchor = anchor;
            newArrow = arrow;
            newPersisting = persisting;
            newMirroring = mirroring;
        }

        protected void executeCommand() {
            target.setEndPoints(newAnchor, newArrow);
            target.setPersisting(newPersisting);
            target.setMirroring(newMirroring);
        }

        protected Command myUndoCommand() {
            return new LOSCommand(target, oldAnchor, oldArrow, oldPersisting, oldMirroring);
        }
    }
}