001    /*
002     * Copyright 2012 Apache Software Foundation.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.apache.oozie.util;
017    
018    import edu.uci.ics.jung.algorithms.layout.StaticLayout;
019    import edu.uci.ics.jung.graph.DirectedSparseGraph;
020    import edu.uci.ics.jung.graph.Graph;
021    import edu.uci.ics.jung.graph.util.Context;
022    import edu.uci.ics.jung.visualization.VisualizationImageServer;
023    import edu.uci.ics.jung.visualization.renderers.Renderer;
024    import edu.uci.ics.jung.visualization.util.ArrowFactory;
025    import java.awt.*;
026    import java.awt.geom.Ellipse2D;
027    import java.awt.geom.Point2D;
028    import java.awt.image.BufferedImage;
029    import java.io.IOException;
030    import java.io.OutputStream;
031    import java.io.StringReader;
032    import java.util.HashMap;
033    import java.util.Iterator;
034    import java.util.LinkedHashMap;
035    import java.util.Map;
036    import javax.imageio.ImageIO;
037    import javax.xml.parsers.SAXParser;
038    import javax.xml.parsers.SAXParserFactory;
039    import org.apache.commons.collections15.Transformer;
040    import org.apache.oozie.client.WorkflowAction;
041    import org.apache.oozie.client.WorkflowAction.Status;
042    import org.apache.oozie.client.WorkflowJob;
043    import org.apache.oozie.client.rest.JsonWorkflowJob;
044    import org.xml.sax.Attributes;
045    import org.xml.sax.InputSource;
046    import org.xml.sax.SAXException;
047    import org.xml.sax.XMLReader;
048    import org.xml.sax.helpers.DefaultHandler;
049    
050    /**
051     * Class to generate and plot runtime workflow DAG
052     */
053    public class GraphGenerator {
054    
055        private String xml;
056        private JsonWorkflowJob job;
057        private boolean showKill = false;
058    
059        /**
060         * C'tor
061         * @param xml The workflow definition XML
062         * @param job Current status of the job
063         * @param showKill Flag to whether show 'kill' node
064         */
065        public GraphGenerator(String xml, JsonWorkflowJob job, boolean showKill) {
066            if(job == null) {
067                throw new IllegalArgumentException("JsonWorkflowJob can't be null");
068            }
069            this.xml = xml;
070            this.job = job;
071            this.showKill = showKill;
072        }
073    
074        /**
075         * C'tor
076         * @param xml
077         * @param job
078         */
079        public GraphGenerator(String xml, JsonWorkflowJob job) {
080            this(xml, job, false);
081        }
082    
083        /**
084         * Overridden to thwart finalizer attack
085         */
086        @Override
087        public void finalize() {
088            // No-op; just to avoid finalizer attack
089            // as the constructor is throwing an exception
090        }
091    
092        /**
093         * Stream the PNG file to client
094         * @param out
095         * @throws Exception
096         */
097        public void write(OutputStream out) throws Exception {
098            SAXParserFactory spf = SAXParserFactory.newInstance();
099            spf.setNamespaceAware(true);
100            SAXParser saxParser = spf.newSAXParser();
101            XMLReader xmlReader = saxParser.getXMLReader();
102            xmlReader.setContentHandler(new XMLParser(out));
103            xmlReader.parse(new InputSource(new StringReader(xml)));
104        }
105    
106        private class XMLParser extends DefaultHandler {
107    
108            private OutputStream out;
109            private LinkedHashMap<String, OozieWFNode> tags;
110    
111            private String action = null;
112            private String actionOK = null;
113            private String actionErr = null;
114            private String actionType = null;
115            private String fork;
116            private String decision;
117    
118            public XMLParser(OutputStream out) {
119                this.out = out;
120            }
121    
122            @Override
123            public void startDocument() throws SAXException {
124                tags = new LinkedHashMap();
125            }
126    
127            @Override
128            public void endDocument() throws SAXException {
129    
130                if(tags.isEmpty()) {
131                    // Nothing to do here!
132                    return;
133                }
134    
135                int maxX = Integer.MIN_VALUE;
136                int maxY = Integer.MIN_VALUE;
137                int minX = Integer.MAX_VALUE;
138                int currX = 45;
139                int currY = 45;
140                final int xMargin = 205;
141                final int yMargin = 50;
142                final int xIncr = 215; // The widest element is 200 pixels (Rectangle)
143                final int yIncr = 255; // The tallest element is 150 pixels; (Diamond)
144                HashMap<String, WorkflowAction> actionMap = new HashMap<String, WorkflowAction>();
145    
146                // Create a hashmap for faster lookups
147                // Also override showKill if there's any failed action
148                boolean found = false;
149                for(WorkflowAction wfAction : job.getActions()) {
150                    actionMap.put(wfAction.getName(), wfAction);
151                    if(!found) {
152                        switch(wfAction.getStatus()) {
153                            case KILLED:
154                            case ERROR:
155                            case FAILED:
156                                showKill = true; // Assuming on error the workflow eventually ends with kill node
157                                found = true;
158                        }
159                    }
160                }
161    
162                // Start building the graph
163                DirectedSparseGraph<OozieWFNode, String> dg = new DirectedSparseGraph<OozieWFNode, String>();
164                for(Map.Entry<String, OozieWFNode> entry : tags.entrySet()) {
165                    String name = entry.getKey();
166                    OozieWFNode node = entry.getValue();
167                    if(actionMap.containsKey(name)) {
168                        node.setStatus(actionMap.get(name).getStatus());
169                    }
170    
171                    // Set (x,y) coords of the vertices if not already set
172                    if(node.getLocation().equals(new Point(0, 0))) {
173                        node.setLocation(currX, currY);
174                    }
175    
176                    float childStep = showKill ? -(((float)node.getArcs().size() - 1 ) / 2)
177                            : -((float)node.getArcs().size() / 2 - 1);
178                    int nodeX = node.getLocation().x;
179                    int nodeY = node.getLocation().y;
180                    for(Map.Entry<String, Boolean> arc : node.getArcs().entrySet()) {
181                        if(!showKill && arc.getValue() && tags.get(arc.getKey()).getType().equals("kill")) {
182                            // Don't show kill node (assumption: only error goes to kill node;
183                            // No ok goes to kill node)
184                            continue;
185                        }
186                        OozieWFNode child = tags.get(arc.getKey());
187                        if(child == null) {
188                            continue; // or throw error?
189                        }
190                        dg.addEdge(name + "-->" + arc.getKey(), node, child);
191                        // TODO: Experimental -- should we set coords even if they're already set?
192                        //if(child.getLocation().equals(new Point(0, 0))) {
193                            int childX = (int)(nodeX + childStep * xIncr);
194                            int childY = nodeY + yIncr;
195                            child.setLocation(childX, childY);
196    
197                            if(minX > childX) {
198                                minX = childX;
199                            }
200                            if(maxX < childX) {
201                                maxX = childX;
202                            }
203                            if(maxY < childY) {
204                                maxY = childY;
205                            }
206                        //}
207                        childStep += 1;
208                    }
209    
210                    currY += yIncr;
211                    currX = nodeX;
212                    if(minX > nodeX) {
213                        minX = nodeX;
214                    }
215                    if(maxX < nodeX) {
216                        maxX = nodeX;
217                    }
218                    if(maxY < nodeY) {
219                        maxY = nodeY;
220                    }
221                } // Done building graph
222    
223                final int padX = minX < 0 ? -minX: 0;
224    
225                Transformer<OozieWFNode, Point2D> locationInit = new Transformer<OozieWFNode, Point2D>() {
226    
227                    @Override
228                    public Point2D transform(OozieWFNode node) {
229                        if(padX == 0) {
230                            return node.getLocation();
231                        } else {
232                            return new Point(node.getLocation().x + padX + xMargin, node.getLocation().y);
233                        }
234                    }
235    
236                };
237    
238                StaticLayout<OozieWFNode, String> layout = new StaticLayout<OozieWFNode, String>(dg, locationInit, new Dimension(maxX + padX + xMargin, maxY));
239                layout.lock(true);
240                VisualizationImageServer<OozieWFNode, String> vis = new VisualizationImageServer<OozieWFNode, String>(layout, new Dimension(maxX + padX + 2 * xMargin, maxY + yMargin));
241    
242                vis.getRenderContext().setEdgeArrowTransformer(new ArrowShapeTransformer());
243                vis.getRenderContext().setArrowDrawPaintTransformer(new ArcPaintTransformer());
244                vis.getRenderContext().setEdgeDrawPaintTransformer(new ArcPaintTransformer());
245                vis.getRenderContext().setEdgeStrokeTransformer(new ArcStrokeTransformer());
246                vis.getRenderContext().setVertexShapeTransformer(new NodeShapeTransformer());
247                vis.getRenderContext().setVertexFillPaintTransformer(new NodePaintTransformer());
248                vis.getRenderContext().setVertexStrokeTransformer(new NodeStrokeTransformer());
249                vis.getRenderContext().setVertexLabelTransformer(new NodeLabelTransformer());
250                vis.getRenderContext().setVertexFontTransformer(new NodeFontTransformer());
251                vis.getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR);
252                vis.setBackground(Color.WHITE);
253    
254                Dimension d = vis.getSize();
255                BufferedImage img = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB);
256                Graphics2D g = img.createGraphics();
257                vis.paintAll(g);
258    
259                try {
260                    ImageIO.write(img, "png", out);
261                } catch (IOException ioe) {
262                    throw new SAXException(ioe);
263                }
264            }
265    
266            @Override
267            public void startElement(String namespaceURI,
268                                    String localName,
269                                    String qName,
270                                    Attributes atts)
271                throws SAXException {
272    
273                if(localName.equalsIgnoreCase("start")) {
274                    String start = localName.toLowerCase();
275                    if(!tags.containsKey(start)) {
276                        OozieWFNode v = new OozieWFNode(start, start);
277                        v.addArc(atts.getValue("to"));
278                        tags.put(start, v);
279                    }
280                } else if(localName.equalsIgnoreCase("action")) {
281                    action = atts.getValue("name");
282                } else if(action != null && actionType == null) {
283                    actionType = localName.toLowerCase();
284                } else if(localName.equalsIgnoreCase("ok") && action != null && actionOK == null) {
285                        actionOK = atts.getValue("to");
286                } else if(localName.equalsIgnoreCase("error") && action != null && actionErr == null) {
287                        actionErr = atts.getValue("to");
288                } else if(localName.equalsIgnoreCase("fork")) {
289                    fork = atts.getValue("name");
290                    if(!tags.containsKey(fork)) {
291                        tags.put(fork, new OozieWFNode(fork, localName.toLowerCase()));
292                    }
293                } else if(localName.equalsIgnoreCase("path")) {
294                    tags.get(fork).addArc(atts.getValue("start"));
295                } else if(localName.equalsIgnoreCase("join")) {
296                    String join = atts.getValue("name");
297                    if(!tags.containsKey(join)) {
298                        OozieWFNode v = new OozieWFNode(join, localName.toLowerCase());
299                        v.addArc(atts.getValue("to"));
300                        tags.put(join, v);
301                    }
302                } else if(localName.equalsIgnoreCase("decision")) {
303                    decision = atts.getValue("name");
304                    if(!tags.containsKey(decision)) {
305                        tags.put(decision, new OozieWFNode(decision, localName.toLowerCase()));
306                    }
307                } else if(localName.equalsIgnoreCase("case")
308                        || localName.equalsIgnoreCase("default")) {
309                    tags.get(decision).addArc(atts.getValue("to"));
310                } else if(localName.equalsIgnoreCase("kill")
311                        || localName.equalsIgnoreCase("end")) {
312                    String name = atts.getValue("name");
313                    if(!tags.containsKey(name)) {
314                        tags.put(name, new OozieWFNode(name, localName.toLowerCase()));
315                    }
316                }
317            }
318    
319            @Override
320            public void endElement(String namespaceURI,
321                                    String localName,
322                                    String qName)
323                    throws SAXException {
324                if(localName.equalsIgnoreCase("action")) {
325                    tags.put(action, new OozieWFNode(action, actionType));
326                    tags.get(action).addArc(this.actionOK);
327                    tags.get(action).addArc(this.actionErr, true);
328                    action = null;
329                    actionOK = null;
330                    actionErr = null;
331                    actionType = null;
332                }
333            }
334    
335            private class OozieWFNode {
336                private String name;
337                private String type;
338                private Point loc;
339                private HashMap<String, Boolean> arcs;
340                private Status status = null;
341    
342                public OozieWFNode(String name,
343                        String type,
344                        HashMap<String, Boolean> arcs,
345                        Point loc,
346                        Status status) {
347                    this.name = name;
348                    this.type = type;
349                    this.arcs = arcs;
350                    this.loc = loc;
351                    this.status = status;
352                }
353    
354                public OozieWFNode(String name, String type, HashMap<String, Boolean> arcs) {
355                    this(name, type, arcs, new Point(0, 0), null);
356                }
357    
358                public OozieWFNode(String name, String type) {
359                    this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), null);
360                }
361    
362                public OozieWFNode(String name, String type, WorkflowAction.Status status) {
363                    this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), status);
364                }
365    
366                public void addArc(String arc, boolean isError) {
367                    arcs.put(arc, isError);
368                }
369    
370                public void addArc(String arc) {
371                    addArc(arc, false);
372                }
373    
374                public void setName(String name) {
375                    this.name = name;
376                }
377    
378                public void setType(String type) {
379                    this.type = type;
380                }
381    
382                public void setLocation(Point loc) {
383                    this.loc = loc;
384                }
385    
386                public void setLocation(double x, double y) {
387                    loc.setLocation(x, y);
388                }
389    
390                public void setStatus(WorkflowAction.Status status) {
391                    this.status = status;
392                }
393    
394                public String getName() {
395                    return name;
396                }
397    
398                public String getType() {
399                    return type;
400                }
401    
402                public HashMap<String, Boolean> getArcs() {
403                    return arcs;
404                }
405    
406                public Point getLocation() {
407                    return loc;
408                }
409    
410                public WorkflowAction.Status getStatus() {
411                    return status;
412                }
413    
414                @Override
415                public String toString() {
416                    StringBuilder s = new StringBuilder();
417    
418                    s.append("Node: ").append(name).append("\t");
419                    s.append("Type: ").append(type).append("\t");
420                    s.append("Location: (").append(loc.getX()).append(", ").append(loc.getY()).append(")\t");
421                    s.append("Status: ").append(status).append("\n");
422                    Iterator<Map.Entry<String, Boolean>> it = arcs.entrySet().iterator();
423                    while(it.hasNext()) {
424                        Map.Entry<String, Boolean> entry = it.next();
425    
426                        s.append("\t").append(entry.getKey());
427                        if(entry.getValue().booleanValue()) {
428                            s.append(" on error\n");
429                        } else {
430                            s.append("\n");
431                        }
432                    }
433    
434                    return s.toString();
435                }
436            }
437    
438            private class NodeFontTransformer implements Transformer<OozieWFNode, Font> {
439                private final Font font = new Font("Default", Font.BOLD, 15);
440    
441                @Override
442                public Font transform(OozieWFNode node) {
443                    return font;
444                }
445            }
446    
447            private class ArrowShapeTransformer implements Transformer<Context<Graph<OozieWFNode, String>, String>,  Shape> {
448                private final Shape arrow = ArrowFactory.getWedgeArrow(10.0f, 20.0f);
449    
450                @Override
451                public Shape transform(Context<Graph<OozieWFNode, String>, String> i) {
452                    return arrow;
453                }
454            }
455    
456            private class ArcPaintTransformer implements Transformer<String, Paint> {
457                // Paint based on transition
458                @Override
459                public Paint transform(String arc) {
460                    int sep = arc.indexOf("-->");
461                    String source = arc.substring(0, sep);
462                    String target = arc.substring(sep + 3);
463                    OozieWFNode src = tags.get(source);
464                    OozieWFNode tgt = tags.get(target);
465    
466                    if(src.getType().equals("start")) {
467                        if(tgt.getStatus() == null) {
468                            return Color.LIGHT_GRAY;
469                        } else {
470                            return Color.GREEN;
471                        }
472                    }
473    
474                    if(src.getArcs().get(target)) {
475                        // Dealing with error transition (i.e. target is error)
476                        if(src.getStatus() == null) {
477                            return Color.LIGHT_GRAY;
478                        }
479                        switch(src.getStatus()) {
480                            case KILLED:
481                            case ERROR:
482                            case FAILED:
483                                return Color.RED;
484                            default:
485                                return Color.LIGHT_GRAY;
486                        }
487                    } else {
488                        // Non-error
489                        if(src.getType().equals("decision")) {
490                            // Check for target too
491                            if(tgt.getStatus() != null) {
492                                return Color.GREEN;
493                            } else {
494                                return Color.LIGHT_GRAY;
495                            }
496                        } else {
497                            if(src.getStatus() == null) {
498                                return Color.LIGHT_GRAY;
499                            }
500                            switch(src.getStatus()) {
501                                case OK:
502                                case DONE:
503                                case END_RETRY:
504                                case END_MANUAL:
505                                    return Color.GREEN;
506                                default:
507                                    return Color.LIGHT_GRAY;
508                            }
509                        }
510                    }
511                }
512            }
513    
514            private class NodeStrokeTransformer implements Transformer<OozieWFNode, Stroke> {
515                private final Stroke stroke1 = new BasicStroke(2.0f);
516                private final Stroke stroke2 = new BasicStroke(4.0f);
517    
518                @Override
519                public Stroke transform(OozieWFNode node) {
520                    if(node.getType().equals("start")
521                            || node.getType().equals("end")
522                            || node.getType().equals("kill")) {
523                        return stroke2;
524                    }
525                    return stroke1;
526                }
527            }
528    
529            private class NodeLabelTransformer implements Transformer<OozieWFNode, String> {
530                /*
531                * 20 chars in rectangle in 2 rows max
532                * 14 chars in diamond in 2 rows max
533                * 9 in triangle in 2 rows max
534                * 8 in invtriangle in 2 rows max
535                * 8 in circle in 2 rows max
536                */
537                @Override
538                public String transform(OozieWFNode node) {
539                    //return node.getType();
540                    String name = node.getName();
541                    String type = node.getType();
542                    StringBuilder s = new StringBuilder();
543                    if(type.equals("decision")) {
544                        if(name.length() <= 14) {
545                            return name;
546                        } else {
547                            s.append("<html>").append(name.substring(0, 12)).append("-<br />");
548                            if(name.substring(13).length() > 14) {
549                                s.append(name.substring(12, 25)).append("...");
550                            } else {
551                                s.append(name.substring(12));
552                            }
553                            s.append("</html>");
554                            return s.toString();
555                        }
556                    } else if(type.equals("fork")) {
557                        if(name.length() <= 9) {
558                            return "<html><br />" + name + "</html>";
559                        } else {
560                            s.append("<html><br />").append(name.substring(0, 7)).append("-<br />");
561                            if(name.substring(8).length() > 9) {
562                                s.append(name.substring(7, 15)).append("...");
563                            } else {
564                                s.append(name.substring(7));
565                            }
566                            s.append("</html>");
567                            return s.toString();
568                        }
569                    } else if(type.equals("join")) {
570                        if(name.length() <= 8) {
571                            return "<html>" + name + "</html>";
572                        } else {
573                            s.append("<html>").append(name.substring(0, 6)).append("-<br />");
574                            if(name.substring(7).length() > 8) {
575                                s.append(name.substring(6, 13)).append("...");
576                            } else {
577                                s.append(name.substring(6));
578                            }
579                            s.append("</html>");
580                            return s.toString();
581                        }
582                    } else if(type.equals("start")
583                            || type.equals("end")
584                            || type.equals("kill")) {
585                        if(name.length() <= 8) {
586                            return "<html>" + name + "</html>";
587                        } else {
588                            s.append("<html>").append(name.substring(0, 6)).append("-<br />");
589                            if(name.substring(7).length() > 8) {
590                                s.append(name.substring(6, 13)).append("...");
591                            } else {
592                                s.append(name.substring(6));
593                            }
594                            s.append("</html>");
595                            return s.toString();
596                        }
597                    }else {
598                        if(name.length() <= 20) {
599                            return name;
600                        } else {
601                            s.append("<html>").append(name.substring(0, 18)).append("-<br />");
602                            if(name.substring(19).length() > 20) {
603                                s.append(name.substring(18, 37)).append("...");
604                            } else {
605                                s.append(name.substring(18));
606                            }
607                            s.append("</html>");
608                            return s.toString();
609                        }
610                    }
611                }
612            }
613    
614            private class NodePaintTransformer implements Transformer<OozieWFNode, Paint> {
615                @Override
616                public Paint transform(OozieWFNode node) {
617                    WorkflowJob.Status jobStatus = job.getStatus();
618                    if(node.getType().equals("start")) {
619                        return Color.WHITE;
620                    } else if(node.getType().equals("end")) {
621                        if(jobStatus == WorkflowJob.Status.SUCCEEDED) {
622                            return Color.GREEN;
623                        }
624                        return Color.BLACK;
625                    } else if(node.getType().equals("kill")) {
626                        if(jobStatus == WorkflowJob.Status.FAILED
627                                || jobStatus == WorkflowJob.Status.KILLED) {
628                            return Color.RED;
629                        }
630                        return Color.WHITE;
631                    }
632    
633                    // Paint based on status for rest
634                    WorkflowAction.Status status = node.getStatus();
635                    if(status == null) {
636                        return Color.LIGHT_GRAY;
637                    }
638                    switch(status) {
639                        case OK:
640                        case DONE:
641                        case END_RETRY:
642                        case END_MANUAL:
643                            return Color.GREEN;
644                        case PREP:
645                        case RUNNING:
646                        case USER_RETRY:
647                        case START_RETRY:
648                        case START_MANUAL:
649                            return Color.YELLOW;
650                        case KILLED:
651                        case ERROR:
652                        case FAILED:
653                            return Color.RED;
654                        default:
655                            return Color.LIGHT_GRAY;
656                    }
657                }
658            }
659    
660            private class NodeShapeTransformer implements Transformer<OozieWFNode, Shape> {
661                private final Ellipse2D.Double circle = new Ellipse2D.Double(-40, -40, 80, 80);
662                private final Rectangle rect = new Rectangle(-100, -30, 200, 60);
663                private final Polygon diamond = new Polygon(new int[]{-75, 0, 75, 0}, new int[]{0, 75, 0, -75}, 4);
664                private final Polygon triangle = new Polygon(new int[]{-85, 85, 0}, new int[]{0, 0, -148}, 3);
665                private final Polygon invtriangle = new Polygon(new int[]{-85, 85, 0}, new int[]{0, 0, 148}, 3);
666    
667                @Override
668                public Shape transform(OozieWFNode node) {
669                    if("start".equals(node.getType())
670                        || "end".equals(node.getType())
671                        || "kill".equals(node.getType())) {
672                        return circle;
673                    }
674                    if("fork".equals(node.getType())) {
675                        return triangle;
676                    }
677                    if("join".equals(node.getType())) {
678                        return invtriangle;
679                    }
680                    if("decision".equals(node.getType())) {
681                        return diamond;
682                    }
683                    return rect; // All action nodes
684                }
685            }
686    
687            private class ArcStrokeTransformer implements Transformer<String, Stroke> {
688                private final Stroke stroke1 = new BasicStroke(2.0f);
689                private final Stroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[] {10.0f}, 0.0f);
690    
691                // Draw based on transition
692                @Override
693                public Stroke transform(String arc) {
694                    int sep = arc.indexOf("-->");
695                    String source = arc.substring(0, sep);
696                    String target = arc.substring(sep + 3);
697                    OozieWFNode src = tags.get(source);
698                    if(src.getArcs().get(target)) {
699                            if(src.getStatus() == null) {
700                                return dashed;
701                            }
702                            switch(src.getStatus()) {
703                                case KILLED:
704                                case ERROR:
705                                case FAILED:
706                                    return stroke1;
707                                default:
708                                    return dashed;
709                            }
710                    } else {
711                        return stroke1;
712                    }
713                }
714            }
715        }
716    }