001/* The graph controller for the ptolemy schematic editor ports
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
027 */
028package ptolemy.vergil.actor;
029
030import java.awt.Color;
031import java.awt.Font;
032import java.awt.Shape;
033import java.awt.geom.Ellipse2D;
034import java.awt.geom.Line2D;
035import java.awt.geom.Rectangle2D;
036import java.util.HashMap;
037import java.util.List;
038
039import javax.swing.Action;
040import javax.swing.SwingConstants;
041
042import diva.canvas.CanvasUtilities;
043import diva.canvas.CompositeFigure;
044import diva.canvas.Figure;
045import diva.canvas.Site;
046import diva.canvas.connector.FixedNormalSite;
047import diva.canvas.connector.PerimeterSite;
048import diva.canvas.connector.TerminalFigure;
049import diva.canvas.toolbox.BasicFigure;
050import diva.canvas.toolbox.LabelFigure;
051import diva.graph.GraphController;
052import diva.graph.GraphPane;
053import diva.util.java2d.Polygon2D;
054import ptolemy.actor.IOPort;
055import ptolemy.actor.PubSubPort;
056import ptolemy.actor.PublisherPort;
057import ptolemy.actor.SubscriberPort;
058import ptolemy.actor.gui.Configuration;
059import ptolemy.actor.parameters.ParameterPort;
060import ptolemy.data.type.Typeable;
061import ptolemy.kernel.InstantiableNamedObj;
062import ptolemy.kernel.Port;
063import ptolemy.kernel.util.Attribute;
064import ptolemy.kernel.util.IllegalActionException;
065import ptolemy.kernel.util.InternalErrorException;
066import ptolemy.kernel.util.KernelException;
067import ptolemy.kernel.util.Locatable;
068import ptolemy.kernel.util.Location;
069import ptolemy.kernel.util.NamedObj;
070import ptolemy.vergil.basic.BasicGraphController;
071import ptolemy.vergil.basic.BasicGraphFrame;
072import ptolemy.vergil.basic.WithIconGraphController;
073import ptolemy.vergil.icon.EditorIcon;
074import ptolemy.vergil.kernel.AttributeController;
075import ptolemy.vergil.toolbox.EditIconAction;
076import ptolemy.vergil.toolbox.RemoveIconAction;
077import ptolemy.vergil.toolbox.SnapConstraint;
078
079///////////////////////////////////////////////////////////////////
080//// ExternalIOPortController
081
082/**
083 This class provides interaction with nodes that represent Ptolemy II
084 ports inside a composite.  It provides a double click binding and context
085 menu entry to edit the parameters of the port ("Configure") and a
086 command to get documentation.
087 It can have one of two access levels, FULL or PARTIAL.
088 If the access level is FULL, the the context menu also
089 contains a command to rename the node.
090 Note that whether the port is an input or output or multiport cannot
091 be controlled via this interface.  The "Configure Ports" command of
092 the container should be invoked instead.
093
094 @author Steve Neuendorffer and Edward A. Lee, Elaine Cheong
095 @version $Id$
096 @since Ptolemy II 2.0
097 @Pt.ProposedRating Red (eal)
098 @Pt.AcceptedRating Red (johnr)
099 */
100public class ExternalIOPortController extends AttributeController {
101    /** Create a port controller associated with the specified graph
102     *  controller.  The controller is given full access.
103     *  @param controller The associated graph controller.
104     */
105    public ExternalIOPortController(GraphController controller) {
106        this(controller, FULL);
107    }
108
109    /** Create a port controller associated with the specified graph
110     *  controller.
111     *  @param controller The associated graph controller.
112     *  @param access The access level.
113     */
114    public ExternalIOPortController(GraphController controller, Access access) {
115        super(controller, access);
116        setNodeRenderer(new PortRenderer());
117    }
118
119    ///////////////////////////////////////////////////////////////////
120    ////                         public members                    ////
121
122    /** Prototype input port. */
123    public static final IOPort _GENERIC_INPUT = new IOPort();
124
125    /** Prototype output port. */
126    public static final IOPort _GENERIC_OUTPUT = new IOPort();
127
128    /** Prototype inout port. */
129    public static final IOPort _GENERIC_INOUT = new IOPort();
130
131    /** Prototype input multiport. */
132    public static final IOPort _GENERIC_INPUT_MULTIPORT = new IOPort();
133
134    /** Prototype output multiport. */
135    public static final IOPort _GENERIC_OUTPUT_MULTIPORT = new IOPort();
136
137    /** Prototype inout multiport. */
138    public static final IOPort _GENERIC_INOUT_MULTIPORT = new IOPort();
139
140    /** Polygon coordinates for input output port. */
141    public static Integer[] IOPORT_COORDINATES = new Integer[] { 0, 4, 0, 9, 6,
142            4, 12, 4, 12, -4, 6, -4, 0, -9, 0, -4, -8, -4 };
143
144    /** Polygon coordinates for input port. */
145    public static Integer[] IPORT_COORDINATES = new Integer[] { 0, 4, 0, 9, 12,
146            0, 0, -9, 0, -4, -8, -4 };
147
148    /** Polygon coordinates for output port. */
149    public static Integer[] OPORT_COORDINATES = new Integer[] { -8, 9, -2, 4,
150            12, 4, 12, -4, -2, -4, -8, -9 };
151
152    /** Polygon coordinates for input output multiport. */
153    public static Integer[] MULTI_IPORT_COORDINATES = new Integer[] { -5, 4, -5,
154            9, 1, 4, 1, 9, 7, 4, 12, 0, 7, -4, 1, -9, 1, -4, -5, -9, -5, -4, -8,
155            -4 };
156
157    /** Polygon coordinates for output multiport. */
158    public static Integer[] MULTI_OPORT_COORDINATES = new Integer[] { -8, 4, -8,
159            9, -2, 4, -2, 9, 4, 4, 12, 4, 12, -4, 4, -4, -2, -9, -2, -4, -8,
160            -9 };
161
162    /** Polygon coordinates for input multiport. */
163    public static Integer[] MULTI_IOPORT_COORDINATES = new Integer[] { -4, 4,
164            -4, 9, 2, 4, 2, 9, 8, 4, 12, 4, 12, -4, 8, -4, 2, -9, 2, -4, -4, -9,
165            -4, -4, -8, -4 };
166
167    // Static initializer.
168    static {
169        try {
170            _GENERIC_INPUT.setInput(true);
171            _GENERIC_OUTPUT.setOutput(true);
172            _GENERIC_INOUT.setInput(true);
173            _GENERIC_INOUT.setOutput(true);
174            _GENERIC_INPUT_MULTIPORT.setInput(true);
175            _GENERIC_OUTPUT_MULTIPORT.setOutput(true);
176            _GENERIC_INOUT_MULTIPORT.setInput(true);
177            _GENERIC_INOUT_MULTIPORT.setOutput(true);
178            _GENERIC_INPUT_MULTIPORT.setMultiport(true);
179            _GENERIC_OUTPUT_MULTIPORT.setMultiport(true);
180            _GENERIC_INOUT_MULTIPORT.setMultiport(true);
181
182            // Need location attributes for these ports in order to
183            // be able to render them.
184            new Location(_GENERIC_INPUT, "_location");
185            new Location(_GENERIC_OUTPUT, "_location");
186            new Location(_GENERIC_INOUT, "_location");
187            new Location(_GENERIC_INPUT_MULTIPORT, "_location");
188            new Location(_GENERIC_OUTPUT_MULTIPORT, "_location");
189            new Location(_GENERIC_INOUT_MULTIPORT, "_location");
190        } catch (KernelException ex) {
191            // Should not occur.
192            throw new InternalErrorException(null, ex, null);
193        }
194    }
195
196    /** Move the node's figure to the location specified in the node's
197     *  semantic object, if that object is an instance of Locatable.
198     *  If the semantic object is not a location, then do nothing.
199     *  If the figure associated with the semantic object is an instance
200     *  of TerminalFigure, then modify the location to ensure that the
201     *  connect site snaps to grid.
202     *  @param node The object to locate.
203     */
204    @Override
205    public void locateFigure(Object node) {
206        Figure nf = getController().getFigure(node);
207
208        try {
209            if (hasLocation(node)) {
210                double[] location = getLocation(node);
211                if (node instanceof Location) {
212                    NamedObj port = ((Location) node).getContainer();
213
214                    // In case the location is (0,0) we try to come up with a
215                    // better one.
216                    if (port instanceof IOPort && location[0] == 0.0
217                            && location[1] == 0.0) {
218                        BasicGraphController controller = (BasicGraphController) getController();
219                        BasicGraphFrame frame = controller.getFrame();
220
221                        // We have a bootstrapping problem. In case the window
222                        // is just being opened, the rendering happens before the
223                        // creation of the the JGraph, and we don't know the actual
224                        // size of the window (hence the magic numbers below in case
225                        // frame.getJGraph() == null.
226                        if (frame.getJGraph() != null) {
227                            GraphPane pane = controller.getGraphPane();
228                            location = WithIconGraphController
229                                    .getNewPortLocation(pane, frame,
230                                            (IOPort) port);
231                        } else {
232                            IOPort ioPort = (IOPort) port;
233                            if (ioPort.isInput() && ioPort.isOutput()) {
234                                double[] newLocation = _inoutputPortLocations
235                                        .get(ioPort);
236                                if (newLocation != null) {
237                                    location = newLocation;
238                                } else {
239                                    // Put at the bottom
240                                    location[0] = 300.0
241                                            + _inoutputPortLocations.size()
242                                                    * 40;
243                                    location[1] = 380.0;
244                                    _inoutputPortLocations.put(ioPort,
245                                            location);
246                                }
247                            } else if (ioPort.isInput()) {
248                                double[] newLocation = _inputPortLocations
249                                        .get(ioPort);
250                                if (newLocation != null) {
251                                    location = newLocation;
252                                } else {
253                                    // Put at the left side
254                                    location[0] = 20.0;
255                                    location[1] = 200.0
256                                            + _inputPortLocations.size() * 40;
257                                    _inputPortLocations.put(ioPort, location);
258                                }
259                            } else if (ioPort.isOutput()) {
260                                double[] newLocation = _outputPortLocations
261                                        .get(ioPort);
262                                if (newLocation != null) {
263                                    location = newLocation;
264                                } else {
265                                    // Put at the right side
266                                    location[0] = 580.0;
267                                    location[1] = 200.0
268                                            + _outputPortLocations.size() * 40;
269                                    _outputPortLocations.put(ioPort, location);
270                                }
271                            } else {
272                                double[] newLocation = _otherPortLocations
273                                        .get(ioPort);
274                                if (newLocation != null) {
275                                    location = newLocation;
276                                } else {
277                                    // Put in the middle
278                                    location[0] = 300.0;
279                                    location[1] = 200.0
280                                            + _otherPortLocations.size() * 40;
281                                    _otherPortLocations.put(ioPort, location);
282                                }
283                            }
284
285                        }
286                        location = SnapConstraint.constrainPoint(location[0],
287                                location[1]);
288                    }
289                }
290                CanvasUtilities.translateTo(nf, location[0], location[1]);
291            }
292        } catch (Throwable throwable) {
293            // FIXME: Ignore if there is no valid location.  This
294            // happens occasionally due to a race condition in the
295            // Bouncer demo.  Occasionally, the repaint thread will
296            // attempt to locate the bouncing icon before the location
297            // parameter has been evaluated, causing an exception to
298            // be thrown.  Basically the lazy parameter evaluation
299            // mechanism causes rerendering in Diva to be rentrant,
300            // which it shouldn't be.  Unfortunately, I have no idea
301            // how to fix it... SN 5/5/2003
302        }
303    }
304
305    /** Set the configuration.  This is used in derived classes to
306     *  to open files (such as documentation).  The configuration is
307     *  is important because it keeps track of which files are already
308     *  open and ensures that there is only one editor operating on the
309     *  file at any one time.
310     *  @param configuration The configuration.
311     */
312    @Override
313    public void setConfiguration(Configuration configuration) {
314        super.setConfiguration(configuration);
315
316        if (_configuration != null) {
317            // Create an Appearance submenu.
318            _createAppearanceSubmenu();
319        }
320    }
321
322    ///////////////////////////////////////////////////////////////////
323    ////                         protected methods                 ////
324
325    /** Override the base class to return true if the specified node
326     *  contains an attribute named "_hideInside".  This ensures that
327     *  ports can be hidden on the outside while still being visible
328     *  on the outside.
329     */
330    @Override
331    protected boolean _hide(java.lang.Object node) {
332        if (node instanceof Locatable) {
333            if (((Locatable) node).getContainer()
334                    .getAttribute("_hideInside") != null) {
335                return true;
336            }
337        }
338
339        if (node instanceof NamedObj) {
340            if (((NamedObj) node).getAttribute("_hideInside") != null) {
341                return true;
342            }
343        }
344
345        return false;
346    }
347
348    /** Given a port, return a reasonable tooltip message for that port.
349     *  @param port The port.
350     *  @return The name, type, and whether it's a multiport.
351     */
352    protected String _portTooltip(final Port port) {
353        String tipText = port.getName();
354
355        if (port instanceof IOPort) {
356            IOPort ioport = (IOPort) port;
357
358            if (ioport.isInput()) {
359                tipText += ", Input";
360            }
361
362            if (ioport.isOutput()) {
363                tipText += ", Output";
364            }
365
366            if (ioport.isMultiport()) {
367                tipText += ", Multiport";
368            }
369
370            try {
371                tipText = tipText + ", type:" + ((Typeable) port).getType();
372            } catch (ClassCastException ex) {
373                // Do nothing.
374            } catch (IllegalActionException ex) {
375                // Do nothing.
376            }
377        }
378
379        return tipText;
380    }
381
382    ///////////////////////////////////////////////////////////////////
383    ////                         protected members                 ////
384
385    /** The action that handles edit custom icon. */
386    protected EditIconAction _editIconAction = new EditIconAction();
387
388    /** The font used to label a port. */
389    protected static Font _labelFont = new Font("SansSerif", Font.PLAIN, 12);
390
391    /** The action that handles removing a custom icon. */
392    protected RemoveIconAction _removeIconAction = new RemoveIconAction();
393
394    ///////////////////////////////////////////////////////////////////
395    ////                         private methods                   ////
396
397    /**
398     * Create an Appearance submenu.
399     */
400    private void _createAppearanceSubmenu() {
401        _editIconAction.setConfiguration(_configuration);
402        _removeIconAction.setConfiguration(_configuration);
403        Action[] actions = { _editIconAction, _removeIconAction };
404        _appearanceMenuActionFactory.addActions(actions, "Appearance");
405    }
406
407    ///////////////////////////////////////////////////////////////////
408    ////                         inner classes                     ////
409
410    /** Render the external ports of a graph as a 5-sided tab thingy.
411     *  Multiports are rendered hollow,
412     *  while single ports are rendered filled.
413     *  Publisher and subscriber ports are rendered specially.
414     */
415    public class PortRenderer extends IconRenderer {
416
417        /** Render a port.  If the argument implements Locatable,
418         *  then render the port that is the container of that locatable.
419         *  If the argument is an instance of _GENERIC_INPUT,
420         *  _GENERIC_OUTPUT, or _GENERIC_INOUT, then render an input,
421         *  output, or inout port with no name.  If the argument is null,
422         *  then render a port that is neither an input nor an output.
423         *  @param n An instance of Locatable or one of the objects
424         *   _GENERIC_INPUT, _GENERIC_OUTPUT, or _GENERIC_INOUT.
425         *  @return The figure that is rendered.
426         */
427        @Override
428        public Figure render(Object n) {
429            Figure figure = null;
430            Locatable location = (Locatable) n;
431            if (location == null) {
432                Polygon2D.Double polygon = new Polygon2D.Double();
433                polygon.moveTo(0, 0);
434                polygon.lineTo(0, 10);
435                polygon.lineTo(12, 0);
436                polygon.lineTo(0, -10);
437                polygon.closePath();
438
439                figure = new BasicFigure(polygon, Color.black);
440                figure.setToolTipText("Unknown port");
441                return figure;
442            }
443
444            final Port port = (Port) location.getContainer();
445            List iconList = port.attributeList(EditorIcon.class);
446
447            // Check to see whether there is an icon that has been created,
448            // but not inserted.
449            if (iconList.size() > 0) {
450                EditorIcon icon = (EditorIcon) iconList
451                        .get(iconList.size() - 1);
452                figure = icon.createFigure();
453            } else {
454                Attribute iconDescription = port
455                        .getAttribute("_iconDescription");
456                if (iconDescription != null) {
457                    figure = super.render(n);
458                }
459            }
460
461            // Wrap the figure in a TerminalFigure to set the direction that
462            // connectors exit the port.  Note that this direction is the
463            // OPPOSITE direction that is used to layout the port in the
464            // Entity Controller.
465            int direction;
466            Shape shape;
467            Polygon2D.Double polygon = new Polygon2D.Double();
468            Color fill = Color.black;
469            if (figure == null) {
470                if (!(port instanceof IOPort)) {
471                    polygon.moveTo(-6, 6);
472                    polygon.lineTo(0, 6);
473                    polygon.lineTo(8, 0);
474                    polygon.lineTo(0, -6);
475                    polygon.lineTo(-6, -6);
476                } else {
477                    IOPort ioport = (IOPort) port;
478                    polygon.moveTo(-8, 4);
479                    if (ioport.isMultiport()) {
480                        if (ioport instanceof ParameterPort) {
481                            // It would be better to couple these to the
482                            // parameters by position, but this is impossible
483                            // in diva, so we assign a special color.
484                            // FIXME: are Multiport ParameterPorts possible?
485                            // FIXME: Should this be lightGrey?
486                            fill = Color.darkGray;
487                        } else if (ioport instanceof PublisherPort
488                                || ioport instanceof SubscriberPort) {
489                            fill = Color.CYAN;
490                        } else {
491                            fill = Color.white;
492                        }
493                        if (ioport.isOutput() && ioport.isInput()) {
494                            polygon = _createPolygon(MULTI_IOPORT_COORDINATES,
495                                    polygon);
496                        } else if (ioport.isOutput()) {
497                            polygon = _createPolygon(MULTI_OPORT_COORDINATES,
498                                    polygon);
499                        } else if (ioport.isInput()) {
500                            polygon = _createPolygon(MULTI_IPORT_COORDINATES,
501                                    polygon);
502                        } else {
503                            polygon = null;
504                        }
505                    } else {
506                        if (ioport instanceof ParameterPort) {
507                            // It would be better to couple these to the
508                            // parameters by position, but this is impossible
509                            // in diva, so we assign a special color.
510                            // FIXME: what about multiports para
511                            fill = Color.lightGray;
512                        } else if (ioport instanceof PublisherPort
513                                || ioport instanceof SubscriberPort) {
514                            fill = Color.CYAN;
515                        } else {
516                            fill = Color.black;
517                        }
518                        if (ioport.isOutput() && ioport.isInput()) {
519                            polygon = _createPolygon(IOPORT_COORDINATES,
520                                    polygon);
521                        } else if (ioport.isOutput()) {
522                            polygon = _createPolygon(OPORT_COORDINATES,
523                                    polygon);
524                        } else if (ioport.isInput()) {
525                            polygon = _createPolygon(IPORT_COORDINATES,
526                                    polygon);
527                        } else {
528                            polygon = null;
529                        }
530                    }
531                }
532
533                if (polygon == null) {
534                    Ellipse2D.Double ellipse = new Ellipse2D.Double(0.0, 0.0,
535                            16.0, 16.0);
536                    shape = ellipse;
537                } else {
538                    polygon.closePath();
539                    shape = polygon;
540                }
541                if (port instanceof ParameterPort) {
542                    figure = new BasicFigure(shape, new Color(0, 0, 0, 0),
543                            (float) 0.0);
544                } else {
545                    figure = new BasicFigure(shape, fill, (float) 1.5);
546                }
547            }
548
549            if (!(port instanceof IOPort)) {
550                direction = SwingConstants.NORTH;
551            } else {
552                IOPort ioport = (IOPort) port;
553
554                if (ioport.isInput() && ioport.isOutput()) {
555                    direction = SwingConstants.NORTH;
556                } else if (ioport.isInput()) {
557                    direction = SwingConstants.EAST;
558                } else if (ioport.isOutput()) {
559                    direction = SwingConstants.WEST;
560                } else {
561                    // should never happen
562                    direction = SwingConstants.NORTH;
563                }
564            }
565
566            double normal = CanvasUtilities.getNormal(direction);
567            String name = port.getDisplayName();
568            Rectangle2D backBounds = figure.getBounds();
569            figure = new CompositeFigure(figure) {
570                // Override this because we want to show the type.
571                // It doesn't work to set it once because the type
572                // has not been resolved, and anyway, it may
573                // change. NOTE: This is copied from above.
574                @Override
575                public String getToolTipText() {
576                    return _portTooltip(port);
577                }
578            };
579
580            if (name != null && !name.equals("")
581                    && !(port instanceof ParameterPort)) {
582                // Do not create a label if there is a custom icon.
583                List<EditorIcon> icons = port.attributeList(EditorIcon.class);
584                if (icons.size() == 0) {
585                    LabelFigure label = new LabelFigure(name, _labelFont, 1.0,
586                            SwingConstants.SOUTH_WEST);
587
588                    // Shift the label slightly right so it doesn't
589                    // collide with ports.
590                    label.translateTo(backBounds.getX(), backBounds.getY());
591                    ((CompositeFigure) figure).add(label);
592                }
593            }
594
595            if (port instanceof IOPort) {
596                // Create a diagonal connector for multiports, if necessary.
597                IOPort ioPort = (IOPort) port;
598
599                if (ioPort.isMultiport()) {
600                    int numberOfLinks = ioPort.insideRelationList().size();
601
602                    if (numberOfLinks > 1) {
603                        // The diagonal is necessary.
604                        // Line depends on the orientation.
605                        double startX, startY, endX, endY;
606                        Rectangle2D bounds = figure.getShape().getBounds2D();
607                        double x = bounds.getX();
608                        double y = bounds.getY();
609                        double width = bounds.getWidth();
610                        double height = bounds.getHeight();
611                        int extent = numberOfLinks - 1;
612
613                        if (direction == SwingConstants.EAST) {
614                            startX = x + width;
615                            startY = y + height / 2;
616                            endX = startX + extent
617                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
618                            endY = startY + extent
619                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
620                        } else if (direction == SwingConstants.WEST) {
621                            startX = x;
622                            startY = y + height / 2;
623                            endX = startX - extent
624                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
625                            endY = startY - extent
626                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
627                        } else if (direction == SwingConstants.NORTH) {
628                            startX = x + width / 2;
629                            startY = y;
630                            endX = startX - extent
631                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
632                            endY = startY - extent
633                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
634                        } else {
635                            // Coverity: Logically dead code:
636                            // direction can only be EAST, WEST or
637                            // NORTH.
638
639                            // However, if we don't have this else
640                            // clause, then the compiler indicates
641                            // that startX, startY, endX and endY are
642                            // not initialized.  One fix would be to
643                            // set them when we declare them, but then
644                            // set them again in the clauses above.
645
646                            startX = x + width / 2;
647                            startY = y + height;
648                            endX = startX + extent
649                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
650                            endY = startY + extent
651                                    * IOPortController.MULTIPORT_CONNECTION_SPACING;
652                        }
653
654                        Line2D line = new Line2D.Double(startX, startY, endX,
655                                endY);
656                        Figure lineFigure = new BasicFigure(line, fill,
657                                (float) 2.0);
658                        ((CompositeFigure) figure).add(lineFigure);
659                    }
660                }
661
662                _createPubSubLabels(ioPort, (CompositeFigure) figure);
663
664                figure = new PortTerminal(ioPort, figure, normal, true);
665            } else {
666                Site tsite = new PerimeterSite(figure, 0);
667                tsite.setNormal(normal);
668                tsite = new FixedNormalSite(tsite);
669                figure = new TerminalFigure(figure, tsite) {
670                    // Override this because the tooltip may
671                    // change over time.  I.e., the port may
672                    // change from being an input or output, etc.
673                    @Override
674                    public String getToolTipText() {
675                        return _portTooltip(port);
676                    }
677                };
678            }
679
680            // Have to do this as well or awt will not render a tooltip.
681            figure.setToolTipText(port.getName());
682            AttributeController.renderHighlight(port, figure);
683
684            return figure;
685        }
686
687    }
688
689    ///////////////////////////////////////////////////////////////////
690    ////                         private methods                   ////
691
692    private Polygon2D.Double _createPolygon(Integer[] coordinates,
693            Polygon2D.Double polygon) {
694        for (int i = 0; i < coordinates.length; i = i + 2) {
695            polygon.lineTo(coordinates[i], coordinates[i + 1]);
696        }
697        return polygon;
698    }
699
700    /** Create a label showing the channel and initial values
701     *  for PubSubPort. If the port argument is not an instance
702     *  of PubSubPort, then do nothing.
703     *  @param port The port.
704     *  @param figure The composite figure to add the label to.
705     */
706    private void _createPubSubLabels(IOPort port, CompositeFigure figure) {
707        if (!(port instanceof PubSubPort)) {
708            return;
709        }
710        try {
711            String channel = null;
712            if (((InstantiableNamedObj) port.getContainer())
713                    .isWithinClassDefinition()) {
714                // If the port is in a class definition, do not expand it, it might contain $foo.$bar.
715                channel = "Channel: "
716                        + ((PubSubPort) port).channel.getExpression();
717            } else {
718                channel = "Channel: "
719                        + ((PubSubPort) port).channel.stringValue();
720            }
721            // The anchor argument below is (sadly) ignored.
722            Figure label = new LabelFigure(channel, _labelFont, 0.0,
723                    SwingConstants.SOUTH_EAST, _pubSubLabelColor);
724            double labelHeight = label.getBounds().getHeight();
725            Rectangle2D bounds = figure.getShape().getBounds2D();
726            label.translate(-8.0, bounds.getMaxY() + labelHeight + 4);
727            figure.add(label);
728
729            String initialTokens = ((PubSubPort) port).initialTokens
730                    .getExpression();
731            if (!initialTokens.trim().equals("")) {
732                initialTokens = "Initial tokens: " + initialTokens;
733                label = new LabelFigure(initialTokens, _labelFont, 0.0,
734                        SwingConstants.SOUTH_EAST, _pubSubLabelColor);
735                label.translate(-8.0, bounds.getMaxY() + 2 * labelHeight + 8);
736                figure.add(label);
737            }
738        } catch (Exception e) {
739            // Ignore and display question marks.
740            e.printStackTrace();
741        }
742    }
743
744    ///////////////////////////////////////////////////////////////////
745    ////                         private members                   ////
746
747    /** Color for publish and subscribe labels. */
748    private static Color _pubSubLabelColor = new Color(0.0f, 0.4f, 0.4f, 1.0f);
749
750    // The following maps are to keep track of the ports that already needed
751    // to be located (since they had location 0,0).
752    private HashMap<Object, double[]> _inputPortLocations = new HashMap<Object, double[]>();
753    private HashMap<Object, double[]> _outputPortLocations = new HashMap<Object, double[]>();
754    private HashMap<Object, double[]> _inoutputPortLocations = new HashMap<Object, double[]>();
755    private HashMap<Object, double[]> _otherPortLocations = new HashMap<Object, double[]>();
756}