001/* A graph model for ptolemy fsm models.
002
003 Copyright (c) 1999-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.modal;
029
030import java.util.HashSet;
031import java.util.Iterator;
032import java.util.LinkedList;
033import java.util.List;
034import java.util.Set;
035
036import diva.graph.GraphEvent;
037import diva.graph.GraphUtilities;
038import diva.graph.modular.EdgeModel;
039import diva.graph.modular.MutableEdgeModel;
040import diva.graph.modular.NodeModel;
041import diva.util.NullIterator;
042import ptolemy.actor.TypedActor;
043import ptolemy.domains.modal.kernel.State;
044import ptolemy.domains.modal.kernel.Transition;
045import ptolemy.kernel.ComponentEntity;
046import ptolemy.kernel.ComponentPort;
047import ptolemy.kernel.ComponentRelation;
048import ptolemy.kernel.CompositeEntity;
049import ptolemy.kernel.Port;
050import ptolemy.kernel.Relation;
051import ptolemy.kernel.util.ChangeListener;
052import ptolemy.kernel.util.ChangeRequest;
053import ptolemy.kernel.util.Flowable;
054import ptolemy.kernel.util.IllegalActionException;
055import ptolemy.kernel.util.InternalErrorException;
056import ptolemy.kernel.util.Locatable;
057import ptolemy.kernel.util.NamedObj;
058import ptolemy.moml.MoMLChangeRequest;
059import ptolemy.vergil.basic.AbstractBasicGraphModel;
060import ptolemy.vergil.basic.NamedObjNodeModel;
061import ptolemy.vergil.kernel.Link;
062
063///////////////////////////////////////////////////////////////////
064//// FSMGraphModel
065
066/**
067 A graph model for graphically manipulating ptolemy FSM models.
068
069 @author Steve Neuendorffer
070 @version $Id$
071 @since Ptolemy II 8.0
072 @Pt.ProposedRating Yellow (neuendor)
073 @Pt.AcceptedRating Red (johnr)
074 */
075public class FSMGraphModel extends AbstractBasicGraphModel {
076    /** Construct a new graph model whose root is the given composite entity.
077     *  @param composite The top-level composite entity for the model.
078     */
079    public FSMGraphModel(CompositeEntity composite) {
080        super(composite);
081        _linkSet = new HashSet();
082        _update();
083    }
084
085    ///////////////////////////////////////////////////////////////////
086    ////                         public methods                    ////
087
088    /** Disconnect an edge from its two endpoints and notify graph
089     *  listeners with an EDGE_HEAD_CHANGED and an EDGE_TAIL_CHANGED
090     *  event whose source is the given source.
091     *  @param eventSource The source of the event that will be dispatched,
092     *   e.g. the view that made this call.
093     *  @param edge The edge.
094     */
095    @Override
096    public void disconnectEdge(Object eventSource, Object edge) {
097        if (!(getEdgeModel(edge) instanceof ArcModel)) {
098            return;
099        }
100
101        ArcModel model = (ArcModel) getEdgeModel(edge);
102        Object head = model.getHead(edge);
103        Object tail = model.getTail(edge);
104        model.removeEdge(edge);
105
106        if (head != null) {
107            GraphEvent e = new GraphEvent(eventSource,
108                    GraphEvent.EDGE_HEAD_CHANGED, edge, head);
109            dispatchGraphEvent(e);
110        }
111
112        if (tail != null) {
113            GraphEvent e = new GraphEvent(eventSource,
114                    GraphEvent.EDGE_TAIL_CHANGED, edge, tail);
115            dispatchGraphEvent(e);
116        }
117    }
118
119    /** Return a MoML String that will delete the given edge from the
120     *  Ptolemy model.
121     *  @param edge The edge.
122     *  @return A valid MoML string.
123     */
124    @Override
125    public String getDeleteEdgeMoML(Object edge) {
126        // Note: the abstraction here is rather broken.  Ideally this
127        // should look like getDeleteNodeMoML()
128        if (!(getEdgeModel(edge) instanceof ArcModel)) {
129            return "";
130        }
131
132        ArcModel model = (ArcModel) getEdgeModel(edge);
133        return model.getDeleteEdgeMoML(edge);
134    }
135
136    /** Return a MoML String that will delete the given node from the
137     *  Ptolemy model.
138     *  @param node The node.
139     *  @return A valid MoML string.
140     */
141    @Override
142    public String getDeleteNodeMoML(Object node) {
143        if (!(getNodeModel(node) instanceof NamedObjNodeModel)) {
144            return "";
145        }
146
147        NamedObjNodeModel model = (NamedObjNodeModel) getNodeModel(node);
148        return model.getDeleteNodeMoML(node);
149    }
150
151    /** Return the model for the given edge object.  If the object is not
152     *  an edge, then return null.
153     *  @param edge An object which is assumed to be in this graph model.
154     *  @return An instance of ArcModel if the object is an Arc.
155     *   Otherwise return null.
156     */
157    @Override
158    public EdgeModel getEdgeModel(Object edge) {
159        if (edge instanceof Link) {
160            return _arcModel;
161        } else {
162            return null;
163        }
164    }
165
166    /** Return the node model for the given object.  If the object is not
167     *  a node, then return null.
168     *  @param node An object which is assumed to be in this graph model.
169     *  @return The node model for the specified node, or null if there
170     *   is none.
171     */
172    @Override
173    public NodeModel getNodeModel(Object node) {
174        if (node instanceof Locatable) {
175            Object container = ((Locatable) node).getContainer();
176
177            if (container instanceof ComponentEntity) {
178                return _stateModel;
179            } else if (container instanceof ComponentPort) {
180                return _portModel;
181            }
182        }
183
184        return super.getNodeModel(node);
185    }
186
187    /** Return the semantic object corresponding to the given node, edge,
188     *  or composite.  A "semantic object" is an object associated with
189     *  a node in the graph.  In this case, if the node is icon, the
190     *  semantic object is the entity containing the icon.  If it is
191     *  an arc, then the semantic object is the arc's relation.
192     *  @param element A graph element.
193     *  @return The semantic object associated with this element, or null
194     *  if the object is not recognized.
195     */
196    @Override
197    public Object getSemanticObject(Object element) {
198        if (element instanceof Link) {
199            return ((Link) element).getRelation();
200        }
201
202        return super.getSemanticObject(element);
203    }
204
205    /** Delete a node from its parent graph and notify
206     *  graph listeners with a NODE_REMOVED event.
207     *  @param eventSource The source of the event that will be dispatched,
208     *   e.g. the view that made this call.
209     *  @param node The node to be removed.
210     */
211    @Override
212    public void removeNode(Object eventSource, Object node) {
213        if (!(getNodeModel(node) instanceof NamedObjNodeModel)) {
214            return;
215        }
216
217        NamedObjNodeModel model = (NamedObjNodeModel) getNodeModel(node);
218        model.removeNode(eventSource, node);
219    }
220
221    // FIXME: The following methods are probably inappropriate.
222    // They make it impossible to have customized models for
223    // particular links or icons. getLinkModel() and
224    // getNodeModel() should be sufficient.
225    // Big changes needed, however to make this work.
226    // The huge inner classes below should be factored out as
227    // separate classes.  EAL
228
229    /** Get the port model.
230     *  @return The port model.
231     */
232    public PortModel getPortModel() {
233        return _portModel;
234    }
235
236    /** Get the state model.
237     *  @return The state model.
238     */
239    public StateModel getStateModel() {
240        return _stateModel;
241    }
242
243    /** Get the arc model.
244     *  @return The acr model.
245     */
246    public ArcModel getArcModel() {
247        return _arcModel;
248    }
249
250    // End of FIXME.
251    ///////////////////////////////////////////////////////////////////
252    ////                         protected methods                 ////
253
254    /** Update the graph model.  This is called whenever a change request is
255     *  executed.  In this class the internal set of link objects is
256     *  verified to be correct.
257     */
258    @Override
259    protected boolean _update() {
260        // Go through all the links that currently exist, and remove
261        // any that don't have both ends in the model.
262        Iterator links = _linkSet.iterator();
263
264        while (links.hasNext()) {
265            Link link = (Link) links.next();
266            Relation relation = link.getRelation();
267
268            if (relation == null) {
269                continue;
270            }
271
272            // Check that the relation hasn't been removed.
273            // If we do not do this, then redo will thrown an exception
274            // if we open ct/demo/BouncingBall/BouncingBall.xml, look
275            // inside the Ball Model composite actor,
276            // delete the stop actor and then do undo and then redo.
277            if (relation.getContainer() == null) {
278                link.setHead(null);
279                link.setTail(null);
280                links.remove();
281                continue;
282            }
283
284            boolean headOK = GraphUtilities.isContainedNode(link.getHead(),
285                    getRoot(), this);
286            boolean tailOK = GraphUtilities.isContainedNode(link.getTail(),
287                    getRoot(), this);
288
289            // If the head or tail has been removed, then remove this link.
290            if (!(headOK && tailOK)) {
291                //Object headObj = getSemanticObject(link.getHead());
292                //Object tailObj = getSemanticObject(link.getTail());
293                link.setHead(null);
294                link.setTail(null);
295                link.setRelation(null);
296                links.remove();
297
298                NamedObj container = getPtolemyModel();
299
300                // remove the relation  This should trigger removing the
301                // other link. This will only happen when we've deleted
302                // the state at one end of the model.
303                // Note that the source is NOT the graph model, so this
304                // will trigger the ChangeRequest listener to
305                // redraw the graph again.
306                MoMLChangeRequest request = new MoMLChangeRequest(container,
307                        container, "<deleteRelation name=\""
308                                + relation.getName(container) + "\"/>\n");
309
310                // Need to merge the undo for this request in with one that
311                // triggered it
312                request.setMergeWithPreviousUndo(true);
313                request.setUndoable(true);
314                container.requestChange(request);
315                return false;
316            }
317        }
318
319        // Now create Links for links that may be new
320        Iterator relations = ((CompositeEntity) getPtolemyModel())
321                .relationList().iterator();
322
323        while (relations.hasNext()) {
324            _updateLinks((ComponentRelation) relations.next());
325        }
326
327        return super._update();
328    }
329
330    ///////////////////////////////////////////////////////////////////
331    ////                         private methods                   ////
332    // Check to make sure that there is an Link object representing
333    // the given relation.
334    private void _updateLinks(ComponentRelation relation) {
335        Iterator links = _linkSet.iterator();
336        Link foundLink = null;
337
338        while (links.hasNext()) {
339            Link link = (Link) links.next();
340
341            // only consider links that are associated with this relation.
342            if (link.getRelation() == relation) {
343                foundLink = link;
344                break;
345            }
346        }
347
348        // A link exists, so there is nothing to do.
349        if (foundLink != null) {
350            return;
351        }
352
353        List linkedPortList = relation.linkedPortList();
354
355        if (linkedPortList.size() != 2) {
356            // Do nothing...  somebody else should take care of removing this,
357            // because we have no way of representing it in this editor.
358            return;
359        }
360
361        Port port1 = (Port) linkedPortList.get(0);
362        Locatable location1 = _getLocation(port1.getContainer());
363        Port port2 = (Port) linkedPortList.get(1);
364        Locatable location2 = _getLocation(port2.getContainer());
365
366        Link link;
367
368        try {
369            link = new Link();
370        } catch (Exception e) {
371            throw new InternalErrorException(
372                    "Failed to create " + "new link, even though one does not "
373                            + "already exist:" + e.getMessage());
374        }
375
376        link.setRelation(relation);
377
378        // We have to get the direction of the arc correct.
379        if (((Flowable) port1.getContainer()).getIncomingPort().equals(port1)) {
380            link.setHead(location1);
381            link.setTail(location2);
382        } else {
383            link.setHead(location2);
384            link.setTail(location1);
385        }
386
387        _linkSet.add(link);
388    }
389
390    ///////////////////////////////////////////////////////////////////
391    ////                         private variables                 ////
392    // The set of all links in the model.
393    private Set _linkSet;
394
395    // The models of the different types of nodes and edges.
396    private ArcModel _arcModel = new ArcModel();
397
398    private PortModel _portModel = new PortModel();
399
400    private StateModel _stateModel = new StateModel();
401
402    ///////////////////////////////////////////////////////////////////
403    ////                         inner classes                     ////
404
405    /** The model for arcs between states.
406     */
407    public class ArcModel implements MutableEdgeModel {
408        /** Return true if the head of the given edge can be attached to the
409         *  given node.
410         *  @param edge The edge to attach, which is assumed to be an arc.
411         *  @param node The node to attach to.
412         *  @return True if the node is an icon.
413         */
414        @Override
415        public boolean acceptHead(Object edge, Object node) {
416            if (node instanceof Locatable) {
417                return true;
418            } else {
419                return false;
420            }
421        }
422
423        /** Return true if the tail of the given edge can be attached to the
424         *  given node.
425         *  @param edge The edge to attach, which is assumed to be an arc.
426         *  @param node The node to attach to.
427         *  @return True if the node is an icon.
428         */
429        @Override
430        public boolean acceptTail(Object edge, Object node) {
431            if (node instanceof Locatable) {
432                return true;
433            } else {
434                return false;
435            }
436        }
437
438        /** Return the head node of the given edge.
439         *  @param edge The edge, which is assumed to be an instance of Arc.
440         *  @return The node that is the head of the specified edge.
441         *  @see #getTail(Object)
442         *  @see #setHead(Object, Object)
443         */
444        @Override
445        public Object getHead(Object edge) {
446            return ((Link) edge).getHead();
447        }
448
449        /** Return a MoML String that will delete the given edge from the
450         *  Ptolemy model.
451         *  @param edge The edge to be removed.
452         *  @return A valid MoML string.
453         */
454        public String getDeleteEdgeMoML(Object edge) {
455            final Link link = (Link) edge;
456            //NamedObj linkHead = (NamedObj) link.getHead();
457            //NamedObj linkTail = (NamedObj) link.getTail();
458            Relation linkRelation = link.getRelation();
459
460            // This moml is parsed to execute the change
461            StringBuffer moml = new StringBuffer();
462
463            // Make the request in the context of the container.
464            // JDK1.2.2 fails to compile the next line.
465            NamedObj container = getPtolemyModel();
466
467            moml.append(_deleteRelation(container, linkRelation));
468
469            // See whether refinement(s) need to be removed.
470            CompositeEntity master = (CompositeEntity) linkRelation
471                    .getContainer();
472
473            // Nothing to do if there is no container.
474            if (master != null) {
475                // Remove any referenced refinements that are not also
476                // referenced by other states.
477                TypedActor[] refinements = null;
478
479                try {
480                    if (linkRelation instanceof Transition) {
481                        refinements = ((Transition) linkRelation)
482                                .getRefinement();
483                    }
484                } catch (IllegalActionException e) {
485                    // Ignore, no refinement to remove.
486                }
487
488                // The following code executes only if the link is a Transition,
489                // so we can sure the entities are instances of State.
490                if (refinements != null) {
491                    for (TypedActor refinement : refinements) {
492                        // By default, if no other state or transition refers
493                        // to this refinement, then we will remove it.
494                        boolean removeIt = true;
495                        Iterator states = master.entityList(State.class)
496                                .iterator();
497
498                        while (removeIt && states.hasNext()) {
499                            State state = (State) states.next();
500                            TypedActor[] stateRefinements = null;
501
502                            try {
503                                stateRefinements = state.getRefinement();
504                            } catch (IllegalActionException e1) {
505                                // Ignore, no refinement to check.
506                            }
507
508                            if (stateRefinements == null) {
509                                continue;
510                            }
511
512                            for (TypedActor stateRefinement : stateRefinements) {
513                                if (stateRefinement == refinement) {
514                                    removeIt = false;
515                                    break;
516                                }
517                            }
518                        }
519
520                        // Next check transitions.
521                        Iterator transitions = master.relationList().iterator();
522
523                        while (removeIt && transitions.hasNext()) {
524                            Relation transition = (Relation) transitions.next();
525
526                            if (transition == linkRelation
527                                    || !(transition instanceof Transition)) {
528                                continue;
529                            }
530
531                            TypedActor[] transitionRefinements = null;
532
533                            try {
534                                transitionRefinements = ((Transition) transition)
535                                        .getRefinement();
536                            } catch (IllegalActionException e1) {
537                                // Ignore, no refinement to check.
538                            }
539
540                            if (transitionRefinements == null) {
541                                continue;
542                            }
543
544                            for (TypedActor transitionRefinement : transitionRefinements) {
545                                if (transitionRefinement == refinement) {
546                                    removeIt = false;
547                                    break;
548                                }
549                            }
550                        }
551
552                        if (removeIt) {
553                            moml.append("<deleteEntity name=\""
554                                    + ((NamedObj) refinement).getName(container)
555                                    + "\"/>\n");
556                        }
557                    }
558                }
559            }
560
561            return moml.toString();
562        }
563
564        /** Return the tail node of the specified edge.
565         *  @param edge The edge, which is assumed to be an instance of Link.
566         *  @return The node that is the tail of the specified edge.
567         *  @see #getHead(Object)
568         *  @see #setTail(Object, Object)
569         */
570        @Override
571        public Object getTail(Object edge) {
572            return ((Link) edge).getTail();
573        }
574
575        /** Return true if this edge is directed.
576         *  All transitions are directed, so this always returns true.
577         *  @param edge The edge, which is assumed to be an arc.
578         *  @return True.
579         */
580        @Override
581        public boolean isDirected(Object edge) {
582            return true;
583        }
584
585        /** Remove the given edge and delete its associated relation.
586         *  This class queues a new change request with the ptolemy model
587         *  to make this modification.
588         *  @param edge The edge, which is assumed to be an arc.
589         */
590        public void removeEdge(final Object edge) {
591            final Link link = (Link) edge;
592            Relation linkRelation = link.getRelation();
593
594            // This moml is parsed to execute the change
595            final StringBuffer moml = new StringBuffer();
596
597            // Make the request in the context of the container.
598            final NamedObj container = getPtolemyModel();
599            moml.append(_deleteRelation(container, linkRelation));
600
601            MoMLChangeRequest request = new MoMLChangeRequest(
602                    FSMGraphModel.this, container, moml.toString()) {
603                @Override
604                protected void _execute() throws Exception {
605                    super._execute();
606                    link.setHead(null);
607                    link.setTail(null);
608                    link.setRelation(null);
609                }
610            };
611
612            // Handle what happens if the mutation fails.
613            request.addChangeListener(new ChangeListener() {
614                @Override
615                public void changeFailed(ChangeRequest change,
616                        Exception exception) {
617                    // Ignore... nothing we can do about it anyway.
618                }
619
620                @Override
621                public void changeExecuted(ChangeRequest change) {
622                    _linkSet.remove(edge);
623                }
624            });
625            request.setUndoable(true);
626            container.requestChange(request);
627        }
628
629        /** Connect the given edge to the given head node. If the specified
630         *  head is null, then any pre-existing relation associated with
631         *  this edge will be deleted.
632         *  This class queues a new change request with the ptolemy model
633         *  to make this modification.
634         *  @param edge The edge, which is assumed to be an arc.
635         *  @param newLinkHead The new head for the edge, which is assumed to
636         *   be an icon.
637         *  @see #setTail(Object, Object)
638         *  @see #getHead(Object)
639         */
640        @Override
641        public void setHead(final Object edge, final Object newLinkHead) {
642            final Link link = (Link) edge;
643            NamedObj linkHead = (NamedObj) link.getHead();
644            NamedObj linkTail = (NamedObj) link.getTail();
645            Relation linkRelation = link.getRelation();
646
647            // This moml is parsed to execute the change
648            final StringBuffer moml = new StringBuffer();
649
650            // This moml is parsed in case the change fails.
651            final StringBuffer failmoml = new StringBuffer();
652            moml.append("<group>\n");
653            failmoml.append("<group>\n");
654
655            // Make the request in the context of the container.
656            final NamedObj container = getPtolemyModel();
657
658            // If there is a previous connection, remove it.
659            if (linkRelation != null) {
660                if (newLinkHead == null) {
661                    // There will be no further connection, so just
662                    // delete the relation.
663                    moml.append(_deleteRelation(container, linkRelation));
664                } else {
665                    // There will be a further connection, so preserve
666                    // the relation.
667                    moml.append(_unlinkHead(container, linkHead, linkRelation));
668                }
669            }
670
671            String relationName = null;
672
673            if (newLinkHead != null) {
674                // create moml to make the new link.
675                relationName = _linkHead(container, moml, failmoml,
676                        (NamedObj) newLinkHead, linkTail, linkRelation);
677            }
678
679            moml.append("</group>\n");
680            failmoml.append("</group>\n");
681
682            final String relationNameToAdd = relationName;
683            MoMLChangeRequest request = new MoMLChangeRequest(
684                    FSMGraphModel.this, container, moml.toString()) {
685                @Override
686                protected void _execute() throws Exception {
687                    super._execute();
688                    link.setHead(newLinkHead);
689
690                    if (relationNameToAdd != null) {
691                        ComponentRelation relation = ((CompositeEntity) getPtolemyModel())
692                                .getRelation(relationNameToAdd);
693                        link.setRelation(relation);
694                    }
695                }
696            };
697
698            // Handle what happens if the mutation fails.
699            request.addChangeListener(new ChangeListener() {
700                @Override
701                public void changeFailed(ChangeRequest change,
702                        Exception exception) {
703                    // If we fail here, then we remove the link entirely.
704                    _linkSet.remove(link);
705                    link.setHead(null);
706                    link.setTail(null);
707                    link.setRelation(null);
708
709                    // and queue a new change request to clean up the model
710                    // Note: JDK1.2.2 requires that this variable not be
711                    // called request or we get a compile error.
712                    MoMLChangeRequest requestChange = new MoMLChangeRequest(
713                            FSMGraphModel.this, container, failmoml.toString());
714
715                    // Fail moml execution not undoable
716                    container.requestChange(requestChange);
717                }
718
719                @Override
720                public void changeExecuted(ChangeRequest change) {
721                    if (GraphUtilities.isPartiallyContainedEdge(edge, getRoot(),
722                            FSMGraphModel.this)) {
723                        _linkSet.add(edge);
724                    } else {
725                        _linkSet.remove(edge);
726                    }
727                }
728            });
729            request.setUndoable(true);
730            container.requestChange(request);
731        }
732
733        /** Connect the given edge to the given tail node. If the specified
734         *  tail is null, then any pre-existing relation associated with
735         *  this edge will be deleted.
736         *  This class queues a new change request with the ptolemy model
737         *  to make this modification.
738         *  @param edge The edge, which is assumed to be an arc.
739         *  @param newLinkTail The new tail for the edge, which is assumed to
740         *  be an icon.
741         *  @see #setHead(Object, Object)
742         *  @see #getTail(Object)
743         */
744        @Override
745        public void setTail(final Object edge, final Object newLinkTail) {
746            final Link link = (Link) edge;
747            NamedObj linkHead = (NamedObj) link.getHead();
748            NamedObj linkTail = (NamedObj) link.getTail();
749            Relation linkRelation = link.getRelation();
750
751            // This moml is parsed to execute the change
752            final StringBuffer moml = new StringBuffer();
753
754            // This moml is parsed in case the change fails.
755            final StringBuffer failmoml = new StringBuffer();
756            moml.append("<group>\n");
757            failmoml.append("<group>\n");
758
759            // Make the request in the context of the container.
760            final NamedObj container = getPtolemyModel();
761
762            // If there is a previous connection, remove it.
763            if (linkRelation != null) {
764                if (newLinkTail == null) {
765                    // There will be no further connection, so just
766                    // delete the relation.
767                    moml.append(_deleteRelation(container, linkRelation));
768                } else {
769                    // There will be a further connection, so preserve
770                    // the relation.
771                    moml.append(_unlinkTail(container, linkTail, linkRelation));
772                }
773            }
774
775            String relationName = null;
776
777            if (newLinkTail != null) {
778                // create moml to make the new links.
779                relationName = _linkTail(container, moml, failmoml, linkHead,
780                        (NamedObj) newLinkTail, linkRelation);
781            }
782
783            moml.append("</group>\n");
784            failmoml.append("</group>\n");
785
786            final String relationNameToAdd = relationName;
787
788            MoMLChangeRequest request = new MoMLChangeRequest(
789                    FSMGraphModel.this, container, moml.toString()) {
790                @Override
791                protected void _execute() throws Exception {
792                    super._execute();
793                    link.setTail(newLinkTail);
794
795                    if (relationNameToAdd != null) {
796                        link.setRelation(((CompositeEntity) getPtolemyModel())
797                                .getRelation(relationNameToAdd));
798                    }
799                }
800            };
801
802            // Handle what happens if the mutation fails.
803            request.addChangeListener(new ChangeListener() {
804                @Override
805                public void changeFailed(ChangeRequest change,
806                        Exception exception) {
807                    // If we fail here, then we remove the link entirely.
808                    _linkSet.remove(link);
809                    link.setHead(null);
810                    link.setTail(null);
811                    link.setRelation(null);
812
813                    // and queue a new change request to clean up the model
814                    // Note: JDK1.2.2 requires that this variable not be
815                    // called request or we get a compile error.
816                    MoMLChangeRequest requestChange = new MoMLChangeRequest(
817                            FSMGraphModel.this, container, failmoml.toString());
818
819                    // fail moml execution not undaoble
820                    container.requestChange(requestChange);
821                }
822
823                @Override
824                public void changeExecuted(ChangeRequest change) {
825                    if (GraphUtilities.isPartiallyContainedEdge(edge, getRoot(),
826                            FSMGraphModel.this)) {
827                        _linkSet.add(edge);
828                    } else {
829                        _linkSet.remove(edge);
830                    }
831                }
832            });
833            request.setUndoable(true);
834            container.requestChange(request);
835        }
836
837        ///////////////////////////////////////////////////////////////////
838        ////                         private methods                   ////
839
840        /** Return moml to remove a relation in the specified container.
841         */
842        private String _deleteRelation(NamedObj container, Relation relation) {
843            return "<deleteRelation name=\"" + relation.getName(container)
844                    + "\"/>\n";
845        }
846
847        /** Append moml to the given buffer that connects a link with the
848         *  given head.  If the relation argument is non-null, then that
849         *  relation is used, and null is returned.  Otherwise, a new
850         *  relation is created, and its name is returned. Names in the
851         *  generated moml will be relative to the specified container.
852         *  The failmoml argument is also populated, but with MoML code
853         *  to execute if the link fails.  This disconnects any partially
854         *  constructed link.
855         */
856        private String _linkHead(NamedObj container, StringBuffer moml,
857                StringBuffer failmoml, NamedObj linkHead, NamedObj linkTail,
858                Relation linkRelation) {
859            if (linkHead != null && linkTail != null) {
860                NamedObj head = (NamedObj) getSemanticObject(linkHead);
861                NamedObj tail = (NamedObj) getSemanticObject(linkTail);
862
863                if (head instanceof Flowable && tail instanceof Flowable) {
864                    // When we connect two states, we actually connect the
865                    // appropriate ports.
866                    Flowable headState = (Flowable) head;
867                    Flowable tailState = (Flowable) tail;
868                    ComponentPort headPort = headState.getIncomingPort();
869                    ComponentPort tailPort = tailState.getOutgoingPort();
870                    NamedObj ptolemyModel = getPtolemyModel();
871
872                    // If the context is not the entity that we're editing,
873                    // then we need to set the context correctly.
874                    if (ptolemyModel != container) {
875                        String contextString = "<entity name=\""
876                                + ptolemyModel.getName(container) + "\">\n";
877                        moml.append(contextString);
878                        failmoml.append(contextString);
879                    }
880
881                    boolean createdNewRelation = false;
882                    String relationName = null;
883
884                    if (linkRelation != null) {
885                        // Pre-existing relation. Use it.
886                        relationName = linkRelation.getName(ptolemyModel);
887                    } else {
888                        createdNewRelation = true;
889
890                        // Linking two ports with a new relation.
891                        relationName = ptolemyModel.uniqueName("relation");
892
893                        // Set the exitAngle based on other relations.
894                        // Count the number of connections between the
895                        // headPort and the tailPort.
896                        Iterator ports = tailPort.deepConnectedPortList()
897                                .iterator();
898                        int count = 0;
899
900                        while (ports.hasNext()) {
901                            if (ports.next() == headPort) {
902                                count++;
903                            }
904                        }
905
906                        // Increase the angle as the count increases.
907                        // Any function of "count" will work here that starts
908                        // at PI/5 and approaches something less than PI
909                        // as count gets large.  Unfortunately, self-loops
910                        // again have to be handled specially.
911                        double angle;
912
913                        if (headPort.getContainer() != tailPort
914                                .getContainer()) {
915                            angle = Math.PI / 5.0
916                                    + 1.5 * Math.atan(0.3 * count);
917                        } else {
918                            angle = Math.PI / 3.0
919                                    - 0.75 * Math.atan(0.3 * count);
920                        }
921
922                        // Create the new relation.
923                        // Note that we specify no class so that we use the
924                        // container's factory method when this gets parsed
925                        moml.append("<relation name=\"" + relationName
926                                + "\"><property name=\"exitAngle\" value=\""
927                                + angle + "\"/></relation>\n");
928                        moml.append("<link port=\""
929                                + tailPort.getName(ptolemyModel)
930                                + "\" relation=\"" + relationName + "\"/>\n");
931                    }
932
933                    moml.append("<link port=\"" + headPort.getName(ptolemyModel)
934                            + "\" relation=\"" + relationName + "\"/>\n");
935
936                    // Record moml so that we can blow away these
937                    // links in case we can't create them
938                    failmoml.append("<unlink port=\""
939                            + headPort.getName(ptolemyModel) + "\" relation=\""
940                            + relationName + "\"/>\n");
941
942                    if (linkRelation == null) {
943                        failmoml.append("<unlink port=\""
944                                + tailPort.getName(ptolemyModel)
945                                + "\" relation=\"" + relationName + "\"/>\n");
946                        failmoml.append("<deleteRelation name=\"" + relationName
947                                + "\"/>\n");
948                    }
949
950                    // close the context
951                    if (ptolemyModel != container) {
952                        moml.append("</entity>");
953                        failmoml.append("</entity>");
954                    }
955
956                    if (createdNewRelation) {
957                        return relationName;
958                    } else {
959                        return null;
960                    }
961                } else {
962                    throw new RuntimeException(
963                            "Attempt to create a link between non-states: "
964                                    + "Head = " + head + ", Tail = " + tail);
965                }
966            } else {
967                // No Linking to do.
968                return null;
969            }
970        }
971
972        /** Append moml to the given buffer that connects a link with the
973         *  given tail.  If the relation argument is non-null, then that
974         *  relation is used, and null is returned.  Otherwise, a new
975         *  relation is created, and its name is returned. Names in the
976         *  generated moml will be relative to the specified container.
977         *  The failmoml argument is also populated, but with MoML code
978         *  to execute if the link fails.  This disconnects any partially
979         *  constructed link.
980         */
981        private String _linkTail(NamedObj container, StringBuffer moml,
982                StringBuffer failmoml, NamedObj linkHead, NamedObj linkTail,
983                Relation linkRelation) {
984            // NOTE: This method is almost identical to the previous
985            // one, but just enough different that it isn't obvious
986            // how to combine them into one method.
987            if (linkHead != null && linkTail != null) {
988                NamedObj head = (NamedObj) getSemanticObject(linkHead);
989                NamedObj tail = (NamedObj) getSemanticObject(linkTail);
990
991                if (head instanceof Flowable && tail instanceof Flowable) {
992                    // When we connect two states, we actually connect the
993                    // appropriate ports.
994                    Flowable headState = (Flowable) head;
995                    Flowable tailState = (Flowable) tail;
996                    ComponentPort headPort = headState.getIncomingPort();
997                    ComponentPort tailPort = tailState.getOutgoingPort();
998                    NamedObj ptolemyModel = getPtolemyModel();
999
1000                    // If the context is not the entity that we're editing,
1001                    // then we need to set the context correctly.
1002                    if (ptolemyModel != container) {
1003                        String contextString = "<entity name=\""
1004                                + ptolemyModel.getName(container) + "\">\n";
1005                        moml.append(contextString);
1006                        failmoml.append(contextString);
1007                    }
1008
1009                    boolean createdNewRelation = false;
1010                    String relationName = null;
1011
1012                    if (linkRelation != null) {
1013                        // Pre-existing relation. Use it.
1014                        relationName = linkRelation.getName(ptolemyModel);
1015                    } else {
1016                        createdNewRelation = true;
1017
1018                        // Linking two ports with a new relation.
1019                        relationName = ptolemyModel.uniqueName("relation");
1020
1021                        // Note that we specify no class so that we use the
1022                        // container's factory method when this gets parsed
1023                        moml.append(
1024                                "<relation name=\"" + relationName + "\"/>\n");
1025                        moml.append("<link port=\""
1026                                + headPort.getName(ptolemyModel)
1027                                + "\" relation=\"" + relationName + "\"/>\n");
1028                    }
1029
1030                    moml.append("<link port=\"" + tailPort.getName(ptolemyModel)
1031                            + "\" relation=\"" + relationName + "\"/>\n");
1032
1033                    // Record moml so that we can blow away these
1034                    // links in case we can't create them
1035                    failmoml.append("<unlink port=\""
1036                            + tailPort.getName(ptolemyModel) + "\" relation=\""
1037                            + relationName + "\"/>\n");
1038
1039                    if (linkRelation == null) {
1040                        failmoml.append("<unlink port=\""
1041                                + headPort.getName(ptolemyModel)
1042                                + "\" relation=\"" + relationName + "\"/>\n");
1043                        failmoml.append("<deleteRelation name=\"" + relationName
1044                                + "\"/>\n");
1045                    }
1046
1047                    // close the context
1048                    if (ptolemyModel != container) {
1049                        moml.append("</entity>");
1050                        failmoml.append("</entity>");
1051                    }
1052
1053                    if (createdNewRelation) {
1054                        return relationName;
1055                    } else {
1056                        return null;
1057                    }
1058                } else {
1059                    throw new RuntimeException(
1060                            "Attempt to create a link between non-states: "
1061                                    + "Head = " + head + ", Tail = " + tail);
1062                }
1063            } else {
1064                // No Linking to do.
1065                return null;
1066            }
1067        }
1068
1069        /** Return moml to unlink a relation with the given head in the
1070         *  specified container.
1071         */
1072        private String _unlinkHead(NamedObj container, NamedObj linkHead,
1073                Relation relation) {
1074            NamedObj head = (NamedObj) getSemanticObject(linkHead);
1075            Flowable headState = (Flowable) head;
1076            ComponentPort headPort = headState.getIncomingPort();
1077            return "<unlink port=\"" + headPort.getName(container)
1078                    + "\" relation=\"" + relation.getName(container) + "\"/>\n";
1079        }
1080
1081        /** Return moml to unlink a relation with the given tail in the
1082         *  specified container.
1083         */
1084        private String _unlinkTail(NamedObj container, NamedObj linkTail,
1085                Relation relation) {
1086            NamedObj tail = (NamedObj) getSemanticObject(linkTail);
1087            Flowable tailState = (Flowable) tail;
1088            ComponentPort tailPort = tailState.getOutgoingPort();
1089            return "<unlink port=\"" + tailPort.getName(container)
1090                    + "\" relation=\"" + relation.getName(container) + "\"/>\n";
1091        }
1092    }
1093
1094    /** The model for external ports.
1095     */
1096    public class PortModel extends NamedObjNodeModel {
1097        /** Return a MoML String that will delete the given node from the
1098         *  Ptolemy model. This assumes that the context is the container
1099         *  of the port to be deleted.
1100         *  @param node The node to be deleted.
1101         *  @return A valid MoML string.
1102         */
1103        @Override
1104        public String getDeleteNodeMoML(Object node) {
1105            NamedObj deleteObj = ((Locatable) node).getContainer();
1106            String moml = "<deletePort name=\"" + deleteObj.getName()
1107                    + "\"/>\n";
1108            return moml;
1109        }
1110
1111        /** Return the graph parent of the given node.
1112         *  @param node The node, which is assumed to be an icon contained in
1113         *   this graph model.
1114         *  @return The container of the icon's container, which should
1115         *   be the root of this graph model.
1116         */
1117        @Override
1118        public Object getParent(Object node) {
1119            return ((Locatable) node).getContainer().getContainer();
1120        }
1121
1122        /** Return an iterator over the edges coming into the given node.
1123         *  This method first ensures that there is an arc
1124         *  object for every link.
1125         *  The iterator is constructed by
1126         *  removing any arcs that do not have the given node as head.
1127         *  @param node The node, which is assumed to be an icon contained in
1128         *   this graph model.
1129         *  @return An iterator of Arc objects, all of which have
1130         *   the given node as their head.
1131         */
1132        @Override
1133        public Iterator inEdges(Object node) {
1134            return new NullIterator();
1135        }
1136
1137        /** Return an iterator over the edges coming into the given node.
1138         *  This method first ensures that there is an arc
1139         *  object for every link. The iterator is constructed by
1140         *  removing any arcs that do not have the given node as tail.
1141         *  @param node The node, which is assumed to be an icon contained in
1142         *   this graph model.
1143         *  @return An iterator of Arc objects, all of which have
1144         *   the given node as their tail.
1145         */
1146        @Override
1147        public Iterator outEdges(Object node) {
1148            return new NullIterator();
1149        }
1150
1151        /** Remove the given node from the model.  The node is assumed
1152         *  to be an icon.
1153         */
1154        @Override
1155        public void removeNode(final Object eventSource, Object node) {
1156            NamedObj deleteObj = ((Locatable) node).getContainer();
1157            String elementName = null;
1158
1159            if (deleteObj instanceof ComponentPort) {
1160                // Object is an entity.
1161                elementName = "deletePort";
1162            } else {
1163                throw new InternalErrorException(
1164                        "Attempt to remove a node that is not an Entity. "
1165                                + "node = " + node);
1166            }
1167
1168            String moml = "<" + elementName + " name=\"" + deleteObj.getName()
1169                    + "\"/>\n";
1170
1171            // Make the request in the context of the container.
1172            NamedObj container = deleteObj.getContainer();
1173            MoMLChangeRequest request = new MoMLChangeRequest(
1174                    FSMGraphModel.this, container, moml);
1175            request.setUndoable(true);
1176            request.addChangeListener(new ChangeListener() {
1177                @Override
1178                public void changeFailed(ChangeRequest change,
1179                        Exception exception) {
1180                    // If we fail, then issue structureChanged.
1181                    dispatchGraphEvent(new GraphEvent(eventSource,
1182                            GraphEvent.STRUCTURE_CHANGED, getRoot()));
1183                }
1184
1185                @Override
1186                public void changeExecuted(ChangeRequest change) {
1187                    // If we succeed, then issue structureChanged, since
1188                    // this is likely connected to something.
1189                    dispatchGraphEvent(new GraphEvent(eventSource,
1190                            GraphEvent.STRUCTURE_CHANGED, getRoot()));
1191                }
1192            });
1193            request.setUndoable(true);
1194            container.requestChange(request);
1195        }
1196    }
1197
1198    /** The model for an icon that represent states.
1199     */
1200    public class StateModel extends NamedObjNodeModel {
1201        /** Return a MoML String that will delete the given node from the
1202         *  Ptolemy model. This assumes that the context is the container
1203         *  of the state.
1204         *  @param node The node to be deleted.
1205         *  @return A valid MoML string.
1206         */
1207        @Override
1208        public String getDeleteNodeMoML(Object node) {
1209            NamedObj deleteObj = ((Locatable) node).getContainer();
1210            NamedObj container = deleteObj.getContainer();
1211
1212            StringBuffer moml = new StringBuffer("<group>\n");
1213
1214            moml.append("<deleteEntity name=\"" + deleteObj.getName(container)
1215                    + "\"/>\n");
1216
1217            CompositeEntity master = (CompositeEntity) deleteObj.getContainer();
1218
1219            // Nothing to do if there is no container.
1220            if (master != null) {
1221                // Remove any referenced refinements that are not also
1222                // referenced by other states.
1223                TypedActor[] refinements = null;
1224
1225                try {
1226                    if (deleteObj instanceof State) {
1227                        refinements = ((State) deleteObj).getRefinement();
1228                    }
1229                } catch (IllegalActionException ex) {
1230                    // Ignore, no refinement to remove.
1231                }
1232
1233                if (refinements != null) {
1234                    for (TypedActor refinement : refinements) {
1235                        // By default, if no other state or transition refers
1236                        // to this refinement, then we will remove it.
1237                        boolean removeIt = true;
1238                        Iterator states = master.entityList(State.class)
1239                                .iterator();
1240
1241                        while (removeIt && states.hasNext()) {
1242                            State state = (State) states.next();
1243
1244                            if (state == deleteObj) {
1245                                continue;
1246                            }
1247
1248                            TypedActor[] stateRefinements = null;
1249
1250                            try {
1251                                stateRefinements = state.getRefinement();
1252                            } catch (IllegalActionException ex) {
1253                                // Ignore, no refinement to check
1254                            }
1255
1256                            if (stateRefinements == null) {
1257                                continue;
1258                            }
1259
1260                            for (TypedActor stateRefinement : stateRefinements) {
1261                                if (stateRefinement == refinement) {
1262                                    removeIt = false;
1263                                    break;
1264                                }
1265                            }
1266                        }
1267
1268                        // Next check transitions.
1269                        Iterator transitions = master.relationList().iterator();
1270
1271                        while (removeIt && transitions.hasNext()) {
1272                            Relation transition = (Relation) transitions.next();
1273
1274                            if (!(transition instanceof Transition)) {
1275                                continue;
1276                            }
1277
1278                            TypedActor[] transitionRefinements = null;
1279
1280                            try {
1281                                transitionRefinements = ((Transition) transition)
1282                                        .getRefinement();
1283                            } catch (IllegalActionException e) {
1284                                // Ignore, no refinement to check.
1285                            }
1286
1287                            if (transitionRefinements == null) {
1288                                continue;
1289                            }
1290
1291                            for (TypedActor transitionRefinement : transitionRefinements) {
1292                                if (transitionRefinement == refinement) {
1293                                    removeIt = false;
1294                                    break;
1295                                }
1296                            }
1297                        }
1298
1299                        if (removeIt) {
1300                            moml.append("<deleteEntity name=\""
1301                                    + ((NamedObj) refinement).getName(container)
1302                                    + "\"/>\n");
1303                        }
1304                    }
1305                }
1306            }
1307
1308            moml.append("</group>\n");
1309            return moml.toString();
1310        }
1311
1312        /** Return the graph parent of the given node.
1313         *  @param node The node, which is assumed to be a location.
1314         *  @return The container of the icon's container, which should
1315         *   be the root of this graph model.
1316         */
1317        @Override
1318        public Object getParent(Object node) {
1319            return ((Locatable) node).getContainer().getContainer();
1320        }
1321
1322        /** Return an iterator over the edges coming into the given node.
1323         *  This method first ensures that there is an arc
1324         *  object for every link. The iterator is constructed by
1325         *  removing any arcs that do not have the given node as head.
1326         *  @param node The node, which is assumed to be a location.
1327         *  @return An iterator of link objects, all of which have
1328         *   the given node as their head.
1329         */
1330        @Override
1331        public Iterator inEdges(Object node) {
1332            Locatable icon = (Locatable) node;
1333
1334            // Go through all the links, creating a list of
1335            // those we are connected to.
1336            List stateLinkList = new LinkedList();
1337            Iterator links = _linkSet.iterator();
1338
1339            while (links.hasNext()) {
1340                Link link = (Link) links.next();
1341                NamedObj head = (NamedObj) link.getHead();
1342
1343                if (head != null && head.equals(icon)) {
1344                    stateLinkList.add(link);
1345                }
1346            }
1347
1348            return stateLinkList.iterator();
1349        }
1350
1351        /** Return an iterator over the edges coming into the given node.
1352         *  This method first ensures that there is an arc
1353         *  object for every link. The iterator is constructed by
1354         *  removing any arcs that do not have the given node as tail.
1355         *  @param node The node, which is assumed to be a location.
1356         *  @return An iterator of Link objects, all of which have
1357         *   the given node as their tail.
1358         */
1359        @Override
1360        public Iterator outEdges(Object node) {
1361            Locatable icon = (Locatable) node;
1362
1363            // Go through all the links, creating a list of
1364            // those we are connected to.
1365            List stateLinkList = new LinkedList();
1366            Iterator links = _linkSet.iterator();
1367
1368            while (links.hasNext()) {
1369                Link link = (Link) links.next();
1370                Object tail = link.getTail();
1371
1372                if (tail != null && tail.equals(icon)) {
1373                    stateLinkList.add(link);
1374                }
1375            }
1376
1377            return stateLinkList.iterator();
1378        }
1379
1380        /** Remove the given node from the model.  The node is assumed
1381         *  to be a Locatable belonging to an entity.
1382         */
1383        @Override
1384        public void removeNode(final Object eventSource, Object node) {
1385            NamedObj deleteObj = ((Locatable) node).getContainer();
1386
1387            // First remove all the in and out edges.
1388            // This isn't done automatically by the Ptolemy kernel, because
1389            // the kernel only removes the links to the relations. The
1390            // relations themselves are left in place.  But in the FSM
1391            // domain, it makes no sense to have a relation with only one
1392            // link to it.  So we remove the relations.
1393            Iterator inEdges = inEdges(node);
1394
1395            while (inEdges.hasNext()) {
1396                disconnectEdge(eventSource, inEdges.next());
1397            }
1398
1399            Iterator outEdges = outEdges(node);
1400
1401            while (outEdges.hasNext()) {
1402                disconnectEdge(eventSource, outEdges.next());
1403            }
1404
1405            String elementName = null;
1406
1407            if (deleteObj instanceof ComponentEntity) {
1408                // Object is an entity.
1409                elementName = "deleteEntity";
1410            } else {
1411                throw new InternalErrorException(
1412                        "Attempt to remove a node that is not an Entity. "
1413                                + "node = " + node);
1414            }
1415
1416            String moml = "<" + elementName + " name=\"" + deleteObj.getName()
1417                    + "\"/>\n";
1418
1419            // Make the request in the context of the container.
1420            NamedObj container = deleteObj.getContainer();
1421            MoMLChangeRequest request = new MoMLChangeRequest(
1422                    FSMGraphModel.this, container, moml);
1423            request.addChangeListener(new ChangeListener() {
1424                @Override
1425                public void changeFailed(ChangeRequest change,
1426                        Exception exception) {
1427                    // If we fail, then issue structureChanged.
1428                    dispatchGraphEvent(new GraphEvent(eventSource,
1429                            GraphEvent.STRUCTURE_CHANGED, getRoot()));
1430                }
1431
1432                @Override
1433                public void changeExecuted(ChangeRequest change) {
1434                    // If we succeed, then issue structureChanged, since
1435                    // this is likely connected to something.
1436                    dispatchGraphEvent(new GraphEvent(eventSource,
1437                            GraphEvent.STRUCTURE_CHANGED, getRoot()));
1438                }
1439            });
1440            request.setUndoable(true);
1441            container.requestChange(request);
1442        }
1443    }
1444}