001/* The node controller for ports contained in entities.
002
003 Copyright (c) 1998-2016 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026 */
027package ptolemy.vergil.actor;
028
029import java.awt.Color;
030import java.awt.Font;
031import java.awt.Shape;
032import java.awt.geom.AffineTransform;
033import java.awt.geom.Ellipse2D;
034import java.awt.geom.Line2D;
035import java.awt.geom.Path2D;
036import java.awt.geom.Rectangle2D;
037import java.util.List;
038
039import javax.swing.SwingConstants;
040
041import diva.canvas.CanvasUtilities;
042import diva.canvas.CompositeFigure;
043import diva.canvas.Figure;
044import diva.canvas.Site;
045import diva.canvas.connector.PerimeterSite;
046import diva.canvas.connector.TerminalFigure;
047import diva.canvas.interactor.CompositeInteractor;
048import diva.canvas.toolbox.BasicFigure;
049import diva.canvas.toolbox.LabelFigure;
050import diva.graph.GraphController;
051import diva.graph.NodeRenderer;
052import diva.util.java2d.Polygon2D;
053import diva.util.java2d.ShapeUtilities;
054import ptolemy.actor.CommunicationAspect;
055import ptolemy.actor.IOPort;
056import ptolemy.actor.PubSubPort;
057import ptolemy.actor.PublisherPort;
058import ptolemy.actor.SubscriberPort;
059import ptolemy.actor.gui.ColorAttribute;
060import ptolemy.actor.gui.PtolemyPreferences;
061import ptolemy.actor.parameters.ParameterPort;
062import ptolemy.data.ArrayToken;
063import ptolemy.data.BooleanToken;
064import ptolemy.data.DoubleToken;
065import ptolemy.data.IntToken;
066import ptolemy.data.Token;
067import ptolemy.data.expr.Parameter;
068import ptolemy.data.type.Typeable;
069import ptolemy.kernel.InstantiableNamedObj;
070import ptolemy.kernel.Port;
071import ptolemy.kernel.util.Attribute;
072import ptolemy.kernel.util.IllegalActionException;
073import ptolemy.kernel.util.NameDuplicationException;
074import ptolemy.kernel.util.NamedObj;
075import ptolemy.kernel.util.StringAttribute;
076import ptolemy.vergil.kernel.AttributeController;
077
078///////////////////////////////////////////////////////////////////
079//// IOPortController
080
081/**
082 This class provides interaction with nodes that represent Ptolemy II
083 ports on an actor.  It provides a double click binding and context
084 menu entry to edit the parameters of the port ("Configure") and a
085 command to get documentation.
086 It can have one of two access levels, FULL or PARTIAL.
087 If the access level is FULL, the the context menu also
088 contains a command to rename the node.
089 Note that whether the port is an input or output or multiport cannot
090 be controlled via this interface.  The "Configure Ports" command of
091 the container should be invoked instead.
092
093 @author Steve Neuendorffer and Edward A. Lee
094 @version $Id$
095 @since Ptolemy II 2.0
096 @Pt.ProposedRating Red (eal)
097 @Pt.AcceptedRating Red (johnr)
098 */
099public class IOPortController extends AttributeController {
100    /** Create a port controller associated with the specified graph
101     *  controller.  The controller is given full access.
102     *  @param controller The associated graph controller.
103     */
104    public IOPortController(GraphController controller) {
105        this(controller, FULL);
106    }
107
108    /** Create a port controller associated with the
109     *  specified graph controller.
110     *  @param controller The associated graph controller.
111     *  @param access The access level.
112     */
113    public IOPortController(GraphController controller, Access access) {
114        super(controller, access);
115
116        // Override this default value to ensure that ports are
117        // not decorated to indicate that they are derived.
118        // Instead, the entire actor will be decorated.
119        _decoratable = false;
120
121        setNodeRenderer(new EntityPortRenderer());
122
123        //        // "Listen to Actor"
124        //        _menuFactory.addMenuItemFactory(new MenuActionFactory(
125        //                new ListenToPortAction()));
126
127        // Ports of entities do not use a selection interactor with
128        // the same selection model as the rest of the first level figures.
129        // If this were allowed, then the port would be able to be deleted.
130        CompositeInteractor interactor = new CompositeInteractor();
131        setNodeInteractor(interactor);
132        interactor.addInteractor(_menuCreator);
133    }
134
135    ///////////////////////////////////////////////////////////////////
136    ////                         public variables                  ////
137
138    /** The spacing between individual connections to a multiport. */
139    public static final double MULTIPORT_CONNECTION_SPACING = 5.0;
140
141    ///////////////////////////////////////////////////////////////////
142    ////                         public methods                    ////
143
144    /** Return one of {-270, -180, -90, 0, 90, 180, 270} specifying
145     *  the orientation of a port. This depends on whether the port
146     *  is an input, output, or both, whether the port has a parameter
147     *  named "_cardinal" that specifies a cardinality, and whether the
148     *  containing actor has a parameter named "_rotatePorts" that
149     *  specifies a rotation of the ports.  In addition, if the
150     *  containing actor has a parameter named "_flipPortsHorizontal"
151     *  or "_flipPortsVertical" with value true, then any ports that end up on the left
152     *  or right (top or bottom) will be reversed.
153     *  @param port The port.
154     *  @return One of {-270, -180, -90, 0, 90, 180, 270}.
155     */
156    public static int getCardinality(Port port) {
157        // Determine whether the port has an attribute that specifies
158        // which side of the icon it should be on, and whether the
159        // actor has an attribute that rotates the ports. If both
160        // are present, the port attribute takes precedence.
161
162        boolean isInput = false;
163        boolean isOutput = false;
164        boolean isInputOutput = false;
165
166        // Figure out what type of port we're dealing with.
167        // If ports are not IOPorts, then draw then as ports with
168        // no direction.
169        if (port instanceof IOPort) {
170            isInput = ((IOPort) port).isInput();
171            isOutput = ((IOPort) port).isOutput();
172            isInputOutput = isInput && isOutput;
173        }
174
175        StringAttribute cardinal = null;
176        int portRotation = 0;
177        try {
178            cardinal = (StringAttribute) port.getAttribute("_cardinal",
179                    StringAttribute.class);
180            NamedObj container = port.getContainer();
181            if (container != null) {
182                Parameter rotationParameter = (Parameter) container
183                        .getAttribute("_rotatePorts", Parameter.class);
184                if (rotationParameter != null) {
185                    Token rotationValue = rotationParameter.getToken();
186                    if (rotationValue instanceof IntToken) {
187                        portRotation = ((IntToken) rotationValue).intValue();
188                    }
189                }
190            }
191        } catch (IllegalActionException ex) {
192            // Ignore and use defaults.
193        }
194        if (cardinal == null) {
195            // Port cardinality is not specified in the port.
196            if (isInputOutput) {
197                portRotation += -90;
198            } else if (isOutput) {
199                portRotation += 180;
200            }
201        } else if (cardinal.getExpression().equalsIgnoreCase("NORTH")) {
202            portRotation = 90;
203        } else if (cardinal.getExpression().equalsIgnoreCase("SOUTH")) {
204            portRotation = -90;
205        } else if (cardinal.getExpression().equalsIgnoreCase("EAST")) {
206            portRotation = 180;
207        } else if (cardinal.getExpression().equalsIgnoreCase("WEST")) {
208            portRotation = 0;
209        } else { // this shouldn't happen either
210            portRotation += -90;
211        }
212
213        // Ensure that the port rotation is one of
214        // {-270, -180, -90, 0, 90, 180, 270}.
215        portRotation = 90 * (portRotation / 90 % 4);
216
217        // Finally, check for horizontal or vertical flipping.
218        try {
219            NamedObj container = port.getContainer();
220            if (container != null) {
221                Parameter flipHorizontalParameter = (Parameter) container
222                        .getAttribute("_flipPortsHorizontal", Parameter.class);
223                if (flipHorizontalParameter != null) {
224                    Token rotationValue = flipHorizontalParameter.getToken();
225                    if (rotationValue instanceof BooleanToken
226                            && ((BooleanToken) rotationValue).booleanValue()) {
227                        if (portRotation == 0 || portRotation == -180) {
228                            portRotation += 180;
229                        } else if (portRotation == 180) {
230                            portRotation = 0;
231                        }
232                    }
233                }
234                Parameter flipVerticalParameter = (Parameter) container
235                        .getAttribute("_flipPortsVertical", Parameter.class);
236                if (flipVerticalParameter != null) {
237                    Token rotationValue = flipVerticalParameter.getToken();
238                    if (rotationValue instanceof BooleanToken
239                            && ((BooleanToken) rotationValue).booleanValue()) {
240                        if (portRotation == -270 || portRotation == -90) {
241                            portRotation += 180;
242                        } else if (portRotation == 90 || portRotation == 270) {
243                            portRotation -= 180;
244                        }
245                    }
246                }
247            }
248        } catch (IllegalActionException ex) {
249            // Ignore and use defaults.
250        }
251
252        return portRotation;
253    }
254
255    /** Return the direction associated with the specified angle,
256     *  which is assumed to be one of {-270, -180, -90, 0, 90, 180, 270}.
257     *  @param portRotation The angle
258     *  @return One of SwingUtilities.NORTH, SwingUtilities.EAST,
259     *  SwingUtilities.SOUTH, or SwingUtilities.WEST.
260     */
261    public static int getDirection(int portRotation) {
262        int direction;
263        if (portRotation == 90 || portRotation == -270) {
264            direction = SwingConstants.NORTH;
265        } else if (portRotation == 180 || portRotation == -180) {
266            direction = SwingConstants.EAST;
267        } else if (portRotation == 270 || portRotation == -90) {
268            direction = SwingConstants.SOUTH;
269        } else {
270            direction = SwingConstants.WEST;
271        }
272        return direction;
273    }
274
275    /** Return one of {-270, -180, -90, 0, 90, 180, 270} specifying
276     *  the orientation of a port. This depends on whether the port
277     *  is an input, output, or both, whether the port has a parameter
278     *  named "_cardinal" that specifies a cardinality, and whether the
279     *  containing actor has a parameter named "_rotatePorts" that
280     *  specifies a rotation of the ports.  In addition, if the
281     *  containing actor has a parameter named "_flipPortsHorizontal"
282     *  or "_flipPortsVertical" with value true, then any ports that end up on the left
283     *  or right (top or bottom) will be reversed.
284     *  @deprecated Use public getCardinality() method instead
285     *  @param port The port.
286     *  @return One of {-270, -180, -90, 0, 90, 180, 270}.
287     *  @see ptolemy.vergil.actor.IOPortController#getCardinality(Port)
288     */
289    @Deprecated
290    protected static int _getCardinality(Port port) {
291        return getCardinality(port);
292    }
293
294    /**
295     * Get the class label of the component.
296     * @return the class label of the component.
297     */
298    @Override
299    protected String _getComponentType() {
300        return "Port";
301    }
302
303    /** Return the direction associated with the specified angle,
304     *  which is assumed to be one of {-270, -180, -90, 0, 90, 180, 270}.
305     *  @deprecated Use public getDirection() method instead
306     *  @param portRotation The angle
307     *  @return One of SwingUtilities.NORTH, SwingUtilities.EAST,
308     *  SwingUtilities.SOUTH, or SwingUtilities.WEST.
309     *  @see ptolemy.vergil.actor.IOPortController#getDirection(int)
310     */
311    @Deprecated
312    protected static int _getDirection(int portRotation) {
313        return getDirection(portRotation);
314    }
315
316    /** The text used in the MoveAction.TO_FIRST action menu choice.
317     */
318    @Override
319    protected String _moveToFirstDescription() {
320        return "Move to First";
321    }
322
323    /** The text used in the MoveAction.TO_LAST action menu choice.
324     */
325    @Override
326    protected String _moveToLastDescription() {
327        return "Move to Last";
328    }
329
330    ///////////////////////////////////////////////////////////////////
331    ////                         private variables                 ////
332
333    /** Color for publish and subscribe labels. */
334    private static Color _pubSubLabelColor = new Color(0.0f, 0.4f, 0.4f, 1.0f);
335
336    /** Font for port labels. */
337    private static Font _pubSubLabelFont = new Font("SansSerif", Font.PLAIN,
338            10);
339
340    ///////////////////////////////////////////////////////////////////
341    ////                         inner classes                     ////
342
343    /**
344     * Render the ports of components as triangles. Multiports are rendered
345     * hollow, while single ports are rendered filled in black. ParameterPort
346     * is filled in grey. PublisherPort and SubscriberPort are filled with
347     * cyan.
348     */
349    public class EntityPortRenderer implements NodeRenderer {
350
351        /**  Render a visual representation of the given node. If the
352         * StringAttribute _color of the node is set then use that color to
353         * render the node. If the StringAttribute _explanation of the node is
354         * set then use it to set the tooltip.
355         * @see diva.graph.NodeRenderer#render(java.lang.Object)
356         */
357        @Override
358        public Figure render(Object n) {
359            final Port port = (Port) n;
360
361            // If the port has an attribute called "_hide", then
362            // do not render it.
363            if (_isPropertySet(port, "_hide")) {
364                return null;
365            }
366
367            boolean isInput = false;
368            boolean isOutput = false;
369            boolean isInputOutput = false;
370
371            // Figure out what type of port we're dealing with.
372            // If ports are not IOPorts, then draw then as ports with
373            // no direction.
374            if (port instanceof IOPort) {
375                isInput = ((IOPort) port).isInput();
376                isOutput = ((IOPort) port).isOutput();
377                isInputOutput = isInput && isOutput;
378            }
379
380            // The shape that the port will have. These are all
381            // created as oriented to the West, i.e. the left side of
382            // the actor.
383            Shape shape;
384
385            // NOTE: The preferences mechanism may set this.
386            Token portSize = PtolemyPreferences.preferenceValue(port,
387                    "_portSize");
388
389            // Default size is 4.0.
390            double size = 4.0;
391            if (portSize instanceof DoubleToken) {
392                size = ((DoubleToken) portSize).doubleValue();
393            }
394            double halfSize = size / 2.0;
395            double doubleSize = size * 2.0;
396
397            if (size == 0.0) {
398                // Use an empty ellipse as the shape.
399                shape = new Ellipse2D.Double();
400            } else {
401                if (isInputOutput) {
402                    Polygon2D.Double polygon = new Polygon2D.Double();
403                    polygon.moveTo(0, -size);
404                    polygon.lineTo(-size, -size);
405                    polygon.lineTo(-halfSize, 0);
406                    polygon.lineTo(-size, size);
407                    polygon.lineTo(size, size);
408                    polygon.lineTo(halfSize, 0);
409                    polygon.lineTo(size, -size);
410                    polygon.lineTo(0, -size);
411                    polygon.closePath();
412                    shape = polygon;
413                } else if (isInput) {
414                    Polygon2D.Double polygon = new Polygon2D.Double();
415                    polygon.moveTo(-size, 0);
416                    polygon.lineTo(-size, size);
417                    polygon.lineTo(size, 0);
418                    polygon.lineTo(-size, -size);
419                    polygon.lineTo(-size, 0);
420                    polygon.closePath();
421                    shape = polygon;
422                } else if (isOutput) {
423                    Polygon2D.Double polygon = new Polygon2D.Double();
424                    polygon.moveTo(size, 0);
425                    polygon.lineTo(size, -size);
426                    polygon.lineTo(-size, 0);
427                    polygon.lineTo(size, size);
428                    polygon.lineTo(size, 0);
429                    polygon.closePath();
430                    shape = polygon;
431                } else {
432                    shape = new Ellipse2D.Double(-size, -size, doubleSize,
433                            doubleSize);
434                }
435            }
436            boolean isPubSubPort = port instanceof PubSubPort;
437            Color fill;
438            if (port instanceof ParameterPort) {
439                fill = Color.lightGray;
440            } else if (isPubSubPort) {
441                fill = Color.CYAN;
442            } else if (port instanceof IOPort
443                    && ((IOPort) port).isMultiport()) {
444                fill = Color.white;
445            } else {
446                fill = Color.black;
447            }
448
449            // Handle communication aspects.
450            try {
451                if (port instanceof IOPort) {
452                    List<CommunicationAspect> communicationAspects = ((IOPort) port)
453                            .getCommunicationAspects();
454                    if (communicationAspects != null
455                            && communicationAspects.size() > 0) {
456                        Object object = null;
457                        if (((IOPort) port).isOutput()) {
458                            object = communicationAspects.get(0);
459                        } else {
460                            object = communicationAspects
461                                    .get(communicationAspects.size() - 1);
462                        }
463                        ColorAttribute color = null;
464                        if (object != null) {
465                            color = (ColorAttribute) ((NamedObj) object)
466                                    .getAttribute(
467                                            CommunicationAspect.decoratorHighlightColorName);
468                        }
469                        if (color != null) {
470                            fill = color.asColor();
471                        } else {
472                            fill = Color.BLACK;
473                        }
474
475                        StringAttribute info = (StringAttribute) port
476                                .getAttribute("_showInfo");
477                        if (info == null) {
478                            info = new StringAttribute(port, "_showInfo");
479                        }
480
481                        StringBuffer aspectsStringBuffer = new StringBuffer();
482                        for (int j = 0; j < communicationAspects.size(); j++) {
483                            NamedObj namedObj = (NamedObj) communicationAspects
484                                    .get(j);
485                            if (namedObj != null) {
486                                if (aspectsStringBuffer.length() > 0) {
487                                    aspectsStringBuffer.append(", ");
488                                }
489                                aspectsStringBuffer.append(namedObj.getName());
490                            }
491                        }
492                        info.setExpression(
493                                "Aspects: " + aspectsStringBuffer.toString());
494                    } else {
495                        // No CommunicationAspect in use anymore, clean up _showInfo
496                        // string.
497
498                        // Use Attribute here instead of StringAttribute since
499                        // the attribute could be e.g. a StringParameter, which
500                        // is an Attribute but not a StringAttribute
501                        Attribute info = port.getAttribute("_showInfo");
502                        if (info != null && info instanceof StringAttribute) {
503                            String infoString = ((StringAttribute) info)
504                                    .getValueAsString();
505                            if (infoString.contains("Aspects:")) {
506                                int start = infoString.indexOf("Aspects: ");
507                                int end = infoString.length();
508                                String aspectInfo = infoString.substring(start,
509                                        end);
510                                infoString = infoString.replace(aspectInfo, "");
511                                infoString = infoString.trim();
512                                ((StringAttribute) info)
513                                        .setExpression(infoString);
514                            }
515                        }
516                    }
517                }
518            } catch (IllegalActionException e1) {
519                // TODO Auto-generated catch block
520                e1.printStackTrace();
521            } catch (NameDuplicationException e) {
522                // Ignore. This exception should be thrown because before adding the
523                // new ColorAttribute any previous attribute with the same name is
524                // removed.
525                e.printStackTrace();
526            }
527
528            ColorAttribute colorAttribute;
529            try {
530                colorAttribute = (ColorAttribute) port.getAttribute("_color",
531                        ColorAttribute.class);
532                if (colorAttribute != null) {
533                    Color color = colorAttribute.asColor();
534                    fill = color;
535                }
536            } catch (IllegalActionException e1) {
537                // TODO Auto-generated catch block
538                e1.printStackTrace();
539            }
540
541            //            StringAttribute _colorAttr = (StringAttribute) (port
542            //                    .getAttribute("_color"));
543            //
544            //            if (_colorAttr != null) {
545            //                String _color = _colorAttr.getExpression();
546            //                fill = SVGUtilities.getColor(_color);
547            //            }
548
549            //ActorGraphModel model = (ActorGraphModel) getController()
550            //        .getGraphModel();
551
552            int portRotation = getCardinality(port);
553            int direction = getDirection(portRotation);
554
555            // Transform the port shape so it is facing the right way.
556            double rotation = portRotation;
557            AffineTransform transform = AffineTransform
558                    .getRotateInstance(Math.toRadians(rotation));
559            shape = ShapeUtilities.transformModify(shape, transform);
560
561            // Create a figure with a tooltip.
562            Figure figure = new BasicFigure(shape, fill, (float) 1.5) {
563                // Override this because we want to show the type.
564                // It doesn't work to set it once because the type
565                // has not been resolved, and anyway, it may
566                // change. NOTE: This is copied below.
567                @Override
568                public String getToolTipText() {
569                    String tipText = port.getName();
570                    String displayName = port.getDisplayName();
571                    if (!tipText.equals(displayName)) {
572                        tipText = displayName + " (" + tipText + ")";
573                    }
574                    StringAttribute _explAttr = (StringAttribute) port
575                            .getAttribute("_explanation");
576
577                    if (_explAttr != null) {
578                        tipText = _explAttr.getExpression();
579                    } else if (port instanceof Typeable) {
580                        try {
581                            tipText = tipText + ", type:"
582                                    + ((Typeable) port).getType();
583                        } catch (IllegalActionException ex) {
584                            // System.out.println("Tooltip failed: " + ex);
585                        }
586                    }
587
588                    return tipText;
589                }
590            };
591            // Have to do this also, or the AWT doesn't display any
592            // tooltip at all.
593            String tipText = port.getName();
594            String displayName = port.getDisplayName();
595            if (!tipText.equals(displayName)) {
596                tipText = displayName + " (" + tipText + ")";
597            }
598            figure.setToolTipText(tipText);
599
600            double normal = CanvasUtilities.getNormal(direction);
601
602            if (port instanceof IOPort) {
603                // Create a diagonal connector for multiports, if necessary.
604                IOPort ioPort = (IOPort) port;
605
606                if (ioPort.isMultiport()) {
607                    int numberOfLinks = ioPort.linkedRelationList().size();
608
609                    if (numberOfLinks > 1) {
610                        // The diagonal is necessary.
611                        CompositeFigure compositeFigure = new CompositeFigure(
612                                figure) {
613                            // Override this because we want to show the type.
614                            // It doesn't work to set it once because the type
615                            // has not been resolved, and anyway, it may
616                            // change. NOTE: This is copied from above.
617                            @Override
618                            public String getToolTipText() {
619                                String tipText = port.getName();
620                                String displayName = port.getDisplayName();
621                                if (!tipText.equals(displayName)) {
622                                    tipText = displayName + " (" + tipText
623                                            + ")";
624                                }
625                                StringAttribute _explAttr = (StringAttribute) port
626                                        .getAttribute("_explanation");
627
628                                if (_explAttr != null) {
629                                    tipText = _explAttr.getExpression();
630                                } else if (port instanceof Typeable) {
631                                    try {
632                                        tipText = tipText + ", type:"
633                                                + ((Typeable) port).getType();
634                                    } catch (IllegalActionException ex) {
635                                        // System.out.println("Tooltip failed: " + ex);
636                                    }
637                                }
638                                return tipText;
639                            }
640                        };
641
642                        // Line depends on the orientation.
643                        double startX;
644
645                        // Line depends on the orientation.
646                        double startY;
647
648                        // Line depends on the orientation.
649                        double endX;
650
651                        // Line depends on the orientation.
652                        double endY;
653                        Rectangle2D bounds = figure.getShape().getBounds2D();
654                        double x = bounds.getX();
655                        double y = bounds.getY();
656                        double width = bounds.getWidth();
657                        double height = bounds.getHeight();
658                        int extent = numberOfLinks - 1;
659
660                        if (direction == SwingConstants.EAST) {
661                            startX = x + width;
662                            startY = y + height / 2;
663                            endX = startX
664                                    + extent * MULTIPORT_CONNECTION_SPACING;
665                            endY = startY
666                                    + extent * MULTIPORT_CONNECTION_SPACING;
667                        } else if (direction == SwingConstants.WEST) {
668                            startX = x;
669                            startY = y + height / 2;
670                            endX = startX
671                                    - extent * MULTIPORT_CONNECTION_SPACING;
672                            endY = startY
673                                    - extent * MULTIPORT_CONNECTION_SPACING;
674                        } else if (direction == SwingConstants.NORTH) {
675                            startX = x + width / 2;
676                            startY = y;
677                            endX = startX
678                                    - extent * MULTIPORT_CONNECTION_SPACING;
679                            endY = startY
680                                    - extent * MULTIPORT_CONNECTION_SPACING;
681                        } else {
682                            startX = x + width / 2;
683                            startY = y + height;
684                            endX = startX
685                                    + extent * MULTIPORT_CONNECTION_SPACING;
686                            endY = startY
687                                    + extent * MULTIPORT_CONNECTION_SPACING;
688                        }
689
690                        Line2D line = new Line2D.Double(startX, startY, endX,
691                                endY);
692                        Figure lineFigure = new BasicFigure(line, fill,
693                                (float) 2.0);
694                        compositeFigure.add(lineFigure);
695                        figure = compositeFigure;
696                    }
697                }
698                if (port instanceof PublisherPort) {
699                    CompositeFigure compositeFigure = new CompositeFigure(
700                            figure);
701                    Path2D path = new Path2D.Double();
702                    path.moveTo(0.5 * size, -size);
703                    path.lineTo(2.5 * size, 0);
704                    path.lineTo(0.5 * size, size);
705
706                    path.moveTo(2 * size, -size);
707                    path.lineTo(4 * size, 0);
708                    path.lineTo(2 * size, size);
709
710                    Figure pathFigure = new BasicFigure(path);
711                    compositeFigure.add(pathFigure);
712
713                    String channel = "???";
714                    try {
715                        if (((InstantiableNamedObj) port.getContainer())
716                                .isWithinClassDefinition()) {
717                            // If the port is in a class definition, do not expand it, it might contain $foo.$bar.
718                            channel = ((PublisherPort) port).channel
719                                    .getExpression();
720                        } else {
721                            channel = ((PublisherPort) port).channel
722                                    .stringValue();
723                        }
724                        // Also display the number of initial tokens in parens.
725                        Token initialTokens = ((PublisherPort) port).initialTokens
726                                .getToken();
727                        if (initialTokens instanceof ArrayToken) {
728                            int number = ((ArrayToken) initialTokens).length();
729                            channel = channel + " (" + number + ")";
730                        }
731                    } catch (IllegalActionException e) {
732                        // Ignore and display question marks.
733                        e.printStackTrace();
734                    }
735                    // The anchor argument below is (sadly) ignored.
736                    Figure label = new LabelFigure(channel, _pubSubLabelFont,
737                            0.0, SwingConstants.SOUTH_EAST, _pubSubLabelColor);
738                    double labelHeight = label.getBounds().getHeight();
739                    label.translate(5 * size, 0.5 * labelHeight);
740                    compositeFigure.add(label);
741
742                    figure = compositeFigure;
743                } else if (port instanceof SubscriberPort) {
744                    CompositeFigure compositeFigure = new CompositeFigure(
745                            figure);
746                    Path2D path = new Path2D.Double();
747                    path.moveTo(-2 * size, -size);
748                    path.lineTo(-2 * size, size);
749
750                    path.moveTo(-3 * size, -size);
751                    path.lineTo(-3 * size, size);
752
753                    path.moveTo(-4 * size, -size);
754                    path.lineTo(-4 * size, size);
755
756                    Figure pathFigure = new BasicFigure(path);
757                    compositeFigure.add(pathFigure);
758
759                    String channel = "???";
760                    try {
761                        if (((InstantiableNamedObj) port.getContainer())
762                                .isWithinClassDefinition()) {
763                            // If the port is in a class definition, do not expand it, it might contain $foo.$bar.
764                            channel = ((SubscriberPort) port).channel
765                                    .getExpression();
766                        } else {
767                            channel = ((SubscriberPort) port).channel
768                                    .stringValue();
769                        }
770                        // Also display the number of initial tokens in parens.
771                        Token initialTokens = ((SubscriberPort) port).initialTokens
772                                .getToken();
773                        if (initialTokens instanceof ArrayToken) {
774                            int number = ((ArrayToken) initialTokens).length();
775                            channel = channel + " (" + number + ")";
776                        }
777                    } catch (IllegalActionException e) {
778                        // Ignore and display question marks.
779                        e.printStackTrace();
780                    }
781                    // The anchor argument below is (sadly) ignored.
782                    Figure label = new LabelFigure(channel, _pubSubLabelFont,
783                            0.0, SwingConstants.SOUTH_EAST, _pubSubLabelColor);
784                    double labelHeight = label.getBounds().getHeight();
785                    double labelWidth = label.getBounds().getWidth();
786                    label.translate(-labelWidth - 5 * size, 0.5 * labelHeight);
787                    compositeFigure.add(label);
788
789                    figure = compositeFigure;
790                }
791                figure = _decoratePortFigure(n, figure);
792                // Wrap the figure in a TerminalFigure to set the direction that
793                // connectors exit the port. Note that this direction is the
794                // same direction that is used to layout the port in the
795                // Entity Controller.
796                figure = new PortTerminal(ioPort, figure, normal, false);
797            } else {
798                figure = _decoratePortFigure(n, figure);
799                Site tsite = new PerimeterSite(figure, 0);
800                tsite.setNormal(normal);
801                figure = new TerminalFigure(figure, tsite);
802            }
803
804            // New way to specify a highlight color.
805            AttributeController.renderHighlight(port, figure);
806
807            return figure;
808        }
809
810        /** Decorate the figure according to the properties of the node. This
811         *  method does nothing, but subclasses may override this method to
812         *  decorate the port's figure (e.g., add highlighting color).
813         *
814         *  @param node The node.
815         *  @param figure The port's figure before decoration.
816         *  @return The port's figure after decoration (which may or may not be
817         *   the figure given in the parameter).
818         */
819        protected Figure _decoratePortFigure(Object node, Figure figure) {
820            return figure;
821        }
822    }
823}