package org.archstudio.bna.utils; import java.awt.geom.Point2D; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.WeakHashMap; import org.archstudio.bna.IBNAModel; import org.archstudio.bna.IBNAView; import org.archstudio.bna.IBNAWorld; import org.archstudio.bna.ICoordinate; import org.archstudio.bna.ICoordinateMapper; import org.archstudio.bna.IMutableCoordinateMapper; import org.archstudio.bna.IThing; import org.archstudio.bna.IThingPeer; import org.archstudio.bna.facets.IHasAnchorPoint; import org.archstudio.bna.facets.IHasBoundingBox; import org.archstudio.bna.facets.IHasLife; import org.archstudio.bna.facets.IHasWorld; import org.archstudio.bna.facets.peers.IHasInnerViewPeer; import org.archstudio.bna.logics.background.LifeSapperLogic; import org.archstudio.bna.logics.background.RotatingOffsetLogic; import org.archstudio.bna.logics.coordinating.MirrorValueLogic; import org.archstudio.bna.logics.editing.SnapToGridLogic; import org.archstudio.bna.things.borders.PulsingBorderThing; import org.archstudio.bna.things.utility.EnvironmentPropertiesThing; import org.archstudio.bna.things.utility.WorldThingPeer; import org.archstudio.sysutils.Finally; import org.archstudio.sysutils.SystemUtils; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Lists; public class BNAUtils2 { /** The set of running flying threads. Synchronized with {@link BNAUtils#lock()}. */ private static final Map<ICoordinateMapper, FlyingThread> flyingThreads = new WeakHashMap<>(); /** * A reference to a thing. */ public static final class ThingReference { /** The view containing the thing, or <code>null</code> if unavailable. */ private final IBNAView view; /** The world containing the thing. */ private final IBNAWorld world; /** The model containing the thing. */ private final IBNAModel model; /** The thing. */ private final IThing thing; /** * Creates a new instance with a <code>null</code> view. * * @param world * The world containing the thing. * @param thing * The thing. */ public ThingReference(IBNAWorld world, IThing thing) { this.view = null; this.world = Preconditions.checkNotNull(world); this.model = world.getBNAModel(); this.thing = Preconditions.checkNotNull(thing); } /** * Creates a new instance with a view, world, and thing. * * @param view * The view containing the thing. * @param thing * The thing. */ public ThingReference(IBNAView view, IThing thing) { this.view = Preconditions.checkNotNull(view); this.world = view.getBNAWorld(); this.model = world.getBNAModel(); this.thing = Preconditions.checkNotNull(thing); } /** * Returns the view containing the thing, if available, or <code>null</code> otherwise. * * @return the view containing the thing, if available, or <code>null</code> otherwise. */ public @Nullable IBNAView getView() { return view; } /** * Returns the world containing the thing. * * @return the world containing the thing. */ public IBNAWorld getWorld() { return world; } /** * Returns the model containing the thing. * * @return the model containing the thing. */ public IBNAModel getModel() { return model; } /** * Returns the thing. * * @return the thing. */ public IThing getThing() { return thing; } } /** * A thing or a view under a location, not both. See {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} * for a description of how these are determined. */ public static final class ThingsAtLocation { /** The original view searched. */ private final IBNAView originalView; /** The original location searched. */ private final ICoordinate originalLocation; /** * The thing under the original location of the original view. See * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} for a description of how this is calculated. * (Mutable because {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} will directly set this). */ private ThingReference thingAtLocation = null; /** * The view under the original location of the original view. See * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} for a description of how this is calculated. * (Mutable because {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} will directly set this). */ private IBNAView viewAtLocation = null; /** * The thing under the original location, passing through empty space in worlds until a thing is hit, or * <code>null</code> if nothing is under the location. See * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} for a description of how this is calculated. * (Mutable because {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)} will directly set this). */ private ThingReference backgroundThingAtLocation = null; /** * Initializes this reference with the original view and location. * * @param originalView * The original view searched. * @param originalLocation * The original location searched. */ public ThingsAtLocation(IBNAView originalView, ICoordinate originalLocation) { this.originalView = Preconditions.checkNotNull(originalView); this.originalLocation = Preconditions.checkNotNull(originalLocation); } /** * Returns the original view searched. * * @return the original view searched. */ public IBNAView getOriginalView() { return originalView; } /** * Returns the original location searched. * * @return the original location searched. */ public ICoordinate getOriginalLocation() { return originalLocation; } /** * Returns the thing under the original location of the original view as calculated by * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)}. * * @return the thing under the original location of the original view as calculated by * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)}. */ @Nullable public ThingReference getThingAtLocation() { return thingAtLocation; } /** * Returns the view under the original location of the original view as calculated by * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)}. * * @return the view under the original location of the original view as calculated by * {@link BNAUtils2#getThingsAtLocation(IBNAView, ICoordinate)}. */ @Nullable public IBNAView getViewAtLocation() { return viewAtLocation; } /** * Returns the thing under the original location, passing through empty space in worlds until a thing is hit, or * <code>null</code> if nothing is under the location. * * @returns the thing under the original location, passing through empty space in worlds until a thing is hit, * or <code>null</code> if nothing is under the location. */ public ThingReference getBackgroundThingAtLocation() { return backgroundThingAtLocation; } /** * A convenience method that returns the thing under the location if {@link #getThingAtLocation()} returns a * non-null value, or <code>null</code> otherwise. * * @return the thing under the location if {@link #getThingAtLocation()} returns a non-null value, or * <code>null</code> otherwise. */ public IThing getThing() { return thingAtLocation != null ? thingAtLocation.getThing() : null; } /** * A convenience method that returns the thing's view under the location if {@link #getThingAtLocation()} * returns a non-null value, or {@link #getViewAtLocation()} otherwise. * * @return the thing's view under the location if {@link #getThingAtLocation()} returns a non-null value, or * {@link #getViewAtLocation()} otherwise. */ public IBNAView getView() { return thingAtLocation != null ? thingAtLocation.getView() : viewAtLocation; } } /** * A thread to "fly" to a {@link ICoordinateMapper} from its current location to a new location. */ private static final class FlyingThread extends Thread { /** The {@link ICoordinateMapper} used to fly. */ private final IMutableCoordinateMapper cm; /** The destination world point. */ private final Point toWorldPoint; /** The duration to take for the flight, in milliseconds. */ private final int durationMillis; /** The central point of the IBNAView control. */ private final Point controlCentralPoint; /** The starting world point. */ private final Point fromWorldPoint; /** The starting scale. */ private final double originalScale; /** The difference between the starting and destination world point. */ private final Point worldPointDiff; /** The difference between the starting and destination world point. */ private final double midpointScale; /** Whether the thread has finished. Used to stop the thread early if another flight takes place. */ private boolean finished = false; /** * Flies the coordinate mapper from its current location to a new location. * * @param view * The view that is being flown over. * @param cm * The coordinate mapper used to fly. * @param toWorldPoint * The destination world point. * @param durationMillis * The duration to take for the flight, in milliseconds. */ public FlyingThread(IBNAView view, IMutableCoordinateMapper cm, Point toWorldPoint, int durationMillis) { BNAUtils.checkLock(); Preconditions.checkArgument(durationMillis >= 0); this.cm = Preconditions.checkNotNull(cm); this.toWorldPoint = Preconditions.checkNotNull(toWorldPoint); this.durationMillis = durationMillis; // Determine the destination local point. Point controlSize = view.getBNAUI().getComposite().getSize(); controlCentralPoint = new Point(controlSize.x / 2, controlSize.y / 2); fromWorldPoint = cm.localToWorld(controlCentralPoint); originalScale = cm.getLocalScale(); worldPointDiff = new Point(toWorldPoint.x - fromWorldPoint.x, toWorldPoint.y - fromWorldPoint.y); // Determine the scale necessary at the midpoint to show both the start and end points in the view. double xScale = (double) controlSize.x / Math.abs(worldPointDiff.x); double yScale = (double) controlSize.y / Math.abs(worldPointDiff.y); midpointScale = Math.min(Math.min(xScale, yScale), cm.getLocalScale()); // Destroy any previously running threads. Otherwise, they will interfere with each other. try (Finally lock = BNAUtils.lock()) { FlyingThread previousThread = flyingThreads.get(cm); if (previousThread != null) { previousThread.finish(); } } } /** Performs the flight. */ @Override public void run() { try { long startTime = System.currentTimeMillis(); long endTime = startTime + durationMillis; double midpointScaleDiff = midpointScale - originalScale; long currentTimeMillis; while ((currentTimeMillis = System.currentTimeMillis()) < endTime) { // A value from 0 (origin) to 1 (destination). double distanceFactor = Math.sin(Math.PI / 2 * (currentTimeMillis - startTime) / durationMillis); // An intermediate point between fromWorldPoint and toWorldPoint, determined by distanceFactor. Point intermediateWorldPoint = new Point(// fromWorldPoint.x + SystemUtils.round(worldPointDiff.x * distanceFactor), // fromWorldPoint.y + SystemUtils.round(worldPointDiff.y * distanceFactor)); // An intermediate scale from 0 (origin), 1 (midpoint), 0 (destination). double scaleFactor = Math.sin(Math.PI * (currentTimeMillis - startTime) / durationMillis); double intermediateScale = Math.max(0.0001, originalScale + midpointScaleDiff * scaleFactor); try (Finally lock = BNAUtils.lock()) { if (finished) { break; } cm.setLocalScaleAndAlign(intermediateScale, controlCentralPoint, intermediateWorldPoint); } try { // Sleep for the remaining milliseconds in this frame (60 fps). synchronized (this) { sleep(System.currentTimeMillis() - currentTimeMillis + 1000 / 60); } } catch (InterruptedException e) { e.printStackTrace(); } } } finally { finish(); } } /** * Marks the flight finished, moves to the destination, and removes the thread. May be called multiple times and * before the flight finishes. */ public synchronized void finish() { try (Finally lock = BNAUtils.lock()) { if (!finished) { finished = true; cm.setLocalScaleAndAlign(originalScale, controlCentralPoint, toWorldPoint); flyingThreads.remove(cm); } } } } /** * Sets the "new thing spot" that logics use as the location for newly created things. * * @param world * The world in which to set the new thing spot. * @param worldX * The x location for the new spot. * @param worldY * The y location for the new spot. */ public static final void setNewThingSpot(IBNAWorld world, int worldX, int worldY) { Preconditions.checkNotNull(world); BNAUtils.checkLock(); EnvironmentPropertiesThing ept = EnvironmentPropertiesThing.createIn(world); if (world.getThingLogicManager().getThingLogic(SnapToGridLogic.class) != null) { int gridSpacing = GridUtils.getGridSpacing(world); worldX -= worldX % gridSpacing; worldY -= worldY % gridSpacing; ept.setNewThingSpot(new Point(worldX, worldY)); } else { ept.setNewThingSpot(new Point(worldX, worldY)); } } /** * Gets the "new thing spot" that logics use as the location for newly created things. * * @param world * The world in which to get the new thing spot. * @param increment * Whether to increment the "new thing spot" so that future calls will return a location to the right and * below that returned by this call. */ public static final Point getNewThingSpot(IBNAWorld world, boolean increment) { Preconditions.checkNotNull(world); BNAUtils.checkLock(); EnvironmentPropertiesThing ept = EnvironmentPropertiesThing.createIn(world); Point newThingSpot = ept.getNewThingSpot(); if (increment) { int gridSpacing = GridUtils.getGridSpacing(world); ept.setNewThingSpot(new Point(newThingSpot.x + gridSpacing, newThingSpot.y + gridSpacing)); } return newThingSpot; } /** * Performs a breadth first search of a view and all its descendants for things matching the predicate filter. * * @param view * The view to search. * @param filter * The filter to select things. * @return a list of things matching the filter. */ public static final List<ThingReference> findThings(IBNAView view, Predicate<IThing> filter) { List<ThingReference> references = Lists.newArrayList(); Set<IBNAWorld> worldsSearched = new HashSet<>(); Queue<IBNAView> viewsToSearch = new LinkedList<>(); viewsToSearch.add(view); while (!viewsToSearch.isEmpty()) { IBNAView viewToSearch = viewsToSearch.remove(); if (worldsSearched.add(viewToSearch.getBNAWorld())) { for (IThing thing : viewToSearch.getBNAWorld().getBNAModel().getAllThings()) { if (filter.apply(thing)) { references.add(new ThingReference(viewToSearch, thing)); } if (thing instanceof IHasWorld) { IBNAWorld world = ((IHasWorld) thing).getWorld(); if (world != null) { WorldThingPeer<?> peer = (WorldThingPeer<?>) viewToSearch.getThingPeer(thing); viewsToSearch.add(peer.getInnerView()); } } } } } return references; } /** * "Flies" a view to the specific location. * * @param view * The view with the world point to fly to. * @param toWorldPoint * The world point in the view to move to the center of the view. * @param duration * The number of milliseconds to remain in flight. */ public static final void flyViewTo(IBNAView view, Point toWorldPoint, int duration) { // If the given view is not the top-level view then translate the world coordinates into world coordinates // for the top view. Then change the view to be the top level view. if (view.getParentView() != null) { IBNAView topLevelView = view.getParentView(); while (topLevelView.getParentView() != null) { topLevelView = topLevelView.getParentView(); } Point2D localPoint = view.getCoordinateMapper().worldToLocal(BNAUtils.toPoint2D(toWorldPoint)); toWorldPoint = BNAUtils.toPoint(topLevelView.getCoordinateMapper().localToWorld(localPoint)); view = topLevelView; } final Control control = view.getBNAUI().getComposite(); if (control == null) { return; } final IMutableCoordinateMapper cm = SystemUtils.castOrNull(view.getCoordinateMapper(), IMutableCoordinateMapper.class); if (cm == null) { return; } FlyingThread thread = new FlyingThread(view, cm, toWorldPoint, duration); thread.start(); } /** * Creates a pulsing, expanding marker around a thing. Used to highlight the position of the thing when searching * for it. * * @param world * The world in which to create the pulse. * @param thing * The thing around which to do the pulse notification, or <code>null</code>, in which case no pulse is * created. * @param duration * The number of milliseconds to perform the pulse. */ public static final void pulseNotify(IBNAWorld world, @Nullable IThing thing, int duration) { Preconditions.checkNotNull(world); BNAUtils.checkLock(); if (thing != null) { MirrorValueLogic mirrorLogic = world.getThingLogicManager().addThingLogic(MirrorValueLogic.class); world.getThingLogicManager().addThingLogic(RotatingOffsetLogic.class); world.getThingLogicManager().addThingLogic(LifeSapperLogic.class); PulsingBorderThing pulseThing = null; if (thing instanceof IHasBoundingBox) { pulseThing = world.getBNAModel().addThing(new PulsingBorderThing(null)); mirrorLogic.mirrorValue(thing, IHasBoundingBox.BOUNDING_BOX_KEY, pulseThing); } if (pulseThing != null) { pulseThing.set(IHasLife.LIFE_KEY, duration); } } } /** * Returns a reasonable point that represents a thing, or <code>null</code> if none can be determined. * * @param world * The world containing the thing. * @param thing * The thing to find a central point for. * @return A central point for the thing, or <code>null</code> if none can be found. */ public static final Point getRepresentativePoint(IBNAWorld world, IThing thing) { IThing rootThing = Assemblies.getRoot(world.getBNAModel(), thing); // Check for an anchor point. IHasAnchorPoint anchoredThing = Assemblies.getThingOfType(world.getBNAModel(), rootThing, IHasAnchorPoint.class); if (anchoredThing != null) { return BNAUtils.toPoint(anchoredThing.getAnchorPoint()); } // Check for a bounding box. IHasBoundingBox boundedThing = Assemblies.getThingOfType(world.getBNAModel(), rootThing, IHasBoundingBox.class); if (boundedThing != null) { Rectangle r = boundedThing.getBoundingBox(); return new Point(r.x + r.width / 2, r.y + r.height / 2); } return null; } /** * Determining what is under a point is complicated by the presence of sub-worlds and, specifically, empty space in * sub-worlds. For purposes of explaining the complication, assume there are three things, stacked in the following * order (from back to front): * <ol> * <li>A brick thing representing the outer component containing the sub-structure.</li> * <li>A mapping thing from an interface on the outer component to an interface in the sub-structure.</li> * <li>The world thing containing the sub-structure.</li> * </ol> * <p/> * In this context, consider the possible user actions and behaviors: * <ul> * <li>Left clicking on a component in the sub-structure selects that component.</li> * <li>Right clicking on a component in the sub-structure displays the context menu for that component.</li> * <li>Left clicking on the interface mapping selects that mapping.</li> * <li>Right clicking on the interface mapping displays the context menu for that interface mapping.</li> * <li>However, left clicking on an empty space in the sub-structure could mean two different things: * <ul> * <li>Select the outer component.</li> * <li>Start a marquee selection box in the sub-structure.</li> * </ul> * <li>Right clicking on an empty space in the sub-structure can also mean two different things: * <ul> * <li>Display a context menu for the outer component.</li> * <li>Display a context menu for the sub-structure.</li> * </ul> * </ul> * We handle the ambiguities as follows: * <ol> * <li>If a thing is under a location (possibly passing through empty space in worlds), and it does <b>not</b> have * a world in its assembly, we assume the user is interacting with that thing. In out example, if the user clicks on * the mapping, they are interacting with the mapping.</li> * <li>If a thing is under the location, and it <b>does</b> have a world in its assembly, and we have passed through * empty space in a world, we assume the user is interacting with the world rather than the thing. In our example, * if a user clicks on empty space in the sub-structure (even though there is the outer component behind it), they * are interacting with the sub-structure.</li> * <li>If a thing is under the location, and it <b>does</b> have a world in its assembly, and we have <b>not</b> * passed through empty space in a world, we assume the user is interacting with the thing. In our example, if a * user clicks on the outer component, outside the sub-structure, they are interacting with the outer component. * </li> * <li>If there is only empty space (possibly passing through empty space in worlds) under the location, we return * the inner most world.</li> * <li>If there is nothing under the location, we return the top-most world.</li> * </ol> * This doesn't work in all circumstances, but seems to work in most cases. * <p/> * Finally, the background thing is the thing that is under the location when simply passing through empty space of * worlds. This is used, for example, when determining what tool tip should be displayed for the location of the * pointer. * * @param view * The view containing the top world to search. * @param location * The location to search. * @return an instance of {@link ThingsAtLocation} with a non-null {@link ThingsAtLocation#getThingAtLocation()} if * a thing is under the location, otherwise a non-null {@link ThingsAtLocation#getViewAtLocation()} for the * view under the location. */ public static final ThingsAtLocation getThingsAtLocation(IBNAView view, ICoordinate location) { boolean passedThroughWorld = false; ThingsAtLocation thingsAtLocation = new ThingsAtLocation(view, location); thingsAtLocation.viewAtLocation = view; for (IThing thing : view.getThingsAt(location)) { if (thing instanceof IHasWorld) { // If a world, search within it. IThingPeer<?> worldPeer = view.getThingPeer(thing); if (worldPeer instanceof IHasInnerViewPeer) { IBNAView innerView = ((IHasInnerViewPeer<?>) worldPeer).getInnerView(); if (innerView != null) { passedThroughWorld = true; ICoordinate innerLocation = DefaultCoordinate.forLocal(location.getLocalPoint(), innerView.getCoordinateMapper()); ThingsAtLocation innerThingsAtLocation = getThingsAtLocation(innerView, innerLocation); if (innerThingsAtLocation.thingAtLocation != null) { return innerThingsAtLocation; } if (innerThingsAtLocation.viewAtLocation != null) { thingsAtLocation.viewAtLocation = innerThingsAtLocation.viewAtLocation; } thingsAtLocation.backgroundThingAtLocation = innerThingsAtLocation.backgroundThingAtLocation; } } } else { if (passedThroughWorld && Assemblies.getThingOfType(view.getBNAWorld().getBNAModel(), thing, IHasWorld.class) != null) { if (thingsAtLocation.backgroundThingAtLocation == null) { thingsAtLocation.backgroundThingAtLocation = new ThingReference(view, thing); } return thingsAtLocation; } thingsAtLocation.viewAtLocation = null; thingsAtLocation.thingAtLocation = new ThingReference(view, thing); thingsAtLocation.backgroundThingAtLocation = thingsAtLocation.thingAtLocation; return thingsAtLocation; } } return thingsAtLocation; } }