tufts.vue.LWComponent.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.LWComponent.java

Source

/*
* Copyright 2003-2010 Tufts University  Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 * http://www.osedu.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

package tufts.vue;

import tufts.Util;
import static tufts.Util.*;
import tufts.vue.ds.Schema;
import tufts.vue.ds.Field;

import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.Color;
import java.awt.Font;
import java.awt.Stroke;
import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.AlphaComposite;
import java.awt.font.TextAttribute;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.awt.Transparency;

import java.util.*;
import java.util.regex.*;
import java.net.*;

import javax.swing.text.StyleConstants;
//import tufts.vue.beans.UserMapType; // remove: old SB stuff we never used
import tufts.vue.filter.*;

import edu.tufts.vue.metadata.MetadataList;
import edu.tufts.vue.metadata.VueMetadataElement;

/**
 * VUE base class for all components to be rendered and edited in the MapViewer.
 *
 * This class is way too big.  A bunch of inner classes could be separated out (e.g., Key, maybe
 * Property, property event change raising (notify), etc).  Beyond that, it could use a major separation of
 * concerns (e.g., rendering, persistance, "dataSet" meta-data?).
 *
 * @version $Revision: 1.531 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $
 * @author Scott Fraize
 */

// todo: on init, we need to force the constraint of size being set before
// label (applies to XML restore & duplicate) to support backward compat before
// line-wrapped text.  Otherwise, in LWNode's, setting label before size set will cause
// the size to be set.

