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