/* * 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 java.util.*; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; /** * Traversals are used to descend into a map, starting from any given root * node, and visiting desired nodes based on the accept and acceptTraversal * methods. If acceptTraversal returns true, the node's children are * traversed, if not, all the children are excluded. If accept returns * true, and all ancestors acceptTraversal returned true, the node is visited. * * If POST_ORDER, an accepted node who's acceptTraversal returns false * is also not accepted, if PRE_ORDER, and accepted node is visited even * if it's acceptTraversal returns false. * * This class is meant to be overridden to do something useful. * * @version $Revision: 1.54 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $ * @author Scott Fraize * */ // todo for core traversal functionality: add capability for handling // LWComponent.ChildKind, so we have the option of traversing ChildKind.ANY, as opposed // to the current impl, which does a traversal that gives us all the same components // that ChildKind.PROPER does. // todo: now that LWTraversal takes a PickContext, this is really no longer a generic // traversal: it's a PickTraversal public class LWTraversal { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(LWTraversal.class); protected static final boolean PRE_ORDER = true; protected static final boolean POST_ORDER = false; protected final boolean preOrder; protected boolean done = false; protected int depth = 0; protected int depthVisible = 0; protected final PickContext pc; private final List<LWComponent> pickCache = new ArrayList(); private boolean iteratingPickCache; /** Note: if preOrder is true, a node can be visited if accept(node) is true, even if acceptTraversal(node) is false */ LWTraversal(boolean preOrder, PickContext pc) { this.preOrder = preOrder; this.pc = pc; if (DEBUG.PICK && DEBUG.WORK) Log.debug("pick cache: " + Util.tags(pickCache)); } LWTraversal(PickContext pc) { this(POST_ORDER, pc); } public void traverse(LWComponent c) { if (c == null) return; if (preOrder && accept(c)) { visit(c); if (done) return; } if (!acceptTraversal(c)) { if (DEBUG.PICK) eoutln("TRV DENY: " + c); } else { if (DEBUG.PICK) eoutln("Travers0: " + c); if (acceptChildren(c)) { final boolean painted = c.isPainted(); if (DEBUG.PICK) eoutln("Travers1: " + c + "; painted=" + painted); depth++; if (painted) depthVisible++; List<LWComponent> pickables = null; final List<LWComponent> curCache; if (iteratingPickCache) { // We really only need this if getPickList actually returns slide icons, // but we don't know that in advance, so we have to allocate just in case. // This currently only happens if we encounter a node with a slide icon that // is a descendent of another node that also has a slide icon. if (DEBUG.PICK && DEBUG.WORK) Log.debug("allocating tmp pick cache for " + c); curCache = new ArrayList(); } else { curCache = pickCache; } try { // todo performace: get rid of all this pickCache stuff, and change // getPickList to getPickIter, and require it to iterate in the pick // order (reverse of child order), and the impl's can use some kind // of ReverseListIter by default, and combine it with some kind of // GroupIterator that would wrap children iterator + seenSlideIcons // iter (and then apply the reverse on top of it). if (pc != null) pickables = c.getPickList(pc, curCache); else pickables = c.getChildList(); } catch (Throwable t) { Util.printStackTrace(t, "pickList fetch failed for " + c + "; pickables=" + pickables); } if (depth > 15) { Util.printStackTrace("aborting pick at depth " + depth + " in case of loop; pickables: " + pickables); done = true; return; } if (pickables == pickCache) iteratingPickCache = true; try { if (DEBUG.PICK && DEBUG.META) Util.dumpCollection(pickables); traversePicks(c, pickables); } catch (Throwable t) { Util.printStackTrace(t, "traversal failure on " + c + "; pickables=" + pickables); } finally { depth--; if (painted) depthVisible--; if (pickables == pickCache) iteratingPickCache = false; } // if (true || c.isManagingChildLocations()) // traverseChildrenZoomFocusIsUnderSiblings(c.getPickList(pc, pickList)); // //traverseChildrenZoomUnderSiblings(c.getChildList()); // else // traverseChildren(c.getPickList(pc, pickList)); // //traverseChildren(c.getChildList()); } if (done) return; if (!preOrder && accept(c)) visit(c); } } public boolean acceptChildren(LWComponent c) { return true; } public void traversePicks(LWComponent curTop, java.util.List<LWComponent> children) { // if we encounter a zoomed rollover, all siblings get priority // (so you can get to siblings that might have been obscured by it's increased size) // Note that this shouldn't be used in instances where the siblings might be actually // overlapping when non-zoomed, as we get flashing back and forth every time // the mouse moves. LWComponent zoomedFocus = null; if (DEBUG.PICK && DEBUG.META) eoutln("TRAVERSE " + Util.tags(children)); for (ListIterator<LWComponent> i = children.listIterator(children.size()); i.hasPrevious();) { final LWComponent c = i.previous(); if (c == curTop) { Util.printStackTrace("found local root in pick list (loop!), skipping: " + c); continue; } if (false && c.isZoomedFocus()) { // Sibling priority turned off 2008-04-22 -- SMF zoomedFocus = c; } else { traverse(c); if (done) return; } } if (zoomedFocus != null) traverse(zoomedFocus); } // public void traverseChildren(java.util.List<LWComponent> pickChildren) // { // // default behaviour of traversals is to traverse list in reverse so // // that top-most components are seen first // // TODO: could more cleanly handle our slide-icon hack by having a special // // call to ask for the child list, and if someone has a slide icon, return // // a list with that always at the the end (on top) // for (ListIterator<LWComponent> i = pickChildren.listIterator(pickChildren.size()); i.hasPrevious();) { // traverse(i.previous()); // if (done) // return; // } // } // public void traverseChildrenZoomFocusIsUnderSiblings(java.util.List<LWComponent> children) // { // // if we encounder a zoomed rollover, all siblings get priority // // (so you can get to siblings that might have been obscurved by it's increased size) // // Note that this shouldn't be used in instances where the siblings might be actually // // overlapping when non-zoomed, as we get flashing back and forth every time // // the mouse moves. // LWComponent zoomedFocus = null; // if (DEBUG.PICK && DEBUG.META) eoutln("TRAVERSE " + Util.tags(children)); // for (ListIterator<LWComponent> i = children.listIterator(children.size()); i.hasPrevious();) { // final LWComponent c = i.previous(); // if (c.isZoomedFocus()) { // zoomedFocus = c; // } else { // traverse(c); // if (done) // return; // } // } // if (zoomedFocus != null) // traverse(zoomedFocus); // } /** It's possible we may accept the traversal of an object, without accepting it for a visit * (e.g., we're only interesting in visiting children). However, if we're POST_ORDER, * and a node is NOT accepted for traversal, it is also not accepted for visiting. */ public boolean acceptTraversal(LWComponent c) { return c.hasPicks(); } //public boolean acceptTraversal(LWComponent c) { return c.hasChidren(); } /** * @return true if this component meets our criteria for visiting */ public boolean accept(LWComponent c) { return true; } /** Visit the node: it's been accepted, now analyize and/or do something with it */ public void visit(LWComponent c) { eoutln("VISITED: " + c); } protected void eout(String s) { for (int x = 0; x < depth; x++) System.out.print(" "); System.out.print(s); } protected void eoutln(String s) { eout(s + "\n"); } /** * The primary code for interative, GUI based traversals of the map. Based on the PickContext, * this will visit all "relevant" nodes in a current viewer (not hidden, filtered, possibly of * a desired type (e.g., modified by the kind of tool currently active), etc.) */ public static abstract class Picker extends LWTraversal { //protected final PickContext pc; //protected final boolean strayChildren = true; // for now, always search for children even outside of bounds (performance issue only) Picker(PickContext pc) { //this.pc = pc; super(pc); if (DEBUG.PICK) System.out.println("Picker created: " + getClass().getName() + "; " + pc); } /** If we reject traversal, we are also rejecting all children of this object */ @Override public boolean acceptTraversal(LWComponent target) { if (target == pc.dropping) { if (DEBUG.PICK) eoutln("DENIED: dropping == target: " + target); return false; } if (target != pc.root) { // The below checks never apply to the root if (target.isLocked()) { if (DEBUG.PICK) eoutln("DENIED: locked " + target); return false; } if (!target.isDrawn()) { if (DEBUG.PICK) eoutln("DENIED: not-drawn " + target); return false; } // TODO BUG: if any parent(s) are filtered, we need to allow // depths down through all filtered parents to select any // still visible children. if (depthVisible > pc.maxDepth) { if (DEBUG.PICK) eoutln("DENIED: depth " + target + " depthVisible " + depthVisible + " > maxDepth " + pc.maxDepth); return false; } } if (pc.dropping != null && target instanceof LWLink && pc.dropping instanceof LWComponent) { // If we're dragging a node around a map that has links to it, and // we drag it over another node to drop it in, we'd often hit (pick) the // connected links, and in this case, we clearly should just be ignoring // them (never picking any links connected to us). // This code would probably be better handled via some LWComponent API, // so we're not directly referring to LWLink here. if (((LWLink)target).hasEndpoint((LWComponent)pc.dropping)) { if (DEBUG.PICK) eoutln("DENIED: dropping: " + pc.dropping + "; onto connected link: " + target.getDiagnosticLabel()); return false; } } //else return strayChildren || c.contains(mapX, mapY); // for now, ALWAYS work as if strayChildren was true return true; } @Override public boolean acceptChildren(LWComponent c) { // NEED TO ALLOW THIS FOR GROUPS, so can // use our new clean pickDistance for finding // the children first, then letting the group's // pickChild choose itself or the child depending // on pickDepth -- can we integrate with our // getPickLevel() feature? Just throw it out if we gotta.. //return pc.pickDepth >= c.getPickLevel(); //return true; //return c.hasChildren(); if (c.isPathwayOwned() && c instanceof LWSlide && pc.root != c) return false; else return c.hasPicks(); } /** Should we visit the given LWComponent to see if we might have picked it? * This is in effect a fast-reject. */ @Override public boolean accept(LWComponent c) { if (c == pc.excluded) return false; else if (c.isFiltered()) // note: children may NOT be filtered out, but default acceptTraversal will get us there return false; else if (pc.ignoreSelected && c.isSelected()) return false; //else if (pc.pickType != null && !pc.pickType.isInstance(c)) else if (pc.acceptor != null && !pc.acceptor.accept(pc, c)) return false; else return true; } /** create subclasses with this overridden to do useful stuff */ public abstract void visit(LWComponent c); } public static class PointPick extends LWTraversal.Picker { private final Point2D.Float mapPoint = new Point2D.Float(); private final Point2D.Float zeroPoint = new Point2D.Float(); private LWComponent hit; private LWComponent closeHit; private float closestDistSq = Float.POSITIVE_INFINITY; public PointPick(PickContext pc) { super(pc); mapPoint.x = pc.x; mapPoint.y = pc.y; } public PointPick(MapMouseEvent e) { this(e.getViewer().getPickContext(e.getMapX(), e.getMapY())); } // public boolean acceptTraversal(LWComponent c) { // if (super.acceptTraversal(c)) { // return true; // } else { // return false; // } // } @Override public void visit(LWComponent c) { if (DEBUG.PICK) eout(" VISIT: " + c); // todo performance: if we don't ever move to having cached map transforms, // we could keep an AffineTransform in the picker, and for each traversal, // apply transformDownA on a clone of the top level object, passing that // transform down to further depth visits to be cloned and hava // transformDownA applied to it. Then use that transform here to apply it's // inverse transform to the point. c.transformMapToZeroPoint(mapPoint, zeroPoint); //if (DEBUG.PICK && DEBUG.META) eoutln("relative pick: " + zeroPoint); // note: passing an uncloned PickContext down to each visited component // is a bit risky, as all implementations must be sure not to modify // it in any way. final float hitResult = c.pickDistance(zeroPoint.x, zeroPoint.y, pc); //if (DEBUG.PICK && DEBUG.META) { if (DEBUG.PICK) { if (hitResult > 0) System.out.format("; distance=%.2f\n", Math.sqrt(hitResult)); else System.out.println(""); } // Note that as soon as a we have a direct-hit, we stop checking for close // hits, even tho we may ultimately pick a close hit instead. This sounds // wrong at first because how can we know the closest if we haven't checked // all possibilities? This is okay because the only time we prioritize a // close-hit over a direct hit is if the close hit is a descendent of the // direct hit (the direct hit is the "background" of the close hit), and we // can only get a direct hit on an ancestor (background) of a close hit once // we've checked for close-hits on all it's descendents (picking is a // depth-first visiting operation). if (hitResult == 0) { // zero distance means direct hit within the visible bounds of the object hit = c; done = true; } else if (hitResult < 0) { // distance result -1: do nothing -- a complete miss } else if (hitResult < closestDistSq) { // the result is the square of the distance from the object closeHit = c; closestDistSq = hitResult; } // if (c.contains(p, pc.zoom)) { // // If we expand impl to handle the contained children optimization (non-strayChildren): // // Since we're POST_ORDER, if strayChildren is false, we already know this // // object contains the point, because acceptTraversal had to accept it. // //System.out.println("hit with " + p); // hit = c; // done = true; // return; // } } public static LWComponent pick(PickContext pc) { return new PointPick(pc).traverseAndPick(pc.root); } public static LWComponent pick(MapMouseEvent e) { PointPick pick = new PointPick(e); return pick.traverseAndPick(pick.pc.root); // pick.traverse(pick.pc.root); // return pick.getPicked(); } public LWComponent traverseAndPick(LWComponent root) { if (DEBUG.PERF) { final long start = System.nanoTime(); traverse(root); final LWComponent pick = getPicked(); final long delta = System.nanoTime() - start; Log.debug(String.format("PointPick elapsed: %.1fms", delta / 100000.0)); return pick; } else { traverse(root); return getPicked(); } } public LWComponent getPicked() { if (DEBUG.PICK) { if (pc.dropping != null) eoutln("PointPick: DROPPING: " + Util.tags(pc.dropping)); eoutln("PointPick: DIRECT-HIT: " + hit); eout(String.format("PointPick: CLOSE-HIT: %s; distance=%.2f;", closeHit, Math.sqrt(closestDistSq))); } if (hit == null || (closeHit != null && closeHit.hasAncestor(hit))) { // Anytime we have a close-hit on something and a direct hit on any // ancestor of the close-hit, we want to allow for the close-hit as // ancestors are considered "background" and should get lower priority. // We do NOT want to just always check for the close-hit, in case we // have a direct hit on say, a node, and a close hit on a sibling such // as a link that is connected to the node. In that case we want to // stay with the direct hit on the node and ignore the link, no matter // how close we may be to it. The one case this isn't ideal is if there // is a link to a node that is currently linking to it's center, and not // an edge, in which case the link actually does overlap it's sibling, // and we could theoretically find out which portion overlaps and allow // a close-hit on that, but this is a rare case and dealing with it // would hardly be worth it. final float closeEnoughSq; if (pc.zoom < 1) { // allow more slop if zoomed way out (links are very small and hard to hit) closeEnoughSq = (8 / pc.zoom) * (8 / pc.zoom); } else if (pc.zoom >= 4) { final float zf = pc.zoom / 2; closeEnoughSq = 8/zf * 8/zf; } else closeEnoughSq = 8 * 8; if (DEBUG.PICK) System.out.format(" closeEnough=%.2f;", Math.sqrt(closeEnoughSq)); //if (hit == null && closestDistSq < closeEnoughSq) { if (closestDistSq < closeEnoughSq) { if (DEBUG.PICK) System.out.println(" (CLOSE ENOUGH)"); //if (DEBUG.PICK) eoutln("PointPick: closeHit: " + closeHit + " distance: " + Math.sqrt(closestDistSq)); hit = closeHit; } else { if (DEBUG.PICK) System.out.println(""); } } else { if (DEBUG.PICK) System.out.println(""); } LWComponent picked = null; if (hit != null && hit.isPathwayOwned() && hit instanceof LWSlide) { // allow a slide-icon to be picked no matter what picked = hit; } else if (hit != null) { final LWContainer parent = hit.getParent(); if (parent != null) { // This is a special case for handling LWGroups's to replace our getPickLevel functionality: final LWGroup topGroupAncestor = (LWGroup) parent.getTopMostAncestorOfType(LWGroup.class, pc.root); if (topGroupAncestor != null) { if (pc.pickDepth > 0) { // DEEP PICK: // Even if deep picking, only pick the deepest group we can find, // not the contents. TODO: Really, we should be picking the second // top-most (that is, only penetrate through the top-level group when // deep picking, not straight to the bottom.) final LWGroup groupAncestor = (LWGroup) parent.getAncestorOfType(LWGroup.class, pc.root); if (groupAncestor != topGroupAncestor) picked = groupAncestor; } else { // SHALLOW PICK: picked = topGroupAncestor; } } if (picked == null) { // TODO FIX: if a CURVED LINK is a child of a slide (or a node or anything else for that matter), // the curved link can actually be well outside it's parent, but still end up picking the parent // here, because we were CLOSE ENOUGH on the child. (see test-linkhack.vue) picked = parent.pickChild(pc, hit); } } else { // would normally only get here for an LWMap picked = hit; } if (picked == hit) { // only make use of defaultPick if pickChild didn't // already redirect us to something else if (picked != null) picked = picked.defaultPick(pc); } if (picked != null && pc.dropping != null) picked = picked.defaultDropTarget(pc); } if (picked != null && picked != pc.root && !picked.hasAncestor(pc.root)) { // Just in case, NEVER allow anything above the current pick root to be picked (e.g., above the current focal). // The group picking code is pretty hairy, and although it should catch this now, // we double-check here just in case. if (DEBUG.Enabled) Log.warn("PointPick: DENIED: " + picked + "; is above pick root: " + pc.root); picked = null; } //if (DEBUG.PICK || (DEBUG.DND && picked != null)) { if (DEBUG.PICK) { eoutln(Util.TERM_GREEN + "PointPick: PICKED: " + picked + Util.TERM_CLEAR); if (DEBUG.PICK) System.out.println(""); } //else if (DEBUG.WORK && picked != null) System.out.println("PICKED " + picked); return picked; } } //public static Rectangle2D mapRect; // for testing rotated transformMapToZeroRect public static class RegionPick extends LWTraversal.Picker { final Rectangle2D mapRect; final java.util.List<LWComponent> hits = new java.util.ArrayList(); public RegionPick(PickContext pc) { super(pc); this.mapRect = new Rectangle2D.Float(pc.x, pc.y, pc.width, pc.height); } @Override public void visit(LWComponent c) { if (DEBUG.PICK) eoutln("VISIT " + c); // region picks should never select the root object the region is // being dragged inside if (c != pc.root && c != pc.excluded && c.intersects(mapRect)) { if (DEBUG.PICK) eoutln(" HIT " + c); hits.add(c); } } @Override public void traverse(LWComponent c) { if (DEBUG.PICK) eoutln("RGN-TRVSE " + c); super.traverse(c); } public static java.util.List<LWComponent> pick(PickContext pc) { return new RegionPick(pc).traverseAndPick(pc.root); } public java.util.List<LWComponent> traverseAndPick(LWComponent root) { if (DEBUG.PERF) { final long start = System.nanoTime(); traverse(root); final long delta = System.nanoTime() - start; Log.debug(String.format("RegionPick elapsed: %.1fms", delta / 100000.0)); return hits; } else { traverse(root); return hits; } } } }