/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.elementclass.connection; import java.awt.Composite; import java.awt.Shape; import java.awt.geom.Area; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import org.simantics.g2d.connection.ConnectionEntity; import org.simantics.g2d.connection.ConnectionEntity.ConnectionEvent; import org.simantics.g2d.connection.ConnectionEntity.ConnectionListener; import org.simantics.g2d.connection.handler.ConnectionHandler; import org.simantics.g2d.diagram.handler.PickRequest.PickPolicy; import org.simantics.g2d.diagram.handler.Topology.Connection; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.handler.Children; import org.simantics.g2d.element.handler.InternalSize; import org.simantics.g2d.element.handler.Outline; import org.simantics.g2d.element.handler.Pick; import org.simantics.g2d.element.handler.Pick2; import org.simantics.g2d.element.handler.SceneGraph; import org.simantics.g2d.element.handler.SelectionOutline; import org.simantics.g2d.element.handler.impl.ConnectionSelectionOutline; import org.simantics.g2d.element.handler.impl.ParentImpl; import org.simantics.g2d.element.handler.impl.SimpleElementLayers; import org.simantics.g2d.element.handler.impl.TextImpl; import org.simantics.g2d.elementclass.PlainElementPropertySetter; import org.simantics.g2d.elementclass.connection.EdgeClass.FixedTransform; import org.simantics.g2d.utils.GeometryUtils; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.IG2DNode; import org.simantics.scenegraph.g2d.nodes.SingleElementNode; import org.simantics.utils.datastructures.ListenerList; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; /** * An element class for single connection entity elements. A connection entity * consists of connection edge segments and branch points as its children. * * @author Tuukka Lehtonen */ public class ConnectionClass { public static final ElementClass CLASS = ElementClass.compile( TextImpl.INSTANCE, FixedTransform.INSTANCE, ConnectionPick.INSTANCE, ConnectionBounds.INSTANCE, ConnectionSelectionOutline.INSTANCE, ConnectionHandlerImpl.INSTANCE, ConnectionChildren.INSTANCE, ParentImpl.INSTANCE, ConnectionSceneGraph.INSTANCE, SimpleElementLayers.INSTANCE, new PlainElementPropertySetter(ElementHints.KEY_SG_NODE) ).setId(ConnectionClass.class.getSimpleName()); private static class ThreadLocalList extends ThreadLocal> { @Override protected java.util.List initialValue() { return new ArrayList(); } }; private static final ThreadLocal> perThreadSceneGraphList = new ThreadLocalList(); private static final ThreadLocal> perThreadBoundsList = new ThreadLocalList(); private static final ThreadLocal> perThreadShapeList = new ThreadLocalList(); private static final ThreadLocal> perThreadPickList = new ThreadLocalList(); static class ConnectionHandlerImpl implements ConnectionHandler { public static final ConnectionHandlerImpl INSTANCE = new ConnectionHandlerImpl(); private static final long serialVersionUID = 3267139233182458330L; @Override public Collection getBranchPoints(IElement connection, Collection result) { ConnectionEntity entity = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (entity == null) return Collections.emptySet(); return entity.getBranchPoints(result); } @Override public Collection getChildren(IElement connection, Collection result) { ConnectionEntity entity = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (entity == null) return Collections.emptySet(); result = entity.getSegments(result); return entity.getBranchPoints(result); } @Override public Collection getSegments(IElement connection, Collection result) { ConnectionEntity entity = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (entity == null) return Collections.emptySet(); return entity.getSegments(result); } @Override public Collection getTerminalConnections(IElement connection, Collection result) { ConnectionEntity entity = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (entity == null) return Collections.emptySet(); return entity.getTerminalConnections(result); } } static final class ConnectionSceneGraph implements SceneGraph { public static final ConnectionSceneGraph INSTANCE = new ConnectionSceneGraph(); private static final long serialVersionUID = 4232871859964883266L; @Override public void init(IElement connection, G2DParentNode parent) { ConnectionEntity ce = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) return; // Painting is single-threaded, it is OK to use a single thread-local collection here. List children = perThreadSceneGraphList.get(); children.clear(); ce.getSegments(children); ce.getBranchPoints(children); //new Exception("painting connection entity " + ce.hashCode() + " with " + children.size() + " segments and branch points").printStackTrace(); if (children.isEmpty()) return; Set tmp = new HashSet(); int zIndex = 0; for (IElement child : children) { ElementClass ec = child.getElementClass(); // Transform transform = child.getElementClass().getSingleItem(Transform.class); // AffineTransform at2 = transform.getTransform(child); // if (at2 == null) // continue; SingleElementNode holder = child.getHint(ElementHints.KEY_SG_NODE); if (holder == null) { holder = parent.addNode(ElementUtils.generateNodeId(child), SingleElementNode.class); child.setHint(ElementHints.KEY_SG_NODE, holder); } holder.setZIndex(++zIndex); Composite composite = child.getHint(ElementHints.KEY_COMPOSITE); //holder.setTransform(at2); holder.setComposite(composite); holder.setVisible(true); // New node handler for (SceneGraph n : ec.getItemsByClass(SceneGraph.class)) { n.init(child, holder); } tmp.add(holder); } // Hide unaccessed nodes (but don't remove) for (IG2DNode node : parent.getNodes()) { if (node instanceof SingleElementNode) { if (!tmp.contains(node)) { ((SingleElementNode)node).setVisible(false); } } else { //System.out.println("WHAT IS THIS: "); //NodeDebug.printSceneGraph(((Node) node)); } } // Don't leave dangling references behind. children.clear(); } @Override public void cleanup(IElement e) { } } static final class ConnectionBounds implements InternalSize, Outline { public static final ConnectionBounds INSTANCE = new ConnectionBounds(); private static final long serialVersionUID = 4232871859964883266L; @Override public Rectangle2D getBounds(IElement e, Rectangle2D size) { ConnectionEntity ce = e.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) return size; Collection parts = perThreadBoundsList.get(); parts.clear(); parts = ce.getSegments(parts); if (parts.isEmpty()) return size; parts = ce.getBranchPoints(parts); Rectangle2D temp = null; for (IElement part : parts) { if (ElementUtils.isHidden(part)) continue; // Using on-diagram coordinates because neither connections nor // edges have a non-identity transform which means that // coordinates are always absolute. Therefore branch point // bounds also need to be calculated in absolute coordinates. Rectangle2D bounds = ElementUtils.getElementBoundsOnDiagram(part, size); if (bounds == null) continue; // System.out.println("InternalSize BOUNDS: " + size + " for part " + part + " " + part.getElementClass()); if (temp == null) { temp = new Rectangle2D.Double(); temp.setRect(bounds); } else Rectangle2D.union(temp, bounds, temp); //System.out.println("InternalSize Combined BOUNDS: " + temp); } if (temp != null) { if (size == null) size = temp; else size.setRect(temp); } // Don't leave dangling references behind. parts.clear(); return size; } private Shape getSelectionShape(IElement forPart) { for (SelectionOutline so : forPart.getElementClass().getItemsByClass(SelectionOutline.class)) { Shape shape = so.getSelectionShape(forPart); if (shape != null) return shape; } // Using on-diagram coordinates because neither connections nor // edges have a non-identity transform which means that // coordinates are always absolute. Therefore branch point // shape also needs to be calculated in absolute coordinates. Shape shape = ElementUtils.getElementShapeOrBoundsOnDiagram(forPart); return shape; //return shape.getBounds2D(); } @Override public Shape getElementShape(IElement e) { ConnectionEntity ce = e.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) return new Rectangle2D.Double(); Collection parts = perThreadShapeList.get(); parts = ce.getSegments(parts); if (parts.isEmpty()) return new Rectangle2D.Double(); parts = ce.getBranchPoints(parts); if (parts.size() == 1) { IElement part = parts.iterator().next(); if (ElementUtils.isHidden(part)) return new Rectangle2D.Double(); Shape shape = getSelectionShape(part); //System.out.println("Outline SHAPE: " + shape); //System.out.println("Outline BOUNDS: " + shape.getBounds2D()); return shape; } //System.out.println("Outline: " + e); Area area = new Area(); for (IElement part : parts) { if (ElementUtils.isHidden(part)) continue; //System.out.println(part); Shape shape = getSelectionShape(part); Rectangle2D bounds = shape.getBounds2D(); // System.out.println(" shape: " + shape); // System.out.println(" bounds: " + bounds); if (bounds.isEmpty()) { double w = bounds.getWidth(); double h = bounds.getHeight(); if (w <= 0.0 && h <= 0.0) continue; // Need to expand shape in either width or height to make it visible. final double exp = 0.1; if (w <= 0.0) shape = org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(bounds, 0, 0, exp, exp); else if (h <= 0.0) shape = org.simantics.scenegraph.utils.GeometryUtils.expandRectangle(bounds, exp, exp, 0, 0); } //System.out.println(" final shape: " + shape); //shape = bounds; Area a = null; if (shape instanceof Area) a = (Area) shape; else a = new Area(shape); area.add(a); } parts.clear(); //System.out.println(" connection area outline: " + area); //System.out.println(" connection area outline bounds: " + area.getBounds2D()); return area; } } public static class ConnectionPick implements Pick2 { public final static ConnectionPick INSTANCE = new ConnectionPick(); private static final long serialVersionUID = 1L; @Override public boolean pickTest(IElement e, Shape s, PickPolicy policy) { ConnectionEntity ce = e.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) return false; // Primarily pick branch points and then edges. Collection parts = perThreadPickList.get(); parts = ce.getBranchPoints(parts); parts = ce.getSegments(parts); if (parts.isEmpty()) return false; for (IElement part : parts) { if (ElementUtils.isHidden(part)) continue; for (Pick pick : part.getElementClass().getItemsByClass(Pick.class)) { //System.out.println("TESTING: " + part + " : " + s + " : " + policy); if (pick.pickTest(part, s, policy)) { //System.out.println(" HIT!"); return true; } } } parts.clear(); return false; } @Override public int pick(IElement e, Shape s, PickPolicy policy, Collection result) { int oldResultSize = result.size(); ConnectionEntity ce = e.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) return 0; // Primarily pick branch points and then edges. List parts = perThreadPickList.get(); parts.clear(); ce.getSegments(parts); int edges = parts.size(); ce.getBranchPoints(parts); int branchPoints = parts.size() - edges; boolean singleEdge = branchPoints == 0 && edges == 1; if (parts.isEmpty()) return 0; // See whether the whole connection is to be picked.. boolean pickConnection = false; wholeConnectionPick: for (Outline outline : e.getElementClass().getItemsByClass(Outline.class)) { Shape elementShape = outline.getElementShape(e); if (elementShape == null) continue; switch (policy) { case PICK_CONTAINED_OBJECTS: if (GeometryUtils.contains(s, elementShape)) { pickConnection = true; break wholeConnectionPick; } break; case PICK_INTERSECTING_OBJECTS: if (GeometryUtils.intersects(s, elementShape)) { pickConnection = true; break wholeConnectionPick; } break; } } ArrayList picks = null; // Pick connection segments for (int i = 0; i < edges; ++i) { IElement part = parts.get(i); if (ElementUtils.isHidden(part)) continue; for (Pick pick : part.getElementClass().getItemsByClass(Pick.class)) { //System.out.println("TESTING SEGMENT: " + part + " : " + s + " : " + policy); if (pick.pickTest(part, s, policy)) { //System.out.println(" HIT!"); if (picks == null) picks = new ArrayList(4); picks.add(part); break; } } } // Pick the whole connection ? if (pickConnection) { if (picks == null) picks = new ArrayList(4); picks.add(e); } // Pick branch/route points for (int i = edges; i < parts.size(); ++i) { IElement part = parts.get(i); if (ElementUtils.isHidden(part)) continue; for (Pick pick : part.getElementClass().getItemsByClass(Pick.class)) { //System.out.println("TESTING BRANCHPOINT: " + part + " : " + s + " : " + policy); if (pick.pickTest(part, s, policy)) { //System.out.println(" HIT!"); if (picks == null) picks = new ArrayList(4); picks.add(part); break; } } } if (picks != null) { // Add the discovered pickable children to the result after the // parent to make the parent the primary pickable. // Skip the children if there is only one child. if (!singleEdge) { result.addAll(picks); } else { result.add(e); } } parts.clear(); return result.size() - oldResultSize; } } private static final Key CHILD_LISTENERS = new KeyOf(ListenerList.class, "CHILD_LISTENERS"); public static class ConnectionChildren implements Children, ConnectionListener { public final static ConnectionChildren INSTANCE = new ConnectionChildren(); private static final long serialVersionUID = 1L; @Override public Collection getChildren(IElement element, Collection result) { ConnectionEntity ce = element.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) { if (result == null) result = new ArrayList(0); return result; } result = ce.getSegments(result); result = ce.getBranchPoints(result); return result; } @Override public void addChildListener(IElement element, ChildListener listener) { ListenerList ll = null; synchronized (element) { ll = element.getHint(CHILD_LISTENERS); if (ll == null) { ll = new ListenerList(ChildListener.class); element.setHint(CHILD_LISTENERS, ll); ConnectionEntity entity = element.getHint(ElementHints.KEY_CONNECTION_ENTITY); entity.setListener(this); } } ll.add(listener); } @Override public void removeChildListener(IElement element, ChildListener listener) { synchronized (element) { ListenerList ll = element.getHint(CHILD_LISTENERS); if (ll == null) return; ll.remove(listener); if (ll.isEmpty()) { ConnectionEntity entity = element.getHint(ElementHints.KEY_CONNECTION_ENTITY); entity.setListener(null); } } } @Override public void connectionChanged(ConnectionEvent event) { fireChildrenChanged(event); } private void fireChildrenChanged(ConnectionEvent event) { ListenerList ll = event.connection.getHint(CHILD_LISTENERS); if (ll == null) return; ChildEvent ce = new ChildEvent(event.connection, event.removedParts, event.addedParts); for (ChildListener cl : ll.getListeners()) { cl.elementChildrenChanged(ce); } } } }