public class LWComponent implements VueConstants, XMLUnmarshalListener {
    protected static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWComponent.class);

    public enum ChildKind {

        /** Include any and all children in the traversable LW hierarchy, such as slides and their
         * children (pathway contents), and actual layer objects -- the only way to make sure you hit
         * every active LWComponent in the runtime related to a particular LWMap (not including the
         * Undo queue).  This will return every LWComponent in the model that has an ID (getID).
         * These are all components that are persisted in some way.
         */
        ANY,

        /** include only default, conceptually significant chilren, leaving out items such a slides, layers and pathways */
        PROPER, // might better be termed what is a "user" or "user-content" object

        /** VISIBLE is PROPER, excluding those that are not currently visible */
        VISIBLE,

        /** EDITABLE is VISIBLE, excluding those that are currently locked */
        EDITABLE

        // VIRTUAL -- would be *just* what ANY currently adds, and exclude PROPER -- currently unsupported
    }

    /** order of result set for getAllDescendents -- not applicable if collection passed in isn't ordered */
    public enum Order {
        /** default traversal order: parents before children */
        TREE,
        /** order for layout operations; children before parents */
        DEPTH
    };

    /*
    // need an IntegerPreference and/or an IntegerRangePreference (that ImagePreference could also use)
    private static final VuePreference SlideIconPref =
    IntegerPreference.create(edu.tufts.vue.preferences.PreferenceConstants.MAPDISPLAY_CATEGORY,
     "slideIconSize",
     "Slide Icon Size",
     "Size of Slide icons displayed on the map",
     true);
        
    */

    private static final Object CAUSE_DEFAULT = "cause_default";
    private static final Object CAUSE_PATHWAY = "cause_pathway";
    //private static final Object CAUSE_PERSIST = "cause_persist";

    public enum HideCause {
        /** each subclass of LWComponent can use this for it's own purposes */
        DEFAULT
        /** we've been hidden by link pruning */
        , PRUNE // (CAUSE_PERSIST),
        /** another layer is set to be the exclusive layer */
        , LAYER_EXCLUDED
        /** we're a member of a pathway that hides when the pathway hides, and all pathways we're on are hidden */
        , HIDES_WITH_PATHWAY(CAUSE_PATHWAY)
        /** we've been hidden by a pathway that is in the process of revealing */
        , PATH_UNREVEALED(CAUSE_PATHWAY)
        /** we've been hidden because the current pathway is all we we want to see, and we're not on it */
        , NOT_ON_CURRENT_PATH(CAUSE_PATHWAY)
        /** we've been hidden due to the collapse of a parent (different from Flag.COLLAPSED, which is for the collapsed parent) */
        , COLLAPSED
        /** we're an LWImage that's a node-icon image, and we're hidden */
        ,IMAGE_ICON_OFF
        // /** a search result has temporarily hidden us */
        // ,SEARCH
        ;
        final int bit = 1 << ordinal();
        final Object type;

        HideCause(Object typeKey) {
            type = typeKey;
        }

        HideCause() {
            type = CAUSE_DEFAULT;
        }
    }

    private static Object FLAG_DEFAULT = "flag_default";
    private static Object FLAG_UNDOABLE = "flag_undoable";

    /** runtime flags explicitly set and cleared by VUE code -- not managed by UNDO */
    public enum Flag {
        /** been deleted (is in undo queue) */
        DELETED,
        /** we've been hidden due to filtering -- note that this isn't a hide-bit in that children may still be visible even when
         * this is set*/
        FILTERED,
        /** is in the process of being deleted */
        DELETING,
        /** is in the process of being un-deleted (undo) */
        UNDELETING,
        /** is selected */
        SELECTED,
        /** is a component serving as a style source */
        STYLE,
        /** is a component serving as a data-style source */
        DATA_STYLE,
        /** cannot move, delete, link to or edit label */
        LOCKED,
        /** can't be moved */
        FIXED_LOCATION,
        /** has been specially styled for for appearance on a slide */
        SLIDE_STYLE,
        /** was created to serve some internal purpose: not intended to exist on a regular user map */
        INTERNAL,
        /** this component should NOT broadast change events */
        EVENT_SILENT,
        /** this component is in a "collapsed" or closed view state */
        COLLAPSED

        /** for links: this is data-relation link */
        , DATA_LINK
        /** for links: this is data-relation link and ALSO a data-count link */
        , DATA_COUNT

        /** currently used for marking LWImage's as being node-icons */
        , ICON

        /** for subclasses that want to distinguish between a default size and a validated size (e.g., LWImage)
         * "default size" could actually mean any suggested or invalid size before a final definite size */
        , UNSIZED

        /** lets us know this is in the process of duplicating */
        , DUPLICATING

        /** this is part of a pruned map sub-graph */
        , PRUNED

        /** this is link with head pruned */
        , PRUNE_HEAD
        /** this is link with tail pruned */
        , PRUNE_TAIL

        ;

        // do we want a generalized LOCKED which means fixed,no-delete,no-duplicate?,no-reorder(forward/back),no-link?

        public final int bit = 1 << ordinal();

        final Object type;

        Flag(Object typeKey) {
            type = typeKey;
        }

        Flag() {
            this(FLAG_DEFAULT);
        }

    }

    /** runtime persistant flags, managed by UNDO */
    public enum State {
        /** a map/layer has been auto-clustered by a data-drop */
        HAS_AUTO_CLUSTERED

        /** a map/layer has been auto-clustered by a data-drop */
        ,PRUNED

        ;

        final int bit = 1 << ordinal();
        final boolean persist;

        State(boolean p) {
            persist = p;
        }

        State() {
            persist = false;
        }
    }

    /** context codes for LWContainer.addChildren */

    public static final Object ADD_DROP = "drop";
    public static final Object ADD_PASTE = "paste";
    public static final Object ADD_DEFAULT = "default";
    public static final Object ADD_PRESORTED = "sorted";
    public static final Object ADD_MERGE = "merge";
    public static final Object ADD_CHILD_TO_SIBLING = "child-to-sibling";

    //Static { for (Hide reason : Hide.values()) { System.out.println(reason + " bit=" + reason.bit); } }

    public static final java.awt.datatransfer.DataFlavor DataFlavor = tufts.vue.gui.GUI
            .makeDataFlavor(LWComponent.class);

    public static final int MIN_SIZE = 10;
    public static final Size MinSize = new Size(MIN_SIZE, MIN_SIZE);
    public static final float NEEDS_DEFAULT = Float.MIN_VALUE;
    public static final java.util.List<LWComponent> NO_CHILDREN = Collections.EMPTY_LIST;

    public static final boolean COLLAPSE_IS_GLOBAL = true;

    protected static boolean isGlobalCollapsed = false;

    static void toggleGlobalCollapsed() {
        if (!COLLAPSE_IS_GLOBAL)
            throw new Error("disabled");
        isGlobalCollapsed = !isGlobalCollapsed;
    }

    public interface Listener extends java.util.EventListener {
        public void LWCChanged(LWCEvent e);
    }

    /*
     * Meta-data persistant information
     */
    protected String label = null; // protected for debugging purposes
    private String notes = null;
    private Resource resource = null;
    private String mLabelFormat; // if there's a data-format, it's stored here

    /*
     * Persistent information
     */

    private String ID = null;

    // todo: next major re-architecting: instead of x/y width/height,
    // keep a Point2D.Float bounds up to date (and can skip creating
    // a rectangles constantly).  (Might also keep a mapBounds?)

    protected float x;
    protected float y;
    // TODO: if we want to support some kind of keep-relative alignment for an object
    // (in it's parent), we couldn't just use a special object on a generic x/y value
    // ptr -- we still need ACTUAL x/y values to render, but we could have an
    // xAnchor/yAnchor, which could even be a list of actions to perform every time the
    // object is laid out, or it's parent resizes.

    //private MetadataList metadataList = new MetadataList();
    private MetadataList metadataList = null;
    private static final NodeFilter NEEDS_NODE_FILTER = new NodeFilter();
    private NodeFilter nodeFilter = NEEDS_NODE_FILTER;
    private URI uri;
    protected float width = NEEDS_DEFAULT;
    protected float height = NEEDS_DEFAULT;

    /** creation time-stamp (when this node first joined a map) */
    private long mCreated;

    /** cached affine transform for use by getZeroTransform() */
    private transient final AffineTransform _zeroTransform = new AffineTransform();
    protected transient double scale = 1.0;
    private transient AffineTransform mTemporaryTransform;

    protected transient TextBox labelBox = null;
    protected transient BasicStroke stroke = STROKE_ZERO;
    //protected transient boolean selected = false;

    protected int mHideBits = 0x0; // any bit set means we're hidden (not managed by undo)
    //protected int mFilterBits = 0x0; // may need this to get pathway filtering not in conflict with search filtering
    protected volatile int mFlags = 0x0; // explicitly set/cleared: not managed by undo
    protected int mState = 0x0; // managed by undo (and individual bits may optionally be persisted)

    protected transient LWContainer parent;
    protected transient LWComponent mParentStyle;
    protected transient LWComponent mSyncSource; // "semantic source" for nodes on slide to refer back to the concept map
    protected transient Collection<LWComponent> mSyncClients; // set of sync sources that point back to us

    /** list of links that contain us as an endpoint */
    private transient List<LWLink> mLinks;

    /** list of pathways that we are a member of */
    private transient List<LWPathway> mPathways;
    /** list of all pathway entries that refer to us (one for each time we appear on an individual pathway) */
    protected transient List<LWPathway.Entry> mEntries;

    /** properties for use by model clients (e.g., UI components) */
    protected transient HashMap mClientData;
    // need MetaMap (a multi-map) for XML data-sets that can have more than one value per key
    protected transient MetaMap mDataMap;

    // todo memory perf: mEntries should subclass ArrayList and implement this iter
    // so they can be allocated together, instead of leaving this slot here unused
    // for ever node w/out pathway entries.
    private SlideIconIter mVisibleSlideIconIterator;

    private transient long mSupportedPropertyKeys;

    // TODO PERFORMANCE: change support could be handled generically, and we could at least lazy-create
    protected transient final LWChangeSupport mChangeSupport = new LWChangeSupport(this);

    protected transient boolean mXMLRestoreUnderway = false; // are we in the middle of a restore?

    protected transient BufferedImage mImageBuffer;

    public static final Comparator XSorter = new Comparator<LWComponent>() {
        public int compare(LWComponent c1, LWComponent c2) {
            // we multiply up the result so as not to loose differential precision in the integer result
            return (int) (128f * (c1.x - c2.x));
        }
    };
    public static final Comparator YSorter = new Comparator<LWComponent>() {
        public int compare(LWComponent c1, LWComponent c2) {
            return (int) (128f * (c1.y - c2.y));
        }
    };

    public static final Comparator GridSorter = new Comparator<LWComponent>() {
        public int compare(LWComponent c1, LWComponent c2) {
            if (c1.y == c2.y)
                return XSorter.compare(c1, c2);
            else
                return YSorter.compare(c1, c2);

        }
    };

    /** constructor */
    public LWComponent() {
        if (DEBUG.PARENTING)
            Log.debug("construct of " + Util.tag(this));
        mSupportedPropertyKeys = Key.PropertyMaskForClass(getClass());
        if (mSupportedPropertyKeys == 0) {
            // this can happen during init before circular dependencies are resolved
            if (DEBUG.INIT || DEBUG.STYLE)
                Util.printStackTrace("ZERO PROPERTY BITS IN " + Util.tag(this));
        } else {
            // not on by default:
            disableProperty(KEY_Alignment);
        }

    }

    //     /** for internal proxy instances only */
    //     private LWComponent(String label) {
    //         setLabel(label);
    //     }

    public long getSupportedPropertyBits() {
        return mSupportedPropertyKeys;
    }

    /** Convenience: If key not a real Key (a String), always return true */
    public boolean supportsProperty(Object key) {
        if (key instanceof Key)
            return supportsProperty((Key) key);
        else
            return false;
    }

    /** @return true if the given property is currently supported on this component */
    public boolean supportsProperty(Key key) {
        return (mSupportedPropertyKeys & key.bit) != 0;
    }

    public void disableProperty(Key key) {
        disablePropertyBits(key.bit);
    }

    public void enableProperty(Key key) {
        enablePropertyBits(key.bit);
    }

    protected void disablePropertyBits(long bits) {
        mSupportedPropertyKeys &= ~bits;
    }

    protected void enablePropertyBits(long bits) {
        mSupportedPropertyKeys |= bits;
    }

    protected void disablePropertyTypes(KeyType type) {
        for (Key key : Key.AllKeys)
            if (key.type == type || (type == KeyType.STYLE && key.type == KeyType.SUB_STYLE))
                disableProperty(key);
    }

    /** Apply all style properties from styleSource to this component */
    public void copyStyle(LWComponent styleSource) {
        copyStyle(styleSource, ~0L);
    }

    public void copyStyle(LWComponent styleSource, long permittedPropertyBits) {
        if (DEBUG.STYLE || styleSource == null) {
            System.out.println("COPY STYLE of " + Util.tags(styleSource) + " ==>> " + Util.tags(this)
                    + " permitBits=" + Long.bitCount(permittedPropertyBits));
        }
        if (styleSource == null)
            return;
        for (Key key : Key.AllKeys)
            //if (key.isStyleProperty && styleSource.supportsProperty(key) && (permittedPropertyBits & key.bit) != 0)
            if (styleSource.isStyling(key) && (permittedPropertyBits & key.bit) != 0)
                key.copyValue(styleSource, this);
    }

    public void copyProperties(LWComponent source, long propertyBits) {
        if (DEBUG.STYLE)
            System.out
                    .println("COPY PROPS of " + source + " ==>> " + this + " bits=" + Long.bitCount(propertyBits));
        for (Key key : Key.AllKeys)
            if ((propertyBits & key.bit) != 0 && source.supportsProperty(key))
                key.copyValue(source, this);
    }

    public void applyCSS(edu.tufts.vue.style.Style cssStyle) {
        System.out.println("Applying CSS style " + cssStyle.getName() + ":");
        for (Map.Entry<String, String> se : cssStyle.getAttributes().entrySet()) {

            final String cssName = se.getKey().trim().toLowerCase(); // todo: shouldn't have to trim this
            final String cssValue = se.getValue().trim();
            boolean applied = false;

            System.err.format("%-35s CSS key %-17s value %-15s", toString(), '\'' + cssName + '\'',
                    '\"' + cssValue + '\"');

            for (Key key : Key.AllKeys) {
                if (key.cssName == null)
                    continue;
                //out("Checking key [" + cssName + "] against [" + key.cssName + "]");

                if (key.cssName.indexOf(";") > 0) {
                    String[] names = key.cssName.split(";");
                    for (int i = 0; i < names.length; i++) {
                        if (supportsProperty(key) && names[i].equals(cssName)) {
                            applied = key.setValueFromCSS(this, names[i], cssValue);
                        }
                    }
                } else if (supportsProperty(key) && cssName.equals(key.cssName)) {
                    //out("Matched supported property key " + key.cssName);

                    applied = key.setValueFromCSS(this, cssName, cssValue);

                    /*
                    final Property slot = key.getSlot(this);
                    if (slot == Key.NO_SLOT_PROVIDED) {
                    out("Can't apply CSS Style property to non-slotted key: " + cssName + " -> " + key);
                    } else {
                    try {
                        slot.setFromCSS(cssName, cssValue);
                        System.err.println("applied value: " + slot);
                        applied = true;
                        break;
                    } catch (Throwable t) {
                        System.err.println();
                        tufts.Util.printStackTrace(new Throwable(t), "failed to apply CSS key/value " + cssName + "=" + cssValue);
                    }
                    }
                    */
                }
            }
            setFont(cssStyle.getFont());
            if (!applied)
                System.err.println("UNHANDLED");

        }
    }

    /**
     * Describes a property on a VUE LWComponent, and provides an info string for creating Undo names,
     * and for diagnostic output.  Implies the ability to set/get the value on an LWComponent by some means.
     */
    // todo: consdier moving all the Key/Property code to some kind of superclass to LWComponent -- LWStyle? Vnode? LWKey? LWState?
    // We'd move it elsewhere, but we'd have to export all sorts of stuff to make all thats needed available,
    // as they get everything currently being inner classes.

    // The generic type TSubclass allows the inner-class impl's of getValue & setValue, in subclasses
    // of LWComponent, to use their own type in the first argument to set/getValue, omitting
    // the need for casts in the method.

    public enum KeyType {
        Default, STYLE, SUB_STYLE, DATA
    };

    // todo: TValue may be overkill -- may want to revert to using just Object
    public static class Key<TSubclass extends LWComponent, TValue> {
        /** A name for this key (used for undo labels & debugging) */
        public final String name;
        /** A name for a CSS property that can be used to initialize the value for this key */
        public final String cssName;
        /** The unique bit for this property key.
        (Implies a max of 64 keys that can be known as active to our tools -- use a BitSet if need more) */
        public final long bit;
        //         /** True if this key for a style property -- a property that moves from style holders to LWCopmonents
        //          * pointing to it via mParentStyle */
        //         public final boolean isStyleProperty;

        public final KeyType type;

        public final boolean isColor;

        /* True this property is a sub-part of some other property */
        //public final boolean isSubProperty;

        public static final java.util.List<Key> AllKeys = new java.util.ArrayList<Key>();

        private static int InstanceCount; // increment for each key instance, to establish the appropriate bit
        private static final java.util.Map<Class, Long> ClassProperties = new java.util.HashMap<Class, Long>();

        /** Get the supported property bit mask for the given class in the LWComponent inheritance tree
         * This will only return accurate results after all Key's in the codebase have been initialized. */
        static long PropertyMaskForClass(Class<? extends LWComponent> clazz) {
            final Long bitsForClass = ClassProperties.get(clazz); // property bits for this class
            if (bitsForClass == null) {
                // If we found nothing, this must be the first instance of a new object
                // for some subclass of LWComponent that doesn't declare any of it's
                // own keys.  Merge the bits for all superclasses and put it in the
                // map for future reference.
                long propMaskForClass = 0L;
                for (Class c = clazz; c != null; c = c.getSuperclass())
                    propMaskForClass |= PartialPropertyMaskForClass(c);

                if (DEBUG.INIT)
                    Log.debug(String.format("CACHED PROPERTY BITS for %s: %d", clazz,
                            Long.bitCount(propMaskForClass)));
                ClassProperties.put(clazz, propMaskForClass);

                return propMaskForClass;
            } else
                return bitsForClass;
        }

        /** @return the currently stored property mask for the given class: only used during initialization
         * Will return 0L (no bit set) if the given class is not in the map (e.g., java.lang.Object)
         * This is used to disambiguate between properties that apply only to a particular
         * LWComponent subclass while we produce the ultimate merged results for all classes in
         * the hierarchy.
         */
        private static long PartialPropertyMaskForClass(Class clazz) {
            final Long bitsForClass = ClassProperties.get(clazz); // property bits for this class
            if (bitsForClass == null)
                return 0L;
            else
                return bitsForClass;
        }

        public boolean isStyleProperty() {
            return type == KeyType.STYLE;
        }

        public Key(String name) {
            this(name, KeyType.Default);
        }

        public Key(String name, KeyType keyType) {
            this(name, null, keyType);
        }

        public Key(String name, String cssName) {
            this(name, cssName, KeyType.STYLE);
        }

        protected Key(String name, String cssName, KeyType keyType) {
            this.name = name;
            this.cssName = cssName;
            this.type = keyType;
            this.isColor = name.endsWith("color"); // todo: hack -- make more explicit
            if (InstanceCount >= Long.SIZE) {
                this.bit = 0;
                tufts.Util.printStackTrace(
                        Key.class + ": " + InstanceCount + "th key created -- need to re-implement (try BitSet)");
            } else
                this.bit = 1 << InstanceCount;
            AllKeys.add(this);

            // Note: this only works if the key is in fact declared in the enclosing class to
            // which it applies.  If we want to declare keys elsewhere, we'll need to add
            // a Class argument to the constructor.
            final Class clazz = getClass().getEnclosingClass(); // the class that own's the Key
            long propMaskForClass = (PartialPropertyMaskForClass(clazz) | bit); // add the new bit

            // Now be sure to mix in all properties found in all super-classes:
            for (Class c = clazz; c != null; c = c.getSuperclass())
                propMaskForClass |= PartialPropertyMaskForClass(c);

            ClassProperties.put(clazz, propMaskForClass);

            if (DEBUG.INIT || DEBUG.STYLE)
                Log.debug(String.format("KEY %-20s %-11s %-22s bit#%2d; %25s now has %2d properties", name,
                        //isStyleProperty ? "STYLE;" : "",
                        keyType, cssName == null ? "" : cssName, InstanceCount, clazz.getName(),
                        Long.bitCount(propMaskForClass)));
            InstanceCount++;

            //System.out.println("BITS FOR " + LWImage.class + " " + PropertyMaskForClass(LWImage.class));

            // Could build list of all key (and thus slot) values here for each subclass,
            // but where would we attach it?  Would need to pass in the class variable
            // in the constructor, and hash it to a list for the class.  Then the
            // problem would be that each list would only contain the subclass items,
            // not the super -- tho could we just iterate up through the supers getting
            // their lists to build the full list for each class?  (e.g., for duplicate,
            // persistance, or runtime diagnostic property editors)

            // OH: we also need to build the bitfield for the enclosing class:
            // the runtime-constant bit-mask representing all the properties
            // handled by this class / subclass of LWComponent

        }

        private static final LWComponent EmptyStyle = new LWComponent();
        static final Property NO_SLOT_PROVIDED = EmptyStyle.mFillColor; // any slot will do
        //private static final Property BAD_SLOT = EmptyStyle.mStrokeColor; // any (different) slot will do

        /** If this isn't overriden to return non-null, getValue & setValue must be overriden to provide the setter/getter impl  */
        Property getSlot(TSubclass c) {
            return NO_SLOT_PROVIDED;
        }

        boolean isSlotted(TSubclass c) {
            return getSlot(c) != NO_SLOT_PROVIDED;
        }

        // If we wanted to get rid of the slot decl's in the key's (for those that use
        // slots), we could, in our defult slot-using set/getValue, search all property
        // objects in the LWComponent, and if any of them match our key, we know that's
        // that slot, and if none of them do, then we have in internal error: coder
        // should have impl'd set/getValue themselves.

        /** non slot-based property keys can override this */
        TValue getValue(TSubclass c) {
            final Property propertySlot = getSlotSafely(c);
            try {
                if (propertySlot == NO_SLOT_PROVIDED) {
                    if (DEBUG.META)
                        Log.error(this
                                + ";\n\tNo property, or: no slot, and getValue not overriden on client subclass:\n\t"
                                + (c == null ? null : c.getClass()) + ";\n\t" + c, new Throwable());
                    else
                        Log.warn(c == null ? null : c.getClass() + "; has no property of type: " + this);
                    return null;
                } else
                    return (TValue) propertySlot.get();
            } catch (Throwable t) {
                if (DEBUG.META)
                    tufts.Util.printStackTrace(new Throwable(t),
                            this + ": property slot get() failed " + propertySlot);
                else
                    Log.warn(this + ": property slot get() failed " + propertySlot + " " + t);
                return DEBUG.Enabled ? (TValue) "<unsupported for this object>" : null;
                //return null;
            }
        }

        // The design of the below two methods, setValue and setValueWithContext, is such that either
        // can be overridden depending on the needs of the Key implementation, but never BOTH.
        // Also, the setValue(TSubclass, TValue) method can never be called directly, except by
        // the single call made to it in setValueWithContext.  This is so that the context
        // argument can be completely ignored in simple Key implementations, and only made
        // use of where needed.

        /** non slot-based property keys can override this -- only override ONE of the two setValue methods -- note that this
         particular method should NOT EVER be called directly -- only overridden */
        // what we really want here is some kind of access modifier that make this protected for overriding, but private to calling
        protected void setValue(TSubclass c, TValue value) {
            setValueBySlot(c, value);
        }

        /** non slot-based property keys can override this -- only override ONE of the two setValue methods
         * Override the 2-argument version unless the context is needed */
        void setValueWithContext(TSubclass c, TValue value, Object context) {
            // if this has NOT been overriden (the usual case), defer to the default non-context setValue, which will defer to slot
            // based value-set if needed.
            setValue(c, value); // this is the only place that the 2-arg setValue should ever be called
        }

        private final void setValueBySlot(TSubclass c, TValue value) {
            final Property slot = getSlotSafely(c);
            if (slot == null || slot == NO_SLOT_PROVIDED)
                return;
            if (value instanceof String) {
                // If a String value comes in, this allows us to auto-parse it
                slot.setFromString((String) value);
            } else {
                slot.set(value);
            }
        }

        private Property getSlotSafely(TSubclass c) {
            Property slot = null;
            try {
                slot = getSlot(c);
            } catch (ClassCastException e) {
                String msg = "Property not supported: " + this + " on\t" + c + " (getSlot failed; returned null)";
                //tufts.Util.printStackTrace(e, msg);
                Log.warn(msg + "; " + e);
                return null;
            } catch (Throwable t) {
                tufts.Util.printStackTrace(new Throwable(t), this + ": bad slot? unimplemented get/setValue?");
                return null;
            }
            //if (slot == NO_SLOT_PROVIDED) tufts.Util.printStackTrace(this + ": no slot provided");
            return slot;
        }

        /** non slot-based property keys can override this */
        String getStringValue(TSubclass c) {
            final Property slot = getSlotSafely(c);
            if (slot == NO_SLOT_PROVIDED || slot == null) {
                // If there is no slot provided, we must get the value from the overridden
                // getter, getValue.
                Object typedValue = null;
                try {
                    // Call the overriden getValue:
                    typedValue = getValue(c);
                } catch (ClassCastException e) {
                    final String msg = "Property not supported(getStringValue): " + this + " on\t" + c;
                    if (DEBUG.META)
                        tufts.Util.printStackTrace(e, msg);
                    else
                        Log.warn(msg + "; " + e);
                    return DEBUG.Enabled ? "<unsupported for this object>" : null;
                }
                return typedValue == null ? null : typedValue.toString(); // produce something
                //              } else if (slot == null) {
                //                 // If a slot was provided, but it failed, no sense in trying
                //                 // the default getValue, which presumably wasn't overriden if
                //                 // a slot was provided.
                //                 //tufts.Util.printStackTrace(this + ": bad slot");
                //                 return DEBUG.Enabled ? "<unsupported for this object>" : null;
            } else
                return slot.asString();
        }

        void setStringValue(TSubclass c, String stringValue) {
            Property slot = getSlotSafely(c);
            if (slot != NO_SLOT_PROVIDED) {
                slot.setFromString(stringValue);
            } else {
                TValue v = getValue(c);
                // handle a few special cases for standard java types, even if there's no slot (Property object) to parse the string
                // This won't work if getValue returns null, as we'll have no class object to check for type information.
                if (v instanceof String)
                    stringSet(c, stringValue);
                else if (v instanceof Integer)
                    stringSet(c, Integer.valueOf(stringValue));
                else if (v instanceof Long)
                    stringSet(c, Long.valueOf(stringValue));
                else if (v instanceof Float)
                    stringSet(c, Float.valueOf(stringValue));
                else if (v instanceof Double)
                    stringSet(c, Double.valueOf(stringValue));
                else if (v instanceof Boolean)
                    stringSet(c, Boolean.valueOf(stringValue));
                else
                    Log.error(this + ":setValue(" + stringValue + "); no slot provided for parsing string value",
                            new Throwable("HERE"));
            }
        }

        private final void stringSet(TSubclass c, Object v) {
            setValueWithContext(c, (TValue) v, PROPERTY_SET_DEFAULT); // may want a seperate context here, e.g., PROPERTY_SET_FROM_STRING
        }

        /** @return true if was successful */
        boolean setValueFromCSS(TSubclass c, String cssKey, String cssValue) {
            final Property slot = getSlot(c);
            if (slot == Key.NO_SLOT_PROVIDED) {
                c.out("Can't auto-apply CSS Style property to non-slotted key: " + cssName + " -> " + this);
                return false;
            }
            try {
                slot.setFromCSS(cssName, cssValue);
                System.err.println("applied value: " + slot);
                return true;
            } catch (Throwable t) {
                System.err.println();
                tufts.Util.printStackTrace(new Throwable(t),
                        "failed to apply CSS key/value " + cssName + "=" + cssValue);
            }
            return false;
        }

        /** @return true if the value for this Key in LWComponent is equivalent to otherValue
         * Override to provide non-standard equivalence.
         * The default provided here uses Object.equals to compare the values.
         */
        boolean valueEquals(TSubclass c, Object otherValue) {
            final TValue value = getValue(c);
            return value == otherValue || (otherValue != null && otherValue.equals(value));
        }

        void copyValue(TSubclass source, TSubclass target) {
            if (!source.supportsProperty(this)) {
                if (DEBUG.STYLE && DEBUG.META)
                    System.err.println(
                            "   COPY-VALUE: " + this + "; source doesn't support this property; " + source);
            } else if (!target.supportsProperty(this)) {
                if (DEBUG.STYLE && DEBUG.META)
                    System.err.println(
                            "   COPY-VALUE: " + this + "; target doesn't support this property; " + target);
            } else {
                final TValue newValue = getValue(source);
                final TValue oldValue = getValue(target);

                if (newValue != oldValue && (newValue == null || !newValue.equals(oldValue))) {
                    if (DEBUG.STYLE)
                        System.out.format("    COPY-VALUE: %s %s%-15s%s %-40s -> %s over (%s)\n", source,
                                TERM_PURPLE, this.name, TERM_CLEAR,
                                //"(" + newValue + ")",
                                Util.tags(getStringValue(source)), target, oldValue);
                    setValueWithContext(target, newValue, PROPERTY_SET_DEFAULT); // may want a specific context here, e.g., PROPERTY_COPY
                }

                //if (DEBUG.STYLE) System.err.print(" COPY-VALUE: " + this + "(");
                //if (DEBUG.STYLE) System.err.println(copyValue + ") -> " + target);
            }
        }

        public String toString() {
            return name;
        } // must == name for now until tool panels handle new key objects (is this true yet?)
        //public String toString() { return type + "{" + name + "}"; }
    }

    /**
     * This class allows us to define an arbitrary property for a LWComponent, and define a default
     * set of setters and getters that automatically handle stuff like undo and positing change
     * notifications.  It also allows us to easily attach meta-data to the property itself: e.g.,
     * it's locked, it's overriding a parent style value, it's caching some related computed value,
     * etc.
     */
    protected abstract class Property<T> {

        final Key key;
        protected T value;

        Property(Key key) {
            this.key = key;
        }

        T get() {
            return value;
        }

        public void setTo(T newValue) {
            set(newValue);
        }

        boolean isChanged(T newValue) {
            if (this.value == newValue || (newValue != null && newValue.equals(this.value)))
                return false;
            else
                return true;
        }

        void set(T newValue) {
            //final Object old = get(); // if "get" actually does anything tho, this is a BAD idea; if needbe, create a "curValue"

            if (!isChanged(newValue))
                return;
            final Object oldValue = this.value;
            take(newValue);
            onChange();

            // RAISE CHANGE EVENT (for observers -- e.g., repaint, UndoManager, editors, etc)
            // maybe: if (alive()) ?
            LWComponent.this.notify(this.key, oldValue);

            // note: if could handle event raising in Key class, we could make this class static.
            // Tho then to bind to a particular property on a particular LWComponent, you'd need a
            // Binding object.  Do we actually bind directly to individual Property instance's
            // anywhere? (Answer: seems no -- was just able to make this class protected instead of
            // public) Could we possibly change class hierarchy such that LWComponent.Key is typed, and
            // subclassed for the various types, and then do away with the Property class hierarchy
            // entirely? -- SMF 2012
        }

        /** This JUST changes the stored value: no notifications of any kind will be triggered, no undo recorded. */
        void take(T o) {
            this.value = o;
            if (DEBUG.TOOL)
                System.out.printf("     TAKING: %-30s -> %s\n", vtag(key, o, this), LWComponent.this);
        }

        /** impl's can override this to do something after the value has changed (after take() has been called),
         * and before listeners have been notified */
        void onChange() {
        }

        void setFromString(String s) {
            try {
                setBy(s);
            } catch (Throwable t) {
                Log.error("bad value for " + this + ": [" + s + "] " + t);
            }
        }

        void setFromCSS(String cssKey, String value) {
            throw new UnsupportedOperationException(this + " unimplemented setFromCSS " + cssKey + " = " + value);
            //VUE.Log.error("unimplemented setFromCSS " + cssKey + " = " + value);
        }

        void setBy(String fromValue) {
            // Could get rid all of the setBy's (and then mayve even all the StyleProp subclasses!!)
            // If we just had mapper class that took a type, a value, and returned a string (e.g., Font.class, Object value)
            Log.error("unimplememnted: " + this + " setBy " + fromValue.getClass() + " " + fromValue);
        }

        /** override to provide an impl other than value.toString() */
        String asString() {
            return value == null ? null : value.toString();
        }

        /*
        void setByUser(Object newValue) { // for tools.  Actually, tools using generic setProperty right now...
        out("SetByUser: " + key + " " + newValue);
        set(newValue);
        }
        */

        /** used for debugging */
        public String toString() {
            return key + "[" + value + "]";
        }

    }

    public class EnumProperty<T extends Enum> extends Property<T> {
        EnumProperty(Key key, T defaultValue) {
            super(key);
            value = defaultValue;
            //System.out.println("enum values: " + Arrays.asList(defaultValue.getClass().getEnumConstants()));
            //System.out.println("enum test: " + Enum.valueOf(defaultValue.getClass(), "DASH1"));
        }

        void setBy(String s) {
            // note: value can never be null, or we'll need to store the Enum class reference elsewhere
            // (e.g., in the Key -- better there anyway, where we could provide a generic "values"
            // to list the supported values)
            set((T) Enum.valueOf(value.getClass(), s.trim()));
        }
    }

    private static final String _DefaultString = "";

    public class StringProperty extends Property<java.lang.String> {
        StringProperty(Key key) {
            super(key);
            value = _DefaultString;
        }

        void setBy(String s) {
            set(s);
        }
    }

    public class BooleanProperty extends Property<java.lang.Boolean> {
        BooleanProperty(Key key, Boolean defaultValue) {
            super(key);
            value = defaultValue;
        }

        BooleanProperty(Key key) {
            this(key, Boolean.FALSE);
        }

        void setBy(String s) {
            set(Boolean.valueOf(s));
        }
    }

    abstract public class NumberProperty<T> extends Property<T> {
        NumberProperty(Key key) {
            super(key);
        }

        void setFromCSS(String cssKey, String value) {
            if (value.endsWith("pt") || value.endsWith("px"))
                setBy(value.substring(0, value.length() - 2));
            else
                throw new IllegalArgumentException("unhandled CSS number conversion for [" + value + "]");

        }

    }

    static class PropertyValueVeto extends RuntimeException {
        PropertyValueVeto(String msg) {
            super(msg);
        }
    }

    private static final Integer _DefaultInteger = new Integer(0);

    public class IntProperty extends NumberProperty<java.lang.Integer> {
        IntProperty(Key key, Integer defaultValue) {
            super(key);
            value = defaultValue;
        }

        IntProperty(Key key) {
            this(key, _DefaultInteger);
        }

        void setBy(String s) {
            set(new Integer(s));
        }
    }

    private static final Float _DefaultFloat = new Float(0f);

    public class FloatProperty extends NumberProperty<java.lang.Float> {
        FloatProperty(Key key) {
            super(key);
            value = _DefaultFloat;
        }

        void setBy(String s) {
            set(new Float(s));
        }
    }

    public class FontProperty extends Property<java.awt.Font> {
        FontProperty(Key key) {
            super(key);
            value = VueConstants.FONT_DEFAULT;
        }

        final void setBy(String s) {
            //check for underline

            String p = s.substring(s.indexOf("-") + 1, s.length());
            p = p.substring(0, p.indexOf("-"));

            if (p.endsWith("underline")) { //do something
                LWComponent.this.mFontUnderline.set("underline");
                s = s.replaceAll(p, p.substring(0, p.indexOf("underline")));
            }
            Font f = Font.decode(s);

            set(f);
        }

        final String asString() {
            //if (this.font == null || this.font == getParent().getFont())
            //return null;

            final Font font = get();
            String strStyle;

            if (font.isBold()) {
                strStyle = font.isItalic() ? "bolditalic" : "bold";
            } else {
                strStyle = font.isItalic() ? "italic" : "plain";
            }

            if (LWComponent.this.mFontUnderline.get().equals("underline"))
                strStyle = strStyle.concat("underline");
            return font.getName() + "-" + strStyle + "-" + font.getSize();
        }
    }

    /**
     * Handles CSS font-style value "italic" ("normal", or anything else, has no effect as of yet)
     * Also handles CSS font-weight value of "bold" (anything else is ignored for now)
     * todo: no hook for font-weight yet, permits invalid CSS
     */
    public class CSSFontStyleProperty extends IntProperty {
        CSSFontStyleProperty(Key key) {
            super(key);
        }

        void setFromCSS(String cssKey, String value) {
            // todo: this ignoring the key, which will permit non-confomant CSS
            if ("italic".equalsIgnoreCase(value))
                set(java.awt.Font.ITALIC);
            else if ("bold".equalsIgnoreCase(value))
                set(java.awt.Font.BOLD);
            else
                set(0);
        }
    }

    /*
    public class CSSFontSizeProperty extends IntProperty {
    CSSFontSizeProperty(Key key) { super(key); }
    void setFromCSS(String cssKey, String value) {
        if (value.endsWith("pt"))
            setBy(value.substring(0, value.length()-2));
        else
            throw new IllegalArgumentException("unhandled CSS font size [" + value + "]");
        
    }
    }
    */

    public class CSSFontFamilyProperty extends StringProperty {
        CSSFontFamilyProperty(Key key) {
            super(key);
        }

        void setFromCSS(String cssKey, String value) {
            // no translation needed for now: just use the raw name -- if it's a preference list tho, we'll need to handle it
            setBy(value);
        }
    }

    public class ColorProperty extends Property<java.awt.Color> {
        private static final short ALPHA_NOT_PERMITTED = Short.MIN_VALUE;
        private static final short NO_ALPHA_SET = -1;
        private short fixedAlpha = NO_ALPHA_SET;

        ColorProperty(Key key) {
            super(key);
        }

        ColorProperty(Key key, Color defaultValue) {
            this(key);
            this.value = defaultValue;
        }

        public boolean isTransparent() {
            return value == null || value.getAlpha() == 0;
        }

        public boolean isTranslucent() {
            return value == null || value.getAlpha() != 0xFF;
        }

        void setAllowAlpha(boolean allow) {
            if (allow)
                fixedAlpha = NO_ALPHA_SET;
            else
                fixedAlpha = ALPHA_NOT_PERMITTED;
        }

        /** alpha should be in the range 0-255 */
        void setFixedAlpha(int alpha) {
            if (alpha > 255)
                alpha = 255;
            else if (alpha < 0)
                alpha = 0;
            fixedAlpha = (short) alpha;
            //out("SET FIXED ALPHA " + fixedAlpha);
        }

        @Override
        void set(Color newColor) {

            if (fixedAlpha < 0) {
                // the common case
                super.set(newColor);
            } else {

                if (value == newColor)
                    return;

                // enforce the fixed alpha on any incoming color:
                if (newColor != null && newColor.getAlpha() != fixedAlpha && newColor.getAlpha() != 0) {
                    //out("COLOR VALUE: " + newColor + " " + ColorToString(newColor) + " alpha=" + newColor.getAlpha());
                    newColor = new Color((newColor.getRGB() & 0xFFFFFF) + (fixedAlpha << 24), true);
                    //out("used fixed alpha " + fixedAlpha + " producing " + newColor + " alpha=" + newColor.getAlpha()
                    //+ " " + ColorToString(newColor));
                }
                super.set(newColor);
            }
        }

        @Override
        void take(Color c) {
            if (fixedAlpha < NO_ALPHA_SET && (c == null || c.getAlpha() != 0xFF))
                throw new PropertyValueVeto(key + "; color with translucence: " + c + " alpha=" + c.getAlpha()
                        + " not allowed on " + LWComponent.this);

            //             if (LWComponent.this instanceof LWNode)
            //                 super.take(c == null ? null : new Color(c.getRGB() + (fixedAlpha << 24), true));
            //             //super.take(c == null ? null : new Color(c.getRGB() + ((128 & 0xFF) << 24), true));
            //             //super.take(c == null ? null : new Color(c.getRGB() + 0x20000000, true));
            //             else
            super.take(c);
        }

        @Override
        void setBy(String s) {
            set(StringToColor(s));
        }

        @Override
        void setFromCSS(String key, String value) {
            // todo: CSS Style object could include the already instanced Color object
            // we ignore key: assume that whatever it is is a color value
            setBy(value);
        }

        /** @return a value between 0.0 and 1.0 representing brightness: the saturation % of the strongest channel
         * e.g.: white returns 1, black returns 0
         */
        public float brightness() {
            return Util.brightness(value);
        }

        //         dynamic version not workng
        //         ///** @return the color, but with 50% alpha (half transparent) */
        //         public final Color getWithAlpha(float alpha) {
        //             return new Color(value.getRGB() + (((byte)(alpha*256)) << 6), true);
        //             //return new Color(value.getRGB() + 0x80000000, true);
        //         }

        public boolean equals(Color c) {
            return value == c || (c != null && c.equals(value));
        }

        String asString() {
            return ColorToString(get());
        }
    }

    public static Color StringToColor(final String s) {
        if (s.trim().length() < 1)
            return null;

        Color c = null;
        try {
            c = VueResources.parseColor(s);
        } catch (NumberFormatException e) {
            tufts.Util.printStackTrace(new Throwable(e), "LWComponent.StringToColor[" + s + "]");
        }
        return c;
    }

    private static String ColorToDebugString(final Color c) {
        final String s = ColorToString(c);
        if (s == null)
            return "#null00";
        else
            return s;
    }

    public static String ColorToString(final Color c) {
        // if null, or no hue and no alpha, return null
        //if (c == null || ((c.getRGB() & 0xFFFFFF) == 0 && c.getAlpha() == 255))
        if (c == null)
            return null;

        if (c.getAlpha() == 255) // opaque: only bother to save hue info
            return String.format("#%06X", c.getRGB() & 0xFFFFFF);
        else
            return String.format("#%08X", c.getRGB());
    }

    public enum Alignment {
        LEFT, CENTER, RIGHT
    }

    public static final Key KEY_FillColor = new Key("fill.color", "background") {
        final Property getSlot(LWComponent c) {
            return c.mFillColor;
        }
    };
    public static final Key KEY_TextColor = new Key("text.color", "font-color") {
        final Property getSlot(LWComponent c) {
            return c.mTextColor;
        }
    };
    public static final Key KEY_StrokeColor = new Key("stroke.color", "border-color") {
        final Property getSlot(LWComponent c) {
            return c.mStrokeColor;
        }
    };
    //public static final Key KEY_StrokeStyle = new Key("stroke.style", "border-style")   { final Property getSlot(LWComponent c) { return null; } };
    public static final Key KEY_StrokeWidth = new Key("stroke.width", "stroke-width") {
        final Property getSlot(LWComponent c) {
            return c.mStrokeWidth;
        }
    };
    public static final Key KEY_StrokeStyle = new Key<LWComponent, StrokeStyle>("stroke.style", KeyType.STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mStrokeStyle;
        }
    };
    public static final Key KEY_Alignment = new Key<LWComponent, Alignment>("alignment", KeyType.STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mAlignment;
        }
    };

    /* font.size: point size for font */
    /* font.style: @See java.awt.Font 0x0=Plain, 0x1=Bold On, 0x2=Italic On */
    /* font.name: family name of the font */

    /** Aggregate font key, which represents the combination of it's three sub-properties */
    public static final Key KEY_Font = new Key("font", KeyType.STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mFont;
        }
    };
    public static final Key KEY_FontSize = new Key("font.size", KeyType.SUB_STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mFontSize;
        }
    };
    public static final Key KEY_FontStyle = new Key("font.style", KeyType.SUB_STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mFontStyle;
        }
    };
    public static final Key KEY_FontUnderline = new Key("font.underline", KeyType.SUB_STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mFontUnderline;
        }
    };
    public static final Key KEY_FontName = new Key("font.name", KeyType.SUB_STYLE) {
        final Property getSlot(LWComponent c) {
            return c.mFontName;
        }
    };

    public static final Key KEY_Collapsed = new Key<LWComponent, Boolean>("collapsed") {
        @Override
        public void setValue(LWComponent c, Boolean collapsed) {
            c.setCollapsed(collapsed);
        }

        @Override
        public Boolean getValue(LWComponent c) {
            return c.isCollapsed() ? Boolean.TRUE : Boolean.FALSE;
        }
    };

    public static final Key KEY_Created = new Key<LWComponent, Long>("created") {
        @Override
        public Long getValue(LWComponent c) {
            return c.getCreated();
        }

        @Override
        public String getStringValue(LWComponent c) {
            return new Date(c.getCreated()).toString();
        }
    };

    public final ColorProperty mFillColor = new ColorProperty(KEY_FillColor);
    public final ColorProperty mTextColor = new ColorProperty(KEY_TextColor, java.awt.Color.black) {
        //{ color = java.awt.Color.black; } // default value
        void onChange() {
            if (labelBox != null)
                labelBox.copyStyle(LWComponent.this); // todo better: handle thru style.textColor notification?
        }
    };
    public final ColorProperty mStrokeColor = new ColorProperty(KEY_StrokeColor, java.awt.Color.darkGray);
    public final FloatProperty mStrokeWidth = new FloatProperty(KEY_StrokeWidth) {
        void onChange() {
            rebuildStroke();
        }
    };
    public final EnumProperty<Alignment> mAlignment = new EnumProperty(KEY_Alignment, Alignment.LEFT) {
        void onChange() {
            layout(KEY_Alignment);
        }
    };

    public final EnumProperty<StrokeStyle> mStrokeStyle = new EnumProperty(KEY_StrokeStyle, StrokeStyle.SOLID) {
        void onChange() {
            rebuildStroke();
        }
    };

    public enum StrokeStyle {

        SOLID(1, 0), DOTTED(1, 1), DASHED(2, 2), DASH2(3, 2), DASH3(5, 3);

        private final float[] dashPattern = new float[2];

        StrokeStyle(float dashOn, float dashOff) {
            dashPattern[0] = dashOn; // pixels on (drawn)
            dashPattern[1] = dashOff; // pixels off (whitespace)
        }

        public BasicStroke makeStroke(double width) {
            return makeStroke((float) width);
        }

        public BasicStroke makeStroke(float width) {
            if (this == SOLID)
                return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
            //return new BasicStroke(width, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
            //return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);
            else
                return new BasicStroke(width, BasicStroke.CAP_BUTT // anything else will mess with the dash pattern
                        , BasicStroke.JOIN_BEVEL, 10f // miter-limit
                        //, 0f // miter-limit
                        , dashPattern, 0f); // dash-phase (offset to start of pattern -- apparently pixels, not index)
        }
        // todo opt: better: could cache the strokes here for each dash pattern/size

    }

    private void rebuildStroke() {
        final float width = mStrokeWidth.get();
        if (width > 0)
            this.stroke = mStrokeStyle.get().makeStroke(width);
        else
            this.stroke = STROKE_ZERO;
        /*/ below code was broken in previous code.  Node child layout does NOT
        // appear to be taking into account total bounds with at the moment anyway...
        // (Or was that just for Groups?  No, those appear to be handling the full bounds change.)
        // Also, want to make generic with a flag in Key if layout needed when
        // the given property changes.
        if (getParent() != null) {
        // because stroke affects bounds-width, may need to re-layout parent
        getParent().layout();
        }
        layout();*/
    }

    public final IntProperty mFontStyle = new CSSFontStyleProperty(KEY_FontStyle) {
        void onChange() {
            rebuildFont();
        }
    };
    public final IntProperty mFontSize = new IntProperty(KEY_FontSize) {
        void onChange() {
            rebuildFont();
        }
    };
    public final StringProperty mFontName = new CSSFontFamilyProperty(KEY_FontName) {
        void onChange() {
            rebuildFont();
        }
    };

    public final StringProperty mFontUnderline = new StringProperty(KEY_FontUnderline) {
        boolean isChanged(String newValue) {
            return true;
        }

        @Override
        void onChange() {
            rebuildFont();
            if (labelBox != null) {
                labelBox.copyStyle(LWComponent.this);
                layout(this.key); // could make this generic: add a key bit that says "layout needed on-change";
            }
        }

    };

    private boolean fontIsRebuilding; // todo: use a bit flag

    private void rebuildFont() {
        // This so at least for now we have backward compat with the old font property (esp. for tools & persistance)
        fontIsRebuilding = true;
        try {
            Font f = new Font(mFontName.get(), mFontStyle.get(), mFontSize.get());
            mFont.set(f);

        } finally {
            fontIsRebuilding = false;
        }
    }

    public final FontProperty mFont = new FontProperty(KEY_Font) {
        @Override
        void onChange() {
            if (!fontIsRebuilding) {
                final Font f = get();

                mFontStyle.take(f.getStyle());

                mFontSize.take(f.getSize());
                mFontName.take(f.getName());
            }

            if (labelBox != null) {
                labelBox.copyStyle(LWComponent.this);
                layout(this.key); // could make this generic: add a key bit that says "layout needed on-change";
            }
        }
    };

    public static final String KEY_LabelFormat = "label.format";
    public static final Key KEY_Label = new Key<LWComponent, String>("label", KeyType.DATA) {
        @Override
        public void setValueWithContext(LWComponent c, String val, Object context) {
            if (context == PROPERTY_SET_UNDO)
                c.setLabelImpl(val, true, false);
            else
                c.setLabel(val);
        }

        @Override
        public String getValue(LWComponent c) {
            return c.getLabel();
        }
    };
    public static final Key KEY_Notes = new Key<LWComponent, String>("notes", KeyType.DATA) {
        @Override
        public void setValue(LWComponent c, String val) {
            c.setNotes(val);
        }

        @Override
        public String getValue(LWComponent c) {
            return c.getNotes();
        }
    };

    //===================================================================================================
    //
    // End of Key's and Properties
    //
    //===================================================================================================

    // for debug
    private static String vtag(Object key, Object val, Property p) {
        if (val == null) {
            return key + "(null)";
        } else if (val.getClass() == String.class) {
            return key + "(\"" + val + "\")";
        }

        String typeName = val.getClass().getName();
        String valType = typeName.substring(typeName.lastIndexOf('.') + 1);
        String valRep = (p == null ? val.toString() : p.asString());

        String extra = "";

        //if (p != null) extra = val.toString();
        //valType += "@" + Integer.toHexString(val.hashCode());

        return key + " " + valType + "(" + valRep + ")" + extra + "";
    }

    // todo: this not yet implemented as actually persistant -- it
    // meant to be picked out of client data at save time and be
    // specially persisted
    public static class PersistClientDataKey {
        final String name;

        public PersistClientDataKey(String s) {
            name = s;
        }

        @Override
        public String toString() {
            return name;
        }
    }

    // maybe rename these store/fetchProperty

    /** set a generic property in the model -- users of this API need to ensure the keys
     * they're using are unique with respect to any other potential users of this API.
     * Setting a property value to null removes the property. This is basically "cookies"
     * for LWComponents at runtime.
     */
    public void setClientData(Object key, Object o) {
        if (mClientData == null) {
            if (o == null) // storing null means remove value
                return;
            else
                mClientData = new HashMap(2);
        }

        if (DEBUG.DATA) {
            String keyName = key instanceof Class ? key.toString() : Util.tags(key);
            out("setClientData: " + keyName + "=" + Util.tags(o));
        }

        if (o == null) {
            mClientData.remove(key);
            if (mClientData.size() == 0)
                mClientData = null;
        } else {
            mClientData.put(key, o);
        }
    }

    public <T> T putClientData(T o) {
        setClientData(o.getClass(), o);
        return o;
    }

    public Object getClientData(Object key) {
        return mClientData == null ? null : mClientData.get(key);
    }

    public void setClientData(Class classKey, String subKey, Object o) {
        setClientData(classKey.getName() + "/" + subKey, o);
    }

    public boolean hasClientData(Object o) {
        return getClientData(o) != null;
    }

    public void flushAllClientData() {
        if (mClientData != null) {
            if (DEBUG.DATA && mClientData.size() > 0) {
                Log.debug("flushing all client data;");
                Util.dump(mClientData);
            }
            mClientData = null;
        }
    }

    //     public void setInstanceProperty(String subKey, Object o) {
    //         setClientProperty(o.getClass().getName() + "/" + subKey, o);
    //     }

    //     public void setInstanceProperty(Object o) {
    //         setClientProperty(o.getClass(), o);
    //     }

    // todo: support client data via arbitrary ENUM values (e.g., with EnumMap, and maybe bits via an EnumSet)

    /** convenience typing fetch when using a Class as a property key, that returns a value casted to the class type */
    public <A> A getClientData(Class<A> classKey, String subKey) {
        final Object o = getClientData(classKey.getName() + "/" + subKey);
        if (classKey == Boolean.class && o == null)
            return (A) Boolean.FALSE; // special hack for null == boolean false
        else
            return (A) o;
    }

    /** convenience typing fetch when using a Class as a property key, that returns a value casted to the class type */
    public <A> A getClientData(Class<A> classKey) {
        return (A) getClientData((Object) classKey);
    }

    /**
     * Get the named property value from this component.
     * @param key property key (see LWKey)
     * @return object representing appropriate value, or null if none found (note: properties may be null also -- todo: fix)
     */

    public Object getPropertyValue(final Object key) {
        if (key instanceof Key) {
            // If getValue on the key was overriden, we may still need to trap an exception here
            try {
                return ((Key) key).getValue(this);
            } catch (ClassCastException e) {
                String msg = "Property not supported(getPropertyValue): " + key + " on " + this
                        + " (returned null)";
                if (DEBUG.META)
                    tufts.Util.printStackTrace(e, msg);
                else
                    Log.warn(msg + "; " + e);
                return null;
            }
        }

        // Old property keys that don't make use of the Key class yet:
        if (key == LWKey.Resource)
            return getResource();
        if (key == LWKey.Location)
            return getLocation();
        if (key == LWKey.Size)
            return new Size(this.width, this.height);
        if (key == LWKey.Hidden)
            return isHidden() ? Boolean.TRUE : Boolean.FALSE;

        Log.warn(this + " getPropertyValue; unsupported property [" + key + "] (returning null)");
        if (key == null)
            Util.printStackTrace("key was null");
        //throw new RuntimeException("Unknown property key[" + key + "]");
        return null;
    }

    public static final Object PROPERTY_SET_DEFAULT = "default";
    public static final Object PROPERTY_SET_UNDO = "undo";
    //public static final Object PROPERTY_SET_USER = "propertyUser";

    void undoProperty(Object key, Object val) {
        setPropertyImpl(key, val, PROPERTY_SET_UNDO);
    }

    public final void setProperty(final Object key, final Object val) {
        setPropertyImpl(key, val, PROPERTY_SET_DEFAULT);
    }

    protected void setPropertyImpl(final Object key, final Object val, final Object context) {
        if (DEBUG.TOOL || DEBUG.UNDO)
            Log.debug("setPropertyImpl[" + context + "] " + this + " " + vtag(key, val, null));

        if (key instanceof Key) {
            final Key k = (Key) key;
            k.setValueWithContext(this, val, context);
        }
        // Old property keys that don't make use of the Key class yet:
        //else if (key == LWKey.Hidden)        setHidden( ((Boolean)val).booleanValue());
        else if (key == KEY_LabelFormat)
            setLabelFormat((String) val);
        else if (key == LWKey.DataUpdate)
            setDataMap((MetaMap) val);
        else if (key == LWKey.Scale)
            setScale((Double) val);
        else if (key == LWKey.Resource)
            setResource((Resource) val);
        else if (key == LWKey.Location) {

            // This is a bit of a hack, in that we're relying on the fact that the only
            // thing to call setProperty with a Location key right now is the
            // UndoManager.  In any case, on undo, we do NOT want to additionally make
            // mapLocationChanged calls on all descendents (for absolute map location
            // objects; e.g. LWLink's).  Location changes as a result of these calls
            // were already recorded as events and will be undone on their own.

            final Point2D.Float loc = (Point2D.Float) val;
            setLocation(loc.x, loc.y, this, false);
            //setLocation( (Point2D) val);
        } else if (key == LWKey.Size) {
            Size s = (Size) val;
            setSize(s.width, s.height);
        } else if (key == LWKey.Frame) {
            Rectangle2D.Float r = (Rectangle2D.Float) val;
            setFrame(r.x, r.y, r.width, r.height);
        }
        //         else if (key == LWKey.Hidden) {
        //             // would need HIDE_CAUSE
        //             //setHidden(((Boolean)val).booleanValue());
        //             Log.debug("setProperty " + key + "=" + val + " on " + this);
        //         }
        else {
            //out("setProperty: unknown key [" + key + "] with value [" + val + "]");
            tufts.Util.printStackTrace(
                    "FYI: Unhandled Property key: " + key.getClass() + "[" + key + "] with value [" + val + "]");
        }
    }

    /**
     * This is used during duplication of group's of LWComponent's
     * (e.g., a random selection, or a set of children, or an entire map),
     * to reconnect links within the group after duplication, and
     * passing flags into the dupe context.
     */
    public static class LinkPatcher {
        private java.util.Map<LWComponent, LWComponent> mCopies = new java.util.IdentityHashMap();
        private java.util.Map<LWComponent, LWComponent> mOriginals = new java.util.IdentityHashMap();

        public LinkPatcher() {
            if (DEBUG.DND)
                Log.debug("LinkPatcher: created");
        }

        public void reset() {
            mCopies.clear();
            mOriginals.clear();
        }

        public void track(LWComponent original, LWComponent copy) {
            if (DEBUG.DND && DEBUG.META)
                Log.debug("LinkPatcher: tracking " + copy);
            mCopies.put(original, copy);
            mOriginals.put(copy, original);
        }

        //public Collection getCopies() { return mCopies.values(); }

        public void reconnectLinks() {

            // Find all LWLink instances in the set of copied
            // objects, and fix their endpoint pointers to
            // point to the right object within the copied set.

            for (LWComponent c : mCopies.values()) {
                if (!(c instanceof LWLink))
                    continue;

                final LWLink linkCopy = (LWLink) c;
                final LWLink linkOriginal = (LWLink) mOriginals.get(linkCopy);

                final LWComponent headCopy = mCopies.get(linkOriginal.getHead());
                final LWComponent tailCopy = mCopies.get(linkOriginal.getTail());

                if (DEBUG.DND)
                    Log.debug("LinkPatcher: reconnecting " + linkCopy + " endpoints:" + "\n\t" + headCopy + "\n\t"
                            + tailCopy);

                linkCopy.setHead(headCopy);
                linkCopy.setTail(tailCopy);
            }
        }
    }

    public static class CopyContext {
        final boolean dupeChildren;
        LinkPatcher patcher;

        CopyContext() {
            this(true);
        }

        CopyContext(boolean dupeChildren) {
            this.dupeChildren = dupeChildren;
        }

        CopyContext(LinkPatcher lp, boolean dupeChildren) {
            this.patcher = lp;
            this.dupeChildren = dupeChildren;
        }

        void reset() {
            if (patcher != null)
                patcher.reset();
        }

        void complete() {
            if (patcher != null)
                patcher.reconnectLinks();
        }
    }

    protected void copySupportedProperties(LWComponent c) {
        mSupportedPropertyKeys = c.mSupportedPropertyKeys;
    }

    public boolean canDuplicate() {
        return true;
    }

    public LWComponent duplicate() {
        return duplicate(new CopyContext());
    }

    /**
     * Create a component with duplicate content & style.  Does not duplicate any links
     * to this component, and leaves it an unparented orphan.  Leaving the parent null
     * is important until we know what we're doing with the duplicate. E.g., it may
     * just sit in a scratch buffer and used as a down-stream duplicating source
     * for pasting, and never be added to the model anywhere.
     *
     * @param CopyContext may be null.  If not, it's used when duplicating group's of
     * objects containing links that need to be reconnected at the end of the duplicate.
     */

    public LWComponent duplicate(CopyContext cc) {
        final LWComponent c;
        try {
            c = getClass().newInstance();
        } catch (Throwable t) {
            Log.error("duplicate " + getClass(), t);
            return null;
        }
        return duplicateTo(c, cc);
    }

    /**
     * Provided for subclass impl's that need to support final members, which can use a
     * pre-constructed empty object in their override of duplicate v.s. relying on the default use
     * of newInstance via calls to super.duplicate. Technically, this also permits any subclass of
     * instance of LWComponent to "duplicate" an instance of different subclass, and only the
     * compatible properties will copy themselves over.
     */
    protected <Ts extends LWComponent> Ts duplicateTo(Ts c, CopyContext cc) {
        c.setFlag(Flag.DUPLICATING);
        c.mXMLRestoreUnderway = true; // todo: this flag really "initUnderway" -- need to double-check all our semantics tho...

        c.copySupportedProperties(this);

        c.x = this.x;
        c.y = this.y;
        c.width = this.width;
        c.height = this.height;
        c.scale = this.scale;
        c.stroke = this.stroke; // cached info only

        c.copyMetaData(this);

        c.copyStyle(this); // this copies over all compatiable Property values

        c.setAutoSized(isAutoSized()); // may be sensitive to order of operations during init

        if (c instanceof LWText) {
            c.label = this.label;
            ((LWText) c).getRichLabelBox().setText(((LWText) this).getRichLabelBox().getRichText());
        } else {
            c.setLabelImpl(this.label, true, false);
            c.getLabelBox().setSize(getLabelBox().getSize());
        }

        if (hasResource())
            c.setResource(getResource());
        if (hasNotes())
            c.setNotes(getNotes());

        if (cc.patcher != null)
            cc.patcher.track(this, c);

        c.mXMLRestoreUnderway = false;
        c.layout("duplicate");
        c.clearFlag(Flag.DUPLICATING);

        return c;
    }

    protected void copyMetaData(final LWComponent source) {
        if (source.metadataList != null) {
            // duplicate original style meta-data list
            final List<VueMetadataElement> srcVMD = source.getMetadataList().getMetadata();
            final List<VueMetadataElement> targetVMD = this.getMetadataList().getMetadata();
            for (VueMetadataElement vme : srcVMD)
                targetVMD.add(vme);
        }
        // duplicate data-set data
        if (source.mDataMap != null)
            mDataMap = source.mDataMap.clone();
    }

    protected boolean isPresentationContext() {
        if (true)
            return false;// turned off for now
        if (parent == null)
            return false; // this means presentation nodes will report wrong sizes during restores...
        else
            return parent.isPresentationContext();
    }

    protected final void ensureID(LWComponent c) {
        ensureID(c, true);
    }

    /**
     * Make sure this LWComponent has an ID -- will have an effect on
     * on any brand new LWComponent exactly once per VM instance.
     * @param descendents - if true, will also ensure all descendents
     */
    protected final void ensureID(LWComponent c, boolean descendents) {
        if (c.getID() == null) {
            String id = getNextUniqueID();
            // no ID may be available if we're an orphan: it will be
            // patched up when we eventually get added to to a map
            if (id != null)
                c.setID(id);
        }

        //         if (descendents) {
        //             for (LWComponent child : c.getAllDescendents(ChildKind.ANY))
        //                 ensureID(child, false);
        //         }
        // Not safe: tho shouldn't happen, if any parent<->child loops make their way into the hierarchy, we can stack overflow here
        // [doesn't matter: getAllDescendents will just stack-overflow instead]
        if (descendents) {
            for (LWComponent child : c.getChildList()) {
                ensureID(child);
            }
        }
    }

    protected String getNextUniqueID() {
        if (getParent() == null) {
            //throw new IllegalStateException("LWComponent has null parent; needs a parent instance subclassed from LWContainer that implements getNextUniqueID: " + this);
            //if (DEBUG.PARENTING) tufts.Util.printStackTrace("getNextUniqueID: returning null for current orphan " + this);
            if (DEBUG.PARENTING)
                out("getNextUniqueID: returning null for current orphan");
            return null;
        } else
            return getParent().getNextUniqueID();
    }

    //private static int MapDepth;
    public LWMap getMap() {
        if (parent == null) {
            return null;
        } else {
            //             if (++MapDepth >= 64) { // DEBUG
            //                 Util.printStackTrace("PARENT LOOP at depth " + MapDepth);
            //                 System.err.println("LWC: " + this);
            //                 return null;
            //             }
            //             final LWMap m = parent.getMap();
            //             MapDepth--;
            //             return m;
            return parent.getMap();

        }
    }

    public UndoManager getUndoManager() {
        final LWMap map = getMap();
        if (map == null)
            return null;
        else
            return map.getUndoManager();
    }

    protected void addCleanupTask(Runnable task) {
        addCleanupTask(task, this);
        //addCleanupTask(task, this, null);
    }

    //    protected void addCleanupTask(Runnable task, Object taskKey, Object srcMsg) {
    protected void addCleanupTask(Runnable task, Object taskKey) {
        final UndoManager um = getUndoManager();

        if (um != null) {
            if (um.isUndoing()) {
                if (DEBUG.WORK || DEBUG.UNDO)
                    System.out.println("Ignoring cleanup task during undo: " + task + " for " + this);
            } else if (um.hasCleanupTask(taskKey)) {
                if (DEBUG.WORK || DEBUG.UNDO)
                    System.out.println("Ignoring duplicate cleanup task: " + task + " for " + this);
            } else {

                boolean debug = DEBUG.WORK || DEBUG.UNDO;
                if (isDeleted()) {
                    Util.printStackTrace("warning: adding cleanup task when deleted");
                    debug = true;
                }

                //                 if (debug) {
                //                     System.out.println(TERM_RED + "ADDING CLEANUP TASK: " + task
                //                                        + (srcMsg==null?"":("on " + srcMsg))
                //                                        + (task == this ? "" : (" for " + this))
                //                                        + TERM_CLEAR);
                //                 }

                um.addCleanupTask(this, task);
            }
        }
    }

    public UserMapType getUserMapType() {
        throw new UnsupportedOperationException("deprecated");
    }

    public static final String EnumeratedValueKey = "@ValueOf";

    //public void setDataInstanceValue(String key, Object value) {
    public void setDataInstanceValue(tufts.vue.ds.Field field, Object value) {
        //getMetadataList().add(key, value.toString());
        final String key = field.getName();
        getMetadataList().add(key, value.toString());
        getDataMap().put(key, value);
        getDataMap().setSchema(field.getSchema());
        getDataMap().put(EnumeratedValueKey, field.getName());
        //getDataMap().put("@Schema", field.getSchema());
    }

    public TableBag getDataTable() {
        return mDataMap;
    }

    private MetaMap getDataMap() {
        if (mDataMap == null) {
            mDataMap = new MetaMap();
        }
        return mDataMap;
    }

    /** for castor peristance */
    public MetaMap getPersistDataMap() {
        if (mXMLRestoreUnderway)
            return getDataMap();
        else
            return mDataMap; // if null on save, nothing to persist
    }

    /** for castor peristance */
    public void setPersistDataMap(MetaMap dataMap) {
        mDataMap = dataMap;
    }

    /** replace ALL data on this node at once, generating events for undo */
    public void setDataMap(MetaMap dataMap) {
        if (mDataMap == dataMap)
            return;
        Object old = this.mDataMap;
        mDataMap = dataMap;
        notify(LWKey.DataUpdate, old);
        if (mLabelFormat != null) {
            setLabelImpl(fillLabelFormat(mLabelFormat), true, false);
        }
    }

    //     public Collection<Map.Entry<String,Object>> getPersistDataMap() {
    //         if (mXMLRestoreUnderway)
    //             return getDataMap().entries();
    //         else if (mDataMap != null)
    //             return mDataMap.entries();
    //         else
    //             return null;
    //     }

    //public void addDataValues(final Iterable<Map.Entry<String,String>> entries) {
    //     public void addDataValues(final Iterable<Map.Entry> entries) {
    //         getMetadataList().add(entries);
    //         getDataMap().putAllStrings(entries);
    //     }

    /** used for new node creation: todo -- get rid of this when we get rid of getMetadataList() */
    public void takeAllDataValues(MetaMap map) {
        setPersistDataMap(map);

        //----------------------------------------------------------------------------------------
        // TODO: GET RID OF THIS DUPLICATION.  This is done so that these can be UI editable and so
        // RDFIndex will index these, making them searchable. Of course, only the COPIES are
        // editable -- the originals stay the same.  But adding these to the RDFIndex directly from
        // the mDataMap would be trivial to add in RDFIndex.java, and do we really need/want to be
        // able to edit them?  Of course, what we really want is to get rid of the
        // VueMetaDataElement and related MetadataList classes completely.
        // ----------------------------------------------------------------------------------------

        getMetadataList().add(map.entries()); // duplicate in old meta-data for now
    }

    public void addDataValue(String key, String value) {
        // TODO PERFORMANCE: GET RID OF THIS:
        getMetadataList().add(key, value);

        // just leave this:
        getDataMap().add(key, value);
    }

    public String getDataValue(String key) {
        if (mDataMap == null)
            return null;
        return mDataMap.getString(key);

        //         VueMetadataElement vme = getMetadataList().get(key);
        //         return vme == null ? null : vme.getValue();

    }

    // TODO: rename all these getDataValue* to getSingleValue*

    public String getDataValueFieldName() {
        if (mDataMap == null)
            return null;

        String fieldName = mDataMap.getString(EnumeratedValueKey);

        //         //-----------------------------------------------------------------------------
        //         // backward compat before @schema.field stored, and
        //         // only @schema was stored.  The first entry should always be
        //         // the schmatic field.  todo: remove this eventually
        //         if (fieldName == null && isDataValueNode()) {
        //             final Map.Entry firstEntry = mDataMap.entries().iterator().next();
        //             fieldName = firstEntry.getKey().toString();
        //         }
        //         //-----------------------------------------------------------------------------

        return fieldName;
    }

    /** @return the Field if this a single-value field node, null otherwise */
    public Field getDataValueField() {
        if (mDataMap == null || mDataMap.getSchema() == null)
            return null;

        String fieldName = getDataValueFieldName();

        if (fieldName != null)
            return mDataMap.getSchema().getField(fieldName);
        else
            return null;
    }

    /** @return true if the data-set data for this node represents a SINGLE VALUE from a field (e.g., one of an enumeration)
     * Should always return the opposite of isDataRow */
    public boolean isDataValueNode() {
        return mDataMap != null && mDataMap.hasKey(EnumeratedValueKey);
    }

    /** @return true if this is a single value data node of the given name from *any* schema */
    public boolean isDataValueNode(String name) {
        return name.equals(getDataValueFieldName());
    }

    /** @return true if this is a single value data node for the given Field in it's Schema */
    public boolean isDataValueNode(Field field) {
        return getDataSchema() == field.getSchema() && field.getName().equals(getDataValueFieldName());
    }

    /**
     * @return true if this is a data-row node from the given schema.
     * todo: schema checking is currently weak -- only checks for key field
     */
    public boolean isDataRow(Schema schema) {
        //return hasDataKey(schema.getKeyField().getName()) && !isDataValueNode();
        return getDataSchema() == schema && !isDataValueNode();
    }

    public boolean isDataRowNode() {
        return getDataSchema() != null && !isDataValueNode();
    }

    public MetaMap getRawData() {
        //if (mDataMap == null) Util.printStackTrace("NULLDATA");
        return mDataMap;
    }

    public final boolean isDataNode() {
        return mDataMap != null;
    }

    public boolean hasDataKey(String key) {
        return mDataMap != null && mDataMap.hasKey(key);
    }

    public Schema getDataSchema() {
        if (mDataMap != null)
            return mDataMap.getSchema();
        else
            return null;
    }

    /** @return true if a schema-handle was turned into a live schema reference */
    boolean validateSchemaReference() {

        final MetaMap data = getRawData();
        if (data == null)
            return false;

        final Schema schema = data.getSchema();
        if (schema != null) {
            Schema liveSchema = Schema.lookupAuthority(schema);
            if (schema != liveSchema) {
                data.setSchema(liveSchema);
                if (DEBUG.DATA)
                    Log.debug("updated schema " + this);
                return true;
            }
        }

        return false;
    }

    /** @return null -- this is only needed for LWMap, but is implemented here to force
     * the order of persistance based on the castor mapping, so schemas can persist
     * before nodes in the LWMap, which overrides this to return schemas included in the
     * map.  If this was only declared in the mapping file as an LWMap persistance item,
     * it would persist after all LWComponent mappings (LWMap subclasses LWComponent).
     */
    public Collection<tufts.vue.ds.Schema> getIncludedSchemas() {
        return null;
    }

    public boolean hasDataValue(String key, CharSequence value) {
        return mDataMap != null && mDataMap.hasEntry(key, value);
        //return isSchematicFieldNode() && mDataMap.containsEntry(key, value);
        //         if (mDataMap == null)
        //             return false;
        //         return mDataMap.containsKey("@Schema") && mDataMap.containsEntry(key, value);
        // //         VueMetadataElement vme = getMetadataList().get(key);
        // //         return vme == null ? false : value.equals(vme.getValue());
    }

    //     public void containsMetaData(String key, Object value) {
    //         getMetadataList().add(key, value.toString());
    //     }

    /**
     * Metadata List for use with RDF Index It is sufficient for the minimal RDF functionality to
     * be able to retrieve this list from the LWComponent using this method and add elements
     * directly to the list as needed.  LWComponent may choose to create notifications/modifcations
     * for any data added directly through LWComponent itself in future.
     **/
    public MetadataList getMetadataList() {
        if (metadataList == null)
            metadataList = new MetadataList();
        return metadataList;
    }

    public void setMetadataList(MetadataList list) {
        metadataList = list;
    }

    public void setXMLmetadataList(MetadataList list) {
        setMetadataList(list);
    }

    public MetadataList getXMLmetadataList() {
        if (mXMLRestoreUnderway) {
            return getMetadataList();
        } else {
            // persist underway:
            if (metadataList != null && metadataList.isEmpty())
                return null;
            else
                return metadataList;
        }
    }

    /** see edu.tufts.vue.metadata.VueMetadataElement for metadata types **/
    public boolean hasMetaData(int type) {
        if (metadataList != null)
            return metadataList.hasMetadata(type);
        else
            return false;
        // return ( (metadataList != null) && (getMetaDataAsHTML().length() > 0) );
    }

    public boolean hasMetaData() {
        return hasMetaData(edu.tufts.vue.metadata.VueMetadataElement.CATEGORY);
    }

    public String getMetaDataAsHTML() {
        return getMetaDataAsHTML(edu.tufts.vue.metadata.VueMetadataElement.CATEGORY);
    }

    /** see edu.tufts.vue.metadata.VueMetadataElement for metadata types */
    public String getMetaDataAsHTML(int type) {
        if (metadataList != null)
            return metadataList.getMetadataAsHTML(type);
        else
            return "";
    }

    /**
     * @return true if should not be drawn due to a currently applied filter, false if not.
     * Note that FILTERING is diffrent than HIDING via a HideCause.  A hidden node
     * hides all of its children, but a filtered not does not (which can create
     * some strange visual situations, but there you have it).
     **/
    public boolean isFiltered() {
        return hasFlag(Flag.FILTERED);
    }

    /**
     * [This sets the flag for the component so that it is either
     * hidden or visible based on a match to the active LWCFilter.]
     * 2012: are LWCFilters still used?
     **/
    public void setFiltered(boolean filtered) {
        //if (DEBUG.SEARCH&&DEBUG.TEST) Log.debug("setFiltered " + filtered + "; " + this);
        setFlag(Flag.FILTERED, filtered);
    }

    //     protected void setFilterBits(int bits) {
    //         final boolean wasFiltered = isFiltered()
    //         mHideBits = bits;
    //         if (wasHidden != isHidden())
    //             notify(LWKey.Hidden);
    //         //notify(LWKey.Hidden, wasHidden); // if we need it to be undoable
    //     }

    protected boolean alive() {
        // "internal" objects should always report events (e.g., special styles, such as data-styles)
        return parent != null || hasFlag(Flag.INTERNAL); // re-enabled for data-style records reporting thier changes to the DataTree
    }

    /** This should only be called once when added to the map, and on deserializations */
    public void setCreated(final long time) {
        //Log.debug(String.format("setCreated %s; %s", new Date(time), this));
        if (mCreated != 0 && alive()) {
            Log.warn("setCreated erasing " + mCreated + "=" + new Date(mCreated) + " with: " + time + "="
                    + new Date(time) + "; " + this);
        }
        mCreated = time;

    }

    public long getCreated() {
        return mCreated;
    }

    /**
     * Called during restore from presistance, or when newly added to a container.
     * Must be called at some point before any attempt to persist, with a unique
     * identifier within the entire LWMap.  This is how components are referenced
     * in the persisted data.
     */
    public void setID(String ID) {
        if (this.ID != null)
            throw new IllegalStateException("Can't set ID to [" + ID + "], already set on " + this);
        //System.out.println("setID [" + ID + "] on " + this);
        this.ID = ID;

        // special case: if undo of add of any component that was brand new, this is
        // a new component creation, and to undo it is actually a delete.
        // UndoManager handles the hierarchy end of this, but we need this here
        // to differentiate hierarchy events that are just reparentings from
        // new creation events.

        if (!mXMLRestoreUnderway) {

            // setting the creation time when the ID is set is appropriate because the
            // ID is set whenever a node newly joins a map: e.g., a duplicate of a node
            // will have it's ID re-set when it's added to a new map.  This is often
            // still the fall-back though: LWContainer will apply the exact same stamp
            // to collections of nodes that are newly added at the same time.

            if (mCreated == 0) {
                if (DEBUG.WORK)
                    Log.debug("fallback timestamp: " + this);
                setCreated(System.currentTimeMillis());
            }

            notifyForce(LWKey.Created, new Undoable() {
                void undo() {
                    // parent may already have deleted it for us, so only delete if need be
                    // todo performance: force parents to always handle this so can skip creating this event
                    // (has impact when creating handling thousands of nodes)
                    if (!isDeleted())
                        removeFromModel();
                }
            });
        }
    }

    //     /** set the ID string, no questions asked */
    //     protected void takeID(String ID) {
    //         this.ID = ID;
    //     }

    public void setLabel(String label) {
        setLabelImpl(label, true, true);
    }

    // todo: rename / use a package private setLabelImpl
    void setLabel0(String newLabel, boolean setDocument) {
        setLabelImpl(newLabel, setDocument, true);
    }

    /** for persistance */
    public String getLabelFormat() {
        return mLabelFormat;
    }

    /** for persistance -- will not update the label */
    public void setLabelFormat(String s) {
        if (s == mLabelFormat || (s != null && s.equals(mLabelFormat)))
            return;

        final Object old = mLabelFormat;
        mLabelFormat = s;
        if (alive())
            notify(KEY_LabelFormat, old);
        //if (alive()) notify("label.format", new Undoable(oldFormat) { void undo() { setLabelFormat(oldFormat); }} );
    }

    public void setLabelTemplate(String s) {
        setLabelImpl(s, false, false);
    }

    private static boolean isDataFormatString(String s) {
        return s != null && s.indexOf('$') >= 0;
        // todo: check with full regex: e.g: \$\{.+\}
        // however, it's okay to over-match, as replacements that can't be understood are left as-is
    }

    /**
     * @param setDocument -- we're called by TextBox after document edit with setDocument=false,
     * so we don't attempt to re-update the TextBox, which has just been
     * updated.
     *
     * @param allowFillData -- called with allowFillData=false if we don't
     * want to actually do a data-fill and just want to leave the
     * label as the actual format (e.g.  we just computed filled data
     * and now want to set the label for real, or this is an undo).
     */
    protected void setLabelImpl(String newLabel, final boolean setDocument, final boolean allowDataFill) {
        if (DEBUG.TEXT || DEBUG.DATA)
            out("setLabelImpl " + Util.tags(newLabel) + " setDoc=" + setDocument + " allowDataFill="
                    + allowDataFill);
        newLabel = cleanControlChars(newLabel);

        if (!mXMLRestoreUnderway && allowDataFill && !isStyling(LWKey.Label)) {

            // If we're "styling" the label, DATA_STYLE is set -- if this is a
            // DATA_STYLE, we never want to attempt a template fill -- this is a styling
            // node where templates themseleves are stored.

            if (isDataFormatString(newLabel)) {
                final String filled = fillLabelFormat(newLabel);
                setLabelFormat(newLabel);
                newLabel = filled;
            } else {
                // clear out the saved template for dynamic data-update
                setLabelFormat(null);
            }
        }

        if (this.label == newLabel)
            return;
        if (this.label != null && this.label.equals(newLabel))
            return;

        Object old = this.label;

        if (newLabel == null || newLabel.length() == 0) {
            this.label = null;
            if (labelBox != null)
                labelBox.setText("");
        } else {
            this.label = newLabel;
            if (DEBUG.TEXT || DEBUG.DATA)
                out("setLabelImpl textSet " + Util.tags(newLabel));
            // todo opt: only need to do this if node or link (LWImage?)
            // Handle this more completely -- shouldn't need to create
            // label box at all -- why can't do entirely lazily?
            if (this.labelBox == null) {
                // figure out how to skip this:
                //getLabelBox();
            } else if (setDocument) {
                try {
                    // note: this needs to happen before the call to layout below
                    getLabelBox().setText(newLabel);
                } catch (Throwable t) {
                    Log.error(String.format("failed to set label '%s' on %s in %s", newLabel,
                            Util.tags(getLabelBox()), this), t);
                }
            }
        }
        layout();
        notify(LWKey.Label, old);
    }

    public void wrapLabelToWidth(final int charWidth) {
        final String existingLabel = getLabel();
        final String wrappedLabel = Util.formatLines(existingLabel, charWidth);
        if (wrappedLabel != existingLabel) {
            if (DEBUG.Enabled)
                Log.debug(this + " wrapped to: " + Util.tags(wrappedLabel));
            setLabelImpl(wrappedLabel, true, false);
        }
    }

    private static final int MaxLabelLineLength = VueResources.getInt("dataNode.labelLength", 50);

    private String fillLabelFormat(final String fmt) {
        final String[] parts = fmt.split("\\$");

        if (parts.length == 1)
            return fmt;

        final StringBuilder buf = new StringBuilder();
        boolean anyReplacement = false;

        int part = 0;
        for (String s : parts) {
            //Log.debug("got _part[" + s + "]");
            boolean noValueFound = true;
            try {
                final int braceOpen = s.indexOf('{');
                final int braceClose = s.indexOf('}');
                if (braceOpen == 0 && braceClose > 1) {
                    final String keyName = s.substring(1, braceClose).trim();
                    //Log.debug("got __key[" + keyName + "]");
                    final String value = findLabelFormatDataValue(keyName);
                    if (value != null) {
                        //Log.debug("got value[" + value + "]");

                        // replace ${someDataKey} with the value found
                        buf.append(org.apache.commons.lang.StringEscapeUtils.unescapeHtml(value));

                        // include untouched any/all text found after the '}'
                        buf.append(s.substring(braceClose + 1, s.length()));

                        noValueFound = false;
                        anyReplacement = true;
                    }
                }
            } catch (Throwable t) {
                Log.error("exception processing replacement braces " + Util.tags(s) + " in format " + Util.tags(fmt)
                        + " for " + this, t);
            }

            if (noValueFound) {
                // leave un-touched
                if (part > 0)
                    buf.append('$'); // restore the '$' we split on
                buf.append(s);
            }
            part++;
        }

        if (DEBUG.TEXT || DEBUG.DATA) {
            if (anyReplacement)
                Log.debug(this + " FILL made from " + Util.tags(fmt) + "->" + Util.tags(buf.toString()));
            else
                Log.debug(this + " FILL; NO REPLACEMENTS MADE in " + Util.tags(fmt));
        }

        return anyReplacement ? Util.formatLines(buf.toString(), MaxLabelLineLength) : fmt;
    }

    private String findLabelFormatDataValue(String key) {

        String value = getDataValue(key);

        if (value == null && hasResource())
            value = getResource().getProperty(key);

        if (value == null) {
            VueMetadataElement vme = getMetadataList().get(key);
            value = (vme == null ? null : vme.getValue());
        }

        return tufts.vue.ds.Field.valueText(value);
    }

    protected tufts.vue.TextBox getLabelBox() {
        try {
            if (this.labelBox == null) {
                synchronized (this) {
                    if (this.labelBox == null)
                        this.labelBox = new tufts.vue.TextBox(this, this.label);
                }
            }
        } catch (Throwable t) {
            //Util.printStackTrace(t, "failed to init labelBox for " + this);
            Log.error("failed to init labelBox for " + this, t);
        }

        return this.labelBox;
    }

    public void setNotes(String pNotes) {
        pNotes = cleanControlChars(pNotes);
        Object old = this.notes;
        if (pNotes == null) {
            this.notes = null;
        } else {
            String trimmed = pNotes.trim();
            if (trimmed.length() > 0)
                this.notes = pNotes;
            else
                this.notes = null;
        }
        layout();
        notify(LWKey.Notes, old);
    }

    /*
    public void setMetaData(String metaData)
    {
    this.metaData = metaData;
    layout();
    notify("meta-data");
    }
    // todo: setCategory still relevant?
    public void setCategory(String category)
    {
    this.category = category;
    layout();
    notify("category");
    }
    */
    /*
    public String getCategory()
    {
    return this.category;
    }
    */

    public void takeResource(Resource resource) {
        this.resource = resource;
    }

    public void setResource(Resource resource) {
        if (DEBUG.CASTOR)
            out("SETTING RESOURCE TO " + (resource == null ? "" : resource.getClass()) + " [" + resource + "]");
        Object old = this.resource;
        takeResource(resource);
        layout();
        if (DEBUG.CASTOR)
            out("NOTIFYING");
        notify(LWKey.Resource, old);

        /*
        try {
        layout();u
        } catch (Exception e) {u
        e.printStackTrace();
        if (DEBUG.CASTOR) System.exit(-1);
        }
        */
    }

    public Resource getResource() {
        return this.resource;
    }

    public Resource.Factory getResourceFactory() {
        final LWMap map = getMap();
        if (map == null)
            return Resource.getFactory();
        else
            return map.getResourceFactory();
    }

    /** convenience delegate to resource factory */
    public void setResource(String spec) {
        setResource(getResourceFactory().get(spec));
    }

    /** convenience delegate to resource factory */
    public void setResource(java.net.URL url) {
        setResource(getResourceFactory().get(url));
    }

    /** convenience delegate to resource factory */
    public void setResource(java.net.URI uri) {
        setResource(getResourceFactory().get(uri));
    }

    /** convenience delegate to resource factory */
    public void setResource(java.io.File file) {
        setResource(getResourceFactory().get(file));
    }

    //     public void setResource(String urn)
    //     {
    //         if (urn == null || urn.length() == 0)
    //             setResource((Resource)null);
    //         else
    //             setResource(new MapResource(urn));
    //     }

    public String getID() {
        return this.ID;
    }

    public int getNumericID() {
        return idStringToInt(getID());
    }

    /** for use during restore */
    protected final int idStringToInt(String idStr) {
        //         if (idStr != null && idStr.charAt(0) == '<') {
        //             // special case for internal use objects, marked with '<' as initial character
        //             return -1;
        //         }

        int id = -1;
        try {
            id = Integer.parseInt(idStr);
        } catch (Throwable t) {
            Log.warn("non-numeric ID: '" + idStr + "' " + t);
            //             System.err.println(e + " invalid ID: '" + idStr + "'");
            //             e.printStackTrace();
        }
        return id;
    }

    /*  public String getStyledLabel()
      {
         return this.label;
        
      }*/
    public String getLabel() {
        return this.label;
        /*
        if (this.label == null)
           return null;
            
        String noHTMLString = this.label.replaceAll("\\<.*?\\>","");
        noHTMLString = noHTMLString.replaceAll("\\&.*?\\;","");
        noHTMLString = noHTMLString.replaceAll("\n","");
        noHTMLString = noHTMLString.replaceAll("\\<!--.*?--\\>","");
        noHTMLString = noHTMLString.replaceAll(" {2,}", " ").trim();
            
        return noHTMLString;*/
    }

    /**
     * @return a label suitable for displaying in a list: if this component
     * has no label set, generate a unique name for it, and if the label has any newlines
     * in it, replace them with spaces.
     */
    public String getDisplayLabel() {
        if (hasLabel()) {
            try {
                return getLabel().replace('\n', ' ');
            } catch (Throwable t) {
                return getUniqueComponentTypeLabel() + "[" + t + "]";
            }
        } else
            return getUniqueComponentTypeLabel();
    }

    /** for debug */
    public static String tag(LWComponent c) {
        return c == null ? "" : c.getUniqueComponentTypeLabel();
    }

    public String getDiagnosticLabel() {
        if (hasLabel()) {
            return getUniqueComponentTypeLabel() + ": " + getLabel().replace('\n', ' ');
        } else
            return getUniqueComponentTypeLabel();
    }

    /** return a guaranteed unique name for this LWComponent */
    public String getUniqueComponentTypeLabel() {
        return getComponentTypeLabel() + " #" + getID();
    }

    /** return a type name for this LWComponent */
    public String getComponentTypeLabel() {
        final String name = getClass().getName();
        if (name.startsWith("tufts.vue.LW"))
            return name.substring(12);
        else if (name.startsWith("tufts.vue."))
            return name.substring(10);
        else
            return name;
    }

    String toName() {
        if (getLabel() == null)
            return getDisplayLabel();
        else
            return getComponentTypeLabel() + "[" + getLabel() + "]";
    }

    /** @deprecated
     * left in for (possible future) backward file compatibility
     * do nothing with this data anymore for now.
     **/
    public synchronized NodeFilter getNodeFilter() {
        // if the double-checked locking idiom was reliable in java, we'd use it here, but
        // since it's not, we synchronize this whole method.
        if (nodeFilter == NEEDS_NODE_FILTER) {
            //Util.printStackTrace("lazy create of node filter for " + this);
            nodeFilter = new NodeFilter();
        }
        return nodeFilter;
    }

    /** @deprecated -- for persistance */
    public void setXMLnodeFilter(NodeFilter nodeFilter) {
        this.nodeFilter = nodeFilter;
    }

    /** @deprecated -- return null if the node filter is empty, so we don't bother with entry in the save file */
    public NodeFilter getXMLnodeFilter() {
        if (mXMLRestoreUnderway) {
            // in case validation is on:
            return nodeFilter;
        } else if (nodeFilter == NEEDS_NODE_FILTER || (nodeFilter != null && nodeFilter.size() < 1)) {
            return null;
        } else
            return nodeFilter;
    }

    /** does this support a user editable label? */
    // TODO: resolve this with supportsProperty(LWKey.Label) (perhaps lose this method)
    public boolean supportsUserLabel() {
        return supportsProperty(LWKey.Label);
    }

    /** does this support user resizing? */
    // TODO: change these "supports" calls to an arbitrary property list
    // that could have arbitrary properties added to it by plugged-in non-standard tools
    public boolean supportsUserResize() {
        return false;
    }

    /** @return false: subclasses (e.g. containers), override to return true if allows children dragged in and out
     * by a user.
     */
    public boolean supportsChildren() {
        return false;
    }

    /** @Return true: subclasses (e.g. containers), override to return false if you never want this component
    reparented by users */
    // todo: handle via API that LWGroup can declare
    public boolean supportsReparenting() {
        return parent instanceof LWGroup == false;
    }

    /** @return true: by default, all objects can be selected with other objects at the same time */
    public boolean supportsMultiSelection() {
        return true;
    }

    /** @return false by default -- only containers can have slides */
    public boolean supportsSlide() {
        return false;
    }

    /** @return false by default -- override to initiate dupe and system drag */
    public boolean supportsCopyOnDrag() {
        return false;
    }

    /** @return true if we allow a link to the target, and the target allows a link to us.
     * Eventually we can use this to check ontology information.
     * @param target -- the target to check.  If null, tells is if this component allows
     * link to nothing / allows links at all.
     */
    public boolean canLinkTo(LWComponent target) {
        return canLinkToImpl(target) && (target == null || target.canLinkToImpl(this));
    }

    /** @return true -- subclass impl's can override */
    protected boolean canLinkToImpl(LWComponent target) {
        return hasFlag(Flag.LOCKED) == false;
    }

    public boolean hasLabel() {
        return this.label != null && this.label.length() > 0;
    }

    public String getNotes() {
        return this.notes;
    }

    public boolean hasNotes() {
        return this.notes != null && this.notes.length() > 0;
    }

    public boolean hasResource() {
        return this.resource != null;
    }

    public boolean hasLinks() {
        return mLinks != null && mLinks.size() > 0;
    }

    /*
    public String getMetaData()
    {
    return this.metaData;
    }
    public boolean hasMetaData()
    {
    return this.metaData != null;gajendracircle
    }
    */
    public boolean inPathway() {
        return mPathways != null && mPathways.size() > 0;
    }

    public boolean inVisiblePathway() {
        if (inPathway())
            for (LWPathway p : mPathways)
                if (p.isDrawn())
                    return true;
        return false;
    }

    /** Is component in the given pathway? */
    // rename onPathway?
    public boolean inPathway(LWPathway path) {
        if (mPathways == null || path == null)
            return false;

        for (LWPathway p : mPathways)
            if (p == path)
                return true;

        return false;
    }

    /** @return null if we're in more than one visible pathway, or the LWPathway we're on if it's the only visible one */
    public LWPathway getExclusiveVisiblePathway() {
        if (mPathways == null)
            return null;

        boolean foundOne = false;
        LWPathway singleVisible = null;
        for (LWPathway p : mPathways) {
            if (p.isDrawn()) {
                if (foundOne)
                    return null;
                foundOne = true;
                singleVisible = p;
            }
        }

        return singleVisible;
    }

    public List<LWPathway> getPathways() {
        return mPathways == null ? java.util.Collections.EMPTY_LIST : mPathways;
    }

    /**
     * @return true if this component is in a pathway that is
     * drawn with decorations (e.g., not a reveal-way)
     */
    public boolean inDrawnPathway() {
        if (mPathways == null)
            return false;

        for (LWPathway p : mPathways)
            if (p.isVisible() && !p.isRevealer())
                return true;

        return false;
    }

    public boolean hasEntries() {
        return mEntries != null && mEntries.size() > 0;
    }

    public int numEntries() {
        return mEntries == null ? 0 : mEntries.size();
    }

    public List<LWPathway.Entry> getEntries() {
        return mEntries;
    }

    protected void addEntryRef(LWPathway.Entry e) {
        if (mEntries == null) {
            mEntries = new ArrayList();
            mVisibleSlideIconIterator = new SlideIconIter();
        }
        if (!mEntries.contains(e))
            mEntries.add(e);
        addPathwayRef(e.pathway);
    }

    protected void removeEntryRef(LWPathway.Entry e) {
        if (mEntries == null) {
            Util.printStackTrace(this + "; no entries! can't remove: " + e);
            return;
        }
        if (!mEntries.remove(e))
            Util.printStackTrace(this + "; Warning: didn't contain entry " + e);
        removePathwayRef(e.pathway);
    }

    private void addPathwayRef(LWPathway p) {
        if (mPathways == null)
            mPathways = new ArrayList();
        if (!mPathways.contains(p)) {
            mPathways.add(p);
            // todo: too late, UNDELETING flag already cleared (call is from pathway on pathway undelete)
            // okay for now: re-layout on undo should be harmless, but generates lots
            // of needless location events that clog up event debugging when doing undo's
            if (!hasFlag(Flag.UNDELETING) && LWIcon.IconPref.getPathwayIconValue())
                layout();
        }
        //notify("pathway.add");
    }

    private void removePathwayRef(LWPathway p) {
        if (mPathways == null) {
            if (DEBUG.META)
                tufts.Util.printStackTrace("attempt to remove non-existent pathwayRef to " + p + " in " + this);
            return;
        }
        mPathways.remove(p);
        // clear any hidden bits that may be set as a result
        // of the membership in the pathway.
        for (HideCause cause : HideCause.values())
            if (cause.type == CAUSE_PATHWAY)
                clearHidden(cause);

        if (!hasFlag(Flag.DELETING) && LWIcon.IconPref.getPathwayIconValue()) {
            // todo: handle at higher level or have icon block listen for some event
            layout();
        }
        //notify("pathway.remove");
    }

    /** @deprecated - not really deprecated, but intended for persistance only */
    public java.awt.Dimension getXMLtextBox() {
        return null;
        // NOT CURRENTLY USED
        /*
        if (this.labelBox == null)
        return null;
        else
        return this.labelBox.getSize();
        */
    }

    /** @deprecated - not really deprecated, intended for persistance only */
    public void setXMLtextBox(java.awt.Dimension d) {
        //this.textSize = d;
    }

    /** for persistance */
    // todo: move all this XML handling stuff to a special castor property mapper,
    // presumably in conjunction with re-architecting the whole mapping style &
    // save mechanism.
    public String getXMLlabel() {
        return this.label;
        //return tufts.Util.encodeUTF(this.label);
    }

    /** for persistance */
    public void setXMLlabel(String text) {
        setLabel(unEscapeNewlines(text));
        //this.label = unEscapeNewlines(text);
        //getLabelBox().setText(this.label);
        // we want to make sure layout() is not called,
        // and currently there's no need to do notify's during init.
    }

    /** for persistance */
    public String getXMLnotes() {
        //return this.notes;
        // TODO: can escape newlines new with &#xa; and tab with &#x9;
        return escapeWhitespace(this.notes);
    }

    /** for persistance -- gets called by castor after it reads in XML */
    public void setXMLnotes(String text) {
        setNotes(decodeCastorMultiLineText(text));
    }

    protected static String decodeCastorMultiLineText(String text) {

        // If castor xml indent was on when save was done
        // (org.exolab.castor.indent=true in castor.properties
        // somewhere in the classpath, to make the XML more human
        // readable) it will break up elements like: <note>many chars
        // of text...</note> with newlines and whitespaces to indent
        // the new lines in the XML -- however, on reading them back
        // in, it puts this white space into the string you saved!  So
        // when we save we're sure to manually encode newlines and
        // runs of white space, so when we get here, if see any actual
        // newlines followed by runs of white space, we know to trash
        // them because it was castor formatting fluff.  (btw, this
        // isn't a problem for labels because they're XML attributes,
        // not elements, which are quoted).

        // Update: As of castor 0.9.7, this no longer appears true
        // (it doesn't indent new text lines with white space
        // even after wrapping them), but we still need this
        // here to deal with old save files.

        text = text.replaceAll("\n[ \t]*%nl;", "%nl;");
        text = text.replaceAll("\n[ \t]*", " ");
        return unEscapeWhitespace(text);
    }

    // FYI, this is no longer needed for castor XML attributes, as
    // of version 0.9.7 it automatically encodes & preserves them.
    // Note that this is still NOT true for XML elements.
    private static String escapeNewlines(String text) {
        if (text == null)
            return null;
        else {
            return text.replaceAll("[\n\r]", "%nl;");
        }
    }

    private static String unEscapeNewlines(String text) {
        if (text == null)
            return null;
        else {
            return text.replaceAll("%nl;", "\n");
        }

    }

    public static String escapeWhitespace(String text) {
        if (text == null)
            return null;
        else {
            text = text.replaceAll("%", "%pct;");
            // replace all instances of two spaces with space+%sp;
            // to break them up (and thus we wont lose space runs)
            text = text.replaceAll("  ", " %sp;");
            text = text.replaceAll("\t", "%tab;");
            return escapeNewlines(text);
        }
    }

    public static String unEscapeWhitespace(String text) {
        if (text == null)
            return null;
        else {
            text = unEscapeNewlines(text);
            text = text.replaceAll("%tab;", "\t");
            text = text.replaceAll("%sp;", " ");
            return text.replaceAll("%pct;", "%");
        }
    }

    private static final Object LAYOUT_DEFAULT = "default";

    /** Layout this component and all children, if any.  Normally this would only be called on an
     * LWMap, but in some cases, any component might effectively be "at the top level" while in an
     * intermediate state, such as components in a cut-buffer before they've been pasted out to a
     * map. */
    public void layoutAll(Object triggerKey) {
        layout(triggerKey);
    }

    /**
     * If this component supports special layout for it's children,
     * or resizes based on font, label, etc, do it here.
     */
    public final void layout() {
        if (mXMLRestoreUnderway == false)
            layout(LAYOUT_DEFAULT);
    }

    final void layout(Object triggerKey) {
        if (mXMLRestoreUnderway == false) {

            layoutImpl(triggerKey);

            if (triggerKey == LWMap.NODE_INIT_LAYOUT) {
                validateInitialValues();
                layoutSlideIcons(null);
            } else if (triggerKey == LWMap.LINK_INIT_LAYOUT) {
                validateInitialValues();
            }
            // need a reshape/reshapeImpl for this (size/location changes)
            //if (mSlideIconBounds != null)
            //    mSlideIconBounds.x = Float.NaN; // invalidate
        }

        updateConnectedLinks(null);
    }

    protected boolean validateInitialValues() {
        boolean bad = false;

        // Note that if ANY component in the map has a NaN coordinate or dimension,
        // it can put the AWT graphics routines into an unrecoverable state that
        // may prevent the entire map from drawing sanely or at all.

        if (Float.isNaN(x)) {
            Log.warn("bad x " + this);
            x = 0;
            bad = true;
        }
        if (Float.isNaN(y)) {
            Log.warn("bad y " + this);
            y = 0;
            bad = true;
        }
        if (Float.isNaN(width)) {
            Log.warn("bad width " + this);
            width = 0;
            bad = true;
        }
        if (Float.isNaN(height)) {
            Log.warn("bad height " + this);
            height = 0;
            bad = true;
        }

        if (supportsProperty(KEY_FontSize) && mFontSize.get() < 1) {
            Log.warn("bad font size " + mFontSize.get() + " " + this);
            mFontSize.take(1); // don't risk triggering an event at a bad time
            bad = true;
        }

        return bad;
    }

    protected void layoutImpl(Object triggerKey) {
    }

    /** @return true: default is always autoSized */
    //public boolean isAutoSized() { return true; }
    public boolean isAutoSized() {
        return false;
    } // LAYOUT-NEW

    /** do nothing: default is always autoSized */
    public void setAutoSized(boolean t) {
    }

    public void setToNaturalSize() {
        setAutoSized(false);
    }

    private static boolean eq(Object a, Object b) {
        return a == b || (a != null && a.equals(b));
    }

    public boolean isTransparent() {
        return mFillColor.isTransparent();
    }

    public boolean isTranslucent() {
        return mFillColor.isTranslucent();
    }

    /**
     * Color to use at draw time. LWNode overrides to provide darkening of children.
     * We also use this for the background color in active on-map text edits.
     */
    public Color getRenderFillColor(DrawContext dc) {
        if (mFillColor.isTransparent()) {
            if (dc != null && dc.focal == this) {
                //System.out.println("     DC FILL: " + dc.getFill() + " " + this);
                return dc.getBackgroundFill();
            } else if (parent != null) {
                //System.out.println(" PARENT FILL: " + parent.getRenderFillColor(dc) + " " + this);
                return parent.getRenderFillColor(dc);
            }
        }
        //System.out.println("DEFAULT FILL: " + mFillColor.get() + " " + this);
        return mFillColor.get();
    }

    public Color getFinalFillColor(DrawContext dc) {
        if (mFillColor.isTransparent()) {
            Color c = null;
            if (getParent() != null)
                return getParent().getFinalFillColor(dc);
            else if (dc != null)
                return dc.getBackgroundFill();
            else
                return null;
        } else
            return getFillColor();
    }

    public static Color getContrastColor(Color c) {
        if (c != null) {
            if (c.equals(Color.black))
                return Color.darkGray;
            else
                return c.darker();
        } else {
            return DEBUG.BOXES ? Color.red : Color.gray;
        }
    }

    public Color getContrastStrokeColor(DrawContext dc) {
        final Color renderFill = getRenderFillColor(dc);
        if (renderFill != null && !isTransparent()) {
            return getContrastColor(renderFill);
        } else {
            // transparent fill: just use stroke color
            return getStrokeColor();
            // transparent fill: base on stroke color
            //return getStrokeColor().brighter();
        }
    }

    //private LWPathway lastPriorit;

    public Color getPriorityPathwayColor(DrawContext dc) {
        final LWPathway exclusive = getExclusiveVisiblePathway();
        if (exclusive != null)
            return exclusive.getColor();
        else if (inPathway(VUE.getActivePathway()) && VUE.getActivePathway().isDrawn())
            return VUE.getActivePathway().getColor();
        else
            return null;
        //return getRenderFillColor(dc);
    }

    void takeFillColor(Color color) {
        mFillColor.take(color);
    }

    // We still need these standard style setters & getters for backward compat
    // with all sorts of old code, and espcially for persistance (the castor
    // mapping, which refers to these methods)

    public float getStrokeWidth() {
        return mStrokeWidth.get();
    }

    public void setStrokeWidth(float w) {
        mStrokeWidth.set(w);
    }

    /** @return null for SOLID (ordinal 0, the default, as for old save files), or otherwise, the ordinal of the style enum
     * Castor will not bother to generate the attribute/element when it's value is null. */
    public Integer getXMLstrokeStyle() {
        int code = mStrokeStyle.get().ordinal();
        return code == 0 ? null : code;
    }

    public void setXMLstrokeStyle(Integer ordinal) {
        // todo: have the Key class process enum's generically, caching the results of Class<? extends Enum>.getEnumConstants()
        for (StrokeStyle ss : StrokeStyle.values()) {
            if (ss.ordinal() == ordinal) {
                mStrokeStyle.set(ss);
                break;
            }
        }
    }

    public Color getFillColor() {
        return mFillColor.get();
    }

    public void setFillColor(Color c) {
        mFillColor.set(c);
    }

    public String getXMLfillColor() {
        return mFillColor.asString();
    }

    public void setXMLfillColor(String xml) {
        mFillColor.setFromString(xml);
    }

    public Color getTextColor() {
        return mTextColor.get();
    }

    public void setTextColor(Color c) {
        mTextColor.set(c);
    }

    public String getXMLtextColor() {
        return mTextColor.asString();
    }

    public void setXMLtextColor(String xml) {
        mTextColor.setFromString(xml);
    }

    public Color getStrokeColor() {
        return mStrokeColor.get();
    }

    public void setStrokeColor(Color c) {
        mStrokeColor.set(c);
    }

    public String getXMLstrokeColor() {
        return mStrokeColor.asString();
    }

    public void setXMLstrokeColor(String xml) {
        mStrokeColor.setFromString(xml);
    }

    public Font getFont() {
        return mFont.get();
    }

    public void setFont(Font font) {
        mFont.set(font);
    }

    public String getXMLfont() {
        return mFont.asString();
    }

    public void setXMLfont(String xml) {
        mFont.setFromString(xml);
    }

    /**
     * The first time a TextBox is created for edit, it may not have been laid out
     * by it's parent, which is where it normally gets it's location.  This
     * initializes the location of the TextBox for first usage.  The default
     * impl here centers the TextBox in the LWComponent.
     */
    public void initTextBoxLocation(TextBox textBox) {
        textBox.setBoxCenter(getWidth() / 2, getHeight() / 2);
    }

    public final LWContainer getParent() {
        return this.parent;
    }

    /** @return what layer we're inside */
    public LWMap.Layer getLayer() {
        if (parent == null)
            return null;
        else if (parent instanceof LWMap.Layer)
            return (LWMap.Layer) parent;
        else
            return parent.getLayer();
    }

    /**
     * for castor persistance
     * @return null if we are not a child of a layer, or the layer if it's our immediate parent  */
    public LWMap.Layer getPersistLayer() {
        if (parent instanceof LWMap.Layer)
            return (LWMap.Layer) parent;
        else
            return null;
    }

    /** for castor persistance */
    public void setPersistLayer(LWMap.Layer layer) {
        //if (!VUE.VUE3_LAYERS) return;
        setParent(layer);
    }

    public int getDepth() {
        if (parent == null)
            return 0;
        else
            return parent.getDepth() + 1;
    }

    public int getIndex() {
        if (parent == null)
            return -1;
        else
            return parent.indexOf(this);
    }

    protected void setParent(LWContainer newParent) {

        if (DEBUG.UNDO)
            System.err.println("*** SET-PARENT: " + newParent + " for " + this);

        //final boolean linkNotify = (!mXMLRestoreUnderway && parent != null);
        if (parent == newParent) {
            // This is normal.
            // (e.g., one case: during undo of reparenting operations)
            //if (DEBUG.Enabled) Util.printStackTrace("redundant set-parent in " + this + "; parent=" + newParent);
            return;
        }

        if (newParent.hasAncestor(this)) {
            Util.printStackTrace("ATTEMPTED PARENT LOOP " + this + " can't make a child our parent: " + newParent);
            return;
        }

        //-----------------------------------------------------------------------------
        // We want to make sure schema references are current every time a node is put
        // into to the user space (may be seen or edited by a user).  This includes new
        // nodes to the user space, as well as pre-existing nodes being returned to the
        // user space (from an undo queue or cut buffer).
        //
        // So this handles updating the schema reference during restore, during undo,
        // and for paste operations of nodes that may have been in the cut/copy buffer.
        // The reason we need to do this is that a data source may have been loaded
        // while this nodes were out of user space, creating a new live schema that one
        // of these nodes may now want to be pointing to.
        //
        // TODO: at the moment, this is a bit overkill, as it will also be run every
        // time a node is reparented at all, tho the operation is idempotent so we can
        // live with it.  A combination of calling this in restoreToModel (for undo),
        // and only calling this here if parent was previously null (persistance,
        // cut/paste), should handle that.

        if (!mXMLRestoreUnderway) {
            // handled specially during restored
            validateSchemaReference();
        }
        //-----------------------------------------------------------------------------

        parent = newParent;

        //layout(); // for preference change updates

        //         if (linkNotify && mLinks.size() > 0)
        //             for (LWLink link : mLinks)
        //                 link.notifyEndpointReparented(this);
    }

    //protected void reparentNotify(LWContainer parent) {}

    /**
     * for now (2007-11-30) just records sync source in case we want to use it later,
     * but does not set up data synchronization (as per Melanie 2007-11-14)
     */
    public void setSyncSource(LWComponent source) {

        mSyncSource = source;

        if (true)
            return; // all dynamic data syncing disabled for now as per Melanie -- SMF 2007-11-14

        if (mSyncClients != null) {
            out("blowing away sync clients on syncSource set");
            // just in case
            mSyncClients.clear();
            mSyncClients = null;
        }
        mSyncSource.addSyncClient(this);
    }

    public LWComponent getSyncSource() {
        return mSyncSource;
    }

    protected void addSyncClient(LWComponent c) {
        if (mSyncClients == null)
            mSyncClients = new HashSet();
        mSyncClients.add(c);
    }

    /** set the given component as the style for this object, applying it's style properties to us
     * Note: this will force the STYLE bit to be set on parentStyle if it already isn't set
     */
    public void setStyle(LWComponent parentStyle) {
        if (DEBUG.STYLE)
            out("setStyle " + parentStyle);
        takeStyle(parentStyle);
        if (parentStyle != null) {
            parentStyle.setFlag(Flag.STYLE);
            if (!mXMLRestoreUnderway) // we can skip the copy during restore
                copyStyle(parentStyle);
        }
    }

    void takeStyle(LWComponent parentStyle) {
        mParentStyle = parentStyle;
    }

    /** for castor persist */
    public LWComponent getStyle() {
        return mParentStyle;
    }

    public boolean isStyle() {
        return hasFlag(Flag.STYLE);
        //return isStyle;
    }

    /** @return Boolean.TRUE if this component is serving as an active style for other objects, null otherwise */
    public Boolean getPersistIsStyle() {
        return isStyle() ? Boolean.TRUE : null;
    }

    public void setPersistIsStyle(Boolean b) {
        setFlag(Flag.STYLE, b.booleanValue());

    }

    /** @return Boolean.TRUE if this component has marked as having the special "slide" style, null otherwise */
    public Boolean getPersistIsSlideStyled() {
        return hasFlag(Flag.SLIDE_STYLE) ? Boolean.TRUE : null;
    }

    public void setPersistIsSlideStyled(Boolean b) {
        setFlag(Flag.SLIDE_STYLE, b.booleanValue());

    }

    /** @deprecated: tmp back compat only */
    public void setParentStyle(LWComponent c) {
        setStyle(c);
    }

    /** @deprecated: tmp back compat only */
    public Boolean getPersistIsStyleParent() {
        return null;
    }

    /** @deprecated: tmp back compat only */
    public void setPersistIsStyleParent(Boolean b) {
        setPersistIsStyle(b);
    }

    /** @deprecated: tmp back compat only */
    public LWComponent getParentStyle() {
        return null;
    }

    /**
     * @return 0 by default
     * the pick depth (in PickContext) must be >= what this returns for descdents of this component
     * be picked (selected, etc).  Mostly meaningful when an LWContainer subclass implements
     * and returns something > 0, tho a single component could use this to become a "background" item.
     * You can think of this as establishing a "wall" in the depth hierarchy, past which pick
     * traversals will not descend unless given a high enough pickDepth to jump the wall.
     */
    public int getPickLevel() {
        return 0;
    }

    //private static LWComponent ProxySlideComponent = new LWComponent("<global-slide-proxy>");

    /** return the component to be picked if we're picked: e.g., may return null if you only want children picked, and not the parent */
    protected LWComponent defaultPick(PickContext pc) {
        // If we're dropping something, never allow us to be picked
        // if we're a descendent of what's being dropped! (would be a parent/child loop)
        if (pc.dropping != null && pc.dropping instanceof LWContainer && hasAncestor((LWComponent) pc.dropping))
            return null;
        //         else if (isDrawingSlideIcon() && getMapSlideIconBounds().contains(pc.x, pc.y)) {
        //             return getEntryToDisplay().getSlide();
        //         }
        else
            return defaultPickImpl(pc);
    }

    protected LWComponent defaultPickImpl(PickContext pc) {
        return this;
    }

    /** If PickContext.dropping is a LWComponent, return parent (as we can't take children),
     * otherwise return self
     */
    protected LWComponent defaultDropTarget(PickContext pc) {
        // TODO: if this is a system drag, dropping is null,
        // and we don't know if this is a localDrop of a node,
        // or a drop of a resource, so, for example, links
        // will incorrectly get targeted for local node system drops.
        // (tho when dropped, it'll still just get added to the parent).
        if (pc.dropping instanceof LWComponent)
            return getParent();
        else
            return this;
    }

    public boolean isOrphan() {
        return this.parent == null;
    }

    public boolean atTopLevel() {
        return parent != null && parent.isTopLevel();
    }

    public boolean isTopLevel() {
        return false;
    }

    public boolean hasChildren() {
        return false;
    }

    /** @return false; overriding impl's should return true if this
     * component has children, and those children are always fully contained
     * within the bounds of the parent */
    public boolean fullyContainsChildren() {
        return false;
    }

    public boolean hasChild(LWComponent c) {
        return false;
    }

    public boolean isLaidOut() {
        return isManagedLocation();
    }

    public boolean isManagedLocation() {
        return (parent != null && parent.isManagingChildLocations()) || (isSelected() && isAncestorSelected());
    }

    public boolean isManagingChildLocations() {
        return false;
    }

    /** @return true - A single component always "has content" -- subclasses override to provide varying semantics */
    public boolean hasContent() {
        return true;
    }

    /** @return false by default */
    public boolean isTextNode() {
        return false;
    }

    /** @return false by default */
    public boolean isLikelyTextNode() {
        return false;
    }

    /** @return false by default */
    public boolean isExternalResourceLinkForPresentations() {
        return false;
    }

    public final void addChild(LWComponent c) {
        addChildren(Collections.singletonList(c), ADD_DEFAULT);
    }

    public final void dropChild(LWComponent c) {
        addChildren(Collections.singletonList(c), ADD_DROP);
    }

    public final void pasteChild(LWComponent c) {
        addChildren(Collections.singletonList(c), ADD_PASTE);
    }

    public final void addChildren(List<? extends LWComponent> children) {
        addChildren(children, ADD_DEFAULT);
    }

    /**
     * Although unsupported on LWComponents (must be an LWContainer subclass to support children),
     * this method appears here for typing convenience and debug.  If a non LWContainer subclass
     * calls this, it's a no-op, and a diagnostic stack trace is dumped to the console.
     */
    public void addChildren(Collection<? extends LWComponent> children, Object context) {
        Util.printStackTrace(
                this + ": can't take children; ignored: " + Util.tags(children) + "; context=" + context);
    }

    /** return true if this component is only a "virtual" member of the map:
     * It may report that it's parent is in the map, but that parent doesn't
     * list the component as a child (so it will never be drawn or traversed
     * when handling the entire map).
     */
    public boolean isMapVirtual() {
        return getParent() == null || !getParent().hasChild(this);
    }

    /** @return 0 -- override to support children */
    public int numChildren() {
        return 0;
    }

    /** @deprecated - use getChildren */
    public java.util.List<LWComponent> getChildList() {
        return NO_CHILDREN;
    }

    public java.util.List<LWComponent> getChildren() {
        return NO_CHILDREN;
    }

    /** @return 0 -- override for container impls */
    public int getDescendentCount() {
        return 0;
    }

    /** @return: always null */
    public LWComponent getChild(int index) {
        return null;
    }

    public boolean hasPicks() {
        return (hasChildren() && !isCollapsed()) || hasEntries();
    }

    /** ordered for drawing and picking */
    private final class SlideIconIter implements Iterator<LWSlide>, Iterable<LWSlide> {
        int nextIndex;
        LWSlide nextSlide;
        LWSlide onTop;
        DrawContext dc;
        LWPathway activePathway;
        LWPathway.Entry activeEntry;

        private SlideIconIter() {
            //System.out.println("\nSlideIter, entries=" + mEntries.size());
            advance();
        }

        private void advance() {
            //out("advance; nextIndex =" + nextIndex);
            nextSlide = null;
            int i = nextIndex;
            for (; i < mEntries.size(); i++) {
                final LWPathway.Entry e = mEntries.get(i);
                //out("inspecting index " + i + " " + e);
                if (e.hasVisibleSlide()) {
                    final LWSlide slide = e.getSlide();
                    if (activePathway == null && slide.isSelected()) {
                        onTop = slide;
                        //} else if (slide.getEntry().pathway == activePathway) {
                    } else if (slide.getPathwayEntry() == activeEntry) {
                        onTop = slide;
                    } else {
                        nextSlide = slide;
                        break;
                    }
                }
            }

            nextIndex = i + 1;

            // if we're at the end, provide the selected (if there was one)
            if (nextSlide == null) {
                nextSlide = onTop;
                onTop = null;
            }
        }

        public boolean hasNext() {
            final boolean t = (nextSlide != null);
            //out("hasNext " + t);
            if (nextIndex > 100) {
                Util.printStackTrace("loop");
                return false;
            }
            return t;
            //return nextSlide != null;
        }

        public LWSlide next() {
            if (nextSlide == null) {
                if (DEBUG.Enabled)
                    Util.printStackTrace(this + "; next at end of SlideIconIter; entries=" + mEntries);
                return null;
            }
            final LWSlide s = nextSlide;
            advance();
            //out("return " + s);
            return s;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public Iterator<LWSlide> iterator() {
            // reset when re-used
            nextIndex = 0;
            nextSlide = null;
            onTop = null;
            if (dc != null && dc.isPresenting()) {
                activePathway = VUE.getActivePathway();
                //activeEntry = VUE.getActiveEntry();
            } else {
                activePathway = null;
                //activeEntry = null;
            }
            activeEntry = VUE.getActiveEntry();
            advance();
            return this;
        }
    }

    /** @return the slides for drawing as slide icons in the current picking and drawing order */
    private final Iterable<LWSlide> seenSlideIcons(DrawContext dc) {
        //         if (mEntries == null || mEntries.size() == 0) {
        //             // this is sort of overkill, as we shouldn't even be calling this if hasEntries is false
        //             return Util.EmptyIterable;
        //         } else
        if (mEntries.size() == 1) {
            final LWPathway.Entry e = mEntries.get(0);
            if (e.hasVisibleSlide())
                return Util.iterable(e.getSlide());
            else
                return Util.EmptyIterable;
        } else {
            mVisibleSlideIconIterator.dc = dc;
            return mVisibleSlideIconIterator;
        }
        //return new SlideIter();
    }

    /**
     * @return a list, to be traversed in reverse order.  If a new list needs to be constructed,
     * it will dumped into stored, which will be returned.  Otherwise, an internal list may be returned.
     */
    public List<LWComponent> getPickList(PickContext pc, List<LWComponent> stored) {
        Iterable<LWSlide> seenSlides = null;
        if (pc.root != this && hasEntries() && (seenSlides = seenSlideIcons(pc.dc)) != Util.EmptyIterable) {
            // todo performance: see LWTraversal for comments: change impl to return a ReverseListIterator, etc.a
            stored.clear();
            if (!isCollapsed())
                stored.addAll(getChildren());
            for (LWSlide s : seenSlides)
                stored.add(s);
            return stored;
        } else
            return (List) getChildren();
    }

    public java.util.Iterator<? extends LWComponent> getChildIterator() {
        return EmptyIterator;
    }

    /** The default is to get all ChildKind.PROPER children (backward compatability)
     * This impl always returns an empty list.  Subclasses that can have proper
     * children provide the impl for that
     */
    public Collection<LWComponent> getAllDescendents() {
        // Default is only CHILD_PROPER, and by definition,
        // LWComponents have no proper children.
        // return getAllDescendents(CHILD_PROPER);
        return java.util.Collections.EMPTY_LIST;
    }

    public Collection<LWComponent> getAllDescendents(final ChildKind kind) {
        if (kind == ChildKind.PROPER)
            return java.util.Collections.EMPTY_LIST;
        else
            return getAllDescendents(kind, new java.util.ArrayList(), Order.TREE);
    }

    public final Collection<LWComponent> getAllDescendents(final ChildKind kind,
            final Collection<LWComponent> bag) {
        return getAllDescendents(kind, bag, Order.TREE);
    }

    /** @return bag -- a noop -- this is meant to be overriden */
    public Collection<LWComponent> getAllDescendents(final ChildKind kind, final Collection<LWComponent> bag,
            Order order) {
        return bag;
    }

    /** @return getDescendentsOfType(ChildKind.PROPER, clazz) */
    public <A extends LWComponent> Iterable<A> getDescendentsOfType(Class<A> clazz) {
        return getDescendentsOfType(ChildKind.PROPER, clazz);
    }

    /** @see LWContainer (this impl EmptyIterator) */
    public <A extends LWComponent> Iterable<A> getDescendentsOfType(ChildKind kind, Class<A> clazz) {
        return EmptyIterable;
    }

    /** @see LWContainer (this impl EmptyIterator) */
    public <A extends LWComponent> Iterable<A> getChildrenOfType(Class<A> clazz) {
        return EmptyIterable;
    }

    /** @see LWContainer (this impl EmptyIterator) */
    public Iterator<LWNode> getAllNodesIterator() {
        return EmptyIterator;
    }

    /** @see LWContainer (this impl EmptyIterator) */
    public Iterator<LWLink> getAllLinksIterator() {
        return EmptyIterator;
    }

    /** @see LWContainer (this impl EmptyIterator) */
    public Iterator<LWNode> getChildNodeIterator() {
        return EmptyIterator;
    }

    /** @see LWContainer (this impl EmptyIterator) */
    public Iterator<LWLink> getChildLinkIterator() {
        return EmptyIterator;
    }

    /** for tracking who's linked to us */
    void addLinkRef(LWLink link) {
        if (DEBUG.UNDO)
            out(this + " adding link ref to " + link);
        if (mLinks == null)
            mLinks = new ArrayList(4);
        if (mLinks.contains(link)) {
            //tufts.Util.printStackTrace("addLinkRef: " + this + " already contains " + link);
            if (DEBUG.Enabled)
                Log.warn("addLinkRef: " + this + " already contains " + link);
        } else {
            mLinks.add(link);
            notify(LWKey.LinkAdded, link); // informational only event
        }
    }

    /** for tracking who's linked to us */
    void removeLinkRef(LWLink link) {
        if (DEBUG.EVENTS || DEBUG.UNDO)
            out("removeLinkRef: " + link);
        if (mLinks == null || !mLinks.remove(link))
            Log.warn("removeLinkRef: " + this + " didn't contain " + link);
        clearHidden(HideCause.PRUNE); // todo: ONLY clear this if we were pruned by the given link!
        notify(LWKey.LinkRemoved, link); // informational only event
    }

    /** @return us all the links who have us as one of their endpoints */
    public List<LWLink> getLinks() {
        return mLinks == null ? Collections.EMPTY_LIST : mLinks;
    }

    public List<LWLink> getIncomingLinks() {
        // Note: it's inefficient to call getIncomingLinks() AND getOutgoingLinks() because bi-directional
        // links will be returned by both and you'll double-process them.  If you want both incoming and
        // outgoing links just call getLinks() and there won't be duplicates.
        List<LWLink> incomingLinks = new ArrayList<LWLink>();

        for (LWLink link : getLinks()) {
            int arrowState = link.getArrowState();
            boolean arrowHead = arrowState == LWLink.ARROW_HEAD, arrowTail = arrowState == LWLink.ARROW_TAIL,
                    arrowNonDirectional = arrowState == LWLink.ARROW_BOTH || arrowState == LWLink.ARROW_NONE;

            if (this == link.getHead()) {
                if (arrowHead || arrowNonDirectional) {
                    incomingLinks.add(link);
                }
            } else if (this == link.getTail()) {
                if (arrowTail || arrowNonDirectional) {
                    incomingLinks.add(link);
                }
            }
        }

        return incomingLinks;
    }

    public List<LWLink> getOutgoingLinks() {
        // Note: it's inefficient to call getIncomingLinks() AND getOutgoingLinks() because bi-directional
        // links will be returned by both and you'll double-process them.  If you want both incoming and
        // outgoing links just call getLinks() and there won't be duplicates.
        List<LWLink> outgoingLinks = new ArrayList<LWLink>();

        for (LWLink link : getLinks()) {
            int arrowState = link.getArrowState();
            boolean arrowHead = arrowState == LWLink.ARROW_HEAD, arrowTail = arrowState == LWLink.ARROW_TAIL,
                    arrowNonDirectional = arrowState == LWLink.ARROW_BOTH || arrowState == LWLink.ARROW_NONE;

            if (this == link.getHead()) {
                if (arrowTail || arrowNonDirectional) {
                    outgoingLinks.add(link);
                }
            } else if (this == link.getTail()) {
                if (arrowHead || arrowNonDirectional) {
                    outgoingLinks.add(link);
                }
            }
        }

        return outgoingLinks;
    }

    //     /** get all links to us + to any descendents */
    //     public List getAllLinks() {
    //         return getLinks();
    //     }

    /** @return all components at the far end of any links that are connected to us
     * Note that if this is called on an LWLink, it will only return objects linking to us,
     * not the objects at our endpoints.
     **/
    public Collection<LWComponent> getLinked() {
        // default uses a set, in case there are multiple links to the same endpoint
        return getLinked(new HashSet(getLinks().size()));
    }

    protected Collection<LWComponent> getLinked(Collection<LWComponent> bag) {
        return getLinked(getLinks(), bag);
    }

    /** given the set of links, which should be sub-set of our own links, return a set containting the far endpoints */
    public Collection<LWComponent> getLinked(Collection<LWLink> links, Collection<LWComponent> bag) {
        for (LWLink link : links) {
            final LWComponent head = link.getHead();
            final LWComponent tail = link.getTail();
            if (head == tail)
                ; // ignore circular links
            else if (head == this && tail != null)
                bag.add(tail);
            else if (tail == this && head != null)
                bag.add(head);
        }
        return bag;
    }

    /** @return all nodes that are *probable* clustering targets for this node.  Currently,
     * this usually means most or all of the nodes this node is linked to */
    public Collection<LWComponent> getClustered() {
        final List<LWLink> links = getLinks();
        int dcl = 0;
        for (LWLink l : links)
            if (l.isDataCountLink())
                dcl++;

        if (dcl == 0 || dcl == links.size())
            return getLinked(links, new HashSet(links.size()));
        else
            return getLinked(getPriorityDataLinks(getLinks()), new HashSet(dcl));
    }

    /** @return list will only contain LWLink */
    private static Collection<LWLink> getPriorityDataLinks(Collection<LWLink> links) {
        // What we really need here is a way to tell the significance of the
        // count-link itself.  E.g., if we find a count-link to an endpoint that has
        // NO OTHER count-links to it all, that's an easy case -- we want to cluster
        // that item near us.  But if that endpoint has any OTHER count links,
        // either we do NOT want it clustering near us, or we might want to go so
        // far as to find the count link with the highest count and use that, etc.
        // In any case, that's all getting quite complicated to add now.  We'd have
        // to fetch all the endpoints and inspect them in conjunction with the
        // links.

        // For now, we just ignore count-links entirely, even tho they
        // produce FANTASTIC clustering in certian special cases.
        // (e.g., all the countries clustering around a region they're in)

        //final Collection<LWLink> countDataLinks = new ArrayList();
        final Collection<LWLink> normalDataLinks = new ArrayList(links.size());

        // todo performance: just produce integer counts, then construct the
        // lists in the rare case they're needed
        for (LWLink l : links) {
            if (l.isDataCountLink())
                ;//countDataLinks.add(l);
            else
                normalDataLinks.add(l);
        }

        return normalDataLinks;

        //             if (countDataLinks.size() == 1 && otherDataLinks.size() > 0) {
        //                 return otherDataLinks;
        //             } else {
        //                 return links;
        //             }
    }

    /** @return all components directly connected to this one: for most components, this
     * is just all the LWLink's that connect to us.  For LWLinks, it's mainly it's endpoints,rg
     * plus also any LWLink that may be directly connected to the link itself
     */
    public Collection<? extends LWComponent> getConnected() {
        return Collections.unmodifiableList(getLinks());
    }

    /** @return a list of every component connected to this one via links, including the links themselves */
    public Collection<LWComponent> getLinkChain() {
        return getLinkChain(new HashSet(), null);
    }

    /**
     * @return a list of every component connected to this one via links, including the links themselves
     * @param bag - the collection to store the results in.  Any component already in the bag will not
     * have it's outbound links followed -- this provides inherent loop protection.
     * Note that if this collection isn't a Set of some kind, components will appear in the bag more than once.
     * (Once for every time they were visited).
     */
    public final Collection<LWComponent> getLinkChain(Collection bag, LWComponent backstop) {
        if (DEBUG.LINK)
            Log.debug("getLinkChain: " + this);
        return getLinkChainImpl(bag, backstop, 0);
    }

    private static void tabout(int depth, String s) {
        for (int x = 0; x < depth; x++)
            System.out.print("    ");
        System.out.println(s);
    }

    private Collection<LWComponent> getLinkChainImpl(Collection bag, LWComponent backstop, int depth) {
        if (!bag.add(this)) {
            // already added to the set with all connections -- don't process again
            if (DEBUG.LINK)
                tabout(depth, "    (dupe)" + this);
            return bag;
        }

        if (DEBUG.LINK)
            tabout(depth, "  DESCEND>" + this);

        for (LWComponent c : getConnected()) {
            depth++; // for debug
            try {
                if (c != backstop) {
                    if (c instanceof LWLink && ((LWLink) c).isPrunedBelow(this)) {
                        bag.add(c);
                        if (DEBUG.LINK)
                            tabout(depth, "  (pruned)" + c); // note: could also be a dupe at this point
                    } else {
                        if (DEBUG.LINK)
                            tabout(depth, "  include>" + c);
                        c.getLinkChainImpl(bag, backstop, depth);
                    }
                } else {
                    // loop-prevention: never traverse through the backstop,
                    // otherwise the entire map could wind up pruned, such that
                    // nothing is visible
                    if (DEBUG.LINK)
                        tabout(depth, "(BACKSTOP)" + c);
                }
            } catch (Throwable t) {
                Log.error("processing " + c + " at depth " + depth, t);
            } finally {
                depth--; // for debug
            }
        }

        return bag;
    }

    public Rectangle2D.Float getFanBounds() {
        return getFanBounds(null);

    }

    /** @return the union of the bounds of the current component, all connected links, and all far endpoints
     * of those links.
     */
    public Rectangle2D.Float getFanBounds(Rectangle2D.Float rect) {
        if (rect == null)
            rect = getMapBounds();
        else
            rect.setRect(getMapBounds());

        for (LWLink link : getLinks()) {
            final LWComponent head = link.getHead();
            final LWComponent tail = link.getTail();

            rect.add(link.getPaintBounds());

            if (head != this) {
                if (head != null)
                    rect.add(head.getPaintBounds());
            } else if (tail != this) {
                if (tail != null)
                    rect.add(tail.getPaintBounds());
            }
        }
        return rect;
    }

    public Rectangle2D.Float getCenteredFanBounds() {
        return expandToCenteredBounds(getFanBounds());
    }

    /** get bounds that are centered on this node that fully include the given bounds */
    public Rectangle2D.Float expandToCenteredBounds(Rectangle2D.Float r) {
        // expand the given rectangle in all directions such that the distance
        // from our center point of this component to each edge is the same.

        final float cx = getMapCenterX();
        final float cy = getMapCenterY();

        final float topDiff = cy - r.y;
        final float botDiff = (r.y + r.height) - cy;
        final float leftDiff = cx - r.x;
        final float rightDiff = (r.x + r.width) - cx;

        if (topDiff > botDiff) {
            // expand below us
            r.height = topDiff * 2;
        } else if (botDiff > topDiff) {
            // expand above us
            r.y = cy - botDiff;
            r.height = botDiff * 2;
        }
        if (leftDiff > rightDiff) {
            // expand to the right
            r.width = leftDiff * 2;
        } else if (rightDiff > leftDiff) {
            // expand to the left
            r.x = cx - rightDiff;
            r.width = rightDiff * 2;
        }

        return r;
    }

    /*
     * Return an iterator over all link endpoints,
     * which will all be instances of LWComponent.
     * If this is a LWLink, it should include it's
     * own endpoints in the list.
        
    public java.util.Iterator<LWComponent> getLinkEndpointsIterator()
    {
    return
        new java.util.Iterator<LWComponent>() {
            java.util.Iterator i = getLinkRefs().iterator();
            public boolean hasNext() {return i.hasNext();}
      public LWComponent next()
            {
                LWLink l = (LWLink) i.next();
                LWComponent head = l.getHead();
                LWComponent tail = l.getTail();
        
                // Every link, as it's connected to us, should have us as one of
                // it's endpoints -- so return the opposite endpoint.  TODO: now
                // that links can have null endpoints, this iterator can return null
                // -- hasNext will have to get awfully fancy to handle this.
        
                if (head == LWComponent.this)
                    return tail;
                else
                    return head;
            }
      public void remove() {
      throw new UnsupportedOperationException();
            }
        };
    }
     */

    /* include all links and far endpoints of links connected to this component
    public java.util.List getAllConnectedComponents()
    {
    List list = new java.util.ArrayList(mLinks.size());
    for (LWLink l : mLinks) {
        list.add(l);
        if (l.getHead() != this)
            list.add(l.getHead());
        else if (l.getTail() != this) // todo opt: remove extra check eventually
            list.add(l.getTail());
        else
            // todo: actually, I think we want to support these
            throw new IllegalStateException("link to self on " + this);
        
    }
    return list;
    }
    */

    public int countLinksTo(LWComponent c) {
        if (c == null || mLinks == null)
            return 0;

        int count = 0;
        for (LWLink link : mLinks)
            if (link.hasEndpoint(c))
                count++;
        return count;
    }

    /** @return true if there are any links between us and the given component */
    public boolean hasLinkTo(LWComponent c) {
        if (c == null || mLinks == null)
            return false;

        for (LWLink link : mLinks)
            if (link.hasEndpoint(c))
                return true;
        return false;
    }

    /** @return true if there are any links between us and the given component */
    public boolean hasDirectedLinkTo(LWComponent c) {
        if (c == null || mLinks == null)
            return false;

        for (LWLink link : mLinks) {
            LWComponent tail = link.getTail();
            if (link.hasEndpoint(c) && tail.equals(c))
                return true;
        }
        return false;
    }

    /** @return true if there are any links between us and the given component */
    public boolean hasMultipleLinksTo(LWComponent c) {
        if (c == null || mLinks == null)
            return false;

        int count = 0;
        for (LWLink link : mLinks)
            if (link.hasEndpoint(c))
                count++;

        if (count > 1)
            return true;
        else
            return false;
    }

    /** @return true of this component has any connections (links) to the given component.
     *  LWLink overrides to include it's endpoints in the definition of "connected" to.
     */
    public boolean isConnectedTo(LWComponent c) {
        return hasLinkTo(c);
    }

    public int countCurvedLinksTo(LWComponent c) {
        int count = 0;
        for (LWLink link : getLinks())
            if (link.hasEndpoint(c) && link.isCurved())
                count++;
        return count;
    }

    /** supports ensure link paint order code */
    protected LWComponent getParentWithParent(LWContainer parent) {
        if (getParent() == parent)
            return this;
        if (getParent() == null)
            return null;
        return getParent().getParentWithParent(parent);
    }

    /** @return a collection of our ancestors.  default impl returns a list with nearest ancestor first */
    public List<LWComponent> getAncestors() {
        return (List) getAncestors(new ArrayList(8));
    }

    protected Collection<LWComponent> getAncestors(Collection bag) {
        if (parent != null) {
            bag.add(parent);
            return parent.getAncestors(bag);
        } else
            return bag;
    }

    public boolean hasAncestor(LWComponent c) {
        final LWComponent parent = getParent();
        if (parent == null)
            return false;
        else if (c == parent)
            return true;
        else
            return parent.hasAncestor(c);
    }

    public boolean hasAncestorOfType(Class clazz) {
        return getParentOfType(clazz) != null;
    }

    /** @return the first ancestor, EXCLUDING this component (starting with the parent), that is of the given type, or null if none found */
    public <T extends LWComponent> T getParentOfType(Class<T> clazz) {
        return getParentOfType(clazz, null);
    }

    /** never ascend above root */
    public <T extends LWComponent> T getParentOfType(Class<T> clazz, LWComponent root) {
        LWComponent parent = getParent();
        if (parent == null)
            return null;
        else
            return parent.getAncestorOfType(clazz, root);
    }

    /** @return the first ancestor, INCLUDING this component, that is of the given type, or null if none found */
    // TODO: including this component is confusing...
    public <T extends LWComponent> T getAncestorOfType(Class<T> clazz) {
        return getAncestorOfType(clazz, null);
    }

    /** never ascend above root */
    public <T extends LWComponent> T getAncestorOfType(Class<T> clazz, LWComponent root) {
        if (clazz.isInstance(this))
            return (T) this;
        else if (this == root)
            return null;
        else
            return getParentOfType(clazz, root);
    }

    public LWComponent getTopMostAncestorOfType(Class clazz) {
        return getTopMostAncestorOfType(clazz, null);
    }

    /** never ascend above root */
    public LWComponent getTopMostAncestorOfType(Class clazz, LWComponent root) {
        LWComponent topAncestor = getAncestorOfType(clazz, root);
        LWComponent nextAncestor = topAncestor;

        if (nextAncestor != null) {
            for (;;) {
                nextAncestor = nextAncestor.getParentOfType(clazz, root);
                if (nextAncestor != null)
                    topAncestor = nextAncestor;
                else
                    break;
                //if (DEBUG.PICK) out("nextAncestor of type " + clazz + ": " + topAncestor);
            }
        }

        return topAncestor;
    }

    /** @return by default, return the class object as returned by getClass().  Subclasses can override to provide differentiation between runtime sub-types.
     * E.g., a node class could return getClass() by default, but the constant string "textNode" for runtime instances that we
     * want the tool code to treat is coming from a different class.  Also note that supported property bits for
     * all instances with a given type token should be the same.
     */
    public Object getTypeToken() {
        // todo: should really return null if we detect this is an instance of an anonymous class
        // -- we don't want to be duplicating and using a style holder an instance of an anon
        // glass that might be overriding god knows what and affecting property setting/getting
        // Not that this will probably hurt anything: it'll never be referenced by a VueTool,
        // so we'll never see it even if it winds up in the typed style cache.
        return getClass();
    }

    /** @return the viewer margin in pixels when we're the focal -- default is 30 */
    public int getFocalMargin() {
        return 30;
    }

    protected void takeScale(double newScale) {
        if (DEBUG.LAYOUT)
            out("takeScale " + newScale);
        this.scale = newScale;
    }

    protected void setScale(double newScale) {
        if (this.scale == newScale)
            return;
        final double oldScale = this.scale;
        //if (DEBUG.LAYOUT) out("setScale " + newScale);
        //if (DEBUG.LAYOUT) tufts.Util.printClassTrace("tufts.vue", "setScale " + scale);
        takeScale(newScale);

        // can only do this via debug inspector right now, and is causing lots of
        // suprious events during init:
        //if (LWLink.LOCAL_LINKS && !mXMLRestoreUnderway)
        if (!mXMLRestoreUnderway)
            notify(LWKey.Scale, oldScale); // todo: make scale a real property

        updateConnectedLinks(null);
        //System.out.println("Scale set to " + scale + " in " + this);
    }

    /**
     * @return the scale value relative to it's parent.  So for a 50% scale in it's parent,
     * it just returns 0.5.  E.g., this would mean if the parent was also scaled at 50%,
     * the net on-map visible scaled size of the component would be 25%.
     */
    public double getScale() {
        return this.scale;
    }

    /** @return the on-map scale at 100% map scale (the concatentation of our scale plus all parent scales) */
    public double getMapScale() {
        if (getParent() == null)
            return getScale();
        else
            return getParent().getMapScale() * getScale();
    }

    /** Convenience for returning float */
    public final float getScaleF() {
        return (float) getScale();
    }

    /** Convenience for returning float */
    public final float getMapScaleF() {
        return (float) getMapScale();
    }

    public Size getMinimumSize() {
        return MinSize;
    }

    public void setFrame(Rectangle2D r) {
        setFrame((float) r.getX(), (float) r.getY(), (float) r.getWidth(), (float) r.getHeight());
    }

    /**
     * Default impl just call's setSize, then setLocation.  You
     * may want to override if want to constrain in some way,
     * such as to underlying content (e.g., an image).
     */
    public void setFrame(float x, float y, float w, float h) {
        if (DEBUG.LAYOUT)
            out("*** setFrame " + x + "," + y + " " + w + "x" + h);

        setSize(w, h); // todo: can use setSizeImpl w/internal flag?
        setLocation(x, y);

        /*
        Object old = new Rectangle2D.Float(this.x, this.y, getWidth(), getHeight());
        takeLocation(x, y);
        takeSize(w, h);
        updateConnectedLinks();
        notify(LWKey.Frame, old);
        */
    }

    /** default calls setFrame -- override to provide constraints */
    public void userSetFrame(float x, float y, float w, float h) {
        setFrame(x, y, w, h);
    }

    protected void userSetFrame(float x, float y, float w, float h, MapMouseEvent e) {
        userSetFrame(x, y, w, h);
    }

    // todo: handle via disabling a location property?
    public void setMoveable(boolean moveable) {
        setFlag(Flag.FIXED_LOCATION, !moveable);
    }

    public boolean isMoveable() {
        return hasFlag(Flag.FIXED_LOCATION) == false;
    }

    /** @return true if this component is "owned" by the pathway -- e.g., a slide that only appears as an icon */
    public boolean isPathwayOwned() {
        return false;
    }

    //private boolean linkNotificationDisabled = false;
    protected void takeLocation(float x, float y) {
        if (DEBUG.LAYOUT) {
            out("takeLocation " + x + "," + y);
            //if (DEBUG.META) tufts.Util.printStackTrace("takeLocation");
        }
        if (x != x || y != y) { // checking for Float.NaN
            String msg = "bad location: " + x + "," + y + " for " + this;
            if (DEBUG.Enabled) {
                Log.warn(msg, new Throwable("HERE"));
                //System.exit(-1);
            } else {
                Log.warn(msg);
            }
            if (x == x)
                this.x = x;
            if (y == y)
                this.y = y;
        } else {
            this.x = x;
            this.y = y;
        }
    }

    //     public void userTranslate(float dx, float dy) {
    //         translate(dx, dy);
    //     }

    /** Translate this component within it's parent by the given amount */
    public void translate(float dx, float dy) {
        setLocation(this.x + dx, this.y + dy);
    }

    /** Translate this component within it's parent by the given amount -- quietly w/out generating events */
    public void takeTranslation(float dx, float dy) {
        takeLocation(this.x + dx, this.y + dy);
    }

    //     // moved to LWGroup -- currently only usage point
    //     /** translate across the map in absolute map coordinates */
    //     public void translateOnMap(double dx, double dy)
    //     {
    //         // If this node exists in a scaled context, which means it's parent is scaled or
    //         // the parent itself is in a scaled context, we need to adjust the dx/dy for
    //         // that scale. The scale of this object being "dragged" by the call to
    //         // translateOnMap is irrelevant -- here we're concerned with it's location in
    //         // it's parent, not it's contents.  So we need to beef up the translation amount
    //         // by the context scale so drags across the map will actually stay with the
    //         // mouse.  E.g., if this object exists in a parent scaled down 50% (scale=0.5),
    //         // to move this object 2 pixels to the right in absolute top-level map
    //         // coordinates, we need to change it's internal location within it's parent by 4
    //         // pixels (2 / 0.5 = 4) to have that show up on the map (when itself displayed
    //         // at 100% scale) as a movement of 4 pixels.

    //         final double scale = getParent().getMapScale();
    //         if (scale != 1.0) {
    //             dx /= scale;
    //             dy /= scale;
    //         }

    //         translate((float) dx, (float) dy);

    //     }

    /** set the absolute map location -- meant to be overriden for special cases (e.g., the special selection group) */
    public void setMapLocation(double x, double y) {
        throw new UnsupportedOperationException("unimplemented in " + this);
        //         final double scale = getMapScale();
        //         out("map scale: " + scale);
        //         if (scale != 1.0) {
        //             final double oldMapX = getMapX();
        //             final double oldMapY = getMapY();
        //             final double dx = (x - oldMapX) * scale;
        //             final double dy = (y - oldMapY) * scale;
        //             setLocation((float) (oldMapX + dx),
        //                         (float) (oldMapY + dy));
        //         } else
        //             setLocation((float) x, (float) y);
    }

    /**
     * Set the location of this object within it's parent. E.g., if the parent is a group or a slide,
     * setLocation(0,0) would move the component to the upper left corner of it's parent.  If the
     * parent is a map, (0,0) has no special meaning as the origin of Maps, while it does exist,
     * has no special meaning when they draw.
     */
    public void setLocation(float x, float y) {
        setLocation(x, y, this, true);
    }

    /** Special setLocation to permit event notification during coordinate system changes for objects not yet added to the map */
    protected void setLocation(float x, float y, LWComponent hearableEventSource,
            boolean issueMapLocationChangeCalls) {
        if (this.x == x && this.y == y)
            return;

        final Point2D.Float oldValue = new Point2D.Float(this.x, this.y);
        takeLocation(x, y);

        //if (!linkNotificationDisabled)
        //    updateConnectedLinks();

        if (hearableEventSource != this)
            hearableEventSource.notifyProxy(new LWCEvent(hearableEventSource, this, LWKey.Location, oldValue));
        else //if (hearableEventSource != null) // if null, skip event delivery
            notify(LWKey.Location, oldValue);

        //        if (issueMapLocationChangeCalls && parent != null) {
        if (issueMapLocationChangeCalls) {

            // NEED TO DEAL WITH COORDINATE SYSTEM CHANGES
            // And need to be able to capture old map location from our OLD parent
            // during reparenting....

            // reparenting may want to force a location in the new parent, at it's
            // current map location, but relative to the new parent's location,
            // even if it's about to be moved/laid-out elsewhere, so that once
            // we get here, the below code should always work.  Or, we could
            // even have establishLocalCoordinates call us here with extra info... (oldMapX/oldMapY)
            // or, we could implement the general setMapLocation and have establishLocalCoords call that...

            // This code only works if we're moving within a single parent: no coordinate system changes!

            // Would be better to merge this somehow with notifyHierarchChanged?

            final double scale;
            if (parent != null)
                scale = parent.getMapScale(); // we move within the scale of our parent
            else
                scale = 1.0;
            if (DEBUG.LAYOUT)
                out("notifyMapLocationChanged: using scale " + scale);
            notifyMapLocationChanged(this, (x - oldValue.x) * scale, (y - oldValue.y) * scale);
        } else {
            // this always needs to happen no matter what, even during undo
            // (e.g., the shape of curves isn't stored anywhere -- always needs to be recomputed)
            //if (!linkNotificationDisabled)
            if (updatingLinks())
                updateConnectedLinks(this);
        }
    }

    /**
     * Tell all links that have us as an endpoint that we've
     * moved or resized so the link knows to recompute it's
     * connection points.
     */
    protected void updateConnectedLinks(LWComponent movingSrc) {
        //if (!linkNotificationDisabled) // todo: if still end up using this feature, need to pass this bit on down to children
        if (updatingLinks())
            if (mLinks != null && mLinks.size() > 0)
                for (LWLink link : mLinks)
                    link.notifyEndpointMoved(movingSrc, this);
    }

    //     boolean isFocal;
    //     void setFocal(boolean isFocal) {
    //         this.isFocal = isFocal;
    //     }

    /** a notification to the component that it's absolute map location has changed by the given absolute map dx / dy */
    // todo: may be better named ancestorMoved or ancestorTranslated or some such
    protected void notifyMapLocationChanged(LWComponent movingSrc, double mdx, double mdy) {
        //if (!linkNotificationDisabled) // todo: if still end up using this feature, need to pass this bit on down to children
        if (updatingLinks())
            updateConnectedLinks(movingSrc);
    }

    protected void notifyMapScaleChanged(double oldParentMapScale, double newParentMapScale) {
    }

    //     /** A notification to the component that it or some ancestor is about to change parentage */
    //     public void notifyHierarchyChanging() {}

    /** A notification to the component that it or some ancestor changed parentage */
    public void notifyHierarchyChanged() {
        if (mLinks != null && mLinks.size() > 0)
            for (LWLink link : mLinks)
                link.notifyEndpointHierarchyChanged(this);

    }

    public final void setLocation(double x, double y) {
        setLocation((float) x, (float) y);
    }

    public final void setLocation(Point2D p) {
        setLocation((float) p.getX(), (float) p.getY());
    }

    /** default calls setLocation -- override to provide constraints */
    public void userSetLocation(float x, float y) {
        setLocation(x, y);
    }

    public void setCenterAt(Point2D p) {
        setCenterAt(p.getX(), p.getY());
    }

    public void setCenterAt(double x, double y) {
        setLocation((float) x - getWidth() / 2, (float) y - getHeight() / 2);
    }

    public Point2D getLocation() {
        return new Point2D.Float(getX(), getY());
    }

    /** set component to this many pixels in size, quietly, with no event notification */
    protected void takeSize(float w, float h) {
        //if (this.width == w && this.height == h)
        //return;
        if (DEBUG.LAYOUT)
            out("*** takeSize (LWC)  " + w + "x" + h);
        this.width = w;
        this.height = h;
    }

    protected float mAspect = 0;

    public void setAspect(float aspect) {
        mAspect = aspect;
        if (DEBUG.IMAGE)
            out("setAspect " + aspect);
    }

    /** set component to this many pixels in size */
    public final void setSize(float w, float h) {
        setSizeImpl(w, h, false);
    }

    /** set component to this many pixels in size
     * @param intetrnal -- if true, the event is not undoable
     */
    protected void setSizeImpl(float w, float h, boolean internal) {
        if (this.width == w && this.height == h)
            return;

        if (DEBUG.LAYOUT)
            out("*** setSize  (LWC)  " + w + "x" + h);

        final boolean quiet = (this.width == NEEDS_DEFAULT);

        //final boolean skipUndo = internal;

        final boolean skipUndo;
        if (!internal && !javax.swing.SwingUtilities.isEventDispatchThread()) {
            // There was a reason this was important to do -- I think in some cases this could
            // leave undoable state in the undo queue after sizes changes that were really from
            // initializations, and we do NOT want to allow any undo back to a random pre-init
            // size.  [my guess is that this had/has to do with autosized] TODO 2012: BUG: selection
            // format action "fill to width" is no longer capture what it does via undo..
            if (DEBUG.THREAD || DEBUG.UNDO || DEBUG.EVENTS)
                Log.info("skipping undo on non-AWT size change: " + this + "; newSize=" + w + "x" + h);
            skipUndo = true;
        } else {
            skipUndo = internal;
        }

        final Object old = skipUndo ? LWCEvent.NO_OLD_VALUE : new Size(width, height);

        if (mAspect > 0) {
            Size constrained = ConstrainToAspect(mAspect, w, h);
            w = constrained.width;
            h = constrained.height;
        }

        if (w < MIN_SIZE)
            w = MIN_SIZE;
        if (h < MIN_SIZE)
            h = MIN_SIZE;
        takeSize(w, h);
        if (isLaidOut())
            getParent().layout();
        updateConnectedLinks(null);
        if (!quiet && isAutoSized())
            notify(LWKey.Size, old); // technically only needed if is user-sized (otherwise layout code handles this)
    }

    public static Size ConstrainToAspect(double aspect, double w, double h) {
        if (DEBUG.IMAGE)
            Log.debug("constrainToAspect " + aspect + " " + w + "x" + h);
        // Given width & height are MINIMUM size: expand to keep aspect

        if (w <= 0)
            w = 1;
        if (h <= 0)
            h = 1;
        double tmpAspect = w / h; // aspect we would have if we did not constrain it

        //if (DEBUG.IMAGE) Log.debug("ConstrainToAspect " + tmpAspect);
        // a = w / h
        // w = a*h
        // h = w/a
        //         if (DEBUG.PRESENT || DEBUG.IMAGE) {
        //             out("keepAspect=" + mAspect);
        //             out(" tmpAspect=" + tmpAspect);
        //         }
        //             if (h == this.height) {
        //                 out("case0");
        //                 h = (float) (w / mAspect);
        //             } else if (w == this.width) {
        //                 out("case1");
        //                 w = (float) (h * mAspect);
        //             } else
        if (tmpAspect > aspect) {
            //out("case2: expand height");
            h = w / aspect;
        } else if (tmpAspect < aspect) {
            //out("case3: expand width");
            w = h * aspect;
        }
        //else out("NO ASPECT CHANGE");

        Size s = new Size(w, h);

        if (DEBUG.IMAGE)
            Log.debug("constrainToAspect out: " + s);
        return s;

        /*
          if (false) {
          if (h == this.height || tmpAspect < mAspect)
          h = (float) (w / mAspect);
          else if (w == this.width || tmpAspect > mAspect)
          w = (float) (h * mAspect);
          } else {
          if (tmpAspect < mAspect)
          h = (float) (w / mAspect);
          else if (tmpAspect > mAspect)
          w = (float) (h * mAspect);
          }
        */

    }

    /** default calls setSize -- override to provide constraints */
    public void userSetSize(float w, float h) {
        setSize(w, h);
    }

    protected void userSetSize(float w, float h, MapMouseEvent e) {
        userSetSize(w, h);
    }

    /* set on screen visible component size to this many pixels in size -- used for user set size from
     * GUI interaction -- takes into account any current scale factor
     * (do we still need this? I think this should be deprecated -- SMF)
     */

    //     public void setAbsoluteSize(float w, float h)
    //     {
    //         if (true||DEBUG.LAYOUT) out("*** setAbsoluteSize " + w + "x" + h);
    //         setSize(w / getScaleF(), h / getScaleF());
    //         //setSize(w / getMapScaleF(), h / getMapScaleF());
    //     }

    /** for XML restore only -- issues no event updates */
    public void setX(float x) {
        this.x = x;
    }

    /** for XML restore only -- issues no event updates */
    public void setY(float y) {
        this.y = y;
    }

    /** for castor restore -- will not trigger any events */
    public void setWidth(float w) {
        this.width = w;
    }

    /** for castor restore -- will not trigger any events */
    public void setHeight(float h) {
        this.height = h;
    }

    /*
     * getMapXXX methods are for values in absolute map positions and scales (needed for VUE.RELATIVE_COORDS == true)
     * getScaledXXX methods are for VUE.RELATIVE_COORDS == false, tho I think we can get rid of them?  -- SMF
     *
     * "Map" values are absolute on-screen values that are true for any component in a map rendered at 100% scale (the size & location)
     * (better naming scheme might be "getRenderXXX" or "getAbsoluteXX" ?)
     */

    public float getX() {
        return this.x;
    }

    public float getY() {
        return this.y;
    }

    public float getWidth() {
        return this.width;
    }

    public float getHeight() {
        return this.height;
    }

    /** @return the width inside the local parent (width * scale) */
    public float getLocalWidth() {
        return (float) (this.width * getScale());
    }

    /** @return the height inside the local parent (height * scale) */
    public float getLocalHeight() {
        return (float) (this.height * getScale());
    }

    /** @return on-map width when viewed at 100% */
    public float getMapWidth() {
        return (float) (this.width * getMapScale());
    }

    /** @return on-map height when viewed at 100% */
    public float getMapHeight() {
        return (float) (this.height * getMapScale());
    }

    /** @return local width including any border stroke ((width + stroke) * scale) */
    public float getLocalBorderWidth() {
        return (float) ((this.width + mStrokeWidth.get()) * getScale());
    }

    /** @return local height including any border stroke ((height + stroke) * scale) */
    public float getLocalBorderHeight() {
        return (float) ((this.height + mStrokeWidth.get()) * getScale());
    }

    /** convenience */
    public Size getSize() {
        return new Size(this.width, this.height);
    }

    protected double getMapXPrecise() {
        if (parent == null) {
            //if (DEBUG.Enabled && this instanceof LWMap == false)
            //    Util.printStackTrace("fetching mapX for unparented non-map: " + this);
            return getX();
        } else {
            return parent.getMapXPrecise() + getX() * parent.getMapScale();
        }
    }

    protected double getMapYPrecise() {
        if (parent == null) {
            return getY();
        } else {
            if (parent == this) { // DEBUG
                Util.printStackTrace("PARENT LOOP " + this);
                return getY();
            }
            return parent.getMapYPrecise() + getY() * parent.getMapScale();
        }
    }

    public float getMapX() {
        return (float) getMapXPrecise();
    }

    public float getMapY() {
        return (float) getMapYPrecise();
    }

    /** @return center x of the component in absolute map coordinates */
    public float getMapCenterX() {
        return getMapX() + getMapWidth() / 2;
    }

    /** @return center y of the component in absolute map coordinates */
    public float getMapCenterY() {
        return getMapY() + getMapHeight() / 2;
    }

    /** @return the center of this node in map coordinates */
    public Point2D getMapCenter() {
        return new Point2D.Float(getMapCenterX(), getMapCenterY());
    }

    //     // these two don't handle scale properly yet: need to adjust for parent scales...
    //     protected float getCenterX(LWContainer ancestor) {
    //         return (float) getAncestorX(ancestor) + getScaledWidth() / 2;
    //     }
    //     protected float getCenterY(LWContainer ancestor) {
    //         return (float) getAncestorY(ancestor) + getScaledHeight() / 2;
    //     }

    //     // these two don't handle scale properly yet
    //     public float getLinkConnectionX(LWContainer ancestor) {
    //         //return getCenterX(ancestor);
    //         return (float) getAncestorX(ancestor) + getScaledWidth() / 2;
    //     }
    //     public float getLinkConnectionY(LWContainer ancestor) {
    //         //return getCenterY(ancestor);
    //         return (float) getAncestorY(ancestor) + getScaledHeight() / 2;
    //     }

    protected void getLinkConnectionCenterRelativeTo(Point2D.Float point, LWContainer relative) {
        //if (relative == null) Util.printStackTrace("null relative for " + this + ": " + relative);

        if (relative == this) {

            if (DEBUG.Enabled)
                Util.printStackTrace("debug: " + this + " is computing link connetion center relative to itself");
            //final float scale = getMapScaleF();

            point.x = getZeroCenterX();
            point.y = getZeroCenterY();
            //point.x = getZeroCenterX() * scale;
            //point.y = getZeroCenterY() * scale;

        } else if (relative == null) {
            //} else if (relative == null || relative == parent) {

            // if relative is null, just return available local data w/out accessing the parent.
            // This can happen normally during init.

            if (this instanceof LWLink) {
                point.x = getZeroCenterX();
                point.y = getZeroCenterY();
            } else {
                // works for connecting to something scaled for a map link to a scaled map-node:
                //point.x = getX() + getZeroCenterX() * scale;
                //point.y = getY() + getZeroCenterY() * scale;
                // works for connecting to inside a scaled context (e.g., a scaled down on-map slide)
                point.x = getX() + getZeroCenterX();
                point.y = getY() + getZeroCenterY();
            }

        } else if (true /*|| ROTATE_TEST*/) {

            // can we construct a relativing x-hierarchy transformer in one pass?
            // e.g., a combination of transformDown's then I guess transformUp's (would need that),
            // on an AffineTransform, should produce a x-hierarchy transformer.

            // Anyway, this is the safest method possible: transform up to the map,
            // then back down to the other context, taking no shortcuts.

            point.x = getZeroCenterX();
            point.y = getZeroCenterY();
            if (DEBUG.LINK)
                out("    ZeroCenter: " + point);
            transformZeroToMapPoint(point, point);
            if (DEBUG.LINK)
                out("     MapCenter: " + point);
            relative.transformMapToZeroPoint(point, point);
            if (DEBUG.LINK)
                out("RelativeCenter: " + point + " to " + relative);

        } else {

            // note that this is the NET scale -- scale effective at the map level
            // -- THIS ISN'T CORRECT -- we need the scale relative to relative...
            final float scale = getMapScaleF();

            if (this instanceof LWLink) {
                // todo: consider getMapX/Y on LWLink override to return getParent().getMapX/Y (need to check all calls tho...)
                point.x = parent.getMapX() + getZeroCenterX() * scale;
                point.y = parent.getMapY() + getZeroCenterY() * scale;
            } else {
                point.x = getMapX() + getZeroCenterX() * scale;
                point.y = getMapY() + getZeroCenterY() * scale;
            }

            // point now has map coords -- now make relative to desired component
            // (the x/y needed if drawn in the component, that produces the same
            // ultimate map location).  Normally, relative should always
            // be one of our ancestors, as this is for special link code that
            // should only ever be interested in an ancestor value, tho we compute
            // it generically just in case.

            if (DEBUG.Enabled) {
                if (relative != null && !hasAncestor(relative)) {
                    // only if not the special invisible link endpoint, which has no parent (thus no ancestors)
                    if (getClass().getEnclosingClass() != LinkTool.LinkModeTool.class) {
                        //String msg = "debug: " + this + " is computing link connetion center relative to a non-ancestor: " + relative;
                        String msg = "non-ancestor: " + relative + " used as parent-relative of " + this;
                        if (DEBUG.META)
                            Util.printStackTrace(msg);
                        else
                            Log.debug(msg);
                    }
                }
            }

            relative.transformMapToZeroPoint(point, point);
        }
    }

    /** @return our center in our zero-based coordinate space: e.g., 1/2 our width.  Links
     * will compute differentely, as their zero-based coordinate space is their parent's space (same as local space) */
    protected float getZeroCenterX() {
        return getWidth() / 2;
    }

    protected float getZeroCenterY() {
        return getHeight() / 2;
    }

    //-----------------------------------------------------------------------------
    // experimental relatve-to-a-given-ancestor coord fetchers
    // TODO: NOT WORTH THE TROUBLE RIGHT NOW OF USING THE ANCESTOR OPTIMIZATION:
    // Just get the freakin mapx of the desired relative-to component --
    // someday those values may be cached in the object/transform anyway.
    // Oh tho -- I think in LWLink we need the mapX of US, plus the mapX of the target
    // (if KEEP the ancestor code, implement generically so can pass in any value: e.g, LWLink.mCurveCenterX)
    //-----------------------------------------------------------------------------

    protected double getAncestorX(LWContainer ancestor) {
        if (ancestor == parent) // quick check for the common case
            return getX();
        else if (parent == null) {
            Util.printStackTrace("didn't find ancestor " + ancestor + " for " + this);
            return getX();
        } else
            return parent.getAncestorX(ancestor) + getX() * parent.getMapScale();
    }

    protected double getAncestorY(LWContainer ancestor) {
        if (ancestor == parent) // quick check for the common case
            return getY();
        else if (parent == null) {
            Util.printStackTrace("didn't find ancestor " + ancestor + " for " + this);
            return getY();
        } else
            return parent.getAncestorY(ancestor) + getY() * parent.getMapScale();
    }

    //     protected double ancestorY(double y, LWContainer ancestor) {
    //         if (ancestor == parent) // quick check for the common case
    //             return y;
    //         else if (parent == null) {
    //              Util.printStackTrace("didn't find ancestor " + ancestor + " for " + this);
    //              return y;
    //         } else
    //             return parent.ancestorY(y, ancestor) + getY() * parent.getMapScale();
    //     }

    //-----------------------------------------------------------------------------
    //-----------------------------------------------------------------------------
    //-----------------------------------------------------------------------------

    /**
     * @return java.net.URI
     *
     * returns a unique URI for a component. If component already has one it is returned else an new uri is created and returned.
     * Would be nice if these were somehow persistenly unique via specified naming authorities.  As they're currently
     * not, this is kind of overkill for simple runtime unique indexing string lookups. These *are* persisted in
     * in save files, tho I'm not sure if there's any point in doing so.
     */
    public java.net.URI getURI() {
        //if (isStyle) return null;
        if (uri == null) {
            try {
                uri = new URI(edu.tufts.vue.rdf.RDFIndex.getUniqueId());
            } catch (Throwable t) {
                tufts.Util.printStackTrace(t, "Failed to create an uri for  " + label);
            }
        }
        return uri;
    }

    public void setURI(URI uri) {
        //         if (isStyle) {
        //             VUE.Log.warn("attempt to set URI on a style: " + this + "; uri=" + uri);
        //             return;
        //         }
        this.uri = uri;
    }

    /* Methods to persist url through castor
    * We don't want to save URI object
    *
    */
    public void setURIString(String URIString) {
        //         if (isStyle) {
        //             VUE.Log.warn("attempt to set URIString on a style: " + this + "; uriString=" + uri);
        //             return;
        //         }
        try {
            uri = new URI(URIString);
            //edu.tufts.vue.rdf.VueIndexedObjectsMap.setID(uri,this);
        } catch (Throwable t) {
            tufts.Util.printStackTrace(t, "Failed to set an uri for  " + label);
        }

    }

    public String getURIString() {
        return getURI().toString();
    }

    /*
    public void setShape(Shape shape)
    {
    throw new UnsupportedOperationException("unimplemented setShape in " + this);
    }
    */

    /** @return our shape, full transformed into map coords and ultimate scale when drawn at 100% map zoom
     * this is used for portal clipping, and will be imperfect for some scaled shapes, such as RountRect's
     * This only works for raw shapes that are RectangularShapes -- other Shape types just return the bounding
     * box in map coordinates (e.g., a link shape)
     */
    public RectangularShape getMapShape() {
        // Will not work for shapes like RoundRect when scaled -- e..g, corner scaling will be off

        final Shape s = getZeroShape();
        //        if (getMapScale() != 1f && s instanceof RectangularShape) { // todo: do if any transform, not just scale
        if (s instanceof RectangularShape) {
            // todo: cache this: only need to updaate if location, size or scale changes
            // (Also, on the scale or location change of any parent!)
            RectangularShape rshape = (RectangularShape) s;
            rshape = (RectangularShape) rshape.clone();
            AffineTransform a = getZeroTransform();
            Point2D.Float loc = new Point2D.Float();
            a.transform(loc, loc);
            rshape.setFrame(loc.x, loc.y, rshape.getWidth() * a.getScaleX(), rshape.getHeight() * a.getScaleY());
            //System.out.println("TRANSFORMED SHAPE: " + rshape + " for " + this);
            return rshape;
        } else {
            return getMapBounds();
        }
    }

    /** @return the raw shape of this object, not including any shape (the stroke is laid on top of the raw shape).
    This is the zero based non-scaled shape (always at 0,0) */
    private Shape getShape() {
        return getZeroShape();
    }

    protected Rectangle2D.Float mZeroBounds; // don't pre-allocate -- won't be used by overriding impl's

    /** @return the raw, zero based, non-scaled shape; default impl returns same as getZeroBounds */
    public Shape getZeroShape() {
        if (mZeroBounds == null)
            mZeroBounds = new Rectangle2D.Float();
        mZeroBounds.width = getWidth();
        mZeroBounds.height = getHeight();
        return mZeroBounds;
    }

    /**
     * @return the raw, zero based, non-scaled bounds.
     *
     * Altho the x/y of the rectangle will normally be 0,0 (suggesting we could just use
     * a size object here), that's not always the case: a component who shares it's
     * coordinate space with it's parent (such as a link) will usually have a non-zero
     * x/y in the zero bounds.
     */
    protected Rectangle2D.Float getZeroBounds() {
        return new Rectangle2D.Float(0, 0, getWidth(), getHeight());
    }

    //     protected Size getZeroPaintSize() {

    //         final float strokeWidth = getStrokeWidth()l

    //         if (strokeWidth > 0) {
    //             return new Size(getWidth() + strokeWidth, getHeight() + strokeWidth);
    //         } else {
    //             return new Size(getWidth(), getHeight());
    //         }
    //     }

    /** @return the PARENT based bounds  -- this is the local component x,y  width*scale,height*scale, where scale
     * is any local scale this component has (not the total map scale: the scale that includes the scaling of all ancestors) */
    public Rectangle2D.Float getLocalBounds() {
        return new Rectangle2D.Float(getX(), getY(), getLocalWidth(), getLocalHeight());
    }

    /** @return the layout bounds -- this is the local bounds, plus an extra "hangoff" decorations that are not considered
     * part of the formal bounds of the object.  When the object is the focal, these items are not displayed.
     */
    public Rectangle2D.Float getLayoutBounds() {
        return getLocalBounds();
    }

    /** @return the local (parent-based) border bounds */
    public Rectangle2D.Float getLocalBorderBounds() {
        return addLocalStrokeToBounds(getLocalBounds());
    }

    /** @return the PARENT based, non-scaled bounds including all extra-shape artifacts, such as a stroke */
    public Rectangle2D.Float getLocalPaintBounds() {
        return addStrokeToBounds(getLocalBounds(), 0f);
    }

    /** @return getMapBounds() -- map-coord (absolute) bounds of the stroke shape (not including any stroke width) */
    public final Rectangle2D.Float getBounds() {
        return getMapBounds();
    }

    /** @return map-coord (absolute) bounds of the stroke shape (not including any stroke width) */
    public Rectangle2D.Float getMapBounds() {
        return new Rectangle2D.Float(getMapX(), getMapY(), getMapWidth(), getMapHeight());
    }

    /**
     * Return absolute map bounds for hit detection & clipping.  This will vary
     * depenending on current stroke width, if in a visible pathway,
     * etc.
     */
    public Rectangle2D.Float getPaintBounds() {
        if (LWPathway.isShowingSlideIcons() && inDrawnPathway()) {
            Rectangle2D.Float b = addStrokeToBounds(getMapBounds(), LWPathway.PathBorderStrokeWidth);
            if (farthestVisibleSlideCorner != null) {
                //if (DEBUG.WORK) out("IN DRAWN PATHWAY w/CORNER");
                b.add(farthestVisibleSlideCorner);
            }
            return b;
        } else
            return addStrokeToBounds(getMapBounds(), 0);
    }

    /** @return bounds to use when this is the focal */
    public Rectangle2D.Float getFocalBounds() {
        // do not include any slide icons
        return addStrokeToBounds(getMapBounds(), this instanceof LWImage ? 0 : 25);
        //return getFanBounds(new Rectangle2D.Float());
    }

    /**
     * Return absolute map bounds including any border stroke -- used by Groups.
     */
    public Rectangle2D.Float getBorderBounds() {
        return addStrokeToBounds(getMapBounds(), 0);
    }

    /** take the given map bounds, and add the scaled stroke width plus any extra if given */
    private Rectangle2D.Float addStrokeToBounds(Rectangle2D.Float r, float extra) {
        float strokeWidth = getStrokeWidth() + extra;

        if (strokeWidth > 0) {
            strokeWidth *= getMapScale();
            final float exteriorStroke = strokeWidth / 2;
            r.x -= exteriorStroke;
            r.y -= exteriorStroke;
            r.width += strokeWidth;
            r.height += strokeWidth;
        }

        // we need this adjustment for repaint optimzation to
        // work properly -- would be a bit cleaner to compensate
        // for this in the viewer
        //if (isIndicated() && STROKE_INDICATION.getLineWidth() > strokeWidth)
        //    strokeWidth += STROKE_INDICATION.getLineWidth();

        return r;
    }

    private Rectangle2D.Float addLocalStrokeToBounds(Rectangle2D.Float r) {
        float strokeWidth = getStrokeWidth();

        if (strokeWidth > 0) {
            strokeWidth *= getScale();
            final float exteriorStroke = strokeWidth / 2;
            r.x -= exteriorStroke;
            r.y -= exteriorStroke;
            r.width += strokeWidth;
            r.height += strokeWidth;
        }
        return r;
    }

    /** @return an AffineTransform that when applied to a graphics context, will have us drawing properly
     * relative to this component, including any applicable scaling.  So after this is applied,
     * 0,0 will draw in the upper left hand corner of the component */
    //create and recursively set a transform to get from the Map to this object's coordinate space
    // note: structure is same in the different transform methods
    // TODO OPT: can cache this transform: if track all ancestor hierarcy, location AND scale changes,
    // can skip recomputing it each time.
    public final AffineTransform getZeroTransform() {
        return loadZeroTransform(_zeroTransform);
        //         final AffineTransform a;
        //         if (parent == null) {
        //             a = new AffineTransform();
        //         } else {
        //             a = parent.getZeroTransform();
        //         }
        //         return transformDownA(a);
    }

    protected final AffineTransform loadZeroTransform(final AffineTransform a) {
        if (parent == null) {
            a.setToIdentity();
            return transformDownA(a);
        } else {
            return transformDownA(parent.loadZeroTransform(a));
        }
    }

    /**
     * @return the transform that takes us from the given ancestor down to our local coordinate space/scale
     * @param ancestor -- the ancestor to get a transform relative to.  If null, this will return the
     * same result as getLocalTransform (relative to the map)
     */
    protected AffineTransform getRelativeTransform(LWContainer ancestor) {

        if (parent == ancestor || parent == null)
            return transformDownA(new AffineTransform());
        else
            return transformDownA(parent.getRelativeTransform(ancestor));
    }

    final boolean isZoomedFocus() {
        return mTemporaryTransform != null;
    }

    /**
     * Called by model clients (e.g., MapViewer) for temporarily applying a special transform to the
     * drawing and picking of a component without touching the underlying data model (no persistent
     * changes to the component are made).  The transform must be set to null to be cleared.
     * This is what zoom-rollover uses to temporarily zoom-up a node.
     */
    void setZoomedFocus(AffineTransform tx) {

        mTemporaryTransform = tx;

        //linkNotificationDisabled = isZoomedFocus;
    }

    //-----------------------------------------------------------------------------
    //-----------------------------------------------------------------------------
    //
    // transformDownA + transformDownG are the two core routines that everything
    // ultimately uses -- e.g., placing a test rotation in these methods makes
    // it work everywhere that's using the transformation code (drawing, picking,
    // and link connections)
    //
    //-----------------------------------------------------------------------------
    //-----------------------------------------------------------------------------

    /**
     * Transform the given AffineTransform down from our parent to us, the child.
     */
    protected AffineTransform transformDownA(final AffineTransform a) {
        if (mTemporaryTransform != null) {
            a.concatenate(mTemporaryTransform);
        } else {
            a.translate(this.x, this.y);
            if (this.scale != 1)
                a.scale(this.scale, this.scale);
        }

        return a;
    }

    /** transform relative to the child after already being transformed relative to the parent */
    protected void transformDownG(final Graphics2D a) {
        if (mTemporaryTransform != null) {
            a.transform(mTemporaryTransform);
        } else {
            a.translate(this.x, this.y);
            if (this.scale != 1)
                a.scale(this.scale, this.scale);
        }
    }

    //     /** set by model clients (e.g., MapViewer) for the zoomed rollover component */
    //     private static double ZoomRolloverScale;
    //     void setZoomedFocus(double zoomFactor) {
    //         if (zoomFactor > 0) {
    //             isZoomedFocus = true;
    //             ZoomRolloverScale = zoomFactor;
    //         } else {
    //             isZoomedFocus = false;
    //         }
    //         //linkNotificationDisabled = isZoomedFocus;
    //     }

    //     private final static boolean ROTATE_TEST = false;
    //     private static final int RotSteps = 180;
    //     private static final double RotStep = Math.PI * 2 / RotSteps;
    //     private static int RotCount = 0;

    //     /**
    //      * Transform the given AffineTransform down from our parent to us, the child.
    //      */
    //     protected AffineTransform transformDownA(final AffineTransform a)
    //     {
    //         if (ROTATE_TEST && parent instanceof LWMap) {

    //             // rotate around center (relative to map-bounds)

    //             final double hw = getWidth() / 2;
    //             final double hh = getHeight() / 2;
    //             a.translate(getX() + hw, getY() + hh);
    //             a.scale(scale, scale);
    //             a.rotate(Math.PI / 8);
    //             a.translate(-hw, -hh);

    //         } else {

    //             if (isZoomedFocus) {
    //                 if (false && this instanceof LWSlide) {
    //                     final double scale = SlideIconScale * 2;
    //                     a.scale(scale, scale);

    //                 } else if (true) {

    //                     a.concatenate(ZoomRolloverTransform);

    //                 } else {

    //                     // Zoom on-center.

    //                     // To make this simple, we first translate to the local center (our
    //                     // center location in parent coords, compensating for any of our own
    //                     // scale), then apply the new zoomed scale, then translate back out
    //                     // by our raw width.  This isn't done often, so no point in over
    //                     // optimizing.

    //                     final double halfWidth = getWidth() / 2;
    //                     final double halfHeight = getHeight() / 2;
    //                     final double ourScale = getScale();

    //                     // Translate to local center:
    //                     a.translate(getX() + halfWidth * ourScale,
    //                                 getY() + halfHeight * ourScale);

    //                     if (DEBUG.VIEWER) {
    //                         // note that due to nature of this testing uber-hack, the more
    //                         // children something has, the faster it rotates.
    //                         a.rotate(RotStep * RotCount);
    //                         if (++RotCount >= RotSteps)
    //                             RotCount = 0;
    //                     }

    //                     // Set the super-zoom scale:
    //                     //a.scale(ZoomRolloverScale, ZoomRolloverScale);
    //                     a.translate(-halfWidth, -halfHeight);
    //                 }
    //             } else {

    //                 //-------------------------------------------------------
    //                 // This is the default, standard case:
    //                 //-------------------------------------------------------

    //                 a.translate(this.x, this.y);
    //                 if (this.scale != 1)
    //                     a.scale(this.scale, this.scale);
    //             }

    //         }
    //         return a;
    //     }

    // //     // When working on transformDownA, comment this code in, and comment out transformDownG
    // //     /** Must include overrides of all AffineTransform methods used in transformDownA */
    // //     private static final class GCAffineProxy extends AffineTransform {
    // //         private Graphics2D g;
    // //         @Override
    // //         public void translate(double x, double y) { g.translate(x, y); }
    // //         @Override
    // //         public void scale(double xs, double ys) { g.scale(xs, ys); }
    // //         @Override
    // //         public void rotate(double t) { g.rotate(t); }
    // //         @Override
    // //         public void concatenate(AffineTransform tx) { g.transform(tx); }
    // //     }

    // //     private static final GCAffineProxy GCAP = new GCAffineProxy();

    // //     /** transform relative to the child after already being transformed relative to the parent */
    // //     protected void transformDownG(final Graphics2D g) {
    // //         GCAP.g = g; // not exactly thread-safe -- this temporary while we work on this code (cut/paste duplicate when done)
    // //         transformDownA(GCAP);
    // //     }

    //     /** transform relative to the child after already being transformed relative to the parent */
    //     protected void transformDownG(final Graphics2D a)
    //     {
    //         //-----------------------------------------------------------------------------
    //         // NOTE THAT THE CODE IN THIS METHOD IS A PURE DUPLICATE OF transformDownA
    //         // That is, it is literally a cut & paste of the body of transformDownA.
    //         // The only difference is that our argument is of type Graphics2D, instead
    //         // of AffineTransform -- we only call methods common to both classes.
    //         // (and we don't return the passed in argument in this method)
    //         //-----------------------------------------------------------------------------

    //         if (ROTATE_TEST && parent instanceof LWMap) {

    //             // rotate around center (relative to map-bounds)

    //             final double hw = getWidth() / 2;
    //             final double hh = getHeight() / 2;
    //             a.translate(getX() + hw, getY() + hh);
    //             a.scale(scale, scale);
    //             a.rotate(Math.PI / 8);
    //             a.translate(-hw, -hh);

    //         } else {

    //             if (false && isZoomedFocus) {
    //                 if (false && this instanceof LWSlide) {
    //                     final double scale = SlideIconScale * 2;
    //                     a.scale(scale, scale);
    //                 } else {

    //                     // Zoom on-center.

    //                     // To make this simple, we first translate to the local center (our
    //                     // center location in parent coords, compensating for any of our own
    //                     // scale), then apply the new zoomed scale, then translate back out
    //                     // by our raw width.  This isn't done often, so no point in over
    //                     // optimizing.

    //                     final double halfWidth = getWidth() / 2;
    //                     final double halfHeight = getHeight() / 2;
    //                     final double ourScale = getScale();

    //                     // Translate to local center:
    //                     a.translate(getX() + halfWidth * ourScale,
    //                                 getY() + halfHeight * ourScale);

    //                     if (DEBUG.VIEWER) {
    //                         // note that due to nature of this testing uber-hack, the more
    //                         // children something has, the faster it rotates.
    //                         a.rotate(RotStep * RotCount);
    //                         if (++RotCount >= RotSteps)
    //                             RotCount = 0;
    //                     }

    //                     // Set the super-zoom scale:
    //                     //a.scale(ZoomRolloverScale, ZoomRolloverScale);
    //                     a.translate(-halfWidth, -halfHeight);
    //                 }
    //             } else {

    //                 //-------------------------------------------------------
    //                 // This is the default, standard case:
    //                 //-------------------------------------------------------

    //                 a.translate(this.x, this.y);
    //                 if (this.scale != 1)
    //                     a.scale(this.scale, this.scale);
    //             }

    //         }
    //     }

    /** Will transform all the way from the the map down to the component, wherever nested/scaled.
     * So drawing at 0,0 will draw in the upper left of the component. */
    public void transformZero(final Graphics2D g) {

        // todo: need a relative to parent transform only for cascading application during drawing
        // (and ultimate picking when impl is optimized)

        if (parent == null) {
            ;
        } else {
            parent.transformZero(g);
        }

        transformDownG(g);
    }

    public Point2D.Float transformMapToZeroPoint(Point2D.Float mapPoint) {
        return (Point2D.Float) transformMapToZeroPoint(mapPoint, mapPoint);
    }

    /**
     * @param mapPoint, a point in map coordinates to transform to local coordinates
     * @param zeroPoint the destination Point2D to place the resulting transformed coordinate -- may be
     * the same object as mapPoint (it will be written over)
     * @return the transformed point (will be zeroPoint if transformed, mapPoint if no transformation was needed,
     * although mapPoint x/y values should stil be copied to zeroPoint)
     */
    public Point2D transformMapToZeroPoint(Point2D.Float mapPoint, Point2D.Float zeroPoint) {

        // This doesn't work if we're a link!
        //         if (!isZoomedFocus && scale == 1.0 && parent instanceof LWMap && !ROTATE_TEST) { // OPTIMIZATION
        //             zeroPoint.x = mapPoint.x - this.x;
        //             zeroPoint.y = mapPoint.y - this.y;
        //             return zeroPoint;
        //         }

        try {
            getZeroTransform().inverseTransform(mapPoint, zeroPoint);
        } catch (java.awt.geom.NoninvertibleTransformException e) {
            Util.printStackTrace(e);
        }
        return zeroPoint;
    }

    protected Point2D transformZeroToMapPoint(Point2D.Float zeroPoint, Point2D.Float mapPoint) {

        // This doesn't work if we're a link!
        //         if (!isZoomedFocus && scale == 1.0 && parent instanceof LWMap && !ROTATE_TEST) { // OPTIMIZATION
        //             mapPoint.x = zeroPoint.x + this.x;
        //             mapPoint.y = zeroPoint.y + this.y;
        //             return mapPoint;
        //         }

        getZeroTransform().transform(zeroPoint, mapPoint);
        return mapPoint;
    }

    /**
     * @param mapRect -- incoming rectangle to transform to be relative to 0,0 of this component
     * @param zeroRect -- result is placed here -- will be created if is null
     * @return zeroRect
     *
     * E.g., if the incoming mapRect was from map coords 100,100->120,120, and this component was at 100,100,
     * the resulting zeroRect in this case would be 0,0->20,20 (assuming no scale or rotation).
     *
     */
    //protected Rectangle2D transformMapToZeroRect(Rectangle2D mapRect, Rectangle2D zeroRect)
    protected Rectangle2D transformMapToZeroRect(Rectangle2D mapRect) {
        //         if (zeroRect == null)
        //             zeroRect = (Rectangle2D) mapRect.clone(); // simpler than newInstace, tho we won't need the data-copy in the end
        Rectangle2D zeroRect = new Rectangle2D.Float();

        // If want to handle rotation, we'll need to transform each corner of the
        // rectangle separately, generating Polygon2D (which sun never implemented!)  or
        // a GeneralPath, in either case changing this method to return a Shape.  Better
        // would be to keep a cached rotated map Shape in each object, tho that means
        // solving the general problem of making sure we're updated any time our
        // ultimate map location/size/scale/rotation, etc, changes, which of course
        // changes if any of those values change on any ancestor.  If we did that, we'd
        // also be able to fully cache the _zeroTransform w/out having to recompute it
        // for each call just in case.  (Which would mean getting rid of this method
        // entirely and using the map shape in intersects, etc) Of course, crap, we
        // couldn't do all this for links, could we?  Tho maybe via special handing in an
        // override... tho that would only work for the transform, not the shape, as the
        // parent shape is useless to the link. FYI, currently, we only use this
        // for doing intersections of links and non-rectangular nodes

        //         final double[] points = new double[8];
        //         final double width = zeroRect.getWidth();
        //         final double height = zeroRect.getHeight();
        //         // UL
        //         points[0] = zeroRect.getX();
        //         points[1] = zeroRect.getY();
        //         // UR
        //         points[2] = points[0] + width;
        //         points[3] = points[1];
        //         // LL
        //         points[4] = points[0];
        //         points[5] = points[1] + height;
        //         // LR
        //         points[6] = points[0] + width;
        //         points[7] = points[1] + height;

        // Now that we know the below code can never handle rotation, we also might as
        // well toss out using the transform entirely and just use getMapScale /
        // getMapX/Y to mod a Rectangle2D.Float directly... Tho then our zoomed rollover
        // mod, which is in the transformDown code would stop working for rectangle
        // picking & clipping, tho we shouldn't need rect picking for zoomed rollovers,
        // (only point picking) and the zoomed rollover always draws no matter what (in
        // the MapViewer), so that may be moot, tho would need to fully test to be sure.
        // All of the this also applies to transformZeroToMapRect below.

        final AffineTransform tx = getZeroTransform();
        final double[] points = new double[4];
        points[0] = mapRect.getX();
        points[1] = mapRect.getY();
        points[2] = points[0] + mapRect.getWidth();
        points[3] = points[1] + mapRect.getHeight();
        try {
            tx.inverseTransform(points, 0, points, 0, 2);
        } catch (java.awt.geom.NoninvertibleTransformException e) {
            Util.printStackTrace(e);
        }

        zeroRect.setRect(points[0], points[1], points[2] - points[0], points[3] - points[1]);

        return zeroRect;

    }

    /**
     * This will take the given rectangle in local coordinates, and transform it
     * into map coordinates.  The passed in Rectangle2D will be modified
     * and returned.
     */
    public Rectangle2D transformZeroToMapRect(Rectangle2D zeroRect) {
        return transformZeroToMapRect(zeroRect, zeroRect);
    }

    /**
     * This will take the given zeroRect rectangle in local coordinates, and transform it
     * into map coordinates, setting mapRect and returning it.  If mapRect is null,
     * a new rectangle will be created and returned.
     */
    public Rectangle2D transformZeroToMapRect(Rectangle2D zeroRect, Rectangle2D mapRect) {
        final AffineTransform tx = getZeroTransform();
        final double[] points = new double[4];

        points[0] = zeroRect.getX();
        points[1] = zeroRect.getY();
        points[2] = points[0] + zeroRect.getWidth();
        points[3] = points[1] + zeroRect.getHeight();
        tx.transform(points, 0, points, 0, 2);

        if (mapRect == null)
            mapRect = new Rectangle2D.Float();

        mapRect.setRect(points[0], points[1], points[2] - points[0], points[3] - points[1]);

        return mapRect;

        // Non-rotating & non-transform using version:
        //         final double scale = getMapScale();
        //         // would this be right? scale the x/y first?
        //         if (scale != 1) {
        //             rect.x *= scale;
        //             rect.y *= scale;
        //             rect.width *= scale;
        //             rect.height *= scale;
        //         }
        //         if (this instanceof LWLink) {
        //             // todo: eventually rewrite this routine entirely to use the transformations
        //             // (will need that if ever want to handle rotation, as well as to skip this
        //             // special case for links).
        //             rect.x += getParent().getMapX();
        //             rect.y += getParent().getMapY();
        //         } else {
        //             rect.x += getMapX();
        //             rect.y += getMapY();
        //         }

    }

    /**
     * Default implementation: checks bounding box
     * Subclasses should override and compute via shape.
     * INTERSECTIONS always intersect based on map bounds, as opposed to contains, which tests a local point.
     */
    public final boolean intersects(Rectangle2D mapRect) {
        return intersectsImpl(mapRect);
        //         final boolean hit = intersectsImpl(rect);
        //         //if (DEBUG.PAINT) System.out.println("INTERSECTS " + fmt(rect) + " " + (hit?"YES":"NO ")
        //         //+ " for " + fmt(getPaintBounds()) + " " + this);
        //         return hit;
    }

    /** default impl intersects the render/paint bounds, including any borders (we use this for draw clipping as well as selection) */
    protected boolean intersectsImpl(Rectangle2D mapRect) {
        //if (DEBUG.CONTAINMENT) System.out.println("INTERSECTS " + Util.fmt(rect));
        final Rectangle2D bounds = getPaintBounds();
        final boolean hit = mapRect.intersects(bounds);
        if (DEBUG.PAINT && DEBUG.PICK)
            System.out.println("INTERSECTS " + fmt(mapRect) + " " + (hit ? "YES" : "NO ") + " for " + fmt(bounds)
                    + " of " + this);
        //Util.printClassTrace("tufts.vue.LW", "INTERSECTS " + this);
        return hit;
    }

    public boolean requiresPaint(DrawContext dc) {
        return requiresPaintImpl(dc) != null;
    }

    /** @return true if this component currently requires painting and intersects the master paint region */
    protected Object requiresPaintImpl(DrawContext dc) {
        if (dc.skipDraw == this)
            return null;

        // always draw the focal
        if (dc.focal == this)
            return "isFocal";

        if (isZoomedFocus())
            return null;

        // if filtered, don't draw, unless has children, in which case
        // we need to draw just in case any of the children are NOT filtered.
        //if (isHidden() || (isFiltered() && !hasChildren()))
        if (!hasDraws())
            return null;

        if (dc.isClipOptimized()) {

            //-----------------------------------------------------------------------------
            // Returning true when parent.fullyContainsChildren() is true will prevent a
            // ton of intersects calls (and subsequent map-bounds computations involving
            // transform fetches and their application to rectangles) when we have lots
            // objects that are going to need drawing no matter what (e.g., lots of
            // slide icons visible and we're zoomed out), tho it will cause the pixel
            // drawing code to be invoked more often that it needs to when zoomed in.
            // It's a basic trade-off.
            //
            // NOT checking this optimizes us for fast painting when zoomed way in on
            // sub-components of the map/slides (e.g., during presentations), and that's
            // the current chosen priority.
            //
            // As either method can safely be used (checking or not checking), we allow
            // the check, but only if it looks like we're reasonably zoomed-out.  Either
            // method is okay because this check is just an early way to say something
            // requires painting, and it's always okay to paint -- the worse that
            // happens is something off screen is painted, and we waste time in the
            // graphics pipeline having it clipped.  Essentailly, when run, this check
            // just lets us skip the intersects call below.
            //
            // The reason this is meaningful is we only get here if the parent
            // has already determined it needs to paint, and if that's the case,
            // and it fully contains it's children, if the parent is likely to
            // be fully on-screen, we should just go ahead and paint all the children.

            if (parent != null && dc.focal != parent && dc.zoom <= 1.0 && parent.fullyContainsChildren()) {
                //return "parentIsLikelySlideIcon " + dc.zoom;
                return "parentIsLikelySlideIcon";
            }

            //-----------------------------------------------------------------------------

            if (hasEntries() && !(this instanceof LWImage)) {

                // HACK: for now, if we have ANY pathway entries, we say we have to draw, so
                // that if they're needed, any slide icons will draw (even if the parent
                // node is clipped: this is because the slide icons lie outside the
                // node).  Really, we only need to return true here if we're on any
                // pathways that are visible & showing slide icons, and we have at least
                // one actual slide.  Todo: cache that info so we can check it here
                // (such a bit would need to update when any pathway visibility changes,
                // or it's show icons bit flips, or our pathway memberships change,
                // etc....)

                // Also, once we'd determined there were slide icons to draw, we'd
                // also want to check each of their bounds to see if they're within
                // the master clip rect, tho right now they all scrunch together,
                // so that would be a bit of overkill.

                // 2009-10-21: too expensive for images as it may force them to load a
                // full image representation which takes up tons of memory.  Images
                // don't display slide icons, so I don't think we ever needed it
                // for those anyway.

                return "hasEntriesHack";
            }

            if (intersects(dc.getMasterClipRect()))
                return "inClipRegion";

            //             if (isDrawingSlideIcon())
            //                 return getMapSlideIconBounds().intersects(dc.getMasterClipRect());
            //             else

            return null;

        } else {

            // Not clip optimized means don't bother to check the master clip to see if
            // we need to draw: just always draw everything no matter where it is
            // (unless it was hidden, etc).  E.g., if we're drawing to generate an
            // image, or drawing a zoomed rollover, we already know we just need to draw
            // the component no matter what.

            // More examples: when drawing raw, always draw everything, don't check
            // against the master "map" clip rect, as that's only for drawing map
            // elements (e.g., we may be drawing a LWComponent that's a decoration or
            // GUI element, like a navigation node, or a master slide background).

            return "noClip";
        }

    }

    //     /**
    //      * We divide area around the bounding box into 8 regions -- directly
    //      * above/below/left/right can compute distance to nearest edge
    //      * with a single subtract.  For the other regions out at the
    //      * corners, do a distance calculation to the nearest corner.
    //      * Behaviour undefined if x,y are within component bounds.
    //      */
    //     public float distanceToEdgeSq(float x, float y)
    //     {
    //         float ex = this.x + getWidth();
    //         float ey = this.y + getHeight();

    //         if (x >= this.x && x <= ex) {
    //             // we're directly above or below this component
    //             return y < this.y ? this.y - y : y - ey;
    //         } else if (y >= this.y && y <= ey) {
    //             // we're directly to the left or right of this component
    //             return x < this.x ? this.x - x : x - ex;
    //         } else {
    //             // This computation only makes sense following the above
    //             // code -- we already know we must be closest to a corner
    //             // if we're down here.
    //             float nearCornerX = x > ex ? ex : this.x;
    //             float nearCornerY = y > ey ? ey : this.y;
    //             float dx = nearCornerX - x;
    //             float dy = nearCornerY - y;
    //             return dx*dx + dy*dy;
    //         }
    //     }

    //     public Point2D nearestPoint(float x, float y)
    //     {
    //         float ex = this.x + getWidth();
    //         float ey = this.y + getHeight();
    //         Point2D.Float p = new Point2D.Float(x, y);

    //         if (x >= this.x && x <= ex) {
    //             // we're directly above or below this component
    //             if (y < this.y)
    //                 p.y = this.y;
    //             else
    //                 p.y = ey;
    //         } else if (y >= this.y && y <= ey) {
    //             // we're directly to the left or right of this component
    //             if (x < this.x)
    //                 p.x = this.x;
    //             else
    //                 p.x = ex;
    //         } else {
    //             // This computation only makes sense following the above
    //             // code -- we already know we must be closest to a corner
    //             // if we're down here.
    //             float nearCornerX = x > ex ? ex : this.x;
    //             float nearCornerY = y > ey ? ey : this.y;
    //             p.x = nearCornerX;
    //             p.y = nearCornerY;
    //         }
    //         return p;
    //     }

    //     public float distanceToEdge(float x, float y)
    //     {
    //         return (float) Math.sqrt(distanceToEdgeSq(x, y));
    //     }

    //     /**
    //      * Return the square of the distance from x,y to the center of
    //      * this components bounding box.
    //      */
    //     public float distanceToCenterSq(float x, float y)
    //     {
    //         float cx = getCenterX();
    //         float cy = getCenterY();
    //         float dx = cx - x;
    //         float dy = cy - y;
    //         return dx*dx + dy*dy;
    //     }

    //     public float distanceToCenter(float x, float y)
    //     {
    //         return (float) Math.sqrt(distanceToCenterSq(x, y));
    //     }

    //     public void drawPathwayDecorations(DrawContext dc)
    //     {
    //         if (mPathways == null)
    //             return;

    //         if (LWPathway.PathwayAsDots || this instanceof LWLink)
    //             LWPathway.drawPathwayDot(dc.create(), this);

    //         if (!LWPathway.PathwayAsDots && isTransparent()) {
    //             for (LWPathway path : mPathways) {
    //                 //if (!dc.isFocused && path.isDrawn()) {
    //                 if (path.isDrawn()) {
    //                     path.drawPathwayBorder(dc.create(), this);
    //                 }
    //             }
    //         }
    //     }

    //     /** if this component is selected and we're not printing, draw a selection indicator */
    //     // todo: drawing of selection should be handled by the MapViewer and/or the currently
    //     // active tool -- not in the component code
    //     protected void drawSelectionDecorations(DrawContext dc) {
    //         if (isSelected() && dc.isInteractive()) {
    //             LWPathway p = VUE.getActivePathway();
    //             if (p != null && p.isVisible() && p.getCurrentNode() == this) {
    //                 // SPECIAL CASE:
    //                 // as the current element on the current pathway draws a huge
    //                 // semi-transparent stroke around it, skip drawing our fat
    //                 // transparent selection stroke on this node.  So we just
    //                 // do nothing here.
    //             } else {
    //                 dc.g.setColor(COLOR_HIGHLIGHT);
    //                 dc.g.setStroke(new BasicStroke(getStrokeWidth() + SelectionStrokeWidth));
    //                 transformZero(dc.g);
    //                 dc.g.draw(getZeroShape());
    //             }
    //         }
    //     }

    /** @return true if the given x/y (already transformed to our local coordinate space), is within our shape */
    public final boolean contains(float x, float y, PickContext pc) {
        if (containsImpl(x, y, pc))
            return true;
        //         else if (isDrawingSlideIcon()) {
        //             if (DEBUG.PICK) out("Checking slide icon bounds " + getSlideIconBounds());
        //             return getSlideIconBounds().contains(x, y);
        //         }
        else
            return false;
    }

    /** @return 0 means a hit, -1 a completely miss, > 0 means distance (squared), to be sorted out by caller  */
    protected float pickDistance(float x, float y, PickContext pc) {
        return contains(x, y, pc) ? 0 : -1;
    }

    /**
     * Default implementation: checks bounding box, including any stroke width.
     * Subclasses should override for more accurate hit detection.
     */
    protected boolean containsImpl(float x, float y, PickContext pc) {
        final float stroke = getStrokeWidth() / 2;

        return x >= -stroke && y >= -stroke && x <= getWidth() + stroke && y <= getHeight() + stroke;
    }

    /** For using a node in a non-map context (e.g., as an on-screen button) */
    // todo: this is bounding box only: odd shapes will have imperfect hit detection
    // also, if we ever add rotation of arbitrary LWComponents, this won't handle it --
    // will need need to dump this hack and do all in LWTraversal, or have the
    // local LWComponent contains/intersects code adjust for the local transformation
    // themselves.
    public boolean containsLocalCoord(float x, float y) {
        return x >= this.x && y >= this.y && x <= (this.x + getLocalWidth()) && y <= (this.y + getLocalHeight());
    }

    public static final float SlideIconScale = 0.125f;
    //     private Rectangle2D.Float mSlideIconBounds;
    //     public Rectangle2D.Float getSlideIconBounds() {
    //         if (mSlideIconBounds == null)
    //             mSlideIconBounds = computeSlideIconBounds(new Rectangle2D.Float());
    //         else if (true || mSlideIconBounds.x == Float.NaN) // need a reshape/reshapeImpl trigger on move/resize to properly re-validate (wait: NaN != NaN !)
    //             computeSlideIconBounds(mSlideIconBounds);
    //         return mSlideIconBounds;
    //     }

    //     public Rectangle2D.Float getMapSlideIconBounds() {
    //         Rectangle2D.Float slideIcon = (Rectangle2D.Float) getSlideIconBounds().clone();
    //         final float scale = getMapScaleF();
    //         // Compress the local slide icon coords into the node's scale space:
    //         slideIcon.x *= scale;
    //         slideIcon.y *= scale;
    //         // Now make them absolute map coordintes (no longer local):
    //         slideIcon.x += getMapX();
    //         slideIcon.y += getMapY();
    //         // Now scale down size:
    //         slideIcon.width *= scale;
    //         slideIcon.height *= scale;

    //         return slideIcon;
    //     }

    /** @return the local lower right hand corner of the component: for rectangular shapes, this is just [width,height]
     * Non-rectangular shapes can override to do something fancier. */
    protected Point2D getZeroSouthEastCorner() {
        return new Point2D.Float(getWidth(), getHeight());
    }

    private static final Point2D ZeroNorthWestCorner = new Point2D.Float();

    protected Point2D getZeroNorthWestCorner() {
        return ZeroNorthWestCorner;
    }

    //     protected Rectangle2D.Float computeSlideIconBounds(Rectangle2D.Float rect)
    //     {
    //         // TODO: below should take into account actual slide size...
    //         final float width = LWSlide.SlideWidth * SlideIconScale;
    //         final float height = LWSlide.SlideHeight * SlideIconScale;

    //         Point2D.Float corner = getZeroCorner();

    //         float xoff = corner.x - 60;
    //         float yoff = corner.y - 60;

    //         // If shape is small, try and keep it from overlapping too much (esp the label)
    //         if (xoff < getWidth() / 2f)
    //             xoff = getWidth() / 2f;
    //         if (yoff < getHeight() * 0.75f)
    //             yoff = getHeight() * 0.75f;

    //         // This can happen for wierd shapes (e.g., shield)
    //         if (xoff > corner.x)
    //             xoff = corner.x;
    //         if (yoff > corner.y)
    //             yoff = corner.y;

    //         rect.setRect(xoff,
    //                      yoff,
    //                      width,
    //                      height);

    //         return rect;
    //     }

    private Point2D.Float getSlideIconStackLocation() {
        final Point2D corner = getZeroSouthEastCorner();

        float xoff = (float) corner.getX() - 60;
        float yoff = (float) corner.getY() - 60;

        // If shape is small, try and keep it from overlapping too much (esp the label)
        if (xoff < getWidth() / 2f)
            xoff = getWidth() / 2f;
        if (yoff < getHeight() * 0.75f)
            yoff = getHeight() * 0.75f;

        // todo: can reuse getZeroCorner point2D instead of creating anew...
        return new Point2D.Float(xoff, yoff);

    }

    //protected final Rectangle2D debugZeroRect = new Rectangle2D.Double();

    /**
     * Intended for use in an LWContainer where the parent has already
     * been drawn, and the DrawContext is currently transformed to the
     * parent.  This performs the final transform for this child and
     * transforms it.
     */
    public void drawLocal(DrawContext dc) {
        // this will cascade to all children when they draw, combining with their calls to transformDown
        transformDownG(dc.g);

        final AffineTransform zeroTransform = DEBUG.BOXES ? dc.g.getTransform() : null;

        //if (dc.focal == this || dc.isFocused()) // prevents slide icons from appearing in portals
        if (dc.focal == this)
            drawZero(dc);
        else
            drawZeroDecorated(dc, true);

        if (DEBUG.BOXES)
            drawDebugInfo(dc, zeroTransform);
    }

    private void drawDebugInfo(DrawContext dc, AffineTransform zeroTransform) {

        if (this instanceof LWLink)
            return;

        dc.g.setTransform(zeroTransform);

        dc.setAbsoluteStroke(1);

        //dc.g.setColor(Color.blue);
        //dc.g.draw(debugZeroRect);

        // scaling testing -- draw an exactly 8x8 pixel (rendered) box
        dc.g.setColor(Color.green);
        dc.g.drawRect(0, 0, 7, 7);

        // show the center-point to corner intersect line (debug slide icon placement):
        dc.g.setColor(Color.red);
        //dc.setAbsoluteStroke(1);
        dc.g.setStroke(STROKE_ONE);
        dc.g.draw(new Line2D.Float(new Point2D.Float(getWidth() / 2, getHeight() / 2), getZeroSouthEastCorner()));

        if (DEBUG.LINK && isSelected() && getLinks().size() > 0) {
            final Rectangle2D.Float pureFan = getFanBounds();
            final Rectangle2D.Float fan = getCenteredFanBounds();
            final float cx = getMapCenterX();
            final float cy = getMapCenterY();
            final Line2D xaxis = new Line2D.Float(fan.x, cy, fan.x + fan.width, cy);
            final Line2D yaxis = new Line2D.Float(cx, fan.y, cx, fan.y + fan.height);
            dc.setMapDrawing();
            dc.setAbsoluteStroke(4);
            //dc.g.setColor(getRenderFillColor(dc));
            dc.g.setColor(Color.blue);
            dc.g.draw(pureFan);

            dc.setAbsoluteStroke(2);
            dc.g.setColor(Color.red);
            dc.g.draw(fan);
            dc.g.draw(xaxis);
            dc.g.draw(yaxis);
        }
    }

    /**
     *
     * This is NOT the method used to draw a component during routine drawing of the
     * entire map (unless this is the map itself).  This is for directly forcing the
     * drawing or redrawing a single component at it's proper map location.  The passed
     * in DrawContext gc is expected to be transformed for drawing the top-level map
     * (minimally transformed).  If you are going to use the passed in DrawContext after
     * this call for other map drawing operations, be sure to pass in dc.create() from
     * the caller, as this call will leave the DrwaContext it in a generally undefined state
     * (e.g., probably rooted at the node).
     *
     */
    public void draw(DrawContext dc) {
        //         if (dc.isPrintQuality()) {
        //             dc.setClipOptimized(false); // ensure all children draw even if not inside clip
        //             // in interactive presentation mode, the above causes repaint-region's to
        //             // cause all images to draw in a slide focal, which can cause them to LOSE
        //             // resolution under low memory conditions.
        //         }
        transformZero(dc.g);
        if (dc.focal == this) {
            drawZero(dc);
        } else {
            drawZeroDecorated(dc, false);
            //             if (isZoomedFocus()) {
            //                 // include any slide icons
            //                 drawDecorated(dc);
            //             } else {
            //                 if (dc.drawPathways())
            //                     drawPathwayDecorations(dc);
            //                 drawRaw(dc);
            //             }
        }
    }

    public final void drawZero(DrawContext dc) {
        final AffineTransform zeroTransform = DEBUG.PDF ? dc.g.getTransform() : null;

        dc.checkComposite(this);
        if (DEBUG.Enabled)
            dc.recordDebug(this);

        try {

            if (!isTopLevel()) { // e.g., isn't a Layer, which is never selected
                // TODO: this should be a flag set up in the DrawContext
                if (VUE.getInteractionToolsPanel() != null) {
                    final double alpha = VUE.getInteractionToolsPanel().getAlpha();
                    if (alpha != 1 && !selectedOrParent())
                        dc.setAlpha(alpha); // fade nodes not in selection
                }
            }

            drawImpl(dc);

        } catch (RuntimeException e) {
            Log.error("drawImpl failed: " + e);
            try {
                dc.setAlpha(0.5);
                dc.g.setColor(Color.red);
                dc.g.fill(getZeroShape());
            } catch (Throwable t) {
                Util.printStackTrace(t);
            } finally {
                throw e;
            }
        }

        if (isDeleted()) {
            // debug
            dc.setAlpha(0.5);
            dc.g.setColor(Color.yellow);
            dc.g.fill(getZeroShape());
        }

        if (DEBUG.PDF && DEBUG.META && this instanceof LWLink == false) {
            dc = dc.create();
            dc.g.setTransform(zeroTransform);
            dc.g.setColor(Color.blue);
            dc.g.setFont(VueConstants.FixedSmallFont);
            dc.setAbsoluteScale(1);
            final Color c1 = getFillColor();
            final Color c2 = getRenderFillColor(dc);
            dc.g.drawString(fmt(c1), 0, 10);
            if (c1 == null || !c1.equals(c2))
                dc.g.drawString(fmt(c2), 0, 20);
        }

    }

    //     public void drawFit(java.awt.Graphics g, int xoff, int yoff) {
    //         //drawFit(dc, dc.getMasterClipRect(), borderGap);
    //         drawFit(new DrawContext(g, this), 0);
    //     }

    /** fit and center us into the total clip bounds of the given dc -- border gap pixels will multiplied by final scale value */
    public void drawFit(DrawContext dc, int borderGap) {
        drawFit(dc, dc.getMasterClipRect(), borderGap);
    }

    /**
     * fit and center us into the given frame
     * @param borderGap: if positive, a fixed amount of pixels, if NEGATIVE, the % of viewport size to leave as a margin
     */
    public void drawFit(DrawContext dc, Rectangle2D frame, int borderGap) {
        final Point2D.Float offset = new Point2D.Float();
        final Size size = new Size(frame);
        final double zoom = ZoomTool.computeZoomFit(size, borderGap, addStrokeToBounds(getZeroBounds(), 0), offset);
        if (DEBUG.PDF)
            out("drawFit into " + fmt(frame) + " zoom " + zoom);
        dc.g.translate(-offset.x + frame.getX(), -offset.y + frame.getY());
        dc.g.scale(zoom, zoom);
        dc.setClipOptimized(false);
        drawZero(dc);
    }

    //private static final double PathwayOnTopZoomThreshold = 1.5;
    public static final double PathwayOnTopZoomThreshold = 3;

    /**
     * Draw any needed pathway decorations and related slide icons,
     * before/after calling drawZero, depending on desired impl.
     */
    protected final void drawZeroDecorated(DrawContext dc, boolean drawSlides) {
        if (dc.drawPathways() && mPathways != null) {
            LWPathway.decorateUnder(this, dc);
            if (dc.zoom > PathwayOnTopZoomThreshold) {
                // force the over decorations to be under
                LWPathway.decorateOver(this, dc);
                drawZero(dc);
            } else {
                drawZero(dc);
                LWPathway.decorateOver(this, dc);
            }
        } else {
            drawZero(dc);
        }

        // see VUE-896 (and VUE-892) only show slide icon if node is not filtered
        if (drawSlides && mEntries != null && !isFiltered())
            drawSlideIconStack(dc);
        //else
        //farthestVisibleSlideCorner = null;
    }

    //     /** If there's a pathway entry we want to be showing, return it, otherwise, null */
    //     LWPathway.Entry getEntryToDisplay()
    //     {
    //         LWPathway path = VUE.getActivePathway();

    //         if (!inPathway(path)) {
    //             if (mPathways != null && mPathways.size() > 0)
    //                 path = mPathways.get(0); // show the first pathway it's in if it's not in the active pathway
    //             else
    //                 path = null;
    //         }

    //         if (path != null && path.isShowingSlides()) {
    //             final LWPathway.Entry entry = path.getCurrentEntry();
    //             // This is just in case the node is in the pathway more than once: if it is,
    //             // and the current entry is for this node, use that, otherwise, just
    //             // use the first entry for the the node.
    //             if (entry != null && entry.node == this)
    //                 return entry;
    //             else
    //                 return path.getEntry(path.firstIndexOf(this));
    //         }
    //         return null;
    //     }

    //     public boolean isDrawingSlideIcon() {
    //         final LWPathway.Entry entry = getEntryToDisplay();
    //         return entry != null && !entry.isMapView;
    //     }

    private Point2D.Float farthestVisibleSlideCorner;

    private void recordCorner(Point2D.Float p) {
        //if (p == null) return; // try always leaving last corner for now
        if (DEBUG.WORK)
            out("corner=" + p);
        farthestVisibleSlideCorner = p;
    }

    /** @return a slide to be drawn last, or null if none in particular */

    // TODO: need to do this as part of layout: need to trigger layout if any pathway
    // visibility or membership changes.  Currently, the paint bounds falls behind as
    // farthestVisibleCorner doesn't update till draw time... We special case a call to
    // this during the init (restore) layout, so at least auto-fit at startup works, but
    // if we ever hava a viewer that's implementing a constant auto-fit feature, it will
    // fall behind until we handle this in proper model/view split fashion.

    private final void layoutSlideIcons(DrawContext dc) {
        if (mEntries == null)
            return;

        final Point2D.Float corner = getSlideIconStackLocation();

        float xoff = corner.x;
        float yoff = corner.y;

        //         if (false && dc != null && dc.isPresenting()) {
        //             // if presenting, let the position the active pathway slide as the last slide in the stack
        //             for (LWSlide slide : seenSlideIcons(dc)) {
        //                 slide.takeLocation(xoff, yoff);
        //                 yoff += slide.getLocalHeight() / 6;
        //                 xoff += slide.getLocalWidth() / 6;
        //             }
        //         } else {

        // if NOT presenting, leave the slides arranged in the order
        // of the pathway list (TODO: entries order isn't synced with this...)

        LWSlide lastSlide = null;

        for (LWPathway.Entry e : mEntries) {
            if (e.hasVisibleSlide()) {
                final LWSlide slide = e.getSlide();
                lastSlide = slide;

                // TODO BUG: during a presentation, if you use 'p' to change to exclusive
                // pathway display mode and back, it actually results in the movement of the
                // slide icons -- so unless the slide you're on happens to be the first slide
                // icon, it will move when you do this, and the viewer isn't catching this, and
                // the slide moves up and to the left in the middle of the presentation.  The
                // real fix for this involves the complete reworking of the MapViewer to just
                // be a Focal/Tree viewer, which can truly throw out all map coordinates, and
                // just deal with wherever it's rooted in the hierachy.  We're probably 2/3 of
                // the way there now.  Hopefully we'll get there someday... SMF 2007-11-05

                // so if is the focal somewhere, it can know to zoom fit, tho ideally
                // the viewer would just ignore the location on focals...
                // slide.setLocation(xoff, yoff); // No good: if slide's parent moves, the slide moves..
                slide.takeLocation(xoff, yoff);

                final float scaledSlideWidth = slide.getLocalWidth();
                final float scaledSlideHeight = slide.getLocalHeight();
                yoff += scaledSlideWidth / 6;
                xoff += scaledSlideHeight / 6;
            }
        }

        if (lastSlide != null) {
            corner.x = lastSlide.getMapX() + lastSlide.getLocalWidth();
            corner.y = lastSlide.getMapY() + lastSlide.getLocalHeight();
            recordCorner(corner);
            //out("far corner: " + Util.fmt(corner));
        } else
            recordCorner(null);

        // Now just in case, layout all the non-visible ones after the visible, in case
        // they get manually selected via the pathway panel and temporarily shown
        // (only one can be shown at a time, so they can all occupy the last slot)

        for (LWPathway.Entry e : mEntries) {
            if (!e.pathway.isShowingSlides() && e.canProvideSlide()) {
                e.getSlide().takeLocation(xoff, yoff);
            }
        }
        //}
    }

    private void drawSlideIconStack(final DrawContext dc) {
        layoutSlideIcons(dc);

        dc.setBackgroundFill(null); // always make sure the slide icons fill
        for (LWSlide slide : seenSlideIcons(dc)) {
            if (dc.skipDraw != slide) {
                drawSlideIcon(dc.push(), slide);
                dc.pop();
            }
        }

    }

    protected static final BasicStroke SlideIconPathwayStroke = new BasicStroke(
            (float) (LWPathway.PathBorderStrokeWidth / SlideIconScale), BasicStroke.CAP_ROUND,
            BasicStroke.JOIN_ROUND);

    private void drawSlideIcon(final DrawContext dc, final LWSlide slide) {
        slide.transformDownG(dc.g);

        //        final boolean drewBorder;

        //         //if (dc.isPresenting() || slide.isSelected()) {
        //         if (dc.isPresenting() || slide.getPathwayEntry() == VUE.getActiveEntry()) {
        //             // every slide icon should be a slide with an entry...
        //             dc.g.setColor(slide.getPathwayEntry().pathway.getColor());
        //             //dc.g.setColor(Color.red);
        //             dc.g.setStroke(SlideIconPathwayStroke);
        //             dc.g.draw(slide.getZeroShape());
        //             drewBorder = true;
        //         } else {
        //             drewBorder = false;
        //         }

        //         final AffineTransform zeroTransform = dc.g.getTransform();
        //         final Shape curClip = dc.g.getClip();
        //         dc.g.clip(slide.getZeroShape());
        slide.drawZero(dc);

        //         if (!drewBorder && !dc.isAnimating()) {
        //             dc.g.setClip(curClip); // TODO: this is clearing the underlying clip and allowing the border to draw over the scroll bars, etc!
        //             // Generic non-presentation unselected slide icon: draw a gray border
        //             //dc.g.setColor(slide.getRenderFillColor(dc).brighter());
        //             dc.g.setTransform(zeroTransform);
        //             dc.g.setColor(Color.darkGray);
        //             dc.g.setStroke(STROKE_FIVE);
        //             dc.g.draw(slide.getZeroShape());
        //         }

    }

    /*
    protected final void drawDecorated(DrawContext dc)
    {
    final LWPathway.Entry entry = getEntryToDisplay();
    //final boolean drawSlide = (entry != null);
    final boolean drawSlide = (entry != null && !entry.isMapView);
        
    if (dc.drawPathways() && dc.focal != this)
        drawPathwayDecorations(dc);
        
    if (drawSlide) {
        
        drawZero(dc);
        
        final LWSlide slide = entry.getSlide();
        
        //double slideX = getCenterX() - (slide.getWidth()*slideScale) / 2;
        //double slideY = getCenterY() - (slide.getHeight()*slideScale) / 2;
        //dc.g.translate(slideX, slideY);
        
        Rectangle2D.Float slideFrame = getSlideIconBounds();
        
        //slide.setLocation(slideFrame.x, slideFrame.y);
        
        dc.setClipOptimized(false);
        dc.g.translate(slideFrame.x, slideFrame.y);
        dc.g.scale(SlideIconScale, SlideIconScale);
        
        // A hack so that when LWLinks (hasAbsoluteMapLocation) pop to map drawing, they
        // don't pop up beyond this point.
        //dc.mapTransform = dc.g.getTransform();
        
        //dc.g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.9f));
        //entry.pathway.getMasterSlide().drawImpl(dc);
        slide.drawImpl(dc);
        
        //Rectangle2D border = slideFrame;
        // todo: move to LWSlide.drawImpl:
        Rectangle2D border = slide.getBounds();
        final Color slideFill = slide.getRenderFillColor(dc);
        final Color iconBorder;
        // todo: create a contrastColor, which node icon border's can also use
        if (brightness(slideFill) == 0)
            iconBorder = Color.gray;
        else if (brightness(slideFill) > 0.5)
            iconBorder = slideFill.darker();
        else
            iconBorder = slideFill.brighter();
        //out("slideFillr: " + slideFill);
        //out("iconBorder: " + iconBorder);
        dc.g.setColor(iconBorder);
        dc.g.setStroke(VueConstants.STROKE_SEVEN);
        dc.g.draw(border);
        
    } else {
        
        //if (entry != null && !dc.isFocused) {
        if (entry != null) {
            // if we had an entry, but it was a map-view slide, do something to make it look slide-like
            dc.g.setColor(entry.pathway.getMasterSlide().getFillColor());
            if (entry.node instanceof LWGroup) {
                if (!dc.isPresenting())
                    dc.g.fill(entry.node.getZeroBounds());
            } else if (dc.focal != this && entry.node.isTranslucent()) {
                Area toFill = new Area(entry.node.getZeroBounds());
                toFill.subtract(new Area(entry.node.getZeroShape()));
                dc.g.fill(toFill);
            }
        }
        
        drawZero(dc);
    }
    }
    */

    /** default impl: does nothing -- meant to be overriden */
    protected void drawImpl(DrawContext dc) {
    }

    /** default impl: does nothing -- meant to be overriden -- meant to potentially cache all children */
    public void preCacheContent() {
    }

    protected void preCacheImpl() {
    }

    protected static void preCacheDescendents(LWComponent focal) {
        if (focal == null)
            return;
        //Log.debug("PRE CACHE FOCAL " + focal);
        for (LWComponent c : focal.getAllDescendents()) {
            //Log.debug("PRE-CACHE-CHILD " + c);
            c.preCacheImpl();
        }
    }

    protected LWChangeSupport getChangeSupport() {
        return mChangeSupport;
    }

    public synchronized void addLWCListener(Listener listener) {
        mChangeSupport.addListener(listener, null);
    }

    public synchronized void addLWCListener(Listener listener, LWComponent.Key singleEventKey) {
        mChangeSupport.addListener(listener, singleEventKey);
    }

    /** @param eventMask is a string constant (from LWKey) or an array of such. If one
     of these non-null values, only events matching those keys will be delievered */
    public synchronized void addLWCListener(Listener listener, Object... eventsDesired) {
        mChangeSupport.addListener(listener, eventsDesired);
    }

    public synchronized void removeLWCListener(Listener listener) {
        mChangeSupport.removeListener(listener);
    }

    /** convenince method for remove a (possible) old listener, and attaching a (possible) new listener */
    public static void swapLWCListener(Listener listener, LWComponent oldSource, LWComponent newSource) {
        if (oldSource != null)
            oldSource.removeLWCListener(listener);
        if (newSource != null)
            newSource.addLWCListener(listener);
    }

    public synchronized void removeAllLWCListeners() {
        mChangeSupport.removeAllListeners();
    }

    private boolean isStyling(Key key) {
        return supportsProperty(key)
                //&& (key.isStyleProperty() || key == LWKey.Label);
                && (key.isStyleProperty() || (key == LWKey.Label && hasFlag(Flag.DATA_STYLE)));
    }

    protected synchronized void notifyLWCListeners(LWCEvent e) {
        if (isDeleted() && !permitZombieEvent(e)) {
            // note: this test shortcuts much more detailed diagnostics in LWChangeSupport
            // for tracking zombie events -- comment out for advanced debugging
            if (DEBUG.Enabled)
                Log.debug(this + " ignoring: " + e);
            return;
        }

        if (hasFlag(Flag.EVENT_SILENT))
            return;

        //         //if (e.key.isSignal || e.key == LWKey.Location && e.source == this) {
        //         if (e.key == LWKey.UserActionCompleted || e.key == LWKey.Location && e.source == this) {
        //             // only keep if the location event is on us:
        //             // if this is our child that moved, obviously
        //             // clear the cache (we look different)
        //             //out("*** KEEPING IMAGE CACHE ***");
        //             ; // keep the cached image
        //         } else {
        //             //out("*** CLEARING IMAGE CACHE");
        //             //mImageBuffer = null;
        //         }

        mChangeSupport.notifyListeners(this, e);

        if (e.component == this && e.key instanceof Key) {
            // if parent is null, we're still initializing
            final Key key = (Key) e.key;

            //Log.debug("Checking for style update " + e + "\n\tisStyle: " + isStyle() + "\n\tisStyling: " + isStyling(key));

            if (isStyle() && isStyling(key))
                updateStyleWatchers(key, e);

            // sync sources not in use: never do this 2007-11-30 SMF
            //if (key.type == KeyType.DATA)
            //  syncUpdate(key);
        }

        //         if (isStyle() && getParent() == null)
        //             ; // ignore events from non-embedded style objects (e.g., EditorManager constructs)
        //         else
        //             mChangeSupport.notifyListeners(this, e);

        //         if (getParent() != null && e.component == this && e.key instanceof Key) {
        //             // if parent is null, we're still initializing
        //             final Key key = (Key) e.key;

        //             if (isStyle() && key.isStyleProperty)
        //                 updateStyleWatchers(key, e);

        //             // sync sources not in use: never do this 2007-11-30 SMF
        //             //if (key.type == KeyType.DATA)
        //             //  syncUpdate(key);
        //         }

        // labels need own call to this due to TextBox use of setLabel0
    }

    /** @return false -- subclasses can override */
    protected boolean permitZombieEvent(LWCEvent e) {
        return hasFlag(Flag.INTERNAL);
    }

    /** Copy the value for the given key either back to our sync source, or to our sync clients */
    private boolean syncUnderway = false;

    private void syncUpdate(Key key) {

        if (syncUnderway)
            return;

        syncUnderway = true;
        try {
            doSyncUpdate(key);
        } finally {
            syncUnderway = false;
        }
    }

    protected void doSyncUpdate(Key key) {
        // currently we only allow one or the other: you can be a source, or a client
        // this is all we need for now (a node can be synced to nodes on multiple
        // slides on different pathways, but a node in a slide can only refer
        // back to one source)
        if (mSyncSource != null) {
            Log.debug("[" + key + "] UPDATING SYNC SOURCE: " + this + " -> " + mSyncSource);
            if (!mSyncSource.isDeleted())
                key.copyValue(this, mSyncSource);

        } else if (mSyncClients != null && !mSyncClients.isEmpty()) {

            for (LWComponent c : mSyncClients) {
                Log.debug("[" + key + "] UPDATING SYNC CLIENT: " + this + " -> " + c);
                //Util.printStackTrace("SYNCTRACE " + this);
                if (!c.isDeleted())
                    key.copyValue(this, c);
            }
        }
    }

    /** If the event is a change for a style property, apply the change to all
    LWComponents that refer to us as their style parent */
    protected void updateStyleWatchers(Key key, LWCEvent e) {
        if (!isStyling(key) || mXMLRestoreUnderway) {
            // nothing to do if this isn't a style property that's changing
            return;
        }

        // Now we know a styled property is changing.  Since they Key itself
        // knows how to get/set/copy values, we can now just find all the
        // components "listening" to this style (pointing to it), and copy over
        // the value that just changed on the style object.

        if (DEBUG.Enabled)
            out("\nSTYLE OBJECT UPDATING STYLED CHILDREN with " + key + "; value " + Util.tags(e.getOldValue()));
        //final LWPathway path = ((MasterSlide)getParent()).mOwner;

        // We can traverse all objects in the system, looking for folks who
        // point to us.  But once slides are owned by the pathway, we'll have a
        // list of all slides here from the pathway, and we can just traverse
        // those and check for updates amongst the children, as we happen
        // to know that this style object only applies to slides
        // (as opposed to ontology style objects)

        // todo: this not a fast way to traverse & find what we need to change...
        for (LWComponent dest : findPotentialStyleWatchers()) {
            // we should never be point back to ourself, but we check just in case
            if (dest.mParentStyle == this && dest.supportsProperty(key) && dest != this) {
                // Only copy over the style value if was previously set to our existing style value
                try {
                    if (key.isStyleProperty()) {
                        if (key.valueEquals(dest, e.getOldValue())) {
                            key.copyValue(this, dest);
                        } else if (DEBUG.STYLE) {
                            System.err.println(" SKIP-USER-SET: " + dest + "; target has user-override value: "
                                    + Util.tags(key.getValue(dest)));
                        }
                    } else {
                        if (DEBUG.STYLE)
                            Log.debug("DATA-STYLE COPY " + key + " -> " + dest);
                        key.copyValue(this, dest);
                    }
                } catch (Throwable t) {
                    tufts.Util.printStackTrace(t, "Failed to copy value from " + e + " old=" + e.getOldValue());
                }
            }
        }
    }

    private Collection<LWComponent> findPotentialStyleWatchers() {
        final LWMap map;
        if (getParent() == null)
            //map = getClientData(LWMap.class, "styledMap");
            map = VUE.getActiveMap(); // a bit of a hack
        else
            map = getMap();
        return map.getAllDescendents(ChildKind.ANY);
        //return getMap().getAllDescendents(ChildKind.ANY);
    }

    /**
     * A third party can ask this object to raise an event
     * on behalf of the source.
     */
    public void notify(Object source, String what) {
        notifyLWCListeners(new LWCEvent(source, this, what));
    }

    void notifyProxy(LWCEvent e) {
        notifyLWCListeners(e);
    }

    //     /** This generates an event with NO COMPONENT IN IT: we just want access to the model hierarchy at
    //      * this point, but the component itself is not interesting here.
    //      */
    //     void notifyProxy(Object source, String what)
    //     {
    //         notifyProxy(new LWCEvent(source, null, what));
    //     }

    protected void notify(String what, LWComponent contents) {
        if (alive())
            notifyLWCListeners(new LWCEvent(this, contents, what));
    }

    protected void notify(String what, Object oldValue) {
        //         // TODO PERFORMANCE: have EVENT_SILENT always be true for initializing nodes -- this
        //         // should speed things up when creating hundreds/thousands(!) of nodes, including
        //         // during map restores -- we can skip creating an event that will never be delievered
        //         // for ever property set that happens
        //         if (hasFlag(Flag.EVENT_SILENT))
        //             return;
        if (alive())
            notifyLWCListeners(new LWCEvent(this, this, what, oldValue));
    }

    /** same as notify(String, Object), but will do notification even if the LWComponent isn't "alive" yet */
    protected void notifyForce(String what, Object oldValue) {
        notifyLWCListeners(new LWCEvent(this, this, what, oldValue));
    }

    protected void notify(Key key, Object oldValue) {
        if (alive())
            notifyLWCListeners(new LWCEvent(this, this, key, oldValue));
    }

    protected void notify(Key key, boolean oldValue) {
        if (alive())
            notify(key, oldValue ? Boolean.TRUE : Boolean.FALSE);
    }

    protected void notify(String what) {
        // todo: we still need both src & component? (this,this)
        notifyLWCListeners(new LWCEvent(this, this, what, LWCEvent.NO_OLD_VALUE));
    }

    /**a notify with an array of components
       added by Daisuke Fujiwara
     */
    protected void notify(String what, List<LWComponent> componentList) {
        notifyLWCListeners(new LWCEvent(this, componentList, what));
    }

    /**
     * Delete this single component: equivalent to a user-delete action. This component
     * will be removed from it's parent, and disconnected from all relationships in the
     * model.
     */
    public void delete() {
        if (!isDeleted())
            getParent().deleteChildPermanently(this);
        else
            Log.debug("attempt to delete already deleted: " + this);
    }

    /**
     * Do final cleanup needed now that this LWComponent has
     * been removed from the model.  Calling this on an already
     * deleted LWComponent has no effect. This will raise
     * an LWKey.Deleting event before the component is actually deleted.
     */
    protected void removeFromModel() {
        if (isDeleted()) {
            if (DEBUG.PARENTING || DEBUG.EVENTS)
                out(this + " removeFromModel(lwc): ignoring (already removed)");
            return;
        }
        if (DEBUG.PARENTING || DEBUG.EVENTS)
            out(this + " removeFromModel(lwc)");
        //throw new IllegalStateException(this + ": attempt to delete already deleted");
        setFlag(Flag.DELETING);
        notify(LWKey.Deleting);
        prepareToRemoveFromModel();
        removeAllLWCListeners();
        disconnectFromLinks(); // if any of the links themseleves are being deleted, we don't actually need to disconnect
        clearFlag(Flag.DELETING);
        setDeleted(true);
    }

    /**
     * For subclasses to override that need to do cleanup
     * activity before the the default LWComponent removeFromModel
     * cleanup runs.
     */
    protected void prepareToRemoveFromModel() {
    }

    /** undelete -- called just before the UndoManager calls setParent */
    protected void restoreToModel() {
        // TODO: UNDELETING flag may be functionally vestigal at this point.
        // Also, would want to split this in to a final restoreToModel and
        // an overridable restoreToModelImpl wrapped by the calls that
        // force having the UNDELETING bit set, if we really want to rely on it.

        setFlag(Flag.UNDELETING);

        if (DEBUG.PARENTING || DEBUG.EVENTS)
            out("restoreToModel: " + this);

        if (!isDeleted())
            if (DEBUG.Enabled)
                out("FYI: already restored");

        // we need this only in case node-icon preferences have changed since this object left the
        // model -- otherwise nothing could have happened to this component to change it.  Any
        // possible size/location events that could happen as a result will be ignored as this
        // component doesn't have it's parent set yet.
        layout();

        setDeleted(false);

        clearFlag(Flag.UNDELETING);
    }

    public boolean isDeleted() {
        return hasFlag(Flag.DELETED);
    }

    private void setDeleted(boolean deleted) {
        if (deleted) {
            //mHideBits |= HideCause.DELETED.bit; // direct set: don't trigger notify
            setFlag(Flag.DELETED);
            if (DEBUG.PARENTING || DEBUG.UNDO || DEBUG.EVENTS)
                if (parent != null)
                    out("parent not yet null in setDeleted true (ok for undo of creates)");
            this.parent = null;
        } else
            clearFlag(Flag.DELETED);
    }

    private void disconnectFromLinks() {
        // iterate through copy of the list, as it may be modified concurrently during removals
        if (mLinks != null) {
            for (LWLink link : mLinks.toArray(new LWLink[mLinks.size()]))
                link.disconnectFrom(this);
        }
        clearHidden(HideCause.PRUNE);
    }

    //     public void setSelected(boolean selected) {
    //         this.selected = selected;
    //     }
    //     public final boolean isSelected() {
    //         return this.selected;
    //     }
    public void setSelected(boolean selected) {
        setFlag(Flag.SELECTED, selected);
    }

    public final boolean isSelected() {
        return hasFlag(Flag.SELECTED);
    }

    protected boolean selectedOrParent() {
        return parent == null ? isSelected() : (isSelected() || parent.selectedOrParent());
        //return parent == null ? isSelected() : (parent.selectedOrParent() | isSelected());
    }

    public final boolean isAncestorSelected() {
        return parent == null ? false : parent.selectedOrParent();
    }

    public final boolean isPruned() {
        //return hasState(State.PRUNED); // for undo attempts
        return hasFlag(Flag.PRUNED);
    }

    public final void setPruned(boolean pruned) {
        //setState(State.PRUNED, pruned); // for undo attempts
        setFlag(Flag.PRUNED, pruned);
    }

    public static final Key KEY_State = new Key<LWComponent, Integer>("state") {
        @Override
        public void setValue(LWComponent c, Integer state) {
            c.mState = state;
        } // for undo

        @Override
        public Integer getValue(LWComponent c) {
            return c.mState;
        } // for undo/debug

        @Override
        public String getStringValue(LWComponent c) {
            return Integer.toHexString(c.mState);
        }
        // arch: would be nice if all the stuff for dealing with bitfields was handled in a single
        // BooleanKey which could be used for multiple bit fields.
    };

    public void setState(State s) {
        setState(s, true);
    }

    public void setState(State s, boolean on) {
        final int old = mState;
        if (on)
            mState |= s.bit;
        else
            mState &= ~s.bit;

        // Problems getting below to fully work:
        //      (1) map not repainting when undoing a clear all pruning (should be easy)
        //      (2) LWLink internal head/tail prune states also need to be synced -- would need to make these
        //      state bits as well for being able to undo individual mouse-click prunes.

        //         if (s == State.PRUNED) {
        //             // this is a hack, but could make this work for undo:
        //             if (on) {
        //                 if (LWLink.isPruningEnabled()) {
        //                     setHidden(HideCause.PRUNE, true);
        //                 }
        //             } else {
        //                 setHidden(HideCause.PRUNE, false);
        //             }
        //         }

        if (mState != old)
            notify(KEY_State, Integer.valueOf(old));
    }

    public boolean hasState(State s) {
        return (mState & s.bit) != 0;
    }

    public void setFlag(Flag flag) {
        if (DEBUG.DATA)
            out("setFlag " + flag);
        mFlags |= flag.bit;
    }

    public void clearFlag(Flag flag) {
        if (DEBUG.DATA)
            out("clearFlag " + flag);
        mFlags &= ~flag.bit;
    }

    public void setFlag(Flag flag, boolean set) {
        if (set)
            setFlag(flag);
        else
            clearFlag(flag);
    }

    public boolean hasFlag(Flag flag) {
        return (mFlags & flag.bit) != 0;
    }

    public boolean hasAnyFlag(int bits) {
        return (mFlags & bits) != 0;
    }

    public void setLocked(boolean locked) {
        if (hasFlag(Flag.LOCKED) != locked) {
            setFlag(Flag.LOCKED, locked);
            notify("locked");
        }
    }

    public final boolean isLocked() {
        return hasFlag(Flag.LOCKED);
    }

    public void setCollapsed(boolean collapsed) {
        // Default does nothing: only LWNode impl currebntly allows a collapsed state.
        // Move up the LWNode impl (which is generic) if we want more
        // than just LWNode's to support a collapsed state.
    }

    public boolean isCollapsed() {
        if (COLLAPSE_IS_GLOBAL)
            return false; // LWNode overrides
        //return isGlobalCollapsed;
        else
            return hasFlag(Flag.COLLAPSED);
    }

    public boolean isAncestorCollapsed() {
        //         if (COLLAPSE_IS_GLOBAL) {
        //             return isGlobalCollapsed;
        //         }

        if (parent != null) {
            if (parent.isCollapsed())
                return true;
            else
                return parent.isAncestorCollapsed();
        } else
            return false;
    }

    public Boolean getXMLlocked() {
        return isLocked() ? Boolean.TRUE : null;
    }

    public void setXMLlocked(Boolean b) {
        setLocked(b);
    }

    //     /** debug -- names of set HideBits */
    //     String getDescriptionOfSetBits() {
    //         StringBuffer buf = new StringBuffer();
    //         for (HideCause reason : HideCause.values()) {
    //             if (isHidden(reason)) {
    //                 if (buf.length() > 0)
    //                     buf.append(',');
    //                 buf.append(reason);
    //             }
    //         }
    //         return buf.toString();
    //     }

    String getDescriptionOfSetBits() {
        String s = "";
        if (mHideBits != 0)
            s += getDescriptionOfSetBits(HideCause.class, mHideBits);
        if (mFlags != 0) {
            if (s.length() > 0)
                s += "; ";
            s += getDescriptionOfSetBits(Flag.class, mFlags);
        }
        return s;
    }

    String getDescriptionOfSetBits(Class enumType, long bits) {
        final StringBuilder buf = new StringBuilder();
        //buf.append(enumType.getSimpleName());
        buf.append(enumType.getSimpleName().substring(0, 2));
        buf.append('(');
        boolean first = true;
        for (Object eValue : enumType.getEnumConstants()) {
            final Enum e = (Enum) eValue;
            if ((bits & (1 << e.ordinal())) != 0) {
                if (!first)
                    buf.append(',');
                buf.append(eValue);
                //buf.append(':');buf.append(e.ordinal());
                first = false;
            }
        }
        buf.append(')');
        return buf.toString();
    }

    public void setVisible(boolean visible) {
        setHidden(HideCause.DEFAULT, !visible);
    }

    public void setHidden(HideCause cause, boolean hide) {
        if (hide)
            setHidden(cause);
        else
            clearHidden(cause);
    }

    public void setHidden(HideCause cause) {
        if (DEBUG.EVENTS)
            out("setHidden " + cause);
        setHideBits(mHideBits | cause.bit);
    }

    public void clearHidden(HideCause cause) {
        //Log.debug(this, new Throwable("clearHidden"));
        if (DEBUG.EVENTS)
            out("clrHidden " + cause);
        setHideBits(mHideBits & ~cause.bit);
    }

    private void setHideBits(int bits) {
        final boolean wasHidden = isHidden();
        mHideBits = bits;
        if (wasHidden != isHidden())
            notify(LWKey.Hidden);
        //notify(LWKey.Hidden, wasHidden); // if we need it to be undoable
    }

    /**
     * @return true if this component has been hidden.  Note that this
     * is different from isFiltered.  All children of a hidden component
     * are also hidden, but not all children of a filtered component
     * are hidden.
     */
    public final boolean isHidden() {
        return !isVisible();
    }

    public boolean isHidden(HideCause cause) {
        return (mHideBits & cause.bit) != 0;
    }

    public boolean isVisible() {
        return mHideBits == 0;
    }

    /** persist with value true only if HideCause.DEFAULT is set */
    public Boolean getXMLhidden() {
        return isHidden(HideCause.DEFAULT) ? Boolean.TRUE : null;
    }

    public void setXMLhidden(Boolean b) {
        setVisible(!b.booleanValue());
    }

    /** persist with a true value only if HideCause.PRUNE is set */
    public Boolean getXMLpruned() {
        // note: could store this as two bits on the links instead and reconsitute
        // from that as opposed to saving on every node
        return isPruned() ? Boolean.TRUE : null;
    }

    public void setXMLpruned(Boolean b) {
        // note: should normally only be called if b is true,
        // as when false it shouldn't be persisted at all
        setPruned(b.booleanValue());
    }

    /** @deprecated -- use hasDraws() */
    public final boolean isDrawn() {
        return hasDraws();
    }

    /** @return true if ths component is going to be painting itself (independent of any children may do so) */
    public boolean isPainted() {
        return isVisible() && !isFiltered();
    }

    /** @return true if this node may have any drawing to do: (e.g., itself or children)
     * Note that a return of true does not guarantee that we will draw anything,
     * but if it returns false it does guarantee that nothing needs drawing */
    public boolean hasDraws() {
        if (isFiltered())
            return hasChildren();
        else
            return isVisible();
    }

    protected boolean updatingLinks() {
        return !isZoomedFocus() || DEBUG.VIEWER;
    }

    public void mouseEntered(MapMouseEvent e) {
        if (DEBUG.ROLLOVER)
            System.out.println("MouseEntered:     " + this);
        //e.getViewer().setIndicated(this);
        mouseOver(e);
    }

    public void mouseMoved(MapMouseEvent e) {
        //System.out.println("MouseMoved " + this);
        mouseOver(e);
    }

    public void mouseOver(MapMouseEvent e) {
        //System.out.println("MouseOver " + this);
    }

    public void mouseExited(MapMouseEvent e) {
        if (DEBUG.ROLLOVER)
            System.out.println(" MouseExited:     " + this);
        //e.getViewer().clearIndicated();
    }

    /** pre-digested single-click
     * @return true if you do anything with it, otherwise
     * the viewer can/will provide default action.
     */
    public boolean handleSingleClick(MapMouseEvent e) {
        return false;
    }

    /** pre-digested double-click
     * @return true if you do anything with it, otherwise
     * the viewer can/will provide default action.
     * Default action: if we have a resource, launch
     * it in a browser, otherwise, do nothing.
     */
    public boolean handleDoubleClick(MapMouseEvent e) {
        if (hasResource()) {
            out("Displaying content for: " + getResource());
            getResource().displayContent();
            return true;
        } else if (this instanceof LWGroup) // todo: in override
        {
            //} else if (this instanceof LWSlide || this instanceof LWGroup || this instanceof LWPortal)
            // MapViewer "null remote focal" code would need fixing to enable selection if a portal is the focal
            // (the selected objects are not children of the focal, so they don't look like we should be seeing them)
            VUE.getReturnToMapButton().setVisible(true);
            VUE.depthSelectionControl.setVisible(false);
            return doZoomingDoubleClick(e);
        } else
            return false;
    }

    public static final boolean SwapFocalOnSlideZoom = true;
    private static final boolean AnimateOnZoom = true;

    protected boolean doZoomingDoubleClick(MapMouseEvent e) {
        //   System.out.println("zooming double click");

        final MapViewer viewer = e.getViewer();

        if (viewer.getFocal() == this) {
            viewer.popFocal(MapViewer.POP_TO_TOP, MapViewer.ANIMATE);
            return true;
            //return false;
        }
        VUE.getActiveMap().setTempBounds(VUE.getActiveViewer().getVisibleMapBounds());
        final Rectangle2D viewerBounds = viewer.getVisibleMapBounds();
        final Rectangle2D mapBounds = getMapBounds();
        final Rectangle2D overlap = viewerBounds.createIntersection(mapBounds);
        final double overlapArea = overlap.getWidth() * overlap.getHeight();
        //final double viewerArea = viewerBounds.getWidth() * viewerBounds.getHeight();
        final double nodeArea = mapBounds.getWidth() * mapBounds.getHeight();
        final boolean clipped = overlapArea < nodeArea;

        final double overlapWidth = mapBounds.getWidth() / viewerBounds.getWidth();
        final double overlapHeight = mapBounds.getHeight() / viewerBounds.getHeight();

        final boolean focusNode; // otherwise, re-focus map

        // Note: this code is way more complicated than we're making use of right now --
        // we always fully load objects (slides) as the focal when we zoom to them.
        // This code permitted double-clicking through a slide-icon stack, where we'd
        // zoom to the slide icon, but retain the map focal.  The overlap herustics here
        // determined how much of the current view was occupied by the current clicked
        // on zoom-to object.  If mostly in view, assume we want to "de-focus" (zoom
        // back out to the map from our "virtual focal" zoomed-to node), but if mostly
        // not in view, re-center on this object.  When last tested, this was smart
        // enough to allow you to simply cycle through a stack of slide-icons with
        // double clicking on the exposed edge of the nearby slide icons (of course,
        // this code was on LWSlide back then...)

        if (DEBUG.Enabled) {
            outf(" overlapWidth %4.1f%%", overlapWidth * 100);
            outf("overlapHeight %4.1f%%", overlapHeight * 100);
            outf("clipped=" + clipped);
        }

        if (clipped) {
            focusNode = true;
        } else if (overlapWidth > 0.8 || overlapHeight > 0.8) {
            focusNode = false;
        } else
            focusNode = true;

        if (focusNode) {
            viewer.clearRollover();

            if (SwapFocalOnSlideZoom) {
                // loadfocal animate only currently works when popping (to a parent focal)
                //viewer.loadFocal(this, true, AnimateOnZoom);
                ZoomTool.setZoomFitRegion(viewer, mapBounds, 0, AnimateOnZoom);
                viewer.loadFocal(this);
            } else {
                ZoomTool.setZoomFitRegion(viewer, mapBounds, -LWPathway.PathBorderStrokeWidth / 2, AnimateOnZoom);
            }
        } else {
            // just re-fit to the map
            viewer.fitToFocal(AnimateOnZoom);
        }

        return true;
    }

    /** any XML tag found in a save file that does not match a mapping in from the current mapping file shows
     * up here -- they appear to always be instances of org.exolab.castor.types.AnyNode */
    public void addObject(final Object o) {
        final String s = o.toString();
        final String name;
        if (o instanceof String)
            name = "String[";
        else
            name = Util.tag(o);

        if (s.length() > 1024)
            Log.info(this + " ignoring XML: " + name + "[" + s.substring(0, 1024) + "...x" + s.length());
        else
            Log.info(this + " ignoring XML: " + name + s + "]");
    }

    /** interface {@link XMLUnmarshalListener} -- does nothing here */
    public void XML_initialized(Object context) {
        mXMLRestoreUnderway = true;
    }

    public void XML_fieldAdded(Object context, String name, Object child) {
        if (DEBUG.XML && DEBUG.META)
            out("XML_fieldAdded <" + name + "> = " + child);
    }

    /** interface {@link XMLUnmarshalListener} */
    public void XML_addNotify(Object context, String name, Object parent) {
        if (DEBUG.XML && DEBUG.META)
            tufts.Util.printClassTrace("tufts.vue",
                    "XML_addNotify; name=" + name + "\n\tparent: " + parent + "\n\t child: " + this + "\n");

        // TODO: moving this layout from old position at end of LWMap.completeXMLRestore
        // to here may have unpredictable results... watch of bad states after restores.
        // The advantage of doing it here is that virtual children are handled,
        // and "off map" children, such as slide children are properly handled.
        //layout("XML_addNotify");
    }

    /** interface {@link XMLUnmarshalListener} -- call's layout */
    public void XML_completed(Object context) {
        // 2007-06-12 SMF -- do NOT turn this off yet -- let the LWMap
        // turn it off when EVERYONE is done.
        //mXMLRestoreUnderway = false;

        /*
        // TODO: TEMPORARY DEBUG: never restore slides as format changes at moment
        //mSlides.clear();
        for (LWSlide slide : mSlides.values()) {
        // slides are virtual children of the node: we're their
        // parent, tho they're not formal children of ours.
        slide.setParent((LWContainer)this);
        // TODO: currently, this means non-container objects, such as LWImages,
        // can't have slides -- prob good to remove that restriction.
        // What would break if the parent ref were just a LWComponent?
        }
        */

        if (DEBUG.XML)
            System.out.println("XML_completed " + this);
        //layout(); need to wait till scale values are all set: so the LWMap needs to trigger this
    }

    /** clear the restore underway bit */
    public void markAsRestored() {
        mXMLRestoreUnderway = false;
    }

    protected static final double OPAQUE = 1.0;

    /**
     * @param alpha -- an alpha value for the whole image
     * @param maxSize -- if non-null, the max width/height of the produced image (may be smaller)
     * @param zoom -- a zoom for the map size in producing the image (currently ignored if maxSize is provided)
     */
    protected BufferedImage getAsImage(double alpha, Dimension maxSize, double zoom) {
        return createImage(alpha, maxSize, (Color) null, zoom);
    }

    public BufferedImage getAsImage(double alpha, Dimension maxSize) {
        return getAsImage(alpha, maxSize, 1.0);
    }

    public BufferedImage getAsImage(double zoom) {
        return getAsImage(OPAQUE, null, zoom);
    }

    public BufferedImage getAsImage() {
        return getAsImage(OPAQUE, null, 1.0);
    }

    public BufferedImage createImage(double alpha, Dimension maxSize) {
        return createImage(alpha, maxSize, null, 1.0);
    }

    /** @return the map bounds to use for rendering when generating an image of this LWComponent */
    protected Rectangle2D.Float getImageBounds() {
        final Rectangle2D.Float bounds = (Rectangle2D.Float) getPaintBounds().clone();

        int growth = 1; // just in case / rounding errors

        if (this instanceof LWMap)
            growth += 15;

        if (growth > 0)
            grow(bounds, growth);

        return bounds;
    }

    private static double computeZoomAndSize(Rectangle2D.Float bounds, Dimension maxSize, double zoomRequest,
            Size sizeResult) {
        double fitZoom = 1.0;

        if (maxSize != null) {
            if (bounds.width > maxSize.width || bounds.height > maxSize.height) {
                fitZoom = ZoomTool.computeZoomFit(maxSize, 0, bounds, null);
                sizeResult.width = (float) Math.ceil(bounds.width * fitZoom);
                sizeResult.height = (float) Math.ceil(bounds.height * fitZoom);
            }
        } else if (zoomRequest != 1.0) {
            sizeResult.width *= zoomRequest;
            sizeResult.height *= zoomRequest;
            fitZoom = zoomRequest;
        }

        return fitZoom;
    }

    /**
     * Create a new buffered image, of max dimension maxSize, and render the LWComponent
     * (and all it's children), to it using the given alpha.
     * @param alpha 0.0 (invisible) to 1.0 (no alpha)
     * @param maxSize max dimensions for image. May be null.  Image may be smaller than maxSize.
     * @param fillColor -- if non-null, will be rendered as background for image.  If null, presume alpha 0 fill.
     * @param zoomRequest -- desired zoom; ignored if maxSize is non-null
     * also set, background fill will have transparency of alpha^3 to enhance contrast.
     */

    // Note: as of Mac OS X 10.4.10 (Intel), when a java drag source declares it can
    // generate an image (as we do when we Apple-drag something), if you drop it on the
    // desktop, it will create a special mac "picture clipping", which is some kind of
    // raw format, probabaly TIFF, tho you CANNOT open these in Preview.  Apparently
    // there's some kind of bug in the special .pictClipping, where sometimes when
    // opening it up it shows entirely as a blank space (I think if the image starts to
    // get "very large"), tho the data is actually there -- if you drag the picture
    // clipping into an Apple Mail message, it shows up again (and if you dragged from
    // VUE to Apple Mail in the first place, it also works fine).  Note that AFTER
    // dragging into Apple Mail, you can THEN double-click the attachment, and it will
    // open it up in Preview as a .tiff file (Apple Mail appears to be converting the
    // .pictClipping to tiff).  Note that uncompressed TIFF isn't exactly a friendly
    // mail attachment format as it's huge.  But once you open the image in Preview, you
    // have the option of saving it / exporting it as a jpeg, and you can even adjust
    // the quality to your liking.

    public BufferedImage createImage(double alpha, Dimension maxSize, Color fillColor, double zoomRequest) {
        final Rectangle2D.Float bounds = getImageBounds();

        if (DEBUG.IMAGE) {
            System.out.println();
            System.out.println(TERM_CYAN + "createImage: " + this + "\n\t zoomRequst: " + zoomRequest
                    + "\n\t    maxSize: " + maxSize + "\n\t  mapBounds: " + fmt(bounds) + "\n\t  fillColor: "
                    + fillColor + "\n\t      alpha: " + alpha + TERM_CLEAR);
        }

        final Size imageSize = new Size(bounds);
        final double usedZoom = computeZoomAndSize(bounds, maxSize, zoomRequest, imageSize);

        // Image type ARGB is needed if at any point in the generated image, there is a
        // not 100% opaque pixel all the way through the background.  So TYPE_INT_RGB
        // will handle transparency with a map fine -- but we need TYPE_INT_ARGB if,
        // say, we're generating drag image that we want to be a borderless node (fully
        // transparent image border), or if the whole drag image itself is
        // semi-transparent.

        final int imageType;
        final int transparency;

        if (fillColor == null || alpha != OPAQUE || fillColor.getAlpha() != 255) {
            imageType = BufferedImage.TYPE_INT_ARGB;
            transparency = Transparency.TRANSLUCENT;
        } else {
            imageType = BufferedImage.TYPE_INT_RGB;
            transparency = Transparency.OPAQUE;
        }

        //        final boolean fillHasAlpha = (fillColor != null && fillColor.getAlpha() != 255);
        //         //if (alpha == OPAQUE && fillColor != null && fillColor.getAlpha() == 255) {
        //         if (alpha == OPAQUE && (fillColor == null || fillColor.getAlpha() == 255)) {
        //             imageType = BufferedImage.TYPE_INT_RGB;
        //             transparency = Transparency.OPAQUE;
        //         } else {
        //             imageType = BufferedImage.TYPE_INT_ARGB;
        //             transparency = Transparency.TRANSLUCENT;
        //         }

        final int width = imageSize.pixelWidth();
        final int height = imageSize.pixelHeight();

        if (width >= 512 || height >= 512)
            Log.info("creating large image: " + imageSize + " = approx " + Util.abbrevBytes(width * height * 4));

        try {
            Log.info(this + "; createImage:" + "\n\t requestSize: " + imageSize + "\n\trequestAlpha: " + alpha
                    + "\n\t requestFill: " + fillColor + "\n\t   pixelSize: " + width + "x" + height
                    + "\n\t renderScale: " + usedZoom + "\n\t        type: "
                    + (imageType == BufferedImage.TYPE_INT_RGB ? "RGB (opaque)" : "ARGB (translucent)"));
        } catch (Throwable t) {
            Log.error("logging", t);
        }

        //         if (DEBUG.IMAGE) out(TERM_CYAN
        //                              + "createImage:"
        //                              //+ "\n\tfinal size: " + width + "x" + height
        //                              + "\n\t neededSize: " + imageSize
        //                              + "\n\t   usedZoom: " + usedZoom
        //                              + "\n\t       type: " + (imageType == BufferedImage.TYPE_INT_RGB ? "OPAQUE" : "TRANSPARENT")
        //                              + TERM_CLEAR);

        if (mImageBuffer != null && mImageBuffer.getWidth() == width && mImageBuffer.getHeight() == height
                && mImageBuffer.getType() == imageType) {
            // todo: could also re-use if cached image is > our needed size as long it's
            // an ARGB and we fill it with full alpha first, tho we really shouldn't
            // have each component caching it's own image: some kind of small
            // recently used image buffers cache would make more sense.
            if (DEBUG.DND || DEBUG.IMAGE)
                out(TERM_CYAN + "\ngot cached image: " + mImageBuffer + TERM_CLEAR);
        } else {
            try {

                // TODO: manage this in a separate cache -- not one per node

                mImageBuffer = tufts.vue.gui.GUI.getDeviceConfigForWindow(null).createCompatibleImage(width, height,
                        transparency);

            } catch (Throwable t) {
                Log.error("creating image", t);
                Log.error("creating image: failing node: " + Util.tags(this));
                return null;
            }

            if (DEBUG.DND || DEBUG.IMAGE)
                out(TERM_RED + "created image: " + mImageBuffer + TERM_CLEAR);
            else
                Log.info("created image " + mImageBuffer);

        }

        drawImage((Graphics2D) mImageBuffer.getGraphics(), alpha, maxSize, fillColor, zoomRequest);

        return mImageBuffer;
    }

    /**
     * Useful for drawing drag images into an existing graphics buffer, or drawing exportable images.
     *
     * @param alpha 0.0 (invisible) to 1.0 (no alpha -- completely opaque)
     * @param maxSize max dimensions for image. May be null.  Image may be smaller than maxSize.
     * @param fillColor -- if non-null, will be rendered as background for image.
     * @param zoomRequest -- desired zoom; ignored if maxSize is non-null
     * also set, background fill will have transparency of alpha^3 to enhance contrast.
     */

    public void drawImage(Graphics2D g, double alpha, Dimension maxSize, Color fillColor, double zoomRequest) {
        //if (DEBUG.IMAGE) out("drawImage; size " + maxSize);

        final boolean drawBorder = false;// this instanceof LWMap; // hack for dragged images of LWMaps

        final Rectangle2D.Float bounds = getImageBounds();
        final Rectangle clip = g.getClipBounds();
        final Size fillSize = new Size(bounds);
        final double zoom = computeZoomAndSize(bounds, maxSize, zoomRequest, fillSize);

        if (DEBUG.IMAGE)
            out(TERM_GREEN + "drawImage:" + "\n\t   mapBounds: " + fmt(bounds) + "\n\t        fill: " + fillColor
                    + "\n\t     maxSize: " + maxSize + "\n\t zoomRequest: " + zoomRequest + "\n\t     fitZoom: "
                    + zoom + "\n\t    fillSize: " + fillSize + "\n\t          gc: " + g + "\n\t        clip: "
                    + fmt(clip) + "\n\t       alpha: " + alpha + TERM_CLEAR);

        final int width = fillSize.pixelWidth();
        final int height = fillSize.pixelHeight();

        final DrawContext dc = new DrawContext(g, this);

        dc.setInteractive(false);

        if (alpha == OPAQUE) {
            dc.setPrintQuality();
        } else {
            // if alpha, assume drag image (todo: better specified as an argument)
            dc.setDraftQuality();
        }

        dc.setBackgroundFill(getRenderFillColor(null)); // sure we want null here?
        dc.setClipOptimized(false); // always draw all children -- don't bother to check bounds
        if (DEBUG.IMAGE)
            out(TERM_GREEN + "drawImage: " + dc + TERM_CLEAR);

        if (fillColor != null) {
            //             if (false && alpha != OPAQUE) {
            //                 Color c = fillColor;
            //                 // if we have an alpha and a fill, amplify the alpha on the background fill
            //                 // by changing the fill to one that has alpha*alpha, for a total of
            //                 // alpha*alpha*alpha given our GC already has an alpha set.
            //                 fillColor = new Color(c.getRed(), c.getGreen(), c.getBlue(), (int) (alpha*alpha*255+0.5));
            //             }
            if (alpha != OPAQUE)
                dc.setAlpha(alpha, AlphaComposite.SRC); // erase any underlying in cache
            if (DEBUG.IMAGE)
                out("drawImage: fill=" + fillColor);
            g.setColor(fillColor);
            g.fillRect(0, 0, width, height);
        } else { //if (alpha != OPAQUE) {
            // we didn't have a fill, but we have an alpha: make sure any cached data is cleared
            // todo?: if fill is null, we need to clear as well -- it means we have implied alpha on any non-drawn bits
            // TODO: if this is a selection drag, we usually want to fill with the map color (or ideally, the color
            // of the common parent, e.g., a slide, if there's one common parent)
            dc.g.setComposite(AlphaComposite.Clear);
            g.fillRect(0, 0, width, height);
        }

        //if (alpha != OPAQUE)
        dc.setAlpha(alpha, AlphaComposite.SRC);

        if (DEBUG.IMAGE && DEBUG.META) {
            // Fill the entire imageable area
            g.setColor(Color.green);
            g.fillRect(0, 0, Short.MAX_VALUE, Short.MAX_VALUE);
        }

        final AffineTransform rawTransform = g.getTransform();

        if (zoom != 1.0)
            dc.g.scale(zoom, zoom);

        // translate so that the upper left corner of the map region
        // we're drawing is at 0,0 on the underlying image

        g.translate(-bounds.getX(), -bounds.getY());

        // GC *must* have a bounds set or we get NPE's in JComponent (textBox) rendering
        dc.setMasterClip(bounds);

        if (DEBUG.IMAGE && DEBUG.META) {
            // fill the clipped area so we can check our clip bounds
            dc.g.setColor(Color.red);
            dc.g.fillRect(-Short.MAX_VALUE / 2, -Short.MAX_VALUE / 2, // larger values than this can blow out internal GC code and we get nothing
                    Short.MAX_VALUE, Short.MAX_VALUE);
        }

        if (this instanceof LWImage) {
            // for some reason, raw images don't seem to want to draw unless we fill first
            dc.g.setColor(Color.white);
            dc.g.fill(bounds);
        }

        // render to the image through the DrawContext/GC pointing to it
        draw(dc);

        if (drawBorder) {
            g.setTransform(rawTransform);
            //g.setColor(Color.red);
            //g.fillRect(0,0, Short.MAX_VALUE, Short.MAX_VALUE);
            if (DEBUG.IMAGE) {
                g.setColor(Color.black);
                dc.setAntiAlias(false);
            } else
                g.setColor(Color.darkGray);
            g.drawRect(0, 0, width - 1, height - 1);
        }

        if (DEBUG.IMAGE)
            out(TERM_GREEN + "drawImage: completed\n" + TERM_CLEAR);

    }

    private String cleanControlChars(String s) {
        if (s == null)
            return null;
        String patternString = "";
        for (int i = 0; i < 9; i++) {
            patternString += "(\\u000" + i + ")|";
        }
        //u000D = carriage return
        //u000C = form feed
        //u000A = line feed
        /// = tab
        //(\\u000F)| = tab
        // = tab
        patternString += "(\\u000B)|(\\u000F)|(\\u0010)|(\\u0011)|(\\u0012)|(\\u0013)|(\\u0014)";
        patternString += "(\\u0015)|(\\u0016)|(\\u0017)";

        Pattern control = Pattern.compile(patternString); // need to make this better
        Matcher m = control.matcher(s);
        s = m.replaceAll("");

        // todo performance: if no modifications are made, pass
        // back the same passed in object

        return s;
    }

    /** subclasses override this to add info to toString()
     (return super.paramString() + new info) */
    public String paramString() {
        if (hasFlag(Flag.STYLE) || hasFlag(Flag.DATA_STYLE)) {
            return ColorToDebugString(getFillColor());
        } else {
            return String.format(" %+4.0f,%+4.0f %3.0fx%-3.0f", getX(), getY(), width, height);
        }
    }

    protected void out(String s) {
        //if (DEBUG.Enabled) Log.debug(s + "; " + this);
        //         String typeName = getClass().getSimpleName();
        //         if (typeName.startsWith("LW"))
        //             typeName = typeName.substring(2);
        //if (DEBUG.Enabled) LWLog.debug(String.format("%6s[%-12.12s] %s", typeName, getDisplayLabel(), s));
        LWLog.debug(String.format("%s %s", this, s));
    }

    protected void outf(String format, Object... args) {
        Util.outf(Log, format, args);
    }

    public String toString() {
        String typeName = getClass().getSimpleName();
        if (typeName.startsWith("LW"))
            typeName = typeName.substring(2);
        String label = "";
        String s;
        if (getLabel() != null) {
            label = " " + Util.tags(getDisplayLabel());
            //             if (true||isAutoSized())
            //                 label = " \"" + getDisplayLabel() + "\"";
            //             else
            //                 label = " (" + getDisplayLabel() + ")";
        }

        if (getID() == null) {
            s = String.format("%-15s[", String.format("%s.%08x", typeName, System.identityHashCode(this))
            //typeName + "." + Integer.toHexString(System.identityHashCode(this))
            );
            //s += tufts.Util.pad(9, Integer.toHexString(hashCode()));
        } else {
            s = String.format("%6s[%-8s", typeName, getID());
            //s = String.format("%-17s", typeName + "[" + getID());
            //s += tufts.Util.pad(4, getID());
        }
        //if (this.scale != 1f) s += "z" + this.scale + " ";
        s += describeBits();
        s += " " + paramString();
        if (getScale() != 1f)
            s += String.format(" z%.2f", getScale());
        //         if (mHideBits != 0) s += " " + getDescriptionOfSetBits(HideCause.class, mHideBits);
        //         if (mFlags != 0) s += " " + getDescriptionOfSetBits(Flag.class, mFlags);
        s += label;
        if (getResource() != null)
            s += " " + getResource().getSpec();
        //s += " <" + getResource() + ">";
        s += "]";
        return s;
    }

    protected String describeBits() {
        String s = "";
        if (mHideBits != 0)
            s += " " + getDescriptionOfSetBits(HideCause.class, mHideBits);
        if (mFlags != 0) {
            if (mHideBits != 0)
                s += " ";
            s += getDescriptionOfSetBits(Flag.class, mFlags);
        }
        return s;
    }

    public static void main(String args[]) throws Exception {
        VUE.init(args);

        /*
        for (java.lang.reflect.Field f : LWComponent.class.getDeclaredFields()) {
        Class type = f.getType();
        if (type == Key.class)
            System.out.println("KEY: " + f);
        else
            System.out.println("Field: " + f + " (" + type + ")");
        }
        */

        // for debug: ensure basic LW types created first

        new LWNode();
        new LWLink();
        new LWImage();

        //NodeTool.getTool();

        VueToolbarController.getController(); // make sure the tools are initialized

        edu.tufts.vue.style.StyleReader.readStyles("compare.weight.css");

        java.util.Set<String> sortedKeys = new java.util.TreeSet<String>(edu.tufts.vue.style.StyleMap.keySet());

        for (String key : sortedKeys) {
            final Object style = edu.tufts.vue.style.StyleMap.getStyle(key);
            System.out.println("Found CSS style key; " + key + ": " + style);
            //System.out.println("Style key: " + se.getKey() + ": " + se.getValue());
        }

        new LWNode().applyCSS(edu.tufts.vue.style.StyleMap.getStyle("node.w1"));
        new LWLink().applyCSS(edu.tufts.vue.style.StyleMap.getStyle("link.w1"));

    }

    protected static final org.apache.log4j.Logger LWLog = org.apache.log4j.Logger.getLogger(LW.class);

}

/** for debug */
final class LW {
}

/*
private final java.lang.reflect.Field field;
public Key(String name, String fieldName) {
    this(name);
    
    // this successfully auto-generates the slot reference, tho not really worth
    // it, as requiring the extra code snippet for grabbing the slot (Property)
    // object at least eliminates any typo's.  If we were to bother with this,
    // we'd want to generate a Field ref to an actual member field that had the
    // real value, and wasn't a slot.  Then the renderers, etc, could get
    // directly at the real value without using the slot -- a tad faster.  Then
    // stuff like the auto-notify code would all need to happen in the key, tho
    // then all our "traditional" setters (for hand-coding convenience, and at
    // least for save file backward compat) would need to use the Key to do the
    // setting for the appropriate triggers (except for "take" usage)
    
    java.lang.reflect.Field f = null;
    if (fieldName != null) {
        try {
            f = LWComponent.class.getField(fieldName);
            System.out.println("Found field: " + f);
        } catch (Throwable t) {
            tufts.Util.printStackTrace(t);
        }
    }
    field = f;
}
Property getSlot(LWComponent c) {
    try {
        return (Property) field.get(c);
    } catch (Throwable t) {
        tufts.Util.printStackTrace(t);
    }
    return null;
}
*/