/******************************************************************************* * 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; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.util.Collection; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.handler.DataElementMap; import org.simantics.g2d.diagram.handler.Topology.Terminal; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.SceneGraphNodeKey; import org.simantics.g2d.element.handler.InternalSize; import org.simantics.g2d.element.handler.LifeCycle; import org.simantics.g2d.element.handler.Outline; import org.simantics.g2d.element.handler.Rotate; import org.simantics.g2d.element.handler.SceneGraph; import org.simantics.g2d.element.handler.TerminalLayout; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.element.handler.Text; import org.simantics.g2d.element.handler.impl.BorderColorImpl; import org.simantics.g2d.element.handler.impl.DefaultTransform; import org.simantics.g2d.element.handler.impl.FillColorImpl; import org.simantics.g2d.element.handler.impl.SimpleElementLayers; import org.simantics.g2d.element.handler.impl.StaticSymbolImpl; import org.simantics.g2d.element.handler.impl.TextImpl; import org.simantics.g2d.image.Image; import org.simantics.g2d.image.impl.ShapeImage; import org.simantics.g2d.utils.Alignment; import org.simantics.g2d.utils.geom.DirectionSet; import org.simantics.scenegraph.Node; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.g2d.nodes.FlagNode; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; /** * @author Tuukka Lehtonen */ public class FlagClass { public static enum Type { /// The input part of a pair of flags. In, /// The output part of a pair of flags. Out; public Type other() { return this == Out ? In: Out; } } public static class Mode { public static final Mode External = new External(1); public static final Mode Internal = new Mode() { public String toString() { return "Internal"; } }; } public static class External extends Mode { public final int count; public External(int count) { this.count = count; } @Override public String toString() { return "External(" + count + ")"; } } private static final double GLOBAL_SCALE = 0.1; private static final double FLAG_SIZE_SCALE = 3 * GLOBAL_SCALE; public static final double DEFAULT_WIDTH = 70 * FLAG_SIZE_SCALE; public static final double DEFAULT_HEIGHT = 20 * FLAG_SIZE_SCALE; public static final double DEFAULT_BEAK_ANGLE = 60; public static final Key KEY_FLAG_TYPE = new KeyOf(Type.class, "FLAG_TYPE"); public static final Key KEY_EXTERNAL = new KeyOf(Boolean.class, "FLAG_EXTERNAL"); public static final Key KEY_FLAG_MODE = new KeyOf(Mode.class, "FLAG_MODE"); public static final Key KEY_FLAG_WIDTH = new KeyOf(Double.class, "FLAG_WIDTH"); public static final Key KEY_FLAG_HEIGHT = new KeyOf(Double.class, "FLAG_HEIGHT"); public static final Key KEY_FLAG_BEAK_ANGLE = new KeyOf(Double.class, "FLAG_BEAK_ANGLE"); public static final Key KEY_FLAG_TEXT = new KeyOf(String[].class, "FLAG_TEXT"); public static final Key KEY_FLAG_TEXT_AREA = new KeyOf(Rectangle2D.class, "FLAG_TEXT_AREA_SIZE"); public static final Key KEY_SHAPE = new KeyOf(Shape.class, "SHAPE"); public static final Key KEY_TEXT_HORIZONTAL_ALIGN = new KeyOf(Alignment.class, "TEXT_HORIZONTAL_ALIGN"); public static final Key KEY_TEXT_VERTICAL_ALIGN = new KeyOf(Alignment.class, "TEXT_VERTICAL_ALIGN"); public static final Key KEY_SG_NODE = new SceneGraphNodeKey(Node.class, "FLAG_SG_NODE"); /** * Indicates that this flag is connected to another flag. */ private static final Key KEY_FLAG_CONNECTION_DATA = new KeyOf(DataConnection.class, "FLAG_CONNECTION_DATA"); private static final Key KEY_FLAG_CONNECTION_ELEMENTS = new KeyOf(ElementConnection.class, "FLAG_CONNECTION_ELEMENTS"); public interface Connection { T getFirst(); T getSecond(); } private static class Conn implements Connection { private final T first; private final T second; public Conn(T first, T second) { this.first = first; this.second = second; } @Override public T getFirst() { return first; } @Override public T getSecond() { return second; } } private static class ElementConnection extends Conn { public ElementConnection(IElement first, IElement second) { super(first, second); if (first == null) throw new IllegalArgumentException("first is null"); if (second == null) throw new IllegalArgumentException("second is null"); } } private static class DataConnection extends Conn { public DataConnection(Object first, Object second) { super(first, second); if (first == null) throw new IllegalArgumentException("first is null"); // Second may be null to indicate "not-connected" } public boolean isConnected() { return getSecond() != null; } } public static final FlagHandler FLAG_HANDLER = new FlagHandler() { private static final long serialVersionUID = -4258875745321808416L; @Override public Type getType(IElement e) { return FlagClass.getType(e); } @Override public void setType(IElement e, Type type) { e.setHint(KEY_FLAG_TYPE, type); } @Override public boolean isExternal(IElement e) { return Boolean.TRUE.equals(e.getHint(KEY_EXTERNAL)); } @Override public void setExternal(IElement e, boolean external) { e.setHint(KEY_EXTERNAL, Boolean.valueOf(external)); } @Override public Connection getConnection(IElement e) { return e.getHint(KEY_FLAG_CONNECTION_ELEMENTS); } @Override public Connection getConnectionData(IElement e) { DataConnection dc = e.getHint(KEY_FLAG_CONNECTION_DATA); return (dc != null && dc.isConnected()) ? dc : null; } @Override public void connect(IElement e1, IElement e2) { assert e1 != null && e2 != null; ElementConnection ce = new ElementConnection(e1, e2); e1.setHint(KEY_FLAG_CONNECTION_ELEMENTS, ce); e2.setHint(KEY_FLAG_CONNECTION_ELEMENTS, ce); } @Override public void connectData(IElement e1, Object o1, Object o2) { e1.removeHint(KEY_FLAG_CONNECTION_ELEMENTS); e1.setHint(KEY_FLAG_CONNECTION_DATA, new DataConnection(o1, o2)); } @Override public void disconnect(IElement local) { assert local != null; local.removeHint(KEY_FLAG_CONNECTION_ELEMENTS); DataConnection c = (DataConnection) local.removeHint(KEY_FLAG_CONNECTION_DATA); if (c != null) { IElement remote = otherElement(local, c); if (remote != null) { local.removeHint(KEY_FLAG_CONNECTION_ELEMENTS); remote.removeHint(KEY_FLAG_CONNECTION_DATA); } } } @Override public boolean isWithinDiagram(IDiagram d, Connection c) { assert d != null; assert c != null; if (c instanceof DataConnection) return bothOnDiagram(d, (DataConnection) c); if (c instanceof ElementConnection) return bothOnDiagram(d, (ElementConnection) c); return false; } @Override public IElement getCorrespondence(IElement end) { assert end != null; DataConnection dc = (DataConnection) end.getHint(KEY_FLAG_CONNECTION_DATA); if (dc != null && dc.isConnected()) return otherElement(end, dc); ElementConnection ec = (ElementConnection) end.getHint(KEY_FLAG_CONNECTION_ELEMENTS); if (ec != null) return otherElement(end, ec); return null; } boolean bothOnDiagram(IDiagram d, DataConnection c) { if (!c.isConnected()) return false; DataElementMap dem = d.getDiagramClass().getSingleItem(DataElementMap.class); IElement eout = dem.getElement(d, c.getFirst()); IElement ein = dem.getElement(d, c.getSecond()); return eout != null && ein != null; } boolean bothOnDiagram(IDiagram d, ElementConnection c) { DataElementMap dem = d.getDiagramClass().getSingleItem(DataElementMap.class); Object o1 = dem.getData(d, c.getFirst()); Object o2 = dem.getData(d, c.getSecond()); return o1 != null && o2 != null; } public IElement otherElement(IElement e, DataConnection c) { if (!c.isConnected()) return null; IDiagram d = ElementUtils.peekDiagram(e); if (d == null) return null; DataElementMap dem = d.getDiagramClass().getSingleItem(DataElementMap.class); Object o = dem.getData(d, e); if (c.getFirst().equals(o)) return dem.getElement(d, c.getSecond()); if (c.getSecond().equals(o)) return dem.getElement(d, c.getFirst()); throw new IllegalArgumentException("specified object '" + o + "' is neither of the connected objects: first='" + c.getSecond() + "', second='" + c.getFirst() + "'"); } public IElement otherElement(IElement e, ElementConnection c) { IElement a = c.getFirst(); IElement b = c.getSecond(); if (e == a) return b; if (e == b) return a; throw new IllegalArgumentException("specified element '" + e + "' is neither of the connected objects: first='" + c.getSecond() + "', second='" + c.getFirst() + "'"); } }; static final Shape staticShape; static { Path2D path = new Path2D.Double(); staticShape = path; createFlagShape(path, Type.In, Mode.External, DEFAULT_WIDTH, DEFAULT_HEIGHT, getBeakLength(DEFAULT_HEIGHT, DEFAULT_BEAK_ANGLE)); } public static final BasicStroke STROKE = new BasicStroke(0.15f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); static final Image DEFAULT_IMAGE = new ShapeImage(staticShape, null, STROKE); static final StaticSymbolImpl DEFAULT_STATIC_SYMBOL = new StaticSymbolImpl(DEFAULT_IMAGE); static final FlagSize DEFAULT_FLAG_SIZE = new FlagSize(DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_BEAK_ANGLE); static final Initializer DEFAULT_INITIALIZER = new Initializer(Type.In, Mode.External); public static final ElementClass FLAGCLASS = ElementClass.compile( DEFAULT_INITIALIZER, FLAG_HANDLER, DefaultTransform.INSTANCE, DEFAULT_FLAG_SIZE, BorderColorImpl.BLACK, FillColorImpl.WHITE, TextImpl.INSTANCE, FlagTerminalTopology.DEFAULT, FlagSceneGraph.INSTANCE, DEFAULT_STATIC_SYMBOL ).setId(FlagClass.class.getSimpleName()); public static ElementClass create(Terminal terminal) { return ElementClass.compile( DEFAULT_INITIALIZER, FLAG_HANDLER, DefaultTransform.INSTANCE, DEFAULT_FLAG_SIZE, BorderColorImpl.BLACK, FillColorImpl.WHITE, TextImpl.INSTANCE, new FlagTerminalTopology(terminal), FlagSceneGraph.INSTANCE, DEFAULT_STATIC_SYMBOL, SimpleElementLayers.INSTANCE ).setId(FlagClass.class.getSimpleName()); } public static ElementClass create(Terminal terminal, SceneGraph scn) { return ElementClass.compile( DEFAULT_INITIALIZER, FLAG_HANDLER, DefaultTransform.INSTANCE, DEFAULT_FLAG_SIZE, BorderColorImpl.BLACK, FillColorImpl.WHITE, TextImpl.INSTANCE, new FlagTerminalTopology(terminal), scn, DEFAULT_STATIC_SYMBOL, SimpleElementLayers.INSTANCE ).setId(FlagClass.class.getSimpleName()); } static class Initializer implements LifeCycle { private static final long serialVersionUID = 4404942036933073584L; private final Type type; private final Mode mode; Initializer(Type type, Mode mode) { assert type != null; assert mode != null; this.type = type; this.mode = mode; } @Override public void onElementActivated(IDiagram d, IElement e) { } @Override public void onElementCreated(IElement e) { e.setHint(KEY_FLAG_TYPE, type); e.setHint(KEY_FLAG_MODE, mode); //e.setHint(ElementHints.KEY_COMPOSITE, AlphaComposite.SrcOver.derive(0.5f)); } @Override public void onElementDeactivated(IDiagram d, IElement e) { } @Override public void onElementDestroyed(IElement e) { } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + mode.hashCode(); result = prime * result + type.hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Initializer other = (Initializer) obj; if (!mode.equals(other.mode)) return false; if (!type.equals(other.type)) return false; return true; } }; public static Path2D createFlagShape(Path2D path, Type type, Mode mode, double width, double height, double beakLength) { double hh = height / 2; path.reset(); switch (type) { case Out: if (mode instanceof External) { path.moveTo(0, hh); path.lineTo(width, hh); path.lineTo(width+beakLength, 0); path.lineTo(width, -hh); path.lineTo(0, -hh); path.closePath(); path.moveTo(width, hh); path.lineTo(width, -hh); int count = ((External)mode).count; if(count > 1) { double shadow=hh*0.25; double ix = beakLength - 0.5*shadow*(1.0 + beakLength/hh); double iy = hh * (ix / beakLength - 1.0); for(int sid=1;sid<=Math.min(count-1, 4);++sid) { double dis = sid*shadow; path.moveTo(dis, hh+dis-shadow); path.lineTo(dis, hh+dis); path.lineTo(dis+width, hh+dis); path.lineTo(dis+width+beakLength, dis); path.lineTo(width + ix + dis, iy + dis); } } else { double left = 0; double right = width - 0; if (left < right) { path.moveTo(left, 0); path.lineTo(right, 0); } } } else if (mode == Mode.Internal) { path.moveTo(0, hh); path.lineTo(beakLength, 0); path.lineTo(0, -hh); path.closePath(); } break; case In: path.moveTo(0, 0); if (mode instanceof External) { path.lineTo(-beakLength, -hh); path.lineTo(-width-beakLength, -hh); path.lineTo(-width-beakLength, hh); path.lineTo(-beakLength, hh); path.closePath(); path.moveTo(-beakLength, -hh); path.lineTo(-beakLength, hh); int count = ((External)mode).count; if(count > 1) { double shadow=hh*0.25; double ix = beakLength - 0.5*shadow*(1.0 + beakLength/hh); double iy = hh * (ix / beakLength - 1.0); double xDisp = -width-beakLength; for(int sid=1;sid<=Math.min(count-1, 4);++sid) { double dis = sid*shadow; path.moveTo(xDisp+dis, hh+dis-shadow); path.lineTo(xDisp+dis, hh+dis); path.lineTo(xDisp+dis+width, hh+dis); path.lineTo(xDisp+dis+width+beakLength, dis); path.lineTo(xDisp+width + ix + dis, iy + dis); } } else { double left = -width-beakLength+0; double right = -beakLength-0; if (left < right) { path.moveTo(left, 0); path.lineTo(right, 0); } } } else if (mode == Mode.Internal) { path.lineTo(-beakLength, -hh); path.lineTo(-beakLength, hh); path.closePath(); } break; } return path; } public static Path2D createFlagShape(IElement e) { Type type = getType(e); Mode mode = e.getHint(KEY_FLAG_MODE); double width = e.getHint(KEY_FLAG_WIDTH); double height = e.getHint(KEY_FLAG_HEIGHT); double beakLength = getBeakLength(e); Path2D path = new Path2D.Double(); createFlagShape(path, type, mode, width, height, beakLength); return path; } static class FlagSize implements InternalSize, Outline, LifeCycle { private static final long serialVersionUID = 829379327756475944L; private final double length; private final double thickness; private final double beakAngle; public FlagSize(double length, double thickness, double beakAngle) { this.length = length; this.thickness = thickness; this.beakAngle = beakAngle; } @Override public Shape getElementShape(IElement e) { Shape shape = e.getHint(KEY_SHAPE); if (shape != null) return shape; return createFlagShape(e); } @Override public Rectangle2D getBounds(IElement e, Rectangle2D size) { if (size == null) size = new Rectangle2D.Double(); Shape shape = getElementShape(e); size.setFrame(shape.getBounds2D()); return size; } @Override public void onElementActivated(IDiagram d, IElement e) { } @Override public void onElementCreated(IElement e) { e.setHint(KEY_FLAG_WIDTH, length); e.setHint(KEY_FLAG_HEIGHT, thickness); e.setHint(KEY_FLAG_BEAK_ANGLE, beakAngle); } @Override public void onElementDeactivated(IDiagram d, IElement e) { } @Override public void onElementDestroyed(IElement e) { } @Override public int hashCode() { final int prime = 31; int result = 1; long temp; temp = Double.doubleToLongBits(beakAngle); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(length); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(thickness); result = prime * result + (int) (temp ^ (temp >>> 32)); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; FlagSize other = (FlagSize) obj; if (Double.doubleToLongBits(beakAngle) != Double.doubleToLongBits(other.beakAngle)) return false; if (Double.doubleToLongBits(length) != Double.doubleToLongBits(other.length)) return false; if (Double.doubleToLongBits(thickness) != Double.doubleToLongBits(other.thickness)) return false; return true; } } static class FlagSceneGraph implements SceneGraph { private static final long serialVersionUID = 35208146123929197L; public static final FlagSceneGraph INSTANCE = new FlagSceneGraph(); @Override public void cleanup(IElement e) { ElementUtils.removePossibleNode(e, KEY_SG_NODE); } @Override public void init(IElement e, G2DParentNode parent) { Color fc = ElementUtils.getFillColor(e, Color.WHITE); Color bc = ElementUtils.getBorderColor(e, Color.BLACK); Color tc = ElementUtils.getTextColor(e, Color.BLACK); Outline outline = e.getElementClass().getSingleItem(Outline.class); Shape shape = outline.getElementShape(e); Type type = getType(e); double dir = getDirection(e); double width = e.getHint(KEY_FLAG_WIDTH); double height = e.getHint(KEY_FLAG_HEIGHT); double beakAngle = e.getHint(KEY_FLAG_BEAK_ANGLE); String[] flagText = e.getHint(KEY_FLAG_TEXT); if (flagText == null) { // fallback option. Text t = e.getElementClass().getAtMostOneItemOfClass(Text.class); if (t != null) { String text = t.getText(e); if (text != null) flagText = new String[] { text }; } } // DEBUG TEXT //flagText = new String[] { String.format("%3.1f", dir) + " deg", "FOO"}; Rectangle2D textArea = e.getHint(KEY_FLAG_TEXT_AREA); if (textArea == null) { double beakLength = getBeakLength(height, beakAngle); textArea = type == Type.In ? new Rectangle2D.Double(-width-beakLength, -height*0.5, width, height) : new Rectangle2D.Double(0, -height*0.5, width, height); } Alignment horizAlign = ElementUtils.getHintOrDefault(e, KEY_TEXT_HORIZONTAL_ALIGN, Alignment.LEADING); Alignment vertAlign = ElementUtils.getHintOrDefault(e, KEY_TEXT_VERTICAL_ALIGN, Alignment.CENTER); FlagNode flag = ElementUtils.getOrCreateNode(e, parent, KEY_SG_NODE, ElementUtils.generateNodeId(e), FlagNode.class); flag.init(shape, flagText, STROKE, bc, fc, tc, (float) width, (float) height, (float) dir, (float) beakAngle, textArea, horizAlign.ordinal(), vertAlign.ordinal()); AffineTransform at = ElementUtils.getTransform(e); if(at != null) flag.setTransform(at); } } static class TerminalPoint implements Terminal { } public static class FlagTerminalTopology implements TerminalTopology, TerminalLayout { private static final long serialVersionUID = -4194634598346105458L; public static final Terminal DEFAULT_T0 = new TerminalPoint(); public static final FlagTerminalTopology DEFAULT = new FlagTerminalTopology(DEFAULT_T0); final Terminal T0; public FlagTerminalTopology(Terminal t) { this.T0 = t; } @Override public void getTerminals(IElement e, Collection result) { result.add(T0); } @Override public AffineTransform getTerminalPosition(IElement node, Terminal t) { if (t == T0) { return new AffineTransform(); } return null; } @Override public boolean getTerminalDirection(IElement node, Terminal t, DirectionSet directions) { Type type = getType(node); double d = getDirection(node); if (t == T0) { switch (type) { case In: directions.add(d); break; case Out: directions.add(Math.IEEEremainder(d + 180.0, 360.0)); break; } //System.out.println("directions T0: " + Arrays.toString(directions.toArray())); return true; } return false; } // static final Path2D terminalShape; // // static { // double s = .5; // Path2D p = new Path2D.Double(); // p.moveTo(s, s); // p.lineTo(s, -s); // p.lineTo(-s, -s); // p.lineTo(-s, s); // p.closePath(); // terminalShape = p; // } @Override public Shape getTerminalShape(IElement node, Terminal t) { //return terminalShape; //return null; // For each terminal, return the whole shape of the element. return ElementUtils.getElementShapeOrBounds(node); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((T0 == null) ? 0 : T0.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; FlagTerminalTopology other = (FlagTerminalTopology) obj; if (T0 == null) { if (other.T0 != null) return false; } else if (!T0.equals(other.T0)) return false; return true; } } public static AffineTransform getTransform(IElement e) { AffineTransform at = ElementUtils.getTransform(e); if (at == null) return new AffineTransform(); return at; } public static double getDirection(IElement e) { Rotate rotate = e.getElementClass().getAtMostOneItemOfClass(Rotate.class); if (rotate != null) { return rotate.getAngle(e); } return 0.0; } public static Type getType(IElement e) { Type t = e.getHint(KEY_FLAG_TYPE); return t != null ? t : Type.In; } public static Mode getMode(IElement e) { Mode m = e.getHint(KEY_FLAG_MODE); return m != null ? m : Mode.External; } public static double getBeakLength(IElement e) { double height = e.getHint(KEY_FLAG_HEIGHT); double beakAngle = e.getHint(KEY_FLAG_BEAK_ANGLE); beakAngle = Math.min(180, Math.max(10, beakAngle)); return height / (2*Math.tan(Math.toRadians(beakAngle) / 2)); } public static double getBeakLength(double height, double beakAngle) { beakAngle = Math.min(180, Math.max(10, beakAngle)); return height / (2*Math.tan(Math.toRadians(beakAngle) / 2)); } }