package org.archstudio.bna.utils; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.awt.Insets; import java.util.Collection; import java.util.Collections; import java.util.Map; import org.archstudio.bna.IBNAModel; import org.archstudio.bna.IBNAWorld; import org.archstudio.bna.IThing; import org.archstudio.bna.IThingLogicManager; import org.archstudio.bna.constants.StickyMode; import org.archstudio.bna.facets.IHasAnchorPoint; import org.archstudio.bna.facets.IHasBoundingBox; import org.archstudio.bna.facets.IHasStickyShape; import org.archstudio.bna.facets.IHasWorld; import org.archstudio.bna.keys.IThingKey; import org.archstudio.bna.keys.IThingRefKey; import org.archstudio.bna.keys.ThingKey; import org.archstudio.bna.keys.ThingRefKey; import org.archstudio.bna.logics.coordinating.MirrorBoundingBoxLogic; import org.archstudio.bna.logics.coordinating.MirrorValueLogic; import org.archstudio.bna.logics.coordinating.OrientDirectionalLabelLogic; import org.archstudio.bna.logics.coordinating.StickPointLogic; import org.archstudio.bna.logics.events.WorldThingExternalEventsLogic; import org.archstudio.bna.things.labels.AnchoredLabelThing; import org.archstudio.bna.things.labels.BoundedLabelThing; import org.archstudio.bna.things.labels.DirectionalLabelThing; import org.archstudio.bna.things.shapes.EllipseThing; import org.archstudio.bna.things.shapes.EndpointThing; import org.archstudio.bna.things.shapes.MappingThing; import org.archstudio.bna.things.shapes.PolygonThing; import org.archstudio.bna.things.shapes.RectangleThing; import org.archstudio.bna.things.shapes.ReshapeHandleThing; import org.archstudio.bna.things.shapes.SplineThing; import org.archstudio.bna.things.utility.NoThing; import org.archstudio.bna.things.utility.WorldThing; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.swt.graphics.RGB; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * Assemblies link individual things together to make <i>simple</i> composite things. In an assembly of things, a single * thing is treated as the "root" of the assembly and the other things are treated as the "parts" of the assembly. It * doesn't matter what thing is the root and what things are the parts, so long as they are consistently used. A thing * may be a part of one (and only one) assembly and a root of one (and only one) assembly at a time. * <p> * Below is an example of an assembly of things to make a "component" assembly: * * <pre> * Assembly Root Key Things Assembly Part Key * * TOP * ____________________ * / (PART)/ * .<---------------- / WorldThing / <--- WORLD_KEY ---. * | /___________________/ | * | ____________________ | * | / (PART)/ | * |<---------------- / BoundedLabelThing / <--- TEXT_KEY ----| * | /___________________/ | * | ____________________ | * | / (ROOT)/ | * '--- ROOT_KEY ---> / RectangleThing / ----------------->' * /___________________/ * * BOTTOM * * </pre> * * The above things are created and organized by the following code. Note that the {@link MirrorValueLogic} is * automatically configured to mirror the bounds of the RectangleThing (the root) to the BoundedLabelThing and * WorldThing (the parts). * * <pre> * // Creates the Assembly * RectangleThing root = Assemblies.createRectangle(model, null, null); * Assemblies.addWorld(model, null, root); * * // Examples of Asembly Methods * WorldThing world = Assemblies.getPart(model, root, WORLD_KEY) // returns WorldThing * Assemblies.isRoot(root) // returns true * Assemblies.isPart(root) // returns false * Assemblies.getPartName(world) // returns WORLD_KEY * Assemblies.getRootWithPart(model, world) // returns root * </pre> * * Advantages of assemblies include: * <ul> * <li>Logics are automatically added and configured during creation of assemblies</li> * <li>Generally, properties only need to be set on one of the things (logics mirror those values to other things)</li> * <li>An entire assembly may be {@link #createRectangle(IBNAWorld, Object, IThing) constructed} and * {@link #removeRootAndParts(IBNAModel, IThing) removed} in unison</li> * <li>{@link #getEditableThing(IBNAModel, IThing, Class, String...) User editable} operations can be targeted to * different parts of an assembly</li> * </ul> * <p> * New Assemblies can be constructed in your own <i>MyAssemblies</i> class and by calling the static methods of this * class. */ @NonNullByDefault public final class Assemblies { private Assemblies() { } private static final IThingKey<Boolean> IS_ROOT_KEY = ThingKey.create("is-assembly-root"); private static final IThingRefKey<IThing> ROOT_KEY = ThingRefKey.create("my-assembly-root"); private static final IThingKey<IThingRefKey<?>> PART_NAME_KEY = ThingKey.create("my-assembly-part-name"); public static final boolean isRoot(IThing potentialRootThing) { checkNotNull(potentialRootThing); return potentialRootThing.has(IS_ROOT_KEY, true); } public static final boolean isPart(IThing potentialPartThing) { checkNotNull(potentialPartThing); return potentialPartThing.has(ROOT_KEY); } public static final void markRoot(IThing root) { checkNotNull(root); root.set(IS_ROOT_KEY, true); } public static final <T extends IThing> void markPart(IThing root, IThingRefKey<T> name, T part) { checkNotNull(root); checkNotNull(name); checkNotNull(part); markRoot(root); root.set(name, part.getID()); part.set(ROOT_KEY, root.getID()); part.set(PART_NAME_KEY, name); } public static final void unmarkPart(IBNAModel model, IThing part) { checkNotNull(model); checkNotNull(part); checkArgument(part.has(ROOT_KEY)); checkArgument(part.has(PART_NAME_KEY)); IThing root = checkNotNull(model.getThing(part.get(ROOT_KEY))); root.remove(part.get(PART_NAME_KEY)); part.remove(ROOT_KEY); part.remove(PART_NAME_KEY); } public static final void unmarkPart(IThing root, IThing part) { checkNotNull(root); checkNotNull(part); checkArgument(root.getID().equals(part.get(ROOT_KEY))); checkArgument(part.has(PART_NAME_KEY)); root.remove(part.get(PART_NAME_KEY)); part.remove(ROOT_KEY); part.remove(PART_NAME_KEY); } @SuppressWarnings("unchecked") @Nullable public static final <T extends IThing> T getPart(IBNAModel model, @Nullable IThing root, IThingRefKey<T> name) { checkNotNull(model); checkNotNull(name); if (root == null) { return null; } IThing t = name.get(root, model); if (t != null && t.has(ROOT_KEY, root.getID())) { return (T) t; } return null; } public static final Map<IThingRefKey<?>, IThing> getParts(IBNAModel model, @Nullable IThing root) { checkNotNull(model); if (root == null) { return Collections.emptyMap(); } Map<IThingRefKey<?>, IThing> allParts = Maps.newHashMap(); for (Map.Entry<IThingKey<?>, ?> entry : root.entrySet()) { if (entry.getKey() instanceof IThingRefKey) { IThing t = model.getThing(entry.getValue()); if (t != null && t.has(ROOT_KEY, root.getID())) { allParts.put((IThingRefKey<?>) entry.getKey(), t); } } } return allParts; } public static final IThingRefKey<?> getPartName(IThing part) { checkNotNull(part); return part.get(PART_NAME_KEY); } @Nullable public static final IThing getRootWithPart(IBNAModel model, @Nullable IThing part) { checkNotNull(model); if (part == null) { return null; } return model.getThing(part.get(ROOT_KEY)); } @Nullable public static final IThing getRoot(IBNAModel model, @Nullable IThing rootOrPart) { checkNotNull(model); if (rootOrPart == null) { return null; } if (isRoot(rootOrPart)) { return rootOrPart; } IThing rootThing = getRootWithPart(model, rootOrPart); if (rootThing != null) { return rootThing; } return rootOrPart; } public static final void removeRootAndParts(IBNAModel model, @Nullable IThing root) { checkNotNull(model); if (root == null) { return; } if (isPart(root)) { unmarkPart(model, root); } _removeRootAndParts(model, root); } private static final void _removeRootAndParts(IBNAModel model, @Nullable IThing root) { for (IThing part : getParts(model, root).values()) { _removeRootAndParts(model, part); } model.removeThing(root); } /** * Returns all of the things involved in an assembly assuming that the rootOrPart is a root, then assuming that the * rootOrPart is a part of an assembly. */ public static final Collection<IThing> getAssemblyThings(IBNAModel model, @Nullable IThing rootOrPart) { checkNotNull(model); if (rootOrPart == null) { return Collections.emptyList(); } Collection<IThing> allRelatedParts = Lists.newArrayList(); allRelatedParts.add(rootOrPart); // if this is a root add the assembly if (isRoot(rootOrPart)) { allRelatedParts.addAll(getParts(model, rootOrPart).values()); } // if this is a part of some assembly, add that assembly IThing otherRoot = getRootWithPart(model, rootOrPart); if (otherRoot != null) { allRelatedParts.add(otherRoot); allRelatedParts.addAll(getParts(model, otherRoot).values()); } return allRelatedParts; } /** * Returns the editable thing in an assembly that is an instance of the editableClass and has one of the specified * editableQualities, or <code>null</code> if there is none. */ @SuppressWarnings("unchecked") @Nullable public static final <T extends IThing> T getEditableThing(IBNAModel model, @Nullable IThing rootOrPart, Class<T> editableClass, String... editableQualities) { checkNotNull(model); checkNotNull(editableClass); checkNotNull(editableQualities); if (rootOrPart == null) { return null; } //HACK: a workaround for interacting with hierarchies, see #25 if (rootOrPart instanceof IHasWorld && ((IHasWorld) rootOrPart).getWorld() != null) { return null; } for (IThing editableThing : Assemblies.getAssemblyThings(model, rootOrPart)) { if (UserEditableUtils.isEditableForAllQualities(editableThing, editableQualities)) { if (editableClass.isInstance(editableThing)) { return (T) editableThing; } } } return null; } /** * Returns the thing that implements the specific class within an assembly, or <code>null</code> if there is none. */ @SuppressWarnings("unchecked") @Nullable public static final <T extends IThing> T getThingOfType(IBNAModel model, @Nullable IThing rootOrPart, Class<T> ofType) { checkNotNull(model); checkNotNull(ofType); if (rootOrPart == null) { return null; } //HACK: a workaround for interacting with hierarchies, see #25 if (rootOrPart instanceof IHasWorld && ((IHasWorld) rootOrPart).getWorld() != null) { return null; } for (IThing relevantThing : Assemblies.getAssemblyThings(model, rootOrPart)) { if (ofType.isInstance(relevantThing)) { return (T) relevantThing; } } return null; } /** * Returns the thing that has the given property within an assembly, or <code>null</code> if there is none. */ @Nullable public static final IThing getThingWithProperty(IBNAModel model, @Nullable IThing rootOrPart, IThingKey<?> withProperty) { checkNotNull(model); checkNotNull(withProperty); if (rootOrPart == null) { return null; } //HACK: a workaround for interacting with hierarchies, see #25 if (rootOrPart instanceof IHasWorld && ((IHasWorld) rootOrPart).getWorld() != null) { return null; } for (IThing relevantThing : Assemblies.getAssemblyThings(model, rootOrPart)) { if (relevantThing.has(withProperty) && relevantThing.get(withProperty) != null) { return relevantThing; } } return null; } public static enum Layer { BASE, SPLINE, MIDDLE, ARROWHEAD, TOP } public static final IThing getLayer(IBNAModel model, Object layerThingID) { IThing layerThing = model.getThing(layerThingID); if (layerThing == null) { layerThing = model.getThing(layerThingID); if (layerThing == null) { for (Object layerID : Layer.values()) { model.addThing(new NoThing(layerID)); } layerThing = model.getThing(layerThingID); } } if (layerThing == null) { throw new IllegalArgumentException("Unknown layer: " + layerThingID); } return layerThing; } public static final IThingRefKey<AnchoredLabelThing> ANCHORED_TEXT_KEY = ThingRefKey .create("assembly-anchored_text"); public static final IThingRefKey<BoundedLabelThing> BOUNDED_TEXT_KEY = ThingRefKey.create("assembly-bounded-text"); public static final IThingRefKey<DirectionalLabelThing> DIRECTION_KEY = ThingRefKey.create("assembly-direction"); public static final IThingRefKey<WorldThing> WORLD_KEY = ThingRefKey.create("assembly-world"); public static final EllipseThing createEllipse(IBNAWorld world, @Nullable Object id, @Nullable IThing parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); EllipseThing bkg = model .addThing(new EllipseThing(id), parent != null ? parent : getLayer(model, Layer.MIDDLE)); BoundedLabelThing label = model.addThing(new BoundedLabelThing(null), bkg); markRoot(bkg); markPart(bkg, BOUNDED_TEXT_KEY, label); IThingLogicManager tlm = world.getThingLogicManager(); MirrorBoundingBoxLogic mbbl = tlm.addThingLogic(MirrorBoundingBoxLogic.class); mbbl.mirrorBoundingBox(bkg, label, new Insets(3, 3, 3, 3)); return bkg; } public static final RectangleThing createRectangle(IBNAWorld world, @Nullable Object id, @Nullable IThing parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); RectangleThing bkg = model.addThing(new RectangleThing(id), parent != null ? parent : getLayer(model, Layer.MIDDLE)); BoundedLabelThing label = model.addThing(new BoundedLabelThing(null), bkg); markRoot(bkg); markPart(bkg, BOUNDED_TEXT_KEY, label); IThingLogicManager tlm = world.getThingLogicManager(); MirrorBoundingBoxLogic mbbl = tlm.addThingLogic(MirrorBoundingBoxLogic.class); mbbl.mirrorBoundingBox(bkg, label, new Insets(3, 3, 3, 3)); return bkg; } public static final <T extends IHasBoundingBox> T addWorld(IBNAWorld world, @Nullable Object id, T backgroundThing) { checkNotNull(world); checkNotNull(backgroundThing); IBNAModel model = world.getBNAModel(); WorldThing worldThing = model.addThing(new WorldThing(id), backgroundThing); markRoot(backgroundThing); markPart(backgroundThing, WORLD_KEY, worldThing); IThingLogicManager tlm = world.getThingLogicManager(); tlm.addThingLogic(WorldThingExternalEventsLogic.class); MirrorBoundingBoxLogic mbbl = tlm.addThingLogic(MirrorBoundingBoxLogic.class); mbbl.mirrorBoundingBox(backgroundThing, worldThing, new Insets(6, 6, 6, 6)); return backgroundThing; } public static final PolygonThing createPolygon(IBNAWorld world, @Nullable Object id, @Nullable IThing parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); PolygonThing bkg = model .addThing(new PolygonThing(id), parent != null ? parent : getLayer(model, Layer.MIDDLE)); markRoot(bkg); return bkg; } public static final EndpointThing createEndpoint(IBNAWorld world, @Nullable Object id, @Nullable IHasStickyShape parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); EndpointThing bkg = model.addThing(new EndpointThing(id), parent != null ? parent : getLayer(model, Layer.TOP)); bkg.setColor(new RGB(255, 255, 255)); bkg.setSecondaryColor(null); DirectionalLabelThing direction = model.addThing(new DirectionalLabelThing(null), bkg); direction.setLocalInsets(new Insets(2, 2, 2, 2)); markRoot(bkg); markPart(bkg, DIRECTION_KEY, direction); IThingLogicManager tlm = world.getThingLogicManager(); MirrorBoundingBoxLogic mbbl = tlm.addThingLogic(MirrorBoundingBoxLogic.class); mbbl.mirrorBoundingBox(bkg, direction, new Insets(0, 0, 0, 0)); if (parent != null) { StickPointLogic stickLogic = tlm.addThingLogic(StickPointLogic.class); stickLogic.stick(bkg, IHasAnchorPoint.ANCHOR_POINT_KEY, StickyMode.EDGE, parent); if (parent instanceof IHasBoundingBox) { OrientDirectionalLabelLogic orientLogic = tlm.addThingLogic(OrientDirectionalLabelLogic.class); orientLogic.orient((IHasBoundingBox) parent, direction); } } return bkg; } public static final SplineThing createSpline(IBNAWorld world, @Nullable Object id, @Nullable IThing parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); SplineThing bkg = model.addThing(new SplineThing(id), parent != null ? parent : getLayer(model, Layer.SPLINE)); markRoot(bkg); return bkg; } public static final MappingThing createMapping(IBNAWorld world, @Nullable Object id, @Nullable IThing parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); MappingThing bkg = model .addThing(new MappingThing(id), parent != null ? parent : getLayer(model, Layer.SPLINE)); markRoot(bkg); return bkg; } public static final ReshapeHandleThing createHandle(IBNAWorld world, @Nullable Object id, IThing parent) { checkNotNull(world); IBNAModel model = world.getBNAModel(); ReshapeHandleThing bkg = model.addThing(new ReshapeHandleThing(id), parent); markRoot(bkg); return bkg; } }